ESPHome 2026.3.0
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 {
7namespace modbus {
8
9static const char *const TAG = "modbus";
10
11// Maximum bytes to log for Modbus frames (truncated if larger)
12static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
13
14// Approximate bits per character on the wire (depends on parity/stop bit config)
15static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11;
16// Milliseconds per second
17static constexpr uint32_t MS_PER_SEC = 1000;
18
20 if (this->flow_control_pin_ != nullptr) {
21 this->flow_control_pin_->setup();
22 }
23
24 this->frame_delay_ms_ =
25 std::max(2, // 1750us minimum per spec - rounded up to 2ms.
26 // 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
27 (uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1);
28
29 // When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a
30 // meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay.
31 // Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks.
32 static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50;
33 size_t rx_threshold = this->parent_->get_rx_full_threshold();
36 ? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1
37 : DEFAULT_LONG_RX_BUFFER_DELAY_MS;
38}
39
41 // First process all available incoming data.
43
44 // If the response frame is finished (including interframe delay) - we timeout.
45 // The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
46 // when the buffer is filling the back half of the response
47 const uint16_t timeout = std::max(
48 (uint16_t) this->frame_delay_ms_,
49 (uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
50 : 0));
51 // We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
52 // It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
53 // If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop
54 // then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
55 // So in this component we don't use any cached timestamp values to avoid these annoying bugs
56 if (millis() - this->last_modbus_byte_ > timeout) {
57 this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
58 }
59
60 // If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response
61 if (this->waiting_for_response_ != 0 &&
62 millis() - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ &&
63 (this->rx_buffer_.empty() || this->rx_buffer_[0] != this->waiting_for_response_)) {
64 ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send",
65 this->waiting_for_response_, millis() - this->last_send_);
66 this->waiting_for_response_ = 0;
67 }
68
69 // If there's no response pending and there's commands in the buffer
70 this->send_next_frame_();
71}
72
74 const uint32_t now = millis();
75
76 // We block transmission in any of these case:
77 // 1. There are bytes in the UART Rx buffer
78 // 2. There are bytes in our Rx buffer
79 // 3. We're waiting for a response
80 // 4. The last sent byte isn't more than frame_delay ms ago (i.e. wait to tell receivers that our previous Tx is done)
81 // 5. The last received byte isn't more than frame_delay ms ago (i.e. wait to be sure there isn't more Rx coming)
82 // 6. If we're a client - also wait for the turnaround delay, to give the servers time to process the previous message
83 return this->available() || !this->rx_buffer_.empty() || (this->waiting_for_response_ != 0) ||
85 (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)) ||
86 (now - this->last_modbus_byte_ <
87 this->frame_delay_ms_ + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0));
88}
89
90bool Modbus::tx_buffer_empty() { return this->tx_buffer_.empty(); }
91
93 // Read all available bytes in batches to reduce UART call overhead.
94 size_t avail = this->available();
95 uint8_t buf[64];
96 while (avail > 0) {
97 size_t to_read = std::min(avail, sizeof(buf));
98 if (!this->read_array(buf, to_read)) {
99 break;
100 }
101 avail -= to_read;
102 for (size_t i = 0; i < to_read; i++) {
103 if (this->rx_buffer_.empty()) {
104 ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i],
105 millis() - this->last_send_);
106 } else {
107 ESP_LOGVV(TAG, "Received byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i],
108 millis() - this->last_send_);
109 }
110
111 // If the bytes in the rx buffer do not parse, clear out the buffer
112 if (!this->parse_modbus_byte_(buf[i])) {
113 this->clear_rx_buffer_(LOG_STR("parse failed"), true);
114 }
115 this->last_modbus_byte_ = millis();
116 }
117 }
118}
119
120bool Modbus::parse_modbus_byte_(uint8_t byte) {
121 size_t at = this->rx_buffer_.size();
122 this->rx_buffer_.push_back(byte);
123 const uint8_t *raw = &this->rx_buffer_[0];
124
125 // Byte 0: modbus address (match all)
126 if (at == 0)
127 return true;
128 // Byte 1: function code
129 if (at == 1)
130 return true;
131 // Byte 2: Size (with modbus rtu function code 4/3)
132 // See also https://en.wikipedia.org/wiki/Modbus
133 if (at == 2)
134 return true;
135
136 uint8_t address = raw[0];
137 uint8_t function_code = raw[1];
138
139 uint8_t data_len = raw[2];
140 uint8_t data_offset = 3;
141
142 // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
143 if (((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT) &&
144 (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END)) ||
145 ((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT) &&
146 (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END))) {
147 // Handle user-defined function, since we don't know how big this ought to be,
148 // ideally we should delegate the entire length detection to whatever handler is
149 // installed, but wait, there is the CRC, and if we get a hit there is a good
150 // chance that this is a complete message ... admittedly there is a small chance is
151 // isn't but that is quite small given the purpose of the CRC in the first place
152
153 data_len = at - 2;
154 data_offset = 1;
155
156 uint16_t computed_crc = crc16(raw, data_offset + data_len);
157 uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
158
159 if (computed_crc != remote_crc)
160 return true;
161
162 ESP_LOGD(TAG, "User-defined function %02X found", function_code);
163
164 } else {
165 // data starts at 2 and length is 4 for read registers commands
166 if (this->role == ModbusRole::SERVER) {
167 if (function_code == ModbusFunctionCode::READ_COILS ||
172 data_offset = 2;
173 data_len = 4;
174 } else if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
175 if (at < 6) {
176 return true;
177 }
178 data_offset = 2;
179 // starting address (2 bytes) + quantity of registers (2 bytes) + byte count itself (1 byte) + actual byte count
180 data_len = 2 + 2 + 1 + raw[6];
181 }
182 } else {
183 // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands
184 if (function_code == ModbusFunctionCode::WRITE_SINGLE_COIL ||
188 data_offset = 2;
189 data_len = 4;
190 }
191 }
192
193 // Error ( msb indicates error )
194 // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc
196 data_offset = 2;
197 data_len = 1;
198 }
199
200 // Byte data_offset..data_offset+data_len-1: Data
201 if (at < data_offset + data_len)
202 return true;
203
204 // Byte 3+data_len: CRC_LO (over all bytes)
205 if (at == data_offset + data_len)
206 return true;
207
208 // Byte data_offset+len+1: CRC_HI (over all bytes)
209 uint16_t computed_crc = crc16(raw, data_offset + data_len);
210 uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
211 if (computed_crc != remote_crc) {
212 if (this->disable_crc_) {
213 ESP_LOGD(TAG, "CRC check failed %" PRIu32 "ms after last send; ignoring", millis() - this->last_send_);
214#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
215 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
216#endif
217 ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc,
218 format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size()));
219 } else {
220 ESP_LOGW(TAG, "CRC check failed %" PRIu32 "ms after last send", millis() - this->last_send_);
221#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
222 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
223#endif
224 ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc,
225 format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size()));
226 return false;
227 }
228 }
229 }
230 std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
231 bool found = false;
232 for (auto *device : this->devices_) {
233 if (device->address_ == address) {
234 found = true;
235 if (this->role == ModbusRole::SERVER) {
236 if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
238 device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
239 uint16_t(data[3]) | (uint16_t(data[2]) << 8));
240 } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
242 device->on_modbus_write_registers(function_code, data);
243 }
244 } else { // We're a client
245 // Is it an error response?
247 uint8_t exception = raw[2];
248 ESP_LOGW(TAG,
249 "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32
250 "ms after last send",
251 function_code, exception, address, millis() - this->last_send_);
252 if (this->waiting_for_response_ == address) {
253 device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception);
254 } else {
255 // Ignore modbus exception not related to a pending command
256 ESP_LOGD(TAG, "Ignoring error - not expecting a response from %" PRIu8 "", address);
257 }
258 } else { // Not an error response
259 if (this->waiting_for_response_ == address) {
260 device->on_modbus_data(data);
261 } else {
262 // Ignore modbus response not related to a pending command
263 ESP_LOGW(TAG, "Ignoring response - not expecting a response from %" PRIu8 ", %" PRIu32 "ms after last send",
264 address, millis() - this->last_send_);
265 }
266 }
267 }
268 }
269 }
270
271 if (!found && this->role == ModbusRole::CLIENT) {
272 ESP_LOGW(TAG, "Got frame from unknown address %" PRIu8 ", %" PRIu32 "ms after last send", address,
273 millis() - this->last_send_);
274 }
275
276 this->clear_rx_buffer_(LOG_STR("parse succeeded"));
277
278 if (this->waiting_for_response_ == address)
279 this->waiting_for_response_ = 0;
280
281 return true;
282}
283
285 if (this->tx_buffer_.empty())
286 return;
287
288 if (this->tx_blocked())
289 return;
290
291 const ModbusDeviceCommand &frame = this->tx_buffer_.front();
292
293 if (this->role == ModbusRole::CLIENT) {
294 this->waiting_for_response_ = frame.data.get()[0];
295 }
296
297 if (this->flow_control_pin_ != nullptr) {
298 this->flow_control_pin_->digital_write(true);
299 this->write_array(frame.data.get(), frame.size);
300 this->flush();
301 this->flow_control_pin_->digital_write(false);
302 this->last_send_tx_offset_ = 0;
303 } else {
304 this->write_array(frame.data.get(), frame.size);
305 this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
306 }
307
308#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
309 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
310#endif
311 ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send", format_hex_pretty_to(hex_buf, frame.data.get(), frame.size),
312 millis() - this->last_send_);
313 this->last_send_ = millis();
314 this->tx_buffer_.pop_front();
315 if (!this->tx_buffer_.empty()) {
316 ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size());
317 }
318}
319
321 ESP_LOGCONFIG(TAG,
322 "Modbus:\n"
323 " Send Wait Time: %d ms\n"
324 " Turnaround Time: %d ms\n"
325 " Frame Delay: %d ms\n"
326 " Long Rx Buffer Delay: %d ms\n"
327 " CRC Disabled: %s",
329 this->long_rx_buffer_delay_ms_, YESNO(this->disable_crc_));
330 LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
331}
333 // After UART bus
334 return setup_priority::BUS - 1.0f;
335}
336
337void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
338 uint8_t payload_len, const uint8_t *payload) {
339 static const size_t MAX_VALUES = 128;
340
341 // Only check max number of registers for standard function codes
342 // Some devices use non standard codes like 0x43
343 if (number_of_entities > MAX_VALUES && function_code <= ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
344 ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES);
345 return;
346 }
347
348 uint8_t data[MAX_FRAME_SIZE];
349 size_t pos = 0;
350
351 data[pos++] = address;
352 data[pos++] = function_code;
353 if (this->role == ModbusRole::CLIENT) {
354 data[pos++] = start_address >> 8;
355 data[pos++] = start_address >> 0;
356 if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
358 data[pos++] = number_of_entities >> 8;
359 data[pos++] = number_of_entities >> 0;
360 }
361 }
362
363 if (payload != nullptr) {
364 if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
365 function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
366 data[pos++] = payload_len; // Byte count is required for write
367 } else {
368 payload_len = 2; // Write single register or coil
369 }
370 if (payload_len + pos + 2 > MAX_FRAME_SIZE) { // Check if payload fits (accounting for CRC)
371 ESP_LOGE(TAG, "Payload too large to send: %d bytes", payload_len);
372 return;
373 }
374 for (int i = 0; i < payload_len; i++) {
375 data[pos++] = payload[i];
376 }
377 }
378
379 this->queue_raw_(data, pos);
380}
381
382// Helper function for lambdas
383// Send raw command. Except CRC everything must be contained in payload
384void Modbus::send_raw(const std::vector<uint8_t> &payload) {
385 if (payload.empty()) {
386 return;
387 }
388 // Frame size: payload + CRC(2)
389 if (payload.size() + 2 > MAX_FRAME_SIZE) {
390 ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %d bytes", MAX_FRAME_SIZE);
391 return;
392 }
393 // Use stack buffer - Modbus frames are small and bounded
394 uint8_t data[MAX_FRAME_SIZE];
395
396 std::memcpy(data, payload.data(), payload.size());
397
398 this->queue_raw_(data, payload.size());
399}
400
401// Assume data and length is valid and append CRC, then queue for sending. Used internally to avoid unnecessary copying
402// of data into vectors
403void Modbus::queue_raw_(const uint8_t *data, uint16_t len) {
404 if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) {
405 this->tx_buffer_.emplace_back(data, len);
406 } else {
407#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
408 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
409#endif
410 ESP_LOGE(TAG, "Write buffer full, dropped: %s", format_hex_pretty_to(hex_buf, data, len));
411 }
412}
413
414void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) {
415 size_t at = this->rx_buffer_.size();
416 if (at > 0) {
417 if (warn) {
418 ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason),
419 millis() - this->last_send_);
420 } else {
421 ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason),
422 millis() - this->last_send_);
423 }
424 this->rx_buffer_.clear();
425 }
426}
427
428} // namespace modbus
429} // namespace esphome
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
virtual void setup()=0
virtual void digital_write(bool value)=0
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:89
bool parse_modbus_byte_(uint8_t byte)
Definition modbus.cpp:120
void send_raw(const std::vector< uint8_t > &payload)
Definition modbus.cpp:384
void setup() override
Definition modbus.cpp:19
uint16_t frame_delay_ms_
Definition modbus.h:75
uint16_t send_wait_time_
Definition modbus.h:77
uint8_t waiting_for_response_
Definition modbus.h:79
uint32_t last_modbus_byte_
Definition modbus.h:72
GPIOPin * flow_control_pin_
Definition modbus.h:82
std::vector< ModbusDevice * > devices_
Definition modbus.h:85
uint32_t last_send_tx_offset_
Definition modbus.h:74
void loop() override
Definition modbus.cpp:40
void queue_raw_(const uint8_t *data, uint16_t len)
Definition modbus.cpp:403
float get_setup_priority() const override
Definition modbus.cpp:332
uint16_t long_rx_buffer_delay_ms_
Definition modbus.h:76
void dump_config() override
Definition modbus.cpp:320
void receive_and_parse_modbus_bytes_()
Definition modbus.cpp:92
std::vector< uint8_t > rx_buffer_
Definition modbus.h:84
void clear_rx_buffer_(const LogString *reason, bool warn=false)
Definition modbus.cpp:414
void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len=0, const uint8_t *payload=nullptr)
Definition modbus.cpp:337
uint16_t turnaround_delay_ms_
Definition modbus.h:78
static constexpr size_t RX_FULL_THRESHOLD_UNSET
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
UARTComponent * parent_
Definition uart.h:73
FlushResult flush()
Definition uart.h:48
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
const uint8_t FUNCTION_CODE_MASK
const uint8_t FUNCTION_CODE_EXCEPTION_MASK
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT
Modbus definitions from specs: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3....
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END
constexpr float BUS
For communication buses like i2c/spi.
Definition component.h:25
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
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:73
std::string size_t len
Definition helpers.h:892
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:353
size_t size_t pos
Definition helpers.h:929
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:1200
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
static void uint32_t
std::unique_ptr< uint8_t[]> data
Definition modbus.h:27