Compare commits

...

2 Commits

Author SHA1 Message Date
Bartosz Wieczorek
f828f2666b Change to module logger for better logs 2025-11-26 11:22:46 +01:00
Bartosz Wieczorek
7d2a5a1072 Add deb packages configuration 2025-11-26 07:06:36 +01:00
36 changed files with 1350 additions and 332 deletions

View File

@ -4,8 +4,8 @@ project(Ranczo-IO)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# add_compile_options(-g -fsanitize=address,undefined,float-divide-by-zero,float-cast-overflow,null -fsanitize-address-use-after-scope -fno-sanitize-recover=all -fno-sanitize=alignment -fno-omit-frame-pointer)
# add_link_options(-g -fsanitize=address,undefined,float-divide-by-zero,float-cast-overflow,null -fsanitize-address-use-after-scope -fno-sanitize-recover=all -fno-sanitize=alignment -fno-omit-frame-pointer)
add_compile_options(-g -fsanitize=address,undefined,float-divide-by-zero,float-cast-overflow,null -fsanitize-address-use-after-scope -fno-sanitize-recover=all -fno-sanitize=alignment -fno-omit-frame-pointer)
add_link_options(-g -fsanitize=address,undefined,float-divide-by-zero,float-cast-overflow,null -fsanitize-address-use-after-scope -fno-sanitize-recover=all -fno-sanitize=alignment -fno-omit-frame-pointer)
include(CheckIPOSupported)
check_ipo_supported(RESULT supported OUTPUT error)
@ -28,7 +28,7 @@ set(BOOST_ROOT /usr/local/boost-1.89)
find_package(Boost 1.89 REQUIRED COMPONENTS json mqtt5)
# spdlog
set(SPDLOG_USE_STD_FORMAT OFF)
set(SPDLOG_USE_STD_FORMAT ON)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog
@ -36,12 +36,17 @@ FetchContent_Declare(
GIT_SHALLOW TRUE
)
set(EXPECTED_BUILD_TESTS FALSE)
FetchContent_Declare(
tl_expected
GIT_REPOSITORY https://github.com/TartanLlama/expected.git
GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(tl_expected)
FetchContent_Declare(date_src
URL https://github.com/HowardHinnant/date/archive/refs/tags/v3.0.3.zip
URL_MD5 0a21a588a31fb234202546deccea65e4
UPDATE_DISCONNECTED 1
)
# c++20 format library, super usefull, super fast
set(FMT_TEST OFF CACHE INTERNAL "disabling fmt tests")
@ -50,7 +55,7 @@ FetchContent_Declare(fmt
URL https://github.com/fmtlib/fmt/archive/refs/tags/10.2.1.zip
URL_MD5 1bba4e8bdd7b0fa98f207559ffa380a3
)
FetchContent_MakeAvailable(fmt spdlog tl_expected)
FetchContent_MakeAvailable(fmt spdlog tl_expected date_src)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
# add_compile_definitions(BOOST_ASIO_ENABLE_HANDLER_TRACKING)
@ -58,3 +63,19 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(libs)
add_subdirectory(services)
add_subdirectory(tests)
set(CPACK_GENERATOR "DEB")
# Nazwa "globalna" projektu
set(CPACK_PACKAGE_NAME "ranczo-io")
set(CPACK_PACKAGE_VENDOR "evlabs")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_CONTACT "b.wieczorek@dx.net.pl")
# Używamy komponentów, żeby mieć osobne DEB-y
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
set(CPACK_COMPONENTS_ALL floorheating temperature) # później dodasz temperature itd.
include(CPack)

View File

@ -125,6 +125,16 @@ using ::boost::system::errc::make_error_code;
} \
} while(0)
/// asserts that a _log objject is available, usefull for modules
#define ASYNC_CHECK_LOG(failable, ...) \
do { \
auto _result = co_await (failable); \
if(!_result) { \
_log.error(__VA_ARGS__); \
co_return unexpected{_result.error()}; \
} \
} while(0)
#define ASYNC_CHECK_TRANSFORM_ERROR(failable, transform) \
do { \
auto _result = co_await (failable); \

View File

@ -9,6 +9,9 @@ add_library(ranczo-io_utils
asio_watchdog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/asio_watchdog.hpp
modbus.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/modbus.hpp
config.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/config.hpp
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/mqtt_topic_builder.hpp
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/memory_resource.hpp
)
add_library(ranczo-io::utils ALIAS ranczo-io_utils)
@ -26,6 +29,7 @@ target_link_libraries(ranczo-io_utils
spdlog::spdlog
fmt::fmt
modbus
date::date
PRIVATE
SQLite::SQLite3
)

View File

@ -23,4 +23,18 @@ std::optional< std::chrono::system_clock::time_point > parse_timestamp_utc(std::
return std::nullopt;
}
}
std::pmr::string to_iso_timestamp(std::chrono::system_clock::time_point tp, std::pmr::memory_resource *mr)
{
BOOST_ASSERT(mr);
char buf[32]={};
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::tm tm = *std::gmtime(&t);
std::strftime(buf, sizeof(buf), "%FT%TZ", &tm);
return {buf, mr};
}
} // namespace ranczo::date

View File

@ -1,7 +1,4 @@
#include "config.hpp"
#include <chrono>
#include <ratio>
#include <spdlog/spdlog.h>
#include <ranczo-io/utils/modbus.hpp>
@ -13,8 +10,10 @@
#include <modbus/modbus.h>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <memory>
#include <ratio>
#include <string>
namespace ranczo {
@ -29,18 +28,18 @@ ModbusTcpContext::~ModbusTcpContext() {
awaitable_expected< void > ModbusTcpContext::async_connect() {
co_return co_await async_call< void >([this](modbus_t *& out_ctx) -> expected< void > {
spdlog::info("Modbus create ctx for {}:{}", host_.c_str(), port_);
_log.info("create ctx");
modbus_t * raw = ::modbus_new_tcp(host_.c_str(), port_);
if(!raw)
return unexpected(errno_errc());
std::unique_ptr< modbus_t, CtxDeleter > ctx(raw);
if(::modbus_connect(ctx.get()) == -1) {
spdlog::error("Modbus failed to conenct to {}:{}", host_.c_str(), port_);
_log.error("failed to conenct");
return unexpected(errno_errc());
}
_connected = true;
spdlog::info("Modbus connected to {}:{}", host_.c_str(), port_);
_log.info("connected");
timeval tv{2, 0};
::modbus_set_response_timeout(ctx.get(), tv.tv_sec, tv.tv_usec);
@ -67,7 +66,7 @@ awaitable_expected< void > ranczo::ModbusTcpContext::wait_between_commands() {
// Jeśli timer ma expiry w przyszłości czekamy
if(expiry > now) {
boost::system::error_code ec;
spdlog::trace("Timer waits {:.2f}us to expire time between ",
_log.trace("Timer waits {:.2f}us to expire time between ",
std::chrono::duration_cast< std::chrono::duration< double, std::micro > >(expiry - now).count());
co_await delay_timer_.async_wait(asio::redirect_error(asio::use_awaitable, ec));
@ -113,7 +112,7 @@ template < typename T, typename F >
awaitable_expected< T > ModbusTcpContext::call_with_lock(F && op) {
// Throttling zapewnij kilka ms przerwy między komendami
ASYNC_CHECK(wait_between_commands());
co_return co_await async_call< T >([this, op = std::forward< F >(op)](modbus_t *& out) -> expected< T > {
on_exit _{[&]() { // Ustaw nowe "okno" od teraz za command_interval_ ms
delay_timer_.expires_at(std::chrono::steady_clock::now() + command_interval_);
@ -153,13 +152,13 @@ awaitable_expected< uint16_t > ModbusDevice::async_read_holding_register(uint16_
auto ctx = ctx_; // kopia shared_ptr dla bezpieczeństwa w tasku
co_return co_await ctx->call_with_lock< std::uint16_t >([this, address](modbus_t * c) -> expected< std::uint16_t > {
if(::modbus_set_slave(c, unit_id_) == -1) {
spdlog::error("Modbus modbus_set_slave for {}/{} failed with {}", unit_id_, address, errno);
_log.error("modbus_set_slave address {} failed with {}", address, errno);
return unexpected(errno_errc());
}
uint16_t val = 0;
int rc = ::modbus_read_registers(c, static_cast< int >(address), 1, &val);
if(rc == -1) {
spdlog::error("Modbus modbus_read_registers for {}/{} failed with {}", unit_id_, address, errno);
_log.error("modbus_read_registers address {} failed with {}", address, errno);
return unexpected(errno_errc());
}
return val;
@ -170,12 +169,12 @@ awaitable_expected< void > ModbusDevice::async_write_coil(uint16_t address, bool
auto ctx = ctx_;
co_return co_await ctx->call_with_lock< void >([this, address, value](modbus_t * c) -> expected< void > {
if(::modbus_set_slave(c, unit_id_) == -1) {
spdlog::error("Modbus modbus_set_slave for {}/{} failed with {}", unit_id_, address, errno);
_log.error("Modbus modbus_set_slave address {} failed with {}", address, errno);
return unexpected(errno_errc());
}
const int rc = ::modbus_write_bit(c, static_cast< int >(address), value ? 1 : 0);
if(rc == -1) {
spdlog::error("Modbus modbus_write_bit for {}/{} failed with {}", unit_id_, address, errno);
_log.error("Modbus modbus_write_bit address {} failed with {}", address, errno);
return unexpected(errno_errc());
}
return {};

View File

@ -1,5 +1,9 @@
#include <optional>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <ranczo-io/utils/json_helpers.hpp>
#include <ranczo-io/utils/memory_resource.hpp>
#include <ranczo-io/utils/logger.hpp>
#include <config.hpp>
@ -31,6 +35,8 @@ class perf_counter {
std::string _name;
clock::time_point _begin;
ranczo::ModuleLogger _log{spdlog::default_logger(), "PerfCounter " + _name};
public:
explicit perf_counter(std::string_view name)
: _name(name),
@ -41,31 +47,7 @@ class perf_counter {
const auto end = clock::now();
const auto delta = std::chrono::duration_cast<std::chrono::microseconds>(end - _begin).count();
spdlog::info("[perf] {} took {} us", _name, delta);
}
};
// Adapter to use std::pmr::memory_resource with boost::json
class pmr_memory_resource_adapter : public boost::json::monotonic_resource::memory_resource {
std::pmr::memory_resource * std_resource_;
public:
explicit pmr_memory_resource_adapter(std::pmr::memory_resource * res) : std_resource_(res) {}
protected:
void * do_allocate(std::size_t size, std::size_t alignment) override {
return std_resource_->allocate(size, alignment);
}
void do_deallocate(void * p, std::size_t size, std::size_t alignment) override {
std_resource_->deallocate(p, size, alignment);
}
bool do_is_equal(const memory_resource & other) const noexcept override {
auto * o = dynamic_cast< const pmr_memory_resource_adapter * >(&other);
if(!o)
return false;
return std_resource_->is_equal(*o->std_resource_);
_log.debug("{} took {} us", _name, delta);
}
};
@ -127,11 +109,12 @@ struct AsyncMqttClient::SubscribtionToken {
struct AsyncMqttClient::AsyncMqttClientImpl {
const boost::asio::any_io_executor & _executor;
ModuleLogger _log;
client_type _mqtt_client;
std::vector< std::tuple< Topic, callback_t, std::unique_ptr< AsyncMqttClient::SubscribtionToken > > > _callbacks;
AsyncMqttClientImpl(const boost::asio::any_io_executor & executor) : _executor{executor}, _mqtt_client{_executor} {
spdlog::trace("Creating mqtt client");
AsyncMqttClientImpl(const boost::asio::any_io_executor & executor) : _executor{executor}, _log{spdlog::default_logger(), "MQTT Impl"}, _mqtt_client{_executor} {
_log.trace("Creating client");
// Configure the Client.
// It is mandatory to call brokers() and async_run() to configure the Brokers to connect to and start the Client.
_mqtt_client
@ -139,25 +122,25 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
.async_run(boost::asio::detached); // Start the client.
start();
spdlog::trace("Creating mqtt client done");
_log.trace("Creating client done");
}
~AsyncMqttClientImpl() = default;
const AsyncMqttClient::SubscribtionToken * add_callback(std::string_view topic, callback_t cb) {
spdlog::trace("MQTT impl registering callback for {}", topic);
_log.trace("registering callback for {}", topic);
auto token = std::make_unique< AsyncMqttClient::SubscribtionToken >(boost::uuids::random_generator()());
auto * token_ptr = token.get();
_callbacks.emplace_back(Topic{topic}, std::move(cb), std::move(token));
spdlog::trace("MQTT impl registering callback for {} DONE", topic);
_log.trace("registering callback for {} DONE", topic);
return token_ptr;
}
void remove_callback(const AsyncMqttClient::SubscribtionToken * token) {
spdlog::trace("MQTT impl removing callback by token");
_log.trace("removing callback by token");
auto it = std::find_if(_callbacks.begin(), _callbacks.end(), [&](const auto & tuple) {
return std::get< std::unique_ptr< AsyncMqttClient::SubscribtionToken > >(tuple).get() == token;
@ -166,10 +149,10 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
if(it != _callbacks.end()) {
_callbacks.erase(it);
} else {
spdlog::error("MQTT impl token not found");
_log.error("token not found");
}
spdlog::trace("MQTT impl removing callback DONE");
_log.trace("removing callback DONE");
}
bool has_subscription_for_topic(std::string_view topic) const {
@ -182,7 +165,7 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
BOOST_ASSERT(!topic.empty());
BOOST_ASSERT(cb);
spdlog::trace("MQTT impl subscribe_with_callback on topic: {}", topic);
_log.trace("subscribe_with_callback on topic: {}", topic);
const bool already = has_subscription_for_topic(topic);
@ -190,24 +173,24 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
const auto * tok = add_callback(topic, std::move(cb));
if(already) {
spdlog::trace("MQTT impl subscription to {} already active, only callback registered", topic);
_log.trace("subscription to {} already active, only callback registered", topic);
co_return tok;
}
spdlog::trace("MQTT impl subscription to {} started", topic);
_log.trace("subscription to {} started", topic);
auto status = co_await subscribe(topic);
if(!status) {
spdlog::error("MQTT impl failed to subscribe for topic {}", topic);
_log.error("failed to subscribe for topic {}", topic);
remove_callback(tok);
co_return unexpected{status.error()};
}
spdlog::trace("MQTT impl subscription to {} DONE", topic);
_log.trace("subscription to {} DONE", topic);
co_return tok;
}
awaitable_expected< void > subscribe(std::string_view topic) {
spdlog::trace("MQTT impl subscribe to {}", topic);
_log.trace("subscribe to {}", topic);
// switch to mqtt strand
co_await boost::asio::dispatch(_mqtt_client.get_executor(), boost::asio::use_awaitable);
@ -222,36 +205,36 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
}};
// Subscribe to a single Topic.
spdlog::trace("MQTT calling async_subscribe");
_log.trace("calling async_subscribe");
auto && [ec, sub_codes, sub_props] =
co_await _mqtt_client.async_subscribe(sub_topic, boost::mqtt5::subscribe_props{}, use_nothrow_awaitable);
spdlog::trace("MQTT calling async_subscribe DONE");
_log.trace("calling async_subscribe DONE");
// Note: you can subscribe to multiple Topics in one mqtt_client::async_subscribe call.
// An error can occur as a result of:
// a) wrong subscribe parameters
// b) mqtt_client::cancel is called while the Client is in the process of subscribing
if(ec) {
spdlog::error("MQTT subscribe error: {}", ec.message());
_log.error("subscribe error: {}", ec.message());
for(int i{}; i < sub_codes.size(); i++)
spdlog::error("MQTT subscribe suberror[{}]: {}", i, sub_codes[i].message());
_log.error("subscribe suberror[{}]: {}", i, sub_codes[i].message());
co_return unexpected{ec};
}
if(sub_codes.empty()) {
spdlog::error("MQTT subscribe result contains no subcodes");
_log.error("subscribe result contains no subcodes");
co_return unexpected{make_error_code(boost::system::errc::protocol_error)};
}
spdlog::info("MQTT subscribed to {}", topic);
_log.info("subscribed to {}", topic);
for(int i{}; i < sub_codes.size(); i++)
spdlog::info("MQTT subscribe accepted: {}", sub_codes[i].message());
_log.info("_log.subscribe accepted: {}", sub_codes[i].message());
co_return _void{}; // Success
}
awaitable_expected< void > publish(std::string_view topic, const boost::json::value & value) noexcept {
spdlog::trace("MQTT impl publish on: {}", topic);
_log.trace("publish on: {}", topic);
// 1) execute in context of mqtt strand
co_await boost::asio::dispatch(_mqtt_client.get_executor(), boost::asio::use_awaitable);
@ -262,16 +245,16 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
try {
payload = boost::json::serialize(value);
} catch(const std::bad_alloc & e) {
spdlog::error("METT cought bad_alloca exception: {}", e.what());
_log.error("cought bad_alloca exception: {}", e.what());
co_return unexpected{make_error_code(std::errc::not_enough_memory)};
}
// 3) QoS 0: only error code returned
auto [ec] = co_await _mqtt_client.async_publish< boost::mqtt5::qos_e::at_most_once >(
t, payload, boost::mqtt5::retain_e::no, boost::mqtt5::publish_props{}, boost::asio::as_tuple(boost::asio::use_awaitable));
std::move(t), std::move(payload), boost::mqtt5::retain_e::no, boost::mqtt5::publish_props{}, boost::asio::as_tuple(boost::asio::use_awaitable));
if(ec) {
spdlog::error("MQTT publish returned an exception: {}", ec.message());
_log.error("publish returned an exception: {}", ec.message());
co_return unexpected{ec};
}
@ -281,7 +264,7 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
void
handle_received_message(const std::string & topic, const std::string & payload, const boost::mqtt5::publish_props & publish_props) {
perf_counter _ {"handle_received_message"};
spdlog::debug("MQTT received topic {}, payload {}", topic, payload);
_log.debug("received topic {}, payload {}", topic, payload);
bool run = false;
@ -299,11 +282,9 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
-> boost::asio::awaitable< void > {
try {
// --- PMR dla requestu ---
std::array< std::uint8_t, 2048 > requestBuffer{};
requestBuffer.fill(0);
std::pmr::monotonic_buffer_resource mr_req{requestBuffer.data(), requestBuffer.size()};
pmr_memory_resource_adapter adapter_req(&mr_req);
memory_resource::MonotonicStack_2k_Resource mr;
json::pmr_memory_resource_adapter adapter_req(&mr);
boost::json::storage_ptr sp_req(&adapter_req);
// parse payload z PMR
@ -322,42 +303,42 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
};
if(responseTopic) {
spdlog::trace("MQTT Impl got response topic, handling it properly");
_log.trace("got response topic, handling it properly");
// --- PMR dla response ---
std::optional< boost::json::value > response;
std::array< std::uint8_t, 2048 > responseBuffer{0};
std::pmr::monotonic_buffer_resource mr_resp{responseBuffer.data(), responseBuffer.size()};
pmr_memory_resource_adapter adapter_resp(&mr_resp);
json::pmr_memory_resource_adapter adapter_resp(&mr_resp);
boost::json::storage_ptr sp_resp(&adapter_resp);
response.emplace(boost::json::object_kind, sp_resp);
// wywołanie callbacka z możliwością wypełnienia response
if(auto result = co_await handler(cbdata, response); !result) {
spdlog::warn("MQTT callback error: {}", result.error().message());
_log.warn("callback error: {}", result.error().message());
}
// jeśli mamy ustawiony topic i wypełniony JSON -> odeślij status
if(response && cbdata.responseTopic) {
spdlog::debug("MQTT sending response on topic {}", *cbdata.responseTopic);
_log.debug("sending response on topic {}", *cbdata.responseTopic);
auto pub_res = co_await publish(*cbdata.responseTopic, *response);
if(!pub_res) {
spdlog::warn("MQTT response publish error: {}", pub_res.error().message());
_log.warn("response publish error: {}", pub_res.error().message());
}
}
}else{
// wywołanie callbacka z pustym response
std::optional< boost::json::value > response;
if(auto result = co_await handler(cbdata, response); !result) {
spdlog::warn("MQTT callback error: {}", result.error().message());
_log.warn("callback error: {}", result.error().message());
}
}
} catch(const boost::system::system_error & e) {
spdlog::error("MQTT received bad json, parsing failed with error: {}/{}", e.code().message(), e.what());
_log.error("received bad json, parsing failed with error: {}/{}", e.code().message(), e.what());
} catch(const std::exception & e) {
spdlog::error("MQTT callback threw exception: {}", e.what());
_log.error("callback threw exception: {}", e.what());
} catch(...) {
spdlog::error("MQTT callback threw unknown exception");
_log.error("callback threw unknown exception");
}
co_return;
@ -367,27 +348,27 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
}
if(!run) {
spdlog::warn("MQTT received unsupported topic: {}", topic);
_log.warn("received unsupported topic: {}", topic);
}
}
awaitable_expected< void > listen() {
spdlog::trace("MQTT client start");
_log.trace("client start");
for(;;) {
auto && [ec, topic, payload, publish_props] = co_await _mqtt_client.async_receive(use_nothrow_awaitable);
if(ec) {
spdlog::warn("MQTT receive fail: {}", ec.message());
_log.warn("receive fail: {}", ec.message());
continue;
}
try {
handle_received_message(topic, payload, publish_props);
} catch(const boost::system::system_error & e) {
spdlog::error("MQTT received bad json, parsing failed with error: {}/{}", e.code().message(), e.what());
_log.error("received bad json, parsing failed with error: {}/{}", e.code().message(), e.what());
continue;
} catch(const std::exception & e) {
spdlog::error("MQTT caught an exception: {}, during callback execution", e.what());
_log.error("caught an exception: {}, during callback execution", e.what());
continue;
}
}
@ -395,7 +376,7 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
co_return _void{};
}
void start() {
spdlog::trace("co_spawn mqtt client");
_log.trace("co_spawn mqtt client");
boost::asio::co_spawn(_mqtt_client.get_executor(), listen(), boost::asio::detached);
}
@ -413,7 +394,7 @@ awaitable_expected< const AsyncMqttClient::SubscribtionToken * > AsyncMqttClient
BOOST_ASSERT(not topic.empty());
BOOST_ASSERT(cb);
spdlog::debug("MQTT subscribe");
_log.debug("subscribe");
co_return ASYNC_TRY(_impl->subscribe_with_callback(topic, std::move(cb)));
}
@ -421,7 +402,7 @@ awaitable_expected< void > AsyncMqttClient::publish(std::string_view topic, cons
BOOST_ASSERT(_impl);
BOOST_ASSERT(not topic.empty());
spdlog::debug("MQTT client publish on {}", topic);
_log.debug("publish on {}", topic);
ASYNC_CHECK(_impl->publish(topic, value));
co_return _void{};
@ -430,7 +411,7 @@ awaitable_expected< void > AsyncMqttClient::publish(std::string_view topic, cons
awaitable_expected< void > AsyncMqttClient::listen() const noexcept {
BOOST_ASSERT(_impl);
spdlog::debug("MQTT client listen");
_log.debug("listen");
ASYNC_CHECK(_impl->listen());
co_return _void{};
@ -439,7 +420,7 @@ awaitable_expected< void > AsyncMqttClient::listen() const noexcept {
void AsyncMqttClient::cancel() {
BOOST_ASSERT(_impl);
spdlog::debug("MQTT client cancel");
_log.debug("cancel");
_impl->cancel();
}

View File

@ -1,6 +1,7 @@
// settings_store.hpp
#pragma once
#include "ranczo-io/utils/logger.hpp"
#include "tl/expected.hpp"
#include <boost/system/detail/errc.hpp>
#include <config.hpp>
@ -73,6 +74,7 @@ class SettingsStore {
private:
Variant data_;
ModuleLogger _log{spdlog::default_logger(), "SettingsStore::Value"};
};
// --------------------- Ctor / Dtor ---------------------
@ -128,6 +130,7 @@ class SettingsStore {
executor_type main_exec_;
mutable boost::asio::thread_pool db_pool_;
std::string db_path_;
ModuleLogger _log{spdlog::default_logger(), "SettingsStore " + db_path_};
sqlite3 * db_ = nullptr;
};
@ -148,7 +151,7 @@ class ComponentSettingsStore {
template < typename T >
T get_value(std::string_view key) {
BOOST_ASSERT(store_.contains(key));
BOOST_ASSERT(store_.contains(component_, key)); // TODO type fits
return store_.get(component_, key).value().get_if< T >();
}
@ -158,14 +161,14 @@ class ComponentSettingsStore {
if(not contains) {
auto status = co_await async_save(key, _default);
if(not status) {
spdlog::warn("Cant save {}/{} to configuration store, returning default", component_, key);
_log.warn("Cant save {}/{} to configuration store, returning default", component_, key);
co_return _default;
}
}
// storage contains value, this should be safe
auto value = co_await async_get(key);
if(not value) {
spdlog::error("Component {} should contain {} but it dosn't, returning default", component_, key);
_log.error("Component {} should contain {} but it dosn't, returning default", component_, key);
co_return _default;
}
@ -201,6 +204,7 @@ class ComponentSettingsStore {
private:
SettingsStore & store_; // nie-właściciel
std::string component_; // np. "network", "feature", ...
ModuleLogger _log{spdlog::default_logger(), "ComponentSettingsStore " + component_};
};
} // namespace ranczo

View File

@ -2,10 +2,15 @@
#include <chrono>
#include <optional>
#include <ranczo-io/utils/memory_resource.hpp>
#include <string_view>
namespace ranczo::date {
// Convert ISO-8601/RFC3339-ish string ("YYYY-MM-DDTHH:MM:SS[.fff]Z") to system_clock::time_point.
// Falls back to nullopt on parse failure.
std::optional< std::chrono::system_clock::time_point > parse_timestamp_utc(std::string_view sv) noexcept;
std::pmr::string to_iso_timestamp(std::chrono::system_clock::time_point tp,
std::pmr::memory_resource * mr = memory_resource::default_resource());
} // namespace ranczo::date

View File

@ -1,13 +1,40 @@
#pragma once
#include <memory_resource>
#include <optional>
#include <boost/json/monotonic_resource.hpp>
namespace boost::json{
class value;
}
namespace ranczo::json{
// Adapter to use std::pmr::memory_resource with boost::json
class pmr_memory_resource_adapter : public boost::json::monotonic_resource::memory_resource {
std::pmr::memory_resource * upstream;
public:
explicit pmr_memory_resource_adapter(std::pmr::memory_resource * res) : upstream(res) {}
protected:
void * do_allocate(std::size_t size, std::size_t alignment) override {
return upstream->allocate(size, alignment);
}
void do_deallocate(void * p, std::size_t size, std::size_t alignment) override {
upstream->deallocate(p, size, alignment);
}
bool do_is_equal(const boost::json::monotonic_resource::memory_resource & other) const BOOST_NOEXCEPT override {
auto * o = dynamic_cast< const pmr_memory_resource_adapter * >(&other);
if(!o)
return false;
return upstream->is_equal(*o->upstream);
}
};
std::optional< double > as_number(const boost::json::value& v);
}

View File

@ -0,0 +1,128 @@
#pragma once
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include <memory>
#include <memory_resource>
#include <source_location>
#include <string_view>
#define LEVEL_trace spdlog::level::trace
#define LEVEL_debug spdlog::level::debug
#define LEVEL_info spdlog::level::info
#define LEVEL_warn spdlog::level::warn
#define LEVEL_error spdlog::level::err
#define MAKE_OVERLOADS(NAME) \
template < typename... Args > \
void NAME##_at(const std::source_location & loc, fmt::format_string< Args... > fmt, Args &&... args) const { \
_log(LEVEL_##NAME, loc, fmt, std::forward< Args >(args)...); \
} \
\
void NAME(fmt::format_string<> fmt, const std::source_location & loc = std::source_location::current()) const { \
_log(LEVEL_##NAME, loc, fmt); \
} \
\
template < typename A1 > \
void NAME(fmt::format_string< A1 > fmt, A1 && a1, const std::source_location & loc = std::source_location::current()) const { \
_log(LEVEL_##NAME, loc, fmt, std::forward< A1 >(a1)); \
} \
\
template < typename A1, typename A2 > \
void NAME(fmt::format_string< A1, A2 > fmt, A1 && a1, A2 && a2, const std::source_location & loc = std::source_location::current()) \
const { \
_log(LEVEL_##NAME, loc, fmt, std::forward< A1 >(a1), std::forward< A2 >(a2)); \
} \
\
template < typename A1, typename A2, typename A3 > \
void NAME(fmt::format_string< A1, A2, A3 > fmt, \
A1 && a1, \
A2 && a2, \
A3 && a3, \
const std::source_location & loc = std::source_location::current()) const { \
_log(LEVEL_##NAME, loc, fmt, std::forward< A1 >(a1), std::forward< A2 >(a2), std::forward< A3 >(a3)); \
} \
\
template < typename A1, typename A2, typename A3, typename A4 > \
void NAME(fmt::format_string< A1, A2, A3, A4 > fmt, \
A1 && a1, \
A2 && a2, \
A3 && a3, \
A4 && a4, \
const std::source_location & loc = std::source_location::current()) const { \
_log(LEVEL_##NAME, loc, fmt, std::forward< A1 >(a1), std::forward< A2 >(a2), std::forward< A3 >(a3), std::forward< A4 >(a4)); \
} \
\
template < typename A1, typename A2, typename A3, typename A4, typename A5 > \
void NAME(fmt::format_string< A1, A2, A3, A4, A5 > fmt, \
A1 && a1, \
A2 && a2, \
A3 && a3, \
A4 && a4, \
A5 && a5, \
const std::source_location & loc = std::source_location::current()) const { \
_log(LEVEL_##NAME, \
loc, \
fmt, \
std::forward< A1 >(a1), \
std::forward< A2 >(a2), \
std::forward< A3 >(a3), \
std::forward< A4 >(a4), \
std::forward< A5 >(a5)); \
}
namespace ranczo {
class ModuleLogger {
public:
ModuleLogger(std::shared_ptr< spdlog::logger > base,
std::string_view module,
std::pmr::memory_resource * mr = std::pmr::get_default_resource())
: _mr{mr}, _base(std::move(base)), _module{module, _mr} {}
MAKE_OVERLOADS(trace)
MAKE_OVERLOADS(debug)
MAKE_OVERLOADS(info)
MAKE_OVERLOADS(warn)
MAKE_OVERLOADS(error)
private:
inline std::string_view short_filename(std::string_view full) const {
using sv = std::string_view;
// usuń trailing slash jeśli jest
while(!full.empty() && full.back() == '/')
full.remove_suffix(1);
// znajdź początek ostatniego katalogu
size_t last_slash = full.rfind('/');
if(last_slash == sv::npos)
return full; // był tylko plik bez katalogu
// znajdź wcześniejszy slash, aby wyciąć nazwę katalogu
size_t prev_slash = full.rfind('/', last_slash - 1);
if(prev_slash == sv::npos)
return full.substr(0); // format "dir/file" już OK
return full.substr(prev_slash + 1);
}
// wersja z std::source_location
inline std::string_view short_filename(const std::source_location & loc) const {
return short_filename(loc.file_name());
}
template < typename... Args >
void _log(spdlog::level::level_enum lvl, const std::source_location & loc, fmt::format_string< Args... > fmt, Args &&... args) const {
if(!_base || !_base->should_log(lvl))
return;
auto msg = fmt::format(fmt, std::forward< Args >(args)...);
_base->log(lvl, "[{}({}:{})] {}", _module, short_filename(loc.file_name()), loc.line(), msg);
}
std::pmr::memory_resource * _mr;
std::shared_ptr< spdlog::logger > _base;
std::pmr::string _module;
};
} // namespace ranczo

View File

@ -0,0 +1,305 @@
#pragma once
#include "ranczo-io/utils/logger.hpp"
#include <algorithm>
#include <boost/assert/source_location.hpp>
#include <cstddef>
#include <cstdint>
#include <memory_resource>
#include <source_location>
#include <spdlog/spdlog.h>
#include <string>
namespace ranczo {
namespace memory_resource {
namespace literals {
constexpr std::uint64_t operator""_kB(unsigned long long kilobytes) {
return kilobytes * 1024ULL;
}
constexpr std::uint64_t operator""_MB(unsigned long long megabytes) {
return megabytes * 1024ULL * 1024ULL;
}
} // namespace literals
// -------------------------------------------------------------------------
// Global domyślne resource (jak miałeś)
// -------------------------------------------------------------------------
struct default_memory_resource {
static inline std::pmr::memory_resource * _mr = std::pmr::get_default_resource();
};
inline void set_default_resource(std::pmr::memory_resource * memory_resource) {
default_memory_resource{}._mr = memory_resource;
}
inline std::pmr::memory_resource * default_resource() {
return default_memory_resource{}._mr;
}
// -------------------------------------------------------------------------
// Wspólny tracker statystyk + logowanie
// -------------------------------------------------------------------------
struct allocation_stats {
private:
static std::array< char, 6 > make_id(const void * self) {
std::array< char, 6 > out{};
constexpr char alphabet[] = "0123456789abcdef"; // jak hex, 5 znaków
std::uintptr_t v = reinterpret_cast< std::uintptr_t >(self);
v ^= (v >> 17);
v ^= (v << 7);
v ^= (v >> 11);
for(int i = 0; i < 5; ++i) {
out[i] = alphabet[v & 0xF];
v >>= 4;
}
out[5] = '\0';
return out;
}
std::string_view _name;
std::source_location _sl;
std::array< char, 6 > _id{}; // 5 znaków + '\0'
ModuleLogger _log{spdlog::default_logger(), std::string{"alloc "} + std::string{_name} + "#" + _id.data()};
public:
std::size_t current_bytes{0};
std::size_t peak_bytes{0};
std::size_t total_allocations{0};
std::size_t total_deallocations{0};
// name: np. "MonotonicResource", "StackPoolResource", itd.
allocation_stats(std::string_view name, std::source_location loc = std::source_location::current())
: _name{name}, _sl{loc}, _id{make_id(this)} {}
~allocation_stats() {
if(total_allocations == 0 && total_deallocations == 0) {
// nic nie alokował zwykle szkoda logować
return;
}
if(total_allocations == total_deallocations) {
_log.info_at(_sl,
"balanced: alloc={}, dealloc={}, peak={}, current={}",
total_allocations,
total_deallocations,
peak_bytes,
current_bytes);
} else {
_log.warn_at(_sl,
"LEAK/MISMATCH: alloc={}, dealloc={}, peak={}, current={}",
total_allocations,
total_deallocations,
peak_bytes,
current_bytes);
}
}
void on_allocate(std::size_t bytes) {
current_bytes += bytes;
peak_bytes = std::max(peak_bytes, current_bytes);
++total_allocations;
_log.trace_at(_sl, "+{} bytes -> current={}, peak={}", bytes, current_bytes, peak_bytes);
}
void on_deallocate(std::size_t bytes) {
if(bytes <= current_bytes) {
current_bytes -= bytes;
} else {
current_bytes = 0;
}
++total_deallocations;
_log.trace_at(_sl, "-{} bytes -> current={}, peak={}", bytes, current_bytes, peak_bytes);
}
};
// -------------------------------------------------------------------------
// Monotonic heap-backed
// -------------------------------------------------------------------------
class MonotonicHeapResource : public std::pmr::memory_resource {
public:
MonotonicHeapResource( //
std::size_t N,
std::pmr::memory_resource * upstream = default_resource(),
std::source_location sl = std::source_location::current())
: _upstream{upstream}, _buffer{( char * ) _upstream->allocate(N)}, _n{N}, _mr{_buffer, N, upstream}, _sl{sl} {}
~MonotonicHeapResource() {
_upstream->deallocate(_buffer, _n);
}
private:
std::pmr::memory_resource * _upstream;
alignas(std::max_align_t) char * _buffer;
std::size_t _n;
std::pmr::monotonic_buffer_resource _mr;
std::source_location _sl;
allocation_stats _stats{"mono_stack", _sl};
protected:
void * do_allocate(std::size_t bytes, std::size_t alignment) override {
void * p = _mr.allocate(bytes, alignment);
_stats.on_allocate(bytes);
return p;
}
void do_deallocate(void * p, std::size_t bytes, std::size_t alignment) override {
// for monotonic this is a noop
_stats.on_deallocate(0);
_mr.deallocate(p, bytes, alignment);
}
bool do_is_equal(const memory_resource & other) const noexcept override {
return _mr.is_equal(other);
}
};
// -------------------------------------------------------------------------
// Monotonic stack-backed
// -------------------------------------------------------------------------
template < std::size_t N >
class MonotonicStackResource : public std::pmr::memory_resource {
public:
MonotonicStackResource( //
std::pmr::memory_resource * upstream = default_resource(),
std::source_location sl = std::source_location::current())
: _buffer{}, _mr{_buffer, N, upstream}, _sl{sl} {}
private:
alignas(std::max_align_t) char _buffer[N];
std::pmr::monotonic_buffer_resource _mr{_buffer, N};
std::source_location _sl;
allocation_stats _stats{"mono_stack", _sl};
protected:
void * do_allocate(std::size_t bytes, std::size_t alignment) override {
void * p = _mr.allocate(bytes, alignment);
_stats.on_allocate(bytes);
return p;
}
void do_deallocate(void * p, std::size_t bytes, std::size_t alignment) override {
// for monotonic this is a noop
_stats.on_deallocate(0);
_mr.deallocate(p, bytes, alignment);
}
bool do_is_equal(const memory_resource & other) const noexcept override {
return _mr.is_equal(other);
}
};
using MonotonicStack_512_Resource = MonotonicStackResource< 512 >;
using MonotonicStack_1k_Resource = MonotonicStackResource< 1 * 1024 >;
using MonotonicStack_2k_Resource = MonotonicStackResource< 2 * 1024 >;
// -----------------------------
// Pool allocator: stack-backed
// -----------------------------
template < std::size_t N >
class StackPoolResource : public std::pmr::memory_resource {
public:
explicit StackPoolResource( //
std::pmr::memory_resource * upstream = default_resource(),
std::source_location sl = std::source_location::current())
: _stack{upstream, sl}, opts{}, _mr{opts, &_stack}, _sl{sl} {}
private:
MonotonicStackResource< N > _stack;
std::pmr::pool_options opts{};
std::pmr::unsynchronized_pool_resource _mr{opts, &_stack};
std::source_location _sl;
allocation_stats _stats{"stack_unsunchro", _sl};
protected:
void * do_allocate(std::size_t bytes, std::size_t alignment) override {
void * p = _mr.allocate(bytes, alignment);
_stats.on_allocate(bytes);
return p;
}
void do_deallocate(void * p, std::size_t bytes, std::size_t alignment) override {
_stats.on_deallocate(bytes);
_mr.deallocate(p, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource & other) const noexcept override {
return _mr.is_equal(other);
}
};
// -------------------------------------------------------------------------
// Pool allocator: heap-backed
// -------------------------------------------------------------------------
class HeapPoolResource : public std::pmr::memory_resource {
public:
explicit HeapPoolResource( //
std::size_t N,
std::pmr::memory_resource * upstream = default_resource(),
std::source_location sl = std::source_location::current())
: _heap{N, upstream}, _sl{sl} {}
private:
MonotonicHeapResource _heap;
std::pmr::pool_options opts{};
std::pmr::unsynchronized_pool_resource _mr{opts, &_heap};
std::source_location _sl;
allocation_stats _stats{"mono_heap", _sl};
protected:
void * do_allocate(std::size_t bytes, std::size_t alignment) override {
void * p = _mr.allocate(bytes, alignment);
_stats.on_allocate(bytes);
return p;
}
void do_deallocate(void * p, std::size_t bytes, std::size_t alignment) override {
_stats.on_deallocate(bytes);
_mr.deallocate(p, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource & other) const noexcept override {
return _mr.is_equal(other);
}
};
using StackPool_512_Resource = StackPoolResource< 512 >;
using StackPool_1k_Resource = StackPoolResource< 1 * 1024 >;
using StackPool_2k_Resource = StackPoolResource< 2 * 1024 >;
using StackPool_4k_Resource = StackPoolResource< 4 * 1024 >;
using StackPool_8k_Resource = StackPoolResource< 8 * 1024 >;
// using HeapPool_Default_Resource = HeapPoolResource;
// -------------------------------------------------------------------------
// Helper: alokacja z source_location (opcjonalne użycie)
// -------------------------------------------------------------------------
inline void * allocate(std::pmr::memory_resource & r,
std::size_t bytes,
std::size_t alignment = alignof(std::max_align_t),
std::source_location loc = std::source_location::current()) {
// UWAGA: source_location wykorzystujemy tylko do logowania w helperze.
// W samym memory_resource nie ma parametru source_location,
// więc tam widzimy tylko lokalizację wewnątrz alokatora.
spdlog::info("allocate() helper: {} bytes at {}:{} ({})", bytes, loc.file_name(), loc.line(), loc.function_name());
return r.allocate(bytes, alignment);
}
} // namespace memory_resource
} // namespace ranczo

View File

@ -2,6 +2,8 @@
#pragma once
#include "config.hpp"
#include "ranczo-io/utils/logger.hpp"
#include "spdlog/spdlog.h"
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/thread_pool.hpp>
@ -19,7 +21,11 @@ namespace ranczo {
class ModbusTcpContext : public std::enable_shared_from_this< ModbusTcpContext > {
public:
ModbusTcpContext(boost::asio::io_context & io, std::string host, int port, std::size_t pool_size = 1)
: io_(io), host_(std::move(host)), port_(port), pool_(static_cast< unsigned >(pool_size)) {}
: io_(io),
_log{spdlog::default_logger(), "Modbus " + host + ":" + std::to_string(port)},
host_(std::move(host)),
port_(port),
pool_(static_cast< unsigned >(pool_size)) {}
~ModbusTcpContext();
@ -27,13 +33,13 @@ class ModbusTcpContext : public std::enable_shared_from_this< ModbusTcpContext >
awaitable_expected< void > async_connect();
awaitable_expected< void > async_close();
bool connected()const;
bool connected() const;
// Udostępnione wywołanie: lambda dostaje wskaźnik na aktywny kontekst libmodbus
template < typename T, typename F >
awaitable_expected< T > call_with_lock(F && op);
awaitable_expected<void> wait_between_commands();
awaitable_expected< void > wait_between_commands();
private:
struct CtxDeleter {
@ -50,6 +56,7 @@ class ModbusTcpContext : public std::enable_shared_from_this< ModbusTcpContext >
awaitable_expected< T > async_call(Maker && maker);
boost::asio::io_context & io_;
ModuleLogger _log;
std::string host_;
int port_;
boost::asio::thread_pool pool_;
@ -72,9 +79,9 @@ class ModbusDevice {
// FC 0x03: pojedynczy holding register
awaitable_expected< std::uint16_t > async_read_holding_register(std::uint16_t address);
// FC 0x05 ZAPIS CEWKI (coil/bit): true=ON, false=OFF
awaitable_expected<void> async_write_coil(std::uint16_t address, bool value);
awaitable_expected< void > async_write_coil(std::uint16_t address, bool value);
private:
static boost::system::error_code errno_errc() {
@ -83,6 +90,7 @@ class ModbusDevice {
std::shared_ptr< ModbusTcpContext > ctx_;
int unit_id_;
ModuleLogger _log{spdlog::default_logger(), "ModbusDevice " + std::to_string(unit_id_)};
};
// Pomocnicza fabryka

View File

@ -1,5 +1,7 @@
#pragma once
#include <ranczo-io/utils/logger.hpp>
#include <boost/json/value.hpp>
#include <config.hpp>
@ -35,6 +37,7 @@ class AsyncMqttClient {
struct AsyncMqttClientImpl;
std::unique_ptr< AsyncMqttClientImpl > _impl;
ModuleLogger _log{spdlog::default_logger(), "MQTT Client"};
AsyncMqttClient(const executor & executor);
~AsyncMqttClient();
@ -49,6 +52,7 @@ class AsyncMqttClient {
awaitable_expected< void > listen() const noexcept;
void cancel();
};
} // namespace ranczo

View File

@ -2,6 +2,7 @@
#include <array>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <memory_resource>
#include <string>
#include <string_view>
@ -52,11 +53,40 @@ std::string make_topic(const Args &... args) {
return result;
}
template < typename... Args >
std::pmr::string make_topic(std::pmr::memory_resource &mr, const Args &... args) {
using namespace std::string_literals;
constexpr size_t count = sizeof...(Args);
if constexpr(count == 0) {
return "";
}
auto separator = R"(/)"s;
std::array< std::string_view, count > segments{std::string_view(args)...};
size_t total_size = length(args...) + segments.size();
std::pmr::string result{&mr};
result.reserve(total_size);
result.append(segments[0]);
for(size_t i = 1; i < count; ++i) {
result.append(separator);
result.append(segments[i]);
}
return result;
}
// Full topic: home/<room>/<type>/<place>/{N}/<action>
inline std::string buildTopic(std::string_view room, std::string_view type, std::string_view place, uint number, std::string_view action) {
using namespace std::string_view_literals;
return make_topic("home"sv, room, type, place, std::to_string(number), action);
}
inline std::pmr::string buildTopic(std::pmr::memory_resource &mr, std::string_view room, std::string_view type, std::string_view place, uint number, std::string_view action) {
using namespace std::string_view_literals;
return make_topic(mr, "home"sv, room, type, place, std::to_string(number), action);
}
namespace topic {
namespace heating {
@ -101,6 +131,10 @@ namespace topic {
using namespace std::string_view_literals;
return buildTopic( room, "heating"sv, "floor"sv, zone,"state"sv);
}
inline std::pmr::string state(std::pmr::memory_resource&mr, std::string_view room, int zone =1) {
using namespace std::string_view_literals;
return buildTopic(mr, room, "heating"sv, "floor"sv, zone, "state"sv);
}
// Topic:
// home/<room>/heating/floor/<N>/config
@ -151,6 +185,11 @@ namespace topic {
using namespace std::string_view_literals;
return buildTopic(room, "sensor"sv, "floor"sv, zone, "temperature"sv);
}
inline std::pmr::string floor(std::pmr::memory_resource&mr, std::string_view room, int zone) {
BOOST_ASSERT(zone>0);// no broadcast
using namespace std::string_view_literals;
return buildTopic(mr, room, "sensor"sv, "floor"sv, zone, "temperature"sv);
}
// Topic:
// home/<room>/sensor/air/<N>/temperature
@ -172,6 +211,12 @@ namespace topic {
using namespace std::string_view_literals;
return buildTopic(room, "sensor"sv, "air"sv,zone, "temperature"sv);
}
inline std::pmr::string air(std::pmr::memory_resource&mr, std::string_view room, int zone) {
BOOST_ASSERT(zone>0);// no broadcast
using namespace std::string_view_literals;
return buildTopic(mr, room, "sensor"sv, "air"sv, zone, "temperature"sv);
}
} // namespace temperature
} // namespace topic

View File

@ -1,3 +1,10 @@
set(SYSTEMD_UNIT_DIR "lib/systemd/system" CACHE PATH "systemd unit dir")
set(RANCZO_USER "ranczoio")
set(RANCZO_GROUP "ranczoio")
include(GNUInstallDirs)
add_subdirectory(floorheat_svc)
add_subdirectory(temperature_svc)
add_subdirectory(output_svc)

View File

@ -3,6 +3,11 @@ add_executable(ranczo-io_floorheating
temperature_controller.hpp temperature_controller.cpp
relay.hpp relay.cpp
thermometer.hpp thermometer.cpp
ranczo-io_floorheating.service.in
postinst
prerm
postrm
)
target_link_libraries(ranczo-io_floorheating
@ -11,23 +16,47 @@ target_link_libraries(ranczo-io_floorheating
fmt::fmt
)
include(GNUInstallDirs)
install(
TARGETS ranczo-io_floorheating
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT floorheating
)
enable_testing()
# Opis komponentu floorheating
set(CPACK_COMPONENT_FLOORHEATING_DISPLAY_NAME "Ranczo-IO Floor Heating Service" CACHE INTERNAL "package section")
set(CPACK_COMPONENT_FLOORHEATING_DESCRIPTION "Serwis sterujący ogrzewaniem podłogowym." CACHE INTERNAL "package section")
set(CPACK_COMPONENT_FLOORHEATING_REQUIRED ON CACHE INTERNAL "package section")
add_test(NAME ranczo_io_floorheating_integration
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/integration_tests/run_docker_integration_tests.sh
# Nazwa samego pakietu DEB dla komponentu
set(CPACK_DEBIAN_FLOORHEATING_PACKAGE_NAME "ranczo-io-floorheating" CACHE INTERNAL "package name")
set(CPACK_DEBIAN_FLOORHEATING_PACKAGE_SECTION "utils" CACHE INTERNAL "package section")
set(CPACK_DEBIAN_FLOORHEATING_PACKAGE_MAINTAINER "b.wieczorek@dx.net.pl" CACHE INTERNAL "package maintainer")
set(CPACK_DEBIAN_FLOORHEATING_PACKAGE_CONTROL_EXTRA
"${CMAKE_CURRENT_SOURCE_DIR}/postinst"
"${CMAKE_CURRENT_SOURCE_DIR}/prerm"
"${CMAKE_CURRENT_SOURCE_DIR}/postrm"
CACHE INTERNAL "package extra"
)
set_tests_properties(ranczo_io_floorheating_integration PROPERTIES
ENVIRONMENT INSTALL_DIR=${CMAKE_BINARY_DIR}/install
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/integration_tests
TIMEOUT 120
set(CPACK_DEBIAN_FLOORHEATING_PACKAGE_DEPENDS "libsqlite3-0, systemd (>= 245)" CACHE INTERNAL "package depends")
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io_floorheating.service.in
${CMAKE_CURRENT_BINARY_DIR}/ranczo-io_floorheating.service
@ONLY
)
# Instalacja unita systemd
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/ranczo-io_floorheating.service
DESTINATION ${SYSTEMD_UNIT_DIR}
COMPONENT floorheating
)
# tworzenie katalogów
install(DIRECTORY
DESTINATION var/lib/ranczo-io/floorheating
COMPONENT floorheating
)

View File

@ -252,8 +252,8 @@ int main() {
_heaters.clear(); // remove all heaters
mqttProxyClient.cancel();
mqttClient.cancel();
modbusDevices.clear();
// modbus_relayBoardsCtx->async_close();
work_guard.reset();
io_context.stop(); // ok na tym samym wątku

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Jeżeli tworzysz usera, możesz to zrobić tutaj (albo w osobnym pakiecie)
# adduser --system --group --no-create-home ranczoio || true
systemctl daemon-reload || true
systemctl enable --now ranczo-io-floorheating.service || true
;;
esac
exit 0

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
remove)
systemctl daemon-reload || true
;;
purge)
systemctl disable ranczo-io-floorheating.service || true
systemctl daemon-reload || true
;;
esac
exit 0

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
case "$1" in
remove|upgrade|deconfigure)
systemctl stop ranczo-io-floorheating.service || true
;;
esac
exit 0

View File

@ -0,0 +1,20 @@
[Unit]
Description=Ranczo-IO Floor Heating service
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=@RANCZO_USER@
Group=@RANCZO_GROUP@
ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/ranczo-io_floorheating
WorkingDirectory=/var/lib/ranczo-io/floorheating
Restart=on-failure
RestartSec=5
# /run/ranczo-io-floorheating będzie robione automatycznie
RuntimeDirectory=ranczo-io-floorheating
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target

View File

@ -1,31 +1,34 @@
#pragma once
#include <boost/align/detail/align.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/json/object.hpp>
#include <boost/regex/config.hpp>
#include <boost/smart_ptr/shared_ptr.hpp>
#include <config.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <fmt/format.h>
#include <ranczo-io/utils/logger.hpp>
#include <ranczo-io/utils/modbus.hpp>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <spdlog/spdlog.h>
#include <string>
namespace ranczo {
class Relay {
public:
virtual ~Relay() = default;
enum class State{
On,Off
};
virtual awaitable_expected< State > state() const noexcept = 0;
virtual awaitable_expected< void > on() noexcept = 0;
virtual awaitable_expected< void > off() noexcept = 0;
enum class State { On, Off };
virtual awaitable_expected< State > state() const noexcept = 0;
virtual awaitable_expected< void > on() noexcept = 0;
virtual awaitable_expected< void > off() noexcept = 0;
};
class ModbusRelay : public Relay {
boost::asio::any_io_executor _executor;
ModuleLogger _log;
std::shared_ptr< ranczo::ModbusDevice > _dev;
int _channel;
Relay::State _state;
@ -34,21 +37,31 @@ class ModbusRelay : public Relay {
public:
// Prost y mapping: coil_addr = base_addr + channel
ModbusRelay(boost::asio::any_io_executor ex, std::shared_ptr< ranczo::ModbusDevice > dev, int channel, std::uint16_t base_coil_addr = 0)
: _executor{ex}, _dev{std::move(dev)}, _channel{channel}, _coil_addr{static_cast< std::uint16_t >(base_coil_addr + channel)} {
: _executor{ex},
_log{spdlog::default_logger(),
[&]() {
BOOST_ASSERT(dev);
return std::string{"Modbus Relay"} + std::to_string(dev->unit()) + "/" + std::to_string(channel);
}()},
_dev{std::move(dev)},
_channel{channel},
_coil_addr{static_cast< std::uint16_t >(base_coil_addr + channel)} {
BOOST_ASSERT(_dev);
boost::asio::co_spawn(
_executor,
[=, this]() -> awaitable_expected< void > {
auto state = co_await _dev->async_read_holding_register(channel);
if(state.has_value()) {
spdlog::info("Modbus relay device {}/{} status at create: {}", _dev->unit(), channel, state.value());
_log.info("status at create: {}", state.value());
} else {
spdlog::info("Modbus relay device {}/{} failed to read status {}", _dev->unit(), channel, state.error().value());
_log.info("failed to read status {}", state.error().value());
}
co_return _void{};
},
boost::asio::detached);
}
awaitable_expected< Relay::State > state() const noexcept override {
co_return _state;
}
@ -56,25 +69,25 @@ class ModbusRelay : public Relay {
awaitable_expected< void > on() noexcept override {
BOOST_ASSERT(_dev);
spdlog::info("relay {}/{} ON (coil={})", _dev->unit(), _channel, _coil_addr);
_log.debug("ON (coil={})", _coil_addr);
// auto r = co_await _dev->async_write_coil(_coil_addr, true);
// if(!r)
// co_return unexpected(r.error());
_state = Relay::State::On ;
_state = Relay::State::On;
co_return _void{};
}
awaitable_expected< void > off() noexcept override {
BOOST_ASSERT(_dev);
spdlog::info("relay {}/{} OFF (coil={})", _dev->unit(), _channel, _coil_addr);
_log.debug("OFF (coil={})", _coil_addr);
// auto r = co_await _dev->async_write_coil(_coil_addr, false);
// if(!r)
// co_return unexpected(r.error());
_state = Relay::State::Off;
co_return _void{};
}

View File

@ -1,4 +1,5 @@
#include "temperature_controller.hpp"
#include "thermometer.hpp"
#include "config.hpp"
#include <boost/asio/co_spawn.hpp>
@ -12,18 +13,20 @@
#include <boost/system/detail/error_code.hpp>
#include <boost/system/errc.hpp>
#include <boost/system/result.hpp>
#include <cstddef>
#include <functional>
#include <memory>
#include <optional>
#include <spdlog/spdlog.h>
#include "ranczo-io/utils/config.hpp"
#include "services/floorheat_svc/relay.hpp"
#include "thermometer.hpp"
#include <ranczo-io/utils/config.hpp>
#include <ranczo-io/utils/date_utils.hpp>
#include <ranczo-io/utils/json_helpers.hpp>
#include <ranczo-io/utils/logger.hpp>
#include <ranczo-io/utils/memory_resource.hpp>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
#include <services/floorheat_svc/relay.hpp>
#include <chrono>
@ -42,6 +45,17 @@ namespace ranczo {
enum class ThermostatState { Enabled, Disabled, Error };
enum class Trend { Fall, Const, Rise };
std::pmr::string Trend_to_string(Trend state) {
switch(state) {
case Trend::Fall:
return "Fall";
case Trend::Const:
return "Const";
default:
return "Rise";
}
}
std::optional< ThermostatState > ThermostatState_from_string(std::optional< std::string > state) {
if(not state) {
return std::nullopt;
@ -193,7 +207,15 @@ namespace commands {
return cmd;
}
static constexpr std::string_view topic_suffix = "slope_window";
static constexpr std::string_view topic_suffix = "slope_diff";
};
struct StatusRequest {
static expected< StatusRequest > from_payload(const boost::json::value & payload) {
return {};
}
static constexpr std::string_view topic_suffix = "status";
};
} // namespace commands
@ -208,10 +230,12 @@ struct ThermometerMeasurements {
Thermometer::ThermometerData data;
};
executor & _io;
ModuleLogger _log;
std::unique_ptr< Thermometer > _sensor;
boost::circular_buffer< Measurement > _history;
ThermometerMeasurements(executor & io, std::unique_ptr< Thermometer > sensor) : _io{io}, _sensor{std::move(sensor)}, _history{200} {}
ThermometerMeasurements(executor & io, std::unique_ptr< Thermometer > sensor)
: _io{io}, _log{spdlog::default_logger(), "ThermometerMeasurements"}, _sensor{std::move(sensor)}, _history{200} {}
/**
* @brief start the service, we can't make it happen in ctor so we need a different approach
@ -232,7 +256,7 @@ struct ThermometerMeasurements {
BOOST_ASSERT(_sensor);
// subscribe to a thermometer readings
ASYNC_CHECK_MSG(subscribeToTemperatureUpdate(), "subscribtion to temperature stream failed");
ASYNC_CHECK_LOG(subscribeToTemperatureUpdate(), "subscribtion to temperature stream failed");
// spins own mqtt listener 'thread' and executes on update callback on every temperature update
boost::asio::co_spawn(_io, _sensor->listen(), boost::asio::detached);
@ -250,7 +274,10 @@ struct ThermometerMeasurements {
auto timeDiff = std::chrono::system_clock::now() - _history.back().when;
if(timeDiff < std::chrono::nanoseconds{0}) {
spdlog::warn("temperature measurements are from the future");
memory_resource::MonotonicStackResource<64> mr;
auto now_ts = date::to_iso_timestamp(std::chrono::system_clock::now(), &mr);
auto meas_ts = date::to_iso_timestamp(_history.back().when, &mr);
_log.warn("measurements are from the future (measurement: {}, now: {})", meas_ts, now_ts);
}
return timeDiff;
@ -267,6 +294,9 @@ struct ThermometerMeasurements {
return _history.back().data.temp_c();
}
std::size_t size() const noexcept {
return _history.size();
}
/**
* @brief temperatureTrend
* @param window which should be used to get the trend
@ -276,12 +306,12 @@ struct ThermometerMeasurements {
std::optional< Trend > temperatureTrend(std::chrono::nanoseconds window = std::chrono::minutes(5),
double epsilon_deg_per_min = 0.2) const {
if(auto last = timeSinceLastRead(); not last.has_value()) {
spdlog::debug("No temperature samples available");
_log.debug("No temperature samples available");
return std::nullopt;
}
if(_history.size() < 2) {
spdlog::debug("Too few samples in the last {} minutes, no trend can be calculated",
_log.debug("Too few samples in the last {} minutes, no trend can be calculated",
std::chrono::duration_cast< std::chrono::duration< double, std::ratio< 60 > > >(window).count());
return Trend::Const;
}
@ -331,7 +361,7 @@ struct ThermometerMeasurements {
// fallback: pochylona między pierwszą i ostatnią próbką
const double dt_s = std::chrono::duration< double >(pts.back().when - pts.front().when).count();
if(dt_s <= 0.0) {
spdlog::warn("No time spread in samples (dt = {}).", dt_s);
_log.warn("No time spread in samples (dt = {}).", dt_s);
return Trend::Const;
}
slope_deg_per_s = (pts.back().temp - pts.front().temp) / dt_s;
@ -339,7 +369,7 @@ struct ThermometerMeasurements {
const double slope_deg_per_min = slope_deg_per_s * 60.0;
spdlog::debug(
_log.debug(
"Trend (5 min): samples={}, slope={:.4f} °C/min, threshold={:.4f} °C/min", pts.size(), slope_deg_per_min, epsilon_deg_per_min);
if(slope_deg_per_min > epsilon_deg_per_min)
@ -354,6 +384,7 @@ struct ThermometerMeasurements {
struct RelayThermostat::Impl : private boost::noncopyable {
private:
executor & _io;
ModuleLogger _log;
AsyncMqttClient & _mqtt;
ComponentSettingsStore _settings;
@ -476,22 +507,19 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// check if temperature is constantly read
if(not checkLastTemperatureRead()) {
spdlog::warn("temperature sensor timeout (> 5 minutes without update) for {}/{}", _room, _zone);
_log.warn("Temperature sensor timeout (> 5 minutes without update)");
co_return false;
}
auto tempOpt = _thermo.currentTemperature();
if(!tempOpt) {
spdlog::warn("No temperature samples for {}/{}", _room, _zone);
_log.warn("No temperature samples");
co_return false;
}
auto trendOpt = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
if(!trendOpt) {
spdlog::warn("No temperature samples for {}/{} for last {}s",
_room,
_zone,
std::chrono::duration_cast< std::chrono::seconds >(_slopeWindow).count());
_log.warn("No temperature samples for last {}s", std::chrono::duration_cast< std::chrono::seconds >(_slopeWindow).count());
co_return false;
}
auto trend = *trendOpt;
@ -501,13 +529,13 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// 2a) relay OFF, a temperatura rośnie => przekaźnik zawiesił się na ON
if(!relayOn && trend == Trend::Rise) {
spdlog::warn("relay stuck ON: temperature rising while relay is commanded OFF for {}/{}", _room, _zone);
_log.warn("relay stuck ON: temperature rising while relay is commanded OFF");
co_return false;
}
// 2b) relay ON, a trend != Rise => przekaźnik zawiesił się na OFF
if(relayOn && trend != Trend::Rise) {
spdlog::warn("relay stuck OFF: temperature not rising while relay is commanded ON for {}/{}", _room, _zone);
_log.warn("relay stuck OFF: temperature not rising while relay is commanded ON");
co_return false;
}
@ -519,13 +547,13 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// only relay->state can fail
auto preconditionsMet = ASYNC_TRY(preconditions());
if(preconditionsMet) {
spdlog::info("Switching back to Enabled due to met preconditions {}/{}", _room, _zone);
_log.info("Switching back to Enabled due to met preconditions");
_state = ThermostatState::Enabled; // should be from previous
} else {
auto relay_on = ASYNC_TRY(_relay->state()) == Relay::State::On;
if(relay_on) {
spdlog::warn("Forcing relay OFF in ERROR state for {}/{}", _room, _zone);
ASYNC_CHECK_MSG(_relay->off(), "Emergency relay OFF failed!");
_log.warn("Forcing relay OFF in ERROR state");
ASYNC_CHECK_LOG(_relay->off(), "Emergency relay OFF failed!");
}
}
co_return _void{};
@ -534,8 +562,8 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > handleDisabledState() {
auto st = ASYNC_TRY(_relay->state());
if(st == Relay::State::On) {
spdlog::info("RelayThermostat disabling relay because thermostat is Disabled for {}/{}", _room, _zone);
ASYNC_CHECK_MSG(safe_off(), "relay OFF failed"); // TODO basically PANIC mode on fail
_log.info("disabling relay because thermostat is Disabled");
ASYNC_CHECK_LOG(safe_off(), "relay OFF failed"); // TODO basically PANIC mode on fail
}
co_return _void{};
}
@ -545,7 +573,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
auto preconditionsMet = ASYNC_TRY(preconditions());
if(not preconditionsMet) {
spdlog::warn("RelayThermostat turning set disabled state due to failed preconditions");
_log.warn("turning set disabled state due to failed preconditions");
_state = ThermostatState::Error;
co_return _void{};
}
@ -561,25 +589,21 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// grzejemy jeżeli temp < setpoint - histereza
if(!relayOn) {
if(temp < _targetTemperature - _hysteresis && minElapsed >= _tickTime) {
spdlog::info("RelayThermostat turning relay ON for {}/{} (temp = {}, setpoint = {}, h = {})",
_room,
_zone,
_log.info("turning relay ON (temp = {}, setpoint = {}, h = {})",
temp,
_targetTemperature,
_hysteresis);
ASYNC_CHECK_MSG(_relay->on(), "Enabling relay failed");
ASYNC_CHECK_LOG(_relay->on(), "Enabling relay failed");
_lastStateChange = now;
}
} else {
// wyłączamy grzanie jeżeli temp > setpoint + histereza
if(temp > _targetTemperature + _hysteresis && minElapsed >= _tickTime) {
spdlog::info("RelayThermostat turning relay OFF for {}/{} (temp = {}, setpoint = {}, h = {})",
_room,
_zone,
_log.info("turning relay OFF (temp = {}, setpoint = {}, h = {})",
temp,
_targetTemperature,
_hysteresis);
ASYNC_CHECK_MSG(safe_off(), "Disabling relay failed");
ASYNC_CHECK_LOG(safe_off(), "Disabling relay failed");
_lastStateChange = now;
}
}
@ -594,7 +618,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
// Bezpieczeństwo: wyłącz przekaźnik natychmiastowo
auto expectedState = co_await _relay->state();
if(!expectedState) {
spdlog::warn("Cant get relay {} state", this->_room);
_log.warn("Cant get relay state");
--retries;
continue;
}
@ -602,7 +626,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
if(st == Relay::State::On) {
auto expectedOff = co_await _relay->off();
if(!expectedOff) {
spdlog::warn("Cant turn off relay {}", this->_room);
_log.warn("Cant turn off relay");
--retries;
continue;
}
@ -614,14 +638,14 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > error(std::string_view reason) {
if(_state == ThermostatState::Error) {
spdlog::error("RelayThermostat {}/{} additional error while already in ERROR state: {}", _room, _zone, reason);
_log.error("additional error while already in ERROR state: {}",reason);
co_return _void{};
}
spdlog::error("RelayThermostat {}/{} entering ERROR state: {}", _room, _zone, reason);
_log.error("entering ERROR state: {}", reason);
_state = ThermostatState::Error;
ASYNC_CHECK_MSG(safe_off(), "");
ASYNC_CHECK_LOG(safe_off(), "");
// TODO: tu możesz wysłać komunikat MQTT, zapisać do DB itp.
co_return _void{};
@ -637,6 +661,8 @@ struct RelayThermostat::Impl : private boost::noncopyable {
std::string_view room,
int zone)
: _io{io},
_log{spdlog::default_logger(),
[&]() -> std::string { return "RelayThermostat impl: " + std::string{room} + "/" + std::to_string(zone); }()},
_settings{setup, std::string{room}},
_mqtt{mqtt},
_relay{std::move(relay)},
@ -653,7 +679,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > start() {
using namespace std::placeholders;
using namespace std::chrono;
spdlog::info("RelayThermostat::start room : {}", _room);
_log.info("Start");
auto toSec = [](auto t) { return seconds{t}.count(); };
@ -669,12 +695,12 @@ struct RelayThermostat::Impl : private boost::noncopyable {
_sensorTimeout = seconds{co_await _settings.async_get_store_default("sensor_timeout_s", toSec(minutes{5}))};
// subscribe to a thermostat commands feed
spdlog::info("RelayThermostat::start room : {} subscribe to mqtt", _room);
ASYNC_CHECK_MSG(subscribeToAllCommands(), "subscribe to command stream failed");
_log.info("Start: subscribe to mqtt");
ASYNC_CHECK_LOG(subscribeToAllCommands(), "subscribe to command stream failed"); // todo pass logger??
// detaching listening thread
spdlog::info("RelayThermostat::start room : {} listen", _room);
ASYNC_CHECK_MSG(_thermo.start(), "Start thermometer service failed");
_log.info("Start: listen");
ASYNC_CHECK_LOG(_thermo.start(), "Start thermometer service failed");
// pętla sterowania
boost::asio::co_spawn(_io, controlLoop(), boost::asio::detached);
@ -684,8 +710,8 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable_expected< void > subscribe(std::string_view topic,
std::function< awaitable_expected< void >(const AsyncMqttClient::CallbackData &, AsyncMqttClient::ResponseData & resp) > cb) {
spdlog::trace("RelayThermostat room {} subscribing to {}", _room, topic);
ASYNC_CHECK_MSG(_mqtt.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
_log.trace("Subscribing to {}", topic);
ASYNC_CHECK_LOG(_mqtt.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
co_return _void{};
}
@ -696,6 +722,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
ASYNC_CHECK(subscribeCommand< commands::TickTimeChange >());
ASYNC_CHECK(subscribeCommand< commands::SlopeWindowChange >());
ASYNC_CHECK(subscribeCommand< commands::SlopeTemperatureDiffChange >());
ASYNC_CHECK(subscribeCommand< commands::StatusRequest >());
co_return _void{};
}
@ -719,6 +746,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
/// TODO command can return a true/false status for ok/nok case
auto status = ASYNC_TRY(handle_command(cmd));
if(resp) {
_log.trace("command {} request has a response topic, write response", Command::topic_suffix);
(*resp) = boost::json::object{{"status", status ? "ok" : "nok"}, {"details", "heater updated"}};
}
@ -735,24 +763,24 @@ struct RelayThermostat::Impl : private boost::noncopyable {
awaitable< void > update_config(std::string_view key, const T & value) noexcept {
auto _result = co_await _settings.async_save("hysteresis", _hysteresis);
if(not _result) {
spdlog::warn("Failed to update configuration paremeter {} for {}/{}", key, _room, _zone);
_log.warn("Failed to update configuration paremeter {}", key);
}
co_return;
}
// przeciążone handlery dla poszczególnych komend:
awaitable_expected< bool > handle_command(const commands::TemperatureSetpointChange & cmd) {
spdlog::info("Heater target temperature update {} for {}/{}", _targetTemperature, _room, _zone);
_log.info("Heater target temperature update {}", _targetTemperature);
_targetTemperature = cmd.setpoint_c;
co_await update_config("target_temperature", _targetTemperature);
co_return true;
}
awaitable_expected< bool > handle_command(const commands::StateChange & cmd) {
spdlog::info("Heater state update {} for {}/{}", static_cast< int >(cmd.state), _room, _zone);
_log.info("Heater state update {}", static_cast< int >(cmd.state));
// W stanie ERROR nie wolno włączyć przekaźnika (Enabled)
if(_state == ThermostatState::Error && cmd.state == ThermostatState::Enabled) {
spdlog::warn("Ignoring attempt to enable thermostat in ERROR state for {}/{}", _room, _zone);
_log.warn("Ignoring attempt to enable thermostat in ERROR state");
co_return false;
}
@ -762,7 +790,7 @@ struct RelayThermostat::Impl : private boost::noncopyable {
}
awaitable_expected< bool > handle_command(const commands::HisteresisChange & cmd) {
spdlog::info("Heater histeresis update {} for {}/{}", cmd.histeresis, _room, _zone);
_log.info("Heater histeresis update {}", cmd.histeresis);
_hysteresis = cmd.histeresis;
/// TODO check if histeresis has ok value
co_await update_config("hysteresis", _hysteresis);
@ -770,25 +798,66 @@ struct RelayThermostat::Impl : private boost::noncopyable {
}
awaitable_expected< bool > handle_command(const commands::TickTimeChange & cmd) {
spdlog::info("Heater tick time update {}ns for {}/{}", cmd.tickTime.count(), _room, _zone);
_log.info("Heater tick time update {}ns", cmd.tickTime.count());
_tickTime = cmd.tickTime;
co_await update_config("tick_time_s", std::chrono::duration_cast< std::chrono::seconds >(_tickTime).count());
co_return true;
}
awaitable_expected< bool > handle_command(const commands::SlopeWindowChange & cmd) {
spdlog::info("Heater slope window update {}ns for {}/{}", cmd.window.count(), _room, _zone);
_log.info("Heater slope window update {}ns", cmd.window.count());
_slopeWindow = cmd.window;
co_await update_config("slope_window_s", std::chrono::duration_cast< std::chrono::seconds >(_slopeWindow).count());
co_return true;
}
awaitable_expected< bool > handle_command(const commands::SlopeTemperatureDiffChange & cmd) {
spdlog::info("Heater slope temperature update {}C for {}/{}", cmd.dT_c, _room, _zone);
_log.info("Heater slope temperature update {}C", cmd.dT_c);
_slopeDT_c = cmd.dT_c;
co_await update_config("slope_delta_t", _slopeDT_c);
co_return true;
}
awaitable_expected< bool > handle_command(const commands::StatusRequest & cmd) {
_log.info("Heater got status report request");
co_return ASYNC_TRY(publish_status());
}
awaitable_expected< bool > publish_status() {
_log.trace("Publish status");
using namespace std::chrono;
memory_resource::MonotonicStack_1k_Resource mr;
json::pmr_memory_resource_adapter adapter_req{&mr};
boost::json::storage_ptr sp{&adapter_req};
boost::json::object obj{sp};
// timestamp — aktualny stan obiektu
obj["timestamp"] = date::to_iso_timestamp(std::chrono::system_clock::now(), &mr);
// proste pola
obj["room"] = _room;
obj["zone"] = _zone;
obj["state"] = static_cast< int >(_state);
obj["target_temperature"] = _targetTemperature;
auto trend = _thermo.temperatureTrend(_slopeWindow, _slopeDT_c);
if(trend) {
obj["current_trend"] = Trend_to_string(*trend);
}
obj["measurements_size"] = _thermo.size();
obj["hysteresis"] = _hysteresis;
obj["slope_dt_c"] = _slopeDT_c;
// durations → sekundy + "_s"
using seconds_d = duration< double >;
obj["tick_time_s"] = duration_cast< seconds_d >(_tickTime).count();
obj["slope_window_s"] = duration_cast< seconds_d >(_slopeWindow).count();
obj["sensor_timeout_s"] = duration_cast< seconds_d >(_sensorTimeout).count();
auto topic = topic::heating::state(mr, _room, _zone);
ASYNC_CHECK(_mqtt.publish(topic, obj));
co_return true;
}
};
RelayThermostat::RelayThermostat(executor & io,

View File

@ -46,13 +46,4 @@ class RelayThermostat : public TemperatureController {
void stop() noexcept override;
};
// class MultiZoneRelayThermostat : public TemperatureController {
// struct Impl;
// std::unique_ptr< Impl > _impl;
// public:
// std::vector< std::shared_ptr< RelayThermostat > > _zones;
// MultiZoneRelayThermostat(executor & executor, AsyncMqttClient & mqtt, std::span< std::shared_ptr< RelayThermostat > > zones);
// };
} // namespace ranczo

View File

@ -15,33 +15,33 @@
namespace ranczo {
awaitable_expected< void > MqttThermometer::on_update(async_cb handler) noexcept {
spdlog::trace("MqttThermometer::on_update procedure start");
_log.trace("on_update procedure start");
handler_ = std::move(handler);
const auto topic =
cfg_.type == "floor" ? topic::temperature::floor(cfg_.room, cfg_.zone) : topic::temperature::air(cfg_.room, cfg_.zone);
spdlog::trace("MqttThermometer::on_update procedure subscribe on topic: {}", topic);
auto token = ASYNC_TRY(mqtt_.subscribe(topic, [this](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData &resp ) -> awaitable_expected< void > {
_log.trace("on_update procedure subscribe on topic: {}", topic);
_subscribtionToken = ASYNC_TRY(mqtt_.subscribe(topic, [this](const AsyncMqttClient::CallbackData & data, AsyncMqttClient::ResponseData &resp ) -> awaitable_expected< void > {
// Ensure controller state is touched on our strand
co_await boost::asio::dispatch(strand_, boost::asio::use_awaitable);
// Parse numeric value (root number or object[key])
if(auto v = to_thermometer_data(data.request); v.has_value()) {
// current_ = v->value;
spdlog::trace("MqttThermometer::on_update handler RUN");
_log.trace("on_update handler RUN");
if(handler_)
ASYNC_CHECK(handler_(*v));
spdlog::trace("MqttThermometer::on_update handler RUN OK");
_log.trace("on_update handler RUN OK");
} else {
spdlog::warn("MqttThermometer::on_update Thermometer data {} is invalid", boost::json::serialize(data.request));
_log.warn("on_update Thermometer data {} is invalid", boost::json::serialize(data.request));
}
co_return _void{};
}));
spdlog::trace("MqttThermometer::on_update subscribtrion OK");
_log.trace("on_update subscribtrion OK");
co_return _void{};
}
awaitable_expected< void > MqttThermometer::listen() noexcept {
spdlog::trace("MqttThermometer::listen");
_log.trace("listen");
// Not owning the underlying client loop: nothing to do here.
// Keep an idle await so co_spawn'd task lives until cancel() (optional design).
boost::asio::steady_timer idle{strand_};
@ -49,7 +49,7 @@ awaitable_expected< void > MqttThermometer::listen() noexcept {
idle.expires_after(std::chrono::hours(24));
auto [ec] = co_await idle.async_wait(boost::asio::as_tuple(boost::asio::use_awaitable));
if(ec == boost::asio::error::operation_aborted) {
spdlog::info("Temperature readding on {} aborted, exiting", cfg_.room);
_log.info("Temperature readding aborted, exiting");
co_return _void{};
}
}

View File

@ -2,6 +2,9 @@
#include <boost/asio/strand.hpp>
#include <config.hpp>
#include <ranczo-io/utils/logger.hpp>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <spdlog/spdlog.h>
namespace boost::json {
class value;
@ -68,8 +71,10 @@ class MqttThermometer : public Thermometer {
boost::asio::strand< boost::asio::any_io_executor > strand_;
AsyncMqttClient & mqtt_;
Settings cfg_{};
ModuleLogger _log{spdlog::default_logger(), "MqttThermo " + cfg_.room};
async_cb handler_{};
const AsyncMqttClient::SubscribtionToken * _subscribtionToken;
};
} // namespace ranczo

View File

@ -1,6 +1,12 @@
add_executable(ranczo-io_temperature
main.cpp
ds18b20_sensor.cpp ds18b20_sensor.hpp
ds18b20_sensor.cpp ds18b20_sensor.hpp
measurement_publisher.cpp measurement_publisher.hpp
ranczo-io_temperature.service.in
postinst
prerm
postrm
)
target_link_libraries(ranczo-io_temperature
PUBLIC
@ -17,4 +23,41 @@ install(
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT temperature
)
# Opis komponentu temperature
set(CPACK_COMPONENT_TEMPERATURE_DISPLAY_NAME "Ranczo-IO Temperature Service")
set(CPACK_COMPONENT_TEMPERATURE_DESCRIPTION "Serwis odczytujący temperaturę z czujników")
set(CPACK_COMPONENT_TEMPERATURE_REQUIRED ON)
# Nazwa samego pakietu DEB dla komponentu
set(CPACK_DEBIAN_TEMPERATURE_PACKAGE_NAME "ranczo-io-temperature" CACHE INTERNAL "package name")
set(CPACK_DEBIAN_TEMPERATURE_PACKAGE_SECTION "utils" CACHE INTERNAL "package section")
set(CPACK_DEBIAN_TEMPERATURE_PACKAGE_MAINTAINER "b.wieczorek@dx.net.pl" CACHE INTERNAL "package maintainer")
set(CPACK_DEBIAN_TEMPERATURE_PACKAGE_CONTROL_EXTRA
"${CMAKE_CURRENT_SOURCE_DIR}/postinst"
"${CMAKE_CURRENT_SOURCE_DIR}/prerm"
"${CMAKE_CURRENT_SOURCE_DIR}/postrm"
CACHE INTERNAL "package extra"
)
set(CPACK_DEBIAN_TEMPERATURE_PACKAGE_DEPENDS "libsqlite3-0, liburing2, systemd (>= 245)" CACHE INTERNAL "package depends")
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io_temperature.service.in
${CMAKE_CURRENT_BINARY_DIR}/ranczo-io_temperature.service
@ONLY
)
# Instalacja unita systemd
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/ranczo-io_temperature.service
DESTINATION ${SYSTEMD_UNIT_DIR}
COMPONENT temperature
)
# tworzenie katalogów
install(DIRECTORY
DESTINATION var/lib/ranczo-io/temperature
COMPONENT temperature
)

View File

@ -2,13 +2,22 @@
#include "spdlog/spdlog.h"
#include <memory_resource>
#include <ranczo-io/utils/memory_resource.hpp>
#include <algorithm>
#include <boost/asio/buffer.hpp>
#include <boost/asio/completion_condition.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <exception>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <vector>
#define BOOST_ASIO_HAS_IO_URING
#define BOOST_ASIO_HAS_FILE
@ -23,22 +32,18 @@ struct DS18B20ParseResult {
float temperature_c = 0.0f; // ważne tylko jeśli has_temperature == true
};
inline DS18B20ParseResult parse_w1_slave(const std::string & content) {
inline DS18B20ParseResult parse_w1_slave(std::pmr::memory_resource &mr, std::string_view content) {
DS18B20ParseResult result{};
// --- 1. Podziel na dwie linie ---
std::string line1;
std::string line2;
std::pmr::string line1{&mr};
std::pmr::string line2{&mr};
// znajdź pierwsze \n
auto pos_newline = content.find('\n');
if(pos_newline == std::string::npos) {
// tylko jedna linia albo w ogóle
line1 = content;
} else {
line1 = content.substr(0, pos_newline);
// reszta po pierwszym \n
auto pos_newline2 = content.find('\n', pos_newline + 1);
if(pos_newline2 == std::string::npos) {
line2 = content.substr(pos_newline + 1);
@ -47,8 +52,7 @@ inline DS18B20ParseResult parse_w1_slave(const std::string & content) {
}
}
// proste trimowanie końcowych spacji/CR
auto rtrim = [](std::string & s) {
auto rtrim = [](std::pmr::string & s) {
while(!s.empty() && (s.back() == '\r' || s.back() == ' ' || s.back() == '\t')) {
s.pop_back();
}
@ -56,104 +60,150 @@ inline DS18B20ParseResult parse_w1_slave(const std::string & content) {
rtrim(line1);
rtrim(line2);
// --- 2. CRC: sprawdź "YES" w pierwszej linii ---
if(line1.find("YES") != std::string::npos) {
result.crc_ok = true;
} else {
result.crc_ok = false;
// nadal możemy próbować parsować temperaturę, jeśli chcesz to kwestia polityki
}
// --- 3. Temperatura: szukamy "t=" w drugiej linii ---
auto t_pos = line2.find("t=");
if(t_pos == std::string::npos) {
// brak temperatury
result.has_temperature = false;
return result;
}
std::string temp_str = line2.substr(t_pos + 2); // po "t=" do końca linii
std::pmr::string temp_str = line2.substr(t_pos + 2); // po "t=" do końca linii
// przytnij białe znaki na końcu
rtrim(temp_str);
// może być puste, albo jakieś śmieci
if(temp_str.empty()) {
result.has_temperature = false;
return result;
}
auto stoi = [](const std::pmr::string & __str, size_t * __idx = 0, int __base = 10) {
return __gnu_cxx::__stoa< long, int >(&std::strtol, "stoi", __str.c_str(), __idx, __base);
};
try {
// DS18B20 podaje t w tysięcznych stopnia
int temp_milli = std::stoi(temp_str);
int temp_milli = stoi(temp_str);
result.temperature_c = static_cast< float >(temp_milli) / 1000.0f;
result.has_temperature = true;
} catch(const std::exception &) {
// np. stoi rzuciło wyjątek traktujemy jako brak poprawnej temperatury
result.has_temperature = false;
}
return result;
}
awaitable_expected< float > DS18B20Sensor::read_temperature() const {
using boost::asio::stream_file;
auto fullpath = device_id_ / std::filesystem::path{"w1_slave"};
stream_file file(executor_, fullpath, boost::asio::stream_file::flags::read_only);
awaitable_expected< float > DS18B20Sensor::read_temperature() const noexcept{
try {
memory_resource::MonotonicStack_512_Resource mr;
// Create a buffer to hold the file data
constexpr size_t bufferSize = 256;
std::array< char, bufferSize > buffer{0};
spdlog::debug("start read {}", device_id_.string());
// Read the file asynchronously
boost::system::error_code ec;
auto n = co_await async_read(file,
boost::asio::buffer(buffer.data(), buffer.size()),
boost::asio::transfer_all(),
boost::asio::redirect_error(boost::asio::use_awaitable, ec));
using boost::asio::stream_file;
auto fullpath = device_id_ / std::filesystem::path{"w1_slave"};
stream_file file(executor_, fullpath, boost::asio::stream_file::flags::read_only);
if(ec && ec != boost::asio::error::eof) {
spdlog::error("read_temperature: {} -> {}", fullpath.string(), ec.message());
co_return unexpected(ec);
}
// Create a buffer to hold the file data
std::pmr::vector<char> buffer{128, &mr};
// budujemy string z realnie przeczytanych bajtów
std::string content(buffer.data(), n);
spdlog::debug("w1_slave content for {}:\n{}", device_id_.string(), content);
spdlog::debug("start read {}", device_id_.string());
// Read the file asynchronously
boost::system::error_code ec;
auto n = co_await async_read(file,
boost::asio::buffer(buffer),
boost::asio::transfer_all(),
boost::asio::redirect_error(boost::asio::use_awaitable, ec));
auto parsed = parse_w1_slave(content);
if(ec && ec != boost::asio::error::eof) {
spdlog::error("read_temperature: {} -> {}", fullpath.string(), ec.message());
co_return unexpected(ec);
}
if(!parsed.crc_ok) {
spdlog::warn("CRC not OK for sensor {}", device_id_.string());
// tu możesz zwrócić błąd logiczny zamiast system_error
std::pmr::string content(buffer.data(), buffer.size(), &mr);
spdlog::debug("w1_slave content for {}:\n{}", device_id_.string(), content);
auto parsed = parse_w1_slave(mr, content);
if(!parsed.crc_ok) {
spdlog::warn("CRC not OK for sensor {}", device_id_.string());
co_return unexpected(make_error_code(boost::system::errc::io_error));
}
if(!parsed.has_temperature) {
spdlog::warn("No temperature parsed for sensor {}", device_id_.string());
co_return unexpected(make_error_code(boost::system::errc::io_error));
}
co_return parsed.temperature_c;
} catch(const std::exception & e) {
spdlog::warn("Cought std::exception {}", e.what());
co_return unexpected(make_error_code(boost::system::errc::io_error));
}
}
if(!parsed.has_temperature) {
spdlog::warn("No temperature parsed for sensor {}", device_id_.string());
co_return unexpected(make_error_code(boost::system::errc::io_error));
std::pmr::vector< std::pmr::string > OneWireBus::scanBus(std::pmr::memory_resource &mr, const std::filesystem::path & bus_path) {
namespace fs = std::filesystem;
std::pmr::vector< std::pmr::string > result{&mr};
fs::path slaves_file = bus_path / "w1_master_slaves";
if(!fs::exists(slaves_file))
return result;
std::ifstream f(slaves_file);
std::pmr::string id{&mr};
while(f >> id) {
if(id.rfind("28-", 0) == 0) { // DS18B20 prefix
result.push_back(id);
}
}
co_return parsed.temperature_c;
std::sort(begin(result), end(result));
return result;
}
boost::asio::awaitable< void > OneWireBus::run() {
memory_resource::StackPool_8k_Resource mr;
auto sensor_ids = scanBus(mr, bus_path_);
for(auto & id : sensor_ids) {
spdlog::info("found sensor {} on bus {}", id, bus_path_);
this->add_sensor(id);
}
for(;;) {
// 1. Czekamy określony interwał dla całej magistrali
// we wait for a while
timer_.expires_after(interval_);
co_await timer_.async_wait(boost::asio::use_awaitable);
// 2. Dla każdego sensora wykonujemy odczyt
// scan our bus, the elements here can change
memory_resource::MonotonicStack_1k_Resource smr;
auto current_sensor_ids = scanBus(smr, bus_path_);
if(current_sensor_ids != sensor_ids) {
spdlog::info("Number of sensors changed on bus {}", bus_path_);
clearSensors();
sensor_ids = current_sensor_ids;
for(auto & id : sensor_ids) {
spdlog::info("found sensor {} on bus {}", id, bus_path_);
this->add_sensor(id);
}
}
// read out all the sensors
for(const auto & sensor : sensors_) {
auto temp = co_await sensor.read_temperature();
if(!temp) {
spdlog::warn("Read error from sensor {}", sensor.device_id());
} else {
spdlog::info("Read from sensor {} : {}°C", sensor.device_id(), *temp);
co_await _pub.get().publish_measurement("DS18B20", sensor.device_id(), *temp);
}
}
}
}
void OneWireBus::stop() {}
} // namespace ranczo

View File

@ -3,17 +3,23 @@
#include <boost/asio/steady_timer.hpp>
#include <config.hpp>
#include <filesystem>
#include <functional>
#include <memory_resource>
#include <string>
#include <string_view>
#include <ranczo-io/utils/memory_resource.hpp>
#include "measurement_publisher.hpp"
namespace ranczo {
class DS18B20Sensor {
public:
DS18B20Sensor(boost::asio::any_io_executor exec, std::filesystem::path device_id) : executor_(exec), device_id_{std::move(device_id)} {}
awaitable_expected< float > read_temperature() const;
awaitable_expected< float > read_temperature() const noexcept;
std::string device_id() const {
return device_id_.string();
return device_id_.filename().string();
}
private:
@ -23,24 +29,38 @@ class DS18B20Sensor {
class OneWireBus {
public:
OneWireBus(boost::asio::any_io_executor exec, std::string bus_path, std::chrono::seconds interval = std::chrono::seconds(2))
: executor_(exec), timer_(exec), bus_path_(std::move(bus_path)), interval_(interval) {}
OneWireBus(boost::asio::any_io_executor exec, MqttMeasurementPublisher &pub, std::string_view bus_path, std::chrono::seconds interval = std::chrono::seconds(2))
: _mr{memory_resource::default_resource()}, _pub{pub}, executor_(exec), timer_(exec), bus_path_(std::move(bus_path), _mr), interval_(interval) {}
void add_sensor(std::string id) {
std::filesystem::path path;
OneWireBus(const OneWireBus &) = delete;
OneWireBus(OneWireBus && rhs) noexcept = default;
OneWireBus & operator=(const OneWireBus &) = delete;
OneWireBus & operator=(OneWireBus && rhs) =default;
boost::asio::awaitable< void > run();
void stop();
private:
void add_sensor(const std::pmr::string & id) {
std::filesystem::path path{};
path += bus_path_ + "/" + id;
sensors_.emplace_back(executor_, path);
}
boost::asio::awaitable< void > run();
void clearSensors() {
sensors_.clear();
}
private:
std::pmr::memory_resource* _mr{};
std::pmr::vector< std::pmr::string > scanBus(std::pmr::memory_resource & mr, const std::filesystem::path & bus_path);
boost::asio::any_io_executor executor_;
std::reference_wrapper<MqttMeasurementPublisher> _pub;
boost::asio::steady_timer timer_;
std::string bus_path_;
std::pmr::string bus_path_;
std::chrono::seconds interval_;
std::vector< DS18B20Sensor > sensors_;
std::pmr::vector< DS18B20Sensor > sensors_{_mr};
};
} // namespace ranczo

View File

@ -1,5 +1,3 @@
#include <ranczo-io/utils/mqtt_client.hpp>
#include <boost/asio.hpp>
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/awaitable.hpp>
@ -10,60 +8,29 @@
#include <boost/asio/this_coro.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory_resource>
#include <span>
#include <string>
#include <tuple>
#include <vector>
#include <spdlog/spdlog.h>
#include <ranczo-io/utils/config.hpp>
#include <ranczo-io/utils/modbus.hpp>
#include <ranczo-io/utils/mqtt_client.hpp>
#include "measurement_publisher.hpp"
#include "ds18b20_sensor.hpp"
namespace ranczo {
/// TODO
/// * Zapis ustawień
/// * Nasłuchiwanie na MQTT
/// * Poprawa ścieżka do deviców DS18
} // namespace ranczo
// Modified completion token that will prevent co_await from throwing exceptions.
#include "ranczo-io/utils/memory_resource.hpp"
#include "spdlog/common.h"
#include "spdlog/spdlog.h"
using namespace std::chrono_literals;
using namespace std::string_view_literals;
namespace fs = std::filesystem;
// ----------------------
// Funkcja inicjalizująca
// ----------------------
std::vector< std::string > scan_sensors_for_bus(const fs::path & bus_path) {
std::vector< std::string > result;
fs::path slaves_file = bus_path / "w1_master_slaves";
if(!fs::exists(slaves_file))
return result;
std::ifstream f(slaves_file);
std::string id;
while(f >> id) {
if(id.rfind("28-", 0) == 0) { // DS18B20 prefix
result.push_back(id);
}
}
return result;
}
enum class Type { floor, air };
std::vector< std::tuple< std::string, std::string, Type > > mapping = {
std::vector< std::tuple< std::string_view, std::string_view, Type > > mapping = {
{"28-0119513810ff", "maciek_room", Type::floor},
{"28-0119517084ff", "office", Type::floor},
{"28-0119514282ff", "aska_room", Type::floor},
@ -80,52 +47,91 @@ std::vector< std::tuple< std::string, std::string, Type > > mapping = {
};
int main() {
boost::asio::io_context io;
using namespace ranczo;
boost::asio::io_context io_context;
memory_resource::HeapPoolResource _pool{1024*1024*1024}; // 1M
std::pmr::set_default_resource(&_pool);
// Register signal handler
spdlog::set_level(spdlog::level::trace);
spdlog::debug("Debug Registering signal handlers");
spdlog::info("Registering signal handlers");
// promisa do „zatrzymaj program”
std::promise< void > stop_promise;
auto stop_future = stop_promise.get_future();
boost::asio::executor_work_guard< boost::asio::io_context::executor_type > work_guard = boost::asio::make_work_guard(io_context);
boost::asio::any_io_executor io_executor = io_context.get_executor();
SettingsStore store{"temperature_svc.db", io_executor, 1};
ComponentSettingsStore mqttconfig{store, "mqtt"};
ComponentSettingsStore mappingConfig{store, "mapping"};
if(not mqttconfig.contains("host"))
mqttconfig.save("host", "192.168.10.10");
if(not mqttconfig.contains("port"))
mqttconfig.save("port", 2502);
std::jthread io_thread([&] {
spdlog::info("io_context thread started");
io_context.run();
spdlog::info("io_context thread finished");
});
// create a modbus client to use with all internal objects
AsyncMqttClient mqttClient{io_executor};
MqttMeasurementPublisher pub{mqttClient, std::span{ mapping}, &_pool};
const fs::path w1_root = "/home/bartoszek/mnt/w1remote/devices";
std::pmr::vector< fs::path > bus_paths{&_pool};
// -----------------------------
// 1. ZNAJDŹ magistrale w1_bus_masterX
// -----------------------------
std::vector< fs::path > bus_paths;
spdlog::info("Iterating paths");
for(auto & entry : fs::directory_iterator(w1_root)) {
auto pos = entry.path().filename().string().rfind("w1_bus_master", 0);
if(pos != std::string::npos) {
bus_paths.push_back(entry.path());
}
}
spdlog::info("Iterating paths end");
if(bus_paths.empty()) {
std::cerr << "Nie znaleziono żadnych magistral 1-Wire\n";
spdlog::error("Nie znaleziono żadnych magistral 1-Wire");
return 1;
}
// -----------------------------
// 2. DLA KAŻDEJ MAGISTRALI twórz osobny STRAND
// -----------------------------
std::vector< ranczo::OneWireBus > buses;
std::pmr::vector< ranczo::OneWireBus > buses{&_pool};
buses.reserve(bus_paths.size());
for(auto & bus : bus_paths) {
auto strand = boost::asio::make_strand(io);
auto & bus_obj = buses.emplace_back(strand, bus.string(), std::chrono::seconds(5));
auto sensor_ids = scan_sensors_for_bus(bus);
for(auto & id : sensor_ids) {
spdlog::info("found sensor {} on bus {}", id, bus.string());
bus_obj.add_sensor(id);
}
spdlog::info("found search bus {} ", bus.string());
auto strand = boost::asio::make_strand(io_context);
auto & bus_obj = buses.emplace_back(strand, pub, bus.string(), std::chrono::seconds(5));
boost::asio::co_spawn(strand, bus_obj.run(), boost::asio::detached);
}
// -----------------------------
// 4. START IO CONTEXT
// -----------------------------
io.run();
boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait([&](const boost::system::error_code & ec, int signo) {
if(ec)
return; // canceled podczas shutdown
spdlog::warn("Signal {} received", signo);
// Zainicjuj graceful shutdown na wątku io_context (bezpiecznie):
boost::asio::post(io_context, [&] {
spdlog::info("Graceful shutdown start");
mqttClient.cancel();
work_guard.reset();
io_context.stop(); // ok na tym samym wątku
spdlog::info("Graceful shutdown posted");
});
stop_promise.set_value(); // pobudzi main
});
if(io_thread.joinable())
io_thread.join();
return 0;
}

View File

@ -0,0 +1,76 @@
#include "measurement_publisher.hpp"
#include <ranczo-io/utils/json_helpers.hpp>
#include <ranczo-io/utils/memory_resource.hpp>
#include <ranczo-io/utils/mqtt_client.hpp>
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
#include <spdlog/spdlog.h>
#include <boost/json.hpp>
#include <fmt/chrono.h>
#include <fmt/format.h>
#include <date/date.h>
#include <string>
// namespace fmt::pmr {
// using memory_buffer = ::fmt::basic_memory_buffer< char, inline_buffer_size, std::pmr::polymorphic_allocator<char>>;
// }
namespace ranczo {
MqttMeasurementPublisher::MqttMeasurementPublisher(AsyncMqttClient & client,
std::span< std::tuple< std::string_view, std::string_view, Type > > ds18b20mapping,
std::pmr::memory_resource * mr,
std::source_location sl)
: _client(client), _mr(4096, mr, sl) {
for(const auto & entry : ds18b20mapping) {
const auto & sensor_id_view = std::get< 0 >(entry); // id DS-a
const auto & room_name_view = std::get< 1 >(entry); // nazwa pomieszczenia
const auto & sensor_type_enum = std::get< 2 >(entry); // Type
std::pmr::string sensor_id{sensor_id_view, &_mr};
std::pmr::string room_name{room_name_view, &_mr};
_mapping.emplace(std::move(sensor_id), std::make_pair(std::move(room_name), sensor_type_enum));
}
}
awaitable_expected< void >
MqttMeasurementPublisher::publish_measurement(std::string_view sensor_type, std::string_view sensor_id, double measurement) noexcept {
using namespace memory_resource::literals;
memory_resource::MonotonicHeapResource mr{1_kB, &_mr};
if(not _mapping.contains(sensor_id)){
spdlog::warn("Nie znaleziono {}", sensor_id);
co_return _void{};
}
const auto &[room, type] = _mapping.find(sensor_id)->second;
auto topic = ranczo::topic::temperature::floor(mr, room, 1);
using namespace std::chrono;
const auto now = date::floor< std::chrono::milliseconds >(system_clock::now());
const auto timestamp_str = date::format("%FT%TZ", now); // std::string
boost::json::monotonic_resource json_mr;
json::pmr_memory_resource_adapter adapter_req(&mr);
auto sp = boost::json::storage_ptr{&json_mr};
boost::json::object obj(sp);
obj["value"] = measurement;
obj["timestamp"] = timestamp_str; // kopiowany do JSON
obj["unit"] = "°C";
obj["update"] = false; // na razie brak cache → zawsze false
boost::json::value payload{std::move(obj)};
// 4. Log (topic + JSON)
spdlog::debug("Publishing MQTT measurement topic='{}'", topic);
// 5. Wyślij po MQTT
co_return co_await _client.publish(topic, payload);
}
} // namespace ranczo

View File

@ -0,0 +1,38 @@
#pragma once
#include <config.hpp>
#include <functional>
#include <ranczo-io/utils/memory_resource.hpp>
#include <map>
#include <memory_resource>
#include <source_location>
#include <span>
#include <string>
#include <string_view>
#include <utility>
enum class Type { floor, air };
namespace ranczo {
class AsyncMqttClient;
class MqttMeasurementPublisher {
public:
using executor_type = boost::asio::any_io_executor;
MqttMeasurementPublisher(AsyncMqttClient & client,
std::span< std::tuple< std::string_view, std::string_view, Type > > ds18b20mapping,
std::pmr::memory_resource * mr = std::pmr::get_default_resource(),
std::source_location sl = std::source_location::current()
);
awaitable_expected< void > publish_measurement(std::string_view sensor_type, std::string_view sensor_id, double measurement) noexcept;
private:
AsyncMqttClient & _client;
memory_resource::HeapPoolResource _mr;
std::pmr::map< std::pmr::string, std::pair< std::pmr::string, Type >, std::less<> > _mapping{&_mr};
};
} // namespace ranczo

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Jeżeli tworzysz usera, możesz to zrobić tutaj (albo w osobnym pakiecie)
# adduser --system --group --no-create-home ranczoio || true
systemctl daemon-reload || true
systemctl enable --now ranczo-io-floorheating.service || true
;;
esac
exit 0

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
remove)
systemctl daemon-reload || true
;;
purge)
systemctl disable ranczo-io-floorheating.service || true
systemctl daemon-reload || true
;;
esac
exit 0

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
case "$1" in
remove|upgrade|deconfigure)
systemctl stop ranczo-io-floorheating.service || true
;;
esac
exit 0

View File

@ -0,0 +1,20 @@
[Unit]
Description=Ranczo-IO Temperature read service
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=@RANCZO_USER@
Group=@RANCZO_GROUP@
ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/ranczo-io_temperature
WorkingDirectory=/var/lib/ranczo-io/temperature
Restart=on-failure
RestartSec=5
# /run/ranczo-io-temperature będzie robione automatycznie
RuntimeDirectory=ranczo-io-temperature
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target