ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
modbus.cpp
Go to the documentation of this file.
1#include "modbus.h"
4#include "esphome/core/log.h"
5
6namespace esphome::modbus {
7
8static const char *const TAG = "modbus";
9
10// Maximum bytes to log for Modbus frames (truncated if larger)
11static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
12
13// Approximate bits per character on the wire (depends on parity/stop bit config)
14static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11;
15// Milliseconds per second
16static constexpr uint32_t MS_PER_SEC = 1000;
17
19 if (this->flow_control_pin_ != nullptr) {
20 this->flow_control_pin_->setup();
21 }
22
23 this->frame_delay_ms_ =
24 std::max(2, // 1750us minimum per spec - rounded up to 2ms.
25 // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
26 (uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1);
27
28 // When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a
29 // meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay.
30 // Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks.
31 static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50;
32 size_t rx_threshold = this->parent_->get_rx_full_threshold();
35 ? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1
36 : DEFAULT_LONG_RX_BUFFER_DELAY_MS;
37}
38
40 // Receive any available bytes from UART
41 this->receive_bytes_();
42
43 // Parse bytes into frames and process them
44 this->parse_modbus_frames();
45}
46
48 // Call base class to receive bytes and parse frames
49 this->Modbus::loop();
50
51 // If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response
52 if (this->waiting_for_response_.has_value()) {
53 ModbusDeviceCommand &wfr = this->waiting_for_response_.value();
54 uint8_t expected_address = wfr.frame.data.data()[0];
55 if (this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ &&
56 (this->rx_buffer_.empty() || this->rx_buffer_[0] != expected_address)) {
57 ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", expected_address,
58 this->last_receive_check_ - this->last_send_);
59 this->notify_no_response_(wfr);
60 this->waiting_for_response_.reset();
61 }
62 }
63
64 // If there's no response pending and there's commands in the buffer
65 this->send_next_frame_();
66}
67
69 // If the response frame is finished (including interframe delay) - we timeout.
70 // The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
71 // when the buffer is filling the back half of the response
72 const uint16_t timeout = std::max(
73 (uint16_t) this->frame_delay_ms_,
74 (uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
75 : 0));
76
77 return this->last_receive_check_ - this->last_modbus_byte_ > timeout;
78}
79
81 // We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
82 // It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
83 // If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop
84 // then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
85 // So in this component we don't use any cached timestamp values to avoid these annoying bugs
86 const uint32_t now = millis();
87 return std::max({(int32_t) 0,
88 (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ - (now - this->last_send_)),
89 (int32_t) (this->frame_delay_ms_ - (now - this->last_modbus_byte_))});
90}
91
93 const uint32_t now = millis();
94 return std::max({(int32_t) 0,
95 (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
96 (now - this->last_send_)),
97 (int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
98}
99
101 // We block transmission in any of these cases:
102 // 1. There are bytes in the UART Rx buffer
103 // 2. There are bytes in our Rx buffer
104 // 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)
105 // 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)
106 // N.B. We allow a small delay (MODBUS_TX_MAX_DELAY_MS) to avoid looping on small delays. This gets handled by
107 // send_frame_.
108 return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_MS;
109}
110
112 // We block transmission in any of these case:
113 // 1. We're waiting for a response
114 // 2. Any of the base class tx_blocked conditions
115 return (this->waiting_for_response_.has_value()) || this->Modbus::tx_blocked();
116}
117
118bool ModbusClientHub::tx_buffer_empty() { return this->tx_buffer_.empty(); }
119
121 this->last_receive_check_ = millis();
122 size_t bytes = this->available();
123
124 if (bytes) {
125 size_t buffer_size = this->rx_buffer_.size();
127 this->rx_buffer_.resize(buffer_size + bytes);
128 if (!this->read_array(this->rx_buffer_.data() + buffer_size, bytes)) {
129 this->rx_buffer_.resize(buffer_size);
130 return;
131 }
132 if (buffer_size == 0) {
133 ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "ms after last send",
134 this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), millis() - this->last_send_);
135 }
136 }
137}
138
140 if (!this->rx_buffer_.empty()) {
141 size_t size;
142 do {
143 size = this->rx_buffer_.size();
144 if (!this->parse_modbus_server_frame_())
145 this->clear_rx_buffer_(LOG_STR("parse failed"), true);
146 } while (!this->rx_buffer_.empty() && size > this->rx_buffer_.size());
147 if (this->timeout_())
148 this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
149 }
150}
151
153 while (!this->rx_buffer_.empty()) {
154 size_t size = this->rx_buffer_.size();
155 ESP_LOGVV(TAG, "Parsing frames buffer size = %" PRIu32, size);
156 bool retry_as_client = false;
157 if (this->expecting_peer_response_ != 0) {
158 if (!this->parse_modbus_server_frame_()) {
159 ESP_LOGV(TAG, "Stop expecting peer response from %" PRIu8 " due to parse failure, and retry parse",
161 this->expecting_peer_response_ = 0;
162 retry_as_client = true;
163 } else if (this->timeout_() && size == this->rx_buffer_.size()) {
164 // If we timed out and the above parse attempt did not consume data, stop expecting a response
165 ESP_LOGV(TAG,
166 "Stop expecting peer response from %" PRIu8 " due to timeout after partial response, and retry parse",
168 this->expecting_peer_response_ = 0;
169 retry_as_client = true;
170 }
171 } else {
172 if (!this->parse_modbus_client_frame_())
173 this->clear_rx_buffer_(LOG_STR("parse failed"), true);
174 }
175 // Stop if the buffer didn't shrink (no frame consumed) and no mode switch triggered a retry
176 if (!retry_as_client && size <= this->rx_buffer_.size())
177 break;
178 }
179 if (this->timeout_())
180 this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
181}
182
183uint16_t Modbus::find_custom_frame_end_(uint16_t min_length) const {
184 // Custom functions could be any length - we have to rely on the CRC to determine completeness.
185 // If a CRC match is never found, the buffer will eventually overflow and be cleared.
186 const uint8_t *raw = &this->rx_buffer_[0];
187 const size_t size = this->rx_buffer_.size();
188 for (uint16_t len = min_length; len <= std::min(size, size_t(MAX_FRAME_SIZE)); len++) {
189 if (crc16(raw, len) == 0)
190 return len;
191 }
192 return 0;
193}
194
196 size_t size = this->rx_buffer_.size();
197 uint16_t frame_length = helpers::server_frame_length(this->rx_buffer_.data(), this->rx_buffer_.size());
198
199 if (size < frame_length)
200 return true;
201
202 uint8_t address = this->rx_buffer_[0];
203 uint8_t function_code = this->rx_buffer_[1];
204
205 if (helpers::is_function_code_custom(function_code)) {
206 frame_length = this->find_custom_frame_end_(frame_length);
207 if (frame_length == 0)
208 return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
209 ESP_LOGD(TAG, "User-defined function %02X found", function_code);
210 } else {
211 if (crc16(&this->rx_buffer_[0], frame_length) != 0)
212 return false;
213 }
214
215 // Process before clearing: process_modbus_server_frame (receiving a response or peer message) never sends a reply
216 // synchronously. We can safely point directly into rx_buffer_ and avoid a copy.
217 uint8_t data_offset = helpers::server_frame_data_offset(this->rx_buffer_.data(), this->rx_buffer_.size());
218 const uint8_t *data = this->rx_buffer_.data() + data_offset;
219 uint16_t data_len = frame_length - 2 - data_offset;
220
221 this->process_modbus_server_frame(address, function_code, data, data_len);
222 this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
223
224 return true;
225}
226
228 size_t size = this->rx_buffer_.size();
229 uint16_t frame_length = helpers::client_frame_length(this->rx_buffer_.data(), this->rx_buffer_.size());
230
231 if (size < frame_length)
232 return true;
233
234 uint8_t address = this->rx_buffer_[0];
235 uint8_t function_code = this->rx_buffer_[1];
236
237 if (helpers::is_function_code_custom(function_code)) {
238 frame_length = this->find_custom_frame_end_(frame_length);
239 if (frame_length == 0)
240 return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
241 ESP_LOGD(TAG, "User-defined function %02X found", function_code);
242 } else {
243 if (crc16(&this->rx_buffer_[0], frame_length) != 0)
244 return false;
245 }
246
247 // Clear before processing: process_modbus_client_frame_ dispatches to a server device which sends
248 // a response immediately. We need to clear the rx buffer first so the response doesn't snag tx_blocked.
249 // This requires copying the frame data to a local buffer beforehand.
250 uint8_t data_offset = helpers::client_frame_data_offset(this->rx_buffer_.data(), this->rx_buffer_.size());
251 uint16_t data_len = frame_length - 2 - data_offset;
252 uint8_t data[MAX_FRAME_SIZE] = {};
253 std::memcpy(data, this->rx_buffer_.data() + data_offset, data_len);
254 this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
255
256 this->process_modbus_client_frame_(address, function_code, data);
257
258 return true;
259}
260
261void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data,
262 uint16_t len) {
263 if (!this->waiting_for_response_.has_value()) {
264 ESP_LOGW(TAG,
265 "Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send",
267 return;
268 } else { // We are waiting for a response
269 // Check if the response matches the expected address and function code
270
271 ModbusDeviceCommand &wfr = this->waiting_for_response_.value();
272 uint8_t expected_address = wfr.frame.data.data()[0];
273 uint8_t expected_function_code = wfr.frame.data.data()[1];
274 if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) {
275 ESP_LOGW(TAG,
276 "Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32
277 "ms after last send",
278 address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code,
279 this->last_modbus_byte_ - this->last_send_);
280 // Invalidate the device; the entry survives as an interrupted shell so the late response is ignored.
281 // A retry requested here stays queued behind the shell until the send-wait timeout clears it.
282 this->notify_no_response_(wfr);
283 wfr.interrupted = true;
284 return;
285 }
286
287 if (wfr.interrupted) {
288 ESP_LOGW(TAG,
289 "Ignoring response from %" PRIu8 " - transmission interrupted by previous unexpected response, %" PRIu32
290 "ms after last send",
291 address, this->last_modbus_byte_ - this->last_send_);
292 return;
293 } else { // We have a valid device waiting for this response
294
296 this->waiting_for_response_.reset();
297 // Is it an error response?
298 if (helpers::is_function_code_exception(function_code)) {
299 uint8_t exception = len > 0 ? data[0] : 0;
300 ESP_LOGW(TAG,
301 "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send",
302 function_code, exception, address, this->last_modbus_byte_ - this->last_send_);
303 if (device)
305
306 } else if (device) { // Not an error response
307 // on_modbus_data is existing public API taking const std::vector<uint8_t>&
308 device->on_modbus_data(std::vector<uint8_t>(data, data + len));
309 } else { // Not an error response, but no device to respond to
310 ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send",
311 address, this->last_modbus_byte_ - this->last_send_);
312 }
313 }
314 }
315}
316
317void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *, uint16_t) {
318 if (this->find_device_(address) != nullptr) {
319 ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
320 }
321
322 if (this->expecting_peer_response_ == address) {
323 ESP_LOGV(TAG, "Expected response from peer %" PRIu8 " received", address);
324 } else {
325 ESP_LOGV(TAG, "Unexpected response from peer %" PRIu8 " received", address);
326 }
327
328 // This always resets, even if the address doesn't match.
329 // If an unexpected response is received, we can't trust that a correct response will follow (it shouldn't).
330 this->expecting_peer_response_ = 0;
331}
332
334 for (auto *device : this->devices_) {
335 if (device->get_address() == address) {
336 return device;
337 }
338 }
339 return nullptr;
340}
341
342bool ModbusServerHub::check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
343 uint16_t number_of_registers) {
344 if ((uint32_t) start_address + number_of_registers > 0x10000u) {
345 ESP_LOGW(TAG, "Register address out of range - start: %" PRIu16 " num: %" PRIu16, start_address,
346 number_of_registers);
347 this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
348 return false;
349 }
350 return true;
351}
352
353void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data) {
354 ModbusServerDevice *device = this->find_device_(address);
355 if (device == nullptr) {
357 ESP_LOGV(TAG, "Request to peer %" PRIu8 " received", address);
358 return;
359 }
360
362 uint8_t response_buffer[modbus::MAX_RAW_SIZE];
363 const uint8_t *response_data = response_buffer;
364 uint16_t response_len = 0;
365
366 switch (static_cast<ModbusFunctionCode>(function_code)) {
369 // PDU data: start address(2) + quantity(2).
370 uint16_t start_address = helpers::get_data<uint16_t>(data, 0);
371 uint16_t number_of_registers = helpers::get_data<uint16_t>(data, 2);
372 if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ) {
373 ESP_LOGW(TAG, "Invalid number of registers %" PRIu16, number_of_registers);
374 this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
375 return;
376 }
377 if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) {
378 return;
379 }
380 RegisterValues registers;
381 if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS) {
382 status = device->on_read_holding_registers(start_address, number_of_registers, registers);
383 } else {
384 status = device->on_read_input_registers(start_address, number_of_registers, registers);
385 }
386
387 // A handler that returns an exception leaves registers partially filled, so check the exception
388 // first and forward it before validating the register count on the success path.
389 if (status.has_value()) {
390 this->send_exception_(address, function_code, status.value());
391 return;
392 }
393
394 if (registers.size() != number_of_registers) {
395 ESP_LOGE(TAG, "Incorrect response %" PRIu16 " requested, %zu returned", number_of_registers, registers.size());
397 return;
398 }
399
400 response_buffer[response_len++] = static_cast<uint8_t>(number_of_registers * 2); // actual byte count
401 for (auto r : registers) {
402 auto register_bytes = decode_value(r);
403 response_buffer[response_len++] = register_bytes[0];
404 response_buffer[response_len++] = register_bytes[1];
405 }
406 break;
407 }
410 // PDU data: start address(2) [+ quantity(2) + byte count(1)] + register values.
411 // A single-register write always targets one register; for a multiple-register write the
412 // quantity is in the frame and its byte count must equal quantity * 2. The register values are
413 // assembled into registers below so the handler doesn't have to know the request framing.
414 uint16_t start_address = helpers::get_data<uint16_t>(data, 0);
415 uint16_t number_of_registers = 1;
416 uint16_t values_offset = 2; // single write: values follow the 2-byte start address
417 if (static_cast<ModbusFunctionCode>(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
418 number_of_registers = helpers::get_data<uint16_t>(data, 2);
419 uint8_t number_of_bytes = helpers::get_data<uint8_t>(data, 4);
420 values_offset = 5; // multiple write: values follow start address(2) + quantity(2) + byte count(1)
421 if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_WRITE ||
422 number_of_registers * 2 != number_of_bytes) {
423 ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 " or bytes %" PRIu8, number_of_registers,
424 number_of_bytes);
425 this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
426 return;
427 }
428 if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) {
429 return;
430 }
431 }
432 // Assemble the register values (host byte order) so the handler never sees wire framing.
433 RegisterValues registers;
434 for (uint16_t i = 0; i < number_of_registers; i++) {
435 registers.push_back(helpers::get_data<uint16_t>(data, values_offset + i * 2));
436 }
437 status = device->on_write_registers(start_address, registers);
438 response_data = data; // echo the request header per Modbus 6.6, 6.12
439 response_len = 4;
440 break;
441 }
442 default:
443 ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code);
444 this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
445 return;
446 }
447 if (status.has_value()) {
448 this->send_exception_(address, function_code, status.value());
449 } else {
450 this->send_response_(address, function_code, response_data, response_len);
451 }
452}
453
455 if (this->tx_blocked()) {
456 ESP_LOGE(TAG, "Attempted to send while transmission blocked");
457 return false;
458 }
459 if (frame.size() > MAX_FRAME_SIZE) {
460 ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %" PRIu16 " bytes", MAX_FRAME_SIZE);
461 return false;
462 }
463
464 const int32_t tx_delay_remaining = this->tx_delay_remaining();
465 if (tx_delay_remaining > 0) {
467 }
468
469 if (this->flow_control_pin_ != nullptr) {
470 this->flow_control_pin_->digital_write(true);
471 this->write_array(frame.data.data(), frame.size());
472 this->flush();
473 this->flow_control_pin_->digital_write(false);
474 this->last_send_tx_offset_ = 0;
475 } else {
476 this->write_array(frame.data.data(), frame.size());
477 this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
478 }
479
480 uint32_t now = millis();
481#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
482 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
483#endif
484 ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive",
485 format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_,
486 now - this->last_modbus_byte_);
487 this->last_send_ = now;
488 return true;
489}
490
492 if (this->tx_buffer_.empty()) {
493 return;
494 }
495
496 if (this->tx_blocked()) {
497 return;
498 }
499
500 ModbusDeviceCommand &command = this->tx_buffer_.front();
501
502 if (this->send_frame_(command.frame)) {
503 this->waiting_for_response_ = std::move(command);
504 } else {
505 if (command.device)
506 command.device->on_modbus_not_sent();
507 }
508
509 this->tx_buffer_.pop_front();
510
511 if (!this->tx_buffer_.empty()) {
512 ESP_LOGV(TAG, "Write queue contains %zu items.", this->tx_buffer_.size());
513 }
514}
515
517 ESP_LOGCONFIG(TAG,
518 "Modbus:\n"
519 " Send Wait Time: %" PRIu16 " ms\n"
520 " Turnaround Time: %" PRIu16 " ms\n"
521 " Frame Delay: %" PRIu16 " ms\n"
522 " Long Rx Buffer Delay: %" PRIu16 " ms",
525 LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
526}
528 ESP_LOGCONFIG(TAG,
529 "Modbus:\n"
530 " Frame Delay: %" PRIu16 " ms\n"
531 " Long Rx Buffer Delay: %" PRIu16 " ms",
533 LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
534}
535
537 // After UART bus
538 return setup_priority::BUS - 1.0f;
539}
540
541void ModbusServerHub::send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload,
542 uint16_t payload_len) {
543 // Build the raw frame (address + function code + payload) in a stack buffer; it's consumed
544 // immediately by send_raw_ and a full raw frame never exceeds MAX_RAW_SIZE.
545 if (payload_len + 2 > MAX_RAW_SIZE) {
546 ESP_LOGE(TAG, "Server response too large (%" PRIu16 " bytes)", static_cast<uint16_t>(payload_len + 2));
547 return;
548 }
549 uint8_t raw_frame[MAX_RAW_SIZE];
550 raw_frame[0] = address;
551 raw_frame[1] = function_code;
552 std::memcpy(raw_frame + 2, payload, payload_len);
553 this->send_raw_(raw_frame, payload_len + 2);
554}
555
556void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code) {
557 uint8_t raw_frame[3];
558 raw_frame[0] = address;
559 raw_frame[1] = function_code | FUNCTION_CODE_EXCEPTION_MASK;
560 raw_frame[2] = static_cast<uint8_t>(exception_code);
561 this->send_raw_(raw_frame, 3);
562}
563
564// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
566 if (wfr.device == nullptr)
567 return;
568 const bool retry = wfr.device->on_modbus_no_response();
569 // The callback may have detached the device (e.g. clear_tx_queue_for_device()); honor the detach
570 // over the retry request rather than re-queueing a frame that can no longer be routed.
571 if (retry && wfr.device != nullptr)
572 this->requeue_waiting_frame_(wfr);
573 // The old transaction is over either way; never deliver anything else to the device through it.
574 wfr.device = nullptr;
575}
576
578 const ModbusFrame &frame = wfr.frame;
579 if (this->tx_buffer_.size() >= MODBUS_TX_BUFFER_SIZE) {
580 ESP_LOGE(TAG, "Write buffer full, dropped retry for address %" PRIu8, frame.data.data()[0]);
581 if (wfr.device != nullptr)
583 return;
584 }
585 // Re-queue a copy (not a move): the waiting entry may have to survive as an interrupted shell.
586 this->tx_buffer_.emplace_back(wfr.device, frame.data.data()[0], frame.data.data() + 1, frame.size() - 3);
587}
588
589void ModbusClientHub::queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device) {
590 if (pdu_len == 0) {
591 if (device)
593 return;
594 }
595
596 if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) {
597#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
598 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
599#endif
600 ESP_LOGV(TAG, "Adding frame to tx queue: %" PRIu8 ":%s", address, format_hex_pretty_to(hex_buf, pdu, pdu_len));
601 this->tx_buffer_.emplace_back(device, address, pdu, pdu_len);
602 } else {
603#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
604 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
605#endif
606 ESP_LOGE(TAG, "Write buffer full, dropped: %" PRIu8 ":%s", address, format_hex_pretty_to(hex_buf, pdu, pdu_len));
607 if (device)
609 }
610}
611
613 // Remove any pending commands for this address from the tx buffer
614 auto &tx_buffer = this->tx_buffer_;
615 tx_buffer.erase(
616 std::remove_if(tx_buffer.begin(), tx_buffer.end(),
617 [address](const ModbusDeviceCommand &cmd) { return cmd.frame.data.data()[0] == address; }),
618 tx_buffer.end());
619
620 if (clear_sent && this->waiting_for_response_.has_value() && this->waiting_for_response_.value().device) {
621 if (this->waiting_for_response_.value().frame.data.data()[0] == address) {
622 ESP_LOGV(TAG, "Clearing waiting for response for address %" PRIu8, address);
623 // Invalidate the waiting device so it won't process a response.
624 this->waiting_for_response_.value().device = nullptr;
625 }
626 }
627}
629 // Remove any pending commands for this address from the tx buffer
630 auto &tx_buffer = this->tx_buffer_;
631 tx_buffer.erase(std::remove_if(tx_buffer.begin(), tx_buffer.end(),
632 [device](const ModbusDeviceCommand &cmd) { return cmd.device == device; }),
633 tx_buffer.end());
634
635 if (this->waiting_for_response_.has_value() && this->waiting_for_response_.value().device) {
636 if (this->waiting_for_response_.value().device == device) {
637 ESP_LOGV(TAG, "Clearing waiting for response");
638 // Invalidate the waiting device so it won't process a response.
639 this->waiting_for_response_.value().device = nullptr;
640 }
641 }
642}
643
644void ModbusClientHub::send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device) {
645 if (payload.size() < 2) {
646 if (device)
648 return;
649 }
650 this->queue_raw_(payload[0], payload.data() + 1, static_cast<uint16_t>(payload.size() - 1), device);
651}
652
653// Send raw command for server replies immediately. Except CRC everything must be contained in payload
654void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
655 if (len == 0) {
656 return;
657 }
658 if (len > MAX_RAW_SIZE) {
659 ESP_LOGE(TAG, "Server send frame too large (%" PRIu16 " bytes)", len);
660 return;
661 }
662
663 // In the rare case that the server is blocked (frame delay has not elapsed), we delay the send.
664 // This should only happen at low baud rates with long frame delays.
665 if (this->tx_blocked()) {
666 // Stash the raw payload in a single member buffer so the deferred callback can rebuild the frame
667 // without a heap allocation. Only one server reply is ever in flight, and the named timeout ensures
668 // only one deferred send is pending, so a single buffer is sufficient.
669 std::memcpy(this->deferred_payload_.data(), payload, len);
671 this->set_timeout("deferred_send", this->tx_delay_remaining(), [this]() {
672 ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
673 this->deferred_payload_len_ - 1);
674 this->send_frame_(frame);
675 });
676 } else {
677 ModbusFrame frame(payload[0], payload + 1, len - 1);
678 this->send_frame_(frame);
679 }
680}
681
682void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_to_clear) {
683 size_t bytes = this->rx_buffer_.size();
684 if (bytes_to_clear > 0 && bytes >= bytes_to_clear)
685 bytes = bytes_to_clear;
686 if (bytes > 0) {
687 if (warn) {
688 ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
689 millis() - this->last_send_);
690 } else {
691 ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
692 millis() - this->last_send_);
693 }
694 if (bytes == this->rx_buffer_.size()) {
695 this->rx_buffer_.clear();
696 } else {
697 this->rx_buffer_.erase(this->rx_buffer_.begin(), this->rx_buffer_.begin() + bytes);
698 }
699 }
700}
701
702} // 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:222
size_t size() const
Definition helpers.h:282
void push_back(const T &value)
Definition helpers.h:255
virtual bool on_modbus_no_response()
Called when no (valid) response arrived; return true to have the hub re-queue the frame for a retry.
Definition modbus.h:189
virtual void on_modbus_data(const std::vector< uint8_t > &data)
Definition modbus.h:183
virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code)
Definition modbus.h:184
std::optional< ModbusDeviceCommand > waiting_for_response_
Definition modbus.h:132
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:628
void parse_modbus_frames() override
Definition modbus.cpp:139
void requeue_waiting_frame_(ModbusDeviceCommand &wfr)
Definition modbus.cpp:577
void notify_no_response_(ModbusDeviceCommand &wfr)
Definition modbus.cpp:565
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
Definition modbus.h:105
void clear_tx_queue_for_address(uint8_t address, bool clear_sent=true)
Definition modbus.cpp:612
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override
Definition modbus.cpp:261
uint8_t uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:105
void queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device=nullptr)
Definition modbus.cpp:589
int32_t tx_delay_remaining() override
Definition modbus.cpp:92
void send_raw(const std::vector< uint8_t > &payload, ModbusClientDevice *device=nullptr)
Definition modbus.cpp:644
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:137
void setup() override
Definition modbus.cpp:18
uint16_t frame_delay_ms_
Definition modbus.h:74
bool parse_modbus_server_frame_()
Definition modbus.cpp:195
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:454
uint32_t last_modbus_byte_
Definition modbus.h:70
GPIOPin * flow_control_pin_
Definition modbus.h:77
uint32_t last_send_tx_offset_
Definition modbus.h:73
virtual bool tx_blocked()
Definition modbus.cpp:100
void clear_rx_buffer_(const LogString *reason, bool warn=false, size_t bytes_to_clear=0)
Definition modbus.cpp:682
void loop() override
Definition modbus.cpp:39
virtual void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len)=0
float get_setup_priority() const override
Definition modbus.cpp:536
uint16_t long_rx_buffer_delay_ms_
Definition modbus.h:75
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:80
std::vector< uint8_t > rx_buffer_
Definition modbus.h:79
uint16_t find_custom_frame_end_(uint16_t min_length) const
Definition modbus.cpp:183
uint32_t last_receive_check_
Definition modbus.h:71
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:250
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:246
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:242
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:161
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data)
Definition modbus.cpp:353
void parse_modbus_frames() override
Definition modbus.cpp:152
bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_registers)
Definition modbus.cpp:342
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:333
void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code)
Definition modbus.cpp:556
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:654
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:541
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override
Definition modbus.cpp:317
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
Definition modbus.h:165
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 server_frame_data_offset(const uint8_t *frame, size_t size)
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.
uint16_t client_frame_length(const uint8_t *frame, 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_function_code_exception(uint8_t function_code)
const uint8_t FUNCTION_CODE_MASK
const uint8_t FUNCTION_CODE_EXCEPTION_MASK
std::optional< ModbusExceptionCode > ResponseStatus
Definition modbus.h:221
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:340
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:1400
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:902
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
ModbusClientDevice * device
Definition modbus.h:86
SmallInlineBuffer< MODBUS_FRAME_INLINE_SIZE > data
Definition modbus.h:30
uint16_t size() const
Definition modbus.h:41