Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions components/uartex/README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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) |
Expand All @@ -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

Expand Down
19 changes: 17 additions & 2 deletions components/uartex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions components/uartex/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
69 changes: 65 additions & 4 deletions components/uartex/parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ bool Parser::has_footer()
void Parser::clear()
{
buffer_.clear();
dynamic_total_len_ = 0;
}

bool Parser::parse_header()
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
9 changes: 9 additions & 0 deletions components/uartex/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,22 @@ 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<unsigned char> header_;
std::vector<unsigned char> header_mask_;
std::vector<unsigned char> footer_;
std::vector<unsigned char> buffer_;
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};
};

16 changes: 16 additions & 0 deletions components/uartex/uartex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions components/uartex/uartex.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ struct header_t
std::vector<uint8_t> 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:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<rx_data_length_t> conf_rx_data_length_{};
optional<header_t> rx_header_{};
optional<std::vector<uint8_t>> rx_footer_{};
optional<std::vector<uint8_t>> tx_header_{};
Expand Down
2 changes: 1 addition & 1 deletion components/uartex/version.h
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#pragma once
#define UARTEX_VERSION "6.2.1-260121"
#define UARTEX_VERSION "6.3.0-260123"