ESPHome 2026.9.0
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;
255 ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
256 void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
257 uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) {
258 this->queue_pdu(address,
261 device);
262 };
266 bool queue_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr,
268 // Remove before 2027.2.0. Deliberately the void, no-options signature 2026.7.4 shipped: nothing
269 // external can rely on the later additions under this name.
270 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
271 "reports whether the request was accepted. Removed in 2027.2.0",
272 "2026.8.0")
273 void send_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr) {
274 this->queue_pdu(address, pdu, device);
275 }
276 ESPDEPRECATED("Use queue_pdu(payload[0], <pdu bytes>, device) instead. Removed in 2027.2.0", "2026.8.0")
277 void send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device = nullptr);
278 // Clear all commands matching the given address; each unsent request resolves via on_not_sent(), but a
279 // frame on the wire still runs to its usual terminal.
281 // Clear all commands for a given device; no callbacks are delivered.
283
284 protected:
285 int32_t tx_delay_remaining() override;
286 void parse_modbus_frames() override;
287 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
288 void send_next_frame_();
289 // Deliver owed callbacks from a quiescent hub and apply lifecycle bookkeeping; see FrameState.
290 void sweep_();
291 // The selection function: best READY entry (ordered by priority; FIFO by seq within each group), or nullptr.
293 // Locate the single entry waiting for a response (WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED).
295 // End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState.
296 void expire_waiting_();
297
300
301 // Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select
302 // while it is set, so at most one frame is awaiting a response.
304
305 // Set whenever a transition leaves owed callbacks behind; quiet loop() passes skip the sweep.
306 bool sweep_needed_{false};
307 // Monotonic stamp source for ModbusDeviceCommand::seq.
308 uint16_t next_seq_{0};
309
310 // Plain append-order container; ordering lives in select_next_ready_(), lifecycle in FrameState.
311 std::deque<ModbusDeviceCommand> tx_buffer_;
312};
313
314// Transaction status: std::nullopt on success, otherwise a Modbus exception code
315using ResponseStatus = std::optional<ExceptionCode>;
316
318inline bool succeeded(ResponseStatus status) { return !status.has_value(); }
319
320// Register values exchanged with server handlers, in address order. Sized at the larger of the two protocol
321// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
322// the capacity of this type.
324
325class ModbusServerHub : public Modbus {
326 public:
327 ModbusServerHub() = default;
328 void dump_config() override;
329 void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
330
331 protected:
332 void parse_modbus_frames() override;
334 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
335 void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span<const uint8_t> data);
336 // Dispatches a broadcast (address 0) write to every registered device; broadcasts are never answered.
337 void process_broadcast_frame_(uint8_t function_code, std::span<const uint8_t> data);
338 // Parses a WRITE_SINGLE_REGISTER / WRITE_MULTIPLE_REGISTERS PDU into start_address and the address order register
339 // values, validating the register count and address range. Shared by unicast and broadcast writes.
340 ResponseStatus parse_write_single_(std::span<const uint8_t> data, uint16_t &start_address, RegisterValues &registers);
341 ResponseStatus parse_write_multiple_(std::span<const uint8_t> data, uint16_t &start_address,
342 RegisterValues &registers);
343 // Assembles host-order registers from the big-endian bytes in values and appends them to registers.
344 void assemble_registers_(std::span<const uint8_t> values, RegisterValues &registers);
346 // Returns std::nullopt if [start_address, start_address + count) fits in a 16-bit address space, otherwise
347 // ILLEGAL_DATA_ADDRESS. The caller sends the exception reply if one is required. Shared by the
348 // register/coil/discrete-input handlers, which all use a 16-bit address space.
349 ResponseStatus check_address_range_(uint16_t start_address, uint16_t count);
350
351 // Parses read request data. max_entities is the protocol ceiling for the function code; entity_name labels
352 // the rejection log.
353 ResponseStatus parse_read_request_(std::span<const uint8_t> data, uint16_t max_entities, const LogString *entity_name,
354 uint16_t &start_address, uint16_t &count);
355
356 // Parses single-coil write data
357 ResponseStatus parse_write_single_coil_(std::span<const uint8_t> data, uint16_t &start_address, bool &value);
358
359 // Parses write-multiple-coil data into a packed-bit view pointing straight into the receive buffer, so the
360 // coil values are never copied.
361 ResponseStatus parse_write_multiple_coils_(std::span<const uint8_t> data, uint16_t &start_address, uint16_t &count,
362 std::span<const uint8_t> &packed_bytes);
363
364 // Builds the body of a register read response into response_buffer. Returns false once an exception has
365 // been sent: the one the handler reported via status, or SERVICE_DEVICE_FAILURE if it returned the wrong
366 // number of registers, the count exceeds the protocol read limit, or the body does not fit.
367 bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status,
368 uint16_t number_of_registers, const RegisterValues &registers,
369 std::span<uint8_t> response_buffer, uint16_t &response_len);
370 void send_raw_(const uint8_t *payload, uint16_t len);
371 // Sends and logs the exception reply when status holds one; returns true if the request was rejected.
372 bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status);
373 void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code);
374 void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
376 std::vector<ModbusServerDevice *> devices_;
377
378 // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time.
379 // Only one server reply can be waiting at once, so a single fixed buffer avoids heap allocation.
380 std::array<uint8_t, MAX_RAW_SIZE> deferred_payload_;
382};
383
404 public:
408 if (this->parent_ != nullptr)
409 this->clear_tx_queue_for_device();
410 }
415 void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
416 void set_address(uint8_t address) { this->address_ = address; }
421 virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
422 this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
423 }
427 virtual void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
428 this->dispatch_response_(request_pdu, {}, exception_code);
429 }
432 virtual void on_not_sent(std::span<const uint8_t> request_pdu) {
433#pragma GCC diagnostic push
434#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
435 this->on_modbus_not_sent();
436#pragma GCC diagnostic pop
437 }
439 virtual void on_sent(std::span<const uint8_t> request_pdu) {}
442 virtual bool on_no_response(std::span<const uint8_t> request_pdu) {
443#pragma GCC diagnostic push
444#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
445 return this->on_modbus_no_response();
446#pragma GCC diagnostic pop
447 }
448 // Remove before 2027.2.0
449 ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
450 virtual void on_modbus_not_sent() {}
451 // Remove before 2027.2.0
452 ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0")
453 virtual bool on_modbus_no_response() { return false; }
454
459 virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
460 ResponseStatus status) {}
461 virtual void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
462 ResponseStatus status) {
463 this->on_read_registers(EntityType::HOLDING, start_address, registers, status);
464 }
465 virtual void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
466 ResponseStatus status) {
467 this->on_read_registers(EntityType::INPUT_REGISTER, start_address, registers, status);
468 }
472 virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status) {}
473 virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {
474 this->on_read_bits(EntityType::COIL, start_address, bits, status);
475 }
476 virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) {
477 this->on_read_bits(EntityType::DISCRETE_INPUT, start_address, bits, status);
478 }
488 virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) {}
489 virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) {}
490 virtual void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
491 ResponseStatus status) {}
492 virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {}
497 virtual void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
498 ResponseStatus status);
499 ESPDEPRECATED("Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0", "2026.8.0")
500 void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
501 const uint8_t *payload = nullptr) {
502 this->parent_->queue_pdu(
503 this->address_,
505 this);
506 }
508 bool queue_pdu(std::span<const uint8_t> pdu, CommandOptions options = {}) {
509 return this->parent_->queue_pdu(this->address_, pdu, this, options);
510 }
511 // Remove before 2027.2.0. As on the hub, this is the signature 2026.7.4 shipped: void, no options.
512 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
513 "reports whether the request was accepted. Removed in 2027.2.0",
514 "2026.8.0")
515 void send_pdu(std::span<const uint8_t> pdu) { this->queue_pdu(pdu); }
516 ESPDEPRECATED("Use queue_pdu() instead (the device address is prepended for you). Removed in 2027.2.0", "2026.8.0")
517 void send_raw(const std::vector<uint8_t> &payload) {
518 if (payload.empty())
519 return; // too short to contain a PDU; refused at the door like any invalid send
520 this->parent_->queue_pdu(payload[0], std::span<const uint8_t>(payload).subspan(1), this);
521 }
522 // The typed request builders below all queue through queue_pdu() and share its return contract.
523 // Reads use the table-appropriate function code; an unreadable entity type maps to INVALID, which
524 // create_read_pdu() rejects into an empty PDU and queue_pdu() refuses with a false return.
525 bool read_entities(EntityType entity_type, uint16_t start_address, uint16_t number_of_entities,
526 CommandOptions options = {}) {
529 options);
530 }
531 bool read_input_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
532 return this->queue_pdu(
534 }
535 bool read_holding_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
536 return this->queue_pdu(
538 }
539 bool read_coils(uint16_t start_address, uint16_t number_of_coils, CommandOptions options = {}) {
540 return this->queue_pdu(helpers::create_read_pdu(FunctionCode::READ_COILS, start_address, number_of_coils), options);
541 }
542 bool read_discrete_inputs(uint16_t start_address, uint16_t number_of_inputs, CommandOptions options = {}) {
543 return this->queue_pdu(
545 }
546 bool write_single_register(uint16_t start_address, uint16_t value, CommandOptions options = {}) {
547 return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value), options);
548 }
549 bool write_single_coil(uint16_t address, bool value, CommandOptions options = {}) {
550 return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value), options);
551 }
552 bool write_multiple_registers(uint16_t start_address, std::span<const uint16_t> values, CommandOptions options = {}) {
553 // Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's.
554 if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS)
555 return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values), options);
556 return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values), options);
557 }
560 bool write_multiple_coils(uint16_t start_address, std::span<const bool> values, CommandOptions options = {}) {
561 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values), options);
562 }
565 bool write_multiple_coils(uint16_t start_address, PackedBits bits, CommandOptions options = {}) {
566 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits), options);
567 }
571 bool read_write_multiple_registers(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address,
572 std::span<const uint16_t> write_values) {
573 return this->queue_pdu(helpers::create_read_write_multiple_registers_pdu(read_start_address, read_count,
574 write_start_address, write_values));
575 }
576 inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); }
577 inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); }
578
579 bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
580
581 protected:
583 void dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
584 ResponseStatus status);
585
587 uint8_t address_{0};
588 bool custom_response_warned_{false}; // first unhandled custom response warns; repeats log at VERBOSE
589};
590
591// Compatibility shim adapting the span-based hooks back to the pre-2026.8 on_modbus_data()/
592// on_modbus_error() signatures (the owning-vector heap copy exists only on this deprecated path).
593// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0).
594class ESPDEPRECATED("Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0",
595 "2026.8.0") ModbusDevice : public ModbusClientDevice {
596 public:
597 using ModbusClientDevice::ModbusClientDevice;
598 virtual void on_modbus_data(const std::vector<uint8_t> &data) {}
599 virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
600
601 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
602 // Custom (user-defined) function codes historically delivered the payload starting AT the function
603 // code byte (frame data_offset 1). server_pdu_payload() drops that byte, so pass the whole PDU for
604 // them - external components match the first byte against the code they sent (issue #17994).
605 auto payload = !response_pdu.empty() && helpers::is_function_code_custom(response_pdu[0])
606 ? response_pdu
607 : helpers::server_pdu_payload(response_pdu);
608 this->on_modbus_data(std::vector<uint8_t>(payload.begin(), payload.end()));
609 }
610 void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) override {
611 this->on_modbus_error(request_pdu.empty() ? 0 : request_pdu[0], static_cast<uint8_t>(exception_code));
612 }
613};
614
616 public:
617 virtual ~ModbusServerDevice() = default;
619 // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
624 void set_address(uint8_t address) { this->address_ = address; }
625 uint8_t get_address() const { return this->address_; }
626 virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers,
627 RegisterValues &registers) {
629 };
630 virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
631 RegisterValues &registers) {
632 return this->on_read_registers(start_address, number_of_registers, registers);
633 };
634 virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
635 RegisterValues &registers) {
636 return this->on_read_registers(start_address, number_of_registers, registers);
637 };
638 virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers) {
640 };
644 virtual ResponseStatus on_read_bits(uint16_t start_address, MutablePackedBits bits) {
646 };
647 virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits) {
648 return this->on_read_bits(start_address, bits);
649 };
650 virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits) {
651 return this->on_read_bits(start_address, bits);
652 };
655 virtual ResponseStatus on_write_coils(uint16_t start_address, PackedBits bits) {
657 };
658
659 protected:
660 uint8_t address_{0};
661};
662
663} // 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:152
uint8_t * init(size_t size)
Resize to size bytes of (uninitialized) storage and return a writable pointer to fill.
Definition helpers.h:200
size_t size() const
Definition helpers.h:219
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:232
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:421
virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:492
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:461
virtual void on_write_multiple_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:490
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:439
ModbusClientDevice(const ModbusClientDevice &)=delete
virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status)
Write acknowledgements.
Definition modbus.h:488
uint16_t uint16_t uint8_t payload_len
Definition modbus.h:500
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:472
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
Definition modbus.h:415
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:442
virtual void on_custom_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu, ResponseStatus status)
Catch-all for custom function codes and anything that is not a standard-conformant transaction (see d...
Definition modbus.cpp:1393
ModbusClientDevice(ModbusClientDevice &&)=delete
virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:476
uint16_t uint16_t number_of_entities
Definition modbus.h:500
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") virtual bool on_modbus_no_response()
Definition modbus.h:452
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:459
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:427
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
Definition modbus.h:406
void set_address(uint8_t address)
Definition modbus.h:416
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:432
virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:473
virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status)
Definition modbus.h:489
uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:501
virtual void on_read_input_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:465
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent()
Definition modbus.h:449
uint8_t uint16_t start_address
Definition modbus.h:256
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:1175
void parse_modbus_frames() override
Definition modbus.cpp:191
ModbusDeviceCommand * find_waiting_()
Definition modbus.cpp:914
std::span< const uint8_t > pdu
Definition modbus.h:273
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
Definition modbus.h:257
uint8_t uint16_t uint16_t number_of_entities
Definition modbus.h:256
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:922
uint8_t uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:257
int32_t tx_delay_remaining() override
Definition modbus.cpp:138
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:311
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:257
bool queue_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr, CommandOptions options={})
Queue a request.
Definition modbus.cpp:1058
void clear_tx_queue_for_address(uint8_t address)
Definition modbus.cpp:1165
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:334
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:29
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:261
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:777
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:146
void clear_rx_buffer_(const LogString *reason, bool warn=false, size_t bytes_to_clear=0)
Definition modbus.cpp:1229
void loop() override
Definition modbus.cpp:61
uint32_t frame_delay_us_
Definition modbus.h:78
float get_setup_priority() const override
Definition modbus.cpp:873
bool exceeded_rx_full_threshold_
Definition modbus.h:84
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:132
uint16_t find_frame_end_by_crc_(uint16_t min_length) const
Definition modbus.cpp:241
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:647
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
void set_address(uint8_t address)
Definition modbus.h:624
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:638
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:644
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:634
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:626
virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:650
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:655
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:630
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:376
ResponseStatus check_address_range_(uint16_t start_address, uint16_t count)
Definition modbus.cpp:411
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:457
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:607
void parse_modbus_frames() override
Definition modbus.cpp:204
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:386
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:484
void process_broadcast_frame_(uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:509
void register_device(ModbusServerDevice *device)
Definition modbus.h:329
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:402
ResponseStatus parse_write_single_coil_(std::span< const uint8_t > data, uint16_t &start_address, bool &value)
Definition modbus.cpp:471
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:564
void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code)
Definition modbus.cpp:906
void assemble_registers_(std::span< const uint8_t > values, RegisterValues &registers)
Definition modbus.cpp:503
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:1195
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:878
ResponseStatus parse_write_multiple_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:440
bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status)
Definition modbus.cpp:893
ResponseStatus parse_write_single_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:432
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
Definition modbus.h:380
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.
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:315
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:318
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:957
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:968
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