diff --git a/components/uartex/README.md b/components/uartex/README.md index 223bb51..3f70606 100644 --- a/components/uartex/README.md +++ b/components/uartex/README.md @@ -1,7 +1,7 @@ # UARTEx Component [![ESPHome](https://img.shields.io/badge/ESPHome-Custom%20Component-blue)](https://esphome.io/) -[![Version](https://img.shields.io/badge/version-6.1.0-green)](https://github.com/eigger/espcomponents) +[![Version](https://img.shields.io/badge/version-6.3.0-green)](https://github.com/eigger/espcomponents) A custom ESPHome component that extends UART communication to easily integrate various serial protocols with Home Assistant. @@ -96,6 +96,7 @@ uartex: |--------|------|---------|-------------| | `rx_timeout` | time | `10ms` | Receive timeout (max: 2000ms) | | `rx_length` | int | - | Fixed packet length (1-256) | +| `rx_data_length` | schema | - | Dynamic length parsing (see below) | | `tx_delay` | time | `50ms` | Delay between transmissions (max: 2000ms) | | `tx_timeout` | time | `50ms` | ACK response timeout (max: 2000ms) | | `tx_retry_cnt` | int | `3` | Retry count on ACK failure (1-10) | @@ -111,7 +112,45 @@ uartex: | `tx_checksum2` | enum | - | Transmit checksum (multi-byte) | | `rx_priority` | enum | `data` | Processing priority: `data`, `loop` | -> **Note**: Use either `rx_checksum` or `rx_checksum2`, not both. Same applies to `tx_checksum` and `tx_checksum2`. +> **Note**: +> - Use either `rx_length` or `rx_data_length`, not both. +> - Use either `rx_checksum` or `rx_checksum2`, not both. Same applies to `tx_checksum` and `tx_checksum2`. + +#### Dynamic Length Parsing (`rx_data_length`) + +For protocols where the packet length is embedded within the packet itself: + +```yaml +uartex: + rx_header: [0xAA, 0xBB] + rx_data_length: + offset: 0 # Length field position (after header) + length: 1 # Length field size (1-4 bytes) + endian: big # Byte order: big, little + adjust: 0 # Length adjustment value + rx_checksum: add +``` + +**Options**: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `offset` | int | **Required** | Position of length field after header (0-128) | +| `length` | int | `1` | Size of length field in bytes (1-4) | +| `endian` | enum | `big` | Byte order: `big`, `little` | +| `adjust` | int | `0` | Value to add to parsed length (-128 to 128) | + +**Packet Structure Example**: +``` +Packet: [Header:2][Length:1][Data:N][Checksum:1] + [0xAA][0xBB][0x03][0x11][0x22][0x33][0xCC] + ↑ + Length field = 3 (data bytes) + +Total calculated: header(2) + offset(0) + length_field(1) + data(3) + adjust(0) + checksum(1) = 7 bytes +``` + +**Use Case**: Protocols where data length varies and is specified in the packet header. #### Checksum Types diff --git a/components/uartex/__init__.py b/components/uartex/__init__.py index 8a4bfe2..96173bd 100644 --- a/components/uartex/__init__.py +++ b/components/uartex/__init__.py @@ -13,7 +13,7 @@ CONF_STATE, CONF_MASK, CONF_MATCH, \ CONF_STATE_ON, CONF_STATE_OFF, CONF_COMMAND_ON, CONF_COMMAND_OFF, \ CONF_COMMAND_UPDATE, CONF_RX_TIMEOUT, CONF_TX_TIMEOUT, CONF_TX_RETRY_CNT, CONF_TX_COMMAND_QUEUE_SIZE, \ - CONF_STATE_RESPONSE, CONF_LENGTH, CONF_PRECISION, CONF_RX_LENGTH, \ + CONF_STATE_RESPONSE, CONF_LENGTH, CONF_PRECISION, CONF_RX_LENGTH, CONF_RX_DATA_LENGTH, CONF_ADJUST, \ CONF_TX_CTRL_PIN, CONF_TX_DELAY, CONF_DISABLED, CONF_ASCII, CONF_SIGNED, CONF_ENDIAN, CONF_DECODE AUTO_LOAD = ["text_sensor"] @@ -154,6 +154,16 @@ def command_hex_schema(value): return COMMAND_SCHEMA(value) return shorthand_command_hex(value) +RX_DATA_LENGTH_SCHEMA = cv.Schema({ + cv.Required(CONF_OFFSET): cv.int_range(min=0, max=128), + cv.Optional(CONF_LENGTH, default=1): cv.int_range(min=1, max=4), + cv.Optional(CONF_ENDIAN, default="big"): validate_endian, + cv.Optional(CONF_ADJUST, default=0): cv.int_range(min=-128, max=128), +}) + +def rx_data_length_schema(value): + return RX_DATA_LENGTH_SCHEMA(value) + # UARTEx Schema CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTExComponent), @@ -187,6 +197,7 @@ def command_hex_schema(value): } ), cv.Optional(CONF_RX_LENGTH): cv.int_range(min=1, max=256), + cv.Optional(CONF_RX_DATA_LENGTH): rx_data_length_schema, cv.Optional(CONF_TX_CTRL_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RX_HEADER): header_schema, cv.Optional(CONF_RX_FOOTER): validate_hex_data, @@ -219,7 +230,7 @@ def command_hex_schema(value): cv.Optional(CONF_DISABLED, default=False): cv.boolean, cv.Optional(CONF_ASCII, default=False): cv.boolean, }), -}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA), cv.has_at_most_one_key(CONF_RX_CHECKSUM, CONF_RX_CHECKSUM_2), cv.has_at_most_one_key(CONF_TX_CHECKSUM, CONF_TX_CHECKSUM_2)) +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA), cv.has_at_most_one_key(CONF_RX_LENGTH, CONF_RX_DATA_LENGTH), cv.has_at_most_one_key(CONF_RX_CHECKSUM, CONF_RX_CHECKSUM_2), cv.has_at_most_one_key(CONF_TX_CHECKSUM, CONF_TX_CHECKSUM_2)) async def to_code(config): cg.add_global(uartex_ns.using) @@ -276,6 +287,10 @@ async def to_code(config): if CONF_RX_LENGTH in config: cg.add(var.set_rx_length(config[CONF_RX_LENGTH])) + if CONF_RX_DATA_LENGTH in config: + conf = config[CONF_RX_DATA_LENGTH] + cg.add(var.set_rx_data_length(conf[CONF_OFFSET], conf[CONF_LENGTH], conf[CONF_ENDIAN], conf[CONF_ADJUST])) + if CONF_TX_CTRL_PIN in config: tx_ctrl_pin = await cg.gpio_pin_expression(config[CONF_TX_CTRL_PIN]) cg.add(var.set_tx_ctrl_pin(tx_ctrl_pin)) diff --git a/components/uartex/const.py b/components/uartex/const.py index c581719..96f40a7 100644 --- a/components/uartex/const.py +++ b/components/uartex/const.py @@ -4,6 +4,8 @@ CONF_UARTEX_ID = 'uartex_id' CONF_RX_TIMEOUT = 'rx_timeout' CONF_RX_LENGTH = 'rx_length' +CONF_RX_DATA_LENGTH = 'rx_data_length' +CONF_ADJUST = 'adjust' CONF_TX_DELAY = 'tx_delay' CONF_TX_TIMEOUT = 'tx_timeout' CONF_TX_RETRY_CNT = 'tx_retry_cnt' diff --git a/components/uartex/parser.cpp b/components/uartex/parser.cpp index d4693cf..2faa243 100644 --- a/components/uartex/parser.cpp +++ b/components/uartex/parser.cpp @@ -89,6 +89,7 @@ bool Parser::has_footer() void Parser::clear() { buffer_.clear(); + dynamic_total_len_ = 0; } bool Parser::parse_header() @@ -115,15 +116,66 @@ bool Parser::parse_footer() if (footer_.empty()) return false; if (buffer_.size() < footer_.size()) return false; if (total_len_ > 0 && buffer_.size() != total_len_) return false; + + // Dynamic data length mode: don't match footer until we have enough bytes + if (has_data_length_) + { + if (!calculate_dynamic_length()) return false; + if (buffer_.size() < dynamic_total_len_) return false; + } + return std::equal(buffer_.end() - footer_.size(), buffer_.end(), footer_.begin()); } bool Parser::parse_length() { - if (total_len_ == 0) return false; - if (footer_.size() > 0) return false; - if (buffer_.size() != total_len_) return false; - if (checksum_len_ > 0) return false; + // Fixed total length mode + if (total_len_ > 0) + { + if (footer_.size() > 0) return false; + if (buffer_.size() != total_len_) return false; + if (checksum_len_ > 0) return false; + return true; + } + + // Dynamic data length mode + if (!has_data_length_) return false; + if (!calculate_dynamic_length()) return false; + if (buffer_.size() < dynamic_total_len_) return false; + + return true; +} + +bool Parser::calculate_dynamic_length() +{ + // Check minimum required size to read length field + size_t min_size = header_.size() + data_length_offset_ + data_length_size_; + if (buffer_.size() < min_size) return false; + + // Calculate dynamic total length if not yet calculated + if (dynamic_total_len_ == 0) + { + size_t length_pos = header_.size() + data_length_offset_; + uint32_t data_len = 0; + + if (data_length_big_endian_) + { + for (uint8_t i = 0; i < data_length_size_; i++) + { + data_len = (data_len << 8) | buffer_[length_pos + i]; + } + } + else + { + for (int8_t i = data_length_size_ - 1; i >= 0; i--) + { + data_len = (data_len << 8) | buffer_[length_pos + i]; + } + } + + dynamic_total_len_ = header_.size() + data_length_offset_ + data_length_size_ + data_len + data_length_adjust_ + checksum_len_ + footer_.size(); + } + return true; } @@ -167,4 +219,13 @@ void Parser::set_buffer_len(size_t len) { buffer_len_ = len; buffer_.reserve(len + 1); +} + +void Parser::set_data_length(uint8_t offset, uint8_t length, bool big_endian, int8_t adjust) +{ + has_data_length_ = true; + data_length_offset_ = offset; + data_length_size_ = length; + data_length_big_endian_ = big_endian; + data_length_adjust_ = adjust; } \ No newline at end of file diff --git a/components/uartex/parser.h b/components/uartex/parser.h index 2979c33..4e65dfb 100644 --- a/components/uartex/parser.h +++ b/components/uartex/parser.h @@ -31,7 +31,9 @@ class Parser void set_checksum_len(size_t len); void set_total_len(size_t len); void set_buffer_len(size_t len); + void set_data_length(uint8_t offset, uint8_t length, bool big_endian, int8_t adjust); private: + bool calculate_dynamic_length(); std::vector header_; std::vector header_mask_; std::vector footer_; @@ -39,5 +41,12 @@ class Parser size_t checksum_len_; size_t total_len_; size_t buffer_len_; + // Dynamic data length parsing + bool has_data_length_{false}; + uint8_t data_length_offset_{0}; + uint8_t data_length_size_{1}; + bool data_length_big_endian_{true}; + int8_t data_length_adjust_{0}; + size_t dynamic_total_len_{0}; }; diff --git a/components/uartex/uartex.cpp b/components/uartex/uartex.cpp index c6242be..9732b32 100644 --- a/components/uartex/uartex.cpp +++ b/components/uartex/uartex.cpp @@ -8,6 +8,12 @@ void UARTExComponent::dump_config() #ifdef ESPHOME_LOG_HAS_DEBUG log_config(TAG, "rx_timeout", this->conf_rx_timeout_); log_config(TAG, "rx_length", this->conf_rx_length_); + if (this->conf_rx_data_length_.has_value()) + { + auto& dl = this->conf_rx_data_length_.value(); + ESP_LOGD(TAG, " rx_data_length: offset=%d, length=%d, endian=%s, adjust=%d", + dl.offset, dl.length, dl.endian == ENDIAN_BIG ? "big" : "little", dl.adjust); + } log_config(TAG, "tx_timeout", this->conf_tx_timeout_); log_config(TAG, "tx_delay", this->conf_tx_delay_); log_config(TAG, "tx_retry_cnt", this->conf_tx_retry_cnt_); @@ -44,6 +50,11 @@ void UARTExComponent::setup() } if (this->rx_footer_.has_value()) this->rx_parser_.add_footers(this->rx_footer_.value()); this->rx_parser_.set_total_len(this->conf_rx_length_); + if (this->conf_rx_data_length_.has_value()) + { + auto& dl = this->conf_rx_data_length_.value(); + this->rx_parser_.set_data_length(dl.offset, dl.length, dl.endian == ENDIAN_BIG, dl.adjust); + } this->rx_parser_.set_buffer_len(this->parent_->get_rx_buffer_size()); if (this->error_) this->error_->publish_state("None"); if (this->version_) this->version_->publish_state(UARTEX_VERSION); @@ -290,6 +301,11 @@ void UARTExComponent::set_rx_length(uint16_t rx_length) this->conf_rx_length_ = rx_length; } +void UARTExComponent::set_rx_data_length(uint8_t offset, uint8_t length, ENDIAN endian, int8_t adjust) +{ + this->conf_rx_data_length_ = rx_data_length_t{offset, length, endian, adjust}; +} + void UARTExComponent::set_rx_timeout(uint16_t timeout) { this->conf_rx_timeout_ = timeout; diff --git a/components/uartex/uartex.h b/components/uartex/uartex.h index b8eb069..fe1ab45 100644 --- a/components/uartex/uartex.h +++ b/components/uartex/uartex.h @@ -45,6 +45,14 @@ struct header_t std::vector mask; }; +struct rx_data_length_t +{ + uint8_t offset{0}; + uint8_t length{1}; + ENDIAN endian{ENDIAN_BIG}; + int8_t adjust{0}; +}; + class UARTExComponent : public uart::UARTDevice, public Component { public: @@ -84,6 +92,7 @@ class UARTExComponent : public uart::UARTDevice, public Component void set_tx_retry_cnt(uint16_t tx_retry_cnt); void set_tx_command_queue_size(uint16_t size); void set_rx_length(uint16_t rx_length); + void set_rx_data_length(uint8_t offset, uint8_t length, ENDIAN endian, int8_t adjust); void set_rx_timeout(uint16_t timeout); void set_tx_ctrl_pin(InternalGPIOPin *pin); void enqueue_tx_data(const tx_data_t data, bool low_priority = false); @@ -119,6 +128,7 @@ class UARTExComponent : public uart::UARTDevice, public Component uint16_t conf_tx_retry_cnt_{3}; uint16_t conf_tx_command_queue_size_{10}; uint16_t conf_rx_length_{0}; + optional conf_rx_data_length_{}; optional rx_header_{}; optional> rx_footer_{}; optional> tx_header_{}; diff --git a/components/uartex/version.h b/components/uartex/version.h index 4a0ef0b..d7382e2 100644 --- a/components/uartex/version.h +++ b/components/uartex/version.h @@ -1,2 +1,2 @@ #pragma once -#define UARTEX_VERSION "6.2.1-260121" +#define UARTEX_VERSION "6.3.0-260123"