mirror of
https://github.com/esphome/esphome.git
synced 2026-07-08 16:05:34 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69c6b3e38d | |||
| b4ad0eb86b | |||
| 7f0e826c32 | |||
| 76ee3fe887 | |||
| af4a6e7ec3 | |||
| 40c3a4320f | |||
| 3c2dad67f4 | |||
| f823a23ea4 | |||
| 9aed1d2700 | |||
| 9857d508d9 | |||
| ad7c980c4b |
@@ -240,12 +240,13 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
}
|
||||
|
||||
void APIServer::dump_config() {
|
||||
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Server:\n"
|
||||
" Address: %s:%u\n"
|
||||
" Listen backlog: %u\n"
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
|
||||
network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
|
||||
@@ -113,7 +113,9 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
|
||||
elif file_type in ("mp3", "mpeg", "mpga"):
|
||||
elif file_type in ("mp1", "mp2", "mp3", "mpeg", "mpga"):
|
||||
# With puremagic >=2.0 this can cause some MP3 (Layer III) files to be labeled as "mp1"/"mp2".
|
||||
# Treat those labels as MP3 so we still pick the MP3 decoder.
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
|
||||
elif file_type == "flac":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
|
||||
|
||||
@@ -109,7 +109,9 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
|
||||
CONF_SIGNING_KEY = "signing_key"
|
||||
@@ -167,6 +169,20 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
}
|
||||
|
||||
# NVS encryption (HMAC peripheral scheme) is only available on variants that
|
||||
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
|
||||
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
|
||||
# should be added here.
|
||||
NVS_ENCRYPTION_HMAC_VARIANTS = {
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
}
|
||||
|
||||
COMPILER_OPTIMIZATIONS = {
|
||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
|
||||
@@ -1349,6 +1365,29 @@ def final_validate(config):
|
||||
"Binaries will NOT be signed automatically during build. "
|
||||
"You must sign them externally before flashing."
|
||||
)
|
||||
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
|
||||
variant = config[CONF_VARIANT]
|
||||
if variant in NVS_ENCRYPTION_HMAC_VARIANTS:
|
||||
_LOGGER.warning(
|
||||
"NVS encryption will burn an HMAC key into eFuse key block %d on the "
|
||||
"first boot of each device. This is PERMANENT and IRREVERSIBLE: "
|
||||
"the block cannot be erased or reused afterwards. Enabling (or "
|
||||
"later disabling) encryption also wipes any previously saved "
|
||||
"preferences once, because the older data can no longer be read.",
|
||||
nvs_enc[CONF_KEY_ID],
|
||||
)
|
||||
else:
|
||||
supported = ", ".join(
|
||||
sorted(VARIANT_FRIENDLY[v] for v in NVS_ENCRYPTION_HMAC_VARIANTS)
|
||||
)
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"NVS encryption (HMAC scheme) is not supported on "
|
||||
f"{VARIANT_FRIENDLY[variant]} (it has no HMAC peripheral). "
|
||||
f"Supported variants: {supported}.",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_ENCRYPTION],
|
||||
)
|
||||
)
|
||||
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
|
||||
project = full_config[CONF_ESPHOME].get(CONF_PROJECT)
|
||||
errs.extend(
|
||||
@@ -1609,6 +1648,15 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
),
|
||||
cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema(
|
||||
{
|
||||
# eFuse key block (0-5) that stores the HMAC key from
|
||||
# which the NVS encryption keys are derived. The block is
|
||||
# written on first boot if empty -- an irreversible
|
||||
# operation -- so it must be chosen explicitly.
|
||||
cv.Required(CONF_KEY_ID): cv.int_range(min=0, max=5),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||
): cv.boolean,
|
||||
@@ -2451,6 +2499,20 @@ async def to_code(config):
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
# Encrypt NVS using the HMAC peripheral scheme. The NVS encryption keys are
|
||||
# derived at runtime from an HMAC key stored in the configured eFuse block
|
||||
# (no flash encryption required). The HMAC key is generated and burned into
|
||||
# the eFuse block on first boot if it is empty. With the scheme selected,
|
||||
# nvs_sec_provider registers it at startup and the default nvs_flash_init()
|
||||
# (used in esp32/preferences.cpp) transparently performs the secure init, so
|
||||
# no C++ changes are needed.
|
||||
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID", nvs_enc[CONF_KEY_ID]
|
||||
)
|
||||
|
||||
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
|
||||
|
||||
cg.add_define(
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#else
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
#include <cinttypes>
|
||||
extern "C" {
|
||||
#include <esp_hosted.h>
|
||||
#include <esp_hosted_misc.h>
|
||||
@@ -33,6 +35,19 @@ namespace esphome::esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
// Bringing up the remote BT controller issues synchronous RPCs to the
|
||||
// co-processor with 5 second response timeouts, and the default task watchdog
|
||||
// is also 5 seconds. If the co-processor firmware does not answer (for example
|
||||
// factory firmware without Bluetooth support), the watchdog would reboot the
|
||||
// device before the RPC could return an error, causing a boot loop. Raise the
|
||||
// watchdog for the duration of the bring-up so failures surface as error
|
||||
// returns instead. 60 seconds covers the worst case: transport reconnect
|
||||
// (up to ~20s), version preflight (1s), controller init/enable (5s each) and
|
||||
// the bluedroid host bring-up over the hosted HCI transport.
|
||||
static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
#endif
|
||||
|
||||
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
|
||||
#define GAP_SCAN_COMPLETE_EVENTS \
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
|
||||
@@ -164,6 +179,9 @@ void ESP32BLE::advertising_init_() {
|
||||
|
||||
bool ESP32BLE::ble_setup_() {
|
||||
esp_err_t err;
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
|
||||
#endif
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
|
||||
// start bt controller
|
||||
@@ -192,15 +210,35 @@ bool ESP32BLE::ble_setup_() {
|
||||
|
||||
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
||||
#else
|
||||
esp_hosted_connect_to_slave(); // NOLINT
|
||||
if (esp_hosted_connect_to_slave() != ESP_OK) { // NOLINT
|
||||
ESP_LOGE(TAG, "Co-processor transport failed; BLE disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast preflight (1 second RPC timeout): verifies the co-processor answers
|
||||
// RPCs at all before the 5 second timeout BT controller RPCs below, and
|
||||
// before hosted_hci_bluedroid_open(), which aborts if the transport is down.
|
||||
esp_hosted_coprocessor_fwver_t fw_ver{};
|
||||
if (esp_hosted_get_coprocessor_fwversion(&fw_ver) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Co-processor not responding; BLE disabled. Update its firmware with the esp32_hosted "
|
||||
"update component");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32, fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
|
||||
if (esp_hosted_bt_controller_init() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
|
||||
ESP_LOGE(TAG,
|
||||
"BT controller init failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
|
||||
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
|
||||
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_enable() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
|
||||
ESP_LOGE(TAG,
|
||||
"BT controller enable failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
|
||||
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
|
||||
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -332,6 +370,10 @@ bool ESP32BLE::ble_setup_() {
|
||||
}
|
||||
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
// Same 5 second RPCs as the bring-up path; see HOSTED_BT_WDT_TIMEOUT_MS
|
||||
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
|
||||
#endif
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
|
||||
@@ -377,12 +419,12 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
}
|
||||
#else
|
||||
if (esp_hosted_bt_controller_disable() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ from esphome.const import (
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
# esp32_ble raises the task watchdog around the remote BT controller bring-up
|
||||
AUTO_LOAD = ["watchdog"]
|
||||
|
||||
CONF_ACTIVE_HIGH = "active_high"
|
||||
CONF_BUS_WIDTH = "bus_width"
|
||||
|
||||
@@ -94,11 +94,12 @@ void ESPHomeOTAComponent::setup() {
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::dump_config() {
|
||||
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Over-The-Air updates:\n"
|
||||
" Address: %s:%u\n"
|
||||
" Version: %d",
|
||||
network::get_use_address(), this->port_, USE_OTA_VERSION);
|
||||
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
if (!this->password_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Password configured");
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.network import add_use_address, ip_address_literal
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -543,7 +543,7 @@ async def to_code(config):
|
||||
await _to_code_rp2040(var, config)
|
||||
|
||||
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
add_use_address(var, config[CONF_USE_ADDRESS])
|
||||
# enable_on_boot defaults to true in C++ - only set if false
|
||||
if not config[CONF_ENABLE_ON_BOOT]:
|
||||
cg.add(var.set_enable_on_boot(False))
|
||||
|
||||
@@ -145,6 +145,8 @@ class EthernetComponent final : public Component {
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
network::IPAddress get_dns_address(uint8_t num);
|
||||
/// Returns nullptr when no explicit use_address is configured and the address is
|
||||
/// derived at runtime from the device name (see network::get_use_address_to()).
|
||||
const char *get_use_address() const { return this->use_address_; }
|
||||
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
@@ -346,7 +348,7 @@ class EthernetComponent final : public Component {
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
const char *use_address_{nullptr};
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
UNIT_EMPTY,
|
||||
UNIT_KELVIN,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_LITRE_PER_HOUR,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@cfeenstra1024"]
|
||||
@@ -37,7 +38,6 @@ CONF_TEMP2 = "temp2"
|
||||
CONF_TEMP_DIFF = "temp_diff"
|
||||
|
||||
UNIT_GIGA_JOULE = "GJ"
|
||||
UNIT_LITRE_PER_HOUR = "l/h"
|
||||
|
||||
# Note: The sensor units are set automatically based un the received data from the meter
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -213,17 +213,19 @@ LightColorValues LightCall::validate_() {
|
||||
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
|
||||
bool explicit_turn_off_request = this->has_state() && !this->state_;
|
||||
|
||||
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
|
||||
if (this->has_brightness() && this->brightness_ == 0.0f) {
|
||||
// Treat zero brightness as an implicit turn-off when no state was explicitly requested.
|
||||
if (this->has_brightness() && this->brightness_ == 0.0f && !this->has_state()) {
|
||||
this->state_ = false;
|
||||
this->set_flag_(FLAG_HAS_STATE);
|
||||
if (color_mode & ColorCapability::BRIGHTNESS) {
|
||||
// Reset brightness so the light has nonzero brightness when turned back on.
|
||||
}
|
||||
|
||||
// Make sure a turn-on makes the light visible: if the resulting brightness would be zero
|
||||
// (e.g. restored from a brightness=0 turn-off), reset it to full brightness.
|
||||
if (this->has_state() && this->state_ && (color_mode & ColorCapability::BRIGHTNESS)) {
|
||||
float brightness = this->has_brightness() ? this->brightness_ : this->parent_->remote_values.get_brightness();
|
||||
if (brightness == 0.0f) {
|
||||
this->brightness_ = 1.0f;
|
||||
} else {
|
||||
// Light doesn't support brightness; clear the flag to avoid a spurious
|
||||
// "brightness not supported" warning during capability validation.
|
||||
this->clear_flag_(FLAG_HAS_BRIGHTNESS);
|
||||
this->set_flag_(FLAG_HAS_BRIGHTNESS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -414,6 +414,10 @@ async def to_code(configs):
|
||||
await cg.register_component(lv_component, config)
|
||||
if rotation := config.get(CONF_ROTATION):
|
||||
cg.add(lv_component.set_rotation(rotation))
|
||||
if paused := config[df.CONF_PAUSED]:
|
||||
cg.add(lv_component.set_paused(paused, False))
|
||||
if refr_time := config.get(df.CONF_REFRESH_INTERVAL):
|
||||
cg.add(lv_component.set_refresh_interval(refr_time.total_milliseconds))
|
||||
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
|
||||
|
||||
lv_scr_act = get_screen_active(lv_component)
|
||||
@@ -598,6 +602,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
|
||||
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_REFRESH_INTERVAL): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
@@ -642,6 +647,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
|
||||
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
cv.Optional(df.CONF_PAUSED, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA)
|
||||
|
||||
@@ -758,12 +758,14 @@ CONF_PAD_COLUMN = "pad_column"
|
||||
CONF_PAGE = "page"
|
||||
CONF_PAGE_WRAP = "page_wrap"
|
||||
CONF_PASSWORD_MODE = "password_mode"
|
||||
CONF_PAUSED = "paused"
|
||||
CONF_PIVOT_X = "pivot_x"
|
||||
CONF_PIVOT_Y = "pivot_y"
|
||||
CONF_PLACEHOLDER_TEXT = "placeholder_text"
|
||||
CONF_POINTS = "points"
|
||||
CONF_PREVIOUS = "previous"
|
||||
CONF_RADIUS = "radius"
|
||||
CONF_REFRESH_INTERVAL = "refresh_interval"
|
||||
CONF_REPEAT_COUNT = "repeat_count"
|
||||
CONF_RECOLOR = "recolor"
|
||||
CONF_RESUME_ON_INPUT = "resume_on_input"
|
||||
|
||||
@@ -401,7 +401,10 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
|
||||
}
|
||||
|
||||
void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
|
||||
if (!this->is_paused()) {
|
||||
// no guard here for display busy, since LVGL will not call flush_cb until the refresh timer fires,
|
||||
// and while the display is busy this is reset to 5 minutes. If that expires and the display is still
|
||||
// busy there are bigger problems.
|
||||
if (!this->paused_) {
|
||||
auto now = millis();
|
||||
this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p));
|
||||
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1,
|
||||
@@ -620,20 +623,20 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
|
||||
void LvglComponent::draw_end_() {
|
||||
if (this->draw_end_callback_ != nullptr)
|
||||
this->draw_end_callback_->trigger();
|
||||
// Only reachable once the display is idle again: while busy, the display's refr_timer_ is
|
||||
// paused (see loop()), so LVGL never renders/flushes and this event never fires.
|
||||
if (this->update_when_display_idle_) {
|
||||
for (auto *disp : this->displays_)
|
||||
disp->update();
|
||||
}
|
||||
}
|
||||
|
||||
bool LvglComponent::is_paused() const {
|
||||
if (this->paused_)
|
||||
return true;
|
||||
if (this->update_when_display_idle_) {
|
||||
for (auto *disp : this->displays_) {
|
||||
if (!disp->is_idle())
|
||||
return true;
|
||||
}
|
||||
bool LvglComponent::displays_busy_() const {
|
||||
if (!this->update_when_display_idle_)
|
||||
return false;
|
||||
for (auto *disp : this->displays_) {
|
||||
if (!disp->is_idle())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -777,6 +780,8 @@ void LvglComponent::setup() {
|
||||
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
|
||||
lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this);
|
||||
}
|
||||
this->refr_timer_ = lv_display_get_refr_timer(this->disp_);
|
||||
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
|
||||
#if LV_USE_LOG
|
||||
lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
|
||||
auto next = strchr(buf, ')');
|
||||
@@ -802,21 +807,32 @@ void LvglComponent::update() {
|
||||
}
|
||||
|
||||
void LvglComponent::loop() {
|
||||
if (this->is_paused()) {
|
||||
if (this->paused_ && this->show_snow_)
|
||||
if (this->paused_) {
|
||||
if (this->show_snow_)
|
||||
this->write_random_();
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto now = millis();
|
||||
lv_timer_handler();
|
||||
auto elapsed = millis() - now;
|
||||
if (elapsed > 15) {
|
||||
ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now));
|
||||
}
|
||||
#else
|
||||
lv_timer_handler();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
// Pause/resume the display's own refresh timer to track its busy state. While paused, LVGL
|
||||
// still keeps track of invalidated areas but won't render or flush them, so nothing needs to
|
||||
// be discarded or replayed: once resumed, the accumulated areas are simply drawn as normal.
|
||||
// Input events and other timers keep being processed below regardless of this state.
|
||||
if (this->update_when_display_idle_) {
|
||||
bool busy = this->displays_busy_();
|
||||
if (busy && !this->refr_timer_paused_) {
|
||||
this->refr_timer_paused_ = true;
|
||||
// calling lv_timer_pause() here would be ineffective; LVGL pauses and resumes the timer based on its own internal
|
||||
// state, which is not aware of the display's busy state. Instead, we extend the timer period to avoid it firing
|
||||
// while the display is busy.
|
||||
lv_timer_set_period(this->refr_timer_, 5 * 60 * 1000);
|
||||
} else if (!busy && this->refr_timer_paused_) {
|
||||
this->refr_timer_paused_ = false;
|
||||
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
|
||||
// Don't wait for the timer's next natural period: refresh right away now that the
|
||||
// display is idle again.
|
||||
lv_timer_ready(this->refr_timer_);
|
||||
}
|
||||
}
|
||||
lv_timer_handler();
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -214,9 +214,14 @@ class LvglComponent final : public PollingComponent {
|
||||
// @param paused If true, pause the display. If false, resume the display.
|
||||
// @param show_snow If true, show the snow effect when paused.
|
||||
void set_paused(bool paused, bool show_snow);
|
||||
void set_refresh_interval(uint32_t period) {
|
||||
this->refr_timer_period_ = period;
|
||||
if (this->refr_timer_ != nullptr)
|
||||
lv_timer_set_period(this->refr_timer_, period);
|
||||
}
|
||||
|
||||
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
|
||||
bool is_paused() const;
|
||||
// Returns true if the display has been explicitly paused via set_paused().
|
||||
bool is_paused() const { return this->paused_; }
|
||||
// If the display is paused and we have resume_on_input_ set to true, resume the display.
|
||||
void maybe_wakeup() {
|
||||
if (this->paused_ && this->resume_on_input_) {
|
||||
@@ -299,6 +304,9 @@ class LvglComponent final : public PollingComponent {
|
||||
// Not checking for non-null callback since the
|
||||
// LVGL callback that calls it is not set in that case
|
||||
void draw_start_() const { this->draw_start_callback_->trigger(); }
|
||||
// Returns true if update_when_display_idle is enabled and at least one underlying display
|
||||
// component is currently busy (e.g. mid-refresh).
|
||||
bool displays_busy_() const;
|
||||
|
||||
void write_random_();
|
||||
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
|
||||
@@ -316,6 +324,14 @@ class LvglComponent final : public PollingComponent {
|
||||
|
||||
uint8_t *draw_buf_{};
|
||||
lv_display_t *disp_{};
|
||||
// The display's own periodic refresh timer, effectively paused while the display is busy (see
|
||||
// displays_busy_()) so LVGL neither renders nor flushes to it, without losing track of
|
||||
// invalidated areas. Other timers (indev reading, animations, ...) keep running as normal.
|
||||
lv_timer_t *refr_timer_{};
|
||||
// Tracks whether refr_timer_ is currently paused, so loop() can detect the busy -> idle edge
|
||||
// and kick off an immediate refresh instead of waiting for the timer's next natural period.
|
||||
bool refr_timer_paused_{};
|
||||
uint32_t refr_timer_period_{16};
|
||||
uint16_t width_{};
|
||||
uint16_t height_{};
|
||||
bool paused_{};
|
||||
|
||||
@@ -59,6 +59,19 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
|
||||
return IPAddress(str(ip))
|
||||
|
||||
|
||||
def add_use_address(var: cg.MockObj, use_address: str) -> None:
|
||||
"""Generate a set_use_address() call only when the address must be baked in.
|
||||
|
||||
The default "<name>.local" is not stored in the firmware; it is rebuilt at
|
||||
runtime from the device name (see network::get_use_address_to()), which also
|
||||
picks up the MAC suffix when name_add_mac_suffix is enabled. A compile-time
|
||||
string could never include that suffix, so baking it in would log the wrong
|
||||
address.
|
||||
"""
|
||||
if use_address != f"{CORE.name}.local":
|
||||
cg.add(var.set_use_address(use_address))
|
||||
|
||||
|
||||
def require_high_performance_networking() -> None:
|
||||
"""Request high performance networking for network and WiFi.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_NETWORK
|
||||
|
||||
namespace esphome::network {
|
||||
@@ -20,6 +22,27 @@ bool is_disabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf) {
|
||||
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
|
||||
const char *addr = nullptr;
|
||||
#if defined(USE_ETHERNET)
|
||||
addr = ethernet::global_eth_component->get_use_address();
|
||||
#elif defined(USE_MODEM)
|
||||
addr = modem::global_modem_component->get_use_address();
|
||||
#elif defined(USE_WIFI)
|
||||
addr = wifi::global_wifi_component->get_use_address();
|
||||
#elif defined(USE_OPENTHREAD)
|
||||
addr = openthread::global_openthread_component->get_use_address();
|
||||
#endif
|
||||
if (addr != nullptr && addr[0] != '\0')
|
||||
return addr;
|
||||
// No explicit use_address configured: the address is the runtime device name
|
||||
// (which includes the MAC suffix when name_add_mac_suffix is enabled) plus ".local"
|
||||
const auto &name = App.get_name();
|
||||
make_name_with_suffix_to(buf.data(), buf.size(), name.c_str(), name.size(), '.', "local", 5);
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
network::IPAddresses get_ip_addresses() {
|
||||
#ifdef USE_ETHERNET
|
||||
if (ethernet::global_eth_component != nullptr)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "ip_address.h"
|
||||
@@ -53,30 +54,12 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() {
|
||||
|
||||
/// Return whether the network is disabled (only wifi for now)
|
||||
bool is_disabled();
|
||||
/// Get the active network hostname
|
||||
ESPHOME_ALWAYS_INLINE inline const char *get_use_address() {
|
||||
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
|
||||
#ifdef USE_ETHERNET
|
||||
return ethernet::global_eth_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#ifdef USE_MODEM
|
||||
return modem::global_modem_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI
|
||||
return wifi::global_wifi_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#ifdef USE_OPENTHREAD
|
||||
return openthread::global_openthread_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
|
||||
// Fallback when no network component is defined (e.g., host platform)
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
/// Buffer size for get_use_address_to(): 63-char DNS label + ".local" + null terminator
|
||||
static constexpr size_t USE_ADDRESS_BUFFER_SIZE = 70;
|
||||
/// Get the active network address for logging. Returns the explicitly configured
|
||||
/// use_address when one was set, otherwise formats "<name>.local" from the runtime
|
||||
/// device name into buf (so it includes the MAC suffix from name_add_mac_suffix).
|
||||
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf);
|
||||
IPAddresses get_ip_addresses();
|
||||
|
||||
} // namespace esphome::network
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
|
||||
require_vfs_select,
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
from esphome.components.network import add_use_address
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
@@ -288,7 +289,7 @@ async def to_code(config):
|
||||
enable_mdns_storage()
|
||||
|
||||
ot = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
add_use_address(ot, config[CONF_USE_ADDRESS])
|
||||
await cg.register_component(ot, config)
|
||||
if (poll_period := config.get(CONF_POLL_PERIOD)) is not None:
|
||||
cg.add(ot.set_poll_period(poll_period))
|
||||
|
||||
@@ -39,6 +39,8 @@ class OpenThreadComponent final : public Component {
|
||||
void on_factory_reset(std::function<void()> callback);
|
||||
void defer_factory_reset_external_callback();
|
||||
|
||||
/// Returns nullptr when no explicit use_address is configured and the address is
|
||||
/// derived at runtime from the device name (see network::get_use_address_to()).
|
||||
const char *get_use_address() const { return this->use_address_; }
|
||||
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
#if CONFIG_OPENTHREAD_MTD
|
||||
@@ -76,7 +78,7 @@ class OpenThreadComponent final : public Component {
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
const char *use_address_{nullptr};
|
||||
};
|
||||
|
||||
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INVERTED,
|
||||
@@ -16,6 +17,11 @@ from esphome.const import (
|
||||
CODEOWNERS = ["@remcom"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_ANALOG_GAIN = "analog_gain"
|
||||
CONF_CHANNEL_MIX = "channel_mix"
|
||||
CONF_VOLUME_MIN_DB = "volume_min_db"
|
||||
CONF_VOLUME_MAX_DB = "volume_max_db"
|
||||
|
||||
pcm5122_ns = cg.esphome_ns.namespace("pcm5122")
|
||||
PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice)
|
||||
CONF_PCM5122 = "pcm5122"
|
||||
@@ -27,26 +33,60 @@ PCM5122_BITS_PER_SAMPLE_ENUM = {
|
||||
32: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_32,
|
||||
}
|
||||
|
||||
pcm5122_analog_gain = pcm5122_ns.enum("PCM5122AnalogGain")
|
||||
PCM5122_ANALOG_GAIN_ENUM = {
|
||||
"0db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_0DB,
|
||||
"-6db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_MINUS_6DB,
|
||||
}
|
||||
|
||||
pcm5122_channel_mix = pcm5122_ns.enum("PCM5122ChannelMix")
|
||||
PCM5122_CHANNEL_MIX_ENUM = {
|
||||
"stereo": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_STEREO,
|
||||
"left": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_LEFT_ONLY,
|
||||
"right": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_RIGHT_ONLY,
|
||||
"swapped": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_SWAPPED,
|
||||
}
|
||||
|
||||
_validate_bits = cv.float_with_unit("bits", "bit")
|
||||
|
||||
|
||||
def _validate_volume_range(config):
|
||||
if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]:
|
||||
raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}")
|
||||
return config
|
||||
|
||||
|
||||
PCM5122GPIOPin = pcm5122_ns.class_(
|
||||
"PCM5122GPIOPin",
|
||||
cg.GPIOPin,
|
||||
cg.Parented.template(PCM5122),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PCM5122),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
|
||||
_validate_bits, cv.enum(PCM5122_BITS_PER_SAMPLE_ENUM)
|
||||
),
|
||||
cv.Optional(CONF_ANALOG_GAIN, default="0db"): cv.enum(
|
||||
PCM5122_ANALOG_GAIN_ENUM, lower=True
|
||||
),
|
||||
cv.Optional(CONF_CHANNEL_MIX, default="stereo"): cv.enum(
|
||||
PCM5122_CHANNEL_MIX_ENUM, lower=True
|
||||
),
|
||||
cv.Optional(CONF_VOLUME_MIN_DB, default="-52.5dB"): cv.All(
|
||||
cv.decibel, cv.float_range(min=-103.0, max=24.0)
|
||||
),
|
||||
cv.Optional(CONF_VOLUME_MAX_DB, default="0dB"): cv.All(
|
||||
cv.decibel, cv.float_range(min=-103.0, max=24.0)
|
||||
),
|
||||
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x4D))
|
||||
.extend(i2c.i2c_device_schema(0x4D)),
|
||||
_validate_volume_range,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,3 +136,10 @@ async def to_code(config):
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
|
||||
cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN]))
|
||||
cg.add(var.set_channel_mix(config[CONF_CHANNEL_MIX]))
|
||||
cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB]))
|
||||
cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB]))
|
||||
if enable_pin_config := config.get(CONF_ENABLE_PIN):
|
||||
enable_pin = await cg.gpio_pin_expression(enable_pin_config)
|
||||
cg.add(var.set_enable_pin(enable_pin))
|
||||
|
||||
@@ -10,6 +10,12 @@ namespace esphome::pcm5122 {
|
||||
static const char *const TAG = "pcm5122";
|
||||
|
||||
void PCM5122::setup() {
|
||||
// Hold XSMT low (soft mute asserted) until init completes
|
||||
if (this->enable_pin_ != nullptr) {
|
||||
this->enable_pin_->setup();
|
||||
this->enable_pin_->digital_write(false);
|
||||
}
|
||||
|
||||
// Select page 0 and verify chip presence via I2C ACK
|
||||
if (!this->select_page_(0)) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
@@ -51,7 +57,22 @@ void PCM5122::setup() {
|
||||
}
|
||||
this->reg(PCM5122_REG_AUDIO_FORMAT) = PCM5122_AUDIO_FORMAT_I2S | alen;
|
||||
|
||||
if (!this->write_channel_mix_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->write_analog_gain_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// PLL reference clock: BCK
|
||||
if (!this->select_page_(0)) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
optional<uint8_t> pll_ref = this->read_byte(PCM5122_REG_PLL_REF);
|
||||
if (!pll_ref.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read PLL_REF");
|
||||
@@ -67,15 +88,40 @@ void PCM5122::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Release XSMT (soft un-mute) now that init has completed
|
||||
if (this->enable_pin_ != nullptr) {
|
||||
this->enable_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
|
||||
void PCM5122::dump_config() {
|
||||
const char *channel_mix_str;
|
||||
switch (this->channel_mix_) {
|
||||
case PCM5122_CHANNEL_MIX_LEFT_ONLY:
|
||||
channel_mix_str = "left only";
|
||||
break;
|
||||
case PCM5122_CHANNEL_MIX_RIGHT_ONLY:
|
||||
channel_mix_str = "right only";
|
||||
break;
|
||||
case PCM5122_CHANNEL_MIX_SWAPPED:
|
||||
channel_mix_str = "swapped";
|
||||
break;
|
||||
default:
|
||||
channel_mix_str = "stereo";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "Audio DAC:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Bits per sample: %u\n"
|
||||
" Analog gain: %s\n"
|
||||
" Channel mix: %s\n"
|
||||
" Volume range: %.1f dB to %.1f dB\n"
|
||||
" Muted: %s",
|
||||
this->bits_per_sample_, YESNO(this->is_muted_));
|
||||
this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB",
|
||||
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
}
|
||||
|
||||
bool PCM5122::set_mute_off() {
|
||||
@@ -118,11 +164,11 @@ bool PCM5122::write_mute_() {
|
||||
}
|
||||
|
||||
bool PCM5122::write_volume_() {
|
||||
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFF = mute (-0.5 dB/step).
|
||||
// Note: volume=0.0 maps to -52.5 dB (still audible), not true silence.
|
||||
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFE = -103 dB, 0xFF = mute (-0.5 dB/step).
|
||||
// Note: volume=0.0 maps to volume_min_db_, which is not true silence unless set to -103 dB.
|
||||
// Use set_mute_on() for silence.
|
||||
const uint8_t dvol_max_volume = 0x30; // 0 dB at full scale
|
||||
const uint8_t dvol_min_volume = 0x99; // -52.5 dB at minimum
|
||||
const uint8_t dvol_max_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_max_db_ * 2.0f));
|
||||
const uint8_t dvol_min_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_min_db_ * 2.0f));
|
||||
|
||||
const uint8_t volume_byte =
|
||||
dvol_max_volume + static_cast<uint8_t>(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume)));
|
||||
@@ -137,4 +183,52 @@ bool PCM5122::write_volume_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::write_analog_gain_() {
|
||||
uint8_t gain_byte = this->analog_gain_;
|
||||
if (!this->select_page_(1) || !this->write_byte(PCM5122_REG_ANALOG_GAIN, gain_byte)) {
|
||||
ESP_LOGE(TAG, "Writing analog gain failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::write_channel_mix_() {
|
||||
uint8_t channel_mix_byte = this->channel_mix_;
|
||||
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_DAC_DATA_PATH, channel_mix_byte)) {
|
||||
ESP_LOGE(TAG, "Writing channel mix failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::set_standby(bool enable) {
|
||||
bool prev_standby = this->standby_;
|
||||
this->standby_ = enable;
|
||||
if (!this->write_power_control_()) {
|
||||
this->standby_ = prev_standby;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::set_powerdown(bool enable) {
|
||||
bool prev_powerdown = this->powerdown_;
|
||||
this->powerdown_ = enable;
|
||||
if (!this->write_power_control_()) {
|
||||
this->powerdown_ = prev_powerdown;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCM5122::write_power_control_() {
|
||||
uint8_t power_byte =
|
||||
(this->standby_ ? PCM5122_POWER_CONTROL_RQST : 0) | (this->powerdown_ ? PCM5122_POWER_CONTROL_RQPD : 0);
|
||||
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_POWER_CONTROL, power_byte)) {
|
||||
ESP_LOGE(TAG, "Writing power control failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/components/audio_dac/audio_dac.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
@@ -10,11 +11,13 @@ namespace esphome::pcm5122 {
|
||||
// Page 0 register addresses
|
||||
static const uint8_t PCM5122_REG_PAGE_SELECT = 0x00;
|
||||
static const uint8_t PCM5122_REG_RESET = 0x01;
|
||||
static const uint8_t PCM5122_REG_POWER_CONTROL = 0x02;
|
||||
static const uint8_t PCM5122_REG_MUTE = 0x03;
|
||||
static const uint8_t PCM5122_REG_GPIO_ENABLE = 0x08;
|
||||
static const uint8_t PCM5122_REG_PLL_REF = 0x0D;
|
||||
static const uint8_t PCM5122_REG_ERROR_DETECT = 0x25;
|
||||
static const uint8_t PCM5122_REG_AUDIO_FORMAT = 0x28;
|
||||
static const uint8_t PCM5122_REG_DAC_DATA_PATH = 0x2A;
|
||||
static const uint8_t PCM5122_REG_DVOL_LEFT = 0x3D;
|
||||
static const uint8_t PCM5122_REG_DVOL_RIGHT = 0x3E;
|
||||
static const uint8_t PCM5122_REG_GPIO_OUTPUT_SELECT = 0x50; // Base address; GPIO n uses offset n-1
|
||||
@@ -23,6 +26,9 @@ static const uint8_t PCM5122_REG_GPIO_OUTPUT = 0x56;
|
||||
static const uint8_t PCM5122_REG_GPIO_INVERT = 0x57;
|
||||
static const uint8_t PCM5122_REG_GPIO_INPUT = 0x77;
|
||||
|
||||
// Page 1 register addresses
|
||||
static const uint8_t PCM5122_REG_ANALOG_GAIN = 0x02;
|
||||
|
||||
// Register values for init sequence
|
||||
static const uint8_t PCM5122_RESET_MODULES = 0x10; // RSTM: reset audio modules
|
||||
static const uint8_t PCM5122_AUDIO_FORMAT_I2S = 0x00; // AFMT = I2S (bits [5:4] = 00)
|
||||
@@ -35,12 +41,33 @@ static const uint8_t PCM5122_ERROR_DETECT_DISABLE_DIV_AUTOSET = (1 << 1);
|
||||
static const uint8_t PCM5122_PLL_REF_MASK = (7 << 4); // SREF bits [6:4]
|
||||
static const uint8_t PCM5122_PLL_REF_SOURCE_BCK = (1 << 4); // SREF = 001 (BCK)
|
||||
|
||||
// Page 0, Register 2 (Power Control): RQST = standby request, RQPD = powerdown request (§10.5.3)
|
||||
static const uint8_t PCM5122_POWER_CONTROL_RQST = (1 << 4);
|
||||
static const uint8_t PCM5122_POWER_CONTROL_RQPD = (1 << 0);
|
||||
|
||||
// Page 1, Register 2 (Analog Gain Control): LAGN/RAGN select 0 dB or -6 dB analog gain (§8.3.5.5)
|
||||
static const uint8_t PCM5122_ANALOG_GAIN_LAGN = (1 << 4);
|
||||
static const uint8_t PCM5122_ANALOG_GAIN_RAGN = (1 << 0);
|
||||
|
||||
enum PCM5122BitsPerSample : uint8_t {
|
||||
PCM5122_BITS_PER_SAMPLE_16 = 16,
|
||||
PCM5122_BITS_PER_SAMPLE_24 = 24,
|
||||
PCM5122_BITS_PER_SAMPLE_32 = 32,
|
||||
};
|
||||
|
||||
enum PCM5122AnalogGain : uint8_t {
|
||||
PCM5122_ANALOG_GAIN_0DB = 0x00,
|
||||
PCM5122_ANALOG_GAIN_MINUS_6DB = PCM5122_ANALOG_GAIN_LAGN | PCM5122_ANALOG_GAIN_RAGN,
|
||||
};
|
||||
|
||||
// Page 0, Register 0x2A (DAC Data Path): AUPL/AUPR select which channel's data feeds each output (§7.4.2.42)
|
||||
enum PCM5122ChannelMix : uint8_t {
|
||||
PCM5122_CHANNEL_MIX_STEREO = 0x11, // Left data -> left out, right data -> right out
|
||||
PCM5122_CHANNEL_MIX_LEFT_ONLY = 0x12, // Left data -> both outputs
|
||||
PCM5122_CHANNEL_MIX_RIGHT_ONLY = 0x21, // Right data -> both outputs
|
||||
PCM5122_CHANNEL_MIX_SWAPPED = 0x22, // Left/right outputs swapped
|
||||
};
|
||||
|
||||
class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -48,6 +75,11 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
|
||||
void set_bits_per_sample(PCM5122BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
|
||||
void set_analog_gain(PCM5122AnalogGain analog_gain) { this->analog_gain_ = analog_gain; }
|
||||
void set_channel_mix(PCM5122ChannelMix channel_mix) { this->channel_mix_ = channel_mix; }
|
||||
void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; }
|
||||
void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; }
|
||||
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
|
||||
|
||||
bool set_mute_off() override;
|
||||
bool set_mute_on() override;
|
||||
@@ -56,17 +88,30 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
|
||||
bool is_muted() override;
|
||||
float volume() override;
|
||||
|
||||
bool set_standby(bool enable);
|
||||
bool set_powerdown(bool enable);
|
||||
|
||||
friend class PCM5122GPIOPin;
|
||||
|
||||
protected:
|
||||
bool select_page_(uint8_t page);
|
||||
bool write_mute_();
|
||||
bool write_volume_();
|
||||
bool write_analog_gain_();
|
||||
bool write_channel_mix_();
|
||||
bool write_power_control_();
|
||||
|
||||
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
|
||||
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
|
||||
GPIOPin *enable_pin_{nullptr};
|
||||
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
|
||||
float volume_min_db_{-52.5f}; // Matches the previous hardcoded minimum (0x99)
|
||||
float volume_max_db_{0.0f}; // Matches the previous hardcoded maximum (0x30)
|
||||
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
|
||||
bool is_muted_{false};
|
||||
bool standby_{false};
|
||||
bool powerdown_{false};
|
||||
PCM5122BitsPerSample bits_per_sample_{PCM5122_BITS_PER_SAMPLE_16};
|
||||
PCM5122AnalogGain analog_gain_{PCM5122_ANALOG_GAIN_0DB};
|
||||
PCM5122ChannelMix channel_mix_{PCM5122_CHANNEL_MIX_STEREO};
|
||||
};
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_POWER_MODE, ENTITY_CATEGORY_CONFIG
|
||||
|
||||
from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns
|
||||
|
||||
PCM5122PowerSwitch = pcm5122_ns.class_("PCM5122PowerSwitch", switch.Switch)
|
||||
|
||||
pcm5122_power_switch_mode = pcm5122_ns.enum("PCM5122PowerSwitchMode")
|
||||
PCM5122_POWER_SWITCH_MODE_ENUM = {
|
||||
"standby": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_STANDBY,
|
||||
"powerdown": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_POWERDOWN,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
PCM5122PowerSwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_PCM5122): cv.use_id(PCM5122),
|
||||
cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.enum(
|
||||
PCM5122_POWER_SWITCH_MODE_ENUM, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_parented(var, config[CONF_PCM5122])
|
||||
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "power_switch.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
|
||||
void PCM5122PowerSwitch::write_state(bool state) {
|
||||
bool ok = (this->mode_ == PCM5122_POWER_SWITCH_MODE_STANDBY) ? this->parent_->set_standby(state)
|
||||
: this->parent_->set_powerdown(state);
|
||||
if (ok)
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
#include "../pcm5122.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
|
||||
enum PCM5122PowerSwitchMode : uint8_t {
|
||||
PCM5122_POWER_SWITCH_MODE_STANDBY,
|
||||
PCM5122_POWER_SWITCH_MODE_POWERDOWN,
|
||||
};
|
||||
|
||||
class PCM5122PowerSwitch final : public switch_::Switch, public Parented<PCM5122> {
|
||||
public:
|
||||
void set_power_mode(PCM5122PowerSwitchMode mode) { this->mode_ = mode; }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
PCM5122PowerSwitchMode mode_{PCM5122_POWER_SWITCH_MODE_POWERDOWN};
|
||||
};
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "usb_uart.h"
|
||||
#include "usb/usb_host.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/uart/uart_debugger.h"
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
@@ -396,7 +397,14 @@ int USBUartTypeFT23XX::set_dtr_rts_(USBUartChannel *channel) {
|
||||
}
|
||||
|
||||
void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
|
||||
if (!channel->initialised_.load() || channel->input_started_.load())
|
||||
if (!channel->initialised_.load())
|
||||
return;
|
||||
|
||||
// Use compare_exchange_strong to avoid a check-then-act race: start_input() is called
|
||||
// from both the USB task (self-restart on success) and the main loop (backpressure
|
||||
// restart), so a plain load()/store() pair can let both threads submit a transfer.
|
||||
auto started = false;
|
||||
if (!channel->input_started_.compare_exchange_strong(started, true))
|
||||
return;
|
||||
|
||||
const auto *ep = channel->cdc_dev_.in_ep;
|
||||
@@ -408,39 +416,55 @@ void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FTDI prepends a 2-byte modem/line status header to every bulk IN packet.
|
||||
size_t uart_data_len = (status.data_len > 2) ? (status.data_len - 2) : 0;
|
||||
|
||||
if (uart_data_len > 0) {
|
||||
ESP_LOGV(TAG, "RX callback: Received %zu bytes, channel=%d", uart_data_len, channel->index_);
|
||||
if (!channel->dummy_receiver_) {
|
||||
// Copy the entire received UART payload into the ring buffer in one
|
||||
// operation to avoid per-byte overhead and reduce the chance of
|
||||
// heap activity in hot paths.
|
||||
channel->input_buffer_.push(status.data + 2, uart_data_len);
|
||||
UsbDataChunk *chunk = this->chunk_pool_.allocate();
|
||||
if (chunk == nullptr) {
|
||||
this->usb_data_queue_.increment_dropped_count();
|
||||
channel->input_started_.store(false);
|
||||
// Queue is full — wake the main loop to drain it, then let read_array()
|
||||
// retrigger start_input() rather than spinning here in the USB task.
|
||||
this->enable_loop_soon_any_context();
|
||||
App.wake_loop_threadsafe();
|
||||
return;
|
||||
}
|
||||
// Strip the 2-byte FTDI header before queuing.
|
||||
memcpy(chunk->data, status.data + 2, uart_data_len);
|
||||
chunk->length = static_cast<uint16_t>(uart_data_len);
|
||||
chunk->channel = channel;
|
||||
this->usb_data_queue_.push(chunk);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
if (channel->debug_) {
|
||||
// Debug path creates a temporary vector for logging only; this is
|
||||
// acceptable because debug mode is opt-in and not used in release.
|
||||
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
|
||||
std::vector<uint8_t>(status.data + 2, status.data + 2 + uart_data_len), ',',
|
||||
channel->debug_prefix_);
|
||||
}
|
||||
#endif
|
||||
this->enable_loop_soon_any_context();
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
} else {
|
||||
} else if (status.data_len >= 2) {
|
||||
ESP_LOGVV(TAG, "RX: Status packet, modem=0x%02X line=0x%02X, ch=%d", status.data[0], status.data[1],
|
||||
channel->index_);
|
||||
}
|
||||
|
||||
channel->input_started_.store(false);
|
||||
if (channel->dummy_receiver_ ||
|
||||
channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
|
||||
this->start_input(channel);
|
||||
}
|
||||
this->start_input(channel);
|
||||
};
|
||||
|
||||
channel->input_started_.store(true);
|
||||
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
|
||||
if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
|
||||
ESP_LOGE(TAG, "RX transfer submission failed for ep=0x%02X", ep->bEndpointAddress);
|
||||
channel->input_started_.store(false);
|
||||
}
|
||||
}
|
||||
|
||||
void USBUartTypeFT23XX::on_rx_overflow(USBUartChannel *channel) {
|
||||
ESP_LOGW(TAG, "RX buffer overflow on channel %d, clearing to resync", channel->index_);
|
||||
channel->input_buffer_.clear();
|
||||
}
|
||||
|
||||
void USBUartTypeFT23XX::enable_channels() {
|
||||
|
||||
@@ -228,6 +228,11 @@ void USBUartComponent::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// If there is not enough space for the full chunk, let the device subclass
|
||||
// handle it (e.g. FTDI clears the buffer to prevent mid-telegram corruption).
|
||||
if (channel->input_buffer_.get_free_space() < chunk->length) {
|
||||
this->on_rx_overflow(channel);
|
||||
}
|
||||
// Push data to ring buffer (now safe in main loop)
|
||||
channel->input_buffer_.push(chunk->data, chunk->length);
|
||||
|
||||
|
||||
@@ -192,9 +192,13 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
|
||||
void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); }
|
||||
|
||||
void start_input(USBUartChannel *channel);
|
||||
virtual void start_input(USBUartChannel *channel);
|
||||
void start_output(USBUartChannel *channel);
|
||||
|
||||
// Called from loop() when input_buffer_ has insufficient space for the incoming chunk.
|
||||
// Default is a no-op; override in device-specific subclasses that need resync on overflow.
|
||||
virtual void on_rx_overflow(USBUartChannel *channel) {}
|
||||
|
||||
// Lock-free data transfer from USB task to main loop
|
||||
static constexpr int USB_DATA_QUEUE_SIZE = 32;
|
||||
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
|
||||
@@ -248,7 +252,8 @@ class USBUartTypeFT23XX : public USBUartTypeCdcAcm {
|
||||
public:
|
||||
USBUartTypeFT23XX(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
|
||||
|
||||
void start_input(USBUartChannel *channel);
|
||||
void start_input(USBUartChannel *channel) override;
|
||||
void on_rx_overflow(USBUartChannel *channel) override;
|
||||
|
||||
protected:
|
||||
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
|
||||
|
||||
@@ -421,10 +421,11 @@ void WebServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
#endif
|
||||
|
||||
void WebServer::dump_config() {
|
||||
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Web Server:\n"
|
||||
" Address: %s:%u",
|
||||
network::get_use_address(), this->base_->get_port());
|
||||
network::get_use_address_to(addr_buf), this->base_->get_port());
|
||||
}
|
||||
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
|
||||
request_wifi,
|
||||
)
|
||||
from esphome.components.network import (
|
||||
add_use_address,
|
||||
has_high_performance_networking,
|
||||
ip_address_literal,
|
||||
)
|
||||
@@ -585,7 +586,7 @@ def wifi_network(config, ap, static_ip):
|
||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
add_use_address(var, config[CONF_USE_ADDRESS])
|
||||
|
||||
# Track if any network uses Enterprise authentication
|
||||
has_eap = False
|
||||
|
||||
@@ -501,6 +501,8 @@ class WiFiComponent final : public Component {
|
||||
|
||||
network::IPAddress get_dns_address(int num);
|
||||
network::IPAddresses get_ip_addresses();
|
||||
/// Returns nullptr when no explicit use_address is configured and the address is
|
||||
/// derived at runtime from the device name (see network::get_use_address_to()).
|
||||
const char *get_use_address() const { return this->use_address_; }
|
||||
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
|
||||
|
||||
@@ -996,7 +998,7 @@ class WiFiComponent final : public Component {
|
||||
private:
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
const char *use_address_{nullptr};
|
||||
};
|
||||
|
||||
extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -1255,6 +1255,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
|
||||
UNIT_KILOWATT = "kW"
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_LITRE = "L"
|
||||
UNIT_LITRE_PER_HOUR = "L/h"
|
||||
UNIT_LITRE_PER_SECOND = "L/s"
|
||||
UNIT_LUX = "lx"
|
||||
UNIT_MEGAJOULE = "MJ"
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ resvg-py==0.3.3
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
smpclient==7.2.0
|
||||
smpclient==7.3.0
|
||||
requests==2.34.2
|
||||
py7zr==1.1.3
|
||||
platformdirs==4.10.0 # native esp-idf toolchain global cache dir
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
nvs_encryption:
|
||||
key_id: 0
|
||||
@@ -175,6 +175,29 @@ def test_esp32_default_toolchain_is_esp_idf(
|
||||
r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]",
|
||||
id="ignore_efuse_mac_crc_only_on_esp32",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32",
|
||||
"board": "esp32dev",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"nvs_encryption": {"key_id": 0}},
|
||||
},
|
||||
},
|
||||
r"NVS encryption \(HMAC scheme\) is not supported on ESP32 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]",
|
||||
id="nvs_encryption_unsupported_on_esp32",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"nvs_encryption": {"key_id": 6}},
|
||||
},
|
||||
},
|
||||
r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]",
|
||||
id="nvs_encryption_key_id_out_of_range",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_esp32_configuration_errors(
|
||||
@@ -214,6 +237,21 @@ def test_execute_from_psram_p4_sdkconfig(
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_nvs_encryption_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that nvs_encryption sets the HMAC scheme sdkconfig options."""
|
||||
generate_main(component_config_path("nvs_encryption_s3.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_NVS_ENCRYPTION") is True
|
||||
assert sdkconfig.get("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC") is True
|
||||
assert sdkconfig.get("CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID") == 0
|
||||
# The permanent/irreversible eFuse burn is warned about at config time.
|
||||
assert "PERMANENT and IRREVERSIBLE" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "expect_warning"),
|
||||
[
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
esphome:
|
||||
name: test-not-paused
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin:
|
||||
number: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- obj:
|
||||
id: root_obj
|
||||
@@ -0,0 +1,27 @@
|
||||
esphome:
|
||||
name: test-paused
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin:
|
||||
number: GPIO3
|
||||
|
||||
lvgl:
|
||||
paused: true
|
||||
widgets:
|
||||
- obj:
|
||||
id: root_obj
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Tests for the LVGL ``paused`` option code generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_SET_PAUSED_RE = re.compile(r"->set_paused\((.+?)\);")
|
||||
|
||||
|
||||
def _extract_set_paused(main_cpp: str) -> list[str]:
|
||||
"""Return the normalised argument text of every set_paused() call found.
|
||||
|
||||
Whitespace within and around the arguments is collapsed so unrelated
|
||||
code-generation formatting changes don't break these tests.
|
||||
"""
|
||||
return [" ".join(m.group(1).split()) for m in _SET_PAUSED_RE.finditer(main_cpp)]
|
||||
|
||||
|
||||
class TestPausedCodeGeneration:
|
||||
"""Verify that the ``paused`` option drives the set_paused() call."""
|
||||
|
||||
def test_paused_true_generates_set_paused(
|
||||
self, generate_main, component_config_path
|
||||
):
|
||||
"""``paused: true`` emits a set_paused(true, false) call."""
|
||||
main_cpp = generate_main(component_config_path("paused.yaml"))
|
||||
calls = _extract_set_paused(main_cpp)
|
||||
assert calls == ["true, false"]
|
||||
|
||||
def test_paused_default_omits_set_paused(
|
||||
self, generate_main, component_config_path
|
||||
):
|
||||
"""Without ``paused`` (default false) no set_paused call is generated."""
|
||||
main_cpp = generate_main(component_config_path("not_paused.yaml"))
|
||||
assert _extract_set_paused(main_cpp) == []
|
||||
@@ -0,0 +1,9 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
nvs_encryption:
|
||||
key_id: 0
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -24,20 +24,6 @@ binary_sensor:
|
||||
name: Button A checked
|
||||
widget: button_a
|
||||
state: checked
|
||||
- platform: lvgl
|
||||
id: button_checker
|
||||
name: LVGL button
|
||||
widget: button_button
|
||||
state: checked
|
||||
on_state:
|
||||
then:
|
||||
- lvgl.checkbox.update:
|
||||
id: checkbox_id
|
||||
state:
|
||||
checked: !lambda |-
|
||||
auto y = x; // block inlining of one line return
|
||||
return y;
|
||||
|
||||
- platform: lvgl
|
||||
id: button_presser
|
||||
name: Button pressed
|
||||
@@ -49,6 +35,9 @@ lvgl:
|
||||
rotation: 90
|
||||
log_level: debug
|
||||
resume_on_input: true
|
||||
paused: true
|
||||
update_when_display_idle: true
|
||||
refresh_interval: 30ms
|
||||
on_pause:
|
||||
- logger.log: LVGL is Paused
|
||||
- lvgl.display.set_rotation: 90
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
packages:
|
||||
lvgl: !include lvgl-package.yaml
|
||||
lvgl_package: !include lvgl-package.yaml
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
lvgl: !include common.yaml
|
||||
|
||||
sensor:
|
||||
- platform: rotary_encoder
|
||||
@@ -77,5 +78,3 @@ lvgl:
|
||||
- component.update: tft_display
|
||||
- delay: 60s
|
||||
- lvgl.resume:
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,6 +4,11 @@ audio_dac:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x4D
|
||||
bits_per_sample: 32bit
|
||||
analog_gain: -6db
|
||||
channel_mix: swapped
|
||||
volume_min_db: -60dB
|
||||
volume_max_db: -3dB
|
||||
enable_pin: GPIO12
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
@@ -22,3 +27,9 @@ binary_sensor:
|
||||
number: 4
|
||||
mode:
|
||||
input: true
|
||||
|
||||
switch:
|
||||
- platform: pcm5122
|
||||
pcm5122: pcm5122_dac
|
||||
name: PCM5122 Power Down
|
||||
power_mode: powerdown
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: use-address-runtime
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: use-address-mac
|
||||
name_add_mac_suffix: true
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
@@ -322,6 +322,49 @@ async def test_light_calls(
|
||||
assert state.state is True
|
||||
assert state.brightness == pytest.approx(0.75)
|
||||
|
||||
# Test 31: Setting brightness to 0 without an explicit state implicitly turns
|
||||
# the light off; turning it back on (without an explicit brightness) then
|
||||
# restores full brightness so the light is visible again.
|
||||
client.light_command(key=rgbcw_light.key, state=True, brightness=0.5)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.state is True
|
||||
assert state.brightness == pytest.approx(0.5)
|
||||
|
||||
# Brightness 0 with no explicit state -> implicit turn-off
|
||||
client.light_command(key=rgbcw_light.key, brightness=0.0)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.state is False
|
||||
assert state.brightness == pytest.approx(0.0)
|
||||
# Turning on without an explicit brightness restores it to full brightness
|
||||
client.light_command(key=rgbcw_light.key, state=True)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.state is True
|
||||
assert state.brightness == pytest.approx(1.0)
|
||||
|
||||
# Test 31b: An explicit turn-on with brightness 0 still resets to full
|
||||
# brightness - a turn-on must never leave the light on-but-invisible. This
|
||||
# is the same path the restore logic exercises (set_state(true) +
|
||||
# set_brightness(0) from a persisted brightness=0 turn-off).
|
||||
client.light_command(key=rgbcw_light.key, state=True, brightness=0.0)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.state is True
|
||||
assert state.brightness == pytest.approx(1.0)
|
||||
|
||||
# Test 32: Turning a light on when it already has nonzero brightness leaves
|
||||
# the brightness unchanged (the reset only happens when brightness is 0).
|
||||
client.light_command(key=rgbcw_light.key, state=True, brightness=0.4)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.brightness == pytest.approx(0.4)
|
||||
|
||||
client.light_command(key=rgbcw_light.key, state=False)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.state is False
|
||||
|
||||
client.light_command(key=rgbcw_light.key, state=True)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
assert state.state is True
|
||||
assert state.brightness == pytest.approx(0.4)
|
||||
|
||||
# Final cleanup - turn all lights off
|
||||
for light in lights:
|
||||
client.light_command(
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Integration tests for the runtime-built use_address.
|
||||
|
||||
The default "<name>.local" address is no longer stored as a compile-time string;
|
||||
it is built at runtime from the device name. This also fixes the logged address
|
||||
when name_add_mac_suffix is enabled: the baked string used to miss the MAC
|
||||
suffix, so it never matched the actual mDNS hostname.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
|
||||
MAC_SUFFIX = "abf679"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_use_address_runtime(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""The API dump_config logs "<name>.local" built from the device name."""
|
||||
address_seen = asyncio.Event()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if "Address: use-address-runtime.local:" in line:
|
||||
address_seen.set()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "use-address-runtime"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(address_seen.wait(), timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Did not log 'Address: use-address-runtime.local:'")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_use_address_runtime_mac_suffix(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""With name_add_mac_suffix the logged address includes the MAC suffix."""
|
||||
address_seen = asyncio.Event()
|
||||
expected = f"Address: use-address-mac-{MAC_SUFFIX}.local:"
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if expected in line:
|
||||
address_seen.set()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == f"use-address-mac-{MAC_SUFFIX}"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(address_seen.wait(), timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Did not log '{expected}'")
|
||||
Reference in New Issue
Block a user