ESPHome 2026.9.0-dev
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
90template<typename... Ts> class ReadCommandOptions {
91 public:
92 // Poll: re-queue after each success until downgraded (replay with false) or failed. The hub strips
93 // it for mutating function codes at the door (see modbus::CommandOptions).
94 TEMPLATABLE_VALUE(bool, continuous)
95
96 protected:
98 modbus::CommandOptions command_options_(const Ts &...x) const {
99 return {.continuous = this->continuous_.value(x...)};
100 }
101};
102
110template<typename... Ts>
111class ModbusClientSendAction : public ClientActionBase<Ts...>, public ReadCommandOptions<Ts...> {
112 public:
114
115 Trigger<std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
116 return &this->response_trigger_;
117 }
118
119 void play(const Ts &...x) override { this->send_or_resolve_(this->pdu_.value(x...), this->command_options_(x...)); }
120
121 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
122 this->response_trigger_.trigger(request_pdu, response_pdu);
123 }
124
125 protected:
127};
128
139template<typename... Ts> class TypedClientActionBase : public ClientActionBase<Ts...> {
140 public:
142 return &this->custom_response_trigger_;
143 }
147
148 void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
149 modbus::ResponseStatus status) override {
150 if (!this->custom_response_handled_) {
151 modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
152 return;
153 }
154 this->custom_response_trigger_.trigger(request_pdu, response_pdu);
155 }
156
157 protected:
160};
161
164template<typename... Ts>
166 public:
167 explicit ReadRegistersAction(bool holding) : holding_(holding) {}
169 TEMPLATABLE_VALUE(uint16_t, count)
170
171 Trigger<std::span<const uint16_t>> *get_response_trigger() { return &this->response_trigger_; }
172
173 void play(const Ts &...x) override {
174 const auto function_code =
176 this->send_or_resolve_(
177 modbus::helpers::create_read_pdu(function_code, this->start_address_.value(x...), this->count_.value(x...)),
178 this->command_options_(x...));
179 }
180 void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
181 modbus::ResponseStatus status) override {
183 this->response_trigger_.trigger(registers);
184 }
185
186 protected:
189};
190
193template<typename... Ts> class ReadBitsAction : public TypedClientActionBase<Ts...>, public ReadCommandOptions<Ts...> {
194 public:
195 explicit ReadBitsAction(bool coils) : coils_(coils) {}
197 TEMPLATABLE_VALUE(uint16_t, count)
198
199 Trigger<modbus::PackedBits> *get_response_trigger() { return &this->response_trigger_; }
200
201 void play(const Ts &...x) override {
202 const auto function_code =
204 this->send_or_resolve_(
205 modbus::helpers::create_read_pdu(function_code, this->start_address_.value(x...), this->count_.value(x...)),
206 this->command_options_(x...));
207 }
209 modbus::ResponseStatus status) override {
211 this->response_trigger_.trigger(bits);
212 }
213
214 protected:
216 bool coils_;
217};
218
221template<typename... Ts> class WriteSingleRegisterAction : public TypedClientActionBase<Ts...> {
222 public:
224 TEMPLATABLE_VALUE(uint16_t, value)
225
227
228 void play(const Ts &...x) override {
229 this->send_or_resolve_(
230 modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)));
231 }
232 void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override {
235 }
236
237 protected:
239};
240
243template<typename... Ts> class WriteSingleCoilAction : public TypedClientActionBase<Ts...> {
244 public:
246 TEMPLATABLE_VALUE(bool, value)
247
249
250 void play(const Ts &...x) override {
251 this->send_or_resolve_(
252 modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)));
253 }
254 void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override {
257 }
258
259 protected:
261};
262
267template<typename... Ts> class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...> {
268 public:
270
271
272 void set_values_static(const uint16_t *values, size_t len) {
273 this->values_.data = values;
274 this->len_ = static_cast<ssize_t>(len);
275 }
278 void set_values_template(std::vector<uint16_t> (*func)(Ts...)) {
279 this->values_.func = func;
280 this->len_ = -1; // sentinel: template mode
281 }
282
284
285 void play(const Ts &...x) override {
286 const uint16_t start = this->start_address_.value(x...);
287 // An empty or over-long set rejects into an empty PDU inside the builder, which logs the reason;
288 // the empty PDU then resolves via on_not_sent like any refused send.
289 if (this->len_ >= 0) {
291 start, std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))));
292 return;
293 }
294 const std::vector<uint16_t> values = this->values_.func(x...);
295 this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values)));
296 }
297 void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
298 modbus::ResponseStatus status) override {
301 }
302
303 protected:
305 ssize_t len_{-1}; // -1 = template mode, >= 0 = static mode with this many registers
306 union Values {
307 std::vector<uint16_t> (*func)(Ts...);
308 const uint16_t *data;
310};
311
316template<typename... Ts> class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...> {
317 public:
319
320
321 void set_values_static(const uint8_t *packed, size_t count) {
322 this->values_.packed = packed;
323 this->count_ = static_cast<ssize_t>(count);
324 }
326 void set_values_template(std::vector<bool> (*func)(Ts...)) {
327 this->values_.func = func;
328 this->count_ = -1; // sentinel: template mode
329 }
330
332
333 void play(const Ts &...x) override {
334 const uint16_t start = this->start_address_.value(x...);
335 if (this->count_ >= 0) {
336 const auto count = static_cast<uint16_t>(this->count_);
338 start,
339 modbus::PackedBits(std::span<const uint8_t>(this->values_.packed, modbus::packed_bit_bytes(count)), count)));
340 return;
341 }
342 // The builder packs and bound-checks; an over-long set is rejected and logged there.
344 }
350
351 protected:
353 ssize_t count_{-1}; // -1 = template mode, >= 0 = static mode with this many coils
354 union Values {
355 std::vector<bool> (*func)(Ts...);
356 const uint8_t *packed;
358};
359
362template<typename... Ts> class ReadWriteMultipleRegistersAction : public TypedClientActionBase<Ts...> {
363 public:
364 TEMPLATABLE_VALUE(uint16_t, read_address)
365 TEMPLATABLE_VALUE(uint16_t, read_count)
366 TEMPLATABLE_VALUE(uint16_t, write_address)
367
369 void set_values_static(const uint16_t *values, size_t len) {
370 this->values_.data = values;
371 this->len_ = static_cast<ssize_t>(len);
372 }
374 void set_values_template(std::vector<uint16_t> (*func)(Ts...)) {
375 this->values_.func = func;
376 this->len_ = -1; // sentinel: template mode
377 }
378
380
381 void play(const Ts &...x) override {
382 const uint16_t read_start = this->read_address_.value(x...);
383 const uint16_t read_count = this->read_count_.value(x...);
384 const uint16_t write_start = this->write_address_.value(x...);
385 // An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent.
386 if (this->len_ >= 0) {
388 read_start, read_count, write_start,
389 std::span<const uint16_t>(this->values_.data, static_cast<size_t>(this->len_))));
390 return;
391 }
392 const std::vector<uint16_t> values = this->values_.func(x...);
394 read_start, read_count, write_start, std::span<const uint16_t>(values)));
395 }
396 // The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read.
397 void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
398 modbus::ResponseStatus status) override {
400 this->response_trigger_.trigger(registers);
401 }
402
403 protected:
405 ssize_t len_{-1}; // -1 = template mode, >= 0 = static mode with this many write registers
406 union Values {
407 std::vector<uint16_t> (*func)(Ts...);
408 const uint16_t *data;
410};
411
412} // 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:1379
void set_address(uint8_t address)
Definition modbus.h:407
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_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
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:306
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:309
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