ESPHome 2026.9.1
Loading...
Searching...
No Matches
modbus_client.h
Go to the documentation of this file.
1#pragma once
2
6
7#include <span>
8#include <vector>
9
11
18template<typename... Ts> class ClientActionBase : public Action<Ts...>, public modbus::ModbusClientDevice {
19 public:
20 TEMPLATABLE_VALUE(uint8_t, target_address) // the modbus device address
21
22 Trigger<std::span<const uint8_t>> *get_sent_trigger() { return &this->sent_trigger_; }
26
30 using retry_func_t = bool (*)(std::span<const uint8_t>);
31 void set_retry(retry_func_t f) { this->retry_func_ = f; }
32
35 void on_sent(std::span<const uint8_t> request_pdu) override { this->sent_trigger_.trigger(request_pdu); }
41 void on_not_sent(std::span<const uint8_t> request_pdu) override { this->not_sent_trigger_.trigger(request_pdu); }
45 void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override {
46 this->error_trigger_.trigger(request_pdu, exception_code);
47 }
51 bool on_no_response(std::span<const uint8_t> request_pdu) override {
52 this->no_response_trigger_.trigger(request_pdu);
53 if (this->retry_func_ != nullptr)
54 return this->retry_func_(request_pdu);
55 return false;
56 }
59 void play_complex(const Ts &...x) override {
60 this->set_address(this->target_address_.value(x...));
62 }
63
64 protected:
71 void send_or_resolve_(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
72 if (!this->queue_pdu(pdu, options))
73 this->on_not_sent(pdu);
74 }
75
81};
82
89template<typename... Ts> class ReadCommandOptions {
90 public:
91 // Poll: re-queue after each success until downgraded (replay with false) or failed. The hub strips
92 // it for mutating function codes at the door (see modbus::CommandOptions).
93 TEMPLATABLE_VALUE(bool, continuous)
94 TEMPLATABLE_VALUE(bool, allow_broadcast_read)
95
96 protected:
99 return {.continuous = this->continuous_.value(x...),
100 .allow_broadcast_read = this->allow_broadcast_read_.value(x...)};
101 }
102};
103
105template<typename... Ts> class WriteCommandOptions {
106 public:
107 TEMPLATABLE_VALUE(bool, expect_broadcast_write_response)
108
109 protected:
111 void apply_write_command_options_(modbus::CommandOptions &options, const Ts &...x) const {
112 options.expect_broadcast_write_response = this->expect_broadcast_write_response_.value(x...);
113 }
114 modbus::CommandOptions write_command_options_(const Ts &...x) const {
116 this->apply_write_command_options_(options, x...);
117 return options;
118 }
119};
120
129template<typename... Ts>
131 public ReadCommandOptions<Ts...>,
132 public WriteCommandOptions<Ts...> {
133 public:
135
136 Trigger<std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
137 return &this->response_trigger_;
138 }
139
140 void play(const Ts &...x) override {
142 this->apply_write_command_options_(options, x...);
143 this->send_or_resolve_(this->pdu_.value(x...), options);
144 }
145
146 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
147 this->response_trigger_.trigger(request_pdu, response_pdu);
148 }
149
150 protected:
152};
153
164template<typename... Ts> class TypedClientActionBase : public ClientActionBase<Ts...> {
165 public:
167 return &this->custom_response_trigger_;
168 }
172
173 void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
174 modbus::ResponseStatus status) override {
175 if (!this->custom_response_handled_) {
176 modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
177 return;
178 }
179 this->custom_response_trigger_.trigger(request_pdu, response_pdu);
180 }
181
182 protected:
185};
186
189template<typename... Ts>
191 public:
192 explicit ReadRegistersAction(bool holding) : holding_(holding) {}
194 TEMPLATABLE_VALUE(uint16_t, count)
195
196 Trigger<std::span<const uint16_t>> *get_response_trigger() { return &this->response_trigger_; }
197
198 void play(const Ts &...x) override {
199 const auto function_code =
201 this->send_or_resolve_(
202 modbus::helpers::create_read_pdu(function_code, this->start_address_.value(x...), this->count_.value(x...)),
203 this->command_options_(x...));
204 }
205 void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
206 modbus::ResponseStatus status) override {
208 this->response_trigger_.trigger(registers);
209 }
210
211 protected:
214};
215
218template<typename... Ts> class ReadBitsAction : public TypedClientActionBase<Ts...>, public ReadCommandOptions<Ts...> {
219 public:
220 explicit ReadBitsAction(bool coils) : coils_(coils) {}
222 TEMPLATABLE_VALUE(uint16_t, count)
223
224 Trigger<modbus::PackedBits> *get_response_trigger() { return &this->response_trigger_; }
225
226 void play(const Ts &...x) override {
227 const auto function_code =
229 this->send_or_resolve_(
230 modbus::helpers::create_read_pdu(function_code, this->start_address_.value(x...), this->count_.value(x...)),
231 this->command_options_(x...));
232 }
234 modbus::ResponseStatus status) override {
236 this->response_trigger_.trigger(bits);
237 }
238
239 protected:
241 bool coils_;
242};
243
246template<typename... Ts>
248 public:
250 TEMPLATABLE_VALUE(uint16_t, value)
251
253
254 void play(const Ts &...x) override {
255 this->send_or_resolve_(
256 modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)),
257 this->write_command_options_(x...));
258 }
259 void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override {
262 }
263
264 protected:
266};
267
270template<typename... Ts>
272 public:
274 TEMPLATABLE_VALUE(bool, value)
275
277
278 void play(const Ts &...x) override {
279 this->send_or_resolve_(
280 modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)),
281 this->write_command_options_(x...));
282 }
283 void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override {
286 }
287
288 protected:
290};
291
296template<typename... Ts>
298 public:
300
301
302 void set_values_static(const uint16_t *values, size_t len) {
303 this->values_.data = values;
304 this->len_ = static_cast<ssize_t>(len);
305 }
308 void set_values_template(std::vector<uint16_t> (*func)(Ts...)) {
309 this->values_.func = func;
310 this->len_ = -1; // sentinel: template mode
311 }
312
314
315 void play(const Ts &...x) override {
316 const uint16_t start = this->start_address_.value(x...);
317 // An empty or over-long set rejects into an empty PDU inside the builder, which logs the reason;
318 // the empty PDU then resolves via on_not_sent like any refused send.
319 if (this->len_ >= 0) {
321 start, std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))),
322 this->write_command_options_(x...));
323 return;
324 }
325 const std::vector<uint16_t> values = this->values_.func(x...);
326 this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values)),
327 this->write_command_options_(x...));
328 }
329 void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
330 modbus::ResponseStatus status) override {
333 }
334
335 protected:
337 ssize_t len_{-1}; // -1 = template mode, >= 0 = static mode with this many registers
338 union Values {
339 std::vector<uint16_t> (*func)(Ts...);
340 const uint16_t *data;
342};
343
348template<typename... Ts>
350 public:
352
353
354 void set_values_static(const uint8_t *packed, size_t count) {
355 this->values_.packed = packed;
356 this->count_ = static_cast<ssize_t>(count);
357 }
359 void set_values_template(std::vector<bool> (*func)(Ts...)) {
360 this->values_.func = func;
361 this->count_ = -1; // sentinel: template mode
362 }
363
365
366 void play(const Ts &...x) override {
367 const uint16_t start = this->start_address_.value(x...);
368 if (this->count_ >= 0) {
369 const auto count = static_cast<uint16_t>(this->count_);
370 this->send_or_resolve_(
372 start, modbus::PackedBits(std::span<const uint8_t>(this->values_.packed, modbus::packed_bit_bytes(count)),
373 count)),
374 this->write_command_options_(x...));
375 return;
376 }
377 // The builder packs and bound-checks; an over-long set is rejected and logged there.
379 this->write_command_options_(x...));
380 }
386
387 protected:
389 ssize_t count_{-1}; // -1 = template mode, >= 0 = static mode with this many coils
390 union Values {
391 std::vector<bool> (*func)(Ts...);
392 const uint8_t *packed;
394};
395
398template<typename... Ts>
400 public:
401 TEMPLATABLE_VALUE(uint16_t, read_address)
402 TEMPLATABLE_VALUE(uint16_t, read_count)
403 TEMPLATABLE_VALUE(uint16_t, write_address)
404
406 void set_values_static(const uint16_t *values, size_t len) {
407 this->values_.data = values;
408 this->len_ = static_cast<ssize_t>(len);
409 }
411 void set_values_template(std::vector<uint16_t> (*func)(Ts...)) {
412 this->values_.func = func;
413 this->len_ = -1; // sentinel: template mode
414 }
415
417
418 void play(const Ts &...x) override {
419 const uint16_t read_start = this->read_address_.value(x...);
420 const uint16_t read_count = this->read_count_.value(x...);
421 const uint16_t write_start = this->write_address_.value(x...);
422 // An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent.
423 if (this->len_ >= 0) {
425 read_start, read_count, write_start,
426 std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))),
427 this->command_options_(x...));
428 return;
429 }
430 const std::vector<uint16_t> values = this->values_.func(x...);
432 read_start, read_count, write_start, std::span<const uint16_t>(values)),
433 this->command_options_(x...));
434 }
435 // The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read.
436 void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
437 modbus::ResponseStatus status) override {
439 this->response_trigger_.trigger(registers);
440 }
441
442 protected:
444 ssize_t len_{-1}; // -1 = template mode, >= 0 = static mode with this many write registers
445 union Values {
446 std::vector<uint16_t> (*func)(Ts...);
447 const uint16_t *data;
449};
450
451} // namespace esphome::modbus_client
uint8_t address
Definition bl0906.h:4
uint8_t status
Definition bl0942.h:8
virtual void play_complex(const Ts &...x)
Definition automation.h:489
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:232
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:461
virtual void on_custom_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu, ResponseStatus status)
Catch-all for custom function codes and anything that is not a standard-conformant transaction (see d...
Definition modbus.cpp:1393
void set_address(uint8_t address)
Definition modbus.h:416
Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first),...
Shared base for the modbus_client actions.
Trigger< std::span< const uint8_t > > sent_trigger_
void on_sent(std::span< const uint8_t > request_pdu) override
The frame was written to the wire: fires once per transmission, before any reply, and never for a sen...
Trigger< std::span< const uint8_t >, modbus::ExceptionCode > * get_error_trigger()
Trigger< std::span< const uint8_t > > not_sent_trigger_
void play_complex(const Ts &...x) override
Stamp the templated device address before every play(): subclasses cannot forget it,...
TEMPLATABLE_VALUE(uint8_t, target_address) Trigger< std
void on_not_sent(std::span< const uint8_t > request_pdu) override
Never reached the wire, from either of two sources.
bool(*)(std::span< const uint8_t >) retry_func_t
The retry decision for on_no_response: given the request PDU, return true to have the hub re-queue th...
void send_or_resolve_(std::span< const uint8_t > pdu, modbus::CommandOptions options={})
The hub refuses some sends at the door with no callback (a duplicate write already pending,...
bool on_no_response(std::span< const uint8_t > request_pdu) override
No reply within send_wait_time.
Trigger< std::span< const uint8_t > > * get_no_response_trigger()
Trigger< std::span< const uint8_t >, modbus::ExceptionCode > error_trigger_
Trigger< std::span< const uint8_t > > * get_not_sent_trigger()
Trigger< std::span< const uint8_t > > no_response_trigger_
void on_error(std::span< const uint8_t > request_pdu, modbus::ExceptionCode exception_code) override
A Modbus exception reply.
modbus_client.send: fire a raw PDU (function code + data; the hub adds address and CRC).
Trigger< std::span< const uint8_t >, std::span< const uint8_t > > response_trigger_
void on_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu) override
TEMPLATABLE_VALUE(modbus::helpers::PduBuffer, pdu) Trigger< std
modbus_client.read_coils / read_discrete_inputs: on_response delivers the bits as a PackedBits view (...
TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(uint16_t
void play(const Ts &...x) override
Trigger< modbus::PackedBits > response_trigger_
void on_read_bits(modbus::EntityType entity_type, uint16_t start_address, modbus::PackedBits bits, modbus::ResponseStatus status) override
count Trigger< modbus::PackedBits > * get_response_trigger()
The read-side per-command options (modbus::CommandOptions), declared once for every action that sends...
modbus::CommandOptions command_options_(const Ts &...x) const
The options for this send, with every templatable value resolved against the action's arguments.
TEMPLATABLE_VALUE(bool, continuous) TEMPLATABLE_VALUE(bool
modbus_client.read_holding_registers / read_input_registers: on_response delivers the registers in ho...
count Trigger< std::span< const uint16_t > > * get_response_trigger()
void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span< const uint16_t > registers, modbus::ResponseStatus status) override
TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(uint16_t
Trigger< std::span< const uint16_t > > response_trigger_
modbus_client.read_write_multiple_registers (FC 0x17): writes one register block and reads another ba...
void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span< const uint16_t > registers, modbus::ResponseStatus status) override
Trigger< std::span< const uint16_t > > response_trigger_
Trigger< std::span< const uint16_t > > * get_response_trigger()
TEMPLATABLE_VALUE(uint16_t, read_address) TEMPLATABLE_VALUE(uint16_t
void set_values_template(std::vector< uint16_t >(*func)(Ts...))
Lambda config: the write registers are only known at play() time.
union esphome::modbus_client::ReadWriteMultipleRegistersAction::Values values_
Typed actions: these do NOT override the raw on_response, so the base ModbusClientDevice default runs...
void on_custom_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu, modbus::ResponseStatus status) override
Trigger< std::span< const uint8_t >, std::span< const uint8_t > > custom_response_trigger_
Trigger< std::span< const uint8_t >, std::span< const uint8_t > > * get_custom_response_trigger()
void set_custom_response_handled()
Set by codegen when the config declares on_custom_response.
The write-side per-command options (command_options_schema(direction="write") adds exactly these keys...
modbus_client.write_multiple_coils: on_response is the acknowledgement (no arguments).
union esphome::modbus_client::WriteMultipleCoilsAction::Values values_
TEMPLATABLE_VALUE(uint16_t, start_address) void set_values_static(const uint8_t *packed
Static config: packed is the wire layout (LSB first) held in flash, count the number of coils.
void on_write_multiple_coils(uint16_t start_address, modbus::PackedBits bits, modbus::ResponseStatus status) override
void set_values_template(std::vector< bool >(*func)(Ts...))
Lambda config: the coils are only known at play() time.
modbus_client.write_multiple_registers: on_response is the acknowledgement (no arguments).
TEMPLATABLE_VALUE(uint16_t, start_address) void set_values_static(const uint16_t *values
Static config: the registers live in flash, so play() neither allocates nor copies.
void set_values_template(std::vector< uint16_t >(*func)(Ts...))
Lambda config: the registers are only known at play() time.
union esphome::modbus_client::WriteMultipleRegistersAction::Values values_
void on_write_multiple_registers(uint16_t start_address, std::span< const uint16_t > registers, modbus::ResponseStatus status) override
modbus_client.write_single_coil: on_response is the acknowledgement (no arguments).
void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override
TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(bool
modbus_client.write_single_register: on_response is the acknowledgement (the ack only echoes the requ...
TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(uint16_t
void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override
uint8_t options
__int64 ssize_t
Definition httplib.h:178
PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits)
Create modbus write multiple coils command (function 0x0F) from bits packed as on the wire.
WriteSinglePdu create_write_single_coil_pdu(uint16_t address, bool value)
Create modbus write single coil command Function 0x05 Write Single Coil.
ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities)
Create a modbus read request PDU.
WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value)
Create modbus write single register command Function 0x06 Write Single Register.
PduBuffer create_write_registers_pdu(uint16_t start_address, std::span< const uint16_t > values)
Create modbus write multiple registers command Function 0x10 Write Multiple Registers.
PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address, std::span< const uint16_t > write_values)
Create modbus read/write multiple registers command Function 0x17 Read/Write Multiple Registers Write...
std::optional< ExceptionCode > ResponseStatus
Definition modbus.h:315
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:318
constexpr size_t packed_bit_bytes(size_t bits)
Bits pack 8 per data byte, rounded up to whole bytes.
STL namespace.
uint16_t x
Definition tt21100.cpp:5