[zigbee_proxy] Auto-detect USB UART channel from uart_id, drop usb_uart_id

The usb_uart_id key was redundant: uart_id already points at the channel.
A new usb_uart.is_usb_uart_channel() helper checks the config tree (use_id
resolution does not narrow the ID type), and zigbee_proxy uses it to enable
the RX callback fast path and USB timeout defaults automatically.
This commit is contained in:
kbx81
2026-08-03 16:55:28 -05:00
parent ca42862742
commit 5846977cf6
4 changed files with 59 additions and 16 deletions
+16 -1
View File
@@ -16,7 +16,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER,
CONF_ID,
)
from esphome.core import CORE
from esphome.core import CORE, ID
from esphome.cpp_types import Component
AUTO_LOAD = ["uart", "usb_host", "bytebuffer"]
@@ -26,6 +26,21 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
def is_usb_uart_channel(uart_id: ID) -> bool:
"""Return True if the given ID refers to a channel of a configured usb_uart device.
Lets UART device components detect that their uart_id points at a USB UART
channel (use_id resolution does not narrow the ID's type) so they can enable
USB-specific features such as the RX callback.
"""
return any(
channel[CONF_ID] == uart_id
for device in CORE.config.get("usb_uart") or []
for channel in device[CONF_CHANNELS]
)
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
UART_PARITY_OPTIONS = {
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
+19 -12
View File
@@ -1,13 +1,18 @@
import esphome.codegen as cg
from esphome.components import uart, usb_uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ID,
CONF_POWER_SAVE_MODE,
CONF_UART_ID,
CONF_WIFI,
)
import esphome.final_validate as fv
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["api", "uart"]
CONF_BUFFER_SIZE = "buffer_size"
CONF_INITIAL_TIMEOUT = "initial_timeout"
CONF_MIN_TIMEOUT = "min_timeout"
CONF_MAX_TIMEOUT = "max_timeout"
@@ -47,10 +52,10 @@ CONFIG_SCHEMA = cv.All(
esp8266=512,
default=1024,
),
# When usb_uart_id is present the component registers an RX callback
# for zero-wakeup-cycle data delivery and selects USB-optimized ACK
# timeout defaults. Explicit timeout keys always win.
cv.Optional(CONF_USB_UART_ID): cv.use_id(usb_uart.USBUartChannel),
cv.Optional(CONF_USB_UART_ID): cv.invalid(
"'usb_uart_id' has been removed; a USB UART channel is now "
"detected automatically from 'uart_id'"
),
cv.Optional(CONF_INITIAL_TIMEOUT): cv.int_range(min=10, max=10000),
cv.Optional(CONF_MIN_TIMEOUT): cv.int_range(min=10, max=5000),
cv.Optional(CONF_MAX_TIMEOUT): cv.int_range(min=50, max=10000),
@@ -74,14 +79,16 @@ async def to_code(config):
if CONF_BUFFER_SIZE in config:
cg.add_define("ZIGBEE_PROXY_BUFFER_SIZE", config[CONF_BUFFER_SIZE])
# Select timeout defaults based on UART transport type.
# USB CDC ACM with the RX callback has ~3-5 ms round-trip latency; hardware
# UART is similar (~2-5 ms). Different defaults are kept so that future
# non-callback USB paths still get conservative starting values.
is_usb = CONF_USB_UART_ID in config
# A uart_id pointing at a USB UART channel is detected automatically: the
# component then registers an RX callback for zero-wakeup-cycle data delivery
# and selects USB-optimized ACK timeout defaults. Explicit timeout keys always
# win. USB CDC ACM with the RX callback has ~3-5 ms round-trip latency;
# hardware UART is similar (~2-5 ms). Different defaults are kept so that
# future non-callback USB paths still get conservative starting values.
is_usb = usb_uart.is_usb_uart_channel(config[CONF_UART_ID])
if is_usb:
cg.add_define("USE_ZIGBEE_PROXY_USB_UART")
usb_ch = await cg.get_variable(config[CONF_USB_UART_ID])
usb_ch = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.set_usb_uart_channel(usb_ch))
initial_timeout = config.get(
@@ -14,8 +14,8 @@
// Forward-declare USBUartChannel so the set_usb_uart_channel() setter can be declared
// without pulling usb_uart.h into every translation unit that includes this header.
// USE_ZIGBEE_PROXY_USB_UART is defined by the Python to_code() only when usb_uart_id
// is present in the YAML, ensuring the header is actually in the build path.
// USE_ZIGBEE_PROXY_USB_UART is defined by the Python to_code() only when uart_id
// resolves to a USB UART channel, ensuring the header is actually in the build path.
#ifdef USE_ZIGBEE_PROXY_USB_UART
namespace esphome::usb_uart {
class USBUartChannel;
@@ -100,7 +100,7 @@ class ZigbeeProxy : public uart::UARTDevice, public Component {
void set_max_timeout(uint32_t timeout_ms) { this->timeout_config_.max_timeout_ms = timeout_ms; }
#ifdef USE_ZIGBEE_PROXY_USB_UART
/// Called from generated code when usb_uart_id is configured.
/// Called from generated code when uart_id resolves to a USB UART channel.
/// Registers an RX callback on the channel so incoming bytes are processed
/// immediately in the same USBUartComponent::loop() iteration they arrive,
/// without waiting for the next ZigbeeProxy::loop() call.
@@ -0,0 +1,21 @@
wifi:
ssid: test
password: password
power_save_mode: none
api:
usb_host:
usb_uart:
- type: CDC_ACM
vid: 0x303A
pid: 0x831A
channels:
- id: zigbee_usb_channel
baud_rate: 460800
# uart_id points at a USB UART channel; the component must detect this
# automatically and enable the RX callback fast path (USE_ZIGBEE_PROXY_USB_UART)
zigbee_proxy:
uart_id: zigbee_usb_channel