mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 19:06:37 +00:00
[modbus] CRC scan all unknown function codes (#18483)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
92f55f721f
commit
285a508e09
@@ -219,14 +219,25 @@ void ModbusServerHub::parse_modbus_frames() {
|
||||
this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
|
||||
}
|
||||
|
||||
uint16_t Modbus::find_custom_frame_end_(uint16_t min_length) const {
|
||||
// Custom functions could be any length - we have to rely on the CRC to determine completeness.
|
||||
uint16_t Modbus::find_frame_end_by_crc_(uint16_t min_length) const {
|
||||
// Unknown-length functions (user-defined codes, unimplemented management codes, unassigned values)
|
||||
// could be any length - we have to rely on the CRC to determine completeness.
|
||||
// If a CRC match is never found, the buffer will eventually overflow and be cleared.
|
||||
const uint8_t *raw = &this->rx_buffer_[0];
|
||||
const size_t size = this->rx_buffer_.size();
|
||||
for (uint16_t len = min_length; len <= std::min(size, size_t(MAX_FRAME_SIZE)); len++) {
|
||||
if (crc16(raw, len) == 0)
|
||||
return len;
|
||||
const auto max_len = static_cast<uint16_t>(std::min(size, size_t(MAX_FRAME_SIZE)));
|
||||
if (min_length > max_len)
|
||||
return 0;
|
||||
// The Modbus CRC (poly 0xa001, refin/refout false) keeps its running state in the returned value,
|
||||
// so we seed once over the first min_length bytes and extend one byte at a time instead of
|
||||
// recomputing the whole prefix for every candidate length.
|
||||
uint16_t crc = crc16(raw, min_length);
|
||||
if (crc == 0)
|
||||
return min_length;
|
||||
for (uint16_t len = min_length; len < max_len; len++) {
|
||||
crc = crc16(&raw[len], 1, crc);
|
||||
if (crc == 0)
|
||||
return len + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -241,11 +252,11 @@ bool Modbus::parse_modbus_server_frame_() {
|
||||
uint8_t address = this->rx_buffer_[0];
|
||||
uint8_t function_code = this->rx_buffer_[1];
|
||||
|
||||
if (helpers::is_function_code_custom(function_code)) {
|
||||
frame_length = this->find_custom_frame_end_(frame_length);
|
||||
if (helpers::is_function_code_unknown_length(function_code)) {
|
||||
frame_length = this->find_frame_end_by_crc_(frame_length);
|
||||
if (frame_length == 0)
|
||||
return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
|
||||
ESP_LOGD(TAG, "User-defined function %02X found", function_code);
|
||||
ESP_LOGD(TAG, "Unknown-length function %02X found", function_code);
|
||||
} else {
|
||||
if (crc16(&this->rx_buffer_[0], frame_length) != 0)
|
||||
return false;
|
||||
@@ -272,11 +283,11 @@ bool ModbusServerHub::parse_modbus_client_frame_() {
|
||||
uint8_t address = this->rx_buffer_[0];
|
||||
uint8_t function_code = this->rx_buffer_[1];
|
||||
|
||||
if (helpers::is_function_code_custom(function_code)) {
|
||||
frame_length = this->find_custom_frame_end_(frame_length);
|
||||
if (helpers::is_function_code_unknown_length(function_code)) {
|
||||
frame_length = this->find_frame_end_by_crc_(frame_length);
|
||||
if (frame_length == 0)
|
||||
return size < MAX_FRAME_SIZE; // Continue to parse until we hit max size
|
||||
ESP_LOGD(TAG, "User-defined function %02X found", function_code);
|
||||
ESP_LOGD(TAG, "Unknown-length function %02X found", function_code);
|
||||
} else {
|
||||
if (crc16(&this->rx_buffer_[0], frame_length) != 0)
|
||||
return false;
|
||||
|
||||
@@ -82,7 +82,7 @@ class Modbus : public uart::UARTDevice, public Component {
|
||||
bool send_frame_(const ModbusFrame &frame);
|
||||
// Scans forward from min_length to find a frame boundary by CRC match for custom function codes.
|
||||
// Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE.
|
||||
uint16_t find_custom_frame_end_(uint16_t min_length) const;
|
||||
uint16_t find_frame_end_by_crc_(uint16_t min_length) const;
|
||||
|
||||
uint32_t last_modbus_byte_{0};
|
||||
uint32_t last_receive_check_{0};
|
||||
|
||||
@@ -55,6 +55,38 @@ inline bool is_function_code_custom(uint8_t function_code) {
|
||||
masked_function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END);
|
||||
}
|
||||
|
||||
/// True for any function code whose frame length the parsers cannot predict - everything the
|
||||
/// server_pdu_length()/client_pdu_length() switches fall through to `default` on (keep the case list
|
||||
/// in step with those switches). Deliberately wider than is_function_code_custom(): the user-defined
|
||||
/// ranges are unknown to the parser too, but so are the assigned-but-unimplemented codes
|
||||
/// (READ_EXCEPTION_STATUS, DIAGNOSTICS, GET_COMM_EVENT_*, REPORT_SERVER_ID) and every unassigned value.
|
||||
/// The 0x80 exception flag is masked off first, so a frame with it set classifies by its base code -
|
||||
/// even though a spec exception reply has a known 2-byte PDU. That is deliberate, matching what
|
||||
/// is_function_code_custom() has always done: some vendors use codes with the 0x80 bit set as ordinary
|
||||
/// codes with longer payloads, so the response parser CRC-scans these rather than assuming the spec
|
||||
/// length. For an intact spec exception the scan matches at its first candidate, so only a corrupt one
|
||||
/// pays (recovery by timeout instead of an immediate CRC failure).
|
||||
inline bool is_function_code_unknown_length(uint8_t function_code) {
|
||||
switch (static_cast<FunctionCode>(function_code & FUNCTION_CODE_MASK)) {
|
||||
case FunctionCode::READ_COILS:
|
||||
case FunctionCode::READ_DISCRETE_INPUTS:
|
||||
case FunctionCode::READ_HOLDING_REGISTERS:
|
||||
case FunctionCode::READ_INPUT_REGISTERS:
|
||||
case FunctionCode::WRITE_SINGLE_COIL:
|
||||
case FunctionCode::WRITE_SINGLE_REGISTER:
|
||||
case FunctionCode::WRITE_MULTIPLE_COILS:
|
||||
case FunctionCode::WRITE_MULTIPLE_REGISTERS:
|
||||
case FunctionCode::READ_FILE_RECORD:
|
||||
case FunctionCode::WRITE_FILE_RECORD:
|
||||
case FunctionCode::MASK_WRITE_REGISTER:
|
||||
case FunctionCode::READ_WRITE_MULTIPLE_REGISTERS:
|
||||
case FunctionCode::READ_FIFO_QUEUE:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the expected length of a server response PDU based on the function code.
|
||||
// If too few bytes have arrived to determine the length, returns the minimum length. `size` is the
|
||||
// number of bytes available so far, which may exceed the eventual PDU (e.g. include the frame's CRC
|
||||
|
||||
Reference in New Issue
Block a user