From e7a2b395fd0c822ec0be6c5ddd8cd5a4d4db5c70 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 May 2025 14:15:46 +1000 Subject: [PATCH] [uart] Add packet_transport platform (#8214) Co-authored-by: Faidon Liambotis Co-authored-by: clydeps Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../uart/packet_transport/__init__.py | 20 +++++ .../uart/packet_transport/uart_transport.cpp | 88 +++++++++++++++++++ .../uart/packet_transport/uart_transport.h | 41 +++++++++ tests/components/uart/test.esp32-idf.yaml | 3 + 5 files changed, 153 insertions(+) create mode 100644 esphome/components/uart/packet_transport/__init__.py create mode 100644 esphome/components/uart/packet_transport/uart_transport.cpp create mode 100644 esphome/components/uart/packet_transport/uart_transport.h diff --git a/CODEOWNERS b/CODEOWNERS index 46e0e6c579..d6381f9799 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -468,6 +468,7 @@ esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core esphome/components/uart/button/* @ssieb +esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli diff --git a/esphome/components/uart/packet_transport/__init__.py b/esphome/components/uart/packet_transport/__init__.py new file mode 100644 index 0000000000..58c6296e2f --- /dev/null +++ b/esphome/components/uart/packet_transport/__init__.py @@ -0,0 +1,20 @@ +from esphome.components.packet_transport import ( + PacketTransport, + new_packet_transport, + transport_schema, +) +from esphome.cpp_types import PollingComponent + +from .. import UART_DEVICE_SCHEMA, register_uart_device, uart_ns + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["uart"] + +UARTTransport = uart_ns.class_("UARTTransport", PacketTransport, PollingComponent) + +CONFIG_SCHEMA = transport_schema(UARTTransport).extend(UART_DEVICE_SCHEMA) + + +async def to_code(config): + var, _ = await new_packet_transport(config) + await register_uart_device(var, config) diff --git a/esphome/components/uart/packet_transport/uart_transport.cpp b/esphome/components/uart/packet_transport/uart_transport.cpp new file mode 100644 index 0000000000..aa11ae0772 --- /dev/null +++ b/esphome/components/uart/packet_transport/uart_transport.cpp @@ -0,0 +1,88 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "uart_transport.h" + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart_transport"; + +void UARTTransport::loop() { + PacketTransport::loop(); + + while (this->parent_->available()) { + uint8_t byte; + if (!this->parent_->read_byte(&byte)) { + ESP_LOGW(TAG, "Failed to read byte from UART"); + return; + } + if (byte == FLAG_BYTE) { + if (this->rx_started_ && this->receive_buffer_.size() > 6) { + auto len = this->receive_buffer_.size(); + auto crc = crc16(this->receive_buffer_.data(), len - 2); + if (crc != (this->receive_buffer_[len - 2] | (this->receive_buffer_[len - 1] << 8))) { + ESP_LOGD(TAG, "CRC mismatch, discarding packet"); + this->rx_started_ = false; + this->receive_buffer_.clear(); + continue; + } + this->receive_buffer_.resize(len - 2); + this->process_(this->receive_buffer_); + this->rx_started_ = false; + } else { + this->rx_started_ = true; + } + this->receive_buffer_.clear(); + this->rx_control_ = false; + continue; + } + if (!this->rx_started_) + continue; + if (byte == CONTROL_BYTE) { + this->rx_control_ = true; + continue; + } + if (this->rx_control_) { + byte ^= 0x20; + this->rx_control_ = false; + } + if (this->receive_buffer_.size() == MAX_PACKET_SIZE) { + ESP_LOGD(TAG, "Packet too large, discarding"); + this->rx_started_ = false; + this->receive_buffer_.clear(); + continue; + } + this->receive_buffer_.push_back(byte); + } +} + +void UARTTransport::update() { + this->updated_ = true; + this->resend_data_ = true; + PacketTransport::update(); +} + +/** + * Write a byte to the UART bus. If the byte is a flag or control byte, it will be escaped. + * @param byte The byte to write. + */ +void UARTTransport::write_byte_(uint8_t byte) const { + if (byte == FLAG_BYTE || byte == CONTROL_BYTE) { + this->parent_->write_byte(CONTROL_BYTE); + byte ^= 0x20; + } + this->parent_->write_byte(byte); +} + +void UARTTransport::send_packet(std::vector &buf) const { + this->parent_->write_byte(FLAG_BYTE); + for (uint8_t byte : buf) { + this->write_byte_(byte); + } + auto crc = crc16(buf.data(), buf.size()); + this->write_byte_(crc & 0xFF); + this->write_byte_(crc >> 8); + this->parent_->write_byte(FLAG_BYTE); +} +} // namespace uart +} // namespace esphome diff --git a/esphome/components/uart/packet_transport/uart_transport.h b/esphome/components/uart/packet_transport/uart_transport.h new file mode 100644 index 0000000000..db32859452 --- /dev/null +++ b/esphome/components/uart/packet_transport/uart_transport.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/packet_transport/packet_transport.h" +#include +#include "../uart.h" + +namespace esphome { +namespace uart { + +/** + * A transport protocol for sending and receiving packets over a UART connection. + * The protocol is based on Asynchronous HDLC framing. (https://en.wikipedia.org/wiki/High-Level_Data_Link_Control) + * There are two special bytes: FLAG_BYTE and CONTROL_BYTE. + * A 16-bit CRC is appended to the packet, then + * the protocol wraps the resulting data between FLAG_BYTEs. + * Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte + * XORed with 0x20. + */ +static const uint16_t MAX_PACKET_SIZE = 508; +static const uint8_t FLAG_BYTE = 0x7E; +static const uint8_t CONTROL_BYTE = 0x7D; + +class UARTTransport : public packet_transport::PacketTransport, public UARTDevice { + public: + void loop() override; + void update() override; + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + protected: + void write_byte_(uint8_t byte) const; + void send_packet(std::vector &buf) const override; + bool should_send() override { return true; }; + size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } + std::vector receive_buffer_{}; + bool rx_started_{}; + bool rx_control_{}; +}; + +} // namespace uart +} // namespace esphome diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index bef5b460ab..5a0ed7eba7 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -13,3 +13,6 @@ uart: rx_buffer_size: 512 parity: EVEN stop_bits: 2 + +packet_transport: + - platform: uart