[uart] Keep debug delimiters and event match data in shared PROGMEM tables

This commit is contained in:
J. Nick Koston
2026-10-03 14:56:07 -05:00
parent 565ee2a14b
commit 633cf73a9d
8 changed files with 146 additions and 47 deletions
+8 -7
View File
@@ -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, {})
+26 -11
View File
@@ -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))
+9 -10
View File
@@ -1,6 +1,6 @@
#include "uart_event.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <algorithm>
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;
+28 -9
View File
@@ -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 <type_traits>
#include <vector>
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<decltype(UARTEventMatcher::event_name), const char *>,
"UARTEventMatcher fields must stay word sized");
static_assert(std::is_same_v<decltype(UARTEventMatcher::data), const uint8_t *>,
"UARTEventMatcher fields must stay word sized");
static_assert(std::is_same_v<decltype(UARTEventMatcher::data_len), size_t>,
"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<EventMatcher> matchers_;
const UARTEventMatcher *matchers_{nullptr};
std::vector<uint8_t> buffer_;
size_t max_matcher_len_ = 0;
uint16_t matcher_count_{0};
uint16_t max_matcher_len_{0};
};
} // namespace esphome::uart
+4 -3
View File
@@ -3,6 +3,7 @@
#include <vector>
#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;
}
+10 -7
View File
@@ -36,11 +36,13 @@ class UARTDebugger final : public Component, public Trigger<UARTDirection, std::
/// this timeout, logging will be triggered.
void set_after_timeout(uint32_t timeout) { this->after_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<UARTDirection, std::
size_t after_bytes_;
uint32_t after_timeout_;
uint32_t last_time_{};
std::vector<uint8_t> 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_{};
@@ -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]
@@ -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