ESPHome 2026.8.2
Loading...
Searching...
No Matches
modbus_helpers.h
Go to the documentation of this file.
1#pragma once
2
3#include <cmath>
4#include <optional>
5#include <span>
6#include <string>
7#include <vector>
8
11
13
14// Pure read codes (0x01-0x04): they only read, so they are idempotent and safe to retry.
15inline bool is_function_code_read_only(uint8_t function_code) {
16 FunctionCode masked_function_code = static_cast<FunctionCode>(function_code & FUNCTION_CODE_MASK);
17 return masked_function_code == FunctionCode::READ_COILS ||
18 masked_function_code == FunctionCode::READ_DISCRETE_INPUTS ||
19 masked_function_code == FunctionCode::READ_HOLDING_REGISTERS ||
20 masked_function_code == FunctionCode::READ_INPUT_REGISTERS;
21}
22
23// Codes whose response carries read-back data: the pure reads plus 0x17, which reads and writes at once.
24inline bool is_function_code_read(uint8_t function_code) {
25 return is_function_code_read_only(function_code) ||
27}
28
29// Codes that mutate registers or coils: the pure writes, 0x16 mask-write, and 0x17 read/write multiple.
30inline bool is_function_code_write(uint8_t function_code) {
31 FunctionCode masked_function_code = static_cast<FunctionCode>(function_code & FUNCTION_CODE_MASK);
32 return masked_function_code == FunctionCode::WRITE_SINGLE_COIL ||
33 masked_function_code == FunctionCode::WRITE_SINGLE_REGISTER ||
34 masked_function_code == FunctionCode::WRITE_MULTIPLE_COILS ||
35 masked_function_code == FunctionCode::WRITE_MULTIPLE_REGISTERS ||
36 masked_function_code == FunctionCode::MASK_WRITE_REGISTER ||
37 masked_function_code == FunctionCode::READ_WRITE_MULTIPLE_REGISTERS;
38}
39
40// True if [start_address, start_address + count) fits within the 16-bit Modbus address space. The 32-bit
41// promotion is the overflow guard - a 16-bit sum could wrap and pass.
42inline bool address_range_fits(uint16_t start_address, size_t count) {
43 return uint32_t(start_address) + count <= 0x10000u;
44}
45
46inline bool is_function_code_exception(uint8_t function_code) {
47 return (static_cast<uint8_t>(function_code) & FUNCTION_CODE_EXCEPTION_MASK) != 0;
48}
49
50inline bool is_function_code_custom(uint8_t function_code) {
51 uint8_t masked_function_code = function_code & FUNCTION_CODE_MASK;
52 return (masked_function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT &&
53 masked_function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END) ||
54 (masked_function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT &&
55 masked_function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END);
56}
57
89
90// Returns the expected length of a server response PDU based on the function code.
91// If too few bytes have arrived to determine the length, returns the minimum length. `size` is the
92// number of bytes available so far, which may exceed the eventual PDU (e.g. include the frame's CRC
93// bytes): only fixed header positions are interpreted, so surplus bytes are never misread.
94uint16_t server_pdu_length(const uint8_t *frame, size_t size);
95// Frame counterpart: address(1) + PDU + CRC(2). Passes every received byte after the address through,
96// so header fields (e.g. a byte count) are interpreted as soon as they arrive.
97inline uint16_t server_frame_length(const uint8_t *frame, size_t size) {
98 if (size < 2)
99 return MIN_FRAME_SIZE; // function code not received yet
100 return server_pdu_length(frame + 1, size - 1) + 3;
101}
102
103// Returns the expected length of a client request PDU based on the function code.
104// Same contract as server_pdu_length(): `size` is bytes available so far, may exceed the PDU.
105uint16_t client_pdu_length(const uint8_t *frame, size_t size);
106inline uint16_t client_frame_length(const uint8_t *frame, size_t size) {
107 if (size < 2)
108 return MIN_FRAME_SIZE; // function code not received yet
109 return client_pdu_length(frame + 1, size - 1) + 3;
110}
111
112// Returns true if pdu is a complete transaction whose shape is consistent with its function code.
113// Unlike *_pdu_length(), `size` here is the exact PDU length: a size mismatch is non-conformant.
114// Function codes with nothing variable to cross-check are validated by their fixed length alone: the
115// single writes (except 0x05's value field, which must be 0x0000 or 0xFF00), mask-write and FIFO, and
116// - deliberately - custom/unknown codes and exception responses, so a dispatcher can still route
117// them by function code rather than reject them outright. Tests pin this contract.
118bool is_server_pdu_standard(const uint8_t *pdu, size_t size);
119
120// Client counterpart: additionally checks quantity bounds and address-range arithmetic per function code.
121// The same acceptance rule applies to custom/unknown function codes.
122bool is_client_pdu_standard(const uint8_t *pdu, size_t size);
123
124// Remove before 2027.2.0
125ESPDEPRECATED("Use server_pdu_payload() on the response PDU instead. Removed in 2027.2.0", "2026.8.0")
126inline uint8_t server_frame_data_offset(const uint8_t *frame, size_t size) {
127 if (size < 2)
128 return 0;
129 switch (static_cast<FunctionCode>(frame[1])) {
134 return 3; // address(1) + function(1) + byte count(1) + data + CRC(2)
135 default:
136 return 2;
137 }
138}
139
147inline std::span<const uint8_t> server_pdu_payload(std::span<const uint8_t> pdu) {
148 if (pdu.empty())
149 return {};
150 const size_t offset = (!is_function_code_exception(pdu[0]) && is_function_code_read(pdu[0])) ? 2 : 1;
151 return pdu.size() > offset ? pdu.subspan(offset) : std::span<const uint8_t>();
152}
153
154inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; }
155
156enum class SensorValueType : uint8_t {
157 RAW = 0x00, // variable length
158 U_WORD = 0x1, // 1 Register unsigned
159 U_DWORD = 0x2, // 2 Registers unsigned
160 S_WORD = 0x3, // 1 Register signed
161 S_DWORD = 0x4, // 2 Registers signed
162 BIT = 0x5,
163 U_DWORD_R = 0x6, // 2 Registers unsigned
164 S_DWORD_R = 0x7, // 2 Registers unsigned
165 U_QWORD = 0x8,
166 S_QWORD = 0x9,
167 U_QWORD_R = 0xA,
168 S_QWORD_R = 0xB,
169 FP32 = 0xC,
170 FP32_R = 0xD,
171 U_WORD_S = 0xE, // 1 Register unsigned, bytes swapped
172 S_WORD_S = 0xF, // 1 Register signed, bytes swapped
173};
174
178
183
198
199inline FunctionCode modbus_register_write_function(EntityType reg_type, bool multiple = false) {
200 switch (reg_type) {
201 case EntityType::COIL:
205 // These register types can't be written (per spec)
208 default:
210 }
211}
212
213inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
214
223inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
224 if (value.length() < pos * 2 + 2)
225 return 0;
226 return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
227}
228
235inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
236 return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
237}
238
245inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
246 return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
247}
248
255inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
256 return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
257}
258
259// Extract data from modbus response buffer
266template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
267 if (sizeof(T) == sizeof(uint8_t)) {
268 return T(data[buffer_offset]);
269 }
270 if (sizeof(T) == sizeof(uint16_t)) {
271 return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
272 }
273 if (sizeof(T) == sizeof(uint32_t)) {
274 return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
275 static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
276 }
277 if (sizeof(T) == sizeof(uint64_t)) {
278 return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
279 (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
280 }
281 static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
282 sizeof(T) == sizeof(uint64_t),
283 "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
284 return T{};
285}
286
287template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
288 return get_data<T>(data.data(), buffer_offset);
289}
290
299inline bool bit_from_packed(int bit, std::span<const uint8_t> data) {
300 auto data_byte = bit / 8;
301 return (data[data_byte] & (1 << (bit % 8))) > 0;
302}
303
304// Remove before 2027.2.0
305ESPDEPRECATED("Use bit_from_packed() instead. Removed in 2027.2.0", "2026.8.0")
306inline bool coil_from_vector(int coil, std::span<const uint8_t> data) { return bit_from_packed(coil, data); }
307
315template<typename Out, typename Bits> void pack_bits(Out &out, const Bits &bits) {
316 uint8_t byte = 0;
317 uint8_t bit = 0;
318 for (bool b : bits) {
319 if (b)
320 byte |= (1 << bit);
321 if (++bit == 8) {
322 out.push_back(byte);
323 byte = 0;
324 bit = 0;
325 }
326 }
327 if (bit != 0) // flush the final partial byte
328 out.push_back(byte);
329}
330
341template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
342 auto result = (mask & data);
343 if (result == 0 || mask == 0xFFFFFFFF) {
344 return result;
345 }
346 for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
347 if (pos < 32 && (mask & (1UL << pos)) != 0)
348 return result >> pos;
349 }
350 return 0;
351}
352
353// Logs an error for an unsupported value type. Defined in the .cpp so logging stays out of headers.
355
359template<typename Container> void number_to_payload(Container &data, int64_t value, SensorValueType value_type) {
360 switch (value_type) {
363 data.push_back(value & 0xFFFF);
364 break;
367 data.push_back(byteswap(static_cast<uint16_t>(value & 0xFFFF)));
368 break;
372 data.push_back((value & 0xFFFF0000) >> 16);
373 data.push_back(value & 0xFFFF);
374 break;
378 data.push_back(value & 0xFFFF);
379 data.push_back((value & 0xFFFF0000) >> 16);
380 break;
383 data.push_back((value & 0xFFFF000000000000) >> 48);
384 data.push_back((value & 0xFFFF00000000) >> 32);
385 data.push_back((value & 0xFFFF0000) >> 16);
386 data.push_back(value & 0xFFFF);
387 break;
390 data.push_back(value & 0xFFFF);
391 data.push_back((value & 0xFFFF0000) >> 16);
392 data.push_back((value & 0xFFFF00000000) >> 32);
393 data.push_back((value & 0xFFFF000000000000) >> 48);
394 break;
395 default:
396 log_unsupported_value_type(value_type);
397 break;
398 }
399}
400
409std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type,
410 uint8_t offset, uint32_t bitmask);
411
413inline std::optional<int64_t> payload_to_number(std::span<const uint8_t> data, SensorValueType sensor_value_type,
414 uint8_t offset, uint32_t bitmask) {
415 return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask);
416}
417
418// Remove before 2027.2.0
419ESPDEPRECATED("Use the std::span overload returning std::optional<int64_t> instead. Removed in 2027.2.0", "2026.8.0")
420inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
421 uint32_t bitmask) {
422 // Released behavior: a too-short payload logs an error and decodes to 0.
423 return payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask).value_or(0);
424}
425
433std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type);
434
435// Named PDU buffer types: the builders' storage strategy (currently stack-allocated StaticVector,
436// right-sized per shape) can be swapped in one place without touching every signature.
441using CoilPackBuffer = StaticVector<uint8_t, packed_bit_bytes(MAX_NUM_OF_COILS_TO_WRITE)>;
442
449ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities);
450
470PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities,
471 const uint8_t *values = nullptr, size_t values_len = 0);
472
481PduBuffer create_write_registers_pdu(uint16_t start_address, std::span<const uint16_t> values);
482
494PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count,
495 uint16_t write_start_address,
496 std::span<const uint16_t> write_values);
497
504WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value);
505
513
521PduBuffer create_write_coils_pdu(uint16_t start_address, std::span<const bool> values);
522
532PduBuffer create_write_coils_pdu(uint16_t start_address, const std::vector<bool> &values);
533
540PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits);
541
547template<typename Container> void float_to_payload(Container &data, float value, SensorValueType value_type) {
548 int64_t val;
549
550 if (value_type_is_float(value_type)) {
551 val = bit_cast<uint32_t>(value);
552 } else {
553 val = llroundf(value);
554 }
555
556 number_to_payload(data, val, value_type);
557}
558
559// Remove before 2027.2.0
560ESPDEPRECATED("Use the container overload of float_to_payload() instead. Removed in 2027.2.0", "2026.8.0")
561inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
562 std::vector<uint16_t> data;
563 float_to_payload(data, value, value_type);
564 return data;
565}
566
567} // 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
Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first),...
uint16_t type
mopeka_std_values val[3]
bool value_type_is_float(SensorValueType v)
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.
bool is_function_code_read_only(uint8_t function_code)
uint8_t client_frame_data_offset(const uint8_t *, size_t)
void number_to_payload(Container &data, int64_t value, SensorValueType value_type)
Append the Modbus register words for value to data.
void float_to_payload(Container &data, float value, SensorValueType value_type)
Append a float converted to register words to any push_back container (heap-free with StaticVector).
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.
T get_data(const uint8_t *data, size_t buffer_offset)
Extract data from modbus response buffer.
ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities)
Create a modbus read request PDU.
bool is_function_code_read(uint8_t function_code)
bool is_function_code_write(uint8_t function_code)
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)
FunctionCode modbus_register_write_function(EntityType reg_type, bool multiple=false)
std::span< const uint8_t > server_pdu_payload(std::span< const uint8_t > pdu)
Returns the payload portion of a server response PDU: the bytes after the function code,...
uint64_t qword_from_hex_str(const std::string &value, uint8_t pos)
Get a qword from a hex string.
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...
bool is_server_pdu_standard(const uint8_t *pdu, size_t size)
uint32_t dword_from_hex_str(const std::string &value, uint8_t pos)
Get a dword from a hex string.
FunctionCode modbus_register_read_function(EntityType reg_type)
uint8_t byte_from_hex_str(const std::string &value, uint8_t pos)
Get a byte from a hex string byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 byte_from_h...
uint16_t server_frame_length(const uint8_t *frame, size_t size)
bool is_function_code_unknown_length(uint8_t function_code)
True for any function code whose frame length the parsers cannot predict - everything the server_pdu_...
void log_unsupported_value_type(SensorValueType value_type)
bool is_function_code_custom(uint8_t function_code)
bool is_client_pdu_standard(const uint8_t *pdu, size_t size)
uint16_t word_from_hex_str(const std::string &value, uint8_t pos)
Get a word from a hex string.
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.
bool bit_from_packed(int bit, std::span< const uint8_t > data)
Extract coil data from modbus response buffer Responses for coil are packed into bytes .
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...
bool is_entity_type_binary(EntityType type)
Coils and discrete inputs are the bit-addressed entity tables; the other types are 16-bit registers.
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.
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 size_t packed_bit_bytes(size_t bits)
Bits pack 8 per data byte, rounded up to whole bytes.
size_t size_t pos
Definition helpers.h:1062
To bit_cast(const From &src)
Convert data between types, without aliasing issues or undefined behaviour.
Definition helpers.h:84
STL namespace.
static void uint32_t
void byteswap()