ESPHome 2026.9.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
10// A quantity/address pair is standard when the quantity is non-zero, within the per-table maximum,
11// and the range [start_address, start_address + quantity) stays inside the 16-bit address space.
12// Non-logging twin of register_block_in_range(): the same three predicates for the parser side, taking a
13// uint16_t quantity. register_block_in_range() is the builder-side variant that also logs which half failed.
14static bool quantity_in_range(uint16_t start_address, uint16_t quantity, uint16_t max_quantity) {
15 return quantity != 0 && quantity <= max_quantity && address_range_fits(start_address, quantity);
16}
17
18// The spec allows exactly ON (0xFF00) and OFF (0x0000) for a single-coil value, on the request and
19// on its echoed response alike.
20static bool is_canonical_coil_value(uint8_t high_byte, uint8_t low_byte) {
21 return (high_byte == 0xFF || high_byte == 0x00) && low_byte == 0x00;
22}
23
24uint16_t server_pdu_length(const uint8_t *frame, size_t size) {
25 if (size < MIN_PDU_SIZE)
26 return MIN_PDU_SIZE;
27 if (is_function_code_exception(frame[0])) {
28 return 2; // function(1) + exception(1)
29 }
30 switch (static_cast<FunctionCode>(frame[0])) {
33 // function(1) + byte count(1) + packed coil bytes
34 return 2 + (size > 1 ? std::min(frame[1], uint8_t(packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ))) : 0);
37 // function(1) + byte count(1) + register data
38 return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
43 return 5; // function(1) + output/register address(2) + value(2)
44 // Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
47 // function(1) + byte count(1) + data
48 return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0);
50 return 7; // function(1) + reference address(2) + AND mask(2) + OR mask(2)
52 // function(1) + byte count(1) + data
53 return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
55 // function(1) + fifo address(2)
56 return 3;
57 default:
58 return MIN_PDU_SIZE; // unknown length
59 }
60}
61
62uint16_t client_pdu_length(const uint8_t *frame, size_t size) {
63 if (size < MIN_PDU_SIZE)
64 return MIN_PDU_SIZE;
65 if (is_function_code_exception(frame[0])) {
66 return 2; // never a valid request; sized like the exception reply so the CRC fails at once
67 }
68 switch (static_cast<FunctionCode>(frame[0])) {
73 // function(1) + start address(2) + quantity(2)
76 return 5; // function(1) + output/register address(2) + value(2)
78 // function(1) + start address(2) + quantity(2) + byte count(1) + packed coil data (8 coils per byte).
79 return 6 + (size > 5 ? std::min(frame[5], uint8_t(packed_bit_bytes(MAX_NUM_OF_COILS_TO_WRITE))) : 0);
81 // function(1) + start address(2) + quantity(2) + byte count(1) + register data (2 bytes per register).
82 return 6 + (size > 5 ? std::min(frame[5], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0);
83 // Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
86 // function(1) + byte count(1) + data
87 return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0);
89 return 7; // function(1) + reference address(2) + AND mask(2) + OR mask(2)
91 // function(1) + read start address(2) + read quantity(2) + write start address(2) +
92 // write quantity(2) + byte count(1) + data
93 return 10 + (size > 9 ? std::min(frame[9], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2)) : 0);
95 // function(1) + fifo address(2)
96 return 3;
97 default:
98 return MIN_PDU_SIZE; // unknown length
99 }
100}
101
102bool is_server_pdu_standard(const uint8_t *pdu, size_t size) {
103 if (server_pdu_length(pdu, size) != size)
104 return false;
105
106 const auto function_code = static_cast<FunctionCode>(pdu[0]);
107 switch (function_code) {
110 // A conformant bit-read response carries at least one packed byte (up to 2000 bits = 250 bytes).
111 return pdu[1] != 0 && pdu[1] <= uint8_t(packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ));
114 // Registers are 2 bytes each: the byte count must be a non-zero even count within the read maximum.
115 return pdu[1] != 0 && pdu[1] % 2 == 0 && pdu[1] <= uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2);
118 return pdu[1] <= uint8_t(MAX_PDU_SIZE - 2);
120 return pdu[1] != 0 && pdu[1] % 2 == 0 && pdu[1] <= uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2);
123 // The response echoes start address and quantity: bound them like the request side does.
124 const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS;
125 const uint16_t start_address = get_data<uint16_t>(pdu, 1);
126 const uint16_t quantity = get_data<uint16_t>(pdu, 3);
127 const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE;
128 return quantity_in_range(start_address, quantity, max_quantity);
129 }
131 // The response echoes the request, so the same ON/OFF constraint applies.
132 return is_canonical_coil_value(pdu[3], pdu[4]);
133 default:
134 return true; // All other function codes validated by length alone
135 }
136}
137
138bool is_client_pdu_standard(const uint8_t *pdu, size_t size) {
139 if (client_pdu_length(pdu, size) != size)
140 return false;
141
142 const auto function_code = static_cast<FunctionCode>(pdu[0]);
143 switch (function_code) {
148 const bool bits =
149 function_code == FunctionCode::READ_COILS || function_code == FunctionCode::READ_DISCRETE_INPUTS;
150 const uint16_t start_address = get_data<uint16_t>(pdu, 1);
151 const uint16_t quantity = get_data<uint16_t>(pdu, 3);
152 const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_READ : MAX_NUM_OF_REGISTERS_TO_READ;
153 return quantity_in_range(start_address, quantity, max_quantity);
154 }
157 const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS;
158 const uint16_t start_address = get_data<uint16_t>(pdu, 1);
159 const uint16_t quantity = get_data<uint16_t>(pdu, 3);
160 const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE;
161 // Coils are packed 8 per data byte; registers are 2 bytes each.
162 const size_t expected_data_bytes = bits ? packed_bit_bytes(quantity) : quantity * 2;
163 return quantity_in_range(start_address, quantity, max_quantity) && pdu[5] == expected_data_bytes;
164 }
167 return pdu[1] <= MAX_PDU_SIZE - 2;
169 const uint16_t start_address_read = get_data<uint16_t>(pdu, 1);
170 const uint16_t quantity_read = get_data<uint16_t>(pdu, 3);
171 const uint16_t start_address_write = get_data<uint16_t>(pdu, 5);
172 const uint16_t quantity_write = get_data<uint16_t>(pdu, 7);
173 return quantity_in_range(start_address_read, quantity_read, MAX_NUM_OF_REGISTERS_TO_READ) &&
174 quantity_in_range(start_address_write, quantity_write, MAX_NUM_OF_REGISTERS_TO_WRITE_RW) &&
175 pdu[9] == quantity_write * 2;
176 }
178 // The one variable field in an otherwise fixed-shape PDU: the spec allows exactly ON/OFF.
179 return is_canonical_coil_value(pdu[3], pdu[4]);
180 default:
181 return true; // All other function codes validated by length alone
182 }
183}
184
185static size_t required_payload_size(SensorValueType sensor_value_type) {
186 switch (sensor_value_type) {
191 return 2;
198 return 4;
203 return 8;
205 default:
206 return 0;
207 }
208}
209
211 ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast<uint16_t>(value_type));
212}
213
214std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type,
215 uint8_t offset, uint32_t bitmask) {
216 int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
217
218 // Validate offset against the buffer for all types, including RAW/unsupported, so
219 // a malformed or misconfigured frame still produces an error log.
220 if (static_cast<size_t>(offset) > size) {
221 ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
222 static_cast<unsigned int>(offset), size);
223 return std::nullopt;
224 }
225
226 const size_t required_size = required_payload_size(sensor_value_type);
227 if (required_size == 0) {
228 return value;
229 }
230
231 if (size - offset < required_size) {
232 ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
233 static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), size, required_size);
234 return std::nullopt;
235 }
236
237 switch (sensor_value_type) {
239 value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
240 break;
242 uint16_t word = byteswap(get_data<uint16_t>(data, offset));
243 value = mask_and_shift_by_rightbit(word, bitmask);
244 break;
245 }
248 value = get_data<uint32_t>(data, offset);
249 value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
250 break;
253 value = get_data<uint32_t>(data, offset);
254 value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
255 value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
256 break;
258 value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), bitmask); // default is 0xFFFF ;
259 break;
261 uint16_t word = byteswap(get_data<uint16_t>(data, offset));
262 value = mask_and_shift_by_rightbit(static_cast<int16_t>(word), bitmask);
263 break;
264 }
266 value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
267 break;
269 value = get_data<uint32_t>(data, offset);
270 // Currently the high word is at the low position
271 // the sign bit is therefore at low before the switch
272 uint32_t sign_bit = (value & 0x8000) << 16;
274 static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
275 } break;
278 // Ignore bitmask for QWORD
279 value = get_data<uint64_t>(data, offset);
280 break;
283 // Ignore bitmask for QWORD
284 uint64_t tmp = get_data<uint64_t>(data, offset);
285 value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
286 } break;
288 default:
289 break;
290 }
291 return value;
292}
293
294std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type) {
295 // RAW and BIT carry no fixed-width number, so there is nothing to decode whatever the span holds.
296 // register_width_for() reports 1 for them, so this must be checked before the width test below.
297 if (sensor_value_type == SensorValueType::RAW || sensor_value_type == SensorValueType::BIT) {
298 return 0;
299 }
300 const uint16_t required_words = register_width_for(sensor_value_type);
301 if (required_words > count) {
302 ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%u",
303 static_cast<unsigned int>(sensor_value_type), count, static_cast<unsigned int>(required_words));
304 return std::nullopt;
305 }
306 // Registers are the wire's own unit, so decode them directly rather than serializing back to bytes.
307 // Each case defers to registers_to_value() so the word order and sign rules have one definition, with
308 // two deliberate exceptions matching what the byte decoder returned: the float types yield their bit
309 // pattern rather than a float, and U_QWORD shares the signed branch because the return type is int64_t.
310 switch (sensor_value_type) {
328 return registers_to_uint32(registers[0], registers[1]);
330 return registers_to_uint32(registers[1], registers[0]);
331 // Signed for both: an unsigned QWORD above INT64_MAX has to come back as a negative int64_t.
338 default:
339 return 0;
340 }
341}
342
343// Append a 16-bit value to a PDU in big-endian (wire) byte order.
344template<size_t CAP> static void append_pdu_word(StaticVector<uint8_t, CAP> &pdu, uint16_t value) {
345 pdu.push_back(value >> 8);
346 pdu.push_back(value >> 0);
347}
348
349// Every request PDU opens with the same 5-byte layout: function code, then two big-endian 16-bit
350// fields (start address + quantity for reads and multi-writes, address + value for single writes).
351template<size_t CAP>
352static void append_pdu_header(StaticVector<uint8_t, CAP> &pdu, FunctionCode function_code, uint16_t first,
353 uint16_t second) {
354 pdu.push_back(static_cast<uint8_t>(function_code));
355 append_pdu_word(pdu, first);
356 append_pdu_word(pdu, second);
357}
358
359// Zero the unused bits of a multi-coil write's final data byte, as the spec requires. Kept in one
360// place so the generic and typed coil builders produce identical wire bytes for the same write.
361// The caller must pass a span whose LAST byte is the final packed-bit byte - both builders pass the
362// whole PDU, which qualifies because the coil data is always the PDU's tail.
363static void mask_trailing_pad_bits(std::span<uint8_t> data, uint16_t bit_count) {
364 if (data.empty() || bit_count % 8 == 0)
365 return;
366 data.back() &= static_cast<uint8_t>((1 << (bit_count % 8)) - 1);
367}
368
369ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities) {
370 ReadPdu pdu; // declared before every return so NRVO fires (all paths return the same object)
371 if (number_of_entities == 0) {
372 ESP_LOGE(TAG, "Number of entities is zero for function code %02X", static_cast<uint8_t>(function_code));
373 return pdu;
374 }
375 if (!address_range_fits(start_address, number_of_entities)) {
376 ESP_LOGE(TAG, "Read of %u entities at %u runs past the 16-bit address space, dropping request", number_of_entities,
377 start_address);
378 return pdu;
379 }
380
381 switch (function_code) {
383 if (number_of_entities > MAX_NUM_OF_COILS_TO_READ) {
384 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum coils to read %u for function code %02X",
385 number_of_entities, MAX_NUM_OF_COILS_TO_READ, static_cast<uint8_t>(function_code));
386 return pdu;
387 }
388 break;
390 if (number_of_entities > MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) {
391 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum discrete inputs to read %u for function code %02X",
392 number_of_entities, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ, static_cast<uint8_t>(function_code));
393 return pdu;
394 }
395 break;
398 if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_READ) {
399 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to read %u for function code %02X",
400 number_of_entities, MAX_NUM_OF_REGISTERS_TO_READ, static_cast<uint8_t>(function_code));
401 return pdu;
402 }
403 break;
404 default:
405 ESP_LOGE(TAG, "Unsupported function code %02X for read PDU creation", static_cast<uint8_t>(function_code));
406 return pdu;
407 }
408
409 append_pdu_header(pdu, function_code, start_address, number_of_entities);
410 return pdu;
411}
412
413PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities,
414 const uint8_t *values, size_t values_len) {
415 PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object)
416 if (is_function_code_read_only(static_cast<uint8_t>(function_code))) {
417 if (values != nullptr || values_len > 0) {
418 ESP_LOGW(TAG, "Values provided for read function code %02X, but will be ignored",
419 static_cast<uint8_t>(function_code));
420 }
421 auto read_pdu = create_read_pdu(function_code, start_address, number_of_entities);
422 pdu.assign(read_pdu.begin(), read_pdu.end());
423 return pdu;
424 }
425 // Exact codes only: is_function_code_write() masks the exception bit, which would let the
426 // exception-flagged forms (0x85/0x86/0x8F/0x90) build a request announcing itself as an exception.
427 const bool is_single =
428 function_code == FunctionCode::WRITE_SINGLE_COIL || function_code == FunctionCode::WRITE_SINGLE_REGISTER;
429 const bool is_multi =
431 if (!is_single && !is_multi) {
432 ESP_LOGE(TAG, "Unsupported function code %02X for client PDU creation", static_cast<uint8_t>(function_code));
433 return pdu;
434 }
435
436 // Generic write builder: raw caller-supplied bytes, so we can only guard against the PDU byte capacity here.
437 if (values == nullptr || values_len == 0) {
438 ESP_LOGE(TAG, "No values provided for write function code %02X", static_cast<uint8_t>(function_code));
439 return pdu;
440 }
441 if (number_of_entities == 0) {
442 ESP_LOGE(TAG, "Number of entities is zero for function code %02X", static_cast<uint8_t>(function_code));
443 return pdu;
444 }
445 // number_of_entities is ignored for single write, so only validate it for the multiple variants.
446 // The bound is per function code (coils pack 8 per byte, so their quantity limit is far higher) -
447 // the same limits is_client_pdu_standard() accepts, so builder and validator agree.
448 const uint16_t max_entities =
449 function_code == FunctionCode::WRITE_MULTIPLE_COILS ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE;
450 if (!is_single && number_of_entities > max_entities) {
451 ESP_LOGE(TAG, "number_of_entities %u exceeds maximum %u for function code %02X", number_of_entities, max_entities,
452 static_cast<uint8_t>(function_code));
453 return pdu;
454 }
455 if (!is_single && !address_range_fits(start_address, number_of_entities)) {
456 ESP_LOGE(TAG, "Write of %u entities at %u runs past the 16-bit address space, dropping request", number_of_entities,
457 start_address);
458 return pdu;
459 }
460
461 if (is_single) {
462 // Write single register or coil: the two value bytes are the header's second field.
463 if (values_len < 2) {
464 ESP_LOGE(TAG, "values_len %zu too small for write-single command (need 2), dropping request", values_len);
465 return pdu;
466 }
467 // The spec allows exactly ON (0xFF00) and OFF (0x0000) for a single-coil write - the same rule
468 // is_client_pdu_standard() enforces, so a built frame cannot be misclassified on reply.
469 if (function_code == FunctionCode::WRITE_SINGLE_COIL && !is_canonical_coil_value(values[0], values[1])) {
470 ESP_LOGE(TAG, "Invalid single-coil value %02X%02X (must be FF00 or 0000), dropping request", values[0],
471 values[1]);
472 return pdu;
473 }
474 append_pdu_header(pdu, function_code, start_address, uint16_t((values[0] << 8) | values[1]));
475 return pdu;
476 }
477 // The quantity is spec-bounded above, so the data length just has to agree with it exactly
478 // (registers are 2 bytes each, coils pack 8 per byte).
479 // Checked before the header append: a failed check must return an empty PDU, not a 5-byte partial one.
480 const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS;
481 const size_t expected_len = bits ? packed_bit_bytes(number_of_entities) : static_cast<size_t>(number_of_entities) * 2;
482 if (values_len != expected_len) {
483 ESP_LOGE(TAG, "values_len %zu does not match %u entities (expected %zu) for function code %02X, dropping request",
484 values_len, number_of_entities, expected_len, static_cast<uint8_t>(function_code));
485 return pdu;
486 }
487 append_pdu_header(pdu, function_code, start_address, number_of_entities);
488 pdu.push_back(values_len); // Byte count is required for write multiple
489 for (size_t i = 0; i < values_len; i++)
490 pdu.push_back(values[i]);
491 if (bits)
492 mask_trailing_pad_bits(pdu, number_of_entities);
493 return pdu;
494}
495
496// Validate one register block for a client builder: a non-zero quantity within max_quantity that does not
497// run past the 16-bit address space (register count × 2 stays within MAX_PDU_SIZE as a result). On failure
498// it logs the reason and returns false, on which the caller returns an empty PDU. `role` names the block in
499// the log ("Read"/"Write"). Logging twin of quantity_in_range(): the same three predicates, split so each
500// failure names its reason, and taking size_t so an oversize span is caught before any narrowing.
501static bool register_block_in_range(const LogString *role, uint16_t start_address, size_t quantity,
502 uint16_t max_quantity) {
503 if (quantity == 0 || quantity > max_quantity) {
504 ESP_LOGE(TAG, "%s count %zu out of range [1, %u], dropping request", LOG_STR_ARG(role), quantity, max_quantity);
505 return false;
506 }
507 if (!address_range_fits(start_address, quantity)) {
508 ESP_LOGE(TAG, "%s of %zu registers at %u runs past the 16-bit address space, dropping request", LOG_STR_ARG(role),
509 quantity, start_address);
510 return false;
511 }
512 return true;
513}
514
515// The ceiling comes from the buffer itself: push_back() drops silently, so a bound wider than the buffer
516// would put a truncated frame on the wire.
517template<typename Pdu> static Pdu build_write_registers_pdu(uint16_t start_address, std::span<const uint16_t> values) {
518 constexpr auto max_registers = static_cast<uint16_t>((Pdu::capacity() - WRITE_MULTIPLE_HEADER_SIZE) / 2);
519 Pdu pdu; // declared before every return so NRVO fires (all paths return the same object)
520 if (!register_block_in_range(LOG_STR("Write"), start_address, values.size(), max_registers)) {
521 return pdu;
522 }
523 append_pdu_header(pdu, FunctionCode::WRITE_MULTIPLE_REGISTERS, start_address, values.size());
524 pdu.push_back(static_cast<uint8_t>(values.size() * 2)); // byte count
525 for (auto v : values) {
526 append_pdu_word(pdu, v);
527 }
528 return pdu;
529}
530
531static_assert((PduBuffer::capacity() - WRITE_MULTIPLE_HEADER_SIZE) / 2 == MAX_NUM_OF_REGISTERS_TO_WRITE,
532 "a full-frame PDU must hold exactly MAX_NUM_OF_REGISTERS_TO_WRITE registers");
533static_assert((WriteFewRegistersPdu::capacity() - WRITE_MULTIPLE_HEADER_SIZE) / 2 == MAX_FEW_REGISTERS,
534 "the small write buffer must hold exactly MAX_FEW_REGISTERS registers");
535
536PduBuffer create_write_registers_pdu(uint16_t start_address, std::span<const uint16_t> values) {
537 return build_write_registers_pdu<PduBuffer>(start_address, values);
538}
539
540WriteFewRegistersPdu create_write_few_registers_pdu(uint16_t start_address, std::span<const uint16_t> values) {
541 return build_write_registers_pdu<WriteFewRegistersPdu>(start_address, values);
542}
543
544PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count,
545 uint16_t write_start_address,
546 std::span<const uint16_t> write_values) {
547 PduBuffer pdu;
548 if (!register_block_in_range(LOG_STR("Read"), read_start_address, read_count, MAX_NUM_OF_REGISTERS_TO_READ)) {
549 return pdu;
550 }
551 if (!register_block_in_range(LOG_STR("Write"), write_start_address, write_values.size(),
552 MAX_NUM_OF_REGISTERS_TO_WRITE_RW)) {
553 return pdu;
554 }
555 // fc + read start(2) + read qty(2) + write start(2) + write qty(2) + write byte count(1) + write values.
556 const auto write_count = static_cast<uint16_t>(write_values.size());
558 append_pdu_word(pdu, read_start_address);
559 append_pdu_word(pdu, read_count);
560 append_pdu_word(pdu, write_start_address);
561 append_pdu_word(pdu, write_count);
562 pdu.push_back(static_cast<uint8_t>(write_count * 2)); // byte count
563 for (auto v : write_values) {
564 append_pdu_word(pdu, v);
565 }
566 return pdu;
567}
568
569WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value) {
570 WriteSinglePdu pdu;
571 append_pdu_header(pdu, FunctionCode::WRITE_SINGLE_REGISTER, start_address, value);
572 return pdu;
573}
574
576 WriteSinglePdu pdu;
577 append_pdu_header(pdu, FunctionCode::WRITE_SINGLE_COIL, address, value ? 0xFF00 : 0x0000);
578 return pdu;
579}
580
581// Shared core for the two coil-write overloads: validates, then builds into the caller's named
582// pdu (left empty on failure). Each overload's returns all name one local, so NRVO fires.
583static void build_write_coils_pdu(PduBuffer &pdu, uint16_t start_address, PackedBits bits) {
584 const uint16_t count = bits.size();
585 const std::span<const uint8_t> packed_bits = bits.bytes();
586 if (count == 0) {
587 ESP_LOGE(TAG, "No coils requested for write multiple coils, dropping request");
588 return;
589 }
590 if (count > MAX_NUM_OF_COILS_TO_WRITE) {
591 ESP_LOGE(TAG, "count %u exceeds maximum coils to write %u, dropping request", count, MAX_NUM_OF_COILS_TO_WRITE);
592 return;
593 }
594 if (!address_range_fits(start_address, count)) {
595 ESP_LOGE(TAG, "Write of %u coils at %u runs past the 16-bit address space, dropping request", count, start_address);
596 return;
597 }
598 const size_t byte_count = packed_bit_bytes(count);
599 if (packed_bits.size() < byte_count) {
600 ESP_LOGE(TAG, "packed_bits (%zu bytes) does not cover %u coils (%zu bytes), dropping request", packed_bits.size(),
601 count, byte_count);
602 return;
603 }
604 append_pdu_header(pdu, FunctionCode::WRITE_MULTIPLE_COILS, start_address, count);
605 pdu.push_back(static_cast<uint8_t>(byte_count));
606 for (size_t i = 0; i != byte_count; i++) {
607 pdu.push_back(packed_bits[i]);
608 }
609 mask_trailing_pad_bits(pdu, count);
610}
611
612PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits) {
613 PduBuffer pdu;
614 build_write_coils_pdu(pdu, start_address, bits);
615 return pdu;
616}
617
618// Shared by the two bool-container overloads: both index the same way, so the packing is written once.
619template<typename BoolContainer>
620static PduBuffer create_write_coils_pdu_from_bools(uint16_t start_address, const BoolContainer &values) {
621 PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object)
622 const size_t count = values.size();
623 // Bound before packing so the transient buffer below cannot overflow; the shared core validates the rest.
624 if (count > MAX_NUM_OF_COILS_TO_WRITE) {
625 ESP_LOGE(TAG, "values.size() %zu exceeds maximum coils to write %u, dropping request", count,
626 MAX_NUM_OF_COILS_TO_WRITE);
627 return pdu;
628 }
629 CoilPackBuffer packed;
630 pack_bits(packed, values);
631 build_write_coils_pdu(pdu, start_address, PackedBits(std::span<const uint8_t>(packed.data(), packed.size()), count));
632 return pdu;
633}
634
635PduBuffer create_write_coils_pdu(uint16_t start_address, std::span<const bool> values) {
636 return create_write_coils_pdu_from_bools(start_address, values);
637}
638
639PduBuffer create_write_coils_pdu(uint16_t start_address, const std::vector<bool> &values) {
640 return create_write_coils_pdu_from_bools(start_address, values);
641}
642} // namespace esphome::modbus::helpers
uint8_t address
Definition bl0906.h:4
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:227
size_t size() const
Definition helpers.h:292
void push_back(const T &value)
Definition helpers.h:265
static constexpr size_t capacity()
Definition helpers.h:293
void assign(InputIt first, InputIt last)
Definition helpers.h:275
Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first),...
std::span< const uint8_t > bytes() const
The underlying packed bytes: exactly ceil(size() / 8) bytes, even when the view was constructed over ...
uint16_t size() const
Number of bits in the view.
uint8_t second
bool address_range_fits(uint16_t start_address, size_t count)
PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits)
Create modbus write multiple coils command (function 0x0F) from bits packed as on the wire.
StaticVector< uint8_t, MAX_PDU_SIZE > PduBuffer
WriteFewRegistersPdu create_write_few_registers_pdu(uint16_t start_address, std::span< const uint16_t > values)
Create modbus write multiple registers command (function 0x10) on a right-sized stack buffer.
bool is_function_code_read_only(uint8_t function_code)
WriteSinglePdu create_write_single_coil_pdu(uint16_t address, bool value)
Create modbus write single coil command Function 0x05 Write Single Coil.
std::span< const uint8_t > data
void pack_bits(Out &out, const Bits &bits)
Append packed bytes (LSB first) for the given bits onto a growable byte container.
constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word)
Combine two register words into a 32-bit value.
T get_data(const uint8_t *data, size_t buffer_offset)
Extract data from modbus response buffer.
constexpr auto registers_to_value(const uint16_t *registers)
Decode one value whose type is known at compile time, from registers in host byte order.
ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities)
Create a modbus read request PDU.
constexpr uint16_t register_width_for(SensorValueType v)
Number of 16-bit registers a value of this type occupies (RAW counts as one register).
WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value)
Create modbus write single register command Function 0x06 Write Single Register.
PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len)
Create a modbus client pdu for reading/writing single/multiple coils/register/inputs.
uint16_t server_pdu_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...
bool is_server_pdu_standard(const uint8_t *pdu, size_t size)
void log_unsupported_value_type(SensorValueType value_type)
bool is_client_pdu_standard(const uint8_t *pdu, size_t size)
PduBuffer create_write_registers_pdu(uint16_t start_address, std::span< const uint16_t > values)
Create modbus write multiple registers command Function 0x10 Write Multiple Registers.
StaticVector< uint8_t, packed_bit_bytes(MAX_NUM_OF_COILS_TO_WRITE)> CoilPackBuffer
Scratch space for packing coils into wire layout: one bit per coil, sized for the spec maximum.
PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address, std::span< const uint16_t > write_values)
Create modbus read/write multiple registers command Function 0x17 Read/Write Multiple Registers Write...
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).
uint16_t client_pdu_length(const uint8_t *frame, size_t size)
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.
constexpr size_t packed_bit_bytes(size_t bits)
Bits pack 8 per data byte, rounded up to whole bytes.
static void uint32_t
void byteswap()