mirror of
https://github.com/esphome/esphome.git
synced 2026-07-02 05:22:50 +00:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7dabe236e | |||
| 600ca01fd3 | |||
| 65051153ac | |||
| 514c0c8331 | |||
| dc634b8c7b | |||
| 66a4acafd0 | |||
| 3bf45d8fe0 | |||
| 9cd7c5e700 | |||
| d79cf1d718 | |||
| 3d8a3a91f2 | |||
| 3fd3dcc7e5 | |||
| 7b5a4b466a | |||
| 92642df419 | |||
| f5f99071fb | |||
| cb15e98765 | |||
| 2f2c7ac393 | |||
| d9788aaefc | |||
| f7b410fd0c | |||
| e261b5de65 | |||
| 7ecdf6db2e | |||
| 83d02c602a | |||
| 6d16c57747 | |||
| 45c0e6ef7f | |||
| 320474b62d | |||
| a3c483edf3 | |||
| 036be63f7b | |||
| bbfe324dd6 | |||
| de3292c828 | |||
| 67ab2e143c | |||
| 9abc112f76 | |||
| b5880df93c | |||
| 2352c732de | |||
| 77264de3f6 | |||
| 42da281854 | |||
| 06cc5a29a7 | |||
| 98b4e1ea15 | |||
| 0bf6e1e839 | |||
| 3fe84eadef | |||
| 12eed0d384 | |||
| 28e8250b69 | |||
| 0297260a57 | |||
| d4f7cb984c | |||
| 08187a01b1 | |||
| daf3502e15 | |||
| 08cab43548 | |||
| 5cbe936256 | |||
| 729d3d4bc2 | |||
| 8af0991590 | |||
| 99d968f80a | |||
| 705d548435 | |||
| 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.2
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
@@ -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
|
||||
@@ -67,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS *
|
||||
// A stalled handshake from a buggy client or network glitch holds a connection
|
||||
// slot, which can prevent legitimate clients from reconnecting. Also hardens
|
||||
// against the less likely case of intentional connection slot exhaustion.
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
|
||||
//
|
||||
// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak
|
||||
// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to
|
||||
// 28-30s. See https://github.com/esphome/esphome/issues/14999
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
|
||||
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
|
||||
@@ -1320,16 +1321,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 +1634,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 +1775,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,
|
||||
|
||||
@@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
|
||||
switch (event) {
|
||||
// server response on RSSI request:
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr))
|
||||
return;
|
||||
if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) {
|
||||
int8_t rssi = param->read_rssi_cmpl.rssi;
|
||||
ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const {
|
||||
obj.year = this->year_;
|
||||
obj.month = this->month_;
|
||||
obj.day_of_month = this->day_;
|
||||
obj.day_of_week = 0;
|
||||
obj.day_of_year = 0;
|
||||
obj.is_dst = false;
|
||||
obj.hour = this->hour_;
|
||||
obj.minute = this->minute_;
|
||||
obj.second = this->second_;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -70,6 +70,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
|
||||
public:
|
||||
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
|
||||
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
|
||||
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
|
||||
void play(const Ts &...x) override {
|
||||
// If the listener is already set, do nothing
|
||||
@@ -115,6 +116,7 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...
|
||||
public:
|
||||
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
|
||||
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
|
||||
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
|
||||
void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); }
|
||||
|
||||
|
||||
@@ -82,12 +82,18 @@ void ESP32BLETracker::setup() {
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->scan_continuous_before_ota_ = this->scan_continuous_;
|
||||
this->stop_scan();
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->disconnect();
|
||||
}
|
||||
#endif
|
||||
} else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
|
||||
this->scan_continuous_before_ota_ = false;
|
||||
this->scan_continuous_ = true;
|
||||
// Do not restart scanning immediately here; allow loop() to
|
||||
// safely restart scanning once the scanner and all clients are idle.
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -429,6 +429,9 @@ class ESP32BLETracker : public Component,
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
bool scan_continuous_before_ota_{false};
|
||||
#endif
|
||||
bool ble_was_disabled_{true};
|
||||
bool raw_advertisements_{false};
|
||||
bool parse_advertisements_{false};
|
||||
|
||||
@@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() {
|
||||
}
|
||||
|
||||
// Publish initial OFF state for sensors that haven't received events yet
|
||||
bool all_initial_published = true;
|
||||
for (auto *child : this->children_) {
|
||||
this->publish_initial_state_if_needed_(child, now);
|
||||
if (!child->initial_state_published_) {
|
||||
all_initial_published = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->setup_mode_) {
|
||||
// Only disable loop once all initial states are published
|
||||
if (!this->setup_mode_ && all_initial_published) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,8 +19,9 @@ from esphome.const import (
|
||||
PLATFORM_ESP8266,
|
||||
ThreadModel,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||
from .const import (
|
||||
@@ -40,12 +42,42 @@ from .const import (
|
||||
)
|
||||
from .gpio import PinInitialState, add_pin_initial_states_array
|
||||
|
||||
CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float"
|
||||
# Heuristically matches scanf/sscanf calls with float format specifiers.
|
||||
# Standard scanf float conversions: %f %F %e %E %g %G %a %A
|
||||
# With optional modifiers: %*f (suppression), %8f (width), %lf %Lf (length)
|
||||
# Also matches non-standard patterns like %.2f as a heuristic — these are
|
||||
# invalid in scanf but users may write them by analogy with printf.
|
||||
# Uses [^;]*? to stay within a single statement, preventing false positives
|
||||
# from e.g. sscanf(buf, "%d", &x); printf("%f", val);
|
||||
_SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]")
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
AUTO_LOAD = ["preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
|
||||
def lambdas_use_scanf_float(config: ConfigType) -> bool:
|
||||
"""Check if any lambda in the config uses scanf with a float format specifier.
|
||||
|
||||
Comments are stripped before matching to avoid false positives from
|
||||
commented-out code. The cost of a false positive is only ~8KB flash.
|
||||
"""
|
||||
stack: list = [config]
|
||||
while stack:
|
||||
obj = stack.pop()
|
||||
if isinstance(obj, Lambda):
|
||||
src = obj.comment_remover(obj.value)
|
||||
if _SCANF_FLOAT_RE.search(src):
|
||||
return True
|
||||
elif isinstance(obj, dict):
|
||||
stack.extend(obj.values())
|
||||
elif isinstance(obj, list):
|
||||
stack.extend(obj)
|
||||
return False
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
CORE.data[KEY_ESP8266] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266
|
||||
@@ -181,6 +213,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_SERIAL): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
|
||||
}
|
||||
),
|
||||
set_core_data,
|
||||
@@ -201,16 +234,23 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
|
||||
cg.add_platformio_option(
|
||||
"extra_scripts",
|
||||
[
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
"pre:remove_float_scanf.py",
|
||||
"post:post_build.py",
|
||||
],
|
||||
)
|
||||
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
|
||||
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
|
||||
enable_scanf_float = True
|
||||
_LOGGER.warning(
|
||||
"Lambda uses scanf with a float format specifier; "
|
||||
"enabling scanf float support (~8KB flash)"
|
||||
)
|
||||
|
||||
extra_scripts = [
|
||||
"pre:testing_mode.py",
|
||||
"pre:exclude_updater.py",
|
||||
"pre:exclude_waveform.py",
|
||||
]
|
||||
if not enable_scanf_float:
|
||||
extra_scripts.append("pre:remove_float_scanf.py")
|
||||
extra_scripts.append("post:post_build.py")
|
||||
cg.add_platformio_option("extra_scripts", extra_scripts)
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -677,7 +677,6 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01);
|
||||
}
|
||||
out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0;
|
||||
control_out_buffer[4] = 0; // This byte should be cleared before setting values
|
||||
out_data->display_status = this->get_display_state() ? 1 : 0;
|
||||
this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
|
||||
out_data->health_mode = this->get_health_mode() ? 1 : 0;
|
||||
|
||||
@@ -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;
|
||||
@@ -68,6 +74,10 @@ void HttpRequestUpdate::update() {
|
||||
}
|
||||
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
|
||||
#ifdef USE_ESP32
|
||||
if (this->update_task_handle_ != nullptr) {
|
||||
ESP_LOGW(TAG, "Update check already in progress");
|
||||
return;
|
||||
}
|
||||
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
|
||||
#else
|
||||
this->update_task(this);
|
||||
@@ -77,134 +87,151 @@ 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]() {
|
||||
#ifdef USE_ESP32
|
||||
this_update->update_task_handle_ = nullptr;
|
||||
#endif
|
||||
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();
|
||||
|
||||
@@ -534,10 +534,11 @@ void LD2450Component::handle_periodic_data_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Store target info for zone target count
|
||||
this->target_info_[index].x = tx;
|
||||
this->target_info_[index].y = ty;
|
||||
this->target_info_[index].is_moving = is_moving;
|
||||
// Store target info for zone target count. Zero out untracked targets (td==0)
|
||||
// so stale coordinates don't produce ghost counts in count_targets_in_zone_().
|
||||
this->target_info_[index].x = (td > 0) ? tx : 0;
|
||||
this->target_info_[index].y = (td > 0) ? ty : 0;
|
||||
this->target_info_[index].is_moving = (td > 0) && is_moving;
|
||||
|
||||
} // End loop thru targets
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -81,18 +81,32 @@ def _get_data() -> LightData:
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def generate_gamma_table(gamma_correct: float) -> list[HexInt]:
|
||||
"""Generate a 256-entry uint16 gamma lookup table.
|
||||
|
||||
For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve
|
||||
the invariant that non-zero input always produces non-zero output. Without
|
||||
this, small brightness values (e.g. 1%) get quantized to exactly 0.0,
|
||||
which breaks zero_means_zero logic in FloatOutput.
|
||||
"""
|
||||
if gamma_correct > 0:
|
||||
return [
|
||||
HexInt(
|
||||
max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
|
||||
if i > 0
|
||||
else HexInt(0)
|
||||
)
|
||||
for i in range(256)
|
||||
]
|
||||
return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
|
||||
|
||||
|
||||
def _get_or_create_gamma_table(gamma_correct):
|
||||
data = _get_data()
|
||||
if gamma_correct in data.gamma_tables:
|
||||
return data.gamma_tables[gamma_correct]
|
||||
|
||||
if gamma_correct > 0:
|
||||
forward = [
|
||||
HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
|
||||
for i in range(256)
|
||||
]
|
||||
else:
|
||||
forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
|
||||
forward = generate_gamma_table(gamma_correct)
|
||||
|
||||
gamma_str = f"{gamma_correct}".replace(".", "_")
|
||||
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
|
||||
|
||||
@@ -154,6 +154,16 @@ class LightColorValues {
|
||||
}
|
||||
|
||||
/// Convert these light color values to an CWWW representation with the given parameters.
|
||||
///
|
||||
/// Note on gamma and constant_brightness: This method operates on the raw/internal channel
|
||||
/// values stored in this object. For cold_white_ and warm_white_ specifically, these
|
||||
/// may already be gamma-uncorrected when derived from a color_temperature value.
|
||||
/// For constant_brightness=false, additional gamma for the output can be applied after
|
||||
/// this method since gamma commutes with simple multiplication. For constant_brightness=true,
|
||||
/// the caller (LightState::current_values_as_cwww) must apply gamma to the individual
|
||||
/// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does
|
||||
/// not commute with gamma. See LightState::current_values_as_cwww() for the correct
|
||||
/// implementation.
|
||||
void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const {
|
||||
if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) {
|
||||
const float cw_level = this->cold_white_;
|
||||
|
||||
@@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f
|
||||
}
|
||||
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
|
||||
bool constant_brightness) {
|
||||
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness);
|
||||
this->current_values.as_rgb(red, green, blue);
|
||||
*red = this->gamma_correct_lut(*red);
|
||||
*green = this->gamma_correct_lut(*green);
|
||||
*blue = this->gamma_correct_lut(*blue);
|
||||
*cold_white = this->gamma_correct_lut(*cold_white);
|
||||
*warm_white = this->gamma_correct_lut(*warm_white);
|
||||
this->current_values_as_cwww(cold_white, warm_white, constant_brightness);
|
||||
}
|
||||
void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature,
|
||||
float *white_brightness) {
|
||||
@@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue,
|
||||
*white_brightness = this->gamma_correct_lut(*white_brightness);
|
||||
}
|
||||
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
|
||||
this->current_values.as_cwww(cold_white, warm_white, constant_brightness);
|
||||
*cold_white = this->gamma_correct_lut(*cold_white);
|
||||
*warm_white = this->gamma_correct_lut(*warm_white);
|
||||
if (!constant_brightness) {
|
||||
// Without constant_brightness, gamma commutes with simple multiplication:
|
||||
// gamma(white_level * cw) = gamma(white_level) * gamma(cw)
|
||||
// (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b))
|
||||
// so applying gamma after is mathematically equivalent and simpler.
|
||||
this->current_values.as_cwww(cold_white, warm_white, false);
|
||||
*cold_white = this->gamma_correct_lut(*cold_white);
|
||||
*warm_white = this->gamma_correct_lut(*warm_white);
|
||||
return;
|
||||
}
|
||||
|
||||
// For constant_brightness mode, gamma MUST be applied to the individual
|
||||
// channel values BEFORE the balancing formula (max/sum ratio), not after.
|
||||
//
|
||||
// Why: The cold_white_ and warm_white_ values stored in LightColorValues
|
||||
// are gamma-uncorrected (see transform_parameters_() which applies
|
||||
// gamma_uncorrect to the linear CW/WW fractions derived from color
|
||||
// temperature). Applying gamma_correct here recovers the original linear
|
||||
// fractions, which the constant_brightness formula then uses to distribute
|
||||
// power evenly. The max/sum formula ensures cold+warm PWM output sums to
|
||||
// a constant, keeping total power (and perceived brightness) the same
|
||||
// across all color temperatures.
|
||||
//
|
||||
// Applying gamma AFTER the formula would be incorrect because gamma is
|
||||
// nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced
|
||||
// ratio would be distorted, causing a severe brightness dip at mid-range
|
||||
// color temperatures.
|
||||
const auto &v = this->current_values;
|
||||
if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) {
|
||||
*cold_white = *warm_white = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const float cw_level = this->gamma_correct_lut(v.get_cold_white());
|
||||
const float ww_level = this->gamma_correct_lut(v.get_warm_white());
|
||||
const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness());
|
||||
const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero.
|
||||
*cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum;
|
||||
*warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum;
|
||||
}
|
||||
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
|
||||
auto traits = this->get_traits();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -56,6 +56,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
logger_ns = cg.esphome_ns.namespace("logger")
|
||||
@@ -323,19 +324,34 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
|
||||
async def to_code(config):
|
||||
baud_rate = config[CONF_BAUD_RATE]
|
||||
@coroutine_with_priority(CoroPriority.EARLY_INIT)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
baud_rate: int = config[CONF_BAUD_RATE]
|
||||
level = config[CONF_LEVEL]
|
||||
CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
|
||||
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
|
||||
tx_buffer_size = config[CONF_TX_BUFFER_SIZE]
|
||||
cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size)
|
||||
log = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
baud_rate,
|
||||
)
|
||||
if CORE.is_esp32:
|
||||
# Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early
|
||||
# so the constructor can allocate the buffer immediately, preventing a race
|
||||
# where another task logs before the buffer is initialized.
|
||||
task_log_buffer_size = 0
|
||||
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
elif CORE.is_host:
|
||||
task_log_buffer_size = 64 # Fixed 64 slots for host
|
||||
if task_log_buffer_size > 0:
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
log = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
baud_rate,
|
||||
task_log_buffer_size,
|
||||
)
|
||||
else:
|
||||
log = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
baud_rate,
|
||||
)
|
||||
if CORE.is_esp32 or CORE.is_host:
|
||||
cg.add(log.create_pthread_key())
|
||||
# set_uart_selection() must be called before pre_setup() because
|
||||
# pre_setup() switches on uart_ to decide which hardware to initialize
|
||||
@@ -347,24 +363,28 @@ async def to_code(config):
|
||||
HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]]
|
||||
)
|
||||
)
|
||||
# pre_setup() must be called before init_log_buffer() because
|
||||
# init_log_buffer() calls disable_loop() which may log at VV level,
|
||||
# and global_logger must be set before any logging occurs.
|
||||
# pre_setup() sets global_logger and must run before any other code
|
||||
# that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV).
|
||||
cg.add(log.pre_setup())
|
||||
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
if task_log_buffer_size > 0:
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
cg.add(log.init_log_buffer(task_log_buffer_size))
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("MPSC_PBUF", True)
|
||||
elif CORE.is_host:
|
||||
cg.add(log.create_pthread_key())
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host
|
||||
|
||||
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
|
||||
cg.add(log.set_log_level(initial_level))
|
||||
|
||||
# Schedule the rest of logger setup at DIAGNOSTICS priority, after
|
||||
# Application is constructed (CORE priority) but before most components.
|
||||
CORE.add_job(_late_logger_init, config)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
|
||||
async def _late_logger_init(config: ConfigType) -> None:
|
||||
"""Finish logger setup after Application is constructed."""
|
||||
log = await cg.get_variable(config[CONF_ID])
|
||||
level = config[CONF_LEVEL]
|
||||
baud_rate: int = config[CONF_BAUD_RATE]
|
||||
if CORE.using_zephyr:
|
||||
task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0)
|
||||
if task_log_buffer_size > 0:
|
||||
zephyr_add_prj_conf("MPSC_PBUF", True)
|
||||
|
||||
# Enable runtime tag levels if logs are configured or explicitly enabled
|
||||
logs_config = config[CONF_LOGS]
|
||||
if logs_config or config[CONF_RUNTIME_TAG_LEVELS]:
|
||||
|
||||
@@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) {
|
||||
return this->current_level_;
|
||||
}
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) {
|
||||
#else
|
||||
Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) {
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->main_task_ = xTaskGetCurrentTaskHandle();
|
||||
#elif defined(USE_ZEPHYR)
|
||||
this->main_task_ = k_current_get();
|
||||
#elif defined(USE_HOST)
|
||||
this->main_thread_ = pthread_self();
|
||||
this->main_thread_ = pthread_self();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
// Host uses slot count instead of byte size
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
||||
|
||||
#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
// Start with loop disabled when using task buffer
|
||||
// The loop will be enabled automatically when messages arrive
|
||||
// Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_()
|
||||
this->disable_loop_when_buffer_empty_();
|
||||
this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size);
|
||||
// Note: we don't disable loop here because the component isn't registered with App yet.
|
||||
// The loop self-disables on its first iteration when it finds no messages to process.
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
void Logger::loop() {
|
||||
|
||||
@@ -143,9 +143,10 @@ enum UARTSelection : uint8_t {
|
||||
*/
|
||||
class Logger final : public Component {
|
||||
public:
|
||||
explicit Logger(uint32_t baud_rate);
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void init_log_buffer(size_t total_buffer_size);
|
||||
explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size);
|
||||
#else
|
||||
explicit Logger(uint32_t baud_rate);
|
||||
#endif
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
void loop() override;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -597,173 +597,173 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
|
||||
|
||||
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
||||
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||
{ // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task
|
||||
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
||||
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||
|
||||
if (output_transfer_buffer == nullptr) {
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
|
||||
if (output_transfer_buffer == nullptr) {
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
|
||||
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
}
|
||||
|
||||
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||
|
||||
bool sent_finished = false;
|
||||
|
||||
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
|
||||
FixedVector<SourceSpeaker *> speakers_with_data;
|
||||
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
speakers_with_data.init(this_mixer->source_speakers_.size());
|
||||
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
|
||||
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
||||
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
|
||||
break;
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
}
|
||||
|
||||
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
|
||||
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
|
||||
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
||||
|
||||
const uint32_t output_frames_free =
|
||||
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||
|
||||
speakers_with_data.clear();
|
||||
transfer_buffers_with_data.clear();
|
||||
bool sent_finished = false;
|
||||
|
||||
for (auto &speaker : this_mixer->source_speakers_) {
|
||||
if (speaker->is_running() && !speaker->get_pause_state()) {
|
||||
// Speaker is running and not paused, so it possibly can provide audio data
|
||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
||||
if (transfer_buffer.use_count() == 0) {
|
||||
// No transfer buffer allocated, so skip processing this speaker
|
||||
continue;
|
||||
}
|
||||
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
|
||||
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
|
||||
FixedVector<SourceSpeaker *> speakers_with_data;
|
||||
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
speakers_with_data.init(this_mixer->source_speakers_.size());
|
||||
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
|
||||
|
||||
if (transfer_buffer->available() > 0) {
|
||||
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
|
||||
transfer_buffers_with_data.push_back(transfer_buffer);
|
||||
speakers_with_data.push_back(speaker);
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
||||
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
|
||||
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
|
||||
|
||||
const uint32_t output_frames_free =
|
||||
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
||||
|
||||
speakers_with_data.clear();
|
||||
transfer_buffers_with_data.clear();
|
||||
|
||||
for (auto &speaker : this_mixer->source_speakers_) {
|
||||
if (speaker->is_running() && !speaker->get_pause_state()) {
|
||||
// Speaker is running and not paused, so it possibly can provide audio data
|
||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
||||
if (transfer_buffer.use_count() == 0) {
|
||||
// No transfer buffer allocated, so skip processing this speaker
|
||||
continue;
|
||||
}
|
||||
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
|
||||
|
||||
if (transfer_buffer->available() > 0) {
|
||||
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
|
||||
transfer_buffers_with_data.push_back(transfer_buffer);
|
||||
speakers_with_data.push_back(speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transfer_buffers_with_data.empty()) {
|
||||
// No audio available for transferring, block task temporarily
|
||||
delay(TASK_DELAY_MS);
|
||||
continue;
|
||||
}
|
||||
if (transfer_buffers_with_data.empty()) {
|
||||
// No audio available for transferring, block task temporarily
|
||||
delay(TASK_DELAY_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t frames_to_mix = output_frames_free;
|
||||
uint32_t frames_to_mix = output_frames_free;
|
||||
|
||||
if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) {
|
||||
// Only one speaker has audio data, just copy samples over
|
||||
if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) {
|
||||
// Only one speaker has audio data, just copy samples over
|
||||
|
||||
audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();
|
||||
audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();
|
||||
|
||||
if (active_stream_info.get_sample_rate() ==
|
||||
this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
|
||||
// Speaker's sample rate matches the output speaker's, copy directly
|
||||
if (active_stream_info.get_sample_rate() ==
|
||||
this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
|
||||
// Speaker's sample rate matches the output speaker's, copy directly
|
||||
|
||||
const uint32_t frames_available_in_buffer =
|
||||
active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available());
|
||||
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
|
||||
copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), active_stream_info,
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
const uint32_t frames_available_in_buffer =
|
||||
active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available());
|
||||
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
|
||||
copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()),
|
||||
active_stream_info, reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
// Set playback delay for newly contributing source
|
||||
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[0]->playback_delay_frames_.store(
|
||||
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
|
||||
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
|
||||
// Set playback delay for newly contributing source
|
||||
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[0]->playback_delay_frames_.store(
|
||||
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
|
||||
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Update source speaker pending frames
|
||||
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
} else {
|
||||
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
|
||||
if (!this_mixer->output_speaker_->is_stopped()) {
|
||||
if (!sent_finished) {
|
||||
this_mixer->output_speaker_->finish();
|
||||
sent_finished = true; // Avoid repeatedly sending the finish command
|
||||
}
|
||||
} else {
|
||||
// Speaker has finished writing the current audio, update the stream information and restart the speaker
|
||||
this_mixer->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_,
|
||||
active_stream_info.get_sample_rate());
|
||||
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
||||
this_mixer->output_speaker_->start();
|
||||
// Reset pipeline frame count since we're starting fresh with a new sample rate
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
sent_finished = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Determine how many frames to mix
|
||||
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(
|
||||
transfer_buffers_with_data[i]->available());
|
||||
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
|
||||
}
|
||||
int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start());
|
||||
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
|
||||
|
||||
// Mix two streams together
|
||||
for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) {
|
||||
mix_audio_samples(primary_buffer, primary_stream_info,
|
||||
reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()),
|
||||
speakers_with_data[i]->get_audio_stream_info(),
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
if (i != transfer_buffers_with_data.size() - 1) {
|
||||
// Need to mix more streams together, point primary buffer and stream info to the already mixed output
|
||||
primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end());
|
||||
primary_stream_info = this_mixer->audio_stream_info_.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Update source speaker pending frames
|
||||
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
||||
// Get current pipeline depth for delay calculation (before incrementing)
|
||||
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count
|
||||
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
|
||||
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
// Set playback delay for newly contributing sources
|
||||
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
|
||||
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
transfer_buffers_with_data[i]->decrease_buffer_length(
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
|
||||
}
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count (once, not per source)
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
} else {
|
||||
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
|
||||
if (!this_mixer->output_speaker_->is_stopped()) {
|
||||
if (!sent_finished) {
|
||||
this_mixer->output_speaker_->finish();
|
||||
sent_finished = true; // Avoid repeatedly sending the finish command
|
||||
}
|
||||
} else {
|
||||
// Speaker has finished writing the current audio, update the stream information and restart the speaker
|
||||
this_mixer->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_,
|
||||
active_stream_info.get_sample_rate());
|
||||
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
||||
this_mixer->output_speaker_->start();
|
||||
// Reset pipeline frame count since we're starting fresh with a new sample rate
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
sent_finished = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Determine how many frames to mix
|
||||
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
const uint32_t frames_available_in_buffer =
|
||||
speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available());
|
||||
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
|
||||
}
|
||||
int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start());
|
||||
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
|
||||
|
||||
// Mix two streams together
|
||||
for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) {
|
||||
mix_audio_samples(primary_buffer, primary_stream_info,
|
||||
reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()),
|
||||
speakers_with_data[i]->get_audio_stream_info(),
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
if (i != transfer_buffers_with_data.size() - 1) {
|
||||
// Need to mix more streams together, point primary buffer and stream info to the already mixed output
|
||||
primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end());
|
||||
primary_stream_info = this_mixer->audio_stream_info_.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Get current pipeline depth for delay calculation (before incrementing)
|
||||
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
|
||||
|
||||
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
|
||||
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
// Set playback delay for newly contributing sources
|
||||
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
|
||||
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
transfer_buffers_with_data[i]->decrease_buffer_length(
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
|
||||
}
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count (once, not per source)
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||
}
|
||||
|
||||
// Reset pipeline frame count since the task is stopping
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
|
||||
output_transfer_buffer.reset();
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
|
||||
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt";
|
||||
|
||||
// Maximum number of MQTT component resends per loop iteration.
|
||||
// Limits work to avoid triggering the task watchdog on reconnect.
|
||||
static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8;
|
||||
|
||||
// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
|
||||
PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
|
||||
"Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
|
||||
@@ -396,9 +400,16 @@ void MQTTClientComponent::loop() {
|
||||
this->resubscribe_subscriptions_();
|
||||
|
||||
// Process pending resends for all MQTT components centrally
|
||||
// This is more efficient than each component polling in its own loop
|
||||
for (MQTTComponent *component : this->children_) {
|
||||
component->process_resend();
|
||||
// Limit work per loop iteration to avoid triggering task WDT on reconnect
|
||||
{
|
||||
uint8_t resend_count = 0;
|
||||
for (MQTTComponent *component : this->children_) {
|
||||
if (component->is_resend_pending()) {
|
||||
component->process_resend();
|
||||
if (++resend_count >= MAX_RESENDS_PER_LOOP)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -147,6 +147,9 @@ class MQTTComponent : public Component {
|
||||
/// Internal method for the MQTT client base to schedule a resend of the state on reconnect.
|
||||
void schedule_resend_state();
|
||||
|
||||
/// Check if a resend is pending (called by MQTTClientComponent to rate-limit work)
|
||||
bool is_resend_pending() const { return this->resend_state_; }
|
||||
|
||||
/// Process pending resend if needed (called by MQTTClientComponent)
|
||||
void process_resend();
|
||||
|
||||
|
||||
@@ -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...> {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <openthread/instance.h>
|
||||
#include <openthread/thread.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -28,6 +29,8 @@ class OpenThreadComponent : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::WIFI; }
|
||||
|
||||
bool is_connected() const { return this->connected_; }
|
||||
/// Returns true once esp_openthread_init() has completed and the OT lock is usable.
|
||||
bool is_lock_initialized() const { return this->lock_initialized_; }
|
||||
network::IPAddresses get_ip_addresses();
|
||||
std::optional<otIp6Address> get_omr_address();
|
||||
void ot_main();
|
||||
@@ -51,6 +54,7 @@ class OpenThreadComponent : public Component {
|
||||
uint32_t poll_period_{0};
|
||||
#endif
|
||||
std::optional<int8_t> output_power_{};
|
||||
std::atomic<bool> lock_initialized_{false};
|
||||
bool teardown_started_{false};
|
||||
bool teardown_complete_{false};
|
||||
bool connected_{false};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esp_openthread_lock.h"
|
||||
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -81,6 +82,9 @@ void OpenThreadComponent::ot_main() {
|
||||
// Initialize the OpenThread stack
|
||||
// otLoggingSetLevel(OT_LOG_LEVEL_DEBG);
|
||||
ESP_ERROR_CHECK(esp_openthread_init(&config));
|
||||
// Mark lock as initialized so InstanceLock callers know it's safe to acquire.
|
||||
// Must be set after esp_openthread_init() which creates the internal semaphore.
|
||||
this->lock_initialized_ = true;
|
||||
// Fetch OT instance once to avoid repeated call into OT stack
|
||||
otInstance *instance = esp_openthread_get_instance();
|
||||
|
||||
@@ -180,7 +184,8 @@ void OpenThreadComponent::ot_main() {
|
||||
|
||||
esp_openthread_launch_mainloop();
|
||||
|
||||
// Clean up
|
||||
// Clean up - reset lock flag before deinit destroys the semaphore
|
||||
this->lock_initialized_ = false;
|
||||
esp_openthread_deinit();
|
||||
esp_openthread_netif_glue_deinit();
|
||||
esp_netif_destroy(openthread_netif);
|
||||
@@ -210,6 +215,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() {
|
||||
otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); }
|
||||
|
||||
std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
|
||||
if (!global_openthread_component->is_lock_initialized()) {
|
||||
return {};
|
||||
}
|
||||
if (esp_openthread_lock_acquire(delay)) {
|
||||
return InstanceLock();
|
||||
}
|
||||
@@ -217,6 +225,18 @@ std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
|
||||
}
|
||||
|
||||
InstanceLock InstanceLock::acquire() {
|
||||
// Wait for the lock to be created by ot_main() before attempting to acquire it.
|
||||
// esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init().
|
||||
constexpr uint32_t lock_init_timeout_ms = 10000;
|
||||
uint32_t start = millis();
|
||||
while (!global_openthread_component->is_lock_initialized()) {
|
||||
if (millis() - start > lock_init_timeout_ms) {
|
||||
ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms);
|
||||
abort();
|
||||
}
|
||||
delay(10);
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
while (!esp_openthread_lock_acquire(100)) {
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -95,10 +95,6 @@ void PMSX003Component::loop() {
|
||||
// Just go ahead and read stuff
|
||||
break;
|
||||
}
|
||||
} else if (now - this->last_update_ < this->update_interval_) {
|
||||
// Otherwise just leave the sensor powered up and come back when we hit the update
|
||||
// time
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - this->last_transmission_ >= 500) {
|
||||
@@ -114,10 +110,11 @@ void PMSX003Component::loop() {
|
||||
this->read_byte(&this->data_[this->data_index_]);
|
||||
auto check = this->check_byte_();
|
||||
if (!check.has_value()) {
|
||||
// finished
|
||||
this->parse_data_();
|
||||
if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) {
|
||||
this->parse_data_();
|
||||
this->last_update_ = now;
|
||||
}
|
||||
this->data_index_ = 0;
|
||||
this->last_update_ = now;
|
||||
} else if (!*check) {
|
||||
// wrong data
|
||||
this->data_index_ = 0;
|
||||
@@ -138,7 +135,7 @@ optional<bool> PMSX003Component::check_byte_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1);
|
||||
ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -5,6 +5,30 @@
|
||||
namespace esphome {
|
||||
namespace sdl {
|
||||
|
||||
int Sdl::get_width() {
|
||||
switch (this->rotation_) {
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||
return this->get_height_internal();
|
||||
case display::DISPLAY_ROTATION_0_DEGREES:
|
||||
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||
default:
|
||||
return this->get_width_internal();
|
||||
}
|
||||
}
|
||||
|
||||
int Sdl::get_height() {
|
||||
switch (this->rotation_) {
|
||||
case display::DISPLAY_ROTATION_0_DEGREES:
|
||||
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||
return this->get_height_internal();
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||
default:
|
||||
return this->get_width_internal();
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::setup() {
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
|
||||
@@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
x = this->width_ - x - 1;
|
||||
y = this->height_ - y - 1;
|
||||
} else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) {
|
||||
auto tmp = x;
|
||||
x = this->width_ - y - 1;
|
||||
y = tmp;
|
||||
} else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) {
|
||||
auto tmp = y;
|
||||
y = this->height_ - x - 1;
|
||||
x = tmp;
|
||||
}
|
||||
|
||||
SDL_Rect rect{x, y, 1, 1};
|
||||
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
|
||||
SDL_UpdateTexture(this->texture_, &rect, &data, 2);
|
||||
|
||||
@@ -33,8 +33,8 @@ class Sdl : public display::Display {
|
||||
this->pos_x_ = pos_x;
|
||||
this->pos_y_ = pos_y;
|
||||
}
|
||||
int get_width() override { return this->width_; }
|
||||
int get_height() override { return this->height_; }
|
||||
int get_width() override;
|
||||
int get_height() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
void dump_config() override { LOG_DISPLAY("", "SDL", this); }
|
||||
void add_key_listener(int32_t keycode, std::function<void(bool)> &&callback) {
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -44,20 +44,27 @@ def validate_sensors(config):
|
||||
return config
|
||||
|
||||
|
||||
GAS_SENSOR = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_,
|
||||
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_,
|
||||
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_,
|
||||
cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_,
|
||||
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
|
||||
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_,
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
def _gas_sensor_schema(index_offset_default: int):
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_INDEX_OFFSET, default=index_offset_default
|
||||
): cv.int_,
|
||||
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_,
|
||||
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_,
|
||||
cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_,
|
||||
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
|
||||
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_,
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
VOC_SENSOR = _gas_sensor_schema(100)
|
||||
NOX_SENSOR = _gas_sensor_schema(1)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -68,13 +75,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(GAS_SENSOR),
|
||||
).extend(VOC_SENSOR),
|
||||
cv.Optional(CONF_NOX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(GAS_SENSOR),
|
||||
).extend(NOX_SENSOR),
|
||||
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_COMPENSATION): cv.Schema(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "sht4x.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -9,14 +10,12 @@ static const char *const TAG = "sht4x";
|
||||
static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
|
||||
static const uint8_t SERIAL_NUMBER_COMMAND = 0x89;
|
||||
|
||||
void SHT4XComponent::start_heater_() {
|
||||
uint8_t cmd[] = {this->heater_command_};
|
||||
|
||||
ESP_LOGD(TAG, "Heater turning on");
|
||||
if (this->write(cmd, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to turn on heater"));
|
||||
}
|
||||
}
|
||||
// Conversion constants from SHT4x datasheet
|
||||
static constexpr float TEMPERATURE_OFFSET = -45.0f;
|
||||
static constexpr float TEMPERATURE_SPAN = 175.0f;
|
||||
static constexpr float HUMIDITY_OFFSET = -6.0f;
|
||||
static constexpr float HUMIDITY_SPAN = 125.0f;
|
||||
static constexpr float RAW_MAX = 65535.0f;
|
||||
|
||||
void SHT4XComponent::read_serial_number_() {
|
||||
uint16_t buffer[2];
|
||||
@@ -39,8 +38,8 @@ void SHT4XComponent::setup() {
|
||||
this->read_serial_number_();
|
||||
|
||||
if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) {
|
||||
uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
|
||||
ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval);
|
||||
this->heater_interval_ = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
|
||||
ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_);
|
||||
|
||||
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
|
||||
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
|
||||
@@ -62,8 +61,6 @@ void SHT4XComponent::setup() {
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
|
||||
|
||||
this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,19 +103,27 @@ void SHT4XComponent::update() {
|
||||
// Evaluate and publish measurements
|
||||
if (this->temp_sensor_ != nullptr) {
|
||||
// Temp is contained in the first result word
|
||||
float sensor_value_temp = buffer[0];
|
||||
float temp = -45 + 175 * sensor_value_temp / 65535;
|
||||
|
||||
float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast<float>(buffer[0]) / RAW_MAX;
|
||||
this->temp_sensor_->publish_state(temp);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
// Relative humidity is in the second result word
|
||||
float sensor_value_rh = buffer[1];
|
||||
float rh = -6 + 125 * sensor_value_rh / 65535;
|
||||
|
||||
float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast<float>(buffer[1]) / RAW_MAX;
|
||||
this->humidity_sensor_->publish_state(rh);
|
||||
}
|
||||
|
||||
// Fire heater after measurement to maximize cooldown time before the next reading.
|
||||
// The heater command produces a measurement that we don't need (datasheet 4.9).
|
||||
if (this->heater_interval_ > 0) {
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_heater_millis_ >= this->heater_interval_) {
|
||||
ESP_LOGD(TAG, "Heater turning on");
|
||||
if (this->write_command(this->heater_command_)) {
|
||||
this->last_heater_millis_ = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
SHT4XHEATERTIME heater_time_;
|
||||
float duty_cycle_;
|
||||
|
||||
void start_heater_();
|
||||
void read_serial_number_();
|
||||
uint8_t heater_command_;
|
||||
uint32_t heater_interval_{0};
|
||||
uint32_t last_heater_millis_{0};
|
||||
uint32_t serial_number_;
|
||||
|
||||
sensor::Sensor *temp_sensor_{nullptr};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -38,14 +38,18 @@ void SX127x::write_register_(uint8_t reg, uint8_t value) {
|
||||
void SX127x::read_fifo_(std::vector<uint8_t> &packet) {
|
||||
this->enable();
|
||||
this->write_byte(REG_FIFO & 0x7F);
|
||||
this->read_array(packet.data(), packet.size());
|
||||
for (auto &byte : packet) {
|
||||
byte = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void SX127x::write_fifo_(const std::vector<uint8_t> &packet) {
|
||||
this->enable();
|
||||
this->write_byte(REG_FIFO | 0x80);
|
||||
this->write_array(packet.data(), packet.size());
|
||||
for (const auto &byte : packet) {
|
||||
this->transfer_byte(byte);
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -606,6 +606,16 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu
|
||||
}
|
||||
|
||||
void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) {
|
||||
// Always cancel max-runtime timers and clear exceeded flags when transitioning to idle/off,
|
||||
// even if supplemental_action_ is already idle (early-return path). This prevents a stale
|
||||
// heating_max_runtime_exceeded_ flag from triggering supplemental on the next heating cycle
|
||||
// when HEATING_MAX_RUN_TIME fires while the main action is already IDLE.
|
||||
if (action == climate::CLIMATE_ACTION_OFF || action == climate::CLIMATE_ACTION_IDLE) {
|
||||
this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME);
|
||||
this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME);
|
||||
this->cooling_max_runtime_exceeded_ = false;
|
||||
this->heating_max_runtime_exceeded_ = false;
|
||||
}
|
||||
// setup_complete_ helps us ensure an action is called immediately after boot
|
||||
if ((action == this->supplemental_action_) && this->setup_complete_) {
|
||||
// already in target mode
|
||||
@@ -975,8 +985,10 @@ void ThermostatClimate::cooling_on_timer_callback_() {
|
||||
void ThermostatClimate::fan_mode_timer_callback_() {
|
||||
ESP_LOGVV(TAG, "fan_mode timer expired");
|
||||
this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON));
|
||||
if (this->supports_fan_only_action_uses_fan_mode_timer_)
|
||||
if (this->supports_fan_only_action_uses_fan_mode_timer_) {
|
||||
this->switch_to_action_(this->compute_action_());
|
||||
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::fanning_off_timer_callback_() {
|
||||
|
||||
@@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = {
|
||||
|
||||
def _load_tzdata(iana_key: str) -> bytes | None:
|
||||
# From https://tzdata.readthedocs.io/en/latest/#examples
|
||||
if not iana_key:
|
||||
return None
|
||||
try:
|
||||
package_loc, resource = iana_key.rsplit("/", 1)
|
||||
except ValueError:
|
||||
return None
|
||||
package = "tzdata.zoneinfo." + package_loc.replace("/", ".")
|
||||
# Handle top-level timezone entries like "UTC", "GMT"
|
||||
package = "tzdata.zoneinfo"
|
||||
resource = iana_key
|
||||
else:
|
||||
package = "tzdata.zoneinfo." + package_loc.replace("/", ".")
|
||||
|
||||
try:
|
||||
return (resources.files(package) / resource).read_bytes()
|
||||
except (FileNotFoundError, ModuleNotFoundError):
|
||||
except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError):
|
||||
return None
|
||||
|
||||
|
||||
@@ -279,13 +284,23 @@ def validate_tz(value: str) -> str:
|
||||
tzfile = _load_tzdata(value)
|
||||
if tzfile is not None:
|
||||
value = _extract_tz_string(tzfile)
|
||||
is_iana = True
|
||||
else:
|
||||
is_iana = False
|
||||
|
||||
# Validate that the POSIX TZ string is parseable (skip empty strings)
|
||||
if value:
|
||||
try:
|
||||
parse_posix_tz_python(value)
|
||||
except ValueError as e:
|
||||
raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e
|
||||
if is_iana:
|
||||
raise cv.Invalid(f"Invalid POSIX timezone string '{value}': {e}") from e
|
||||
raise cv.Invalid(
|
||||
f"Invalid POSIX timezone string '{value}': {e}. "
|
||||
f"If you meant to use an IANA timezone, check the list of valid "
|
||||
f"timezones at "
|
||||
f"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
|
||||
) from e
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ namespace tormatic {
|
||||
|
||||
static const char *const TAG = "tormatic.cover";
|
||||
|
||||
// Time to poll the UART when flushing after desync. At 9600 baud, a full
|
||||
// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived.
|
||||
static constexpr uint32_t DRAIN_TIMEOUT_MS = 15;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void Tormatic::setup() {
|
||||
@@ -255,32 +259,51 @@ void Tormatic::stop_at_target_() {
|
||||
// Read a GateStatus from the unit. The unit only sends messages in response to
|
||||
// status requests or commands, so a message needs to be sent first.
|
||||
optional<GateStatus> Tormatic::read_gate_status_() {
|
||||
if (this->available() < sizeof(MessageHeader)) {
|
||||
if (!this->pending_hdr_) {
|
||||
if (this->available() < sizeof(MessageHeader)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this->pending_hdr_ = this->read_data_<MessageHeader>();
|
||||
if (!this->pending_hdr_) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Sanity check: valid messages have small payloads (3-4 bytes). A large
|
||||
// or impossible payload_size means the stream is out of sync (corrupted
|
||||
// byte, dropped data, etc.). Flush the buffer so we can resync on the
|
||||
// next request/response cycle.
|
||||
if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) {
|
||||
ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size());
|
||||
this->pending_hdr_.reset();
|
||||
this->drain_rx_();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all payload bytes to arrive before processing.
|
||||
if (this->available() < this->pending_hdr_->payload_size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto o_hdr = this->read_data_<MessageHeader>();
|
||||
if (!o_hdr) {
|
||||
ESP_LOGE(TAG, "Timeout reading message header");
|
||||
return {};
|
||||
}
|
||||
auto hdr = o_hdr.value();
|
||||
auto hdr = *this->pending_hdr_;
|
||||
this->pending_hdr_.reset();
|
||||
|
||||
switch (hdr.type) {
|
||||
case STATUS: {
|
||||
if (hdr.payload_size() != sizeof(StatusReply)) {
|
||||
ESP_LOGE(TAG, "Header specifies payload size %d but size of StatusReply is %d", hdr.payload_size(),
|
||||
sizeof(StatusReply));
|
||||
this->drain_rx_(hdr.payload_size());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Read a StatusReply requested by update().
|
||||
auto o_status = this->read_data_<StatusReply>();
|
||||
if (!o_status) {
|
||||
return {};
|
||||
}
|
||||
auto status = o_status.value();
|
||||
|
||||
return status.state;
|
||||
return o_status->state;
|
||||
}
|
||||
|
||||
case COMMAND:
|
||||
@@ -343,16 +366,24 @@ template<typename T> optional<T> Tormatic::read_data_() {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Drain up to n amount of bytes from the uart rx buffer.
|
||||
// Drain bytes from the uart rx buffer. When n > 0, drain exactly n bytes
|
||||
// (caller must ensure they are available). When n == 0, poll for 15ms to
|
||||
// guarantee a full packet time at 9600 baud has elapsed, consuming any
|
||||
// bytes still in transit.
|
||||
void Tormatic::drain_rx_(uint16_t n) {
|
||||
uint8_t data;
|
||||
uint16_t count = 0;
|
||||
while (this->available()) {
|
||||
this->read_byte(&data);
|
||||
count++;
|
||||
|
||||
if (n > 0 && count >= n) {
|
||||
return;
|
||||
if (n > 0) {
|
||||
for (uint16_t i = 0; i < n; i++) {
|
||||
if (!this->read_byte(&data)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < DRAIN_TIMEOUT_MS) {
|
||||
if (this->available()) {
|
||||
this->read_byte(&data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom
|
||||
void handle_gate_status_(GateStatus s);
|
||||
|
||||
uint32_t seq_tx_{0};
|
||||
optional<MessageHeader> pending_hdr_{};
|
||||
|
||||
GateStatus current_status_{PAUSED};
|
||||
|
||||
|
||||
@@ -30,12 +30,17 @@ enum UARTDirection {
|
||||
const LogString *parity_to_str(UARTParityOptions parity);
|
||||
|
||||
/// Result of a flush() call.
|
||||
// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro.
|
||||
// Save and restore around the enum to avoid collisions with our scoped enum value.
|
||||
#pragma push_macro("SUCCESS")
|
||||
#undef SUCCESS
|
||||
enum class FlushResult {
|
||||
SUCCESS, ///< Confirmed: all bytes left the TX FIFO.
|
||||
TIMEOUT, ///< Confirmed: timed out before TX completed.
|
||||
FAILED, ///< Confirmed: driver or hardware error.
|
||||
ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed.
|
||||
};
|
||||
#pragma pop_macro("SUCCESS")
|
||||
|
||||
class UARTComponent {
|
||||
public:
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_private/gpio.h"
|
||||
#include "soc/gpio_num.h"
|
||||
#include "soc/uart_pins.h"
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
#include "esphome/core/application.h"
|
||||
@@ -21,6 +23,20 @@ namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart.idf";
|
||||
|
||||
/// Check if a pin number matches one of the default UART0 GPIO pins.
|
||||
/// These pins may have residual IOMUX state from the ROM bootloader that
|
||||
/// must be cleared before UART reconfiguration.
|
||||
///
|
||||
/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix,
|
||||
/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only
|
||||
/// calls gpio_input_enable() which does NOT clear the IOMUX function select.
|
||||
/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later
|
||||
/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active,
|
||||
/// causing TX data to loop back into RX on the same pin.
|
||||
static constexpr bool is_default_uart0_pin(int8_t pin_num) {
|
||||
return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM;
|
||||
}
|
||||
|
||||
uart_config_t IDFUARTComponent::get_config_() {
|
||||
uart_parity_t parity = UART_PARITY_DISABLE;
|
||||
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
|
||||
@@ -131,6 +147,33 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
// uart_param_config must be called after uart_driver_install and before any
|
||||
// other uart_set_*() calls. The driver installation resets the UART peripheral
|
||||
// registers to their default state, overwriting any previously configured baud
|
||||
// rate or framing settings. Calling uart_param_config here ensures the requested
|
||||
// settings are applied after the reset and before pin routing, inversion, and
|
||||
// threshold configuration.
|
||||
uart_config_t uart_config = this->get_config_();
|
||||
err = uart_param_config(this->uart_num_, &uart_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
// Clear residual IOMUX function on UART0 default pins left by the ROM bootloader.
|
||||
// See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug.
|
||||
if (is_default_uart0_pin(tx)) {
|
||||
gpio_func_sel(static_cast<gpio_num_t>(tx), PIN_FUNC_GPIO);
|
||||
}
|
||||
if (is_default_uart0_pin(rx)) {
|
||||
gpio_func_sel(static_cast<gpio_num_t>(rx), PIN_FUNC_GPIO);
|
||||
}
|
||||
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
@@ -146,10 +189,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
uint32_t invert = 0;
|
||||
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
|
||||
invert |= UART_SIGNAL_TXD_INV;
|
||||
@@ -189,22 +228,15 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
|
||||
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
|
||||
err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install()
|
||||
err = uart_set_mode(this->uart_num_, mode);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
uart_config_t uart_config = this->get_config_();
|
||||
err = uart_param_config(this->uart_num_, &uart_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// Register ISR callback to wake the main loop when UART data arrives.
|
||||
// The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to
|
||||
@@ -299,6 +331,9 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
}
|
||||
|
||||
bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
if (len == 0) {
|
||||
return false;
|
||||
}
|
||||
size_t length_to_read = len;
|
||||
int32_t read_len = 0;
|
||||
if (!this->check_read_timeout_(len))
|
||||
@@ -306,11 +341,10 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
if (this->has_peek_) {
|
||||
length_to_read--;
|
||||
*data = this->peek_byte_;
|
||||
data++;
|
||||
this->has_peek_ = false;
|
||||
}
|
||||
if (length_to_read > 0)
|
||||
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
read_len = uart_read_bytes(this->uart_num_, data + (len - length_to_read), length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||
|
||||
@@ -235,16 +235,14 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) {
|
||||
}
|
||||
if (!this->check_read_timeout_(len))
|
||||
return false;
|
||||
uint8_t *data_ptr = data;
|
||||
size_t length_to_read = len;
|
||||
if (this->has_peek_) {
|
||||
length_to_read--;
|
||||
*data_ptr = this->peek_byte_;
|
||||
data_ptr++;
|
||||
*data = this->peek_byte_;
|
||||
this->has_peek_ = false;
|
||||
}
|
||||
if (length_to_read > 0) {
|
||||
int sz = ::read(this->file_descriptor_, data_ptr, length_to_read);
|
||||
int sz = ::read(this->file_descriptor_, data + (len - length_to_read), length_to_read);
|
||||
if (sz == -1) {
|
||||
this->update_error_(strerror(errno));
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user