[modbus] Fix parsing & split out server mode (#11969)

This commit is contained in:
Bonne Eggleston
2026-06-21 13:32:35 -05:00
committed by GitHub
parent dbdf125ec8
commit 63d8a344c5
20 changed files with 1211 additions and 532 deletions
+176 -1
View File
@@ -1,10 +1,83 @@
#include "modbus_helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::modbus::helpers {
static const char *const TAG = "modbus_helpers";
uint16_t server_frame_length(const uint8_t *frame, size_t size) {
if (size < 2)
return MIN_FRAME_SIZE;
if (is_function_code_exception(frame[1])) {
return 5; // address(1) + function(1) + exception(1) + CRC(2)
}
switch (static_cast<ModbusFunctionCode>(frame[1])) {
case ModbusFunctionCode::READ_COILS:
case ModbusFunctionCode::READ_DISCRETE_INPUTS:
case ModbusFunctionCode::READ_HOLDING_REGISTERS:
case ModbusFunctionCode::READ_INPUT_REGISTERS:
// address(1) + function(1) + byte count(1) + data + CRC(2)
return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
case ModbusFunctionCode::WRITE_SINGLE_COIL:
case ModbusFunctionCode::WRITE_SINGLE_REGISTER:
case ModbusFunctionCode::WRITE_MULTIPLE_COILS:
case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS:
return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2)
// Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
case ModbusFunctionCode::READ_FILE_RECORD:
case ModbusFunctionCode::WRITE_FILE_RECORD:
// address(1) + function(1) + byte count(1) + data + CRC(2)
return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_FRAME_SIZE - 5)) : 0);
case ModbusFunctionCode::MASK_WRITE_REGISTER:
return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2)
case ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS:
// address(1) + function(1) + byte count(1) + data + CRC(2)
return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
case ModbusFunctionCode::READ_FIFO_QUEUE:
// address(1) + function(1) + fifo address(2) CRC(2)
return 6;
default:
return MIN_FRAME_SIZE; // unknown length
}
}
uint16_t client_frame_length(const uint8_t *frame, size_t size) {
if (size < 2)
return MIN_FRAME_SIZE;
switch (static_cast<ModbusFunctionCode>(frame[1])) {
case ModbusFunctionCode::READ_COILS:
case ModbusFunctionCode::READ_DISCRETE_INPUTS:
case ModbusFunctionCode::READ_HOLDING_REGISTERS:
case ModbusFunctionCode::READ_INPUT_REGISTERS:
// address(1) + function(1) + start address(2) + quantity(2) + CRC(2)
case ModbusFunctionCode::WRITE_SINGLE_COIL:
case ModbusFunctionCode::WRITE_SINGLE_REGISTER:
return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2)
case ModbusFunctionCode::WRITE_MULTIPLE_COILS:
case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS:
// address(1) + function(1) + start address(2) + quantity(2) + byte count(1) + data + CRC(2)
return 9 + (size > 6 ? std::min(frame[6], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0);
// Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
case ModbusFunctionCode::READ_FILE_RECORD:
case ModbusFunctionCode::WRITE_FILE_RECORD:
// address(1) + function(1) + byte count(1) + data + CRC(2)
return 5 + (size > 2 ? std::min(frame[2], uint8_t(MAX_FRAME_SIZE - 5)) : 0);
case ModbusFunctionCode::MASK_WRITE_REGISTER:
return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2)
case ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS:
// address(1) + function(1) + read start address(2) + read quantity(2) + write start address(2) +
// write quantity(2) + byte count(1) + data + CRC(2)
return 13 + (size > 10 ? std::min(frame[10], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE * 2)) : 0);
case ModbusFunctionCode::READ_FIFO_QUEUE:
// address(1) + function(1) + fifo address(2) CRC(2)
return 6;
default:
return MIN_FRAME_SIZE; // unknown length
}
}
static size_t required_payload_size(SensorValueType sensor_value_type) {
switch (sensor_value_type) {
case SensorValueType::U_WORD:
@@ -67,7 +140,7 @@ void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueTy
}
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
uint32_t bitmask, bool *error_return) {
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
// Validate offset against the buffer for all types, including RAW/unsupported, so
@@ -75,6 +148,8 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
if (static_cast<size_t>(offset) > data.size()) {
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
static_cast<unsigned int>(offset), data.size());
if (error_return)
*error_return = true;
return value;
}
@@ -87,6 +162,8 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
required_size);
if (error_return)
*error_return = true;
return value;
}
@@ -136,4 +213,102 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
}
return value;
}
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) {
if (is_function_code_read(static_cast<uint8_t>(function_code))) {
if (values != nullptr || values_len > 0) {
ESP_LOGW(TAG, "Values provided for read function code %02X, but will be ignored",
static_cast<uint8_t>(function_code));
}
} else if (is_function_code_write(static_cast<uint8_t>(function_code))) {
if (values == nullptr || values_len == 0) {
ESP_LOGE(TAG, "No values provided for write function code %02X", static_cast<uint8_t>(function_code));
return {};
}
} else {
ESP_LOGE(TAG, "Unsupported function code %02X for client PDU creation", static_cast<uint8_t>(function_code));
return {};
}
if (number_of_entities == 0) {
ESP_LOGE(TAG, "Number of entities is zero for function code %02X", static_cast<uint8_t>(function_code));
return {};
}
switch (function_code) {
case ModbusFunctionCode::READ_COILS:
if (number_of_entities > MAX_NUM_OF_COILS_TO_READ) {
ESP_LOGE(TAG, "number_of_entities %u exceeds maximum coils to read %u for function code %02X",
number_of_entities, MAX_NUM_OF_COILS_TO_READ, static_cast<uint8_t>(function_code));
return {};
}
break;
case ModbusFunctionCode::READ_DISCRETE_INPUTS:
if (number_of_entities > MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) {
ESP_LOGE(TAG, "number_of_entities %u exceeds maximum discrete inputs to read %u for function code %02X",
number_of_entities, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ, static_cast<uint8_t>(function_code));
return {};
}
break;
case ModbusFunctionCode::READ_HOLDING_REGISTERS:
case ModbusFunctionCode::READ_INPUT_REGISTERS:
if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to read %u for function code %02X",
number_of_entities, MAX_NUM_OF_REGISTERS_TO_READ, static_cast<uint8_t>(function_code));
return {};
}
break;
case ModbusFunctionCode::WRITE_SINGLE_COIL:
case ModbusFunctionCode::WRITE_SINGLE_REGISTER:
break; // number_of_entities is ignored for single write, so no need to validate
case ModbusFunctionCode::WRITE_MULTIPLE_COILS:
case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS:
if (number_of_entities > MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGE(TAG, "number_of_entities %u exceeds maximum registers to write %u for function code %02X",
number_of_entities, MAX_NUM_OF_REGISTERS_TO_WRITE, static_cast<uint8_t>(function_code));
return {};
}
break;
default:
ESP_LOGE(TAG, "Unsupported function code %u for client PDU creation", static_cast<unsigned int>(function_code));
return {};
}
StaticVector<uint8_t, MAX_PDU_SIZE> pdu;
pdu.push_back(static_cast<uint8_t>(function_code));
pdu.push_back(start_address >> 8);
pdu.push_back(start_address >> 0);
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
pdu.push_back(number_of_entities >> 8);
pdu.push_back(number_of_entities >> 0);
}
if (is_function_code_write(static_cast<uint8_t>(function_code))) {
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
// 6 bytes of overhead (fc + start_addr×2 + qty×2 + byte_count) leave MAX_PDU_SIZE-6 bytes for values
static constexpr size_t MAX_WRITE_MULTIPLE_VALUES_LEN = MAX_PDU_SIZE - 6;
if (values_len > MAX_WRITE_MULTIPLE_VALUES_LEN) {
ESP_LOGE(TAG, "values_len %zu exceeds PDU capacity %zu, dropping request", values_len,
MAX_WRITE_MULTIPLE_VALUES_LEN);
return {};
}
pdu.push_back(values_len); // Byte count is required for write multiple
for (size_t i = 0; i < values_len; i++)
pdu.push_back(values[i]);
} else {
// Write single register or coil (2 bytes)
if (values_len < 2) {
ESP_LOGE(TAG, "values_len %zu too small for write-single command (need 2), dropping request", values_len);
return {};
}
pdu.push_back(values[0]);
pdu.push_back(values[1]);
}
}
return pdu;
}
} // namespace esphome::modbus::helpers