ESPHome 2026.10.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().
116 // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes.
117 bool continuous{false};
118};
119
123 // Place-in-line stamp (hub's free-running counter); selection takes the oldest for round-robin
124 // fairness within a class. Meant to wrap.
125 uint16_t seq{0};
127 // Accepted requests this entry stands for, capped at max_pending(); drains one terminal each.
128 // A continuous poll is a subscription: pending fixed at 1, removed only by cancellation or failure.
129 uint8_t pending{1};
130 // The entry's LIVE effective options, not a record of the caller's request
132
133 // Build a command from a PDU span (caller bounds it to MAX_PDU_SIZE) and pre-normalized options;
134 // fully initialized here.
135 ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span<const uint8_t> pdu,
136 CommandOptions options = {}, uint16_t seq = 0)
137 : device(device), frame(address, pdu.data(), static_cast<uint16_t>(pdu.size())), seq(seq), options(options) {}
138
140 if (this->options.continuous)
142 if (helpers::is_function_code_write(this->frame.pdu()[0])) {
144 }
146 }
147
148 // Requests this entry can serve
149 uint8_t max_pending() const {
150 const uint8_t fc = this->frame.pdu()[0];
151 return (helpers::is_function_code_read_only(fc) && !this->options.continuous) ? 2 : 1;
152 }
153 // Device-scoped clear: detach with no callback. An entry still waiting for a response keeps its state as a
154 // reply-ignoring shell that resolves silently; any other goes RETIRED.
156 if (!this->waiting_state())
158 this->pending = 0;
159 this->device = nullptr;
160 }
161 // Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already
162 // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with no terminal callback.
165 this->pending = 0;
166 }
167 // Re-ready for another transmission, restamped to the tail of its class
168 void requeue(uint16_t seq) {
169 this->state = FrameState::READY;
170 this->seq = seq;
171 }
172 // Re-task a frame that lives on: upgrade a one-shot to a continuous poll, or downgrade a poll back to
173 // a one-shot.
174 void make_continuous(bool continuous) {
175 if (continuous) {
176 this->options.continuous = true;
177 this->pending = 1;
178 } else {
179 this->increment_pending();
180 this->options.continuous = false;
181 }
182 }
183 // Address-scoped clear: keep pending and device so the sweep delivers one on_not_sent() per un-delivered
184 // request. An entry still waiting for a response keeps its in-flight request (whose usual terminal is
185 // still coming) and drains only its duplicates.
186 void retire() {
187 if (this->state == FrameState::WAITING) {
189 } else if (this->state == FrameState::INTERRUPTED) {
191 } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED
193 }
194 this->options = {}; // reset every option
195 }
196
197 // True while the entry is still waiting for a response
198 bool waiting_state() const {
199 return this->state == FrameState::WAITING || this->state == FrameState::INTERRUPTED ||
201 }
202
204 if (this->pending > 0) {
205 this->pending--;
206 return true;
207 }
208 return false;
209 }
210
212 if (this->pending < this->max_pending()) {
213 this->pending++;
214 return true;
215 }
216 return false;
217 }
218
219 // Terminal/lifecycle methods: each owns its transition, callback, and pending accounting and
220 // returns whether a callback ran.
221 bool sent();
222 bool response(std::span<const uint8_t> response_pdu);
223 bool error(ExceptionCode exception_code);
224 bool interrupt();
225 bool timed_out();
226 bool notify_retired();
227
229 bool same_frame(uint8_t address, std::span<const uint8_t> pdu) const {
230 const auto own_pdu = this->frame.pdu();
231 return own_pdu.size() == pdu.size() && this->frame.address() == address &&
232 memcmp(own_pdu.data(), pdu.data(), pdu.size()) == 0;
233 }
234};
235
236class ModbusClientHub : public Modbus {
237 public:
238 ModbusClientHub() = default;
239 void dump_config() override;
240 void loop() override;
241 // Config arrives in milliseconds; stored internally in microseconds like all other timing.
242 void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_us_ = time_in_ms * 1000UL; }
243 void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; }
244 bool tx_buffer_empty();
245 bool tx_blocked() override;
246 ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
247 void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
248 uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) {
249 this->queue_pdu(address,
252 device);
253 };
257 bool queue_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr,
259 // Remove before 2027.2.0. Deliberately the void, no-options signature 2026.7.4 shipped: nothing
260 // external can rely on the later additions under this name.
261 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
262 "reports whether the request was accepted. Removed in 2027.2.0",
263 "2026.8.0")
264 void send_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr) {
265 this->queue_pdu(address, pdu, device);
266 }
267 ESPDEPRECATED("Use queue_pdu(payload[0], <pdu bytes>, device) instead. Removed in 2027.2.0", "2026.8.0")
268 void send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device = nullptr);
269 // Clear all commands matching the given address; each unsent request resolves via on_not_sent(), but a
270 // frame on the wire still runs to its usual terminal.
272 // Clear all commands for a given device; no callbacks are delivered.
274
275 protected:
276 int32_t tx_delay_remaining() override;
277 void parse_modbus_frames() override;
278 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
279 void send_next_frame_();
280 // Deliver owed callbacks from a quiescent hub and apply lifecycle bookkeeping; see FrameState.
281 void sweep_();
282 // The selection function: best READY entry (ordered by priority; FIFO by seq within each group), or nullptr.
284 // Locate the single entry waiting for a response (WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED).
286 // End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState.
287 void expire_waiting_();
288
291
292 // Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select
293 // while it is set, so at most one frame is awaiting a response.
295
296 // Set whenever a transition leaves owed callbacks behind; quiet loop() passes skip the sweep.
297 bool sweep_needed_{false};
298 // Monotonic stamp source for ModbusDeviceCommand::seq.
299 uint16_t next_seq_{0};
300
301 // Plain append-order container; ordering lives in select_next_ready_(), lifecycle in FrameState.
302 std::deque<ModbusDeviceCommand> tx_buffer_;
303};
304
305// Transaction status: std::nullopt on success, otherwise a Modbus exception code
306using ResponseStatus = std::optional<ExceptionCode>;
307
309inline bool succeeded(ResponseStatus status) { return !status.has_value(); }
310
311// Register values exchanged with server handlers, in address order. Sized at the larger of the two protocol
312// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
313// the capacity of this type.
315
316class ModbusServerHub : public Modbus {
317 public:
318 ModbusServerHub() = default;
319 void dump_config() override;
320 void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
321
322 protected:
323 void parse_modbus_frames() override;
325 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
326 void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span<const uint8_t> data);
327 // Dispatches a broadcast (address 0) write to every registered device; broadcasts are never answered.
328 void process_broadcast_frame_(uint8_t function_code, std::span<const uint8_t> data);
329 // Parses a WRITE_SINGLE_REGISTER / WRITE_MULTIPLE_REGISTERS PDU into start_address and the address order register
330 // values, validating the register count and address range. Shared by unicast and broadcast writes.
331 ResponseStatus parse_write_single_(std::span<const uint8_t> data, uint16_t &start_address, RegisterValues &registers);
332 ResponseStatus parse_write_multiple_(std::span<const uint8_t> data, uint16_t &start_address,
333 RegisterValues &registers);
334 // Assembles host-order registers from the big-endian bytes in values and appends them to registers.
335 void assemble_registers_(std::span<const uint8_t> values, RegisterValues &registers);
337 // Returns std::nullopt if [start_address, start_address + count) fits in a 16-bit address space, otherwise
338 // ILLEGAL_DATA_ADDRESS. The caller sends the exception reply if one is required. Shared by the
339 // register/coil/discrete-input handlers, which all use a 16-bit address space.
340 ResponseStatus check_address_range_(uint16_t start_address, uint16_t count);
341
342 // Parses read request data. max_entities is the protocol ceiling for the function code; entity_name labels
343 // the rejection log.
344 ResponseStatus parse_read_request_(std::span<const uint8_t> data, uint16_t max_entities, const LogString *entity_name,
345 uint16_t &start_address, uint16_t &count);
346
347 // Parses single-coil write data
348 ResponseStatus parse_write_single_coil_(std::span<const uint8_t> data, uint16_t &start_address, bool &value);
349
350 // Parses write-multiple-coil data into a packed-bit view pointing straight into the receive buffer, so the
351 // coil values are never copied.
352 ResponseStatus parse_write_multiple_coils_(std::span<const uint8_t> data, uint16_t &start_address, uint16_t &count,
353 std::span<const uint8_t> &packed_bytes);
354
355 // Builds the body of a register read response into response_buffer. Returns false once an exception has
356 // been sent: the one the handler reported via status, or SERVICE_DEVICE_FAILURE if it returned the wrong
357 // number of registers, the count exceeds the protocol read limit, or the body does not fit.
358 bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status,
359 uint16_t number_of_registers, const RegisterValues &registers,
360 std::span<uint8_t> response_buffer, uint16_t &response_len);
361 void send_raw_(const uint8_t *payload, uint16_t len);
362 // Sends and logs the exception reply when status holds one; returns true if the request was rejected.
363 bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status);
364 void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code);
365 void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
367 std::vector<ModbusServerDevice *> devices_;
368
369 // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time.
370 // Only one server reply can be waiting at once, so a single fixed buffer avoids heap allocation.
371 std::array<uint8_t, MAX_RAW_SIZE> deferred_payload_;
373};
374
395 public:
399 if (this->parent_ != nullptr)
400 this->clear_tx_queue_for_device();
401 }
406 void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
407 void set_address(uint8_t address) { this->address_ = address; }
412 virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
413 this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
414 }
418 virtual void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) {
419 this->dispatch_response_(request_pdu, {}, exception_code);
420 }
423 virtual void on_not_sent(std::span<const uint8_t> request_pdu) {
424#pragma GCC diagnostic push
425#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
426 this->on_modbus_not_sent();
427#pragma GCC diagnostic pop
428 }
430 virtual void on_sent(std::span<const uint8_t> request_pdu) {}
433 virtual bool on_no_response(std::span<const uint8_t> request_pdu) {
434#pragma GCC diagnostic push
435#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
436 return this->on_modbus_no_response();
437#pragma GCC diagnostic pop
438 }
439 // Remove before 2027.2.0
440 ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
441 virtual void on_modbus_not_sent() {}
442 // Remove before 2027.2.0
443 ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0")
444 virtual bool on_modbus_no_response() { return false; }
445
450 virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span<const uint16_t> registers,
451 ResponseStatus status) {}
452 virtual void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
453 ResponseStatus status) {
454 this->on_read_registers(EntityType::HOLDING, start_address, registers, status);
455 }
456 virtual void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
457 ResponseStatus status) {
458 this->on_read_registers(EntityType::INPUT_REGISTER, start_address, registers, status);
459 }
463 virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status) {}
464 virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {
465 this->on_read_bits(EntityType::COIL, start_address, bits, status);
466 }
467 virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status) {
468 this->on_read_bits(EntityType::DISCRETE_INPUT, start_address, bits, status);
469 }
479 virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status) {}
480 virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status) {}
481 virtual void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
482 ResponseStatus status) {}
483 virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status) {}
488 virtual void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
489 ResponseStatus status);
490 ESPDEPRECATED("Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0", "2026.8.0")
491 void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
492 const uint8_t *payload = nullptr) {
493 this->parent_->queue_pdu(
494 this->address_,
496 this);
497 }
499 bool queue_pdu(std::span<const uint8_t> pdu, CommandOptions options = {}) {
500 return this->parent_->queue_pdu(this->address_, pdu, this, options);
501 }
502 // Remove before 2027.2.0. As on the hub, this is the signature 2026.7.4 shipped: void, no options.
503 ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it "
504 "reports whether the request was accepted. Removed in 2027.2.0",
505 "2026.8.0")
506 void send_pdu(std::span<const uint8_t> pdu) { this->queue_pdu(pdu); }
507 ESPDEPRECATED("Use queue_pdu() instead (the device address is prepended for you). Removed in 2027.2.0", "2026.8.0")
508 void send_raw(const std::vector<uint8_t> &payload) {
509 if (payload.empty())
510 return; // too short to contain a PDU; refused at the door like any invalid send
511 this->parent_->queue_pdu(payload[0], std::span<const uint8_t>(payload).subspan(1), this);
512 }
513 // The typed request builders below all queue through queue_pdu() and share its return contract.
514 // Reads use the table-appropriate function code; an unreadable entity type maps to INVALID, which
515 // create_read_pdu() rejects into an empty PDU and queue_pdu() refuses with a false return.
516 bool read_entities(EntityType entity_type, uint16_t start_address, uint16_t number_of_entities,
517 CommandOptions options = {}) {
520 options);
521 }
522 bool read_input_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
523 return this->queue_pdu(
525 }
526 bool read_holding_registers(uint16_t start_address, uint16_t number_of_registers, CommandOptions options = {}) {
527 return this->queue_pdu(
529 }
530 bool read_coils(uint16_t start_address, uint16_t number_of_coils, CommandOptions options = {}) {
531 return this->queue_pdu(helpers::create_read_pdu(FunctionCode::READ_COILS, start_address, number_of_coils), options);
532 }
533 bool read_discrete_inputs(uint16_t start_address, uint16_t number_of_inputs, CommandOptions options = {}) {
534 return this->queue_pdu(
536 }
537 bool write_single_register(uint16_t start_address, uint16_t value) {
538 return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value));
539 }
540 bool write_single_coil(uint16_t address, bool value) {
541 return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value));
542 }
543 bool write_multiple_registers(uint16_t start_address, std::span<const uint16_t> values) {
544 // Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's.
545 if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS)
546 return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values));
547 return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values));
548 }
551 bool write_multiple_coils(uint16_t start_address, std::span<const bool> values) {
552 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values));
553 }
556 bool write_multiple_coils(uint16_t start_address, PackedBits bits) {
557 return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits));
558 }
562 bool read_write_multiple_registers(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address,
563 std::span<const uint16_t> write_values) {
564 return this->queue_pdu(helpers::create_read_write_multiple_registers_pdu(read_start_address, read_count,
565 write_start_address, write_values));
566 }
567 inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); }
568 inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); }
569
570 bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
571
572 protected:
574 void dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
575 ResponseStatus status);
576
578 uint8_t address_{0};
579 bool custom_response_warned_{false}; // first unhandled custom response warns; repeats log at VERBOSE
580};
581
582// Compatibility shim adapting the span-based hooks back to the pre-2026.8 on_modbus_data()/
583// on_modbus_error() signatures (the owning-vector heap copy exists only on this deprecated path).
584// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0).
585class ESPDEPRECATED("Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0",
586 "2026.8.0") ModbusDevice : public ModbusClientDevice {
587 public:
588 using ModbusClientDevice::ModbusClientDevice;
589 virtual void on_modbus_data(const std::vector<uint8_t> &data) {}
590 virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
591
592 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
593 // Custom (user-defined) function codes historically delivered the payload starting AT the function
594 // code byte (frame data_offset 1). server_pdu_payload() drops that byte, so pass the whole PDU for
595 // them - external components match the first byte against the code they sent (issue #17994).
596 auto payload = !response_pdu.empty() && helpers::is_function_code_custom(response_pdu[0])
597 ? response_pdu
598 : helpers::server_pdu_payload(response_pdu);
599 this->on_modbus_data(std::vector<uint8_t>(payload.begin(), payload.end()));
600 }
601 void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code) override {
602 this->on_modbus_error(request_pdu.empty() ? 0 : request_pdu[0], static_cast<uint8_t>(exception_code));
603 }
604};
605
607 public:
608 virtual ~ModbusServerDevice() = default;
610 // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
615 void set_address(uint8_t address) { this->address_ = address; }
616 uint8_t get_address() const { return this->address_; }
617 virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers,
618 RegisterValues &registers) {
620 };
621 virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
622 RegisterValues &registers) {
623 return this->on_read_registers(start_address, number_of_registers, registers);
624 };
625 virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
626 RegisterValues &registers) {
627 return this->on_read_registers(start_address, number_of_registers, registers);
628 };
629 virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers) {
631 };
635 virtual ResponseStatus on_read_bits(uint16_t start_address, MutablePackedBits bits) {
637 };
638 virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits) {
639 return this->on_read_bits(start_address, bits);
640 };
641 virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits) {
642 return this->on_read_bits(start_address, bits);
643 };
646 virtual ResponseStatus on_write_coils(uint16_t start_address, PackedBits bits) {
648 };
649
650 protected:
651 uint8_t address_{0};
652};
653
654} // 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:412
virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:483
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:452
virtual void on_write_multiple_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:481
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:430
ModbusClientDevice(const ModbusClientDevice &)=delete
virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status)
Write acknowledgements.
Definition modbus.h:479
uint16_t uint16_t uint8_t payload_len
Definition modbus.h:491
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:463
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
Definition modbus.h:406
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:433
virtual void on_custom_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu, ResponseStatus status)
Catch-all for custom function codes and anything that is not a standard-conformant transaction (see d...
Definition modbus.cpp:1379
ModbusClientDevice(ModbusClientDevice &&)=delete
virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:467
uint16_t uint16_t number_of_entities
Definition modbus.h:491
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") virtual bool on_modbus_no_response()
Definition modbus.h:443
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:450
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:418
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
Definition modbus.h:397
void set_address(uint8_t address)
Definition modbus.h:407
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:423
virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:464
virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status)
Definition modbus.h:480
uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:492
virtual void on_read_input_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:456
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent()
Definition modbus.h:440
uint8_t uint16_t start_address
Definition modbus.h:247
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:1161
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:264
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
Definition modbus.h:248
uint8_t uint16_t uint16_t number_of_entities
Definition modbus.h:247
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:248
int32_t tx_delay_remaining() override
Definition modbus.cpp:138
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:302
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:248
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:1151
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:242
void set_turnaround_time(uint16_t time_in_ms)
Definition modbus.h:243
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:1215
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:638
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
void set_address(uint8_t address)
Definition modbus.h:615
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:629
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:635
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:625
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:617
virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:641
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:646
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:621
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:367
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:320
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:1181
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:371
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:306
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:309
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:139
ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span< const uint8_t > pdu, CommandOptions options={}, uint16_t seq=0)
Definition modbus.h:135
void make_continuous(bool continuous)
Definition modbus.h:174
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:229
ModbusClientDevice * device
Definition modbus.h:121
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