Compare commits

..
Author SHA1 Message Date
J. Nick Koston ba86816d32 [web_server_base] Define the constructor out of line 2026-09-12 06:09:41 -05:00
J. Nick Koston 2db6d8a82e [web_server_base] Use a user provided default constructor for WebServerBase 2026-09-12 05:35:04 -05:00
ebb9037ea1 [bridge] New component and cdc_acm_uart platform (#11689)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-09-11 22:08:23 -05:00
Clyde StubbsandClaude Sonnet 5 eecea15f4f [core][lvgl] Migrate codegen helpers from LVGL to core code (#19105)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 23:42:08 +00:00
J. Nick Koston ff9b2a1c83 [remote_receiver] Size the RMT ring buffer from receive symbols by default (#19100) 2026-09-11 18:20:40 -05:00
esphome[bot] 9f9df85aa0 Bump aioesphomeapi from 46.4.0 to 46.4.1 (#19104) 2026-09-11 23:04:48 +00:00
puddly 2b71d5496d [const] Centralize definition of CONF_MANUFACTURER (#19098) 2026-09-11 15:45:02 -05:00
esphome[bot] 2578f17dc8 Bump bundled esphome-device-builder to 1.14.7 (#19096) 2026-09-11 14:25:41 -05:00
J. Nick Koston 37e9b2b7af [remote_base] Make protocol methods non-virtual and size receiver lists from codegen (#19084) 2026-09-11 14:12:40 -05:00
Jesse Hills 6a21ab4ea7 [esp32] Trim mbedTLS to client-only defaults and stub vasprintf on the C6 (#19088) 2026-09-11 08:30:51 +00:00
Keith BurzinskiandClaude Fable 5.1 380938177c [uart] Add apply_settings_live() for in-place ESP-IDF reconfiguration (#19087)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 03:25:52 -05:00
J. Nick Koston f66ef23256 [core] Support set_internal() during setup, log error after setup (#19069) 2026-09-11 03:20:30 -05:00
170 changed files with 2492 additions and 2649 deletions
+3
View File
@@ -629,6 +629,9 @@ file does, and it is the authority when they disagree. The most useful starting
_request_listener_slot()
cg.add(hub.register_listener(var))
```
When several instances each own a list declared at the same size (one per hub of a
`MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`;
the define is then the largest count any one key requested instead of the total.
```cpp
#ifdef MY_COMPONENT_LISTENER_COUNT
void register_listener(MyComponentListener *listener);
+3
View File
@@ -100,6 +100,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bridge/* @kbx81
esphome/components/bthome_mithermometer/* @nagyrobi
esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow
@@ -111,6 +112,8 @@ esphome/components/captive_portal/* @esphome/core
esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/cdc_acm_uart/* @kbx81
esphome/components/cdc_acm_uart/bridge/* @kbx81
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/ch423/* @dwmw2
esphome/components/chsc6x/* @kkosik20
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.7
RUN \
platformio settings set enable_telemetry No \
-44
View File
@@ -76,7 +76,6 @@ service APIConnection {
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_usb_info(SerialProxyGetUsbInfoRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
}
@@ -229,11 +228,6 @@ enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
// A serial device attached through a USB bridge. Set by the device configuration, never
// by the user; identifies ports whose USB identity can be read with
// SerialProxyGetUsbInfoRequest. Deliberately not a USB endpoint type: serial_proxy
// carries serial devices only, whatever bridge chip connects them.
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3;
}
message SerialProxyInfo {
@@ -2873,44 +2867,6 @@ message SerialProxySetModeRequest {
SerialProxyMode mode = 2;
}
// Ask for the USB identity of the device behind a USB_SERIAL port. Read-only, so no
// subscription is required -- a client typically uses this to decide which port to
// subscribe to. Answered with NOT_SUPPORTED on ports that are not USB_SERIAL.
message SerialProxyGetUsbInfoRequest {
option (id) = 153;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
}
// The USB identity of the device currently behind a port, read live from the cached
// USB descriptors. Fields are zero/empty while no device is connected.
//
// Sent in reply to SerialProxyGetUsbInfoRequest, and also unsolicited to every connected
// client whenever a USB device is attached to or removed from a USB_SERIAL port. A client
// therefore subscribes to this message before querying the current state, so the
// connect-time query and later hotplug events are handled by one path. The port's
// subscription is untouched by a hotplug: the session belongs to the client, which decides
// whether to keep it.
message SerialProxyUsbInfo {
option (id) = 154;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyStatus status = 2; // NOT_SUPPORTED when the port is not USB_SERIAL
bool connected = 3; // True when a USB device is currently attached
uint32 vendor_id = 4;
uint32 product_id = 5;
uint32 bcd_device = 6;
uint32 interface_number = 7; // bInterfaceNumber a host driver binds to, as Linux reports it
string manufacturer = 8;
string product = 9;
string serial_number = 10;
string interface_description = 11; // iInterface string of that interface; empty when the device has none
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
-24
View File
@@ -48,9 +48,6 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_host/usb_host.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
@@ -1645,27 +1642,6 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
}
}
void APIConnection::on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg) {
auto &proxies = App.get_serial_proxies();
SerialProxyUsbInfo resp{};
resp.instance = msg.instance;
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
} else {
#ifdef USE_SERIAL_PROXY_USB_INFO
// The message's strings are views into this buffer, which outlives the send below
usb_host::UsbDeviceInfo info;
proxies[msg.instance]->get_usb_info(info, resp);
#else
resp.status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
#endif
}
if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
}
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
-1
View File
@@ -243,7 +243,6 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
-40
View File
@@ -4266,46 +4266,6 @@ bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_va
}
return true;
}
bool SerialProxyGetUsbInfoRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
default:
return false;
}
return true;
}
uint8_t *SerialProxyUsbInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->connected);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->vendor_id);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->product_id);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->bcd_device);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->interface_number);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->manufacturer);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->product);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->serial_number);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->interface_description);
return pos;
}
uint32_t SerialProxyUsbInfo::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, this->instance);
size += this->status ? 2 : 0;
size += ProtoSize::calc_bool(1, this->connected);
size += ProtoSize::calc_uint32(1, this->vendor_id);
size += ProtoSize::calc_uint32(1, this->product_id);
size += ProtoSize::calc_uint32(1, this->bcd_device);
size += ProtoSize::calc_uint32(1, this->interface_number);
size += ProtoSize::calc_length(1, this->manufacturer.size());
size += ProtoSize::calc_length(1, this->product.size());
size += ProtoSize::calc_length(1, this->serial_number.size());
size += ProtoSize::calc_length(1, this->interface_description.size());
return size;
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
-42
View File
@@ -23,7 +23,6 @@ enum SerialProxyPortType : uint32_t {
SERIAL_PROXY_PORT_TYPE_TTL = 0,
SERIAL_PROXY_PORT_TYPE_RS232 = 1,
SERIAL_PROXY_PORT_TYPE_RS485 = 2,
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3,
};
enum EntityCategory : uint32_t {
ENTITY_CATEGORY_NONE = 0,
@@ -3425,47 +3424,6 @@ class SerialProxySetModeRequest final : public ProtoDecodableMessage {
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyGetUsbInfoRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 153;
static constexpr uint8_t ESTIMATED_SIZE = 4;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_usb_info_request"); }
#endif
uint32_t instance{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyUsbInfo final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 154;
static constexpr uint8_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_usb_info"); }
#endif
uint32_t instance{0};
enums::SerialProxyStatus status{};
bool connected{false};
uint32_t vendor_id{0};
uint32_t product_id{0};
uint32_t bcd_device{0};
uint32_t interface_number{0};
StringRef manufacturer{};
StringRef product{};
StringRef serial_number{};
StringRef interface_description{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
-22
View File
@@ -143,8 +143,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::S
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232");
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485");
case enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_USB_SERIAL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -2825,26 +2823,6 @@ const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
const char *SerialProxyGetUsbInfoRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
return out.c_str();
}
const char *SerialProxyUsbInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyUsbInfo"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
dump_field(out, ESPHOME_PSTR("connected"), this->connected);
dump_field(out, ESPHOME_PSTR("vendor_id"), this->vendor_id);
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
dump_field(out, ESPHOME_PSTR("product"), this->product);
dump_field(out, ESPHOME_PSTR("serial_number"), this->serial_number);
dump_field(out, ESPHOME_PSTR("interface_description"), this->interface_description);
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -722,17 +722,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_serial_proxy_set_mode_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetUsbInfoRequest::MESSAGE_TYPE: {
SerialProxyGetUsbInfoRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_usb_info_request"), msg);
#endif
this->on_serial_proxy_get_usb_info_request(msg);
break;
}
#endif
default:
break;
-4
View File
@@ -238,10 +238,6 @@ class APIServerConnectionBase {
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
-12
View File
@@ -404,18 +404,6 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
}
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
void APIServer::send_serial_proxy_usb_info(const SerialProxyUsbInfo &msg) {
// Unsolicited: a hotplug is rare and the message small, so every client hears of it
// rather than maintaining a subscription for the one client there normally is
for (auto &c : this->active_clients()) {
if (!c->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "USB info notification");
}
}
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
-4
View File
@@ -194,10 +194,6 @@ class APIServer final : public Component,
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// Tell every client that the USB device behind a serial proxy port changed
void send_serial_proxy_usb_info(const SerialProxyUsbInfo &msg);
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
+4
View File
@@ -0,0 +1,4 @@
CODEOWNERS = ["@kbx81"]
DOMAIN = "bridge"
IS_PLATFORM_COMPONENT = True
@@ -0,0 +1 @@
CODEOWNERS = ["@kbx81"]
@@ -0,0 +1,114 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32, uart, usb_cdc_acm
from esphome.components.bridge import DOMAIN as BRIDGE_DOMAIN
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3
import esphome.config_validation as cv
from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["tinyusb", "uart", "usb_cdc_acm"]
CONF_DTR_PIN = "dtr_pin"
CONF_RTS_PIN = "rts_pin"
CONF_USB_CDC_ACM_ID = "usb_cdc_acm_id"
cdc_acm_uart_ns = cg.esphome_ns.namespace("cdc_acm_uart")
CDCACMUARTBridge = cdc_acm_uart_ns.class_("CDCACMUARTBridge", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CDCACMUARTBridge),
cv.Required(CONF_UART_ID): cv.use_id(uart.IDFUARTComponent),
cv.Required(CONF_USB_CDC_ACM_ID): cv.use_id(usb_cdc_acm.USBCDCACMInstance),
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
}
).extend(cv.COMPONENT_SCHEMA),
# Narrower than usb_cdc_acm's variant list on purpose: S31/H4 untested on
# hardware; extend once verified.
esp32.only_on_variant(
supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3],
),
)
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.
if CONF_DEBUG in uart_conf:
raise cv.Invalid(
"A bridged UART cannot use 'debug'; the bridge bypasses the UART "
"component's read/write path.",
[CONF_DEBUG],
)
return uart_conf
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
# Bridges of any platform must own their interfaces exclusively; shared ring
# buffers and overwritten callbacks would corrupt both streams silently. The
# seen-set is keyed on the bridge domain so future platforms share it.
# Other components bind either interface through the same uart_id key (the CDC
# instance is itself a uart::UARTComponent) and would race the worker tasks.
# Bare `id:` references (a uart.write action) cannot be distinguished; not caught.
data = full_config.data.setdefault(BRIDGE_DOMAIN, {})
for conf_key, label in (
(CONF_UART_ID, "UART"),
(CONF_USB_CDC_ACM_ID, "USB CDC-ACM interface"),
):
owned_id = str(config[conf_key])
used = data.setdefault(conf_key, set())
if owned_id in used:
raise cv.Invalid(
f"The {label} '{owned_id}' is already bridged by another 'bridge' "
f"instance; each bridge requires its own {label}.",
[conf_key],
)
used.add(owned_id)
for domain, domain_conf in full_config.items():
if domain == BRIDGE_DOMAIN:
continue
if _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}.",
[conf_key],
)
fv.id_declaration_match_schema(_reject_debug)(config[CONF_UART_ID])
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
uart_component = await cg.get_variable(config[CONF_UART_ID])
usb_cdc = await cg.get_variable(config[CONF_USB_CDC_ACM_ID])
var = cg.new_Pvariable(config[CONF_ID], uart_component, usb_cdc)
await cg.register_component(var, config)
if dtr_pin_config := config.get(CONF_DTR_PIN):
dtr_pin = await cg.gpio_pin_expression(dtr_pin_config)
cg.add(var.set_dtr_pin(dtr_pin))
if rts_pin_config := config.get(CONF_RTS_PIN):
rts_pin = await cg.gpio_pin_expression(rts_pin_config)
cg.add(var.set_rts_pin(rts_pin))
@@ -0,0 +1,468 @@
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "cdc_acm_uart_bridge.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <algorithm>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "driver/uart.h"
#include "soc/soc_caps.h"
namespace esphome::cdc_acm_uart {
static const char *const TAG = "cdc_acm_uart";
static constexpr size_t UART_TASK_STACK_SIZE = 4096;
static constexpr size_t RINGBUF_RETRY_CHUNK_SIZE = 64;
static constexpr uint32_t LOG_THROTTLE_MS = 1000;
static constexpr uint32_t UART_RELOAD_SETTLE_MS = 20;
// Above the default priority but below the USB/Wi-Fi system tasks.
static constexpr UBaseType_t TASK_PRIORITY = 4;
static bool should_log_now(uint32_t *last_ms, uint32_t interval_ms) {
uint32_t now = millis();
if ((now - *last_ms) >= interval_ms) {
*last_ms = now;
return true;
}
return false;
}
static bool ringbuf_send_with_retry(RingbufHandle_t ringbuf, const uint8_t *data, size_t len, uint32_t *log_ms) {
if (len == 0) {
return true;
}
if (xRingbufferSend(ringbuf, data, len, pdMS_TO_TICKS(1)) == pdTRUE) {
return true;
}
size_t offset = 0;
while (offset < len) {
size_t chunk = std::min(RINGBUF_RETRY_CHUNK_SIZE, len - offset);
if (xRingbufferSend(ringbuf, data + offset, chunk, pdMS_TO_TICKS(1)) != pdTRUE) {
if (should_log_now(log_ms, LOG_THROTTLE_MS)) {
ESP_LOGW(TAG, "USB TX buffer full; some data is lost");
}
return false;
}
offset += chunk;
}
return true;
}
void CDCACMUARTBridge::setup() {
// Line state starts deasserted (no host yet); active-low DTR#/RTS# wiring is
// handled by configuring the pins inverted, so deasserted idles HIGH.
if (this->dtr_pin_ != nullptr) {
this->dtr_pin_->setup();
this->dtr_pin_->digital_write(false);
}
if (this->rts_pin_ != nullptr) {
this->rts_pin_->setup();
this->rts_pin_->digital_write(false);
}
// A failed UART never assigned its port number, so the worker tasks would run
// against an indeterminate port.
if (this->uart_parent_->is_failed()) {
ESP_LOGE(TAG, "UART parent failed; aborting");
this->mark_failed();
return;
}
this->configured_baud_rate_ = this->uart_parent_->get_baud_rate();
this->configured_parity_ = this->uart_parent_->get_parity();
this->configured_stop_bits_ = this->uart_parent_->get_stop_bits();
this->configured_data_bits_ = this->uart_parent_->get_data_bits();
// usb_cdc_acm sets up first (priority IO > HARDWARE). Any interface failing marks
// the hub failed, and a failed hub no longer runs loop(), so line coding and line
// state events would never reach this bridge even if its own interface is healthy.
if (this->usb_cdc_parent_->get_parent()->is_failed()) {
ESP_LOGE(TAG, "USB CDC ACM failed; aborting");
this->mark_failed();
return;
}
// Per-instance task names (keyed on the CDC interface number) keep task dumps
// unambiguous with multiple bridges.
char tx_task_name[] = "cdc_uart_tx_0";
char rx_task_name[] = "cdc_uart_rx_0";
const char itf_char = format_hex_char(this->usb_cdc_parent_->get_itf());
tx_task_name[sizeof(tx_task_name) - 2] = itf_char;
rx_task_name[sizeof(rx_task_name) - 2] = itf_char;
xTaskCreate(uart_tx_task_fn, tx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_tx_task_handle_);
if (this->uart_tx_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create UART TX task");
this->mark_failed();
return;
}
xTaskCreate(uart_rx_task_fn, rx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_rx_task_handle_);
if (this->uart_rx_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create UART RX task");
vTaskDelete(this->uart_tx_task_handle_);
this->uart_tx_task_handle_ = nullptr;
this->mark_failed();
return;
}
// Only register callbacks once both tasks exist, so a failed setup never drives
// DTR/RTS from a dead bridge.
this->usb_cdc_parent_->set_line_state_callback([this](bool dtr, bool rts) { this->set_line_state(dtr, rts); });
this->usb_cdc_parent_->set_line_coding_callback([this](uint32_t, uint8_t, uint8_t, uint8_t) {
this->host_coding_seen_ = true;
// Another component owns the UART's framing while paused; resume() re-syncs.
if (this->paused_ == 0) {
this->set_line_coding();
}
});
// Release the workers only now: until here a failed setup may still delete the TX
// task, which is safe only while it is parked and owns nothing in the driver.
xTaskNotifyGive(this->uart_tx_task_handle_);
xTaskNotifyGive(this->uart_rx_task_handle_);
// loop() only services line-coding reloads; stay off the main loop until one is
// scheduled.
this->disable_loop();
}
void CDCACMUARTBridge::dump_config() {
ESP_LOGCONFIG(TAG,
"CDC-ACM UART Bridge:\n"
" UART Bus: %u\n"
" USB CDC Interface: %u",
this->uart_parent_->get_hw_serial_number(), this->usb_cdc_parent_->get_itf());
LOG_PIN(" DTR Pin: ", this->dtr_pin_);
LOG_PIN(" RTS Pin: ", this->rts_pin_);
}
void CDCACMUARTBridge::on_shutdown() {
// The UART (BUS) shuts down after this component (HARDWARE) and deletes its driver,
// freeing the ring buffer and mutexes the worker tasks block on. Suspending the
// tasks unlinks them from those objects first.
if (this->uart_rx_task_handle_ != nullptr) {
vTaskSuspend(this->uart_rx_task_handle_);
}
if (this->uart_tx_task_handle_ != nullptr) {
vTaskSuspend(this->uart_tx_task_handle_);
}
}
void CDCACMUARTBridge::loop() {
switch (this->state_) {
case MainState::MAIN_STATE_RELOAD_PENDING:
if ((App.get_loop_component_start_time() - this->reload_requested_at_) < UART_RELOAD_SETTLE_MS) {
return;
}
// Deliberately not gated on tx_idle_(): a host that re-codes the line mid-stream
// wants the new framing now, and its own in-flight bytes are its concern.
// apply_settings_live() rewrites the framing registers without reinstalling the
// driver, so the worker tasks blocked inside it are undisturbed.
this->uart_parent_->apply_settings_live();
this->state_ = MainState::MAIN_STATE_RUNNING;
break;
case MainState::MAIN_STATE_PAUSING:
case MainState::MAIN_STATE_RESUMING:
// Let a host write that was in flight drain, FIFO included, before a reload
// flushes the FIFOs and truncates it.
if (!this->tx_idle_()) {
return;
}
if (this->state_ == MainState::MAIN_STATE_PAUSING) {
this->restore_configured_framing_();
this->state_ = MainState::MAIN_STATE_PAUSED;
} else {
this->finish_resume_();
}
break;
default:
break;
}
this->disable_loop();
}
void CDCACMUARTBridge::set_line_coding() {
if (!this->sync_host_framing_()) {
return;
}
// Coalesce rapid line-coding updates from the host.
this->reload_requested_at_ = App.get_loop_component_start_time();
this->state_ = MainState::MAIN_STATE_RELOAD_PENDING;
// Main-loop context (via USBCDCACMInstance::process_events_).
this->enable_loop();
}
bool CDCACMUARTBridge::sync_host_framing_() {
// usb_cdc_acm has already translated the wire coding onto the CDC instance (main
// loop); mirror it here so the framing translation has a single source of truth.
bool changed = false;
// Reject 0 (the CDC B0/hang-up encoding; older IDF revisions divide by the rate)
// and rates above the SoC ceiling. Anything in between is the driver's call,
// matching what a YAML-configured UART accepts.
const uint32_t baud = this->usb_cdc_parent_->get_baud_rate();
if (baud == 0 || baud > SOC_UART_BITRATE_MAX) {
ESP_LOGW(TAG, "Ignoring unsupported baud rate %" PRIu32 " from host; keeping %" PRIu32, baud,
this->uart_parent_->get_baud_rate());
} else if (this->uart_parent_->get_baud_rate() != baud) {
this->uart_parent_->set_baud_rate(baud);
changed = true;
}
const uint8_t stop_bits = this->usb_cdc_parent_->get_stop_bits();
if (this->uart_parent_->get_stop_bits() != stop_bits) {
this->uart_parent_->set_stop_bits(stop_bits);
changed = true;
}
const auto parity = this->usb_cdc_parent_->get_parity();
if (this->uart_parent_->get_parity() != parity) {
this->uart_parent_->set_parity(parity);
changed = true;
}
// USB CDC permits data-bit counts the UART cannot represent (up to 16).
const uint8_t data_bits = this->usb_cdc_parent_->get_data_bits();
if (data_bits < 5 || data_bits > 8) {
ESP_LOGW(TAG, "Ignoring unsupported data bits %u from host; keeping %u", data_bits,
this->uart_parent_->get_data_bits());
} else if (this->uart_parent_->get_data_bits() != data_bits) {
this->uart_parent_->set_data_bits(data_bits);
changed = true;
}
if (changed) {
ESP_LOGV(TAG, "Line coding: baud=%" PRIu32 ", data_bits=%u, stop_bits=%u, parity=%u",
this->uart_parent_->get_baud_rate(), this->uart_parent_->get_data_bits(),
this->uart_parent_->get_stop_bits(), static_cast<uint8_t>(this->uart_parent_->get_parity()));
}
return changed;
}
void CDCACMUARTBridge::pause() {
if (this->state_ == MainState::MAIN_STATE_PAUSING || this->state_ == MainState::MAIN_STATE_PAUSED) {
return;
}
this->paused_ = 1;
// A null RX task means setup() has not completed (or failed): nothing to stop, and
// the framing snapshot does not exist yet. Should setup() run later, the RX task
// starts parked.
if (this->uart_rx_task_handle_ == nullptr) {
this->state_ = MainState::MAIN_STATE_PAUSED;
return;
}
// Drops a coalesced host reload or a pending resume; loop() restores the framing
// once any host write in flight has drained.
this->state_ = MainState::MAIN_STATE_PAUSING;
this->enable_loop();
}
void CDCACMUARTBridge::resume() {
if (this->state_ != MainState::MAIN_STATE_PAUSING && this->state_ != MainState::MAIN_STATE_PAUSED) {
return;
}
if (this->uart_rx_task_handle_ == nullptr) {
this->paused_ = 0;
this->state_ = MainState::MAIN_STATE_RUNNING;
return;
}
// A restore still waiting on the TX side is moot: the host's framing is kept.
if (!this->tx_idle_()) {
this->state_ = MainState::MAIN_STATE_RESUMING;
this->enable_loop();
return;
}
this->finish_resume_();
this->disable_loop();
}
void CDCACMUARTBridge::finish_resume_() {
// Take the bus back at a known framing before either task runs again: the host's
// if it ever sent one, else the YAML framing (the other owner may have changed it).
if (this->host_coding_seen_) {
this->sync_host_framing_();
this->uart_parent_->apply_settings_live();
} else {
this->restore_configured_framing_();
}
this->paused_ = 0;
this->state_ = MainState::MAIN_STATE_RUNNING;
this->drive_line_state_();
xTaskNotifyGive(this->uart_rx_task_handle_);
}
bool CDCACMUARTBridge::tx_idle_() {
const auto uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
return this->tx_busy_ == 0 && uart_wait_tx_done(uart_num, 0) == ESP_OK;
}
void CDCACMUARTBridge::restore_configured_framing_() {
// Always applied: the cached settings can lead the hardware by a pending reload,
// so they are no proof of what is live.
this->uart_parent_->set_baud_rate(this->configured_baud_rate_);
this->uart_parent_->set_parity(this->configured_parity_);
this->uart_parent_->set_stop_bits(this->configured_stop_bits_);
this->uart_parent_->set_data_bits(this->configured_data_bits_);
this->uart_parent_->apply_settings_live();
}
void CDCACMUARTBridge::set_line_state(bool dtr, bool rts) {
ESP_LOGV(TAG, "Line state: DTR=%d, RTS=%d", dtr, rts);
this->host_dtr_ = dtr;
this->host_rts_ = rts;
// Frozen while paused: a host opening the port must not reset a peer that another
// component is talking to.
if (this->paused_ == 0) {
this->drive_line_state_();
}
}
void CDCACMUARTBridge::drive_line_state_() {
if (this->dtr_pin_ != nullptr) {
this->dtr_pin_->digital_write(this->host_dtr_);
}
if (this->rts_pin_ != nullptr) {
this->rts_pin_->digital_write(this->host_rts_);
}
}
void CDCACMUARTBridge::uart_rx_task_fn(void *arg) {
auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
bridge->uart_rx_task_();
}
void CDCACMUARTBridge::uart_tx_task_fn(void *arg) {
auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
bridge->uart_tx_task_();
}
void CDCACMUARTBridge::uart_rx_task_() {
TaskHandle_t usb_tx_handle = this->usb_cdc_parent_->get_tx_task_handle();
RingbufHandle_t usb_tx_ringbuf = this->usb_cdc_parent_->get_tx_ringbuf();
uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
// Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
uint32_t tx_full_log_ms = millis() - LOG_THROTTLE_MS;
uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
uint8_t *data = this->uart_rx_buffer_.data();
const size_t buf_size = this->uart_rx_buffer_.size();
// Released by setup() once both tasks exist.
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
while (true) {
if (this->paused_ != 0) {
// Parked until resume() notifies; nothing is read, so the other owner sees
// every byte.
this->rx_parked_ = 1;
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
this->rx_parked_ = 0;
continue;
}
// Block until at least one byte is available from UART.
int total_rx_size = uart_read_bytes(uart_num, data, 1, pdMS_TO_TICKS(UART_RX_WAIT_MS));
if (total_rx_size < 0) {
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
ESP_LOGE(TAG, "UART read failed: %d", total_rx_size);
}
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
if (total_rx_size == 0) {
continue;
}
// pause() landed during the read: don't forward a byte to a host that is gone.
if (this->paused_ != 0) {
continue;
}
// Drain the currently buffered burst without waiting.
while (true) {
int rx_data_size = uart_read_bytes(uart_num, data + total_rx_size, buf_size - total_rx_size, 0);
if (rx_data_size < 0) {
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
ESP_LOGE(TAG, "UART read failed: %d", rx_data_size);
}
break;
}
if (rx_data_size == 0) {
break;
}
ESP_LOGV(TAG, "UART RX: %d bytes", rx_data_size);
total_rx_size += rx_data_size;
if (total_rx_size >= (int) buf_size) {
break;
}
}
ringbuf_send_with_retry(usb_tx_ringbuf, data, total_rx_size, &tx_full_log_ms);
ESP_LOGV(TAG, "UART RX: waking up USB TX task");
xTaskNotifyGive(usb_tx_handle);
}
}
void CDCACMUARTBridge::uart_tx_task_() {
RingbufHandle_t usb_rx_ringbuf = this->usb_cdc_parent_->get_rx_ringbuf();
uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
uint8_t *data_to_uart = this->uart_tx_buffer_.data();
const size_t buf_size = this->uart_tx_buffer_.size();
size_t rx_size;
// Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
uint32_t drop_log_ms = millis() - LOG_THROTTLE_MS;
// Released by setup() once both tasks exist.
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
while (true) {
ESP_LOGV(TAG, "Waiting for data to send to UART");
esp_err_t ret = usb_cdc_acm::ringbuf_read_bytes(usb_rx_ringbuf, data_to_uart, buf_size, &rx_size, portMAX_DELAY);
if (ret != ESP_OK) {
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
ESP_LOGE(TAG, "USB RX RingBuf read failed");
}
// Yield: this task runs above the main loop, so a persistent failure must not
// become a tight loop.
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
// Another component owns the UART; host bytes must not interleave with its traffic.
// tx_busy_ goes up before the check so is_paused() cannot miss a write in flight.
this->tx_busy_ = 1;
if (this->paused_ != 0) {
this->tx_busy_ = 0;
if (should_log_now(&drop_log_ms, LOG_THROTTLE_MS)) {
ESP_LOGW(TAG, "Paused; dropping %zu bytes from host", rx_size);
}
continue;
}
ESP_LOGV(TAG, "Sending %zu bytes to UART", rx_size);
// Signed: uart_write_bytes() returns -1 on error.
int xfer_size = uart_write_bytes(uart_num, data_to_uart, rx_size);
this->tx_busy_ = 0;
if (xfer_size < 0) {
if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
ESP_LOGE(TAG, "UART write failed: %d", xfer_size);
}
} else if (static_cast<size_t>(xfer_size) != rx_size) {
ESP_LOGW(TAG, "UART write incomplete (%d/%zu bytes)", xfer_size, rx_size);
}
}
}
} // namespace esphome::cdc_acm_uart
#endif
@@ -0,0 +1,117 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/components/uart/uart_component_esp_idf.h"
#include "esphome/components/usb_cdc_acm/usb_cdc_acm.h"
#include "esphome/core/component.h"
#include <array>
#include <atomic>
#include "sdkconfig.h"
namespace esphome::cdc_acm_uart {
class CDCACMUARTBridge final : public Component {
public:
// Upper bound on the RX task's blocking read, so pause() takes effect without
// aborting the read. Arriving bytes still unblock it immediately.
static constexpr uint32_t UART_RX_WAIT_MS = 250;
CDCACMUARTBridge(uart::IDFUARTComponent *uart_parent, usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent)
: uart_parent_(uart_parent), usb_cdc_parent_(usb_cdc_parent) {}
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void set_dtr_pin(GPIOPin *dtr_pin) { this->dtr_pin_ = dtr_pin; }
void set_rts_pin(GPIOPin *rts_pin) { this->rts_pin_ = rts_pin; }
void set_line_coding();
void set_line_state(bool dtr, bool rts);
/**
* Stop forwarding in both directions and hand the UART back to its configured
* framing, so another component may use the bus. Main-loop only. The RX task parks
* within UART_RX_WAIT_MS (a byte it was already reading is discarded). A host write
* already in flight is allowed to drain first, which at low baud rates can take
* seconds; the framing is restored only after that, so poll is_paused() rather than
* waiting a fixed interval. Host bytes not yet written to the UART are discarded.
* The DTR/RTS outputs hold their state while paused and follow the host again on
* resume().
*/
void pause();
/**
* Re-apply the host's line coding and line state, then resume forwarding. Main-loop
* only. Deferred until any host write still draining has finished, so the reload
* never truncates it.
*/
void resume();
/// True once both worker tasks are off the bus and the configured framing is restored.
/// With no RX task (setup() failed or has not run) there is nothing to wait for.
bool is_paused() const {
return this->state_ == MainState::MAIN_STATE_PAUSED &&
(this->uart_rx_task_handle_ == nullptr || this->rx_parked_ != 0);
}
protected:
static void uart_rx_task_fn(void *arg);
static void uart_tx_task_fn(void *arg);
void uart_rx_task_();
void uart_tx_task_();
void restore_configured_framing_();
// True when the TX task has no write in flight and the UART TX FIFO has drained.
bool tx_idle_();
void finish_resume_();
void drive_line_state_();
// Copy the host's line coding onto the UART settings; true if anything changed.
bool sync_host_framing_();
TaskHandle_t uart_rx_task_handle_{nullptr};
TaskHandle_t uart_tx_task_handle_{nullptr};
GPIOPin *dtr_pin_{nullptr};
GPIOPin *rts_pin_{nullptr};
uint32_t reload_requested_at_{0};
// Worker staging, each sized to the CDC ring buffer it feeds or drains.
std::array<uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE> uart_rx_buffer_{};
std::array<uint8_t, CONFIG_TINYUSB_CDC_RX_BUFSIZE> uart_tx_buffer_{};
uart::IDFUARTComponent *uart_parent_;
usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent_;
// YAML framing, captured at setup; the host's line coding overwrites the UART's
// settings, so pause() needs the original to restore.
uint32_t configured_baud_rate_{0};
uart::UARTParityOptions configured_parity_{uart::UART_CONFIG_PARITY_NONE};
uint8_t configured_stop_bits_{0};
uint8_t configured_data_bits_{0};
// Written on the main loop, read by both worker tasks. uint8_t rather than bool:
// GCC on Xtensa emits an out-of-line call for atomic<bool>.
std::atomic<uint8_t> paused_{0};
// Raised by the RX task while parked and by the TX task around each UART write, so
// the pause hand-off knows when the bus is actually free.
std::atomic<uint8_t> rx_parked_{0};
std::atomic<uint8_t> tx_busy_{0};
// Main-loop state; paused_ mirrors it for the worker tasks.
enum class MainState : uint8_t {
MAIN_STATE_RUNNING,
MAIN_STATE_RELOAD_PENDING, // host line coding debounced, forwarding continues
MAIN_STATE_PAUSING, // waiting for TX idle to restore the configured framing
MAIN_STATE_PAUSED,
MAIN_STATE_RESUMING, // resume() requested while a host write still drains
};
MainState state_{MainState::MAIN_STATE_RUNNING};
// Host line state, recorded even while paused so resume() can re-drive the pins.
bool host_dtr_{false};
bool host_rts_{false};
// True once the host has sent any line coding; resume() then re-syncs to it.
bool host_coding_seen_{false};
};
} // namespace esphome::cdc_acm_uart
#endif
+1
View File
@@ -30,6 +30,7 @@ CONF_KEYS = "keys"
CONF_LABEL = "label"
CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
CONF_MANUFACTURER = "manufacturer"
CONF_NOX_INDEX = "nox_index"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive"
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
from esphome.components import climate_ir, remote_base
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
@@ -12,4 +12,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
async def to_code(config: ConfigType) -> None:
remote_base.request_protocol("coolix") # used from C++
await climate_ir.new_climate_ir(config)
+118
View File
@@ -189,6 +189,13 @@ PSRAM_XIP_VARIANTS = {
VARIANT_ESP32S31,
}
# Variants whose ROM exports a full-format vsnprintf but no vasprintf
# (esp32c6.rom.newlib-normal.ld). There, the newlib printf engine is only
# linked because esp_http_client calls vasprintf; see vasprintf_stubs.cpp.
# The other variants either export both (classic ESP32, nano-format only) or
# neither, so the engine is already in the image and the wrap saves nothing.
ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
@@ -1732,6 +1739,8 @@ CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_MBEDTLS_TLS_SERVER = "disable_mbedtls_tls_server"
CONF_DISABLE_MBEDTLS_TLS_EXTRAS = "disable_mbedtls_tls_extras"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
@@ -1746,6 +1755,8 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_MBEDTLS_TLS_SERVER_REQUIRED = "mbedtls_tls_server_required"
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED = "mbedtls_tls_extras_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
@@ -1830,6 +1841,30 @@ def require_mbedtls_pkcs7() -> None:
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
def require_mbedtls_tls_server() -> None:
"""Mark that the mbedTLS server-side TLS/DTLS handshake is required.
Call this from components that accept TLS connections (OpenThread's DTLS
commissioner does). This prevents CONFIG_MBEDTLS_TLS_CLIENT_ONLY from
being selected.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] = True
def require_mbedtls_tls_extras(options: Iterable[str] | None = None) -> None:
"""Mark TLS features disabled by ``disable_mbedtls_tls_extras`` as required.
``options`` names the entries of ``MBEDTLS_TLS_EXTRA_OPTIONS`` to keep;
omit it to keep all of them. Call this from components that need AES-CCM,
deterministic ECDSA signing, static RSA/ECDH key exchange, TLS
renegotiation or session tickets, or that run a TLS client against
servers ESPHome cannot vet (wpa_supplicant's EAP client). A user-supplied
sdkconfig_options value is never overridden either.
"""
required = CORE.data[KEY_ESP32].setdefault(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
required.update(MBEDTLS_TLS_EXTRA_OPTIONS if options is None else options)
def require_mbedtls_sha512() -> None:
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
@@ -1987,6 +2022,8 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_SERVER, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_TLS_EXTRAS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
@@ -2302,6 +2339,69 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
# TLS features an HTTPS/MQTT client talking to a modern server never
# negotiates. Static RSA and static ECDH key exchange have no forward secrecy
# and are gone in TLS 1.3, renegotiation is deprecated, esp-tls never enables
# session tickets, AES-CCM ciphersuites are not offered by web servers, and
# deterministic ECDSA only matters when signing with a private key. Together
# they cost ~10 KB of flash whenever TLS is linked (http_request, mqtt).
# wpa_supplicant's EAP client is a second TLS client that talks to RADIUS
# servers ESPHome cannot vet, and a failed EAP handshake leaves the device
# off the network, so the wifi component re-enables all of these when eap is
# configured.
# The EC public key parsing extras stay enabled: they decide whether a peer
# certificate with a compressed point or explicit curve parameters parses,
# which no component can know ahead of time.
MBEDTLS_TLS_EXTRA_OPTIONS = (
"CONFIG_MBEDTLS_KEY_EXCHANGE_RSA",
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA",
"CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA",
"CONFIG_MBEDTLS_SSL_RENEGOTIATION",
"CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS",
"CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS",
"CONFIG_MBEDTLS_CCM_C",
"CONFIG_MBEDTLS_ECDSA_DETERMINISTIC",
)
# Members of the mbedTLS "TLS Protocol Role" Kconfig choice. Setting one
# member is only valid when the user has not already chosen another.
MBEDTLS_TLS_ROLE_OPTIONS = (
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
"CONFIG_MBEDTLS_TLS_DISABLED",
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_mbedtls_tls_sdkconfig(
disable_tls_server: bool, disable_tls_extras: bool
) -> None:
"""Trim mbedTLS to what a TLS client needs unless a component asked otherwise.
Runs at FINAL priority so every require_mbedtls_tls_server() and
require_mbedtls_tls_extras() call has happened. Only the server-side
handshake (~7 KB) is a separate option; nothing in ESPHome accepts TLS
connections, but OpenThread's DTLS commissioner does. A user-supplied
sdkconfig_options value always wins; for the TLS role choice, any member
the user set leaves the whole choice alone so the pair cannot conflict.
"""
data = CORE.data[KEY_ESP32]
sdkconfig = data[KEY_SDKCONFIG_OPTIONS]
if (
disable_tls_server
and not data.get(KEY_MBEDTLS_TLS_SERVER_REQUIRED, False)
and not any(option in sdkconfig for option in MBEDTLS_TLS_ROLE_OPTIONS)
):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_CLIENT_ONLY", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", False)
if disable_tls_extras:
required = data.get(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set())
for option in MBEDTLS_TLS_EXTRA_OPTIONS:
if option not in required:
set_idf_sdkconfig_default(option, False)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_network_sdkconfig() -> None:
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
@@ -2566,6 +2666,17 @@ async def to_code(config):
else:
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
# _dtoa_r and their helpers); every other caller resolves to the
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
# because libsrc.a is scanned before the IDF libraries that
# reference the symbol, so the stub would otherwise never be pulled
# from the archive.
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
cg.add_define("USE_ESP32_VASPRINTF_STUB")
cg.add_build_flag("-Wl,--wrap=vasprintf")
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
else:
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
@@ -2991,6 +3102,13 @@ async def to_code(config):
# FINAL priority: runs after every require_certificate_bundle() call
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
# FINAL priority: runs after every require_mbedtls_tls_*() call
CORE.add_job(
_reconcile_mbedtls_tls_sdkconfig,
advanced[CONF_DISABLE_MBEDTLS_TLS_SERVER],
advanced[CONF_DISABLE_MBEDTLS_TLS_EXTRAS],
)
# FINAL: require_*() calls can come from to_code at or below this priority, so an
# inline read would be iteration-order-dependent; reconcile once after every job ran.
CORE.add_job(
@@ -0,0 +1,53 @@
/*
* Linker wrap stub for vasprintf() on variants whose ROM exports a
* full-format vsnprintf() but no vasprintf() (ESP32-C6, newlib only).
*
* On those chips every snprintf/vsnprintf call in the image resolves to
* the ROM, so the newlib printf engine (_svfprintf_r, _dtoa_r and their
* helpers, ~20 KB) is not linked at all until something references a
* printf-family function the ROM lacks. esp_http_client does exactly that
* through vasprintf() in its header and auth helpers, so adding
* http_request to a build costs the whole engine on top of the HTTP and
* TLS code itself.
*
* This stub reimplements vasprintf() on top of the ROM vsnprintf(), which
* keeps the engine out of the image. It is only compiled in when codegen
* defines USE_ESP32_VASPRINTF_STUB, which is gated on the variant's ROM
* linker script and on the same newlib condition as printf_stubs.cpp.
*/
#include "esphome/core/defines.h"
#if defined(USE_ESP_IDF) && defined(USE_ESP32_VASPRINTF_STUB)
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
namespace esphome::esp32 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
int __wrap_vasprintf(char **strp, const char *fmt, va_list ap) {
va_list ap_copy;
va_copy(ap_copy, ap);
int len = vsnprintf(nullptr, 0, fmt, ap_copy);
va_end(ap_copy);
if (len < 0) {
return len;
}
// vasprintf's contract is a malloc'd buffer the caller releases with free()
char *buf = static_cast<char *>(malloc(static_cast<size_t>(len) + 1)); // NOLINT(cppcoreguidelines-no-malloc)
if (buf == nullptr) {
return -1;
}
vsnprintf(buf, static_cast<size_t>(len) + 1, fmt, ap);
*strp = buf;
return len;
}
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP_IDF && USE_ESP32_VASPRINTF_STUB
@@ -3,6 +3,7 @@ import encodings
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32_ble
from esphome.components.const import CONF_MANUFACTURER
from esphome.components.esp32 import request_bluetooth
from esphome.components.esp32_ble import BTLoggers, bt_uuid
import esphome.config_validation as cv
@@ -41,7 +42,6 @@ CONF_DESCRIPTORS = "descriptors"
CONF_ENDIANNESS = "endianness"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_INDICATE = "indicate"
CONF_MANUFACTURER = "manufacturer"
CONF_MANUFACTURER_DATA = "manufacturer_data"
CONF_MAX_CLIENTS = "max_clients"
CONF_ON_WRITE = "on_write"
-5
View File
@@ -59,11 +59,6 @@ void Infrared::setup() {
// Set up traits based on configuration
this->traits_.set_supports_transmitter(this->has_transmitter());
this->traits_.set_supports_receiver(this->has_receiver());
// Register as listener for received IR data
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void Infrared::dump_config() {
+2 -1
View File
@@ -119,7 +119,8 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
/// Set the remote receiver component
/// Set the remote receiver component; the listener registration happens from codegen, see
/// remote_base.attach_receiver
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
/// Set the remote transmitter component
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
+7 -3
View File
@@ -3,7 +3,12 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import infrared, remote_receiver, remote_transmitter
from esphome.components import (
infrared,
remote_base,
remote_receiver,
remote_transmitter,
)
from esphome.components.const import CONF_RECEIVER_FREQUENCY
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
@@ -82,8 +87,7 @@ async def to_code(config: dict[str, Any]) -> None:
# Link receiver if specified
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID)
# Set receiver demodulation frequency if specified (metadata only, no hardware effect)
if CONF_RECEIVER_FREQUENCY in config:
@@ -97,10 +97,6 @@ void RfProxy::setup() {
// remote_transmitter/receiver always uses OOK (on-off keying)
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void RfProxy::dump_config() {
+2 -1
View File
@@ -56,7 +56,8 @@ class RfProxy final : public radio_frequency::RadioFrequency {
/// Set the remote transmitter component
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
/// Set the remote receiver component
/// Set the remote receiver component; the listener registration happens from codegen, see
/// remote_base.attach_receiver
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
@@ -1,7 +1,12 @@
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
import esphome.codegen as cg
from esphome.components import radio_frequency, remote_receiver, remote_transmitter
from esphome.components import (
radio_frequency,
remote_base,
remote_receiver,
remote_transmitter,
)
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
@@ -66,5 +71,4 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_transmitter(transmitter))
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID)
+1 -2
View File
@@ -14,7 +14,7 @@ from esphome.const import (
CONF_TIMEOUT,
)
from esphome.core import Lambda
from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_generator import StaticCastExpression, TemplateArguments, get_variable
from esphome.cpp_types import nullptr
from .defines import (
@@ -30,7 +30,6 @@ from .defines import (
CONF_SHOW_SNOW,
CONF_TOP_LAYER,
PARTS,
StaticCastExpression,
add_warning,
get_focused_widgets,
get_options,
+1 -42
View File
@@ -10,12 +10,7 @@ from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import (
CallExpression,
LambdaExpression,
MockObj,
MockObjClass,
)
from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
@@ -157,17 +152,6 @@ def get_refreshed_widgets() -> set:
return _get_data(KEY_REFRESHED_WIDGETS, set())
class StaticCastExpression(Expression):
__slots__ = ("type", "exp")
def __init__(self, type: Any, exp: SafeExpType):
self.type = str(type)
self.exp = cg.safe_exp(exp)
def __str__(self):
return f"static_cast<{self.type}>({self.exp})"
def add_define(macro: str, value="1"):
lv_defines = get_defines()
value = str(value)
@@ -192,31 +176,6 @@ def addr(arg) -> MockObj:
return MockObj(f"&{arg}")
def call_lambda(lamb: LambdaExpression) -> Expression:
"""
Given a lambda, either reduce to a simple expression or call it, possibly with parameters
from the surrounding context
:param lamb:
:return:
"""
expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"):
# Convert a lambda returning a simple expression to just that expression
expr = cg.RawExpression(expr[6:-1].strip())
# Don't cast if the return type is a class
if isinstance(lamb.return_type, MockObjClass):
return expr
return StaticCastExpression(lamb.return_type, expr)
# If lambda has parameters, call it with their names
# Parameter names come from hardcoded component code (like "x", "it", "event")
# not from user input, so they're safe to use directly
if lamb.parameters and lamb.parameters.parameters:
return CallExpression(
lamb, *[MockObj(x.id) for x in lamb.parameters.parameters]
)
return CallExpression(lamb)
class LValidator:
"""
A validator for a particular type used in LVGL. Usable in configs as a validator, also
+1 -3
View File
@@ -16,7 +16,7 @@ from esphome.const import (
CONF_VALUE,
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda
from esphome.cpp_types import ESPTime, int32, uint32
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -33,9 +33,7 @@ from .defines import (
LV_FONTS,
LValidator,
LvConstant,
StaticCastExpression,
add_lv_use,
call_lambda,
get_esphome_fonts_used,
get_lv_fonts_used,
get_lv_images_used,
+1 -2
View File
@@ -16,7 +16,7 @@ from esphome.const import (
)
from esphome.core import ID, EsphomeError, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObj
from esphome.cpp_generator import MockObj, call_lambda
from esphome.schema_extractors import EnableSchemaExtraction
from esphome.types import Expression
@@ -42,7 +42,6 @@ from ..defines import (
STATES,
LValidator,
add_lv_use,
call_lambda,
get_styles_used,
get_theme_widget_map,
get_widget_map,
+2 -1
View File
@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, remote_transmitter, sensor, uart
from esphome.components import climate, remote_base, remote_transmitter, sensor, uart
from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode
from esphome.components.remote_base import CONF_TRANSMITTER_ID
import esphome.config_validation as cv
@@ -280,6 +280,7 @@ async def to_code(config):
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
if CONF_TRANSMITTER_ID in config:
remote_base.request_protocol("midea") # ir_transmitter.h uses it from C++
cg.add_define("USE_REMOTE_TRANSMITTER")
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter_))
+5 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
from esphome.components import climate_ir, remote_base
import esphome.config_validation as cv
from esphome.const import CONF_USE_FAHRENHEIT
from esphome.types import ConfigType
@@ -19,5 +19,9 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
async def to_code(config: ConfigType) -> None:
# midea_ir uses MideaProtocol from C++ and auto-loads coolix, whose coolix.cpp uses
# CoolixProtocol even when no coolix climate is configured
remote_base.request_protocol("midea")
remote_base.request_protocol("coolix")
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))
+10
View File
@@ -13,6 +13,8 @@ from esphome.components.esp32 import (
get_esp32_variant,
include_builtin_idf_component,
only_on_variant,
require_mbedtls_tls_extras,
require_mbedtls_tls_server,
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
@@ -109,6 +111,14 @@ def set_sdkconfig_options(config: ConfigType) -> None:
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
# OpenThread's DTLS commissioner is a TLS server, and its crypto platform
# uses AES-CCM and deterministic ECDSA directly. Keep the esp32 component
# from trimming them out of mbedTLS.
require_mbedtls_tls_server()
require_mbedtls_tls_extras(
("CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC")
)
if not config.get(CONF_TLV):
if pan_id := config.get(CONF_PAN_ID):
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id)
+102 -7
View File
@@ -1,6 +1,11 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -40,11 +45,14 @@ from esphome.const import (
CONF_ZERO,
)
from esphome.core import ID, coroutine
from esphome.cpp_generator import MockObj
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.util import Registry, SimpleRegistry
AUTO_LOAD = ["binary_sensor"]
CONF_RECEIVER_ID = "receiver_id"
CONF_TRANSMITTER_ID = "transmitter_id"
CONF_FIRST = "first"
@@ -90,9 +98,42 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
)
async def register_listener(var, config):
# Listener and dumper lists are StaticVectors sized from these counts, so every registration
# must go through add_listener / add_dumper. Every receiver's list gets the same capacity, so
# the slots are keyed by receiver and the define is the largest count any one receiver needs.
LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT"
DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT"
_request_listener_slot = cg.slot_counter(LISTENER_COUNT_DEFINE)
_request_dumper_slot = cg.slot_counter(DUMPER_COUNT_DEFINE)
def add_listener(receiver: MockObj, listener: MockObj) -> None:
_request_listener_slot(str(receiver))
cg.add(receiver.register_listener(listener))
def add_dumper(receiver: MockObj, dumper: MockObj) -> None:
_request_dumper_slot(str(receiver))
cg.add(receiver.register_dumper(dumper))
async def register_listener(var: MockObj, config: ConfigType) -> None:
receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
cg.add(receiver.register_listener(var))
add_listener(receiver, var)
async def attach_receiver(
var: MockObj, config: ConfigType, key: str = CONF_RECEIVER_ID
) -> None:
"""Link the configured receiver to an entity and register the entity as its listener.
The C++ set_receiver() no longer registers the listener; the slot for it is counted here.
"""
receiver = await cg.get_variable(config[key])
cg.add(var.set_receiver(receiver))
add_listener(receiver, var)
async def register_transmittable(var, config):
@@ -100,8 +141,53 @@ async def register_transmittable(var, config):
cg.add(var.set_transmitter(transmitter_))
def register_binary_sensor(name, type, schema):
return BINARY_SENSOR_REGISTRY.register(name, type, schema)
# Registry names that share a protocol source file
def _protocol_stem(name: str) -> str:
if name.startswith("rc_switch"):
return "rc_switch"
if name == "canalsatld":
return "canalsat"
return name
def protocol_define(name: str) -> str:
return f"USE_REMOTE_PROTOCOL_{_protocol_stem(name).upper()}"
_PROTOCOL_STEMS = sorted(
path.name.removesuffix("_protocol.cpp")
for path in Path(__file__).parent.glob("*_protocol.cpp")
)
def request_protocol(name: str) -> None:
"""Keep a protocol's source file in the build; components using it from C++ must call this."""
if _protocol_stem(name) not in _PROTOCOL_STEMS:
raise ValueError(
f"Unknown remote protocol {name!r}; expected one of {', '.join(_PROTOCOL_STEMS)}"
)
cg.add_define(protocol_define(name))
# Only the protocol sources a configuration uses are compiled
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS}
)
def register_binary_sensor(
name: str, type: MockObj, schema: cv.Schema | dict
) -> Callable[[Callable[[MockObj, ConfigType], Any]], Callable]:
registerer = BINARY_SENSOR_REGISTRY.register(name, type, schema)
def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable:
async def new_func(var: MockObj, config: ConfigType) -> None:
request_protocol(name)
await coroutine(func)(var, config)
return registerer(new_func)
return decorator
def register_trigger(name, type, data_type):
@@ -114,6 +200,7 @@ def register_trigger(name, type, data_type):
def decorator(func):
async def new_func(config):
request_protocol(name)
var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
await coroutine(func)(var, config)
await automation.build_automation(var, [(data_type, "x")], config)
@@ -131,6 +218,7 @@ def register_dumper(name, type, schema=None):
def decorator(func):
async def new_func(config, dumper_id):
request_protocol(name)
var = cg.new_Pvariable(dumper_id)
await coroutine(func)(var, config)
return var
@@ -171,6 +259,7 @@ def register_action(name, type_, schema):
def decorator(func):
async def new_func(config, action_id, template_arg, args):
request_protocol(name)
var = cg.new_Pvariable(action_id, template_arg)
await register_transmittable(var, config)
if CONF_REPEAT in config:
@@ -213,7 +302,13 @@ DUMPER_REGISTRY = Registry()
def validate_dumpers(value):
if isinstance(value, str) and value.lower() == "all":
return validate_dumpers(list(DUMPER_REGISTRY.keys()))
return cv.validate_registry("dumper", DUMPER_REGISTRY)(value)
entries = cv.validate_registry("dumper", DUMPER_REGISTRY)(value)
# a dumper listed twice would register twice; the receiver holds one secondary dumper
return list(
{
next(k for k in entry if k in DUMPER_REGISTRY): entry for entry in entries
}.values()
)
def validate_triggers(base_schema):
@@ -1439,7 +1534,7 @@ def validate_rc_switch_raw_code(value):
def build_rc_switch_protocol(config):
if isinstance(config, int):
return rc_switch_protocols[config]
return rc_switch_protocol(config)
pl = config[CONF_PULSE_LENGTH]
return RCSwitchBase(
config[CONF_SYNC][0] * pl,
@@ -1526,7 +1621,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema(
}
)
rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS
rc_switch_protocol = ns.rc_switch_protocol
RCSwitchData = ns.struct("RCSwitchData")
RCSwitchBase = ns.class_("RCSwitchBase")
RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger)
@@ -191,9 +191,9 @@ class ABBWelcomeData {
class ABBWelcomeProtocol : public RemoteProtocol<ABBWelcomeData> {
public:
void encode(RemoteTransmitData *dst, const ABBWelcomeData &src) override;
optional<ABBWelcomeData> decode(RemoteReceiveData src) override;
void dump(const ABBWelcomeData &data) override;
void encode(RemoteTransmitData *dst, const ABBWelcomeData &src);
optional<ABBWelcomeData> decode(RemoteReceiveData src);
void dump(const ABBWelcomeData &data);
protected:
void encode_byte_(RemoteTransmitData *dst, uint8_t data) const;
@@ -15,9 +15,9 @@ struct AEHAData {
class AEHAProtocol : public RemoteProtocol<AEHAData> {
public:
void encode(RemoteTransmitData *dst, const AEHAData &data) override;
optional<AEHAData> decode(RemoteReceiveData src) override;
void dump(const AEHAData &data) override;
void encode(RemoteTransmitData *dst, const AEHAData &data);
optional<AEHAData> decode(RemoteReceiveData src);
void dump(const AEHAData &data);
private:
std::string format_data_(const std::vector<uint8_t> &data);
@@ -16,9 +16,9 @@ struct Beo4Data {
class Beo4Protocol : public RemoteProtocol<Beo4Data> {
public:
void encode(RemoteTransmitData *dst, const Beo4Data &data) override;
optional<Beo4Data> decode(RemoteReceiveData src) override;
void dump(const Beo4Data &data) override;
void encode(RemoteTransmitData *dst, const Beo4Data &data);
optional<Beo4Data> decode(RemoteReceiveData src);
void dump(const Beo4Data &data);
};
DECLARE_REMOTE_PROTOCOL(Beo4)
@@ -13,9 +13,9 @@ struct BrennenstuhlData {
class BrennenstuhlProtocol : public RemoteProtocol<BrennenstuhlData> {
public:
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override;
optional<BrennenstuhlData> decode(RemoteReceiveData src) override;
void dump(const BrennenstuhlData &data) override;
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data);
optional<BrennenstuhlData> decode(RemoteReceiveData src);
void dump(const BrennenstuhlData &data);
};
DECLARE_REMOTE_PROTOCOL(Brennenstuhl)
@@ -21,9 +21,9 @@ struct ByronSXData {
class ByronSXProtocol : public RemoteProtocol<ByronSXData> {
public:
void encode(RemoteTransmitData *dst, const ByronSXData &data) override;
optional<ByronSXData> decode(RemoteReceiveData src) override;
void dump(const ByronSXData &data) override;
void encode(RemoteTransmitData *dst, const ByronSXData &data);
optional<ByronSXData> decode(RemoteReceiveData src);
void dump(const ByronSXData &data);
};
DECLARE_REMOTE_PROTOCOL(ByronSX)
@@ -19,9 +19,9 @@ struct CanalSatLDData : public CanalSatData {};
class CanalSatBaseProtocol : public RemoteProtocol<CanalSatData> {
public:
void encode(RemoteTransmitData *dst, const CanalSatData &data) override;
optional<CanalSatData> decode(RemoteReceiveData src) override;
void dump(const CanalSatData &data) override;
void encode(RemoteTransmitData *dst, const CanalSatData &data);
optional<CanalSatData> decode(RemoteReceiveData src);
void dump(const CanalSatData &data);
protected:
uint16_t frequency_;
@@ -21,9 +21,9 @@ struct CoolixData {
class CoolixProtocol : public RemoteProtocol<CoolixData> {
public:
void encode(RemoteTransmitData *dst, const CoolixData &data) override;
optional<CoolixData> decode(RemoteReceiveData data) override;
void dump(const CoolixData &data) override;
void encode(RemoteTransmitData *dst, const CoolixData &data);
optional<CoolixData> decode(RemoteReceiveData data);
void dump(const CoolixData &data);
};
DECLARE_REMOTE_PROTOCOL(Coolix)
@@ -13,9 +13,9 @@ struct DishData {
class DishProtocol : public RemoteProtocol<DishData> {
public:
void encode(RemoteTransmitData *dst, const DishData &data) override;
optional<DishData> decode(RemoteReceiveData src) override;
void dump(const DishData &data) override;
void encode(RemoteTransmitData *dst, const DishData &data);
optional<DishData> decode(RemoteReceiveData src);
void dump(const DishData &data);
};
DECLARE_REMOTE_PROTOCOL(Dish)
@@ -20,9 +20,9 @@ struct DooyaData {
class DooyaProtocol : public RemoteProtocol<DooyaData> {
public:
void encode(RemoteTransmitData *dst, const DooyaData &data) override;
optional<DooyaData> decode(RemoteReceiveData src) override;
void dump(const DooyaData &data) override;
void encode(RemoteTransmitData *dst, const DooyaData &data);
optional<DooyaData> decode(RemoteReceiveData src);
void dump(const DooyaData &data);
};
DECLARE_REMOTE_PROTOCOL(Dooya)
@@ -19,9 +19,9 @@ struct DraytonData {
class DraytonProtocol : public RemoteProtocol<DraytonData> {
public:
void encode(RemoteTransmitData *dst, const DraytonData &data) override;
optional<DraytonData> decode(RemoteReceiveData src) override;
void dump(const DraytonData &data) override;
void encode(RemoteTransmitData *dst, const DraytonData &data);
optional<DraytonData> decode(RemoteReceiveData src);
void dump(const DraytonData &data);
};
DECLARE_REMOTE_PROTOCOL(Drayton)
@@ -21,9 +21,9 @@ struct DysonData {
class DysonProtocol : public RemoteProtocol<DysonData> {
public:
void encode(RemoteTransmitData *dst, const DysonData &data) override;
optional<DysonData> decode(RemoteReceiveData src) override;
void dump(const DysonData &data) override;
void encode(RemoteTransmitData *dst, const DysonData &data);
optional<DysonData> decode(RemoteReceiveData src);
void dump(const DysonData &data);
};
DECLARE_REMOTE_PROTOCOL(Dyson)
@@ -31,9 +31,9 @@ class GoboxProtocol : public RemoteProtocol<GoboxData> {
void dump_timings_(const RawTimings &timings) const;
public:
void encode(RemoteTransmitData *dst, const GoboxData &data) override;
optional<GoboxData> decode(RemoteReceiveData src) override;
void dump(const GoboxData &data) override;
void encode(RemoteTransmitData *dst, const GoboxData &data);
optional<GoboxData> decode(RemoteReceiveData src);
void dump(const GoboxData &data);
};
DECLARE_REMOTE_PROTOCOL(Gobox)
@@ -13,9 +13,9 @@ struct HaierData {
class HaierProtocol : public RemoteProtocol<HaierData> {
public:
void encode(RemoteTransmitData *dst, const HaierData &data) override;
optional<HaierData> decode(RemoteReceiveData src) override;
void dump(const HaierData &data) override;
void encode(RemoteTransmitData *dst, const HaierData &data);
optional<HaierData> decode(RemoteReceiveData src);
void dump(const HaierData &data);
protected:
void encode_byte_(RemoteTransmitData *dst, uint8_t item);
@@ -14,9 +14,9 @@ struct JVCData {
class JVCProtocol : public RemoteProtocol<JVCData> {
public:
void encode(RemoteTransmitData *dst, const JVCData &data) override;
optional<JVCData> decode(RemoteReceiveData src) override;
void dump(const JVCData &data) override;
void encode(RemoteTransmitData *dst, const JVCData &data);
optional<JVCData> decode(RemoteReceiveData src);
void dump(const JVCData &data);
};
DECLARE_REMOTE_PROTOCOL(JVC)
@@ -24,9 +24,9 @@ struct KeeloqData {
class KeeloqProtocol : public RemoteProtocol<KeeloqData> {
public:
void encode(RemoteTransmitData *dst, const KeeloqData &data) override;
optional<KeeloqData> decode(RemoteReceiveData src) override;
void dump(const KeeloqData &data) override;
void encode(RemoteTransmitData *dst, const KeeloqData &data);
optional<KeeloqData> decode(RemoteReceiveData src);
void dump(const KeeloqData &data);
};
DECLARE_REMOTE_PROTOCOL(Keeloq)
+3 -3
View File
@@ -16,9 +16,9 @@ struct LGData {
class LGProtocol : public RemoteProtocol<LGData> {
public:
void encode(RemoteTransmitData *dst, const LGData &data) override;
optional<LGData> decode(RemoteReceiveData src) override;
void dump(const LGData &data) override;
void encode(RemoteTransmitData *dst, const LGData &data);
optional<LGData> decode(RemoteReceiveData src);
void dump(const LGData &data);
};
DECLARE_REMOTE_PROTOCOL(LG)
@@ -27,9 +27,9 @@ struct MagiQuestData {
class MagiQuestProtocol : public RemoteProtocol<MagiQuestData> {
public:
void encode(RemoteTransmitData *dst, const MagiQuestData &data) override;
optional<MagiQuestData> decode(RemoteReceiveData src) override;
void dump(const MagiQuestData &data) override;
void encode(RemoteTransmitData *dst, const MagiQuestData &data);
optional<MagiQuestData> decode(RemoteReceiveData src);
void dump(const MagiQuestData &data);
};
DECLARE_REMOTE_PROTOCOL(MagiQuest)
@@ -67,9 +67,9 @@ class MideaData {
class MideaProtocol : public RemoteProtocol<MideaData> {
public:
void encode(RemoteTransmitData *dst, const MideaData &src) override;
optional<MideaData> decode(RemoteReceiveData src) override;
void dump(const MideaData &data) override;
void encode(RemoteTransmitData *dst, const MideaData &src);
optional<MideaData> decode(RemoteReceiveData src);
void dump(const MideaData &data);
};
DECLARE_REMOTE_PROTOCOL(Midea)
@@ -13,9 +13,9 @@ struct MirageData {
class MirageProtocol : public RemoteProtocol<MirageData> {
public:
void encode(RemoteTransmitData *dst, const MirageData &data) override;
optional<MirageData> decode(RemoteReceiveData src) override;
void dump(const MirageData &data) override;
void encode(RemoteTransmitData *dst, const MirageData &data);
optional<MirageData> decode(RemoteReceiveData src);
void dump(const MirageData &data);
protected:
void encode_byte_(RemoteTransmitData *dst, uint8_t item);
@@ -14,9 +14,9 @@ struct NECData {
class NECProtocol : public RemoteProtocol<NECData> {
public:
void encode(RemoteTransmitData *dst, const NECData &data) override;
optional<NECData> decode(RemoteReceiveData src) override;
void dump(const NECData &data) override;
void encode(RemoteTransmitData *dst, const NECData &data);
optional<NECData> decode(RemoteReceiveData src);
void dump(const NECData &data);
};
DECLARE_REMOTE_PROTOCOL(NEC)
@@ -24,9 +24,9 @@ class NexaProtocol : public RemoteProtocol<NexaData> {
void zero(RemoteTransmitData *dst) const;
void sync(RemoteTransmitData *dst) const;
void encode(RemoteTransmitData *dst, const NexaData &data) override;
optional<NexaData> decode(RemoteReceiveData src) override;
void dump(const NexaData &data) override;
void encode(RemoteTransmitData *dst, const NexaData &data);
optional<NexaData> decode(RemoteReceiveData src);
void dump(const NexaData &data);
};
DECLARE_REMOTE_PROTOCOL(Nexa)
@@ -16,9 +16,9 @@ struct PanasonicData {
class PanasonicProtocol : public RemoteProtocol<PanasonicData> {
public:
void encode(RemoteTransmitData *dst, const PanasonicData &data) override;
optional<PanasonicData> decode(RemoteReceiveData src) override;
void dump(const PanasonicData &data) override;
void encode(RemoteTransmitData *dst, const PanasonicData &data);
optional<PanasonicData> decode(RemoteReceiveData src);
void dump(const PanasonicData &data);
};
DECLARE_REMOTE_PROTOCOL(Panasonic)
@@ -13,9 +13,9 @@ struct PioneerData {
class PioneerProtocol : public RemoteProtocol<PioneerData> {
public:
void encode(RemoteTransmitData *dst, const PioneerData &data) override;
optional<PioneerData> decode(RemoteReceiveData src) override;
void dump(const PioneerData &data) override;
void encode(RemoteTransmitData *dst, const PioneerData &data);
optional<PioneerData> decode(RemoteReceiveData src);
void dump(const PioneerData &data);
};
DECLARE_REMOTE_PROTOCOL(Pioneer)
@@ -30,9 +30,9 @@ class ProntoProtocol : public RemoteProtocol<ProntoData> {
std::string compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase);
public:
void encode(RemoteTransmitData *dst, const ProntoData &data) override;
optional<ProntoData> decode(RemoteReceiveData src) override;
void dump(const ProntoData &data) override;
void encode(RemoteTransmitData *dst, const ProntoData &data);
optional<ProntoData> decode(RemoteReceiveData src);
void dump(const ProntoData &data);
};
DECLARE_REMOTE_PROTOCOL(Pronto)
@@ -14,9 +14,9 @@ struct RC5Data {
class RC5Protocol : public RemoteProtocol<RC5Data> {
public:
void encode(RemoteTransmitData *dst, const RC5Data &data) override;
optional<RC5Data> decode(RemoteReceiveData src) override;
void dump(const RC5Data &data) override;
void encode(RemoteTransmitData *dst, const RC5Data &data);
optional<RC5Data> decode(RemoteReceiveData src);
void dump(const RC5Data &data);
};
DECLARE_REMOTE_PROTOCOL(RC5)
@@ -15,9 +15,9 @@ struct RC6Data {
class RC6Protocol : public RemoteProtocol<RC6Data> {
public:
void encode(RemoteTransmitData *dst, const RC6Data &data) override;
optional<RC6Data> decode(RemoteReceiveData src) override;
void dump(const RC6Data &data) override;
void encode(RemoteTransmitData *dst, const RC6Data &data);
optional<RC6Data> decode(RemoteReceiveData src);
void dump(const RC6Data &data);
};
DECLARE_REMOTE_PROTOCOL(RC6)
@@ -1,29 +1,21 @@
#include "rc_switch_protocol.h"
#include <iterator>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::remote_base {
static const char *const TAG = "remote.rc_switch";
const RCSwitchBase RC_SWITCH_PROTOCOLS[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false),
RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false),
RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false),
RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false),
RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false),
RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false),
RCSwitchBase(10350, 450, 450, 900, 900, 450, true),
RCSwitchBase(300, 9300, 150, 900, 900, 150, false),
RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)};
RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low,
uint32_t one_high, uint32_t one_low, bool inverted)
: sync_high_(sync_high),
sync_low_(sync_low),
zero_high_(zero_high),
zero_low_(zero_low),
one_high_(one_high),
one_low_(one_low),
inverted_(inverted) {}
RCSwitchBase rc_switch_protocol(uint8_t index) {
RCSwitchBase protocol;
// entry 0 is the all-zero protocol, so an out of range index from a lambda transmits nothing
if (index >= std::size(RC_SWITCH_PROTOCOLS))
index = 0;
progmem_memcpy(&protocol, &RC_SWITCH_PROTOCOLS[index], sizeof(protocol));
return protocol;
}
void RCSwitchBase::one(RemoteTransmitData *dst) const {
if (!this->inverted_) {
@@ -133,11 +125,11 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *o
optional<RCSwitchData> RCSwitchBase::decode(RemoteReceiveData &src) const {
RCSwitchData out;
uint8_t out_nbits;
for (uint8_t i = 1; i <= 8; i++) {
for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) {
src.reset();
const RCSwitchBase *protocol = &RC_SWITCH_PROTOCOLS[i];
if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) {
out.protocol = i;
out.protocol = static_cast<uint8_t>(i);
return out;
}
}
@@ -246,7 +238,7 @@ bool RCSwitchRawReceiver::matches(RemoteReceiveData src) {
return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_);
}
bool RCSwitchDumper::dump(RemoteReceiveData src) {
for (uint8_t i = 1; i <= 8; i++) {
for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) {
src.reset();
uint64_t out_data;
uint8_t out_nbits;
@@ -257,7 +249,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) {
buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0';
buffer[out_nbits] = '\0';
ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer);
ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", static_cast<unsigned>(i), buffer);
// only send first decoded protocol
return true;
@@ -16,9 +16,16 @@ class RCSwitchBase {
public:
using ProtocolData = RCSwitchData;
RCSwitchBase() = default;
RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high,
uint32_t one_low, bool inverted);
constexpr RCSwitchBase() = default;
constexpr RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low,
uint32_t one_high, uint32_t one_low, bool inverted)
: sync_high_(sync_high),
sync_low_(sync_low),
zero_high_(zero_high),
zero_low_(zero_low),
one_high_(one_high),
one_low_(one_low),
inverted_(inverted) {}
void one(RemoteTransmitData *dst) const;
@@ -58,10 +65,28 @@ class RCSwitchBase {
uint32_t zero_low_{};
uint32_t one_high_{};
uint32_t one_low_{};
bool inverted_{};
uint32_t inverted_{}; // bool widened so every field is a word: the table is read from flash
};
extern const RCSwitchBase RC_SWITCH_PROTOCOLS[9];
// Constant-initialized and kept in flash on every platform. The decoder reads entries in place
// through a pointer, which ESP8266 only allows while every field is a whole word; copies out of
// the table go through rc_switch_protocol()
static_assert(sizeof(RCSwitchBase) == 7 * sizeof(uint32_t), "RCSwitchBase must stay word-only for flash reads");
inline constexpr RCSwitchBase RC_SWITCH_PROTOCOLS[] PROGMEM = {
{0, 0, 0, 0, 0, 0, false},
{350, 10850, 350, 1050, 1050, 350, false},
{650, 6500, 650, 1300, 1300, 650, false},
{3000, 7100, 400, 1100, 900, 600, false},
{380, 2280, 380, 1140, 1140, 380, false},
{3000, 7000, 500, 1000, 1000, 500, false},
{10350, 450, 450, 900, 900, 450, true},
{300, 9300, 150, 900, 900, 150, false},
{250, 2500, 250, 1250, 250, 250, false},
};
/// RAM copy of RC_SWITCH_PROTOCOLS[index] (0 when out of range) for the transmit actions and the dumper, made with
/// progmem_memcpy so no byte load ever touches the flash table on ESP8266
RCSwitchBase rc_switch_protocol(uint8_t index);
uint64_t decode_binary_string(const std::string &data);
+29 -10
View File
@@ -99,29 +99,48 @@ bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) {
/* RemoteReceiverBase */
// Slots are counted at code generation; a registration from C++ setup() has none
#ifdef REMOTE_BASE_LISTENER_COUNT
void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) {
if (this->listeners_.size() == REMOTE_BASE_LISTENER_COUNT) {
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("listener"),
LOG_STR_LITERAL("listener"));
return;
}
this->listeners_.push_back(listener);
}
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) {
if (dumper->is_secondary()) {
this->secondary_dumpers_.push_back(dumper);
} else {
if (this->secondary_dumper_ == nullptr) {
this->secondary_dumper_ = dumper;
return;
}
} else if (this->dumpers_.size() != REMOTE_BASE_DUMPER_COUNT) {
this->dumpers_.push_back(dumper);
return;
}
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("dumper"),
LOG_STR_LITERAL("dumper"));
}
#endif
void RemoteReceiverBase::call_listeners_() {
void RemoteReceiverBase::call_listeners_dumpers_() {
#ifdef REMOTE_BASE_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_receive(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
}
void RemoteReceiverBase::call_dumpers_() {
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
bool success = false;
for (auto *dumper : this->dumpers_) {
if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)))
success = true;
}
if (!success) {
for (auto *dumper : this->secondary_dumpers_)
dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
}
if (!success && this->secondary_dumper_ != nullptr)
this->secondary_dumper_->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
#endif
}
void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); }
+52 -22
View File
@@ -1,12 +1,14 @@
#pragma once
#include <concepts>
#include <utility>
#include <vector>
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::remote_base {
@@ -141,6 +143,22 @@ class RemoteRMTChannel {
#endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
// Protocol shapes, checked where a protocol is used so a missing method fails at the use site
// instead of deep inside a template body. Receive-only protocols such as RCSwitchBase decode
// without encoding.
template<typename T>
concept RemoteProtocolDecoder = requires(T proto, RemoteReceiveData src) {
{ proto.decode(src) } -> std::same_as<optional<typename T::ProtocolData>>;
};
template<typename T>
concept RemoteProtocolDumper = RemoteProtocolDecoder<T> && requires(T proto, const typename T::ProtocolData &data) {
proto.dump(data);
};
template<typename T>
concept RemoteProtocolEncoder = requires(T proto, RemoteTransmitData *dst, const typename T::ProtocolData &data) {
proto.encode(dst, data);
};
class RemoteTransmitterBase : public RemoteComponentBase {
public:
RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
@@ -162,8 +180,8 @@ class RemoteTransmitterBase : public RemoteComponentBase {
this->temp_.reset();
return TransmitCall(this);
}
template<typename Protocol>
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
template<RemoteProtocolEncoder Protocol>
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
auto call = this->transmit();
Protocol().encode(call.get_data(), data);
call.set_send_times(send_times);
@@ -194,24 +212,37 @@ class RemoteReceiverDumperBase {
class RemoteReceiverBase : public RemoteComponentBase {
public:
RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
void register_listener(RemoteReceiverListener *listener) { this->listeners_.push_back(listener); }
// Slots are counted at code generation; without one the call fails at compile time with the same message
// the runtime check logs
#ifdef REMOTE_BASE_LISTENER_COUNT
void register_listener(RemoteReceiverListener *listener);
#else
template<typename T> void register_listener(T *) {
static_assert(sizeof(T) == 0, "No listener slot: register it from to_code() with remote_base.add_listener");
}
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
void register_dumper(RemoteReceiverDumperBase *dumper);
#else
template<typename T> void register_dumper(T *) {
static_assert(sizeof(T) == 0, "No dumper slot: register it from to_code() with remote_base.add_dumper");
}
#endif
void set_tolerance(uint32_t tolerance, ToleranceMode tolerance_mode) {
this->tolerance_ = tolerance;
this->tolerance_mode_ = tolerance_mode;
}
protected:
void call_listeners_();
void call_dumpers_();
void call_listeners_dumpers_() {
this->call_listeners_();
this->call_dumpers_();
}
void call_listeners_dumpers_();
std::vector<RemoteReceiverListener *> listeners_;
std::vector<RemoteReceiverDumperBase *> dumpers_;
std::vector<RemoteReceiverDumperBase *> secondary_dumpers_;
#ifdef REMOTE_BASE_LISTENER_COUNT
StaticVector<RemoteReceiverListener *, REMOTE_BASE_LISTENER_COUNT> listeners_;
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
StaticVector<RemoteReceiverDumperBase *, REMOTE_BASE_DUMPER_COUNT> dumpers_;
RemoteReceiverDumperBase *secondary_dumper_{nullptr}; // runs only when no primary dumper matched
#endif
RawTimings temp_;
uint32_t tolerance_{25};
ToleranceMode tolerance_mode_{TOLERANCE_MODE_PERCENTAGE};
@@ -229,15 +260,14 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial
/* TEMPLATES */
// Protocols are used only through their concrete type (see the RemoteProtocol* concepts); encode/decode/dump
// stay non-virtual so unused ones link out
template<typename T> class RemoteProtocol {
public:
using ProtocolData = T;
virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0;
virtual optional<ProtocolData> decode(RemoteReceiveData src) = 0;
virtual void dump(const ProtocolData &data) = 0;
};
template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
template<RemoteProtocolDecoder T> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
public:
RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {}
@@ -255,7 +285,7 @@ template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBin
T::ProtocolData data_;
};
template<typename T>
template<RemoteProtocolDecoder T>
class RemoteReceiverTrigger final : public Trigger<typename T::ProtocolData>, public RemoteReceiverListener {
protected:
bool on_receive(RemoteReceiveData src) override {
@@ -276,8 +306,8 @@ class RemoteTransmittable {
void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
protected:
template<typename Protocol>
void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
template<RemoteProtocolEncoder Protocol>
void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
}
RemoteTransmitterBase *transmitter_;
@@ -298,7 +328,7 @@ template<typename... Ts> class RemoteTransmitterActionBase : public RemoteTransm
virtual void encode(RemoteTransmitData *dst, Ts... x) = 0;
};
template<typename T> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
template<RemoteProtocolDumper T> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
public:
bool dump(RemoteReceiveData src) override {
auto proto = T();
@@ -12,9 +12,9 @@ struct RoombaData {
class RoombaProtocol : public RemoteProtocol<RoombaData> {
public:
void encode(RemoteTransmitData *dst, const RoombaData &data) override;
optional<RoombaData> decode(RemoteReceiveData src) override;
void dump(const RoombaData &data) override;
void encode(RemoteTransmitData *dst, const RoombaData &data);
optional<RoombaData> decode(RemoteReceiveData src);
void dump(const RoombaData &data);
};
DECLARE_REMOTE_PROTOCOL(Roomba)
@@ -16,9 +16,9 @@ struct Samsung36Data {
class Samsung36Protocol : public RemoteProtocol<Samsung36Data> {
public:
void encode(RemoteTransmitData *dst, const Samsung36Data &data) override;
optional<Samsung36Data> decode(RemoteReceiveData src) override;
void dump(const Samsung36Data &data) override;
void encode(RemoteTransmitData *dst, const Samsung36Data &data);
optional<Samsung36Data> decode(RemoteReceiveData src);
void dump(const Samsung36Data &data);
};
DECLARE_REMOTE_PROTOCOL(Samsung36)
@@ -14,9 +14,9 @@ struct SamsungData {
class SamsungProtocol : public RemoteProtocol<SamsungData> {
public:
void encode(RemoteTransmitData *dst, const SamsungData &data) override;
optional<SamsungData> decode(RemoteReceiveData src) override;
void dump(const SamsungData &data) override;
void encode(RemoteTransmitData *dst, const SamsungData &data);
optional<SamsungData> decode(RemoteReceiveData src);
void dump(const SamsungData &data);
};
DECLARE_REMOTE_PROTOCOL(Samsung)
@@ -16,9 +16,9 @@ struct SonyData {
class SonyProtocol : public RemoteProtocol<SonyData> {
public:
void encode(RemoteTransmitData *dst, const SonyData &data) override;
optional<SonyData> decode(RemoteReceiveData src) override;
void dump(const SonyData &data) override;
void encode(RemoteTransmitData *dst, const SonyData &data);
optional<SonyData> decode(RemoteReceiveData src);
void dump(const SonyData &data);
};
DECLARE_REMOTE_PROTOCOL(Sony)
@@ -17,9 +17,9 @@ struct SymphonyData {
class SymphonyProtocol : public RemoteProtocol<SymphonyData> {
public:
void encode(RemoteTransmitData *dst, const SymphonyData &data) override;
optional<SymphonyData> decode(RemoteReceiveData src) override;
void dump(const SymphonyData &data) override;
void encode(RemoteTransmitData *dst, const SymphonyData &data);
optional<SymphonyData> decode(RemoteReceiveData src);
void dump(const SymphonyData &data);
};
DECLARE_REMOTE_PROTOCOL(Symphony)
@@ -14,9 +14,9 @@ struct ToshibaAcData {
class ToshibaAcProtocol : public RemoteProtocol<ToshibaAcData> {
public:
void encode(RemoteTransmitData *dst, const ToshibaAcData &data) override;
optional<ToshibaAcData> decode(RemoteReceiveData src) override;
void dump(const ToshibaAcData &data) override;
void encode(RemoteTransmitData *dst, const ToshibaAcData &data);
optional<ToshibaAcData> decode(RemoteReceiveData src);
void dump(const ToshibaAcData &data);
};
DECLARE_REMOTE_PROTOCOL(ToshibaAc)
@@ -16,9 +16,9 @@ struct TotoData {
class TotoProtocol : public RemoteProtocol<TotoData> {
public:
void encode(RemoteTransmitData *dst, const TotoData &data) override;
optional<TotoData> decode(RemoteReceiveData src) override;
void dump(const TotoData &data) override;
void encode(RemoteTransmitData *dst, const TotoData &data);
optional<TotoData> decode(RemoteReceiveData src);
void dump(const TotoData &data);
};
DECLARE_REMOTE_PROTOCOL(Toto)
+11 -7
View File
@@ -114,15 +114,18 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
cv.Optional(CONF_TOLERANCE, default="25%"): validate_tolerance,
cv.SplitDefault(
CONF_BUFFER_SIZE,
esp32="10000b",
esp32_c2="1000b",
esp32_c61="1000b",
esp32=cv.UNDEFINED,
# the pulse ring needs a size; only RMT targets size themselves in setup()
**{
f"esp32_{variant.removeprefix('ESP32').lower()}": "1000b"
for variant in esp32_rmt.VARIANTS_NO_RMT
},
esp8266="1000b",
bk72xx="1000b",
ln882x="1000b",
rtl87xx="1000b",
rp2="1000b",
): cv.validate_bytes,
): cv.All(cv.validate_bytes, cv.int_range(min=64)),
cv.Optional(CONF_FILTER, default="50us"): cv.All(
cv.positive_time_period_microseconds,
cv.Range(max=TimePeriod(microseconds=4294967295)),
@@ -221,11 +224,11 @@ async def to_code(config: ConfigType) -> None:
dumpers = await remote_base.build_dumpers(config[CONF_DUMP])
for dumper in dumpers:
cg.add(var.register_dumper(dumper))
remote_base.add_dumper(var, dumper)
triggers = await remote_base.build_triggers(config)
for trigger in triggers:
cg.add(var.register_listener(trigger))
remote_base.add_listener(var, trigger)
await cg.register_component(var, config)
cg.add(
@@ -233,7 +236,8 @@ async def to_code(config: ConfigType) -> None:
config[CONF_TOLERANCE][CONF_VALUE], config[CONF_TOLERANCE][CONF_TYPE]
)
)
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
if CONF_BUFFER_SIZE in config:
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
cg.add(var.set_filter_us(config[CONF_FILTER]))
cg.add(var.set_idle_us(config[CONF_IDLE]))
@@ -47,7 +47,7 @@ struct RemoteReceiverComponentStore {
/// The position last read from
volatile uint32_t buffer_read{0};
bool overflow{false};
uint32_t buffer_size{1000};
uint32_t buffer_size{0};
uint32_t receive_size{0};
uint32_t filter_symbols{0};
esp_err_t error{ESP_OK};
@@ -101,7 +101,7 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase,
HighFrequencyLoopRequester high_freq_;
#endif
uint32_t buffer_size_{};
uint32_t buffer_size_{}; // 0 on RMT targets: sized from receive_symbols in setup()
uint32_t filter_us_{10};
uint32_t idle_us_{10000};
};
@@ -10,6 +10,7 @@
namespace esphome::remote_receiver {
static const char *const TAG = "remote_receiver";
static constexpr uint32_t DEFAULT_BUFFER_SLOTS = 4;
static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) {
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
@@ -104,7 +105,11 @@ void RemoteReceiverComponent::setup() {
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;
this->store_.filter_symbols = this->filter_symbols_;
this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t);
this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_);
// one slot per pending rmt_receive; two are the floor (one filling while one is decoded), and
// the default of four covers a few frames queued across a stalled loop pass
const uint32_t slot_size = event_size + this->store_.receive_size;
this->store_.buffer_size =
this->buffer_size_ != 0 ? std::max(slot_size * 2, this->buffer_size_) : slot_size * DEFAULT_BUFFER_SLOTS;
this->store_.buffer = new uint8_t[this->store_.buffer_size];
error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size,
&this->store_.config);
@@ -117,20 +122,23 @@ void RemoteReceiverComponent::setup() {
}
void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Receiver:\n"
" Clock resolution: %" PRIu32 " hz\n"
" RMT symbols: %" PRIu32 "\n"
" Filter symbols: %" PRIu32 "\n"
" Receive symbols: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n"
" Carrier frequency: %" PRIu32 " hz\n"
" Carrier duty: %u%%\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_,
this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%",
this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_);
ESP_LOGCONFIG(
TAG,
"Remote Receiver:\n"
" Clock resolution: %" PRIu32 " hz\n"
" RMT symbols: %" PRIu32 "\n"
" Filter symbols: %" PRIu32 "\n"
" Receive symbols: %" PRIu32 "\n"
" Buffer size: %" PRIu32 " bytes\n"
" Tolerance: %" PRIu32 "%s\n"
" Carrier frequency: %" PRIu32 " hz\n"
" Carrier duty: %u%%\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_,
this->store_.buffer_size, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_);
LOG_PIN(" Pin: ", this->pin_);
if (this->is_failed()) {
ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_),
+1 -1
View File
@@ -3,6 +3,7 @@ from dataclasses import dataclass, field
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32, network, psram, socket, wifi
from esphome.components.const import CONF_MANUFACTURER
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
@@ -33,7 +34,6 @@ CONF_DISPLAY_OFFSET = "display_offset"
CONF_SENDSPIN_ID = "sendspin_id"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_MANUFACTURER = "manufacturer"
# An empty device information string would be sent to the server as an empty value rather than
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
+3 -33
View File
@@ -18,10 +18,9 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_UART_ID
from esphome.const import CONF_ID, CONF_NAME
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
@@ -35,14 +34,11 @@ SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
# User-selectable electrical types. USB_SERIAL is deliberately absent: it is derived
# from the uart_id pointing at a usb_uart channel, never set by the user.
SERIAL_PROXY_PORT_TYPES = {
"TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL,
"RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232,
"RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485,
}
PORT_TYPE_USB_SERIAL = SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_USB_SERIAL
CONF_DTR_PIN = "dtr_pin"
CONF_PORT_TYPE = "port_type"
@@ -67,7 +63,7 @@ CONFIG_SCHEMA = (
{
cv.GenerateID(): cv.declare_id(SerialProxy),
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
}
@@ -77,26 +73,6 @@ CONFIG_SCHEMA = (
)
def _uses_usb_uart(config: ConfigType, full_config: ConfigType) -> bool:
from esphome.components.usb_uart import is_usb_uart_channel
return is_usb_uart_channel(config[CONF_UART_ID], full_config)
def _final_validate(config: ConfigType) -> ConfigType:
if _uses_usb_uart(config, fv.full_config.get()):
if CONF_PORT_TYPE in config:
raise cv.Invalid(
f"{CONF_PORT_TYPE} is set automatically for USB serial ports"
)
elif CONF_PORT_TYPE not in config:
raise cv.Invalid(f"{CONF_PORT_TYPE} is required")
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_serial_proxy_count_define() -> None:
"""Emit the SERIAL_PROXY_COUNT define once with the final instance count."""
@@ -111,13 +87,7 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config)
cg.add(cg.App.register_serial_proxy(var))
cg.add(var.set_name(config[CONF_NAME]))
if _uses_usb_uart(config, CORE.config):
cg.add(var.set_port_type(PORT_TYPE_USB_SERIAL))
channel = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.set_usb_channel(channel))
cg.add_define("USE_SERIAL_PROXY_USB_INFO")
else:
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add_define("USE_SERIAL_PROXY")
# Track instance count for the FINAL priority define
@@ -12,10 +12,6 @@
#include "esphome/components/api/api_server.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_uart/usb_uart.h"
#endif
namespace esphome::serial_proxy {
static const char *const TAG = "serial_proxy";
@@ -34,13 +30,6 @@ void SerialProxy::setup() {
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
this->outgoing_msg_.instance = this->instance_index_;
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
// The define is global, so a hardware UART port in the same config also gets here
if (this->usb_channel_ != nullptr) {
this->usb_channel_->get_parent()->add_on_connection_callback(
[this](bool connected) { this->on_usb_connection_changed_(connected); });
}
#endif
#ifdef USE_SERIAL_PROXY_TAP
// A tap sets itself up before this runs (its setup priority is higher), so it may
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
@@ -167,10 +156,9 @@ void SerialProxy::dump_config() {
" RTS Pin: %s\n"
" DTR Pin: %s",
this->instance_index_, this->name_ != nullptr ? this->name_ : "",
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL ? LOG_STR_LITERAL("USB_SERIAL")
: LOG_STR_LITERAL("TTL"),
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: LOG_STR_LITERAL("TTL"),
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
}
@@ -212,6 +200,11 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
}
if (flow_control) {
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
// Some bridges watch line-coding changes as a signalling channel (a magic baud
@@ -222,8 +215,7 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
uart::UART_CONFIG_PARITY_ODD,
};
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity] &&
uart_comp->get_flow_control() == flow_control) {
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
@@ -232,7 +224,6 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
uart_comp->set_baud_rate(baudrate);
uart_comp->set_stop_bits(stop_bits);
uart_comp->set_data_bits(data_size);
uart_comp->set_flow_control(flow_control);
uart_comp->set_parity(PARITY_MAP[parity]);
@@ -342,55 +333,6 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
#ifdef USE_SERIAL_PROXY_USB_INFO
void SerialProxy::on_usb_connection_changed_(bool connected) {
ESP_LOGD(TAG, "USB device %s serial proxy [%" PRIu32 "]",
connected ? LOG_STR_LITERAL("attached to") : LOG_STR_LITERAL("removed from"), this->instance_index_);
#ifdef USE_SERIAL_PROXY_TAP
// Before telling clients, so a tap never acknowledges a frame from the old device after
// a client has been told it is gone. The subscriber and the mode stay: both belong to
// the client's session, and only the client knows whether that session is over.
if (!connected && this->tap_ != nullptr) {
this->tap_->on_device_disconnected();
}
#endif
#ifdef USE_API
if (api::global_api_server == nullptr) {
return;
}
// The message's strings are views into this buffer, which outlives the send below
usb_host::UsbDeviceInfo info;
api::SerialProxyUsbInfo msg{};
msg.instance = this->instance_index_;
this->get_usb_info(info, msg);
api::global_api_server->send_serial_proxy_usb_info(msg);
#endif
}
#ifdef USE_API
void SerialProxy::get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyUsbInfo &msg) const {
if (this->usb_channel_ == nullptr) {
msg.status = api::enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
return;
}
if (!this->usb_channel_->get_parent()->get_device_info(info)) {
// No device attached right now; not an error
return;
}
msg.connected = true;
msg.vendor_id = info.vendor_id;
msg.product_id = info.product_id;
msg.bcd_device = info.bcd_device;
msg.interface_number = this->usb_channel_->get_interface_number();
msg.manufacturer = StringRef(info.manufacturer);
msg.product = StringRef(info.product);
msg.serial_number = StringRef(info.serial_number);
// Lives in the channel for as long as the device is attached
msg.interface_description = StringRef(this->usb_channel_->get_interface_string());
}
#endif
#endif
uint32_t SerialProxy::get_modem_pins() const {
return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
(this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
+1 -37
View File
@@ -20,15 +20,6 @@
#include "esphome/components/api/api_pb2.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
namespace esphome::usb_uart {
class USBUartChannel;
} // namespace esphome::usb_uart
namespace esphome::usb_host {
struct UsbDeviceInfo;
} // namespace esphome::usb_host
#endif
// Forward-declare types needed outside the USE_API guard.
namespace esphome::api {
class APIConnection;
@@ -89,11 +80,6 @@ class SerialProxyTap {
/// else with the device -- reflash it, most likely -- so anything the tap believes about
/// it should be treated as suspect.
virtual void on_protocol_disabled() = 0;
/// The device behind the port went away -- unplugged, or its power was cut. Whatever
/// session the tap had observed ended with it; the device that appears next starts from
/// scratch and may not even be the same one. Only a USB UART can report this.
virtual void on_device_disconnected() = 0;
};
#endif
@@ -128,7 +114,7 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Configure UART parameters and apply them
/// @param api_connection The API connection requesting the change
/// @param baudrate Baud rate in bits per second
/// @param flow_control True to request hardware flow control
/// @param flow_control True to enable hardware flow control
/// @param parity Parity setting (0=none, 1=even, 2=odd)
/// @param stop_bits Number of stop bits (1 or 2)
/// @param data_size Number of data bits (5-8)
@@ -169,17 +155,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
#ifdef USE_SERIAL_PROXY_USB_INFO
/// Attach the USB UART channel behind this port (from code generation)
void set_usb_channel(usb_uart::USBUartChannel *channel) { this->usb_channel_ = channel; }
#ifdef USE_API
/// Fill a USB info message for this port. The message's strings are views into info,
/// so info must outlive the send.
void get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyUsbInfo &msg) const;
#endif
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Attach a traffic observer. At most one, set once at setup time.
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
@@ -243,12 +218,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
bool tap_observing_() const;
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// The USB device behind this port was attached or removed. Reports the port's new USB
/// identity to every API client, and ends the tap's view of the old device.
void on_usb_connection_changed_(bool connected);
#endif
/// Instance index for identifying this proxy in API messages
uint32_t instance_index_{0};
@@ -282,11 +251,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
#ifdef USE_SERIAL_PROXY_TAP
SerialProxyTap *tap_{nullptr};
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// The USB UART channel behind this port; nullptr on non-USB ports
usb_uart::USBUartChannel *usb_channel_{nullptr};
#endif
};
} // namespace esphome::serial_proxy
+2 -1
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
from esphome.components import climate_ir, remote_base
import esphome.config_validation as cv
from esphome.const import CONF_MODEL
from esphome.types import ConfigType
@@ -26,5 +26,6 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).exten
async def to_code(config: ConfigType) -> None:
remote_base.request_protocol("toshiba_ac") # used from C++
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_model(config[CONF_MODEL]))
-9
View File
@@ -166,14 +166,6 @@ class UARTComponent {
// @return Baud rate in bits per second.
uint32_t get_baud_rate() const { return baud_rate_; }
// Requests hardware (RTS/CTS) flow control.
// @param flow_control True to request hardware flow control.
void set_flow_control(bool flow_control) { this->flow_control_ = flow_control; }
// Gets whether hardware flow control was requested.
// @return True when hardware flow control was requested.
bool get_flow_control() const { return this->flow_control_; }
#if defined(USE_ESP8266) || defined(USE_ESP32)
/**
* Load the UART settings.
@@ -221,7 +213,6 @@ class UARTComponent {
uint8_t stop_bits_{0};
uint8_t data_bits_{0};
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
bool flow_control_{false};
#ifdef USE_UART_DEBUGGER
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
#endif
@@ -160,6 +160,7 @@ void IDFUARTComponent::load_settings(bool dump_config) {
this->mark_failed();
return;
}
this->last_good_framing_ = this->framing_();
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
@@ -189,18 +190,9 @@ void IDFUARTComponent::load_settings(bool dump_config) {
setup_pin_if_needed(this->tx_pin_);
}
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;
}
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
invert |= UART_SIGNAL_RXD_INV;
}
if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) {
invert |= UART_SIGNAL_RTS_INV;
}
err = uart_set_line_inverse(this->uart_num_, invert);
// Must precede uart_set_pin() so an inverted TX line never shows the wrong idle
// level; apply_line_settings_() repeats it later for the reset registers.
err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_());
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
this->mark_failed();
@@ -214,25 +206,7 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
if (this->apply_line_settings_() != ESP_OK) {
this->mark_failed();
return;
}
@@ -250,6 +224,99 @@ void IDFUARTComponent::load_settings(bool dump_config) {
}
}
uint32_t IDFUARTComponent::line_inversion_mask_() {
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;
}
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
invert |= UART_SIGNAL_RXD_INV;
}
if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) {
invert |= UART_SIGNAL_RTS_INV;
}
return invert;
}
esp_err_t IDFUARTComponent::apply_line_settings_() {
// uart_param_config() resets these; call after every use of it.
esp_err_t err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_());
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
return err;
}
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
return err;
}
err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
return err;
}
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
void IDFUARTComponent::set_framing_(const Framing &framing) {
this->baud_rate_ = framing.baud_rate;
this->data_bits_ = framing.data_bits;
this->stop_bits_ = framing.stop_bits;
this->parity_ = framing.parity;
this->rx_full_threshold_ = framing.rx_full_threshold;
}
esp_err_t IDFUARTComponent::apply_settings_live() {
if (this->is_failed()) {
return ESP_ERR_INVALID_STATE;
}
// No driver yet: nothing to reconfigure in place.
if (!uart_is_driver_installed(this->uart_num_)) {
this->load_settings(false);
return this->is_failed() ? ESP_FAIL : ESP_OK;
}
// Keeps the driver ring buffers; flushes both hardware FIFOs (in-flight bytes lost).
uart_config_t uart_config = this->get_config_();
esp_err_t err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
// Failure leaves the registers reset; put back the last accepted framing so the
// getters still describe the hardware.
if (this->last_good_framing_.baud_rate == 0) {
ESP_LOGE(TAG, "uart_param_config (live) failed: %s; no previous framing to restore", esp_err_to_name(err));
this->mark_failed();
return err;
}
ESP_LOGW(TAG, "uart_param_config (live) failed: %s; restoring %" PRIu32 " baud", esp_err_to_name(err),
this->last_good_framing_.baud_rate);
this->set_framing_(this->last_good_framing_);
uart_config = this->get_config_();
esp_err_t restore_err = uart_param_config(this->uart_num_, &uart_config);
if (restore_err != ESP_OK) {
ESP_LOGE(TAG, "UART left unconfigured after failed live reconfigure: %s", esp_err_to_name(restore_err));
this->mark_failed();
return err;
}
// Previous framing is live again; report the refusal (line-setting errors log).
this->apply_line_settings_();
return err;
}
this->last_good_framing_ = this->framing_();
// The new framing is live; a line-setting failure here only logs.
this->apply_line_settings_();
return ESP_OK;
}
void IDFUARTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_);
LOG_PIN(" TX Pin: ", this->tx_pin_);
@@ -52,13 +52,49 @@ class IDFUARTComponent final : public UARTComponent, public Component {
void load_settings(bool dump_config) override;
using UARTComponent::load_settings; // also bring in the no-arg overload for convenience
/**
* Apply the current framing (baud rate, parity, data/stop bits) to the installed
* driver in place, without the delete/reinstall of load_settings(). Tasks blocked in
* the driver survive and the ring buffers are kept, but both hardware FIFOs are
* flushed: a frame in flight reaches the peer truncated and bytes not yet out of the
* RX FIFO are dropped. No lock is taken: quiesce writers first if that matters.
* rx_full_threshold is not rescaled (call set_rx_full_threshold_ms() first if it
* should follow the baud rate); a rollback restores the value from the last accepted
* configuration, undoing a standalone set_rx_full_threshold() made since. Without an
* installed driver this is a full load_settings(false) instead.
*
* @return ESP_OK once the new framing is live (a line-setting error after that only
* logs). On rejection (unreachable baud rate) the previous framing is restored and
* the driver's error returned; if the restore fails too the component is marked
* failed. ESP_ERR_INVALID_STATE if already failed; ESP_FAIL if the fallback
* load_settings() fails.
*/
esp_err_t apply_settings_live();
void on_shutdown() override;
protected:
void check_logger_conflict() override;
uint32_t line_inversion_mask_();
// Re-applies what uart_param_config() resets: inversion, RX threshold/timeout, mode.
esp_err_t apply_line_settings_();
uart_port_t uart_num_{UART_NUM_MAX};
uart_config_t get_config_();
struct Framing {
uint32_t baud_rate;
uint8_t data_bits;
uint8_t stop_bits;
UARTParityOptions parity;
size_t rx_full_threshold; // sized for the baud rate, so rolled back with it
};
Framing framing_() const {
return {this->baud_rate_, this->data_bits_, this->stop_bits_, this->parity_, this->rx_full_threshold_};
}
void set_framing_(const Framing &framing);
// Last framing the driver accepted; baud_rate 0 means none yet.
Framing last_good_framing_{};
bool has_peek_{false};
uint8_t peek_byte_;
uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).
@@ -7,15 +7,47 @@
#include "esphome/core/lock_free_queue.h"
#include "esphome/components/uart/uart_component.h"
#include <array>
#include <atomic>
#include <cstring>
#include <functional>
#include "freertos/ringbuf.h"
#include "esp_err.h"
#include "tinyusb_cdc_acm.h"
namespace esphome::usb_cdc_acm {
static const uint8_t EVENT_QUEUE_SIZE = 12;
// Drain up to out_buf_sz bytes from a byte ring buffer, handling FreeRTOS's wrapped
// case with a second read. Shared with the cdc_acm_uart bridge platform, whose worker
// tasks drain the same ring buffers.
inline esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
TickType_t x_ticks_to_wait) {
size_t read_sz;
uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, x_ticks_to_wait, out_buf_sz));
if (buf == nullptr) {
return ESP_FAIL;
}
memcpy(out_buf, buf, read_sz);
vRingbufferReturnItem(ring_buf, (void *) buf);
*rx_data_size = read_sz;
// Buffer's data can be wrapped, in which case we should perform another read
if (*rx_data_size < out_buf_sz) {
buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
if (buf != nullptr) {
memcpy(out_buf + *rx_data_size, buf, read_sz);
vRingbufferReturnItem(ring_buf, (void *) buf);
*rx_data_size += read_sz;
}
}
return ESP_OK;
}
// Callback types for line coding and line state changes
using LineCodingCallback = std::function<void(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits)>;
using LineStateCallback = std::function<void(bool dtr, bool rts)>;
@@ -103,6 +135,8 @@ class USBCDCACMInstance final : public uart::UARTComponent, public Parented<USBC
RingbufHandle_t usb_tx_ringbuf_{nullptr};
RingbufHandle_t usb_rx_ringbuf_{nullptr};
// TX task staging; a member rather than a stack array so it does not size the task stack.
std::array<uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE> usb_tx_staging_{};
// Non-zero while the TX task holds bytes it has pulled from the ring buffer but not
// yet handed to TinyUSB; lets flush() account for data that is in neither the ring
// buffer nor TinyUSB's FIFO.
@@ -104,30 +104,6 @@ static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *ev
instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits);
}
static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
TickType_t x_ticks_to_wait) {
size_t read_sz;
uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, x_ticks_to_wait, out_buf_sz));
if (buf == nullptr) {
return ESP_FAIL;
}
memcpy(out_buf, buf, read_sz);
vRingbufferReturnItem(ring_buf, (void *) buf);
*rx_data_size = read_sz;
// Buffer's data can be wrapped, in which case we should perform another read
buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
if (buf != nullptr) {
memcpy(out_buf + *rx_data_size, buf, read_sz);
vRingbufferReturnItem(ring_buf, (void *) buf);
*rx_data_size += read_sz;
}
return ESP_OK;
}
//==============================================================================
// USBCDCACMInstance Implementation
//==============================================================================
@@ -192,7 +168,7 @@ void USBCDCACMInstance::usb_tx_task_fn(void *arg) {
}
void USBCDCACMInstance::usb_tx_task() {
uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0};
uint8_t *data = this->usb_tx_staging_.data();
size_t tx_data_size = 0;
// Back-dated so a stall within the first LOG_THROTTLE_MS of uptime still logs
// immediately (unsigned arithmetic keeps this wrap-safe).
+2 -49
View File
@@ -26,8 +26,6 @@ USBClient = usb_host_ns.class_("USBClient", Component)
DOMAIN = "usb_host"
CONF_VID = "vid"
CONF_PID = "pid"
CONF_MANUFACTURER = "manufacturer"
CONF_PRODUCT = "product"
CONF_ENABLE_HUBS = "enable_hubs"
CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests"
CONF_MAX_PACKET_SIZE = "max_packet_size"
@@ -49,46 +47,7 @@ def usb_device_schema(
schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t})
else:
schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t})
return schema.extend(
{
cv.Optional(CONF_MANUFACTURER): cv.string_strict,
cv.Optional(CONF_PRODUCT): cv.string_strict,
}
)
_validate_filters_complete = cv.has_none_or_all_keys(CONF_MANUFACTURER, CONF_PRODUCT)
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
"""Reject invalid USB configuration."""
for config in configs:
_validate_filters_complete(config)
for index, first in enumerate(configs):
# Ensure matching logic does not overlap between entries
for second in configs[index + 1 :]:
# A zero VID or PID is a wildcard for that field, so only a differing non-zero
# value separates two entries
if not all(
first[key] == 0 or second[key] == 0 or first[key] == second[key]
for key in (CONF_VID, CONF_PID)
):
continue
# An unset filter constrains nothing, so only a differing value separates them
if not all(
(a := first.get(key)) is None
or (b := second.get(key)) is None
or a == b
for key in (CONF_MANUFACTURER, CONF_PRODUCT)
):
continue
raise cv.Invalid(
f"USB configs overlap: {first[CONF_ID]!r}, {second[CONF_ID]!r}"
)
return configs
return schema
def _set_max_packet_size(config: dict) -> dict:
@@ -113,9 +72,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAX_PACKET_SIZE, default=64): cv.one_of(
64, 128, 256, 512, 1024, int=True
),
cv.Optional(CONF_DEVICES): cv.All(
cv.ensure_list(usb_device_schema()), validate_usb_clients
),
cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
}
),
only_on_variant(
@@ -134,10 +91,6 @@ CONFIG_SCHEMA = cv.All(
async def register_usb_client(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID])
await cg.register_component(var, config)
if (manufacturer := config.get(CONF_MANUFACTURER)) is not None:
cg.add(var.set_manufacturer_filter(manufacturer))
if (product := config.get(CONF_PRODUCT)) is not None:
cg.add(var.set_product_filter(product))
return var
-70
View File
@@ -5,7 +5,6 @@
defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <vector>
#include "usb/usb_host.h"
#include <freertos/FreeRTOS.h>
@@ -13,7 +12,6 @@
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include <atomic>
#include <span>
namespace esphome::usb_host {
@@ -119,24 +117,6 @@ struct UsbEvent {
// callback function type.
// USB string descriptors hold at most 126 characters; one more for the terminator
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
/// Identity of a connected USB device, copied out of the descriptors the USB host
/// stack caches for the lifetime of the connection
struct UsbDeviceInfo {
uint16_t vendor_id;
uint16_t product_id;
uint16_t bcd_device;
char manufacturer[DESC_STRING_BUF_SIZE];
char product[DESC_STRING_BUF_SIZE];
char serial_number[DESC_STRING_BUF_SIZE];
};
/// Copy a USB string descriptor into a NUL-terminated buffer, dropping characters outside
/// Latin-1. A missing descriptor copies as an empty string.
void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer);
enum ClientState {
USB_CLIENT_INIT = 0,
USB_CLIENT_OPEN,
@@ -164,32 +144,6 @@ class USBClient : public Component {
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
const std::vector<uint8_t> &data = {});
/// Copy the connected device's identity out of the cached USB descriptors.
/// Returns false when no device is connected.
bool get_device_info(UsbDeviceInfo &info) const;
/// Read a string descriptor the host stack does not cache, an interface string say, into
/// buffer. Uses a transfer of its own: the pooled ones hold one packet, and a string
/// descriptor can be four times that. The callback runs on the USB task once buffer holds
/// the string (empty on failure). Returns false when nothing was started. One read at a
/// time; main loop only.
bool read_string_descriptor(uint8_t index, std::span<char, DESC_STRING_BUF_SIZE> buffer,
const transfer_cb_t &callback);
/// Narrow which device this client claims, beyond the VID/PID it was constructed
/// with, by requiring a descriptor string to match exactly.
void set_manufacturer_filter(const char *manufacturer) { this->manufacturer_filter_ = manufacturer; }
void set_product_filter(const char *product) { this->product_filter_ = product; }
/// Register a callback for the device this client claims being connected (true) or
/// removed (false). Fires only for a device that passed every filter and was fully
/// opened, so a device another client claims is never reported. Called from the main
/// loop: connected once the device is ready to use (a subclass may hold this back until
/// its own setup of the device has finished), removed after on_disconnected() has run.
template<typename F> void add_on_connection_callback(F &&callback) {
this->connection_callback_.add(std::forward<F>(callback));
}
// Lock-free event queue and pool for USB task to main loop communication
// Must be public for access from static callbacks
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
@@ -207,23 +161,11 @@ class USBClient : public Component {
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect();
virtual void on_connected() {}
/// Whether the device's descriptor strings satisfy every filter that is set.
bool descriptor_strings_match_(const usb_device_info_t &dev_info) const;
/// Whether the subclass reports the device as connected itself, once its own setup of
/// the device has finished, rather than as soon as the device has been opened
virtual bool reports_connection_itself() const { return false; }
/// Report the claimed device to the connection callbacks. Idempotent; a subclass that
/// reports itself calls this once the device is ready to use.
void report_connected_();
virtual void on_disconnected() {
// Reset all requests to available (all bits to 0)
this->trq_in_use_.store(0);
}
static void string_descriptor_callback(usb_transfer_t *xfer);
// USB task management
static void usb_task_fn(void *arg);
[[noreturn]] void usb_task_loop_() const;
@@ -231,11 +173,6 @@ class USBClient : public Component {
// Members ordered to minimize struct padding on 32-bit platforms
TransferRequest requests_[MAX_REQUESTS]{};
TaskHandle_t usb_task_handle_{nullptr};
// Dedicated transfer for read_string_descriptor(), allocated on first use and kept
usb_transfer_t *string_transfer_{nullptr};
transfer_cb_t string_callback_;
char *string_buffer_{nullptr};
std::atomic<bool> string_read_busy_{false};
usb_host_client_handle_t handle_{};
usb_device_handle_t device_handle_{};
int device_addr_{-1};
@@ -244,15 +181,8 @@ class USBClient : public Component {
// Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
// Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
std::atomic<trq_bitmask_t> trq_in_use_;
LazyCallbackManager<void(bool)> connection_callback_;
// Descriptor strings a device must report to be claimed; nullptr means no constraint
const char *manufacturer_filter_{nullptr};
const char *product_filter_{nullptr};
uint16_t vid_{};
uint16_t pid_{};
// Whether the connection callbacks were told about the current device, so a removal is
// only ever reported for a device that was reported connected
bool connection_reported_{false};
};
class USBHost final : public Component {
public:
+10 -162
View File
@@ -143,8 +143,10 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL);
}
#endif
// bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType),
// so character count = (bLength - 2) / 2.
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2)
return "(unspecified)";
@@ -160,127 +162,6 @@ static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<c
return buffer.data();
}
// A missing descriptor copies as an empty string, unlike the "(unspecified)"
// placeholder the logging helper above uses
void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
buffer[0] = '\0';
if (desc == nullptr || desc->bLength < 2)
return;
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
}
// Descriptor strings are UTF-16, so a character above Latin-1 can never match.
static bool descriptor_string_equals(const usb_str_desc_t *desc, const char *expected) {
const int char_count = (desc == nullptr || desc->bLength < 2) ? 0 : (desc->bLength - 2) / 2;
for (int i = 0; i != char_count; i++) {
const uint16_t c = desc->wData[i];
if (c >= 0x100 || expected[i] == '\0' || static_cast<char>(c) != expected[i])
return false;
}
return expected[char_count] == '\0';
}
bool USBClient::descriptor_strings_match_(const usb_device_info_t &dev_info) const {
if (this->manufacturer_filter_ != nullptr &&
!descriptor_string_equals(dev_info.str_desc_manufacturer, this->manufacturer_filter_))
return false;
if (this->product_filter_ != nullptr && !descriptor_string_equals(dev_info.str_desc_product, this->product_filter_))
return false;
return true;
}
bool USBClient::get_device_info(UsbDeviceInfo &info) const {
if (this->state_ != USB_CLIENT_CONNECTED)
return false;
const usb_device_desc_t *desc;
if (usb_host_get_device_descriptor(this->device_handle_, &desc) != ESP_OK)
return false;
info.vendor_id = desc->idVendor;
info.product_id = desc->idProduct;
info.bcd_device = desc->bcdDevice;
usb_device_info_t dev_info;
if (usb_host_device_info(this->device_handle_, &dev_info) != ESP_OK)
return false;
copy_descriptor_string(dev_info.str_desc_manufacturer, info.manufacturer);
copy_descriptor_string(dev_info.str_desc_product, info.product);
copy_descriptor_string(dev_info.str_desc_serial_num, info.serial_number);
return true;
}
static constexpr size_t MAX_STRING_DESC_SIZE = 255; // bLength is one byte
static constexpr uint16_t LANGID_EN_US = 0x0409;
// CALLBACK CONTEXT: USB task
void USBClient::string_descriptor_callback(usb_transfer_t *xfer) {
auto *client = static_cast<USBClient *>(xfer->context);
TransferStatus status{};
status.error_code = xfer->status;
status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
status.endpoint = xfer->bEndpointAddress;
// The buffer starts with the setup packet; the descriptor follows it
status.data = xfer->data_buffer + SETUP_PACKET_SIZE;
status.data_len =
xfer->actual_num_bytes > static_cast<int>(SETUP_PACKET_SIZE) ? xfer->actual_num_bytes - SETUP_PACKET_SIZE : 0;
const auto *desc = reinterpret_cast<const usb_str_desc_t *>(status.data);
// A short read leaves bLength claiming bytes that never arrived; treat it as missing
const bool complete = status.success && status.data_len >= 2 && desc->bLength <= status.data_len;
copy_descriptor_string(complete ? desc : nullptr,
std::span<char, DESC_STRING_BUF_SIZE>(client->string_buffer_, DESC_STRING_BUF_SIZE));
client->string_read_busy_.store(false);
if (client->string_callback_ != nullptr) {
client->string_callback_(status);
}
}
bool USBClient::read_string_descriptor(uint8_t index, std::span<char, DESC_STRING_BUF_SIZE> buffer,
const transfer_cb_t &callback) {
buffer[0] = '\0';
if (this->state_ != USB_CLIENT_CONNECTED || index == 0) {
return false;
}
if (this->string_transfer_ == nullptr) {
if (usb_host_transfer_alloc(SETUP_PACKET_SIZE + MAX_STRING_DESC_SIZE, 0, &this->string_transfer_) != ESP_OK) {
ESP_LOGE(TAG, "String descriptor transfer alloc failed");
return false;
}
this->string_transfer_->context = this;
this->string_transfer_->callback = string_descriptor_callback;
}
bool idle = false;
if (!this->string_read_busy_.compare_exchange_strong(idle, true)) {
ESP_LOGW(TAG, "String descriptor read already in progress");
return false;
}
auto *setup = reinterpret_cast<usb_setup_packet_t *>(this->string_transfer_->data_buffer);
setup->bmRequestType = USB_DIR_IN | USB_TYPE_STANDARD | USB_RECIP_DEVICE;
setup->bRequest = USB_B_REQUEST_GET_DESCRIPTOR;
setup->wValue = (USB_B_DESCRIPTOR_TYPE_STRING << 8) | index;
// The host stack read the device strings in US English; Linux asks for it first as well,
// so all three report the same text
setup->wIndex = LANGID_EN_US;
setup->wLength = MAX_STRING_DESC_SIZE;
this->string_transfer_->num_bytes = SETUP_PACKET_SIZE + MAX_STRING_DESC_SIZE;
this->string_transfer_->bEndpointAddress = USB_DIR_IN;
this->string_transfer_->device_handle = this->device_handle_;
this->string_buffer_ = buffer.data();
this->string_callback_ = callback;
auto err = usb_host_transfer_submit_control(this->handle_, this->string_transfer_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit string descriptor read, err=%s", esp_err_to_name(err));
this->string_read_busy_.store(false);
return false;
}
return true;
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
auto *client = static_cast<USBClient *>(ptr);
@@ -421,10 +302,12 @@ void USBClient::handle_open_state_() {
return;
}
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if ((this->vid_ != 0 && desc->idVendor != this->vid_) || (this->pid_ != 0 && desc->idProduct != this->pid_)) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
if (this->vid_ != 0 || this->pid_ != 0) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
}
}
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
@@ -433,16 +316,6 @@ void USBClient::handle_open_state_() {
this->disconnect();
return;
}
// Scoped so the buffers do not outlive this cold branch
if (!this->descriptor_strings_match_(dev_info)) {
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
ESP_LOGD(TAG, "Device does not match filter, closing. Manuf: %s; Prod: %s",
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
get_descriptor_string(dev_info.str_desc_product, buf_product));
this->disconnect();
return;
}
this->state_ = USB_CLIENT_CONNECTED;
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
@@ -463,18 +336,6 @@ void USBClient::handle_open_state_() {
usb_client_print_config_descriptor(config_desc, nullptr);
#endif
this->on_connected();
// on_connected() may have rejected the device (no usable interface, say) and closed it
if (this->state_ == USB_CLIENT_CONNECTED && !this->reports_connection_itself()) {
this->report_connected_();
}
}
void USBClient::report_connected_() {
if (this->state_ != USB_CLIENT_CONNECTED || this->connection_reported_) {
return;
}
this->connection_reported_ = true;
this->connection_callback_.call(true);
}
void USBClient::on_opened(uint8_t addr) {
@@ -545,10 +406,6 @@ TransferRequest *USBClient::get_trq_() {
}
void USBClient::disconnect() {
// Also reached for a device this client opened and then declined, or lost before it was
// ready; neither was reported as connected, so neither is reported as removed
const bool was_reported = this->connection_reported_;
this->connection_reported_ = false;
this->on_disconnected();
auto err = usb_host_device_close(this->handle_, this->device_handle_);
if (err != ESP_OK) {
@@ -557,9 +414,6 @@ void USBClient::disconnect() {
this->state_ = USB_CLIENT_INIT;
this->device_handle_ = nullptr;
this->device_addr_ = -1;
if (was_reported) {
this->connection_callback_.call(false);
}
}
// THREAD CONTEXT: Called from main loop thread only
@@ -703,12 +557,6 @@ void USBClient::dump_config() {
" Vendor id %04X\n"
" Product id %04X",
this->vid_, this->pid_);
if (this->manufacturer_filter_ != nullptr) {
ESP_LOGCONFIG(TAG, " Manufacturer %s", this->manufacturer_filter_);
}
if (this->product_filter_ != nullptr) {
ESP_LOGCONFIG(TAG, " Product %s", this->product_filter_);
}
}
// THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediately after transfer callback completes
+11 -25
View File
@@ -6,7 +6,6 @@ from esphome.components.usb_host import (
get_max_packet_size,
register_usb_client,
usb_device_schema,
validate_usb_clients,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -17,7 +16,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER,
CONF_ID,
)
from esphome.core import CORE, ID
from esphome.core import CORE
from esphome.cpp_types import Component
from esphome.types import ConfigType
@@ -28,16 +27,6 @@ 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, full_config: ConfigType) -> bool:
"""Return True if the given ID refers to a channel of a configured usb_uart device."""
return any(
channel[CONF_ID] == uart_id
for device in full_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,
@@ -155,19 +144,16 @@ def channel_schema(type_: "Type") -> cv.Schema:
)
CONFIG_SCHEMA = cv.All(
cv.ensure_list(
cv.typed_schema(
{
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
channel_schema(it)
)
for it in uart_types
},
upper=True,
)
),
validate_usb_clients,
CONFIG_SCHEMA = cv.ensure_list(
cv.typed_schema(
{
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
channel_schema(it)
)
for it in uart_types
},
upper=True,
)
)
+2 -3
View File
@@ -88,11 +88,10 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
continue;
}
// No communication interface: the data interface is the one a host binds to
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) {
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber, 0xFF, 0, data_desc->iInterface}});
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}});
} else {
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber, 0xFF, 0, data_desc->iInterface}});
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}});
}
}
return cdc_devs;
-3
View File
@@ -217,9 +217,6 @@ static optional<CdcEps> get_uart(const usb_config_desc_t *config_desc, uint8_t i
}
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
// No communication interface: the data interface is the one a host binds to
eps.interrupt_interface_number = 0xFF;
return eps;
}
+1 -2
View File
@@ -189,8 +189,7 @@ std::vector<CdcEps> USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev
}
if (in_ep && out_ep) {
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber,
intf->iInterface, intf->iInterface});
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber});
break; // PL2303 is single-port
}
}
+1 -53
View File
@@ -43,17 +43,14 @@ static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t in
if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) {
eps.notify_ep = ep;
eps.interrupt_interface_number = intf_desc->bInterfaceNumber;
eps.interrupt_interface_string_index = intf_desc->iInterface;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.in_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.out_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
} else {
ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
continue;
@@ -245,7 +242,7 @@ void USBUartComponent::loop() {
this->chunk_pool_.release(chunk);
// Invoke the RX callback (if registered) immediately after data lands in the
// ring buffer. This lets consumers such as ZigbeeProxyTap process incoming bytes
// ring buffer. This lets consumers such as ZigbeeProxy process incoming bytes
// in the same loop iteration they are delivered, avoiding an extra wakeup cycle.
if (channel->rx_callback_) {
channel->rx_callback_();
@@ -484,7 +481,6 @@ void USBUartTypeCdcAcm::on_disconnected() {
channel->input_started_.store(true);
channel->output_started_.store(true);
channel->input_buffer_.clear();
channel->interface_string_[0] = '\0';
// Drain any pending output chunks and return them to the pool
{
UsbOutputChunk *chunk;
@@ -563,38 +559,11 @@ void USBUartComponent::start_config_(bool reload) {
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_in_flight_ = false;
this->cfg_string_done_ = false;
this->cfg_string_in_flight_ = false;
this->cfg_done_.store(false);
this->cfg_active_ = true;
this->enable_loop();
}
bool USBUartComponent::fetch_interface_string_(USBUartChannelBase *channel) {
const CdcEps &eps = channel->cdc_dev_;
const uint8_t index =
eps.interrupt_interface_number != 0xFF ? eps.interrupt_interface_string_index : eps.bulk_interface_string_index;
channel->interface_string_[0] = '\0';
if (index == 0) {
return false;
}
this->cfg_done_.store(false);
const bool submitted =
this->read_string_descriptor(index, channel->interface_string_, [this](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGW(TAG, "Interface string read failed: %s", esp_err_to_name(status.error_code));
}
// Release: publishes the string before the loop observes cfg_done_.
this->cfg_done_.store(true, std::memory_order_release);
this->enable_loop_soon_any_context();
App.wake_loop_threadsafe();
});
if (!submitted) {
ESP_LOGW(TAG, "Interface string read submit failed");
}
return submitted;
}
void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
const std::vector<uint8_t> &data) {
this->cfg_done_.store(false);
@@ -628,14 +597,6 @@ bool USBUartComponent::run_config_machine_() {
if (!this->cfg_active_)
return false;
if (this->cfg_string_in_flight_) {
// Acquire: pairs with the release in fetch_interface_string_'s callback.
if (!this->cfg_done_.load(std::memory_order_acquire))
return false;
this->cfg_string_in_flight_ = false;
this->cfg_done_.store(false);
}
if (this->cfg_in_flight_) {
// Acquire: pairs with the release in config_transfer_'s callback.
if (!this->cfg_done_.load(std::memory_order_acquire))
@@ -666,16 +627,6 @@ bool USBUartComponent::run_config_machine_() {
? this->cfg_single_
: (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr);
// Once per channel on init, before its settings: the interface string is part of the
// identity reported to clients, and the connected report waits for this machine.
if (channel != nullptr && !this->cfg_reload_ && !this->cfg_string_done_) {
this->cfg_string_done_ = true;
if (this->fetch_interface_string_(channel)) {
this->cfg_string_in_flight_ = true;
return true;
}
}
if (channel != nullptr && channel->initialised_.load()) {
if (!this->cfg_ok_) {
// A previous step in this channel's sequence failed. Abort the rest. On a full init,
@@ -699,14 +650,11 @@ bool USBUartComponent::run_config_machine_() {
// Advance to the next channel (or finish).
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_string_done_ = false;
if (this->cfg_single_ != nullptr) {
this->cfg_active_ = false;
this->cfg_single_ = nullptr;
} else if (++this->cfg_channel_idx_ >= this->channels_.size()) {
this->cfg_active_ = false;
// Init is done and the line settings are on the wire: now the device is ready to use
this->report_connected_();
}
// If the machine just went idle and a reload was requested while it was busy, start it now.
+1 -28
View File
@@ -37,9 +37,6 @@ struct CdcEps {
// Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it
// must remain valid even when the interface itself is not claimed.
uint8_t interrupt_interface_number;
// iInterface of each interface; 0 when the device provides no string for it
uint8_t interrupt_interface_string_index;
uint8_t bulk_interface_string_index;
bool interrupt_interface_claimed{false};
};
@@ -166,30 +163,15 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
/// Register a callback invoked immediately after data is pushed to the input ring buffer.
/// Called from USBUartComponent::loop() in the main loop context.
/// Allows consumers (e.g. ZigbeeProxyTap) to process bytes in the same loop iteration
/// Allows consumers (e.g. ZigbeeProxy) to process bytes in the same loop iteration
/// they arrive, eliminating one full main-loop-wakeup cycle of latency.
void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); }
/// Channel index on the bridge
uint8_t get_index() const { return this->index_; }
/// USB interface number a host driver binds to for this channel: the communication
/// interface of a CDC ACM function, otherwise the data interface.
uint8_t get_interface_number() const {
return this->cdc_dev_.interrupt_interface_number != 0xFF ? this->cdc_dev_.interrupt_interface_number
: this->cdc_dev_.bulk_interface_number;
}
/// iInterface string of that interface; empty when the device has none, or until it has
/// been read after the device connected
const char *get_interface_string() const { return this->interface_string_; }
protected:
// Not directly instantiable; construct a concrete channel type instead.
USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
void check_logger_conflict() override {}
// Larger structures first (8+ bytes)
char interface_string_[usb_host::DESC_STRING_BUF_SIZE]{};
RingBuffer input_buffer_;
LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
@@ -259,9 +241,6 @@ class USBUartComponent : public usb_host::USBClient {
void start_config_(bool reload);
// Advance the config state machine; called from loop(). Returns true if it did work.
bool run_config_machine_();
// Ask the device for the channel's interface string. Returns true when a transfer was
// submitted; its completion is signalled through cfg_done_ like a config step's.
bool fetch_interface_string_(USBUartChannelBase *channel);
// Per-subclass per-channel settings sequence. For the given zero-based step, issue the
// next control transfer via config_transfer_() and return true, or return false when the
@@ -273,10 +252,6 @@ class USBUartComponent : public usb_host::USBClient {
// (e.g. CH34x chip detection). Same contract as config_step_(). Default: no steps.
virtual bool config_device_step(uint8_t step, bool ok, const uint8_t *response) { return false; }
// The device is only usable once the config machine has applied every channel's line
// settings, so the connected report waits for run_config_machine_() to finish the init
bool reports_connection_itself() const override { return true; }
std::vector<USBUartChannelBase *> channels_{};
// Config state machine
@@ -291,8 +266,6 @@ class USBUartComponent : public usb_host::USBClient {
bool cfg_device_phase_{false};
bool cfg_in_flight_{false};
bool cfg_ok_{true};
bool cfg_string_done_{false};
bool cfg_string_in_flight_{false};
};
class USBUartTypeCdcAcm : public USBUartComponent {
@@ -3,6 +3,8 @@
namespace esphome::web_server_base {
WebServerBase::WebServerBase() {}
WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void WebServerBase::add_handler(AsyncWebHandler *handler) {
@@ -112,6 +112,10 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
class WebServerBase final {
public:
// User provided, not "= default": `new(p) WebServerBase()` would zero-fill .bss that is already zero.
// Defined out of line so the member constructors are not inlined into every construction site.
WebServerBase();
// The AsyncWebServer is created once and intentionally never deleted: on Arduino
// platforms ESPAsyncWebServer owns its registered handlers, so destroying it would
// also destroy live components (e.g. the captive portal) out from under us.
+7
View File
@@ -12,6 +12,7 @@ from esphome.components.esp32 import (
get_esp32_variant,
only_on_variant,
request_wifi,
require_mbedtls_tls_extras,
)
from esphome.components.network import (
add_use_address,
@@ -658,6 +659,12 @@ async def to_code(config):
# Disable Enterprise WiFi support if no EAP is configured
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
if has_eap:
# wpa_supplicant's EAP client negotiates with whatever the RADIUS
# server offers, and a failed handshake leaves the device off the
# network, so keep every mbedTLS client feature the esp32 platform
# would otherwise trim.
require_mbedtls_tls_extras()
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
if has_manual_ip:
@@ -1,47 +0,0 @@
import esphome.codegen as cg
from esphome.components import serial_proxy
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["serial_proxy"]
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
zigbee_proxy_tap_ns = cg.esphome_ns.namespace("zigbee_proxy_tap")
ZigbeeProxyTap = zigbee_proxy_tap_ns.class_(
"ZigbeeProxyTap", cg.Component, serial_proxy.SerialProxyTap
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
if (wifi_conf := full_config.get(CONF_WIFI)) and (
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
):
raise cv.Invalid(
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
)
return config
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ZigbeeProxyTap),
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
var = cg.new_Pvariable(config[CONF_ID], sp)
await cg.register_component(var, config)
cg.add_define("USE_ZIGBEE_PROXY_TAP")
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
cg.add_define("USE_SERIAL_PROXY_TAP")

Some files were not shown because too many files have changed in this diff Show More