Compare commits

..
19 changed files with 143 additions and 123 deletions

No files matched your search

@@ -1,6 +1,7 @@
import logging
import esphome.codegen as cg
from esphome.components.const import CONF_ON_NUMERIC_COMPARISON_REQUEST
from esphome.components.nrf52.const import BOOTLOADER_ADAFRUIT
from esphome.components.ota import (
BASE_OTA_SCHEMA,
@@ -12,10 +13,7 @@ from esphome.components.ota import (
)
from esphome.components.zephyr import zephyr_data
from esphome.components.zephyr.const import KEY_BOOTLOADER
from esphome.components.zephyr_ble_server import (
CONF_ON_NUMERIC_COMPARISON_REQUEST,
DOMAIN as ZEPHYR_BLE_SERVER_DOMAIN,
)
from esphome.components.zephyr_ble_server import DOMAIN as ZEPHYR_BLE_SERVER_DOMAIN
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_ERROR, Framework
from esphome.core import CoroPriority, coroutine_with_priority
+1 -2
View File
@@ -5,6 +5,7 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components.const import CONF_ACCEPT, CONF_ON_NUMERIC_COMPARISON_REQUEST
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import (
@@ -101,10 +102,8 @@ BLERemoveBondAction = ble_client_ns.class_(
)
CONF_PASSKEY = "passkey"
CONF_ACCEPT = "accept"
CONF_ON_PASSKEY_REQUEST = "on_passkey_request"
CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_AUTO_CONNECT = "auto_connect"
MULTI_CONF = True
+2
View File
@@ -8,6 +8,7 @@ BYTE_ORDER_BIG = "big_endian"
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_ACCEPT = "accept"
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
CONF_ALLOWED_IPS = "allowed_ips"
@@ -58,6 +59,7 @@ CONF_MAX_PACKET_SIZE = "max_packet_size"
CONF_MODEM = "modem"
CONF_MODULATION = "modulation"
CONF_NOX_INDEX = "nox_index"
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive"
CONF_ON_SCAN_END = "on_scan_end"
+73
View File
@@ -25,16 +25,86 @@ extern "C" {
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <cstring>
#ifdef USE_ARDUINO
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
// Without this, esp_bt_controller_init() fails with ESP_ERR_INVALID_STATE.
extern "C" bool btInUse() { return true; } // NOLINT(readability-identifier-naming)
#endif
#if !defined(CONFIG_BT_CONTROLLER_DISABLED) && \
(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3))
// The ESP32-C3 and ESP32-S3 share one controller (lib_esp32c3_family, RivieraWaves link layer).
#define USE_ESP32_BLE_TERMINATE_PATCH
// Controller internals, reached through r_ip_funcs_p: a table btdm_controller_init() copies into RAM.
extern "C" {
extern void *r_ip_funcs_p;
const char *btdm_controller_get_compile_version();
uint8_t r_lld_con_terminate_max_evt_update(uint8_t link_id, uint8_t acked);
uint8_t *r_lld_con_cntl_pkt_info_get();
}
#endif
namespace esphome::esp32_ble {
ESPHOME_LOG_TAG(TAG, "esp32_ble");
#ifdef USE_ESP32_BLE_TERMINATE_PATCH
// The controller ends its terminate procedure after 6 connection events without an acknowledgement and reports
// the link closed. A peer using peripheral latency listens only every (latency + 1) events, so it can miss all
// six LL_TERMINATE_IND: it keeps the link and searches for the central at full receive current until its own
// supervision timeout. The Core spec keeps the procedure going until the acknowledgement or the supervision
// timeout; this does that, by holding the controller's no-acknowledgement count below its limit. A peer that
// never answers still ends the link at the supervision timeout.
static constexpr size_t TERMINATE_MAX_EVT_SLOT = 0x7dc / sizeof(void *);
static constexpr uint8_t TERMINATE_MAX_EVT = 5; // the controller gives up once its count exceeds this
static uint8_t IRAM_ATTR terminate_until_acked(uint8_t link_id, uint8_t acked) {
// Per link: [2] terminate in progress, [3] connection events without an acknowledgement.
uint8_t *info = r_lld_con_cntl_pkt_info_get() + link_id * 4;
if (!acked && info[2] != 0 && info[3] >= TERMINATE_MAX_EVT)
info[3] = TERMINATE_MAX_EVT - 1;
return r_lld_con_terminate_max_evt_update(link_id, acked);
}
// The controller builds whose r_lld_con_terminate_max_evt_update and r_lld_con_cntl_pkt_info_get the wrapper was
// checked against. The table slot alone does not prove the pkt_info layout, so an unknown build is left alone.
// btdm_controller_get_compile_version() is what ESP-IDF logs at init as "BT controller compile version [...]".
// Recheck on every ESP-IDF bump: add the new build once both functions are confirmed unchanged, or drop the patch once
// espressif/esp-idf#19189 is fixed.
static const char *const TERMINATE_PATCH_CONTROLLER_BUILDS[] = {
"51d9dfd", // ESP-IDF 5.5.5-5.5.6, Arduino 3.3.10-3.3.12
"b7de11e", // ESP-IDF 5.5.4, Arduino 3.3.8-3.3.9: both functions identical to 51d9dfd's
};
static void patch_terminate_until_acked() {
auto **table = static_cast<void **>(r_ip_funcs_p);
if (table == nullptr) {
ESP_LOGW(TAG, "Controller layout not recognised; disconnects keep the controller's 6-event limit");
return;
}
const char *build = btdm_controller_get_compile_version();
bool known_build = false;
for (const char *known : TERMINATE_PATCH_CONTROLLER_BUILDS)
known_build = known_build || strcmp(build, known) == 0;
if (!known_build) {
// Expected after an ESP-IDF bump, and nothing a user can act on
ESP_LOGD(TAG,
"Controller build %s not checked for the terminate patch; disconnects keep the controller's 6-event limit",
build);
return;
}
if (table[TERMINATE_MAX_EVT_SLOT] == reinterpret_cast<void *>(&terminate_until_acked))
return; // already patched: the controller stayed enabled across a setup
if (table[TERMINATE_MAX_EVT_SLOT] != reinterpret_cast<void *>(&r_lld_con_terminate_max_evt_update)) {
ESP_LOGW(TAG, "Controller build %s has an unexpected terminate handler; terminate patch skipped", build);
return;
}
table[TERMINATE_MAX_EVT_SLOT] = reinterpret_cast<void *>(&terminate_until_acked);
}
#endif
#ifdef CONFIG_BT_CONTROLLER_DISABLED
// Bringing up the remote BT controller issues synchronous RPCs to the
// co-processor with 5 second response timeouts, and the default task watchdog
@@ -223,6 +293,9 @@ bool ESP32BLE::ble_setup_() {
return false;
}
}
#ifdef USE_ESP32_BLE_TERMINATE_PATCH
patch_terminate_until_acked();
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
+17
View File
@@ -21,6 +21,23 @@ void I2CBus::i2c_scan_() {
}
}
void I2CBus::dump_scan_results_() {
if (!this->scan_)
return;
ESP_LOGCONFIG(TAG, "Results from bus scan:");
if (this->scan_results_.empty()) {
ESP_LOGCONFIG(TAG, "Found no devices");
return;
}
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
void log_wrong_chip_id(uint8_t id, uint8_t expected) {
ESP_LOGE(TAG, "Wrong chip ID 0x%02X, expected 0x%02X", id, expected);
}
+2
View File
@@ -67,6 +67,8 @@ class I2CBus {
/// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair
/// that contains the address and the corresponding bool presence flag.
void i2c_scan_();
/// @brief Logs the scan results at config level, if the bus was scanned
void dump_scan_results_();
std::vector<std::pair<uint8_t, bool>> scan_results_; ///< array containing scan results
bool scan_{false}; ///< Should we scan ? Can be set in the yaml
};
+1 -14
View File
@@ -85,20 +85,7 @@ void ArduinoI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, " Recovery: failed, SDA is held low on the bus");
break;
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from bus scan:");
if (this->scan_results_.empty()) {
ESP_LOGI(TAG, "Found no devices");
} else {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
}
this->dump_scan_results_();
}
ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
+1 -14
View File
@@ -127,20 +127,7 @@ void IDFI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, " Recovery: failed, SDA is held low on the bus");
break;
}
if (this->scan_) {
ESP_LOGCONFIG(TAG, "Results from bus scan:");
if (this->scan_results_.empty()) {
ESP_LOGCONFIG(TAG, "Found no devices");
} else {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
}
this->dump_scan_results_();
}
ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
+1 -8
View File
@@ -62,14 +62,7 @@ void HostI2CBus::dump_config() {
ESP_LOGE(TAG, " Setup Error: %s", this->first_error_.c_str());
}
if (this->scan_) {
ESP_LOGI(TAG, " Scan Results:");
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, " 0x%02X: Found", s.first);
}
}
}
this->dump_scan_results_();
}
ErrorCode HostI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
+1 -14
View File
@@ -51,20 +51,7 @@ void SoftwareI2CBus::dump_config() {
if (this->recovered_) {
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from bus scan:");
if (this->scan_results_.empty()) {
ESP_LOGI(TAG, "Found no devices");
} else {
for (const auto &s : this->scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
}
this->dump_scan_results_();
}
// Half of one clock period in microseconds, rounded up. The frequency is validated to be non-zero.
+1 -14
View File
@@ -60,20 +60,7 @@ void ZephyrI2CBus::dump_config() {
} else {
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
}
if (this->scan_) {
ESP_LOGI(TAG, "Results from I2C bus scan:");
if (scan_results_.empty()) {
ESP_LOGI(TAG, "Found no I2C devices!");
} else {
for (const auto &s : scan_results_) {
if (s.second) {
ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first);
} else {
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}
}
this->dump_scan_results_();
}
ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
@@ -162,11 +162,6 @@ void SendspinHub::setup() {
if (this->unpaired_access_.has_value()) {
this->client_->set_unpaired_access_enabled(*this->unpaired_access_);
}
#ifdef USE_SENDSPIN_ENABLED_SWITCH
// loop() waits for this, so a restore mode without a state still has to answer.
this->enabled_switch_->restore(true);
#endif
}
void SendspinHub::loop() {
@@ -216,9 +211,6 @@ void SendspinHub::dump_config() {
this->firmware_version_, YESNO(this->task_stack_in_psram_),
YESNO(this->client_ != nullptr && this->client_->is_unpaired_access_enabled()),
this->pairing_code_method_());
#ifdef USE_SENDSPIN_ENABLED_SWITCH
LOG_SWITCH(" ", "Enabled switch", this->enabled_switch_);
#endif
#ifdef USE_SENDSPIN_ARTWORK
// Slot indices come from the order the image platform entries were declared, so the log is the
@@ -14,9 +14,6 @@
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
#include "esphome/components/mdns/mdns_component.h"
#endif
#ifdef USE_SENDSPIN_ENABLED_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <sendspin/client.h>
#include <sendspin/config.h>
@@ -181,9 +178,6 @@ class SendspinHub final : public Component,
/// fire from inside that call. The client stays stopped until this has been called once, by codegen or by the
/// enabled switch. Must be called from the main loop thread.
void set_enabled(bool enabled);
#ifdef USE_SENDSPIN_ENABLED_SWITCH
void set_enabled_switch(switch_::Switch *sw) { this->enabled_switch_ = sw; }
#endif
/// Turns unpaired (Sentinel) access on or off. The client waits for this call, by codegen or by the unpaired
/// access switch, before its first start. May be called before setup(). Main loop only.
@@ -413,9 +407,6 @@ class SendspinHub final : public Component,
LazyCallbackManager<void(const std::string &, StringRef)> pairing_failed_callbacks_{};
const char *static_pairing_code_{nullptr}; // Codegen string literal, or nullptr when not configured
#ifdef USE_SENDSPIN_ENABLED_SWITCH
switch_::Switch *enabled_switch_{nullptr};
#endif
bool pairing_code_display_supported_{false};
bool task_stack_in_psram_{false};
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
+15 -19
View File
@@ -25,18 +25,25 @@ _LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["sendspin"]
SendspinEnabledSwitch = sendspin_ns.class_(
"SendspinEnabledSwitch", switch.Switch, cg.Component
)
SendspinUnpairedAccessSwitch = sendspin_ns.class_(
"SendspinUnpairedAccessSwitch", switch.Switch, cg.Component
)
def _switch_schema(class_: cg.MockObjClass) -> cv.Schema:
return switch.switch_schema(
class_,
block_inverted=True,
default_restore_mode="RESTORE_DEFAULT_ON",
entity_category=ENTITY_CATEGORY_CONFIG,
).extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
return (
switch.switch_schema(
class_,
block_inverted=True,
default_restore_mode="RESTORE_DEFAULT_ON",
entity_category=ENTITY_CATEGORY_CONFIG,
)
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
.extend(cv.COMPONENT_SCHEMA)
)
def _request_switch(config: ConfigType) -> ConfigType:
@@ -48,10 +55,8 @@ def _request_switch(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
CONF_ENABLED: _switch_schema(switch.ApplySwitch),
CONF_UNPAIRED_ACCESS: _switch_schema(SendspinUnpairedAccessSwitch).extend(
cv.COMPONENT_SCHEMA
),
CONF_ENABLED: _switch_schema(SendspinEnabledSwitch),
CONF_UNPAIRED_ACCESS: _switch_schema(SendspinUnpairedAccessSwitch),
},
key=CONF_TYPE,
),
@@ -93,15 +98,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
if config[CONF_TYPE] == CONF_ENABLED:
# The hub restores this switch in its setup() and logs it.
hub = await cg.get_variable(config[CONF_SENDSPIN_ID])
switches = switch.sub_switches(config, parent=hub)
await switches.create(
config, hub.set_enabled_switch, apply="set_enabled(state)", publish="after"
)
cg.add_define("USE_SENDSPIN_ENABLED_SWITCH", True)
return
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_SENDSPIN_ID])
@@ -9,6 +9,21 @@ namespace esphome::sendspin_ {
ESPHOME_LOG_TAG(TAG, "sendspin.switch");
// --- SendspinEnabledSwitch ---
void SendspinEnabledSwitch::setup() {
// The hub waits for this request, so a restore mode without a state still has to answer.
this->restore(true);
}
void SendspinEnabledSwitch::dump_config() { LOG_SWITCH("", "Sendspin Enabled Switch", this); }
// THREAD CONTEXT: Main loop
void SendspinEnabledSwitch::write_state(bool state) {
this->parent_->set_enabled(state);
this->publish_state(state);
}
// --- SendspinUnpairedAccessSwitch ---
void SendspinUnpairedAccessSwitch::setup() {
@@ -9,6 +9,16 @@
namespace esphome::sendspin_ {
/// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()).
class SendspinEnabledSwitch final : public switch_::Switch, public SendspinChild {
public:
void setup() override;
void dump_config() override;
protected:
void write_state(bool state) override;
};
/// @brief Switch that turns unpaired (Sentinel) access on and off through the hub (see
/// SendspinHub::set_unpaired_access_enabled()). The library does not persist the setting, so the switch's restore
/// mode does.
@@ -2,6 +2,7 @@ from dataclasses import dataclass
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_ACCEPT, CONF_ON_NUMERIC_COMPARISON_REQUEST
from esphome.components.nrf52.framework import (
bluetooth_west_projects,
include_west_project,
@@ -54,8 +55,6 @@ def request_ble_l2cap_mtu(l2cap_mtu: int) -> None:
zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server")
BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component)
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_ACCEPT = "accept"
CONFIG_SCHEMA = cv.All(
cv.Schema(
-1
View File
@@ -527,7 +527,6 @@
#define USE_SENDSPIN
#define USE_SENDSPIN_ARTWORK
#define USE_SENDSPIN_CONTROLLER
#define USE_SENDSPIN_ENABLED_SWITCH
#define USE_SENDSPIN_METADATA
#define USE_SENDSPIN_PLAYER
#define USE_SENDSPIN_SWITCH
@@ -8,7 +8,6 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import re
import pytest
@@ -44,16 +43,3 @@ def test_switch_answers_the_hub(
assert "->set_enabled(true);" not in main_cpp
assert f"sendspin_enabled->set_restore_mode(switch_::{restore_mode});" in main_cpp
assert "App.register_switch(sendspin_enabled," in main_cpp
def test_switch_sets_the_hub_then_publishes(
generate_main: GenerateMain, component_config_path: ConfigPath
) -> None:
"""Turning the switch sets the hub first, then publishes."""
main_cpp = generate_main(component_config_path("switch_enabled.yaml"))
# The generated body is the hub call followed directly by the publish
assert re.search(
r"->set_enabled\(state\);\s*entity->publish_state\(state\);", main_cpp
)
assert "->set_enabled_switch(sendspin_enabled);" in main_cpp