mirror of
https://github.com/esphome/esphome.git
synced 2026-11-09 14:45:57 +00:00
Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs
# Conflicts: # esphome/core/defines.h
This commit is contained in:
280 files changed
+9981
-2214
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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};",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ®s);
|
||||
#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
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]() {
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]{};
|
||||
};
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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_() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in new issue
Block a user