[uart] Add wake_loop_on_rx flag for low latency processing (#12172)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Keith Burzinski
2025-12-01 00:33:23 -06:00
committed by GitHub
parent 4335fcdb72
commit 161a18b326
4 changed files with 116 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass
from logging import getLogger
import math
import re
@@ -32,13 +33,15 @@ from esphome.const import (
PLATFORM_HOST,
PlatformFramework,
)
from esphome.core import CORE, ID
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.yaml_util import make_data_base
_LOGGER = getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
DOMAIN = "uart"
uart_ns = cg.esphome_ns.namespace("uart")
UARTComponent = uart_ns.class_("UARTComponent")
@@ -52,6 +55,7 @@ LibreTinyUARTComponent = uart_ns.class_(
)
HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Component)
NATIVE_UART_CLASSES = (
str(IDFUARTComponent),
str(ESP8266UartComponent),
@@ -100,6 +104,30 @@ MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
@dataclass
class UARTData:
"""State data for UART component configuration generation."""
wake_loop_on_rx: bool = False
def _get_data() -> UARTData:
"""Get UART component data from CORE.data."""
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = UARTData()
return CORE.data[DOMAIN]
def request_wake_loop_on_rx() -> None:
"""Request that the UART wake the main loop when data is received.
Components that need low-latency notification of incoming UART data
should call this function during their code generation.
This enables the RX event task which wakes the main loop when data arrives.
"""
_get_data().wake_loop_on_rx = True
def validate_raw_data(value):
if isinstance(value, str):
return value.encode("utf-8")
@@ -335,6 +363,8 @@ async def to_code(config):
if CONF_DEBUG in config:
await debug_to_code(config[CONF_DEBUG], var)
CORE.add_job(final_step)
# A schema to use for all UART devices, all UART integrations must extend this!
UART_DEVICE_SCHEMA = cv.Schema(
@@ -472,6 +502,13 @@ async def uart_write_to_code(config, action_id, template_arg, args):
return var
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional UART features."""
if _get_data().wake_loop_on_rx:
cg.add_define("USE_UART_WAKE_LOOP_ON_RX")
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"uart_component_esp_idf.cpp": {

View File

@@ -112,6 +112,12 @@ void IDFUARTComponent::load_settings(bool dump_config) {
esp_err_t err;
if (uart_is_driver_installed(this->uart_num_)) {
#ifdef USE_UART_WAKE_LOOP_ON_RX
if (this->rx_event_task_handle_ != nullptr) {
vTaskDelete(this->rx_event_task_handle_);
this->rx_event_task_handle_ = nullptr;
}
#endif
err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
@@ -204,6 +210,11 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Start the RX event task to enable low-latency data notifications
this->start_rx_event_task_();
#endif // USE_UART_WAKE_LOOP_ON_RX
if (dump_config) {
ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_);
this->dump_config();
@@ -226,7 +237,11 @@ void IDFUARTComponent::dump_config() {
" Baud Rate: %" PRIu32 " baud\n"
" Data Bits: %u\n"
" Parity: %s\n"
" Stop bits: %u",
" Stop bits: %u"
#ifdef USE_UART_WAKE_LOOP_ON_RX
"\n Wake on data RX: ENABLED"
#endif
,
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_);
this->check_logger_conflict();
}
@@ -337,6 +352,59 @@ void IDFUARTComponent::flush() {
void IDFUARTComponent::check_logger_conflict() {}
#ifdef USE_UART_WAKE_LOOP_ON_RX
void IDFUARTComponent::start_rx_event_task_() {
// Create FreeRTOS task to monitor UART events
BaseType_t result = xTaskCreate(rx_event_task_func, // Task function
"uart_rx_evt", // Task name (max 16 chars)
2240, // Stack size in bytes (~2.2KB); increase if needed for logging
this, // Task parameter (this pointer)
tskIDLE_PRIORITY + 1, // Priority (low, just above idle)
&this->rx_event_task_handle_ // Task handle
);
if (result != pdPASS) {
ESP_LOGE(TAG, "Failed to create RX event task");
return;
}
ESP_LOGV(TAG, "RX event task started");
}
void IDFUARTComponent::rx_event_task_func(void *param) {
auto *self = static_cast<IDFUARTComponent *>(param);
uart_event_t event;
ESP_LOGV(TAG, "RX event task running");
// Run forever - task lifecycle matches component lifecycle
while (true) {
// Wait for UART events (blocks efficiently)
if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) {
switch (event.type) {
case UART_DATA:
// Data available in UART RX buffer - wake the main loop
ESP_LOGVV(TAG, "Data event: %d bytes", event.size);
App.wake_loop_threadsafe();
break;
case UART_FIFO_OVF:
case UART_BUFFER_FULL:
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
uart_flush_input(self->uart_num_);
App.wake_loop_threadsafe();
break;
default:
// Ignore other event types
ESP_LOGVV(TAG, "Event type: %d", event.type);
break;
}
}
}
}
#endif // USE_UART_WAKE_LOOP_ON_RX
} // namespace uart
} // namespace esphome

View File

@@ -53,6 +53,14 @@ class IDFUARTComponent : public UARTComponent, public Component {
bool has_peek_{false};
uint8_t peek_byte_;
#ifdef USE_UART_WAKE_LOOP_ON_RX
// RX notification support
void start_rx_event_task_();
static void rx_event_task_func(void *param);
TaskHandle_t rx_event_task_handle_{nullptr};
#endif // USE_UART_WAKE_LOOP_ON_RX
};
} // namespace uart

View File

@@ -107,6 +107,7 @@
#define USE_TIME
#define USE_TOUCHSCREEN
#define USE_UART_DEBUGGER
#define USE_UART_WAKE_LOOP_ON_RX
#define USE_UPDATE
#define USE_VALVE
#define USE_ZWAVE_PROXY