ESPHome 2026.8.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
14inline bool is_function_code_read(uint8_t function_code) {
15 ModbusFunctionCode masked_function_code = static_cast<ModbusFunctionCode>(function_code & FUNCTION_CODE_MASK);
16 return masked_function_code == ModbusFunctionCode::READ_COILS ||
17 masked_function_code == ModbusFunctionCode::READ_DISCRETE_INPUTS ||
18 masked_function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
19 masked_function_code == ModbusFunctionCode::READ_INPUT_REGISTERS;
20}
21
22inline bool is_function_code_write(uint8_t function_code) {
23 ModbusFunctionCode masked_function_code = static_cast<ModbusFunctionCode>(function_code & FUNCTION_CODE_MASK);
24 return masked_function_code == ModbusFunctionCode::WRITE_SINGLE_COIL ||
25 masked_function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
26 masked_function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
28}
29
30inline bool is_function_code_exception(uint8_t function_code) {
31 return (static_cast<uint8_t>(function_code) & FUNCTION_CODE_EXCEPTION_MASK) != 0;
32}
33
34inline bool is_function_code_custom(uint8_t function_code) {
35 uint8_t masked_function_code = function_code & FUNCTION_CODE_MASK;
36 return (masked_function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT &&
37 masked_function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END) ||
38 (masked_function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT &&
39 masked_function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END);
40}
41
42// Returns the expected length of a server response frame based on the function code
43// If the frame is too short to determine the length, returns the minimum length
44uint16_t server_frame_length(const uint8_t *frame, size_t size);
45
46// Returns the expected length of a client request frame based on the function code
47// If the frame is too short to determine the length, returns the minimum length
48uint16_t client_frame_length(const uint8_t *frame, size_t size);
49
50inline uint8_t server_frame_data_offset(const uint8_t *frame, size_t size) {
51 if (size < 2)
52 return 0;
53 switch (static_cast<ModbusFunctionCode>(frame[1])) {
58 return 3; // address(1) + function(1) + byte count(1) + data + CRC(2)
59 default:
60 return 2;
61 }
62}
63
64inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; }
65
66enum class SensorValueType : uint8_t {
67 RAW = 0x00, // variable length
68 U_WORD = 0x1, // 1 Register unsigned
69 U_DWORD = 0x2, // 2 Registers unsigned
70 S_WORD = 0x3, // 1 Register signed
71 S_DWORD = 0x4, // 2 Registers signed
72 BIT = 0x5,
73 U_DWORD_R = 0x6, // 2 Registers unsigned
74 S_DWORD_R = 0x7, // 2 Registers unsigned
75 U_QWORD = 0x8,
76 S_QWORD = 0x9,
77 U_QWORD_R = 0xA,
78 S_QWORD_R = 0xB,
79 FP32 = 0xC,
80 FP32_R = 0xD
81};
82
86
101
103 switch (reg_type) {
108 // These register types can't be written (per spec)
111 default:
113 }
114}
115
116inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
117
126inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
127 if (value.length() < pos * 2 + 2)
128 return 0;
129 return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
130}
131
138inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
139 return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
140}
141
148inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
149 return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
150}
151
158inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
159 return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
160}
161
162// Extract data from modbus response buffer
169template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
170 if (sizeof(T) == sizeof(uint8_t)) {
171 return T(data[buffer_offset]);
172 }
173 if (sizeof(T) == sizeof(uint16_t)) {
174 return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
175 }
176 if (sizeof(T) == sizeof(uint32_t)) {
177 return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
178 static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
179 }
180 if (sizeof(T) == sizeof(uint64_t)) {
181 return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
182 (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
183 }
184 static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
185 sizeof(T) == sizeof(uint64_t),
186 "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
187 return T{};
188}
189
190template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
191 return get_data<T>(data.data(), buffer_offset);
192}
193
202inline bool bit_from_packed(int bit, std::span<const uint8_t> data) {
203 auto data_byte = bit / 8;
204 return (data[data_byte] & (1 << (bit % 8))) > 0;
205}
206
207// Remove before 2027.2.0
208ESPDEPRECATED("Use bit_from_packed() instead. Removed in 2027.2.0", "2026.8.0")
209inline bool coil_from_vector(int coil, std::span<const uint8_t> data) { return bit_from_packed(coil, data); }
210
221template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
222 auto result = (mask & data);
223 if (result == 0 || mask == 0xFFFFFFFF) {
224 return result;
225 }
226 for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
227 if (pos < 32 && (mask & (1UL << pos)) != 0)
228 return result >> pos;
229 }
230 return 0;
231}
232
233// Logs an error for an unsupported value type. Defined in the .cpp so logging stays out of headers.
235
239template<typename Container> void number_to_payload(Container &data, int64_t value, SensorValueType value_type) {
240 switch (value_type) {
243 data.push_back(value & 0xFFFF);
244 break;
248 data.push_back((value & 0xFFFF0000) >> 16);
249 data.push_back(value & 0xFFFF);
250 break;
254 data.push_back(value & 0xFFFF);
255 data.push_back((value & 0xFFFF0000) >> 16);
256 break;
259 data.push_back((value & 0xFFFF000000000000) >> 48);
260 data.push_back((value & 0xFFFF00000000) >> 32);
261 data.push_back((value & 0xFFFF0000) >> 16);
262 data.push_back(value & 0xFFFF);
263 break;
266 data.push_back(value & 0xFFFF);
267 data.push_back((value & 0xFFFF0000) >> 16);
268 data.push_back((value & 0xFFFF00000000) >> 32);
269 data.push_back((value & 0xFFFF000000000000) >> 48);
270 break;
271 default:
272 log_unsupported_value_type(value_type);
273 break;
274 }
275}
276
285std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type,
286 uint8_t offset, uint32_t bitmask);
287
289inline std::optional<int64_t> payload_to_number(std::span<const uint8_t> data, SensorValueType sensor_value_type,
290 uint8_t offset, uint32_t bitmask) {
291 return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask);
292}
293
294// Remove before 2027.2.0
295ESPDEPRECATED("Use the std::span overload returning std::optional<int64_t> instead. Removed in 2027.2.0", "2026.8.0")
296inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
297 uint32_t bitmask) {
298 // Released behavior: a too-short payload logs an error and decodes to 0.
299 return payload_to_number(std::span<const uint8_t>(data), sensor_value_type, offset, bitmask).value_or(0);
300}
301
309std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type);
310
328 uint16_t number_of_entities, const uint8_t *values = nullptr,
329 size_t values_len = 0);
330
331inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
332 int64_t val;
333
334 if (value_type_is_float(value_type)) {
335 val = bit_cast<uint32_t>(value);
336 } else {
337 val = llroundf(value);
338 }
339
340 std::vector<uint16_t> data;
341 number_to_payload(data, val, value_type);
342 return data;
343}
344
345} // namespace esphome::modbus::helpers
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:222
mopeka_std_values val[3]
ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type)
bool value_type_is_float(SensorValueType v)
uint8_t server_frame_data_offset(const uint8_t *frame, size_t size)
ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type, bool multiple=false)
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.
std::span< const uint8_t > data
T get_data(const uint8_t *data, size_t buffer_offset)
Extract data from modbus response buffer.
std::vector< uint16_t > float_to_payload(float value, SensorValueType value_type)
bool is_function_code_read(uint8_t function_code)
bool is_function_code_write(uint8_t 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...
uint32_t dword_from_hex_str(const std::string &value, uint8_t pos)
Get a dword from a hex string.
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)
void log_unsupported_value_type(SensorValueType value_type)
bool is_function_code_custom(uint8_t function_code)
uint16_t word_from_hex_str(const std::string &value, uint8_t pos)
Get a word from a hex string.
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 .
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.
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
uint16_t size
Definition helpers.cpp:25
size_t size_t pos
Definition helpers.h:1052
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