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