mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b501e48ebd | ||
|
|
0572454bed | ||
|
|
8e50491ef9 | ||
|
|
e3df568cd7 | ||
|
|
1a80e3faf4 | ||
|
|
0a46c81770 | ||
|
|
633cf73a9d | ||
|
|
565ee2a14b | ||
|
|
be0f3a32f9 | ||
|
|
c7967c2601 |
@@ -279,7 +279,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.All(
|
||||
validate_raw_data, cv.Length(max=255)
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
@@ -350,12 +352,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, {})
|
||||
|
||||
@@ -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,9 +11,10 @@ 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]]]:
|
||||
def validate_event_types(value) -> list[tuple[str, list[int]]]:
|
||||
if not isinstance(value, list):
|
||||
raise cv.Invalid("Event type must be a list of key-value mappings.")
|
||||
|
||||
@@ -44,7 +44,7 @@ def validate_event_types(value) -> list[tuple[str, str | list[int]]]:
|
||||
try:
|
||||
# Try to validate as string
|
||||
match_data_str = cv.string_strict(match_data)
|
||||
processed.append((event_name, match_data_str))
|
||||
processed.append((event_name, list(match_data_str.encode("utf-8"))))
|
||||
continue
|
||||
except cv.Invalid:
|
||||
pass # Not string either
|
||||
@@ -56,6 +56,10 @@ 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 len(processed) > 0xFFFF:
|
||||
raise cv.Invalid("event_types can have at most 65535 entries.")
|
||||
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 +81,23 @@ 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 = [int(b) for b in match_data]
|
||||
data = (
|
||||
cg.shared_progmem_array("uart_event_match", cg.uint8, match_data)
|
||||
if match_data
|
||||
else cg.nullptr
|
||||
)
|
||||
match_data_var = cg.static_const_array(
|
||||
match_data_var_id, cg.ArrayInitializer(*match_data)
|
||||
matchers.append(
|
||||
cg.StructInitializer(
|
||||
UARTEventMatcher,
|
||||
("event_name", event_name),
|
||||
("data", data),
|
||||
("data_len", len(match_data)),
|
||||
)
|
||||
)
|
||||
cg.add(var.add_event_matcher(event_name, match_data_var, 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))
|
||||
|
||||
@@ -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 is in flash; ESP8266 only allows word loads there, so read it with 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,41 @@
|
||||
"""uart debug delimiters and uart event patterns live in shared PROGMEM tables."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.uart.event import validate_event_types
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_event_string_patterns_are_utf8_bytes() -> None:
|
||||
assert validate_event_types([{"euro": "€"}]) == [("euro", [0xE2, 0x82, 0xAC])]
|
||||
|
||||
|
||||
def test_event_types_count_is_capped() -> None:
|
||||
with pytest.raises(cv.Invalid, match="at most 65535 entries"):
|
||||
validate_event_types([{f"e{i}": "x"} for i in range(0x10000)])
|
||||
Reference in New Issue
Block a user