ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
modbus.h
Go to the documentation of this file.
1#pragma once
2
5
8
9#include <array>
10#include <cstring>
11#include <memory>
12#include <span>
13#include <vector>
14#include <deque>
15#include <optional>
16
17namespace esphome::modbus {
18
19// Tx queue backstop: duplicates dedup into one entry, so only a runaway generator of distinct frames
20// (e.g. a loop writing a changing value) could grow the heap unboundedly.
21static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128;
22static constexpr uint16_t MODBUS_TX_MAX_DELAY_US = 5000;
23
24// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
25// (address + 5-byte PDU + 2-byte CRC).
26static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
27
29 // Small-buffer-optimized: typical frames fit inline, keeping high-frequency tx traffic off the
30 // heap; only large multi-register or custom frames spill to a single heap allocation.
32
33 // A frame is [address][PDU...][CRC lo][CRC hi]. These are the only places that need to know that layout
34 ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) {
35 uint8_t *buf = this->data.init(pdu_len + 3);
36 buf[0] = address;
37 memcpy(buf + 1, pdu, pdu_len);
38 auto crc = crc16(buf, pdu_len + 1);
39 buf[pdu_len + 1] = crc >> 0;
40 buf[pdu_len + 2] = crc >> 8;
41 }
42
43 uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
44 uint8_t address() const { return this->data.data()[0]; }
47 std::span<const uint8_t> pdu() const { return std::span<const uint8_t>(this->data.data() + 1, this->size() - 3u); }
48};
49
50class Modbus : public uart::UARTDevice, public Component {
51 public:
52 Modbus() = default;
53
54 void setup() override;
55 void loop() override;
56
57 float get_setup_priority() const override;
58 virtual bool tx_blocked();
59
60 void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
61
62 protected:
63 void receive_bytes_();
64 bool timeout_();
65 virtual int32_t tx_delay_remaining();
66 virtual void parse_modbus_frames() = 0;
68 virtual void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) = 0;
69 void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0);
70 bool send_frame_(const ModbusFrame &frame);
71 uint16_t find_frame_end_by_crc_(uint16_t min_length) const;
72
73 // All timestamps and durations below are micros()-based
81 // Bits on the wire per character (start + data + optional parity + stop); 12 at most.
82 uint8_t bits_per_char_{11};
83 // Latched when a read reaches rx_full_threshold, cleared when the buffer drains.
85
87
88 std::vector<uint8_t> rx_buffer_;
89};
90
93
94// Transmit ordering, highest first: writes before one-shot reads before continuous polls.
95enum class CommandPriority : uint8_t { CONTINUOUS = 0, READ, WRITE };
96
97// Per-entry lifecycle state. Waiting states (see waiting_state()) hold the bus; the sweep delivers owed
98// callbacks from a quiescent hub, and an entry is erased once pending == 0 && !waiting_state().
99enum class FrameState : uint8_t {
100 READY = 0,
101 WAITING,
104 TIMED_OUT, // on_no_response delivered at the send-wait timeout; awaiting reschedule/erase
105 INTERRUPTED, // unexpected frame arrived; ignores this transaction, waits out the timeout
106 WAITING_RETIRED, // retired while WAITING: a late response is still delivered as its usual terminal
107 INTERRUPTED_RETIRED, // retired while INTERRUPTED: still distrusts late frames, ends in on_no_response
108 RETIRED, // retired, off the wire
109};
110
111// Per-command send options. Append-only; pass via designated initializers ({.continuous = true}).
112// A new field reaches the queue with no plumbing but arrives inert until it defines three rules:
113// normalization in queue_pdu(), a merge rule for duplicate absorption, and teardown in
114// retire()/silent_retire(). Bit-packed: stored per entry, controller and writer entity, passed by value.
116 // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes.
117 bool continuous : 1 {false};
118 // Wait for the reply to a read sent to address 0, for a device that answers the broadcast address.
119 bool allow_broadcast_read : 1 {false};
121};
122static_assert(sizeof(CommandOptions) == 1, "CommandOptions must stay one byte");
123
127 // Place-in-line stamp (hub's free-running counter); selection takes the oldest for round-robin
128 // fairness within a class. Meant to wrap.
129 uint16_t seq{0};
131 // Accepted requests this entry stands for, capped at max_pending(); drains one terminal each.
132 // A continuous poll is a subscription: pending fixed at 1, removed only by cancellation or failure.
133 uint8_t pending{1};
134 // The entry's LIVE effective options, not a record of the caller's request
136
137 // Build a command from a PDU span (caller bounds it to MAX_PDU_SIZE) and pre-normalized options;
138 // fully initialized here.
139 ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span<const uint8_t> pdu,
140 CommandOptions options = {}, uint16_t seq = 0)
141 : device(device), frame(address, pdu.data(), static_cast<uint16_t>(pdu.size())), seq(seq), options(options) {}
142
144 if (this->options.continuous)
146 if (helpers::is_function_code_write(this->frame.pdu()[0])) {
148 }
150 }
151
152 // Requests this entry can serve
153 uint8_t max_pending() const {
154 const uint8_t fc = this->frame.pdu()[0];
155 return (helpers::is_function_code_read_only(fc) && !this->options.continuous) ? 2 : 1;
156 }
157 // Device-scoped clear: detach with no callback. An entry still waiting for a response keeps its state as a
158 // reply-ignoring shell that resolves silently; any other goes RETIRED.
160 if (!this->waiting_state())
162 this->pending = 0;
163 this->device = nullptr;
164 }
165 bool fire_and_forget() const {
166 return this->frame.address() == BROADCAST_ADDRESS && !this->options.allow_broadcast_read &&
167 !this->options.expect_broadcast_write_response;
168 }
169 // Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already
170 // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with no terminal callback.
173 this->pending = 0;
174 }
175 // Re-ready for another transmission, restamped to the tail of its class
176 void requeue(uint16_t seq) {
177 this->state = FrameState::READY;
178 this->seq = seq;
179 }
180 // Re-task a frame that lives on: upgrade a one-shot to a continuous poll, or downgrade a poll back to
181 // a one-shot.
182 void make_continuous(bool continuous) {
183 if (continuous) {
184 this->options.continuous = true;
185 this->pending = 1;
186 } else {
187 this->increment_pending();
188 this->options.continuous = false;
189 }
190 }
191 // Address-scoped clear: keep pending and device so the sweep delivers one on_not_sent() per un-delivered
192 // request. An entry still waiting for a response keeps its in-flight request (whose usual terminal is
193 // still coming) and drains only its duplicates.
194 void retire() {
195 if (this->state == FrameState::WAITING) {
197 } else if (this->state == FrameState::INTERRUPTED) {
199 } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED
201 }
202 // Only continuous ends with the clear; the delivery flags must survive for a granted retry.
203 this->options.continuous = false;
204 }
205
206 // True while the entry is still waiting for a response
207 bool waiting_state() const {
208 return this->state == FrameState::WAITING || this->state == FrameState::INTERRUPTED ||
210 }
211
213 if (this->pending > 0) {
214 this->pending--;
215 return true;
216 }
217 return false;
218 }
219
221 if (this->pending < this->max_pending()) {
222 this->pending++;
223 return true;
224 }
225 return false;
226 }
227
228 // Terminal/lifecycle methods: each owns its transition, callback, and pending accounting and
229 // returns whether a callback ran.
230 bool sent();
231 bool response(std::span<const uint8_t> response_pdu);
232 bool error(ExceptionCode exception_code);
233 bool interrupt();
234 bool timed_out();
235 bool notify_retired();
236
238 bool same_frame(uint8_t address, std::span<const uint8_t> pdu) const {
239 const auto own_pdu = this->frame.pdu();
240 return own_pdu.size() == pdu.size() && this->frame.address() == address &&
241 memcmp(own_pdu.data(), pdu.data(), pdu.size()) == 0;
242 }
243};
244
245class ModbusClientHub : public Modbus {
246 public:
247 ModbusClientHub() = default;
248 void dump_config() override;
249 void loop() override;
250 // Config arrives in milliseconds; stored internally in microseconds like all other timing.
251 void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_us_ = time_in_ms * 1000UL; }
252 void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; }
253 bool tx_buffer_empty();
254 bool tx_blocked() override;
258 bool queue_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr,
260 // Remove before 2027.2.0. Deliberately the void, no-options signature 2026.7.4 shipped: nothing
261 // external can rely on the later additions under this name.
262 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
263 "reports whether the request was accepted. Removed in 2027.2.0",
264 "2026.8.0")
265 void send_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr) {
266 this->queue_pdu(address, pdu, device);
267 }
268 ESPDEPRECATED("Use queue_pdu(payload[0], <pdu bytes>, device) instead. Removed in 2027.2.0", "2026.8.0")
269 void send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device = nullptr);
270 // Clear all commands matching the given address; each unsent request resolves via on_not_sent(), but a
271 // frame on the wire still runs to its usual terminal.
273 // Clear all commands for a given device; no callbacks are delivered.
275
276 protected:
277 int32_t tx_delay_remaining() override;
278 void parse_modbus_frames() override;
279 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
280 void send_next_frame_();
281 // Deliver owed callbacks from a quiescent hub and apply lifecycle bookkeeping; see FrameState.
282 void sweep_();
283 // The selection function: best READY entry (ordered by priority; FIFO by seq within each group), or nullptr.
285 // Locate the single entry waiting for a response (WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED).
287 // End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState.
288 void expire_waiting_();
289
292
293 // Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select
294 // while it is set, so at most one frame is awaiting a response.
296
297 // Set whenever a transition leaves owed callbacks behind; quiet loop() passes skip the sweep.
298 bool sweep_needed_{false};
299 // Monotonic stamp source for ModbusDeviceCommand::seq.
300 uint16_t next_seq_{0};
301
302 // Plain append-order container; ordering lives in select_next_ready_(), lifecycle in FrameState.
303 std::deque<ModbusDeviceCommand> tx_buffer_;
304};
305
306// Transaction status: std::nullopt on success, otherwise a Modbus exception code
307using ResponseStatus = std::optional<ExceptionCode>;
308
310inline bool succeeded(ResponseStatus status) { return !status.has_value(); }
311
312// Register values exchanged with server handlers, in address order. Sized at the larger of the two protocol
313// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
314// the capacity of this type.
316
317class ModbusServerHub : public Modbus {
318 public:
319 ModbusServerHub() = default;
320 void dump_config() override;
321 void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
322
323 protected:
324 void parse_modbus_frames() override;
326 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
327 void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span<const uint8_t> data);
328 // Dispatches a broadcast (address 0) write to every registered device; broadcasts are never answered.
329 void process_broadcast_frame_(uint8_t function_code, std::span<const uint8_t> data);
330 // Parses a WRITE_SINGLE_REGISTER / WRITE_MULTIPLE_REGISTERS PDU into start_address and the address order register
331 // values, validating the register count and address range. Shared by unicast and broadcast writes.
332 ResponseStatus parse_write_single_(std::span<const uint8_t> data, uint16_t &start_address, RegisterValues &registers);
333 ResponseStatus parse_write_multiple_(std::span<const uint8_t> data, uint16_t &start_address,
334 RegisterValues &registers);
335 // Assembles host-order registers from the big-endian bytes in values and appends them to registers.
336 void assemble_registers_(std::span<const uint8_t> values, RegisterValues &registers);
338 // Returns std::nullopt if [start_address, start_address + count) fits in a 16-bit address space, otherwise
339 // ILLEGAL_DATA_ADDRESS. The caller sends the exception reply if one is required. Shared by the
340 // register/coil/discrete-input handlers, which all use a 16-bit address space.
341 ResponseStatus check_address_range_(uint16_t start_address, uint16_t count);
342
343 // Parses read request data. max_entities is the protocol ceiling for the function code; entity_name labels
344 // the rejection log.
345 ResponseStatus parse_read_request_(std::span<const uint8_t> data, uint16_t max_entities, const LogString *entity_name,
346 uint16_t &start_address, uint16_t &count);
347
348 // Parses single-coil write data
349 ResponseStatus parse_write_single_coil_(std::span<const uint8_t> data, uint16_t &start_address, bool &value);
350
351 // Parses write-multiple-coil data into a packed-bit view pointing straight into the receive buffer, so the
352 // coil values are never copied.
353 ResponseStatus parse_write_multiple_coils_(std::span<const uint8_t> data, uint16_t &start_address, uint16_t &count,
354 std::span<const uint8_t> &packed_bytes);
355
356 // Builds the body of a register read response into response_buffer. Returns false once an exception has
357 // been sent: the one the handler reported via status, or SERVICE_DEVICE_FAILURE if it returned the wrong
358 // number of registers, the count exceeds the protocol read limit, or the body does not fit.
359 bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status,
360 uint16_t number_of_registers, const RegisterValues &registers,
361 std::span<uint8_t> response_buffer, uint16_t &response_len);
362 void send_raw_(const uint8_t *payload, uint16_t len);
363 // Sends and logs the exception reply when status holds one; returns true if the request was rejected.
364 bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status);
365 void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code);
366 void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
368 std::vector<ModbusServerDevice *> devices_;
369
370 // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time.
371 // Only one server reply can be waiting at once, so a single fixed buffer avoids heap allocation.
372 std::array<uint8_t, MAX_RAW_SIZE> deferred_payload_;
374};
375
396 public:
400 if (this->parent_ != nullptr)
401 this->clear_tx_queue_for_device();
402 }
407 void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
408 void set_address(uint8_t address) { this->address_ = address; }
413 virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
414 this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
415 }
419 virtual void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
420 this->dispatch_response_(request_pdu, {}, exception_code);
421 }
424 virtual void on_not_sent(std::span<const uint8_t> request_pdu) {
425#pragma GCC diagnostic push
426#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
427 this->on_modbus_not_sent();
428#pragma GCC diagnostic pop
429 }
431 virtual void on_sent(std::span<const uint8_t> request_pdu) {}
434 virtual bool on_no_response(std::span<const uint8_t> request_pdu) {
435#pragma GCC diagnostic push
436#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
437 return this->on_modbus_no_response();
438#pragma GCC diagnostic pop
439 }
443 virtual void on_response(uint8_t address, std::span<const uint8_t> request_pdu,
444 std::span<const uint8_t> response_pdu) {
445 this->on_response(request_pdu, response_pdu);
446 }
447 virtual void on_error(uint8_t address, std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
448 this->on_error(request_pdu, exception_code);
449 }
450 virtual void on_not_sent(uint8_t address, std::span<const uint8_t> request_pdu) { this->on_not_sent(request_pdu); }
451 virtual void on_sent(uint8_t address, std::span<const uint8_t> request_pdu) { this->on_sent(request_pdu); }
452 virtual bool on_no_response(uint8_t address, std::span<const uint8_t> request_pdu) {
453 return this->on_no_response(request_pdu);
454 }
455 // Remove before 2027.2.0
456 ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
457 virtual void on_modbus_not_sent() {}
458 // Remove before 2027.2.0
459 ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0")
460 virtual bool on_modbus_no_response() { return false; }
461
466 virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
467 ResponseStatus status) {}
468 virtual void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
469 ResponseStatus status) {
470 this->on_read_registers(EntityType::HOLDING, start_address, registers, status);
471 }
472 virtual void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
473 ResponseStatus status) {
474 this->on_read_registers(EntityType::INPUT_REGISTER, start_address, registers, status);
475 }
479 virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status) {}
480 virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {
481 this->on_read_bits(EntityType::COIL, start_address, bits, status);
482 }
483 virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) {
484 this->on_read_bits(EntityType::DISCRETE_INPUT, start_address, bits, status);
485 }
495 virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) {}
496 virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) {}
497 virtual void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
498 ResponseStatus status) {}
499 virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {}
504 virtual void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
505 ResponseStatus status);
506 ESPDEPRECATED("Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0", "2026.8.0")
507 void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
508 const uint8_t *payload = nullptr) {
509 this->parent_->queue_pdu(
510 this->address_,
512 this);
513 }
515 bool queue_pdu(std::span<const uint8_t> pdu, CommandOptions options = {}) {
516 return this->parent_->queue_pdu(this->address_, pdu, this, options);
517 }
518 // Remove before 2027.2.0. As on the hub, this is the signature 2026.7.4 shipped: void, no options.
519 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
520 "reports whether the request was accepted. Removed in 2027.2.0",
521 "2026.8.0")
522 void send_pdu(std::span<const uint8_t> pdu) { this->queue_pdu(pdu); }
523 ESPDEPRECATED("Use queue_pdu() instead (the device address is prepended for you). Removed in 2027.2.0", "2026.8.0")
524 void send_raw(const std::vector<uint8_t> &payload) {
525 if (payload.empty())
526 return; // too short to contain a PDU; refused at the door like any invalid send
527 this->parent_->queue_pdu(payload[0], std::span<const uint8_t>(payload).subspan(1), this);
528 }
529 // The typed request builders below all queue through queue_pdu() and share its return contract.
530 // Reads use the table-appropriate function code; an unreadable entity type maps to INVALID, which
531 // create_read_pdu() rejects into an empty PDU and queue_pdu() refuses with a false return.
532 bool read_entities(EntityType entity_type, uint16_t start_address, uint16_t number_of_entities,
533 CommandOptions options = {}) {
536 options);
537 }
538 bool read_input_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
539 return this->queue_pdu(
541 }
542 bool read_holding_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
543 return this->queue_pdu(
545 }
546 bool read_coils(uint16_t start_address, uint16_t number_of_coils, CommandOptions options = {}) {
547 return this->queue_pdu(helpers::create_read_pdu(FunctionCode::READ_COILS, start_address, number_of_coils), options);
548 }
549 bool read_discrete_inputs(uint16_t start_address, uint16_t number_of_inputs, CommandOptions options = {}) {
550 return this->queue_pdu(
552 }
553 bool write_single_register(uint16_t start_address, uint16_t value, CommandOptions options = {}) {
554 return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value), options);
555 }
556 bool write_single_coil(uint16_t address, bool value, CommandOptions options = {}) {
557 return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value), options);
558 }
559 bool write_multiple_registers(uint16_t start_address, std::span<const uint16_t> values, CommandOptions options = {}) {
560 // Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's.
561 if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS)
562 return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values), options);
563 return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values), options);
564 }
567 bool write_multiple_coils(uint16_t start_address, std::span<const bool> values, CommandOptions options = {}) {
568 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values), options);
569 }
572 bool write_multiple_coils(uint16_t start_address, PackedBits bits, CommandOptions options = {}) {
573 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits), options);
574 }
578 bool read_write_multiple_registers(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address,
579 std::span<const uint16_t> write_values) {
580 return this->queue_pdu(helpers::create_read_write_multiple_registers_pdu(read_start_address, read_count,
581 write_start_address, write_values));
582 }
583 inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); }
584 inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); }
585
586 bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
587
588 protected:
590 void dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
591 ResponseStatus status);
592
594 uint8_t address_{0};
595 bool custom_response_warned_{false}; // first unhandled custom response warns; repeats log at VERBOSE
596};
597
598// Compatibility shim adapting the span-based hooks back to the pre-2026.8 on_modbus_data()/
599// on_modbus_error() signatures (the owning-vector heap copy exists only on this deprecated path).
600// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0).
601class ESPDEPRECATED("Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0",
602 "2026.8.0") ModbusDevice : public ModbusClientDevice {
603 public:
604 using ModbusClientDevice::ModbusClientDevice;
605 virtual void on_modbus_data(const std::vector<uint8_t> &data) {}
606 virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
607
608 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
609 // Custom (user-defined) function codes historically delivered the payload starting AT the function
610 // code byte (frame data_offset 1). server_pdu_payload() drops that byte, so pass the whole PDU for
611 // them - external components match the first byte against the code they sent (issue #17994).
612 auto payload = !response_pdu.empty() && helpers::is_function_code_custom(response_pdu[0])
613 ? response_pdu
614 : helpers::server_pdu_payload(response_pdu);
615 this->on_modbus_data(std::vector<uint8_t>(payload.begin(), payload.end()));
616 }
617 void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) override {
618 this->on_modbus_error(request_pdu.empty() ? 0 : request_pdu[0], static_cast<uint8_t>(exception_code));
619 }
620};
621
623 public:
624 virtual ~ModbusServerDevice() = default;
626 // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
631 void set_address(uint8_t address) { this->address_ = address; }
632 uint8_t get_address() const { return this->address_; }
633 virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers,
634 RegisterValues &registers) {
636 };
637 virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
638 RegisterValues &registers) {
639 return this->on_read_registers(start_address, number_of_registers, registers);
640 };
641 virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
642 RegisterValues &registers) {
643 return this->on_read_registers(start_address, number_of_registers, registers);
644 };
645 virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers) {
647 };
651 virtual ResponseStatus on_read_bits(uint16_t start_address, MutablePackedBits bits) {
653 };
654 virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits) {
655 return this->on_read_bits(start_address, bits);
656 };
657 virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits) {
658 return this->on_read_bits(start_address, bits);
659 };
662 virtual ResponseStatus on_write_coils(uint16_t start_address, PackedBits bits) {
664 };
665
666 protected:
667 uint8_t address_{0};
668};
669
670} // namespace esphome::modbus
uint8_t address
Definition bl0906.h:4
uint8_t status
Definition bl0942.h:8
Small buffer optimization - stores data inline when small, heap-allocates for large data This avoids ...
Definition helpers.h:206
uint8_t * init(size_t size)
Resize to size bytes of (uninitialized) storage and return a writable pointer to fill.
Definition helpers.h:254
size_t size() const
Definition helpers.h:273
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:286
virtual void on_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu)
Low-level response hook: called with the request PDU this device sent and the response PDU received T...
Definition modbus.h:413
virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:499
ModbusClientDevice & operator=(ModbusClientDevice &&)=delete
virtual void on_response(uint8_t address, std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu)
Overloads of the hooks above with the device address of the request first, taken from the request fra...
Definition modbus.h:443
virtual void on_error(uint8_t address, std::span< const uint8_t > request_pdu, ExceptionCode exception_code)
Definition modbus.h:447
virtual void on_read_holding_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:468
virtual void on_write_multiple_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:497
virtual void on_sent(std::span< const uint8_t > request_pdu)
Called when this device's frame is actually written to the wire.
Definition modbus.h:431
ModbusClientDevice(const ModbusClientDevice &)=delete
virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status)
Write acknowledgements.
Definition modbus.h:495
uint16_t uint16_t uint8_t payload_len
Definition modbus.h:507
virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status)
Coil/discrete-input reads are delivered as a PackedBits view (bit 0 = the bit at start_address,...
Definition modbus.h:479
virtual void on_not_sent(uint8_t address, std::span< const uint8_t > request_pdu)
Definition modbus.h:450
virtual bool on_no_response(uint8_t address, std::span< const uint8_t > request_pdu)
Definition modbus.h:452
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
Definition modbus.h:407
ESPDEPRECATED("Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0", "2026.8.0") void send(uint8_t function
virtual bool on_no_response(std::span< const uint8_t > request_pdu)
Called when no matching, uninterrupted response arrived; return true to have the hub re-queue the fra...
Definition modbus.h:434
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:1403
virtual void on_sent(uint8_t address, std::span< const uint8_t > request_pdu)
Definition modbus.h:451
ModbusClientDevice(ModbusClientDevice &&)=delete
virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:483
uint16_t uint16_t number_of_entities
Definition modbus.h:507
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") virtual bool on_modbus_no_response()
Definition modbus.h:459
virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
High-level typed response callbacks, fired by the default on_response()/on_error() with arguments par...
Definition modbus.h:466
virtual void on_error(std::span< const uint8_t > request_pdu, ExceptionCode exception_code)
Low-level error hook: called with the request PDU and the modbus exception code from the error respon...
Definition modbus.h:419
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
Definition modbus.h:398
void set_address(uint8_t address)
Definition modbus.h:408
virtual void on_not_sent(std::span< const uint8_t > request_pdu)
Called when an accepted request was dropped before transmission by clear_tx_queue_for_address().
Definition modbus.h:424
virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:480
virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status)
Definition modbus.h:496
uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:508
virtual void on_read_input_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:472
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent()
Definition modbus.h:456
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:1179
void parse_modbus_frames() override
Definition modbus.cpp:193
ModbusDeviceCommand * find_waiting_()
Definition modbus.cpp:922
std::span< const uint8_t > pdu
Definition modbus.h:265
ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it " "reports whether the request was accepted. Removed in 2027.2.0", "2026.8.0") void send_pdu(uint8_t address
ModbusDeviceCommand * select_next_ready_()
Definition modbus.cpp:930
int32_t tx_delay_remaining() override
Definition modbus.cpp:140
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:303
std::span< const uint8_t > ModbusClientDevice * device
Definition modbus.h:265
bool queue_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr, CommandOptions options={})
Queue a request.
Definition modbus.cpp:1066
void clear_tx_queue_for_address(uint8_t address)
Definition modbus.cpp:1169
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:342
void set_send_wait_time(uint16_t time_in_ms)
Definition modbus.h:251
void set_turnaround_time(uint16_t time_in_ms)
Definition modbus.h:252
void set_flow_control_pin(GPIOPin *flow_control_pin)
Definition modbus.h:60
void setup() override
Definition modbus.cpp:31
uint32_t long_rx_buffer_delay_us_
Definition modbus.h:79
virtual void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu)=0
bool parse_modbus_server_frame_()
Definition modbus.cpp:269
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:785
uint32_t last_modbus_byte_
Definition modbus.h:74
GPIOPin * flow_control_pin_
Definition modbus.h:86
uint32_t last_send_tx_offset_
Definition modbus.h:77
virtual bool tx_blocked()
Definition modbus.cpp:148
void clear_rx_buffer_(const LogString *reason, bool warn=false, size_t bytes_to_clear=0)
Definition modbus.cpp:1234
void loop() override
Definition modbus.cpp:63
uint32_t frame_delay_us_
Definition modbus.h:78
float get_setup_priority() const override
Definition modbus.cpp:881
bool exceeded_rx_full_threshold_
Definition modbus.h:84
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:134
uint16_t find_frame_end_by_crc_(uint16_t min_length) const
Definition modbus.cpp:249
std::vector< uint8_t > rx_buffer_
Definition modbus.h:88
uint32_t rx_detect_latency_us_
Definition modbus.h:80
uint32_t last_receive_check_
Definition modbus.h:75
ModbusServerDevice & operator=(ModbusServerDevice &&)=delete
virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:654
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
void set_address(uint8_t address)
Definition modbus.h:631
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:645
virtual ResponseStatus on_read_bits(uint16_t start_address, MutablePackedBits bits)
Coil/discrete-input reads: set the requested bits (bit 0 = the coil at start_address) with bits....
Definition modbus.h:651
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:641
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:633
virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:657
virtual ResponseStatus on_write_coils(uint16_t start_address, PackedBits bits)
Coil writes deliver the values as a PackedBits view over the hub's receive buffer (only valid during ...
Definition modbus.h:662
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:637
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:368
ResponseStatus check_address_range_(uint16_t start_address, uint16_t count)
Definition modbus.cpp:419
ResponseStatus parse_read_request_(std::span< const uint8_t > data, uint16_t max_entities, const LogString *entity_name, uint16_t &start_address, uint16_t &count)
Definition modbus.cpp:465
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:615
void parse_modbus_frames() override
Definition modbus.cpp:206
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:394
ResponseStatus parse_write_multiple_coils_(std::span< const uint8_t > data, uint16_t &start_address, uint16_t &count, std::span< const uint8_t > &packed_bytes)
Definition modbus.cpp:492
void process_broadcast_frame_(uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:517
void register_device(ModbusServerDevice *device)
Definition modbus.h:321
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:410
ResponseStatus parse_write_single_coil_(std::span< const uint8_t > data, uint16_t &start_address, bool &value)
Definition modbus.cpp:479
bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status, uint16_t number_of_registers, const RegisterValues &registers, std::span< uint8_t > response_buffer, uint16_t &response_len)
Definition modbus.cpp:572
void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code)
Definition modbus.cpp:914
void assemble_registers_(std::span< const uint8_t > values, RegisterValues &registers)
Definition modbus.cpp:511
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:1199
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:886
ResponseStatus parse_write_multiple_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:448
bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status)
Definition modbus.cpp:901
ResponseStatus parse_write_single_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:440
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
Definition modbus.h:372
Mutable counterpart of PackedBits: set() writes bits in place (deliberately no proxy operator[]=).
Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first),...
uint8_t options
unsigned count
Definition gif.cpp:51
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.
WriteFewRegistersPdu create_write_few_registers_pdu(uint16_t start_address, std::span< const uint16_t > values)
Create modbus write multiple registers command (function 0x10) on a right-sized stack buffer.
bool is_function_code_read_only(uint8_t function_code)
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.
bool is_function_code_write(uint8_t function_code)
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_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len)
Create a modbus client pdu for reading/writing single/multiple coils/register/inputs.
FunctionCode modbus_register_read_function(EntityType reg_type)
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:307
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:310
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
Definition helpers.cpp:95
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
STL namespace.
static void uint32_t
bool response(std::span< const uint8_t > response_pdu)
Definition modbus.cpp:965
CommandPriority priority() const
Definition modbus.h:143
ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span< const uint8_t > pdu, CommandOptions options={}, uint16_t seq=0)
Definition modbus.h:139
void make_continuous(bool continuous)
Definition modbus.h:182
bool error(ExceptionCode exception_code)
Definition modbus.cpp:976
bool same_frame(uint8_t address, std::span< const uint8_t > pdu) const
True if this command carries the same wire frame (address + PDU) as the given one.
Definition modbus.h:238
ModbusClientDevice * device
Definition modbus.h:125
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
Definition modbus.h:34
uint8_t address() const
Definition modbus.h:44
SmallInlineBuffer< MODBUS_FRAME_INLINE_SIZE > data
Definition modbus.h:31
std::span< const uint8_t > pdu() const
A PDU is [function code][data...] without address or CRC.
Definition modbus.h:47
uint16_t size() const
Definition modbus.h:43