ESPHome 2026.2.1
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
15 if (this->flow_control_pin_ != nullptr) {
16 this->flow_control_pin_->setup();
17 }
18}
20 const uint32_t now = App.get_loop_component_start_time();
21
22 // Read all available bytes in batches to reduce UART call overhead.
23 size_t avail = this->available();
24 uint8_t buf[64];
25 while (avail > 0) {
26 size_t to_read = std::min(avail, sizeof(buf));
27 if (!this->read_array(buf, to_read)) {
28 break;
29 }
30 avail -= to_read;
31
32 for (size_t i = 0; i < to_read; i++) {
33 if (this->parse_modbus_byte_(buf[i])) {
34 this->last_modbus_byte_ = now;
35 } else {
36 size_t at = this->rx_buffer_.size();
37 if (at > 0) {
38 ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
39 this->rx_buffer_.clear();
40 }
41 }
42 }
43 }
44
45 if (now - this->last_modbus_byte_ > 50) {
46 size_t at = this->rx_buffer_.size();
47 if (at > 0) {
48 ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at);
49 this->rx_buffer_.clear();
50 }
51
52 // stop blocking new send commands after sent_wait_time_ ms after response received
53 if (now - this->last_send_ > send_wait_time_) {
54 if (waiting_for_response > 0) {
55 ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
56 }
58 }
59 }
60}
61
62bool Modbus::parse_modbus_byte_(uint8_t byte) {
63 size_t at = this->rx_buffer_.size();
64 this->rx_buffer_.push_back(byte);
65 const uint8_t *raw = &this->rx_buffer_[0];
66 ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte);
67 // Byte 0: modbus address (match all)
68 if (at == 0)
69 return true;
70 uint8_t address = raw[0];
71 uint8_t function_code = raw[1];
72 // Byte 2: Size (with modbus rtu function code 4/3)
73 // See also https://en.wikipedia.org/wiki/Modbus
74 if (at == 2)
75 return true;
76
77 uint8_t data_len = raw[2];
78 uint8_t data_offset = 3;
79
80 // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
81 if (((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT) &&
82 (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END)) ||
83 ((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT) &&
84 (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END))) {
85 // Handle user-defined function, since we don't know how big this ought to be,
86 // ideally we should delegate the entire length detection to whatever handler is
87 // installed, but wait, there is the CRC, and if we get a hit there is a good
88 // chance that this is a complete message ... admittedly there is a small chance is
89 // isn't but that is quite small given the purpose of the CRC in the first place
90
91 // Fewer than 2 bytes can't calc CRC
92 if (at < 2)
93 return true;
94
95 data_len = at - 2;
96 data_offset = 1;
97
98 uint16_t computed_crc = crc16(raw, data_offset + data_len);
99 uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
100
101 if (computed_crc != remote_crc)
102 return true;
103
104 ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
105
106 } else {
107 // data starts at 2 and length is 4 for read registers commands
108 if (this->role == ModbusRole::SERVER) {
109 if (function_code == ModbusFunctionCode::READ_COILS ||
114 data_offset = 2;
115 data_len = 4;
116 } else if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
117 if (at < 6) {
118 return true;
119 }
120 data_offset = 2;
121 // starting address (2 bytes) + quantity of registers (2 bytes) + byte count itself (1 byte) + actual byte count
122 data_len = 2 + 2 + 1 + raw[6];
123 }
124 } else {
125 // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands
126 if (function_code == ModbusFunctionCode::WRITE_SINGLE_COIL ||
130 data_offset = 2;
131 data_len = 4;
132 }
133 }
134
135 // Error ( msb indicates error )
136 // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc
138 data_offset = 2;
139 data_len = 1;
140 }
141
142 // Byte data_offset..data_offset+data_len-1: Data
143 if (at < data_offset + data_len)
144 return true;
145
146 // Byte 3+data_len: CRC_LO (over all bytes)
147 if (at == data_offset + data_len)
148 return true;
149
150 // Byte data_offset+len+1: CRC_HI (over all bytes)
151 uint16_t computed_crc = crc16(raw, data_offset + data_len);
152 uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
153 if (computed_crc != remote_crc) {
154 if (this->disable_crc_) {
155 ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc);
156 } else {
157 ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
158 return false;
159 }
160 }
161 }
162 std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
163 bool found = false;
164 for (auto *device : this->devices_) {
165 if (device->address_ == address) {
166 found = true;
167 // Is it an error response?
169 ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]);
170 if (waiting_for_response != 0) {
171 device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]);
172 } else {
173 // Ignore modbus exception not related to a pending command
174 ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
175 }
176 continue;
177 }
178 if (this->role == ModbusRole::SERVER) {
179 if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
181 device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
182 uint16_t(data[3]) | (uint16_t(data[2]) << 8));
183 continue;
184 }
185 if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
187 device->on_modbus_write_registers(function_code, data);
188 continue;
189 }
190 }
191 // fallthrough for other function codes
192 device->on_modbus_data(data);
193 }
194 }
196
197 if (!found) {
198 ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address);
199 }
200
201 // reset buffer
202 ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at);
203 this->rx_buffer_.clear();
204 return true;
205}
206
208 ESP_LOGCONFIG(TAG,
209 "Modbus:\n"
210 " Send Wait Time: %d ms\n"
211 " CRC Disabled: %s",
212 this->send_wait_time_, YESNO(this->disable_crc_));
213 LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
214}
216 // After UART bus
217 return setup_priority::BUS - 1.0f;
218}
219
220void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
221 uint8_t payload_len, const uint8_t *payload) {
222 static const size_t MAX_VALUES = 128;
223
224 // Only check max number of registers for standard function codes
225 // Some devices use non standard codes like 0x43
226 if (number_of_entities > MAX_VALUES && function_code <= ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
227 ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES);
228 return;
229 }
230
231 static constexpr size_t ADDR_SIZE = 1;
232 static constexpr size_t FC_SIZE = 1;
233 static constexpr size_t START_ADDR_SIZE = 2;
234 static constexpr size_t NUM_ENTITIES_SIZE = 2;
235 static constexpr size_t BYTE_COUNT_SIZE = 1;
236 static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
237 static constexpr size_t CRC_SIZE = 2;
238 static constexpr size_t MAX_FRAME_SIZE =
239 ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
240 uint8_t data[MAX_FRAME_SIZE];
241 size_t pos = 0;
242
243 data[pos++] = address;
244 data[pos++] = function_code;
245 if (this->role == ModbusRole::CLIENT) {
246 data[pos++] = start_address >> 8;
247 data[pos++] = start_address >> 0;
248 if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
250 data[pos++] = number_of_entities >> 8;
251 data[pos++] = number_of_entities >> 0;
252 }
253 }
254
255 if (payload != nullptr) {
256 if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
257 function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
258 data[pos++] = payload_len; // Byte count is required for write
259 } else {
260 payload_len = 2; // Write single register or coil
261 }
262 for (int i = 0; i < payload_len; i++) {
263 data[pos++] = payload[i];
264 }
265 }
266
267 auto crc = crc16(data, pos);
268 data[pos++] = crc >> 0;
269 data[pos++] = crc >> 8;
270
271 if (this->flow_control_pin_ != nullptr)
272 this->flow_control_pin_->digital_write(true);
273
274 this->write_array(data, pos);
275 this->flush();
276
277 if (this->flow_control_pin_ != nullptr)
278 this->flow_control_pin_->digital_write(false);
280 last_send_ = millis();
281#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
282 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
283#endif
284 ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
285}
286
287// Helper function for lambdas
288// Send raw command. Except CRC everything must be contained in payload
289void Modbus::send_raw(const std::vector<uint8_t> &payload) {
290 if (payload.empty()) {
291 return;
292 }
293
294 if (this->flow_control_pin_ != nullptr)
295 this->flow_control_pin_->digital_write(true);
296
297 auto crc = crc16(payload.data(), payload.size());
298 this->write_array(payload);
299 this->write_byte(crc & 0xFF);
300 this->write_byte((crc >> 8) & 0xFF);
301 this->flush();
302 if (this->flow_control_pin_ != nullptr)
303 this->flow_control_pin_->digital_write(false);
304 waiting_for_response = payload[0];
305#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
306 char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
307#endif
308 ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size()));
309 last_send_ = millis();
310}
311
312} // namespace modbus
313} // namespace esphome
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
virtual void setup()=0
virtual void digital_write(bool value)=0
bool parse_modbus_byte_(uint8_t byte)
Definition modbus.cpp:62
void send_raw(const std::vector< uint8_t > &payload)
Definition modbus.cpp:289
void setup() override
Definition modbus.cpp:14
uint16_t send_wait_time_
Definition modbus.h:49
uint8_t waiting_for_response
Definition modbus.h:39
uint32_t last_modbus_byte_
Definition modbus.h:52
GPIOPin * flow_control_pin_
Definition modbus.h:46
std::vector< ModbusDevice * > devices_
Definition modbus.h:54
void loop() override
Definition modbus.cpp:19
float get_setup_priority() const override
Definition modbus.cpp:215
void dump_config() override
Definition modbus.cpp:207
std::vector< uint8_t > rx_buffer_
Definition modbus.h:51
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:220
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
void write_byte(uint8_t data)
Definition uart.h:18
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
const float BUS
For communication buses like i2c/spi.
Definition component.cpp:81
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
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:729
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:978
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.