ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
modbus_helpers.cpp
Go to the documentation of this file.
1#include "modbus_helpers.h"
2#include "esphome/core/log.h"
3
4#include <algorithm>
5
7
8static const char *const TAG = "modbus_helpers";
9
10uint16_t server_frame_length(const uint8_t *frame, size_t size) {
11 if (size < 2)
12 return MIN_FRAME_SIZE;
13 if (is_function_code_exception(frame[1])) {
14 return 5; // address(1) + function(1) + exception(1) + CRC(2)
15 }
16 switch (static_cast<ModbusFunctionCode>(frame[1])) {
21 // address(1) + function(1) + byte count(1) + data + CRC(2)
22 return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
27 return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2)
28 // Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
31 // address(1) + function(1) + byte count(1) + data + CRC(2)
32 return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_FRAME_SIZE - 5)) : 0);
34 return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2)
36 // address(1) + function(1) + byte count(1) + data + CRC(2)
37 return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
39 // address(1) + function(1) + fifo address(2) CRC(2)
40 return 6;
41 default:
42 return MIN_FRAME_SIZE; // unknown length
43 }
44}
45
46uint16_t client_frame_length(const uint8_t *frame, size_t size) {
47 if (size < 2)
48 return MIN_FRAME_SIZE;
49 switch (static_cast<ModbusFunctionCode>(frame[1])) {
54 // address(1) + function(1) + start address(2) + quantity(2) + CRC(2)
57 return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2)
60 // address(1) + function(1) + start address(2) + quantity(2) + byte count(1) + data + CRC(2)
61 return 9 + (size > 6 ? std::min(frame[6], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0);
62 // Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
65 // address(1) + function(1) + byte count(1) + data + CRC(2)
66 return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_FRAME_SIZE - 5)) : 0);
68 return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2)
70 // address(1) + function(1) + read start address(2) + read quantity(2) + write start address(2) +
71 // write quantity(2) + byte count(1) + data + CRC(2)
72 return 13 + (size > 10 ? std::min(frame[10], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0);
74 // address(1) + function(1) + fifo address(2) CRC(2)
75 return 6;
76 default:
77 return MIN_FRAME_SIZE; // unknown length
78 }
79}
80
81static size_t required_payload_size(SensorValueType sensor_value_type) {
82 switch (sensor_value_type) {
85 return 2;
92 return 4;
97 return 8;
99 default:
100 return 0;
101 }
102}
103
105 ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
106}
107
108std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type,
109 uint8_t offset, uint32_t bitmask) {
110 int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
111
112 // Validate offset against the buffer for all types, including RAW/unsupported, so
113 // a malformed or misconfigured frame still produces an error log.
114 if (static_cast<size_t>(offset) > size) {
115 ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
116 static_cast<unsigned int>(offset), size);
117 return std::nullopt;
118 }
119
120 const size_t required_size = required_payload_size(sensor_value_type);
121 if (required_size == 0) {
122 return value;
123 }
124
125 if (size - offset < required_size) {
126 ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
127 static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), size, required_size);
128 return std::nullopt;
129 }
130
131 switch (sensor_value_type) {
133 value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
134 break;
137 value = get_data<uint32_t>(data, offset);
138 value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
139 break;
142 value = get_data<uint32_t>(data, offset);
143 value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
144 value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
145 break;
147 value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), bitmask); // default is 0xFFFF ;
148 break;
150 value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
151 break;
153 value = get_data<uint32_t>(data, offset);
154 // Currently the high word is at the low position
155 // the sign bit is therefore at low before the switch
156 uint32_t sign_bit = (value & 0x8000) << 16;
158 static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
159 } break;
162 // Ignore bitmask for QWORD
163 value = get_data<uint64_t>(data, offset);
164 break;
167 // Ignore bitmask for QWORD
168 uint64_t tmp = get_data<uint64_t>(data, offset);
169 value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
170 } break;
172 default:
173 break;
174 }
175 return value;
176}
177
178std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type) {
179 const size_t required_size = required_payload_size(sensor_value_type);
180 if (required_size == 0) {
181 return 0; // RAW/unsupported: nothing to read
182 }
183 const size_t required_words = required_size / 2;
184 if (required_words > count) {
185 ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu",
186 static_cast<unsigned int>(sensor_value_type), count, required_words);
187 return std::nullopt;
188 }
189 // Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the
190 // sign-extension behaviour stays identical to the wire path.
191 uint8_t bytes[8]; // at most 4 registers (QWORD)
192 for (size_t i = 0; i < required_words; i++) {
193 uint16_t reg = registers[i];
194 bytes[i * 2] = static_cast<uint8_t>(reg >> 8);
195 bytes[i * 2 + 1] = static_cast<uint8_t>(reg & 0xFF);
196 }
197 return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF);
198}
199
201 uint16_t number_of_entities, const uint8_t *values,
202 size_t values_len) {
203 if (is_function_code_read(static_cast<uint8_t>(function_code))) {
204 if (values != nullptr || values_len > 0) {
205 ESP_LOGW(TAG, "Values provided for read function code %02X, but will be ignored",
206 static_cast<uint8_t>(function_code));
207 }
208 } else if (is_function_code_write(static_cast<uint8_t>(function_code))) {
209 if (values == nullptr || values_len == 0) {
210 ESP_LOGE(TAG, "No values provided for write function code %02X", static_cast<uint8_t>(function_code));
211 return {};
212 }
213 } else {
214 ESP_LOGE(TAG, "Unsupported function code %02X for client PDU creation", static_cast<uint8_t>(function_code));
215 return {};
216 }
217
218 if (number_of_entities == 0) {
219 ESP_LOGE(TAG, "Number of entities is zero for function code %02X", static_cast<uint8_t>(function_code));
220 return {};
221 }
222
223 switch (function_code) {
225 if (number_of_entities > MAX_NUM_OF_COILS_TO_READ) {
226 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum coils to read %u for function code %02X",
227 number_of_entities, MAX_NUM_OF_COILS_TO_READ, static_cast<uint8_t>(function_code));
228 return {};
229 }
230 break;
232 if (number_of_entities > MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) {
233 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum discrete inputs to read %u for function code %02X",
234 number_of_entities, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ, static_cast<uint8_t>(function_code));
235 return {};
236 }
237 break;
240 if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_READ) {
241 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to read %u for function code %02X",
242 number_of_entities, MAX_NUM_OF_REGISTERS_TO_READ, static_cast<uint8_t>(function_code));
243 return {};
244 }
245 break;
248 break; // number_of_entities is ignored for single write, so no need to validate
251 if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_WRITE) {
252 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to write %u for function code %02X",
253 number_of_entities, MAX_NUM_OF_REGISTERS_TO_WRITE, static_cast<uint8_t>(function_code));
254 return {};
255 }
256 break;
257 default:
258 ESP_LOGE(TAG, "Unsupported function code %u for client PDU creation", static_cast<unsigned int>(function_code));
259 return {};
260 }
261
263 pdu.push_back(static_cast<uint8_t>(function_code));
264 pdu.push_back(start_address >> 8);
265 pdu.push_back(start_address >> 0);
266 if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
268 pdu.push_back(number_of_entities >> 8);
269 pdu.push_back(number_of_entities >> 0);
270 }
271
272 if (is_function_code_write(static_cast<uint8_t>(function_code))) {
273 if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
275 // 6 bytes of overhead (fc + start_addr×2 + qty×2 + byte_count) leave MAX_PDU_SIZE-6 bytes for values
276 static constexpr size_t MAX_WRITE_MULTIPLE_VALUES_LEN = MAX_PDU_SIZE - 6;
277 if (values_len > MAX_WRITE_MULTIPLE_VALUES_LEN) {
278 ESP_LOGE(TAG, "values_len %zu exceeds PDU capacity %zu, dropping request", values_len,
279 MAX_WRITE_MULTIPLE_VALUES_LEN);
280 return {};
281 }
282 pdu.push_back(values_len); // Byte count is required for write multiple
283 for (size_t i = 0; i < values_len; i++)
284 pdu.push_back(values[i]);
285 } else {
286 // Write single register or coil (2 bytes)
287 if (values_len < 2) {
288 ESP_LOGE(TAG, "values_len %zu too small for write-single command (need 2), dropping request", values_len);
289 return {};
290 }
291 pdu.push_back(values[0]);
292 pdu.push_back(values[1]);
293 }
294 }
295 return pdu;
296}
297} // namespace esphome::modbus::helpers
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:222
void push_back(const T &value)
Definition helpers.h:255
std::span< const uint8_t > data
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)
uint16_t client_frame_length(const uint8_t *frame, size_t size)
N mask_and_shift_by_rightbit(N data, uint32_t mask)
Extract bits from value and shift right according to the bitmask if the bitmask is 0x00F0 we want the...
uint16_t server_frame_length(const uint8_t *frame, size_t size)
void log_unsupported_value_type(SensorValueType value_type)
StaticVector< uint8_t, MAX_PDU_SIZE > create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len)
Create a modbus clinet pdu for reading/writing single/multiple coils/register/inputs.
std::optional< int64_t > registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type)
Reconstruct a number from register words (host byte order).
bool is_function_code_exception(uint8_t function_code)
std::optional< int64_t > payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask)
Convert a raw response payload to a number.
uint16_t size
Definition helpers.cpp:25
static void uint32_t