ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
modbus_controller.h
Go to the documentation of this file.
1#pragma once
2
4
8
9#include <list>
10#include <queue>
11#include <set>
12#include <utility>
13#include <vector>
14
15namespace esphome {
16namespace modbus_controller {
17
18class ModbusController;
19
24
25// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
26ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
27inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }
28
29ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
30inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) {
32}
33
34ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
35inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) {
37}
38
39ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
40inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }
41
42ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
43inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
45}
46
47ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
48inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
50}
51
52ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
53inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
55}
56
57ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
58inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
60}
61
62template<typename T>
63ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
64T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
65 return modbus::helpers::get_data<T>(data, buffer_offset);
66}
67
68ESPDEPRECATED("Use modbus::helpers::coil_from_vector() instead. Removed in 2026.10.0", "2026.4.0")
69inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
71}
72
73template<typename N>
74ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
75N mask_and_shift_by_rightbit(N data, uint32_t mask) {
77}
78
79ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
80inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
81 modbus::helpers::number_to_payload(data, value, value_type);
82}
83
84ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
85inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
86 uint32_t bitmask) {
87 return modbus::helpers::payload_to_number(data, sensor_value_type, offset, bitmask);
88}
89
90ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
91inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
92 return modbus::helpers::float_to_payload(value, value_type);
93}
94
95class ModbusController;
96
98 public:
99 virtual void parse_and_publish(const std::vector<uint8_t> &data) = 0;
100
101 void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
102 size_t virtual get_register_size() const {
104 return 1;
105 } else { // if CONF_RESPONSE_BYTES is used override the default
107 }
108 }
109 // Override register size for modbus devices not using 1 register for one dword
110 void set_register_size(uint8_t register_size) { response_bytes = register_size; }
113 uint16_t start_address{0};
115 uint8_t offset{0};
116 uint8_t register_count{0};
117 uint8_t response_bytes{0};
118 uint16_t skip_updates{0};
119 std::vector<uint8_t> custom_data{};
120 bool force_new_range{false};
121};
122
124 bool enabled{false};
125 uint16_t register_last_address{0xFFFF};
126 uint16_t register_value{0};
127};
128
130 using ReadLambda = std::function<int64_t()>;
131 using WriteLambda = std::function<bool(int64_t value)>;
132
133 public:
135 this->address = address;
136 this->value_type = value_type;
137 this->register_count = register_count;
138 }
139
140 template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
141 this->read_lambda = [this, user_read_lambda]() -> int64_t {
142 T user_value = user_read_lambda(this->address);
143 if constexpr (std::is_same_v<T, float>) {
144 return bit_cast<uint32_t>(user_value);
145 } else {
146 return static_cast<int64_t>(user_value);
147 }
148 };
149 }
150
151 template<typename T>
152 void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
153 this->write_lambda = [this, user_write_lambda](int64_t number) {
154 if constexpr (std::is_same_v<T, float>) {
155 float float_value = bit_cast<float>(static_cast<uint32_t>(number));
156 return user_write_lambda(this->address, float_value);
157 }
158 return user_write_lambda(this->address, static_cast<T>(number));
159 };
160 }
161
162 // Formats a raw value into a string representation based on the value type for debugging
163 std::string format_value(int64_t value) const {
164 // max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
165 // plus null terminator = 43, rounded to 44 for 4-byte alignment
166 char buf[44];
167 switch (this->value_type) {
173 buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
174 return buf;
180 buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
181 return buf;
184 buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
185 return buf;
186 default:
187 buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
188 return buf;
189 }
190 }
191
192 uint16_t address{0};
194 uint8_t register_count{0};
195 ReadLambda read_lambda;
196 WriteLambda write_lambda;
197};
198
199// ModbusController::create_register_ranges_ tries to optimize register range
200// for this the sensors must be ordered by register_type, start_address and bitmask
202 public:
203 bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
204 // first sort according to register type
205 if (lhs->register_type != rhs->register_type) {
206 return lhs->register_type < rhs->register_type;
207 }
208
209 // ensure that sensor with force_new_range set are before the others
210 if (lhs->force_new_range != rhs->force_new_range) {
211 return lhs->force_new_range > rhs->force_new_range;
212 }
213
214 // sort by start address
215 if (lhs->start_address != rhs->start_address) {
216 return lhs->start_address < rhs->start_address;
217 }
218
219 // sort by offset (ensures update of sensors in ascending order)
220 if (lhs->offset != rhs->offset) {
221 return lhs->offset < rhs->offset;
222 }
223
224 // The pointer to the sensor is used last to ensure that
225 // multiple sensors with the same values can be added with a stable sort order.
226 return lhs < rhs;
227 }
228};
229
230using SensorSet = std::set<SensorItem *, SensorItemsComparator>;
231
236 uint16_t skip_updates; // the config value
237 SensorSet sensors; // all sensors of this range
238 uint16_t skip_updates_counter; // the running value
239};
240
242 public:
243 static const size_t MAX_PAYLOAD_BYTES = 240;
245 uint16_t register_address{0};
246 uint16_t register_count{0};
249 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
251 std::vector<uint8_t> payload = {};
252 bool send();
254 bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
255
257
268 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
269 &&handler);
279 uint16_t start_address, uint16_t register_count);
290 uint16_t register_count, const std::vector<uint16_t> &values);
300 uint16_t value);
309
318 const std::vector<bool> &values);
327 ModbusController *modbusdevice, const std::vector<uint8_t> &values,
328 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
329 &&handler = nullptr);
330
339 ModbusController *modbusdevice, const std::vector<uint16_t> &values,
340 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
341 &&handler = nullptr);
342
343 bool is_equal(const ModbusCommandItem &other);
344
345 protected:
346 // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
348 uint8_t send_count_{0};
349};
350
360 public:
361 void dump_config() override;
362 void loop() override;
363 void setup() override;
364 void update() override;
365
367 void queue_command(const ModbusCommandItem &command);
369 void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
371 void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
373 void on_modbus_data(const std::vector<uint8_t> &data) override;
375 void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
377 void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
379 void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
381 void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
384 void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
385 const std::vector<uint8_t> &data);
387 void set_allow_duplicate_commands(bool allow_duplicate_commands) {
388 this->allow_duplicate_commands_ = allow_duplicate_commands;
389 }
393 void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
395 void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
397 size_t get_command_queue_length() { return command_queue_.size(); }
401 template<typename F> void add_on_command_sent_callback(F &&callback) {
402 this->command_sent_callback_.add(std::forward<F>(callback));
403 }
405 template<typename F> void add_on_online_callback(F &&callback) {
406 this->online_callback_.add(std::forward<F>(callback));
407 }
409 template<typename F> void add_on_offline_callback(F &&callback) {
410 this->offline_callback_.add(std::forward<F>(callback));
411 }
413 void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
415 uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
417 void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
418 this->server_courtesy_response_ = server_courtesy_response;
419 }
422
423 protected:
426 // find register in sensormap. Returns iterator with all registers having the same start address
427 SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
431 void process_modbus_data_(const ModbusCommandItem *response);
433 bool send_next_command_();
435 void dump_sensors_();
439 std::vector<ServerRegister *> server_registers_{};
441 std::vector<RegisterRange> register_ranges_{};
443 std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
445 std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
451 uint16_t command_throttle_{0};
453 bool module_offline_{false};
466 .enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
467};
468
474inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
475 int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
476
477 float float_value;
479 float_value = bit_cast<float>(static_cast<uint32_t>(number));
480 } else {
481 float_value = static_cast<float>(number);
482 }
483
484 return float_value;
485}
486
487} // namespace modbus_controller
488} // namespace esphome
uint8_t address
Definition bl0906.h:4
This class simplifies creating components that periodically check a state.
Definition component.h:602
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
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
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 add_on_command_sent_callback(F &&callback)
Set callback for commands.
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 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 add_on_online_callback(F &&callback)
Set callback for online changes.
void add_on_offline_callback(F &&callback)
Set callback for offline changes.
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)
ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type)
ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type)
bool value_type_is_float(SensorValueType v)
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 .
std::vector< uint16_t > float_to_payload(float value, SensorValueType value_type)
T get_data(const std::vector< uint8_t > &data, size_t buffer_offset)
Extract data from modbus response buffer.
uint64_t qword_from_hex_str(const std::string &value, uint8_t pos)
Get a qword from a hex string.
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...
uint32_t dword_from_hex_str(const std::string &value, uint8_t pos)
Get a dword from a hex string.
uint8_t byte_from_hex_str(const std::string &value, uint8_t pos)
Get a byte from a hex string byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 byte_from_h...
uint16_t word_from_hex_str(const std::string &value, uint8_t pos)
Get a word from a hex string.
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.
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.
ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0") inline bool value_type_is_float(SensorValueType v)
std::set< SensorItem *, SensorItemsComparator > SensorSet
float payload_to_float(const std::vector< uint8_t > &data, const SensorItem &item)
Convert vector<uint8_t> response payload to float.
const std::vector< uint8_t > & data
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
size_t size_t pos
Definition helpers.h:1082
To bit_cast(const From &src)
Convert data between types, without aliasing issues or undefined behaviour.
Definition helpers.h:77
static void uint32_t