Compare commits

..
Author SHA1 Message Date
J. Nick Koston 1031f74457 Merge branch 'dev' into wifi-manual-ip-progmem 2026-10-26 21:33:50 -10:00
J. Nick Koston b5b4a5b642 [wifi] Test the manual IP table and drop the unreachable row limit
Networks are capped at 127 in the schema, so with the AP there are at most 128 rows and the index never reaches 0xFF.
2026-10-26 18:37:07 -10:00
J. Nick Koston 8e5b65cf8a [wifi] Keep manual IP settings in flash
Each WiFiAP now stores a 1 byte row index into one PROGMEM table of all configured manual IPs instead of an optional<ManualIP> copy, saving 26 bytes per network and per fallback AP.
2026-10-26 17:54:14 -10:00
44 changed files with 421 additions and 671 deletions

No files matched your search

@@ -1,7 +1,6 @@
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,
@@ -13,7 +12,10 @@ 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 DOMAIN as ZEPHYR_BLE_SERVER_DOMAIN
from esphome.components.zephyr_ble_server import (
CONF_ON_NUMERIC_COMPARISON_REQUEST,
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
+2 -1
View File
@@ -5,7 +5,6 @@ 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 (
@@ -102,8 +101,10 @@ 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,7 +8,6 @@ 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"
@@ -59,7 +58,6 @@ 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"
+4 -105
View File
@@ -25,86 +25,16 @@ 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
@@ -293,9 +223,6 @@ 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
@@ -722,30 +649,20 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
// never answer an update, and without this the link silently stays on the
// old parameters (visible only as unexplained supervision timeouts).
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: {
// A peer can keep asking for parameters it then rejects: warn once per peer, repeats are
// verbose until an update succeeds or it disconnects.
if (param->update_conn_params.status != ESP_BT_STATUS_SUCCESS) {
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
if (global_ble->conn_param_failed_slot_(param->update_conn_params.bda) != nullptr) {
ESP_LOGV(TAG, "[%s] Conn param update failed again, status=%d", mac_s, param->update_conn_params.status);
} else {
ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status);
static constexpr esp_bd_addr_t FREE{};
uint8_t *slot = global_ble->conn_param_failed_slot_(FREE);
if (slot != nullptr)
memcpy(slot, param->update_conn_params.bda, sizeof(esp_bd_addr_t));
}
} else {
global_ble->forget_conn_param_failure_(param->update_conn_params.bda);
ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status);
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
else {
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
ESP_LOGV(TAG, "[%s] Conn params updated: interval=%u (x1.25ms) latency=%u timeout=%u (x10ms)", mac_s,
param->update_conn_params.conn_int, param->update_conn_params.latency,
param->update_conn_params.timeout);
#endif
}
#endif
return;
}
@@ -767,8 +684,6 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
#ifdef USE_ESP32_BLE_SERVER
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
if (event == ESP_GATTS_DISCONNECT_EVT)
global_ble->forget_conn_param_failure_(param->disconnect.remote_bda);
enqueue_ble_event(event, gatts_if, param);
// Wake up main loop to process GATT event immediately
App.wake_loop_threadsafe();
@@ -778,28 +693,12 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
#ifdef USE_ESP32_BLE_CLIENT
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_DISCONNECT_EVT)
global_ble->forget_conn_param_failure_(param->disconnect.remote_bda);
enqueue_ble_event(event, gattc_if, param);
// Wake up main loop to process GATT event immediately
App.wake_loop_threadsafe();
}
#endif
uint8_t *ESP32BLE::conn_param_failed_slot_(const uint8_t *bda) {
for (auto &slot : this->conn_param_failed_bdas_) {
if (memcmp(slot, bda, sizeof(esp_bd_addr_t)) == 0)
return slot;
}
return nullptr;
}
void ESP32BLE::forget_conn_param_failure_(const uint8_t *bda) {
uint8_t *slot = this->conn_param_failed_slot_(bda);
if (slot != nullptr)
memset(slot, 0, sizeof(esp_bd_addr_t));
}
void ESP32BLE::get_mac_msb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
// The running stack owns the address (on hosted controllers it lives in
// the remote chip's efuse); null before init becomes all-zero.
-5
View File
@@ -174,9 +174,6 @@ class ESP32BLE final : public Component {
// Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops.
void __attribute__((noinline)) loop_handle_state_transition_not_active_();
uint8_t *conn_param_failed_slot_(const uint8_t *bda);
void forget_conn_param_failure_(const uint8_t *bda);
bool ble_setup_();
bool ble_dismantle_();
void request_state_(bool enable);
@@ -247,8 +244,6 @@ class ESP32BLE final : public Component {
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 1 byte (uint8_t typedef)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte
// Peers already warned about a failed conn param update, one per connection; all-zero is free
esp_bd_addr_t conn_param_failed_bdas_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
#ifdef USE_ESP32_BLE_ADVERTISING
uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising
#endif
+12 -9
View File
@@ -7,6 +7,7 @@ from esphome.components.network import (
add_use_address,
get_network_priority,
get_priority_interfaces_from_full_config,
ip_address_literal,
require_ipv4,
)
from esphome.config_helpers import (
@@ -238,6 +239,7 @@ MANUAL_IP_SCHEMA = cv.Schema(
)
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP")
def _is_framework_spi_polling_mode_supported() -> bool:
@@ -623,10 +625,15 @@ def _final_validate_spi(config: ConfigType) -> None:
)
def _manual_ip_table(config: ConfigType) -> list[int]:
"""Pack manual_ip in the ETHERNET_MANUAL_IP layout."""
keys = (CONF_STATIC_IP, CONF_GATEWAY, CONF_SUBNET, CONF_DNS1, CONF_DNS2)
return [octet for key in keys for octet in config[key].packed]
def manual_ip(config: ConfigType) -> cg.StructInitializer:
return cg.StructInitializer(
ManualIP,
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
("gateway", ip_address_literal(config[CONF_GATEWAY])),
("subnet", ip_address_literal(config[CONF_SUBNET])),
("dns1", ip_address_literal(config[CONF_DNS1])),
("dns2", ip_address_literal(config[CONF_DNS2])),
)
def phy_register(address: int, value: int, page: int) -> cg.StructInitializer:
@@ -678,11 +685,7 @@ async def to_code(config: ConfigType) -> None:
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.extern_progmem_array(
"esphome::ethernet::ETHERNET_MANUAL_IP",
cg.uint8,
_manual_ip_table(config[CONF_MANUAL_IP]),
)
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
@@ -10,15 +10,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-
EthernetComponent::EthernetComponent() { global_eth_component = this; }
#ifdef USE_ETHERNET_MANUAL_IP
ManualIP load_manual_ip() {
uint8_t b[ETHERNET_MANUAL_IP_SIZE];
progmem_memcpy(b, ETHERNET_MANUAL_IP, sizeof(b));
auto ip = [&b](size_t i) { return network::IPAddress(b[i], b[i + 1], b[i + 2], b[i + 3]); };
return ManualIP{ip(0), ip(4), ip(8), ip(12), ip(16)};
}
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::notify_ip_state_listeners_() {
auto ips = this->get_ip_addresses();
@@ -103,14 +103,6 @@ struct ManualIP {
network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default.
};
#ifdef USE_ETHERNET_MANUAL_IP
/// The configured static IP, gateway, subnet, dns1 and dns2 as 4 octets each, generated by codegen
/// (components/ethernet/__init__.py) and kept in flash.
static constexpr size_t ETHERNET_MANUAL_IP_SIZE = 5 * 4;
extern const uint8_t ETHERNET_MANUAL_IP[ETHERNET_MANUAL_IP_SIZE] PROGMEM;
ManualIP load_manual_ip();
#endif
struct PHYRegister {
uint32_t address;
uint32_t value;
@@ -159,6 +151,9 @@ class EthernetComponent final : public Component {
#endif
void set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void set_fixed_mac(const std::array<uint8_t, MAC_ADDRESS_SIZE> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
@@ -338,6 +333,9 @@ class EthernetComponent final : public Component {
#endif // USE_RP2
// Common members
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
uint32_t connect_begin_;
// Group all uint8_t types together (enums and bools)
@@ -763,7 +763,9 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
global_eth_component->notify_ip_state_listeners_();
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
@@ -887,13 +889,19 @@ void EthernetComponent::start_connect_() {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
esp_netif_ip_info_t info{};
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
const ManualIP manual_ip = load_manual_ip();
info.ip = manual_ip.static_ip;
info.gw = manual_ip.gateway;
info.netmask = manual_ip.subnet;
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
}
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
@@ -911,32 +919,35 @@ void EthernetComponent::start_connect_() {
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
// Set DNS through esp_netif so the servers are stored in the netif's own
// dns[] array; raw dns_setserver() would be lost when the default-route
// arbitration re-applies the default netif's DNS.
// Log-only on failure: the link still has a working IP/gateway, so degraded
// name resolution does not justify marking the whole component failed.
esp_netif_dns_info_t dns{};
if (manual_ip.dns1.is_set()) {
dns.ip = manual_ip.dns1;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_MAIN, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set main DNS failed: %s", esp_err_to_name(err));
if (this->manual_ip_.has_value()) {
// Set DNS through esp_netif so the servers are stored in the netif's own
// dns[] array; raw dns_setserver() would be lost when the default-route
// arbitration re-applies the default netif's DNS.
// Log-only on failure: the link still has a working IP/gateway, so degraded
// name resolution does not justify marking the whole component failed.
esp_netif_dns_info_t dns{};
if (this->manual_ip_->dns1.is_set()) {
dns.ip = this->manual_ip_->dns1;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_MAIN, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set main DNS failed: %s", esp_err_to_name(err));
}
}
}
if (manual_ip.dns2.is_set()) {
dns.ip = manual_ip.dns2;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_BACKUP, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set backup DNS failed: %s", esp_err_to_name(err));
if (this->manual_ip_->dns2.is_set()) {
dns.ip = this->manual_ip_->dns2;
err = esp_netif_set_dns_info(this->eth_netif_, ESP_NETIF_DNS_BACKUP, &dns);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Set backup DNS failed: %s", esp_err_to_name(err));
}
}
}
#else
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
}
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
@@ -56,12 +56,15 @@ void EthernetComponent::setup() {
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
// Configure static IP (must be done before begin())
// Configure static IP if set (must be done before begin())
#ifdef USE_ETHERNET_MANUAL_IP
{
const ManualIP manual_ip = load_manual_ip();
this->eth_->config(IPAddress(manual_ip.static_ip), IPAddress(manual_ip.gateway), IPAddress(manual_ip.subnet),
IPAddress(manual_ip.dns1), IPAddress(manual_ip.dns2));
if (this->manual_ip_.has_value()) {
IPAddress ip(this->manual_ip_->static_ip);
IPAddress gateway(this->manual_ip_->gateway);
IPAddress subnet(this->manual_ip_->subnet);
IPAddress dns1(this->manual_ip_->dns1);
IPAddress dns2(this->manual_ip_->dns2);
this->eth_->config(ip, gateway, subnet, dns1, dns2);
}
#endif
@@ -287,17 +290,20 @@ void EthernetComponent::start_connect_() {
// Hostname is already set in setup() via LwipIntf::setHostname()
#ifdef USE_ETHERNET_MANUAL_IP
// Static IP was already configured before begin() in setup()
// Set DNS servers
const ManualIP manual_ip = load_manual_ip();
LwIPLock lock;
if (manual_ip.dns1.is_set()) {
ip_addr_t d = manual_ip.dns1;
dns_setserver(0, &d);
}
if (manual_ip.dns2.is_set()) {
ip_addr_t d = manual_ip.dns2;
dns_setserver(1, &d);
if (this->manual_ip_.has_value()) {
// Static IP was already configured before begin() in setup()
// Set DNS servers
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
}
#endif
}
+2 -6
View File
@@ -45,7 +45,6 @@ from .const import (
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
FAMILY_RTL8710B,
FAMILY_RTL8720C,
KEY_BOARD,
KEY_COMPONENT,
KEY_COMPONENT_DATA,
@@ -60,8 +59,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
# Fault handlers the crash handler wraps (BK7238, BK7231N)
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
# Families with a crash handler; the RTL8720C installs it at run time (no wraps).
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_RTL8720C)
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"]
@@ -557,11 +554,10 @@ async def component_to_code(config):
# RAM-executable output section and prints a post-link placement summary.
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
if config[CONF_FAMILY] in CRASH_HANDLER_FAMILIES:
# Crash handler (crash_handler.cpp)
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
# Crash handler (crash_handler.cpp, patch_noinit.py.script)
cg.add_define("USE_LIBRETINY_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
for trap in BK_TRAP_HANDLERS:
cg.add_build_flag(f"-Wl,--wrap={trap}")
cg.add_platformio_option("extra_scripts", ["pre:patch_noinit.py"])
+82 -225
View File
@@ -1,12 +1,8 @@
#ifdef USE_LIBRETINY
#ifdef USE_BK72XX
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY_CRASH_HANDLER
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_VARIANT_RTL8720C)
#error "The LibreTiny crash handler has no fault capture for this chip family"
#endif
#include "crash_handler.h"
#include "core.h"
#include "esphome/core/log.h"
@@ -17,7 +13,6 @@
#include <cstdint>
#include <cstdio>
#ifdef USE_BK72XX
extern "C" {
#include <arm_mcu_pub.h> // MCU_REG_BACKUP_SP_*
#include <intc.h> // struct arm_registers
@@ -44,138 +39,28 @@ extern char _itcmcode_ram_begin[];
extern char _itcmcode_ram_end[];
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_BK72XX
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// Bounds of the code in flash (XIP) and in RAM (linker symbols, rtl8710c_ram.ld).
extern char __xip_code_text_start__[];
extern char __xip_code_text_end__[];
extern char __ram_entry_text_start__[];
extern char __ram_code_text_end__[];
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LIBRETINY_VARIANT_RTL8720C
namespace esphome::libretiny {
ESPHOME_LOG_TAG(TAG, "libretiny.crash");
// 0xDEAD sentinel, then the record layout version.
static constexpr uint32_t CRASH_MAGIC_V2 = 0xDEAD0002;
static constexpr uint32_t CRASH_MAGIC_V1 = 0xDEAD0001;
// BK72xx: the ARM9 exception taken.
static constexpr uint32_t CRASH_REASON_UNDEFINED = 1;
static constexpr uint32_t CRASH_REASON_PREFETCH_ABORT = 2;
static constexpr uint32_t CRASH_REASON_DATA_ABORT = 3;
// RTL8720C: the Cortex-M exception number (3 to 7) plus this offset.
static constexpr uint32_t CRASH_REASON_CORTEX_M_OFFSET = 1;
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort",
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault");
static constexpr size_t MAX_BACKTRACE = 8;
// Words scanned above sp for return addresses.
static constexpr size_t STACK_SCAN_WORDS = 64;
struct CrashRecord {
uint32_t magic;
uint32_t reason;
uint32_t pc;
uint32_t lr;
uint32_t sp;
uint32_t fault_status; // Cortex-M CFSR, 0 on BK72xx
uint32_t fault_address; // Cortex-M BFAR or MMFAR when valid, else 0
uint32_t backtrace[MAX_BACKTRACE];
};
#ifdef USE_BK72XX
// Kept in ITCM, see patch_noinit.py.script.
#define CRASH_RECORD_SECTION ".noinit"
#else
// rtl8710c_ram.ld keeps .ram.bss.noinit* out of the startup zeroing.
#define CRASH_RECORD_SECTION ".ram.bss.noinit.esphome_crash"
#endif
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
__attribute__((section(CRASH_RECORD_SECTION))) static CrashRecord s_record;
static bool s_record_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_record_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool crash_handler_has_data() {
if (!s_record_read) {
s_record_read = true;
s_record_valid = s_record.magic == CRASH_MAGIC_V2;
// Clear the magic so the record is reported once.
s_record.magic = 0;
}
return s_record_valid;
}
// One ESP_LOGE per line: the CLI decodes per line and partial output still shows.
void crash_handler_log() {
if (!crash_handler_has_data())
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(CrashReasonStrings::get_log_str(s_record.reason, 0)));
if (s_record.fault_status != 0) {
ESP_LOGE(TAG, " CFSR: 0x%08" PRIX32 " Address: 0x%08" PRIX32, s_record.fault_status, s_record.fault_address);
}
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_record.pc);
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_record.lr);
ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_record.sp);
uint8_t count = 0;
for (; count < MAX_BACKTRACE && s_record.backtrace[count] != 0; count++) {
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", count, s_record.backtrace[count]);
}
// addr2line hint with every address
char hint[160];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e raw_firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
s_record.pc, s_record.lr);
for (uint8_t i = 0; i < count && pos < (int) sizeof(hint) - 12; i++) {
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_record.backtrace[i]);
}
ESP_LOGE(TAG, "%s", hint);
}
// --- Crash time: exception mode, a few dozen bytes of stack: no printing, no large locals.
static inline bool is_code_addr(uint32_t val) {
uint32_t addr = val & ~1u; // clear the Thumb bit of a return address
#ifdef USE_BK72XX
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
#else
return (addr >= (uint32_t) __xip_code_text_start__ && addr < (uint32_t) __xip_code_text_end__) ||
(addr >= (uint32_t) __ram_entry_text_start__ && addr < (uint32_t) __ram_code_text_end__);
#endif
}
// Return addresses above sp give the backtrace; then the magic marks the record complete.
static void finish_record(uint32_t sp, uint32_t sram_start, uint32_t sram_end) {
uint32_t count = 0;
if (sp >= sram_start && sp < sram_end && (sp & 3) == 0) {
auto *scan = (const uint32_t *) sp; // NOLINT(performance-no-int-to-ptr)
auto *end = (const uint32_t *) sram_end; // NOLINT(performance-no-int-to-ptr)
for (size_t i = 0; i < STACK_SCAN_WORDS && scan < end && count < MAX_BACKTRACE; i++, scan++) {
uint32_t val = *scan;
if (is_code_addr(val) && val != s_record.lr && (val & ~1u) != s_record.pc)
s_record.backtrace[count++] = val;
}
}
if (count < MAX_BACKTRACE)
s_record.backtrace[count] = 0; // terminator, the reader stops at the first 0
s_record.magic = CRASH_MAGIC_V2;
}
#ifdef USE_BK72XX
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort");
// With Wi-Fi up the boot-time watchdog no longer fires: any interrupts-off spin hangs until power is
// cycled (BK7238 and BK7231N; cause not in the open SDK). Re-armed here, long enough for the SDK's
// register dump when sdk_silent is off.
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 500;
static constexpr size_t MAX_BACKTRACE = 8;
// Words scanned above sp for return addresses.
static constexpr size_t STACK_SCAN_WORDS = 64;
// SRAM: 192 KB on the BK7231N, 256 KB on the BK7238.
static constexpr uint32_t SRAM_START = 0x00400000;
#ifdef USE_LIBRETINY_VARIANT_BK7231N
@@ -194,6 +79,65 @@ static constexpr uint32_t CPSR_MODE_UND = 0x1B;
static constexpr uint32_t CPSR_MODE_SYS = 0x1F;
static constexpr uint32_t CPSR_THUMB = 1u << 5;
struct CrashRecord {
uint32_t magic;
uint32_t reason;
uint32_t pc;
uint32_t lr;
uint32_t sp;
uint32_t backtrace[MAX_BACKTRACE];
};
// Kept in ITCM, see patch_noinit.py.script.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
__attribute__((section(".noinit"))) static CrashRecord s_record;
static bool s_record_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_record_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool crash_handler_has_data() {
if (!s_record_read) {
s_record_read = true;
s_record_valid = s_record.magic == CRASH_MAGIC_V1;
// Clear the magic so the record is reported once.
s_record.magic = 0;
}
return s_record_valid;
}
// One ESP_LOGE per line: the CLI decodes per line and partial output still shows.
void crash_handler_log() {
if (!crash_handler_has_data())
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(CrashReasonStrings::get_log_str(s_record.reason, 0)));
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_record.pc);
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_record.lr);
ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_record.sp);
uint8_t count = 0;
for (; count < MAX_BACKTRACE && s_record.backtrace[count] != 0; count++) {
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", count, s_record.backtrace[count]);
}
// addr2line hint with every address
char hint[160];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e raw_firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
s_record.pc, s_record.lr);
for (uint8_t i = 0; i < count && pos < (int) sizeof(hint) - 12; i++) {
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_record.backtrace[i]);
}
ESP_LOGE(TAG, "%s", hint);
}
// --- Crash time: exception mode, interrupts masked, a few dozen bytes of stack: no printing, no
// large locals.
static inline bool is_code_addr(uint32_t val) {
uint32_t addr = val & ~1u; // clear the Thumb bit of a return address
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
}
// PUSH_ALL_ARM_REG (boot_handlers.S) saves every mode's registers at the arm_mcu_pub.h addresses
// before bk_trap_*; lr (r14) follows sp (r13).
struct SavedBank {
@@ -230,9 +174,21 @@ static void record_crash(arm_registers *regs, uint32_t reason, uint32_t pc_offse
s_record.pc = regs->pc - pc_offset;
s_record.lr = lr;
s_record.sp = sp;
s_record.fault_status = 0;
s_record.fault_address = 0;
finish_record(sp, SRAM_START, SRAM_END);
// Return addresses above sp give the backtrace.
uint32_t count = 0;
if (sp >= SRAM_START && sp < SRAM_END && (sp & 3) == 0) {
auto *scan = (const uint32_t *) sp; // NOLINT(performance-no-int-to-ptr)
auto *end = (const uint32_t *) SRAM_END; // NOLINT(performance-no-int-to-ptr)
for (size_t i = 0; i < STACK_SCAN_WORDS && scan < end && count < MAX_BACKTRACE; i++, scan++) {
uint32_t val = *scan;
if (is_code_addr(val) && val != lr && (val & ~1u) != s_record.pc)
s_record.backtrace[count++] = val;
}
}
if (count < MAX_BACKTRACE)
s_record.backtrace[count] = 0; // terminator, the reader stops at the first 0
s_record.magic = CRASH_MAGIC_V1;
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
}
@@ -255,106 +211,7 @@ extern "C" void __wrap_bk_trap_dabt(arm_registers *regs) {
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_BK72XX
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
// The SDK points the fault vectors (3 HardFault to 7 SecureFault) of its RAM vector table (VTOR) at a
// ROM handler that prints a register dump and then spins. crash_handler_install() puts
// esphome_fault_entry() in front of it, as the SDK's own CONFIG_MIIO patch (app_start.c) does.
static_assert(LT_HW_WATCHDOG, "the crash reboot relies on the hardware watchdog");
// The ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
// watchdog from arch_init().
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 1000;
static constexpr uint32_t FIRST_FAULT_VECTOR = 3;
static constexpr uint32_t FAULT_VECTOR_COUNT = 5;
// Architectural System Control Block registers (ARMv8-M).
static constexpr uintptr_t SCB_VTOR = 0xE000ED08;
static constexpr uintptr_t SCB_CFSR = 0xE000ED28;
static constexpr uintptr_t SCB_MMFAR = 0xE000ED34;
static constexpr uintptr_t SCB_BFAR = 0xE000ED38;
static constexpr uint32_t CFSR_MMARVALID = 1u << 7;
static constexpr uint32_t CFSR_BFARVALID = 1u << 15;
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
static constexpr uint32_t SRAM_START = 0x10000000;
static constexpr uint32_t SRAM_END = 0x1003FA00;
// Exception frame: r0-r3, r12, lr, pc, xPSR (the RTL8720C has no FPU, so never the extended frame).
static constexpr uint32_t FRAME_LR = 5;
static constexpr uint32_t FRAME_PC = 6;
static constexpr uint32_t FRAME_XPSR = 7;
static constexpr uint32_t FRAME_WORDS = 8;
static constexpr uint32_t XPSR_STACK_ALIGNED = 1u << 9;
// The ROM handlers fault_entry() continues into, by vector.
static uint32_t s_rom_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static inline uint32_t read_reg(uintptr_t addr) {
return *(const volatile uint32_t *) addr; // NOLINT(performance-no-int-to-ptr)
}
// Returns the ROM handler to continue into. pc, lr and sp stay 0 when the frame is outside DTCM.
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector) {
uint32_t cfsr = read_reg(SCB_CFSR);
uint32_t address = 0;
if (cfsr & CFSR_BFARVALID) {
address = read_reg(SCB_BFAR);
} else if (cfsr & CFSR_MMARVALID) {
address = read_reg(SCB_MMFAR);
}
s_record.reason = vector + CRASH_REASON_CORTEX_M_OFFSET;
s_record.fault_status = cfsr;
s_record.fault_address = address;
auto frame_addr = (uint32_t) frame;
uint32_t sp = 0;
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - FRAME_WORDS * 4 && (frame_addr & 3) == 0) {
s_record.pc = frame[FRAME_PC];
s_record.lr = frame[FRAME_LR];
sp = frame_addr + FRAME_WORDS * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
} else {
s_record.pc = 0;
s_record.lr = 0;
}
s_record.sp = sp;
finish_record(sp, SRAM_START, SRAM_END);
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
return s_rom_fault_vectors[vector - FIRST_FAULT_VECTOR];
}
// Fault vector: calls esphome_record_fault(frame, exception number), then jumps to the ROM handler it returns
// with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the ROM handler
// reads EXC_RETURN from lr.
extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
__asm volatile("push {r0-r3, r4, lr} \n" // r4 only keeps the stack 8-byte aligned
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"it eq \n"
"addeq r0, r0, #24 \n" // MSP: skip the six words pushed above
"mrs r1, ipsr \n"
"ubfx r1, r1, #0, #9 \n" // exception number, 3 to 7
"bl esphome_record_fault \n"
"mov r12, r0 \n" // ROM handler address
"pop {r0-r3, r4, lr} \n"
"bx r12 \n");
}
void crash_handler_install() {
auto *vectors = (uint32_t *) read_reg(SCB_VTOR); // NOLINT(performance-no-int-to-ptr)
for (uint32_t i = 0; i < FAULT_VECTOR_COUNT; i++) {
s_rom_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
vectors[FIRST_FAULT_VECTOR + i] = (uint32_t) &esphome_fault_entry;
}
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
} // namespace esphome::libretiny
#endif // USE_LIBRETINY_CRASH_HANDLER
#endif // USE_LIBRETINY
#endif // USE_BK72XX
+2 -7
View File
@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_LIBRETINY
#ifdef USE_BK72XX
#include "esphome/core/defines.h"
@@ -14,12 +14,7 @@ void crash_handler_log();
/// Returns true if crash data was found this boot, reading it first if needed.
bool crash_handler_has_data();
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
/// Put the crash recorder in front of the SDK's fault handlers.
void crash_handler_install();
#endif
} // namespace esphome::libretiny
#endif // USE_LIBRETINY_CRASH_HANDLER
#endif // USE_LIBRETINY
#endif // USE_BK72XX
@@ -156,7 +156,6 @@ COMPONENT_CODEOWNERS = {
# Extra imports per component; the CLI looks up process_stacktrace on the platform package.
COMPONENT_EXTRA_IMPORTS = {
"bk72xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
"rtl87xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
}
-4
View File
@@ -1,7 +1,6 @@
#ifdef USE_LIBRETINY
#include "core.h"
#include "crash_handler.h"
#include "esphome/core/hal.h"
#include "preferences.h"
@@ -28,9 +27,6 @@ namespace esphome {
void arch_init() {
libretiny::setup_preferences();
lt_wdt_enable(10000L);
#if defined(USE_LIBRETINY_CRASH_HANDLER) && defined(USE_LIBRETINY_VARIANT_RTL8720C)
libretiny::crash_handler_install();
#endif
#ifdef USE_BK72XX
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
-1
View File
@@ -29,7 +29,6 @@ from esphome.components.libretiny.const import (
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
from esphome.types import ConfigType
from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS
@@ -1,6 +1,7 @@
#include "ota_web_server.h"
#ifdef USE_WEBSERVER_OTA
#include "esphome/components/ota/ota_backend_factory.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -8,37 +9,73 @@
#include "esphome/components/captive_portal/captive_portal.h"
#endif
#if USE_ESP32
using PlatformString = std::string;
#elif USE_ARDUINO
using PlatformString = String;
#endif
namespace esphome::web_server {
ESPHOME_LOG_TAG(TAG, "web_server.ota");
bool WebServerOTAComponent::canHandle(AsyncWebServerRequest *request) const {
if (request->method() != HTTP_POST)
return false;
// Check if this is an OTA update request
class OTARequestHandler : public AsyncWebHandler {
public:
OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {}
void handleRequest(AsyncWebServerRequest *request) override;
void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
size_t len, bool final) override;
bool canHandle(AsyncWebServerRequest *request) const override {
if (request->method() != HTTP_POST)
return false;
// Check if this is an OTA update request
#ifdef USE_ESP32
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
bool is_ota_request = request->url_to(url_buf) == "/update";
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
bool is_ota_request = request->url_to(url_buf) == "/update";
#else
bool is_ota_request = request->url() == ESPHOME_F("/update");
bool is_ota_request = request->url() == ESPHOME_F("/update");
#endif
#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
// Captive portal can still perform OTA updates - check if request is from active captive portal
// Note: global_captive_portal is the standard way components communicate in ESPHome
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
captive_portal::global_captive_portal->is_active();
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
// Captive portal can still perform OTA updates - check if request is from active captive portal
// Note: global_captive_portal is the standard way components communicate in ESPHome
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
captive_portal::global_captive_portal->is_active();
#elif defined(USE_WEBSERVER_OTA_DISABLED)
// OTA disabled for web_server and no captive portal compiled in
return false;
// OTA disabled for web_server and no captive portal compiled in
return false;
#else
// OTA enabled for web_server
return is_ota_request;
// OTA enabled for web_server
return is_ota_request;
#endif
}
}
void WebServerOTAComponent::report_ota_progress_(AsyncWebServerRequest *request) {
// NOLINTNEXTLINE(readability-identifier-naming)
bool isRequestHandlerTrivial() const override { return false; }
protected:
void report_ota_progress_(AsyncWebServerRequest *request);
void schedule_ota_reboot_();
void ota_init_(const char *filename);
/// Abort and drop the backend, releasing the lock.
void abort_session_();
/// Reply "busy" without touching the running upload.
void refuse_(AsyncWebServerRequest *request);
uint32_t last_ota_progress_{0};
uint32_t ota_read_length_{0};
WebServerOTAComponent *parent_;
bool ota_success_{false};
private:
ota::OTABackendPtr ota_backend_{nullptr};
/// Request that owns ota_backend_.
const AsyncWebServerRequest *ota_request_{nullptr};
const AsyncWebServerRequest *refused_request_{nullptr};
};
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
const uint32_t now = millis();
if (now - this->last_ota_progress_ > 1000) {
float percentage = 0.0f;
@@ -54,28 +91,28 @@ void WebServerOTAComponent::report_ota_progress_(AsyncWebServerRequest *request)
}
#ifdef USE_OTA_STATE_LISTENER
// Report progress - use notify_state_deferred_ since we're in web server task
this->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0);
this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
this->last_ota_progress_ = now;
}
}
void WebServerOTAComponent::schedule_ota_reboot_() {
void OTARequestHandler::schedule_ota_reboot_() {
ESP_LOGI(TAG, "OTA update successful!");
this->set_timeout(100, []() {
this->parent_->set_timeout(100, []() {
ESP_LOGI(TAG, "Performing OTA reboot now");
App.safe_reboot();
});
}
void WebServerOTAComponent::ota_init_(const char *filename) {
void OTARequestHandler::ota_init_(const char *filename) {
ESP_LOGI(TAG, "OTA Update Start: %s", filename);
this->ota_read_length_ = 0;
this->ota_success_ = false;
}
void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index,
uint8_t *data, size_t len, bool final) {
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index,
uint8_t *data, size_t len, bool final) {
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
// First byte of a new upload: index==0 with actual data. (web_server_idf
@@ -93,7 +130,8 @@ void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const P
if (!this->ota_backend_) {
ESP_LOGE(TAG, "Failed to create OTA backend");
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f,
static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
#endif
return;
}
@@ -115,14 +153,14 @@ void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const P
#ifdef USE_OTA_STATE_LISTENER
// Notify OTA started - use notify_state_deferred_ since we're in web server task
this->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
#endif
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
this->ota_backend_.reset();
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
return;
}
@@ -150,7 +188,7 @@ void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const P
this->ota_backend_.reset();
this->ota_request_ = nullptr;
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
return;
}
@@ -171,7 +209,7 @@ void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const P
this->ota_success_ = true;
#ifdef USE_OTA_STATE_LISTENER
// Report completion before reboot - use notify_state_deferred_ since we're in web server task
this->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
#endif
this->schedule_ota_reboot_();
} else {
@@ -179,7 +217,7 @@ void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const P
// Reset the platform updater before the lock is released.
this->ota_backend_->abort();
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}
this->ota_backend_.reset();
@@ -187,16 +225,16 @@ void WebServerOTAComponent::handleUpload(AsyncWebServerRequest *request, const P
}
}
void WebServerOTAComponent::abort_session_() {
void OTARequestHandler::abort_session_() {
this->ota_backend_->abort();
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
#endif
this->ota_backend_.reset();
this->ota_request_ = nullptr;
}
void WebServerOTAComponent::refuse_(AsyncWebServerRequest *request) {
void OTARequestHandler::refuse_(AsyncWebServerRequest *request) {
this->refused_request_ = request;
request->onDisconnect([this, request]() {
if (this->refused_request_ == request)
@@ -204,7 +242,7 @@ void WebServerOTAComponent::refuse_(AsyncWebServerRequest *request) {
});
}
void WebServerOTAComponent::handleRequest(AsyncWebServerRequest *request) {
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response;
// Use the ota_success_ flag to determine the actual result
const bool refused = request == this->refused_request_;
@@ -239,7 +277,8 @@ void WebServerOTAComponent::setup() {
return;
}
base->add_handler(this);
// The handler lives for the life of the process; WebServerBase never destroys its server
base->add_handler(new OTARequestHandler(this)); // NOLINT
}
void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
@@ -3,44 +3,20 @@
#include "esphome/core/defines.h"
#ifdef USE_WEBSERVER_OTA
#include "esphome/components/ota/ota_backend_factory.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/web_server_base/web_server_base.h"
#include "esphome/core/component.h"
namespace esphome::web_server {
class WebServerOTAComponent final : public AsyncWebHandler, public ota::OTAComponent {
class WebServerOTAComponent final : public ota::OTAComponent {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void handleRequest(AsyncWebServerRequest *request) override;
void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
size_t len, bool final) override;
bool canHandle(AsyncWebServerRequest *request) const override;
// NOLINTNEXTLINE(readability-identifier-naming)
bool isRequestHandlerTrivial() const override { return false; }
protected:
void report_ota_progress_(AsyncWebServerRequest *request);
void schedule_ota_reboot_();
void ota_init_(const char *filename);
/// Abort and drop the backend, releasing the lock.
void abort_session_();
/// Reply "busy" without touching the running upload.
void refuse_(AsyncWebServerRequest *request);
uint32_t last_ota_progress_{0};
uint32_t ota_read_length_{0};
bool ota_success_{false};
private:
ota::OTABackendPtr ota_backend_{nullptr};
/// Request that owns ota_backend_.
const AsyncWebServerRequest *ota_request_{nullptr};
const AsyncWebServerRequest *refused_request_{nullptr};
friend class OTARequestHandler;
};
} // namespace esphome::web_server
+33 -29
View File
@@ -1,3 +1,4 @@
from ipaddress import IPv4Address
import logging
import math
from typing import Any
@@ -19,7 +20,6 @@ from esphome.components.network import (
add_use_address,
get_network_priority,
has_high_performance_wifi,
ip_address_literal,
require_high_performance_wifi,
require_ipv4,
)
@@ -190,7 +190,6 @@ DEFAULT_DPP_TIMEOUT = "1min"
wifi_ns = cg.esphome_ns.namespace("wifi")
EAPAuth = wifi_ns.struct("EAPAuth")
ManualIP = wifi_ns.struct("ManualIP")
WiFiComponent = wifi_ns.class_("WiFiComponent", cg.Component)
WiFiAP = wifi_ns.struct("WiFiAP")
@@ -804,24 +803,20 @@ def eap_auth(config):
)
def safe_ip(ip):
return ip_address_literal(ip)
def manual_ip(config):
if config is None:
return None
return cg.StructInitializer(
ManualIP,
("static_ip", safe_ip(config[CONF_STATIC_IP])),
("gateway", safe_ip(config[CONF_GATEWAY])),
("subnet", safe_ip(config[CONF_SUBNET])),
("dns1", safe_ip(config.get(CONF_DNS1))),
("dns2", safe_ip(config.get(CONF_DNS2))),
def manual_ip_entry(config: ConfigType) -> bytes:
"""Pack manual IP settings as one MANUAL_IP_TABLE row (MANUAL_IP_ENTRY_SIZE bytes)."""
no_dns = IPv4Address(0)
addresses = (
config[CONF_STATIC_IP],
config[CONF_GATEWAY],
config[CONF_SUBNET],
config.get(CONF_DNS1, no_dns),
config.get(CONF_DNS2, no_dns),
)
return b"".join(address.packed for address in addresses)
def wifi_network(config, ap, static_ip):
def wifi_network(config, ap, manual_ip_index: int | None):
cg.add_optional(ap.set_ssid, config.get(CONF_SSID))
cg.add_optional(ap.set_password, config.get(CONF_PASSWORD))
if CONF_EAP in config:
@@ -831,8 +826,7 @@ def wifi_network(config, ap, static_ip):
cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts]))
cg.add_optional(ap.set_hidden, config.get(CONF_HIDDEN))
cg.add_optional(ap.set_channel, config.get(CONF_CHANNEL))
if static_ip is not None:
cg.add(ap.set_manual_ip(manual_ip(static_ip)))
cg.add_optional(ap.set_manual_ip, manual_ip_index)
# priority_ is 0 in C++; skip the setter when the config matches it.
if config.get(CONF_PRIORITY, 0) != 0:
cg.add(ap.set_priority(config[CONF_PRIORITY]))
@@ -851,8 +845,17 @@ async def to_code(config):
# Track if any network uses Enterprise authentication
has_eap = False
# Track if any network uses manual IP
has_manual_ip = False
# Rows of MANUAL_IP_TABLE; networks with the same settings share one. At most
# MAX_WIFI_NETWORKS + 1 (the AP) rows, so an index never reaches MANUAL_IP_NONE (0xFF).
manual_ip_entries: list[bytes] = []
def manual_ip_index(ip_config: ConfigType | None) -> int | None:
if ip_config is None:
return None
entry = manual_ip_entry(ip_config)
if entry not in manual_ip_entries:
manual_ip_entries.append(entry)
return manual_ip_entries.index(entry)
# Initialize FixedVector with the count of networks
networks = config.get(CONF_NETWORKS, [])
@@ -864,24 +867,20 @@ async def to_code(config):
def add_sta(ap: cg.MockObj, network: dict) -> None:
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
cg.add(var.add_sta(wifi_network(network, ap, manual_ip_index(ip_config))))
for network in networks:
if CONF_EAP in network:
has_eap = True
if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP):
has_manual_ip = True
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
if CONF_AP in config:
conf = config[CONF_AP]
ip_config = conf.get(CONF_MANUAL_IP)
if ip_config:
has_manual_ip = True
ap_ip_index = manual_ip_index(conf.get(CONF_MANUAL_IP))
cg.with_local_variable(
conf[CONF_ID],
WiFiAP(),
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))),
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ap_ip_index))),
)
# Skip the setter when the config matches the C++ initializer.
if (ap_timeout := conf[CONF_AP_TIMEOUT]) != DEFAULT_AP_TIMEOUT_PERIOD:
@@ -939,8 +938,13 @@ async def to_code(config):
require_mbedtls_tls_extras()
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
if has_manual_ip:
if manual_ip_entries:
cg.add_define("USE_WIFI_MANUAL_IP")
cg.extern_progmem_array(
"esphome::wifi::MANUAL_IP_TABLE",
cg.uint8,
list(b"".join(manual_ip_entries)),
)
# The C++ initializers are DEFAULT_REBOOT_TIMEOUT, power save NONE and minimum
# auth WPA2; skip the setters when the config matches them.
+16 -7
View File
@@ -1360,17 +1360,15 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
ESP_LOGV(TAG, " Channel not set");
}
#ifdef USE_WIFI_MANUAL_IP
auto manual_ip = ap.get_manual_ip();
if (manual_ip.has_value()) {
ManualIP m = *manual_ip;
if (const auto m = ap.get_manual_ip(); m.has_value()) {
char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str_to(static_ip_buf),
m.gateway.str_to(gateway_buf), m.subnet.str_to(subnet_buf), m.dns1.str_to(dns1_buf),
m.dns2.str_to(dns2_buf));
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m->static_ip.str_to(static_ip_buf),
m->gateway.str_to(gateway_buf), m->subnet.str_to(subnet_buf), m->dns1.str_to(dns1_buf),
m->dns2.str_to(dns2_buf));
} else
#endif
{
@@ -1908,7 +1906,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
// On ESP8266, GOT_IP event may not fire for static IP configurations,
// so notify IP state listeners here as a fallback.
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip()) {
this->notify_ip_state_listeners_();
}
#endif
@@ -2881,6 +2879,17 @@ void WiFiComponent::process_roaming_scan_() {
this->start_connecting(roam_params);
}
#ifdef USE_WIFI_MANUAL_IP
optional<ManualIP> WiFiAP::get_manual_ip() const {
if (!this->has_manual_ip())
return {};
uint8_t b[MANUAL_IP_ENTRY_SIZE];
progmem_memcpy(b, MANUAL_IP_TABLE + this->manual_ip_index_ * MANUAL_IP_ENTRY_SIZE, sizeof(b));
auto ip = [&b](size_t i) { return network::IPAddress(b[i], b[i + 1], b[i + 2], b[i + 3]); };
return ManualIP{ip(0), ip(4), ip(8), ip(12), ip(16)};
}
#endif
WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::wifi
+17 -7
View File
@@ -188,6 +188,14 @@ struct ManualIP {
network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default.
};
#ifdef USE_WIFI_MANUAL_IP
/// One MANUAL_IP_TABLE row: static IP, gateway, subnet, dns1 and dns2 as 4 octets each.
static constexpr size_t MANUAL_IP_ENTRY_SIZE = 20;
static constexpr uint8_t MANUAL_IP_NONE = 0xFF;
/// Every configured manual IP, generated by codegen (components/wifi/__init__.py) and kept in flash.
extern const uint8_t MANUAL_IP_TABLE[] PROGMEM;
#endif
#ifdef USE_WIFI_WPA2_EAP
struct EAPAuth {
std::string identity; // required for all auth types
@@ -300,7 +308,8 @@ class WiFiAP {
void clear_channel() { this->channel_ = 0; }
void set_priority(int8_t priority) { priority_ = priority; }
#ifdef USE_WIFI_MANUAL_IP
void set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
/// index is a row of MANUAL_IP_TABLE
void set_manual_ip(uint8_t index) { this->manual_ip_index_ = index; }
#endif
void set_hidden(bool hidden) { this->hidden_ = hidden; }
StringRef get_ssid() const { return this->ssid_.ref(); }
@@ -314,7 +323,8 @@ class WiFiAP {
bool has_channel() const { return this->channel_ != 0; }
int8_t get_priority() const { return priority_; }
#ifdef USE_WIFI_MANUAL_IP
const optional<ManualIP> &get_manual_ip() const { return this->manual_ip_; }
bool has_manual_ip() const { return this->manual_ip_index_ != MANUAL_IP_NONE; }
optional<ManualIP> get_manual_ip() const;
#endif
bool get_hidden() const { return this->hidden_; }
@@ -324,14 +334,14 @@ class WiFiAP {
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
#ifdef USE_WIFI_MANUAL_IP
optional<ManualIP> manual_ip_;
#endif
// Group small types together to minimize padding
bssid_t bssid_{}; // 6 bytes, all zeros = any/not set
uint8_t channel_{0}; // 1 byte, 0 = auto/not set
int8_t priority_{0}; // 1 byte
bool hidden_{false}; // 1 byte; WiFiAP is byte aligned unless manual IP or EAP adds a 4 byte aligned member
bool hidden_{false}; // 1 byte; WiFiAP is byte aligned unless EAP adds a 4 byte aligned member
#ifdef USE_WIFI_MANUAL_IP
uint8_t manual_ip_index_{MANUAL_IP_NONE};
#endif
};
class WiFiScanResult {
@@ -1050,7 +1060,7 @@ class WiFiComponent final : public Component {
uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed
#ifdef USE_WIFI_AP
uint32_t ap_timeout_{90000}; // Keep in sync with DEFAULT_AP_TIMEOUT in __init__.py
// WiFiAP is byte aligned unless manual IP or EAP is enabled; placed before the
// WiFiAP is byte aligned unless EAP is enabled; placed before the
// 1-byte members so they pack into its trailing bytes instead of padding after it
WiFiAP ap_;
#endif // USE_WIFI_AP
@@ -795,7 +795,7 @@ void WiFiComponent::save_lease_settings_() {
return;
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value())
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip())
return;
#endif
@@ -1093,7 +1093,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
bool static_ip = false;
#ifdef USE_WIFI_MANUAL_IP
const WiFiAP *config = this->get_selected_sta_();
static_ip = config && config->get_manual_ip().has_value();
static_ip = config && config->has_manual_ip();
#endif
#ifdef USE_WIFI_LEASE_CACHE
static_ip = static_ip || this->applied_cached_lease_;
@@ -510,7 +510,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
#endif
// For static IP configurations, GOT_IP event may not fire, so set connected state here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip()) {
this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTED);
#ifdef USE_WIFI_IP_STATE_LISTENERS
this->notify_ip_state_listeners_();
@@ -342,7 +342,7 @@ bool WiFiComponent::wifi_loop_() {
#endif
// For static IP configurations, notify IP listeners immediately as the IP is already configured
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip()) {
s_sta_had_ip = true;
this->notify_ip_state_listeners_();
}
@@ -2,7 +2,6 @@ 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,
@@ -55,6 +54,8 @@ 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 -1
View File
@@ -778,7 +778,7 @@
#endif
#ifdef USE_LIBRETINY
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C)
#ifdef USE_BK72XX
#define USE_LIBRETINY_CRASH_HANDLER
#endif
#define USE_BK72XX_BLE
-1
View File
@@ -67,7 +67,6 @@ STACKTRACE_GATES: Final[dict[str, str]] = {
),
PLATFORM_RP2: _CRASH_HANDLER_GATE,
PLATFORM_BK72XX: _CRASH_HANDLER_GATE,
PLATFORM_RTL87XX: _CRASH_HANDLER_GATE,
PLATFORM_NRF52: r"0x[0-9a-fA-F]{3,}\b|Last crash:",
}
+1 -1
View File
@@ -152,7 +152,7 @@ def _addr2line(tool: str, elf: Path, addr: str) -> str:
def process_crash_handler_line(
config: ConfigType, line: str, backtrace_state: bool
) -> bool:
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx and rtl87xx."""
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2 and bk72xx."""
if CRASH_HANDLER_RE.search(line):
_LOGGER.error("Crash detected - decoding addresses")
return True
-1
View File
@@ -57,7 +57,6 @@ PLATFORM_BUILD_SECONDS = {
"esp32-idf": 55,
"esp32-p4-idf": 55,
"ln882x-ard": 55,
"rtl8720c-ard": 55,
"esp32-s3-idf": 60,
"bk72xx-ard": 70,
"bk7238-ard": 70,
@@ -1,18 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
ethernet:
type: LAN8720
mdc_pin: GPIO23
mdio_pin: GPIO18
clk:
mode: CLK_EXT_IN
pin: GPIO0
manual_ip:
static_ip: 192.168.1.50
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 1.1.1.1
@@ -1,29 +0,0 @@
"""The manual IP table codegen emits must match the ETHERNET_MANUAL_IP layout in ethernet_component.h."""
from collections.abc import Callable
from pathlib import Path
def test_manual_ip_table_layout(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Twenty octets: static IP, gateway, subnet, dns1, dns2 (unset DNS is zero)."""
main_cpp = generate_main(component_config_path("manual_ip.yaml"))
expected = (
"192, 168, 1, 50, 192, 168, 1, 1, 255, 255, 255, 0, 1, 1, 1, 1, 0, 0, 0, 0"
)
assert (
f"esphome::ethernet::ETHERNET_MANUAL_IP[] PROGMEM = {{{expected}}};" in main_cpp
)
assert "set_manual_ip(" not in main_cpp
def test_no_manual_ip_emits_no_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("rmii_defaults.yaml"))
assert "ETHERNET_MANUAL_IP" not in main_cpp
@@ -1,5 +0,0 @@
esphome:
name: test
rtl87xx:
board: generic-rtl8710bn-2mb-788k
@@ -1,5 +0,0 @@
esphome:
name: test
rtl87xx:
board: bw15
@@ -1,4 +1,4 @@
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region) and RTL8720C."""
"""Only BK7238 and BK7231N, the BK chips with an ITCM linker region, get the crash handler."""
from __future__ import annotations
@@ -12,14 +12,12 @@ from esphome.core import CORE
@pytest.mark.parametrize(
("config", "enabled", "bk_hooks"),
("config", "enabled"),
[
("bk7238.yaml", True, True),
("bk72xx.yaml", True, True), # BK7231N
("bk7231t.yaml", False, False),
("rtl8720c.yaml", True, False), # installed at run time
("rtl8710b.yaml", False, False),
("ln882x.yaml", False, False),
("bk7238.yaml", True),
("bk72xx.yaml", True), # BK7231N
("bk7231t.yaml", False),
("ln882x.yaml", False),
],
)
def test_crash_handler_family_gate(
@@ -27,14 +25,13 @@ def test_crash_handler_family_gate(
component_config_path: Callable[[str], Path],
config: str,
enabled: bool,
bk_hooks: bool,
) -> None:
generate_main(component_config_path(config))
defines = {define.name for define in CORE.defines}
assert ("USE_LIBRETINY_CRASH_HANDLER" in defines) is enabled
assert ("USE_CRASH_HANDLER" in defines) is enabled
for trap in BK_TRAP_HANDLERS:
assert (f"-Wl,--wrap={trap}" in CORE.build_flags) is bk_hooks
assert (f"-Wl,--wrap={trap}" in CORE.build_flags) is enabled
assert (
"pre:patch_noinit.py" in CORE.platformio_options["extra_scripts"]
) is bk_hooks
) is enabled
@@ -0,0 +1,32 @@
esphome:
name: test
esp8266:
board: d1_mini
wifi:
# Inherited by every network without its own manual_ip
manual_ip:
static_ip: 192.168.2.20
gateway: 192.168.2.1
subnet: 255.255.255.0
dns1: 192.168.2.1
networks:
- ssid: own
password: password1
manual_ip:
static_ip: 192.168.1.10
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 1.1.1.1
dns2: 8.8.8.8
- ssid: inherits1
password: password2
- ssid: inherits2
password: password3
ap:
password: fallback_password
manual_ip:
static_ip: 192.168.4.1
gateway: 192.168.4.1
subnet: 255.255.255.0
@@ -0,0 +1,25 @@
"""Tests for the manual IP table wifi codegen emits (MANUAL_IP_TABLE in wifi_component.h)."""
from collections.abc import Callable
from pathlib import Path
import re
def test_manual_ip_rows_are_shared(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Networks with the same settings share a row; the AP row packs its missing DNS as zeros."""
main_cpp = generate_main(component_config_path("manual_ip.yaml"))
own = "192, 168, 1, 10, 192, 168, 1, 1, 255, 255, 255, 0, 1, 1, 1, 1, 8, 8, 8, 8"
inherited = (
"192, 168, 2, 20, 192, 168, 2, 1, 255, 255, 255, 0, 192, 168, 2, 1, 0, 0, 0, 0"
)
ap = "192, 168, 4, 1, 192, 168, 4, 1, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0"
assert (
f"esphome::wifi::MANUAL_IP_TABLE[] PROGMEM = {{{own}, {inherited}, {ap}}};"
in main_cpp
)
# Three networks in config order, then the AP
assert re.findall(r"set_manual_ip\((\d+)\)", main_cpp) == ["0", "1", "1", "2"]
@@ -1,13 +0,0 @@
logger:
level: VERBOSE
esphome:
on_boot:
- lambda: |-
int x = 100;
x = clamp(x, 50, 90);
assert(x == 90);
x = clamp_at_least(x, 95);
assert(x == 95);
x = clamp_at_most(x, 40);
assert(x == 40);
+5
View File
@@ -42,6 +42,11 @@ wifi:
- ssid: MySSID2
password: password2
priority: 5
manual_ip:
static_ip: 192.168.2.100
gateway: 192.168.2.1
subnet: 255.255.255.0
dns1: 192.168.2.1
- ssid: MySSID3
password: password3
priority: 0
@@ -5,6 +5,10 @@ wifi:
ap:
ssid: Fallback AP
password: fallback_password
manual_ip:
static_ip: 192.168.4.1
gateway: 192.168.4.1
subnet: 255.255.255.0
packages:
- !include common.yaml
@@ -1,15 +0,0 @@
esphome:
name: componenttestesprtl8720c
friendly_name: $component_name
rtl87xx:
board: bw15
logger:
level: VERY_VERBOSE
packages:
component_under_test: !include
file: $component_test_file
vars:
component_test_file: $component_test_file
+15 -20
View File
@@ -1932,28 +1932,23 @@ def test_start_udp_probe_succeeds(mock_socket: Mock) -> None:
def test_udp_channel_pings_when_all_held() -> None:
"""With every message in flight already held, it asks for responses instead,
at most once per resend timeout."""
sock = Mock()
# A hand-driven clock, so a slow runner cannot pass the resend timeout between pumps
now = [100.0]
with (
patch.object(espota2, "time", Mock(monotonic=lambda: now[0])),
patch("select.select", return_value=([], [], [])),
):
channel = espota2.UdpChannel(sock, b"\x01\x01\x01\x01")
channel.sendall(b"x")
channel._queue[0].held = True
device = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
device.bind(("127.0.0.1", 0))
device.settimeout(1.0)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(device.getsockname())
channel = espota2.UdpChannel(sock, b"\x01\x01\x01\x01")
channel.sendall(b"x")
channel._queue[0].held = True
with patch.object(channel, "_receive"):
channel._pump()
channel._pump()
pinged_at, rto = now[0], channel._rto()
now[0] = pinged_at + rto - 0.01
channel._pump()
pings_before = [c.args[0][0] for c in sock.send.call_args_list]
now[0] = pinged_at + rto
channel._pump()
assert pings_before == [espota2.UDP_MSG_PING]
assert [c.args[0][0] for c in sock.send.call_args_list] == [
espota2.UDP_MSG_PING
] * 2
assert device.recv(2048)[0] == espota2.UDP_MSG_PING
device.settimeout(0.1)
with pytest.raises(TimeoutError):
device.recv(2048)
channel.close()
device.close()
def test_udp_channel_ignores_foreign_datagrams() -> None:
+2 -2
View File
@@ -117,9 +117,9 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2,
PLATFORM_RTL87XX,
ErrorFormat,
Toolchain,
)
@@ -6304,7 +6304,7 @@ def test_run_miniterm_no_stacktrace_analyzer(
"""Platforms without a stacktrace analyzer log an info and stream anyway."""
mock_serial = MockSerial([b"[I][app:100]: Line 1\r\n", MOCK_SERIAL_END])
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_LN882X}
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_RTL87XX}
config = {
CONF_LOGGER: {
CONF_BAUD_RATE: 115200,
+2 -2
View File
@@ -15,7 +15,7 @@ from unittest.mock import Mock
import pytest
from esphome import platform_hooks
from esphome.const import PLATFORM_ESP32, PLATFORM_LN882X, Platform
from esphome.const import PLATFORM_ESP32, PLATFORM_RTL87XX, Platform
def test_no_unregistered_platform_exposes_a_hook() -> None:
@@ -165,7 +165,7 @@ def test_get_stacktrace_handler_reports_missing_analyzer(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level("INFO", logger="esphome.platform_hooks")
assert platform_hooks.get_stacktrace_handler(PLATFORM_LN882X) is None
assert platform_hooks.get_stacktrace_handler(PLATFORM_RTL87XX) is None
assert "no compatible analyzer" in caplog.text
# A capability gap is ordinary; it must not warn.
assert not any(r.levelno >= logging.WARNING for r in caplog.records)
+1 -7
View File
@@ -18,7 +18,6 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2,
PLATFORM_RTL87XX,
@@ -67,10 +66,6 @@ CRASH_SAMPLES: dict[str, dict[str, list[str]]] = {
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
"addresses": ["PC: 0x0001614A (fault location)"],
},
PLATFORM_RTL87XX: {
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
"addresses": ["PC: 0x9B0073A4 (fault location)"],
},
PLATFORM_NRF52: {
"state_markers": ["Last crash:"],
"addresses": [
@@ -236,7 +231,6 @@ DECODER_PATTERNS: dict[str, list[str]] = {
],
PLATFORM_RP2: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_BK72XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_RTL87XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_NRF52: ["STACKTRACE_NRF52_PC_LR_RE"],
}
@@ -554,7 +548,7 @@ def test_processor_registry_miss_disables_at_construction(
caplog.set_level("INFO", logger="esphome.platform_hooks")
with patch.object(stacktrace.platform_hooks, "import_module") as mock_import:
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_LN882X)
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_RTL87XX)
processor.process_line("PC: 0x40104960")
mock_import.assert_not_called()