mirror of
https://github.com/esphome/esphome.git
synced 2026-06-29 12:06:13 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 609003c897 | |||
| 2c10adba85 | |||
| 9e4e2d78dc | |||
| af9366fdd4 | |||
| 448402ca2c | |||
| fc67551edc | |||
| 98d3dce672 | |||
| 4cb93d4df8 | |||
| 91e66cfd9d | |||
| 6cf32af33f | |||
| 6b9be033d6 | |||
| 5cc03d9bef | |||
| 0fa96b6e1e | |||
| be2e4a5278 | |||
| 80bd6489cf | |||
| ccf672d7ee | |||
| 6154b673c2 | |||
| 3bde7ec978 | |||
| 8caa11dcf4 | |||
| 1b70df2c1f | |||
| 4122fa5ddd | |||
| c5d42b0569 | |||
| 37f9541f32 | |||
| 8bbfadb59a | |||
| a40d97f346 | |||
| d6c67d5c35 | |||
| 0816b27398 | |||
| 9133582aa0 | |||
| f36b0fcb61 | |||
| bb0a5dc8a8 | |||
| 0c260e483e | |||
| b8ce907976 | |||
| ffce637ea5 | |||
| d6fba39037 | |||
| 5d5c2723b2 | |||
| 06d1498c47 | |||
| 254e1f3abb | |||
| deb6b97eea | |||
| 22ea2764d4 | |||
| 632dbc8fe8 | |||
| 98d9871620 | |||
| a064eceb9b | |||
| 49107f2174 | |||
| e9c2659147 | |||
| 18b54f075e | |||
| 45e40223ac | |||
| 1ab1534028 | |||
| 039efdb02a | |||
| b0447dc521 | |||
| aacbaab5f8 | |||
| dc5032f72f | |||
| c263c2c382 | |||
| 910784ca84 | |||
| 0b99e8f08d | |||
| 93be539789 | |||
| 390bb0451f | |||
| 14c3e2d9d9 | |||
| 23c7e0f803 | |||
| cb4d1d1b5e | |||
| 2ba807efe8 | |||
| c8cf9b74b1 | |||
| 33475703da | |||
| 1b7d0f9c0b | |||
| 1d881ef6f4 | |||
| 3a838d897f | |||
| da130c900f | |||
| 440734dadf | |||
| df2ddc47ec | |||
| 4b1c4ba5c0 | |||
| 6002badb3c | |||
| e8f51fec88 | |||
| 7cec2d3029 | |||
| 2b0c471ed7 | |||
| 064bd13ebb | |||
| 2627490a11 | |||
| 4219d6d367 |
@@ -945,13 +945,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -10,9 +10,6 @@ name: Codeowner Approved Label
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
@@ -13,9 +13,6 @@ on:
|
||||
# Needs to be pull_request_target to get write permissions
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -65,18 +65,6 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for angle brackets not wrapped in backticks.
|
||||
// Astro docs MDX treats bare < as JSX component opening tags.
|
||||
const stripped = title.replace(/`[^`]*`/g, '');
|
||||
if (/[<>]/.test(stripped)) {
|
||||
core.setFailed(
|
||||
'PR title contains `<` or `>` not wrapped in backticks.\n' +
|
||||
'Astro docs MDX interprets bare `<` as JSX components.\n' +
|
||||
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check title starts with [tag] prefix
|
||||
const bracketPattern = /^\[\w+\]/;
|
||||
if (!bracketPattern.test(title)) {
|
||||
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.6
|
||||
rev: v0.15.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.4.0-dev
|
||||
PROJECT_NUMBER = 2026.3.0
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
|
||||
uint8_t current_sensor_;
|
||||
// The AM43 often gets into a state where it spams loads of battery update
|
||||
// notifications. Here we will limit to no more than every 10s.
|
||||
uint8_t last_battery_update_;
|
||||
uint32_t last_battery_update_;
|
||||
};
|
||||
|
||||
} // namespace am43
|
||||
|
||||
@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
|
||||
|
||||
uint8_t buf[128];
|
||||
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
|
||||
// The ESP's i2c driver has a limited buffer size.
|
||||
// This way of retrieving the data should be wrong according to the datasheet
|
||||
// but it seems to work.
|
||||
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
|
||||
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
|
||||
// and advances its internal pointer after every 4th byte.
|
||||
uint8_t read = std::min(32, fifo_level * 4 - pos);
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
}
|
||||
|
||||
if (millis() - this->gesture_start_ > 500) {
|
||||
|
||||
@@ -69,9 +69,6 @@ service APIConnection {
|
||||
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc zigbee_proxy_frame(ZigbeeProxyFrame) returns (void) {}
|
||||
rpc zigbee_proxy_request(ZigbeeProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
|
||||
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
|
||||
@@ -284,10 +281,6 @@ message DeviceInfoResponse {
|
||||
|
||||
// Serial proxy instance metadata
|
||||
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
|
||||
|
||||
// Indicates if Zigbee proxy support is available and features supported
|
||||
uint32 zigbee_proxy_feature_flags = 26 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
uint64 zigbee_ieee_address = 27 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -2676,29 +2669,3 @@ message BluetoothSetConnectionParamsResponse {
|
||||
uint64 address = 1;
|
||||
int32 error = 2;
|
||||
}
|
||||
|
||||
// ==================== ZIGBEE ====================
|
||||
|
||||
message ZigbeeProxyFrame {
|
||||
option (id) = 148;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
enum ZigbeeProxyRequestType {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2;
|
||||
}
|
||||
|
||||
message ZigbeeProxyRequest {
|
||||
option (id) = 149;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
|
||||
ZigbeeProxyRequestType type = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
#include "esphome/components/zwave_proxy/zwave_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
#include "esphome/components/zigbee_proxy/zigbee_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
@@ -1320,16 +1317,6 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void APIConnection::on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_frame(this, msg);
|
||||
}
|
||||
|
||||
void APIConnection::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_request(this, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
|
||||
@@ -1643,11 +1630,6 @@ void APIConnection::complete_authentication_() {
|
||||
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
if (zigbee_proxy::global_zigbee_proxy != nullptr) {
|
||||
zigbee_proxy::global_zigbee_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
@@ -1789,10 +1771,6 @@ bool APIConnection::send_device_info_response_() {
|
||||
info.port_type = proxy->get_port_type();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
resp.zigbee_proxy_feature_flags = zigbee_proxy::global_zigbee_proxy->get_feature_flags();
|
||||
resp.zigbee_ieee_address = zigbee_proxy::global_zigbee_proxy->get_ieee_address();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
|
||||
@@ -180,12 +180,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) override;
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) override;
|
||||
void send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
|
||||
@@ -142,12 +142,6 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||
buffer.encode_sub_message(25, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
buffer.encode_uint32(26, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
buffer.encode_uint64(27, this->zigbee_ieee_address);
|
||||
#endif
|
||||
}
|
||||
uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
@@ -208,12 +202,6 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
for (const auto &it : this->serial_proxies) {
|
||||
size += ProtoSize::calc_message_force(2, it.calculate_size());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size += ProtoSize::calc_uint32(2, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size += ProtoSize::calc_uint64(2, this->zigbee_ieee_address);
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
@@ -3901,57 +3889,5 @@ uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
bool ZigbeeProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ZigbeeProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); }
|
||||
uint32_t ZigbeeProxyFrame::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->type = static_cast<enums::ZigbeeProxyRequestType>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ZigbeeProxyRequest::encode(ProtoWriteBuffer &buffer) const {
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(this->type));
|
||||
buffer.encode_bytes(2, this->data, this->data_len);
|
||||
}
|
||||
uint32_t ZigbeeProxyRequest::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type));
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -341,13 +341,6 @@ enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
enum ZigbeeProxyRequestType : uint32_t {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
|
||||
@@ -525,7 +518,7 @@ class SerialProxyInfo final : public ProtoMessage {
|
||||
class DeviceInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 319;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 309;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_response"; }
|
||||
#endif
|
||||
@@ -580,12 +573,6 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint32_t zigbee_proxy_feature_flags{0};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint64_t zigbee_ieee_address{0};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer &buffer) const;
|
||||
uint32_t calculate_size() const;
|
||||
@@ -3298,45 +3285,5 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
class ZigbeeProxyFrame final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 148;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "zigbee_proxy_frame"; }
|
||||
#endif
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer &buffer) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
class ZigbeeProxyRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 149;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "zigbee_proxy_request"; }
|
||||
#endif
|
||||
enums::ZigbeeProxyRequestType type{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer &buffer) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifndef USE_API_VARINT64
|
||||
#define USE_API_VARINT64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::api {} // namespace esphome::api
|
||||
|
||||
@@ -806,20 +806,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::ZigbeeProxyRequestType>(enums::ZigbeeProxyRequestType value) {
|
||||
switch (value) {
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE";
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "HelloRequest");
|
||||
@@ -944,12 +930,6 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, "zigbee_proxy_feature_flags", this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, "zigbee_ieee_address", this->zigbee_ieee_address);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2671,19 +2651,6 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
const char *ZigbeeProxyFrame::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ZigbeeProxyFrame");
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ZigbeeProxyRequest");
|
||||
dump_field(out, "type", static_cast<enums::ZigbeeProxyRequestType>(this->type));
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
|
||||
@@ -700,28 +700,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_bluetooth_set_connection_params_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyFrame::MESSAGE_TYPE: {
|
||||
ZigbeeProxyFrame msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_frame"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_frame(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyRequest::MESSAGE_TYPE: {
|
||||
ZigbeeProxyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_request"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -238,12 +238,6 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
virtual void on_zigbee_proxy_frame(const ZigbeeProxyFrame &value){};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
virtual void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
@@ -442,8 +442,12 @@ class ProtoMessage {
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
|
||||
#ifndef USE_HOST
|
||||
protected:
|
||||
#endif
|
||||
// Non-virtual destructor is protected to prevent polymorphic deletion.
|
||||
// On host platform, made public to allow value-initialization of std::array
|
||||
// members (e.g. DeviceInfoResponse::devices) without clang errors.
|
||||
~ProtoMessage() = default;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
|
||||
INT_MASK = 0xF0,
|
||||
THRESH_MASK = 0x0F,
|
||||
R_SPIKE_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xE0,
|
||||
CAP_MASK = 0xF0,
|
||||
LIGHT_MASK = 0xCF,
|
||||
DISTURB_MASK = 0xDF,
|
||||
|
||||
@@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) {
|
||||
|
||||
// First, use the peek buffer if available
|
||||
if (this->has_peek_) {
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
|
||||
#endif
|
||||
data[0] = this->peek_buffer_;
|
||||
this->has_peek_ = false;
|
||||
data++;
|
||||
if (--len == 0) { // Decrement len first, then check it...
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
|
||||
#endif
|
||||
return true; // No more to read
|
||||
return true; // No more to read
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,8 +186,8 @@ async def to_code_base(config):
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
"1.3.40408",
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library",
|
||||
None,
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
|
||||
@@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
|
||||
float DallasTemperatureSensor::get_temp_c_() {
|
||||
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
|
||||
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
|
||||
if (this->scratch_pad_[7] == 0) {
|
||||
return NAN;
|
||||
}
|
||||
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
|
||||
}
|
||||
switch (this->resolution_) {
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace debug {
|
||||
|
||||
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
|
||||
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
|
||||
static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128;
|
||||
|
||||
// buf_append_printf is now provided by esphome/core/helpers.h
|
||||
|
||||
@@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent {
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer);
|
||||
uint32_t get_free_heap_();
|
||||
size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos);
|
||||
void update_platform_();
|
||||
|
||||
@@ -98,7 +98,7 @@ static const char *const WAKEUP_CAUSES[] = {
|
||||
"BT",
|
||||
};
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
const char *wake_reason;
|
||||
unsigned reason = esp_sleep_get_wakeup_cause();
|
||||
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
|
||||
@@ -196,9 +196,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
|
||||
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
|
||||
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
char reset_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer));
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer));
|
||||
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
|
||||
@@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
// ESP8266 doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace debug {
|
||||
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return lt_get_reboot_reason_name(lt_get_reboot_reason());
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); }
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); }
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
// Zephyr doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ void EE895Component::setup() {
|
||||
this->read(serial_number, 20);
|
||||
|
||||
crc16_check = (serial_number[19] << 8) + serial_number[18];
|
||||
if (crc16_check != calc_crc16_(serial_number, 19)) {
|
||||
if (crc16_check != calc_crc16_(serial_number, 18)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
|
||||
address[2] = addr & 0xFF;
|
||||
address[3] = (reg_cnt >> 8) & 0xFF;
|
||||
address[4] = reg_cnt & 0xFF;
|
||||
crc16 = calc_crc16_(address, 6);
|
||||
crc16 = calc_crc16_(address, 5);
|
||||
address[5] = crc16 & 0xFF;
|
||||
address[6] = (crc16 >> 8) & 0xFF;
|
||||
this->write(address, 7);
|
||||
@@ -95,7 +95,7 @@ float EE895Component::read_float_() {
|
||||
uint8_t i2c_response[8];
|
||||
this->read(i2c_response, 8);
|
||||
crc16_check = (i2c_response[7] << 8) + i2c_response[6];
|
||||
if (crc16_check != calc_crc16_(i2c_response, 7)) {
|
||||
if (crc16_check != calc_crc16_(i2c_response, 6)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
this->status_set_warning();
|
||||
return 0;
|
||||
@@ -107,12 +107,9 @@ float EE895Component::read_float_() {
|
||||
}
|
||||
|
||||
uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
|
||||
uint8_t crc_check_buf[22];
|
||||
for (int i = 0; i < len; i++) {
|
||||
crc_check_buf[i + 1] = buf[i];
|
||||
}
|
||||
crc_check_buf[0] = this->address_;
|
||||
return crc16(crc_check_buf, len);
|
||||
uint8_t addr = this->address_;
|
||||
uint16_t crc = crc16(&addr, 1);
|
||||
return crc16(buf, len, crc);
|
||||
}
|
||||
} // namespace ee895
|
||||
} // namespace esphome
|
||||
|
||||
@@ -575,8 +575,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
|
||||
load_ble_event(event, args...);
|
||||
|
||||
// Push the event to the queue
|
||||
// Push always succeeds: pool is sized to queue capacity (N-1), so if
|
||||
// allocate() returned non-null, the queue is guaranteed to have room.
|
||||
global_ble->ble_events_.push(event);
|
||||
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
// Explicit template instantiations for the friend function
|
||||
|
||||
@@ -221,7 +221,13 @@ class ESP32BLE : public Component {
|
||||
|
||||
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
|
||||
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
||||
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements (one slot distinguishes full from empty).
|
||||
// This guarantees allocate() returns nullptr before push() can fail, which:
|
||||
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
|
||||
// 2. Avoids needing release() on the producer path after a failed push(),
|
||||
// preserving the SPSC contract on the pool's internal free list
|
||||
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_;
|
||||
|
||||
// 4-byte aligned members
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
@@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() {
|
||||
for (auto *descriptor : this->descriptors_) {
|
||||
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
vSemaphoreDelete(this->set_value_lock_);
|
||||
}
|
||||
|
||||
BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
|
||||
this->set_value_lock_ = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
|
||||
this->properties_ = (esp_gatt_char_prop_t) 0;
|
||||
|
||||
this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0);
|
||||
@@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
|
||||
|
||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
|
||||
xSemaphoreTake(this->set_value_lock_, 0L);
|
||||
this->value_ = std::move(buffer);
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
}
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
|
||||
|
||||
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
|
||||
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_server {
|
||||
@@ -84,8 +82,6 @@ class BLECharacteristic {
|
||||
|
||||
uint16_t value_read_offset_{0};
|
||||
std::vector<uint8_t> value_;
|
||||
SemaphoreHandle_t set_value_lock_;
|
||||
|
||||
std::vector<BLEDescriptor *> descriptors_;
|
||||
|
||||
struct ClientNotificationEntry {
|
||||
|
||||
@@ -22,9 +22,7 @@ void Mutex::unlock() {}
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
||||
|
||||
// ESP8266 doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
// ESP8266 LwIPLock is defined inline as a no-op in helpers.h
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
wifi_get_macaddr(STATION_IF, mac);
|
||||
|
||||
@@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
|
||||
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
@@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
|
||||
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
|
||||
@@ -163,10 +163,14 @@ class ESPNowComponent : public Component {
|
||||
|
||||
uint8_t own_address_[ESP_NOW_ETH_ALEN]{0};
|
||||
LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{};
|
||||
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{};
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing a pool slot leak.
|
||||
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> receive_packet_pool_{};
|
||||
|
||||
LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{};
|
||||
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{};
|
||||
// Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment.
|
||||
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{};
|
||||
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
|
||||
|
||||
uint8_t wifi_channel_{0};
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_RGB_ORDER,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
fastled_base_ns = cg.esphome_ns.namespace("fastled_base")
|
||||
@@ -41,5 +42,9 @@ async def new_fastled_light(config):
|
||||
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
|
||||
|
||||
cg.add_library("fastled/FastLED", "3.9.16")
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
|
||||
include_builtin_idf_component("esp_lcd")
|
||||
await light.register_light(var, config)
|
||||
return var
|
||||
|
||||
@@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
|
||||
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
|
||||
InterruptLock lock;
|
||||
uint64_t ret = 0;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
for (uint8_t i = 0; i < 64; i++) {
|
||||
ret |= (uint64_t(this->read_bit_()) << i);
|
||||
}
|
||||
return ret;
|
||||
|
||||
@@ -87,19 +87,12 @@ void GreeClimate::transmit_state() {
|
||||
// Calculate the checksum
|
||||
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
|
||||
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
|
||||
} else if (this->model_ == GREE_YAG) {
|
||||
} else {
|
||||
remote_state[7] =
|
||||
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4);
|
||||
} else {
|
||||
remote_state[7] =
|
||||
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4) |
|
||||
(remote_state[7] & 0x0F);
|
||||
}
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
|
||||
@@ -7,50 +7,36 @@ namespace hdc2010 {
|
||||
|
||||
static const char *const TAG = "hdc2010";
|
||||
|
||||
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
|
||||
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
|
||||
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
|
||||
static const uint8_t CONFIG = 0x0E;
|
||||
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
|
||||
// Register addresses
|
||||
static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00;
|
||||
static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01;
|
||||
static constexpr uint8_t REG_HUMIDITY_LOW = 0x02;
|
||||
static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03;
|
||||
static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E;
|
||||
static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F;
|
||||
|
||||
// REG_MEASUREMENT_CONF (0x0F) bit masks
|
||||
static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger
|
||||
static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode
|
||||
static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution
|
||||
static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution
|
||||
|
||||
// REG_RESET_DRDY_INT_CONF (0x0E) bit masks
|
||||
static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode
|
||||
|
||||
void HDC2010Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
const uint8_t data[2] = {
|
||||
0b00000000, // resolution 14bit for both humidity and temperature
|
||||
0b00000000 // reserved
|
||||
};
|
||||
|
||||
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
|
||||
ESP_LOGW(TAG, "Initial config instruction error");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set measurement mode to temperature and humidity
|
||||
// Set 14-bit resolution for both sensors and measurement mode to temp + humidity
|
||||
uint8_t config_contents;
|
||||
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode
|
||||
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
|
||||
// Set rate to manual
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x8F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set temperature resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x3F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set humidity resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0xCF;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
// Set auto measurement rate to manual (on-demand via MEAS_TRIG)
|
||||
this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
|
||||
config_contents &= ~AMM_MASK;
|
||||
this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
|
||||
}
|
||||
|
||||
void HDC2010Component::dump_config() {
|
||||
@@ -67,9 +53,9 @@ void HDC2010Component::dump_config() {
|
||||
void HDC2010Component::update() {
|
||||
// Trigger measurement
|
||||
uint8_t config_contents;
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents |= 0x01;
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
config_contents |= MEAS_TRIG;
|
||||
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
|
||||
// 1ms delay after triggering the sample
|
||||
set_timeout(1, [this]() {
|
||||
@@ -90,8 +76,8 @@ void HDC2010Component::update() {
|
||||
float HDC2010Component::read_temp() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
|
||||
this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1);
|
||||
this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t temp = encode_uint16(byte[1], byte[0]);
|
||||
return (float) temp * 0.0025177f - 40.0f;
|
||||
@@ -100,8 +86,8 @@ float HDC2010Component::read_temp() {
|
||||
float HDC2010Component::read_humidity() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
|
||||
this->read_register(REG_HUMIDITY_LOW, &byte[0], 1);
|
||||
this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t humidity = encode_uint16(byte[1], byte[0]);
|
||||
return (float) humidity * 0.001525879f;
|
||||
|
||||
@@ -23,6 +23,12 @@ namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.update";
|
||||
|
||||
// Wraps UpdateInfo + error for the task→main-loop handoff.
|
||||
struct TaskResult {
|
||||
update::UpdateInfo info;
|
||||
const LogString *error_str{nullptr};
|
||||
};
|
||||
|
||||
static const size_t MAX_READ_SIZE = 256;
|
||||
static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
|
||||
@@ -77,134 +83,148 @@ void HttpRequestUpdate::update() {
|
||||
void HttpRequestUpdate::update_task(void *params) {
|
||||
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
|
||||
|
||||
// Allocate once — every path below returns via the single defer at the end.
|
||||
// On failure, error_str is set; on success it is nullptr.
|
||||
auto *result = new TaskResult();
|
||||
auto *info = &result->info;
|
||||
|
||||
auto container = this_update->request_parent_->get(this_update->source_url_);
|
||||
|
||||
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
|
||||
UPDATE_RETURN;
|
||||
if (container != nullptr)
|
||||
container->end();
|
||||
result->error_str = LOG_STR("Failed to fetch manifest");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *data = allocator.allocate(container->content_length);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer(
|
||||
[this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
{
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *data = allocator.allocate(container->content_length);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
|
||||
container->end();
|
||||
result->error_str = LOG_STR("Failed to allocate memory for manifest");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
}
|
||||
this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>();
|
||||
this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>();
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
result->error_str = LOG_STR("Failed to read manifest");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
|
||||
for (auto build : builds_array) {
|
||||
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
|
||||
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
|
||||
info->title = root[ESPHOME_F("name")].as<std::string>();
|
||||
info->latest_version = root[ESPHOME_F("version")].as<std::string>();
|
||||
|
||||
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
|
||||
for (auto build : builds_array) {
|
||||
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
|
||||
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
|
||||
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
|
||||
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
info->firmware_url = ota[ESPHOME_F("path")].as<std::string>();
|
||||
info->md5 = ota[ESPHOME_F("md5")].as<std::string>();
|
||||
|
||||
if (ota[ESPHOME_F("summary")].is<const char *>())
|
||||
info->summary = ota[ESPHOME_F("summary")].as<std::string>();
|
||||
if (ota[ESPHOME_F("release_url")].is<const char *>())
|
||||
info->release_url = ota[ESPHOME_F("release_url")].as<std::string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>();
|
||||
this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>();
|
||||
|
||||
if (ota[ESPHOME_F("summary")].is<const char *>())
|
||||
this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>();
|
||||
if (ota[ESPHOME_F("release_url")].is<const char *>())
|
||||
this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
result->error_str = LOG_STR("Failed to parse manifest JSON");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
// Merge source_url_ and this_update->update_info_.firmware_url
|
||||
if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
|
||||
std::string path = this_update->update_info_.firmware_url;
|
||||
if (path[0] == '/') {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
|
||||
this_update->update_info_.firmware_url = domain + path;
|
||||
} else {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
|
||||
this_update->update_info_.firmware_url = domain + path;
|
||||
// Merge source_url_ and firmware_url
|
||||
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
|
||||
std::string path = info->firmware_url;
|
||||
if (path[0] == '/') {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
|
||||
info->firmware_url = domain + path;
|
||||
} else {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
|
||||
info->firmware_url = domain + path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||
info->current_version = ESPHOME_PROJECT_VERSION;
|
||||
#else
|
||||
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||
info->current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
bool trigger_update_available = false;
|
||||
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
this_update->update_info_.latest_version == this_update->update_info_.current_version) {
|
||||
this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
|
||||
trigger_update_available = true;
|
||||
}
|
||||
this_update->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
}
|
||||
|
||||
// Defer to main loop to ensure thread-safe execution of:
|
||||
// - status_clear_error() performs non-atomic read-modify-write on component_state_
|
||||
// - publish_state() triggers API callbacks that write to the shared protobuf buffer
|
||||
// which can be corrupted if accessed concurrently from task and main loop threads
|
||||
// - update_available trigger to ensure consistent state when the trigger fires
|
||||
this_update->defer([this_update, trigger_update_available]() {
|
||||
this_update->update_info_.has_progress = false;
|
||||
this_update->update_info_.progress = 0.0f;
|
||||
defer:
|
||||
// Release container before vTaskDelete (which doesn't call destructors)
|
||||
container.reset();
|
||||
|
||||
// Defer to the main loop so all update_info_ and state_ writes happen on the
|
||||
// same thread as readers (API, MQTT, web server). This is a single defer for
|
||||
// both success and error paths to avoid multiple std::function instantiations.
|
||||
// Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
|
||||
this_update->defer([this_update, result]() {
|
||||
if (result->error_str != nullptr) {
|
||||
this_update->status_set_error(result->error_str);
|
||||
delete result;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine new state on main loop (avoids extra lambda captures from task)
|
||||
bool trigger_update_available = false;
|
||||
update::UpdateState new_state;
|
||||
if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) {
|
||||
new_state = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
new_state = update::UPDATE_STATE_AVAILABLE;
|
||||
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
|
||||
trigger_update_available = true;
|
||||
}
|
||||
}
|
||||
|
||||
this_update->update_info_ = std::move(result->info);
|
||||
this_update->state_ = new_state;
|
||||
delete result; // Safe: moved-from state is valid for destruction
|
||||
|
||||
this_update->status_clear_error();
|
||||
this_update->publish_state();
|
||||
|
||||
@@ -26,9 +26,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||
|
||||
// LibreTiny doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
// LibreTiny LwIPLock is defined inline as a no-op in helpers.h
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
WiFi.macAddress(mac);
|
||||
|
||||
@@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_height();
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() {
|
||||
}
|
||||
|
||||
point = buffer[5] & 0xF;
|
||||
if (point > 2) {
|
||||
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
|
||||
point = 2;
|
||||
}
|
||||
|
||||
if (point == 1) {
|
||||
err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1);
|
||||
|
||||
@@ -80,6 +80,7 @@ bool StreamingModel::load_model_() {
|
||||
TfLiteTensor *output = this->interpreter_->output(0);
|
||||
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
|
||||
ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (output->type != kTfLiteUInt8) {
|
||||
|
||||
@@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() {
|
||||
void MQTTBackendESP32::loop() {
|
||||
// process new events
|
||||
// handle only 1 message per loop iteration
|
||||
if (!mqtt_events_.empty()) {
|
||||
auto &event = mqtt_events_.front();
|
||||
mqtt_event_handler_(event);
|
||||
mqtt_events_.pop();
|
||||
Event *event = this->mqtt_event_queue_.pop();
|
||||
if (event != nullptr) {
|
||||
this->mqtt_event_handler_(*event);
|
||||
this->mqtt_event_pool_.release(event);
|
||||
}
|
||||
|
||||
// Log dropped inbound events (check is cheap - single atomic load in common case)
|
||||
uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count();
|
||||
if (inbound_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped);
|
||||
}
|
||||
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
@@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
|
||||
void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id,
|
||||
void *event_data) {
|
||||
MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args);
|
||||
// queue event to decouple processing
|
||||
// queue event to decouple processing from ESP-IDF MQTT task to main loop
|
||||
if (instance) {
|
||||
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
|
||||
instance->mqtt_events_.emplace(event);
|
||||
auto *event = instance->mqtt_event_pool_.allocate();
|
||||
if (event == nullptr) {
|
||||
// Pool exhausted, drop event (counted via queue's dropped counter)
|
||||
instance->mqtt_event_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
event->populate(*static_cast<esp_mqtt_event_t *>(event_data));
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
instance->mqtt_event_queue_.push(event);
|
||||
|
||||
// Wake main loop immediately to process MQTT event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
@@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this_mqtt->mqtt_event_pool_.release(elem);
|
||||
this_mqtt->mqtt_outbound_pool_.release(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload,
|
||||
size_t len) {
|
||||
auto *elem = this->mqtt_event_pool_.allocate();
|
||||
auto *elem = this->mqtt_outbound_pool_.allocate();
|
||||
|
||||
if (!elem) {
|
||||
// Queue is full - increment counter but don't log immediately.
|
||||
@@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos,
|
||||
// Use the helper to allocate and copy data
|
||||
if (!elem->set_data(topic, payload, len)) {
|
||||
// Allocation failed, return elem to pool
|
||||
this->mqtt_event_pool_.release(elem);
|
||||
this->mqtt_outbound_pool_.release(elem);
|
||||
// Increment counter without logging to avoid cascade effect during memory pressure
|
||||
this->mqtt_queue_.increment_dropped_count();
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <cstring>
|
||||
#include <mqtt_client.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
@@ -18,32 +17,39 @@
|
||||
namespace esphome::mqtt {
|
||||
|
||||
struct Event {
|
||||
esp_mqtt_event_id_t event_id;
|
||||
esp_mqtt_event_id_t event_id{};
|
||||
std::vector<char> data;
|
||||
int total_data_len;
|
||||
int current_data_offset;
|
||||
int total_data_len{0};
|
||||
int current_data_offset{0};
|
||||
std::string topic;
|
||||
int msg_id;
|
||||
bool retain;
|
||||
int qos;
|
||||
bool dup;
|
||||
bool session_present;
|
||||
esp_mqtt_error_codes_t error_handle;
|
||||
int msg_id{0};
|
||||
bool retain{false};
|
||||
int qos{0};
|
||||
bool dup{false};
|
||||
bool session_present{false};
|
||||
esp_mqtt_error_codes_t error_handle{};
|
||||
|
||||
// Construct from esp_mqtt_event_t
|
||||
// Any pointer values that are unsafe to keep are converted to safe copies
|
||||
Event(const esp_mqtt_event_t &event)
|
||||
: event_id(event.event_id),
|
||||
data(event.data, event.data + event.data_len),
|
||||
total_data_len(event.total_data_len),
|
||||
current_data_offset(event.current_data_offset),
|
||||
topic(event.topic, event.topic_len),
|
||||
msg_id(event.msg_id),
|
||||
retain(event.retain),
|
||||
qos(event.qos),
|
||||
dup(event.dup),
|
||||
session_present(event.session_present),
|
||||
error_handle(*event.error_handle) {}
|
||||
// Populate from esp_mqtt_event_t
|
||||
// Copies pointer-based data to owned storage for safe cross-thread transfer
|
||||
void populate(const esp_mqtt_event_t &event) {
|
||||
this->event_id = event.event_id;
|
||||
this->data.assign(event.data, event.data + event.data_len);
|
||||
this->total_data_len = event.total_data_len;
|
||||
this->current_data_offset = event.current_data_offset;
|
||||
this->topic.assign(event.topic, event.topic_len);
|
||||
this->msg_id = event.msg_id;
|
||||
this->retain = event.retain;
|
||||
this->qos = event.qos;
|
||||
this->dup = event.dup;
|
||||
this->session_present = event.session_present;
|
||||
this->error_handle = *event.error_handle;
|
||||
}
|
||||
|
||||
// Release owned resources for pool reuse (keeps allocated capacity for efficiency)
|
||||
void release() {
|
||||
this->data.clear();
|
||||
this->topic.clear();
|
||||
}
|
||||
};
|
||||
|
||||
enum MqttQueueTypeT : uint8_t {
|
||||
@@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
static constexpr size_t TASK_STACK_SIZE = 3072;
|
||||
static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations
|
||||
static constexpr ssize_t TASK_PRIORITY = 5;
|
||||
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
|
||||
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
|
||||
static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker
|
||||
|
||||
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
|
||||
void set_client_id(const char *client_id) final { this->client_id_ = client_id; }
|
||||
@@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
bool skip_cert_cn_check_{false};
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
static void esphome_mqtt_task(void *params);
|
||||
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment.
|
||||
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_;
|
||||
NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_;
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL,
|
||||
@@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
CallbackManager<on_message_callback_t> on_message_;
|
||||
CallbackManager<on_publish_user_callback_t> on_publish_;
|
||||
std::string cached_topic_;
|
||||
std::queue<Event> mqtt_events_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements (one slot distinguishes full from empty).
|
||||
// This guarantees allocate() returns nullptr before push() can fail, which:
|
||||
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
|
||||
// 2. Avoids needing release() on the producer path after a failed push(),
|
||||
// preserving the SPSC contract on the pool's internal free list
|
||||
EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_;
|
||||
LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_;
|
||||
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
uint32_t last_dropped_log_time_{0};
|
||||
|
||||
@@ -13,11 +13,6 @@ const std::string &OneWireDevice::get_address_name() {
|
||||
return this->address_name_;
|
||||
}
|
||||
|
||||
void OneWireDevice::set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->address_name_.clear();
|
||||
}
|
||||
|
||||
bool OneWireDevice::send_command_(uint8_t cmd) {
|
||||
if (!this->bus_->select(this->address_))
|
||||
return false;
|
||||
|
||||
@@ -15,7 +15,7 @@ class OneWireDevice {
|
||||
public:
|
||||
/// @brief store the address of the device
|
||||
/// @param address of the device
|
||||
void set_address(uint64_t address);
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
|
||||
void set_index(uint8_t index) { this->index_ = index; }
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ void OnlineImage::update() {
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = ::time(nullptr);
|
||||
this->start_time_ = millis();
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ void OnlineImage::loop() {
|
||||
// Finalize decoding
|
||||
this->end_decode();
|
||||
|
||||
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
|
||||
(uint32_t) (::time(nullptr) - this->start_time_));
|
||||
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 " ms", this->downloader_->get_bytes_read(),
|
||||
millis() - this->start_time_);
|
||||
|
||||
// Save caching headers
|
||||
this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
|
||||
|
||||
@@ -97,7 +97,7 @@ class OnlineImage : public PollingComponent,
|
||||
*/
|
||||
std::string last_modified_ = "";
|
||||
|
||||
time_t start_time_;
|
||||
uint32_t start_time_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||
|
||||
@@ -57,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_,
|
||||
cv.Optional(
|
||||
CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1
|
||||
): cv.positive_not_null_int,
|
||||
): cv.int_,
|
||||
}
|
||||
),
|
||||
cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema(
|
||||
@@ -68,12 +68,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_STARTING_INTEGRAL_TERM, default=0.0): cv.float_,
|
||||
cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_,
|
||||
cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_,
|
||||
cv.Optional(
|
||||
CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1
|
||||
): cv.positive_not_null_int,
|
||||
cv.Optional(
|
||||
CONF_OUTPUT_AVERAGING_SAMPLES, default=1
|
||||
): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1): cv.int_,
|
||||
cv.Optional(CONF_OUTPUT_AVERAGING_SAMPLES, default=1): cv.int_,
|
||||
}
|
||||
),
|
||||
}
|
||||
@@ -106,15 +102,13 @@ async def to_code(config):
|
||||
cg.add(var.set_starting_integral_term(params[CONF_STARTING_INTEGRAL_TERM]))
|
||||
cg.add(var.set_derivative_samples(params[CONF_DERIVATIVE_AVERAGING_SAMPLES]))
|
||||
|
||||
output_samples = params[CONF_OUTPUT_AVERAGING_SAMPLES]
|
||||
cg.add(var.set_output_samples(output_samples))
|
||||
cg.add(var.set_output_samples(params[CONF_OUTPUT_AVERAGING_SAMPLES]))
|
||||
|
||||
if CONF_MIN_INTEGRAL in params:
|
||||
cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL]))
|
||||
if CONF_MAX_INTEGRAL in params:
|
||||
cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL]))
|
||||
|
||||
deadband_output_samples = 1
|
||||
if CONF_DEADBAND_PARAMETERS in config:
|
||||
params = config[CONF_DEADBAND_PARAMETERS]
|
||||
cg.add(var.set_threshold_low(params[CONF_THRESHOLD_LOW]))
|
||||
@@ -122,11 +116,11 @@ async def to_code(config):
|
||||
cg.add(var.set_kp_multiplier(params[CONF_KP_MULTIPLIER]))
|
||||
cg.add(var.set_ki_multiplier(params[CONF_KI_MULTIPLIER]))
|
||||
cg.add(var.set_kd_multiplier(params[CONF_KD_MULTIPLIER]))
|
||||
deadband_output_samples = params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
|
||||
cg.add(var.set_deadband_output_samples(deadband_output_samples))
|
||||
|
||||
# Single shared output buffer sized to max of both modes
|
||||
cg.add(var.init_output_buffer(max(output_samples, deadband_output_samples)))
|
||||
cg.add(
|
||||
var.set_deadband_output_samples(
|
||||
params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
|
||||
)
|
||||
)
|
||||
|
||||
cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE]))
|
||||
|
||||
|
||||
@@ -28,11 +28,7 @@ class PIDClimate : public climate::Climate, public Component {
|
||||
void set_min_integral(float min_integral) { controller_.min_integral_ = min_integral; }
|
||||
void set_max_integral(float max_integral) { controller_.max_integral_ = max_integral; }
|
||||
void set_output_samples(int in) { controller_.output_samples_ = in; }
|
||||
void set_derivative_samples(int in) {
|
||||
controller_.derivative_samples_ = in;
|
||||
if (in > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits)
|
||||
controller_.derivative_window_.init(in);
|
||||
}
|
||||
void set_derivative_samples(int in) { controller_.derivative_samples_ = in; }
|
||||
|
||||
void set_threshold_low(float in) { controller_.threshold_low_ = in; }
|
||||
void set_threshold_high(float in) { controller_.threshold_high_ = in; }
|
||||
@@ -42,10 +38,6 @@ class PIDClimate : public climate::Climate, public Component {
|
||||
void set_starting_integral_term(float in) { controller_.set_starting_integral_term(in); }
|
||||
|
||||
void set_deadband_output_samples(int in) { controller_.deadband_output_samples_ = in; }
|
||||
void init_output_buffer(int size) {
|
||||
if (size > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits)
|
||||
controller_.output_window_.init(size);
|
||||
}
|
||||
|
||||
float get_output_value() const { return output_value_; }
|
||||
float get_error_value() const { return controller_.error_; }
|
||||
|
||||
@@ -21,9 +21,9 @@ float PIDController::update(float setpoint, float process_value) {
|
||||
// u(t) := p(t) + i(t) + d(t)
|
||||
float output = proportional_term_ + integral_term_ + derivative_term_;
|
||||
|
||||
// smooth/sample the output using shared buffer with mode-appropriate sample count
|
||||
// smooth/sample the output
|
||||
int samples = in_deadband() ? deadband_output_samples_ : output_samples_;
|
||||
return ring_buffer_average_(output_window_, output, samples);
|
||||
return weighted_average_(output_list_, output, samples);
|
||||
}
|
||||
|
||||
bool PIDController::in_deadband() {
|
||||
@@ -83,7 +83,7 @@ void PIDController::calculate_derivative_term_(float setpoint) {
|
||||
previous_setpoint_ = setpoint;
|
||||
|
||||
// smooth the derivative samples
|
||||
derivative = ring_buffer_average_(derivative_window_, derivative, derivative_samples_);
|
||||
derivative = weighted_average_(derivative_list_, derivative, derivative_samples_);
|
||||
|
||||
derivative_term_ = kd_ * derivative;
|
||||
|
||||
@@ -93,23 +93,25 @@ void PIDController::calculate_derivative_term_(float setpoint) {
|
||||
}
|
||||
}
|
||||
|
||||
float PIDController::ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples) {
|
||||
// if only 1 sample needed (or invalid), clear the buffer and return
|
||||
if (max_samples <= 1) {
|
||||
buf.clear();
|
||||
float PIDController::weighted_average_(std::deque<float> &list, float new_value, int samples) {
|
||||
// if only 1 sample needed, clear the list and return
|
||||
if (samples == 1) {
|
||||
list.clear();
|
||||
return new_value;
|
||||
}
|
||||
|
||||
// Trim oldest entries to make room (handles mode-switching where buffer
|
||||
// may have more entries than the current mode needs)
|
||||
while (buf.size() >= static_cast<size_t>(max_samples))
|
||||
buf.pop();
|
||||
buf.push(new_value);
|
||||
// add the new item to the list
|
||||
list.push_front(new_value);
|
||||
|
||||
// keep only 'samples' readings, by popping off the back of the list
|
||||
while (samples > 0 && list.size() > static_cast<size_t>(samples))
|
||||
list.pop_back();
|
||||
|
||||
// calculate and return the average of all values in the list
|
||||
float sum = 0;
|
||||
for (auto val : buf)
|
||||
sum += val;
|
||||
return sum / buf.size();
|
||||
for (auto &elem : list)
|
||||
sum += elem;
|
||||
return sum / list.size();
|
||||
}
|
||||
|
||||
float PIDController::calculate_relative_time_() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <deque>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome {
|
||||
@@ -25,10 +24,10 @@ struct PIDController {
|
||||
/// Differential gain K_d.
|
||||
float kd_ = 0;
|
||||
|
||||
// smooth the derivative value using an average over X samples
|
||||
int derivative_samples_ = 1;
|
||||
// smooth the derivative value using a weighted average over X samples
|
||||
int derivative_samples_ = 8;
|
||||
|
||||
/// smooth the output value using an average over X values
|
||||
/// smooth the output value using a weighted average over X values
|
||||
int output_samples_ = 1;
|
||||
|
||||
float threshold_low_ = 0.0f;
|
||||
@@ -51,10 +50,7 @@ struct PIDController {
|
||||
void calculate_proportional_term_();
|
||||
void calculate_integral_term_();
|
||||
void calculate_derivative_term_(float setpoint);
|
||||
|
||||
/// Ring buffer smoothing using FixedRingBuffer (single allocation at setup)
|
||||
float ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples);
|
||||
|
||||
float weighted_average_(std::deque<float> &list, float new_value, int samples);
|
||||
float calculate_relative_time_();
|
||||
|
||||
/// Error from previous update used for derivative term
|
||||
@@ -64,12 +60,12 @@ struct PIDController {
|
||||
float accumulated_integral_ = 0;
|
||||
uint32_t last_time_ = 0;
|
||||
|
||||
// Ring buffer for derivative smoothing
|
||||
FixedRingBuffer<float> derivative_window_;
|
||||
// this is a list of derivative values for smoothing.
|
||||
std::deque<float> derivative_list_;
|
||||
|
||||
// Ring buffer for output smoothing (shared between normal and deadband modes)
|
||||
FixedRingBuffer<float> output_window_;
|
||||
// this is a list of output values for smoothing.
|
||||
std::deque<float> output_list_;
|
||||
|
||||
}; // Struct PIDController
|
||||
}; // Struct PID Controller
|
||||
} // namespace pid
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.const import CONF_CLIMATE_ID
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT
|
||||
|
||||
@@ -22,6 +21,7 @@ PID_CLIMATE_SENSOR_TYPES = {
|
||||
"KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD,
|
||||
}
|
||||
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
PIDClimateSensor,
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.components.image import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
|
||||
from esphome.core import CORE
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
|
||||
@@ -75,6 +76,13 @@ class JPEGFormat(Format):
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
# JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level
|
||||
# ARDUINO_ESP32S3_DEV define) that require esp-dsp headers.
|
||||
# On Arduino this overwrites the stub; on IDF it adds the component.
|
||||
add_idf_component(name="espressif/esp-dsp", ref="1.7.1")
|
||||
|
||||
|
||||
class PNGFormat(Format):
|
||||
|
||||
@@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
|
||||
this->sg_recv_data_state_ = FRAME_DATA_LEN_H;
|
||||
break;
|
||||
case FRAME_DATA_LEN_H:
|
||||
if (value <= 4) {
|
||||
this->sg_data_len_ = value * 256;
|
||||
if (value == 0) {
|
||||
this->sg_frame_buf_[4] = value;
|
||||
this->sg_recv_data_state_ = FRAME_DATA_LEN_L;
|
||||
} else {
|
||||
this->sg_data_len_ = 0;
|
||||
this->sg_recv_data_state_ = FRAME_IDLE;
|
||||
ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value);
|
||||
}
|
||||
break;
|
||||
case FRAME_DATA_LEN_L:
|
||||
this->sg_data_len_ += value;
|
||||
if (this->sg_data_len_ > 32) {
|
||||
this->sg_data_len_ = value;
|
||||
if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) {
|
||||
ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value);
|
||||
this->sg_data_len_ = 0;
|
||||
this->sg_recv_data_state_ = FRAME_IDLE;
|
||||
@@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
|
||||
}
|
||||
break;
|
||||
case FRAME_DATA_BYTES:
|
||||
this->sg_data_len_ -= 1;
|
||||
this->sg_frame_buf_[this->sg_frame_len_++] = value;
|
||||
if (this->sg_data_len_ <= 0) {
|
||||
if (--this->sg_data_len_ == 0) {
|
||||
this->sg_recv_data_state_ = FRAME_DATA_CRC;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -406,7 +406,9 @@ QUANTILE_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float,
|
||||
cv.Optional(CONF_QUANTILE, default=0.9): cv.float_range(
|
||||
min=0, min_included=False, max=1
|
||||
),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -427,9 +429,9 @@ async def quantile_filter_to_code(config, filter_id):
|
||||
MEDIAN_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -449,9 +451,9 @@ async def median_filter_to_code(config, filter_id):
|
||||
MIN_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -483,9 +485,9 @@ async def min_filter_to_code(config, filter_id):
|
||||
MAX_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -509,9 +511,9 @@ async def max_filter_to_code(config, filter_id):
|
||||
SLIDING_AVERAGE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
@@ -540,8 +542,8 @@ EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float,
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
|
||||
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
|
||||
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
|
||||
@@ -41,14 +41,26 @@ void Filter::initialize(Sensor *parent, Filter *next) {
|
||||
}
|
||||
|
||||
// SlidingWindowFilter
|
||||
SlidingWindowFilter::SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at) {
|
||||
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
|
||||
: window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
|
||||
// Allocate ring buffer once at initialization
|
||||
this->window_.init(window_size);
|
||||
}
|
||||
|
||||
optional<float> SlidingWindowFilter::new_value(float value) {
|
||||
// Add value to ring buffer (overwrites oldest when full)
|
||||
this->window_.push_overwrite(value);
|
||||
// Add value to ring buffer
|
||||
if (this->window_count_ < this->window_size_) {
|
||||
// Buffer not yet full - just append
|
||||
this->window_.push_back(value);
|
||||
this->window_count_++;
|
||||
} else {
|
||||
// Buffer full - overwrite oldest value (ring buffer)
|
||||
this->window_[this->window_head_] = value;
|
||||
this->window_head_++;
|
||||
if (this->window_head_ >= this->window_size_) {
|
||||
this->window_head_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should send a result
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
@@ -65,8 +77,9 @@ FixedVector<float> SortedWindowFilter::get_window_values_() {
|
||||
// Copy window without NaN values using FixedVector (no heap allocation)
|
||||
// Returns unsorted values - caller will use std::nth_element for partial sorting as needed
|
||||
FixedVector<float> values;
|
||||
values.init(this->window_.size());
|
||||
for (float v : this->window_) {
|
||||
values.init(this->window_count_);
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
if (!std::isnan(v)) {
|
||||
values.push_back(v);
|
||||
}
|
||||
@@ -137,7 +150,8 @@ float MaxFilter::compute_result() { return this->find_extremum_<std::greater<flo
|
||||
float SlidingWindowMovingAverageFilter::compute_result() {
|
||||
float sum = 0;
|
||||
size_t valid_count = 0;
|
||||
for (float v : this->window_) {
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
if (!std::isnan(v)) {
|
||||
sum += v;
|
||||
valid_count++;
|
||||
@@ -147,7 +161,7 @@ float SlidingWindowMovingAverageFilter::compute_result() {
|
||||
}
|
||||
|
||||
// ExponentialMovingAverageFilter
|
||||
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at)
|
||||
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at)
|
||||
: alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {}
|
||||
optional<float> ExponentialMovingAverageFilter::new_value(float value) {
|
||||
if (!std::isnan(value)) {
|
||||
@@ -169,7 +183,7 @@ optional<float> ExponentialMovingAverageFilter::new_value(float value) {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
void ExponentialMovingAverageFilter::set_send_every(uint16_t send_every) { this->send_every_ = send_every; }
|
||||
void ExponentialMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; }
|
||||
|
||||
// ThrottleAverageFilter
|
||||
@@ -497,7 +511,7 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// StreamingFilter (base class)
|
||||
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
|
||||
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
|
||||
: window_size_(window_size), send_first_at_(send_first_at) {}
|
||||
|
||||
optional<float> StreamingFilter::new_value(float value) {
|
||||
|
||||
@@ -52,7 +52,7 @@ class Filter {
|
||||
*/
|
||||
class SlidingWindowFilter : public Filter {
|
||||
public:
|
||||
SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at);
|
||||
SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) final;
|
||||
|
||||
@@ -60,10 +60,14 @@ class SlidingWindowFilter : public Filter {
|
||||
/// Called by new_value() to compute the filtered result from the current window
|
||||
virtual float compute_result() = 0;
|
||||
|
||||
/// Sliding window ring buffer - automatically overwrites oldest values when full
|
||||
FixedRingBuffer<float> window_;
|
||||
uint16_t send_every_; ///< Send result every N values
|
||||
uint16_t send_at_; ///< Counter for send_every
|
||||
/// Access the sliding window values (ring buffer implementation)
|
||||
/// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
|
||||
FixedVector<float> window_;
|
||||
size_t window_head_{0}; ///< Index where next value will be written
|
||||
size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_)
|
||||
size_t window_size_; ///< Maximum window size
|
||||
size_t send_every_; ///< Send result every N values
|
||||
size_t send_at_; ///< Counter for send_every
|
||||
};
|
||||
|
||||
/** Base class for Min/Max filters.
|
||||
@@ -80,7 +84,8 @@ class MinMaxFilter : public SlidingWindowFilter {
|
||||
template<typename Compare> float find_extremum_() {
|
||||
float result = NAN;
|
||||
Compare comp;
|
||||
for (float v : this->window_) {
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
if (!std::isnan(v)) {
|
||||
result = std::isnan(result) ? v : (comp(v, result) ? v : result);
|
||||
}
|
||||
@@ -234,18 +239,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
|
||||
*/
|
||||
class ExponentialMovingAverageFilter : public Filter {
|
||||
public:
|
||||
ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at);
|
||||
ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(uint16_t send_every);
|
||||
void set_send_every(size_t send_every);
|
||||
void set_alpha(float alpha);
|
||||
|
||||
protected:
|
||||
float accumulator_{NAN};
|
||||
float alpha_;
|
||||
uint16_t send_every_;
|
||||
uint16_t send_at_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
bool first_value_{true};
|
||||
};
|
||||
|
||||
@@ -565,7 +570,7 @@ class ToNTCTemperatureFilter : public Filter {
|
||||
*/
|
||||
class StreamingFilter : public Filter {
|
||||
public:
|
||||
StreamingFilter(uint16_t window_size, uint16_t send_first_at);
|
||||
StreamingFilter(size_t window_size, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) final;
|
||||
|
||||
@@ -579,9 +584,9 @@ class StreamingFilter : public Filter {
|
||||
/// Called by new_value() to reset internal state after sending a result
|
||||
virtual void reset_batch() = 0;
|
||||
|
||||
uint16_t window_size_;
|
||||
uint16_t count_{0};
|
||||
uint16_t send_first_at_;
|
||||
size_t window_size_;
|
||||
size_t count_{0};
|
||||
size_t send_first_at_;
|
||||
bool first_send_{true};
|
||||
};
|
||||
|
||||
|
||||
@@ -417,7 +417,7 @@ void SpeakerMediaPlayer::loop() {
|
||||
this->media_playlist_.pop_front();
|
||||
}
|
||||
// Only delay starting playback if moving on the next playlist item or repeating the current item
|
||||
timeout_ms = this->announcement_playlist_delay_ms_;
|
||||
timeout_ms = this->media_playlist_delay_ms_;
|
||||
}
|
||||
if (!this->media_playlist_.empty()) {
|
||||
PlaylistItem playlist_item = this->media_playlist_.front();
|
||||
|
||||
@@ -50,8 +50,9 @@ void TC74Component::read_temperature_() {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t temperature_reg;
|
||||
if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) {
|
||||
int8_t temperature_reg;
|
||||
if (this->read_register(TC74_REGISTER_TEMPERATURE, reinterpret_cast<uint8_t *>(&temperature_reg), 1) !=
|
||||
i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,16 +26,13 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) {
|
||||
event->data.line_state.dtr = dtr;
|
||||
event->data.line_state.rts = rts;
|
||||
|
||||
if (!this->event_queue_.push(event)) {
|
||||
ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_);
|
||||
// Return event to pool since we couldn't queue it
|
||||
this->event_pool_.release(event);
|
||||
} else {
|
||||
// Wake main loop immediately to process event
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->event_queue_.push(event);
|
||||
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity,
|
||||
@@ -53,16 +50,13 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_
|
||||
event->data.line_coding.parity = parity;
|
||||
event->data.line_coding.data_bits = data_bits;
|
||||
|
||||
if (!this->event_queue_.push(event)) {
|
||||
ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_);
|
||||
// Return event to pool since we couldn't queue it
|
||||
this->event_pool_.release(event);
|
||||
} else {
|
||||
// Wake main loop immediately to process event
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->event_queue_.push(event);
|
||||
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void USBCDCACMInstance::process_events_() {
|
||||
|
||||
@@ -102,7 +102,11 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC
|
||||
LineStateCallback line_state_callback_{nullptr};
|
||||
|
||||
// Lock-free queue and event pool for cross-task event passing
|
||||
EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing both a pool slot leak and an SPSC
|
||||
// violation on the pool's internal free list.
|
||||
EventPool<CDCEvent, EVENT_QUEUE_SIZE - 1> event_pool_;
|
||||
LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_;
|
||||
};
|
||||
|
||||
|
||||
@@ -144,7 +144,10 @@ class USBClient : public Component {
|
||||
// Lock-free event queue and pool for USB task to main loop communication
|
||||
// Must be public for access from static callbacks
|
||||
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
|
||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing a pool slot leak.
|
||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE - 1> event_pool;
|
||||
|
||||
protected:
|
||||
// Process USB events from the queue. Returns true if any work was done.
|
||||
|
||||
@@ -193,7 +193,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *
|
||||
return;
|
||||
}
|
||||
|
||||
// Push to lock-free queue (always succeeds since pool size == queue size)
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
client->event_queue.push(event);
|
||||
|
||||
// Re-enable component loop to process the queued event
|
||||
|
||||
@@ -160,11 +160,9 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) {
|
||||
size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE);
|
||||
memcpy(chunk->data, data, chunk_len);
|
||||
chunk->length = static_cast<uint8_t>(chunk_len);
|
||||
if (!this->output_queue_.push(chunk)) {
|
||||
this->output_pool_.release(chunk);
|
||||
ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len);
|
||||
break;
|
||||
}
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->output_queue_.push(chunk);
|
||||
data += chunk_len;
|
||||
len -= chunk_len;
|
||||
}
|
||||
@@ -320,7 +318,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||
chunk->channel = channel;
|
||||
|
||||
// Push to lock-free queue for main loop processing
|
||||
// Push always succeeds because pool size == queue size
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->usb_data_queue_.push(chunk);
|
||||
|
||||
// Re-enable component loop to process the queued data
|
||||
|
||||
@@ -158,7 +158,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
|
||||
// Larger structures first (8+ bytes)
|
||||
RingBuffer input_buffer_;
|
||||
LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_;
|
||||
EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing a pool slot leak.
|
||||
EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1> output_pool_;
|
||||
std::function<void()> rx_callback_{};
|
||||
CdcEps cdc_dev_{};
|
||||
StringRef debug_prefix_{};
|
||||
@@ -190,7 +193,8 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
// Lock-free data transfer from USB task to main loop
|
||||
static constexpr int USB_DATA_QUEUE_SIZE = 32;
|
||||
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
|
||||
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment.
|
||||
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE - 1> chunk_pool_;
|
||||
|
||||
protected:
|
||||
std::vector<USBUartChannel *> channels_{};
|
||||
|
||||
@@ -19,7 +19,6 @@ CONF_DELTASOL_BS_2009 = "deltasol_bs_2009"
|
||||
CONF_DELTASOL_BS2 = "deltasol_bs2"
|
||||
CONF_DELTASOL_C = "deltasol_c"
|
||||
CONF_DELTASOL_CS2 = "deltasol_cs2"
|
||||
CONF_DELTASOL_CS4 = "deltasol_cs4"
|
||||
CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus"
|
||||
|
||||
CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
|
||||
|
||||
@@ -20,7 +20,6 @@ from .. import (
|
||||
CONF_DELTASOL_BS_PLUS,
|
||||
CONF_DELTASOL_C,
|
||||
CONF_DELTASOL_CS2,
|
||||
CONF_DELTASOL_CS4,
|
||||
CONF_DELTASOL_CS_PLUS,
|
||||
CONF_VBUS_ID,
|
||||
VBus,
|
||||
@@ -32,7 +31,6 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component)
|
||||
DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component)
|
||||
DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component)
|
||||
DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component)
|
||||
DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4BSensor", cg.Component)
|
||||
DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component)
|
||||
VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component)
|
||||
VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component)
|
||||
@@ -188,28 +186,6 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
),
|
||||
}
|
||||
),
|
||||
CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DeltaSol_CS4),
|
||||
cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
|
||||
cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
),
|
||||
CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
|
||||
@@ -374,23 +350,6 @@ async def to_code(config):
|
||||
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
|
||||
cg.add(var.set_s4_error_bsensor(sens))
|
||||
|
||||
elif config[CONF_MODEL] == CONF_DELTASOL_CS4:
|
||||
cg.add(var.set_command(0x0100))
|
||||
cg.add(var.set_source(0x1122))
|
||||
cg.add(var.set_dest(0x0010))
|
||||
if CONF_SENSOR1_ERROR in config:
|
||||
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
|
||||
cg.add(var.set_s1_error_bsensor(sens))
|
||||
if CONF_SENSOR2_ERROR in config:
|
||||
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
|
||||
cg.add(var.set_s2_error_bsensor(sens))
|
||||
if CONF_SENSOR3_ERROR in config:
|
||||
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
|
||||
cg.add(var.set_s3_error_bsensor(sens))
|
||||
if CONF_SENSOR4_ERROR in config:
|
||||
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
|
||||
cg.add(var.set_s4_error_bsensor(sens))
|
||||
|
||||
elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
|
||||
cg.add(var.set_command(0x0100))
|
||||
cg.add(var.set_source(0x2211))
|
||||
|
||||
@@ -110,25 +110,6 @@ void DeltaSolCS2BSensor::handle_message(std::vector<uint8_t> &message) {
|
||||
this->s4_error_bsensor_->publish_state(message[18] & 8);
|
||||
}
|
||||
|
||||
void DeltaSolCS4BSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Deltasol CS4:");
|
||||
LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_);
|
||||
}
|
||||
|
||||
void DeltaSolCS4BSensor::handle_message(std::vector<uint8_t> &message) {
|
||||
if (this->s1_error_bsensor_ != nullptr)
|
||||
this->s1_error_bsensor_->publish_state(message[20] & 1);
|
||||
if (this->s2_error_bsensor_ != nullptr)
|
||||
this->s2_error_bsensor_->publish_state(message[20] & 2);
|
||||
if (this->s3_error_bsensor_ != nullptr)
|
||||
this->s3_error_bsensor_->publish_state(message[20] & 4);
|
||||
if (this->s4_error_bsensor_ != nullptr)
|
||||
this->s4_error_bsensor_->publish_state(message[20] & 8);
|
||||
}
|
||||
|
||||
void DeltaSolCSPlusBSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
|
||||
LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_);
|
||||
|
||||
@@ -94,23 +94,6 @@ class DeltaSolCS2BSensor : public VBusListener, public Component {
|
||||
void handle_message(std::vector<uint8_t> &message) override;
|
||||
};
|
||||
|
||||
class DeltaSolCS4BSensor : public VBusListener, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
|
||||
void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
|
||||
void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
|
||||
void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
|
||||
|
||||
protected:
|
||||
binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
|
||||
binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
|
||||
binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
|
||||
binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
|
||||
|
||||
void handle_message(std::vector<uint8_t> &message) override;
|
||||
};
|
||||
|
||||
class DeltaSolCSPlusBSensor : public VBusListener, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
@@ -36,7 +36,6 @@ from .. import (
|
||||
CONF_DELTASOL_BS_PLUS,
|
||||
CONF_DELTASOL_C,
|
||||
CONF_DELTASOL_CS2,
|
||||
CONF_DELTASOL_CS4,
|
||||
CONF_DELTASOL_CS_PLUS,
|
||||
CONF_VBUS_ID,
|
||||
VBus,
|
||||
@@ -48,7 +47,6 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component)
|
||||
DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component)
|
||||
DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component)
|
||||
DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component)
|
||||
DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4Sensor", cg.Component)
|
||||
DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component)
|
||||
VBusCustom = vbus_ns.class_("VBusCustomSensor", cg.Component)
|
||||
VBusCustomSub = vbus_ns.class_("VBusCustomSubSensor", cg.Component)
|
||||
@@ -440,99 +438,6 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
),
|
||||
}
|
||||
),
|
||||
CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DeltaSol_CS4),
|
||||
cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
|
||||
cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_5): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HOUR,
|
||||
icon=ICON_TIMER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HOUR,
|
||||
icon=ICON_TIMER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_TIME): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MINUTE,
|
||||
icon=ICON_TIMER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_VERSION): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_FLOW_RATE): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
),
|
||||
CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
|
||||
@@ -829,51 +734,7 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(config[CONF_VERSION])
|
||||
cg.add(var.set_version_sensor(sens))
|
||||
|
||||
elif config[CONF_MODEL] == CONF_DELTASOL_CS4:
|
||||
cg.add(var.set_command(0x0100))
|
||||
cg.add(var.set_source(0x1122))
|
||||
cg.add(var.set_dest(0x0010))
|
||||
if CONF_TEMPERATURE_1 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1])
|
||||
cg.add(var.set_temperature1_sensor(sens))
|
||||
if CONF_TEMPERATURE_2 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2])
|
||||
cg.add(var.set_temperature2_sensor(sens))
|
||||
if CONF_TEMPERATURE_3 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3])
|
||||
cg.add(var.set_temperature3_sensor(sens))
|
||||
if CONF_TEMPERATURE_4 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4])
|
||||
cg.add(var.set_temperature4_sensor(sens))
|
||||
if CONF_TEMPERATURE_5 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_5])
|
||||
cg.add(var.set_temperature5_sensor(sens))
|
||||
if CONF_PUMP_SPEED_1 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1])
|
||||
cg.add(var.set_pump_speed1_sensor(sens))
|
||||
if CONF_PUMP_SPEED_2 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2])
|
||||
cg.add(var.set_pump_speed2_sensor(sens))
|
||||
if CONF_OPERATING_HOURS_1 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1])
|
||||
cg.add(var.set_operating_hours1_sensor(sens))
|
||||
if CONF_OPERATING_HOURS_2 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2])
|
||||
cg.add(var.set_operating_hours2_sensor(sens))
|
||||
if CONF_HEAT_QUANTITY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY])
|
||||
cg.add(var.set_heat_quantity_sensor(sens))
|
||||
if CONF_TIME in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TIME])
|
||||
cg.add(var.set_time_sensor(sens))
|
||||
if CONF_VERSION in config:
|
||||
sens = await sensor.new_sensor(config[CONF_VERSION])
|
||||
cg.add(var.set_version_sensor(sens))
|
||||
if CONF_FLOW_RATE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FLOW_RATE])
|
||||
cg.add(var.set_flow_rate_sensor(sens))
|
||||
|
||||
elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
|
||||
if config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
|
||||
cg.add(var.set_command(0x0100))
|
||||
cg.add(var.set_source(0x2211))
|
||||
cg.add(var.set_dest(0x0010))
|
||||
|
||||
@@ -168,52 +168,6 @@ void DeltaSolCS2Sensor::handle_message(std::vector<uint8_t> &message) {
|
||||
this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f);
|
||||
}
|
||||
|
||||
void DeltaSolCS4Sensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Deltasol CS4:");
|
||||
LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature 5", this->temperature5_sensor_);
|
||||
LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_);
|
||||
LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_);
|
||||
LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_);
|
||||
LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_);
|
||||
LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_);
|
||||
LOG_SENSOR(" ", "System Time", this->time_sensor_);
|
||||
LOG_SENSOR(" ", "FW Version", this->version_sensor_);
|
||||
LOG_SENSOR(" ", "Flow Rate", this->flow_rate_sensor_);
|
||||
}
|
||||
|
||||
void DeltaSolCS4Sensor::handle_message(std::vector<uint8_t> &message) {
|
||||
if (this->temperature1_sensor_ != nullptr)
|
||||
this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f);
|
||||
if (this->temperature2_sensor_ != nullptr)
|
||||
this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f);
|
||||
if (this->temperature3_sensor_ != nullptr)
|
||||
this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f);
|
||||
if (this->temperature4_sensor_ != nullptr)
|
||||
this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f);
|
||||
if (this->temperature5_sensor_ != nullptr)
|
||||
this->temperature5_sensor_->publish_state(get_i16(message, 36) * 0.1f);
|
||||
if (this->pump_speed1_sensor_ != nullptr)
|
||||
this->pump_speed1_sensor_->publish_state(message[8]);
|
||||
if (this->pump_speed2_sensor_ != nullptr)
|
||||
this->pump_speed2_sensor_->publish_state(message[12]);
|
||||
if (this->operating_hours1_sensor_ != nullptr)
|
||||
this->operating_hours1_sensor_->publish_state(get_u16(message, 10));
|
||||
if (this->operating_hours2_sensor_ != nullptr)
|
||||
this->operating_hours2_sensor_->publish_state(get_u16(message, 14));
|
||||
if (this->heat_quantity_sensor_ != nullptr)
|
||||
this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28));
|
||||
if (this->time_sensor_ != nullptr)
|
||||
this->time_sensor_->publish_state(get_u16(message, 22));
|
||||
if (this->version_sensor_ != nullptr)
|
||||
this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f);
|
||||
if (this->flow_rate_sensor_ != nullptr)
|
||||
this->flow_rate_sensor_->publish_state(get_u16(message, 38));
|
||||
}
|
||||
|
||||
void DeltaSolCSPlusSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
|
||||
LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_);
|
||||
|
||||
@@ -122,41 +122,6 @@ class DeltaSolCS2Sensor : public VBusListener, public Component {
|
||||
void handle_message(std::vector<uint8_t> &message) override;
|
||||
};
|
||||
|
||||
class DeltaSolCS4Sensor : public VBusListener, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; }
|
||||
void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; }
|
||||
void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; }
|
||||
void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; }
|
||||
void set_temperature5_sensor(sensor::Sensor *sensor) { this->temperature5_sensor_ = sensor; }
|
||||
void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; }
|
||||
void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; }
|
||||
void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; }
|
||||
void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; }
|
||||
void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; }
|
||||
void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; }
|
||||
void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; }
|
||||
void set_flow_rate_sensor(sensor::Sensor *sensor) { this->flow_rate_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *temperature1_sensor_{nullptr};
|
||||
sensor::Sensor *temperature2_sensor_{nullptr};
|
||||
sensor::Sensor *temperature3_sensor_{nullptr};
|
||||
sensor::Sensor *temperature4_sensor_{nullptr};
|
||||
sensor::Sensor *temperature5_sensor_{nullptr};
|
||||
sensor::Sensor *pump_speed1_sensor_{nullptr};
|
||||
sensor::Sensor *pump_speed2_sensor_{nullptr};
|
||||
sensor::Sensor *operating_hours1_sensor_{nullptr};
|
||||
sensor::Sensor *operating_hours2_sensor_{nullptr};
|
||||
sensor::Sensor *heat_quantity_sensor_{nullptr};
|
||||
sensor::Sensor *time_sensor_{nullptr};
|
||||
sensor::Sensor *version_sensor_{nullptr};
|
||||
sensor::Sensor *flow_rate_sensor_{nullptr};
|
||||
|
||||
void handle_message(std::vector<uint8_t> &message) override;
|
||||
};
|
||||
|
||||
class DeltaSolCSPlusSensor : public VBusListener, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
@@ -619,6 +619,8 @@ void VoiceAssistant::start_playback_timeout_() {
|
||||
this->cancel_timeout("speaker-timeout");
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
|
||||
if (this->api_client_ == nullptr)
|
||||
return;
|
||||
api::VoiceAssistantAnnounceFinished msg;
|
||||
msg.success = true;
|
||||
this->api_client_->send_message(msg);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
#include <esp_wpa2.h>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
#include <esp_wpa2.h>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
#include <esp_wpa2.h>
|
||||
@@ -75,11 +75,7 @@ struct IDFWiFiEvent {
|
||||
#if USE_NETWORK_IPV6
|
||||
ip_event_got_ip6_t ip_got_ip6;
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
ip_event_assigned_ip_to_client_t ip_assigned_ip_to_client;
|
||||
#else
|
||||
ip_event_ap_staipassigned_t ip_ap_staipassigned;
|
||||
#endif
|
||||
} data;
|
||||
};
|
||||
|
||||
@@ -120,13 +116,8 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
|
||||
memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t));
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t));
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
|
||||
memcpy(&event.data.ip_assigned_ip_to_client, event_data, sizeof(ip_event_assigned_ip_to_client_t));
|
||||
#else
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
|
||||
memcpy(&event.data.ip_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t));
|
||||
#endif
|
||||
} else {
|
||||
// did not match any event, don't send anything
|
||||
return;
|
||||
@@ -416,7 +407,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (eap_opt.has_value()) {
|
||||
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
|
||||
EAPAuth eap = *eap_opt;
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
@@ -428,7 +419,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
int client_cert_len = strlen(eap.client_cert);
|
||||
int client_key_len = strlen(eap.client_key);
|
||||
if (ca_cert_len) {
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
|
||||
@@ -441,7 +432,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
// validation is not required as the config tool has already validated it
|
||||
if (client_cert_len && client_key_len) {
|
||||
// if we have certs, this must be EAP-TLS
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
|
||||
(uint8_t *) eap.client_key, client_key_len + 1,
|
||||
(uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
@@ -455,7 +446,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
}
|
||||
} else {
|
||||
// in the absence of certs, assume this is username/password based
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
|
||||
@@ -463,7 +454,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "set_username failed %d", err);
|
||||
}
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
@@ -472,7 +463,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, "set_password failed %d", err);
|
||||
}
|
||||
// set TTLS Phase 2, defaults to MSCHAPV2
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_eap_client_set_ttls_phase2_method(eap.ttls_phase_2);
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_set_ttls_phase2_method(eap.ttls_phase_2);
|
||||
@@ -481,7 +472,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, "set_ttls_phase2_method failed %d", err);
|
||||
}
|
||||
}
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
err = esp_wifi_sta_enterprise_enable();
|
||||
#else
|
||||
err = esp_wifi_sta_wpa2_ent_enable();
|
||||
@@ -637,26 +628,14 @@ const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
return "Auth Expired";
|
||||
case WIFI_REASON_AUTH_LEAVE:
|
||||
return "Auth Leave";
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
case WIFI_REASON_DISASSOC_DUE_TO_INACTIVITY:
|
||||
return "Disassociated Due to Inactivity";
|
||||
#else
|
||||
case WIFI_REASON_ASSOC_EXPIRE:
|
||||
return "Association Expired";
|
||||
#endif
|
||||
case WIFI_REASON_ASSOC_TOOMANY:
|
||||
return "Too Many Associations";
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
case WIFI_REASON_CLASS2_FRAME_FROM_NONAUTH_STA:
|
||||
return "Class 2 Frame from Non-Authenticated STA";
|
||||
case WIFI_REASON_CLASS3_FRAME_FROM_NONASSOC_STA:
|
||||
return "Class 3 Frame from Non-Associated STA";
|
||||
#else
|
||||
case WIFI_REASON_NOT_AUTHED:
|
||||
return "Not Authenticated";
|
||||
case WIFI_REASON_NOT_ASSOCED:
|
||||
return "Not Associated";
|
||||
#endif
|
||||
case WIFI_REASON_ASSOC_LEAVE:
|
||||
return "Association Leave";
|
||||
case WIFI_REASON_ASSOC_NOT_AUTHED:
|
||||
@@ -709,7 +688,7 @@ const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
return "Association comeback time too long";
|
||||
case WIFI_REASON_SA_QUERY_TIMEOUT:
|
||||
return "SA query timeout";
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2)
|
||||
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
|
||||
return "No AP found with compatible security";
|
||||
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
|
||||
@@ -938,13 +917,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf);
|
||||
#endif
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
|
||||
const auto &it = data->data.ip_assigned_ip_to_client;
|
||||
#else
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
|
||||
const auto &it = data->data.ip_ap_staipassigned;
|
||||
#endif
|
||||
ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,7 @@ void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); }
|
||||
|
||||
// Zephyr doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
// Zephyr LwIPLock is defined inline as a no-op in helpers.h
|
||||
|
||||
uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp)
|
||||
bool random_bytes(uint8_t *data, size_t len) {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart, usb_uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["api", "uart"]
|
||||
|
||||
CONF_BUFFER_SIZE = "buffer_size"
|
||||
CONF_INITIAL_TIMEOUT = "initial_timeout"
|
||||
CONF_MIN_TIMEOUT = "min_timeout"
|
||||
CONF_MAX_TIMEOUT = "max_timeout"
|
||||
CONF_USB_UART_ID = "usb_uart_id"
|
||||
|
||||
# Default ACK timeout values calibrated for hardware UART (460800 baud, ~2-5 ms round-trip)
|
||||
_DEFAULT_HW_INITIAL_TIMEOUT = 1600
|
||||
_DEFAULT_HW_MIN_TIMEOUT = 400
|
||||
_DEFAULT_HW_MAX_TIMEOUT = 3200
|
||||
|
||||
# Optimized ACK timeout values for USB CDC ACM paths (~3-5 ms round-trip with RX callback)
|
||||
_DEFAULT_USB_INITIAL_TIMEOUT = 30
|
||||
_DEFAULT_USB_MIN_TIMEOUT = 15
|
||||
_DEFAULT_USB_MAX_TIMEOUT = 200
|
||||
|
||||
zigbee_proxy_ns = cg.esphome_ns.namespace("zigbee_proxy")
|
||||
ZigbeeProxy = zigbee_proxy_ns.class_("ZigbeeProxy", cg.Component, uart.UARTDevice)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
full_config = fv.full_config.get()
|
||||
if (wifi_conf := full_config.get(CONF_WIFI)) and (
|
||||
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeProxy),
|
||||
cv.Optional(CONF_BUFFER_SIZE): cv.SplitDefault(
|
||||
cv.int_range(min=256, max=2048),
|
||||
esp8266=512,
|
||||
default=1024,
|
||||
),
|
||||
# When usb_uart_id is present the component registers an RX callback
|
||||
# for zero-wakeup-cycle data delivery and selects USB-optimized ACK
|
||||
# timeout defaults. Explicit timeout keys always win.
|
||||
cv.Optional(CONF_USB_UART_ID): cv.use_id(usb_uart.USBUartChannel),
|
||||
cv.Optional(CONF_INITIAL_TIMEOUT): cv.int_range(min=10, max=10000),
|
||||
cv.Optional(CONF_MIN_TIMEOUT): cv.int_range(min=10, max=5000),
|
||||
cv.Optional(CONF_MAX_TIMEOUT): cv.int_range(min=50, max=10000),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
cg.add_define("USE_ZIGBEE_PROXY")
|
||||
|
||||
# Set buffer size via define for compile-time allocation
|
||||
if CONF_BUFFER_SIZE in config:
|
||||
cg.add_define("ZIGBEE_PROXY_BUFFER_SIZE", config[CONF_BUFFER_SIZE])
|
||||
|
||||
# Select timeout defaults based on UART transport type.
|
||||
# USB CDC ACM with the RX callback has ~3-5 ms round-trip latency; hardware
|
||||
# UART is similar (~2-5 ms). Different defaults are kept so that future
|
||||
# non-callback USB paths still get conservative starting values.
|
||||
is_usb = CONF_USB_UART_ID in config
|
||||
if is_usb:
|
||||
cg.add_define("USE_ZIGBEE_PROXY_USB_UART")
|
||||
usb_ch = await cg.get_variable(config[CONF_USB_UART_ID])
|
||||
cg.add(var.set_usb_uart_channel(usb_ch))
|
||||
|
||||
initial_timeout = config.get(
|
||||
CONF_INITIAL_TIMEOUT,
|
||||
_DEFAULT_USB_INITIAL_TIMEOUT if is_usb else _DEFAULT_HW_INITIAL_TIMEOUT,
|
||||
)
|
||||
min_timeout = config.get(
|
||||
CONF_MIN_TIMEOUT,
|
||||
_DEFAULT_USB_MIN_TIMEOUT if is_usb else _DEFAULT_HW_MIN_TIMEOUT,
|
||||
)
|
||||
max_timeout = config.get(
|
||||
CONF_MAX_TIMEOUT,
|
||||
_DEFAULT_USB_MAX_TIMEOUT if is_usb else _DEFAULT_HW_MAX_TIMEOUT,
|
||||
)
|
||||
|
||||
cg.add(var.set_initial_timeout(initial_timeout))
|
||||
cg.add(var.set_min_timeout(min_timeout))
|
||||
cg.add(var.set_max_timeout(max_timeout))
|
||||
@@ -1,402 +0,0 @@
|
||||
#include "zigbee_proxy.h"
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
static const char *const TAG = "zigbee_proxy";
|
||||
|
||||
// CRC-CCITT lookup table for polynomial 0x1021 (x^16 + x^12 + x^5 + 1)
|
||||
static const uint16_t CRC_TABLE[256] = {
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD,
|
||||
0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A,
|
||||
0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B,
|
||||
0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
|
||||
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861,
|
||||
0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96,
|
||||
0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87,
|
||||
0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
|
||||
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A,
|
||||
0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3,
|
||||
0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290,
|
||||
0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
|
||||
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E,
|
||||
0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F,
|
||||
0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C,
|
||||
0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
|
||||
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83,
|
||||
0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74,
|
||||
0x2E93, 0x3EB2, 0x0ED1, 0x1EF0};
|
||||
|
||||
uint16_t ZigbeeProxy::calculate_crc_(const uint8_t *data, size_t length, uint16_t init) {
|
||||
uint16_t crc = init;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
crc = (crc << 8) ^ CRC_TABLE[(crc >> 8) ^ data[i]];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::validate_frame_crc_() {
|
||||
// CRC is calculated over control byte + data
|
||||
// rx_buffer_[0] contains control byte, rx_buffer_[1..rx_buffer_index_-3] contains data
|
||||
// rx_buffer_[rx_buffer_index_-2] and rx_buffer_[rx_buffer_index_-1] contain CRC
|
||||
if (this->rx_buffer_index_ < 3) {
|
||||
// Frame too short to contain CRC
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate CRC over control + data (exclude CRC bytes)
|
||||
uint16_t calculated = this->calculate_crc_(this->rx_buffer_.data(), this->rx_buffer_index_ - 2);
|
||||
|
||||
// Extract received CRC (big-endian)
|
||||
uint16_t received = (static_cast<uint16_t>(this->rx_buffer_[this->rx_buffer_index_ - 2]) << 8) |
|
||||
this->rx_buffer_[this->rx_buffer_index_ - 1];
|
||||
|
||||
if (calculated != received) {
|
||||
ESP_LOGW(TAG, "CRC validation failed: calculated=0x%04X, received=0x%04X", calculated, received);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ZigbeeProxy::parse_control_byte_(uint8_t control) {
|
||||
// Decode frame type based on bit patterns:
|
||||
// DATA: 0xxxxxxx (bit 7 = 0)
|
||||
// ACK: 10x0xxxx (bits 7-6 = 10, bit 5 = 0)
|
||||
// NAK: 10x1xxxx (bits 7-6 = 10, bit 5 = 1)
|
||||
// RST: 11000000 (0xC0)
|
||||
// RSTACK: 11000001 (0xC1)
|
||||
// ERROR: 11000010 (0xC2)
|
||||
|
||||
AshFrameType frame_type;
|
||||
if ((control & 0x80) == 0) {
|
||||
// Bit 7 = 0: DATA frame
|
||||
frame_type = AshFrameType::DATA;
|
||||
} else if ((control & 0xC0) == 0x80) {
|
||||
// Bits 7-6 = 10: ACK or NAK
|
||||
// ACK format: 100nrPPP (bit 5 = 0)
|
||||
// NAK format: 101nrPPP (bit 5 = 1)
|
||||
if ((control & 0x20) == 0) {
|
||||
frame_type = AshFrameType::ACK;
|
||||
} else {
|
||||
frame_type = AshFrameType::NAK;
|
||||
}
|
||||
} else {
|
||||
// Bits 7-6 = 11: control frames (RST, RSTACK, ERROR)
|
||||
uint8_t control_bits = control & 0x07;
|
||||
if (control_bits == 0x00) {
|
||||
frame_type = AshFrameType::RST;
|
||||
} else if (control_bits == 0x01) {
|
||||
frame_type = AshFrameType::RSTACK;
|
||||
} else if (control_bits == 0x02) {
|
||||
frame_type = AshFrameType::ERROR;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unknown control frame type: 0x%02X", control);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract sequence numbers from DATA frame format: 0ffrPPPP
|
||||
// Bits 6-4 = frmNum, bit 3 = reTx, bits 2-0 = ackNum
|
||||
uint8_t frame_num = (control >> 4) & 0x07; // Bits 6-4
|
||||
uint8_t ack_num = control & 0x07; // Bits 2-0
|
||||
bool retx = (control & 0x08) != 0; // Bit 3 (for DATA frames)
|
||||
|
||||
ESP_LOGV(TAG, "Parsed control byte: type=%d, frmNum=%d, ackNum=%d, reTx=%d", static_cast<int>(frame_type), frame_num,
|
||||
ack_num, retx);
|
||||
|
||||
// Handle frame based on type
|
||||
switch (frame_type) {
|
||||
case AshFrameType::DATA: {
|
||||
// Check sequence number
|
||||
if (frame_num != this->rx_sequence_) {
|
||||
ESP_LOGW(TAG, "Out of sequence DATA frame: expected %d, got %d", this->rx_sequence_, frame_num);
|
||||
this->send_nak_frame_(this->rx_sequence_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for ACK in DATA frame (piggybacked ACK) BEFORE processing
|
||||
// This must happen first because the handler may send new frames
|
||||
if (this->tx_buffer_pending_ && ack_num == ((this->tx_pending_frame_num_ + 1) & ASH_MAX_SEQUENCE)) {
|
||||
// ackNum means "I expect frame N next" = "I received up to N-1"
|
||||
// So if ackNum == pending+1, our pending frame was received
|
||||
uint32_t rtt = millis() - this->ack_timer_start_;
|
||||
this->update_adaptive_timeout_(rtt);
|
||||
this->clear_tx_buffer_();
|
||||
ESP_LOGV(TAG, "ACK received (piggybacked in DATA), RTT: %u ms", rtt);
|
||||
}
|
||||
|
||||
// Increment RX sequence and send ACK (ack_num = next expected frame)
|
||||
this->increment_rx_sequence_();
|
||||
this->send_ack_frame_(this->rx_sequence_);
|
||||
|
||||
// Extract payload (skip control byte, exclude CRC)
|
||||
size_t payload_length = this->rx_buffer_index_ > 3 ? this->rx_buffer_index_ - 3 : 0;
|
||||
const uint8_t *payload = this->rx_buffer_.data() + 1;
|
||||
|
||||
// During boot sequence, route to boot handler
|
||||
if (this->boot_sequence_active_ && payload_length > 0) {
|
||||
this->handle_boot_data_frame_(payload, payload_length);
|
||||
} else if (this->api_connection_ != nullptr && payload_length > 0) {
|
||||
// Forward EZSP payload to client via client-side ASH DATA frame
|
||||
this->forward_ncp_data_to_client_(payload, payload_length);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case AshFrameType::ACK:
|
||||
// Check if this ACKs our pending frame
|
||||
// ackNum means "I expect frame N next" = "I received all frames up to N-1"
|
||||
// So if ackNum == pending+1, our pending frame was acknowledged
|
||||
if (this->tx_buffer_pending_ && ack_num == ((this->tx_pending_frame_num_ + 1) & ASH_MAX_SEQUENCE)) {
|
||||
uint32_t rtt = millis() - this->ack_timer_start_;
|
||||
this->update_adaptive_timeout_(rtt);
|
||||
this->clear_tx_buffer_();
|
||||
ESP_LOGV(TAG, "ACK received for frame %d, RTT: %u ms", this->tx_pending_frame_num_, rtt);
|
||||
}
|
||||
break;
|
||||
|
||||
case AshFrameType::NAK:
|
||||
ESP_LOGW(TAG, "NAK received for frame %d, retransmitting", ack_num);
|
||||
if (this->tx_buffer_pending_) {
|
||||
this->handle_retransmission_();
|
||||
}
|
||||
break;
|
||||
|
||||
case AshFrameType::RST: {
|
||||
ESP_LOGW(TAG, "Received RST frame from NCP, sending RSTACK");
|
||||
// Send RSTACK response
|
||||
uint8_t rstack_data[] = {0x02, 0x01, 0x00}; // RSTACK with reset code
|
||||
this->handle_rstack_frame_(rstack_data, sizeof(rstack_data));
|
||||
break;
|
||||
}
|
||||
|
||||
case AshFrameType::RSTACK:
|
||||
this->handle_rstack_frame_(this->rx_buffer_.data() + 1, this->rx_buffer_index_ - 3);
|
||||
break;
|
||||
|
||||
case AshFrameType::ERROR:
|
||||
this->handle_error_frame_(this->rx_buffer_.data() + 1, this->rx_buffer_index_ - 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool ZigbeeProxy::parse_byte_(uint8_t byte) {
|
||||
// ASH_CAN (0x1A) resets the parser state - discard any partial frame
|
||||
static constexpr uint8_t ASH_CAN_BYTE = 0x1A;
|
||||
if (byte == ASH_CAN_BYTE) {
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->escape_next_byte_ = false;
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this->parsing_state_) {
|
||||
case ParsingState::WAIT_FLAG_START:
|
||||
// Handle escape sequences - NCP may send escaped control byte at frame start
|
||||
if (byte == ASH_ESCAPE_BYTE) {
|
||||
this->escape_next_byte_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->escape_next_byte_) {
|
||||
byte ^= ASH_XOR_BYTE;
|
||||
this->escape_next_byte_ = false;
|
||||
// After unescaping, check if it's a CAN byte (0x1A)
|
||||
if (byte == ASH_CAN_BYTE) {
|
||||
this->rx_buffer_index_ = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// Start of frame with FLAG delimiter
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->escape_next_byte_ = false;
|
||||
this->parsing_state_ = ParsingState::WAIT_CONTROL;
|
||||
ESP_LOGV(TAG, "Frame start detected (FLAG)");
|
||||
} else if (this->ash_state_ == AshState::CONNECTED) {
|
||||
// When connected, NCP often omits leading FLAG on responses
|
||||
// Any byte could be a control byte:
|
||||
// - DATA frames: 0x00-0x7F (bit 7 = 0)
|
||||
// - ACK frames: 0x80-0x9F (bits 7-6 = 10, bit 5 = 0)
|
||||
// - NAK frames: 0xA0-0xBF (bits 7-6 = 10, bit 5 = 1)
|
||||
// - RST/RSTACK/ERROR: 0xC0-0xC2 (bits 7-6 = 11)
|
||||
// Skip reserved bytes that cannot be valid control bytes
|
||||
if (byte != 0x11 && byte != 0x13) {
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
this->parsing_state_ = ParsingState::WAIT_DATA;
|
||||
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
|
||||
}
|
||||
} else if ((byte & 0x80) != 0) {
|
||||
// Before connected, only accept control/management frames (bit 7 set)
|
||||
// This handles RSTACK (0xC1), ACK (0x8X), NAK (0xAX), ERROR (0xC2)
|
||||
this->rx_buffer_index_ = 0;
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
this->parsing_state_ = ParsingState::WAIT_DATA;
|
||||
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
|
||||
}
|
||||
break;
|
||||
|
||||
case ParsingState::WAIT_CONTROL:
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// Empty frame or repeated FLAG
|
||||
ESP_LOGV(TAG, "Empty frame or repeated FLAG, restarting");
|
||||
this->rx_buffer_index_ = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byte == ASH_ESCAPE_BYTE) {
|
||||
this->escape_next_byte_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->escape_next_byte_) {
|
||||
byte ^= ASH_XOR_BYTE;
|
||||
this->escape_next_byte_ = false;
|
||||
}
|
||||
|
||||
// Store control byte
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
this->parsing_state_ = ParsingState::WAIT_DATA;
|
||||
break;
|
||||
|
||||
case ParsingState::WAIT_DATA:
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// End of frame - validate and process
|
||||
ESP_LOGV(TAG, "Frame complete, %u bytes in buffer", this->rx_buffer_index_);
|
||||
if (this->validate_frame_crc_()) {
|
||||
this->parse_control_byte_(this->rx_buffer_[0]);
|
||||
} else {
|
||||
// CRC failed - WARN logs byte count only; hex dump at VERBOSE to avoid heap allocation in production
|
||||
ESP_LOGW(TAG, "CRC failed (%u bytes)", this->rx_buffer_index_);
|
||||
ESP_LOGV(TAG, "CRC failed frame: %s",
|
||||
format_hex_pretty(this->rx_buffer_.data(), this->rx_buffer_index_).c_str());
|
||||
this->send_nak_frame_(this->rx_sequence_);
|
||||
}
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (byte == ASH_ESCAPE_BYTE) {
|
||||
this->escape_next_byte_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->escape_next_byte_) {
|
||||
byte ^= ASH_XOR_BYTE;
|
||||
this->escape_next_byte_ = false;
|
||||
}
|
||||
|
||||
// Check buffer overflow
|
||||
if (this->rx_buffer_index_ >= MAX_ASH_FRAME_SIZE) {
|
||||
ESP_LOGE(TAG, "RX buffer overflow, frame too large");
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store data byte
|
||||
this->rx_buffer_[this->rx_buffer_index_++] = byte;
|
||||
break;
|
||||
|
||||
default:
|
||||
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t length, AshFrameType type,
|
||||
uint8_t frame_num, uint8_t ack_num, bool retx) {
|
||||
size_t pos = 0;
|
||||
|
||||
// Start with FLAG
|
||||
output[pos++] = ASH_FLAG_BYTE;
|
||||
|
||||
// Build control byte
|
||||
uint8_t control = 0;
|
||||
switch (type) {
|
||||
case AshFrameType::DATA:
|
||||
// DATA frame format: 0ffrPPPP
|
||||
// Bit 7 = 0 (DATA indicator), bits 6-4 = frmNum, bit 3 = reTx, bits 2-0 = ackNum
|
||||
control = (frame_num << 4) | (retx ? 0x08 : 0x00) | ack_num;
|
||||
break;
|
||||
case AshFrameType::ACK:
|
||||
control = 0x80 | ack_num;
|
||||
break;
|
||||
case AshFrameType::NAK:
|
||||
control = 0xA0 | ack_num;
|
||||
break;
|
||||
case AshFrameType::RST:
|
||||
control = 0xC0;
|
||||
break;
|
||||
case AshFrameType::RSTACK:
|
||||
control = 0xC1;
|
||||
break;
|
||||
case AshFrameType::ERROR:
|
||||
control = 0xC2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add control byte with stuffing (reserved: FLAG, ESCAPE, XON, XOFF, SUB, CAN)
|
||||
if (control == ASH_FLAG_BYTE || control == ASH_ESCAPE_BYTE || control == 0x11 || control == 0x13 || control == 0x18 ||
|
||||
control == 0x1A) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = control ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = control;
|
||||
}
|
||||
|
||||
// Add data payload with stuffing
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
uint8_t byte = data[i];
|
||||
if (byte == ASH_FLAG_BYTE || byte == ASH_ESCAPE_BYTE || byte == 0x11 || byte == 0x13 || byte == 0x18 ||
|
||||
byte == 0x1A) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = byte ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = byte;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate CRC incrementally over control byte then data (avoids a MAX_ASH_FRAME_SIZE stack copy)
|
||||
uint16_t crc = this->calculate_crc_(&control, 1);
|
||||
if (length > 0) {
|
||||
crc = this->calculate_crc_(data, length, crc);
|
||||
}
|
||||
|
||||
// Add CRC with stuffing (big-endian)
|
||||
uint8_t crc_high = (crc >> 8) & 0xFF;
|
||||
uint8_t crc_low = crc & 0xFF;
|
||||
|
||||
if (crc_high == ASH_FLAG_BYTE || crc_high == ASH_ESCAPE_BYTE || crc_high == 0x11 || crc_high == 0x13 ||
|
||||
crc_high == 0x18 || crc_high == 0x1A) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = crc_high ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = crc_high;
|
||||
}
|
||||
|
||||
if (crc_low == ASH_FLAG_BYTE || crc_low == ASH_ESCAPE_BYTE || crc_low == 0x11 || crc_low == 0x13 || crc_low == 0x18 ||
|
||||
crc_low == 0x1A) {
|
||||
output[pos++] = ASH_ESCAPE_BYTE;
|
||||
output[pos++] = crc_low ^ ASH_XOR_BYTE;
|
||||
} else {
|
||||
output[pos++] = crc_low;
|
||||
}
|
||||
|
||||
// End with FLAG
|
||||
output[pos++] = ASH_FLAG_BYTE;
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
@@ -1,88 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
// ASH Protocol Constants
|
||||
static constexpr uint8_t ASH_FLAG_BYTE = 0x7E; // Frame delimiter
|
||||
static constexpr uint8_t ASH_ESCAPE_BYTE = 0x7D; // Escape/substitution byte
|
||||
static constexpr uint8_t ASH_XOR_BYTE = 0x20; // XOR mask for escaped bytes
|
||||
static constexpr uint8_t ASH_SUBSTITUTE_BYTE = 0x18; // Substitution for invalid bytes
|
||||
|
||||
// Reserved bytes that must be escaped
|
||||
static constexpr uint8_t ASH_RESERVED_BYTES[] = {0x7E, 0x7D, 0x11, 0x13, 0x93, 0xA3};
|
||||
|
||||
// Buffer size configuration
|
||||
#ifdef ZIGBEE_PROXY_BUFFER_SIZE
|
||||
static constexpr size_t MAX_ASH_FRAME_SIZE = ZIGBEE_PROXY_BUFFER_SIZE;
|
||||
#else
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr size_t MAX_ASH_FRAME_SIZE = 512; // Limited RAM on ESP8266
|
||||
#else
|
||||
static constexpr size_t MAX_ASH_FRAME_SIZE = 1024; // Full buffer on ESP32/RP2040
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Protocol limits
|
||||
static constexpr uint8_t ASH_MAX_SEQUENCE = 7; // 3-bit sequence number (0-7)
|
||||
static constexpr uint8_t ASH_TX_WINDOW_SIZE = 1; // Only 1 unacknowledged frame allowed
|
||||
static constexpr uint8_t ASH_MAX_RETRIES = 5; // Maximum retransmission attempts
|
||||
static constexpr uint16_t ASH_CRC_INIT = 0xFFFF; // CRC-CCITT initial value
|
||||
static constexpr uint32_t ASH_RESET_TIMEOUT = 3000; // RST/RSTACK timeout in milliseconds
|
||||
|
||||
// IEEE address size
|
||||
static constexpr size_t ZIGBEE_IEEE_ADDR_SIZE = 8; // 64-bit IEEE address
|
||||
|
||||
// ASH Frame Types (encoded in control byte)
|
||||
// DATA format: 0ffrPPPP - bit 7=0, bits 6-4=frmNum, bit 3=reTx, bits 2-0=ackNum
|
||||
// ACK/NAK format: 10XnrPPP - bit 5 distinguishes ACK(0) from NAK(1)
|
||||
enum class AshFrameType : uint8_t {
|
||||
DATA = 0x00, // Data frame (bit 7 = 0)
|
||||
ACK = 0x80, // Acknowledge frame (100nrPPP, bit 5 = 0)
|
||||
NAK = 0xA0, // Negative acknowledge (101nrPPP, bit 5 = 1)
|
||||
RST = 0xC0, // Reset request (bits 7-6 = 11, bits 2-0 = 000)
|
||||
RSTACK = 0xC1, // Reset acknowledgment (bits 7-6 = 11, bits 2-0 = 001)
|
||||
ERROR = 0xC2, // Error indication (bits 7-6 = 11, bits 2-0 = 010)
|
||||
};
|
||||
|
||||
// ASH Connection State
|
||||
enum class AshState : uint8_t {
|
||||
DISCONNECTED, // Initial state, no connection
|
||||
CONNECTING, // Sent RST, waiting for RSTACK
|
||||
CONNECTED, // Normal operation
|
||||
FAILED, // Too many errors/timeouts, requires reset
|
||||
};
|
||||
|
||||
// Frame Parsing State Machine
|
||||
enum class ParsingState : uint8_t {
|
||||
WAIT_FLAG_START, // Looking for frame start FLAG (0x7E)
|
||||
WAIT_CONTROL, // Reading control byte
|
||||
WAIT_DATA, // Reading data payload
|
||||
WAIT_CRC_HIGH, // Reading CRC high byte
|
||||
WAIT_CRC_LOW, // Reading CRC low byte
|
||||
WAIT_FLAG_END, // Expecting end FLAG (0x7E)
|
||||
};
|
||||
|
||||
// Bootloader detection states
|
||||
enum class BootloaderState : uint8_t {
|
||||
NORMAL, // Normal operation
|
||||
DETECTED, // Bootloader mode detected
|
||||
MENU, // In bootloader menu
|
||||
};
|
||||
|
||||
// EZSP Error Codes (from ERROR frame)
|
||||
enum class EzspError : uint8_t {
|
||||
VERSION_NOT_SET = 0x00,
|
||||
RESET_UNKNOWN = 0x01,
|
||||
RESET_EXTERNAL = 0x02,
|
||||
RESET_POWER_ON = 0x03,
|
||||
RESET_WATCHDOG = 0x04,
|
||||
RESET_ASSERT = 0x05,
|
||||
RESET_BOOTLOADER = 0x06,
|
||||
RESET_SOFTWARE = 0x07,
|
||||
EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT = 0x51,
|
||||
};
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
@@ -1,56 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
// EZSP Protocol Versions
|
||||
static constexpr uint8_t EZSP_MIN_VERSION = 8; // Minimum supported version
|
||||
static constexpr uint8_t EZSP_MAX_VERSION = 13; // Maximum version we request
|
||||
|
||||
// EZSP Frame Control bits
|
||||
static constexpr uint8_t EZSP_FRAME_CONTROL_COMMAND = 0x00; // Host to NCP
|
||||
static constexpr uint8_t EZSP_FRAME_CONTROL_RESPONSE = 0x80; // NCP to Host
|
||||
static constexpr uint8_t EZSP_FRAME_CONTROL_CALLBACK = 0x90; // Async callback from NCP
|
||||
|
||||
// Legacy EZSP frame format (v4-v7): [sequence] [frame_control] [frame_id]
|
||||
// Extended EZSP frame format (v8+): [sequence] [frame_control_low] [frame_control_high] [frame_id_low] [frame_id_high]
|
||||
|
||||
// EZSP Frame IDs - Commands (host to NCP)
|
||||
static constexpr uint16_t EZSP_VERSION = 0x0000; // Version negotiation
|
||||
static constexpr uint16_t EZSP_NETWORK_INIT = 0x0017; // Initialize network
|
||||
static constexpr uint16_t EZSP_NETWORK_STATE = 0x0018; // Get network state
|
||||
static constexpr uint16_t EZSP_GET_EUI64 = 0x0026; // Get IEEE address
|
||||
static constexpr uint16_t EZSP_GET_NETWORK_PARAMETERS = 0x0028; // Get network parameters
|
||||
|
||||
// EZSP Frame IDs - Callbacks (NCP to host, async)
|
||||
static constexpr uint16_t EZSP_STACK_STATUS_HANDLER = 0x0019; // Stack status callback
|
||||
|
||||
// EZSP Network Status
|
||||
enum class EzspNetworkStatus : uint8_t {
|
||||
NO_NETWORK = 0x00,
|
||||
JOINING_NETWORK = 0x01,
|
||||
JOINED_NETWORK = 0x02,
|
||||
JOINED_NETWORK_NO_PARENT = 0x03,
|
||||
LEAVING_NETWORK = 0x04,
|
||||
};
|
||||
|
||||
// Ember Status codes (subset)
|
||||
enum class EmberStatus : uint8_t {
|
||||
SUCCESS = 0x00,
|
||||
NETWORK_UP = 0x90,
|
||||
NETWORK_DOWN = 0x91,
|
||||
NOT_JOINED = 0x93,
|
||||
};
|
||||
|
||||
// Network parameters structure offsets (in getNetworkParameters response)
|
||||
// Response format: [status] [nodeType] [parameters...]
|
||||
// Parameters: [extendedPanId (8)] [panId (2)] [radioTxPower] [radioChannel] [joinMethod] ...
|
||||
static constexpr size_t NETWORK_PARAMS_STATUS_OFFSET = 0;
|
||||
static constexpr size_t NETWORK_PARAMS_NODE_TYPE_OFFSET = 1;
|
||||
static constexpr size_t NETWORK_PARAMS_EXT_PAN_ID_OFFSET = 2;
|
||||
static constexpr size_t NETWORK_PARAMS_PAN_ID_OFFSET = 10;
|
||||
static constexpr size_t NETWORK_PARAMS_CHANNEL_OFFSET = 13;
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,252 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "ash_protocol.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
// Forward-declare USBUartChannel so the set_usb_uart_channel() setter can be declared
|
||||
// without pulling usb_uart.h into every translation unit that includes this header.
|
||||
// USE_ZIGBEE_PROXY_USB_UART is defined by the Python to_code() only when usb_uart_id
|
||||
// is present in the YAML, ensuring the header is actually in the build path.
|
||||
#ifdef USE_ZIGBEE_PROXY_USB_UART
|
||||
namespace esphome::usb_uart {
|
||||
class USBUartChannel;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
// Timeout configuration structure
|
||||
struct TimeoutConfig {
|
||||
uint32_t initial_timeout_ms{1600}; // Initial ACK timeout
|
||||
uint32_t min_timeout_ms{400}; // Minimum adaptive timeout
|
||||
uint32_t max_timeout_ms{3200}; // Maximum adaptive timeout
|
||||
uint32_t current_timeout_ms{1600}; // Current adaptive timeout
|
||||
};
|
||||
|
||||
// Network information structure
|
||||
struct NetworkInfo {
|
||||
std::array<uint8_t, ZIGBEE_IEEE_ADDR_SIZE> ieee_address{};
|
||||
uint16_t pan_id{0};
|
||||
std::array<uint8_t, 8> extended_pan_id{};
|
||||
uint8_t channel{0};
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
enum ZigbeeProxyFeature : uint32_t {
|
||||
FEATURE_ZIGBEE_PROXY_ENABLED = 1 << 0,
|
||||
};
|
||||
|
||||
// Boot-time initialization state machine
|
||||
enum class BootState : uint8_t {
|
||||
IDLE, // Not initializing
|
||||
WAIT_RSTACK, // Sent RST, waiting for RSTACK
|
||||
SEND_VERSION, // Send EZSP version command
|
||||
WAIT_VERSION, // Waiting for version response
|
||||
SEND_NETWORK_INIT, // Send networkInit command
|
||||
WAIT_STACK_STATUS, // Waiting for stackStatusHandler callback
|
||||
SEND_GET_NETWORK_PARAMS, // Send getNetworkParameters command
|
||||
WAIT_NETWORK_PARAMS, // Waiting for network parameters response
|
||||
SEND_FINAL_RST, // Send final RST to reset NCP
|
||||
WAIT_FINAL_RSTACK, // Waiting for final RSTACK
|
||||
COMPLETE, // Boot sequence complete
|
||||
FAILED, // Boot sequence failed
|
||||
};
|
||||
|
||||
class ZigbeeProxy : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
ZigbeeProxy();
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
|
||||
// API integration
|
||||
void api_connection_authenticated(api::APIConnection *conn);
|
||||
void zigbee_proxy_request(api::APIConnection *api_connection, const api::ZigbeeProxyRequest &msg);
|
||||
void zigbee_proxy_frame(api::APIConnection *api_connection, const api::ZigbeeProxyFrame &msg);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
|
||||
// Feature flags
|
||||
uint32_t get_feature_flags() const { return ZigbeeProxyFeature::FEATURE_ZIGBEE_PROXY_ENABLED; }
|
||||
|
||||
// Network information accessors
|
||||
const NetworkInfo &get_network_info() const { return this->network_info_; }
|
||||
uint64_t get_ieee_address() const;
|
||||
|
||||
// Frame sending (from API client to NCP)
|
||||
void send_frame(const uint8_t *data, size_t length);
|
||||
|
||||
// Timeout configuration (callable from Python/API)
|
||||
void set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint32_t max_ms);
|
||||
void set_initial_timeout(uint32_t timeout_ms) { this->timeout_config_.initial_timeout_ms = timeout_ms; }
|
||||
void set_min_timeout(uint32_t timeout_ms) { this->timeout_config_.min_timeout_ms = timeout_ms; }
|
||||
void set_max_timeout(uint32_t timeout_ms) { this->timeout_config_.max_timeout_ms = timeout_ms; }
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY_USB_UART
|
||||
/// Called from generated code when usb_uart_id is configured.
|
||||
/// Registers an RX callback on the channel so incoming bytes are processed
|
||||
/// immediately in the same USBUartComponent::loop() iteration they arrive,
|
||||
/// without waiting for the next ZigbeeProxy::loop() call.
|
||||
void set_usb_uart_channel(usb_uart::USBUartChannel *channel);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// ASH Protocol State Machine
|
||||
void reset_ash_protocol_();
|
||||
void send_rst_frame_();
|
||||
void handle_rstack_frame_(const uint8_t *data, size_t length);
|
||||
void handle_error_frame_(const uint8_t *data, size_t length);
|
||||
bool send_ack_frame_(uint8_t ack_num);
|
||||
bool send_nak_frame_(uint8_t ack_num);
|
||||
bool send_data_frame_(const uint8_t *data, size_t length, bool retransmit = false);
|
||||
|
||||
// Frame parsing and building (implemented in ash_protocol.cpp)
|
||||
bool parse_byte_(uint8_t byte);
|
||||
void parse_control_byte_(uint8_t control);
|
||||
bool validate_frame_crc_();
|
||||
size_t build_frame_(uint8_t *output, const uint8_t *data, size_t length, AshFrameType type, uint8_t frame_num = 0,
|
||||
uint8_t ack_num = 0, bool retx = false);
|
||||
uint16_t calculate_crc_(const uint8_t *data, size_t length, uint16_t init = ASH_CRC_INIT);
|
||||
|
||||
// Sequence number management
|
||||
void increment_tx_sequence_() { this->tx_sequence_ = (this->tx_sequence_ + 1) & ASH_MAX_SEQUENCE; }
|
||||
void increment_rx_sequence_() { this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE; }
|
||||
|
||||
// Timeout management
|
||||
void update_adaptive_timeout_(uint32_t measured_rtt_ms);
|
||||
void start_ack_timer_() { this->ack_timer_start_ = millis(); }
|
||||
bool check_ack_timeout_();
|
||||
|
||||
// Retransmission
|
||||
void handle_retransmission_();
|
||||
void clear_tx_buffer_() {
|
||||
this->tx_buffer_pending_ = false;
|
||||
this->tx_retry_count_ = 0;
|
||||
}
|
||||
|
||||
// Boot-time NCP initialization
|
||||
void start_boot_sequence_();
|
||||
void advance_boot_state_();
|
||||
void handle_boot_data_frame_(const uint8_t *data, size_t length);
|
||||
void send_ezsp_version_();
|
||||
void send_network_init_();
|
||||
void send_get_network_params_();
|
||||
void handle_version_response_(const uint8_t *data, size_t length);
|
||||
void handle_stack_status_(const uint8_t *data, size_t length);
|
||||
void handle_network_params_response_(const uint8_t *data, size_t length);
|
||||
|
||||
// IEEE address and network info
|
||||
bool set_ieee_address_(const uint8_t *new_address);
|
||||
void send_network_info_changed_msg_(api::APIConnection *conn = nullptr);
|
||||
|
||||
// WiFi/Zigbee channel conflict detection
|
||||
void check_wifi_zigbee_conflict_();
|
||||
|
||||
// Bootloader detection
|
||||
void check_bootloader_mode_(const uint8_t *data, size_t length);
|
||||
|
||||
// UART processing
|
||||
void process_uart_();
|
||||
|
||||
// Client-side (left) ASH session
|
||||
void client_parse_byte_(uint8_t byte);
|
||||
void client_parse_control_byte_(uint8_t control);
|
||||
bool client_validate_frame_crc_();
|
||||
void client_send_ack_frame_(uint8_t ack_num);
|
||||
void client_send_rstack_frame_(uint8_t reset_code);
|
||||
void client_send_data_frame_(const uint8_t *data, size_t length);
|
||||
void client_send_error_frame_(uint8_t error_code);
|
||||
void client_send_raw_frame_(const uint8_t *frame, size_t length);
|
||||
void client_reset_session_();
|
||||
|
||||
// Send raw bytes to API client
|
||||
void send_to_client_(const uint8_t *data, size_t length);
|
||||
|
||||
// Forward NCP frames to client
|
||||
void forward_ncp_data_to_client_(const uint8_t *payload, size_t length);
|
||||
void forward_ncp_rstack_to_client_(const uint8_t *data, size_t length);
|
||||
void forward_ncp_error_to_client_(const uint8_t *data, size_t length);
|
||||
|
||||
// Pre-allocated message - always ready to send
|
||||
api::ZigbeeProxyFrame outgoing_proto_msg_;
|
||||
|
||||
// NCP-side (right) ASH buffers
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> rx_buffer_;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> tx_buffer_;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> tx_pending_buffer_; // For retransmission
|
||||
|
||||
// Client-side (left) ASH buffers
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_rx_buffer_;
|
||||
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_tx_buffer_;
|
||||
|
||||
// Network information
|
||||
NetworkInfo network_info_;
|
||||
|
||||
// Timeout configuration
|
||||
TimeoutConfig timeout_config_;
|
||||
|
||||
// Pointers (aligned together)
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
|
||||
// NCP-side (right) 32-bit values
|
||||
uint32_t setup_time_{0}; // Time when last RST frame was sent
|
||||
uint32_t boot_start_time_{0}; // Time when the boot sequence began (for overall timeout)
|
||||
uint32_t ack_timer_start_{0}; // Time when ACK timer started
|
||||
uint32_t last_rtt_ms_{0}; // Last measured round-trip time
|
||||
|
||||
// NCP-side (right) 16-bit values
|
||||
uint16_t rx_buffer_index_{0}; // Index for populating rx_buffer_
|
||||
uint16_t tx_pending_length_{0}; // Length of pending TX frame for retransmission
|
||||
uint16_t calculated_crc_{0}; // CRC calculated during frame reception
|
||||
|
||||
// Client-side (left) 16-bit values
|
||||
uint16_t client_rx_buffer_index_{0};
|
||||
|
||||
// NCP-side (right) 8-bit values
|
||||
uint8_t tx_sequence_{0}; // TX sequence number (0-7)
|
||||
uint8_t rx_sequence_{0}; // RX sequence number (0-7)
|
||||
uint8_t tx_retry_count_{0}; // Number of retransmission attempts
|
||||
uint8_t tx_pending_frame_num_{0}; // Frame number of pending TX frame
|
||||
uint8_t last_ack_sent_{0}; // Last ACK number sent
|
||||
|
||||
// Client-side (left) 8-bit values
|
||||
uint8_t client_tx_sequence_{0}; // Client-facing TX sequence (proxy → client)
|
||||
uint8_t client_rx_sequence_{0}; // Client-facing RX sequence (client → proxy)
|
||||
|
||||
// NCP-side enums and booleans
|
||||
AshState ash_state_{AshState::DISCONNECTED};
|
||||
ParsingState parsing_state_{ParsingState::WAIT_FLAG_START};
|
||||
BootloaderState bootloader_state_{BootloaderState::NORMAL};
|
||||
BootState boot_state_{BootState::IDLE};
|
||||
|
||||
// Client-side enums and booleans
|
||||
AshState client_ash_state_{AshState::DISCONNECTED};
|
||||
ParsingState client_parsing_state_{ParsingState::WAIT_FLAG_START};
|
||||
|
||||
uint8_t ezsp_version_{0}; // NCP's EZSP protocol version
|
||||
uint8_t ezsp_sequence_{0}; // EZSP frame sequence number
|
||||
uint8_t ezsp_requested_version_{0}; // Version we last requested (for re-negotiation)
|
||||
|
||||
bool tx_buffer_pending_{false}; // True if waiting for ACK from NCP
|
||||
bool escape_next_byte_{false}; // True if next NCP byte should be unescaped
|
||||
bool client_escape_next_byte_{false}; // True if next client byte should be unescaped
|
||||
bool network_info_ready_{false}; // True when network info retrieved
|
||||
bool boot_sequence_active_{false}; // True during boot-time init
|
||||
};
|
||||
|
||||
extern ZigbeeProxy *global_zigbee_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
@@ -314,7 +314,7 @@ class Version:
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str) -> Version:
|
||||
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
|
||||
match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value)
|
||||
if match is None:
|
||||
raise ValueError(f"Not a valid version number {value}")
|
||||
major = int(match[1])
|
||||
|
||||
+1
-4
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.4.0-dev"
|
||||
__version__ = "2026.3.0"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
@@ -1235,7 +1235,6 @@ UNIT_LITRE = "L"
|
||||
UNIT_LUX = "lx"
|
||||
UNIT_MEGAJOULE = "MJ"
|
||||
UNIT_METER = "m"
|
||||
UNIT_METER_PER_SECOND = "m/s"
|
||||
UNIT_METER_PER_SECOND_SQUARED = "m/s²"
|
||||
UNIT_MICROAMP = "µA"
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
|
||||
@@ -1245,7 +1244,6 @@ UNIT_MICROSILVERTS_PER_HOUR = "µSv/h"
|
||||
UNIT_MICROTESLA = "µT"
|
||||
UNIT_MILLIAMP = "mA"
|
||||
UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³"
|
||||
UNIT_MILLILITRE = "mL"
|
||||
UNIT_MILLIMETER = "mm"
|
||||
UNIT_MILLISECOND = "ms"
|
||||
UNIT_MILLISIEMENS_PER_CENTIMETER = "mS/cm"
|
||||
@@ -1257,7 +1255,6 @@ UNIT_PARTS_PER_MILLION = "ppm"
|
||||
UNIT_PASCAL = "Pa"
|
||||
UNIT_PERCENT = "%"
|
||||
UNIT_PH = "pH"
|
||||
UNIT_POUND = "lb"
|
||||
UNIT_PULSES = "pulses"
|
||||
UNIT_PULSES_PER_MINUTE = "pulses/min"
|
||||
UNIT_REVOLUTIONS_PER_MINUTE = "RPM"
|
||||
|
||||
@@ -589,10 +589,7 @@ async def _add_looping_components() -> None:
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(cg.global_ns.namespace("esphome").using)
|
||||
# These can be used by user lambdas, put them to default scope
|
||||
# picolibc (IDF 6.0+) declares isnan in global scope, conflicting with using std::isnan
|
||||
cg.add_global(cg.RawStatement("#ifndef __PICOLIBC__"))
|
||||
cg.add_global(cg.RawExpression("using std::isnan"))
|
||||
cg.add_global(cg.RawStatement("#endif"))
|
||||
cg.add_global(cg.RawExpression("using std::min"))
|
||||
cg.add_global(cg.RawExpression("using std::max"))
|
||||
|
||||
|
||||
@@ -138,8 +138,6 @@
|
||||
#define USE_VALVE
|
||||
#define USE_WATER_HEATER
|
||||
#define USE_WATER_HEATER_VISUAL_OVERRIDES
|
||||
#define USE_ZIGBEE_PROXY
|
||||
#define USE_ZIGBEE_PROXY_USB_UART
|
||||
#define USE_ZWAVE_PROXY
|
||||
|
||||
// Feature flags which do not work for zephyr
|
||||
|
||||
@@ -100,6 +100,14 @@ class EntityBase {
|
||||
// Get whether this Entity should be hidden outside ESPHome
|
||||
bool is_internal() const { return this->flags_.internal; }
|
||||
|
||||
// Deprecated: Calling set_internal() at runtime is undefined behavior. Components and clients
|
||||
// are NOT notified of the change, the flag may have already been read during setup, and there
|
||||
// is NO guarantee any consumer will observe the new value. Use the 'internal:' YAML key instead.
|
||||
ESPDEPRECATED("set_internal() is undefined behavior at runtime — components and Home Assistant are NOT "
|
||||
"notified. Use the 'internal:' YAML key instead. Will be removed in 2027.3.0.",
|
||||
"2026.3.0")
|
||||
void set_internal(bool internal) { this->flags_.internal = internal; }
|
||||
|
||||
// Check if this object is declared to be disabled by default.
|
||||
// That means that when the device gets added to Home Assistant (or other clients) it should
|
||||
// not be added to the default view by default, and a user action is necessary to manually add it.
|
||||
|
||||
+16
-137
@@ -301,7 +301,7 @@ template<typename T, size_t N> class StaticVector {
|
||||
/// Not thread-safe. All access (push/pop/iteration) must occur from a single
|
||||
/// context, or the caller must provide external synchronization.
|
||||
template<typename T, size_t N> class StaticRingBuffer {
|
||||
using index_type = std::conditional_t<(N <= std::numeric_limits<uint8_t>::max()), uint8_t, uint16_t>;
|
||||
using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>;
|
||||
|
||||
public:
|
||||
class Iterator {
|
||||
@@ -356,13 +356,6 @@ template<typename T, size_t N> class StaticRingBuffer {
|
||||
index_type size() const { return this->count_; }
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
|
||||
/// Clear all elements (reset to empty)
|
||||
void clear() {
|
||||
this->head_ = 0;
|
||||
this->tail_ = 0;
|
||||
this->count_ = 0;
|
||||
}
|
||||
|
||||
Iterator begin() { return Iterator(this, 0); }
|
||||
Iterator end() { return Iterator(this, this->count_); }
|
||||
ConstIterator begin() const { return ConstIterator(this, 0); }
|
||||
@@ -375,128 +368,6 @@ template<typename T, size_t N> class StaticRingBuffer {
|
||||
index_type count_{0};
|
||||
};
|
||||
|
||||
/// Fixed-capacity circular buffer - allocates once at runtime, never reallocates.
|
||||
/// Runtime-sized equivalent of StaticRingBuffer - use when capacity is only known at initialization.
|
||||
/// Supports FIFO push/pop and iteration over queued elements.
|
||||
/// Not thread-safe.
|
||||
template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> class FixedRingBuffer {
|
||||
using index_type = std::conditional_t<
|
||||
(MAX_CAPACITY <= std::numeric_limits<uint8_t>::max()), uint8_t,
|
||||
std::conditional_t<(MAX_CAPACITY <= std::numeric_limits<uint16_t>::max()), uint16_t, uint32_t>>;
|
||||
|
||||
public:
|
||||
class Iterator {
|
||||
public:
|
||||
Iterator(FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
|
||||
T &operator*() { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; }
|
||||
Iterator &operator++() {
|
||||
++pos_;
|
||||
return *this;
|
||||
}
|
||||
bool operator!=(const Iterator &other) const { return pos_ != other.pos_; }
|
||||
|
||||
private:
|
||||
FixedRingBuffer *buf_;
|
||||
index_type pos_;
|
||||
};
|
||||
|
||||
class ConstIterator {
|
||||
public:
|
||||
ConstIterator(const FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
|
||||
const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; }
|
||||
ConstIterator &operator++() {
|
||||
++pos_;
|
||||
return *this;
|
||||
}
|
||||
bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; }
|
||||
|
||||
private:
|
||||
const FixedRingBuffer *buf_;
|
||||
index_type pos_;
|
||||
};
|
||||
|
||||
FixedRingBuffer() = default;
|
||||
~FixedRingBuffer() {
|
||||
if constexpr (std::is_trivial<T>::value) {
|
||||
::operator delete(this->data_);
|
||||
} else {
|
||||
delete[] this->data_;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable copy
|
||||
FixedRingBuffer(const FixedRingBuffer &) = delete;
|
||||
FixedRingBuffer &operator=(const FixedRingBuffer &) = delete;
|
||||
|
||||
/// Allocate capacity - can only be called once
|
||||
void init(index_type capacity) {
|
||||
if constexpr (std::is_trivial<T>::value) {
|
||||
// Raw allocation without initialization (elements are written before read)
|
||||
// NOLINTNEXTLINE(bugprone-sizeof-expression)
|
||||
this->data_ = static_cast<T *>(::operator new(capacity * sizeof(T)));
|
||||
} else {
|
||||
this->data_ = new T[capacity];
|
||||
}
|
||||
this->capacity_ = capacity;
|
||||
}
|
||||
|
||||
/// Push a value. Returns false if full.
|
||||
bool push(const T &value) {
|
||||
if (this->count_ >= this->capacity_)
|
||||
return false;
|
||||
this->data_[this->tail_] = value;
|
||||
this->tail_ = (this->tail_ + 1) % this->capacity_;
|
||||
++this->count_;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Push a value, overwriting the oldest if full.
|
||||
void push_overwrite(const T &value) {
|
||||
this->data_[this->tail_] = value;
|
||||
this->tail_ = (this->tail_ + 1) % this->capacity_;
|
||||
if (this->count_ >= this->capacity_) {
|
||||
// Buffer full - advance head to drop oldest, count stays at capacity
|
||||
this->head_ = this->tail_;
|
||||
} else {
|
||||
++this->count_;
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the oldest element.
|
||||
void pop() {
|
||||
if (this->count_ > 0) {
|
||||
this->head_ = (this->head_ + 1) % this->capacity_;
|
||||
--this->count_;
|
||||
}
|
||||
}
|
||||
|
||||
T &front() { return this->data_[this->head_]; }
|
||||
const T &front() const { return this->data_[this->head_]; }
|
||||
index_type size() const { return this->count_; }
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
index_type capacity() const { return this->capacity_; }
|
||||
bool full() const { return this->count_ == this->capacity_; }
|
||||
|
||||
/// Clear all elements (reset to empty, keep capacity)
|
||||
void clear() {
|
||||
this->head_ = 0;
|
||||
this->tail_ = 0;
|
||||
this->count_ = 0;
|
||||
}
|
||||
|
||||
Iterator begin() { return Iterator(this, 0); }
|
||||
Iterator end() { return Iterator(this, this->count_); }
|
||||
ConstIterator begin() const { return ConstIterator(this, 0); }
|
||||
ConstIterator end() const { return ConstIterator(this, this->count_); }
|
||||
|
||||
protected:
|
||||
T *data_{nullptr};
|
||||
index_type head_{0};
|
||||
index_type tail_{0};
|
||||
index_type count_{0};
|
||||
index_type capacity_{0};
|
||||
};
|
||||
|
||||
/// Fixed-capacity vector - allocates once at runtime, never reallocates
|
||||
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
|
||||
/// when size is known at initialization but not at compile time
|
||||
@@ -1930,19 +1801,27 @@ class InterruptLock {
|
||||
|
||||
/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
|
||||
*
|
||||
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled.
|
||||
* It ensures thread-safe access to lwIP APIs.
|
||||
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled,
|
||||
* and on RP2040 when CYW43 WiFi is active (cyw43_arch_lwip_begin/end).
|
||||
*
|
||||
* @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp
|
||||
* On platforms without lwIP core locking (ESP8266, LibreTiny, Zephyr),
|
||||
* this is a no-op defined inline so the compiler can eliminate all call overhead.
|
||||
*/
|
||||
class LwIPLock {
|
||||
public:
|
||||
LwIPLock();
|
||||
~LwIPLock();
|
||||
|
||||
// Delete copy constructor and copy assignment operator to prevent accidental copying
|
||||
LwIPLock(const LwIPLock &) = delete;
|
||||
LwIPLock &operator=(const LwIPLock &) = delete;
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_RP2040)
|
||||
// Platforms with potential lwIP core locking — out-of-line implementations in helpers.cpp
|
||||
LwIPLock();
|
||||
~LwIPLock();
|
||||
#else
|
||||
// No lwIP core locking — inline no-ops (empty bodies instead of = default
|
||||
// to prevent clang-tidy unused-variable warnings at call sites)
|
||||
LwIPLock() {}
|
||||
~LwIPLock() {}
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Helper class to request `loop()` to be called as fast as possible.
|
||||
|
||||
+29
-19
@@ -211,6 +211,14 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
|
||||
}
|
||||
target->push_back(item);
|
||||
if (target == &this->to_add_) {
|
||||
this->to_add_count_increment_();
|
||||
}
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
else {
|
||||
this->defer_count_increment_();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout,
|
||||
@@ -387,7 +395,7 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||
// safe when called from the main thread. Other threads must not call this method.
|
||||
|
||||
// If no items, return empty optional
|
||||
if (this->cleanup_() == 0)
|
||||
if (!this->cleanup_())
|
||||
return {};
|
||||
|
||||
SchedulerItem *item = this->items_[0];
|
||||
@@ -421,7 +429,7 @@ void Scheduler::full_cleanup_removed_items_() {
|
||||
this->items_.erase(this->items_.begin() + write, this->items_.end());
|
||||
// Rebuild the heap structure since items are no longer in heap order
|
||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
this->to_remove_ = 0;
|
||||
this->to_remove_clear_();
|
||||
}
|
||||
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
@@ -502,7 +510,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
|
||||
// If we still have too many cancelled items, do a full cleanup
|
||||
// This only happens if cancelled items are stuck in the middle/bottom of the heap
|
||||
if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) {
|
||||
if (this->to_remove_count_() >= MAX_LOGICALLY_DELETED_ITEMS) {
|
||||
this->full_cleanup_removed_items_();
|
||||
}
|
||||
while (!this->items_.empty()) {
|
||||
@@ -529,7 +537,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
LockGuard guard{this->lock_};
|
||||
if (is_item_removed_locked_(item)) {
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
this->to_remove_--;
|
||||
this->to_remove_decrement_();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -538,7 +546,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
if (is_item_removed_(item)) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
this->to_remove_--;
|
||||
this->to_remove_decrement_();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
@@ -566,7 +574,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
|
||||
if (this->is_item_removed_locked_(executed_item)) {
|
||||
// We were removed/cancelled in the function call, recycle and continue
|
||||
this->to_remove_--;
|
||||
this->to_remove_decrement_();
|
||||
this->recycle_item_main_loop_(executed_item);
|
||||
continue;
|
||||
}
|
||||
@@ -576,6 +584,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
// Add new item directly to to_add_
|
||||
// since we have the lock held
|
||||
this->to_add_.push_back(executed_item);
|
||||
this->to_add_count_increment_();
|
||||
} else {
|
||||
// Timeout completed - recycle it
|
||||
this->recycle_item_main_loop_(executed_item);
|
||||
@@ -604,6 +613,10 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
#endif
|
||||
}
|
||||
void HOT Scheduler::process_to_add() {
|
||||
// Fast path: skip lock acquisition when nothing to add.
|
||||
// Worst case is a one-loop-iteration delay before newly added items are processed.
|
||||
if (this->to_add_empty_())
|
||||
return;
|
||||
LockGuard guard{this->lock_};
|
||||
for (auto *&it : this->to_add_) {
|
||||
if (is_item_removed_locked_(it)) {
|
||||
@@ -617,17 +630,14 @@ void HOT Scheduler::process_to_add() {
|
||||
std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
}
|
||||
this->to_add_.clear();
|
||||
this->to_add_count_clear_();
|
||||
}
|
||||
size_t HOT Scheduler::cleanup_() {
|
||||
// Fast path: if nothing to remove, just return the current size
|
||||
// Reading to_remove_ without lock is safe because:
|
||||
// 1. We only call this from the main thread during call()
|
||||
// 2. If it's 0, there's definitely nothing to cleanup
|
||||
// 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration
|
||||
// 4. Not all platforms support atomics, so we accept this race in favor of performance
|
||||
// 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless
|
||||
if (this->to_remove_ == 0)
|
||||
return this->items_.size();
|
||||
bool HOT Scheduler::cleanup_() {
|
||||
// Fast path: if nothing to remove, just check if items exist.
|
||||
// Uses atomic load on platforms with atomics, falls back to always taking the lock otherwise.
|
||||
// Worst case is a one-loop-iteration delay in cleanup.
|
||||
if (this->to_remove_empty_())
|
||||
return !this->items_.empty();
|
||||
|
||||
// We must hold the lock for the entire cleanup operation because:
|
||||
// 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access
|
||||
@@ -642,10 +652,10 @@ size_t HOT Scheduler::cleanup_() {
|
||||
SchedulerItem *item = this->items_[0];
|
||||
if (!this->is_item_removed_locked_(item))
|
||||
break;
|
||||
this->to_remove_--;
|
||||
this->to_remove_decrement_();
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
}
|
||||
return this->items_.size();
|
||||
return !this->items_.empty();
|
||||
}
|
||||
Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() {
|
||||
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
@@ -698,7 +708,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
|
||||
size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name,
|
||||
hash_or_id, type, match_retry);
|
||||
total_cancelled += heap_cancelled;
|
||||
this->to_remove_ += heap_cancelled;
|
||||
this->to_remove_add_(heap_cancelled);
|
||||
}
|
||||
|
||||
// Cancel items in to_add_
|
||||
|
||||
+148
-11
@@ -284,9 +284,9 @@ class Scheduler {
|
||||
#endif
|
||||
}
|
||||
// Cleanup logically deleted items from the scheduler
|
||||
// Returns the number of items remaining after cleanup
|
||||
// Returns true if items remain after cleanup
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
size_t cleanup_();
|
||||
bool cleanup_();
|
||||
// Remove and return the front item from the heap as a raw pointer.
|
||||
// Caller takes ownership and must either recycle or delete the item.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
@@ -395,15 +395,9 @@ class Scheduler {
|
||||
// erase() on every pop, which would be O(n). The queue is processed once per loop -
|
||||
// any items added during processing are left for the next loop iteration.
|
||||
|
||||
// Snapshot the queue end point - only process items that existed at loop start
|
||||
// Items added during processing (by callbacks or other threads) run next loop
|
||||
// No lock needed: single consumer (main loop), stale read just means we process less this iteration
|
||||
size_t defer_queue_end = this->defer_queue_.size();
|
||||
|
||||
// Fast path: nothing to process, avoid lock entirely.
|
||||
// Safe without lock: single consumer (main loop) reads front_, and a stale size() read
|
||||
// from a concurrent push can only make us see fewer items — they'll be processed next loop.
|
||||
if (this->defer_queue_front_ >= defer_queue_end)
|
||||
// Worst case is a one-loop-iteration delay before newly deferred items are processed.
|
||||
if (this->defer_empty_())
|
||||
return;
|
||||
|
||||
// Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total),
|
||||
@@ -412,6 +406,13 @@ class Scheduler {
|
||||
SchedulerItem *item;
|
||||
|
||||
this->lock_.lock();
|
||||
// Reset counter and snapshot queue end under lock
|
||||
this->defer_count_clear_();
|
||||
size_t defer_queue_end = this->defer_queue_.size();
|
||||
if (this->defer_queue_front_ >= defer_queue_end) {
|
||||
this->lock_.unlock();
|
||||
return;
|
||||
}
|
||||
while (this->defer_queue_front_ < defer_queue_end) {
|
||||
// Take ownership of the item, leaving nullptr in the vector slot.
|
||||
// This is safe because:
|
||||
@@ -527,14 +528,150 @@ class Scheduler {
|
||||
Mutex lock_;
|
||||
std::vector<SchedulerItem *> items_;
|
||||
std::vector<SchedulerItem *> to_add_;
|
||||
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Fast-path counter for process_to_add() to skip taking the lock when there is
|
||||
// nothing to add. Uses std::atomic on platforms that support it, plain uint32_t
|
||||
// otherwise. On non-atomic platforms, callers must hold the scheduler lock when
|
||||
// mutating this counter. Not needed on single-threaded platforms where we can
|
||||
// check to_add_.empty() directly.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
std::atomic<uint32_t> to_add_count_{0};
|
||||
#else
|
||||
uint32_t to_add_count_{0};
|
||||
#endif
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Fast-path helper for process_to_add() to decide if it can try the lock-free path.
|
||||
// - On ESPHOME_THREAD_SINGLE: direct container check is safe (no concurrent writers).
|
||||
// - On ESPHOME_THREAD_MULTI_ATOMICS: performs a lock-free check via to_add_count_.
|
||||
// - On ESPHOME_THREAD_MULTI_NO_ATOMICS: always returns false to force the caller
|
||||
// down the locked path; this is NOT a lock-free emptiness check on that platform.
|
||||
bool to_add_empty_() const {
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
return this->to_add_.empty();
|
||||
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
return this->to_add_count_.load(std::memory_order_relaxed) == 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Increment to_add_count_ (no-op on single-threaded platforms)
|
||||
void to_add_count_increment_() {
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
// No counter needed — to_add_empty_() checks the vector directly
|
||||
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
this->to_add_count_.fetch_add(1, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_add_count_++;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reset to_add_count_ (no-op on single-threaded platforms)
|
||||
void to_add_count_clear_() {
|
||||
#ifdef ESPHOME_THREAD_SINGLE
|
||||
// No counter needed — to_add_empty_() checks the vector directly
|
||||
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
|
||||
this->to_add_count_.store(0, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_add_count_ = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Single-core platforms don't need the defer queue and save ~32 bytes of RAM
|
||||
// Using std::vector instead of std::deque avoids 512-byte chunked allocations
|
||||
// Index tracking avoids O(n) erase() calls when draining the queue each loop
|
||||
std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls
|
||||
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Fast-path counter for process_defer_queue_() to skip lock when nothing to process.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
std::atomic<uint32_t> defer_count_{0};
|
||||
#else
|
||||
uint32_t defer_count_{0};
|
||||
#endif
|
||||
|
||||
bool defer_empty_() const {
|
||||
// defer_queue_ only exists on multi-threaded platforms, so no ESPHOME_THREAD_SINGLE path
|
||||
// ESPHOME_THREAD_MULTI_NO_ATOMICS: always take the lock
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return this->defer_count_.load(std::memory_order_relaxed) == 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void defer_count_increment_() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->defer_count_.fetch_add(1, std::memory_order_relaxed);
|
||||
#else
|
||||
this->defer_count_++;
|
||||
#endif
|
||||
}
|
||||
|
||||
void defer_count_clear_() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->defer_count_.store(0, std::memory_order_relaxed);
|
||||
#else
|
||||
this->defer_count_ = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Counter for items marked for removal. Incremented cross-thread in cancel_item_locked_().
|
||||
// On ESPHOME_THREAD_MULTI_ATOMICS this is read without a lock in the cleanup_() fast path;
|
||||
// on ESPHOME_THREAD_MULTI_NO_ATOMICS the fast path is disabled so cleanup_() always takes the lock.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
std::atomic<uint32_t> to_remove_{0};
|
||||
#else
|
||||
uint32_t to_remove_{0};
|
||||
#endif
|
||||
|
||||
// Lock-free check if there are items to remove (for fast-path in cleanup_)
|
||||
bool to_remove_empty_() const {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return this->to_remove_.load(std::memory_order_relaxed) == 0;
|
||||
#elif defined(ESPHOME_THREAD_SINGLE)
|
||||
return this->to_remove_ == 0;
|
||||
#else
|
||||
return false; // Always take the lock path
|
||||
#endif
|
||||
}
|
||||
|
||||
void to_remove_add_(uint32_t count) {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->to_remove_.fetch_add(count, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_remove_ += count;
|
||||
#endif
|
||||
}
|
||||
|
||||
void to_remove_decrement_() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->to_remove_.fetch_sub(1, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_remove_--;
|
||||
#endif
|
||||
}
|
||||
|
||||
void to_remove_clear_() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
this->to_remove_.store(0, std::memory_order_relaxed);
|
||||
#else
|
||||
this->to_remove_ = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t to_remove_count_() const {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
return this->to_remove_.load(std::memory_order_relaxed);
|
||||
#else
|
||||
return this->to_remove_;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Memory pool for recycling SchedulerItem objects to reduce heap churn.
|
||||
// Design decisions:
|
||||
|
||||
@@ -5,6 +5,8 @@ dependencies:
|
||||
version: 2.0.3
|
||||
esphome/micro-opus:
|
||||
version: 0.3.5
|
||||
espressif/esp-dsp:
|
||||
version: "1.7.1"
|
||||
espressif/esp-tflite-micro:
|
||||
version: 1.3.3~1
|
||||
espressif/esp32-camera:
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.1
|
||||
esphome-dashboard==20260210.0
|
||||
aioesphomeapi==44.5.1
|
||||
aioesphomeapi==44.5.2
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.6 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.5 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user