Compare commits
2 Commits
b01271fa3b
...
f828f2666b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f828f2666b | ||
|
|
7d2a5a1072 |
@ -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)
|
||||
|
||||
10
config.hpp
10
config.hpp
@ -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); \
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {};
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
128
libs/ranczo-io/utils/logger.hpp
Normal file
128
libs/ranczo-io/utils/logger.hpp
Normal 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
|
||||
305
libs/ranczo-io/utils/memory_resource.hpp
Normal file
305
libs/ranczo-io/utils/memory_resource.hpp
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
14
services/floorheat_svc/postinst
Normal file
14
services/floorheat_svc/postinst
Normal 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
|
||||
14
services/floorheat_svc/postrm
Normal file
14
services/floorheat_svc/postrm
Normal 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
|
||||
10
services/floorheat_svc/prerm
Normal file
10
services/floorheat_svc/prerm
Normal 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
|
||||
20
services/floorheat_svc/ranczo-io_floorheating.service.in
Normal file
20
services/floorheat_svc/ranczo-io_floorheating.service.in
Normal 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
|
||||
@ -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{};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
76
services/temperature_svc/measurement_publisher.cpp
Normal file
76
services/temperature_svc/measurement_publisher.cpp
Normal 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
|
||||
38
services/temperature_svc/measurement_publisher.hpp
Normal file
38
services/temperature_svc/measurement_publisher.hpp
Normal 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
|
||||
14
services/temperature_svc/postinst
Normal file
14
services/temperature_svc/postinst
Normal 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
|
||||
14
services/temperature_svc/postrm
Normal file
14
services/temperature_svc/postrm
Normal 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
|
||||
10
services/temperature_svc/prerm
Normal file
10
services/temperature_svc/prerm
Normal 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
|
||||
20
services/temperature_svc/ranczo-io_temperature.service.in
Normal file
20
services/temperature_svc/ranczo-io_temperature.service.in
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user