mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 19:06:37 +00:00
[modbus] Frame accessors, PDU-relative lengths, and span-based send_pdu (#17846)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
58385bebff
commit
eaa79b3696
@@ -7,74 +7,157 @@ 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)
|
||||
uint16_t server_pdu_length(const uint8_t *frame, size_t size) {
|
||||
if (size < MIN_PDU_SIZE)
|
||||
return MIN_PDU_SIZE;
|
||||
if (is_function_code_exception(frame[0])) {
|
||||
return 2; // function(1) + exception(1)
|
||||
}
|
||||
switch (static_cast<FunctionCode>(frame[1])) {
|
||||
switch (static_cast<FunctionCode>(frame[0])) {
|
||||
case FunctionCode::READ_COILS:
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
case FunctionCode::READ_HOLDING_REGISTERS:
|
||||
case FunctionCode::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);
|
||||
// function(1) + byte count(1) + data
|
||||
return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
case FunctionCode::WRITE_SINGLE_REGISTER:
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
case FunctionCode::WRITE_MULTIPLE_REGISTERS:
|
||||
return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2)
|
||||
return 5; // function(1) + output/register address(2) + value(2)
|
||||
// Unsupported function codes. Included here to prevent parser failures. Excluding Serial Line specific functions.
|
||||
case FunctionCode::READ_FILE_RECORD:
|
||||
case FunctionCode::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);
|
||||
// function(1) + byte count(1) + data
|
||||
return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0);
|
||||
case FunctionCode::MASK_WRITE_REGISTER:
|
||||
return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2)
|
||||
return 7; // function(1) + reference address(2) + AND mask(2) + OR mask(2)
|
||||
case FunctionCode::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);
|
||||
// function(1) + byte count(1) + data
|
||||
return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0);
|
||||
case FunctionCode::READ_FIFO_QUEUE:
|
||||
// address(1) + function(1) + fifo address(2) CRC(2)
|
||||
return 6;
|
||||
// function(1) + fifo address(2)
|
||||
return 3;
|
||||
default:
|
||||
return MIN_FRAME_SIZE; // unknown length
|
||||
return MIN_PDU_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<FunctionCode>(frame[1])) {
|
||||
uint16_t client_pdu_length(const uint8_t *frame, size_t size) {
|
||||
if (size < MIN_PDU_SIZE)
|
||||
return MIN_PDU_SIZE;
|
||||
switch (static_cast<FunctionCode>(frame[0])) {
|
||||
case FunctionCode::READ_COILS:
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
case FunctionCode::READ_HOLDING_REGISTERS:
|
||||
case FunctionCode::READ_INPUT_REGISTERS:
|
||||
// address(1) + function(1) + start address(2) + quantity(2) + CRC(2)
|
||||
// function(1) + start address(2) + quantity(2)
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
case FunctionCode::WRITE_SINGLE_REGISTER:
|
||||
return 8; // address(1) + function(1) + output/register address(2) + value(2) + CRC(2)
|
||||
return 5; // function(1) + output/register address(2) + value(2)
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
case FunctionCode::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);
|
||||
// function(1) + start address(2) + quantity(2) + byte count(1) + data
|
||||
return 6 + (size > 5 ? std::min(frame[5], 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 FunctionCode::READ_FILE_RECORD:
|
||||
case FunctionCode::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);
|
||||
// function(1) + byte count(1) + data
|
||||
return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_PDU_SIZE - 2)) : 0);
|
||||
case FunctionCode::MASK_WRITE_REGISTER:
|
||||
return 10; // address(1) + function(1) + reference address(2) + AND mask(2) + OR mask(2) + CRC(2)
|
||||
return 7; // function(1) + reference address(2) + AND mask(2) + OR mask(2)
|
||||
case FunctionCode::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);
|
||||
// function(1) + read start address(2) + read quantity(2) + write start address(2) +
|
||||
// write quantity(2) + byte count(1) + data
|
||||
return 10 + (size > 9 ? std::min(frame[9], uint8_t(MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2)) : 0);
|
||||
case FunctionCode::READ_FIFO_QUEUE:
|
||||
// address(1) + function(1) + fifo address(2) CRC(2)
|
||||
return 6;
|
||||
// function(1) + fifo address(2)
|
||||
return 3;
|
||||
default:
|
||||
return MIN_FRAME_SIZE; // unknown length
|
||||
return MIN_PDU_SIZE; // unknown length
|
||||
}
|
||||
}
|
||||
|
||||
bool is_server_pdu_standard(const uint8_t *pdu, size_t size) {
|
||||
if (server_pdu_length(pdu, size) != size)
|
||||
return false;
|
||||
|
||||
switch (static_cast<FunctionCode>(pdu[0])) {
|
||||
case FunctionCode::READ_COILS:
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
// A conformant bit-read response carries at least one packed byte (up to 2000 bits = 250 bytes).
|
||||
return pdu[1] != 0 && pdu[1] <= uint8_t((MAX_NUM_OF_COILS_TO_READ + 7) / 8);
|
||||
case FunctionCode::READ_HOLDING_REGISTERS:
|
||||
case FunctionCode::READ_INPUT_REGISTERS:
|
||||
// Registers are 2 bytes each: the byte count must be a non-zero even count within the read maximum.
|
||||
return pdu[1] != 0 && pdu[1] % 2 == 0 && pdu[1] <= uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2);
|
||||
case FunctionCode::READ_FILE_RECORD:
|
||||
case FunctionCode::WRITE_FILE_RECORD:
|
||||
return pdu[1] <= uint8_t(MAX_PDU_SIZE - 2);
|
||||
case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS:
|
||||
return pdu[1] != 0 && pdu[1] % 2 == 0 && pdu[1] <= uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2);
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
case FunctionCode::WRITE_MULTIPLE_REGISTERS: {
|
||||
// The response echoes start address and quantity: bound them like the request side does.
|
||||
const bool bits = static_cast<FunctionCode>(pdu[0]) == FunctionCode::WRITE_MULTIPLE_COILS;
|
||||
const uint16_t start_address = get_data<uint16_t>(pdu, 1);
|
||||
const uint16_t quantity = get_data<uint16_t>(pdu, 3);
|
||||
const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE;
|
||||
return quantity != 0 && quantity <= max_quantity && (uint32_t) start_address + quantity <= 0x10000u;
|
||||
}
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
// The response echoes the request, so the same ON/OFF constraint applies.
|
||||
return (pdu[3] == 0xFF || pdu[3] == 0x00) && pdu[4] == 0x00;
|
||||
default:
|
||||
return true; // All other function codes validated by length alone
|
||||
}
|
||||
}
|
||||
|
||||
bool is_client_pdu_standard(const uint8_t *pdu, size_t size) {
|
||||
if (client_pdu_length(pdu, size) != size)
|
||||
return false;
|
||||
|
||||
switch (static_cast<FunctionCode>(pdu[0])) {
|
||||
case FunctionCode::READ_COILS:
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
case FunctionCode::READ_HOLDING_REGISTERS:
|
||||
case FunctionCode::READ_INPUT_REGISTERS: {
|
||||
const bool bits = static_cast<FunctionCode>(pdu[0]) == FunctionCode::READ_COILS ||
|
||||
static_cast<FunctionCode>(pdu[0]) == FunctionCode::READ_DISCRETE_INPUTS;
|
||||
const uint16_t start_address = get_data<uint16_t>(pdu, 1);
|
||||
const uint16_t quantity = get_data<uint16_t>(pdu, 3);
|
||||
const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_READ : MAX_NUM_OF_REGISTERS_TO_READ;
|
||||
return quantity != 0 && quantity <= max_quantity && (uint32_t) start_address + quantity <= 0x10000u;
|
||||
}
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
case FunctionCode::WRITE_MULTIPLE_REGISTERS: {
|
||||
const bool bits = static_cast<FunctionCode>(pdu[0]) == FunctionCode::WRITE_MULTIPLE_COILS;
|
||||
const uint16_t start_address = get_data<uint16_t>(pdu, 1);
|
||||
const uint16_t quantity = get_data<uint16_t>(pdu, 3);
|
||||
const uint16_t max_quantity = bits ? MAX_NUM_OF_COILS_TO_WRITE : MAX_NUM_OF_REGISTERS_TO_WRITE;
|
||||
// Coils are packed 8 per data byte; registers are 2 bytes each.
|
||||
const size_t expected_data_bytes = bits ? (static_cast<size_t>(quantity) + 7) / 8 : quantity * 2;
|
||||
return quantity != 0 && quantity <= max_quantity && (uint32_t) start_address + quantity <= 0x10000u &&
|
||||
pdu[5] == expected_data_bytes;
|
||||
}
|
||||
case FunctionCode::READ_FILE_RECORD:
|
||||
case FunctionCode::WRITE_FILE_RECORD:
|
||||
return pdu[1] <= MAX_PDU_SIZE - 2;
|
||||
case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS: {
|
||||
const uint16_t start_address_read = get_data<uint16_t>(pdu, 1);
|
||||
const uint16_t quantity_read = get_data<uint16_t>(pdu, 3);
|
||||
const uint16_t start_address_write = get_data<uint16_t>(pdu, 5);
|
||||
const uint16_t quantity_write = get_data<uint16_t>(pdu, 7);
|
||||
return quantity_read != 0 && quantity_read <= MAX_NUM_OF_REGISTERS_TO_READ && quantity_write != 0 &&
|
||||
quantity_write <= MAX_NUM_OF_REGISTERS_TO_WRITE_RW &&
|
||||
(uint32_t) start_address_read + quantity_read <= 0x10000u &&
|
||||
(uint32_t) start_address_write + quantity_write <= 0x10000u && pdu[9] == quantity_write * 2;
|
||||
}
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
// The one variable field in an otherwise fixed-shape PDU: the spec allows exactly ON/OFF.
|
||||
return (pdu[3] == 0xFF || pdu[3] == 0x00) && pdu[4] == 0x00;
|
||||
default:
|
||||
return true; // All other function codes validated by length alone
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user