add thermometer class
This commit is contained in:
parent
512f72a371
commit
7354016a96
@ -4,6 +4,9 @@ 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)
|
||||
|
||||
include(CheckIPOSupported)
|
||||
check_ipo_supported(RESULT supported OUTPUT error)
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
add_library(ranczo-io_utils
|
||||
mqtt_client.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/mqtt_client.hpp
|
||||
timer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ranczo-io/utils/timer.hpp
|
||||
)
|
||||
|
||||
add_library(ranczo-io::utils ALIAS ranczo-io_utils)
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
#include <optional>
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
|
||||
#include <config.hpp>
|
||||
|
||||
#include <boost/random/uniform_smallint.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/memory_resource.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/mqtt5.hpp>
|
||||
#include <boost/random/uniform_smallint.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
#include <boost/system/result.hpp>
|
||||
#include <boost/mqtt5.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/json/memory_resource.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
// Adapter to use std::pmr::memory_resource with boost::json
|
||||
class pmr_memory_resource_adapter : public boost::json::memory_resource {
|
||||
@ -50,46 +51,47 @@ namespace ranczo {
|
||||
class Topic {
|
||||
public:
|
||||
std::string pattern;
|
||||
|
||||
|
||||
Topic() = default;
|
||||
Topic(std::string_view p) : pattern(p) {}
|
||||
|
||||
|
||||
bool operator==(const std::string_view other) const {
|
||||
return match(other);
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
static std::vector<std::string_view> split(std::string_view str) {
|
||||
std::vector<std::string_view> tokens;
|
||||
static std::vector< std::string_view > split(std::string_view str) {
|
||||
std::vector< std::string_view > tokens;
|
||||
size_t start = 0;
|
||||
while (start < str.size()) {
|
||||
while(start < str.size()) {
|
||||
size_t end = str.find('/', start);
|
||||
if (end == std::string_view::npos) end = str.size();
|
||||
if(end == std::string_view::npos)
|
||||
end = str.size();
|
||||
tokens.emplace_back(str.substr(start, end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
||||
bool match(std::string_view topic) const {
|
||||
auto pattern_parts = split(pattern);
|
||||
auto topic_parts = split(topic);
|
||||
|
||||
auto topic_parts = split(topic);
|
||||
|
||||
size_t i = 0;
|
||||
for (; i < pattern_parts.size(); ++i) {
|
||||
if (pattern_parts[i] == "#") {
|
||||
for(; i < pattern_parts.size(); ++i) {
|
||||
if(pattern_parts[i] == "#") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (i >= topic_parts.size()) {
|
||||
|
||||
if(i >= topic_parts.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pattern_parts[i] != "+" && pattern_parts[i] != topic_parts[i]) {
|
||||
|
||||
if(pattern_parts[i] != "+" && pattern_parts[i] != topic_parts[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return i == topic_parts.size(); // dokładnie tyle samo segmentów
|
||||
}
|
||||
};
|
||||
@ -99,10 +101,15 @@ using client_type = boost::mqtt5::mqtt_client< boost::asio::i
|
||||
|
||||
using mqtt_expected_void = expected< void, boost::system::error_code >;
|
||||
|
||||
///TODO this must old a reference count for all the possible callbacks
|
||||
struct AsyncMqttClient::SubscribtionToken {
|
||||
boost::uuids::uuid uuid;
|
||||
};
|
||||
|
||||
struct AsyncMqttClient::AsyncMqttClientImpl {
|
||||
const boost::asio::any_io_executor & _executor;
|
||||
client_type _mqtt_client;
|
||||
std::vector< std::tuple<Topic, callback_t, boost::uuids::uuid> > _callbacks;
|
||||
std::vector< std::tuple< Topic, callback_t, AsyncMqttClient::SubscribtionToken > > _callbacks;
|
||||
|
||||
AsyncMqttClientImpl(const boost::asio::any_io_executor & executor) : _executor{executor}, _mqtt_client{_executor} {
|
||||
spdlog::trace("Creating mqtt client");
|
||||
@ -148,11 +155,11 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
|
||||
spdlog::error("MQTT subscribe result contains no subcodes");
|
||||
co_return std::unexpected{make_error_code(boost::system::errc::protocol_error)};
|
||||
}
|
||||
|
||||
|
||||
spdlog::info("MQTT subscribed to {}", topic);
|
||||
for(int i{}; i < sub_codes.size(); i++)
|
||||
spdlog::info("MQTT subscribe accepted: {}", sub_codes[i].message());
|
||||
|
||||
|
||||
co_return mqtt_expected_void{}; // Success
|
||||
}
|
||||
|
||||
@ -177,17 +184,18 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
|
||||
pmr_memory_resource_adapter boost_adapter(&mr); // Create a boost adapter
|
||||
boost::json::storage_ptr sp(&boost_adapter); // Construct a non-owning pointer to the resource
|
||||
|
||||
auto value = boost::json::parse(payload, sp); // throws on parse error
|
||||
|
||||
bool run = false;
|
||||
for(const auto & [registeredTopic, cb, uuid] : _callbacks) {
|
||||
if(registeredTopic == topic) {
|
||||
run = true;
|
||||
run = true;
|
||||
auto value = boost::json::parse(payload, sp); // throws on error
|
||||
CallbackData cbdata{.topic = topic, .responseTopic = std::nullopt, .payload = value};
|
||||
|
||||
co_spawn(
|
||||
_executor,
|
||||
[handler = cb, value]() -> boost::asio::awaitable< void > {
|
||||
[handler = cb, cbdata]() -> boost::asio::awaitable< void > {
|
||||
try {
|
||||
if(auto result = co_await handler(value); not result) {
|
||||
if(auto result = co_await handler(cbdata); not result) {
|
||||
spdlog::warn("MQTT callback error: {}", result.error().message());
|
||||
}
|
||||
} catch(const std::exception & e) {
|
||||
@ -221,8 +229,8 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
|
||||
spdlog::trace("co_spawn mqtt client");
|
||||
boost::asio::co_spawn(_mqtt_client.get_executor(), listen(), boost::asio::detached);
|
||||
}
|
||||
|
||||
void cancel(){
|
||||
|
||||
void cancel() {
|
||||
_mqtt_client.cancel();
|
||||
}
|
||||
};
|
||||
@ -230,34 +238,35 @@ struct AsyncMqttClient::AsyncMqttClientImpl {
|
||||
AsyncMqttClient::AsyncMqttClient(const boost::asio::any_io_executor & executor)
|
||||
: _impl{std::make_unique< AsyncMqttClient::AsyncMqttClientImpl >(executor)} {}
|
||||
|
||||
awaitable_expected<void> AsyncMqttClient::subscribe(std::string_view topic, callback_t&& cb) noexcept {
|
||||
awaitable_expected<const AsyncMqttClient::SubscribtionToken * > AsyncMqttClient::subscribe(std::string_view topic, callback_t cb) noexcept {
|
||||
BOOST_ASSERT(_impl);
|
||||
BOOST_ASSERT(not topic.empty());
|
||||
BOOST_ASSERT(cb);
|
||||
|
||||
|
||||
spdlog::trace("MQTT subscribtion to {} started", topic);
|
||||
ASYNC_CHECK_MSG(_impl->subscribe(topic), "MQTT subscribtion to {} failed", topic);
|
||||
|
||||
spdlog::trace("MQTT subscribtion to {} ok, registering callback", topic);
|
||||
_impl->_callbacks.emplace_back(Topic{topic}, cb, boost::uuids::random_generator()());
|
||||
const auto & [_1, _2, token] =
|
||||
_impl->_callbacks.emplace_back(Topic{topic}, cb, AsyncMqttClient::SubscribtionToken{boost::uuids::random_generator()()});
|
||||
|
||||
co_return mqtt_expected_void{};
|
||||
/// TODO return token
|
||||
co_return &token;
|
||||
}
|
||||
|
||||
awaitable_expected<void> AsyncMqttClient::listen() const noexcept{
|
||||
awaitable_expected< void > AsyncMqttClient::listen() const noexcept {
|
||||
BOOST_ASSERT(_impl);
|
||||
|
||||
|
||||
spdlog::info("MQTT client listen");
|
||||
ASYNC_CHECK(_impl->listen());
|
||||
|
||||
|
||||
co_return mqtt_expected_void{};
|
||||
}
|
||||
|
||||
void AsyncMqttClient::cancel()
|
||||
{
|
||||
void AsyncMqttClient::cancel() {
|
||||
BOOST_ASSERT(_impl);
|
||||
spdlog::info("MQTT client cancel");
|
||||
|
||||
|
||||
_impl->cancel();
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/json/value.hpp>
|
||||
#include <config.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::json {
|
||||
class value;
|
||||
}
|
||||
|
||||
namespace boost::asio {
|
||||
class any_io_executor;
|
||||
}
|
||||
@ -18,28 +15,23 @@ using executor = boost::asio::any_io_executor;
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
class IAsyncMqttClient{
|
||||
public:
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
class AsyncMqttClient {
|
||||
public:
|
||||
struct CallbackData {
|
||||
/* topic */
|
||||
std::string_view topic;
|
||||
|
||||
|
||||
/* response topic */
|
||||
std::optional< std::string_view > responseTopic;
|
||||
|
||||
/* value assosiated to request */
|
||||
const boost::json::value & value;
|
||||
|
||||
/* value assosiated to payload */
|
||||
boost::json::value payload;
|
||||
};
|
||||
|
||||
using callback_t = std::function< awaitable_expected< void >(const boost::json::value & value) >;
|
||||
|
||||
struct SubscribtionToken;
|
||||
|
||||
using callback_t = std::function< awaitable_expected< void >(const CallbackData & value) >;
|
||||
|
||||
struct AsyncMqttClientImpl;
|
||||
std::unique_ptr< AsyncMqttClientImpl > _impl;
|
||||
|
||||
@ -47,8 +39,10 @@ class AsyncMqttClient {
|
||||
~AsyncMqttClient();
|
||||
|
||||
/* subscribes to a topic, topic can contain wildcards */
|
||||
awaitable_expected< void > subscribe(std::string_view topic, callback_t && cb) noexcept;
|
||||
awaitable_expected< const SubscribtionToken * > subscribe(std::string_view topic, callback_t cb) noexcept;
|
||||
awaitable_expected< void > unsubscribe(const SubscribtionToken * subscribtionToken) noexcept;
|
||||
|
||||
/* send a json request, expect json response. Only json is supported for simplicity */
|
||||
awaitable_expected< const boost::json::value & > request(std::string_view topic, const boost::json::value & value) noexcept;
|
||||
awaitable_expected< void > publish(std::string_view topic, const boost::json::value & value) noexcept;
|
||||
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <config.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace boost::asio {
|
||||
class any_io_executor;
|
||||
}
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
class SingleShootTimer {
|
||||
public:
|
||||
template < typename T >
|
||||
using awaitable = boost::asio::awaitable< T >;
|
||||
using executor = boost::asio::any_io_executor;
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr< Impl > _impl;
|
||||
|
||||
SingleShootTimer(executor & executor, std::chrono::milliseconds timeout, std::function< void() > cb);
|
||||
~SingleShootTimer();
|
||||
|
||||
awaitable_expected< void > start() const;
|
||||
};
|
||||
|
||||
class PeriodicTimer {
|
||||
public:
|
||||
template < typename T >
|
||||
using awaitable = boost::asio::awaitable< T >;
|
||||
using executor = boost::asio::any_io_executor;
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr< Impl > _impl;
|
||||
|
||||
PeriodicTimer(executor & executor, std::chrono::milliseconds period, std::function< void() > cb);
|
||||
~PeriodicTimer();
|
||||
|
||||
awaitable_expected< void > start() const;
|
||||
};
|
||||
|
||||
} // namespace ranczo
|
||||
113
libs/timer.cpp
113
libs/timer.cpp
@ -1,113 +0,0 @@
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <expected>
|
||||
#include <ranczo-io/utils/timer.hpp>
|
||||
#include "config.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
struct SingleShootTimer::Impl {
|
||||
private:
|
||||
std::chrono::milliseconds _timeout;
|
||||
boost::asio::steady_timer _timer;
|
||||
std::function< void() > _callback;
|
||||
|
||||
public:
|
||||
Impl(boost::asio::any_io_executor & io_context, std::chrono::milliseconds timeout, std::function< void() > cb)
|
||||
: _timeout{timeout}, _timer(io_context, _timeout), _callback{std::move(cb)} {}
|
||||
|
||||
// Coroutine function to handle the timer expiration
|
||||
boost::asio::awaitable< void > timerCoroutine() {
|
||||
assert(_callback);
|
||||
|
||||
try {
|
||||
while(true) {
|
||||
// call my callback
|
||||
_callback();
|
||||
// Wait for the timer to expire
|
||||
co_await _timer.async_wait(boost::asio::use_awaitable);
|
||||
}
|
||||
} catch(const std::exception & e) {
|
||||
spdlog::error("Error: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
awaitable_expected< void > start() {
|
||||
spdlog::trace("co_spawn timer");
|
||||
// Start the coroutine by invoking the timerCoroutine() with boost::asio::co_spawn
|
||||
boost::asio::co_spawn(_timer.get_executor(), timerCoroutine(), boost::asio::detached);
|
||||
co_return expected< void, boost::system::error_code >{};
|
||||
}
|
||||
};
|
||||
|
||||
SingleShootTimer::SingleShootTimer(executor & executor, std::chrono::milliseconds timeout, std::function< void() > cb)
|
||||
: _impl{std::make_unique< Impl >(executor, timeout, std::move(cb))} {}
|
||||
SingleShootTimer::~SingleShootTimer() = default;
|
||||
|
||||
awaitable_expected<void> SingleShootTimer::start() const {
|
||||
assert(_impl);
|
||||
ASYNC_CHECK(_impl->start());
|
||||
co_return expected< void, boost::system::error_code >{};
|
||||
}
|
||||
|
||||
struct PeriodicTimer::Impl {
|
||||
private:
|
||||
std::chrono::milliseconds _interval;
|
||||
boost::asio::steady_timer _timer;
|
||||
std::function< void() > _callback;
|
||||
|
||||
public:
|
||||
Impl(boost::asio::any_io_executor & io_context, std::chrono::milliseconds interval, std::function< void() > cb)
|
||||
: _interval{interval}, _timer(io_context, _interval), _callback{std::move(cb)} {}
|
||||
|
||||
// Coroutine function to handle the timer expiration
|
||||
boost::asio::awaitable< void > timerCoroutine() {
|
||||
assert(_callback);
|
||||
|
||||
try {
|
||||
while(true) {
|
||||
// call my callback
|
||||
_callback();
|
||||
|
||||
// Wait for the timer to expire
|
||||
co_await _timer.async_wait(boost::asio::use_awaitable);
|
||||
|
||||
// Reset the timer for the next interval
|
||||
_timer.expires_after(_interval);
|
||||
}
|
||||
} catch(const std::exception & e) {
|
||||
spdlog::error("Error: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
awaitable_expected< void > start() {
|
||||
spdlog::trace("co_spawn timer");
|
||||
// Start the coroutine by invoking the timerCoroutine() with boost::asio::co_spawn
|
||||
boost::asio::co_spawn(_timer.get_executor(), timerCoroutine(), boost::asio::detached);
|
||||
co_return expected< void, boost::system::error_code >{};
|
||||
}
|
||||
};
|
||||
|
||||
PeriodicTimer::PeriodicTimer(executor & executor, std::chrono::milliseconds period, std::function< void() > cb)
|
||||
: _impl{std::make_unique< Impl >(executor, period, std::move(cb))} {}
|
||||
|
||||
awaitable_expected<void> PeriodicTimer::start() const {
|
||||
BOOST_ASSERT(_impl);
|
||||
ASYNC_CHECK(_impl->start());
|
||||
co_return expected< void, boost::system::error_code >{};
|
||||
}
|
||||
|
||||
PeriodicTimer::~PeriodicTimer() = default;
|
||||
|
||||
} // namespace ranczo
|
||||
@ -1,7 +1,8 @@
|
||||
add_executable(ranczo-io_floorheating
|
||||
main.cpp
|
||||
heater_controller.cpp
|
||||
relay.cpp
|
||||
temperature_controller.hpp temperature_controller.cpp
|
||||
relay.hpp relay.cpp
|
||||
thermometer.hpp thermometer.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(ranczo-io_floorheating
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <config.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::asio {
|
||||
class any_io_executor;
|
||||
}
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
class IHeaterController {
|
||||
public:
|
||||
template < typename T >
|
||||
using awaitable = boost::asio::awaitable< T >;
|
||||
|
||||
virtual ~IHeaterController() = default;
|
||||
|
||||
virtual awaitable_expected< void > start() noexcept = 0;
|
||||
};
|
||||
|
||||
class ResistiveFloorHeater : public IHeaterController {
|
||||
struct Impl;
|
||||
std::unique_ptr< Impl > _impl;
|
||||
|
||||
using executor = boost::asio::any_io_executor;
|
||||
template < typename T >
|
||||
using awaitable = boost::asio::awaitable< T >;
|
||||
|
||||
public:
|
||||
ResistiveFloorHeater(boost::asio::any_io_executor & io, std::string_view room);
|
||||
~ResistiveFloorHeater();
|
||||
|
||||
awaitable_expected< void > start() noexcept override;
|
||||
};
|
||||
} // namespace ranczo
|
||||
@ -1,3 +1,4 @@
|
||||
#include <memory>
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
|
||||
#include <boost/asio/associated_executor.hpp>
|
||||
@ -9,12 +10,12 @@
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "heater_controller.hpp"
|
||||
#include "services/floorheat_svc/relay.hpp"
|
||||
#include "services/floorheat_svc/thermometer.hpp"
|
||||
#include "temperature_controller.hpp"
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
#include <ranczo-io/utils/timer.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <vector>
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
@ -44,7 +45,7 @@ void signal_handler(int signum) {
|
||||
|
||||
int main() {
|
||||
spdlog::set_level(spdlog::level::trace);
|
||||
std::vector< std::shared_ptr< ranczo::IHeaterController > > _heaters;
|
||||
std::vector< std::shared_ptr< ranczo::TemperatureController > > _heaters;
|
||||
|
||||
boost::asio::io_context io_context;
|
||||
g_io = &io_context;
|
||||
@ -55,25 +56,40 @@ int main() {
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
|
||||
boost::asio::any_io_executor io_executor = io_context.get_executor();
|
||||
// PARTER
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "corridor"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "utilityRoom"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "wardrobe"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "bathroom_1"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "bathroom_2"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "beadroom_zone1"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "beadroom_zone2"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "livingroom_zone1"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "livingroom_zone2"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "livingroom_zone3"sv));
|
||||
ranczo::AsyncMqttClient mqttClient{io_executor};
|
||||
|
||||
// PIĘTRO
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "askaRoom"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "maciejRoom"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "playroom"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "office"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "bathroom_1"sv));
|
||||
_heaters.emplace_back(std::make_shared< ranczo::ResistiveFloorHeater >(io_executor, "corridor_up"sv));
|
||||
auto relayThermostatFactory = [&](auto room) {
|
||||
spdlog::debug("ThermostatFactory room {}: create relay", room);
|
||||
auto relay = std::make_unique< ranczo::MqttRelay >(io_executor, mqttClient);
|
||||
|
||||
spdlog::debug("ThermostatFactory room {}: create MqttThermometer", room);
|
||||
|
||||
ranczo::MqttThermometer::Settings settings{.room = room.data()};
|
||||
auto thermo = std::make_unique< ranczo::MqttThermometer >(io_executor, mqttClient, settings);
|
||||
|
||||
spdlog::debug("ThermostatFactory room {}: create Relay Thermostat", room);
|
||||
return std::make_shared< ranczo::RelayThermostat >(io_executor, mqttClient, std::move(relay), std::move(thermo), room);
|
||||
};
|
||||
|
||||
// // PARTER
|
||||
_heaters.emplace_back(relayThermostatFactory("corridor"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("utilityRoom"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("wardrobe"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("bathroom_1"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("bathroom_2"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("beadroom_zone1"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("beadroom_zone2"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("livingroom_zone1"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("livingroom_zone2"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("livingroom_zone3"sv));
|
||||
|
||||
// // PIĘTRO
|
||||
_heaters.emplace_back(relayThermostatFactory("askaRoom"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("maciejRoom"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("playroom"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("office"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("bathroom_1"sv));
|
||||
_heaters.emplace_back(relayThermostatFactory("corridor_up"sv));
|
||||
|
||||
for(auto & heater : _heaters) {
|
||||
co_spawn(io_context, heater->start(), boost::asio::detached);
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <config.hpp>
|
||||
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
|
||||
namespace ranczo {
|
||||
class Relay {
|
||||
public:
|
||||
virtual ~Relay() = default;
|
||||
|
||||
virtual awaitable_expected< void > on() noexcept = 0;
|
||||
virtual awaitable_expected< void > off() noexcept = 0;
|
||||
};
|
||||
|
||||
class MqttRelay : public Relay {
|
||||
// Relay interface
|
||||
public:
|
||||
MqttRelay(boost::asio::any_io_executor ex, AsyncMqttClient & mqtt){
|
||||
|
||||
}
|
||||
|
||||
ranczo::awaitable_expected< void > on() noexcept override {}
|
||||
ranczo::awaitable_expected< void > off() noexcept override {}
|
||||
};
|
||||
} // namespace ranczo
|
||||
@ -1,20 +1,21 @@
|
||||
#include "heater_controller.hpp"
|
||||
#include "temperature_controller.hpp"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "services/floorheat_svc/thermometer.hpp"
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/experimental/parallel_group.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/system/detail/errc.hpp>
|
||||
#include <boost/system/errc.hpp>
|
||||
#include <boost/system/result.hpp>
|
||||
#include <memory>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
#include <ranczo-io/utils/mqtt_topic_builder.hpp>
|
||||
#include <ranczo-io/utils/timer.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/circular_buffer.hpp>
|
||||
@ -41,16 +42,19 @@ struct TemperatureMeasurement {
|
||||
|
||||
enum Trend { Fall, Const, Rise };
|
||||
|
||||
struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
AsyncMqttClient _mqtt_client;
|
||||
struct RelayThermostat::Impl : private boost::noncopyable {
|
||||
executor & _io;
|
||||
|
||||
PeriodicTimer _tickTimer;
|
||||
AsyncMqttClient & _mqtt;
|
||||
|
||||
std::unique_ptr< Relay > _relay;
|
||||
std::unique_ptr< Thermometer > _temp;
|
||||
|
||||
boost::asio::steady_timer _tickTimer;
|
||||
std::chrono::system_clock::time_point _lastStateChange{std::chrono::system_clock::now()};
|
||||
|
||||
std::string _room;
|
||||
|
||||
std::chrono::system_clock::time_point _lastStateChange{std::chrono::system_clock::now()};
|
||||
boost::circular_buffer< TemperatureMeasurement > _measurements;
|
||||
bool _relayState{false};
|
||||
|
||||
double temperature{0.0};
|
||||
double targetTemperature{0.0};
|
||||
@ -58,22 +62,24 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
bool update = false;
|
||||
bool enabled = false;
|
||||
|
||||
Impl(boost::asio::any_io_executor & io, std::string_view room)
|
||||
: _mqtt_client{io},
|
||||
_tickTimer{io, std::chrono::seconds{1}, [&]() { heaterControlLoopTick(); }},
|
||||
_room{room.data(), room.size()},
|
||||
_measurements{100} {}
|
||||
Impl(executor & io,
|
||||
AsyncMqttClient & mqtt,
|
||||
std::unique_ptr< Relay > relay,
|
||||
std::unique_ptr< Thermometer > thermometer,
|
||||
std::string_view room)
|
||||
: _io{io}, _mqtt{mqtt}, _relay{std::move(relay)}, _temp{std::move(thermometer)}, _tickTimer{_io}, _room{room}, _measurements{100} {
|
||||
BOOST_ASSERT(relay);
|
||||
BOOST_ASSERT(not room.empty());
|
||||
}
|
||||
|
||||
~Impl() = default;
|
||||
|
||||
void setHEaterOn() {
|
||||
_lastStateChange = std::chrono::system_clock::now();
|
||||
_relayState = true;
|
||||
}
|
||||
|
||||
void setHeaterOff() {
|
||||
_lastStateChange = std::chrono::system_clock::now();
|
||||
_relayState = false;
|
||||
}
|
||||
|
||||
/// TODO sprawdź po 1 min czy temp faktycznie spada jeśli jest off czy rośnie jeśli jest on
|
||||
@ -131,16 +137,17 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
break;
|
||||
}
|
||||
|
||||
// auto avg = _measurements.size() ? std::accumulate(measurements.begin(), measurements.end(), 0.0) / _measurements.size() :
|
||||
// auto avg = _measurements.size() ? std::accumulate(measurements.begin(), measurements.end(), 0.0) `/ _measurements.size() :
|
||||
// 0.0; spdlog::info("got readout nr {} last temp {} avg {}", _measurements.size(), _measurements.back().temperature_C, avg);
|
||||
update = false;
|
||||
}
|
||||
}
|
||||
|
||||
awaitable_expected< void > subscribe(std::string_view topic,
|
||||
std::function< awaitable_expected< void >(const boost::json::value & object) > cb) {
|
||||
spdlog::trace("Heater subscribing to {}", topic);
|
||||
ASYNC_CHECK_MSG(_mqtt_client.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
|
||||
std::function< awaitable_expected< void >(const AsyncMqttClient::CallbackData&) > cb) {
|
||||
/// TODO fix
|
||||
spdlog::trace("RelayThermostat room {} subscribing to {}", _room, topic);
|
||||
ASYNC_CHECK_MSG(_mqtt.subscribe(topic, std::move(cb)), "Heater faild to subscribe on: {}", topic);
|
||||
co_return heater_expected_void{};
|
||||
}
|
||||
|
||||
@ -169,26 +176,11 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
|
||||
}
|
||||
|
||||
awaitable_expected< void > subscribeToTemperatureUpdate() {
|
||||
auto topic = topic::temperature::floor(_room);
|
||||
|
||||
auto cb = [=, this](const boost::json::value & object) -> awaitable_expected< void > {
|
||||
temperature = TRY(get_value(object, "value"));
|
||||
spdlog::trace("Heater temperature update {} for {}", temperature, _room);
|
||||
_measurements.push_back({std::chrono::system_clock::now(), temperature});
|
||||
update = true;
|
||||
co_return heater_expected_void{};
|
||||
};
|
||||
|
||||
ASYNC_CHECK(subscribe(topic, std::move(cb)));
|
||||
co_return heater_expected_void{};
|
||||
}
|
||||
|
||||
awaitable_expected< void > subscribeToCommandUpdate() {
|
||||
auto topic = topic::heating::command(_room);
|
||||
|
||||
auto cb = [=, this](const boost::json::value & jv) -> awaitable_expected< void > {
|
||||
targetTemperature = TRY(get_value(jv, "value"));
|
||||
auto cb = [=, this](const AsyncMqttClient::CallbackData&data) -> awaitable_expected< void > {
|
||||
targetTemperature = TRY(get_value(data.payload, "value"));
|
||||
spdlog::trace("Heater target temperature update {} for {}", targetTemperature, _room);
|
||||
update = true;
|
||||
co_return heater_expected_void{};
|
||||
@ -199,30 +191,35 @@ struct ResistiveFloorHeater::Impl : private boost::noncopyable {
|
||||
}
|
||||
|
||||
awaitable_expected< void > start() {
|
||||
// subscribe to a thermometer
|
||||
ASYNC_CHECK_MSG(subscribeToTemperatureUpdate(), "subscribe to temp update failed");
|
||||
|
||||
// subscribe to a thermostat
|
||||
ASYNC_CHECK_MSG(subscribeToCommandUpdate(), "subscribe to temp update failed");
|
||||
|
||||
/// TODO subscribe to energy measurements
|
||||
///
|
||||
// subscribe to a thermometer readings
|
||||
boost::asio::co_spawn(_io, _temp->listen(), boost::asio::detached);
|
||||
|
||||
ASYNC_CHECK_MSG(_tickTimer.start(), "failed to start timer");
|
||||
ASYNC_CHECK_MSG(_mqtt_client.listen(), "failed to listen");
|
||||
// subscribe to a thermostat commands feed
|
||||
ASYNC_CHECK_MSG(subscribeToCommandUpdate(), "subscribe to temp update failed");
|
||||
|
||||
/// TODO subscribe to energy measurements
|
||||
|
||||
// ASYNC_CHECK_MSG(_tickTimer.start(), "failed to start timer");
|
||||
// ASYNC_CHECK_MSG(_mqtt.listen(), "failed to listen");
|
||||
|
||||
co_return heater_expected_void{};
|
||||
}
|
||||
};
|
||||
|
||||
ResistiveFloorHeater::ResistiveFloorHeater(boost::asio::any_io_executor & io, std::string_view room)
|
||||
: _impl{std::make_unique< Impl >(io, room)} {}
|
||||
RelayThermostat::RelayThermostat(executor & io, AsyncMqttClient &mqtt, std::unique_ptr< Relay > relay, std::unique_ptr<Thermometer> thermometer, std::string_view room)
|
||||
: _impl{std::make_unique< Impl >(io, mqtt, std::move(relay), std::move(thermometer), room)} {
|
||||
}
|
||||
|
||||
ResistiveFloorHeater::~ResistiveFloorHeater() = default;
|
||||
awaitable_expected< void > ResistiveFloorHeater::start() noexcept {
|
||||
RelayThermostat::~RelayThermostat() = default;
|
||||
awaitable_expected< void > RelayThermostat::start() noexcept {
|
||||
BOOST_ASSERT(_impl);
|
||||
|
||||
return _impl->start();
|
||||
}
|
||||
|
||||
void RelayThermostat::stop() noexcept
|
||||
{
|
||||
BOOST_ASSERT(_impl);
|
||||
}
|
||||
|
||||
} // namespace ranczo
|
||||
41
services/floorheat_svc/temperature_controller.hpp
Normal file
41
services/floorheat_svc/temperature_controller.hpp
Normal file
@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <config.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "relay.hpp"
|
||||
#include "thermometer.hpp"
|
||||
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
|
||||
namespace boost::asio {
|
||||
class any_io_executor;
|
||||
}
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
class TemperatureController {
|
||||
public:
|
||||
template < typename T >
|
||||
using awaitable = boost::asio::awaitable< T >;
|
||||
|
||||
virtual ~TemperatureController() = default;
|
||||
|
||||
virtual awaitable_expected< void > start() noexcept = 0;
|
||||
virtual void stop() noexcept = 0;
|
||||
};
|
||||
|
||||
class RelayThermostat : public TemperatureController {
|
||||
struct Impl;
|
||||
std::unique_ptr< Impl > _impl;
|
||||
|
||||
using executor = boost::asio::any_io_executor;
|
||||
public:
|
||||
RelayThermostat(executor & executor, AsyncMqttClient &mqtt, std::unique_ptr<Relay> relay, std::unique_ptr<Thermometer> thermometer, std::string_view room);
|
||||
~RelayThermostat();
|
||||
|
||||
awaitable_expected< void > start() noexcept override;
|
||||
void stop() noexcept override;
|
||||
};
|
||||
} // namespace ranczo
|
||||
0
services/floorheat_svc/thermometer.cpp
Normal file
0
services/floorheat_svc/thermometer.cpp
Normal file
138
services/floorheat_svc/thermometer.hpp
Normal file
138
services/floorheat_svc/thermometer.hpp
Normal file
@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
#include <boost/asio/as_tuple.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <charconv>
|
||||
#include <config.hpp>
|
||||
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
|
||||
#include <boost/asio/strand.hpp>
|
||||
namespace ranczo {
|
||||
|
||||
using _void = expected< void, boost::system::error_code >;
|
||||
|
||||
struct Thermometer {
|
||||
virtual ~Thermometer() = default;
|
||||
|
||||
// Register async handler for temperature updates (and wire subscriptions under the hood).
|
||||
// Return quickly after the subscription is issued.
|
||||
using async_cb = std::function< awaitable_expected< void >(double) >;
|
||||
|
||||
virtual awaitable_expected< void > on_update(async_cb handler) noexcept = 0;
|
||||
|
||||
// Perpetual listening loop (spawn this, do not co_await in start-up code).
|
||||
virtual awaitable_expected< void > listen() noexcept = 0;
|
||||
|
||||
// Stop internal activity (e.g., cancel). Optional in clean shutdown.
|
||||
virtual void cancel() noexcept = 0;
|
||||
|
||||
// Convenience access to the latest value (if any).
|
||||
virtual std::optional< double > current() const = 0;
|
||||
};
|
||||
|
||||
inline expected< double, boost::system::error_code > to_double(const boost::json::value & v) {
|
||||
if(v.is_double())
|
||||
return v.as_double();
|
||||
if(v.is_int64())
|
||||
return static_cast< double >(v.as_int64());
|
||||
if(v.is_uint64())
|
||||
return static_cast< double >(v.as_uint64());
|
||||
if(v.is_string()) {
|
||||
const auto & s = v.as_string();
|
||||
double out{};
|
||||
if(auto res = std::from_chars(s.data(), s.data() + s.size(), out); res.ec == std::errc{})
|
||||
return out;
|
||||
}
|
||||
return unexpected{make_error_code(boost::system::errc::invalid_argument)};
|
||||
}
|
||||
|
||||
class MqttThermometer : public Thermometer {
|
||||
public:
|
||||
using json = boost::json::value;
|
||||
|
||||
struct Settings {
|
||||
std::string room; // e.g. "bathroom"
|
||||
std::string key{"temperature"}; // JSON key to read if payload is an object
|
||||
};
|
||||
|
||||
MqttThermometer(boost::asio::any_io_executor ex, AsyncMqttClient & mqtt, Settings cfg)
|
||||
: strand_(boost::asio::make_strand(ex)), mqtt_(mqtt), cfg_(std::move(cfg)) {}
|
||||
|
||||
// Register handler + subscribe to the room topic. Do not block.
|
||||
awaitable_expected< void > on_update(async_cb handler) noexcept override {
|
||||
handler_ = std::move(handler);
|
||||
|
||||
co_await mqtt_.subscribe(topic_temperature(), [this](const AsyncMqttClient::CallbackData & data) -> 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 = extract_value(data.payload)) {
|
||||
current_ = *v;
|
||||
if(handler_)
|
||||
co_await handler_(*v);
|
||||
}
|
||||
co_return _void{};
|
||||
});
|
||||
co_return _void{};
|
||||
}
|
||||
|
||||
// Perpetual loop
|
||||
awaitable_expected< void > listen() noexcept override {
|
||||
// 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_};
|
||||
for(;;) {
|
||||
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)
|
||||
co_return _void{};
|
||||
}
|
||||
co_return _void{};
|
||||
}
|
||||
|
||||
void cancel() noexcept override {
|
||||
// subscriptions are managed by mqtt_ implementation
|
||||
///TODO unsubscribe
|
||||
}
|
||||
|
||||
std::optional< double > current() const override {
|
||||
return current_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string topic_temperature() const {
|
||||
return "home/" + cfg_.room + "/heating/floor/temperature";
|
||||
}
|
||||
|
||||
static std::optional< double > as_number(const json & v) {
|
||||
if(v.is_double())
|
||||
return v.as_double();
|
||||
if(v.is_int64())
|
||||
return static_cast< double >(v.as_int64());
|
||||
if(v.is_uint64())
|
||||
return static_cast< double >(v.as_uint64());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional< double > extract_value(const json & payload) const {
|
||||
if(auto num = as_number(payload))
|
||||
return num;
|
||||
if(auto * obj = payload.if_object()) {
|
||||
if(auto * p = obj->if_contains(cfg_.key))
|
||||
return as_number(*p);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
private:
|
||||
boost::asio::strand< boost::asio::any_io_executor > strand_;
|
||||
AsyncMqttClient & mqtt_;
|
||||
Settings cfg_{};
|
||||
|
||||
async_cb handler_{};
|
||||
std::optional< double > current_{};
|
||||
};
|
||||
|
||||
} // namespace ranczo
|
||||
@ -1,14 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <config.hpp>
|
||||
|
||||
#include <ranczo-io/utils/timer.hpp>
|
||||
|
||||
namespace ranczo {
|
||||
class DS18B20Sensor {
|
||||
public:
|
||||
DS18B20Sensor(boost::asio::any_io_executor exec, std::string device_id, std::chrono::seconds interval = std::chrono::seconds(2))
|
||||
: executor_(exec), timer_(exec, interval, [&]() { return this->run(); }), device_id_(std::move(device_id)) {}
|
||||
: executor_(exec) {}
|
||||
|
||||
boost::asio::awaitable< void > run();
|
||||
|
||||
@ -16,7 +15,7 @@ class DS18B20Sensor {
|
||||
awaitable_expected< float > read_temperature();
|
||||
|
||||
boost::asio::any_io_executor executor_;
|
||||
PeriodicTimer timer_;
|
||||
// boost::asio::steady_timer timer_;
|
||||
std::string device_id_;
|
||||
};
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <ranczo-io/utils/mqtt_client.hpp>
|
||||
#include <ranczo-io/utils/timer.hpp>
|
||||
|
||||
namespace ranczo {
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user