ESPHome 2026.8.1
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. Duplicate frames dedup into one entry, so reads can never approach this in a
20// sane config - it exists to stop a runaway generator of distinct frames (e.g. a loop writing a
21// changing value) from growing the heap unboundedly. The deque grows on demand; this reserves nothing.
22// Worst case the cap permits: 128 distinct max-size frames = ~32 kB of spilled frame data plus
23// ~3 kB of deque node storage (typical 8-byte frames stay inline; large PDUs spill to one
24// allocation each) - pathological configs only, but the numbers matter when tuning for ESP8266.
25static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128;
26static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
27
28// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
29// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation.
30static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
31
33 // Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger
34 // multi-register or custom frames spill to a single heap allocation. This keeps the common,
35 // high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn.
36 // The buffer tracks its own length, so no separate size field is needed.
37 SmallInlineBuffer<MODBUS_FRAME_INLINE_SIZE> data; // Modbus RTU max is 256 bytes
38
39 ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) {
40 uint8_t *buf = this->data.init(pdu_len + 3);
41 buf[0] = address;
42 memcpy(buf + 1, pdu, pdu_len);
43 auto crc = crc16(buf, pdu_len + 1);
44 buf[pdu_len + 1] = crc >> 0;
45 buf[pdu_len + 2] = crc >> 8;
46 }
47
48 uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
49
50 // A frame is [address][PDU...][CRC lo][CRC hi]. These are the only places that need to know that layout
51 uint8_t address() const { return this->data.data()[0]; }
55 std::span<const uint8_t> pdu() const { return std::span<const uint8_t>(this->data.data() + 1, this->size() - 3u); }
56};
57
58class Modbus : public uart::UARTDevice, public Component {
59 public:
60 Modbus() = default;
61
62 void setup() override;
63 void loop() override;
64
65 float get_setup_priority() const override;
66 virtual bool tx_blocked();
67
68 void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
69
70 protected:
71 void receive_bytes_();
72 bool timeout_();
73 virtual int32_t tx_delay_remaining();
74 virtual void parse_modbus_frames() = 0;
76 // pdu is the whole PDU (function code + payload, no address/CRC); pdu[0] is the (standard or custom) function code.
77 virtual void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) = 0;
78 void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0);
79 // Transmit a frame. Callers gate on tx_blocked() first, but the pre-send delay can span several ms,
80 // so this re-checks after the delay and returns false without transmitting if a byte arrived in that
81 // window (the caller then leaves its entry to retry). Returns true once the frame has been transmitted.
82 bool send_frame_(const ModbusFrame &frame);
83 // Scans forward from min_length to find a frame boundary by CRC match for custom function codes.
84 // Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE.
85 uint16_t find_frame_end_by_crc_(uint16_t min_length) const;
86
91 uint16_t frame_delay_ms_{5};
93
95
96 std::vector<uint8_t> rx_buffer_;
97};
98
101
102// Transmit ordering, highest first: writes before one-shot reads before continuous polls. Derived
103// at selection time, never caller-chosen or stored.
104enum class CommandPriority : uint8_t { CONTINUOUS = 0, READ, WRITE };
105
106// Per-entry lifecycle state. Waiting states (see waiting_state()) hold the bus; the sweep delivers owed
107// callbacks from a quiescent hub, and an entry is erased once pending == 0 && !waiting_state().
108enum class FrameState : uint8_t {
109 READY = 0,
110 WAITING,
113 TIMED_OUT, // on_no_response delivered at the send-wait timeout; awaiting reschedule/erase
114 INTERRUPTED, // unexpected frame arrived; ignores this transaction, waits out the timeout
115 WAITING_RETIRED, // cleared while WAITING: a late response is still delivered as its usual terminal
116 INTERRUPTED_RETIRED, // cleared while INTERRUPTED: still distrusts late frames, ends in on_no_response
117 RETIRED, // cleared, off the wire
118};
119
120// Per-command send options. Append-only; pass via designated initializers ({.continuous = true}).
122 // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes.
123 bool continuous{false};
124};
125
130 // A continuous poll is a subscription: pending fixed at 1, removed only by cancellation or failure.
131 bool continuous{false};
132 // Accepted requests this entry stands for, capped at max_pending(); drains one terminal each.
133 uint8_t pending{1};
134 // Place-in-line stamp (hub's free-running counter); selection takes the oldest for round-robin
135 // fairness within a class. Meant to wrap.
136 uint16_t seq{0};
137
138 // Build a command from a PDU span (caller bounds it to MAX_PDU_SIZE); fully initialized here.
139 ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span<const uint8_t> pdu,
140 bool continuous = false, uint16_t seq = 0)
141 : device(device),
142 frame(address, pdu.data(), static_cast<uint16_t>(pdu.size())),
144 seq(seq) {}
145
146 // Transmit ordering class, derived (never stored): a continuous poll ranks below every one-shot.
148 return this->continuous ? CommandPriority::CONTINUOUS : classify(this->frame.pdu()[0]);
149 }
150 // Wire-derived class: mutating codes rank WRITE; exception-flagged codes are excluded.
151 static CommandPriority classify(uint8_t function_code) {
152 if (helpers::is_function_code_exception(function_code))
154 if (helpers::is_function_code_write(function_code)) {
156 }
158 }
159
160 // Requests this entry can serve: a standard read twice (run plus one re-run), everything else once.
161 uint8_t max_pending() const {
162 const uint8_t fc = this->frame.pdu()[0];
164 return (requeueable && !this->continuous) ? 2 : 1;
165 }
166 // Device-scoped clear: detach with no callback (device-less, pending 0). An entry still waiting for
167 // a response keeps its state as a reply-ignoring shell that resolves silently; any other goes RETIRED.
169 if (!this->waiting_state())
171 this->pending = 0;
172 this->device = nullptr;
173 }
174 // Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already
175 // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with NO terminal
176 // callback and the sweep erases it. Unlike response()/error()/timed_out(), it delivers nothing.
177 // A broadcast only carries a write or a custom code (reads are refused at queue_pdu()), and every such
178 // code caps pending at 1, so pending is always 1 here - clear it.
181 this->pending = 0;
182 }
183 // Re-ready for another transmission, restamped to the tail of its class (hub passes next_seq_++).
184 void requeue(uint16_t seq) {
185 this->state = FrameState::READY;
186 this->seq = seq;
187 }
188 // Re-task a frame that lives on: upgrade a one-shot to a continuous poll, or downgrade a poll back to
189 // a one-shot. Either way the entry keeps running and owes a request, so this is not a plain setter -
190 // to tear an entry down instead, use retire()/silent_retire(), which leave pending as the count owed.
191 // On: the entry becomes a continuous poll, superseding any absorbed requests (pending resets to the
192 // single subscription). Off: a one-shot duplicate has cancelled the poll, but the entry must still run
193 // once to serve that request - so restore one first. While the flag is still set max_pending() is 1,
194 // so the restore lifts a terminated poll (pending 0, after an error/timeout) back to 1 and is a no-op
195 // on a live poll already at 1; the flag drops afterwards, when a read's cap can widen to 2 without
196 // retroactively inflating that no-op.
198 if (continuous) {
199 this->continuous = true;
200 this->pending = 1;
201 } else {
202 this->increment_pending();
203 this->continuous = false;
204 }
205 }
206 // Address-scoped clear: keep pending and device so the sweep delivers one on_not_sent() per un-run
207 // request. An entry still waiting for a response keeps its in-flight request (whose usual terminal is
208 // still coming) and drains only its duplicates: WAITING -> WAITING_RETIRED, and INTERRUPTED ->
209 // INTERRUPTED_RETIRED which keeps distrusting late frames (they were already interrupted). Any other
210 // state -> RETIRED, draining everything. A cleared frame that then times out still honors a retry:
211 // the clear is address-scoped (any device may call it) while the retry is the owning device's call
212 // via on_no_response - the bus obeys the owner.
213 void retire() {
214 if (this->state == FrameState::WAITING) {
216 } else if (this->state == FrameState::INTERRUPTED) {
218 } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED
220 }
221 this->continuous = false;
222 }
223
224 // True while the entry is still waiting for a response; the erase pass exempts these even at pending 0.
225 bool waiting_state() const {
226 return this->state == FrameState::WAITING || this->state == FrameState::INTERRUPTED ||
228 }
229
231 if (this->pending > 0) {
232 this->pending--;
233 return true;
234 }
235 return false;
236 }
237 // Add one request, honouring the cap; false = already at cap (absorb a duplicate, restore a retry).
239 if (this->pending < this->max_pending()) {
240 this->pending++;
241 return true;
242 }
243 return false;
244 }
245
246 // Terminal/lifecycle methods: each owns its transition, callback, and pending accounting and
247 // returns whether a callback ran. Out-of-line: ModbusClientDevice is incomplete here.
248 bool sent();
249 bool response(std::span<const uint8_t> response_pdu);
250 bool error(ExceptionCode exception_code);
251 bool interrupt();
252 bool timed_out();
253 bool notify_retired();
254
256 bool same_frame(uint8_t address, std::span<const uint8_t> pdu) const {
257 const auto own_pdu = this->frame.pdu();
258 return own_pdu.size() == pdu.size() && this->frame.address() == address &&
259 memcmp(own_pdu.data(), pdu.data(), pdu.size()) == 0;
260 }
261};
262
263class ModbusClientHub : public Modbus {
264 public:
265 ModbusClientHub() = default;
266 void dump_config() override;
267 void loop() override;
268 void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
269 void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
270 bool tx_buffer_empty();
271 bool tx_blocked() override;
272 ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
273 void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
274 uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) {
275 this->queue_pdu(address,
278 device);
279 };
286 bool queue_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr,
288 // Remove before 2027.2.0. Deliberately the signature 2026.7.4 shipped - void, and no CommandOptions:
289 // the bool return and the options argument arrived after that release, so nothing external can be
290 // relying on them under this name. Callers who want the queued/refused answer move to queue_pdu().
291 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
292 "reports whether the request was accepted. Removed in 2027.2.0",
293 "2026.8.0")
294 void send_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr) {
295 this->queue_pdu(address, pdu, device);
296 }
297 ESPDEPRECATED("Use queue_pdu(payload[0], <pdu bytes>, device) instead. Removed in 2027.2.0", "2026.8.0")
298 void send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device = nullptr);
299 // Clear an address's commands; each un-run request resolves via on_not_sent(), but a frame on the
300 // wire still runs to its usual terminal. clear_tx_queue_for_device() instead discards silently.
303
304 protected:
305 int32_t tx_delay_remaining() override;
306 void parse_modbus_frames() override;
307 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
308 void send_next_frame_();
309 // Deliver owed callbacks from a quiescent hub and apply lifecycle bookkeeping; see FrameState.
310 void sweep_();
311 // The selection function: best READY entry (WRITE class first, then one-shot reads, then the
312 // least-recently-served continuous; FIFO by seq within each group), or nullptr.
314 // Locate the single entry waiting for a response (WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED).
316 // End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState.
317 void expire_waiting_();
318
319 uint16_t send_wait_time_{2000};
321
322 // Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select
323 // while it is set, so at most one frame is awaiting a response.
325
326 // Set whenever a transition leaves owed callbacks behind; quiet loop() passes skip the sweep.
327 bool sweep_needed_{false};
328 // Monotonic stamp source for ModbusDeviceCommand::seq.
329 uint16_t next_seq_{0};
330
331 // Plain append-order container; ordering lives in select_next_ready_(), lifecycle in FrameState.
332 std::deque<ModbusDeviceCommand> tx_buffer_;
333};
334
335// Transaction status: std::nullopt on success, otherwise a Modbus exception code
336using ResponseStatus = std::optional<ExceptionCode>;
337
342inline bool succeeded(ResponseStatus status) { return !status.has_value(); }
343
344// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol
345// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
346// the capacity of this type.
348
349class ModbusServerHub : public Modbus {
350 public:
351 ModbusServerHub() = default;
352 void dump_config() override;
353 void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
354
355 protected:
356 void parse_modbus_frames() override;
358 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
359 void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span<const uint8_t> data);
360 // Dispatches a broadcast (address 0) write to every registered device; broadcasts are never answered.
361 void process_broadcast_frame_(uint8_t function_code, std::span<const uint8_t> data);
362 // Parses a WRITE_SINGLE_REGISTER / WRITE_MULTIPLE_REGISTERS PDU into start_address and the host-order register
363 // values, validating the register count and address range. Returns std::nullopt on success, otherwise the Modbus
364 // exception code describing the failure. Shared by unicast writes (which reply with the exception) and broadcast
365 // writes (which silently drop invalid frames).
366 ResponseStatus parse_write_single_(std::span<const uint8_t> data, uint16_t &start_address, RegisterValues &registers);
367 ResponseStatus parse_write_multiple_(std::span<const uint8_t> data, uint16_t &start_address,
368 RegisterValues &registers);
369 // Appends the big-endian register values in values to registers, in host byte order.
370 void assemble_registers_(std::span<const uint8_t> values, RegisterValues &registers);
372 // Returns std::nullopt if [start_address, start_address + count) fits in the 16-bit address space,
373 // otherwise ILLEGAL_DATA_ADDRESS. The caller sends the exception reply if one is required - a broadcast
374 // write is never answered, so the check cannot send it itself. Shared by the register and
375 // coil/discrete-input handlers, which all address the same 16-bit space.
376 ResponseStatus check_address_range_(uint16_t start_address, uint16_t count);
377
378 // Parses a read request PDU (start address(2) + quantity(2)), shared by the register and
379 // coil/discrete-input reads so the two cannot drift apart. max_entities is the protocol ceiling for the
380 // function code; entity_name only labels the rejection log.
381 ResponseStatus parse_read_request_(std::span<const uint8_t> data, uint16_t max_entities, const LogString *entity_name,
382 uint16_t &start_address, uint16_t &count);
383
384 // Parses a single-coil write PDU (FC 0x05), which carries a 2-byte on/off value rather than packed
385 // bytes. The caller packs value into a byte it owns to build the PackedBits view the handlers take.
386 ResponseStatus parse_write_single_coil_(std::span<const uint8_t> data, uint16_t &start_address, bool &value);
387
388 // Parses a multiple-coil write PDU (FC 0x0F) into a packed-bit view pointing straight into the receive
389 // buffer, so the coil values are never copied. Both coil parsers are shared by the addressed and
390 // broadcast paths so the two validate identically.
391 ResponseStatus parse_write_multiple_coils_(std::span<const uint8_t> data, uint16_t &start_address, uint16_t &count,
392 std::span<const uint8_t> &packed_bytes);
393
394 // Builds the body of a register read response (byte count followed by the big-endian register values) into
395 // response_buffer. Shared by every function code that answers with register values, so the read reply stays
396 // identical across them. Returns false once an exception has been sent: the one the handler reported via
397 // status, or SERVICE_DEVICE_FAILURE if it returned the wrong number of registers, the count exceeds the
398 // protocol read limit, or the body does not fit.
399 bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status,
400 uint16_t number_of_registers, const RegisterValues &registers,
401 std::span<uint8_t> response_buffer, uint16_t &response_len);
402 void send_raw_(const uint8_t *payload, uint16_t len);
403 // Sends and logs the exception reply when status holds one; returns true if the request was rejected.
404 // Every parse and handler rejection funnels through here, so the reply and its log cannot drift apart.
405 bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status);
406 void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code);
407 void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
409 std::vector<ModbusServerDevice *> devices_;
410
411 // Stamp of the last "broadcast reached no device" warning, 0 until the first one is logged. Rate limiting
412 // on time rather than on address keeps the log bounded no matter how many addresses a shared bus carries.
414
415 // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time.
416 // Only one server reply can be waiting at once, so a single fixed buffer avoids heap allocation.
417 std::array<uint8_t, MAX_RAW_SIZE> deferred_payload_;
419};
420
441 public:
445 if (this->parent_ != nullptr)
446 this->clear_tx_queue_for_device();
447 }
452 void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
453 void set_address(uint8_t address) { this->address_ = address; }
458 virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
459 this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
460 }
464 virtual void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
465 this->dispatch_response_(request_pdu, {}, exception_code);
466 }
469 virtual void on_not_sent(std::span<const uint8_t> request_pdu) {
470#pragma GCC diagnostic push
471#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
472 this->on_modbus_not_sent();
473#pragma GCC diagnostic pop
474 }
476 virtual void on_sent(std::span<const uint8_t> request_pdu) {}
479 virtual bool on_no_response(std::span<const uint8_t> request_pdu) {
480#pragma GCC diagnostic push
481#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
482 return this->on_modbus_no_response();
483#pragma GCC diagnostic pop
484 }
485 // Remove before 2027.2.0
486 ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
487 virtual void on_modbus_not_sent() {}
488 // Remove before 2027.2.0
489 ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0")
490 virtual bool on_modbus_no_response() { return false; }
491
496 virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
497 ResponseStatus status) {}
498 virtual void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
499 ResponseStatus status) {
500 this->on_read_registers(EntityType::HOLDING, start_address, registers, status);
501 }
502 virtual void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
503 ResponseStatus status) {
504 this->on_read_registers(EntityType::INPUT_REGISTER, start_address, registers, status);
505 }
509 virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status) {}
510 virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {
511 this->on_read_bits(EntityType::COIL, start_address, bits, status);
512 }
513 virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) {
514 this->on_read_bits(EntityType::DISCRETE_INPUT, start_address, bits, status);
515 }
525 virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) {}
526 virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) {}
527 virtual void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
528 ResponseStatus status) {}
529 virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {}
534 virtual void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
535 ResponseStatus status);
536 ESPDEPRECATED("Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0", "2026.8.0")
537 void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
538 const uint8_t *payload = nullptr) {
539 this->parent_->queue_pdu(
540 this->address_,
542 this);
543 }
548 bool queue_pdu(std::span<const uint8_t> pdu, CommandOptions options = {}) {
549 return this->parent_->queue_pdu(this->address_, pdu, this, options);
550 }
551 // Remove before 2027.2.0. As on the hub, this is the signature 2026.7.4 shipped: void, no options.
552 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
553 "reports whether the request was accepted. Removed in 2027.2.0",
554 "2026.8.0")
555 void send_pdu(std::span<const uint8_t> pdu) { this->queue_pdu(pdu); }
556 ESPDEPRECATED("Use queue_pdu() instead (the device address is prepended for you). Removed in 2027.2.0", "2026.8.0")
557 void send_raw(const std::vector<uint8_t> &payload) {
558 if (payload.empty())
559 return; // too short to contain a PDU; refused at the door like any invalid send
560 this->parent_->queue_pdu(payload[0], std::span<const uint8_t>(payload).subspan(1), this);
561 }
562 // The typed request builders below all queue through queue_pdu(), so they share its contract: true
563 // means the request is queued and will resolve in exactly one terminal callback (except a broadcast
564 // (address 0), which is never answered and so gets only on_sent()), false means it was refused outright
565 // with no callback. Neither says the frame has been transmitted - on_sent() does.
566 // Reads via the table-appropriate function code; an unreadable entity type maps to INVALID, which
567 // create_read_pdu() rejects into an empty PDU and queue_pdu() refuses with a false return.
568 bool read_entities(EntityType entity_type, uint16_t start_address, uint16_t number_of_entities,
569 CommandOptions options = {}) {
572 options);
573 }
574 bool read_input_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
575 return this->queue_pdu(
577 }
578 bool read_holding_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
579 return this->queue_pdu(
581 }
582 bool read_coils(uint16_t start_address, uint16_t number_of_coils, CommandOptions options = {}) {
583 return this->queue_pdu(helpers::create_read_pdu(FunctionCode::READ_COILS, start_address, number_of_coils), options);
584 }
585 bool read_discrete_inputs(uint16_t start_address, uint16_t number_of_inputs, CommandOptions options = {}) {
586 return this->queue_pdu(
588 }
589 bool write_single_register(uint16_t start_address, uint16_t value) {
590 return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value));
591 }
592 bool write_single_coil(uint16_t address, bool value) {
593 return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value));
594 }
595 bool write_multiple_registers(uint16_t start_address, std::span<const uint16_t> values) {
596 return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values));
597 }
600 bool write_multiple_coils(uint16_t start_address, std::span<const bool> values) {
601 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values));
602 }
605 bool write_multiple_coils(uint16_t start_address, PackedBits bits) {
606 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits));
607 }
613 bool read_write_multiple_registers(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address,
614 std::span<const uint16_t> write_values) {
615 return this->queue_pdu(helpers::create_read_write_multiple_registers_pdu(read_start_address, read_count,
616 write_start_address, write_values));
617 }
618 inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); }
619 inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); }
620
621 // If more than one device is connected block sending a new command before a response is received
622 ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
623 bool waiting_for_response() { return !this->ready_for_immediate_send(); }
624 bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
625
626 protected:
628 void dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
629 ResponseStatus status);
630
632 uint8_t address_{0};
633 bool custom_response_warned_{false}; // first unhandled custom response warns; repeats log at VERBOSE
634};
635
636// Compatibility shim for external components written against the pre-2026.8 API, which subclassed
637// ModbusDevice and overrode on_modbus_data()/on_modbus_error(). The name is free (nothing in-tree
638// uses it), so instead of a plain alias it adapts the new span-based hooks back to the old
639// signatures: on_modbus_data() receives the response payload as an owning vector (the heap copy
640// exists only on this deprecated path) and on_modbus_error() the function code and exception code.
641// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0)
642class ESPDEPRECATED("Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0",
643 "2026.8.0") ModbusDevice : public ModbusClientDevice {
644 public:
645 using ModbusClientDevice::ModbusClientDevice;
646 virtual void on_modbus_data(const std::vector<uint8_t> &data) {}
647 virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
648
649 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
650 // Custom (user-defined) function codes historically delivered the payload starting AT the function
651 // code byte (frame data_offset 1). server_pdu_payload() drops that byte, so pass the whole PDU for
652 // them - external components match the first byte against the code they sent (issue #17994).
653 auto payload = !response_pdu.empty() && helpers::is_function_code_custom(response_pdu[0])
654 ? response_pdu
655 : helpers::server_pdu_payload(response_pdu);
656 this->on_modbus_data(std::vector<uint8_t>(payload.begin(), payload.end()));
657 }
658 void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) override {
659 this->on_modbus_error(request_pdu.empty() ? 0 : request_pdu[0], static_cast<uint8_t>(exception_code));
660 }
661};
662
664 public:
665 virtual ~ModbusServerDevice() = default;
667 // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
672 void set_address(uint8_t address) { this->address_ = address; }
673 uint8_t get_address() const { return this->address_; }
674 virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers,
675 RegisterValues &registers) {
677 };
678 virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
679 RegisterValues &registers) {
680 return this->on_read_registers(start_address, number_of_registers, registers);
681 };
682 virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
683 RegisterValues &registers) {
684 return this->on_read_registers(start_address, number_of_registers, registers);
685 };
686 virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers) {
688 };
692 virtual ResponseStatus on_read_bits(uint16_t start_address, MutablePackedBits bits) {
694 };
695 virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits) {
696 return this->on_read_bits(start_address, bits);
697 };
698 virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits) {
699 return this->on_read_bits(start_address, bits);
700 };
703 virtual ResponseStatus on_write_coils(uint16_t start_address, PackedBits bits) {
705 };
706
707 protected:
708 uint8_t address_{0};
709};
710
711} // 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:147
uint8_t * init(size_t size)
Resize to size bytes of (uninitialized) storage and return a writable pointer to fill.
Definition helpers.h:195
size_t size() const
Definition helpers.h:214
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:227
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:458
virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:529
ModbusClientDevice & operator=(ModbusClientDevice &&)=delete
virtual void on_read_holding_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:498
virtual void on_write_multiple_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:527
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:476
ModbusClientDevice(const ModbusClientDevice &)=delete
virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status)
Write acknowledgements.
Definition modbus.h:525
uint16_t uint16_t uint8_t payload_len
Definition modbus.h:537
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:509
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
Definition modbus.h:452
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:479
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:1371
ModbusClientDevice(ModbusClientDevice &&)=delete
virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:513
uint16_t uint16_t number_of_entities
Definition modbus.h:537
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") virtual bool on_modbus_no_response()
Definition modbus.h:489
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:496
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:464
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
Definition modbus.h:443
void set_address(uint8_t address)
Definition modbus.h:453
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:469
virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:510
virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status)
Definition modbus.h:526
uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:538
virtual void on_read_input_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:502
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent()
Definition modbus.h:486
uint8_t uint16_t start_address
Definition modbus.h:273
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:1157
void parse_modbus_frames() override
Definition modbus.cpp:174
ModbusDeviceCommand * find_waiting_()
Definition modbus.cpp:903
std::span< const uint8_t > pdu
Definition modbus.h:294
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
Definition modbus.h:274
uint8_t uint16_t uint16_t number_of_entities
Definition modbus.h:273
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:911
uint8_t uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:274
int32_t tx_delay_remaining() override
Definition modbus.cpp:118
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:332
ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0") void send(uint8_t address
uint8_t uint16_t uint16_t uint8_t payload_len
Definition modbus.h:274
bool queue_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr, CommandOptions options={})
Queue a request.
Definition modbus.cpp:1045
void clear_tx_queue_for_address(uint8_t address)
Definition modbus.cpp:1147
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:318
void set_send_wait_time(uint16_t time_in_ms)
Definition modbus.h:268
void set_turnaround_time(uint16_t time_in_ms)
Definition modbus.h:269
void set_flow_control_pin(GPIOPin *flow_control_pin)
Definition modbus.h:68
void setup() override
Definition modbus.cpp:24
uint16_t frame_delay_ms_
Definition modbus.h:91
virtual void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu)=0
bool parse_modbus_server_frame_()
Definition modbus.cpp:245
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:780
uint32_t last_modbus_byte_
Definition modbus.h:87
GPIOPin * flow_control_pin_
Definition modbus.h:94
uint32_t last_send_tx_offset_
Definition modbus.h:90
virtual bool tx_blocked()
Definition modbus.cpp:126
void clear_rx_buffer_(const LogString *reason, bool warn=false, size_t bytes_to_clear=0)
Definition modbus.cpp:1208
void loop() override
Definition modbus.cpp:45
float get_setup_priority() const override
Definition modbus.cpp:862
uint16_t long_rx_buffer_delay_ms_
Definition modbus.h:92
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:106
uint16_t find_frame_end_by_crc_(uint16_t min_length) const
Definition modbus.cpp:222
std::vector< uint8_t > rx_buffer_
Definition modbus.h:96
uint32_t last_receive_check_
Definition modbus.h:88
ModbusServerDevice & operator=(ModbusServerDevice &&)=delete
virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:695
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
void set_address(uint8_t address)
Definition modbus.h:672
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:686
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:692
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:682
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:674
virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:698
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:703
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:678
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:409
ResponseStatus check_address_range_(uint16_t start_address, uint16_t count)
Definition modbus.cpp:395
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:441
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:610
void parse_modbus_frames() override
Definition modbus.cpp:187
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:370
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:468
void process_broadcast_frame_(uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:493
void register_device(ModbusServerDevice *device)
Definition modbus.h:353
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:386
ResponseStatus parse_write_single_coil_(std::span< const uint8_t > data, uint16_t &start_address, bool &value)
Definition modbus.cpp:455
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:567
void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code)
Definition modbus.cpp:895
void assemble_registers_(std::span< const uint8_t > values, RegisterValues &registers)
Definition modbus.cpp:487
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:1177
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:867
ResponseStatus parse_write_multiple_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:424
bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status)
Definition modbus.cpp:882
ResponseStatus parse_write_single_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:416
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
Definition modbus.h:417
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
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.
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...
bool is_function_code_exception(uint8_t function_code)
std::optional< ExceptionCode > ResponseStatus
Definition modbus.h:336
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:342
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:86
const void size_t len
Definition hal.h:64
uint16_t size
Definition helpers.cpp:25
STL namespace.
static void uint32_t
bool response(std::span< const uint8_t > response_pdu)
Definition modbus.cpp:946
CommandPriority priority() const
Definition modbus.h:147
void make_continuous(bool continuous)
Definition modbus.h:197
static CommandPriority classify(uint8_t function_code)
Definition modbus.h:151
bool error(ExceptionCode exception_code)
Definition modbus.cpp:957
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:256
ModbusClientDevice * device
Definition modbus.h:127
ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span< const uint8_t > pdu, bool continuous=false, uint16_t seq=0)
Definition modbus.h:139
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
Definition modbus.h:39
uint8_t address() const
Definition modbus.h:51
SmallInlineBuffer< MODBUS_FRAME_INLINE_SIZE > data
Definition modbus.h:37
std::span< const uint8_t > pdu() const
The PDU: function code + data, without address or CRC.
Definition modbus.h:55
uint16_t size() const
Definition modbus.h:48