ESPHome 2025.9.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
20 CUSTOM = 0x00,
21 READ_COILS = 0x01,
25 WRITE_SINGLE_COIL = 0x05,
27 READ_EXCEPTION_STATUS = 0x07, // not implemented
28 DIAGNOSTICS = 0x08, // not implemented
29 GET_COMM_EVENT_COUNTER = 0x0B, // not implemented
30 GET_COMM_EVENT_LOG = 0x0C, // not implemented
33 REPORT_SERVER_ID = 0x11, // not implemented
34 READ_FILE_RECORD = 0x14, // not implemented
35 WRITE_FILE_RECORD = 0x15, // not implemented
36 MASK_WRITE_REGISTER = 0x16, // not implemented
37 READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented
38 READ_FIFO_QUEUE = 0x18, // not implemented
39};
40
41enum class ModbusRegisterType : uint8_t {
42 CUSTOM = 0x0,
43 COIL = 0x01,
44 DISCRETE_INPUT = 0x02,
45 HOLDING = 0x03,
46 READ = 0x04,
47};
48
49enum class SensorValueType : uint8_t {
50 RAW = 0x00, // variable length
51 U_WORD = 0x1, // 1 Register unsigned
52 U_DWORD = 0x2, // 2 Registers unsigned
53 S_WORD = 0x3, // 1 Register signed
54 S_DWORD = 0x4, // 2 Registers signed
55 BIT = 0x5,
56 U_DWORD_R = 0x6, // 2 Registers unsigned
57 S_DWORD_R = 0x7, // 2 Registers unsigned
58 U_QWORD = 0x8,
59 S_QWORD = 0x9,
60 U_QWORD_R = 0xA,
61 S_QWORD_R = 0xB,
62 FP32 = 0xC,
63 FP32_R = 0xD
64};
65
69
106
107inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
108
117inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
118 if (value.length() < pos * 2 + 1)
119 return 0;
120 return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
121}
122
129inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
130 return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
131}
132
139inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
140 return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
141}
142
149inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
150 return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
151}
152
153// Extract data from modbus response buffer
160template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
161 if (sizeof(T) == sizeof(uint8_t)) {
162 return T(data[buffer_offset]);
163 }
164 if (sizeof(T) == sizeof(uint16_t)) {
165 return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
166 }
167
168 if (sizeof(T) == sizeof(uint32_t)) {
169 return get_data<uint16_t>(data, buffer_offset) << 16 | get_data<uint16_t>(data, (buffer_offset + 2));
170 }
171
172 if (sizeof(T) == sizeof(uint64_t)) {
173 return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
174 (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
175 }
176}
177
186inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
187 auto data_byte = coil / 8;
188 return (data[data_byte] & (1 << (coil % 8))) > 0;
189}
190
201template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
202 auto result = (mask & data);
203 if (result == 0 || mask == 0xFFFFFFFF) {
204 return result;
205 }
206 for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
207 if ((mask & (1 << pos)) != 0)
208 return result >> pos;
209 }
210 return 0;
211}
212
219void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
220
228int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
229 uint32_t bitmask);
230
231class ModbusController;
232
234 public:
235 virtual void parse_and_publish(const std::vector<uint8_t> &data) = 0;
236
237 void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
238 size_t virtual get_register_size() const {
240 return 1;
241 } else { // if CONF_RESPONSE_BYTES is used override the default
243 }
244 }
245 // Override register size for modbus devices not using 1 register for one dword
246 void set_register_size(uint8_t register_size) { response_bytes = register_size; }
249 uint16_t start_address{0};
250 uint32_t bitmask{0};
251 uint8_t offset{0};
252 uint8_t register_count{0};
253 uint8_t response_bytes{0};
254 uint16_t skip_updates{0};
255 std::vector<uint8_t> custom_data{};
256 bool force_new_range{false};
257};
258
260 using ReadLambda = std::function<int64_t()>;
261 using WriteLambda = std::function<bool(int64_t value)>;
262
263 public:
265 this->address = address;
266 this->value_type = value_type;
267 this->register_count = register_count;
268 }
269
270 template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
271 this->read_lambda = [this, user_read_lambda]() -> int64_t {
272 T user_value = user_read_lambda(this->address);
273 if constexpr (std::is_same_v<T, float>) {
274 return bit_cast<uint32_t>(user_value);
275 } else {
276 return static_cast<int64_t>(user_value);
277 }
278 };
279 }
280
281 template<typename T>
282 void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
283 this->write_lambda = [this, user_write_lambda](int64_t number) {
284 if constexpr (std::is_same_v<T, float>) {
285 float float_value = bit_cast<float>(static_cast<uint32_t>(number));
286 return user_write_lambda(this->address, float_value);
287 }
288 return user_write_lambda(this->address, static_cast<T>(number));
289 };
290 }
291
292 // Formats a raw value into a string representation based on the value type for debugging
293 std::string format_value(int64_t value) const {
294 switch (this->value_type) {
300 return std::to_string(static_cast<uint64_t>(value));
306 return std::to_string(value);
309 return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
310 default:
311 return std::to_string(value);
312 }
313 }
314
315 uint16_t address{0};
317 uint8_t register_count{0};
318 ReadLambda read_lambda;
319 WriteLambda write_lambda;
320};
321
322// ModbusController::create_register_ranges_ tries to optimize register range
323// for this the sensors must be ordered by register_type, start_address and bitmask
325 public:
326 bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
327 // first sort according to register type
328 if (lhs->register_type != rhs->register_type) {
329 return lhs->register_type < rhs->register_type;
330 }
331
332 // ensure that sensor with force_new_range set are before the others
333 if (lhs->force_new_range != rhs->force_new_range) {
334 return lhs->force_new_range > rhs->force_new_range;
335 }
336
337 // sort by start address
338 if (lhs->start_address != rhs->start_address) {
339 return lhs->start_address < rhs->start_address;
340 }
341
342 // sort by offset (ensures update of sensors in ascending order)
343 if (lhs->offset != rhs->offset) {
344 return lhs->offset < rhs->offset;
345 }
346
347 // The pointer to the sensor is used last to ensure that
348 // multiple sensors with the same values can be added with a stable sort order.
349 return lhs < rhs;
350 }
351};
352
353using SensorSet = std::set<SensorItem *, SensorItemsComparator>;
354
359 uint16_t skip_updates; // the config value
360 SensorSet sensors; // all sensors of this range
361 uint16_t skip_updates_counter; // the running value
362};
363
365 public:
366 static const size_t MAX_PAYLOAD_BYTES = 240;
368 uint16_t register_address{0};
369 uint16_t register_count{0};
372 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
374 std::vector<uint8_t> payload = {};
375 bool send();
377 bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
378
380
391 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
392 &&handler);
402 uint16_t start_address, uint16_t register_count);
413 uint16_t register_count, const std::vector<uint16_t> &values);
423 uint16_t value);
432
441 const std::vector<bool> &values);
450 ModbusController *modbusdevice, const std::vector<uint8_t> &values,
451 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
452 &&handler = nullptr);
453
462 ModbusController *modbusdevice, const std::vector<uint16_t> &values,
463 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
464 &&handler = nullptr);
465
466 bool is_equal(const ModbusCommandItem &other);
467
468 protected:
469 // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
471 uint8_t send_count_{0};
472};
473
483 public:
484 void dump_config() override;
485 void loop() override;
486 void setup() override;
487 void update() override;
488
490 void queue_command(const ModbusCommandItem &command);
492 void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
494 void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
496 void on_modbus_data(const std::vector<uint8_t> &data) override;
498 void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
500 void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
502 void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
504 void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
507 void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
508 const std::vector<uint8_t> &data);
510 void set_allow_duplicate_commands(bool allow_duplicate_commands) {
511 this->allow_duplicate_commands_ = allow_duplicate_commands;
512 }
516 void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
518 void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
520 size_t get_command_queue_length() { return command_queue_.size(); }
524 void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
526 void add_on_online_callback(std::function<void(int, int)> &&callback);
528 void add_on_offline_callback(std::function<void(int, int)> &&callback);
530 void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
532 uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
533
534 protected:
537 // find register in sensormap. Returns iterator with all registers having the same start address
538 SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
542 void process_modbus_data_(const ModbusCommandItem *response);
544 bool send_next_command_();
546 void dump_sensors_();
550 std::vector<ServerRegister *> server_registers_{};
552 std::vector<RegisterRange> register_ranges_{};
554 std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
556 std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
562 uint16_t command_throttle_{0};
564 bool module_offline_{false};
575};
576
582inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
583 int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
584
585 float float_value;
587 float_value = bit_cast<float>(static_cast<uint32_t>(number));
588 } else {
589 float_value = static_cast<float>(number);
590 }
591
592 return float_value;
593}
594
595inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
596 int64_t val;
597
598 if (value_type_is_float(value_type)) {
599 val = bit_cast<uint32_t>(value);
600 } else {
601 val = llroundf(value);
602 }
603
604 std::vector<uint16_t> data;
605 number_to_payload(data, val, value_type);
606 return data;
607}
608
609} // namespace modbus_controller
610} // namespace esphome
uint8_t address
Definition bl0906.h:4
This class simplifies creating components that periodically check a state.
Definition component.h:425
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
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
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
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:208
To bit_cast(const From &src)
Convert data between types, without aliasing issues or undefined behaviour.
Definition helpers.h:66