diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 598e3df168a..10e495203fe 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -274,7 +274,9 @@ DEBUG_SCHEMA = cv.Schema( cv.Optional( CONF_TIMEOUT, default=AFTER_DEFAULTS[CONF_TIMEOUT] ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data), + cv.Optional(CONF_DELIMITER): cv.templatable( + cv.All(validate_raw_data, cv.Length(max=255)) + ), } ), cv.Optional( @@ -345,12 +347,11 @@ async def debug_to_code(config, parent): after = config[CONF_AFTER] cg.add(trigger.set_after_bytes(after[CONF_BYTES])) cg.add(trigger.set_after_timeout(after[CONF_TIMEOUT])) - if CONF_DELIMITER in after: - data = after[CONF_DELIMITER] - if isinstance(data, bytes): - data = list(data) - for byte in after[CONF_DELIMITER]: - cg.add(trigger.add_delimiter_byte(byte)) + if delimiter := after.get(CONF_DELIMITER): + table = cg.shared_progmem_array( + "uart_debug_delimiter", cg.uint8, [int(b) for b in delimiter] + ) + cg.add(trigger.set_after_delimiter(table, len(delimiter))) if config[CONF_DUMMY_RECEIVER]: dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) await cg.register_component(dummy, {}) diff --git a/esphome/components/uart/event/__init__.py b/esphome/components/uart/event/__init__.py index 64af318a116..4becd8ea0fc 100644 --- a/esphome/components/uart/event/__init__.py +++ b/esphome/components/uart/event/__init__.py @@ -1,8 +1,7 @@ import esphome.codegen as cg from esphome.components import event, uart import esphome.config_validation as cv -from esphome.const import CONF_EVENT_TYPES, CONF_ID -from esphome.core import ID +from esphome.const import CONF_EVENT_TYPES from esphome.types import ConfigType from .. import uart_ns @@ -12,6 +11,7 @@ CODEOWNERS = ["@eoasmxd"] DEPENDENCIES = ["uart"] UARTEvent = uart_ns.class_("UARTEvent", event.Event, uart.UARTDevice, cg.Component) +UARTEventMatcher = uart_ns.struct("UARTEventMatcher") def validate_event_types(value) -> list[tuple[str, str | list[int]]]: @@ -56,6 +56,8 @@ def validate_event_types(value) -> list[tuple[str, str | list[int]]]: if not processed: raise cv.Invalid("event_types must contain at least one event mapping.") + if any(len(data) > 0xFFFF for _, data in processed): + raise cv.Invalid("Event match data can be at most 65535 bytes.") return processed @@ -77,14 +79,27 @@ async def to_code(config: ConfigType) -> None: var = await event.new_event(config, event_types=event_names) await cg.register_component(var, config) await uart.register_uart_device(var, config) - for i, (event_name, match_data) in enumerate(config[CONF_EVENT_TYPES]): - if isinstance(match_data, str): - match_data = [ord(c) for c in match_data] - - match_data_var_id = ID( - f"match_data_{config[CONF_ID]}_{i}", is_declaration=True, type=cg.uint8 + matchers = [] + max_len = 0 + for event_name, match_data in config[CONF_EVENT_TYPES]: + match_data = ( + [ord(c) for c in match_data] + if isinstance(match_data, str) + else [int(b) for b in match_data] ) - match_data_var = cg.static_const_array( - match_data_var_id, cg.ArrayInitializer(*match_data) + data = ( + cg.shared_progmem_array("uart_event_match", cg.uint8, match_data) + if match_data + else cg.nullptr ) - cg.add(var.add_event_matcher(event_name, match_data_var, len(match_data))) + matchers.append( + cg.StructInitializer( + UARTEventMatcher, + ("event_name", event_name), + ("data", data), + ("data_len", len(match_data)), + ) + ) + max_len = max(max_len, len(match_data)) + table = cg.shared_progmem_array("uart_event_matchers", UARTEventMatcher, matchers) + cg.add(var.set_matchers(table, len(matchers), max_len)) diff --git a/esphome/components/uart/event/uart_event.cpp b/esphome/components/uart/event/uart_event.cpp index 02c5f2e631d..9de6fb94827 100644 --- a/esphome/components/uart/event/uart_event.cpp +++ b/esphome/components/uart/event/uart_event.cpp @@ -1,6 +1,6 @@ #include "uart_event.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" -#include namespace esphome::uart { @@ -12,13 +12,6 @@ void UARTEvent::dump_config() { LOG_EVENT("", "UART Event", this); } void UARTEvent::loop() { this->read_data_(); } -void UARTEvent::add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len) { - this->matchers_.push_back({event_name, match_data, match_data_len}); - if (match_data_len > this->max_matcher_len_) { - this->max_matcher_len_ = match_data_len; - } -} - void UARTEvent::read_data_() { while (this->available()) { uint8_t data; @@ -26,12 +19,18 @@ void UARTEvent::read_data_() { this->buffer_.push_back(data); bool match_found = false; - for (const auto &matcher : this->matchers_) { + for (uint16_t i = 0; i < this->matcher_count_; i++) { + const UARTEventMatcher &matcher = this->matchers_[i]; if (this->buffer_.size() < matcher.data_len) { continue; } - if (std::equal(matcher.data, matcher.data + matcher.data_len, this->buffer_.end() - matcher.data_len)) { + const uint8_t *tail = this->buffer_.data() + this->buffer_.size() - matcher.data_len; + size_t pos = 0; + // The pattern may be in flash, which ESP8266 can only read a byte at a time through progmem_read_byte + while (pos < matcher.data_len && progmem_read_byte(matcher.data + pos) == tail[pos]) + pos++; + if (pos == matcher.data_len) { this->trigger(matcher.event_name); this->buffer_.clear(); match_found = true; diff --git a/esphome/components/uart/event/uart_event.h b/esphome/components/uart/event/uart_event.h index 3960ffd5bb8..a676f600418 100644 --- a/esphome/components/uart/event/uart_event.h +++ b/esphome/components/uart/event/uart_event.h @@ -1,31 +1,50 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/components/event/event.h" #include "esphome/components/uart/uart.h" +#include #include namespace esphome::uart { +/// An event type and the byte pattern that fires it; `data` may point to PROGMEM. +struct UARTEventMatcher { + const char *event_name; + const uint8_t *data; + size_t data_len; +}; +// Codegen keeps the matchers in a flash table read with plain loads, which ESP8266 only allows for whole words. +static_assert(std::is_same_v, + "UARTEventMatcher fields must stay word sized"); +static_assert(std::is_same_v, + "UARTEventMatcher fields must stay word sized"); +static_assert(std::is_same_v, + "UARTEventMatcher fields must stay word sized"); +#ifdef USE_ESP8266 +static_assert(sizeof(UARTEventMatcher) == 3 * sizeof(uint32_t), "UARTEventMatcher is read from flash with word loads"); +#endif + class UARTEvent final : public event::Event, public UARTDevice, public Component { public: void setup() override; void loop() override; void dump_config() override; - void add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len); + /// `matchers` is a codegen PROGMEM table that must outlive the component. + void set_matchers(const UARTEventMatcher *matchers, uint16_t count, uint16_t max_len) { + this->matchers_ = matchers; + this->matcher_count_ = count; + this->max_matcher_len_ = max_len; + } protected: - struct EventMatcher { - const char *event_name; - const uint8_t *data; - size_t data_len; - }; - void read_data_(); - std::vector matchers_; + const UARTEventMatcher *matchers_{nullptr}; std::vector buffer_; - size_t max_matcher_len_ = 0; + uint16_t matcher_count_{0}; + uint16_t max_matcher_len_{0}; }; } // namespace esphome::uart diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index e0be599d8db..c102c663df5 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -3,6 +3,7 @@ #include #include "uart_debugger.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -44,15 +45,15 @@ void UARTDebugger::store_byte_(UARTDirection direction, uint8_t byte) { } void UARTDebugger::trigger_after_delimiter_(uint8_t byte) { - if (this->after_delimiter_.empty() || !this->has_buffered_bytes_()) { + if (this->after_delimiter_len_ == 0 || !this->has_buffered_bytes_()) { return; } - if (this->after_delimiter_[this->after_delimiter_pos_] != byte) { + if (progmem_read_byte(this->after_delimiter_ + this->after_delimiter_pos_) != byte) { this->after_delimiter_pos_ = 0; return; } this->after_delimiter_pos_++; - if (this->after_delimiter_pos_ == this->after_delimiter_.size()) { + if (this->after_delimiter_pos_ == this->after_delimiter_len_) { this->fire_trigger_(); this->after_delimiter_pos_ = 0; } diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index b69dcf0676c..7a96779f556 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -36,11 +36,13 @@ class UARTDebugger final : public Component, public Triggerafter_timeout_ = timeout; } - /// Add a delimiter byte. This can be called multiple times to setup a - /// multi-byte delimiter (a typical example would be '\r\n'). - /// When the constructed byte sequence is found in the data stream, - /// logging will be triggered. - void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); } + /// Set a delimiter byte sequence (a typical example would be '\r\n'). + /// When the sequence is found in the data stream, logging will be triggered. + /// `delimiter` is a codegen PROGMEM table that must outlive the debugger. + void set_after_delimiter(const uint8_t *delimiter, uint8_t len) { + this->after_delimiter_ = delimiter; + this->after_delimiter_len_ = len; + } void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); } @@ -51,8 +53,9 @@ class UARTDebugger final : public Component, public Trigger after_delimiter_{}; - size_t after_delimiter_pos_{}; + const uint8_t *after_delimiter_{nullptr}; + uint8_t after_delimiter_len_{0}; + uint8_t after_delimiter_pos_{0}; bool is_triggering_{false}; StringRef debug_prefix_{}; diff --git a/tests/component_tests/uart/config/debug_event_tables.yaml b/tests/component_tests/uart/config/debug_event_tables.yaml new file mode 100644 index 00000000000..080cb2fd8be --- /dev/null +++ b/tests/component_tests/uart/config/debug_event_tables.yaml @@ -0,0 +1,34 @@ +esphome: + name: test + +esp8266: + board: esp01_1m + +uart: + - id: uart_a + tx_pin: GPIO1 + rx_pin: GPIO3 + baud_rate: 9600 + debug: + after: + delimiter: "\r\n" + - id: uart_b + tx_pin: GPIO2 + baud_rate: 9600 + debug: + after: + delimiter: [0x0D, 0x0A] + +event: + - platform: uart + uart_id: uart_a + name: Event A + event_types: + - "start": "*A#" + - "stop": [0x2A, 0x42, 0x23] + - platform: uart + uart_id: uart_a + name: Event B + event_types: + - "start": "*A#" + - "stop": [0x2A, 0x42, 0x23] diff --git a/tests/component_tests/uart/test_debug_event_tables.py b/tests/component_tests/uart/test_debug_event_tables.py new file mode 100644 index 00000000000..14890b5a298 --- /dev/null +++ b/tests/component_tests/uart/test_debug_event_tables.py @@ -0,0 +1,27 @@ +"""uart debug delimiters and uart event patterns live in shared PROGMEM tables.""" + +from collections.abc import Callable +from pathlib import Path +import re + + +def test_debug_delimiter_and_event_matchers_use_shared_tables( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("debug_event_tables.yaml")) + + # "\r\n" and [0x0D, 0x0A] are the same bytes, so both debuggers share one table + delimiters = re.findall(r"->set_after_delimiter\((\w+), 2\);", main_cpp) + assert len(delimiters) == 2 + assert delimiters[0] == delimiters[1] + assert "add_delimiter_byte" not in main_cpp + + # Both events have the same patterns, so they share the byte tables and the matcher table + patterns = re.findall(r"static constexpr uint8_t uart_event_match\w*\[\]", main_cpp) + assert len(patterns) == 2 + calls = re.findall(r"->set_matchers\((\w+), 2, 3\);", main_cpp) + assert len(calls) == 2 + assert calls[0] == calls[1] + assert main_cpp.count("static constexpr uart::UARTEventMatcher") == 1 + assert "add_event_matcher" not in main_cpp