Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs

# Conflicts:
#	esphome/core/defines.h
This commit is contained in:
J. Nick Koston committed 2026-11-04 20:51:14 -06:00
commit 55b020eaf8
280 files changed
+9981 -2214

No files matched your search

+31 -3
View File
@@ -188,9 +188,37 @@ Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `res
drive a state when the restore mode is disabled. A hub that applies its switches' restored states itself
reads `sw->get_initial_write_state()`, which already applies `inverted`.
**Entities that each watch a register field:** one class taking register, shift, mask and match from a
Python tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a
virtual listener base (a second vptr per entity).
**A hub that forwards data to its children:** when what each child does with the data is known at build time
(decode a register field, match a key or tag, publish a value), declare a `cg.HubFunction` once in the hub's
`__init__.py` and have each platform add one item per entity, built from codegen expressions. Code generation writes
one function per hub. Pass `define=` and put the hub's `HUB_PUBLISHER(<name>, args...)` (in a public section; it
ends on `public:`) and its call under that `#ifdef`; call the pointer only when it is not null. The children stay
plain entities, with no listener class or list. A child that lost a `Component` base keeps its dump lines through
a second hub function (`"dump"`, a `const char *` tag parameter, no define of its own) that the hub's
`dump_config()` calls inside the value function's `#ifdef`; every child adds to both.
**Entities that each watch a register field:** when every field (register, shift, mask, match) is known at
build time, use the rule above (see `bq25186`). A binary sensor tests its field value against an on-values
bitmap (`match`); a one-bit field reads the bit (simplified from bq25186's `_state_expression`):
```python
# hub __init__.py
STATUS_PUBLISHER = cg.HubFunction(
"status", # esphome__<hub>__status_fn, passed to HUB_PUBLISHER(status, ...)'s setter
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
# platform to_code, per configured entity
value = (STATUS[reg] >> shift) & mask
if mask != 0x01 or match != 0b10: # 0b10: a one-bit field that is on when set
value = (cg.MockObj(f"{match:#06b}") >> value) & 1
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(value))
```
Store a field description per entity (a struct in a `StaticVector` sized by `cg.slot_counter`) only when the
field is chosen at run time, as in `sen6x`, where the status bit depends on the variant read in `setup()`.
Never a template per bit or a virtual listener base (a second vptr per entity).
## Other helpers
+5 -2
View File
@@ -58,8 +58,11 @@ APIs.
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that is
known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
missed.** When the hub only forwards data to its children and each child's handling is known at build
time, keep no list at all: use `cg.HubFunction` (see the component-codegen skill). When the hub must
store the children (it iterates them for other reasons, or picks fields at run time), a `register_*()`
method called once per child at code generation time has a count that is known at compile time, so it
should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
function that each consumer calls once per slot it will occupy, and after every `to_code` has run it
emits the define with the final count. When nothing registers, no define is emitted and the storage plus
its registration method compile out entirely.
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BUILD_BASE_VERSION=2026.06.1
ARG BUILD_BASE_VERSION=2026.11.0
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
+1
View File
@@ -633,6 +633,7 @@ esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/stcc4/* @j9brown
esphome/components/store_yaml/* @bdraco
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
+9 -8
View File
@@ -94,23 +94,24 @@ without a password leaves OTA open. The API is documented and is meant to be
called by other devices, scripts, and pages.
As defense-in-depth, the web server checks the `Origin` header on browser requests
to its entity control and state endpoints: a request whose `Origin` does not match
the address the device is served on is rejected, and the `allowed_origins` option
to every endpoint served through it, including the web OTA `/update` endpoint
(the captive portal pages are open by design): a request whose `Origin` does not match
the `Host` header of the same request is rejected, and the `allowed_origins` option
widens that list. This blocks the common "confused deputy" (CSRF) case where a page
the operator visits drives the device through their browser. It is **not** an
authentication boundary: it only constrains browsers. Any client that omits the
`Origin` header — `curl`, scripts, or other non-browser callers on the same
network — reaches every endpoint exactly as before. The check also does not cover
the web OTA `/update` endpoint. The device performs no CSRF-token or `Referer`
network — reaches every endpoint exactly as before. The device performs no CSRF-token or `Referer`
validation. The following are therefore **not** vulnerabilities in this repository:
- Requests without an `Origin` header (for example `curl`) reaching the control
endpoints, whether or not `web_server` `auth:` is set.
- Requests from an origin the operator added to `allowed_origins`.
- Cross-origin or CSRF firmware upload through the web OTA endpoint (`/update`) when
web OTA is enabled without `web_server` `auth:`. The `/update` endpoint is not
covered by the `Origin` check; this is the same exposure as running OTA without a
password.
- DNS rebinding: a page whose host name resolves to the device sends a matching
`Origin` and `Host`, so the check lets it through.
- Firmware upload through the web OTA endpoint (`/update`) by a client that sends no
`Origin` header when web OTA is enabled without `web_server` `auth:`; this is the
same exposure as running OTA without a password.
The supported defenses are `web_server` `auth:`, protecting OTA (a web password or
a native OTA password), and keeping devices on a trusted, segmented network. See
+1 -1
View File
@@ -1,5 +1,5 @@
ARG BUILD_VERSION=dev
ARG BUILD_BASE_VERSION=2026.06.1
ARG BUILD_BASE_VERSION=2026.11.0
ARG BUILD_TYPE=docker
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
+6 -2
View File
@@ -45,6 +45,9 @@ CONF_CHOOSE = "choose"
CONF_CHOICE_ID = "choice_id"
_BYTE_VECTOR = cg.std_vector.template(cg.uint8)
def progmem_bytes(name: str, data: bytes | list[int]) -> MockObj:
"""Shared PROGMEM table for constant bytes; equal payloads share one, empty is nullptr."""
if not data:
@@ -60,10 +63,11 @@ async def templatable_bytes(
set_template: MockObj,
set_static: MockObj,
table_name: str,
return_type: MockObj = _BYTE_VECTOR,
) -> None:
"""Set a TemplatableBytes: a lambda via set_template, constant bytes via set_static."""
if cg.is_template(value):
fn = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
fn = await cg.templatable(value, args, return_type)
cg.add(set_template(fn))
else:
cg.add(set_static(*_static_bytes(table_name, value)))
@@ -884,7 +888,7 @@ async def _render_bytes(
return f"{receiver}{target.format(data, parent=parent)};"
if isinstance(value, Lambda):
call = await _call_user_lambda(value, args, cg.std_vector.template(cg.uint8))
call = await _call_user_lambda(value, args, _BYTE_VECTOR)
return [
"{",
f" const std::vector<uint8_t> esphome__bytes = {call};",
+1
View File
@@ -54,6 +54,7 @@ from esphome.cpp_generator import ( # noqa: F401
with_local_variable,
)
from esphome.cpp_helpers import ( # noqa: F401
HubFunction,
add_optional,
add_pin,
add_variable,
+77 -1
View File
@@ -22,6 +22,7 @@ service APIConnection {
rpc device_capabilities (DeviceCapabilitiesRequest) returns (DeviceCapabilitiesResponse) {}
rpc device_wizard (DeviceWizardRequest) returns (DeviceWizardResponse) {}
rpc wizard_input_set (WizardInputSetRequest) returns (void) {}
rpc crash_report (CrashReportRequest) returns (CrashReportResponse) {}
rpc list_entities (ListEntitiesRequest) returns (void) {}
rpc subscribe_states (SubscribeStatesRequest) returns (void) {}
rpc subscribe_logs (SubscribeLogsRequest) returns (void) {}
@@ -83,6 +84,7 @@ service APIConnection {
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
rpc sendspin_pairing_token(SendspinPairingTokenRequest) returns (SendspinPairingTokenResponse) {}
rpc get_yaml(GetYamlRequest) returns (void) {}
}
@@ -401,6 +403,11 @@ message ZWaveProxyCapabilities {
uint32 home_id = 2;
}
message StoreYamlCapabilities {
// Firmware embeds its YAML, so clients can request it with get_yaml
bool supported = 1;
}
message WizardCapabilities {
// True when the device has a wizard configured, so DeviceWizardRequest will be answered
bool configured = 1;
@@ -414,6 +421,12 @@ message SendspinCapabilities {
string client_id = 2 [(max_data_length) = 43];
}
message CrashReportCapabilities {
// Bitmask of CrashReportFlag. ESP32 checks BUILD_MATCHES against the build time stored with the record;
// ESP8266, RP2 and LibreTiny store none and assume it.
uint32 flags = 1;
}
message DeviceCapabilitiesResponse {
option (id) = 150;
option (source) = SOURCE_SERVER;
@@ -425,6 +438,8 @@ message DeviceCapabilitiesResponse {
[(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
WizardCapabilities wizard = 5 [(field_ifdef) = "USE_API_WIZARD"];
SendspinCapabilities sendspin = 6 [(field_ifdef) = "USE_SENDSPIN"];
StoreYamlCapabilities store_yaml = 7 [(field_ifdef) = "USE_STORE_YAML"];
CrashReportCapabilities crash_report = 8 [(field_ifdef) = "USE_CRASH_REPORT"];
}
// ==================== DEVICE WIZARD ====================
@@ -538,6 +553,36 @@ message SubscribeStatesRequest {
// Empty
}
// ==================== CRASH REPORT ====================
// Send only when crash_report.flags has PRESENT. The report is dropped once sent.
message CrashReportRequest {
option (id) = 163;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_CRASH_REPORT";
// Empty
}
message CrashReportCore {
uint32 core = 1;
repeated uint32 backtrace = 2 [(fixed_array_with_length_define) = "CRASH_REPORT_MAX_BACKTRACE"];
}
message CrashReportResponse {
option (id) = 164;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CRASH_REPORT";
uint32 flags = 1; // Bitmask of CrashReportFlag
uint32 reason = 2; // CrashReason
uint32 pc = 3;
uint32 cause = 4;
uint32 fault_address = 5;
uint32 build_time = 6;
uint32 crashed_core = 7;
repeated CrashReportCore cores = 8 [(fixed_array_with_length_define) = "CRASH_REPORT_MAX_CORES"];
}
// ==================== COMMON =====================
enum EntityCategory {
@@ -2192,6 +2237,8 @@ message BluetoothScannerSetModeRequest {
enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
// Not 2 or 4: aioesphomeapi sends API audio as 1 << 2
VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO = 8;
}
message SubscribeVoiceAssistantRequest {
@@ -2225,6 +2272,11 @@ message VoiceAssistantRequest {
uint32 flags = 3;
VoiceAssistantAudioSettings audio_settings = 4;
string wake_word_phrase = 5;
string wake_word_id = 6;
uint32 wake_word_audio_bytes = 7;
float wake_word_probability = 8;
float wake_word_probability_cutoff = 9;
uint32 wake_word_audio_bytes2 = 10;
}
message VoiceAssistantResponse {
@@ -2250,6 +2302,7 @@ enum VoiceAssistantEvent {
VOICE_ASSISTANT_WAKE_WORD_END = 10;
VOICE_ASSISTANT_STT_VAD_START = 11;
VOICE_ASSISTANT_STT_VAD_END = 12;
VOICE_ASSISTANT_WAKE_WORD_REJECTED = 13;
VOICE_ASSISTANT_TTS_STREAM_START = 98;
VOICE_ASSISTANT_TTS_STREAM_END = 99;
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
@@ -2843,7 +2896,7 @@ message InfraredRFReceiveEvent {
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::span<const int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// Sent only to the client that issued an InfraredRFTransmitRawTimingsRequest, once the
@@ -3071,6 +3124,7 @@ message UsbDeviceDescriptor {
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
string interface_description = 7; // iInterface string of that interface, empty when it has none
}
// The identity of the device behind a port. Identifies one physical device among others of
@@ -3143,3 +3197,25 @@ message SendspinPairingTokenResponse {
// The pairing token (Sendspin pairing.md "Pairing Token"). Set only when status is OK.
string token = 2 [(max_data_length) = 127];
}
// ==================== STORE YAML ====================
// Streams the compressed user YAML embedded in firmware; the client decompresses.
// A GetYamlRequest during an active transfer is ignored; reconnect to restart.
message GetYamlRequest {
option (id) = 161;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_STORE_YAML";
option (no_delay) = true;
}
message GetYamlResponse {
option (id) = 162;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_STORE_YAML";
// A zstd frame of the EHY1 file envelope, sent from flash
bytes data = 1 [(force) = true, (pointer_to_buffer) = true, (progmem) = true];
bool done = 2 [(force) = true];
// Set on the first chunk only
uint32 total_size = 3;
}
+80 -3
View File
@@ -366,6 +366,13 @@ void APIConnection::loop() {
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
// Guard inline so the idle hot path pays a compare, not a function call.
if (this->store_yaml_pos_ != STORE_YAML_IDLE) {
this->try_send_store_yaml_();
}
#endif
}
void APIConnection::check_keepalive_(uint32_t now) {
@@ -1222,6 +1229,36 @@ void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
}
#endif
#ifdef USE_STORE_YAML
void APIConnection::on_get_yaml_request() {
// A re-request while a transfer is in flight is ignored (see api.proto)
if (this->store_yaml_pos_ != STORE_YAML_IDLE)
return;
this->store_yaml_pos_ = 0;
this->try_send_store_yaml_();
}
void APIConnection::try_send_store_yaml_() {
// Advance only after a successful send, so a full TX buffer retries the same chunk from loop()
while (this->helper_->can_write_without_blocking()) {
const size_t to_send = std::min(STORE_YAML_DATA_SIZE - this->store_yaml_pos_, MAX_BATCH_PACKET_SIZE);
GetYamlResponse resp;
resp.data = STORE_YAML_DATA + this->store_yaml_pos_;
resp.data_len = to_send;
if (this->store_yaml_pos_ == 0)
resp.total_size = STORE_YAML_DATA_SIZE;
resp.done = this->store_yaml_pos_ + to_send == STORE_YAML_DATA_SIZE;
if (!this->send_message(resp))
return;
if (resp.done) {
this->store_yaml_pos_ = STORE_YAML_IDLE;
return;
}
this->store_yaml_pos_ += to_send;
}
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1316,7 +1353,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe, msg.flags);
}
}
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
@@ -1932,7 +1969,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 19;
resp.api_version_minor = 20;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = esphome_version_ref();
resp.name = StringRef(App.get_name());
@@ -2123,6 +2160,14 @@ bool APIConnection::send_device_capabilities_response_() {
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif
#ifdef USE_STORE_YAML
resp.store_yaml.supported = true;
#endif
#ifdef USE_CRASH_REPORT
CrashReport report;
if (crash_report_get(report))
resp.crash_report.flags = report.flags;
#endif
return this->send_message(resp);
}
@@ -2152,6 +2197,34 @@ void APIConnection::on_device_capabilities_request() {
this->on_fatal_error();
}
}
#ifdef USE_CRASH_REPORT
void APIConnection::on_crash_report_request() {
CrashReport report;
CrashReportResponse resp;
if (crash_report_get(report)) {
resp.flags = report.flags;
resp.reason = static_cast<uint32_t>(report.reason);
resp.pc = report.pc;
resp.cause = report.cause;
resp.fault_address = report.fault_address;
resp.build_time = report.build_time;
resp.crashed_core = report.crashed_core;
for (uint8_t i = 0; i < report.core_count; i++) {
auto &core = resp.cores[i];
core.core = report.cores[i].core;
core.backtrace = report.cores[i].addresses;
core.backtrace_len = report.cores[i].count;
}
resp.cores_len = report.core_count;
}
if (!this->send_message(resp)) {
this->on_fatal_error();
return;
}
if (resp.flags & CRASH_REPORT_FLAG_PRESENT)
crash_report_clear();
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
@@ -2365,10 +2438,14 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
const void *msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
// Skip dump for log messages (recursive logging risk), camera frames (high-frequency noise)
// and YAML chunks (may contain secrets)
if (message_type != SubscribeLogsResponse::MESSAGE_TYPE
#ifdef USE_CAMERA
&& message_type != CameraImageResponse::MESSAGE_TYPE
#endif
#ifdef USE_STORE_YAML
&& message_type != GetYamlResponse::MESSAGE_TYPE
#endif
) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
+18
View File
@@ -32,6 +32,11 @@ namespace esphome::api {
// Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h.
class APIServer;
#ifdef USE_STORE_YAML
/// The zstd YAML envelope generated by store_yaml, STORE_YAML_DATA_SIZE bytes in flash.
extern const uint8_t STORE_YAML_DATA[] PROGMEM;
#endif
// One shared flash string for every refused-frame warning: send_message()
// fails as soon as the TCP buffer is full, and each caller only pays for its
// short name. The guard drops the helper and its arguments below WARN.
@@ -123,6 +128,9 @@ class APIConnection final : public APIServerConnectionBase {
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void on_camera_image_request(const CameraImageRequest &msg);
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request();
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void on_climate_command_request(const ClimateCommandRequest &msg);
@@ -277,6 +285,9 @@ class APIConnection final : public APIServerConnectionBase {
#ifdef USE_API_WIZARD
void on_device_wizard_request();
#endif
#ifdef USE_CRASH_REPORT
void on_crash_report_request();
#endif
#ifdef USE_API_WIZARD_INPUTS
void on_wizard_input_set_request(const WizardInputSetRequest &msg);
#endif
@@ -421,6 +432,13 @@ class APIConnection final : public APIServerConnectionBase {
void try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
void try_send_store_yaml_();
static constexpr size_t STORE_YAML_IDLE = std::numeric_limits<size_t>::max();
// Streaming offset into STORE_YAML_DATA, or STORE_YAML_IDLE
size_t store_yaml_pos_{STORE_YAML_IDLE};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
+120
View File
@@ -299,6 +299,19 @@ uint32_t ZWaveProxyCapabilities::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *StoreYamlCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.supported);
return pos;
}
uint32_t StoreYamlCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_bool(1, msg.supported);
return size;
}
#endif
#ifdef USE_API_WIZARD
uint8_t *WizardCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const WizardCapabilities *>(self);
@@ -327,6 +340,19 @@ uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_CRASH_REPORT
uint8_t *CrashReportCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const CrashReportCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.flags);
return pos;
}
uint32_t CrashReportCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const CrashReportCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.flags);
return size;
}
#endif
uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const DeviceCapabilitiesResponse *>(self);
#ifdef USE_BLUETOOTH_PROXY
@@ -348,6 +374,12 @@ uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__res
#endif
#ifdef USE_SENDSPIN
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.sendspin);
#endif
#ifdef USE_STORE_YAML
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.store_yaml);
#endif
#ifdef USE_CRASH_REPORT
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 8, msg.crash_report);
#endif
return pos;
}
@@ -373,6 +405,12 @@ uint32_t DeviceCapabilitiesResponse::calc_size_msg(const void *self) {
#endif
#ifdef USE_SENDSPIN
size += ProtoSize::calc_message(1, msg.sendspin.calculate_size());
#endif
#ifdef USE_STORE_YAML
size += ProtoSize::calc_message(1, msg.store_yaml.calculate_size());
#endif
#ifdef USE_CRASH_REPORT
size += ProtoSize::calc_message(1, msg.crash_report.calculate_size());
#endif
return size;
}
@@ -405,6 +443,54 @@ void WizardInputSetRequest::decode_field(void *self, uint32_t tag, const uint8_t
}
}
#endif
#ifdef USE_CRASH_REPORT
uint8_t *CrashReportCore::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const CrashReportCore *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.core);
for (uint16_t i = 0; i < msg.backtrace_len; i++) {
pos = ProtoEncode::encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.backtrace[i]);
}
return pos;
}
uint32_t CrashReportCore::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const CrashReportCore *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.core);
for (uint16_t i = 0; i < msg.backtrace_len; i++) {
size += ProtoSize::calc_uint32_force(1, msg.backtrace[i]);
}
return size;
}
uint8_t *CrashReportResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const CrashReportResponse *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.flags);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.reason);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.pc);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.cause);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.fault_address);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.build_time);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.crashed_core);
for (uint16_t i = 0; i < msg.cores_len; i++) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 8, msg.cores[i]);
}
return pos;
}
uint32_t CrashReportResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const CrashReportResponse *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.flags);
size += ProtoSize::calc_uint32(1, msg.reason);
size += ProtoSize::calc_uint32(1, msg.pc);
size += ProtoSize::calc_uint32(1, msg.cause);
size += ProtoSize::calc_uint32(1, msg.fault_address);
size += ProtoSize::calc_uint32(1, msg.build_time);
size += ProtoSize::calc_uint32(1, msg.crashed_core);
for (uint16_t i = 0; i < msg.cores_len; i++) {
size += ProtoSize::calc_message_force(1, msg.cores[i].calculate_size());
}
return size;
}
#endif
#ifdef USE_BINARY_SENSOR
uint8_t *ListEntitiesBinarySensorResponse::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -3028,6 +3114,15 @@ uint8_t *VoiceAssistantRequest::encode_msg(const void *self, uint8_t *__restrict
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.flags);
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.audio_settings);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.wake_word_phrase);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.wake_word_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.wake_word_audio_bytes);
if (uint32_t raw = float_to_raw(msg.wake_word_probability); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 69, raw);
}
if (uint32_t raw = float_to_raw(msg.wake_word_probability_cutoff); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 77, raw);
}
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, msg.wake_word_audio_bytes2);
return pos;
}
uint32_t VoiceAssistantRequest::calc_size_msg(const void *self) {
@@ -3038,6 +3133,11 @@ uint32_t VoiceAssistantRequest::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.flags);
size += ProtoSize::calc_message(1, msg.audio_settings.calculate_size());
size += ProtoSize::calc_length(1, msg.wake_word_phrase.size());
size += ProtoSize::calc_length(1, msg.wake_word_id.size());
size += ProtoSize::calc_uint32(1, msg.wake_word_audio_bytes);
size += ProtoSize::calc_float(1, msg.wake_word_probability);
size += ProtoSize::calc_float(1, msg.wake_word_probability_cutoff);
size += ProtoSize::calc_uint32(1, msg.wake_word_audio_bytes2);
return size;
}
void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -4314,6 +4414,7 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.interface_description);
return pos;
}
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4325,6 +4426,7 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
size += ProtoSize::calc_length(1, msg.interface_description.size());
return size;
}
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4407,6 +4509,24 @@ uint32_t SendspinPairingTokenResponse::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *GetYamlResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
pos = ProtoEncode::encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, msg.done ? 0x01 : 0x00);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.total_size);
return pos;
}
uint32_t GetYamlResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
uint32_t size = 0;
size += ProtoSize::calc_length_force(1, msg.data_len);
size += ProtoSize::calc_bool_force(1);
size += ProtoSize::calc_uint32(1, msg.total_size);
return size;
}
#endif
#ifndef HAS_PROTO_MESSAGE_DUMP
static_assert(!std::is_polymorphic_v<HelloRequest>, "decodable messages carry no vtable");
static_assert(!std::is_polymorphic_v<DisconnectRequest>, "decodable messages carry no vtable");
+123 -3
View File
@@ -262,6 +262,7 @@ enum BluetoothScannerMode : uint32_t {
enum VoiceAssistantSubscribeFlag : uint32_t {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0,
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1,
VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO = 8,
};
enum VoiceAssistantRequestFlag : uint32_t {
VOICE_ASSISTANT_REQUEST_NONE = 0,
@@ -283,6 +284,7 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_WAKE_WORD_END = 10,
VOICE_ASSISTANT_STT_VAD_START = 11,
VOICE_ASSISTANT_STT_VAD_END = 12,
VOICE_ASSISTANT_WAKE_WORD_REJECTED = 13,
VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99,
VOICE_ASSISTANT_INTENT_PROGRESS = 100,
@@ -734,6 +736,23 @@ class ZWaveProxyCapabilities final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class StoreYamlCapabilities final : public ProtoMessage {
public:
bool supported{false};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_API_WIZARD
class WizardCapabilities final : public ProtoMessage {
public:
@@ -769,10 +788,27 @@ class SendspinCapabilities final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_CRASH_REPORT
class CrashReportCapabilities final : public ProtoMessage {
public:
uint32_t flags{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
class DeviceCapabilitiesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 150;
static constexpr uint8_t ESTIMATED_SIZE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 170;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); }
#endif
@@ -793,6 +829,12 @@ class DeviceCapabilitiesResponse final : public ProtoMessage {
#endif
#ifdef USE_SENDSPIN
SendspinCapabilities sendspin{};
#endif
#ifdef USE_STORE_YAML
StoreYamlCapabilities store_yaml{};
#endif
#ifdef USE_CRASH_REPORT
CrashReportCapabilities crash_report{};
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -863,6 +905,53 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
protected:
};
#ifdef USE_CRASH_REPORT
class CrashReportCore final : public ProtoMessage {
public:
uint32_t core{0};
std::array<uint32_t, CRASH_REPORT_MAX_BACKTRACE> backtrace{};
uint16_t backtrace_len{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class CrashReportResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 164;
static constexpr uint8_t ESTIMATED_SIZE = 164;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("crash_report_response"); }
#endif
uint32_t flags{0};
uint32_t reason{0};
uint32_t pc{0};
uint32_t cause{0};
uint32_t fault_address{0};
uint32_t build_time{0};
uint32_t crashed_core{0};
std::array<CrashReportCore, CRASH_REPORT_MAX_CORES> cores{};
uint16_t cores_len{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_BINARY_SENSOR
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
public:
@@ -2953,7 +3042,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage {
class VoiceAssistantRequest final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 90;
static constexpr uint8_t ESTIMATED_SIZE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); }
#endif
@@ -2962,6 +3051,11 @@ class VoiceAssistantRequest final : public ProtoMessage {
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{nullptr, 0}; // null until set, encode only
StringRef wake_word_id{nullptr, 0}; // null until set, encode only
uint32_t wake_word_audio_bytes{0};
float wake_word_probability{0.0f};
float wake_word_probability_cutoff{0.0f};
uint32_t wake_word_audio_bytes2{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -3841,7 +3935,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
uint32_t device_id{0};
#endif
uint32_t key{0};
const std::vector<int32_t> *timings{};
const std::span<const int32_t> *timings{};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -4102,6 +4196,7 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
StringRef interface_description{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -4209,5 +4304,30 @@ class SendspinPairingTokenResponse final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class GetYamlResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 162;
static constexpr uint8_t ESTIMATED_SIZE = 25;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("get_yaml_response"); }
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
bool done{false};
uint32_t total_size{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
} // namespace esphome::api
+69
View File
@@ -675,6 +675,8 @@ const char *proto_enum_to_string<enums::VoiceAssistantSubscribeFlag>(enums::Voic
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE");
case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO:
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO");
case enums::VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO:
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -720,6 +722,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START");
case enums::VOICE_ASSISTANT_STT_VAD_END:
return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END");
case enums::VOICE_ASSISTANT_WAKE_WORD_REJECTED:
return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_REJECTED");
case enums::VOICE_ASSISTANT_TTS_STREAM_START:
return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START");
case enums::VOICE_ASSISTANT_TTS_STREAM_END:
@@ -1127,6 +1131,13 @@ const char *ZWaveProxyCapabilities::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *StoreYamlCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("StoreYamlCapabilities"));
dump_field(out, ESPHOME_PSTR("supported"), this->supported);
return out.c_str();
}
#endif
#ifdef USE_API_WIZARD
const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("WizardCapabilities"));
@@ -1142,6 +1153,13 @@ const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_CRASH_REPORT
const char *CrashReportCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("CrashReportCapabilities"));
dump_field(out, ESPHOME_PSTR("flags"), this->flags);
return out.c_str();
}
#endif
const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceCapabilitiesResponse"));
#ifdef USE_BLUETOOTH_PROXY
@@ -1175,6 +1193,16 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
out.append(2, ' ').append_p(ESPHOME_PSTR("sendspin")).append(": ");
this->sendspin.dump_to(out);
out.append("\n");
#endif
#ifdef USE_STORE_YAML
out.append(2, ' ').append_p(ESPHOME_PSTR("store_yaml")).append(": ");
this->store_yaml.dump_to(out);
out.append("\n");
#endif
#ifdef USE_CRASH_REPORT
out.append(2, ' ').append_p(ESPHOME_PSTR("crash_report")).append(": ");
this->crash_report.dump_to(out);
out.append("\n");
#endif
return out.c_str();
}
@@ -1197,6 +1225,32 @@ const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
out.append_p(ESPHOME_PSTR("ListEntitiesDoneResponse {}"));
return out.c_str();
}
#ifdef USE_CRASH_REPORT
const char *CrashReportCore::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("CrashReportCore"));
dump_field(out, ESPHOME_PSTR("core"), this->core);
for (uint16_t i = 0; i < this->backtrace_len; i++) {
dump_field(out, ESPHOME_PSTR("backtrace"), this->backtrace[i], 4);
}
return out.c_str();
}
const char *CrashReportResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("CrashReportResponse"));
dump_field(out, ESPHOME_PSTR("flags"), this->flags);
dump_field(out, ESPHOME_PSTR("reason"), this->reason);
dump_field(out, ESPHOME_PSTR("pc"), this->pc);
dump_field(out, ESPHOME_PSTR("cause"), this->cause);
dump_field(out, ESPHOME_PSTR("fault_address"), this->fault_address);
dump_field(out, ESPHOME_PSTR("build_time"), this->build_time);
dump_field(out, ESPHOME_PSTR("crashed_core"), this->crashed_core);
for (uint16_t i = 0; i < this->cores_len; i++) {
out.append(4, ' ').append_p(ESPHOME_PSTR("cores")).append(": ");
this->cores[i].dump_to(out);
out.append("\n");
}
return out.c_str();
}
#endif
#ifdef USE_BINARY_SENSOR
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesBinarySensorResponse"));
@@ -2391,6 +2445,11 @@ const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const {
this->audio_settings.dump_to(out);
out.append("\n");
dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase);
dump_field(out, ESPHOME_PSTR("wake_word_id"), this->wake_word_id);
dump_field(out, ESPHOME_PSTR("wake_word_audio_bytes"), this->wake_word_audio_bytes);
dump_field(out, ESPHOME_PSTR("wake_word_probability"), this->wake_word_probability);
dump_field(out, ESPHOME_PSTR("wake_word_probability_cutoff"), this->wake_word_probability_cutoff);
dump_field(out, ESPHOME_PSTR("wake_word_audio_bytes2"), this->wake_word_audio_bytes2);
return out.c_str();
}
const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const {
@@ -2975,6 +3034,7 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
dump_field(out, ESPHOME_PSTR("interface_description"), this->interface_description);
return out.c_str();
}
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
@@ -3016,6 +3076,15 @@ const char *SendspinPairingTokenResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *GetYamlResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("GetYamlResponse"));
dump_progmem_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
dump_field(out, ESPHOME_PSTR("done"), this->done);
dump_field(out, ESPHOME_PSTR("total_size"), this->total_size);
return out.c_str();
}
#endif
} // namespace esphome::api
@@ -27,6 +27,10 @@
#include "esphome/components/select/select_traits.h"
#endif
#ifdef USE_CRASH_REPORT
#include "esphome/core/crash_report.h"
#endif
// Standard library includes that might be needed
#include <set>
#include <span>
@@ -760,6 +760,24 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_sendspin_pairing_token_request();
break;
}
#endif
#ifdef USE_STORE_YAML
case 161 /* GetYamlRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_get_yaml_request"));
#endif
this->on_get_yaml_request();
break;
}
#endif
#ifdef USE_CRASH_REPORT
case 163 /* CrashReportRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_crash_report_request"));
#endif
this->on_crash_report_request();
break;
}
#endif
default:
break;
+7
View File
@@ -39,6 +39,9 @@ class APIServerConnectionBase {
void on_list_entities_request() {};
void on_subscribe_states_request() {};
#ifdef USE_CRASH_REPORT
void on_crash_report_request() {};
#endif
#ifdef USE_COVER
void on_cover_command_request(const CoverCommandRequest &value) {};
@@ -256,6 +259,10 @@ class APIServerConnectionBase {
#ifdef USE_SENDSPIN
void on_sendspin_pairing_token_request() {};
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request() {};
#endif
};
} // namespace esphome::api
+2 -2
View File
@@ -520,13 +520,13 @@ void APIServer::send_serial_proxy_identity(const SerialProxyIdentity &msg) {
#ifdef USE_IR_RF
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
std::span<const int32_t> timings) {
InfraredRFReceiveEvent resp{};
#ifdef USE_DEVICES
resp.device_id = device_id;
#endif
resp.key = key;
resp.timings = timings;
resp.timings = &timings;
for (auto &c : this->active_clients())
c->send_infrared_rf_receive_event(resp);
+2 -1
View File
@@ -30,6 +30,7 @@
#include <array>
#include <memory>
#include <span>
#include <vector>
namespace esphome::api {
@@ -208,7 +209,7 @@ class APIServer final : public Component
void send_serial_proxy_identity(const SerialProxyIdentity &msg);
#endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, std::span<const int32_t> timings);
#endif
bool is_connected() const { return this->api_connection_count_ != 0; }
@@ -160,17 +160,14 @@ void spi_dma_tx_finish_callback(unsigned int param) {
void BekenSPILEDStripLightOutput::setup() {
RAMAllocator<uint8_t> allocator;
if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->internal_accessor_.allocate(allocator, this->num_leds_),
LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
const size_t dma_buffer_size = (this->get_buffer_size_() * 8) + (2 * 64);
this->dma_buf_ = allocator.allocate(dma_buffer_size);
if (this->dma_buf_ == nullptr) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->dma_buf_ != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
memset(this->dma_buf_, 0, dma_buffer_size);
@@ -180,22 +177,16 @@ void BekenSPILEDStripLightOutput::setup() {
value = PWD_SPI_CLK_BIT;
sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_UP, &value);
if (spi_data != nullptr) {
this->mark_failed(LOG_STR("SPI already initialized"));
if (!this->setup_check(spi_data == nullptr, LOG_STR("SPI already initialized")))
return;
}
spi_data = (SpiData *) calloc(1, sizeof(SpiData)); // NOLINT(cppcoreguidelines-no-malloc)
if (spi_data == nullptr) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(spi_data != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
spi_data->dma_tx_semaphore = xSemaphoreCreateBinary();
if (spi_data->dma_tx_semaphore == nullptr) {
this->mark_failed(LOG_STR("TX semaphore init failed"));
if (!this->setup_check(spi_data->dma_tx_semaphore != nullptr, LOG_STR("TX semaphore init failed")))
return;
}
spi_data->first_run = true;
+61 -23
View File
@@ -193,12 +193,54 @@ void BME680BSECComponent::loop() {
}
this->status_comm_result(this->bsec_status_ <= BSEC_OK && this->bme680_status_ <= BME680_OK, nullptr);
// Process a single action from the queue. These are primarily sensor state publishes
// that in totality take too long to send in a single call.
if (this->queue_.size()) {
auto action = std::move(this->queue_.front());
this->queue_.pop();
action();
// Publish a single waiting state. All of them in one call take too long.
if (this->pending_.empty())
return;
const PublishSlot slot = this->pending_.front();
this->pending_.pop();
this->pending_mask_.erase(slot);
switch (slot) {
case PUBLISH_SLOT_IAQ:
this->publish_sensor_(this->iaq_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_IAQ_ACCURACY_TEXT:
this->publish_sensor_(
this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(this->pending_accuracy_, IaqAccuracyStrings::LAST_INDEX));
break;
case PUBLISH_SLOT_IAQ_ACCURACY:
this->publish_sensor_(this->iaq_accuracy_sensor_, this->pending_accuracy_, true);
break;
case PUBLISH_SLOT_SAVE_STATE:
this->save_state_(this->pending_accuracy_);
break;
case PUBLISH_SLOT_CO2_EQUIVALENT:
this->publish_sensor_(this->co2_equivalent_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_BREATH_VOC_EQUIVALENT:
this->publish_sensor_(this->breath_voc_equivalent_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_PRESSURE:
this->publish_sensor_(this->pressure_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_GAS_RESISTANCE:
this->publish_sensor_(this->gas_resistance_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_TEMPERATURE:
this->publish_sensor_(this->temperature_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_HUMIDITY:
this->publish_sensor_(this->humidity_sensor_, this->pending_values_[slot]);
break;
default:
break;
}
}
void BME680BSECComponent::queue_publish_(PublishSlot slot) {
if (!this->pending_mask_.count(slot)) {
this->pending_mask_.insert(slot);
this->pending_.push(slot);
}
}
@@ -385,35 +427,31 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
float signal = outputs[i].signal;
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
case BSEC_OUTPUT_STATIC_IAQ:
this->pending_accuracy_ = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
this->queue_publish_<PUBLISH_SLOT_IAQ>(signal);
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY_TEXT);
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY);
// Queue up an opportunity to save state
this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
} break;
this->queue_publish_(PUBLISH_SLOT_SAVE_STATE);
break;
case BSEC_OUTPUT_CO2_EQUIVALENT:
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_CO2_EQUIVALENT>(signal);
break;
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_BREATH_VOC_EQUIVALENT>(signal);
break;
case BSEC_OUTPUT_RAW_PRESSURE:
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
this->queue_publish_<PUBLISH_SLOT_PRESSURE>(signal / 100.0f);
break;
case BSEC_OUTPUT_RAW_GAS:
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_GAS_RESISTANCE>(signal);
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_TEMPERATURE>(signal);
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_HUMIDITY>(signal);
break;
}
}
+29 -3
View File
@@ -6,8 +6,9 @@
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/preferences.h"
#include "esphome/core/defines.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h"
#include <map>
#include <queue>
#ifdef USE_BSEC
#include <bsec.h>
@@ -67,6 +68,22 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
void loop() override;
protected:
// States waiting for loop() to publish them, one per pass
enum PublishSlot : uint8_t {
PUBLISH_SLOT_IAQ,
PUBLISH_SLOT_CO2_EQUIVALENT,
PUBLISH_SLOT_BREATH_VOC_EQUIVALENT,
PUBLISH_SLOT_PRESSURE,
PUBLISH_SLOT_GAS_RESISTANCE,
PUBLISH_SLOT_TEMPERATURE,
PUBLISH_SLOT_HUMIDITY,
// These take pending_accuracy_ instead of a value
PUBLISH_SLOT_IAQ_ACCURACY_TEXT,
PUBLISH_SLOT_IAQ_ACCURACY,
PUBLISH_SLOT_SAVE_STATE,
PUBLISH_SLOT_COUNT,
};
void set_config_();
float calc_sensor_sample_rate_(SampleRate sample_rate);
void update_subscription_();
@@ -91,7 +108,12 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
void save_state_(
uint8_t accuracy); // Save the bsec_state_data_ member (volatile memory) to the ESP preferences (storage)
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
void queue_publish_(PublishSlot slot);
template<PublishSlot S> void queue_publish_(float value) {
static_assert(S < PUBLISH_SLOT_IAQ_ACCURACY_TEXT, "slot has no value");
this->pending_values_[S] = value;
this->queue_publish_(S);
}
static uint8_t work_buffer_[BSEC_MAX_WORKBUFFER_SIZE];
struct bme680_dev bme680_;
@@ -102,7 +124,11 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
uint32_t millis_overflow_counter_{0};
int64_t next_call_ns_{0};
std::queue<std::function<void()>> queue_;
// Arrival order; a slot already waiting keeps its place and takes the newer value
StaticRingBuffer<PublishSlot, PUBLISH_SLOT_COUNT> pending_;
FiniteSetMask<PublishSlot, DefaultBitPolicy<PublishSlot, PUBLISH_SLOT_COUNT>> pending_mask_;
float pending_values_[PUBLISH_SLOT_IAQ_ACCURACY_TEXT];
uint8_t pending_accuracy_{0};
bool bsec_state_data_valid_;
uint8_t bsec_state_data_[BSEC_MAX_STATE_BLOB_SIZE]; // This is the current snapshot of the BSEC library state
@@ -130,14 +130,62 @@ void BME68xBSEC2Component::check_status_() {
}
void BME68xBSEC2Component::loop() {
// Process a single action from the queue. These are primarily sensor state publishes
// that in totality take too long to send in a single call.
if (this->queue_.size()) {
auto action = std::move(this->queue_.front());
this->queue_.pop();
action();
} else {
// Publish a single waiting state. All of them in one call take too long.
if (this->pending_.empty()) {
this->disable_loop();
return;
}
const PendingPublish pending = this->pending_.front();
this->pending_.pop();
const float value = pending.value;
switch (pending.slot) {
#ifdef USE_SENSOR
case PUBLISH_SLOT_IAQ:
this->publish_sensor_(this->iaq_sensor_, value);
break;
case PUBLISH_SLOT_IAQ_STATIC:
this->publish_sensor_(this->iaq_static_sensor_, value);
break;
case PUBLISH_SLOT_CO2_EQUIVALENT:
this->publish_sensor_(this->co2_equivalent_sensor_, value);
break;
case PUBLISH_SLOT_BREATH_VOC_EQUIVALENT:
this->publish_sensor_(this->breath_voc_equivalent_sensor_, value);
break;
case PUBLISH_SLOT_PRESSURE:
this->publish_sensor_(this->pressure_sensor_, value);
break;
case PUBLISH_SLOT_GAS_RESISTANCE:
this->publish_sensor_(this->gas_resistance_sensor_, value);
break;
case PUBLISH_SLOT_TEMPERATURE:
this->publish_sensor_(this->temperature_sensor_, value);
break;
case PUBLISH_SLOT_HUMIDITY:
this->publish_sensor_(this->humidity_sensor_, value);
break;
case PUBLISH_SLOT_IAQ_ACCURACY:
this->publish_sensor_(this->iaq_accuracy_sensor_, value, true);
break;
#endif
#ifdef USE_TEXT_SENSOR
case PUBLISH_SLOT_IAQ_ACCURACY_TEXT:
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[static_cast<uint8_t>(value)]);
break;
#endif
case PUBLISH_SLOT_SAVE_STATE:
this->save_state_(static_cast<uint8_t>(value));
break;
default:
break;
}
}
void BME68xBSEC2Component::queue_publish_(PublishSlot slot, float value) {
// Only full when the loop stalled for seconds; drop the oldest so the newest reading still goes out
if (!this->pending_.push({value, slot})) {
this->pending_.pop();
this->pending_.push({value, slot});
}
}
@@ -414,44 +462,44 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou
max_accuracy = std::max(outputs[i].accuracy, max_accuracy);
update_accuracy = true;
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_IAQ, signal);
#endif
break;
case BSEC_OUTPUT_STATIC_IAQ:
max_accuracy = std::max(outputs[i].accuracy, max_accuracy);
update_accuracy = true;
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_IAQ_STATIC, signal);
#endif
break;
case BSEC_OUTPUT_CO2_EQUIVALENT:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_CO2_EQUIVALENT, signal);
#endif
break;
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_BREATH_VOC_EQUIVALENT, signal);
#endif
break;
case BSEC_OUTPUT_RAW_PRESSURE:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
this->queue_publish_(PUBLISH_SLOT_PRESSURE, signal / 100.0f);
#endif
break;
case BSEC_OUTPUT_RAW_GAS:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_GAS_RESISTANCE, signal);
#endif
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_TEMPERATURE, signal);
#endif
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_HUMIDITY, signal);
#endif
break;
}
@@ -459,16 +507,13 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou
if (update_accuracy) {
max_accuracy = std::min<uint8_t>(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
#ifdef USE_SENSOR
this->queue_push_(
[this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); });
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY, max_accuracy);
#endif
#ifdef USE_TEXT_SENSOR
this->queue_push_([this, max_accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]);
});
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY_TEXT, max_accuracy);
#endif
// Queue up an opportunity to save state
this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); });
this->queue_publish_(PUBLISH_SLOT_SAVE_STATE, max_accuracy);
}
this->enable_loop();
}
+25 -3
View File
@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#ifdef USE_BSEC2
@@ -15,7 +16,6 @@
#endif
#include <cinttypes>
#include <queue>
#include <bsec2.h>
@@ -83,6 +83,28 @@ class BME68xBSEC2Component : public Component {
virtual uint32_t get_hash() = 0;
protected:
// States waiting for loop() to publish them, one per pass
enum PublishSlot : uint8_t {
PUBLISH_SLOT_IAQ,
PUBLISH_SLOT_IAQ_STATIC,
PUBLISH_SLOT_CO2_EQUIVALENT,
PUBLISH_SLOT_BREATH_VOC_EQUIVALENT,
PUBLISH_SLOT_PRESSURE,
PUBLISH_SLOT_GAS_RESISTANCE,
PUBLISH_SLOT_TEMPERATURE,
PUBLISH_SLOT_HUMIDITY,
PUBLISH_SLOT_IAQ_ACCURACY,
PUBLISH_SLOT_IAQ_ACCURACY_TEXT,
PUBLISH_SLOT_SAVE_STATE,
PUBLISH_SLOT_COUNT,
};
struct __attribute__((packed)) PendingPublish {
float value; // the accuracy for the accuracy and save slots
PublishSlot slot;
};
// A read returns up to 3 fields in parallel mode, each published in full
static constexpr uint8_t PUBLISH_QUEUE_SIZE = 3 * PUBLISH_SLOT_COUNT;
void set_config_(const uint8_t *config, u_int32_t len);
float calc_sensor_sample_rate_(SampleRate sample_rate);
void update_subscription_();
@@ -103,14 +125,14 @@ class BME68xBSEC2Component : public Component {
void load_state_();
void save_state_(uint8_t accuracy);
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
void queue_publish_(PublishSlot slot, float value);
struct bme68x_dev bme68x_;
bsec_bme_settings_t bsec_settings_;
bsec_version_t version_;
struct bme68x_heatr_conf bme68x_heatr_conf_;
std::queue<std::function<void()>> queue_;
StaticRingBuffer<PendingPublish, PUBLISH_QUEUE_SIZE> pending_;
ESPPreferenceObject bsec_state_;
uint8_t const *bsec2_configuration_{nullptr};
+4 -10
View File
@@ -45,10 +45,8 @@ static const LogString *iir_filter_to_str(IIRFilter filter) {
void BMP3XXComponent::setup() {
// Call the Device base class "initialise" function
if (!reset()) {
this->mark_failed(LOG_STR("Failed to reset"));
if (!this->setup_check(this->reset(), LOG_STR("Failed to reset")))
return;
}
if (!this->setup_check(read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)))
return;
@@ -82,10 +80,8 @@ void BMP3XXComponent::setup() {
compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f);
// Initialise the BMP388 IIR filter register
if (!set_iir_filter(this->iir_filter_)) {
this->mark_failed(LOG_STR("Failed to set IIR filter"));
if (!this->setup_check(this->set_iir_filter(this->iir_filter_), LOG_STR("Failed to set IIR filter")))
return;
}
// Set power control registers
pwr_ctrl_.bit.press_en = 1;
@@ -101,10 +97,8 @@ void BMP3XXComponent::setup() {
this->temperature_oversampling_ = OVERSAMPLING_NONE;
}
// Initialise the BMP388 oversampling register
if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) {
this->mark_failed(LOG_STR("Failed to set oversampling register"));
return;
}
this->setup_check(this->set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_),
LOG_STR("Failed to set oversampling register"));
}
void BMP3XXComponent::dump_config() {
+8 -13
View File
@@ -65,19 +65,14 @@ BQ25186Component = bq25186_ns.class_(
BQ25186_CHILD_SCHEMA = cv.Schema(
{cv.GenerateID(CONF_BQ25186_ID): cv.use_id(BQ25186Component)}
)
_request_binary_sensor_slot = cg.slot_counter("BQ25186_BINARY_SENSOR_COUNT")
_request_text_sensor_slot = cg.slot_counter("BQ25186_TEXT_SENSOR_COUNT")
def register_binary_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
_request_binary_sensor_slot(str(hub))
return hub.register_binary_sensor(var)
def register_text_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
_request_text_sensor_slot(str(hub))
return hub.register_text_sensor(var)
BQ25186StatusRegisters = bq25186_ns.class_("BQ25186StatusRegisters")
STATUS_PUBLISHER = cg.HubFunction(
"status",
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
REG_VBAT_CTRL = 0x03
REG_ICHG_CTRL = 0x04
+15 -11
View File
@@ -1,5 +1,4 @@
from functools import partial
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
@@ -9,16 +8,13 @@ from esphome.const import (
DEVICE_CLASS_PLUG,
DEVICE_CLASS_RUNNING,
)
from esphome.cpp_generator import HexIntLiteral
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_binary_sensor
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, STATUS, STATUS_PUBLISHER
DEPENDENCIES = ["bq25186"]
BQ25186BinarySensor = bq25186_ns.class_(
"BQ25186BinarySensor", binary_sensor.BinarySensor
)
REG_STAT0 = 0
REG_STAT1 = 1
REG_FLAG0 = 2
@@ -51,11 +47,19 @@ _FIELDS = (
)
def _state_expression(reg: int, shift: int, mask: int, match: int) -> cg.MockObj:
"""Whether the field (status[reg] >> shift) & mask reads as on, as a bit of match."""
value = (STATUS[reg] >> shift) & HexIntLiteral(mask)
if mask == 0x01 and match == _BIT_SET:
return value
return (cg.MockObj(f"{match:#06b}") >> value) & 1
CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): binary_sensor.binary_sensor_schema(
BQ25186BinarySensor, device_class=device_class
device_class=device_class
)
for key, *_, device_class in _FIELDS
}
@@ -68,6 +72,6 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_BQ25186_ID])
sensors = binary_sensor.sub_binary_sensors(config)
for key, reg, shift, mask, match, _ in _FIELDS:
await sensors(
key, partial(register_binary_sensor, hub), reg, shift, mask, match
)
if (var := await sensors(key, None)) is not None:
state = _state_expression(reg, shift, mask, match)
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
+3 -13
View File
@@ -1,10 +1,4 @@
#include "bq25186.h"
#ifdef USE_BINARY_SENSOR
#include "bq25186_binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "bq25186_text_sensor.h"
#endif
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -144,13 +138,9 @@ void BQ25186Component::update() {
#endif
if (!pending)
this->status_clear_warning();
#ifdef BQ25186_BINARY_SENSOR_COUNT
for (auto *sensor : this->binary_sensors_)
sensor->on_status(status);
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
for (auto *sensor : this->text_sensors_)
sensor->on_status(status);
#ifdef USE_BQ25186_STATUS_PUBLISHER
if (this->status_publisher_ != nullptr)
this->status_publisher_(status);
#endif
}
+21 -13
View File
@@ -7,6 +7,10 @@
#ifdef USE_BQ25186_PG_GPO
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/progmem.h"
#endif
#include <array>
@@ -38,8 +42,21 @@ static constexpr uint8_t BQ25186_SYS_REG_PG_GPO_LEVEL = 0x10;
using BQ25186ConfigRegisters = std::array<uint8_t, BQ25186_CONFIG_REGISTER_COUNT>;
using BQ25186StatusRegisters = std::array<uint8_t, BQ25186_STATUS_REGISTER_COUNT>;
class BQ25186BinarySensor;
class BQ25186TextSensor;
#ifdef USE_TEXT_SENSOR
// STAT0[6:5] charge status, then STAT1[4:3] TS status
PROGMEM_STRING_TABLE(BQ25186StatusStrings, "Not Charging", "Constant Current", "Constant Voltage",
"Charge Done/Disabled", "Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)",
"Warm (Voltage Reduced)");
/// Publish the two-bit field in the low bits of `value` as string `first + field`, only when it differs from `last`
inline void publish_status_text(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t value, uint8_t first) {
value &= 0x03;
if (value == last)
return;
last = value;
sensor->publish_state(BQ25186StatusStrings::get_progmem_str(first + value, BQ25186StatusStrings::LAST_INDEX));
}
#endif
class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
public:
@@ -52,11 +69,8 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
this->configure_on_boot_ = true;
}
bool apply_config(const BQ25186ConfigRegisters &regs);
#ifdef BQ25186_BINARY_SENSOR_COUNT
void register_binary_sensor(BQ25186BinarySensor *sensor) { this->binary_sensors_.push_back(sensor); }
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
void register_text_sensor(BQ25186TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
#ifdef USE_BQ25186_STATUS_PUBLISHER
HUB_PUBLISHER(status, const BQ25186StatusRegisters &)
#endif
#ifdef USE_BQ25186_PG_GPO
/// The /PG/GPO pin is put in general purpose output mode when a switch drives it
@@ -78,12 +92,6 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
#endif
#ifdef BQ25186_BINARY_SENSOR_COUNT
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
StaticVector<BQ25186TextSensor *, BQ25186_TEXT_SENSOR_COUNT> text_sensors_;
#endif
BQ25186ConfigRegisters regs_{}; // the configuration last applied, written again after a chip reset
bool configure_on_boot_{false};
bool configured_{false}; // regs_ went to the chip at least once
@@ -1,31 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "bq25186.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::bq25186 {
/// One field of a status register; `match` has a bit set for each field value that reads as on
class BQ25186BinarySensor final : public binary_sensor::BinarySensor {
public:
BQ25186BinarySensor(uint8_t reg, uint8_t shift, uint8_t mask, uint8_t match)
: reg_(reg), shift_(shift), mask_(mask), match_(match) {}
void on_status(const BQ25186StatusRegisters &status) {
const uint8_t value = (status[this->reg_] >> this->shift_) & this->mask_;
this->publish_state((this->match_ >> value) & 1);
}
protected:
const uint8_t reg_;
const uint8_t shift_;
const uint8_t mask_;
const uint8_t match_;
};
} // namespace esphome::bq25186
#endif // USE_BINARY_SENSOR
@@ -1,22 +0,0 @@
#include "bq25186_text_sensor.h"
#ifdef USE_TEXT_SENSOR
#include "esphome/core/progmem.h"
namespace esphome::bq25186 {
// STAT0[6:5] charge status, then STAT1[4:3] TS status
PROGMEM_STRING_TABLE(StatusStrings, "Not Charging", "Constant Current", "Constant Voltage", "Charge Done/Disabled",
"Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)", "Warm (Voltage Reduced)");
void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
const uint8_t value = (status[this->reg_] >> this->shift_) & 0x03;
if (value == this->last_)
return;
this->last_ = value;
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
}
} // namespace esphome::bq25186
#endif // USE_TEXT_SENSOR
@@ -1,26 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_TEXT_SENSOR
#include "bq25186.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::bq25186 {
/// A two-bit status field; `first` is the string table index of the field's value 0
class BQ25186TextSensor final : public text_sensor::TextSensor {
public:
BQ25186TextSensor(uint8_t reg, uint8_t shift, uint8_t first) : reg_(reg), shift_(shift), first_(first) {}
void on_status(const BQ25186StatusRegisters &status);
protected:
const uint8_t reg_;
const uint8_t shift_;
const uint8_t first_;
uint8_t last_{0xFF}; // the field value last published
};
} // namespace esphome::bq25186
#endif // USE_TEXT_SENSOR
+35 -10
View File
@@ -1,16 +1,24 @@
from functools import partial
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.cpp_generator import (
AssignmentExpression,
HexIntLiteral,
VariableDeclarationExpression,
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_text_sensor
from . import (
BQ25186_CHILD_SCHEMA,
CONF_BQ25186_ID,
STATUS,
STATUS_PUBLISHER,
bq25186_ns,
)
DEPENDENCIES = ["bq25186"]
BQ25186TextSensor = bq25186_ns.class_("BQ25186TextSensor", text_sensor.TextSensor)
REG_STAT0 = 0
REG_STAT1 = 1
@@ -20,12 +28,28 @@ _FIELDS = (
("ts_status", REG_STAT1, 3, 4),
)
def _add_text_status(
hub: cg.MockObj, var: cg.MockObj, reg: int, shift: int, first: int
) -> None:
"""Publish status string first + the two-bit field at status[reg] >> shift, on change."""
last = cg.MockObj(f"esphome__{var}__last")
# 0xFF so the first poll publishes
declaration = VariableDeclarationExpression(cg.uint8, "", last, static=True)
STATUS_PUBLISHER.add(
hub, AssignmentExpression(None, None, declaration, HexIntLiteral(0xFF))
)
STATUS_PUBLISHER.add(
hub,
bq25186_ns.publish_status_text(
parent_ref(var), last, STATUS[reg] >> shift, first
),
)
CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): text_sensor.text_sensor_schema(BQ25186TextSensor)
for key, *_ in _FIELDS
}
{cv.Optional(key): text_sensor.text_sensor_schema() for key, *_ in _FIELDS}
),
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
)
@@ -35,4 +59,5 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_BQ25186_ID])
sensors = text_sensor.sub_text_sensors(config)
for key, reg, shift, first in _FIELDS:
await sensors(key, partial(register_text_sensor, hub), reg, shift, first)
if (var := await sensors(key, None)) is not None:
_add_text_status(hub, var, reg, shift, first)
@@ -10,6 +10,8 @@ CODEOWNERS = ["@nagyrobi"]
AUTO_LOAD = ["ble_device_base"]
DOMAIN = "bthome_mithermometer"
CONF_REPLAY_PROTECTION = "replay_protection"
bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer")
BTHomeMiThermometer = bthome_mithermometer_ns.class_(
@@ -29,14 +31,25 @@ def bthome_mithermometer_base_schema(
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
cv.Optional(CONF_REPLAY_PROTECTION, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(extra_schema)
.extend(ble_device_base.BLE_DEVICE_SCHEMA),
_validate_replay_protection,
)
def _validate_replay_protection(config: ConfigType) -> ConfigType:
if config[CONF_REPLAY_PROTECTION] and CONF_BINDKEY not in config:
raise cv.Invalid(
f"'{CONF_REPLAY_PROTECTION}' requires '{CONF_BINDKEY}'",
path=[CONF_REPLAY_PROTECTION],
)
return config
async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
@@ -47,3 +60,6 @@ async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None:
for index in range(0, len(bindkey), 2)
]
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))
if config[CONF_REPLAY_PROTECTION]:
cg.add_define("USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION")
cg.add(var.set_replay_protection(True))
@@ -1,10 +1,12 @@
#include "bthome_ble.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstring>
#include <span>
@@ -31,6 +33,9 @@ static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
static constexpr size_t BTHOME_NONCE_SIZE = 13;
static constexpr size_t BTHOME_MIC_SIZE = 4;
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
// If no frame with a higher counter has been accepted for this long, a lower
// counter is accepted again (the sensor rebooted, e.g. after a battery swap).
static constexpr uint32_t BTHOME_REPLAY_RESYNC_MS = 10 * 60 * 1000;
// Both callers are log macros (LOGCONFIG / LOGVV); below CONFIG level they
// compile away and an ungated helper trips -Wunused-function.
@@ -156,6 +161,9 @@ void BTHomeMiThermometer::dump_config() {
if (this->has_bindkey_) {
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
ESP_LOGCONFIG(TAG, " Replay Protection: %s", YESNO(this->replay_protection_));
#endif
}
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -253,6 +261,40 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
return true;
}
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
// PVVX sends new data under an unchanged counter, so only an exact repeat (counter and MIC) is a duplicate and
// earlier frames under the current counter can be replayed; the lower-counter rule is the defence. It holds only
// while the sensor keeps being heard: after the resync window one old frame becomes the baseline and recorded
// frames above it can follow in order, the price of recovering from a counter restart. RAM only.
bool BTHomeMiThermometer::check_replay_counter_(uint32_t counter, uint32_t mic) {
const uint32_t now = App.get_loop_component_start_time();
if (counter == this->last_counter_ && mic == this->last_mic_) {
ESP_LOGVV(TAG, "Duplicate BTHome frame (counter %" PRIu32 ")", counter);
return false;
}
if (counter < this->last_counter_) {
if (now - this->last_accepted_ms_ < BTHOME_REPLAY_RESYNC_MS) {
// One warning per run of rejections
if (this->replay_rejected_) {
ESP_LOGVV(TAG, "BTHome frame rejected: counter %" PRIu32 " lower than last %" PRIu32, counter,
this->last_counter_);
} else {
ESP_LOGW(TAG, "BTHome frame rejected: counter %" PRIu32 " lower than last %" PRIu32 " (replay?)", counter,
this->last_counter_);
this->replay_rejected_ = true;
}
return false;
}
ESP_LOGW(TAG, "BTHome counter restarted (%" PRIu32 " -> %" PRIu32 "), resync", this->last_counter_, counter);
}
this->last_counter_ = counter;
this->last_mic_ = mic;
this->last_accepted_ms_ = now;
this->replay_rejected_ = false;
return true;
}
#endif
bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceData &service_data,
const ble_device_base::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -339,6 +381,17 @@ bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceDat
return false;
}
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
// After the MIC and address checks, so other devices never touch the state
if (is_encrypted && this->replay_protection_) {
const uint8_t *tail = data.data() + data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
if (!this->check_replay_counter_(encode_value_le<uint32_t>(tail),
encode_value_le<uint32_t>(tail + BTHOME_COUNTER_SIZE))) {
return false;
}
}
#endif
if (payload_size == 0) {
ESP_LOGVV(TAG, "BTHome payload empty after header");
return false;
@@ -13,6 +13,10 @@
// decryption availability is selected per platform in the .cpp.
namespace esphome::bthome_mithermometer {
namespace testing {
class ReplayClockAccess;
} // namespace testing
class BTHomeMiThermometer final : public ble_device_base::ESPBTDeviceListener, public Component {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
@@ -23,18 +27,36 @@ class BTHomeMiThermometer final : public ble_device_base::ESPBTDeviceListener, p
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(std::initializer_list<uint8_t> bindkey);
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
void set_replay_protection(bool replay_protection) { this->replay_protection_ = replay_protection; }
#endif
void dump_config() override;
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
protected:
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
friend class testing::ReplayClockAccess;
#endif
bool handle_service_data_(const ble_device_base::ServiceData &service_data,
const ble_device_base::ESPBTDevice &device);
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const;
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
bool check_replay_counter_(uint32_t counter, uint32_t mic);
#endif
uint64_t address_{0};
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
uint32_t last_counter_{0};
uint32_t last_mic_{0};
uint32_t last_accepted_ms_{0};
#endif
optional<uint8_t> last_packet_id_{};
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
bool replay_protection_{false};
bool replay_rejected_{false};
#endif
bool has_bindkey_{false};
uint8_t bindkey_[16];
};
+147 -30
View File
@@ -4,10 +4,12 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.config import iter_ids
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM, CONF_TRIGGER_ID
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -24,8 +26,25 @@ CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame"
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
CONF_FD = "fd"
CONF_BIT_RATE_SWITCH = "bit_rate_switch"
CONF_PAD_DATA = "pad_data"
CONF_DEFAULT_FD = "default_fd"
CONF_DEFAULT_BIT_RATE_SWITCH = "default_bit_rate_switch"
CONF_DEFAULT_PAD_DATA = "default_pad_data"
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
# All 29 id bits; received ids never use more, so on_frame skips the & for this mask
CAN_ID_MASK_ALL = 0x1FFFFFFF
CAN_MAX_DATA_LENGTH = 8
# Payload length of each CAN FD DLC, keep in sync with CAN_FD_DLC_LENGTHS in canbus.h
CAN_FD_DATA_LENGTHS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64)
# canbus.send options that need a platform built on CANBUS_FD_SCHEMA
_FD_SEND_KEYS = (CONF_FD, CONF_BIT_RATE_SWITCH, CONF_PAD_DATA)
def _flag(name: str, value: bool) -> str:
return name if value else f"!{name}"
def validate_id(config: ConfigType) -> ConfigType:
@@ -49,11 +68,8 @@ def validate_raw_data(value: Any) -> bytes | list:
canbus_ns = cg.esphome_ns.namespace("canbus")
CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component)
CanbusTrigger = canbus_ns.class_(
"CanbusTrigger",
automation.Trigger.template(cg.std_vector.template(cg.uint8), cg.uint32),
cg.Component,
)
CanFrame = canbus_ns.struct("CanFrame")
CanPayload = canbus_ns.class_("CanPayload")
CanSpeed = canbus_ns.enum("CAN_SPEED")
CAN_SPEEDS = CanSpeed.options(
@@ -81,6 +97,21 @@ CAN_SPEEDS = CanSpeed.options(
prefix="CAN_",
)
# Data phase rates for a CAN FD platform's own schema
CAN_DATA_SPEEDS = {
**CAN_SPEEDS,
**CanSpeed.options(
"2000KBPS",
"3000KBPS",
"4000KBPS",
"5000KBPS",
"6000KBPS",
"7000KBPS",
"8000KBPS",
prefix="CAN_",
),
}
def get_rate(value: str) -> int:
match = re.match(r"(\d+)(?:K(\d+)?)?BPS", value, re.IGNORECASE)
@@ -93,7 +124,7 @@ def get_rate(value: str) -> int:
CANBUS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CanbusComponent),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
CAN_SPEEDS, upper=True
),
@@ -103,10 +134,9 @@ CANBUS_SCHEMA = cv.Schema(
): cv.positive_not_null_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_CAN_ID_MASK, default=0x1FFFFFFF): cv.int_range(
min=0, max=0x1FFFFFFF
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_CAN_ID_MASK, default=CAN_ID_MASK_ALL): cv.int_range(
min=0, max=CAN_ID_MASK_ALL
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
@@ -118,39 +148,67 @@ CANBUS_SCHEMA = cv.Schema(
CANBUS_SCHEMA.add_extra(validate_id)
# A platform whose controller supports CAN FD extends this instead of CANBUS_SCHEMA. Its buses then accept the
# CAN FD options of canbus.send, and register_canbus() turns on USE_CANBUS_FD, which makes every CanFrame
# 64 bytes long.
CANBUS_FD_SCHEMA = CANBUS_SCHEMA.extend(
{
cv.Optional(CONF_DEFAULT_FD, default=False): cv.boolean,
cv.Optional(CONF_DEFAULT_BIT_RATE_SWITCH, default=False): cv.boolean,
cv.Optional(CONF_DEFAULT_PAD_DATA, default=False): cv.boolean,
}
)
def _is_fd_bus(config: ConfigType) -> bool:
return CONF_DEFAULT_FD in config
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_can_id(config[CONF_CAN_ID]))
if config[CONF_USE_EXTENDED_ID]:
cg.add(var.set_use_extended_id(True))
if _is_fd_bus(config):
cg.add_define("USE_CANBUS_FD")
cg.add(var.set_supports_fd(True))
for key, setter in (
(CONF_DEFAULT_FD, var.set_default_fd),
(CONF_DEFAULT_BIT_RATE_SWITCH, var.set_default_bit_rate_switch),
(CONF_DEFAULT_PAD_DATA, var.set_default_pad_data),
):
if config.get(key):
cg.add(setter(True))
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
cg.add(var.set_max_frames_per_loop(max_frames))
for conf in config.get(CONF_ON_FRAME, []):
can_id = conf[CONF_CAN_ID]
can_id_mask = conf[CONF_CAN_ID_MASK]
ext_id = conf[CONF_USE_EXTENDED_ID]
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
)
if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
cg.add(
trigger.set_remote_transmission_request(
conf[CONF_REMOTE_TRANSMISSION_REQUEST]
)
)
await cg.register_component(trigger, conf)
await automation.build_automation(
trigger,
can_id = f"0x{conf[CONF_CAN_ID]:X}"
if (mask := conf[CONF_CAN_ID_MASK]) != CAN_ID_MASK_ALL:
checks = [f"(frame.can_id & 0x{mask:X}) == {can_id}"]
else:
checks = [f"frame.can_id == {can_id}"]
checks.append(_flag("frame.use_extended_id", conf[CONF_USE_EXTENDED_ID]))
if (rtr := conf.get(CONF_REMOTE_TRANSMISSION_REQUEST)) is not None:
checks.append(_flag("frame.remote_transmission_request", rtr))
await automation.build_callback_automation(
var,
"add_frame_callback",
[
(cg.std_vector.template(cg.uint8), "x"),
(CanPayload, "x"),
(cg.uint32, "can_id"),
(cg.bool_, "remote_transmission_request"),
],
conf,
params=[(CanFrame.operator("const").operator("ref"), "frame")],
forward=[
"frame.payload()",
"frame.can_id",
"frame.remote_transmission_request",
],
when=" && ".join(checks),
)
@@ -170,16 +228,66 @@ async def register_canbus(
await setup_canbus_core_(var, config)
def validate_send(action: ConfigType, bus: ConfigType) -> None:
"""Reject a canbus.send that its bus cannot send, as far as the values are known.
Lambda values are checked when the action runs.
"""
data = action[CONF_DATA]
length = None if cg.is_template(data) else len(data)
if not _is_fd_bus(bus):
for key in _FD_SEND_KEYS:
if key in action:
raise cv.Invalid(
f"{key} needs a canbus platform that supports CAN FD", path=[key]
)
fd = False
else:
fd = action.get(CONF_FD, bus[CONF_DEFAULT_FD])
if fd is False and action.get(CONF_BIT_RATE_SWITCH) is True:
raise cv.Invalid("bit_rate_switch requires fd: true")
if fd is True and action[CONF_REMOTE_TRANSMISSION_REQUEST]:
raise cv.Invalid("remote_transmission_request cannot be used with fd: true")
if length is None:
return
if fd is False and length > CAN_MAX_DATA_LENGTH:
raise cv.Invalid("Classic CAN data must be at most 8 bytes", path=[CONF_DATA])
if (
fd is True
and length not in CAN_FD_DATA_LENGTHS
and action.get(CONF_PAD_DATA, bus[CONF_DEFAULT_PAD_DATA]) is False
):
raise cv.Invalid("CAN FD data length requires pad_data: true", path=[CONF_DATA])
def final_validate(config: list[ConfigType]) -> None:
full_config = fv.full_config.get()
for bus_id, path in iter_ids(full_config):
if len(path) < 2 or path[-2:] != ["canbus.send", CONF_CANBUS_ID]:
continue
action_path = path[:-1]
action = full_config.get_config_for_path(action_path)
bus = fv.get_declaring_config(bus_id, full_config)
with cv.prepend_path([cv.ROOT_CONFIG_PATH, *action_path]):
validate_send(action, bus)
FINAL_VALIDATE_SCHEMA = final_validate
# Actions
CANBUS_SEND_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
# One classic CAN frame
cv.Optional(CONF_FD): cv.templatable(cv.boolean),
cv.Optional(CONF_BIT_RATE_SWITCH): cv.templatable(cv.boolean),
cv.Optional(CONF_PAD_DATA): cv.templatable(cv.boolean),
# One CAN FD frame; final_validate holds a classic bus to 8 bytes
cv.Required(CONF_DATA): cv.templatable(
cv.All(validate_raw_data, cv.Length(max=8))
cv.All(validate_raw_data, cv.Length(max=CAN_FD_DATA_LENGTHS[-1]))
),
},
validate_id,
@@ -209,6 +317,14 @@ async def canbus_action_to_code(
cg.add(
var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST])
)
# Only set on a CAN FD bus, which final_validate checks
for key, setter in (
(CONF_FD, var.set_fd),
(CONF_BIT_RATE_SWITCH, var.set_bit_rate_switch),
(CONF_PAD_DATA, var.set_pad_data),
):
if (value := config.get(key)) is not None:
cg.add(setter(await cg.templatable(value, args, cg.bool_)))
await automation.templatable_bytes(
config[CONF_DATA],
@@ -216,5 +332,6 @@ async def canbus_action_to_code(
var.set_data_template,
var.set_data_static,
"canbus_data",
return_type=CanPayload,
)
return var
+104 -52
View File
@@ -22,49 +22,112 @@ void Canbus::dump_config() {
}
}
canbus::Error Canbus::send_data(const CanFrame &frame) {
if (frame.can_id > (frame.use_extended_id ? CAN_EXTENDED_ID_MAX : CAN_STANDARD_ID_MAX)) {
ESP_LOGE(TAG, "CAN ID 0x%08" PRIx32 " exceeds frame identifier width", frame.can_id);
return ERROR_FAILTX;
}
#ifdef USE_CANBUS_FD
if (frame.fd && !this->supports_fd_) {
ESP_LOGE(TAG, "CAN FD frame on a bus without CAN FD");
return ERROR_FAILTX;
}
if (frame.fd ? (frame.remote_transmission_request || can_dlc_to_len(frame.can_data_length_code) == CAN_INVALID_DLC)
: frame.bit_rate_switch) {
ESP_LOGE(TAG, "Invalid CAN frame: fd=%d rtr=%d brs=%d dlc=%" PRIu8, frame.fd, frame.remote_transmission_request,
frame.bit_rate_switch, frame.can_data_length_code);
return ERROR_FAILTX;
}
#endif
return this->send_frame_(frame);
}
#ifdef USE_CANBUS_FD
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const uint8_t *data, size_t len, bool fd, bool bit_rate_switch, bool pad_data) {
#else
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const uint8_t *data, size_t len) {
struct CanFrame can_message;
#endif
CanFrame frame;
frame.use_extended_id = use_extended_id;
frame.remote_transmission_request = remote_transmission_request;
frame.can_id = can_id;
#ifdef USE_CANBUS_FD
frame.fd = fd;
frame.bit_rate_switch = bit_rate_switch;
frame.error_state_indicator = false;
if (fd) {
frame.can_data_length_code = can_len_to_dlc(len, pad_data);
if (frame.can_data_length_code == CAN_INVALID_DLC) {
ESP_LOGE(TAG, "CAN FD cannot carry %zu bytes without pad_data", len);
return ERROR_FAILTX;
}
if (!remote_transmission_request && len != 0) {
std::memcpy(frame.data, data, len);
std::memset(frame.data + len, 0, can_dlc_to_len(frame.can_data_length_code) - len);
}
return this->send_data(frame);
}
#endif
if (len > CAN_MAX_DATA_LENGTH) {
ESP_LOGW(TAG, "Classic CAN carries 8 bytes, %zu given, sent 8", len);
}
// Clamp before narrowing, so a lambda payload of 256 bytes or more cannot wrap to a short length
const uint8_t size = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
if (use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
size);
frame.can_data_length_code = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
// A remote request carries no data, so its bytes are zeroed rather than left from the stack. An empty vector may
// hand over a null pointer, which memcpy must not get even for 0 bytes.
if (remote_transmission_request) {
std::memset(frame.data, 0, frame.can_data_length_code);
} else if (frame.can_data_length_code != 0) {
std::memcpy(frame.data, data, frame.can_data_length_code);
}
#ifdef USE_CANBUS_FD
// The caller's flags can still contradict each other
return this->send_data(frame);
#else
return this->send_frame_(frame);
#endif
}
bool Canbus::lambda_payload_fits_(const CanPayload &payload, [[maybe_unused]] bool fd) {
if (!payload.truncated())
return true;
#ifdef USE_CANBUS_FD
if (fd) {
ESP_LOGE(TAG, "CAN FD carries at most %u bytes, frame not sent", CAN_FRAME_DATA_LENGTH);
return false;
}
// The payload still holds 64 bytes, so send_data() warns as it cuts a classic frame to 8
#else
ESP_LOGW(TAG, "Classic CAN carries 8 bytes, the rest is cut");
#endif
return true;
}
canbus::Error Canbus::send_frame_(const CanFrame &frame) {
const uint8_t size = frame.payload_length();
if (frame.use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", frame.can_id,
TRUEFALSE(frame.remote_transmission_request), size);
} else {
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
size);
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", frame.can_id,
TRUEFALSE(frame.remote_transmission_request), size);
}
can_message.can_data_length_code = size;
can_message.can_id = can_id;
can_message.use_extended_id = use_extended_id;
can_message.remote_transmission_request = remote_transmission_request;
ESP_LOGV_HEX(TAG, frame.data, size, CAN_FRAME_DATA_LENGTH, " data=%s");
for (int i = 0; i < size; i++) {
can_message.data[i] = data[i];
ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]);
}
canbus::Error error = this->send_message(&can_message);
// Drivers take a mutable pointer but do not change the frame
canbus::Error error = this->send_message(const_cast<CanFrame *>(&frame));
if (error != canbus::ERROR_OK) {
if (use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error);
if (frame.use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", frame.can_id, error);
} else {
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error);
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", frame.can_id, error);
}
}
return error;
}
void Canbus::add_trigger(CanbusTrigger *trigger) {
if (trigger->use_extended_id_) {
ESP_LOGVV(TAG, "add trigger for extended canid=0x%08" PRIx32, trigger->can_id_);
} else {
ESP_LOGVV(TAG, "add trigger for std canid=0x%03" PRIx32, trigger->can_id_);
}
this->triggers_.push_back(trigger);
};
void Canbus::loop() {
uint32_t now = App.get_loop_component_start_time();
if ((now - this->last_event_check_time_) >= EVENT_CHECK_INTERVAL_MS) {
@@ -75,38 +138,27 @@ void Canbus::loop() {
struct CanFrame can_message;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
#ifdef USE_CANBUS_FD
// Classic drivers leave the CAN FD flags alone
can_message.fd = false;
can_message.bit_rate_switch = false;
can_message.error_state_indicator = false;
#endif
if (this->read_message(&can_message) != canbus::ERROR_OK) {
break;
}
const uint8_t size = can_message.payload_length();
if (can_message.use_extended_id) {
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
can_message.can_id, size);
} else {
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
can_message.can_id, size);
}
std::vector<uint8_t> data;
// show data received
for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) {
ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]);
data.push_back(can_message.data[i]);
}
this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request,
data);
// fire all triggers
for (auto *trigger : this->triggers_) {
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
(trigger->use_extended_id_ == can_message.use_extended_id) &&
(!trigger->remote_transmission_request_.has_value() ||
trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
}
}
ESP_LOGV_HEX(TAG, can_message.data, size, CAN_FRAME_DATA_LENGTH, " data=%s");
this->frame_callbacks_.call(can_message);
}
if (message_counter == this->max_frames_per_loop_) {
+198 -45
View File
@@ -2,9 +2,16 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/optional.h"
#include <algorithm>
#include <cinttypes>
#include <cstddef>
#include <cstring>
#include <initializer_list>
#include <type_traits>
#include <vector>
namespace esphome::canbus {
@@ -39,28 +46,128 @@ enum CanSpeed : uint8_t {
CAN_250KBPS,
CAN_500KBPS,
CAN_800KBPS,
CAN_1000KBPS
CAN_1000KBPS,
#ifdef USE_CANBUS_FD
// Data phase rates of a CAN FD controller
CAN_2000KBPS,
CAN_3000KBPS,
CAN_4000KBPS,
CAN_5000KBPS,
CAN_6000KBPS,
CAN_7000KBPS,
CAN_8000KBPS,
#endif
};
class CanbusTrigger;
template<typename... Ts> class CanbusSendAction;
/* CAN payload length definitions according to ISO 11898-1 */
static const uint8_t CAN_MAX_DATA_LENGTH = 8;
static constexpr uint8_t CAN_MAX_DATA_LENGTH = 8;
#ifdef USE_CANBUS_FD
static constexpr uint8_t CAN_FD_MAX_DATA_LENGTH = 64;
/// Payload bytes a CanFrame holds: one CAN FD frame when a platform that supports it is configured
static constexpr uint8_t CAN_FRAME_DATA_LENGTH = CAN_FD_MAX_DATA_LENGTH;
#else
/// Payload bytes a CanFrame holds: one classic frame
static constexpr uint8_t CAN_FRAME_DATA_LENGTH = CAN_MAX_DATA_LENGTH;
#endif
static const uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
static constexpr uint32_t CAN_STANDARD_ID_MAX = 0x7FF;
static constexpr uint32_t CAN_EXTENDED_ID_MAX = 0x1FFFFFFF;
/*
Can Frame describes a normative CAN Frame
The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used
So currently the flag is passed to and from the hardware but currently ignored to the user application.
*/
static constexpr uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
#ifdef USE_CANBUS_FD
/// Returned by the length conversions for a value that cannot be represented
static constexpr uint8_t CAN_INVALID_DLC = UINT8_MAX;
/// Payload length of each CAN FD DLC; a classic frame carries min(DLC, 8) bytes
static constexpr uint8_t CAN_FD_DLC_COUNT = 16;
static constexpr uint8_t CAN_FD_DLC_LENGTHS[CAN_FD_DLC_COUNT] PROGMEM = {0, 1, 2, 3, 4, 5, 6, 7,
8, 12, 16, 20, 24, 32, 48, 64};
/// CAN_FD_DLC_LENGTHS[dlc] for dlc below CAN_FD_DLC_COUNT; read from flash at run time
constexpr uint8_t can_fd_dlc_length(uint8_t dlc) {
return std::is_constant_evaluated() ? CAN_FD_DLC_LENGTHS[dlc] : progmem_read_byte(&CAN_FD_DLC_LENGTHS[dlc]);
}
/// Payload length of a CAN FD DLC, or CAN_INVALID_DLC above 15
constexpr uint8_t can_dlc_to_len(uint8_t dlc) {
return dlc < CAN_FD_DLC_COUNT ? can_fd_dlc_length(dlc) : CAN_INVALID_DLC;
}
/// CAN FD DLC for a payload length. With pad, the next longer length is used and the caller zero fills
/// the extra bytes; otherwise a length CAN FD cannot carry returns CAN_INVALID_DLC.
constexpr uint8_t can_len_to_dlc(size_t len, bool pad) {
for (uint8_t dlc = 0; dlc < CAN_FD_DLC_COUNT; dlc++) {
const uint8_t length = can_fd_dlc_length(dlc);
if (length >= len)
return (length == len || pad) ? dlc : CAN_INVALID_DLC;
}
return CAN_INVALID_DLC;
}
#else
/// Payload length of a classic DLC; 9 to 15 still carry 8 bytes
constexpr uint8_t can_dlc_to_len(uint8_t dlc) { return std::min(dlc, CAN_MAX_DATA_LENGTH); }
#endif
/// Frame payload without heap; converts to and from std::vector<uint8_t> for older lambdas.
class CanPayload : public StaticVector<uint8_t, CAN_FRAME_DATA_LENGTH> {
public:
using StaticVector<uint8_t, CAN_FRAME_DATA_LENGTH>::StaticVector;
// Not = default, so value-init does not zero the buffer
CanPayload() {}
// Every constructor that can be given more than one frame records that it cut the rest
template<typename InputIt> CanPayload(InputIt first, InputIt last) {
for (; first != last; ++first) {
if (this->size() == CAN_FRAME_DATA_LENGTH) {
this->truncated_ = true;
break;
}
this->push_back(*first);
}
}
CanPayload(std::initializer_list<uint8_t> data) : CanPayload(data.begin(), data.end()) {}
CanPayload(const std::vector<uint8_t> &data) // NOLINT(google-explicit-constructor)
: CanPayload(data.begin(), data.end()) {}
operator std::vector<uint8_t>() const { return {this->begin(), this->end()}; } // NOLINT(google-explicit-constructor)
/// Whether it was built from more bytes than one frame can carry
bool truncated() const { return this->truncated_; }
protected:
bool truncated_{false};
};
/// One CAN frame as passed between the bus and its driver. The CAN FD flags come after data, so code that
/// brace-initializes a classic frame keeps working.
struct CanFrame {
bool use_extended_id = false;
bool remote_transmission_request = false;
uint32_t can_id; /* 29 or 11 bit CAN_ID */
uint8_t can_data_length_code; /* frame payload length in byte (0 .. CAN_MAX_DATA_LENGTH) */
uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8)));
uint32_t can_id; /* 29 or 11 bit CAN_ID; drivers clear bits 29-31 (on_frame relies on it) */
uint8_t can_data_length_code; /* DLC as on the wire, not a byte count (see payload_length()) */
uint8_t data[CAN_FRAME_DATA_LENGTH] __attribute__((aligned(8)));
#ifdef USE_CANBUS_FD
bool fd = false; /* CAN FD frame; it cannot be a remote transmission request */
bool bit_rate_switch = false; /* CAN FD only: data phase sent at the data bit rate */
bool error_state_indicator = false; /* CAN FD only: received transmitter was error passive */
#endif
/// Payload bytes for the DLC: at most 8 for a classic frame, 0 for an invalid CAN FD DLC
uint8_t payload_length() const {
#ifdef USE_CANBUS_FD
if (this->fd) {
const uint8_t len = can_dlc_to_len(this->can_data_length_code);
return len == CAN_INVALID_DLC ? 0 : len;
}
#endif
return std::min(this->can_data_length_code, CAN_MAX_DATA_LENGTH);
}
CanPayload payload() const {
CanPayload out;
std::memcpy(out.data(), this->data, CAN_FRAME_DATA_LENGTH);
out.resize(this->payload_length());
return out;
}
};
enum CanEventFlags : uint8_t {
@@ -91,8 +198,18 @@ class Canbus : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override;
/// Validates the frame and passes it to the driver.
canbus::Error send_data(const CanFrame &frame);
#ifdef USE_CANBUS_FD
/// Sends up to 8 bytes as a classic frame, longer data is cut. An fd frame carries up to 64 bytes; pad_data
/// zero fills a length CAN FD cannot carry up to the next one.
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
size_t len, bool fd = false, bool bit_rate_switch = false, bool pad_data = false);
#else
/// Sends up to 8 bytes as a classic frame, longer data is cut.
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
size_t len);
#endif
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data) {
return this->send_data(can_id, use_extended_id, remote_transmission_request, data.data(), data.size());
@@ -105,9 +222,28 @@ class Canbus : public Component {
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
#ifdef USE_CANBUS_FD
// Defaults for canbus.send actions only, not for send_data()
void set_default_fd(bool default_fd) { this->default_fd_ = default_fd; }
void set_default_bit_rate_switch(bool default_bit_rate_switch) {
this->default_bit_rate_switch_ = default_bit_rate_switch;
}
void set_default_pad_data(bool default_pad_data) { this->default_pad_data_ = default_pad_data; }
void set_supports_fd(bool supports_fd) { this->supports_fd_ = supports_fd; }
#endif
/// Whether this bus sends and receives CAN FD frames; a classic bus can share a build with a CAN FD one
bool supports_fd() const {
#ifdef USE_CANBUS_FD
return this->supports_fd_;
#else
return false;
#endif
}
virtual CanStatus get_status() { return {}; }
void add_trigger(CanbusTrigger *trigger);
/// The frame is only valid during the call.
template<typename F> void add_frame_callback(F &&callback) { this->frame_callbacks_.add(std::forward<F>(callback)); }
/**
* Add a callback to be called when a CAN message is received. All received messages
* are passed to the callback without filtering.
@@ -118,19 +254,30 @@ class Canbus : public Component {
* - rtr If this is a remote transmission request
* - data The message data
*/
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
template<typename F>
ESPDEPRECATED("Use add_frame_callback(), which does not copy the payload. Removed in 2027.5.0.", "2026.11.0")
void add_callback(F &&callback) {
this->frame_callbacks_.add([cb = std::forward<F>(callback)](const CanFrame &frame) mutable {
cb(frame.can_id, frame.use_extended_id, frame.remote_transmission_request,
std::vector<uint8_t>(frame.data, frame.data + frame.payload_length()));
});
}
protected:
template<typename... Ts> friend class CanbusSendAction;
std::vector<CanbusTrigger *> triggers_{};
uint32_t can_id_;
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
#ifdef USE_CANBUS_FD
bool default_fd_{false};
bool default_bit_rate_switch_{false};
bool default_pad_data_{false};
bool supports_fd_{false};
#endif
uint8_t events_to_log_{0};
bool bus_off_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
CallbackManager<void(const CanFrame &frame)> frame_callbacks_{};
// interval in which to check for new can bus events (bus-off, passive, active)
static constexpr uint32_t EVENT_CHECK_INTERVAL_MS = 100;
@@ -150,6 +297,10 @@ class Canbus : public Component {
#endif
void log_events_(CanEventFlags events, uint32_t now);
/// Logs and sends a frame that is already valid.
canbus::Error send_frame_(const CanFrame &frame);
/// For a canbus.send lambda: a CAN FD frame that did not fit is refused, a classic one is cut to 8 bytes.
bool lambda_payload_fits_(const CanPayload &payload, [[maybe_unused]] bool fd);
virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame) = 0;
@@ -157,7 +308,12 @@ class Canbus : public Component {
};
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
TEMPLATABLE_BYTES(data)
TEMPLATABLE_BUFFER(data, CanPayload)
#ifdef USE_CANBUS_FD
TEMPLATABLE_VALUE(bool, fd)
TEMPLATABLE_VALUE(bool, bit_rate_switch)
TEMPLATABLE_VALUE(bool, pad_data)
#endif
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
@@ -171,11 +327,30 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
this->data_.template visit<CAN_MAX_DATA_LENGTH>(
[&](const uint8_t *data, size_t len) {
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
},
x...);
#ifdef USE_CANBUS_FD
const bool fd = this->fd_.value_or(x..., this->parent_->default_fd_);
const bool bit_rate_switch = this->bit_rate_switch_.value_or(x..., fd && this->parent_->default_bit_rate_switch_);
const bool pad_data = this->pad_data_.value_or(x..., this->parent_->default_pad_data_);
#endif
#ifndef USE_CANBUS_FD
const bool fd = false;
#endif
auto send = [&](const uint8_t *data, size_t len) {
#ifdef USE_CANBUS_FD
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len, fd,
bit_rate_switch, pad_data);
#else
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
#endif
};
if (this->data_.is_static()) {
this->data_.template visit<CAN_FRAME_DATA_LENGTH>(send, x...);
return;
}
// The conversion from a lambda's vector cuts it to one frame, so the length it had is checked here
const CanPayload payload = this->data_.value(x...);
if (this->parent_->lambda_payload_fits_(payload, fd))
send(payload.data(), payload.size());
}
protected:
@@ -184,26 +359,4 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
bool remote_transmission_request_{false};
};
class CanbusTrigger final : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
friend class Canbus;
public:
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
const bool use_extended_id)
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id) {};
void set_remote_transmission_request(bool remote_transmission_request) {
this->remote_transmission_request_ = remote_transmission_request;
}
void setup() override { this->parent_->add_trigger(this); }
protected:
Canbus *parent_;
uint32_t can_id_;
uint32_t can_id_mask_;
bool use_extended_id_;
optional<bool> remote_transmission_request_{};
};
} // namespace esphome::canbus
+5 -10
View File
@@ -22,23 +22,18 @@ void DAC7678Output::setup() {
ESP_LOGV(TAG, "Resetting device");
// Reset device
if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) {
this->mark_failed(LOG_STR("Reset failed"));
if (!this->setup_check(this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000), LOG_STR("Reset failed")))
return;
} else {
ESP_LOGV(TAG, "Reset succeeded");
}
ESP_LOGV(TAG, "Reset succeeded");
delayMicroseconds(1000);
// Set internal reference
if (this->internal_reference_) {
if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) {
this->mark_failed(LOG_STR("Set internal reference failed"));
if (!this->setup_check(this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4),
LOG_STR("Set internal reference failed")))
return;
} else {
ESP_LOGV(TAG, "Internal reference enabled");
}
ESP_LOGV(TAG, "Internal reference enabled");
}
}
+5 -14
View File
@@ -64,10 +64,8 @@ void ENS160Component::setup() {
uint16_t part_id;
if (!this->setup_check(this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast<uint8_t *>(&part_id), 2)))
return;
if (part_id != ENS160_PART_ID) {
this->mark_failed(LOG_STR("Sensor reported an invalid ID. Is this a ENS160?"));
if (!this->setup_check(part_id == ENS160_PART_ID, LOG_STR("Sensor reported an invalid ID. Is this a ENS160?")))
return;
}
// set mode to reset
if (!this->setup_check(this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)))
@@ -80,10 +78,8 @@ void ENS160Component::setup() {
return;
this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2);
if (this->validity_flag_ == INVALID_OUTPUT) {
this->mark_failed(LOG_STR("Invalid Device Status - No valid output"));
if (!this->setup_check(this->validity_flag_ != INVALID_OUTPUT, LOG_STR("Invalid Device Status - No valid output")))
return;
}
// set mode to idle
if (!this->setup_check(this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)))
@@ -110,20 +106,15 @@ void ENS160Component::setup() {
this->firmware_ver_build_ = version_data[2];
// set mode to standard
if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)))
return;
}
// read opmode and check standard mode is achieved before finishing Setup
uint8_t op_mode;
if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(ENS160_REG_OPMODE, &op_mode)))
return;
}
if (!this->setup_check(op_mode == ENS160_OPMODE_STD, LOG_STR("Device failed to achieve Standard Operating Mode")))
return;
this->setup_check(op_mode == ENS160_OPMODE_STD, LOG_STR("Device failed to achieve Standard Operating Mode"));
}
void ENS160Component::update() {
+1 -3
View File
@@ -19,10 +19,8 @@ const LogString *EPaperBase::epaper_state_to_string_() {
}
void EPaperBase::setup() {
if (!this->init_buffer_(this->buffer_length_)) {
this->mark_failed(LOG_STR("Failed to initialise buffer"));
if (!this->setup_check(this->init_buffer_(this->buffer_length_), LOG_STR("Failed to initialise buffer")))
return;
}
this->setup_pins_();
this->spi_setup();
}
@@ -4,6 +4,8 @@ Supported models:
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
7.5" 800x480 panel on an integrated ESP32-S3 board
- goodisplay-gdey0583t81-5.83: Good Display GDEY0583T81 5.83" mono display,
648x480 pixels, also sold by Seeed as 0583BN-T81-D2
Panel configuration and power-on (0x04) are both sent during the INITIALISE
state; the state machine's built-in busy wait then covers the power-on delay
@@ -75,6 +77,17 @@ waveshare_7_5_v2 = uc8179.extend(
height=480,
)
# Good Display GDEY0583T81, 5.83" mono 648x480, UC8179; also sold by Seeed as 0583BN-T81-D2.
# Replaces the waveshare_epaper model gdey0583t81. Busy is low while busy, so set inverted: true.
goodisplay_gdey0583t81 = uc8179.extend(
"goodisplay-gdey0583t81-5.83",
width=648,
height=480,
)
# Name printed on the panel's label as sold by Seeed
goodisplay_gdey0583t81.extend("0583bn-t81-d2")
# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the
# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board
waveshare_7_5_v2.extend(
+4 -3
View File
@@ -236,15 +236,15 @@ COMPILER_OPTIMIZATIONS = {
#
# Note: excluding a component only removes it from the initial build set.
# ESP-IDF's requirement expansion adds an excluded component back when any
# component still in the build REQUIRES it (e.g. espressif/mdns pulls
# "console" back in, esp_http_client pulls "tcp_transport" back in), so
# component still in the build REQUIRES it (e.g. esp_http_client pulls
# "tcp_transport" back in), so
# exclusions here are safe for such components and simply become no-ops in
# builds that need them.
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"app_trace", # CPU trace/SystemView support - unused by ESPHome
"bt", # Bluetooth stack - re-included by request_bluetooth(); its REQUIRES pulls the WiFi stack back
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured
"console", # Console REPL - unused by ESPHome; espressif/mdns needs it only for its CLI, which mdns disables
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp-tls", # TLS wrapper - re-included by request_tls()
"esp_adc", # ADC driver - only needed by adc component
@@ -3003,6 +3003,7 @@ async def _finalize_arduino_aware_flags():
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
cg.add_define("USE_CRASH_REPORT")
elif not CORE.using_arduino:
cg.add_build_flag("-DUSE_ARDUINO")
@@ -5,6 +5,7 @@
#include "crash_handler.h"
#include "esphome/core/build_info_data.h"
#include "esphome/core/crash_report.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -500,6 +501,84 @@ void crash_handler_log() {
} // namespace esphome::esp32
namespace esphome {
static_assert(MAX_BACKTRACE <= CRASH_REPORT_MAX_BACKTRACE, "Crash report backtrace too small for the record");
// The record itself stays valid for log replay this boot
static bool s_report_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Filtered as in log_backtrace()
static uint8_t copy_backtrace(const uint32_t *addrs, uint8_t count, [[maybe_unused]] uint8_t reg_frame_count,
[[maybe_unused]] bool filter, uint32_t *out) {
uint8_t out_count = 0;
for (uint8_t i = 0; i < count; i++) {
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (filter && i >= reg_frame_count && !is_return_addr(addrs[i]))
continue;
#endif
out[out_count++] = addrs[i];
}
return out_count;
}
static CrashReason get_crash_reason() {
switch (s_raw_crash_data.exception) {
case PANIC_EXCEPTION_ABORT:
return CrashReason::CRASH_REASON_ABORT;
case PANIC_EXCEPTION_TWDT:
return CrashReason::CRASH_REASON_TASK_WATCHDOG;
case PANIC_EXCEPTION_IWDT:
return CrashReason::CRASH_REASON_INTERRUPT_WATCHDOG;
default:
return CrashReason::CRASH_REASON_EXCEPTION;
}
}
bool crash_report_get(CrashReport &out) {
if (s_report_read || !esp32::crash_handler_has_data())
return false;
out = {};
out.flags = CRASH_REPORT_FLAG_PRESENT;
const bool build_matches = s_raw_crash_data.build_time == esp32::s_current_build_time;
if (build_matches)
out.flags |= CRASH_REPORT_FLAG_BUILD_MATCHES;
out.reason = get_crash_reason();
out.crashed_core = s_raw_crash_data.crashed_core;
out.pc = s_raw_crash_data.pc;
out.build_time = s_raw_crash_data.build_time;
// A pseudo cause is an IDF panic reason
if (s_raw_crash_data.exception != PANIC_EXCEPTION_ABORT && s_raw_crash_data.exception != PANIC_EXCEPTION_TWDT &&
!s_raw_crash_data.pseudo_excause && esp32::cause_slot_was_written()) {
out.flags |= CRASH_REPORT_FLAG_CAUSE;
out.cause = s_raw_crash_data.cause;
}
if (esp32::has_fault_addr()) {
out.flags |= CRASH_REPORT_FLAG_FAULT_ADDRESS;
out.fault_address = s_raw_crash_data.fault_addr;
}
auto &crashed = out.cores[out.core_count++];
crashed.core = s_raw_crash_data.crashed_core;
crashed.count = copy_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count, build_matches, crashed.addresses.data());
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
auto &other = out.cores[out.core_count++];
other.core = 1 - s_raw_crash_data.crashed_core;
other.count = copy_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
s_raw_crash_data.other_reg_frame_count, build_matches, other.addresses.data());
}
#endif
return true;
}
void crash_report_clear() {
s_report_read = true;
esp32::crash_handler_clear();
}
} // namespace esphome
// --- Panic handler wrapper ---
// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data
// into NOINIT memory before the normal panic handler runs.
@@ -55,10 +55,8 @@ void CameraWebServer::setup() {
config.backlog_conn = 2;
config.lru_purge_enable = true;
if (httpd_start(&this->httpd_, &config) != ESP_OK) {
this->mark_failed(LOG_STR("Failed to start the web server"));
if (!this->setup_check(httpd_start(&this->httpd_, &config) == ESP_OK, LOG_STR("Failed to start the web server")))
return;
}
httpd_uri_t uri = {
.uri = "/",
@@ -69,10 +69,9 @@ void ESP32RMTLEDStripLightOutput::setup() {
size_t buffer_size = this->get_buffer_size_();
RAMAllocator<uint8_t> allocator(this->use_psram_ ? 0 : RAMAllocator<uint8_t>::ALLOC_INTERNAL);
if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->internal_accessor_.allocate(allocator, this->num_leds_),
LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
// copy of the led buffer
@@ -104,10 +103,8 @@ void ESP32RMTLEDStripLightOutput::setup() {
channel.flags.invert_out = this->invert_out_;
channel.flags.with_dma = this->use_dma_;
channel.intr_priority = 0;
if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) {
this->mark_failed(LOG_STR("Channel creation failed"));
if (!this->setup_check(rmt_new_tx_channel(&channel, &this->channel_) == ESP_OK, LOG_STR("Channel creation failed")))
return;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
rmt_simple_encoder_config_t encoder;
@@ -115,23 +112,18 @@ void ESP32RMTLEDStripLightOutput::setup() {
encoder.callback = encoder_callback;
encoder.arg = &this->params_;
encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE;
if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) {
this->mark_failed(LOG_STR("Encoder creation failed"));
if (!this->setup_check(rmt_new_simple_encoder(&encoder, &this->encoder_) == ESP_OK,
LOG_STR("Encoder creation failed")))
return;
}
#else
rmt_copy_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder));
if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) {
this->mark_failed(LOG_STR("Encoder creation failed"));
if (!this->setup_check(rmt_new_copy_encoder(&encoder, &this->encoder_) == ESP_OK, LOG_STR("Encoder creation failed")))
return;
}
#endif
if (rmt_enable(this->channel_) != ESP_OK) {
this->mark_failed(LOG_STR("Enabling channel failed"));
if (!this->setup_check(rmt_enable(this->channel_) == ESP_OK, LOG_STR("Enabling channel failed")))
return;
}
this->accessor_ = &this->internal_accessor_;
}
+1
View File
@@ -424,6 +424,7 @@ async def to_code(config: ConfigType) -> None:
cg.add_define(ThreadModel.SINGLE)
cg.add_define("USE_ESP8266_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
cg.add_define("USE_CRASH_REPORT")
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
+52 -12
View File
@@ -5,6 +5,7 @@
#include "crash_handler.h"
#include "rtc_layout.h"
#include "esphome/core/crash_report.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -123,6 +124,21 @@ static const LogString *get_reset_reason(uint32_t reason) {
return LOG_STR("Unknown");
}
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
// the Arduino core's postmortem handler behavior.
static uint32_t get_exccause() {
static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0;
static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6;
static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5;
static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d;
if (resetInfo.exccause == EXCCAUSE_ILLEGAL_INSTRUCTION &&
(resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) {
return EXCCAUSE_INTEGER_DIVIDE_BY_ZERO;
}
return resetInfo.exccause;
}
// Read backtrace from RTC user memory into caller-provided buffer.
// Returns the number of valid backtrace entries (0 if no data found).
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
@@ -172,18 +188,7 @@ void crash_handler_log() {
// called multiple times (logger init + API subscribe) with the same result.
uint32_t backtrace[MAX_BACKTRACE];
uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
// the Arduino core's postmortem handler behavior.
static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0;
static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6;
static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5;
static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d;
uint32_t exccause = resetInfo.exccause;
if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION &&
(resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) {
exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO;
}
uint32_t exccause = get_exccause();
const LogString *cause = get_exception_cause(exccause);
if (cause != nullptr) {
ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)),
@@ -202,6 +207,41 @@ void crash_handler_log() {
} // namespace esphome::esp8266
namespace esphome {
static_assert(MAX_BACKTRACE <= CRASH_REPORT_MAX_BACKTRACE, "Crash report backtrace too small for the record");
static bool s_report_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool crash_report_get(CrashReport &out) {
if (s_report_read || !esp8266::crash_handler_has_data())
return false;
out = {};
// Never survives into another build
out.flags = CRASH_REPORT_FLAG_PRESENT | CRASH_REPORT_FLAG_BUILD_MATCHES;
if (resetInfo.reason == REASON_WDT_RST) {
// Nothing else is recorded for a hardware WDT reset
out.reason = CrashReason::CRASH_REASON_HARDWARE_WATCHDOG;
return true;
}
out.pc = resetInfo.epc1;
if (resetInfo.reason == REASON_EXCEPTION_RST) {
out.reason = CrashReason::CRASH_REASON_EXCEPTION;
out.flags |= CRASH_REPORT_FLAG_CAUSE | CRASH_REPORT_FLAG_FAULT_ADDRESS;
out.cause = esp8266::get_exccause();
out.fault_address = resetInfo.excvaddr;
} else {
out.reason = CrashReason::CRASH_REASON_SOFT_WATCHDOG;
}
out.core_count = 1;
out.cores[0].count = esp8266::read_rtc_backtrace(out.cores[0].addresses.data(), MAX_BACKTRACE);
return true;
}
void crash_report_clear() { s_report_read = true; }
} // namespace esphome
// --- Custom crash callback ---
// Overrides the weak custom_crash_callback() from Arduino core's
// core_esp8266_postmortem.cpp. Called during exception handling before
+219 -142
View File
@@ -1,4 +1,5 @@
#include "ezo.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
@@ -8,7 +9,57 @@ namespace esphome::ezo {
PROGMEM_STRING_TABLE(EzoCommandTypeStrings, "EZO_READ", "EZO_LED", "EZO_DEVICE_INFORMATION", "EZO_SLOPE",
"EZO_CALIBRATION", "EZO_SLEEP", "EZO_I2C", "EZO_T", "EZO_CUSTOM");
PROGMEM_STRING_TABLE(EzoCalibrationTypeStrings, "LOW", "MID", "HIGH");
// Indexed by EzoCommandId, up to I2C; ids from CAL_LOW to I2C get their value appended
PROGMEM_STRING_TABLE(EzoCommandTextStrings, "i", "Sleep", "Slope,?", "T,?", "Cal,?", "Cal,clear", "L,?", "L,0", "L,1",
"R", "Cal,LOW,", "Cal,MID,", "Cal,HIGH,", "Cal,", "T,", "I2C,");
static_assert(EzoCommandTextStrings::COUNT == static_cast<size_t>(EzoCommandId::EZO_COMMAND_ID_CUSTOM));
static_assert(EzoCommandTextStrings::MAX_LENGTH <= EZO_MAX_COMMAND_LENGTH);
// Indexed by EzoCommandId
static constexpr uint8_t REPLY_TYPES[] PROGMEM = {
EZO_DEVICE_INFORMATION,
EZO_SLEEP,
EZO_SLOPE,
EZO_T,
EZO_CALIBRATION,
EZO_CALIBRATION,
EZO_LED,
EZO_LED,
EZO_LED,
EZO_READ,
EZO_CALIBRATION,
EZO_CALIBRATION,
EZO_CALIBRATION,
EZO_CALIBRATION,
EZO_T,
EZO_I2C,
EZO_CUSTOM,
};
static_assert(sizeof(REPLY_TYPES) == static_cast<size_t>(EzoCommandId::EZO_COMMAND_ID_CUSTOM) + 1);
static EzoCommandType reply_type(EzoCommandId id) {
return static_cast<EzoCommandType>(progmem_read_byte(&REPLY_TYPES[static_cast<uint8_t>(id)]));
}
static constexpr uint32_t LONG_DELAY_MS = 900; // reads and calibration points
static constexpr uint32_t COMMAND_DELAY_MS = 300;
static constexpr size_t MAX_REPLY_LENGTH = 52;
static uint32_t reply_delay_ms(EzoCommandId id) {
return id >= EzoCommandId::EZO_COMMAND_ID_READ && id <= EzoCommandId::EZO_COMMAND_ID_CAL_GENERIC ? LONG_DELAY_MS
: COMMAND_DELAY_MS;
}
// Writes a built-in command's text and returns its length. Values get two decimals and the I2C address none;
// the other commands ignore the value.
static size_t render_command(EzoCommandId id, float value, char *text) {
const int length = ESPHOME_snprintf_P(
text, EZO_MAX_COMMAND_LENGTH + 1,
id >= EzoCommandId::EZO_COMMAND_ID_CAL_LOW ? ESPHOME_PSTR("%s%.*f") : ESPHOME_PSTR("%s"),
LOG_STR_ARG(EzoCommandTextStrings::get_log_str(static_cast<uint8_t>(id), EzoCommandTextStrings::LAST_INDEX)),
id == EzoCommandId::EZO_COMMAND_ID_I2C ? 0 : 2, value);
return length < 0 ? 0 : std::min(static_cast<size_t>(length), EZO_MAX_COMMAND_LENGTH);
}
void EZOSensor::dump_config() {
LOG_SENSOR("", "EZO", this);
@@ -17,73 +68,89 @@ void EZOSensor::dump_config() {
}
void EZOSensor::update() {
// Check if a read is in there already and if not insert on in the second position
if (!this->commands_.empty() && this->commands_.front()->command_type != EzoCommandType::EZO_READ &&
this->commands_.size() > 1) {
bool found = false;
for (auto &i : this->commands_) {
if (i->command_type == EzoCommandType::EZO_READ) {
found = true;
break;
}
}
if (!found) {
auto ezo_command = make_unique<EzoCommand>();
ezo_command->command = "R";
ezo_command->command_type = EzoCommandType::EZO_READ;
ezo_command->delay_ms = 900;
auto it = this->commands_.begin();
++it;
this->commands_.insert(it, std::move(ezo_command));
}
this->read_pending_ = true;
this->enable_loop();
}
void EZOSensor::send_next_() {
const uint32_t now = App.get_loop_component_start_time();
const bool after_read = this->state_ == EzoState::EZO_STATE_IDLE_AFTER_READ;
// A read goes right after the command in flight, but never twice in a row while others wait.
if (this->read_pending_ && (!after_read || this->queue_.empty())) {
ESP_LOGVV(TAG, "Sending command \"R\"");
static constexpr uint8_t READ_COMMAND = 'R';
this->write(&READ_COMMAND, 1);
this->read_pending_ = false;
this->state_ = EzoState::EZO_STATE_SENT_READ;
this->reply_due_ = now + LONG_DELAY_MS;
return;
}
if (this->queue_.empty()) {
this->disable_loop();
return;
}
this->get_state();
const EzoCommand &command = this->queue_.front();
const EzoCommandId id = command.id;
char text[EZO_MAX_COMMAND_LENGTH + 1];
size_t length;
if (id == EzoCommandId::EZO_COMMAND_ID_CUSTOM) {
length = command.text_length;
const size_t first = std::min<size_t>(length, EZO_CUSTOM_TEXT_SIZE - this->custom_text_start_);
memcpy(text, this->custom_text_ + this->custom_text_start_, first);
memcpy(text + first, this->custom_text_, length - first);
text[length] = '\0';
} else {
length = render_command(id, command.value, text);
}
ESP_LOGVV(TAG, "Sending command \"%s\"", text);
this->write(reinterpret_cast<const uint8_t *>(text), length);
if (id == EzoCommandId::EZO_COMMAND_ID_SLEEP || id == EzoCommandId::EZO_COMMAND_ID_I2C) {
// No reply; each queued address change applies its own target
if (id == EzoCommandId::EZO_COMMAND_ID_I2C)
this->address_ = static_cast<uint8_t>(command.value);
this->queue_.pop();
this->state_ = EzoState::EZO_STATE_IDLE;
return;
}
this->state_ = EzoState::EZO_STATE_SENT_QUEUED;
this->reply_due_ = now + reply_delay_ms(id);
}
void EZOSensor::finish_command_() {
if (this->state_ == EzoState::EZO_STATE_SENT_READ) {
this->state_ = EzoState::EZO_STATE_IDLE_AFTER_READ;
return;
}
const EzoCommand &command = this->queue_.front();
if (command.id == EzoCommandId::EZO_COMMAND_ID_CUSTOM) {
this->custom_text_start_ = (this->custom_text_start_ + command.text_length) % EZO_CUSTOM_TEXT_SIZE;
this->custom_text_used_ -= command.text_length;
}
this->queue_.pop();
this->state_ = EzoState::EZO_STATE_IDLE;
}
void EZOSensor::loop() {
if (this->commands_.empty()) {
if (this->state_ == EzoState::EZO_STATE_IDLE || this->state_ == EzoState::EZO_STATE_IDLE_AFTER_READ) {
this->send_next_();
return;
}
EzoCommand *to_run = this->commands_.front().get();
if (!to_run->command_sent) {
const uint8_t *data = reinterpret_cast<const uint8_t *>(to_run->command.c_str());
ESP_LOGVV(TAG, "Sending command \"%s\"", data);
this->write(data, to_run->command.length());
if (to_run->command_type == EzoCommandType::EZO_SLEEP ||
to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data
bool update_address = to_run->command_type == EzoCommandType::EZO_I2C;
this->commands_.pop_front();
if (update_address)
this->address_ = this->new_address_;
return;
}
this->start_time_ = millis();
to_run->command_sent = true;
return;
}
if (millis() - this->start_time_ < to_run->delay_ms)
if (static_cast<int32_t>(App.get_loop_component_start_time() - this->reply_due_) < 0)
return;
uint8_t buf[32];
// Response code, then up to 52 characters (the largest documented reply, EZO-RGB), then a terminator
// that is always set, since a reply that fills the read has none.
uint8_t buf[MAX_REPLY_LENGTH + 2];
buf[0] = 0;
buf[MAX_REPLY_LENGTH + 1] = 0;
if (!this->read_bytes_raw(buf, 32)) {
if (!this->read_bytes_raw(buf, MAX_REPLY_LENGTH + 1)) {
ESP_LOGE(TAG, "read error");
this->commands_.pop_front();
this->finish_command_();
return;
}
@@ -103,138 +170,148 @@ void EZOSensor::loop() {
break;
}
const EzoCommandType command_type =
this->state_ == EzoState::EZO_STATE_SENT_READ ? EzoCommandType::EZO_READ : reply_type(this->queue_.front().id);
// Free the slot first so the callbacks below can queue a follow-up
this->finish_command_();
ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1],
LOG_STR_ARG(EzoCommandTypeStrings::get_log_str(to_run->command_type, EzoCommandTypeStrings::LAST_INDEX)));
LOG_STR_ARG(EzoCommandTypeStrings::get_log_str(command_type, EzoCommandTypeStrings::LAST_INDEX)));
if (buf[0] == 1) {
std::string payload = reinterpret_cast<char *>(&buf[1]);
if (!payload.empty()) {
auto start_location = payload.find(',');
switch (to_run->command_type) {
case EzoCommandType::EZO_READ: {
// some sensors return multiple comma-separated values, terminate string after first one
if (start_location != std::string::npos) {
payload.erase(start_location);
}
auto val = parse_number<float>(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
} else {
this->publish_state(*val);
}
break;
}
case EzoCommandType::EZO_LED:
this->led_callback_.call(payload.back() == '1');
break;
case EzoCommandType::EZO_DEVICE_INFORMATION:
if (start_location != std::string::npos) {
this->device_infomation_callback_.call(payload.substr(start_location + 1));
}
break;
case EzoCommandType::EZO_SLOPE:
if (start_location != std::string::npos) {
this->slope_callback_.call(payload.substr(start_location + 1));
}
break;
case EzoCommandType::EZO_CALIBRATION:
if (start_location != std::string::npos) {
this->calibration_callback_.call(payload.substr(start_location + 1));
}
break;
case EzoCommandType::EZO_T:
if (start_location != std::string::npos) {
this->t_callback_.call(payload.substr(start_location + 1));
}
break;
case EzoCommandType::EZO_CUSTOM:
this->custom_callback_.call(payload);
break;
default:
break;
char *payload = reinterpret_cast<char *>(&buf[1]);
if (buf[0] != 1 || payload[0] == '\0')
return;
char *comma = strchr(payload, ',');
const char *text = comma != nullptr ? comma + 1 : nullptr;
CallbackManager<void(const std::string &)> *callback = nullptr;
switch (command_type) {
case EzoCommandType::EZO_READ: {
// some sensors return multiple comma-separated values, terminate string after first one
if (comma != nullptr)
*comma = '\0';
auto val = parse_number<float>(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload);
} else {
this->publish_state(*val);
}
return;
}
case EzoCommandType::EZO_LED:
this->led_callback_.call(payload[strlen(payload) - 1] == '1');
return;
case EzoCommandType::EZO_DEVICE_INFORMATION:
callback = &this->device_infomation_callback_;
break;
case EzoCommandType::EZO_SLOPE:
callback = &this->slope_callback_;
break;
case EzoCommandType::EZO_CALIBRATION:
callback = &this->calibration_callback_;
break;
case EzoCommandType::EZO_T:
callback = &this->t_callback_;
break;
case EzoCommandType::EZO_CUSTOM:
callback = &this->custom_callback_;
text = payload;
break;
default:
return;
}
this->commands_.pop_front();
if (text != nullptr)
callback->call(std::string(text));
}
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
auto ezo_command = make_unique<EzoCommand>();
ezo_command->command = command;
ezo_command->command_type = command_type;
ezo_command->delay_ms = delay_ms;
this->commands_.push_back(std::move(ezo_command));
}
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%s,%0.2f"),
LOG_STR_ARG(EzoCalibrationTypeStrings::get_log_str(type, EzoCalibrationTypeStrings::LAST_INDEX)),
value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
void EZOSensor::add_command_(EzoCommandId id, float value) {
if (!this->queue_.push(EzoCommand{id, {value}})) {
ESP_LOGW(
TAG, "Command queue full, dropped \"%s\"",
LOG_STR_ARG(EzoCommandTextStrings::get_log_str(static_cast<uint8_t>(id), EzoCommandTextStrings::LAST_INDEX)));
return;
}
this->enable_loop();
}
void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
// max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("I2C,%u"), address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
this->add_command_(EzoCommandId::EZO_COMMAND_ID_I2C, address);
} else {
ESP_LOGE(TAG, "Invalid I2C address");
}
}
void EZOSensor::get_device_information() { this->add_command_("i", EzoCommandType::EZO_DEVICE_INFORMATION); }
void EZOSensor::get_device_information() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_DEVICE_INFORMATION); }
void EZOSensor::set_sleep() { this->add_command_("Sleep", EzoCommandType::EZO_SLEEP); }
void EZOSensor::set_sleep() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_SLEEP); }
void EZOSensor::get_state() { this->add_command_("R", EzoCommandType::EZO_READ, 900); }
void EZOSensor::get_state() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_READ); }
void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_SLOPE); }
void EZOSensor::get_slope() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_SLOPE); }
void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::get_t() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_T_GET); }
void EZOSensor::set_t(float value) {
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("T,%0.2f"), value);
this->add_command_(payload, EzoCommandType::EZO_T);
// A newer value replaces one that is still waiting to be sent
bool in_flight = this->state_ == EzoState::EZO_STATE_SENT_QUEUED;
for (EzoCommand &command : this->queue_) {
if (!in_flight && command.id == EzoCommandId::EZO_COMMAND_ID_T_SET) {
command.value = value;
return;
}
in_flight = false;
}
this->add_command_(EzoCommandId::EZO_COMMAND_ID_T_SET, value);
}
void EZOSensor::set_tempcomp_value(float temp) { this->set_t(temp); }
void EZOSensor::get_calibration() { this->add_command_("Cal,?", EzoCommandType::EZO_CALIBRATION); }
void EZOSensor::get_calibration() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_CAL_GET); }
void EZOSensor::set_calibration_point_low(float value) {
this->set_calibration_point_(EzoCalibrationType::EZO_CAL_LOW, value);
this->add_command_(EzoCommandId::EZO_COMMAND_ID_CAL_LOW, value);
}
void EZOSensor::set_calibration_point_mid(float value) {
this->set_calibration_point_(EzoCalibrationType::EZO_CAL_MID, value);
this->add_command_(EzoCommandId::EZO_COMMAND_ID_CAL_MID, value);
}
void EZOSensor::set_calibration_point_high(float value) {
this->set_calibration_point_(EzoCalibrationType::EZO_CAL_HIGH, value);
this->add_command_(EzoCommandId::EZO_COMMAND_ID_CAL_HIGH, value);
}
void EZOSensor::set_calibration_generic(float value) {
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
ESPHOME_snprintf_P(payload, sizeof(payload), ESPHOME_PSTR("Cal,%0.2f"), value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
this->add_command_(EzoCommandId::EZO_COMMAND_ID_CAL_GENERIC, value);
}
void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommandType::EZO_CALIBRATION); }
void EZOSensor::clear_calibration() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_CAL_CLEAR); }
void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); }
void EZOSensor::get_led_state() { this->add_command_(EzoCommandId::EZO_COMMAND_ID_LED_GET); }
void EZOSensor::set_led_state(bool on) { this->add_command_(on ? "L,1" : "L,0", EzoCommandType::EZO_LED); }
void EZOSensor::set_led_state(bool on) {
this->add_command_(on ? EzoCommandId::EZO_COMMAND_ID_LED_ON : EzoCommandId::EZO_COMMAND_ID_LED_OFF);
}
void EZOSensor::send_custom(const std::string &to_send) {
this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM);
const size_t length = to_send.size();
if (length > EZO_MAX_COMMAND_LENGTH) {
ESP_LOGW(TAG, "Command too long (%u > %u), dropped", static_cast<unsigned>(length),
static_cast<unsigned>(EZO_MAX_COMMAND_LENGTH));
return;
}
if (this->queue_.size() >= EZO_COMMAND_QUEUE_SIZE || this->custom_text_used_ + length > EZO_CUSTOM_TEXT_SIZE) {
ESP_LOGW(TAG, "Command queue full, dropped \"%s\"", to_send.c_str());
return;
}
// Append after the queued texts, wrapping around the end of the ring
const size_t end = (this->custom_text_start_ + this->custom_text_used_) % EZO_CUSTOM_TEXT_SIZE;
const size_t first = std::min(length, EZO_CUSTOM_TEXT_SIZE - end);
memcpy(this->custom_text_ + end, to_send.data(), first);
memcpy(this->custom_text_, to_send.data() + first, length - first);
this->custom_text_used_ += length;
EzoCommand command{EzoCommandId::EZO_COMMAND_ID_CUSTOM, {}};
command.text_length = length;
this->queue_.push(command);
this->enable_loop();
}
} // namespace esphome::ezo
+61 -20
View File
@@ -3,8 +3,9 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <deque>
#include <string>
namespace esphome::ezo {
@@ -22,14 +23,49 @@ enum EzoCommandType : uint8_t {
EZO_CUSTOM
};
enum EzoCalibrationType : uint8_t { EZO_CAL_LOW = 0, EZO_CAL_MID = 1, EZO_CAL_HIGH = 2 };
/// Longest command text, without the terminator. The longest documented command is the datasheet's
/// "Import, 59 6F 75 20 61 72" (25). Longer custom commands are dropped.
static constexpr size_t EZO_MAX_COMMAND_LENGTH = 25;
/// Total command slots, including the one in flight. The periodic read does not take a slot.
static constexpr uint8_t EZO_COMMAND_QUEUE_SIZE = 8;
/// Bytes shared by the texts of all queued custom commands, stored back to back without terminators.
static constexpr uint8_t EZO_CUSTOM_TEXT_SIZE = 64;
class EzoCommand {
public:
std::string command;
uint16_t delay_ms = 0;
bool command_sent = false;
EzoCommandType command_type;
/// Each built-in command; the text is rendered only when it is sent. The order matches the tables in ezo.cpp.
enum class EzoCommandId : uint8_t {
EZO_COMMAND_ID_DEVICE_INFORMATION,
EZO_COMMAND_ID_SLEEP,
EZO_COMMAND_ID_SLOPE,
EZO_COMMAND_ID_T_GET,
EZO_COMMAND_ID_CAL_GET,
EZO_COMMAND_ID_CAL_CLEAR,
EZO_COMMAND_ID_LED_GET,
EZO_COMMAND_ID_LED_OFF,
EZO_COMMAND_ID_LED_ON,
EZO_COMMAND_ID_READ, // READ to CAL_GENERIC wait 900 ms for the reply
EZO_COMMAND_ID_CAL_LOW, // CAL_LOW to I2C send their value
EZO_COMMAND_ID_CAL_MID,
EZO_COMMAND_ID_CAL_HIGH,
EZO_COMMAND_ID_CAL_GENERIC,
EZO_COMMAND_ID_T_SET,
EZO_COMMAND_ID_I2C,
EZO_COMMAND_ID_CUSTOM, // the text is in EZOSensor::custom_text_
};
struct __attribute__((packed)) EzoCommand {
EzoCommandId id;
union __attribute__((packed)) {
float value; // CAL_* and T_SET value, or the I2C address
uint8_t text_length; // CUSTOM: length of its text in EZOSensor::custom_text_
};
};
static_assert(sizeof(EzoCommand) == 5, "a queue slot holds the command id and one float");
enum class EzoState : uint8_t {
EZO_STATE_IDLE,
EZO_STATE_IDLE_AFTER_READ, // a pending read waits for one queued command first
EZO_STATE_SENT_QUEUED, // the queue's front command is waiting for its reply
EZO_STATE_SENT_READ,
};
/// This class implements support for the EZO circuits in i2c mode
@@ -85,21 +121,26 @@ class EZOSensor final : public sensor::Sensor, public PollingComponent, public i
template<typename F> void add_custom_callback(F &&callback) { this->custom_callback_.add(std::forward<F>(callback)); }
protected:
std::deque<std::unique_ptr<EzoCommand>> commands_;
int new_address_;
void add_command_(EzoCommandId id, float value = 0.0f);
void send_next_();
void finish_command_();
void add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms = 300);
void set_calibration_point_(EzoCalibrationType type, float value);
CallbackManager<void(std::string)> device_infomation_callback_{};
CallbackManager<void(std::string)> calibration_callback_{};
CallbackManager<void(std::string)> slope_callback_{};
CallbackManager<void(std::string)> t_callback_{};
CallbackManager<void(std::string)> custom_callback_{};
CallbackManager<void(const std::string &)> device_infomation_callback_{};
CallbackManager<void(const std::string &)> calibration_callback_{};
CallbackManager<void(const std::string &)> slope_callback_{};
CallbackManager<void(const std::string &)> t_callback_{};
CallbackManager<void(const std::string &)> custom_callback_{};
CallbackManager<void(bool)> led_callback_{};
uint32_t start_time_ = 0;
/// Queued commands; the front one stays in place while it is in flight. The periodic read is not queued.
StaticRingBuffer<EzoCommand, EZO_COMMAND_QUEUE_SIZE> queue_;
uint32_t reply_due_{0}; // when the reply to the command in flight can be read
EzoState state_{EzoState::EZO_STATE_IDLE};
bool read_pending_{false}; // a periodic read was requested and is not yet sent
/// Ring of queued custom texts in queue order; the oldest starts at custom_text_start_.
uint8_t custom_text_start_{0};
uint8_t custom_text_used_{0};
char custom_text_[EZO_CUSTOM_TEXT_SIZE];
};
} // namespace esphome::ezo
+3 -3
View File
@@ -66,10 +66,10 @@ void FUSB302B::setup() {
this->interrupt_pin_->setup();
// The task must exist before the interrupt is attached, since the ISR notifies it
if (!this->task_.create(FUSB302B::task_func, "fusb302b", TASK_STACK_SIZE, this, TASK_PRIORITY, false)) {
this->mark_failed(LOG_STR("Failed to create task"));
if (!this->setup_check(
this->task_.create(FUSB302B::task_func, "fusb302b", TASK_STACK_SIZE, this, TASK_PRIORITY, false),
LOG_STR("Failed to create task")))
return;
}
this->interrupt_pin_->attach_interrupt(&FUSB302B::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
+51 -20
View File
@@ -9,6 +9,9 @@
namespace esphome::gpio_expander {
/// Pin number type for an expander with N pins.
template<uint16_t N> using expander_pin_t = std::conditional_t<(N > 256), uint16_t, uint8_t>;
/// @brief A class to cache the read state of a GPIO expander.
/// This class caches reads between GPIO Pins which are on the same bank.
/// This means that for reading whole Port (ex. 8 pins) component needs only one
@@ -25,21 +28,25 @@ namespace esphome::gpio_expander {
/// P - Type for pin number parameters (automatically selected based on N:
/// uint8_t for N<=256, uint16_t for N>256). Can be explicitly specified
/// if needed (e.g., for components like SN74HC165 with >256 pins)
template<typename T, uint16_t N, typename P = typename std::conditional<(N > 256), uint16_t, uint8_t>::type>
class CachedGpioExpander {
///
/// CachedGpioExpanderBase holds the cache; CachedGpioExpander adds the invalidate-on-read flag, and
/// CachedGpioExpanderComponent takes it from the interrupt pin instead.
template<typename T, uint16_t N, typename P = expander_pin_t<N>> class CachedGpioExpanderBase {
public:
/// @brief Read the state of the given pin.
/// By default, each read invalidates the pin's cache entry so the next read
/// of the same pin triggers a fresh hardware read. When invalidate_on_read
/// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_().
/// @param pin Pin number to read
/// @return Pin state
bool digital_read(P pin) {
void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); }
protected:
using bank_t = T;
using pin_t = P;
/// Read pin through the bank cache. With invalidate_on_read the pin is read from hardware again next
/// time; otherwise the bank stays cached until reset_pin_cache_().
bool cached_read_(P pin, bool invalidate_on_read) {
const P bank = pin / BANK_SIZE;
const T pin_mask = (1 << (pin % BANK_SIZE));
const T pin_mask = T(1) << (pin % BANK_SIZE);
// Check if specific pin cache is valid
if (this->read_cache_valid_[bank] & pin_mask) {
if (this->invalidate_on_read_) {
if (invalidate_on_read) {
// Invalidate pin so next read triggers hardware read
this->read_cache_valid_[bank] &= ~pin_mask;
}
@@ -49,14 +56,11 @@ class CachedGpioExpander {
return false;
// Mark bank cache as valid except the pin that is being returned now
// (when not invalidating on read, mark all pins including this one as valid)
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~(this->invalidate_on_read_ ? pin_mask : 0);
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~(invalidate_on_read ? pin_mask : 0);
}
return this->digital_read_cache(pin);
}
void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); }
protected:
/// @brief Read GPIO bank from hardware into internal state
/// @param pin Pin number (used to determine which bank to read)
/// @return true if read succeeded, false on communication error
@@ -77,18 +81,45 @@ class CachedGpioExpander {
/// @brief Invalidate cache. This function should be called in component loop().
void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
/// @brief Control whether digital_read() invalidates the pin's cache entry after reading.
/// When enabled (default), each read self-invalidates so the next read triggers a hardware read.
/// When disabled, cache stays valid until reset_pin_cache_() is explicitly called.
void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; }
static constexpr uint16_t BITS_PER_BYTE = 8;
static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
static constexpr size_t BANKS = N / BANK_SIZE;
static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
T read_cache_valid_[BANKS]{0};
};
/// A CachedGpioExpanderBase whose digital_read() invalidates each pin it returns unless told otherwise.
template<typename T, uint16_t N, typename P = expander_pin_t<N>>
class CachedGpioExpander : public CachedGpioExpanderBase<T, N, P> {
public:
bool digital_read(P pin) { return this->cached_read_(pin, this->invalidate_on_read_); }
protected:
/// @brief Control whether digital_read() invalidates the pin's cache entry after reading.
/// When enabled (default), each read self-invalidates so the next read triggers a hardware read.
/// When disabled, cache stays valid until reset_pin_cache_() is explicitly called.
void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; }
bool invalidate_on_read_{true};
};
/// Adds a store of the last value read from each bank to a read cache Base. digital_read_hw() stores the
/// bank it reads in input_[pin / BANK_SIZE] (a chip read in one transfer is one bank, input_[0]), and
/// digital_read_cache() is provided.
template<typename Base> class GpioExpanderInputs : public Base {
protected:
using typename Base::bank_t;
using typename Base::pin_t;
using Base::BANK_SIZE;
using Base::BANKS;
bool digital_read_cache(pin_t pin) override {
return this->input_[pin / BANK_SIZE] & (bank_t(1) << (pin % BANK_SIZE));
}
/// The input state per bank - 1 means HIGH, 0 means LOW
bank_t input_[BANKS]{};
};
} // namespace esphome::gpio_expander
@@ -5,18 +5,17 @@ namespace esphome::gpio_expander {
// Not a member of the template: GCC 10 (ESP8266) ignores section attributes on template instantiations.
static void IRAM_ATTR interrupt_pin_isr(Component *arg) { arg->enable_loop_soon_any_context(); }
bool InterruptExpanderComponent::attach_interrupt_pin_() {
void InterruptExpanderComponent::setup_interrupt_pin_() {
// Polling mode enables the loop in pin_mode() for the first input; interrupt mode from the ISR.
this->disable_loop();
if (this->interrupt_pin_ == nullptr)
return false;
return;
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&interrupt_pin_isr, static_cast<Component *>(this),
gpio::INTERRUPT_FALLING_EDGE);
// An INT line already low at attach gives no falling edge, so read the inputs now
if (!this->interrupt_pin_->digital_read())
this->enable_loop();
return true;
}
// Input changes that straddle the read leave INT asserted without a new falling edge, so keep looping
@@ -14,9 +14,9 @@ class InterruptExpanderComponent : public Component {
float get_setup_priority() const override { return setup_priority::IO; }
protected:
/// Attach the falling-edge ISR when there is an interrupt pin, then disable the loop until an input
/// needs it. Returns whether the interrupt pin is in use.
bool attach_interrupt_pin_();
/// Call at the end of setup(): attach the falling-edge ISR when there is an interrupt pin, then disable the
/// loop until an input needs it.
void setup_interrupt_pin_();
/// The loop runs again after the INT line falls; stop it once the line is released (HIGH).
/// Only called with an interrupt pin.
void disable_loop_when_released_();
@@ -29,22 +29,19 @@ class InterruptExpanderComponent : public Component {
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Base for expanders that cache reads per loop. With an interrupt pin the cache stays valid until INT fires.
/// Base for expanders that cache reads per loop, with the per-bank input store of GpioExpanderInputs. Without
/// an interrupt pin each read invalidates its pin; with one the cache stays valid until INT fires.
template<typename T, uint16_t N>
class CachedGpioExpanderComponent : public InterruptExpanderComponent, public CachedGpioExpander<T, N> {
class CachedGpioExpanderComponent : public InterruptExpanderComponent,
public GpioExpanderInputs<CachedGpioExpanderBase<T, N>> {
public:
bool digital_read(expander_pin_t<N> pin) { return this->cached_read_(pin, this->interrupt_pin_ == nullptr); }
void loop() override {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr)
this->disable_loop_when_released_();
}
protected:
/// Call at the end of setup().
void setup_interrupt_pin_() {
if (this->attach_interrupt_pin_())
this->set_invalidate_on_read_(false);
}
};
} // namespace esphome::gpio_expander
@@ -13,6 +13,7 @@ from esphome.const import (
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from .. import gree_ns
@@ -82,7 +83,10 @@ def _feature_switch_schema(
entity_category=ENTITY_CATEGORY_CONFIG,
)
@schema_extractor("schema")
def validator(value: ConfigType) -> ConfigType:
if value is SCHEMA_EXTRACT:
return schema
explicit = isinstance(value, dict) and any(
key in value for key in _RESTORE_KEYS
)
@@ -23,12 +23,7 @@ void GroveMotorDriveTB6612FNG::dump_config() {
LOG_I2C_DEVICE(this);
}
void GroveMotorDriveTB6612FNG::setup() {
if (!this->standby()) {
this->mark_failed();
return;
}
}
void GroveMotorDriveTB6612FNG::setup() { this->setup_check(this->standby()); }
bool GroveMotorDriveTB6612FNG::standby() {
uint8_t status = 0;
@@ -68,10 +68,8 @@ void I2SClocklessLedStrip::dump_config() {
float I2SClocklessLedStrip::get_setup_priority() const { return setup_priority::IO; }
void I2SClocklessLedStrip::setup() {
if (!this->allocate_buffers_()) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->allocate_buffers_(), LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
i2s_chan_config_t chan_config = {
.id = I2S_NUM_AUTO,
+1 -1
View File
@@ -120,7 +120,7 @@ bool IrRfEntity::on_receive(remote_base::RemoteReceiveData data) {
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
+3 -5
View File
@@ -19,7 +19,7 @@ static constexpr uint32_t MAX_TRANSFER_TIME_MS = 20;
// --- Loop / scheduling -------------------------------------------------------
void IT8951Display::enqueue_(OpType type, uint16_t a, uint16_t b) {
if (!this->queue_.push_back(Op{type, a, b})) {
if (!this->queue_.push(Op{type, a, b})) {
ESP_LOGE(TAG, "Op queue overflow (cap=%u); dropping op type=%u", static_cast<unsigned>(OP_QUEUE_SIZE),
static_cast<unsigned>(type));
}
@@ -74,7 +74,7 @@ void IT8951Display::loop() {
return;
}
this->queue_.pop_front();
this->queue_.pop();
this->process_op_(queued_op);
}
@@ -330,10 +330,8 @@ void IT8951Display::setup() {
this->buffer_length_ = static_cast<size_t>(this->row_width_) * static_cast<size_t>(this->height_);
RAMAllocator<uint8_t> allocator{};
this->buffer_ = allocator.allocate(this->buffer_length_);
if (this->buffer_ == nullptr) {
this->mark_failed(LOG_STR("Failed to allocate IT8951 framebuffer"));
if (!this->setup_check(this->buffer_ != nullptr, LOG_STR("Failed to allocate IT8951 framebuffer")))
return;
}
// The allocator does not zero memory; start blank (white) so undrawn regions
// (e.g. with auto_clear disabled) don't show garbage on the first update.
this->fill(Color::WHITE);
+2 -54
View File
@@ -15,59 +15,7 @@ namespace esphome::it8951 {
using namespace display;
// --- Bounded op queue --------------------------------------------------------
// Fixed-capacity ring buffer used by the loop scheduler. Replaces std::deque
// to comply with ESPHome's STL container guidelines (std::deque allocates in
// 512-byte blocks regardless of element size). Size analysis: the deepest
// observed scenario is UPDATE_REFRESH (10 enqueued ops) + CHECK_LUT_IDLE's
// 5 push_front rescheduling = 14 simultaneous entries. We use 32 for a
// comfortable margin while keeping RAM cost low (~192 bytes per instance vs
// 512+ bytes for std::deque).
template<typename T, size_t N> class StaticOpQueue {
public:
bool empty() const { return this->count_ == 0; }
size_t size() const { return this->count_; }
static constexpr size_t capacity() { return N; }
bool push_back(const T &value) {
if (this->count_ >= N)
return false;
this->data_[(this->head_ + this->count_) % N] = value;
++this->count_;
return true;
}
bool push_front(const T &value) {
if (this->count_ >= N)
return false;
this->head_ = (this->head_ + N - 1) % N;
this->data_[this->head_] = value;
++this->count_;
return true;
}
void pop_front() {
if (this->count_ == 0)
return;
this->head_ = (this->head_ + 1) % N;
--this->count_;
}
const T &front() const { return this->data_[this->head_]; }
T &front() { return this->data_[this->head_]; }
void clear() {
this->head_ = 0;
this->count_ = 0;
}
private:
T data_[N]{};
size_t head_{0};
size_t count_{0};
};
// Op queue capacity. See StaticOpQueue comment for sizing analysis.
// Deepest observed: UPDATE_REFRESH (10 ops) + 5 CHECK_LUT_IDLE reschedules = 14; 32 for margin
static constexpr size_t OP_QUEUE_SIZE = 32;
// --- Op queue ---------------------------------------------------------------
@@ -278,7 +226,7 @@ class IT8951Display : public Display,
// --- State ---
static constexpr uint32_t BUSY_TIMEOUT_MS = 5000;
StaticOpQueue<Op, OP_QUEUE_SIZE> queue_;
StaticRingBuffer<Op, OP_QUEUE_SIZE> queue_;
Phase phase_{Phase::IDLE};
uint32_t delay_until_{0};
uint32_t phase_started_at_{0};
-3
View File
@@ -10,7 +10,6 @@ from esphome.const import (
CONF_TIMEOUT,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_SIGNAL_STRENGTH,
ENTITY_CATEGORY_CONFIG,
ICON_LIGHTBULB,
ICON_MOTION_SENSOR,
@@ -65,14 +64,12 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
-3
View File
@@ -10,7 +10,6 @@ from esphome.const import (
CONF_TIMEOUT,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_SIGNAL_STRENGTH,
ENTITY_CATEGORY_CONFIG,
ICON_LIGHTBULB,
ICON_MOTION_SENSOR,
@@ -65,14 +64,12 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
+1 -3
View File
@@ -187,10 +187,8 @@ void LD600XComponent::setup() {
// One allocation for the component lifetime; the parser reuses it for every payload.
RAMAllocator<uint8_t> allocator;
uint8_t *buffer = allocator.allocate(capacity);
if (buffer == nullptr) {
this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
if (!this->setup_check(buffer != nullptr, LOG_STR("Failed to allocate frame buffer")))
return;
}
this->parser_.init(buffer, capacity);
this->set_timeout(SETUP_DELAY_MS, [this]() {
+2
View File
@@ -568,6 +568,8 @@ async def component_to_code(config):
# Crash handler (crash_handler.cpp)
cg.add_define("USE_LIBRETINY_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
cg.add_define("USE_CRASH_REPORT")
cg.add_build_flag("-Wl,--wrap=abort")
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
for trap in BK_TRAP_HANDLERS:
cg.add_build_flag(f"-Wl,--wrap={trap}")
+100 -20
View File
@@ -10,6 +10,8 @@
#endif
#include "core.h"
#include <FreeRTOS.h>
#include "esphome/core/crash_report.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -96,8 +98,10 @@ static constexpr uint32_t CRASH_REASON_PREFETCH_ABORT = 2;
static constexpr uint32_t CRASH_REASON_DATA_ABORT = 3;
// Cortex-M families: the exception number (3 to 7) plus this offset.
static constexpr uint32_t CRASH_REASON_CORTEX_M_OFFSET = 1;
// After the Cortex-M ones (4 to 8).
static constexpr uint32_t CRASH_REASON_ABORT = 9;
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort",
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault");
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault", "Abort");
static constexpr size_t MAX_BACKTRACE = 8;
// Words scanned above sp for return addresses.
@@ -155,7 +159,10 @@ void crash_handler_log() {
ESP_LOGE(TAG, " CFSR: 0x%08" PRIX32 " Address: 0x%08" PRIX32, s_record.fault_status, s_record.fault_address);
}
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_record.pc);
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_record.lr);
// 0 for abort(), and for a Cortex-M frame outside SRAM
if (s_record.lr != 0) {
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_record.lr);
}
ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_record.sp);
uint8_t count = 0;
for (; count < MAX_BACKTRACE && s_record.backtrace[count] != 0; count++) {
@@ -163,8 +170,9 @@ void crash_handler_log() {
}
// addr2line hint with every address
char hint[160];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e raw_firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
s_record.pc, s_record.lr);
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e raw_firmware.elf 0x%08" PRIX32, s_record.pc);
if (s_record.lr != 0)
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_record.lr);
for (uint8_t i = 0; i < count && pos < (int) sizeof(hint) - 12; i++) {
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_record.backtrace[i]);
}
@@ -190,6 +198,16 @@ static inline bool is_code_addr(uint32_t val) {
#endif
}
static void fill_record(uint32_t reason, uint32_t pc, uint32_t lr, uint32_t sp, uint32_t fault_status,
uint32_t fault_address) {
s_record.reason = reason;
s_record.pc = pc;
s_record.lr = lr;
s_record.sp = sp;
s_record.fault_status = fault_status;
s_record.fault_address = fault_address;
}
// Return addresses above sp give the backtrace; then the magic marks the record complete.
static void finish_record(uint32_t sp, uint32_t sram_start, uint32_t sram_end) {
uint32_t count = 0;
@@ -264,12 +282,7 @@ static void record_crash(arm_registers *regs, uint32_t reason, uint32_t pc_offse
uint32_t lr = 0;
read_saved_sp_lr(mode, &sp, &lr);
s_record.reason = reason;
s_record.pc = regs->pc - pc_offset;
s_record.lr = lr;
s_record.sp = sp;
s_record.fault_status = 0;
s_record.fault_address = 0;
fill_record(reason, regs->pc - pc_offset, lr, sp, 0, 0);
finish_record(sp, SRAM_START, SRAM_END);
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
@@ -366,22 +379,17 @@ extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *f
} else if (cfsr & CFSR_MMARVALID) {
address = read_reg(SCB_MMFAR);
}
s_record.reason = vector + CRASH_REASON_CORTEX_M_OFFSET;
s_record.fault_status = cfsr;
s_record.fault_address = address;
auto frame_addr = (uint32_t) frame;
uint32_t frame_words = (exc_return & EXC_RETURN_STANDARD_FRAME) ? FRAME_WORDS : EXTENDED_FRAME_WORDS;
uint32_t pc = 0;
uint32_t lr = 0;
uint32_t sp = 0;
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - frame_words * 4 && (frame_addr & 3) == 0) {
s_record.pc = frame[FRAME_PC];
s_record.lr = frame[FRAME_LR];
pc = frame[FRAME_PC];
lr = frame[FRAME_LR];
sp = frame_addr + frame_words * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
} else {
s_record.pc = 0;
s_record.lr = 0;
}
s_record.sp = sp;
fill_record(vector + CRASH_REASON_CORTEX_M_OFFSET, pc, lr, sp, cfsr, address);
finish_record(sp, SRAM_START, SRAM_END);
return s_sdk_fault_vectors[vector - FIRST_FAULT_VECTOR];
}
@@ -416,7 +424,79 @@ void crash_handler_install() {
#endif // USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
// newlib's abort() spins in _exit() with no record and, on the BK7238, no watchdog reset. Interrupts go off first
// because every task's delay() feeds the watchdog.
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" [[noreturn]] void __wrap_abort() {
portDISABLE_INTERRUPTS();
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
uint32_t sp;
__asm volatile("mov %0, sp" : "=r"(sp));
// Inside the bl that called abort(): a noreturn call can be the caller's last instruction, so the return
// address may already be the next function. Bit 0 is the Thumb bit.
fill_record(CRASH_REASON_ABORT, ((uint32_t) __builtin_return_address(0) & ~1u) - 2, 0, sp, 0, 0);
// Valid without a backtrace, so a fault in the stack scan still leaves this record; every entry is cleared, so
// a scan cut short leaves none from an earlier crash
for (auto &addr : s_record.backtrace)
addr = 0;
s_record.magic = CRASH_MAGIC_V2;
// Keeps those writes ahead of the scan's reads
__asm volatile("" ::: "memory");
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
// A fault in the scan then spins for the watchdog instead of replacing this record or running the SDK dump
s_fault_recorded = true;
#endif
finish_record(sp, SRAM_START, SRAM_END);
while (true) {
}
}
} // namespace esphome::libretiny
namespace esphome {
static_assert(libretiny::MAX_BACKTRACE + 1 <= CRASH_REPORT_MAX_BACKTRACE,
"Crash report backtrace too small for the record");
static bool s_report_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool crash_report_get(CrashReport &out) {
if (s_report_read || !libretiny::crash_handler_has_data())
return false;
const auto &record = libretiny::s_record;
out = {};
// The record has no build stamp; like ESP8266 and RP2, assume the running firmware wrote it
out.flags = CRASH_REPORT_FLAG_PRESENT | CRASH_REPORT_FLAG_BUILD_MATCHES;
if (record.reason == libretiny::CRASH_REASON_ABORT) {
out.reason = CrashReason::CRASH_REASON_ABORT;
} else {
out.reason = record.reason != 0 ? CrashReason::CRASH_REASON_EXCEPTION : CrashReason::CRASH_REASON_UNKNOWN;
}
out.pc = record.pc;
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
// Cortex-M CFSR, and the BFAR or MMFAR its valid bits mark (0 is a real address for a null dereference);
// BK72xx records neither
if (record.fault_status != 0) {
out.flags |= CRASH_REPORT_FLAG_CAUSE;
out.cause = record.fault_status;
}
if (record.fault_status & (libretiny::CFSR_BFARVALID | libretiny::CFSR_MMARVALID)) {
out.flags |= CRASH_REPORT_FLAG_FAULT_ADDRESS;
out.fault_address = record.fault_address;
}
#endif
auto &core = out.cores[out.core_count++];
// 0 for abort(), and for a Cortex-M frame outside SRAM
if (record.lr != 0)
core.addresses[core.count++] = record.lr;
for (size_t i = 0; i < libretiny::MAX_BACKTRACE && record.backtrace[i] != 0; i++)
core.addresses[core.count++] = record.backtrace[i];
return true;
}
void crash_report_clear() { s_report_read = true; }
} // namespace esphome
#endif // USE_LIBRETINY_CRASH_HANDLER
#endif // USE_LIBRETINY
@@ -110,10 +110,9 @@ void LN882xSPILEDStripLightOutput::setup() {
// One block for the color, effect and DMA data.
RAMAllocator<uint8_t> allocator;
if (!this->internal_accessor_.allocate(allocator, this->num_leds_, this->dma_data_bytes_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->internal_accessor_.allocate(allocator, this->num_leds_, this->dma_data_bytes_),
LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
this->dma_data_ = this->internal_accessor_.effect_data + this->num_leds_;
spi_dma_init(this->pin_, this->prescaler_);
+2
View File
@@ -618,6 +618,8 @@ async def _late_logger_init(config: ConfigType) -> None:
cg.add_define("USE_ESP32_LOG_V2")
# --wrap: a strong definition collides; see logger_esp32.cpp
cg.add_build_flag("-Wl,--wrap=esp_log_format")
if CORE.is_rp2 and config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
# Define platform support flags for components that need auto-detection
try:
uart_selection(USB_SERIAL_JTAG)
+3
View File
@@ -151,6 +151,9 @@ class Logger final : public Component {
explicit Logger(uint32_t baud_rate);
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
void loop() override;
#endif
#if defined(USE_RP2_CRASH_HANDLER) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
void setup() override;
#endif
/// Manually set the baud rate for serial, set to 0 to disable.
void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
+16 -1
View File
@@ -10,6 +10,20 @@ namespace esphome::logger {
ESPHOME_LOG_TAG(TAG, "logger");
#if defined(USE_RP2_CRASH_HANDLER) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
void Logger::setup() {
if (this->hw_serial_ == nullptr || !crash_log_pending())
return;
// Report the crash each time a host opens the USB port (sets DTR)
this->set_interval(500, [opened_before = false]() mutable {
const bool opened = Serial.dtr();
if (opened && !opened_before)
log_crash_to_client();
opened_before = opened;
});
}
#endif
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
@@ -29,7 +43,8 @@ void Logger::pre_setup() {
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
#ifdef USE_RP2_CRASH_HANDLER
// Nobody has a USB port open this early; setup() reports the crash once a host opens it
#if defined(USE_RP2_CRASH_HANDLER) && !defined(USE_LOGGER_UART_SELECTION_USB_CDC)
rp2::crash_handler_log();
#endif
}
+1 -2
View File
@@ -26,9 +26,8 @@ void MCP23016::setup() {
this->setup_interrupt_pin_();
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_[0]); }
bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin, value, MCP23016_OLAT1); }
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
-3
View File
@@ -35,7 +35,6 @@ class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_
protected:
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
// read a given register
@@ -46,8 +45,6 @@ class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_
void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a);
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x0000};
};
using MCP23016GPIOPin = gpio_expander::ExpanderGPIOPin<MCP23016>;
@@ -7,7 +7,7 @@ namespace esphome::mcp23x08_base {
ESPHOME_LOG_TAG(TAG, "mcp23x08_base");
bool MCP23X08Base::digital_read_hw(uint8_t pin) {
if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_mask_)) {
if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_[0])) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
@@ -19,8 +19,6 @@ void MCP23X08Base::digital_write_hw(uint8_t pin, bool value) {
this->update_reg(pin, value, reg_addr);
}
bool MCP23X08Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) {
uint8_t iodir = mcp23x08_base::MCP23X08_IODIR;
uint8_t gppu = mcp23x08_base::MCP23X08_GPPU;
@@ -25,7 +25,6 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> {
public:
bool digital_read_hw(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool digital_read_cache(uint8_t pin) override;
void pin_mode(uint8_t pin, gpio::Flags flags) override;
void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override;
@@ -34,9 +33,6 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> {
void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) override;
uint8_t olat_{0x00};
/// State read in digital_read_hw
uint8_t input_mask_{0x00};
};
} // namespace esphome::mcp23x08_base
@@ -8,19 +8,11 @@ ESPHOME_LOG_TAG(TAG, "mcp23x17_base");
bool MCP23X17Base::digital_read_hw(uint8_t pin) {
uint8_t data;
if (pin < 8) {
if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &data)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->input_mask_ = encode_uint16(this->input_mask_ >> 8, data);
} else {
if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &data)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->input_mask_ = encode_uint16(data, this->input_mask_ & 0xFF);
if (!this->read_reg(pin < 8 ? mcp23x17_base::MCP23X17_GPIOA : mcp23x17_base::MCP23X17_GPIOB, &data)) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->input_[pin / 8] = data;
return true;
}
@@ -29,8 +21,6 @@ void MCP23X17Base::digital_write_hw(uint8_t pin, bool value) {
this->update_reg(pin, value, reg_addr);
}
bool MCP23X17Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) {
uint8_t iodir = pin < 8 ? mcp23x17_base::MCP23X17_IODIRA : mcp23x17_base::MCP23X17_IODIRB;
uint8_t gppu = pin < 8 ? mcp23x17_base::MCP23X17_GPPUA : mcp23x17_base::MCP23X17_GPPUB;
@@ -37,7 +37,6 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> {
public:
bool digital_read_hw(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool digital_read_cache(uint8_t pin) override;
void pin_mode(uint8_t pin, gpio::Flags flags) override;
void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override;
@@ -47,9 +46,6 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> {
uint8_t olat_a_{0x00};
uint8_t olat_b_{0x00};
/// State read in digital_read_hw
uint16_t input_mask_{0x00};
};
} // namespace esphome::mcp23x17_base
+1 -1
View File
@@ -244,7 +244,7 @@ async def to_code(config: ConfigType) -> None:
ethernet.request_ethernet_ip_state_listener()
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.14.0")
add_idf_component(name="espressif/mdns", ref="1.15.0")
# ESPHome only advertises; the browse APIs are unused
add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_BROWSE", False)
# The mdns console CLI is never used by ESPHome
@@ -503,6 +503,7 @@ void MicroWakeWord::loop() {
ESP_LOGD(TAG, "Detected '%s' with sliding average probability is %.2f and max probability is %.2f",
detection_event.wake_word->c_str(), (detection_event.average_probability / uint8_to_float_divisor),
(detection_event.max_probability / uint8_to_float_divisor));
this->wake_word_detected_callback_.call(detection_event);
this->wake_word_detected_trigger_.trigger(*detection_event.wake_word);
if (this->stop_after_detection_) {
this->stop();
@@ -68,6 +68,11 @@ class MicroWakeWord final : public Component
Trigger<std::string> *get_wake_word_detected_trigger() { return &this->wake_word_detected_trigger_; }
/// The event's string pointers are only valid during the call.
template<typename F> void add_on_wake_word_detected_callback(F &&callback) {
this->wake_word_detected_callback_.add(std::forward<F>(callback));
}
void add_wake_word_model(WakeWordModel *model);
/// @brief Adds a runtime-downloaded wake word model. Must be called from the main loop.
@@ -102,6 +107,14 @@ class MicroWakeWord final : public Component
// Intended for the voice assistant component to fetch VAD status
bool get_vad_state() { return this->vad_state_; }
void set_vad_enabled(bool enabled) {
if (enabled) {
this->vad_model_->enable();
} else {
this->vad_model_->disable();
}
}
#endif
// Intended for the voice assistant component to access which wake words are available
@@ -111,6 +124,7 @@ class MicroWakeWord final : public Component
protected:
microphone::MicrophoneSource *microphone_source_{nullptr};
Trigger<std::string> wake_word_detected_trigger_;
LazyCallbackManager<void(const DetectionEvent &)> wake_word_detected_callback_;
State state_{State::STOPPED};
std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_;
@@ -374,8 +374,10 @@ void WakeWordModel::disable() {
DetectionEvent WakeWordModel::determine_detected() {
DetectionEvent detection_event;
detection_event.wake_word = &this->wake_word_;
detection_event.id = &this->id_;
detection_event.max_probability = 0;
detection_event.average_probability = 0;
detection_event.probability_cutoff = this->probability_cutoff_;
if ((this->ignore_windows_ < 0) || !this->enabled_) {
detection_event.detected = false;
@@ -20,11 +20,13 @@ static const uint32_t STREAMING_MODEL_VARIABLE_ARENA_SIZE = 1024;
struct DetectionEvent {
std::string *wake_word;
const std::string *id{nullptr};
bool detected;
bool partially_detection; // Set if the most recent probability exceed the threshold, but the sliding window average
// hasn't yet
uint8_t max_probability;
uint8_t average_probability;
uint8_t probability_cutoff{0};
bool blocked_by_vad = false;
};
+1 -3
View File
@@ -327,10 +327,8 @@ void ModemComponent::setup() {
break;
}
}
if (this->uart_port_ == UART_NUM_MAX) {
this->mark_failed(LOG_STR("No free UART port for modem"));
if (!this->setup_check(this->uart_port_ != UART_NUM_MAX, LOG_STR("No free UART port for modem")))
return;
}
esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP();
this->ppp_netif_ = esp_netif_new(&netif_ppp_config);
+10
View File
@@ -110,6 +110,7 @@ CONF_WAIT_FOR_CONNECTION = "wait_for_connection"
CONF_PERSIST_SUBSCRIPTIONS = "persist_subscriptions"
CONF_TRANSPORT = "transport"
CONF_WS_PATH = "ws_path"
CONF_MAX_PAYLOAD_SIZE = "max_payload_size"
MQTT_TRANSPORT_TCP = "tcp"
MQTT_TRANSPORT_WS = "ws"
@@ -405,6 +406,14 @@ CONFIG_SCHEMA = cv.All(
CONF_TRANSPORT, default=MQTT_TRANSPORT_TCP
): _validate_transport,
cv.Optional(CONF_WS_PATH): cv.string,
cv.SplitDefault(
CONF_MAX_PAYLOAD_SIZE,
esp8266=8192,
esp32=32768,
bk72xx=16384,
rtl87xx=16384,
ln882x=16384,
): cv.All(cv.validate_bytes, cv.int_range(min=512, max=1048576)),
cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32=False): cv.All(
cv.boolean, cv.only_on_esp32
),
@@ -664,6 +673,7 @@ async def to_code(config):
request_tls()
cg.add_define("USE_MQTT")
cg.add_define("MQTT_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE])
cg.add_global(mqtt_ns.using)
cg.add(var.set_broker_address(config[CONF_BROKER]))
@@ -235,6 +235,12 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b
instance->subacks_.add(mqtt_event->data_len > 0 ? static_cast<uint8_t>(mqtt_event->data[0]) : SUBACK_FAILURE);
}
#endif
if (event_id == MQTT_EVENT_DATA) {
const auto *mqtt_event = static_cast<esp_mqtt_event_t *>(event_data);
// Only the first chunk of an oversized message is queued, so the client logs the drop
if (mqtt_event->total_data_len > MQTT_MAX_PAYLOAD_SIZE && mqtt_event->current_data_offset != 0)
return;
}
// queue event to decouple processing from ESP-IDF MQTT task to main loop
auto *event = instance->mqtt_event_pool_.allocate();
if (event == nullptr) {
+11 -1
View File
@@ -103,6 +103,13 @@ static void format_esphome_topic(char (&topic)[ESPHOME_TOPIC_BUFFER_SIZE], const
void MQTTClientComponent::setup() {
this->mqtt_backend_.set_on_message(
[this](const char *topic, const char *payload, size_t len, size_t index, size_t total) {
// total comes from the broker; reserving it unchecked can exhaust the heap
if (total > MQTT_MAX_PAYLOAD_SIZE) {
if (index == 0) {
ESP_LOGW(TAG, "Dropped %zu byte message on '%s' (max %d)", total, topic, MQTT_MAX_PAYLOAD_SIZE);
}
return;
}
if (index == 0) {
this->payload_buffer_.clear();
this->payload_buffer_.reserve(total);
@@ -299,7 +306,10 @@ void MQTTClientComponent::dump_config() {
" Discovery retain: %s",
this->discovery_info_.prefix.c_str(), YESNO(this->discovery_info_.retain));
}
ESP_LOGCONFIG(TAG, " Topic Prefix: '%s'", this->topic_prefix_.c_str());
ESP_LOGCONFIG(TAG,
" Topic Prefix: '%s'\n"
" Max Payload Size: %d",
this->topic_prefix_.c_str(), MQTT_MAX_PAYLOAD_SIZE);
if (!this->log_message_.topic.empty()) {
ESP_LOGCONFIG(TAG, " Log Topic: '%s'", this->log_message_.topic.c_str());
}
+9 -2
View File
@@ -48,17 +48,24 @@ NdefMessage::NdefMessage(const std::span<const uint8_t> data) {
ESP_LOGVV(TAG, "Lengths: type=%d, payload=%" PRIu32 ", id=%d", type_length, payload_length, id_length);
if (index + type_length > data.size()) {
ESP_LOGE(TAG, "Truncated type; aborting");
break;
}
std::string type_str(data.begin() + index, data.begin() + index + type_length);
index += type_length;
std::string id_str;
if (il) {
if (index + id_length > data.size()) {
ESP_LOGE(TAG, "Truncated ID; aborting");
break;
}
id_str = std::string(data.begin() + index, data.begin() + index + id_length);
index += id_length;
}
if ((data.begin() + index > data.end()) || (data.begin() + index + payload_length > data.end())) {
if (payload_length > data.size() - index) {
ESP_LOGE(TAG, "Corrupt record encountered; NdefMessage constructor aborting");
break;
}
@@ -34,16 +34,12 @@ HdlcReliability::HdlcReliability(PacketInterface *lower)
}
void HdlcReliability::setup() {
if (this->max_packet_size_ == 0) {
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
if (!this->setup_check(this->max_packet_size_ != 0, LOG_STR("max_packet_size too small for the configured layers")))
return;
}
RAMAllocator<uint8_t> allocator;
this->tx_slots_ = allocator.allocate(size_t{this->max_queue_length_} * this->max_packet_size_);
if (this->tx_slots_ == nullptr) {
this->mark_failed(LOG_STR("Not enough memory for the send queue"));
if (!this->setup_check(this->tx_slots_ != nullptr, LOG_STR("Not enough memory for the send queue")))
return;
}
this->lower_->add_packet_interface_listener(
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
}
@@ -41,17 +41,13 @@ void XxteaEncryption::set_key(std::initializer_list<uint32_t> key) {
}
void XxteaEncryption::setup() {
if (this->max_packet_size_ == 0) {
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
if (!this->setup_check(this->max_packet_size_ != 0, LOG_STR("max_packet_size too small for the configured layers")))
return;
}
size_t words = this->lower_->get_max_packet_size() / 4;
RAMAllocator<uint32_t> allocator;
this->tx_buffer_ = allocator.allocate(words * 2);
if (this->tx_buffer_ == nullptr) {
this->mark_failed(LOG_STR("Not enough memory for the packet buffers"));
if (!this->setup_check(this->tx_buffer_ != nullptr, LOG_STR("Not enough memory for the packet buffers")))
return;
}
this->rx_buffer_ = this->tx_buffer_ + words;
if (this->rolling_code_) {
this->pref_ = global_preferences->make_preference<uint32_t>(fnv1a_hash_extend(PREF_HASH, this->name_), true);
+1 -9
View File
@@ -69,18 +69,10 @@ bool PCA6416AComponent::digital_read_hw(uint8_t pin) {
if (!this->read_register_(reg_addr, &value)) {
return false;
}
// Update the appropriate part of input_mask_
if (pin < 8) {
this->input_mask_ = (this->input_mask_ & 0xFF00) | value;
} else {
this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8);
}
this->input_[pin / 8] = value;
return true;
}
bool PCA6416AComponent::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void PCA6416AComponent::digital_write_hw(uint8_t pin, bool value) {
uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1;
this->update_register_(pin, value, reg_addr);
+2 -5
View File
@@ -22,7 +22,6 @@ class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponen
protected:
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool read_register_(uint8_t reg, uint8_t *value);
@@ -32,12 +31,10 @@ class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponen
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t output_0_{0x00};
uint8_t output_1_{0x00};
/// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x00};
/// Storage for last I2C error seen
esphome::i2c::ErrorCode last_error_;
/// Only the PCAL6416A has pull-up resistors
bool has_pullup_{false};
/// Storage for last I2C error seen
esphome::i2c::ErrorCode last_error_;
};
using PCA6416AGPIOPin = gpio_expander::ExpanderGPIOPin<PCA6416AComponent>;
-2
View File
@@ -49,8 +49,6 @@ void PCA9505Component::dump_config() {
bool PCA9505Component::digital_read_hw(uint8_t pin) { return this->read_bank_(pin / 8); }
bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin / 8] & (1 << (pin % 8)); }
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
-3
View File
@@ -24,7 +24,6 @@ class PCA9505Component final : public gpio_expander::CachedGpioExpanderComponent
protected:
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool read_bank_(uint8_t bank);
@@ -35,8 +34,6 @@ class PCA9505Component final : public gpio_expander::CachedGpioExpanderComponent
uint8_t config_[BANK_COUNT]{};
/// The output state per bank - 1 means HIGH, 0 means LOW
uint8_t output_[BANK_COUNT]{};
/// The input pin states per bank - 1 means HIGH, 0 means LOW
uint8_t input_[BANK_COUNT]{};
/// Which pins may pull INT low - 1 means masked, matching the chip
uint8_t mask_[BANK_COUNT]{};
};
+3 -14
View File
@@ -67,15 +67,7 @@ void PCA9554Component::dump_config() {
LOG_I2C_DEVICE(this)
}
bool PCA9554Component::digital_read_hw(uint8_t pin) {
// Read all pins from hardware into input_mask_
return this->read_inputs_(); // Return true if I2C read succeeded, false on error
}
bool PCA9554Component::digital_read_cache(uint8_t pin) {
// Return the cached pin state from input_mask_
return this->input_mask_ & (1 << pin);
}
bool PCA9554Component::digital_read_hw(uint8_t pin) { return this->read_inputs_(); }
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
@@ -113,7 +105,7 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
}
bool PCA9554Component::read_inputs_() {
uint8_t inputs[2];
uint8_t inputs[2]{}; // the second byte stays 0 on the 8-pin parts
if (this->is_failed()) {
ESP_LOGD(TAG, "Device marked failed");
@@ -125,10 +117,7 @@ bool PCA9554Component::read_inputs_() {
ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) err);
return false;
}
this->input_mask_ = inputs[0];
if (this->reg_width_ == 2) {
this->input_mask_ |= inputs[1] << 8;
}
this->input_[0] = encode_value_le<uint16_t>(inputs);
#ifdef USE_PCA9554_PCAL
// The bus answers again, so retry the settings a failed write left off the chip
this->retry_pcal_shadows_();
-3
View File
@@ -50,7 +50,6 @@ class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
#ifdef USE_PCA9554_PCAL
@@ -61,8 +60,6 @@ class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent
uint16_t config_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint16_t output_mask_{0x00};
/// The state of the actual input pins - 1 means HIGH, 0 means LOW
uint16_t input_mask_{0x00};
#ifdef USE_PCA9554_PCAL
// Shadows of the PCAL registers. setup() writes them, so every part starts with the pulls disconnected
// whatever its power-on state is (the PCAL9555 powers up with pull-ups enabled).
+3 -8
View File
@@ -17,7 +17,7 @@ void PCF8574Component::setup() {
this->write_gpio_();
this->read_gpio_();
} else {
this->output_mask_ = this->input_mask_;
this->output_mask_ = this->input_[0];
this->mode_mask_ = ~this->configured_input_pins_;
}
@@ -32,12 +32,7 @@ void PCF8574Component::dump_config() {
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
}
bool PCF8574Component::digital_read_hw(uint8_t pin) {
// Read all pins from hardware into input_mask_
return this->read_gpio_(); // Return true if I2C read succeeded, false on error
}
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
bool PCF8574Component::digital_read_hw(uint8_t pin) { return this->read_gpio_(); }
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
@@ -61,7 +56,7 @@ bool PCF8574Component::read_gpio_() {
uint8_t data[2]{}; // Keep the bytes zero for the PCF8574's one-byte read.
if (!this->status_comm_result(this->read_bytes_raw(data, this->pcf8575_ ? 2 : 1)))
return false;
this->input_mask_ = encode_value_le<uint16_t>(data);
this->input_[0] = encode_value_le<uint16_t>(data);
return true;
}
bool PCF8574Component::write_gpio_() {
-3
View File
@@ -28,7 +28,6 @@ class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent
protected:
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool read_gpio_();
@@ -38,8 +37,6 @@ class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent
uint16_t mode_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint16_t output_mask_{0x00};
/// The state read in read_gpio_ - 1 means HIGH, 0 means LOW
uint16_t input_mask_{0x00};
// Pins configured as inputs before setup; 1 means input. With reset disabled, all other pins
// (including undeclared pins) are treated as outputs seeded from their observed levels.
uint16_t configured_input_pins_{0x00};
@@ -96,7 +96,7 @@ bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) {
if (!this->status_comm_result(this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data),
LOG_STR("Failed to read GPIO state")))
return false;
this->input_mask_ = data;
this->input_[0] = data;
return true;
}
@@ -149,8 +149,6 @@ bool PI4IOE5V6408Component::write_gpio_modes_() {
return true;
}
bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); }
} // namespace esphome::pi4ioe5v6408
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pi4ioe5v6408::PI4IOE5V6408Component, "%u via PI4IOE5V6408")
@@ -22,15 +22,12 @@ class PI4IOE5V6408Component final : public gpio_expander::CachedGpioExpanderComp
protected:
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
/// Mask for the pin mode - 1 means output, 0 means input
uint8_t mode_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t output_mask_{0x00};
/// The state read in digital_read_hw - 1 means HIGH, 0 means LOW
uint8_t input_mask_{0x00};
/// The mask to write as input buffer state - 1 means enabled, 0 means disabled
uint8_t pull_enable_mask_{0x00};
/// The mask to write as pullup state - 1 means pullup, 0 means pulldown
+1 -3
View File
@@ -51,10 +51,8 @@ void Pixoo::setup() {
// packet, so the LED board completes its final DMA block.
this->frame_size_ = this->data_size_ + PACKET_STATIC_LEN + DMA_CHUNK;
if (!this->buffer_.init(this->data_size_)) {
this->mark_failed(LOG_STR("Failed to allocate draw buffer"));
if (!this->setup_check(this->buffer_.init(this->data_size_), LOG_STR("Failed to allocate draw buffer")))
return;
}
// The frame is shipped in one SPI transfer, so keep it in DMA-capable internal RAM.
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
+248 -99
View File
@@ -1,9 +1,9 @@
from collections.abc import Callable
from collections.abc import Awaitable, Callable
import dataclasses
from dataclasses import dataclass
import logging
from pathlib import Path
from typing import Any
from typing import Any, NamedTuple
from esphome import automation
import esphome.codegen as cg
@@ -56,7 +56,14 @@ from esphome.const import (
CONF_WAND_ID,
CONF_ZERO,
)
from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority
from esphome.core import (
CORE,
ID,
CoroPriority,
Lambda,
coroutine,
coroutine_with_priority,
)
from esphome.cpp_generator import MockObj
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
@@ -430,8 +437,18 @@ def register_protocol_action(
call=f"{call}<{ns.class_(f'{protocol}Protocol')}>",
id_key=CONF_TRANSMITTER_ID,
)
_register_apply_transmit(name, action, validator, builder)
async def protocol_builder(
def _register_apply_transmit(
name: str,
action: str,
validator: Any,
builder: Callable[..., Awaitable[MockObj]],
) -> None:
"""Register ``builder`` for ``action`` as a core ``ApplyAction`` that keeps the protocol's source."""
async def transmit_builder(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
@@ -442,7 +459,70 @@ def register_protocol_action(
automation.register_action(
action, automation.ApplyAction, validator, synchronous=True
)(protocol_builder)
)(transmit_builder)
# The transmit data the statements of a ``_register_transmit_action`` encoder fill
_CALL_DATA = "esphome__call.get_data()"
async def _transmit_value(
value: Any,
args: TemplateArgsType,
type_: SafeExpType,
const_fn: Callable[[Any], Any] = cg.safe_exp,
) -> str:
"""Argument text for one templatable transmit value.
A lambda keeps the trigger's own argument types and is always called, so its body may
modify its by-value arguments.
"""
if isinstance(value, Lambda):
lamb = await cg.process_lambda(value, args, return_type=type_)
return str(automation.immediate_lambda_call(lamb))
return str(const_fn(value))
def _register_transmit_action(
name: str,
schema: cv.Schema,
encode: Callable[[ConfigType, TemplateArgsType], Awaitable[list[str]]],
) -> None:
"""Register ``transmit_<name>`` as inline statements, with no C++ action class.
``encode(config, args)`` returns the statements that fill ``_CALL_DATA``;
they run before ``repeat`` is read and the call is performed, in the order the old
``RemoteTransmitterActionBase::play()`` used.
"""
action, _, validator = _transmit_action(name, schema, None)
async def builder(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
_request_protocol_if_in_tree(name)
transmitter = automation.parent_ref(
await cg.get_variable(config[CONF_TRANSMITTER_ID])
)
statements = [
f"auto esphome__call = {transmitter.transmit()};",
*await encode(config, args),
]
if (repeat := config.get(CONF_REPEAT)) is not None:
for key, setter in (
(CONF_TIMES, "set_send_times"),
(CONF_WAIT_TIME, "set_send_wait"),
):
value = await _transmit_value(repeat[key], args, cg.uint32)
statements.append(f"esphome__call.{setter}({value});")
statements.append("esphome__call.perform();")
return automation.apply_statements_action(
action_id, template_arg, args, statements
)
_register_apply_transmit(name, action, validator, builder)
def declare_protocol(name):
@@ -1568,7 +1648,8 @@ def validate_raw_alternating(value):
return value
RawData, RawBinarySensor, RawTrigger, RawAction, RawDumper = declare_protocol("Raw")
RawData, RawBinarySensor, RawTrigger, _, RawDumper = declare_protocol("Raw")
encode_raw_timings = ns.encode_raw_timings
CONF_CODE_STORAGE_ID = "code_storage_id"
RAW_SCHEMA = cv.Schema(
{
@@ -1600,9 +1681,32 @@ def raw_dumper(var, config):
pass
@register_action(
async def _raw_encode(config: ConfigType, args: TemplateArgsType) -> list[str]:
data = _CALL_DATA
code = config[CONF_CODE]
if config[CONF_CODE_STORAGE_ID].is_manual:
_LOGGER.warning(
"transmit_raw ignores code_storage_id '%s'; equal codes share one table",
config[CONF_CODE_STORAGE_ID],
)
if isinstance(code, Lambda):
timings = await _transmit_value(code, args, cg.std_vector.template(cg.int32))
statements = [f"{data}->set_data({timings});"]
else:
# Equal codes share one flash table
table = cg.shared_progmem_array(
"remote_base_raw_code", cg.int32, cg.ArrayInitializer(*code)
)
statements = [f"{encode_raw_timings}({data}, ::{table}, {len(code)});"]
# transmit() already resets the frequency to 0
if (frequency := config[CONF_CARRIER_FREQUENCY]) != 0:
frequency = await _transmit_value(frequency, args, cg.uint32)
statements.append(f"{data}->set_carrier_frequency({frequency});")
return statements
_register_transmit_action(
"raw",
RawAction,
RAW_SCHEMA.extend(
{
cv.Optional(CONF_CARRIER_FREQUENCY, default="0Hz"): cv.All(
@@ -1610,18 +1714,8 @@ def raw_dumper(var, config):
),
}
),
_raw_encode,
)
async def raw_action(var, config, args):
code_ = config[CONF_CODE]
if cg.is_template(code_):
template_ = await cg.templatable(code_, args, cg.std_vector.template(cg.int32))
cg.add(var.set_code_template(template_))
else:
code_ = config[CONF_CODE]
arr = cg.progmem_array(config[CONF_CODE_STORAGE_ID], code_)
cg.add(var.set_code_static(arr, len(code_)))
templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32)
cg.add(var.set_carrier_frequency(templ))
# Drayton
@@ -1979,15 +2073,11 @@ RC_SWITCH_TRANSMITTER = cv.Schema(
rc_switch_protocol = ns.rc_switch_protocol
rc_switch_protocol_copy = ns.rc_switch_protocol_copy
RC_SWITCH_PROTOCOLS = ns.RC_SWITCH_PROTOCOLS
decode_binary_string = ns.decode_binary_string
RCSwitchData = ns.struct("RCSwitchData")
RCSwitchBase = ns.class_("RCSwitchBase")
RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger)
RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteReceiverDumperBase)
RCSwitchRawAction = ns.class_("RCSwitchRawAction", RemoteTransmitterActionBase)
RCSwitchTypeAAction = ns.class_("RCSwitchTypeAAction", RemoteTransmitterActionBase)
RCSwitchTypeBAction = ns.class_("RCSwitchTypeBAction", RemoteTransmitterActionBase)
RCSwitchTypeCAction = ns.class_("RCSwitchTypeCAction", RemoteTransmitterActionBase)
RCSwitchTypeDAction = ns.class_("RCSwitchTypeDAction", RemoteTransmitterActionBase)
RCSwitchRawReceiver = ns.class_("RCSwitchRawReceiver", RemoteReceiverBinarySensorBase)
@@ -1997,19 +2087,6 @@ def rc_switch_raw_binary_sensor(var, config):
cg.add(var.set_code(config[CONF_CODE]))
@register_action(
"rc_switch_raw",
RCSwitchRawAction,
RC_SWITCH_RAW_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
)
async def rc_switch_raw_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_code(await cg.templatable(config[CONF_CODE], args, cg.std_string)))
@register_binary_sensor(
"rc_switch_type_a", RCSwitchRawReceiver, RC_SWITCH_TYPE_A_SCHEMA
)
@@ -2018,23 +2095,6 @@ def rc_switch_type_a_binary_sensor(var, config):
cg.add(var.set_type_a(config[CONF_GROUP], config[CONF_DEVICE], config[CONF_STATE]))
@register_action(
"rc_switch_type_a",
RCSwitchTypeAAction,
RC_SWITCH_TYPE_A_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
)
async def rc_switch_type_a_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string)))
cg.add(
var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.std_string))
)
cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_)))
@register_binary_sensor(
"rc_switch_type_b", RCSwitchRawReceiver, RC_SWITCH_TYPE_B_SCHEMA
)
@@ -2045,21 +2105,6 @@ def rc_switch_type_b_binary_sensor(var, config):
)
@register_action(
"rc_switch_type_b",
RCSwitchTypeBAction,
RC_SWITCH_TYPE_B_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
)
async def rc_switch_type_b_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)))
cg.add(var.set_channel(await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)))
cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_)))
@register_binary_sensor(
"rc_switch_type_c", RCSwitchRawReceiver, RC_SWITCH_TYPE_C_SCHEMA
)
@@ -2075,24 +2120,6 @@ def rc_switch_type_c_binary_sensor(var, config):
)
@register_action(
"rc_switch_type_c",
RCSwitchTypeCAction,
RC_SWITCH_TYPE_C_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
)
async def rc_switch_type_c_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
)
cg.add(var.set_protocol(proto))
cg.add(
var.set_family(await cg.templatable(config[CONF_FAMILY], args, cg.std_string))
)
cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.uint8)))
cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint8)))
cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_)))
@register_binary_sensor(
"rc_switch_type_d",
RCSwitchRawReceiver,
@@ -2103,19 +2130,141 @@ def rc_switch_type_d_binary_sensor(var, config):
cg.add(var.set_type_d(config[CONF_GROUP], config[CONF_DEVICE], config[CONF_STATE]))
@register_action(
"rc_switch_type_d",
RCSwitchTypeDAction,
RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
)
async def rc_switch_type_d_action(var, config, args):
proto = await cg.templatable(
config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=rc_switch_protocol_value
def _rc_switch_bits(code: str) -> int:
"""``decode_binary_string()`` at build time: every character but ``0`` is a one."""
return int("".join("0" if c == "0" else "1" for c in code), 2)
def _rc_switch_letter_index(value: str) -> int:
"""A family or group letter as its index, as the C++ ``tolower(value[0]) - 'a'``."""
return ord(value[0].lower()) - ord("a")
class _RCSwitchArg(NamedTuple):
"""One argument of an ``RCSwitchBase::type_*_code()`` call, read from config."""
key: str
# What the user lambda returns, and how its value becomes the argument
lambda_type: SafeExpType = cg.uint8
convert: str = "{}"
const_fn: Callable[[Any], Any] = cg.safe_exp
def _rc_switch_binary(key: str) -> _RCSwitchArg:
return _RCSwitchArg(
key, cg.std_string, f"{decode_binary_string}({{}})", _rc_switch_bits
)
cg.add(var.set_protocol(proto))
cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string)))
cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint8)))
cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_)))
def _rc_switch_letter(key: str) -> _RCSwitchArg:
return _RCSwitchArg(
key,
cg.std_string,
"static_cast<uint8_t>(tolower({}[0]) - 'a')",
_rc_switch_letter_index,
)
_RC_SWITCH_STATE = _RCSwitchArg(CONF_STATE, cg.bool_)
async def _rc_switch_send(
config: ConfigType, args: TemplateArgsType, code: str, nbits: str
) -> str:
"""The statement that sends ``code``; the protocol is read last, as before."""
proto = await _transmit_value(
config[CONF_PROTOCOL], args, RCSwitchBase, rc_switch_protocol_value
)
return f"{proto}.transmit({_CALL_DATA}, {code}, {nbits});"
async def _rc_switch_raw_encode(
config: ConfigType, args: TemplateArgsType
) -> list[str]:
code = config[CONF_CODE]
if isinstance(code, Lambda):
value = await _transmit_value(code, args, cg.std_string)
statements = [f"const std::string esphome__code = {value};"]
bits, nbits = f"{decode_binary_string}(esphome__code)", "esphome__code.size()"
else:
statements = []
bits, nbits = str(cg.safe_exp(_rc_switch_bits(code))), str(len(code))
return [*statements, await _rc_switch_send(config, args, bits, nbits)]
def _rc_switch_type_encode(
type_code: str, *fields: _RCSwitchArg
) -> Callable[[ConfigType, TemplateArgsType], Awaitable[list[str]]]:
"""Statements running ``RCSwitchBase::<type_code>`` on the fields, in order, then sending."""
async def encode(config: ConfigType, args: TemplateArgsType) -> list[str]:
statements: list[str] = []
values: list[str] = []
for field in fields:
value = config[field.key]
arg = await _transmit_value(value, args, field.lambda_type, field.const_fn)
if isinstance(value, Lambda):
# A local per lambda keeps the old evaluation order
local = f"esphome__{field.key}"
statements.append(f"const auto {local} = {field.convert.format(arg)};")
arg = local
values.append(arg)
return [
*statements,
"uint64_t esphome__code;",
"uint8_t esphome__nbits;",
f"{RCSwitchBase}::{type_code}({', '.join(values)}, &esphome__code, &esphome__nbits);",
await _rc_switch_send(config, args, "esphome__code", "esphome__nbits"),
]
return encode
for _name, _schema, _encode in (
("rc_switch_raw", RC_SWITCH_RAW_SCHEMA, _rc_switch_raw_encode),
(
"rc_switch_type_a",
RC_SWITCH_TYPE_A_SCHEMA,
_rc_switch_type_encode(
"type_a_code",
_rc_switch_binary(CONF_GROUP),
_rc_switch_binary(CONF_DEVICE),
_RC_SWITCH_STATE,
),
),
(
"rc_switch_type_b",
RC_SWITCH_TYPE_B_SCHEMA,
_rc_switch_type_encode(
"type_b_code",
_RCSwitchArg(CONF_ADDRESS),
_RCSwitchArg(CONF_CHANNEL),
_RC_SWITCH_STATE,
),
),
(
"rc_switch_type_c",
RC_SWITCH_TYPE_C_SCHEMA,
_rc_switch_type_encode(
"type_c_code",
_rc_switch_letter(CONF_FAMILY),
_RCSwitchArg(CONF_GROUP),
_RCSwitchArg(CONF_DEVICE),
_RC_SWITCH_STATE,
),
),
(
"rc_switch_type_d",
RC_SWITCH_TYPE_D_SCHEMA,
_rc_switch_type_encode(
"type_d_code",
_rc_switch_letter(CONF_GROUP),
_RCSwitchArg(CONF_DEVICE),
_RC_SWITCH_STATE,
),
),
):
_register_transmit_action(_name, _schema.extend(RC_SWITCH_TRANSMITTER), _encode)
@register_trigger("rc_switch", RCSwitchTrigger, RCSwitchData)
Loaded 100 of 280 files, more files were not shown because too many files have changed in this diff. Show more