ESPHome 2025.12.0-dev
Loading...
Searching...
No Matches
modbus_controller.h
Go to the documentation of this file.
1#pragma once
2
4
7
8#include <list>
9#include <queue>
10#include <set>
11#include <utility>
12#include <vector>
13
14namespace esphome {
15namespace modbus_controller {
16
17class ModbusController;
18
22
23enum class SensorValueType : uint8_t {
24 RAW = 0x00, // variable length
25 U_WORD = 0x1, // 1 Register unsigned
26 U_DWORD = 0x2, // 2 Registers unsigned
27 S_WORD = 0x3, // 1 Register signed
28 S_DWORD = 0x4, // 2 Registers signed
29 BIT = 0x5,
30 U_DWORD_R = 0x6, // 2 Registers unsigned
31 S_DWORD_R = 0x7, // 2 Registers unsigned
32 U_QWORD = 0x8,
33 S_QWORD = 0x9,
34 U_QWORD_R = 0xA,
35 S_QWORD_R = 0xB,
36 FP32 = 0xC,
37 FP32_R = 0xD
38};
39
43
80
81inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
82
91inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
92 if (value.length() < pos * 2 + 1)
93 return 0;
94 return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
95}
96
103inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
104 return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
105}
106
113inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
114 return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
115}
116
123inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
124 return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
125}
126
127// Extract data from modbus response buffer
134template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
135 if (sizeof(T) == sizeof(uint8_t)) {
136 return T(data[buffer_offset]);
137 }
138 if (sizeof(T) == sizeof(uint16_t)) {
139 return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
140 }
141
142 if (sizeof(T) == sizeof(uint32_t)) {
143 return get_data<uint16_t>(data, buffer_offset) << 16 | get_data<uint16_t>(data, (buffer_offset + 2));
144 }
145
146 if (sizeof(T) == sizeof(uint64_t)) {
147 return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
148 (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
149 }
150}
151
160inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
161 auto data_byte = coil / 8;
162 return (data[data_byte] & (1 << (coil % 8))) > 0;
163}
164
175template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
176 auto result = (mask & data);
177 if (result == 0 || mask == 0xFFFFFFFF) {
178 return result;
179 }
180 for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
181 if ((mask & (1 << pos)) != 0)
182 return result >> pos;
183 }
184 return 0;
185}
186
193void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
194
202int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
203 uint32_t bitmask);
204
205class ModbusController;
206
208 public:
209 virtual void parse_and_publish(const std::vector<uint8_t> &data) = 0;
210
211 void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
212 size_t virtual get_register_size() const {
214 return 1;
215 } else { // if CONF_RESPONSE_BYTES is used override the default
217 }
218 }
219 // Override register size for modbus devices not using 1 register for one dword
220 void set_register_size(uint8_t register_size) { response_bytes = register_size; }
223 uint16_t start_address{0};
224 uint32_t bitmask{0};
225 uint8_t offset{0};
226 uint8_t register_count{0};
227 uint8_t response_bytes{0};
228 uint16_t skip_updates{0};
229 std::vector<uint8_t> custom_data{};
230 bool force_new_range{false};
231};
232
234 bool enabled{false};
235 uint16_t register_last_address{0xFFFF};
236 uint16_t register_value{0};
237};
238
240 using ReadLambda = std::function<int64_t()>;
241 using WriteLambda = std::function<bool(int64_t value)>;
242
243 public:
245 this->address = address;
246 this->value_type = value_type;
247 this->register_count = register_count;
248 }
249
250 template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
251 this->read_lambda = [this, user_read_lambda]() -> int64_t {
252 T user_value = user_read_lambda(this->address);
253 if constexpr (std::is_same_v<T, float>) {
254 return bit_cast<uint32_t>(user_value);
255 } else {
256 return static_cast<int64_t>(user_value);
257 }
258 };
259 }
260
261 template<typename T>
262 void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
263 this->write_lambda = [this, user_write_lambda](int64_t number) {
264 if constexpr (std::is_same_v<T, float>) {
265 float float_value = bit_cast<float>(static_cast<uint32_t>(number));
266 return user_write_lambda(this->address, float_value);
267 }
268 return user_write_lambda(this->address, static_cast<T>(number));
269 };
270 }
271
272 // Formats a raw value into a string representation based on the value type for debugging
273 std::string format_value(int64_t value) const {
274 switch (this->value_type) {
280 return std::to_string(static_cast<uint64_t>(value));
286 return std::to_string(value);
289 return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
290 default:
291 return std::to_string(value);
292 }
293 }
294
295 uint16_t address{0};
297 uint8_t register_count{0};
298 ReadLambda read_lambda;
299 WriteLambda write_lambda;
300};
301
302// ModbusController::create_register_ranges_ tries to optimize register range
303// for this the sensors must be ordered by register_type, start_address and bitmask
305 public:
306 bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
307 // first sort according to register type
308 if (lhs->register_type != rhs->register_type) {
309 return lhs->register_type < rhs->register_type;
310 }
311
312 // ensure that sensor with force_new_range set are before the others
313 if (lhs->force_new_range != rhs->force_new_range) {
314 return lhs->force_new_range > rhs->force_new_range;
315 }
316
317 // sort by start address
318 if (lhs->start_address != rhs->start_address) {
319 return lhs->start_address < rhs->start_address;
320 }
321
322 // sort by offset (ensures update of sensors in ascending order)
323 if (lhs->offset != rhs->offset) {
324 return lhs->offset < rhs->offset;
325 }
326
327 // The pointer to the sensor is used last to ensure that
328 // multiple sensors with the same values can be added with a stable sort order.
329 return lhs < rhs;
330 }
331};
332
333using SensorSet = std::set<SensorItem *, SensorItemsComparator>;
334
339 uint16_t skip_updates; // the config value
340 SensorSet sensors; // all sensors of this range
341 uint16_t skip_updates_counter; // the running value
342};
343
345 public:
346 static const size_t MAX_PAYLOAD_BYTES = 240;
348 uint16_t register_address{0};
349 uint16_t register_count{0};
352 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
354 std::vector<uint8_t> payload = {};
355 bool send();
357 bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
358
360
371 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
372 &&handler);
382 uint16_t start_address, uint16_t register_count);
393 uint16_t register_count, const std::vector<uint16_t> &values);
403 uint16_t value);
412
421 const std::vector<bool> &values);
430 ModbusController *modbusdevice, const std::vector<uint8_t> &values,
431 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
432 &&handler = nullptr);
433
442 ModbusController *modbusdevice, const std::vector<uint16_t> &values,
443 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
444 &&handler = nullptr);
445
446 bool is_equal(const ModbusCommandItem &other);
447
448 protected:
449 // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
451 uint8_t send_count_{0};
452};
453
463 public:
464 void dump_config() override;
465 void loop() override;
466 void setup() override;
467 void update() override;
468
470 void queue_command(const ModbusCommandItem &command);
472 void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
474 void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
476 void on_modbus_data(const std::vector<uint8_t> &data) override;
478 void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
480 void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
482 void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
484 void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
487 void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
488 const std::vector<uint8_t> &data);
490 void set_allow_duplicate_commands(bool allow_duplicate_commands) {
491 this->allow_duplicate_commands_ = allow_duplicate_commands;
492 }
496 void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
498 void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
500 size_t get_command_queue_length() { return command_queue_.size(); }
504 void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
506 void add_on_online_callback(std::function<void(int, int)> &&callback);
508 void add_on_offline_callback(std::function<void(int, int)> &&callback);
510 void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
512 uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
514 void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
515 this->server_courtesy_response_ = server_courtesy_response;
516 }
519
520 protected:
523 // find register in sensormap. Returns iterator with all registers having the same start address
524 SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
528 void process_modbus_data_(const ModbusCommandItem *response);
530 bool send_next_command_();
532 void dump_sensors_();
536 std::vector<ServerRegister *> server_registers_{};
538 std::vector<RegisterRange> register_ranges_{};
540 std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
542 std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
548 uint16_t command_throttle_{0};
550 bool module_offline_{false};
563 .enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
564};
565
571inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
572 int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
573
574 float float_value;
576 float_value = bit_cast<float>(static_cast<uint32_t>(number));
577 } else {
578 float_value = static_cast<float>(number);
579 }
580
581 return float_value;
582}
583
584inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
585 int64_t val;
586
587 if (value_type_is_float(value_type)) {
588 val = bit_cast<uint32_t>(value);
589 } else {
590 val = llroundf(value);
591 }
592
593 std::vector<uint16_t> data;
594 number_to_payload(data, val, value_type);
595 return data;
596}
597
598} // namespace modbus_controller
599} // namespace esphome
uint8_t address
Definition bl0906.h:4
This class simplifies creating components that periodically check a state.
Definition component.h:437
static ModbusCommandItem create_custom_command(ModbusController *modbusdevice, const std::vector< uint8_t > &values, std::function< void(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)> &&handler=nullptr)
Create custom modbus command.
bool should_retry(uint8_t max_retries)
Check if the command should be retried based on the max_retries parameter.
bool is_equal(const ModbusCommandItem &other)
static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address, const std::vector< bool > &values)
Create modbus write multiple registers command Function 15 (0Fhex) Write Multiple Coils.
static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value)
Create modbus write single registers command Function 05 (05hex) Write Single Coil.
uint8_t send_count_
How many times this command has been sent.
static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, uint16_t value)
Create modbus write multiple registers command Function 16 (10hex) Write Multiple Registers.
static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, std::function< void(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)> &&handler)
factory methods
static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address, uint16_t register_count, const std::vector< uint16_t > &values)
Create modbus read command Function code 02-04.
std::function< void(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)> on_data_func
void add_sensor_item(SensorItem *item)
Registers a sensor with the controller. Called by esphomes code generator.
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)
default delegate called by process_modbus_data when a response has retrieved from the incoming queue
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final
called when a modbus request (function code 0x03 or 0x04) was parsed without errors
std::queue< std::unique_ptr< ModbusCommandItem > > incoming_queue_
modbus response data waiting to get processed
void set_command_throttle(uint16_t command_throttle)
called by esphome generated code to set the command_throttle period
void set_offline_skip_updates(uint16_t offline_skip_updates)
called by esphome generated code to set the offline_skip_updates
void add_on_online_callback(std::function< void(int, int)> &&callback)
Set callback for online changes.
uint16_t command_throttle_
min time in ms between sending modbus commands
void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)
default delegate called by process_modbus_data when a response for a write response has retrieved fro...
bool allow_duplicate_commands_
if duplicate commands can be sent
bool get_allow_duplicate_commands()
get if a duplicate command can be sent
void add_on_offline_callback(std::function< void(int, int)> &&callback)
Set callback for offline changes.
CallbackManager< void(int, int)> command_sent_callback_
Command sent callback.
void add_server_register(ServerRegister *server_register)
Registers a server register with the controller. Called by esphomes code generator.
std::vector< RegisterRange > register_ranges_
Continuous range of modbus registers.
uint32_t last_command_timestamp_
when was the last send operation
CallbackManager< void(int, int)> offline_callback_
Server offline callback.
void dump_sensors_()
dump the parsed sensormap for diagnostics
std::vector< ServerRegister * > server_registers_
Collection of all server registers for this component.
SensorSet sensorset_
Collection of all sensors for this component.
uint8_t max_cmd_retries_
How many times we will retry a command if we get no response.
bool send_next_command_()
send the next modbus command from the send queue
ServerCourtesyResponse get_server_courtesy_response() const
Get the server courtesy response object.
std::list< std::unique_ptr< ModbusCommandItem > > command_queue_
Hold the pending requests to be sent.
size_t get_command_queue_length()
get the number of queued modbus commands (should be mostly empty)
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override
called when a modbus error response was received
void process_modbus_data_(const ModbusCommandItem *response)
parse incoming modbus data
void set_allow_duplicate_commands(bool allow_duplicate_commands)
Allow a duplicate command to be sent.
void update_range_(RegisterRange &r)
submit the read command for the address range to the send queue
uint8_t get_max_cmd_retries()
get how many times a command will be (re)sent if no response is received
ServerCourtesyResponse server_courtesy_response_
Server courtesy response.
void set_max_cmd_retries(uint8_t max_cmd_retries)
called by esphome generated code to set the max_cmd_retries.
bool module_offline_
if module didn't respond the last command
size_t create_register_ranges_()
parse sensormap_ and create range of sequential addresses
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response)
Called by esphome generated code to set the server courtesy response object.
uint16_t offline_skip_updates_
how many updates to skip if module is offline
bool get_module_offline()
get if the module is offline, didn't respond the last command
void add_on_command_sent_callback(std::function< void(int, int)> &&callback)
Set callback for commands.
void on_modbus_write_registers(uint8_t function_code, const std::vector< uint8_t > &data) final
called when a modbus request (function code 0x06 or 0x10) was parsed without errors
void on_modbus_data(const std::vector< uint8_t > &data) override
called when a modbus response was parsed without errors
void queue_command(const ModbusCommandItem &command)
queues a modbus command in the send queue
SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const
CallbackManager< void(int, int)> online_callback_
Server online callback.
void set_custom_data(const std::vector< uint8_t > &data)
virtual void parse_and_publish(const std::vector< uint8_t > &data)=0
void set_register_size(uint8_t register_size)
bool operator()(const SensorItem *lhs, const SensorItem *rhs) const
void set_write_lambda(const std::function< bool(uint16_t address, const T v)> &&user_write_lambda)
std::string format_value(int64_t value) const
void set_read_lambda(const std::function< T(uint16_t address)> &&user_read_lambda)
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count)
mopeka_std_values val[4]
void number_to_payload(std::vector< uint16_t > &data, int64_t value, SensorValueType value_type)
Convert float value to vector<uint16_t> suitable for sending.
std::set< SensorItem *, SensorItemsComparator > SensorSet
uint8_t byte_from_hex_str(const std::string &value, uint8_t pos)
Get a byte from a hex string hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34 hex_byte_fro...
uint64_t qword_from_hex_str(const std::string &value, uint8_t pos)
Get a qword from a hex string.
uint32_t dword_from_hex_str(const std::string &value, uint8_t pos)
Get a dword from a hex string.
float payload_to_float(const std::vector< uint8_t > &data, const SensorItem &item)
Convert vector<uint8_t> response payload to float.
ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type)
uint16_t word_from_hex_str(const std::string &value, uint8_t pos)
Get a word from a hex string.
std::vector< uint16_t > float_to_payload(float value, SensorValueType value_type)
bool coil_from_vector(int coil, const std::vector< uint8_t > &data)
Extract coil data from modbus response buffer Responses for coil are packed into bytes .
ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type)
bool value_type_is_float(SensorValueType v)
T get_data(const std::vector< uint8_t > &data, size_t buffer_offset)
Extract data from modbus response buffer.
int64_t payload_to_number(const std::vector< uint8_t > &data, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask)
Convert vector<uint8_t> response payload to number.
N mask_and_shift_by_rightbit(N data, uint32_t mask)
Extract bits from value and shift right according to the bitmask if the bitmask is 0x00F0 we want the...
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string str_sprintf(const char *fmt,...)
Definition helpers.cpp:222
To bit_cast(const From &src)
Convert data between types, without aliasing issues or undefined behaviour.
Definition helpers.h:71