Compare commits

..
Author SHA1 Message Date
J. Nick Koston 43c18c9e1c Skip the table for an empty init sequence; test it 2026-10-03 05:52:13 -05:00
J. Nick Koston 96df32024d [core] Let shared_progmem_array emit an unshared array
Identical lambdas can carry separate static state, so callers need a way
to keep such tables apart while still getting a collision free name.
2026-10-02 21:24:29 -05:00
J. Nick Koston 83b38d9cdd [st7701s] Keep the init sequence in a shared PROGMEM table 2026-10-02 20:55:04 -05:00
J. Nick Koston ebef8390aa [core] Emit shared PROGMEM arrays at namespace scope
A setup() local table could only be reused from the scope of its first
use. The arrays are now globals. A lambda in a table only references
variables that codegen has already declared, so writing the global
straight away keeps the order valid.
2026-10-02 20:45:20 -05:00
J. Nick Koston ad1e5ef9d0 [core] Add cg.shared_progmem_array for deduplicated PROGMEM tables
Emits one PROGMEM array per distinct type and contents and returns the
same variable to later callers. The array name is made unique against
every config id and registered variable, so a generated table can never
collide with a user id.
2026-10-02 20:45:20 -05:00
42 changed files with 170 additions and 332 deletions

No files matched your search

+1 -1
View File
@@ -601,7 +601,7 @@ async def to_code(config: ConfigType) -> None:
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
if key := encryption_config.get(CONF_KEY):
cg.add(var.set_noise_psk(new_psk_progmem(key)))
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else:
# No key provided, but encryption desired
+1 -16
View File
@@ -92,21 +92,6 @@ uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) {
}
}
/// Encodes the MCLK pre-multiplier for REG02 bits 3-4. The coefficient table stores the multiplier as the factor
/// itself (1, 2, 4 or 8), while the register takes 0, 1, 2 or 3 (ES8311 datasheet, register 0x02 MULT_PRE).
static uint8_t encode_pre_mult(uint8_t pre_mult) {
switch (pre_mult) {
case 2:
return 1;
case 4:
return 2;
case 8:
return 3;
default:
return 0;
}
}
const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) {
for (const auto &coefficient : ES8311_COEFFICIENTS) {
if (coefficient.mclk == mclk && coefficient.rate == rate)
@@ -142,7 +127,7 @@ bool ES8311::configure_clock_() {
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, &reg02));
reg02 &= 0x07;
reg02 |= (coefficient->pre_div - 1) << 5;
reg02 |= encode_pre_mult(coefficient->pre_mult) << 3;
reg02 |= coefficient->pre_mult << 3;
ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02));
// Register 0x03
+1 -1
View File
@@ -360,7 +360,7 @@ async def to_code(config: ConfigType) -> None:
# Build time key: the ota keeps its own pointer so safe mode, which
# has no api server, still has it
cg.add_define("USE_OTA_ENCRYPTION")
cg.add(var.set_noise_psk(new_psk_progmem(key)))
cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key)))
elif CONF_ENCRYPTION in api_conf:
# Runtime key: found in the api server, or in preferences in safe mode
cg.add_define("USE_OTA_ENCRYPTION")
+1 -2
View File
@@ -726,8 +726,7 @@ async def to_code(config):
)
]
# constexpr Glyph table in flash; on ESP8266 static_const_array would place it in RAM.
glyphs = cg.progmem_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender)
+1 -2
View File
@@ -309,8 +309,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
// Unknown char, skip
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
if (!this->glyphs_.empty()) {
// Full-width read: a narrowing byte load would fault on a PROGMEM table on ESP8266.
int glyph_width = this->glyphs_[0].advance;
uint8_t glyph_width = this->glyphs_[0].advance;
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
-14
View File
@@ -1,7 +1,5 @@
#pragma once
#include <type_traits>
#include "esphome/core/color.h"
#include "esphome/core/datatypes.h"
#include "esphome/core/defines.h"
@@ -38,18 +36,6 @@ class Glyph final {
int width;
int height;
};
// The glyph table lives in flash, read with plain loads, which ESP8266 only allows for whole words.
static_assert(std::is_same_v<decltype(Glyph::code_point), const uint32_t>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::data), const uint8_t *>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::advance), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_x), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_y), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::width), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::height), int>, "Glyph fields must stay word sized");
#ifdef USE_ESP8266
static_assert(alignof(Glyph) == sizeof(uint32_t), "Glyph fields must stay word sized");
static_assert(sizeof(Glyph) == 7 * sizeof(uint32_t), "Glyph is read from flash with word loads");
#endif
class Font final
#ifdef USE_DISPLAY
@@ -7,23 +7,19 @@ from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
validate_entity_domain,
)
CODEOWNERS = ["@landonr"]
DEPENDENCIES = ["api"]
SUPPORTED_DOMAINS = ["input_number", "number"]
HomeassistantNumber = homeassistant_ns.class_(
"HomeassistantNumber", number.Number, cg.Component
)
CONFIG_SCHEMA = cv.All(
CONFIG_SCHEMA = (
number.number_schema(HomeassistantNumber)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
validate_entity_domain("number", SUPPORTED_DOMAINS),
.extend(cv.COMPONENT_SCHEMA)
)
@@ -1,7 +1,5 @@
#include "homeassistant_number.h"
#include <cstring>
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
@@ -82,19 +80,12 @@ void HomeassistantNumber::control(float value) {
this->publish_state(value);
static constexpr auto NUMBER_SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto INPUT_NUMBER_SERVICE_NAME = StringRef::from_lit("input_number.set_value");
static constexpr char INPUT_PREFIX[] = "input_";
static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantActionRequest resp;
// input_number entities have their own set_value action
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = INPUT_NUMBER_SERVICE_NAME;
} else {
resp.service = NUMBER_SERVICE_NAME;
}
resp.service = SERVICE_NAME;
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
@@ -25,10 +25,11 @@ std::string HttpContainer::get_response_header(const std::string &header_name) {
auto lower = str_lower_case(header_name); // NOLINT
for (const auto &entry : this->response_headers_) {
if (entry.name == lower) {
ESP_LOGV(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
return entry.value;
}
}
ESP_LOGW(TAG, "No header with name %s found", lower.c_str());
return "";
}
@@ -268,11 +268,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
}
/**
* @brief Get the value of a response header by name.
* @param header_name The name of the header to retrieve.
* @return The value of the header if found, otherwise an empty string.
*/
std::string get_response_header(const std::string &header_name);
protected:
@@ -60,11 +60,11 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon
gpio_num_t din_pin_{I2S_GPIO_UNUSED};
i2s_chan_handle_t rx_handle_;
bool pdm_{false};
#if SOC_I2S_SUPPORTS_PDM_RX
i2s_pdm_dsr_t pdm_dsr_{I2S_PDM_DSR_8S};
#endif
bool pdm_{false};
bool correct_dc_offset_;
bool locked_driver_{false};
int32_t dc_offset_prev_input_{0};
+1 -4
View File
@@ -176,10 +176,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
bool bluetooth_on_{false};
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
#ifdef USE_TEXT_SENSOR
// Byte aligned, so it packs after zone_config_ instead of padding before data_callback_
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
#ifdef USE_NUMBER
ESPPreferenceObject pref_; // only used when numbers are in use
@@ -198,6 +194,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
#endif
#ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
LazyCallbackManager<void()> data_callback_;
+1 -1
View File
@@ -35,11 +35,11 @@ class LEDCOutput final : public output::FloatOutput, public Component {
InternalGPIOPin *pin_;
uint8_t channel_{};
uint8_t bit_depth_{};
bool initialized_ = false;
float phase_angle_{0.0f};
float frequency_{};
float duty_{0.0f};
uint32_t last_duty_{UINT32_MAX};
bool initialized_ = false;
};
} // namespace esphome::ledc
-5
View File
@@ -32,7 +32,6 @@ from esphome.storage_json import StorageJSON
from . import gpio # noqa: F401
from .const import (
COMPONENT_BK72XX,
COMPONENT_LN882X,
CONF_GPIO_RECOVER,
CONF_LOGLEVEL,
CONF_SDK_SILENT,
@@ -491,10 +490,6 @@ async def component_to_code(config):
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
# LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c).
if config[CONF_COMPONENT_ID] == COMPONENT_LN882X:
cg.add_build_flag("-Wl,--wrap=pvPortMalloc")
# RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake
# required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220)
# RTL8720C (ambz2) requires FreeRTOS 10.x so this only applies to RTL8710B
@@ -1,13 +0,0 @@
// LN882x: a zero-size pvPortMalloc trips the SDK's malloc-failed LN_ASSERT; ask for 1 byte instead.
#ifdef USE_LN882X
#include <stddef.h>
void *__real_pvPortMalloc(size_t size); // NOLINT(readability-identifier-naming)
void *__wrap_pvPortMalloc(size_t size) { // NOLINT(readability-identifier-naming)
return __real_pvPortMalloc(size == 0 ? 1 : size);
}
#endif // USE_LN882X
-9
View File
@@ -48,7 +48,6 @@ from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
@@ -642,15 +641,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
)
def _not_on_esp8266(config: ConfigType) -> ConfigType:
# ESP8266 does not have enough RAM for LVGL to be practical.
if CORE.is_esp8266:
raise cv.Invalid("LVGL is not supported on ESP8266")
return config
LVGL_SCHEMA = cv.All(
_not_on_esp8266,
container_schema(obj_spec, LVGL_TOP_LEVEL_SCHEMA),
cv.has_at_most_one_key(CONF_PAGES, df.CONF_LAYOUT),
add_hello_world,
+2 -2
View File
@@ -1037,7 +1037,7 @@ void lv_mem_init() {}
void lv_mem_deinit() {}
#ifndef USE_ESP32
#if defined(USE_HOST) || defined(USE_RP2) || defined(USE_ESP8266)
void *lv_malloc_core(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
@@ -1053,7 +1053,7 @@ static void *lv_alloc_draw_buf(size_t size, bool internal) {
return malloc(size); // NOLINT
}
#else
#elif defined(USE_ESP32)
static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
static void *lv_alloc_draw_buf(size_t size, bool internal) {
@@ -34,7 +34,7 @@ class MicrophoneSource final {
*/
public:
MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor, bool passive)
: mic_(mic), gain_factor_(gain_factor), passive_(passive), bits_per_sample_(bits_per_sample) {}
: mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor), passive_(passive) {}
/// @brief Enables a channel to be processed through the callback.
///
@@ -81,11 +81,11 @@ class MicrophoneSource final {
std::shared_ptr<std::vector<uint8_t>> processed_samples_;
Microphone *mic_;
uint8_t bits_per_sample_;
std::bitset<8> channels_;
int32_t gain_factor_;
bool enabled_{false};
bool passive_; // Only pass audio if ``mic_`` is already running
uint8_t bits_per_sample_;
};
} // namespace esphome::microphone
@@ -107,12 +107,12 @@ class SourceSpeaker final : public speaker::Speaker, public Component {
bool stop_gracefully_{false};
bool pause_state_{false};
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
esp_audio_libs::gain::GainRamp ducking_ramp_;
std::atomic<uint32_t> pending_playback_frames_{0};
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
EventGroupHandle_t event_group_{nullptr};
uint32_t stopping_start_ms_{0};
+11 -5
View File
@@ -5,11 +5,12 @@ from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ENCRYPTION, CONF_KEY
from esphome.core import CORE
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
DOMAIN = "noise"
# Keep in sync with platformio.ini and esphome/idf_component.yml.
# LIBSODIUM_VERSION must match the version noise-c pins in its manifests.
@@ -73,13 +74,18 @@ def static_encryption_key(conf: ConfigType) -> str | None:
return (conf.get(CONF_ENCRYPTION) or {}).get(CONF_KEY) or None
def new_psk_progmem(key: str) -> MockObj:
def new_psk_progmem(parent_id: ID, key: str) -> MockObj:
"""Emit the decoded key as a PROGMEM array; the component keeps a pointer
so the key never occupies RAM. Components sharing one key (api and ota)
share the array."""
return cg.shared_progmem_array(
"noise_psk", cg.uint8, list(decode_encryption_key(key))
)
decoded = decode_encryption_key(key)
arrays: dict[bytes, MockObj] = CORE.data.setdefault(DOMAIN, {})
if (array := arrays.get(decoded)) is None:
array = arrays[decoded] = cg.progmem_array(
ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8),
list(decoded),
)
return array
def encryption_schema(config: ConfigType | None) -> ConfigType:
+17 -23
View File
@@ -10,9 +10,8 @@ namespace esphome::ring_buffer {
static const char *const TAG = "ring_buffer";
RingBuffer::~RingBuffer() {
// create() only builds the ring buffer once storage_ is allocated
if (this->storage_ != nullptr) {
vRingbufferDelete(this->handle_());
if (this->handle_ != nullptr) {
vRingbufferDelete(this->handle_);
RAMAllocator<uint8_t> allocator;
allocator.deallocate(this->storage_, this->size_);
}
@@ -32,12 +31,7 @@ std::unique_ptr<RingBuffer> RingBuffer::create(size_t len, MemoryPreference pref
return nullptr;
}
// handle_() assumes the returned handle is &structure_; NULL means the arguments were rejected
if (xRingbufferCreateStatic(rb->size_, RINGBUF_TYPE_BYTEBUF, rb->storage_, &rb->structure_) != rb->handle_()) {
allocator.deallocate(rb->storage_, rb->size_);
rb->storage_ = nullptr;
return nullptr;
}
rb->handle_ = xRingbufferCreateStatic(rb->size_, RINGBUF_TYPE_BYTEBUF, rb->storage_, &rb->structure_);
ESP_LOGD(TAG, "Created ring buffer with size %u", len);
return rb;
@@ -45,16 +39,16 @@ std::unique_ptr<RingBuffer> RingBuffer::create(size_t len, MemoryPreference pref
void *RingBuffer::receive_acquire(size_t &length, size_t max_length, TickType_t ticks_to_wait) {
length = 0;
void *buffer_data = xRingbufferReceiveUpTo(this->handle_(), &length, ticks_to_wait, max_length);
void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &length, ticks_to_wait, max_length);
return buffer_data;
}
void RingBuffer::receive_release(void *item) { vRingbufferReturnItem(this->handle_(), item); }
void RingBuffer::receive_release(void *item) { vRingbufferReturnItem(this->handle_, item); }
size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) {
size_t bytes_read = 0;
void *buffer_data = xRingbufferReceiveUpTo(this->handle_(), &bytes_read, ticks_to_wait, len);
void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &bytes_read, ticks_to_wait, len);
if (buffer_data == nullptr) {
return 0;
@@ -62,14 +56,14 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) {
std::memcpy(data, buffer_data, bytes_read);
vRingbufferReturnItem(this->handle_(), buffer_data);
vRingbufferReturnItem(this->handle_, buffer_data);
if (bytes_read < len) {
// Data may have wrapped around, so read a second time to receive the remainder
size_t follow_up_bytes_read = 0;
size_t bytes_remaining = len - bytes_read;
buffer_data = xRingbufferReceiveUpTo(this->handle_(), &follow_up_bytes_read, 0, bytes_remaining);
buffer_data = xRingbufferReceiveUpTo(this->handle_, &follow_up_bytes_read, 0, bytes_remaining);
if (buffer_data == nullptr) {
return bytes_read;
@@ -77,7 +71,7 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) {
std::memcpy((void *) ((uint8_t *) (data) + bytes_read), buffer_data, follow_up_bytes_read);
vRingbufferReturnItem(this->handle_(), buffer_data);
vRingbufferReturnItem(this->handle_, buffer_data);
bytes_read += follow_up_bytes_read;
}
@@ -95,13 +89,13 @@ size_t RingBuffer::write(const void *data, size_t len) {
size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait,
bool write_partial) {
if (!xRingbufferSend(this->handle_(), data, len, ticks_to_wait)) {
if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) {
if (!write_partial) {
return 0; // Not enough space available and not allowed to write partial data
}
// Couldn't fit all the data, write what will fit
size_t free = std::min(this->free(), len);
if (xRingbufferSend(this->handle_(), data, free, 0)) {
if (xRingbufferSend(this->handle_, data, free, 0)) {
return free;
}
return 0;
@@ -111,11 +105,11 @@ size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickT
size_t RingBuffer::available() const {
UBaseType_t ux_items_waiting = 0;
vRingbufferGetInfo(this->handle_(), nullptr, nullptr, nullptr, nullptr, &ux_items_waiting);
vRingbufferGetInfo(this->handle_, nullptr, nullptr, nullptr, nullptr, &ux_items_waiting);
return ux_items_waiting;
}
size_t RingBuffer::free() const { return xRingbufferGetCurFreeSize(this->handle_()); }
size_t RingBuffer::free() const { return xRingbufferGetCurFreeSize(this->handle_); }
BaseType_t RingBuffer::reset() {
// Discards all the available data
@@ -125,15 +119,15 @@ BaseType_t RingBuffer::reset() {
bool RingBuffer::discard_bytes_(size_t discard_bytes) {
size_t bytes_read = 0;
void *buffer_data = xRingbufferReceiveUpTo(this->handle_(), &bytes_read, 0, discard_bytes);
void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &bytes_read, 0, discard_bytes);
if (buffer_data != nullptr)
vRingbufferReturnItem(this->handle_(), buffer_data);
vRingbufferReturnItem(this->handle_, buffer_data);
if (bytes_read < discard_bytes) {
size_t wrapped_bytes_read = 0;
buffer_data = xRingbufferReceiveUpTo(this->handle_(), &wrapped_bytes_read, 0, discard_bytes - bytes_read);
buffer_data = xRingbufferReceiveUpTo(this->handle_, &wrapped_bytes_read, 0, discard_bytes - bytes_read);
if (buffer_data != nullptr) {
vRingbufferReturnItem(this->handle_(), buffer_data);
vRingbufferReturnItem(this->handle_, buffer_data);
bytes_read += wrapped_bytes_read;
}
}
+1 -3
View File
@@ -115,9 +115,7 @@ class RingBuffer {
/// @return True if all bytes were successfully discarded, false otherwise
bool discard_bytes_(size_t discard_bytes);
// xRingbufferCreateStatic() returns the static structure itself as the handle
RingbufHandle_t handle_() const { return const_cast<StaticRingbuffer_t *>(&this->structure_); }
RingbufHandle_t handle_{nullptr};
StaticRingbuffer_t structure_;
uint8_t *storage_{nullptr};
size_t size_{0};
+2 -2
View File
@@ -82,14 +82,14 @@ class Rtttl final : public Component {
uint16_t note_duration_{0};
/// The duration of a whole note in milliseconds.
uint16_t wholenote_duration_;
/// The current state of the RTTTL player.
State state_{State::STOPPED};
/// The time in milliseconds since microcontroller boot when the last note was started.
uint32_t last_note_start_time_;
/// The frequency of the current note in Hz.
uint32_t output_freq_{0};
/// The gain of the output.
float gain_{0.6f};
/// The current state of the RTTTL player.
State state_{State::STOPPED};
#ifdef USE_OUTPUT
/// The output to write the sound to.
+1 -3
View File
@@ -308,9 +308,6 @@ class SendspinHub final : public Component,
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
bool task_stack_in_psram_{false};
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
bool mdns_advertised_{false}; // Last state requested from mdns
#endif
// Requested client state, applied from loop(). Empty until the switch restores its state.
std::optional<bool> enabled_;
@@ -323,6 +320,7 @@ class SendspinHub final : public Component,
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
mdns::MDNSComponent *mdns_{nullptr};
bool mdns_advertised_{false}; // Last state requested from mdns
#endif
};
+1 -1
View File
@@ -127,9 +127,9 @@ class Speaker {
#endif
State state_{STATE_STOPPED};
bool mute_state_{false};
audio::AudioStreamInfo audio_stream_info_;
float volume_{1.0f};
bool mute_state_{false};
#ifdef USE_AUDIO_DAC
audio_dac::AudioDac *audio_dac_{nullptr};
+3 -1
View File
@@ -181,7 +181,9 @@ async def to_code(config: ConfigType) -> None:
sequence = []
for seq in config[CONF_INIT_SEQUENCE]:
sequence.extend(seq)
cg.add(var.set_init_sequence(sequence))
if sequence:
table = cg.shared_progmem_array("st7701s_init_sequence", cg.uint8, sequence)
cg.add(var.set_init_sequence(table, len(sequence)))
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
+2 -2
View File
@@ -144,14 +144,14 @@ void ST7701S::write_sequence_(uint8_t cmd, size_t len, const uint8_t *bytes) {
}
void ST7701S::write_init_sequence_() {
for (size_t i = 0; i != this->init_sequence_.size();) {
for (size_t i = 0; i != this->init_sequence_len_;) {
uint8_t cmd = this->init_sequence_[i++];
size_t len = this->init_sequence_[i++];
if (len == ST7701S_DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
this->write_sequence_(cmd, len, &this->init_sequence_[i]);
this->write_sequence_(cmd, len, this->init_sequence_ + i);
i += len;
ESP_LOGV(TAG, "Command %X, %d bytes", cmd, len);
if (cmd == SW_RESET_CMD)
+7 -2
View File
@@ -62,7 +62,10 @@ class ST7701S final : public display::Display,
void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; }
void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; }
void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; }
void set_init_sequence(const std::vector<uint8_t> &init_sequence) { this->init_sequence_ = init_sequence; }
void set_init_sequence(const uint8_t *init_sequence, size_t len) {
this->init_sequence_ = init_sequence;
this->init_sequence_len_ = len;
}
void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; }
void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; }
void set_offsets(int16_t offset_x, int16_t offset_y) {
@@ -95,7 +98,9 @@ class ST7701S final : public display::Display,
uint16_t vsync_pulse_width_ = 10;
uint16_t vsync_back_porch_ = 10;
uint16_t vsync_front_porch_ = 10;
std::vector<uint8_t> init_sequence_;
// Shared PROGMEM table
const uint8_t *init_sequence_{nullptr};
size_t init_sequence_len_{0};
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
bool pclk_inverted_{true};
@@ -25,8 +25,8 @@ class TemplateNumber final : public number::Number, public PollingComponent {
protected:
void control(float value) override;
bool optimistic_{false};
bool restore_value_{false};
float initial_value_{NAN};
bool restore_value_{false};
Trigger<float> set_trigger_;
TemplateLambda<float> f_;
@@ -44,7 +44,7 @@ enum VoiceAssistantFeature : uint32_t {
FEATURE_MULTI_CHANNEL_AUDIO = 1 << 6,
};
enum class State : uint8_t {
enum class State {
IDLE,
START_MICROPHONE,
STARTING_MICROPHONE,
@@ -96,7 +96,7 @@ struct Configuration {
};
#ifdef USE_MEDIA_PLAYER
enum class MediaPlayerResponseState : uint8_t {
enum class MediaPlayerResponseState {
IDLE,
URL_SENT,
PLAYING,
@@ -237,7 +237,6 @@ class VoiceAssistant final : public Component {
void set_state_(State state);
void set_state_(State state, State desired_state);
bool start_udp_socket_();
void signal_stop_();
void start_playback_timeout_();
@@ -281,6 +280,8 @@ class VoiceAssistant final : public Component {
Trigger<Timer> timer_updated_trigger_;
Trigger<Timer> timer_cancelled_trigger_;
Trigger<const std::vector<Timer> &> timer_tick_trigger_;
bool has_timers_{false};
bool timer_tick_running_{false};
microphone::MicrophoneSource *mic_source_{nullptr};
microphone::MicrophoneSource *mic_source2_{nullptr};
@@ -291,12 +292,19 @@ class VoiceAssistant final : public Component {
size_t speaker_buffer_index_{0};
size_t speaker_buffer_size_{0};
size_t speaker_bytes_received_{0};
bool wait_for_stream_end_{false};
bool stream_ended_{false};
#endif
#ifdef USE_MEDIA_PLAYER
media_player::MediaPlayer *media_player_{nullptr};
std::string tts_response_url_;
bool started_streaming_tts_{false};
MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE};
#endif
bool local_output_{false};
std::string conversation_id_;
std::string wake_word_;
@@ -316,37 +324,29 @@ class VoiceAssistant final : public Component {
// prolonged one can be detected and stopped; 0 means no imbalance is currently being timed.
uint32_t audio_channel_stall_start_{0};
bool use_wake_word_;
uint8_t noise_suppression_level_;
uint8_t auto_gain_;
float volume_multiplier_;
uint32_t conversation_timeout_;
bool continuous_{false};
bool silence_detection_;
bool continue_conversation_{false};
State state_{State::IDLE};
State desired_state_{State::IDLE};
AudioMode audio_mode_{AUDIO_MODE_UDP};
bool udp_socket_running_{false};
bool start_udp_socket_();
Configuration config_{};
#ifdef USE_MICRO_WAKE_WORD
micro_wake_word::MicroWakeWord *micro_wake_word_{nullptr};
#endif
// 1 byte members grouped at the end so they share padding instead of each taking a word
State state_{State::IDLE};
State desired_state_{State::IDLE};
AudioMode audio_mode_{AUDIO_MODE_UDP};
bool has_timers_{false};
bool timer_tick_running_{false};
#ifdef USE_SPEAKER
bool wait_for_stream_end_{false};
bool stream_ended_{false};
#endif
#ifdef USE_MEDIA_PLAYER
MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE};
bool started_streaming_tts_{false};
#endif
bool local_output_{false};
bool use_wake_word_;
uint8_t noise_suppression_level_;
uint8_t auto_gain_;
bool continuous_{false};
bool silence_detection_;
bool continue_conversation_{false};
bool udp_socket_running_{false};
};
extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -1,11 +0,0 @@
esphome:
name: test
esp8266:
board: esp01_1m
font:
- file: ../NotoSans-Regular.ttf
id: noto
size: 12
glyphs: "AB"
+10 -20
View File
@@ -4,10 +4,8 @@ Focuses on verifying that long multi-byte (Chinese/CJK) glyph strings
are correctly processed through the font configuration pipeline.
"""
from collections.abc import Callable
import functools
from pathlib import Path
import re
from unittest.mock import MagicMock, patch
import pytest
@@ -262,12 +260,15 @@ def mock_cg():
with (
patch("esphome.components.font.cg.add_define") as mock_define,
patch("esphome.components.font.cg.progmem_array") as mock_progmem,
patch("esphome.components.font.cg.static_const_array") as mock_static,
patch("esphome.components.font.cg.new_Pvariable") as mock_new_pvar,
):
mock_progmem.return_value = MagicMock()
mock_static.return_value = MagicMock()
yield {
"add_define": mock_define,
"progmem_array": mock_progmem,
"static_const_array": mock_static,
"new_Pvariable": mock_new_pvar,
}
@@ -287,13 +288,13 @@ async def test_to_code_long_latin_generates_all_glyphs(mock_cg):
mock_cg["add_define"].assert_any_call("USE_FONT")
# progmem_array receives the combined bitmap data (non-empty)
# bitmap data first, then the glyph table
assert mock_cg["progmem_array"].call_count == 2
bitmap_data = mock_cg["progmem_array"].call_args_list[0].args[1]
mock_cg["progmem_array"].assert_called_once()
bitmap_data = mock_cg["progmem_array"].call_args.args[1]
assert len(bitmap_data) > 0
# the glyph table receives one entry per unique glyph
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
# static_const_array receives one entry per unique glyph
mock_cg["static_const_array"].assert_called_once()
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
assert len(glyph_initializer) == glyph_count
# new_Pvariable is called with the correct glyph count
@@ -313,7 +314,7 @@ async def test_to_code_glyph_entries_contain_expected_fields(mock_cg):
await to_code(config)
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
for entry in glyph_initializer:
assert len(entry) == 7, f"Glyph entry should have 7 fields, got {len(entry)}"
codepoint = entry[0]
@@ -331,17 +332,6 @@ async def test_to_code_glyphs_sorted_by_utf8(mock_cg):
await to_code(config)
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
codepoints = [entry[0] for entry in glyph_initializer]
assert codepoints == sorted(codepoints)
def test_glyph_table_is_progmem(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The constexpr glyph table goes to flash, not a RAM static const array."""
main_cpp = generate_main(component_config_path("glyph_table.yaml"))
assert re.search(r"static constexpr font::Glyph \w+\[\] PROGMEM = ", main_cpp)
assert "static const font::Glyph" not in main_cpp
@@ -1,32 +0,0 @@
"""Tests for the homeassistant number platform."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.config import read_config
from esphome.core import CORE
VALID_CONFIG = "tests/component_tests/homeassistant/test_number.yaml"
INVALID_CONFIG = "tests/component_tests/homeassistant/test_number_invalid_domain.yaml"
def test_number_and_input_number_entities(
generate_main: Callable[[str | Path], str],
) -> None:
"""Both number and input_number entity IDs are accepted and passed to the C++ class."""
main_cpp = generate_main(VALID_CONFIG)
assert 'ha_number->set_entity_id("number.some_number");' in main_cpp
assert 'ha_input_number->set_entity_id("input_number.some_input");' in main_cpp
def test_number_rejects_unsupported_domain(
capsys: pytest.CaptureFixture[str],
) -> None:
"""An entity ID from another domain fails config validation."""
CORE.config_path = Path(INVALID_CONFIG)
assert read_config({}) is None
assert "is not supported by the number platform" in capsys.readouterr().out
@@ -1,17 +0,0 @@
---
esphome:
name: test
host:
logger:
api:
number:
- platform: homeassistant
entity_id: number.some_number
id: ha_number
- platform: homeassistant
entity_id: input_number.some_input
id: ha_input_number
@@ -1,17 +0,0 @@
---
esphome:
name: test
host:
logger:
api:
number:
- platform: homeassistant
entity_id: number.some_number
id: ha_number
- platform: homeassistant
entity_id: sensor.foo
id: ha_other_number
@@ -1,14 +0,0 @@
"""LVGL rejects ESP8266 at validation time."""
import pytest
from esphome.components import lvgl
from esphome.config_validation import Invalid
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def test_esp8266_is_rejected(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP8266_ARDUINO)
with pytest.raises(Invalid, match="not supported on ESP8266"):
lvgl.CONFIG_SCHEMA({})
@@ -560,7 +560,7 @@ def test_encryption_offer_codegen(
own_key = encrypted and "USE_OTA_ENCRYPTION_PROVISIONED" not in defines_present
assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is own_key
# The api shares the ota's array instead of emitting the same key twice
assert main_cpp.count("noise_psk[] PROGMEM") == (1 if own_key else 0)
assert main_cpp.count("_psk[] PROGMEM") == (1 if own_key else 0)
assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present)
# The noise transport source compiles only when the define is set
assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"])
@@ -0,0 +1,68 @@
"""Tests for the st7701s init sequence codegen."""
from collections.abc import Callable
from pathlib import Path
import re
_YAML = """
esphome:
name: st7701s-test
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
psram:
mode: octal
spi:
clk_pin: 10
mosi_pin: 11
display:
- platform: st7701s
id: panel
dimensions:
width: 480
height: 480
cs_pin: 44
de_pin: 18
hsync_pin: 16
vsync_pin: 17
pclk_pin: 21
init_sequence: {init_sequence}
data_pins: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 14, 15, 38, 39]
"""
def test_init_sequence_is_progmem_table(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
) -> None:
"""The init sequence is a PROGMEM table instead of a vector copied at boot."""
yaml_file = tmp_path / "st7701s.yaml"
yaml_file.write_text(
_YAML.format(init_sequence="[1, [0x23, 0xA, 0xB], delay 20ms]")
)
main_cpp = generate_main(yaml_file)
call = re.search(r"panel->set_init_sequence\((\w+), (\d+)\);", main_cpp)
assert call is not None
table = re.search(
rf"static constexpr uint8_t {call.group(1)}\[\] PROGMEM = \{{([^;]*)\}};",
main_cpp,
)
assert table is not None
assert len(table.group(1).split(",")) == int(call.group(2))
def test_empty_init_sequence_emits_no_table(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
) -> None:
"""An empty init sequence leaves the display without a table instead of a zero length array."""
yaml_file = tmp_path / "st7701s.yaml"
yaml_file.write_text(_YAML.format(init_sequence="[]"))
main_cpp = generate_main(yaml_file)
assert "st7701s_init_sequence" not in main_cpp
assert "set_init_sequence" not in main_cpp
@@ -71,9 +71,6 @@ number:
- platform: homeassistant
entity_id: number.hello_world
id: ha_hello_world_number
- platform: homeassistant
entity_id: input_number.hello_world
id: ha_hello_world_input_number
sensor:
- platform: homeassistant
@@ -21,12 +21,6 @@ api:
- number.set:
id: ha_number
value: 42.5
- number.set:
id: ha_real_number
value: 12.5
- number.set:
id: ha_real_input_number
value: 7
- switch.turn_on: ha_switch
- switch.turn_off: ha_switch
- logger.log: "=== All tests completed ==="
@@ -135,13 +129,6 @@ text_sensor:
# Number component for testing HA number control
number:
# Real homeassistant numbers: the action used depends on the entity domain
- platform: homeassistant
id: ha_real_number
entity_id: number.test_real_number
- platform: homeassistant
id: ha_real_input_number
entity_id: input_number.test_real_input_number
- platform: template
name: "HA Controlled Number"
id: ha_number
@@ -86,15 +86,6 @@ async def test_api_homeassistant(
"nonexistent.action_for_error_test": loop.create_future(), # error_test_call
}
# Futures keyed by (service, entity_id) for the real homeassistant numbers
ha_real_number_futures = {
("number.set_value", "number.test_real_number"): loop.create_future(),
(
"input_number.set_value",
"input_number.test_real_input_number",
): loop.create_future(),
}
# Future for error message test
action_error_received_future = loop.create_future()
@@ -111,10 +102,6 @@ async def test_api_homeassistant(
if not future.done():
future.set_result(service_call)
key = (service_call.service, service_call.data.get("entity_id"))
if key in ha_real_number_futures and not ha_real_number_futures[key].done():
ha_real_number_futures[key].set_result(service_call)
# Immediately respond to the error test call so the test can proceed
# This needs to happen synchronously so ESPHome receives the response
# before logging "=== All tests completed ==="
@@ -214,16 +201,6 @@ async def test_api_homeassistant(
# Empty state with valid entity should work (use different entity to not interfere with test)
client.send_home_assistant_state("sensor.edge_case_empty_state", "", "")
# The real homeassistant numbers reject values outside min/max, which
# default to 0 until Home Assistant provides them
for entity in (
"number.test_real_number",
"input_number.test_real_input_number",
):
client.send_home_assistant_state(entity, "min", "0")
client.send_home_assistant_state(entity, "max", "100")
client.send_home_assistant_state(entity, "step", "0.5")
# List entities and services
_, services = await client.list_entities_services()
@@ -357,21 +334,6 @@ async def test_api_homeassistant(
# The value might be formatted with trailing zeros
assert float(number_call.data["value"]) == 42.5
# 7b. Real homeassistant numbers pick the action from the entity domain
real_number_call = await asyncio.wait_for(
ha_real_number_futures[("number.set_value", "number.test_real_number")],
timeout=2.0,
)
assert float(real_number_call.data["value"]) == 12.5
real_input_number_call = await asyncio.wait_for(
ha_real_number_futures[
("input_number.set_value", "input_number.test_real_input_number")
],
timeout=2.0,
)
assert float(real_input_number_call.data["value"]) == 7
# 8. HA Switch service calls
switch_on_call = await asyncio.wait_for(
service_call_futures["switch.turn_on"], timeout=2.0