ESPHome 2026.8.0b4
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) {
72 if (!this->queue_pdu(pdu))
73 this->on_not_sent(pdu);
74 }
75
81};
82
90template<typename... Ts> class ModbusClientSendAction : public ClientActionBase<Ts...> {
91 public:
93
94 Trigger<std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
95 return &this->response_trigger_;
96 }
97
98 void play(const Ts &...x) override { this->send_or_resolve_(this->pdu_.value(x...)); }
99
100 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
101 this->response_trigger_.trigger(request_pdu, response_pdu);
102 }
103
104 protected:
106};
107
118template<typename... Ts> class TypedClientActionBase : public ClientActionBase<Ts...> {
119 public:
121 return &this->custom_response_trigger_;
122 }
126
127 void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
128 modbus::ResponseStatus status) override {
129 if (!this->custom_response_handled_) {
130 modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
131 return;
132 }
133 this->custom_response_trigger_.trigger(request_pdu, response_pdu);
134 }
135
136 protected:
139};
140
143template<typename... Ts> class ReadRegistersAction : public TypedClientActionBase<Ts...> {
144 public:
145 explicit ReadRegistersAction(bool holding) : holding_(holding) {}
147 TEMPLATABLE_VALUE(uint16_t, count)
148
149 Trigger<std::span<const uint16_t>> *get_response_trigger() { return &this->response_trigger_; }
150
151 void play(const Ts &...x) override {
152 const auto function_code =
154 this->send_or_resolve_(
155 modbus::helpers::create_read_pdu(function_code, this->start_address_.value(x...), this->count_.value(x...)));
156 }
157 void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
158 modbus::ResponseStatus status) override {
160 this->response_trigger_.trigger(registers);
161 }
162
163 protected:
166};
167
170template<typename... Ts> class ReadBitsAction : public TypedClientActionBase<Ts...> {
171 public:
172 explicit ReadBitsAction(bool coils) : coils_(coils) {}
174 TEMPLATABLE_VALUE(uint16_t, count)
175
176 Trigger<modbus::PackedBits> *get_response_trigger() { return &this->response_trigger_; }
177
178 void play(const Ts &...x) override {
179 const auto function_code =
181 this->send_or_resolve_(
182 modbus::helpers::create_read_pdu(function_code, this->start_address_.value(x...), this->count_.value(x...)));
183 }
185 modbus::ResponseStatus status) override {
187 this->response_trigger_.trigger(bits);
188 }
189
190 protected:
192 bool coils_;
193};
194
197template<typename... Ts> class WriteSingleRegisterAction : public TypedClientActionBase<Ts...> {
198 public:
200 TEMPLATABLE_VALUE(uint16_t, value)
201
203
204 void play(const Ts &...x) override {
205 this->send_or_resolve_(
206 modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)));
207 }
208 void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override {
211 }
212
213 protected:
215};
216
219template<typename... Ts> class WriteSingleCoilAction : public TypedClientActionBase<Ts...> {
220 public:
222 TEMPLATABLE_VALUE(bool, value)
223
225
226 void play(const Ts &...x) override {
227 this->send_or_resolve_(
228 modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)));
229 }
230 void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override {
233 }
234
235 protected:
237};
238
243template<typename... Ts> class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...> {
244 public:
246
247
248 void set_values_static(const uint16_t *values, size_t len) {
249 this->values_.data = values;
250 this->len_ = static_cast<ssize_t>(len);
251 }
254 void set_values_template(std::vector<uint16_t> (*func)(Ts...)) {
255 this->values_.func = func;
256 this->len_ = -1; // sentinel: template mode
257 }
258
260
261 void play(const Ts &...x) override {
262 const uint16_t start = this->start_address_.value(x...);
263 // An empty or over-long set rejects into an empty PDU inside the builder, which logs the reason;
264 // the empty PDU then resolves via on_not_sent like any refused send.
265 if (this->len_ >= 0) {
267 start, std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))));
268 return;
269 }
270 const std::vector<uint16_t> values = this->values_.func(x...);
271 this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values)));
272 }
273 void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
274 modbus::ResponseStatus status) override {
277 }
278
279 protected:
281 ssize_t len_{-1}; // -1 = template mode, >= 0 = static mode with this many registers
282 union Values {
283 std::vector<uint16_t> (*func)(Ts...);
284 const uint16_t *data;
286};
287
292template<typename... Ts> class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...> {
293 public:
295
296
297 void set_values_static(const uint8_t *packed, size_t count) {
298 this->values_.packed = packed;
299 this->count_ = static_cast<ssize_t>(count);
300 }
302 void set_values_template(std::vector<bool> (*func)(Ts...)) {
303 this->values_.func = func;
304 this->count_ = -1; // sentinel: template mode
305 }
306
308
309 void play(const Ts &...x) override {
310 const uint16_t start = this->start_address_.value(x...);
311 if (this->count_ >= 0) {
312 const auto count = static_cast<uint16_t>(this->count_);
314 start,
315 modbus::PackedBits(std::span<const uint8_t>(this->values_.packed, modbus::packed_bit_bytes(count)), count)));
316 return;
317 }
318 // The builder packs and bound-checks; an over-long set is rejected and logged there.
320 }
326
327 protected:
329 ssize_t count_{-1}; // -1 = template mode, >= 0 = static mode with this many coils
330 union Values {
331 std::vector<bool> (*func)(Ts...);
332 const uint8_t *packed;
334};
335
338template<typename... Ts> class ReadWriteMultipleRegistersAction : public TypedClientActionBase<Ts...> {
339 public:
340 TEMPLATABLE_VALUE(uint16_t, read_address)
341 TEMPLATABLE_VALUE(uint16_t, read_count)
342 TEMPLATABLE_VALUE(uint16_t, write_address)
343
345 void set_values_static(const uint16_t *values, size_t len) {
346 this->values_.data = values;
347 this->len_ = static_cast<ssize_t>(len);
348 }
350 void set_values_template(std::vector<uint16_t> (*func)(Ts...)) {
351 this->values_.func = func;
352 this->len_ = -1; // sentinel: template mode
353 }
354
356
357 void play(const Ts &...x) override {
358 const uint16_t read_start = this->read_address_.value(x...);
359 const uint16_t read_count = this->read_count_.value(x...);
360 const uint16_t write_start = this->write_address_.value(x...);
361 // An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent.
362 if (this->len_ >= 0) {
364 read_start, read_count, write_start,
365 std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))));
366 return;
367 }
368 const std::vector<uint16_t> values = this->values_.func(x...);
370 read_start, read_count, write_start, std::span<const uint16_t>(values)));
371 }
372 // The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read.
373 void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
374 modbus::ResponseStatus status) override {
376 this->response_trigger_.trigger(registers);
377 }
378
379 protected:
381 ssize_t len_{-1}; // -1 = template mode, >= 0 = static mode with this many write registers
382 union Values {
383 std::vector<uint16_t> (*func)(Ts...);
384 const uint16_t *data;
386};
387
388} // 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:227
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:1360
void set_address(uint8_t address)
Definition modbus.h:453
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.
void send_or_resolve_(std::span< const uint8_t > pdu)
The hub refuses some sends at the door with no callback (a duplicate write already pending,...
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...
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()
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.
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
__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:336
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:342
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