[uart] Share uart_tcp's write pacing and uart_id matcher (#20464)

This commit is contained in:
Bascht74
2026-10-09 15:11:48 -10:00
committed by GitHub
parent e5f5ca07c4
commit 4e4056d790
8 changed files with 131 additions and 40 deletions
@@ -36,19 +36,6 @@ CONFIG_SCHEMA = cv.All(
)
def _subtree_references_uart(node: object, uart_id: str) -> bool:
"""Return True if any dict in the subtree has a uart_id entry naming this bus."""
if isinstance(node, dict):
return any(
(key == CONF_UART_ID and str(value) == uart_id)
or _subtree_references_uart(value, uart_id)
for key, value in node.items()
)
if isinstance(node, list):
return any(_subtree_references_uart(item, uart_id) for item in node)
return False
def _reject_debug(uart_conf: ConfigType) -> ConfigType:
# The worker tasks use the IDF driver directly, so the uart debugger never sees
# bridge traffic and its dummy_receiver would drain RX bytes on the main loop.
@@ -86,7 +73,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
for domain, domain_conf in full_config.items():
if domain == BRIDGE_DOMAIN:
continue
if _subtree_references_uart(domain_conf, owned_id):
if uart.subtree_references_uart(domain_conf, owned_id):
raise cv.Invalid(
f"The {label} '{owned_id}' is also used by '{domain}'; a bridge "
f"requires exclusive use of its {label}.",
+19
View File
@@ -533,6 +533,25 @@ def final_validate_device_schema(
)
def subtree_references_uart(
node: object, uart_id: str, conf_key: str = CONF_UART_ID
) -> bool:
"""Return True if any dict in the subtree has a conf_key entry naming this bus.
For the final validation of a component that needs a UART for itself. Bare
`id:` references (a uart.write action) and lambdas are not found.
"""
if isinstance(node, dict):
return any(
(key == conf_key and str(value) == uart_id)
or subtree_references_uart(value, uart_id, conf_key)
for key, value in node.items()
)
if isinstance(node, list):
return any(subtree_references_uart(item, uart_id, conf_key) for item in node)
return False
async def register_uart_device(var, config):
"""Register a UART device, setting up all the internal values.
@@ -1,9 +1,15 @@
#include "uart_component.h"
#include "esphome/core/application.h"
#include <algorithm>
namespace esphome::uart {
ESPHOME_LOG_TAG(TAG, "uart");
// Keeps the pacing product in 32 bits up to about 10 Mbaud.
static constexpr uint32_t MAX_PACE_SPAN_MS = 4000;
bool UARTComponent::check_read_timeout_(size_t len) {
if (this->available() >= len)
return true;
@@ -19,6 +25,19 @@ bool UARTComponent::check_read_timeout_(size_t len) {
return true;
}
size_t UARTComponent::paced_write_room(uint32_t last_write_ms) {
size_t room = this->available_for_write();
if (room != SIZE_MAX) {
return room;
}
// A pass woken early writes little.
uint32_t span =
std::min({App.get_loop_component_start_time() - last_write_ms, App.get_loop_interval(), MAX_PACE_SPAN_MS});
// 10 bits per byte on the line.
uint32_t paced = this->baud_rate_ / 10 * span / 1000;
return std::max<size_t>(1, paced);
}
void UARTComponent::set_rx_full_threshold_ms(uint8_t time) {
uint8_t bytelength = this->data_bits_ + this->stop_bits_ + 1;
if (this->parity_ != UARTParityOptions::UART_CONFIG_PARITY_NONE)
+4
View File
@@ -86,6 +86,10 @@ class UARTComponent {
// Platforms that cannot tell return SIZE_MAX: write_array() takes everything and may block.
virtual size_t available_for_write() { return SIZE_MAX; }
// Bytes a write can take now. Where available_for_write() cannot tell (SIZE_MAX), paces to the line time since
// last_write_ms, a loop start time: at most one loop interval and 4 s, at least one byte.
size_t paced_write_room(uint32_t last_write_ms);
// Pure virtual method to block until all bytes have been written to the UART bus.
// @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful.
virtual UARTFlushResult flush() = 0;
+1 -13
View File
@@ -71,18 +71,6 @@ CONFIG_SCHEMA = cv.All(
)
def _subtree_references_uart(node: object, uart_id: str) -> bool:
if isinstance(node, dict):
return any(
(key == CONF_UART_ID and str(value) == uart_id)
or _subtree_references_uart(value, uart_id)
for key, value in node.items()
)
if isinstance(node, list):
return any(_subtree_references_uart(item, uart_id) for item in node)
return False
def _reject_dummy_receiver(uart_conf: ConfigType) -> ConfigType:
debug = uart_conf.get(CONF_DEBUG)
if isinstance(debug, dict) and debug.get(CONF_DUMMY_RECEIVER):
@@ -111,7 +99,7 @@ def _final_validate(config: ConfigType) -> ConfigType:
# Bare `id:` references (a uart.write action) and lambdas are not caught.
if not CORE.testing_mode:
for domain, domain_conf in full_config.items():
if domain != DOMAIN and _subtree_references_uart(domain_conf, uart_id):
if domain != DOMAIN and uart.subtree_references_uart(domain_conf, uart_id):
raise cv.Invalid(
f"The UART '{uart_id}' is also used by '{domain}'. "
"uart_tcp requires exclusive use of that UART.",
+1 -13
View File
@@ -11,9 +11,6 @@ namespace esphome::uart_tcp {
ESPHOME_LOG_TAG(TAG, "uart_tcp");
// Keeps the pacing product in 32 bits up to about 10 Mbaud.
static constexpr uint32_t MAX_PACE_SPAN_MS = 4000;
void UartTcp::setup() {
this->link_.begin(TAG);
#ifdef USE_SOCKET_TCP_LISTENER
@@ -75,16 +72,7 @@ void UartTcp::sync_link_() {
void UartTcp::read_socket_() {
// A hardware write blocks until the driver takes every byte. Leave what does
// not fit in the socket, so TCP flow control throttles the peer.
size_t room = this->parent_->available_for_write();
if (room == SIZE_MAX) {
// Capacity unknown on this platform; pace to the UART time since the last write,
// at most one loop interval and 4 s, so a pass woken early by the socket writes little.
uint32_t span = std::min(
{App.get_loop_component_start_time() - this->last_write_ms_, App.get_loop_interval(), MAX_PACE_SPAN_MS});
// 10 bits per byte on the line.
uint32_t paced = this->parent_->get_baud_rate() / 10 * span / 1000;
room = std::max<size_t>(1, paced);
}
size_t room = this->parent_->paced_write_room(this->last_write_ms_);
if (room == 0) {
this->rx_pending_ = true;
return;