mirror of
https://github.com/esphome/esphome.git
synced 2026-09-19 11:08:38 +00:00
Merge branch 'scheduler-raw-pointers' into integration
This commit is contained in:
@@ -435,6 +435,7 @@ esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/serial_proxy/* @kbx81
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
|
||||
|
||||
@@ -2618,6 +2618,14 @@ enum SerialProxyRequestType {
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed
|
||||
SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed
|
||||
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
|
||||
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations
|
||||
message SerialProxyRequest {
|
||||
option (id) = 144;
|
||||
@@ -2628,6 +2636,18 @@ message SerialProxyRequest {
|
||||
SerialProxyRequestType type = 2; // Request type
|
||||
}
|
||||
|
||||
// Response to a SerialProxyRequest (e.g. flush completion or failure)
|
||||
message SerialProxyRequestResponse {
|
||||
option (id) = 147;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
SerialProxyRequestType type = 2; // Which request type this responds to
|
||||
SerialProxyStatus status = 3; // Result status
|
||||
string error_message = 4; // Additional detail on failure (optional)
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
|
||||
@@ -1445,6 +1445,89 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
|
||||
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance,
|
||||
static_cast<uint32_t>(proxies.size()));
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits,
|
||||
msg.data_size);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->write_from_client(msg.data, msg.data_len);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->set_modem_pins(msg.line_states);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
SerialProxyGetModemPinsResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
resp.line_states = proxies[msg.instance]->get_modem_pins();
|
||||
this->send_message(resp);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
switch (msg.type) {
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
proxies[msg.instance]->serial_proxy_request(this, msg.type);
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: {
|
||||
SerialProxyRequestResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH;
|
||||
switch (proxies[msg.instance]->flush_port()) {
|
||||
case uart::FlushResult::SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_OK;
|
||||
break;
|
||||
case uart::FlushResult::ASSUMED_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
|
||||
break;
|
||||
case uart::FlushResult::TIMEOUT:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
|
||||
break;
|
||||
case uart::FlushResult::FAILED:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
this->send_message(resp);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(msg.type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *infrared = static_cast<infrared::Infrared *>(entity);
|
||||
@@ -1666,6 +1749,16 @@ bool APIConnection::send_device_info_response_() {
|
||||
resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags();
|
||||
resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id();
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
size_t serial_proxy_index = 0;
|
||||
for (auto const &proxy : App.get_serial_proxies()) {
|
||||
if (serial_proxy_index >= SERIAL_PROXY_COUNT)
|
||||
break;
|
||||
auto &info = resp.serial_proxies[serial_proxy_index++];
|
||||
info.name = StringRef(proxy->get_name());
|
||||
info.port_type = proxy->get_port_type();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
|
||||
@@ -189,6 +189,15 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override;
|
||||
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override;
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override;
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override;
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg) override;
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event);
|
||||
#endif
|
||||
@@ -254,6 +263,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
|
||||
this->is_authenticated();
|
||||
}
|
||||
bool is_marked_for_removal() const { return this->flags_.remove; }
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection
|
||||
|
||||
@@ -3836,6 +3836,20 @@ bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||
buffer.encode_uint32(1, this->instance);
|
||||
buffer.encode_uint32(2, static_cast<uint32_t>(this->type));
|
||||
buffer.encode_uint32(3, static_cast<uint32_t>(this->status));
|
||||
buffer.encode_string(4, this->error_message);
|
||||
}
|
||||
uint32_t SerialProxyRequestResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->instance);
|
||||
size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type));
|
||||
size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->status));
|
||||
size += ProtoSize::calc_length(1, this->error_message.size());
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
|
||||
@@ -333,6 +333,13 @@ enum SerialProxyRequestType : uint32_t {
|
||||
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
|
||||
};
|
||||
enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_OK = 0,
|
||||
SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1,
|
||||
SERIAL_PROXY_STATUS_ERROR = 2,
|
||||
SERIAL_PROXY_STATUS_TIMEOUT = 3,
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
@@ -3219,6 +3226,25 @@ class SerialProxyRequest final : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class SerialProxyRequestResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 147;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "serial_proxy_request_response"; }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyRequestType type{};
|
||||
enums::SerialProxyStatus status{};
|
||||
StringRef error_message{};
|
||||
void encode(ProtoWriteBuffer &buffer) 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
|
||||
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
||||
|
||||
@@ -789,6 +789,22 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_STATUS_OK:
|
||||
return "SERIAL_PROXY_STATUS_OK";
|
||||
case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS:
|
||||
return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS";
|
||||
case enums::SERIAL_PROXY_STATUS_ERROR:
|
||||
return "SERIAL_PROXY_STATUS_ERROR";
|
||||
case enums::SERIAL_PROXY_STATUS_TIMEOUT:
|
||||
return "SERIAL_PROXY_STATUS_TIMEOUT";
|
||||
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
|
||||
return "SERIAL_PROXY_STATUS_NOT_SUPPORTED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -2608,6 +2624,14 @@ const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyRequestResponse");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
|
||||
dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status));
|
||||
dump_field(out, "error_message", this->error_message);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -233,6 +233,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Serial Proxy component for ESPHome.
|
||||
|
||||
WARNING: This component is EXPERIMENTAL. The API (both Python configuration
|
||||
and C++ interfaces) may change at any time without following the normal
|
||||
breaking changes policy. Use at your own risk.
|
||||
|
||||
Once the API is considered stable, this warning will be removed.
|
||||
|
||||
Provides a proxy to/from a serial interface on the ESPHome device, allowing
|
||||
Home Assistant to connect to the serial port and send/receive data to/from
|
||||
an arbitrary serial device.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
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
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["api", "uart"]
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
|
||||
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
|
||||
|
||||
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
|
||||
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
|
||||
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,
|
||||
}
|
||||
|
||||
CONF_DTR_PIN = "dtr_pin"
|
||||
CONF_PORT_TYPE = "port_type"
|
||||
CONF_RTS_PIN = "rts_pin"
|
||||
|
||||
DOMAIN = "serial_proxy"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SerialProxyData:
|
||||
count: int = 0
|
||||
|
||||
|
||||
def _get_data() -> SerialProxyData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = SerialProxyData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SerialProxy),
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
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,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_serial_proxy_count_define():
|
||||
"""Emit the SERIAL_PROXY_COUNT define once with the final instance count."""
|
||||
count = _get_data().count
|
||||
if count > 0:
|
||||
cg.add_define("SERIAL_PROXY_COUNT", count)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
cg.add(cg.App.register_serial_proxy(var))
|
||||
cg.add(var.set_name(config[CONF_NAME]))
|
||||
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
|
||||
cg.add_define("USE_SERIAL_PROXY")
|
||||
|
||||
# Track instance count for the FINAL priority define
|
||||
data = _get_data()
|
||||
if data.count == 0:
|
||||
# Schedule the count define job only once (on the first instance)
|
||||
CORE.add_job(_add_serial_proxy_count_define)
|
||||
data.count += 1
|
||||
|
||||
if CONF_RTS_PIN in config:
|
||||
rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN])
|
||||
cg.add(var.set_rts_pin(rts_pin))
|
||||
|
||||
if CONF_DTR_PIN in config:
|
||||
dtr_pin = await cg.gpio_pin_expression(config[CONF_DTR_PIN])
|
||||
cg.add(var.set_dtr_pin(dtr_pin))
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "serial_proxy.h"
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::serial_proxy {
|
||||
|
||||
static const char *const TAG = "serial_proxy";
|
||||
|
||||
void SerialProxy::setup() {
|
||||
// Set up modem control pins if configured
|
||||
if (this->rts_pin_ != nullptr) {
|
||||
this->rts_pin_->setup();
|
||||
this->rts_pin_->digital_write(this->rts_state_);
|
||||
}
|
||||
if (this->dtr_pin_ != nullptr) {
|
||||
this->dtr_pin_->setup();
|
||||
this->dtr_pin_->digital_write(this->dtr_state_);
|
||||
}
|
||||
#ifdef USE_API
|
||||
// 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
|
||||
}
|
||||
|
||||
void SerialProxy::loop() {
|
||||
#ifdef USE_API
|
||||
// Detect subscriber disconnect
|
||||
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
|
||||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->api_connection_ = nullptr;
|
||||
}
|
||||
|
||||
if (this->api_connection_ == nullptr)
|
||||
return;
|
||||
|
||||
// Read available data from UART and forward to subscribed client
|
||||
size_t available = this->available();
|
||||
if (available == 0)
|
||||
return;
|
||||
|
||||
// Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE
|
||||
uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE];
|
||||
size_t to_read = std::min(available, sizeof(buffer));
|
||||
|
||||
if (!this->read_array(buffer, to_read))
|
||||
return;
|
||||
|
||||
this->outgoing_msg_.set_data(buffer, to_read);
|
||||
this->api_connection_->send_serial_proxy_data(this->outgoing_msg_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SerialProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Serial Proxy [%u]:\n"
|
||||
" Name: %s\n"
|
||||
" Port Type: %s\n"
|
||||
" 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 ? "RS485"
|
||||
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232"
|
||||
: "TTL",
|
||||
this->rts_pin_ != nullptr ? "configured" : "not configured",
|
||||
this->dtr_pin_ != nullptr ? "configured" : "not configured");
|
||||
}
|
||||
|
||||
void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits,
|
||||
uint8_t data_size) {
|
||||
ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u",
|
||||
this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size);
|
||||
|
||||
auto *uart_comp = this->parent_;
|
||||
if (uart_comp == nullptr) {
|
||||
ESP_LOGE(TAG, "UART component not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all parameters before applying any (values come from a remote client)
|
||||
if (baudrate == 0) {
|
||||
ESP_LOGW(TAG, "Invalid baud rate: 0");
|
||||
return;
|
||||
}
|
||||
if (stop_bits < 1 || stop_bits > 2) {
|
||||
ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits);
|
||||
return;
|
||||
}
|
||||
if (data_size < 5 || data_size > 8) {
|
||||
ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size);
|
||||
return;
|
||||
}
|
||||
if (parity > 2) {
|
||||
ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply validated parameters
|
||||
uart_comp->set_baud_rate(baudrate);
|
||||
uart_comp->set_stop_bits(stop_bits);
|
||||
uart_comp->set_data_bits(data_size);
|
||||
|
||||
// Map parity value to UARTParityOptions
|
||||
static const uart::UARTParityOptions PARITY_MAP[] = {
|
||||
uart::UART_CONFIG_PARITY_NONE,
|
||||
uart::UART_CONFIG_PARITY_EVEN,
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
uart_comp->set_parity(PARITY_MAP[parity]);
|
||||
|
||||
// load_settings() is available on ESP8266 and ESP32 platforms
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
uart_comp->load_settings(true);
|
||||
#endif
|
||||
|
||||
if (flow_control) {
|
||||
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
|
||||
}
|
||||
}
|
||||
|
||||
void SerialProxy::write_from_client(const uint8_t *data, size_t len) {
|
||||
if (data == nullptr || len == 0)
|
||||
return;
|
||||
this->write_array(data, len);
|
||||
}
|
||||
|
||||
void SerialProxy::set_modem_pins(uint32_t line_states) {
|
||||
const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0;
|
||||
const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0;
|
||||
ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr));
|
||||
|
||||
if (this->rts_pin_ != nullptr) {
|
||||
this->rts_state_ = rts;
|
||||
this->rts_pin_->digital_write(rts);
|
||||
}
|
||||
if (this->dtr_pin_ != nullptr) {
|
||||
this->dtr_state_ = dtr;
|
||||
this->dtr_pin_->digital_write(dtr);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SerialProxy::get_modem_pins() const {
|
||||
return (this->rts_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_RTS : 0u) |
|
||||
(this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u);
|
||||
}
|
||||
|
||||
uart::FlushResult SerialProxy::flush_port() {
|
||||
ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_);
|
||||
return this->flush();
|
||||
}
|
||||
|
||||
#ifdef USE_API
|
||||
void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) {
|
||||
switch (type) {
|
||||
case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
if (this->api_connection_ != nullptr) {
|
||||
ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_);
|
||||
break;
|
||||
case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
if (this->api_connection_ != api_connection) {
|
||||
ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%u]", this->instance_index_);
|
||||
return;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::serial_proxy
|
||||
|
||||
#endif // USE_SERIAL_PROXY
|
||||
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
// WARNING: This component is EXPERIMENTAL. The API may change at any time
|
||||
// without following the normal breaking changes policy. Use at your own risk.
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
// Include api_pb2.h only when the API is enabled. The full include is needed
|
||||
// to hold SerialProxyDataReceived by value as a pre-allocated member.
|
||||
// Guarding prevents pulling conflicting Zephyr logging macro names into
|
||||
// translation units that include this header without USE_API defined.
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#endif
|
||||
|
||||
// Forward-declare types needed outside the USE_API guard.
|
||||
namespace esphome::api {
|
||||
class APIConnection;
|
||||
namespace enums {
|
||||
enum SerialProxyPortType : uint32_t;
|
||||
enum SerialProxyRequestType : uint32_t;
|
||||
} // namespace enums
|
||||
} // namespace esphome::api
|
||||
|
||||
namespace esphome::serial_proxy {
|
||||
|
||||
/// Bit flags for the line_states field exchanged with API clients.
|
||||
/// Bit positions are stable API — new signals must use the next available bit.
|
||||
enum SerialProxyLineStateFlag : uint32_t {
|
||||
SERIAL_PROXY_LINE_STATE_FLAG_RTS = 1 << 0, ///< RTS (Request To Send)
|
||||
SERIAL_PROXY_LINE_STATE_FLAG_DTR = 1 << 1, ///< DTR (Data Terminal Ready)
|
||||
};
|
||||
|
||||
/// Maximum bytes to read from UART in a single loop iteration
|
||||
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
|
||||
|
||||
class SerialProxy : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||
|
||||
/// Get the instance index (position in Application's serial_proxies_ vector)
|
||||
uint32_t get_instance_index() const { return this->instance_index_; }
|
||||
|
||||
/// Set the instance index (called by Application::register_serial_proxy)
|
||||
void set_instance_index(uint32_t index) { this->instance_index_ = index; }
|
||||
|
||||
/// Set the human-readable port name (from YAML configuration)
|
||||
void set_name(const char *name) { this->name_ = name; }
|
||||
|
||||
/// Get the human-readable port name
|
||||
const char *get_name() const { return this->name_; }
|
||||
|
||||
/// Set the port type (from YAML configuration)
|
||||
void set_port_type(api::enums::SerialProxyPortType port_type) { this->port_type_ = port_type; }
|
||||
|
||||
/// Get the port type
|
||||
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
|
||||
|
||||
/// Configure UART parameters and apply them
|
||||
/// @param baudrate Baud rate in bits per second
|
||||
/// @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)
|
||||
void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size);
|
||||
|
||||
/// Handle a subscribe/unsubscribe request from an API client
|
||||
void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type);
|
||||
|
||||
/// Write data received from an API client to the serial device
|
||||
/// @param data Pointer to data buffer
|
||||
/// @param len Number of bytes to write
|
||||
void write_from_client(const uint8_t *data, size_t len);
|
||||
|
||||
/// Set modem pin states from a bitmask of SerialProxyLineStateFlag values
|
||||
void set_modem_pins(uint32_t line_states);
|
||||
|
||||
/// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values
|
||||
uint32_t get_modem_pins() const;
|
||||
|
||||
/// Flush the serial port (block until all TX data is sent)
|
||||
uart::FlushResult flush_port();
|
||||
|
||||
/// Set the RTS GPIO pin (from YAML configuration)
|
||||
void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; }
|
||||
|
||||
/// Set the DTR GPIO pin (from YAML configuration)
|
||||
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
/// Instance index for identifying this proxy in API messages
|
||||
uint32_t instance_index_{0};
|
||||
|
||||
/// Subscribed API client (only one allowed at a time)
|
||||
api::APIConnection *api_connection_{nullptr};
|
||||
|
||||
#ifdef USE_API
|
||||
/// Pre-allocated outgoing message; instance field is set once in setup()
|
||||
api::SerialProxyDataReceived outgoing_msg_;
|
||||
#endif
|
||||
|
||||
/// Human-readable port name (points to a string literal in flash)
|
||||
const char *name_{nullptr};
|
||||
|
||||
/// Port type
|
||||
api::enums::SerialProxyPortType port_type_{};
|
||||
|
||||
/// Optional GPIO pins for modem control
|
||||
GPIOPin *rts_pin_{nullptr};
|
||||
GPIOPin *dtr_pin_{nullptr};
|
||||
|
||||
/// Current modem pin states
|
||||
bool rts_state_{false};
|
||||
bool dtr_state_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::serial_proxy
|
||||
|
||||
#endif // USE_SERIAL_PROXY
|
||||
@@ -105,6 +105,9 @@ void socket_wake(); // NOLINT(readability-redundant-declaration)
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
#include "esphome/components/serial_proxy/serial_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
#include "esphome/components/event/event.h"
|
||||
#endif
|
||||
@@ -382,6 +385,13 @@ class Application {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void register_serial_proxy(serial_proxy::SerialProxy *proxy) {
|
||||
proxy->set_instance_index(this->serial_proxies_.size());
|
||||
this->serial_proxies_.push_back(proxy);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void register_event(event::Event *event) {
|
||||
this->events_.push_back(event);
|
||||
@@ -623,6 +633,10 @@ class Application {
|
||||
GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
auto &get_serial_proxies() const { return this->serial_proxies_; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
auto &get_events() const { return this->events_; }
|
||||
GET_ENTITY_METHOD(event::Event, event, events)
|
||||
@@ -888,6 +902,9 @@ class Application {
|
||||
#ifdef USE_INFRARED
|
||||
StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
StaticVector<serial_proxy::SerialProxy *, SERIAL_PROXY_COUNT> serial_proxies_{};
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
|
||||
#endif
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
#define MDNS_SERVICE_COUNT 3
|
||||
#define USE_MDNS_DYNAMIC_TXT
|
||||
#define MDNS_DYNAMIC_TXT_COUNT 2
|
||||
#define SERIAL_PROXY_COUNT 2
|
||||
#define SNTP_SERVER_COUNT 3
|
||||
#define USE_MEDIA_PLAYER
|
||||
#define USE_MEDIA_SOURCE
|
||||
@@ -120,6 +121,7 @@
|
||||
#define USE_SENSOR
|
||||
#define USE_SETUP_HEAP_STATS
|
||||
#define USE_SENSOR_FILTER
|
||||
#define USE_SERIAL_PROXY
|
||||
#define USE_SETUP_PRIORITY_OVERRIDE
|
||||
#define USE_STATUS_LED
|
||||
#define USE_STATUS_SENSOR
|
||||
|
||||
+112
-67
@@ -30,11 +30,6 @@ static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5;
|
||||
// max delay to start an interval sequence
|
||||
static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
|
||||
|
||||
// Prevent inlining of SchedulerItem deletion. On BK7231N (Thumb-1), GCC inlines
|
||||
// ~unique_ptr<SchedulerItem> (~30 bytes each) at every destruction site. Defining
|
||||
// the deleter in the .cpp file ensures a single copy of the destructor + operator delete.
|
||||
void Scheduler::SchedulerItemDeleter::operator()(SchedulerItem *ptr) const noexcept { delete ptr; }
|
||||
|
||||
#if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER)
|
||||
// Helper struct for formatting scheduler item names consistently in logs
|
||||
// Uses a stack buffer to avoid heap allocation
|
||||
@@ -122,8 +117,8 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) {
|
||||
bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id) {
|
||||
for (auto *container : {&this->items_, &this->to_add_}) {
|
||||
for (auto &item : *container) {
|
||||
if (item && this->is_item_removed_locked_(item.get()) &&
|
||||
for (auto *item : *container) {
|
||||
if (item != nullptr && this->is_item_removed_locked_(item) &&
|
||||
this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT,
|
||||
/* match_retry= */ true, /* skip_removed= */ false)) {
|
||||
return true;
|
||||
@@ -147,17 +142,31 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
return;
|
||||
}
|
||||
|
||||
// Take lock early to protect scheduler_item_pool_ access
|
||||
// Take lock early to protect scheduler_item_pool_ access and retry-cancelled check
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// For retries, check if there's a cancelled timeout first - before allocating an item.
|
||||
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
|
||||
// Skip check for defer (delay=0) - deferred retries bypass the cancellation check
|
||||
if (is_retry && delay != 0 && (name_type != NameType::STATIC_STRING || static_name != nullptr) &&
|
||||
type == SchedulerItem::TIMEOUT &&
|
||||
this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) {
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
SchedulerNameLog skip_name_log;
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
|
||||
skip_name_log.format(name_type, static_name, hash_or_id));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and populate the scheduler item
|
||||
auto item = this->get_item_from_pool_locked_();
|
||||
SchedulerItem *item = this->get_item_from_pool_locked_();
|
||||
item->component = component;
|
||||
item->set_name(name_type, static_name, hash_or_id);
|
||||
item->type = type;
|
||||
item->callback = std::move(func);
|
||||
// Reset remove flag - recycled items may have been cancelled (remove=true) in previous use
|
||||
this->set_item_removed_(item.get(), false);
|
||||
this->set_item_removed_(item, false);
|
||||
item->is_retry = is_retry;
|
||||
|
||||
// Determine target container: defer_queue_ for deferred items, to_add_ for everything else.
|
||||
@@ -193,29 +202,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64);
|
||||
this->debug_log_timer_(item, name_type, static_name, hash_or_id, type, delay, now_64);
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
|
||||
if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) &&
|
||||
type == SchedulerItem::TIMEOUT &&
|
||||
this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) {
|
||||
// Skip scheduling - the retry was cancelled
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
SchedulerNameLog skip_name_log;
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
|
||||
skip_name_log.format(name_type, static_name, hash_or_id));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
|
||||
if (!skip_cancel) {
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
|
||||
}
|
||||
target->push_back(std::move(item));
|
||||
target->push_back(item);
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout,
|
||||
@@ -395,7 +390,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||
if (this->cleanup_() == 0)
|
||||
return {};
|
||||
|
||||
auto &item = this->items_[0];
|
||||
SchedulerItem *item = this->items_[0];
|
||||
const auto now_64 = this->millis_64_from_(now);
|
||||
const uint64_t next_exec = item->get_next_execution();
|
||||
if (next_exec < now_64)
|
||||
@@ -414,13 +409,13 @@ void Scheduler::full_cleanup_removed_items_() {
|
||||
// Compact in-place: move valid items forward, recycle removed ones
|
||||
size_t write = 0;
|
||||
for (size_t read = 0; read < this->items_.size(); ++read) {
|
||||
if (!is_item_removed_locked_(this->items_[read].get())) {
|
||||
if (!is_item_removed_locked_(this->items_[read])) {
|
||||
if (write != read) {
|
||||
this->items_[write] = std::move(this->items_[read]);
|
||||
this->items_[write] = this->items_[read];
|
||||
}
|
||||
++write;
|
||||
} else {
|
||||
this->recycle_item_main_loop_(std::move(this->items_[read]));
|
||||
this->recycle_item_main_loop_(this->items_[read]);
|
||||
}
|
||||
}
|
||||
this->items_.erase(this->items_.begin() + write, this->items_.end());
|
||||
@@ -444,7 +439,7 @@ void Scheduler::compact_defer_queue_locked_() {
|
||||
// and recycled on the next loop iteration.
|
||||
size_t remaining = this->defer_queue_.size() - this->defer_queue_front_;
|
||||
for (size_t i = 0; i < remaining; i++) {
|
||||
this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]);
|
||||
this->defer_queue_[i] = this->defer_queue_[this->defer_queue_front_ + i];
|
||||
}
|
||||
// Use erase() instead of resize() to avoid instantiating _M_default_append
|
||||
// (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed.
|
||||
@@ -469,26 +464,26 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
|
||||
if (now_64 - last_print > 2000) {
|
||||
last_print = now_64;
|
||||
std::vector<SchedulerItemPtr> old_items;
|
||||
std::vector<SchedulerItem *> old_items;
|
||||
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(),
|
||||
now_64);
|
||||
// Cleanup before debug output
|
||||
this->cleanup_();
|
||||
while (!this->items_.empty()) {
|
||||
SchedulerItemPtr item;
|
||||
SchedulerItem *item;
|
||||
{
|
||||
LockGuard guard{this->lock_};
|
||||
item = this->pop_raw_locked_();
|
||||
}
|
||||
|
||||
SchedulerNameLog name_log;
|
||||
bool is_cancelled = is_item_removed_(item.get());
|
||||
bool is_cancelled = is_item_removed_(item);
|
||||
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s",
|
||||
item->get_type_str(), LOG_STR_ARG(item->get_source()),
|
||||
name_log.format(item->get_name_type(), item->get_name(), item->get_name_hash_or_id()), item->interval,
|
||||
item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : "");
|
||||
|
||||
old_items.push_back(std::move(item));
|
||||
old_items.push_back(item);
|
||||
}
|
||||
ESP_LOGD(TAG, "\n");
|
||||
|
||||
@@ -512,7 +507,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
}
|
||||
while (!this->items_.empty()) {
|
||||
// Don't copy-by value yet
|
||||
auto &item = this->items_[0];
|
||||
SchedulerItem *item = this->items_[0];
|
||||
if (item->get_next_execution() > now_64) {
|
||||
// Not reached timeout yet, done for this call
|
||||
break;
|
||||
@@ -532,7 +527,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
// Multi-threaded platforms without atomics: must take lock to safely read remove flag
|
||||
{
|
||||
LockGuard guard{this->lock_};
|
||||
if (is_item_removed_locked_(item.get())) {
|
||||
if (is_item_removed_locked_(item)) {
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
this->to_remove_--;
|
||||
continue;
|
||||
@@ -540,7 +535,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
}
|
||||
#else
|
||||
// Single-threaded or multi-threaded with atomics: can check without lock
|
||||
if (is_item_removed_(item.get())) {
|
||||
if (is_item_removed_(item)) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
this->to_remove_--;
|
||||
@@ -561,18 +556,18 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
// Warning: During callback(), a lot of stuff can happen, including:
|
||||
// - timeouts/intervals get added, potentially invalidating vector pointers
|
||||
// - timeouts/intervals get cancelled
|
||||
now = this->execute_item_(item.get(), now);
|
||||
now = this->execute_item_(item, now);
|
||||
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// Only pop after function call, this ensures we were reachable
|
||||
// during the function call and know if we were cancelled.
|
||||
auto executed_item = this->pop_raw_locked_();
|
||||
SchedulerItem *executed_item = this->pop_raw_locked_();
|
||||
|
||||
if (this->is_item_removed_locked_(executed_item.get())) {
|
||||
if (this->is_item_removed_locked_(executed_item)) {
|
||||
// We were removed/cancelled in the function call, recycle and continue
|
||||
this->to_remove_--;
|
||||
this->recycle_item_main_loop_(std::move(executed_item));
|
||||
this->recycle_item_main_loop_(executed_item);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -580,10 +575,10 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
executed_item->set_next_execution(now_64 + executed_item->interval);
|
||||
// Add new item directly to to_add_
|
||||
// since we have the lock held
|
||||
this->to_add_.push_back(std::move(executed_item));
|
||||
this->to_add_.push_back(executed_item);
|
||||
} else {
|
||||
// Timeout completed - recycle it
|
||||
this->recycle_item_main_loop_(std::move(executed_item));
|
||||
this->recycle_item_main_loop_(executed_item);
|
||||
}
|
||||
|
||||
has_added_items |= !this->to_add_.empty();
|
||||
@@ -592,17 +587,33 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
if (has_added_items) {
|
||||
this->process_to_add();
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
// Verify no items were leaked during this call() cycle.
|
||||
// All items must be in items_, to_add_, defer_queue_, or the pool.
|
||||
// Safe to check here because:
|
||||
// - process_defer_queue_ has already run its cleanup_defer_queue_locked_(),
|
||||
// so defer_queue_ contains no nullptr slots inflating the count.
|
||||
// - The while loop above has finished, so no items are held in local variables;
|
||||
// every item has been returned to a container (items_, to_add_, or pool).
|
||||
// Lock needed to get a consistent snapshot of all containers.
|
||||
{
|
||||
LockGuard guard{this->lock_};
|
||||
this->debug_verify_no_leak_();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void HOT Scheduler::process_to_add() {
|
||||
LockGuard guard{this->lock_};
|
||||
for (auto &it : this->to_add_) {
|
||||
if (is_item_removed_locked_(it.get())) {
|
||||
for (auto *&it : this->to_add_) {
|
||||
if (is_item_removed_locked_(it)) {
|
||||
// Recycle cancelled items
|
||||
this->recycle_item_main_loop_(std::move(it));
|
||||
this->recycle_item_main_loop_(it);
|
||||
it = nullptr;
|
||||
continue;
|
||||
}
|
||||
|
||||
this->items_.push_back(std::move(it));
|
||||
this->items_.push_back(it);
|
||||
std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
}
|
||||
this->to_add_.clear();
|
||||
@@ -628,20 +639,18 @@ size_t HOT Scheduler::cleanup_() {
|
||||
// leading to race conditions
|
||||
LockGuard guard{this->lock_};
|
||||
while (!this->items_.empty()) {
|
||||
auto &item = this->items_[0];
|
||||
if (!this->is_item_removed_locked_(item.get()))
|
||||
SchedulerItem *item = this->items_[0];
|
||||
if (!this->is_item_removed_locked_(item))
|
||||
break;
|
||||
this->to_remove_--;
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
}
|
||||
return this->items_.size();
|
||||
}
|
||||
Scheduler::SchedulerItemPtr HOT Scheduler::pop_raw_locked_() {
|
||||
Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() {
|
||||
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
|
||||
// Move the item out before popping - this is the item that was at the front of the heap
|
||||
auto item = std::move(this->items_.back());
|
||||
|
||||
SchedulerItem *item = this->items_.back();
|
||||
this->items_.pop_back();
|
||||
return item;
|
||||
}
|
||||
@@ -699,7 +708,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
|
||||
return total_cancelled > 0;
|
||||
}
|
||||
|
||||
bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) {
|
||||
bool HOT Scheduler::SchedulerItem::cmp(SchedulerItem *a, SchedulerItem *b) {
|
||||
// High bits are almost always equal (change only on 32-bit rollover ~49 days)
|
||||
// Optimize for common case: check low bits first when high bits are equal
|
||||
return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_)
|
||||
@@ -710,23 +719,26 @@ bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const Schedule
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
// This protects scheduler_item_pool_ from concurrent access by other threads
|
||||
// that may be acquiring items from the pool in set_timer_common_().
|
||||
void Scheduler::recycle_item_main_loop_(SchedulerItemPtr item) {
|
||||
if (!item)
|
||||
void Scheduler::recycle_item_main_loop_(SchedulerItem *item) {
|
||||
if (item == nullptr)
|
||||
return;
|
||||
|
||||
if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) {
|
||||
// Clear callback to release captured resources
|
||||
item->callback = nullptr;
|
||||
this->scheduler_item_pool_.push_back(std::move(item));
|
||||
this->scheduler_item_pool_.push_back(item);
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size());
|
||||
#endif
|
||||
} else {
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size());
|
||||
#endif
|
||||
delete item;
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
this->debug_live_items_--;
|
||||
#endif
|
||||
}
|
||||
// else: unique_ptr will delete the item when it goes out of scope
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
@@ -753,21 +765,54 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type,
|
||||
|
||||
// Helper to get or create a scheduler item from the pool
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
Scheduler::SchedulerItemPtr Scheduler::get_item_from_pool_locked_() {
|
||||
SchedulerItemPtr item;
|
||||
Scheduler::SchedulerItem *Scheduler::get_item_from_pool_locked_() {
|
||||
if (!this->scheduler_item_pool_.empty()) {
|
||||
item = std::move(this->scheduler_item_pool_.back());
|
||||
SchedulerItem *item = this->scheduler_item_pool_.back();
|
||||
this->scheduler_item_pool_.pop_back();
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size());
|
||||
#endif
|
||||
} else {
|
||||
item = SchedulerItemPtr(new SchedulerItem());
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Allocated new item (pool empty)");
|
||||
#endif
|
||||
return item;
|
||||
}
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Allocated new item (pool empty)");
|
||||
#endif
|
||||
auto *item = new SchedulerItem();
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
this->debug_live_items_++;
|
||||
#endif
|
||||
return item;
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
bool Scheduler::debug_verify_no_leak_() const {
|
||||
// Invariant: every live SchedulerItem must be in exactly one container.
|
||||
// debug_live_items_ tracks allocations minus deletions.
|
||||
size_t accounted = this->items_.size() + this->to_add_.size() + this->scheduler_item_pool_.size();
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
accounted += this->defer_queue_.size();
|
||||
#endif
|
||||
if (accounted != this->debug_live_items_) {
|
||||
ESP_LOGE(TAG,
|
||||
"SCHEDULER LEAK DETECTED: live=%" PRIu32 " but accounted=%" PRIu32 " (items=%" PRIu32 " to_add=%" PRIu32
|
||||
" pool=%" PRIu32
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
" defer=%" PRIu32
|
||||
#endif
|
||||
")",
|
||||
static_cast<uint32_t>(this->debug_live_items_), static_cast<uint32_t>(accounted),
|
||||
static_cast<uint32_t>(this->items_.size()), static_cast<uint32_t>(this->to_add_.size()),
|
||||
static_cast<uint32_t>(this->scheduler_item_pool_.size())
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
,
|
||||
static_cast<uint32_t>(this->defer_queue_.size())
|
||||
#endif
|
||||
);
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
+44
-50
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
@@ -144,19 +143,6 @@ class Scheduler {
|
||||
};
|
||||
|
||||
protected:
|
||||
struct SchedulerItem;
|
||||
|
||||
// Custom deleter for SchedulerItem unique_ptr that prevents the compiler from
|
||||
// inlining the destructor at every destruction site. On BK7231N (Thumb-1), GCC
|
||||
// inlines ~unique_ptr<SchedulerItem> (~30 bytes: null check + ~std::function +
|
||||
// operator delete) at every destruction site, while ESP32/ESP8266/RTL8720CF outline
|
||||
// it into a single helper. This noinline deleter ensures only one copy exists.
|
||||
// operator() is defined in scheduler.cpp to prevent inlining.
|
||||
struct SchedulerItemDeleter {
|
||||
void operator()(SchedulerItem *ptr) const noexcept;
|
||||
};
|
||||
using SchedulerItemPtr = std::unique_ptr<SchedulerItem, SchedulerItemDeleter>;
|
||||
|
||||
struct SchedulerItem {
|
||||
// Ordered by size to minimize padding
|
||||
Component *component;
|
||||
@@ -216,14 +202,14 @@ class Scheduler {
|
||||
name_.static_name = nullptr;
|
||||
}
|
||||
|
||||
// Destructor - no dynamic memory to clean up
|
||||
// Destructor - no dynamic memory to clean up (callback's std::function handles its own)
|
||||
~SchedulerItem() = default;
|
||||
|
||||
// Delete copy operations to prevent accidental copies
|
||||
SchedulerItem(const SchedulerItem &) = delete;
|
||||
SchedulerItem &operator=(const SchedulerItem &) = delete;
|
||||
|
||||
// Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly
|
||||
// Delete move operations: SchedulerItem objects are managed via raw pointers, never moved directly
|
||||
SchedulerItem(SchedulerItem &&) = delete;
|
||||
SchedulerItem &operator=(SchedulerItem &&) = delete;
|
||||
|
||||
@@ -247,7 +233,7 @@ class Scheduler {
|
||||
name_type_ = type;
|
||||
}
|
||||
|
||||
static bool cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b);
|
||||
static bool cmp(SchedulerItem *a, SchedulerItem *b);
|
||||
|
||||
// Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
|
||||
// The upper 16 bits of the 64-bit value are always zero, which is fine since
|
||||
@@ -298,12 +284,13 @@ class Scheduler {
|
||||
// Returns the number of items remaining after cleanup
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
size_t cleanup_();
|
||||
// Remove and return the front item from the heap
|
||||
// Remove and return the front item from the heap as a raw pointer.
|
||||
// Caller takes ownership and must either recycle or delete the item.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
SchedulerItemPtr pop_raw_locked_();
|
||||
SchedulerItem *pop_raw_locked_();
|
||||
// Get or create a scheduler item from the pool
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
SchedulerItemPtr get_item_from_pool_locked_();
|
||||
SchedulerItem *get_item_from_pool_locked_();
|
||||
|
||||
private:
|
||||
// Helper to cancel items - must be called with lock held
|
||||
@@ -327,19 +314,16 @@ class Scheduler {
|
||||
// Helper function to check if item matches criteria for cancellation
|
||||
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
inline bool HOT matches_item_locked_(const SchedulerItemPtr &item, Component *component, NameType name_type,
|
||||
inline bool HOT matches_item_locked_(SchedulerItem *item, Component *component, NameType name_type,
|
||||
const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type,
|
||||
bool match_retry, bool skip_removed = true) const {
|
||||
// THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded
|
||||
// platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries.
|
||||
// PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_()), but this check
|
||||
// provides defense-in-depth: helper
|
||||
// functions should be safe regardless of caller behavior.
|
||||
// platforms, items can be nulled in defer_queue_ during processing.
|
||||
// Fixes: https://github.com/esphome/esphome/issues/11940
|
||||
if (!item)
|
||||
if (item == nullptr)
|
||||
return false;
|
||||
if (item->component != component || item->type != type ||
|
||||
(skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) {
|
||||
if (item->component != component || item->type != type || (skip_removed && this->is_item_removed_locked_(item)) ||
|
||||
(match_retry && !item->is_retry)) {
|
||||
return false;
|
||||
}
|
||||
// Name type must match
|
||||
@@ -361,10 +345,12 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Helper to recycle a SchedulerItem back to the pool.
|
||||
// Takes a raw pointer — caller transfers ownership. The item is either added to the
|
||||
// pool or deleted if the pool is full.
|
||||
// IMPORTANT: Only call from main loop context! Recycling clears the callback,
|
||||
// so calling from another thread while the callback is executing causes use-after-free.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
void recycle_item_main_loop_(SchedulerItemPtr item);
|
||||
void recycle_item_main_loop_(SchedulerItem *item);
|
||||
|
||||
// Helper to perform full cleanup when too many items are cancelled
|
||||
void full_cleanup_removed_items_();
|
||||
@@ -420,27 +406,28 @@ class Scheduler {
|
||||
// Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total),
|
||||
// recycle each item after re-acquiring the lock for the next iteration (N+1 total).
|
||||
// The lock is held across: recycle → loop condition → move-out, then released for execution.
|
||||
SchedulerItemPtr item;
|
||||
SchedulerItem *item;
|
||||
|
||||
this->lock_.lock();
|
||||
while (this->defer_queue_front_ < defer_queue_end) {
|
||||
// SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_.
|
||||
// This is intentional and safe because:
|
||||
// Take ownership of the item, leaving nullptr in the vector slot.
|
||||
// This is safe because:
|
||||
// 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_)
|
||||
// 3. The lock protects concurrent access, but the nullptr remains until cleanup
|
||||
item = std::move(this->defer_queue_[this->defer_queue_front_]);
|
||||
item = this->defer_queue_[this->defer_queue_front_];
|
||||
this->defer_queue_[this->defer_queue_front_] = nullptr;
|
||||
this->defer_queue_front_++;
|
||||
this->lock_.unlock();
|
||||
|
||||
// Execute callback without holding lock to prevent deadlocks
|
||||
// if the callback tries to call defer() again
|
||||
if (!this->should_skip_item_(item.get())) {
|
||||
now = this->execute_item_(item.get(), now);
|
||||
if (!this->should_skip_item_(item)) {
|
||||
now = this->execute_item_(item, now);
|
||||
}
|
||||
|
||||
this->lock_.lock();
|
||||
this->recycle_item_main_loop_(std::move(item));
|
||||
this->recycle_item_main_loop_(item);
|
||||
}
|
||||
// Clean up the queue (lock already held from last recycle or initial acquisition)
|
||||
this->cleanup_defer_queue_locked_();
|
||||
@@ -520,18 +507,14 @@ class Scheduler {
|
||||
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
|
||||
// Returns the number of items marked for removal
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
__attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItemPtr> &container,
|
||||
__attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container,
|
||||
Component *component, NameType name_type,
|
||||
const char *static_name, uint32_t hash_or_id,
|
||||
SchedulerItem::Type type, bool match_retry) {
|
||||
size_t count = 0;
|
||||
for (auto &item : container) {
|
||||
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
|
||||
// The defer_queue_ uses index-based processing: items are std::moved out but left in the
|
||||
// vector as nullptr until cleanup. Even though this function is called with lock held,
|
||||
// the vector can still contain nullptr items from the processing loop. This check prevents crashes.
|
||||
if (item && this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
|
||||
this->set_item_removed_(item.get(), true);
|
||||
for (auto *item : container) {
|
||||
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
|
||||
this->set_item_removed_(item, true);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -539,15 +522,15 @@ class Scheduler {
|
||||
}
|
||||
|
||||
Mutex lock_;
|
||||
std::vector<SchedulerItemPtr> items_;
|
||||
std::vector<SchedulerItemPtr> to_add_;
|
||||
std::vector<SchedulerItem *> items_;
|
||||
std::vector<SchedulerItem *> to_add_;
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Single-core platforms don't need the defer queue and save ~32 bytes of RAM
|
||||
// Using std::vector instead of std::deque avoids 512-byte chunked allocations
|
||||
// Index tracking avoids O(n) erase() calls when draining the queue each loop
|
||||
std::vector<SchedulerItemPtr> defer_queue_; // FIFO queue for defer() calls
|
||||
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls
|
||||
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
uint32_t to_remove_{0};
|
||||
|
||||
// Memory pool for recycling SchedulerItem objects to reduce heap churn.
|
||||
@@ -558,7 +541,18 @@ class Scheduler {
|
||||
// - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation
|
||||
// can stall the entire system, causing timing issues and dropped events for any components that need
|
||||
// to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
|
||||
std::vector<SchedulerItemPtr> scheduler_item_pool_;
|
||||
std::vector<SchedulerItem *> scheduler_item_pool_;
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
// Leak detection: tracks total live SchedulerItem allocations.
|
||||
// Invariant: debug_live_items_ == items_.size() + to_add_.size() + defer_queue_.size() + scheduler_item_pool_.size()
|
||||
// Verified periodically in call() to catch leaks early.
|
||||
size_t debug_live_items_{0};
|
||||
|
||||
// Verify the scheduler memory invariant: all allocated items are accounted for.
|
||||
// Returns true if no leak detected. Logs an error and asserts on failure.
|
||||
bool debug_verify_no_leak_() const;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
serial_proxy:
|
||||
- id: serial_proxy_1
|
||||
name: Test Serial Port
|
||||
port_type: RS232
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO2
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-bulk-cleanup
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-cancel
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-cancel-regular
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-fifo-simple
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-defer-stress-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-heap-stress-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-internal-id-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-null-name
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-numeric-id-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-rapid-cancel-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-recursive-timeout
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-removed-item-race
|
||||
|
||||
host:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-retry-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-simul-callbacks-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-string-lifetime-test
|
||||
|
||||
external_components:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: sched-string-name-stress
|
||||
|
||||
external_components:
|
||||
|
||||
Reference in New Issue
Block a user