ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
modbus.cpp
Go to the documentation of this file.
1#include "modbus.h"
2
3#include <algorithm>
4
7#include "esphome/core/log.h"
8
9namespace esphome::modbus {
10
11static const char *const TAG = "modbus";
12
13// Maximum bytes to log for Modbus frames (truncated if larger)
14static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
15
16// Approximate bits per character on the wire (depends on parity/stop bit config)
17static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11;
18// Milliseconds per second
19static constexpr uint32_t MS_PER_SEC = 1000;
20
21// Shortest gap between two "no device accepted broadcast" warnings
22static constexpr uint32_t UNACCEPTED_BROADCAST_WARN_INTERVAL_MS = 60 * MS_PER_SEC;
23
25 if (this->flow_control_pin_ != nullptr) {
26 this->flow_control_pin_->setup();
27 }
28
29 this->frame_delay_ms_ =
30 std::max(2, // 1750us minimum per spec - rounded up to 2ms.
31 // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
32 (uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1);
33
34 // When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a
35 // meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay.
36 // Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks.
37 static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50;
38 size_t rx_threshold = this->parent_->get_rx_full_threshold();
41 ? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1
42 : DEFAULT_LONG_RX_BUFFER_DELAY_MS;
43}
44
46 // Receive any available bytes from UART
47 this->receive_bytes_();
48
49 // Parse bytes into frames and process them
50 this->parse_modbus_frames();
51}
52
54 // Drain anything owed since the last loop (e.g. an external clear) before the watchdog runs, so it
55 // never times out an entry whose pending count has not been drained. No-op when nothing is owed.
56 this->sweep_();
57
58 this->Modbus::loop(); // receive bytes and parse frames
59
60 // Send-wait watchdog: only the cheap time check runs at loop rate; expire_waiting_() looks the
61 // entry up and holds off if the response has started arriving.
62 if (this->waiting_for_response_ &&
64 this->expire_waiting_();
65 }
66
67 this->sweep_(); // deliver owed callbacks with the hub quiescent
68 this->send_next_frame_();
69}
70
73 if (cmd == nullptr) {
74 this->waiting_for_response_ = false;
75 return;
76 }
77 if (!this->rx_buffer_.empty() && this->rx_buffer_[0] == cmd->frame.address()) {
78 // The start of the response is in the buffer: let the frame finish arriving.
79 return;
80 }
81 // Only a genuine WAITING entry warrants the log (a cleared or interrupted shell timing out is expected).
82 if (cmd->state == FrameState::WAITING) {
83 ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", cmd->frame.address(),
84 this->last_receive_check_ - this->last_send_);
85 }
86 // Deliver on_no_response directly, the way the parse path delivers response()/error(): the entry
87 // lands in TIMED_OUT and the following sweep reschedules a retry or erases it. Free the
88 // wire first so a resend from inside the callback sees it available.
89 this->waiting_for_response_ = false;
90 this->sweep_needed_ = true;
91 cmd->timed_out();
92}
93
95 // If the response frame is finished (including interframe delay) - we timeout.
96 // The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
97 // when the buffer is filling the back half of the response
98 const uint16_t timeout = std::max(
99 (uint16_t) this->frame_delay_ms_,
100 (uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
101 : 0));
102
103 return this->last_receive_check_ - this->last_modbus_byte_ > timeout;
104}
105
107 // We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
108 // It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
109 // If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop
110 // then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
111 // So in this component we don't use any cached timestamp values to avoid these annoying bugs
112 const uint32_t now = millis();
113 return std::max({(int32_t) 0,
114 (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ - (now - this->last_send_)),
115 (int32_t) (this->frame_delay_ms_ - (now - this->last_modbus_byte_))});
116}
117
119 const uint32_t now = millis();
120 return std::max({(int32_t) 0,
121 (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
122 (now - this->last_send_)),
123 (int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
124}
125
127 // We block transmission in any of these cases:
128 // 1. There are bytes in the UART Rx buffer
129 // 2. There are bytes in our Rx buffer
130 // 3. The last sent byte isn't more than tx_delay ms ago (i.e. wait to tell receivers that our previous Tx is done)
131 // 4. The last received byte isn't more than tx_delay ms ago (i.e. wait to be sure there isn't more Rx coming)
132 // N.B. We allow a small delay (MODBUS_TX_MAX_DELAY_MS) to avoid looping on small delays. This gets handled by
133 // send_frame_.
134 return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_MS;
135}
136
138 // We block transmission in any of these case:
139 // 1. We're waiting for a response (a waiting entry: WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED)
140 // 2. Any of the base class tx_blocked conditions
141 return this->waiting_for_response_ || this->Modbus::tx_blocked();
142}
143
145 // "Empty" for ready_for_immediate_send(): no one-shot is queued ahead of the caller. Entries in
146 // other states are mid-transaction or owed bookkeeping, not queued sends - and a READY continuous
147 // poll does not count either, since it ranks below every one-shot, so a new send goes out first.
148 for (const auto &cmd : this->tx_buffer_) {
149 if (cmd.state == FrameState::READY && !cmd.continuous)
150 return false;
151 }
152 return true;
153}
154
156 this->last_receive_check_ = millis();
157 size_t bytes = this->available();
158
159 if (bytes) {
160 size_t buffer_size = this->rx_buffer_.size();
162 this->rx_buffer_.resize(buffer_size + bytes);
163 if (!this->read_array(this->rx_buffer_.data() + buffer_size, bytes)) {
164 this->rx_buffer_.resize(buffer_size);
165 return;
166 }
167 if (buffer_size == 0) {
168 ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "ms after last send",
169 this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), millis() - this->last_send_);
170 }
171 }
172}
173
175 if (!this->rx_buffer_.empty()) {
176 size_t size;
177 do {
178 size = this->rx_buffer_.size();
179 if (!this->parse_modbus_server_frame_())
180 this->clear_rx_buffer_(LOG_STR("parse failed"), true);
181 } while (!this->rx_buffer_.empty() && size > this->rx_buffer_.size());
182 if (this->timeout_())
183 this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
184 }
185}
186
188 while (!this->rx_buffer_.empty()) {
189 size_t size = this->rx_buffer_.size();
190 ESP_LOGVV(TAG, "Parsing frames buffer size = %" PRIu32, size);
191 bool retry_as_client = false;
192 // A broadcast is a client request, never a peer response; clear any stale expectation (RTU is half-duplex).
193 const bool is_broadcast = this->rx_buffer_[0] == BROADCAST_ADDRESS;
194 if (is_broadcast)
195 this->expecting_peer_response_ = 0;
196 if (this->expecting_peer_response_ != 0) {
197 if (!this->parse_modbus_server_frame_()) {
198 ESP_LOGV(TAG, "Stop expecting peer response from %" PRIu8 " due to parse failure, and retry parse",
200 this->expecting_peer_response_ = 0;
201 retry_as_client = true;
202 } else if (this->timeout_() && size == this->rx_buffer_.size()) {
203 // If we timed out and the above parse attempt did not consume data, stop expecting a response
204 ESP_LOGV(TAG,
205 "Stop expecting peer response from %" PRIu8 " due to timeout after partial response, and retry parse",
207 this->expecting_peer_response_ = 0;
208 retry_as_client = true;
209 }
210 } else {
211 if (!this->parse_modbus_client_frame_())
212 this->clear_rx_buffer_(LOG_STR("parse failed"), true);
213 }
214 // Stop if the buffer didn't shrink (no frame consumed) and no mode switch triggered a retry
215 if (!retry_as_client && size <= this->rx_buffer_.size())
216 break;
217 }
218 if (this->timeout_())
219 this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
220}
221
222uint16_t Modbus::find_custom_frame_end_(uint16_t min_length) const {
223 // Custom functions could be any length - we have to rely on the CRC to determine completeness.
224 // If a CRC match is never found, the buffer will eventually overflow and be cleared.
225 const uint8_t *raw = &this->rx_buffer_[0];
226 const size_t size = this->rx_buffer_.size();
227 for (uint16_t len = min_length; len <= std::min(size, size_t(MAX_FRAME_SIZE)); len++) {
228 if (crc16(raw, len) == 0)
229 return len;
230 }
231 return 0;
232}
233
235 size_t size = this->rx_buffer_.size();
236 uint16_t frame_length = helpers::server_frame_length(this->rx_buffer_.data(), this->rx_buffer_.size());
237
238 if (size < frame_length)
239 return true;
240
241 uint8_t address = this->rx_buffer_[0];
242 uint8_t function_code = this->rx_buffer_[1];
243
244 if (helpers::is_function_code_custom(function_code)) {
245 frame_length = this->find_custom_frame_end_(frame_length);
246 if (frame_length == 0)
247 return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
248 ESP_LOGD(TAG, "User-defined function %02X found", function_code);
249 } else {
250 if (crc16(&this->rx_buffer_[0], frame_length) != 0)
251 return false;
252 }
253
254 // Process before clearing: process_modbus_server_frame (receiving a response or peer message) never sends a reply
255 // synchronously. We can safely point directly into rx_buffer_ and avoid a copy.
256 // The PDU is the frame without the leading address and the trailing CRC.
257 std::span<const uint8_t> pdu(this->rx_buffer_.data() + 1, frame_length - 3);
258
259 this->process_modbus_server_frame(address, pdu);
260 this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
261
262 return true;
263}
264
266 size_t size = this->rx_buffer_.size();
267 uint16_t frame_length = helpers::client_frame_length(this->rx_buffer_.data(), this->rx_buffer_.size());
268
269 if (size < frame_length)
270 return true;
271
272 uint8_t address = this->rx_buffer_[0];
273 uint8_t function_code = this->rx_buffer_[1];
274
275 if (helpers::is_function_code_custom(function_code)) {
276 frame_length = this->find_custom_frame_end_(frame_length);
277 if (frame_length == 0)
278 return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
279 ESP_LOGD(TAG, "User-defined function %02X found", function_code);
280 } else {
281 if (crc16(&this->rx_buffer_[0], frame_length) != 0)
282 return false;
283 }
284
285 // Clear before processing: process_modbus_client_frame_ dispatches to a server device which sends
286 // a response immediately. We need to clear the rx buffer first so the response doesn't snag tx_blocked.
287 // This requires copying the frame data to a local buffer beforehand.
288 uint8_t data_offset = helpers::client_frame_data_offset(this->rx_buffer_.data(), this->rx_buffer_.size());
289 uint16_t data_len = frame_length - 2 - data_offset;
290 uint8_t data_buffer[MAX_FRAME_SIZE] = {};
291 std::memcpy(data_buffer, this->rx_buffer_.data() + data_offset, data_len);
292 std::span<const uint8_t> data(data_buffer, data_len);
293 this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
294
295 if (address == BROADCAST_ADDRESS) {
296 // Keep the unicast response buffers out of the broadcast call chain.
297 this->process_broadcast_frame_(function_code, data);
298 } else {
299 this->process_modbus_client_frame_(address, function_code, data);
300 }
301
302 return true;
303}
304
305// The parser (parse_modbus_server_frame_) guarantees the bounds relied on here: pdu is never empty,
306// and an exception-flagged pdu is at least 2 bytes. Keep that in mind when changing server_pdu_length().
307void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) {
308 const uint8_t function_code = pdu[0];
309 ModbusDeviceCommand *cmd = this->waiting_for_response_ ? this->find_waiting_() : nullptr;
310 if (cmd == nullptr) {
311 ESP_LOGW(TAG,
312 "Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send",
314 return;
315 }
316
317 // Check if the response matches the expected address and function code
318 const uint8_t expected_address = cmd->frame.address();
319 const uint8_t expected_function_code = cmd->frame.pdu()[0];
320 if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) {
321 ESP_LOGW(TAG,
322 "Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32
323 "ms after last send",
324 address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code,
325 this->last_modbus_byte_ - this->last_send_);
326 // Unexpected frame: flip a WAITING entry to an INTERRUPTED shell that ignores the rest of this
327 // transaction and blocks tx until the send-wait timeout, where it gets its on_no_response.
328 cmd->interrupt();
329 return;
330 }
331
333 // An interrupted shell keeps blocking until the send-wait timeout; a late response for it is
334 // ignored and does NOT free the wire. The distrust survives a clear (INTERRUPTED_RETIRED), so a
335 // cleared-interrupted frame still ends in on_no_response rather than delivering a late response.
336 ESP_LOGW(TAG,
337 "Ignoring response from %" PRIu8 " - transmission interrupted by previous unexpected response, %" PRIu32
338 "ms after last send",
339 address, this->last_modbus_byte_ - this->last_send_);
340 return;
341 }
342
343 // Deliver at parse time so the response span can point into the rx buffer (zero copy). error()/
344 // response() set the state and consume the request BEFORE the callback, so a clear from inside it
345 // ("stop polling now") wins. A device-less shell runs no callback and the sweep erases it.
346 this->waiting_for_response_ = false;
347 this->sweep_needed_ = true;
348 if (helpers::is_function_code_exception(function_code)) {
349 uint8_t exception = pdu[1]; // exception frames are fixed-length, so the code is always present
350 ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send",
351 function_code, exception, address, this->last_modbus_byte_ - this->last_send_);
352 cmd->error(static_cast<ExceptionCode>(exception));
353 } else if (!cmd->response(pdu)) {
354 ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send", address,
355 this->last_modbus_byte_ - this->last_send_);
356 }
357}
358
359void ModbusServerHub::process_modbus_server_frame(uint8_t address, std::span<const uint8_t>) {
360 if (this->find_device_(address) != nullptr) {
361 ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
362 }
363
364 if (this->expecting_peer_response_ == address) {
365 ESP_LOGV(TAG, "Expected response from peer %" PRIu8 " received", address);
366 } else {
367 ESP_LOGV(TAG, "Unexpected response from peer %" PRIu8 " received", address);
368 }
369
370 // This always resets, even if the address doesn't match.
371 // If an unexpected response is received, we can't trust that a correct response will follow (it shouldn't).
372 this->expecting_peer_response_ = 0;
373}
374
376 for (auto *device : this->devices_) {
377 if (device->get_address() == address) {
378 return device;
379 }
380 }
381 return nullptr;
382}
383
384ResponseStatus ModbusServerHub::check_address_range_(uint16_t start_address, uint16_t count) {
385 if (!helpers::address_range_fits(start_address, count)) {
386 ESP_LOGW(TAG, "Address out of range - start: %" PRIu16 " num: %" PRIu16, start_address, count);
388 }
389 return std::nullopt;
390}
391
392// Write PDU layout after the function code: start address(2) [+ quantity(2) + byte count(1)] + register values.
393// The value subspans taken at these offsets stay in range because client_pdu_length() clamps the byte count to the
394// same maximum the callers' number_of_registers * 2 == number_of_bytes guard enforces.
395static constexpr size_t WRITE_SINGLE_VALUES_OFFSET = 2;
396static constexpr size_t WRITE_MULTIPLE_VALUES_OFFSET = 5;
397// FC 0x17 writes follow read start(2) + read quantity(2) + write start(2) + write quantity(2) + byte count(1).
398static constexpr size_t READ_WRITE_VALUES_OFFSET = 9;
399// A coil write (FC 0x0F) is function(1) + start(2) + quantity(2) + byte count(1) + packed bits. The largest
400// one (MAX_NUM_OF_COILS_TO_WRITE coils) must fit the received request PDU, so the value subspan taken at
401// WRITE_MULTIPLE_VALUES_OFFSET can never run past it.
402static_assert(1 + WRITE_MULTIPLE_VALUES_OFFSET + packed_bit_bytes(MAX_NUM_OF_COILS_TO_WRITE) <= MAX_PDU_SIZE,
403 "the largest FC 0x0F coil write must fit within MAX_PDU_SIZE");
404
405ResponseStatus ModbusServerHub::parse_write_single_(std::span<const uint8_t> data, uint16_t &start_address,
406 RegisterValues &registers) {
407 start_address = helpers::get_data<uint16_t>(data.data(), 0);
408 // No range check needed: one register can never push start_address + 1 past the address space.
409 this->assemble_registers_(data.subspan(WRITE_SINGLE_VALUES_OFFSET, sizeof(uint16_t)), registers);
410 return std::nullopt;
411}
412
413ResponseStatus ModbusServerHub::parse_write_multiple_(std::span<const uint8_t> data, uint16_t &start_address,
414 RegisterValues &registers) {
415 start_address = helpers::get_data<uint16_t>(data.data(), 0);
416 uint16_t number_of_registers = helpers::get_data<uint16_t>(data.data(), 2);
417 uint8_t number_of_bytes = helpers::get_data<uint8_t>(data.data(), 4);
418 if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_WRITE ||
419 number_of_registers * 2 != number_of_bytes) {
420 ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 " or bytes %" PRIu8, number_of_registers, number_of_bytes);
422 }
423 if (ResponseStatus status = this->check_address_range_(start_address, number_of_registers); status.has_value()) {
424 return status;
425 }
426 this->assemble_registers_(data.subspan(WRITE_MULTIPLE_VALUES_OFFSET, number_of_bytes), registers);
427 return std::nullopt;
428}
429
430ResponseStatus ModbusServerHub::parse_read_request_(std::span<const uint8_t> data, uint16_t max_entities,
431 const LogString *entity_name, uint16_t &start_address,
432 uint16_t &count) {
433 // Every read request is start address(2) + quantity(2); only the protocol ceiling differs per function
434 // code, so registers and coils/discrete inputs validate through here and cannot drift apart.
435 start_address = helpers::get_data<uint16_t>(data.data(), 0);
436 count = helpers::get_data<uint16_t>(data.data(), 2);
437 if (count == 0 || count > max_entities) {
438 ESP_LOGW(TAG, "Invalid number of %s %" PRIu16, LOG_STR_ARG(entity_name), count);
440 }
441 return this->check_address_range_(start_address, count);
442}
443
444ResponseStatus ModbusServerHub::parse_write_single_coil_(std::span<const uint8_t> data, uint16_t &start_address,
445 bool &value) {
446 start_address = helpers::get_data<uint16_t>(data.data(), 0);
447 const uint16_t raw_value = helpers::get_data<uint16_t>(data.data(), WRITE_SINGLE_VALUES_OFFSET);
448 if (raw_value != 0xFF00 && raw_value != 0x0000) {
449 ESP_LOGW(TAG, "Invalid coil value 0x%04X", raw_value);
451 }
452 // No range check needed: one coil can never push start_address + 1 past the address space.
453 value = raw_value == 0xFF00;
454 return std::nullopt;
455}
456
457ResponseStatus ModbusServerHub::parse_write_multiple_coils_(std::span<const uint8_t> data, uint16_t &start_address,
458 uint16_t &count, std::span<const uint8_t> &packed_bytes) {
459 start_address = helpers::get_data<uint16_t>(data.data(), 0);
460 const uint16_t number_of_bits = helpers::get_data<uint16_t>(data.data(), 2);
461 const uint8_t number_of_bytes = helpers::get_data<uint8_t>(data.data(), 4);
462 if (number_of_bits == 0 || number_of_bits > MAX_NUM_OF_COILS_TO_WRITE ||
463 packed_bit_bytes(number_of_bits) != number_of_bytes) {
464 ESP_LOGW(TAG, "Invalid number of coils %" PRIu16 " or bytes %" PRIu8, number_of_bits, number_of_bytes);
466 }
467 if (ResponseStatus status = this->check_address_range_(start_address, number_of_bits); status.has_value()) {
468 return status;
469 }
470 count = number_of_bits;
471 // coil values follow start(2) + quantity(2) + byte count(1)
472 packed_bytes = data.subspan(WRITE_MULTIPLE_VALUES_OFFSET, number_of_bytes);
473 return std::nullopt;
474}
475
476void ModbusServerHub::assemble_registers_(std::span<const uint8_t> values, RegisterValues &registers) {
477 for (size_t offset = 0; offset + 1 < values.size(); offset += 2) {
478 registers.push_back(helpers::get_data<uint16_t>(values.data(), offset));
479 }
480}
481
482void ModbusServerHub::process_broadcast_frame_(uint8_t function_code, std::span<const uint8_t> data) {
483 // Broadcasts are only meaningful for writes and are never answered (Modbus 4.1 / 6.12), so an unsupported
484 // function code or a validation failure is silently dropped instead of replying with an exception. Both
485 // register writes (FC 0x06/0x10) and coil writes (FC 0x05/0x0F) are broadcastable by spec, and each shares
486 // its parser with the addressed path so a broadcast is validated exactly as the unicast form would be.
487 uint16_t start_address;
488 RegisterValues registers;
489 uint16_t coil_count = 0;
490 std::span<const uint8_t> packed_bytes;
491 uint8_t single_bit = 0; // backs packed_bytes for a single-coil write, so it must outlive the loop below
492 bool coils = false;
494 switch (static_cast<FunctionCode>(function_code)) {
496 status = this->parse_write_single_(data, start_address, registers);
497 break;
499 status = this->parse_write_multiple_(data, start_address, registers);
500 break;
502 coils = true;
503 bool value = false;
504 status = this->parse_write_single_coil_(data, start_address, value);
505 single_bit = value ? 0x01 : 0x00;
506 coil_count = 1;
507 packed_bytes = std::span<const uint8_t>(&single_bit, 1);
508 break;
509 }
511 coils = true;
512 status = this->parse_write_multiple_coils_(data, start_address, coil_count, packed_bytes);
513 break;
514 default:
515 // Reads and read/write require a reply, so they are not valid as broadcasts.
516 ESP_LOGV(TAG, "Ignoring broadcast with unsupported function code %" PRIu8, function_code);
517 return;
518 }
519 if (status.has_value()) {
520 return;
521 }
522 // A broadcast is never answered, so a rejecting device has no other feedback channel: report the
523 // per-device outcome at V, and warn if the write reached nobody at all.
524 bool accepted = false;
525 for (auto *device : this->devices_) {
526 // Same handlers as an addressed write - a device cannot tell a broadcast apart, and does not need
527 // to: the hub owns the difference, which is only that no reply is ever sent.
528 const ResponseStatus device_status =
529 coils ? device->on_write_coils(start_address, PackedBits(packed_bytes, coil_count))
530 : device->on_write_registers(start_address, registers);
531 if (device_status.has_value()) {
532 ESP_LOGV(TAG, "Device %" PRIu8 " rejected broadcast write with exception %" PRIu8, device->get_address(),
533 static_cast<uint8_t>(device_status.value()));
534 } else {
535 accepted = true;
536 }
537 }
538 if (!accepted && !this->devices_.empty()) {
539 const uint16_t entity_count = coils ? coil_count : static_cast<uint16_t>(registers.size());
540 const LogString *const entity_name = coils ? LOG_STR("coils") : LOG_STR("registers");
541 // Warn at most once per interval, then drop to VERBOSE: on a shared bus a broadcast aimed at other nodes
542 // repeats forever, so warning per frame would flood the log.
543 const uint32_t now = millis();
544 if (this->last_unaccepted_broadcast_warn_ == 0 ||
545 now - this->last_unaccepted_broadcast_warn_ > UNACCEPTED_BROADCAST_WARN_INTERVAL_MS) {
547 ESP_LOGW(TAG, "No device accepted broadcast write of %" PRIu16 " %s at 0x%04X", entity_count,
548 LOG_STR_ARG(entity_name), start_address);
549 } else {
550 ESP_LOGV(TAG, "No device accepted broadcast write of %" PRIu16 " %s at 0x%04X", entity_count,
551 LOG_STR_ARG(entity_name), start_address);
552 }
553 }
554}
555
557 uint16_t number_of_registers, const RegisterValues &registers,
558 std::span<uint8_t> response_buffer, uint16_t &response_len) {
559 // A handler that returns an exception leaves registers partially filled, so check the exception
560 // first and forward it before validating the register count on the success path.
561 if (this->rejected_(address, function_code, status)) {
562 return false;
563 }
564
565 if (registers.size() != number_of_registers) {
566 ESP_LOGE(TAG, "Incorrect response %" PRIu16 " requested, %zu returned", number_of_registers, registers.size());
567 this->send_exception_(address, function_code, ExceptionCode::SERVICE_DEVICE_FAILURE);
568 return false;
569 }
570
571 // The byte count is a single byte, so the count must stay within the protocol read limit; above it the
572 // static_cast<uint8_t>(number_of_registers * 2) below would silently truncate the byte count.
573 if (number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ) {
574 ESP_LOGE(TAG, "Read response of %" PRIu16 " registers exceeds the limit of %" PRIu16, number_of_registers,
575 MAX_NUM_OF_REGISTERS_TO_READ);
576 this->send_exception_(address, function_code, ExceptionCode::SERVICE_DEVICE_FAILURE);
577 return false;
578 }
579
580 // Byte count(1) + two bytes per register. Checked here rather than at the call sites so the bound travels with
581 // the write itself: a future caller starting at a non-zero response_len, or passing a smaller buffer, is
582 // rejected instead of overrunning it before send_response_'s size guard can fire.
583 const size_t required = static_cast<size_t>(response_len) + 1 + static_cast<size_t>(number_of_registers) * 2;
584 if (required > response_buffer.size()) {
585 ESP_LOGE(TAG, "Read response needs %zu bytes but only %zu are available", required, response_buffer.size());
586 this->send_exception_(address, function_code, ExceptionCode::SERVICE_DEVICE_FAILURE);
587 return false;
588 }
589
590 response_buffer[response_len++] = static_cast<uint8_t>(number_of_registers * 2); // actual byte count
591 for (auto r : registers) {
592 auto register_bytes = decode_value(r);
593 response_buffer[response_len++] = register_bytes[0];
594 response_buffer[response_len++] = register_bytes[1];
595 }
596 return true;
597}
598
599void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code,
600 std::span<const uint8_t> data) {
601 ModbusServerDevice *device = this->find_device_(address);
602 if (device == nullptr) {
604 ESP_LOGV(TAG, "Request to peer %" PRIu8 " received", address);
605 return;
606 }
607
609 uint8_t response_buffer[modbus::MAX_RAW_SIZE];
610 const uint8_t *response_data = response_buffer;
611 uint16_t response_len = 0;
612
613 switch (static_cast<FunctionCode>(function_code)) {
616 uint16_t start_address;
617 uint16_t number_of_registers;
618 status = this->parse_read_request_(data, MAX_NUM_OF_REGISTERS_TO_READ, LOG_STR("registers"), start_address,
619 number_of_registers);
620 if (this->rejected_(address, function_code, status)) {
621 return;
622 }
623 RegisterValues registers;
624 if (static_cast<FunctionCode>(function_code) == FunctionCode::READ_HOLDING_REGISTERS) {
625 status = device->on_read_holding_registers(start_address, number_of_registers, registers);
626 } else {
627 status = device->on_read_input_registers(start_address, number_of_registers, registers);
628 }
629
630 if (!this->build_or_reject_read_response_(address, function_code, status, number_of_registers, registers,
631 response_buffer, response_len)) {
632 return;
633 }
634 break;
635 }
638 // Parse and validate the write PDU into host-order register values; reply with an exception on failure.
639 uint16_t start_address;
640 RegisterValues registers;
641 if (static_cast<FunctionCode>(function_code) == FunctionCode::WRITE_SINGLE_REGISTER) {
642 status = this->parse_write_single_(data, start_address, registers);
643 } else {
644 status = this->parse_write_multiple_(data, start_address, registers);
645 }
646 if (this->rejected_(address, function_code, status)) {
647 return;
648 }
649 status = device->on_write_registers(start_address, registers);
650 response_data = data.data(); // echo the request header per Modbus 6.6, 6.12
651 response_len = 4;
652 break;
653 }
656 uint16_t start_address;
657 uint16_t number_of_bits;
658 status =
659 this->parse_read_request_(data, MAX_NUM_OF_COILS_TO_READ, LOG_STR("bits"), start_address, number_of_bits);
660 if (this->rejected_(address, function_code, status)) {
661 return;
662 }
663 // Response: byte count(1) + packed bytes, written straight into the pre-zeroed response buffer. It
664 // always fits: the parse above caps the count, and a static_assert bounds that against MAX_RAW_SIZE.
665 const uint8_t byte_count = static_cast<uint8_t>(packed_bit_bytes(number_of_bits));
666 response_buffer[response_len++] = byte_count;
667 // Take the packed-bytes span off a span that knows response_buffer's real size, so a future non-zero
668 // response_len (e.g. a prefix written before the packed data) is a bounds error, not a silent overrun.
669 std::span<uint8_t> packed_out = std::span<uint8_t>(response_buffer).subspan(response_len, byte_count);
670 std::fill(packed_out.begin(), packed_out.end(), 0);
671 MutablePackedBits bits(packed_out, number_of_bits);
672 if (static_cast<FunctionCode>(function_code) == FunctionCode::READ_COILS) {
673 status = device->on_read_coils(start_address, bits);
674 } else {
675 status = device->on_read_discrete_inputs(start_address, bits);
676 }
677 if (this->rejected_(address, function_code, status)) {
678 return;
679 }
680 response_len += byte_count;
681 break;
682 }
684 // A single coil is handed to the device as a one-bit packed view, the same form a multiple-coil
685 // write takes, so a device only ever implements one coil write handler.
686 uint16_t start_address;
687 bool value = false;
688 status = this->parse_write_single_coil_(data, start_address, value);
689 if (this->rejected_(address, function_code, status)) {
690 return;
691 }
692 const uint8_t single_bit = value ? 0x01 : 0x00;
693 status = device->on_write_coils(start_address, PackedBits(std::span<const uint8_t>(&single_bit, 1), 1));
694 response_data = data.data(); // echo the request header per Modbus 6.5, 6.11
695 response_len = 4;
696 break;
697 }
699 // Parse and validate the coil write PDU into a packed-bit view; reply with an exception on failure.
700 uint16_t start_address;
701 uint16_t count;
702 std::span<const uint8_t> packed_bytes;
703 status = this->parse_write_multiple_coils_(data, start_address, count, packed_bytes);
704 if (this->rejected_(address, function_code, status)) {
705 return;
706 }
707 status = device->on_write_coils(start_address, PackedBits(packed_bytes, count));
708 response_data = data.data(); // echo the request header per Modbus 6.5, 6.11
709 response_len = 4;
710 break;
711 }
713 // PDU data: read start address(2) + read quantity(2) + write start address(2) + write quantity(2) +
714 // write byte count(1) + write register values. Per Modbus 6.17 the write is performed before the read.
715 uint16_t read_start_address = helpers::get_data<uint16_t>(data.data(), 0);
716 uint16_t number_of_registers = helpers::get_data<uint16_t>(data.data(), 2);
717 uint16_t write_start_address = helpers::get_data<uint16_t>(data.data(), 4);
718 uint16_t number_of_write_registers = helpers::get_data<uint16_t>(data.data(), 6);
719 uint8_t number_of_bytes = helpers::get_data<uint8_t>(data.data(), 8);
720 if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ ||
721 number_of_write_registers == 0 || number_of_write_registers > MAX_NUM_OF_REGISTERS_TO_WRITE_RW ||
722 number_of_write_registers * 2 != number_of_bytes) {
723 ESP_LOGW(TAG, "Invalid number of registers (read %" PRIu16 ", write %" PRIu16 ") or bytes %" PRIu8,
724 number_of_registers, number_of_write_registers, number_of_bytes);
725 this->send_exception_(address, function_code, ExceptionCode::ILLEGAL_DATA_VALUE);
726 return;
727 }
728 status = this->check_address_range_(read_start_address, number_of_registers);
729 if (!status.has_value()) {
730 status = this->check_address_range_(write_start_address, number_of_write_registers);
731 }
732 if (this->rejected_(address, function_code, status)) {
733 return;
734 }
735 // Perform the write first (Modbus 6.17). Scoped so the write values are off the stack before the read
736 // values are allocated, keeping only one RegisterValues buffer live at a time.
737 {
738 RegisterValues write_registers;
739 this->assemble_registers_(data.subspan(READ_WRITE_VALUES_OFFSET, number_of_bytes), write_registers);
740 // Dispatch to the standalone write and read handlers so any device implementing those supports 0x17
741 // without a dedicated handler; a device that maps registers by address reconstructs the read response
742 // from the values it just stored.
743 status = device->on_write_registers(write_start_address, write_registers);
744 }
745 if (this->rejected_(address, function_code, status)) {
746 return;
747 }
748 RegisterValues registers;
749 status = device->on_read_holding_registers(read_start_address, number_of_registers, registers);
750
751 if (!this->build_or_reject_read_response_(address, function_code, status, number_of_registers, registers,
752 response_buffer, response_len)) {
753 return;
754 }
755 break;
756 }
757 default:
758 ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code);
759 this->send_exception_(address, function_code, ExceptionCode::ILLEGAL_FUNCTION);
760 return;
761 }
762 if (!this->rejected_(address, function_code, status)) {
763 this->send_response_(address, function_code, response_data, response_len);
764 }
765}
766
767// Callers gate on tx_blocked() first, but the pre-send delay below can span several ms, so re-check
768// after it and refuse (return false) if a byte arrived in that window rather than transmit over it.
770 const int32_t tx_delay_remaining = this->tx_delay_remaining();
771 if (tx_delay_remaining > 0) {
773 }
774
775 // The delay above can span several ms; a byte arriving in that window blocks transmission after the
776 // caller's gate already passed. Don't collide with the incoming frame - leave the entry to retry.
777 if (this->tx_blocked()) {
778 return false;
779 }
780
781 if (this->flow_control_pin_ != nullptr) {
782 this->flow_control_pin_->digital_write(true);
783 this->write_array(frame.data.data(), frame.size());
784 this->flush();
785 this->flow_control_pin_->digital_write(false);
786 this->last_send_tx_offset_ = 0;
787 } else {
788 this->write_array(frame.data.data(), frame.size());
789 this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
790 }
791
792 uint32_t now = millis();
793#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
794 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
795#endif
796 ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive",
797 format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_,
798 now - this->last_modbus_byte_);
799 this->last_send_ = now;
800 return true;
801}
802
804 if (this->tx_blocked())
805 return;
806
808 if (cmd == nullptr)
809 return;
810
811 if (!this->send_frame_(cmd->frame)) {
812 ESP_LOGV(TAG, "Send deferred for %" PRIu8 ": a frame arrived during the send delay, will retry",
813 cmd->frame.address());
814 return;
815 }
816
817 cmd->sent();
818 if (cmd->frame.address() == BROADCAST_ADDRESS) {
819 // A broadcast (address 0) is never answered (Modbus 4.1), so it is fire-and-forget: on_sent above
820 // reports the transmission, and the entry then retires with no terminal callback instead of
821 // occupying the waiting slot until the send-wait timeout expires. The turnaround delay already
822 // spaces the next frame; the following sweep erases the entry.
823 ESP_LOGV(TAG, "Broadcast to address 0 sent; no reply expected (fire-and-forget)");
824 cmd->complete_broadcast();
825 this->sweep_needed_ = true;
826 return;
827 }
828 this->waiting_for_response_ = true;
829}
830
832 ESP_LOGCONFIG(TAG,
833 "Modbus:\n"
834 " Send Wait Time: %" PRIu16 " ms\n"
835 " Turnaround Time: %" PRIu16 " ms\n"
836 " Frame Delay: %" PRIu16 " ms\n"
837 " Long Rx Buffer Delay: %" PRIu16 " ms",
840 LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
841}
843 ESP_LOGCONFIG(TAG,
844 "Modbus:\n"
845 " Frame Delay: %" PRIu16 " ms\n"
846 " Long Rx Buffer Delay: %" PRIu16 " ms",
848 LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
849}
850
852 // After UART bus
853 return setup_priority::BUS - 1.0f;
854}
855
856void ModbusServerHub::send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload,
857 uint16_t payload_len) {
858 // Build the raw frame (address + function code + payload) in a stack buffer; it's consumed
859 // immediately by send_raw_ and a full raw frame never exceeds MAX_RAW_SIZE.
860 if (payload_len + 2 > MAX_RAW_SIZE) {
861 ESP_LOGE(TAG, "Server response too large (%" PRIu16 " bytes)", static_cast<uint16_t>(payload_len + 2));
862 return;
863 }
864 uint8_t raw_frame[MAX_RAW_SIZE];
865 raw_frame[0] = address;
866 raw_frame[1] = function_code;
867 std::memcpy(raw_frame + 2, payload, payload_len);
868 this->send_raw_(raw_frame, payload_len + 2);
869}
870
871bool ModbusServerHub::rejected_(uint8_t address, uint8_t function_code, ResponseStatus status) {
872 if (!status.has_value())
873 return false;
874 // The one place a rejection becomes an exception reply, so the log carries the transaction context a
875 // device handler never has: which client-facing address and function code drew which exception. DEBUG
876 // rather than WARN because an exception reply is a normal protocol outcome and arrives per frame - a
877 // probing or broken client would otherwise flood the log. The parse helpers still WARN with specifics.
878 ESP_LOGD(TAG, "Exception %" PRIu8 " replied to function 0x%02X for address %" PRIu8,
879 static_cast<uint8_t>(status.value()), function_code, address);
880 this->send_exception_(address, function_code, status.value());
881 return true;
882}
883
884void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code) {
885 uint8_t raw_frame[3];
886 raw_frame[0] = address;
887 raw_frame[1] = function_code | FUNCTION_CODE_EXCEPTION_MASK;
888 raw_frame[2] = static_cast<uint8_t>(exception_code);
889 this->send_raw_(raw_frame, 3);
890}
891
893 for (auto &cmd : this->tx_buffer_) {
894 if (cmd.waiting_state())
895 return &cmd;
896 }
897 return nullptr;
898}
899
901 // Class first (WRITE, then one-shot READ, then CONTINUOUS), oldest within a class. seq is a
902 // free-running counter, so compare each entry's AGE against it (correct across the full range).
903 const uint16_t now = this->next_seq_;
904 const auto age = [now](const ModbusDeviceCommand &cmd) -> uint16_t { return now - cmd.seq; };
905 const auto older = [&age](const ModbusDeviceCommand &a, const ModbusDeviceCommand &b) { return age(a) > age(b); };
906 ModbusDeviceCommand *best = nullptr;
907 for (auto &cmd : this->tx_buffer_) {
908 if (cmd.state != FrameState::READY)
909 continue;
910 if (best == nullptr || cmd.priority() > best->priority() ||
911 (cmd.priority() == best->priority() && older(cmd, *best))) {
912 best = &cmd;
913 }
914 }
915 return best;
916}
917
920 // on_sent() is not a terminal, so nothing is consumed.
921 if (this->device == nullptr)
922 return false;
923 this->device->on_sent(this->frame.pdu());
924 return true;
925}
926
928 if (!this->decrement_pending())
929 return false; // nothing owed - stop the sweep draining this entry
930 if (this->device != nullptr)
931 this->device->on_not_sent(this->frame.pdu());
932 return true; // consumed one debt (delivered, or silent when device-less) - keep draining to zero
933}
934
935bool ModbusDeviceCommand::response(std::span<const uint8_t> response_pdu) {
937 // A continuous poll is never consumed by its own response; a one-shot consumes one request here.
938 if (!this->continuous)
939 this->decrement_pending();
940 if (this->device == nullptr)
941 return false;
942 this->device->on_response(this->frame.pdu(), response_pdu);
943 return true;
944}
945
948 // An exception ends a continuous poll too, so decrement unconditionally.
949 this->decrement_pending();
950 if (this->device == nullptr)
951 return false;
952 this->device->on_error(this->frame.pdu(), exception_code);
953 return true;
954}
955
957 // An unexpected frame distrusts the transaction. A cleared-but-still-waiting shell distrusts too, so
958 // the interrupt survives the clear in either order (WAITING_RETIRED -> INTERRUPTED_RETIRED).
959 if (this->state == FrameState::WAITING) {
961 return true;
962 }
963 if (this->state == FrameState::WAITING_RETIRED) {
965 return true;
966 }
967 return false;
968}
969
971 this->state = FrameState::TIMED_OUT; // advance BEFORE the callback so a clear from inside it wins
972 this->decrement_pending(); // resolve this request (WAITING-origin, so pending >= 1)
973 if (this->device == nullptr)
974 return false; // resolved, no one to tell
975 if (this->device->on_no_response(this->frame.pdu()))
976 this->increment_pending(); // granted retry = re-request (capped)
977 return true;
978}
979
981 if (!this->sweep_needed_)
982 return;
983 this->sweep_needed_ = false;
984 // Serve only the entries present now: a callback may append (a re-send), but those sit beyond
985 // work_set and are left for the next sweep, which bounds the work and is the termination argument.
986 // Entries leave the container only in the erase pass below, so indices/references stay valid.
987 const size_t work_set = this->tx_buffer_.size();
988 // Restart the walk after every callback: a handler may have moved any entry to any state.
989 bool callback_ran = true;
990 while (callback_ran) {
991 callback_ran = false;
992 for (size_t i = 0; i != work_set && !callback_ran; i++) {
993 ModbusDeviceCommand &cmd = this->tx_buffer_[i];
994 switch (cmd.state) {
998 // Off the wire, callback already delivered: reschedule what is still pending, else erase.
999 if (cmd.pending)
1000 cmd.requeue(this->next_seq_++);
1001 break;
1003 // Owes one on_not_sent() per accepted request; notify_retired() consumes one and reports
1004 // whether a debt remained, so the restart loop drains the entry to zero - even a device-less
1005 // shell with pending > 1 (no callback fires, but it still drains rather than stranding).
1006 callback_ran = cmd.notify_retired();
1007 break;
1010 // Cleared shell: drain only the un-run duplicates; the request in flight keeps pending 1
1011 // and gets its usual callback when it resolves.
1012 if (cmd.pending > 1)
1013 callback_ran = cmd.notify_retired();
1014 break;
1015 default: // READY / WAITING / INTERRUPTED: idle or waiting for a response, nothing owed until the timeout
1016 break;
1017 }
1018 }
1019 }
1020 // Erase pass: the only place entries leave the container. Storage order carries no meaning, so a
1021 // finished entry is swap-and-popped; walking backwards means a moved-down entry is already seen.
1022 for (size_t i = this->tx_buffer_.size(); i-- > 0;) {
1023 const ModbusDeviceCommand &cmd = this->tx_buffer_[i];
1024 // pending == 0 is erasable, but shells still waiting for a response are exempt until it resolves.
1025 if (cmd.pending != 0 || cmd.waiting_state())
1026 continue;
1027 if (i + 1 != this->tx_buffer_.size())
1028 this->tx_buffer_[i] = std::move(this->tx_buffer_.back());
1029 this->tx_buffer_.pop_back();
1030 }
1031}
1032
1033// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
1034bool ModbusClientHub::queue_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device,
1036 // Requests refused here never enter the machine and get no callback - the false return is it.
1037 if (pdu.empty()) {
1038 ESP_LOGW(TAG, "Empty PDU refused for address %" PRIu8, address);
1039 return false;
1040 }
1041 // Bound the PDU so the wire frame (address + pdu + CRC) stays within the Modbus RTU 256-byte limit.
1042 if (pdu.size() > MAX_PDU_SIZE) {
1043 ESP_LOGE(TAG, "Frame too large, refused: %" PRIu8 ":%zu bytes", address, pdu.size());
1044 return false;
1045 }
1046 // classify() drives both the broadcast guard and the continuous check below; compute it once.
1048
1049 // A broadcast (address 0) is never answered (Modbus 4.1), so it is only meaningful for a command that
1050 // changes state. Refuse a broadcast that expects a reply - anything but a write or a custom/vendor code -
1051 // as it could never deliver a result, so the caller learns via the false return (and on_not_sent).
1052 // 0x17 (read/write multiple) is a knowing inclusion: classify() treats it as a write, so its write half
1053 // lands on every server and its unanswerable read half is simply discarded. An exception-flagged custom
1054 // code (0x80 bit set) is refused: is_function_code_custom() masks that bit away, so exclude it explicitly
1055 // here to match classify()'s exception-first handling of the write side.
1056 if (address == BROADCAST_ADDRESS && priority != CommandPriority::WRITE &&
1058 ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]);
1059 return false;
1060 }
1061
1062 // continuous is ignored for every mutating code (re-writing a value forever is never intended).
1063 const bool mutates = priority == CommandPriority::WRITE;
1064 bool continuous = false;
1065 if (options.continuous) {
1066 if (mutates) {
1067 ESP_LOGV(TAG, "continuous is ignored for a mutating function (0x%X, address %" PRIu8 ")", pdu[0], address);
1068 } else {
1069 continuous = true;
1070 }
1071 }
1072
1073 // A duplicate of a live entry with the same owner is not queued twice; it resolves against that
1074 // entry: anonymous -> dropped; continuous incoming -> convert the entry to a poll; one-shot onto a
1075 // poll -> downgrade the poll to one-shot; both one-shots -> pending++ below the cap, else refused.
1076 for (auto &item : this->tx_buffer_) {
1077 if (item.state == FrameState::RETIRED || item.state == FrameState::WAITING_RETIRED ||
1078 item.state == FrameState::INTERRUPTED_RETIRED)
1079 continue; // cleared, on their way out: a new identical send queues fresh, never absorbs
1080 if (item.device != device || !item.same_frame(address, pdu))
1081 continue;
1082 if (device == nullptr) {
1083 // A dropped read is routine (DEBUG); a dropped write/custom warns (unobservable without a device).
1084 const bool requeueable =
1086 if (requeueable) {
1087 ESP_LOGD(TAG, "Anonymous duplicate of active frame for %" PRIu8 " (function 0x%X), dropped", address, pdu[0]);
1088 } else {
1089 ESP_LOGW(TAG,
1090 "Anonymous duplicate of active frame for %" PRIu8 " (function 0x%X), dropped - register a "
1091 "device for delivery accounting",
1092 address, pdu[0]);
1093 }
1094 return false; // dropped: no entry, no callbacks - the refusal is the return value
1095 }
1096 if (continuous) {
1097 item.make_continuous(true);
1098 ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", now polled continuously", address);
1099 } else if (item.continuous) {
1100 // A one-shot duplicate downgrades the poll to a one-shot: it runs one more cycle to serve this
1101 // request, then stops (mirrors continuous incoming converting a one-shot the other way).
1102 item.make_continuous(false);
1103 ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", downgraded from continuous to one-shot", address);
1104 } else if (!item.increment_pending()) {
1105 // At the servable cap, so refused. (An absorbed duplicate leaves seq alone - the entry keeps
1106 // its place in line, held by its oldest outstanding request.)
1107 ESP_LOGD(TAG, "Frame already active for %" PRIu8 " with %" PRIu8 " requests pending, refused", address,
1108 item.pending);
1109 return false;
1110 } else {
1111 ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", request absorbed (pending %" PRIu8 ")", address,
1112 item.pending);
1113 }
1114 return true;
1115 }
1116
1117 // Backstop counts every entry; dead ones are gone by the sweep's end, so at worst they cost one
1118 // refusal at the very cap for one loop.
1119 if (this->tx_buffer_.size() >= MODBUS_TX_BUFFER_SIZE) {
1120#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
1121 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
1122#endif
1123 ESP_LOGE(TAG, "Write buffer full, refused: %" PRIu8 ":%s", address,
1124 format_hex_pretty_to(hex_buf, pdu.data(), pdu.size()));
1125 return false;
1126 }
1127#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1128 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
1129#endif
1130 ESP_LOGV(TAG, "Adding frame to tx queue: %" PRIu8 ":%s", address,
1131 format_hex_pretty_to(hex_buf, pdu.data(), pdu.size()));
1132 this->tx_buffer_.emplace_back(device, address, pdu, continuous, this->next_seq_++);
1133 return true;
1134}
1135
1137 // A clear is a pure state flip; the sweep delivers every owed on_not_sent() from a quiescent hub.
1138 for (auto &cmd : this->tx_buffer_) {
1139 if (cmd.frame.address() != address)
1140 continue;
1141 cmd.retire();
1142 this->sweep_needed_ = true;
1143 }
1144}
1145
1147 // Silent teardown (supersede semantics): the caller's own frames vanish without callbacks; see
1148 // the lifecycle note on ModbusClientDevice.
1149 for (auto &cmd : this->tx_buffer_) {
1150 if (cmd.device != device)
1151 continue;
1152 cmd.silent_retire();
1153 this->sweep_needed_ = true;
1154 }
1155}
1156
1157void ModbusClientHub::send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device) {
1158 if (payload.size() < 2) {
1159 ESP_LOGW(TAG, "send_raw() payload too short to contain a PDU, refused");
1160 return;
1161 }
1162 this->queue_pdu(payload[0], std::span<const uint8_t>(payload).subspan(1), device);
1163}
1164
1165// Send raw command for server replies immediately. Except CRC everything must be contained in payload
1166void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
1167 if (len == 0) {
1168 return;
1169 }
1170 if (len > MAX_RAW_SIZE) {
1171 ESP_LOGE(TAG, "Server send frame too large (%" PRIu16 " bytes)", len);
1172 return;
1173 }
1174
1175 // If blocked now (frame delay not elapsed at low baud, or a frame arriving), defer rather than
1176 // busy-waiting the loop; send_frame_ itself re-checks after its delay, so the deferred callback
1177 // just reports whatever it returns.
1178 if (this->tx_blocked()) {
1179 // Stash the raw payload in a single member buffer so the deferred callback can rebuild the frame
1180 // without a heap allocation. Only one server reply is ever waiting, so a single buffer suffices.
1181 std::memcpy(this->deferred_payload_.data(), payload, len);
1182 this->deferred_payload_len_ = len;
1183 this->set_timeout("deferred_send", this->tx_delay_remaining(), [this]() {
1184 ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
1185 this->deferred_payload_len_ - 1);
1186 if (!this->send_frame_(frame))
1187 ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked");
1188 });
1189 return;
1190 }
1191
1192 ModbusFrame frame(payload[0], payload + 1, len - 1);
1193 if (!this->send_frame_(frame))
1194 ESP_LOGE(TAG, "Server reply dropped: a frame arrived during the send delay");
1195}
1196
1197void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_to_clear) {
1198 size_t bytes = this->rx_buffer_.size();
1199 if (bytes_to_clear > 0 && bytes >= bytes_to_clear)
1200 bytes = bytes_to_clear;
1201 if (bytes > 0) {
1202 if (warn) {
1203 ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
1204 millis() - this->last_send_);
1205 } else {
1206 ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
1207 millis() - this->last_send_);
1208 }
1209 if (bytes == this->rx_buffer_.size()) {
1210 this->rx_buffer_.clear();
1211 } else {
1212 this->rx_buffer_.erase(this->rx_buffer_.begin(), this->rx_buffer_.begin() + bytes);
1213 }
1214 }
1215}
1216
1217void ModbusClientDevice::dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
1218 ResponseStatus status) {
1219 if (request_pdu.empty())
1220 return;
1221 auto function_code = static_cast<FunctionCode>(request_pdu[0]);
1222 // All standard requests handled below are function code + start address + count/value (5 bytes);
1223 // anything shorter cannot be parsed and is handed to the catch-all.
1224 if (request_pdu.size() < READ_PDU_SIZE) {
1225 this->on_custom_response(request_pdu, response_pdu, status);
1226 return;
1227 }
1228 const uint16_t start_address = helpers::get_data<uint16_t>(request_pdu.data(), 1);
1229 // count for reads/multi-writes, value for single writes
1230 const uint16_t count_or_value = helpers::get_data<uint16_t>(request_pdu.data(), 3);
1231
1232 // Gatekeeper for the typed dispatch below: anything that is not a standard-conformant transaction is
1233 // handed to on_custom_response() with the raw PDUs, so the decode cases can trust every length, byte
1234 // count, and quantity field without re-clamping.
1235 // - The REQUEST must be standard: nothing upstream validates a caller-built request PDU, so its
1236 // internal byte count, quantity, and address range are checked here (is_client_pdu_standard()).
1237 // - On success, the RESPONSE must be standard (self-consistent; the frame parser already guarantees
1238 // most of this, but the check keeps the safety proof local), and a read response's length must also
1239 // match the REQUESTED count - the per-PDU checks cannot see that relationship, and a short but
1240 // self-consistent response must be diverted, never silently clamped and delivered as complete.
1241 // - On failure (status engaged) the response is empty by design (see on_error()), so only the request
1242 // is validated.
1243 bool custom = !helpers::is_client_pdu_standard(request_pdu.data(), request_pdu.size());
1244 if (!custom && succeeded(status)) {
1245 custom = !helpers::is_server_pdu_standard(response_pdu.data(), response_pdu.size());
1246 if (!custom && helpers::is_function_code_read(static_cast<uint8_t>(function_code))) {
1247 const bool bits =
1248 function_code == FunctionCode::READ_COILS || function_code == FunctionCode::READ_DISCRETE_INPUTS;
1249 const size_t expected_data_size =
1250 bits ? packed_bit_bytes(count_or_value) : static_cast<size_t>(count_or_value) * 2;
1251 if (response_pdu.size() != expected_data_size + 2) {
1252 ESP_LOGD(TAG, "Response length %zu does not match request (expected %zu) for function code 0x%X",
1253 response_pdu.size(), expected_data_size + 2, static_cast<uint8_t>(function_code));
1254 custom = true;
1255 }
1256 }
1257 }
1258 if (custom) {
1259 this->on_custom_response(request_pdu, response_pdu, status);
1260 return;
1261 }
1262
1263 switch (function_code) {
1266 // FC 0x17 lands here too: its read start address and read quantity sit at the same request offsets as a
1267 // plain read's (bytes 1..2 and 3..4), so start_address and count_or_value already hold the read block; its
1268 // response carries only that read data, and the write half is confirmed by the response arriving at all.
1269 // An exception routes here as well (the gate only validates the request when status is set), delivering
1270 // empty registers with the error in status - so a 0x17 subclass handles success and failure in the one
1271 // on_read_holding_registers() callback and never needs to also override on_error().
1273 // Decode the big-endian register words into host byte order. The gate guarantees a success response
1274 // carries exactly count_or_value registers (and count_or_value <= MAX_NUM_OF_REGISTERS_TO_READ, the
1275 // capacity of RegisterValues); a mismatch was diverted to on_custom_response(), never clamped. On
1276 // failure the registers span is empty.
1277 RegisterValues registers;
1278 if (succeeded(status)) {
1279 for (size_t i = 0; i != count_or_value; i++) {
1280 registers.push_back(helpers::get_data<uint16_t>(response_pdu.data(), 2 + 2 * i));
1281 }
1282 }
1283 std::span<const uint16_t> register_span(registers.data(), registers.size());
1284 if (function_code == FunctionCode::READ_INPUT_REGISTERS) {
1285 this->on_read_input_registers(start_address, register_span, status);
1286 } else if (function_code == FunctionCode::READ_HOLDING_REGISTERS ||
1288 this->on_read_holding_registers(start_address, register_span, status);
1289 } else {
1290 // Unreachable for the current case labels; match explicitly so a function code added to this group
1291 // later is diverted to on_custom_response() rather than silently delivered as a holding read.
1292 this->on_custom_response(request_pdu, response_pdu, status);
1293 }
1294 break;
1295 }
1298 // Deliver the bits packed as on the wire; the gate guarantees a success response carries exactly
1299 // (count_or_value + 7) / 8 data bytes. On failure the view is empty AND the count is zero -
1300 // PackedBits::operator[] is unchecked, so size() must never promise bits with no bytes behind them.
1301 std::span<const uint8_t> packed_bytes;
1302 uint16_t count = 0;
1303 if (succeeded(status)) {
1304 packed_bytes = response_pdu.subspan(2);
1305 count = count_or_value;
1306 }
1307 PackedBits bits(packed_bytes, count);
1308 if (function_code == FunctionCode::READ_COILS) {
1309 this->on_read_coils(start_address, bits, status);
1310 } else {
1311 this->on_read_discrete_inputs(start_address, bits, status);
1312 }
1313 break;
1314 }
1315 // Single-write acks echo the value: on success that echo is device-confirmed state - the one
1316 // write whose acknowledgement carries a real read-back - so it is preferred over the request
1317 // copy. On an exception the response has no value and the request copy is the only one.
1320 const uint16_t value = (succeeded(status) && response_pdu.size() >= WRITE_SINGLE_PDU_SIZE)
1321 ? helpers::get_data<uint16_t>(response_pdu.data(), 3)
1322 : count_or_value;
1323 if (function_code == FunctionCode::WRITE_SINGLE_REGISTER) {
1324 this->on_write_single_register(start_address, value, status);
1325 } else {
1326 this->on_write_single_coil(start_address, value == 0xFF00, status);
1327 }
1328 break;
1329 }
1331 // Request layout: [0] function code, [1..2] start address, [3..4] register count, [5] byte count,
1332 // [6..] register data. The gate guarantees the request carries exactly count_or_value registers
1333 // (<= MAX_NUM_OF_REGISTERS_TO_WRITE, within RegisterValues capacity). Decoded from the request and
1334 // delivered regardless of status - see the write-acknowledgement note in modbus.h.
1335 RegisterValues registers;
1336 for (size_t i = 0; i != count_or_value; i++) {
1337 registers.push_back(helpers::get_data<uint16_t>(request_pdu.data(), 6 + 2 * i));
1338 }
1339 std::span<const uint16_t> register_span(registers.data(), registers.size());
1340 this->on_write_multiple_registers(start_address, register_span, status);
1341 break;
1342 }
1344 // Request layout: [0] function code, [1..2] start address, [3..4] coil count, [5] byte count,
1345 // [6..] packed bits. The gate guarantees the request carries exactly (count_or_value + 7) / 8 packed
1346 // bytes. Decoded from the request and delivered regardless of status - see the write-acknowledgement
1347 // note in modbus.h.
1348 std::span<const uint8_t> packed_bytes = request_pdu.subspan(6);
1349 PackedBits bits(packed_bytes, count_or_value);
1350 this->on_write_multiple_coils(start_address, bits, status);
1351 break;
1352 }
1353 default:
1354 this->on_custom_response(request_pdu, response_pdu, status);
1355 break;
1356 }
1357}
1358
1359// Default on_custom_response handler to warn when responses unexpectedly trigger on_custom_response
1360void ModbusClientDevice::on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
1361 ResponseStatus status) {
1362 // The dispatcher never calls this with an empty request, but this is a public virtual - stay safe.
1363 const uint8_t function_code = request_pdu.empty() ? 0 : request_pdu[0];
1364 // Warn once per device, then drop to VERBOSE: a mildly non-conformant peer answers every poll,
1365 // and an unhandled-response warning per transaction would flood the log permanently.
1366 if (!this->custom_response_warned_) {
1367 this->custom_response_warned_ = true;
1368 ESP_LOGW(TAG, "Non-standard request or response for function code 0x%X. No on_custom_response handler declared",
1369 function_code);
1370 } else {
1371 ESP_LOGV(TAG, "Non-standard request or response for function code 0x%X (unhandled)", function_code);
1372 }
1373}
1374
1375} // namespace esphome::modbus
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
uint8_t status
Definition bl0942.h:8
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
virtual void setup()=0
virtual void digital_write(bool value)=0
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:227
size_t size() const
Definition helpers.h:292
void push_back(const T &value)
Definition helpers.h:265
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
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
virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status)
Write acknowledgements.
Definition modbus.h:525
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:1360
virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status)
Definition modbus.h:513
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
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
virtual void on_read_input_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
Definition modbus.h:502
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:1146
void parse_modbus_frames() override
Definition modbus.cpp:174
ModbusDeviceCommand * find_waiting_()
Definition modbus.cpp:892
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
ModbusDeviceCommand * select_next_ready_()
Definition modbus.cpp:900
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
bool queue_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr, CommandOptions options={})
Queue a request.
Definition modbus.cpp:1034
void clear_tx_queue_for_address(uint8_t address)
Definition modbus.cpp:1136
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:307
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:234
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:769
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:1197
void loop() override
Definition modbus.cpp:45
float get_setup_priority() const override
Definition modbus.cpp:851
uint16_t long_rx_buffer_delay_ms_
Definition modbus.h:92
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:106
std::vector< uint8_t > rx_buffer_
Definition modbus.h:96
uint16_t find_custom_frame_end_(uint16_t min_length) const
Definition modbus.cpp:222
uint32_t last_receive_check_
Definition modbus.h:88
virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits)
Definition modbus.h:695
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:686
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_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
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:409
ResponseStatus check_address_range_(uint16_t start_address, uint16_t count)
Definition modbus.cpp:384
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:430
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:599
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:359
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:457
void process_broadcast_frame_(uint8_t function_code, std::span< const uint8_t > data)
Definition modbus.cpp:482
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:375
ResponseStatus parse_write_single_coil_(std::span< const uint8_t > data, uint16_t &start_address, bool &value)
Definition modbus.cpp:444
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:556
void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code)
Definition modbus.cpp:884
void assemble_registers_(std::span< const uint8_t > values, RegisterValues &registers)
Definition modbus.cpp:476
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:1166
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:856
ResponseStatus parse_write_multiple_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:413
bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status)
Definition modbus.cpp:871
ResponseStatus parse_write_single_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues &registers)
Definition modbus.cpp:405
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),...
static constexpr size_t RX_FULL_THRESHOLD_UNSET
UARTFlushResult flush()
Definition uart.h:48
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
UARTComponent * parent_
Definition uart.h:73
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
uint8_t priority
uint8_t options
bool address_range_fits(uint16_t start_address, size_t count)
bool is_function_code_read_only(uint8_t function_code)
uint8_t client_frame_data_offset(const uint8_t *, size_t)
T get_data(const uint8_t *data, size_t buffer_offset)
Extract data from modbus response buffer.
bool is_function_code_read(uint8_t function_code)
uint16_t client_frame_length(const uint8_t *frame, size_t size)
bool is_server_pdu_standard(const uint8_t *pdu, size_t size)
uint16_t server_frame_length(const uint8_t *frame, size_t size)
bool is_function_code_custom(uint8_t function_code)
bool is_client_pdu_standard(const uint8_t *pdu, size_t size)
bool is_function_code_exception(uint8_t function_code)
const uint8_t FUNCTION_CODE_MASK
StaticVector< uint16_t, MAX_NUM_OF_REGISTERS_TO_READ > RegisterValues
Definition modbus.h:347
const uint8_t FUNCTION_CODE_EXCEPTION_MASK
std::optional< ExceptionCode > ResponseStatus
Definition modbus.h:336
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
Definition modbus.h:342
constexpr size_t packed_bit_bytes(size_t bits)
Bits pack 8 per data byte, rounded up to whole bytes.
constexpr float BUS
For communication buses like i2c/spi.
Definition component.h:39
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
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:406
uint16_t size
Definition helpers.cpp:25
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1426
void HOT delay(uint32_t ms)
Definition hal.cpp:85
constexpr std::array< uint8_t, sizeof(T)> decode_value(T val)
Decode a value into its constituent bytes (from most to least significant).
Definition helpers.h:912
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
bool response(std::span< const uint8_t > response_pdu)
Definition modbus.cpp:935
CommandPriority priority() const
Definition modbus.h:147
static CommandPriority classify(uint8_t function_code)
Definition modbus.h:151
bool error(ExceptionCode exception_code)
Definition modbus.cpp:946
ModbusClientDevice * device
Definition modbus.h:127
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