ESPHome 2026.8.0b4
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 <set>
11#include <span>
12#include <utility>
13#include <vector>
14
16
17class ModbusController;
18
23
24// Remove before 2027.2.0 - deprecated names re-exported so external components keep their warning window
25#pragma GCC diagnostic push
26#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
27using modbus::ModbusExceptionCode;
28using modbus::ModbusFunctionCode;
29using modbus::ModbusRegisterType;
30#pragma GCC diagnostic pop
31
32// Remove before 2026.10.0 — these helpers have moved to modbus::helpers
33ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0")
34inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); }
35
36ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0")
37inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) {
39}
40
41ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0")
42inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) {
44}
45
46ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0")
47inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); }
48
49ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
50inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
52}
53
54ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
55inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
57}
58
59ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
60inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
62}
63
64ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0")
65inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
67}
68
69template<typename T>
70ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0")
71T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
72 return modbus::helpers::get_data<T>(data, buffer_offset);
73}
74
75// Span overloads of the deprecated helpers below: read lambdas receive their payload as a
76// std::span<const uint8_t> (previously a const std::vector<uint8_t> &), and a span does not convert to
77// a vector, so existing lambdas calling these by name need an overload that accepts one. These carry
78// this release's deprecation window, since the span forms only exist from it.
79// payload_to_number() deliberately has no such overload: one of its arguments is a modbus::helpers
80// type, so a span call already reaches the helper by argument-dependent lookup, and a forwarder here
81// would only make that call ambiguous.
82// Remove before 2027.2.0.
83template<typename T>
84ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2027.2.0", "2026.8.0")
85T get_data(std::span<const uint8_t> data, size_t buffer_offset) {
86 return modbus::helpers::get_data<T>(data.data(), buffer_offset);
87}
88
89// Remove before 2027.2.0 (window restarted when the migration target changed to bit_from_packed())
90ESPDEPRECATED("Use modbus::helpers::bit_from_packed() instead. Removed in 2027.2.0", "2026.4.0")
91inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
93}
94
95// Remove before 2027.2.0
96ESPDEPRECATED("Use modbus::helpers::bit_from_packed() instead. Removed in 2027.2.0", "2026.8.0")
97inline bool coil_from_vector(int coil, std::span<const uint8_t> data) {
99}
100
101template<typename N>
102ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0")
103N mask_and_shift_by_rightbit(N data, uint32_t mask) {
105}
106
107ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
108inline void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
109 modbus::helpers::number_to_payload(data, value, value_type);
110}
111
112ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0")
113inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
114 uint32_t bitmask) {
115 return modbus::helpers::payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask)
116 .value_or(0);
117}
118
119ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0")
120inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
121 std::vector<uint16_t> data;
122 modbus::helpers::float_to_payload(data, value, value_type);
123 return data;
124}
125
126class ModbusController;
127
129 public:
133 virtual void parse_and_publish(std::span<const uint8_t> data) = 0;
134
137
140 uint16_t write_address() const {
141 return this->range_start_address + (this->addresses_bits() ? this->offset : this->offset / 2);
142 }
143
149 this->offset = offset;
150 }
151
155 void set_address(uint16_t address) {
156 this->start_address = address;
158 }
159
160 void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
161 size_t virtual get_register_size() const {
162 if (this->addresses_bits()) {
163 return 1;
164 } else { // if CONF_RESPONSE_BYTES is used override the default
166 }
167 }
168 // Override register size for modbus devices not using 1 register for one dword
169 void set_register_size(uint8_t register_size) { response_bytes = register_size; }
172 uint16_t start_address{0};
178 uint8_t offset{0};
179 uint8_t register_count{0};
180 uint8_t response_bytes{0};
189 uint16_t skip_updates{0};
190 std::vector<uint8_t> custom_data{};
191 bool force_new_range{false};
192};
193
194// ModbusController::create_polling_commands_ tries to optimize register range
195// for this the sensors must be ordered by register_type, start_address and bitmask
197 public:
198 bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
199 // first sort according to register type
200 if (lhs->register_type != rhs->register_type) {
201 return lhs->register_type < rhs->register_type;
202 }
203
204 // ensure that sensor with force_new_range set are before the others
205 if (lhs->force_new_range != rhs->force_new_range) {
206 return lhs->force_new_range > rhs->force_new_range;
207 }
208
209 // sort by start address
210 if (lhs->start_address != rhs->start_address) {
211 return lhs->start_address < rhs->start_address;
212 }
213
214 // sort by the offset as configured (ensures update of sensors in ascending order). The resolved
215 // `offset` is deliberately not used: ranges are built while iterating this set and assign it, and
216 // a sort key that changed under the iteration would corrupt the set's ordering.
219 }
220
221 // The pointer to the sensor is used last to ensure that
222 // multiple sensors with the same values can be added with a stable sort order.
223 return lhs < rhs;
224 }
225};
226
227using SensorSet = std::set<SensorItem *, SensorItemsComparator>;
228
233 uint16_t skip_updates; // the config value
234 SensorSet sensors; // all sensors of this range
235};
236
241 public:
244 : modbus::ModbusClientDevice(parent, address), controller_(&controller) {}
250 ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address, SensorItem *sensor);
251
252 // The base deletes copy/move (its destructor unregisters the device from the hub queue), but command
253 // items are stored in value containers, so copy/move CONSTRUCTION is re-provided (copy only for the
254 // queue_command() path). Assignment stays deleted: the item's address-in-memory is its hub identity.
256 ModbusCommandItem(ModbusCommandItem &&other) noexcept;
258
259 SensorSet sensors; // sensors served by this command (empty for factory/write commands)
260 uint16_t skip_updates{0};
261 std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> on_data_func;
268 // Set by unqueue_command() when this one-shot has completed. The controller erases flagged items at a
269 // safe point (update()/queue_command()), never from inside the command's own callback.
270 bool pending_removal{false};
271
273 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
275 void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override;
277 void on_not_sent(std::span<const uint8_t> request_pdu) override;
279 void on_sent(std::span<const uint8_t> request_pdu) override;
281 bool on_no_response(std::span<const uint8_t> request_pdu) override;
282
283 uint16_t register_address() const { return this->start_address_; }
284 uint16_t register_count() const { return this->register_count_; }
285 EntityType register_type() const { return this->register_type_; }
286
290 bool send();
291
293
303 ModbusController *modbusdevice, EntityType register_type, uint16_t start_address, uint16_t register_count,
304 std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> &&handler);
315 uint16_t register_count, const std::vector<uint16_t> &values);
325 uint16_t value);
333 static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value);
334
343 const std::vector<bool> &values);
352 ModbusController *modbusdevice, const std::vector<uint8_t> &values,
353 std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> &&handler =
354 nullptr);
355
364 ModbusController *modbusdevice, const std::vector<uint16_t> &values,
365 std::function<void(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data)> &&handler =
366 nullptr);
367
368 protected:
370 uint16_t register_count) {
371 this->function_code_ = function_code;
375 }
377 uint16_t start_address_{0};
378 uint16_t register_count_{0};
381 const std::vector<uint8_t> *custom_data_{nullptr};
383};
384
388inline bool offline_retry_due(uint16_t update_counter, uint16_t module_offline_at, uint16_t offline_skip_updates) {
389 return static_cast<uint16_t>(update_counter + 1 - module_offline_at) % (offline_skip_updates + 1) == 0;
390}
391
400class ModbusController final : public PollingComponent {
401 public:
402 void dump_config() override;
403 // No loop() override: the hub owns transmit/receive timing and each command routes its own
404 // response, so the controller never joins the looping components at all.
405 void setup() override;
406 void update() override;
407
408 // The controller is not itself a modbus device - its commands and writer entities send as their own
409 // devices. It only owns the hub + address so those senders can be built against them.
411 void set_address(uint8_t address) { this->address_ = address; }
412
415 modbus::ModbusClientHub *hub() const { return this->hub_; }
416 uint8_t device_address() const { return this->address_; }
417
419 void queue_command(ModbusCommandItem command);
422 void unqueue_command(const ModbusCommandItem *command);
424 void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
426 void on_write_register_response(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data);
428 void set_online(bool online, int function_code, int register_address);
430 void command_sent(int function_code, int register_address) {
431 this->command_sent_callback_.call(function_code, register_address);
432 }
437 bool can_send() { return this->cmd_non_responses_ <= this->max_cmd_retries_; }
439 void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
443 template<typename F> void add_on_command_sent_callback(F &&callback) {
444 this->command_sent_callback_.add(std::forward<F>(callback));
445 }
447 template<typename F> void add_on_online_callback(F &&callback) {
448 this->online_callback_.add(std::forward<F>(callback));
449 }
451 template<typename F> void add_on_offline_callback(F &&callback) {
452 this->offline_callback_.add(std::forward<F>(callback));
453 }
455 void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
457 uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
458
459 protected:
465 // A custom range polls the first sensor's custom_data (a ready-made raw frame); it needs the
466 // sensor constructor so the command references those bytes and decodes the real function code.
467 // The response still dispatches to every sensor in the range.
468 if (range.register_type == EntityType::CUSTOM && !range.sensors.empty()) {
469 auto &cmd = this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, *range.sensors.begin());
470 cmd.sensors = std::move(range.sensors);
471 cmd.skip_updates = range.skip_updates; // the range's merged rate, not the first sensor's
472 } else {
473 this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, std::move(range));
474 }
475 }
480 uint8_t address_{0};
487 std::vector<ModbusCommandItem> polling_command_items_{};
489 std::list<std::unique_ptr<ModbusCommandItem>> one_shot_command_items_;
496 bool module_offline_{false};
500 uint16_t update_counter_{0};
513};
514
520inline float payload_to_float(std::span<const uint8_t> data, const SensorItem &item, uint8_t offset) {
521 int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, offset, item.bitmask).value_or(0);
522
523 float float_value;
525 float_value = bit_cast<float>(static_cast<uint32_t>(number));
526 } else {
527 float_value = static_cast<float>(number);
528 }
529
530 return float_value;
531}
532
533// Remove before 2027.2.0 (window opened when this helper gained an explicit offset). item.offset is
534// the item's resolved position within its range's response, so this decodes the same bytes as passing
535// that offset explicitly.
536ESPDEPRECATED("Pass the offset explicitly: payload_to_float(data, item, item.offset). Removed in 2027.2.0", "2026.8.0")
537inline float payload_to_float(std::span<const uint8_t> data, const SensorItem &item) {
538 return payload_to_float(data, item, item.offset);
539}
540
541} // namespace esphome::modbus_controller
uint8_t address
Definition bl0906.h:4
This class simplifies creating components that periodically check a state.
Definition component.h:510
Small buffer optimization - stores data inline when small, heap-allocates for large data This avoids ...
Definition helpers.h:147
const std::vector< uint8_t > * custom_data_
Custom polling commands reference the PDU bytes owned by their SensorItem instead of copying them.
static ModbusCommandItem create_read_command(ModbusController *modbusdevice, EntityType register_type, uint16_t start_address, uint16_t register_count, std::function< void(EntityType register_type, uint16_t start_address, std::span< const uint8_t > data)> &&handler)
factory methods
SmallInlineBuffer< 8 > payload
Write data bytes for the command (register/coil values), or the raw frame of a one-shot custom comman...
void on_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu) override
called when a modbus response was parsed without errors
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.
void set_command_(FunctionCode function_code, EntityType register_type, uint16_t start_address, uint16_t register_count)
bool on_no_response(std::span< const uint8_t > request_pdu) override
called on timeout; returns true to have the hub re-queue the frame for a retry
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.
bool send()
Queue this command's frame on the hub.
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_custom_command(ModbusController *modbusdevice, const std::vector< uint8_t > &values, std::function< void(EntityType register_type, uint16_t start_address, std::span< const uint8_t > data)> &&handler=nullptr)
Create custom modbus command.
ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address)
Empty command with no controller connection (kept for source compatibility with value-type usage).
void on_error(std::span< const uint8_t > request_pdu, modbus::ExceptionCode exception_code) override
called when a modbus error (exception) response was received
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(EntityType register_type, uint16_t start_address, std::span< const uint8_t > data)> on_data_func
void on_not_sent(std::span< const uint8_t > request_pdu) override
called when the command could not be sent
void on_sent(std::span< const uint8_t > request_pdu) override
called when the command's frame is actually written to the wire; fires the on_command_sent trigger
ModbusCommandItem & operator=(ModbusCommandItem &&)=delete
void add_sensor_item(SensorItem *item)
Registers a sensor with the controller. Called by esphomes code generator.
uint8_t cmd_non_responses_
consecutive non-responses; drives can_send() and offline detection
void create_polling_command_(RegisterRange &&range)
build one persistent polling command from a range and add it to polling_command_items_
void command_sent(int function_code, int register_address)
Fire the on_command_sent trigger (called when a command's frame reaches the wire).
void set_offline_skip_updates(uint16_t offline_skip_updates)
called by esphome generated code to set the offline_skip_updates
std::list< std::unique_ptr< ModbusCommandItem > > one_shot_command_items_
Dynamically queued one-shot commands (writes, custom commands). std::list keeps stable addresses.
void queue_command(ModbusCommandItem command)
Queues a one-shot modbus command (writes, custom commands); taken by value, so std::move to avoid a c...
void update_range_(ModbusCommandItem &cmd)
send a range's polling command if it is due this update
CallbackManager< void(int, int)> command_sent_callback_
Command sent callback.
void increment_non_response_count()
A command timed out; bump the consecutive-timeout counter used by can_send()/offline detection.
void set_parent(modbus::ModbusClientHub *hub)
CallbackManager< void(int, int)> offline_callback_
Server offline callback.
void add_on_command_sent_callback(F &&callback)
Set callback for commands.
SensorSet sensorset_
Collection of all sensors for this component.
void unqueue_command(const ModbusCommandItem *command)
Flags a finished one-shot command for removal.
uint8_t max_cmd_retries_
How many times we will retry a command if we get no response.
bool can_send()
Whether more retries are allowed before the device is considered offline.
void sweep_completed_one_shots_()
Erases one-shot commands flagged by unqueue_command().
uint8_t get_max_cmd_retries()
get how many times a command will be (re)sent if no response is received
void create_polling_commands_()
parse sensormap_ and create range of sequential addresses Group the registered sensors into contiguou...
uint16_t module_offline_at_
update_counter_ value at which the module went offline (for offline_skip_updates timing)
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
uint16_t offline_skip_updates_
how many updates to skip if module is offline
modbus::ModbusClientHub * hub() const
The hub and modbus address this controller talks to.
bool get_module_offline()
get if the module is offline, didn't respond the last command
void set_online(bool online, int function_code, int register_address)
Update the online/offline state after a response or a run of timeouts, firing the callbacks.
uint16_t update_counter_
counts update() cycles; drives skip_updates and offline timing
void add_on_online_callback(F &&callback)
Set callback for online changes.
void add_on_offline_callback(F &&callback)
Set callback for offline changes.
std::vector< ModbusCommandItem > polling_command_items_
One persistent command per register range, each its own ModbusClientDevice.
void on_write_register_response(EntityType register_type, uint16_t start_address, std::span< const uint8_t > data)
Handles a write command acknowledgement (used by write command on_data_func handlers).
modbus::ModbusClientHub * hub_
The hub this controller's commands/entities send through, and the modbus address they target.
CallbackManager< void(int, int)> online_callback_
Server online callback.
virtual void parse_and_publish(std::span< const uint8_t > data)=0
Parse this sensor's slice out of its range's response and publish it.
void set_custom_data(const std::vector< uint8_t > &data)
void set_address(uint16_t address)
Sets the configured address, and points the range base at it.
void set_offset_from_start_address(uint8_t offset)
Records the offset as configured, and seeds the resolved position with it.
void set_register_size(uint8_t register_size)
uint16_t range_start_address
First register of the range this sensor is polled in; equals start_address for an unpolled item.
uint8_t offset
Position of this sensor's data within its range's response - a byte offset for registers,...
uint8_t offset_from_start_address
The offset exactly as configured: measured from this sensor's own start_address, where offset is meas...
bool addresses_bits() const
Coils and discrete inputs address individual bits; every other type addresses 16-bit registers.
uint16_t write_address() const
Address a write entity (switch/number/select) targets, derived from its resolved position within the ...
bool operator()(const SensorItem *lhs, const SensorItem *rhs) const
Range range
Definition msa3xx.h:0
bool value_type_is_float(SensorValueType v)
void number_to_payload(Container &data, int64_t value, SensorValueType value_type)
Append the Modbus register words for value to data.
void float_to_payload(Container &data, float value, SensorValueType value_type)
Append a float converted to register words to any push_back container (heap-free with StaticVector).
T get_data(const uint8_t *data, size_t buffer_offset)
Extract data from modbus response buffer.
FunctionCode modbus_register_write_function(EntityType reg_type, bool multiple=false)
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.
FunctionCode modbus_register_read_function(EntityType reg_type)
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.
bool bit_from_packed(int bit, std::span< const uint8_t > data)
Extract coil data from modbus response buffer Responses for coil are packed into bytes .
bool is_entity_type_binary(EntityType type)
Coils and discrete inputs are the bit-addressed entity tables; the other types are 16-bit registers.
std::optional< int64_t > payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask)
Convert a raw response payload to a number.
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)
const std::vector< uint8_t > & data
std::set< SensorItem *, SensorItemsComparator > SensorSet
float payload_to_float(std::span< const uint8_t > data, const SensorItem &item, uint8_t offset)
Convert vector<uint8_t> response payload to float.
bool offline_retry_due(uint16_t update_counter, uint16_t module_offline_at, uint16_t offline_skip_updates)
Whether an offline probe is due this update cycle: every offline_skip_updates + 1 cycles,...
size_t size_t pos
Definition helpers.h:1062
To bit_cast(const From &src)
Convert data between types, without aliasing issues or undefined behaviour.
Definition helpers.h:84
STL namespace.
static void uint32_t