#include "config.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ranczo { ModbusTcpContext::~ModbusTcpContext() { if(ctx_) { ::modbus_close(ctx_.get()); ctx_.reset(); } pool_.join(); } 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_); 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_); return unexpected(errno_errc()); } _connected = true; spdlog::info("Modbus connected to {}:{}", host_.c_str(), port_); timeval tv{2, 0}; ::modbus_set_response_timeout(ctx.get(), tv.tv_sec, tv.tv_usec); // ::modbus_set_byte_timeout(ctx.get(), tv.tv_sec, tv.tv_usec); std::scoped_lock lk(mx_); ctx_ = std::move(ctx); out_ctx = ctx_.get(); return {}; }); } bool ModbusTcpContext::connected() const { return _connected; } awaitable_expected< void > ranczo::ModbusTcpContext::wait_between_commands() { namespace asio = boost::asio; using namespace std::chrono; const auto now = steady_clock::now(); const auto expiry = delay_timer_.expiry(); // 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 ", 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)); if(ec && ec != asio::error::operation_aborted) { co_return unexpected(ec); } } co_return _void{}; } awaitable_expected< void > ModbusTcpContext::async_close() { co_return co_await async_call< void >([this](modbus_t *&) -> expected< void > { std::scoped_lock lk(mx_); if(ctx_) { ::modbus_close(ctx_.get()); ctx_.reset(); _connected = false; } return {}; }); } boost::system::error_code ModbusTcpContext::errno_errc() { return {errno, boost::system::generic_category()}; } std::shared_ptr< ModbusTcpContext > ModbusTcpContext::create(boost::asio::io_context & io, std::string host, int port, std::size_t pool_size) { return std::make_shared< ModbusTcpContext >(io, std::move(host), port, pool_size); } struct on_exit { std::function< void() > _fn; ~on_exit() { if(_fn) _fn(); } }; 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_); }}; std::scoped_lock lk(mx_); if(!ctx_) return unexpected(make_error_code(boost::system::errc::not_connected)); out = ctx_.get(); return op(out); }); } template < typename T, typename Maker > awaitable_expected< T > ModbusTcpContext::async_call(Maker && maker) { namespace asio = boost::asio; // Initiator wyprodukuje expected auto initiator = [this, maker = std::forward< Maker >(maker)](auto && completion_handler) mutable { auto ex = asio::get_associated_executor(completion_handler, io_.get_executor()); asio::post(pool_, [this, maker = std::move(maker), completion_handler = std::move(completion_handler), ex]() mutable { // maker może potrzebować zwrócić też wskaźnik na ctx (referencja wyjściowa) modbus_t * ctx_ptr = nullptr; expected< T > result = maker(ctx_ptr); asio::post(ex, [completion_handler = std::move(completion_handler), result = std::move(result)]() mutable { completion_handler(std::move(result)); }); }); }; co_return co_await asio::async_initiate< decltype(asio::use_awaitable), void(expected< T >) >( std::move(initiator), asio::use_awaitable_t<>{}); } awaitable_expected< uint16_t > ModbusDevice::async_read_holding_register(uint16_t address) { address -= 1; 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); 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); return unexpected(errno_errc()); } return val; }); } awaitable_expected< void > ModbusDevice::async_write_coil(uint16_t address, bool value) { 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); 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); return unexpected(errno_errc()); } return {}; }); } } // namespace ranczo