ESPHome 2026.9.0-dev
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
86
91inline bool is_function_code_broadcastable(uint8_t function_code) {
92 uint8_t masked_function_code = function_code & FUNCTION_CODE_MASK;
93 if (is_function_code_read(masked_function_code))
94 return false;
95 return is_function_code_write(masked_function_code) || is_function_code_unknown_length(masked_function_code);
96}
97
98// Returns the expected length of a server response PDU based on the function code.
99// If too few bytes have arrived to determine the length, returns the minimum length. `size` is the
100// number of bytes available so far, which may exceed the eventual PDU (e.g. include the frame's CRC
101// bytes): only fixed header positions are interpreted, so surplus bytes are never misread.
102uint16_t server_pdu_length(const uint8_t *frame, size_t size);
103// Frame counterpart: address(1) + PDU + CRC(2). Passes every received byte after the address through,
104// so header fields (e.g. a byte count) are interpreted as soon as they arrive.
105inline uint16_t server_frame_length(const uint8_t *frame, size_t size) {
106 if (size < 2)
107 return MIN_FRAME_SIZE; // function code not received yet
108 return server_pdu_length(frame + 1, size - 1) + 3;
109}
110
111// Returns the expected length of a client request PDU based on the function code.
112// Same contract as server_pdu_length(): `size` is bytes available so far, may exceed the PDU.
113uint16_t client_pdu_length(const uint8_t *frame, size_t size);
114inline uint16_t client_frame_length(const uint8_t *frame, size_t size) {
115 if (size < 2)
116 return MIN_FRAME_SIZE; // function code not received yet
117 return client_pdu_length(frame + 1, size - 1) + 3;
118}
119
120// Returns true if pdu is a complete transaction whose shape is consistent with its function code.
121// Unlike *_pdu_length(), `size` here is the exact PDU length: a size mismatch is non-conformant.
122// Function codes with nothing variable to cross-check are validated by their fixed length alone: the
123// single writes (except 0x05's value field, which must be 0x0000 or 0xFF00), mask-write and FIFO, and
124// - deliberately - custom/unknown codes and exception responses, so a dispatcher can still route
125// them by function code rather than reject them outright. Tests pin this contract.
126bool is_server_pdu_standard(const uint8_t *pdu, size_t size);
127
128// Client counterpart: additionally checks quantity bounds and address-range arithmetic per function code.
129// The same acceptance rule applies to custom/unknown function codes.
130bool is_client_pdu_standard(const uint8_t *pdu, size_t size);
131
132// Remove before 2027.2.0
133ESPDEPRECATED("Use server_pdu_payload() on the response PDU instead. Removed in 2027.2.0", "2026.8.0")
134inline uint8_t server_frame_data_offset(const uint8_t *frame, size_t size) {
135 if (size < 2)
136 return 0;
137 switch (static_cast<FunctionCode>(frame[1])) {
142 return 3; // address(1) + function(1) + byte count(1) + data + CRC(2)
143 default:
144 return 2;
145 }
146}
147
155inline std::span<const uint8_t> server_pdu_payload(std::span<const uint8_t> pdu) {
156 if (pdu.empty())
157 return {};
158 const size_t offset = (!is_function_code_exception(pdu[0]) && is_function_code_read(pdu[0])) ? 2 : 1;
159 return pdu.size() > offset ? pdu.subspan(offset) : std::span<const uint8_t>();
160}
161
162inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; }
163
170template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
171 if (sizeof(T) == sizeof(uint8_t)) {
172 return T(data[buffer_offset]);
173 }
174 if (sizeof(T) == sizeof(uint16_t)) {
175 return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
176 }
177 if (sizeof(T) == sizeof(uint32_t)) {
178 return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
179 static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
180 }
181 if (sizeof(T) == sizeof(uint64_t)) {
182 return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
183 (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
184 }
185 static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
186 sizeof(T) == sizeof(uint64_t),
187 "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
188 return T{};
189}
190
192inline uint8_t pdu_function_code(std::span<const uint8_t> pdu) {
193 return pdu.empty() ? 0 : (pdu[0] & FUNCTION_CODE_MASK);
194}
195
198inline std::optional<uint16_t> client_pdu_start_address(std::span<const uint8_t> pdu) {
199 if (pdu.size() < 3 || is_function_code_unknown_length(pdu[0]))
200 return std::nullopt;
201 const auto fc = static_cast<FunctionCode>(pdu[0]);
202 // The file-record PDUs are known-length but carry a byte count, not a start address.
204 return std::nullopt;
205 return get_data<uint16_t>(pdu.data(), 1);
206}
207
208enum class SensorValueType : uint8_t {
209 RAW = 0x00, // variable length
210 U_WORD = 0x1, // 1 Register unsigned
211 U_DWORD = 0x2, // 2 Registers unsigned
212 S_WORD = 0x3, // 1 Register signed
213 S_DWORD = 0x4, // 2 Registers signed
214 BIT = 0x5,
215 U_DWORD_R = 0x6, // 2 Registers unsigned
216 S_DWORD_R = 0x7, // 2 Registers signed
217 U_QWORD = 0x8,
218 S_QWORD = 0x9,
219 U_QWORD_R = 0xA,
220 S_QWORD_R = 0xB,
221 FP32 = 0xC,
222 FP32_R = 0xD,
223 U_WORD_S = 0xE, // 1 Register unsigned, bytes swapped
224 S_WORD_S = 0xF, // 1 Register signed, bytes swapped
225};
226
230
232constexpr uint16_t register_width_for(SensorValueType v) {
233 switch (v) {
240 return 2;
245 return 4;
246 default:
247 return 1;
248 }
249}
250
255
270
271inline FunctionCode modbus_register_write_function(EntityType reg_type, bool multiple = false) {
272 switch (reg_type) {
273 case EntityType::COIL:
277 // These register types can't be written (per spec)
280 default:
282 }
283}
284
285inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
286
295inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
296 if (value.length() < pos * 2 + 2)
297 return 0;
298 return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
299}
300
306inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
307 return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
308}
309
315inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
316 return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
317}
318
324inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
325 return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
326}
327
328template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
329 return get_data<T>(data.data(), buffer_offset);
330}
331
340inline bool bit_from_packed(int bit, std::span<const uint8_t> data) {
341 auto data_byte = bit / 8;
342 return (data[data_byte] & (1 << (bit % 8))) > 0;
343}
344
345// Remove before 2027.2.0
346ESPDEPRECATED("Use bit_from_packed() instead. Removed in 2027.2.0", "2026.8.0")
347inline bool coil_from_vector(int coil, std::span<const uint8_t> data) { return bit_from_packed(coil, data); }
348
356template<typename Out, typename Bits> void pack_bits(Out &out, const Bits &bits) {
357 uint8_t byte = 0;
358 uint8_t bit = 0;
359 for (bool b : bits) {
360 if (b)
361 byte |= (1 << bit);
362 if (++bit == 8) {
363 out.push_back(byte);
364 byte = 0;
365 bit = 0;
366 }
367 }
368 if (bit != 0) // flush the final partial byte
369 out.push_back(byte);
370}
371
382template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
383 auto result = (mask & data);
384 if (result == 0 || mask == 0xFFFFFFFF) {
385 return result;
386 }
387 for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
388 if (pos < 32 && (mask & (1UL << pos)) != 0)
389 return result >> pos;
390 }
391 return 0;
392}
393
394// Logs an error for an unsupported value type. Defined in the .cpp so logging stays out of headers.
396
400template<typename Container> void number_to_payload(Container &data, int64_t value, SensorValueType value_type) {
401 switch (value_type) {
404 data.push_back(value & 0xFFFF);
405 break;
408 data.push_back(byteswap(static_cast<uint16_t>(value & 0xFFFF)));
409 break;
413 data.push_back((value & 0xFFFF0000) >> 16);
414 data.push_back(value & 0xFFFF);
415 break;
419 data.push_back(value & 0xFFFF);
420 data.push_back((value & 0xFFFF0000) >> 16);
421 break;
424 data.push_back((value & 0xFFFF000000000000) >> 48);
425 data.push_back((value & 0xFFFF00000000) >> 32);
426 data.push_back((value & 0xFFFF0000) >> 16);
427 data.push_back(value & 0xFFFF);
428 break;
431 data.push_back(value & 0xFFFF);
432 data.push_back((value & 0xFFFF0000) >> 16);
433 data.push_back((value & 0xFFFF00000000) >> 32);
434 data.push_back((value & 0xFFFF000000000000) >> 48);
435 break;
436 default:
437 log_unsupported_value_type(value_type);
438 break;
439 }
440}
441
450std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type,
451 uint8_t offset, uint32_t bitmask);
452
454inline std::optional<int64_t> payload_to_number(std::span<const uint8_t> data, SensorValueType sensor_value_type,
455 uint8_t offset, uint32_t bitmask) {
456 return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask);
457}
458
459// Remove before 2027.2.0
460ESPDEPRECATED("Use the std::span overload returning std::optional<int64_t> instead. Removed in 2027.2.0", "2026.8.0")
461inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
462 uint32_t bitmask) {
463 // Released behavior: a too-short payload logs an error and decodes to 0.
464 return payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask).value_or(0);
465}
466
474std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type);
475
477constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word) {
478 return (static_cast<uint32_t>(high_word) << 16) | low_word;
479}
480
482constexpr uint64_t registers_to_uint64(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
483 return (static_cast<uint64_t>(registers_to_uint32(word0, word1)) << 32) | registers_to_uint32(word2, word3);
484}
485
486// Always false, whatever the type: it exists only to make the static_assert below depend on the
487// template argument. Not a queryable trait.
488template<SensorValueType> inline constexpr bool VALUE_TYPE_SUPPORTED = false;
489
500template<SensorValueType VALUE_TYPE> constexpr auto registers_to_value(const uint16_t *registers) {
501 if constexpr (VALUE_TYPE == SensorValueType::U_WORD) {
502 return registers[0];
503 } else if constexpr (VALUE_TYPE == SensorValueType::S_WORD) {
504 return static_cast<int16_t>(registers[0]);
505 } else if constexpr (VALUE_TYPE == SensorValueType::U_WORD_S) {
506 return byteswap(registers[0]);
507 } else if constexpr (VALUE_TYPE == SensorValueType::S_WORD_S) {
508 return static_cast<int16_t>(byteswap(registers[0]));
509 } else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD) {
510 return registers_to_uint32(registers[0], registers[1]);
511 } else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD_R) {
512 return registers_to_uint32(registers[1], registers[0]);
513 } else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD) {
514 return static_cast<int32_t>(registers_to_uint32(registers[0], registers[1]));
515 } else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD_R) {
516 return static_cast<int32_t>(registers_to_uint32(registers[1], registers[0]));
517 } else if constexpr (VALUE_TYPE == SensorValueType::FP32) {
518 return bit_cast<float>(registers_to_uint32(registers[0], registers[1]));
519 } else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) {
520 return bit_cast<float>(registers_to_uint32(registers[1], registers[0]));
521 } else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD) {
522 return registers_to_uint64(registers[0], registers[1], registers[2], registers[3]);
523 } else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD_R) {
524 return registers_to_uint64(registers[3], registers[2], registers[1], registers[0]);
525 } else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD) {
526 return static_cast<int64_t>(registers_to_uint64(registers[0], registers[1], registers[2], registers[3]));
527 } else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD_R) {
528 return static_cast<int64_t>(registers_to_uint64(registers[3], registers[2], registers[1], registers[0]));
529 } else {
530 static_assert(VALUE_TYPE_SUPPORTED<VALUE_TYPE>, "registers_to_value() does not support this value type");
531 }
532}
533
536template<SensorValueType VALUE_TYPE>
537using RegisterValueType = decltype(registers_to_value<VALUE_TYPE>(static_cast<const uint16_t *>(nullptr)));
538
546template<SensorValueType VALUE_TYPE>
547constexpr std::optional<RegisterValueType<VALUE_TYPE>> value_at(std::span<const uint16_t> registers,
548 uint16_t start_address, uint16_t address) {
549 if (address < start_address)
550 return std::nullopt;
551 const size_t offset = static_cast<size_t>(address) - start_address;
552 if (offset + register_width_for(VALUE_TYPE) > registers.size())
553 return std::nullopt;
554 return registers_to_value<VALUE_TYPE>(registers.data() + offset);
555}
556
558static constexpr uint16_t MAX_FEW_REGISTERS = 4;
559
560// Named PDU buffer types: the builders' storage strategy (currently stack-allocated StaticVector,
561// right-sized per shape) can be swapped in one place without touching every signature.
567using CoilPackBuffer = StaticVector<uint8_t, packed_bit_bytes(MAX_NUM_OF_COILS_TO_WRITE)>;
568
575ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities);
576
596PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities,
597 const uint8_t *values = nullptr, size_t values_len = 0);
598
607PduBuffer create_write_registers_pdu(uint16_t start_address, std::span<const uint16_t> values);
608
616WriteFewRegistersPdu create_write_few_registers_pdu(uint16_t start_address, std::span<const uint16_t> values);
617
629PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count,
630 uint16_t write_start_address,
631 std::span<const uint16_t> write_values);
632
639WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value);
640
648
656PduBuffer create_write_coils_pdu(uint16_t start_address, std::span<const bool> values);
657
667PduBuffer create_write_coils_pdu(uint16_t start_address, const std::vector<bool> &values);
668
675PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits);
676
682template<typename Container> void float_to_payload(Container &data, float value, SensorValueType value_type) {
683 int64_t val;
684
685 if (value_type_is_float(value_type)) {
686 val = bit_cast<uint32_t>(value);
687 } else {
688 val = llroundf(value);
689 }
690
691 number_to_payload(data, val, value_type);
692}
693
694// Remove before 2027.2.0
695ESPDEPRECATED("Use the container overload of float_to_payload() instead. Removed in 2027.2.0", "2026.8.0")
696inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
697 std::vector<uint16_t> data;
698 float_to_payload(data, value, value_type);
699 return data;
700}
701
702} // 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.
constexpr bool VALUE_TYPE_SUPPORTED
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)
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.
constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word)
Combine two register words into a 32-bit value.
decltype(registers_to_value< VALUE_TYPE >(static_cast< const uint16_t * >(nullptr))) RegisterValueType
The type registers_to_value() yields for a given value type.
T get_data(const uint8_t *data, size_t buffer_offset)
Extract data from modbus response buffer.
constexpr std::optional< RegisterValueType< VALUE_TYPE > > value_at(std::span< const uint16_t > registers, uint16_t start_address, uint16_t address)
The value stored at an absolute register address, or nullopt when it is not wholly inside this respon...
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.
bool is_function_code_read(uint8_t function_code)
bool is_function_code_write(uint8_t function_code)
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)
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,...
bool is_function_code_broadcastable(uint8_t function_code)
True when the underlying function code (exception bit masked off) may be broadcast (address 0).
uint8_t pdu_function_code(std::span< const uint8_t > pdu)
Function code of a PDU, exception flag masked; 0 for an empty PDU.
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)
constexpr uint64_t registers_to_uint64(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3)
Combine four register words into a 64-bit value, most significant word first.
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...
std::optional< uint16_t > client_pdu_start_address(std::span< const uint8_t > pdu)
Start address of a standard client request PDU ([fc, addr_hi, addr_lo, ...]).
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:1092
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()