Compare commits

..
Author SHA1 Message Date
J. Nick Koston 4ac5bc6514 Merge branch 'dev' into number-traits-set-range 2026-10-26 05:53:09 -10:00
pre-commit-ci-lite[bot] 4941bc2d36 apply automatic formatting fixes 2026-10-26 15:49:36 +00:00
J. Nick Koston 09667bb79c Merge remote-tracking branch 'origin/dev' into number-traits-set-range
# Conflicts:
#	esphome/components/number/__init__.py
2026-10-26 05:47:58 -10:00
J. Nick Koston 8915d06b05 Merge remote-tracking branch 'origin/dev' into number-traits-set-range
# Conflicts:
#	esphome/components/number/__init__.py
2026-10-19 08:01:39 -10:00
J. Nick Koston 7df9b5ea2f Merge remote-tracking branch 'origin/dev' into HEAD 2026-10-09 18:27:29 -10:00
J. Nick Koston a8ddb2535e Merge remote-tracking branch 'origin/dev' into number-traits-set-range 2026-10-07 16:45:04 -10:00
J. Nick Koston d4922fb446 Merge remote-tracking branch 'origin/dev' into number-traits-set-range 2026-10-04 18:07:01 -05:00
J. Nick Koston 7acbe82c27 Also guard the NumberRange size so an added field fails the build 2026-10-03 05:47:52 -05:00
J. Nick Koston 1a0b43449e Split the field type guards into one static_assert per field
clang-tidy flags the chained is_same_v checks as a redundant expression.
2026-10-02 21:42:36 -05:00
J. Nick Koston 860d819e33 Guard the NumberRange field types read from flash 2026-10-02 20:55:16 -05:00
J. Nick Koston f8f25c1ad4 Make the render time statement a slotted dataclass 2026-10-02 17:47:47 -05:00
J. Nick Koston 22536f9338 Collapse the range statements into one render time statement 2026-10-02 17:47:06 -05:00
J. Nick Koston c64a7fb678 Declare shared range tables in the global section
The table is a lazy global statement that renders only when the range is
shared, so it no longer depends on the first user's position in setup().
One helper builds the setter expressions for both paths.
2026-10-02 17:36:28 -05:00
J. Nick Koston 6793dad8ed Only use a range table when enough numbers share it
Below three users the table and set_range() cost more than the inline
setters, so the choice is made at render time once every number is
known. Lambda bounds (lvgl arc/bar) keep the setters. set_range reads
the table with aligned loads instead of memcpy_P.
2026-10-02 17:32:24 -05:00
J. Nick Koston 3dd9a28ed2 Emit the range table with progmem_array and a generated ID 2026-10-02 17:14:31 -05:00
J. Nick Koston 5a93de82bf [number] Set range from a shared PROGMEM table
Each number used to emit three inline trait setters in setup(). Codegen
now emits one PROGMEM NumberRange per distinct (min, max, step) and each
number makes a single out of line set_range() call with its address.
2026-10-02 17:10:41 -05:00
24 changed files with 229 additions and 271 deletions

No files matched your search

+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"])
+44 -182
View File
@@ -1,4 +1,4 @@
#ifdef USE_LIBRETINY
#ifdef USE_BK72XX
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY_CRASH_HANDLER
@@ -13,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
@@ -40,19 +39,6 @@ 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_RTL87XX
// 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_RTL87XX
namespace esphome::libretiny {
@@ -61,39 +47,50 @@ ESPHOME_LOG_TAG(TAG, "libretiny.crash");
// 0xDEAD sentinel, then the record layout version.
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;
// RTL87xx: 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");
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
static constexpr uint32_t SRAM_END = 0x00430000;
#else
static constexpr uint32_t SRAM_END = 0x00440000;
#endif
static constexpr uint32_t CPSR_MODE_MASK = 0x1F;
static constexpr uint32_t CPSR_MODE_USR = 0x10;
static constexpr uint32_t CPSR_MODE_FIQ = 0x11;
static constexpr uint32_t CPSR_MODE_IRQ = 0x12;
static constexpr uint32_t CPSR_MODE_SVC = 0x13;
static constexpr uint32_t CPSR_MODE_ABT = 0x17;
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 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;
__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)
@@ -115,9 +112,6 @@ void crash_handler_log() {
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);
@@ -135,61 +129,15 @@ void crash_handler_log() {
ESP_LOGE(TAG, "%s", hint);
}
// --- Crash time: exception mode, a few dozen bytes of stack: no printing, no large locals.
// --- 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
#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_V1;
}
#ifdef USE_BK72XX
// 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;
// SRAM: 192 KB on the BK7231N, 256 KB on the BK7238.
static constexpr uint32_t SRAM_START = 0x00400000;
#ifdef USE_LIBRETINY_VARIANT_BK7231N
static constexpr uint32_t SRAM_END = 0x00430000;
#else
static constexpr uint32_t SRAM_END = 0x00440000;
#endif
static constexpr uint32_t CPSR_MODE_MASK = 0x1F;
static constexpr uint32_t CPSR_MODE_USR = 0x10;
static constexpr uint32_t CPSR_MODE_FIQ = 0x11;
static constexpr uint32_t CPSR_MODE_IRQ = 0x12;
static constexpr uint32_t CPSR_MODE_SVC = 0x13;
static constexpr uint32_t CPSR_MODE_ABT = 0x17;
static constexpr uint32_t CPSR_MODE_UND = 0x1B;
static constexpr uint32_t CPSR_MODE_SYS = 0x1F;
static constexpr uint32_t CPSR_THUMB = 1u << 5;
// 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 {
@@ -226,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);
}
@@ -251,105 +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_RTL87XX
// 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 + FRAME_WORDS * 4 <= SRAM_END && (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];
}
// Passes the faulting stack's frame and the vector number to esphome_record_fault, then
// branches to the ROM handler with r0-r3 and lr as they were on entry.
extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
__asm volatile("push {r0-r3, r4, lr} \n"
"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"
"bl esphome_record_fault \n"
"mov r12, r0 \n"
"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_RTL87XX
} // 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_RTL87XX
/// 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
@@ -152,7 +152,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_RTL87XX)
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
+74 -5
View File
@@ -1,3 +1,6 @@
from collections.abc import Callable
from dataclasses import dataclass, field
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -91,10 +94,13 @@ from esphome.core.entity_helpers import (
setup_entity,
setup_unit_of_measurement,
)
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
from esphome.cpp_generator import MockObj, MockObjClass, ProgmemAssignmentExpression
from esphome.types import ConfigType, SafeExpType
CODEOWNERS = ["@esphome/core"]
DOMAIN = "number"
CONF_RANGE_ID = "range_id"
DEVICE_CLASSES = [
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
DEVICE_CLASS_APPARENT_POWER,
@@ -162,6 +168,7 @@ IS_PLATFORM_COMPONENT = True
number_ns = cg.esphome_ns.namespace("number")
Number = number_ns.class_("Number", cg.EntityBase)
NumberPtr = Number.operator("ptr")
NumberRange = number_ns.struct("NumberRange")
ApplyNumber = number_ns.class_("ApplyNumber", Number)
# Triggers
@@ -199,6 +206,7 @@ _NUMBER_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt_component_class("MQTTNumberComponent")
),
cv.GenerateID(CONF_RANGE_ID): cv.declare_id(NumberRange),
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation(
{
@@ -274,13 +282,74 @@ async def _build_number_automations(var, config):
await automation.build_automation(trigger, [(float, "x")], conf)
# A shared table only pays for set_range() and the table itself once this many numbers use it.
RANGE_TABLE_MIN_USERS = 3
@dataclass
class NumberData:
# (min, max, step) -> range_id of every number using it; the first one names the table.
ranges: dict[tuple[float, float, float], list[ID]] = field(default_factory=dict)
def _get_data() -> NumberData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = NumberData()
return CORE.data[DOMAIN]
@dataclass(slots=True)
class _RenderTimeStatement(cg.Statement):
"""Text chosen when main.cpp is written, after every number has been counted."""
render: Callable[[], str]
def __str__(self) -> str:
return self.render()
def _add_range(
var: MockObj,
range_id: ID,
min_value: SafeExpType,
max_value: SafeExpType,
step: SafeExpType,
) -> None:
setters = (
var.traits.set_min_value(min_value),
var.traits.set_max_value(max_value),
var.traits.set_step(step),
)
# Lambda bounds (e.g. lvgl arc/bar) are runtime expressions and can't go in a table.
if not all(isinstance(v, (int, float)) for v in (min_value, max_value, step)):
for expr in setters:
cg.add(expr)
return
key = (float(min_value), float(max_value), float(step))
users = _get_data().ranges.setdefault(key, [])
users.append(range_id)
table = users[0]
def render_table() -> str:
if len(users) < RANGE_TABLE_MIN_USERS:
return ""
return f"{ProgmemAssignmentExpression(NumberRange, table, cg.safe_exp([key]))};"
def render_call() -> str:
if len(users) < RANGE_TABLE_MIN_USERS:
return "\n".join(str(cg.statement(expr)) for expr in setters)
return str(cg.statement(var.set_range(MockObj(table, "."))))
if len(users) == 1:
cg.add_global(_RenderTimeStatement(render_table))
cg.add(_RenderTimeStatement(render_call))
@setup_entity("number")
async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float
):
cg.add(var.traits.set_min_value(min_value))
cg.add(var.traits.set_max_value(max_value))
cg.add(var.traits.set_step(step))
_add_range(var, config[CONF_RANGE_ID], min_value, max_value, step)
# Skip the setter when the config matches the C++ initializer (DEFAULT_MODE).
# The validated value is the enum key string, not the C++ enum expression.
+7
View File
@@ -17,6 +17,13 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
}
void Number::set_range(const NumberRange *range) {
// Aligned 32-bit loads are safe from PROGMEM on ESP8266 (same as pgm_read_float_aligned).
this->traits.set_min_value(range->min_value);
this->traits.set_max_value(range->max_value);
this->traits.set_step(range->step);
}
void Number::publish_state(float state) {
this->set_has_state(true);
this->state = state;
+3
View File
@@ -40,6 +40,9 @@ class Number : public EntityBase {
NumberTraits traits;
// Out of line so each generated call site loads one pointer instead of three floats.
void set_range(const NumberRange *range);
protected:
friend class NumberCall;
+13
View File
@@ -2,6 +2,7 @@
#include <cmath>
#include <cstdint>
#include <type_traits>
namespace esphome::number {
@@ -11,6 +12,18 @@ enum NumberMode : uint8_t {
NUMBER_MODE_SLIDER = 2,
};
/// Boundaries and step for a number; codegen shares one PROGMEM table between numbers with the same values.
struct NumberRange {
float min_value;
float max_value;
float step;
};
// Read straight from flash on ESP8266, so every field must stay a word.
static_assert(std::is_same_v<decltype(NumberRange::min_value), float>, "NumberRange fields must stay float");
static_assert(std::is_same_v<decltype(NumberRange::max_value), float>, "NumberRange fields must stay float");
static_assert(std::is_same_v<decltype(NumberRange::step), float>, "NumberRange fields must stay float");
static_assert(sizeof(NumberRange) == 3 * sizeof(float), "NumberRange is read from flash with word loads");
class NumberTraits {
public:
// Set/get the number value boundaries.
-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 -1
View File
@@ -774,7 +774,7 @@
#endif
#ifdef USE_LIBRETINY
#if defined(USE_BK72XX) || defined(USE_RTL87XX)
#ifdef USE_BK72XX
#define USE_LIBRETINY_CRASH_HANDLER
#endif
#define USE_BK72XX_BLE
-2
View File
@@ -28,7 +28,6 @@ from esphome.const import (
PLATFORM_ESP8266,
PLATFORM_NRF52,
PLATFORM_RP2,
PLATFORM_RTL87XX,
Platform,
)
@@ -65,7 +64,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,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
esp32:
board: esp32dev
number:
- platform: template
id: shared_1
min_value: 0
max_value: 10
step: 1
optimistic: true
- platform: template
id: shared_2
min_value: 0
max_value: 10
step: 1
optimistic: true
- platform: template
id: shared_3
min_value: 0
max_value: 10
step: 1
optimistic: true
- platform: template
id: single
min_value: -5
max_value: 5
step: 0.5
optimistic: true
@@ -3,6 +3,10 @@
from collections.abc import Callable
from pathlib import Path
from esphome.components import number
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, RawExpression
def test_default_mode_is_not_emitted(
generate_main: Callable[[str | Path], str],
@@ -14,3 +18,36 @@ def test_default_mode_is_not_emitted(
assert "auto_number->traits.set_mode(" not in main_cpp
assert "explicit_auto_number->traits.set_mode(" not in main_cpp
assert "box_number->traits.set_mode(number::NUMBER_MODE_BOX);" in main_cpp
def test_range_table_only_for_shared_ranges(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""A range used by enough numbers gets one PROGMEM table; others keep the setters."""
main_cpp = generate_main(component_config_path("range.yaml"))
assert (
"static constexpr number::NumberRange number_numberrange_id[] PROGMEM = "
"{{0.0f, 10.0f, 1.0f}};" in main_cpp
)
assert main_cpp.count("number::NumberRange ") == 1
for var in ("shared_1", "shared_2", "shared_3"):
assert f"{var}->set_range(number_numberrange_id);" in main_cpp
assert "single->set_range(" not in main_cpp
assert "single->traits.set_min_value(-5.0f);" in main_cpp
assert "single->traits.set_max_value(5.0f);" in main_cpp
assert "single->traits.set_step(0.5f);" in main_cpp
def test_range_with_lambda_bounds_uses_setters() -> None:
"""Runtime bounds (lvgl arc/bar lambdas) can't go in a PROGMEM table."""
var = MockObj("lambda_number", "->")
range_id = ID("lambda_range", is_declaration=True, type=number.NumberRange)
number._add_range(var, range_id, RawExpression("get_min()"), 10, 1)
main_cpp = CORE.cpp_main_section
assert "lambda_number->traits.set_min_value(get_min());" in main_cpp
assert "lambda_number->traits.set_max_value(10);" in main_cpp
assert "lambda_number->traits.set_step(1);" in main_cpp
assert "set_range(" not in main_cpp
@@ -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);
@@ -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
+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,
)
@@ -6192,7 +6192,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()