8static const char *
const TAG =
"modbus_helpers";
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);
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;
25 if (
size < MIN_PDU_SIZE)
34 return 2 + (
size > 1 ? std::min(frame[1], uint8_t(
packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ))) : 0);
38 return 2 + (
size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
48 return 2 + (
size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0);
53 return 2 + (
size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
63 if (
size < MIN_PDU_SIZE)
79 return 6 + (
size > 5 ? std::min(frame[5], uint8_t(
packed_bit_bytes(MAX_NUM_OF_COILS_TO_WRITE))) : 0);
82 return 6 + (
size > 5 ? std::min(frame[5], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0);
87 return 2 + (
size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0);
93 return 10 + (
size > 9 ? std::min(frame[9], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2)) : 0);
106 const auto function_code =
static_cast<FunctionCode>(pdu[0]);
107 switch (function_code) {
111 return pdu[1] != 0 && pdu[1] <= uint8_t(
packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ));
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);
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);
132 return is_canonical_coil_value(pdu[3], pdu[4]);
142 const auto function_code =
static_cast<FunctionCode>(pdu[0]);
143 switch (function_code) {
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);
160 const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE;
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;
167 return pdu[1] <= MAX_PDU_SIZE - 2;
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;
179 return is_canonical_coil_value(pdu[3], pdu[4]);
185static size_t required_payload_size(
SensorValueType sensor_value_type) {
186 switch (sensor_value_type) {
211 ESP_LOGE(TAG,
"Invalid data type for modbus number to payload conversion: %d",
static_cast<uint16_t
>(value_type));
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);
226 const size_t required_size = required_payload_size(sensor_value_type);
227 if (required_size == 0) {
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);
237 switch (sensor_value_type) {
254 value =
static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
272 uint32_t sign_bit = (value & 0x8000) << 16;
274 static_cast<int32_t
>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
285 value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
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));
310 switch (sensor_value_type) {
354 pdu.
push_back(
static_cast<uint8_t
>(function_code));
355 append_pdu_word(pdu, first);
356 append_pdu_word(pdu,
second);
363static void mask_trailing_pad_bits(std::span<uint8_t>
data, uint16_t bit_count) {
364 if (
data.empty() || bit_count % 8 == 0)
366 data.back() &=
static_cast<uint8_t
>((1 << (bit_count % 8)) - 1);
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));
376 ESP_LOGE(TAG,
"Read of %u entities at %u runs past the 16-bit address space, dropping request", number_of_entities,
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));
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));
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));
405 ESP_LOGE(TAG,
"Unsupported function code %02X for read PDU creation",
static_cast<uint8_t
>(function_code));
409 append_pdu_header(pdu, function_code, start_address, number_of_entities);
414 const uint8_t *values,
size_t values_len) {
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));
421 auto read_pdu =
create_read_pdu(function_code, start_address, number_of_entities);
422 pdu.
assign(read_pdu.begin(), read_pdu.end());
427 const bool is_single =
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));
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));
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));
448 const uint16_t max_entities =
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));
456 ESP_LOGE(TAG,
"Write of %u entities at %u runs past the 16-bit address space, dropping request", number_of_entities,
463 if (values_len < 2) {
464 ESP_LOGE(TAG,
"values_len %zu too small for write-single command (need 2), dropping request", values_len);
470 ESP_LOGE(TAG,
"Invalid single-coil value %02X%02X (must be FF00 or 0000), dropping request", values[0],
474 append_pdu_header(pdu, function_code, start_address, uint16_t((values[0] << 8) | values[1]));
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));
487 append_pdu_header(pdu, function_code, start_address, number_of_entities);
489 for (
size_t i = 0; i < values_len; i++)
492 mask_trailing_pad_bits(pdu, number_of_entities);
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);
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);
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);
520 if (!register_block_in_range(LOG_STR(
"Write"), start_address, values.size(), max_registers)) {
524 pdu.push_back(
static_cast<uint8_t
>(values.size() * 2));
525 for (
auto v : values) {
526 append_pdu_word(pdu, v);
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");
534 "the small write buffer must hold exactly MAX_FEW_REGISTERS registers");
537 return build_write_registers_pdu<PduBuffer>(start_address, values);
541 return build_write_registers_pdu<WriteFewRegistersPdu>(start_address, values);
545 uint16_t write_start_address,
546 std::span<const uint16_t> write_values) {
548 if (!register_block_in_range(LOG_STR(
"Read"), read_start_address, read_count, MAX_NUM_OF_REGISTERS_TO_READ)) {
551 if (!register_block_in_range(LOG_STR(
"Write"), write_start_address, write_values.size(),
552 MAX_NUM_OF_REGISTERS_TO_WRITE_RW)) {
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));
563 for (
auto v : write_values) {
564 append_pdu_word(pdu, v);
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();
587 ESP_LOGE(TAG,
"No coils requested for write multiple coils, dropping request");
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);
595 ESP_LOGE(TAG,
"Write of %u coils at %u runs past the 16-bit address space, dropping request", count, start_address);
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(),
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]);
609 mask_trailing_pad_bits(pdu, count);
614 build_write_coils_pdu(pdu, start_address, bits);
619template<
typename BoolContainer>
620static PduBuffer create_write_coils_pdu_from_bools(uint16_t start_address,
const BoolContainer &values) {
622 const size_t count = values.
size();
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);
631 build_write_coils_pdu(pdu, start_address, PackedBits(std::span<const uint8_t>(packed.data(), packed.size()), count));
636 return create_write_coils_pdu_from_bools(start_address, values);
640 return create_write_coils_pdu_from_bools(start_address, values);
Minimal static vector - saves memory by avoiding std::vector overhead.
void push_back(const T &value)
static constexpr size_t capacity()
void assign(InputIt first, InputIt last)
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.
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.
@ WRITE_MULTIPLE_REGISTERS
@ READ_WRITE_MULTIPLE_REGISTERS
constexpr size_t packed_bit_bytes(size_t bits)
Bits pack 8 per data byte, rounded up to whole bytes.