mirror of
https://github.com/esphome/esphome.git
synced 2026-05-09 21:28:40 +08:00
Merge branch 'dev' into sendspin-artwork
This commit is contained in:
@@ -415,7 +415,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
|
||||
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
@@ -55,7 +55,7 @@ repos:
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: python3 script/run-in-env.py pylint
|
||||
entry: python script/run-in-env.py pylint
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^esphome/.+\.py$
|
||||
@@ -68,5 +68,5 @@ repos:
|
||||
additional_dependencies: []
|
||||
- id: ci-custom
|
||||
name: ci-custom
|
||||
entry: python3 script/run-in-env.py script/ci-custom.py
|
||||
entry: python script/run-in-env.py script/ci-custom.py
|
||||
language: system
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# ==============================================================================
|
||||
# Installs the latest prerelease of esphome-device-builder when the
|
||||
# `use_new_device_builder` config option is enabled.
|
||||
# This is a temporary install-on-boot step until esphome-device-builder
|
||||
# becomes a direct dependency of esphome.
|
||||
# ==============================================================================
|
||||
|
||||
if ! bashio::config.true 'use_new_device_builder'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bashio::log.info "Installing latest prerelease of esphome-device-builder..."
|
||||
if command -v uv > /dev/null; then
|
||||
uv pip install --system --no-cache-dir --prerelease=allow --upgrade \
|
||||
esphome-device-builder ||
|
||||
bashio::exit.nok "Failed installing esphome-device-builder."
|
||||
else
|
||||
pip install --no-cache-dir --pre --upgrade esphome-device-builder ||
|
||||
bashio::exit.nok "Failed installing esphome-device-builder."
|
||||
fi
|
||||
bashio::log.info "Installed esphome-device-builder."
|
||||
@@ -49,5 +49,12 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
|
||||
rm -rf /config/esphome/.esphome
|
||||
fi
|
||||
|
||||
if bashio::config.true 'use_new_device_builder'; then
|
||||
bashio::log.info "Starting ESPHome Device Builder..."
|
||||
exec esphome-device-builder /config/esphome \
|
||||
--ha-addon \
|
||||
--ingress-port "$(bashio::addon.ingress_port)"
|
||||
fi
|
||||
|
||||
bashio::log.info "Starting ESPHome dashboard..."
|
||||
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
# Community Hass.io Add-ons: ESPHome
|
||||
# Configures NGINX for use with ESPHome
|
||||
# ==============================================================================
|
||||
|
||||
# When the new device builder is enabled it serves HA ingress directly,
|
||||
# so nginx is not used at all -- skip configuration.
|
||||
if bashio::config.true 'use_new_device_builder'; then
|
||||
bashio::log.info "Skipping NGINX setup: new device builder serves ingress directly."
|
||||
bashio::exit.ok
|
||||
fi
|
||||
|
||||
mkdir -p /var/log/nginx
|
||||
|
||||
# Generate Ingress configuration
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
# Runs the NGINX proxy
|
||||
# ==============================================================================
|
||||
|
||||
# The new device builder handles HA ingress itself, so nginx is bypassed.
|
||||
# Block the longrun forever so s6 keeps the dependency satisfied and does
|
||||
# not respawn it.
|
||||
if bashio::config.true 'use_new_device_builder'; then
|
||||
bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
|
||||
exec sleep infinity
|
||||
fi
|
||||
|
||||
bashio::log.info "Waiting for ESPHome dashboard to come up..."
|
||||
|
||||
while [[ ! -S /var/run/esphome.sock ]]; do
|
||||
|
||||
+166
-31
@@ -28,6 +28,7 @@ from esphome.const import (
|
||||
ALLOWED_NAME_CHARS,
|
||||
ARGUMENT_HELP_DEVICE,
|
||||
CONF_API,
|
||||
CONF_AUTH,
|
||||
CONF_BAUD_RATE,
|
||||
CONF_BROKER,
|
||||
CONF_DEASSERT_RTS_DTR,
|
||||
@@ -47,6 +48,8 @@ from esphome.const import (
|
||||
CONF_PORT,
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_TOPIC,
|
||||
CONF_USERNAME,
|
||||
CONF_WEB_SERVER,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_CORE,
|
||||
KEY_NATIVE_IDF,
|
||||
@@ -63,6 +66,7 @@ from esphome.log import AnsiFore, color, setup_log
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import (
|
||||
PICOTOOL_PACKAGE,
|
||||
FlashImage,
|
||||
detect_rp2040_bootsel,
|
||||
get_picotool_path,
|
||||
get_serial_ports,
|
||||
@@ -348,6 +352,17 @@ def choose_upload_log_host(
|
||||
elif bootsel.permission_error:
|
||||
bootsel_permission_error = True
|
||||
|
||||
# Annotate the OTA chooser entry only in the non-default case: when the
|
||||
# config has web_server OTA but no native API OTA, the upload will fall
|
||||
# through to the HTTP path and the user benefits from seeing that
|
||||
# explicitly. The native-API path is the default and gets a plain label
|
||||
# to avoid noise on the most common scenario. For LOGGING the OTA
|
||||
# transport doesn't apply, so always leave the label plain.
|
||||
if purpose == Purpose.UPLOADING and not has_native_ota() and has_web_server_ota():
|
||||
ota_suffix = " via web_server"
|
||||
else:
|
||||
ota_suffix = ""
|
||||
|
||||
def add_ota_options() -> None:
|
||||
"""Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled."""
|
||||
if (discovered := _discover_mac_suffix_devices()) is not None:
|
||||
@@ -355,11 +370,11 @@ def choose_upload_log_host(
|
||||
# intentionally skip the base-name fallback since with
|
||||
# name_add_mac_suffix on, the base name doesn't exist on the net.
|
||||
for host in discovered:
|
||||
options.append((f"Over The Air ({host})", host))
|
||||
options.append((f"Over The Air{ota_suffix} ({host})", host))
|
||||
elif has_resolvable_address():
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
options.append((f"Over The Air{ota_suffix} ({CORE.address})", CORE.address))
|
||||
if has_mqtt_ip_lookup():
|
||||
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
|
||||
options.append((f"Over The Air{ota_suffix} (MQTT IP lookup)", "MQTTIP"))
|
||||
|
||||
if purpose == Purpose.LOGGING:
|
||||
if has_mqtt_logging():
|
||||
@@ -428,7 +443,19 @@ def has_api() -> bool:
|
||||
|
||||
|
||||
def has_ota() -> bool:
|
||||
"""Check if OTA upload is available (requires platform: esphome)."""
|
||||
"""Check if any network OTA upload is available.
|
||||
|
||||
True if the config exposes either ``platform: esphome`` (native API
|
||||
OTA) or ``platform: web_server`` (HTTP OTA). Both reach the device
|
||||
over the same network stack, so the OTA discovery path treats them
|
||||
interchangeably; ``upload_program`` picks the actual transport based
|
||||
on ``--ota-platform`` and what's configured.
|
||||
"""
|
||||
return has_native_ota() or has_web_server_ota()
|
||||
|
||||
|
||||
def has_native_ota() -> bool:
|
||||
"""Check if native API OTA upload is available (``platform: esphome``)."""
|
||||
if CONF_OTA not in CORE.config:
|
||||
return False
|
||||
return any(
|
||||
@@ -437,6 +464,16 @@ def has_ota() -> bool:
|
||||
)
|
||||
|
||||
|
||||
def has_web_server_ota() -> bool:
|
||||
"""Check if web_server OTA upload is available (``platform: web_server``)."""
|
||||
if CONF_OTA not in CORE.config:
|
||||
return False
|
||||
return any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_WEB_SERVER
|
||||
for ota_item in CORE.config[CONF_OTA]
|
||||
)
|
||||
|
||||
|
||||
def has_mqtt_ip_lookup() -> bool:
|
||||
"""Check if MQTT is available and IP lookup is supported."""
|
||||
from esphome.components.mqtt import CONF_DISCOVER_IP
|
||||
@@ -586,8 +623,6 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
from aioesphomeapi import LogParser
|
||||
import serial
|
||||
|
||||
from esphome import platformio_api
|
||||
|
||||
if CONF_LOGGER not in config:
|
||||
_LOGGER.info("Logger is not enabled. Not starting UART logs.")
|
||||
return 1
|
||||
@@ -602,8 +637,11 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
process_stacktrace = getattr(module, "process_stacktrace")
|
||||
except AttributeError:
|
||||
pass
|
||||
except (AttributeError, ImportError):
|
||||
_LOGGER.info(
|
||||
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
|
||||
CORE.target_platform,
|
||||
)
|
||||
|
||||
backtrace_state = False
|
||||
ser = serial.Serial()
|
||||
@@ -646,14 +684,10 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
)
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
if process_stacktrace:
|
||||
if process_stacktrace is not None:
|
||||
backtrace_state = process_stacktrace(
|
||||
config, line, backtrace_state
|
||||
)
|
||||
else:
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
_LOGGER.error("Serial port closed!")
|
||||
return 0
|
||||
@@ -843,22 +877,20 @@ def _make_crystal_freq_callback(
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
from esphome import platformio_api
|
||||
|
||||
first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
|
||||
"upload_speed", os.getenv("ESPHOME_UPLOAD_SPEED", "460800")
|
||||
)
|
||||
|
||||
if file is not None:
|
||||
flash_images = [platformio_api.FlashImage(path=file, offset="0x0")]
|
||||
flash_images = [FlashImage(path=file, offset="0x0")]
|
||||
else:
|
||||
from esphome import platformio_api
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
|
||||
firmware_offset = "0x10000" if CORE.is_esp32 else "0x0"
|
||||
flash_images = [
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
FlashImage(path=idedata.firmware_bin_path, offset=firmware_offset),
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
@@ -1119,25 +1151,83 @@ def upload_program(
|
||||
|
||||
return exit_code, host if exit_code == 0 else None
|
||||
|
||||
ota_conf = {}
|
||||
requested_platform = getattr(args, "ota_platform", None)
|
||||
chosen_platform = _choose_ota_platform(config, requested_platform)
|
||||
|
||||
# Resolve MQTT magic strings to actual IP addresses
|
||||
network_devices = _resolve_network_devices(devices, config, args)
|
||||
|
||||
if chosen_platform == CONF_WEB_SERVER:
|
||||
if getattr(args, "partition_table", False):
|
||||
raise EsphomeError(
|
||||
"--partition-table is only supported with the esphome OTA platform; "
|
||||
"the web_server OTA path can only update the firmware image."
|
||||
)
|
||||
binary = CORE.firmware_bin
|
||||
if getattr(args, "file", None) is not None:
|
||||
binary = Path(args.file)
|
||||
return _upload_via_web_server(config, network_devices, binary)
|
||||
|
||||
return _upload_via_native_api(config, network_devices, args)
|
||||
|
||||
|
||||
def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
|
||||
"""Pick the OTA platform to use, optionally honoring ``--ota-platform``.
|
||||
|
||||
Default behavior prefers ``esphome`` (native API) when it is configured.
|
||||
The native API uses challenge-response auth with MD5/SHA256 hashing of a
|
||||
server-issued nonce, so the password is never sent over the wire; the
|
||||
``web_server`` path uses HTTP Basic auth which transmits credentials in
|
||||
cleartext over the LAN. (The native path also supports gzip compression
|
||||
on ESP8266, where flash space is tight; on ESP32/RP2040/LibreTiny the
|
||||
backend reports ``supports_compression() == false`` and the firmware is
|
||||
sent uncompressed regardless of which platform is used.) Falls back to
|
||||
``web_server`` only when that is the only available platform.
|
||||
"""
|
||||
# Use a dict (insertion-ordered) instead of a list so error messages and
|
||||
# membership checks see one entry per platform even if the user has
|
||||
# multiple ``ota:`` items of the same platform; the web_server OTA
|
||||
# platform's final-validate hook merges duplicates anyway.
|
||||
available: dict[str, None] = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
|
||||
platform = ota_item.get(CONF_PLATFORM)
|
||||
if platform in (CONF_ESPHOME, CONF_WEB_SERVER):
|
||||
available[platform] = None
|
||||
|
||||
if not available:
|
||||
raise EsphomeError(
|
||||
f"Cannot upload Over the Air as the {CONF_OTA} configuration is not "
|
||||
f"present or does not include {CONF_PLATFORM}: {CONF_ESPHOME} or "
|
||||
f"{CONF_PLATFORM}: {CONF_WEB_SERVER}"
|
||||
)
|
||||
|
||||
if requested is not None:
|
||||
if requested not in available:
|
||||
raise EsphomeError(
|
||||
f"--ota-platform {requested} was requested but the configuration "
|
||||
f"only provides: {', '.join(available)}"
|
||||
)
|
||||
return requested
|
||||
|
||||
if CONF_ESPHOME in available:
|
||||
return CONF_ESPHOME
|
||||
return CONF_WEB_SERVER
|
||||
|
||||
|
||||
def _upload_via_native_api(
|
||||
config: ConfigType, network_devices: list[str], args: ArgsProtocol
|
||||
) -> tuple[int, str | None]:
|
||||
ota_conf: ConfigType = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
if ota_item.get(CONF_PLATFORM) == CONF_ESPHOME:
|
||||
ota_conf = ota_item
|
||||
break
|
||||
|
||||
if not ota_conf:
|
||||
raise EsphomeError(
|
||||
f"Cannot upload Over the Air as the {CONF_OTA} configuration is not present or does not include {CONF_PLATFORM}: {CONF_ESPHOME}"
|
||||
)
|
||||
|
||||
from esphome import espota2
|
||||
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD)
|
||||
|
||||
# Resolve MQTT magic strings to actual IP addresses
|
||||
network_devices = _resolve_network_devices(devices, config, args)
|
||||
|
||||
binary = CORE.firmware_bin
|
||||
ota_type = espota2.OTA_TYPE_UPDATE_APP
|
||||
if getattr(args, "partition_table", False):
|
||||
@@ -1161,6 +1251,28 @@ def upload_program(
|
||||
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
|
||||
|
||||
|
||||
def _upload_via_web_server(
|
||||
config: ConfigType, network_devices: list[str], binary: Path
|
||||
) -> tuple[int, str | None]:
|
||||
web_conf = config.get(CONF_WEB_SERVER)
|
||||
if not web_conf:
|
||||
raise EsphomeError(
|
||||
f"Cannot upload via web_server OTA: the {CONF_WEB_SERVER} component "
|
||||
f"is not configured."
|
||||
)
|
||||
|
||||
remote_port = int(web_conf[CONF_PORT])
|
||||
auth = web_conf.get(CONF_AUTH) or {}
|
||||
username = auth.get(CONF_USERNAME)
|
||||
password = auth.get(CONF_PASSWORD)
|
||||
|
||||
from esphome import web_server_ota
|
||||
|
||||
return web_server_ota.run_ota(
|
||||
network_devices, remote_port, username, password, binary
|
||||
)
|
||||
|
||||
|
||||
# Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a
|
||||
# 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as
|
||||
# bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the
|
||||
@@ -1881,6 +1993,17 @@ def parse_args(argv):
|
||||
"--file",
|
||||
help="Manually specify the binary file to upload.",
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--ota-platform",
|
||||
choices=[CONF_ESPHOME, CONF_WEB_SERVER],
|
||||
help=(
|
||||
"OTA platform to use for network uploads. Defaults to "
|
||||
f"'{CONF_ESPHOME}' (native API) when configured because it uses "
|
||||
"challenge-response auth so the password is never sent in "
|
||||
f"cleartext on the wire. Falls back to '{CONF_WEB_SERVER}' "
|
||||
"(HTTP Basic auth) when that is the only configured platform."
|
||||
),
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--partition-table",
|
||||
help="Upload as partition table (OTA).",
|
||||
@@ -1955,6 +2078,17 @@ def parse_args(argv):
|
||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||
action="store_true",
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--ota-platform",
|
||||
choices=[CONF_ESPHOME, CONF_WEB_SERVER],
|
||||
help=(
|
||||
"OTA platform to use for network uploads. Defaults to "
|
||||
f"'{CONF_ESPHOME}' (native API) when configured because it uses "
|
||||
"challenge-response auth so the password is never sent in "
|
||||
f"cleartext on the wire. Falls back to '{CONF_WEB_SERVER}' "
|
||||
"(HTTP Basic auth) when that is the only configured platform."
|
||||
),
|
||||
)
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
"clean-mqtt",
|
||||
@@ -2167,8 +2301,9 @@ def run_esphome(argv):
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
# For logs command, skip updating external components
|
||||
skip_external = args.command == "logs"
|
||||
# Commands that don't need fresh external components: logs just connects
|
||||
# to the device, and clean is about to delete the build directory.
|
||||
skip_external = args.command in ("logs", "clean")
|
||||
config = read_config(
|
||||
dict(args.substitution) if args.substitution else {},
|
||||
skip_external_update=skip_external,
|
||||
|
||||
+7
-5
@@ -98,11 +98,13 @@ _KNOWN_FILE_EXTENSIONS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
# Matches !secret references in YAML text. This is intentionally a simple
|
||||
# regex scan rather than a YAML parse — it may match inside comments or
|
||||
# multi-line strings, which is the conservative direction (include more
|
||||
# secrets rather than fewer).
|
||||
_SECRET_RE = re.compile(r"!secret\s+(\S+)")
|
||||
# Matches !secret references in YAML text. An optional surrounding
|
||||
# quote pair around the key is allowed and ignored: YAML treats
|
||||
# ``!secret 'foo'`` and ``!secret foo`` as the same key. This is
|
||||
# intentionally a simple regex scan rather than a YAML parse — it may
|
||||
# match inside comments or multi-line strings, which is the conservative
|
||||
# direction (include more secrets rather than fewer).
|
||||
_SECRET_RE = re.compile(r"""!secret\s+['"]?([^\s'"]+)""")
|
||||
|
||||
|
||||
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
||||
|
||||
@@ -19,7 +19,7 @@ import contextlib
|
||||
|
||||
from esphome.const import CONF_KEY, CONF_PORT, __version__
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.platformio_api import process_stacktrace
|
||||
from esphome.util import safe_print
|
||||
|
||||
from . import CONF_ENCRYPTION
|
||||
|
||||
@@ -61,10 +61,6 @@ class _LogLineProcessor:
|
||||
self.backtrace_state = self._platform_handler(
|
||||
self._config, raw_line, self.backtrace_state
|
||||
)
|
||||
else:
|
||||
self.backtrace_state = process_stacktrace(
|
||||
self._config, raw_line, backtrace_state=self.backtrace_state
|
||||
)
|
||||
except EsphomeError as exc:
|
||||
self._decode_enabled = False
|
||||
self.backtrace_state = False
|
||||
@@ -106,7 +102,6 @@ async def async_run_logs(
|
||||
noise_psk=noise_psk,
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
)
|
||||
dashboard = CORE.dashboard
|
||||
|
||||
# Try platform-specific stacktrace handler first, fall back to generic
|
||||
platform_process_stacktrace = None
|
||||
@@ -114,7 +109,10 @@ async def async_run_logs(
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
platform_process_stacktrace = getattr(module, "process_stacktrace")
|
||||
except (AttributeError, ImportError):
|
||||
pass
|
||||
_LOGGER.info(
|
||||
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
|
||||
CORE.target_platform,
|
||||
)
|
||||
|
||||
processor = _LogLineProcessor(config, platform_process_stacktrace)
|
||||
|
||||
@@ -128,7 +126,11 @@ async def async_run_logs(
|
||||
f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
|
||||
)
|
||||
for parsed_msg in parse_log_message(text, timestamp):
|
||||
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
|
||||
# safe_print handles the dashboard \033 escaping and falls back
|
||||
# to backslashreplace encoding on stdouts that can't represent
|
||||
# the wifi signal-bar block characters (Windows redirected
|
||||
# cp1252 pipe).
|
||||
safe_print(parsed_msg)
|
||||
for raw_line in text.splitlines():
|
||||
processor.process_line(raw_line)
|
||||
|
||||
|
||||
@@ -18,83 +18,22 @@ class APIConnection;
|
||||
class ListEntitiesIterator final : public ComponentIterator {
|
||||
public:
|
||||
ListEntitiesIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool on_cover(cover::Cover *entity) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool on_fan(fan::Fan *entity) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool on_light(light::LightState *entity) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool on_sensor(sensor::Sensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool on_switch(switch_::Switch *entity) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
bool on_button(button::Button *entity) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
#endif
|
||||
|
||||
// Entity overrides (generated from entity_types.h).
|
||||
// All implementations live in list_entities.cpp via LIST_ENTITIES_HANDLER.
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) bool on_##singular(type *entity) override;
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
bool on_service(UserServiceDescriptor *service) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
bool on_camera(camera::Camera *entity) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool on_climate(climate::Climate *entity) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *entity) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool on_date(datetime::DateEntity *entity) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool on_time(datetime::TimeEntity *entity) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool on_datetime(datetime::DateTimeEntity *entity) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *entity) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *entity) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool on_lock(lock::Lock *entity) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool on_valve(valve::Valve *entity) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *entity) override;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *entity) override;
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
bool on_radio_frequency(radio_frequency::RadioFrequency *entity) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *entity) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
|
||||
|
||||
@@ -67,7 +67,10 @@ INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
|
||||
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
|
||||
#endif
|
||||
|
||||
// Special cases (button and event) are already defined inline in subscribe_state.h
|
||||
// event is an ENTITY_CONTROLLER_TYPE_ but has no state to send.
|
||||
#ifdef USE_EVENT
|
||||
bool InitialStateIterator::on_event(event::Event *entity) { return true; }
|
||||
#endif
|
||||
|
||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
|
||||
@@ -19,78 +19,20 @@ class APIConnection;
|
||||
class InitialStateIterator final : public ComponentIterator {
|
||||
public:
|
||||
InitialStateIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool on_cover(cover::Cover *entity) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool on_fan(fan::Fan *entity) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool on_light(light::LightState *entity) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool on_sensor(sensor::Sensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool on_switch(switch_::Switch *entity) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
bool on_button(button::Button *button) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool on_climate(climate::Climate *entity) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *entity) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool on_date(datetime::DateEntity *entity) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool on_time(datetime::TimeEntity *entity) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool on_datetime(datetime::DateTimeEntity *entity) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *entity) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *entity) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool on_lock(lock::Lock *entity) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool on_valve(valve::Valve *entity) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *entity) override;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *infrared) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
bool on_radio_frequency(radio_frequency::RadioFrequency *radio_frequency) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
|
||||
// Entity overrides (generated from entity_types.h).
|
||||
// ENTITY_TYPE_ entities have no state to send and default to a no-op.
|
||||
// ENTITY_CONTROLLER_TYPE_ entities are implemented in subscribe_state.cpp via INITIAL_STATE_HANDLER,
|
||||
// except on_event which has no state (defined out-of-line in subscribe_state.cpp).
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
|
||||
bool on_##singular(type *entity) override { return true; }
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
bool on_##singular(type *entity) override;
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "atm90e32.h"
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <numbers>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -8,6 +9,25 @@ namespace esphome {
|
||||
namespace atm90e32 {
|
||||
|
||||
static const char *const TAG = "atm90e32";
|
||||
|
||||
static uint32_t pref_hash(const char *prefix, const char *name_space) {
|
||||
auto hash = fnv1_hash(prefix);
|
||||
return fnv1_hash_extend(hash, name_space);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static int migrate_legacy_pref_if_needed(ESPPreferenceObject ¤t_pref, ESPPreferenceObject &legacy_pref,
|
||||
T *scratch) {
|
||||
T current{};
|
||||
if (current_pref.load(¤t)) {
|
||||
return 0;
|
||||
}
|
||||
if (!legacy_pref.load(scratch)) {
|
||||
return 0;
|
||||
}
|
||||
return current_pref.save(scratch) ? 1 : -1;
|
||||
}
|
||||
|
||||
void ATM90E32Component::loop() {
|
||||
if (this->get_publish_interval_flag_()) {
|
||||
this->set_publish_interval_flag_(false);
|
||||
@@ -112,10 +132,14 @@ void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> bu
|
||||
this->cs_->dump_summary(buffer.data(), buffer.size());
|
||||
}
|
||||
|
||||
const char *ATM90E32Component::get_calibration_id_() { return this->instance_id_; }
|
||||
|
||||
void ATM90E32Component::setup() {
|
||||
this->spi_setup();
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
char legacy_cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(legacy_cs);
|
||||
const bool has_distinct_legacy_namespace = strcmp(cs, legacy_cs) != 0;
|
||||
|
||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||
uint16_t high_thresh = 0;
|
||||
@@ -162,15 +186,46 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
||||
o_hash = fnv1_hash_extend(o_hash, cs);
|
||||
uint32_t o_hash = pref_hash("_offset_calibration_", cs);
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
bool migrated_offset = false;
|
||||
if (has_distinct_legacy_namespace) {
|
||||
uint32_t legacy_o_hash = pref_hash("_offset_calibration_", legacy_cs);
|
||||
auto legacy_offset_pref = global_preferences->make_preference<OffsetCalibration[3]>(legacy_o_hash, true);
|
||||
OffsetCalibration offset_data[3]{};
|
||||
int migration_status = migrate_legacy_pref_if_needed(this->offset_pref_, legacy_offset_pref, &offset_data);
|
||||
migrated_offset = migration_status > 0;
|
||||
if (migration_status > 0) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Migrated offset calibrations from legacy storage.", cs);
|
||||
} else if (migration_status < 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Failed to migrate offset calibrations from legacy storage.", cs);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
||||
po_hash = fnv1_hash_extend(po_hash, cs);
|
||||
uint32_t po_hash = pref_hash("_power_offset_calibration_", cs);
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
bool migrated_power_offset = false;
|
||||
if (has_distinct_legacy_namespace) {
|
||||
uint32_t legacy_po_hash = pref_hash("_power_offset_calibration_", legacy_cs);
|
||||
auto legacy_power_offset_pref =
|
||||
global_preferences->make_preference<PowerOffsetCalibration[3]>(legacy_po_hash, true);
|
||||
PowerOffsetCalibration power_offset_data[3]{};
|
||||
int migration_status =
|
||||
migrate_legacy_pref_if_needed(this->power_offset_pref_, legacy_power_offset_pref, &power_offset_data);
|
||||
migrated_power_offset = migration_status > 0;
|
||||
if (migration_status > 0) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Migrated power offset calibrations from legacy storage.", cs);
|
||||
} else if (migration_status < 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Failed to migrate power offset calibrations from legacy storage.", cs);
|
||||
}
|
||||
}
|
||||
|
||||
if (migrated_offset || migrated_power_offset) {
|
||||
global_preferences->sync();
|
||||
}
|
||||
|
||||
this->restore_offset_calibrations_();
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
|
||||
@@ -189,9 +244,27 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
||||
g_hash = fnv1_hash_extend(g_hash, cs);
|
||||
uint32_t g_hash = pref_hash("_gain_calibration_", cs);
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
bool migrated_gain = false;
|
||||
if (has_distinct_legacy_namespace) {
|
||||
uint32_t legacy_g_hash = pref_hash("_gain_calibration_", legacy_cs);
|
||||
auto legacy_gain_calibration_pref = global_preferences->make_preference<GainCalibration[3]>(legacy_g_hash, true);
|
||||
GainCalibration gain_data[3]{};
|
||||
int migration_status =
|
||||
migrate_legacy_pref_if_needed(this->gain_calibration_pref_, legacy_gain_calibration_pref, &gain_data);
|
||||
migrated_gain = migration_status > 0;
|
||||
if (migration_status > 0) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Migrated gain calibrations from legacy storage.", cs);
|
||||
} else if (migration_status < 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Failed to migrate gain calibrations from legacy storage.", cs);
|
||||
}
|
||||
}
|
||||
|
||||
if (migrated_gain) {
|
||||
global_preferences->sync();
|
||||
}
|
||||
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
if (!this->using_saved_calibrations_) {
|
||||
@@ -221,8 +294,7 @@ void ATM90E32Component::setup() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::log_calibration_status_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
|
||||
bool offset_mismatch = false;
|
||||
bool power_mismatch = false;
|
||||
@@ -573,8 +645,7 @@ float ATM90E32Component::get_chip_temperature_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_gain_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->enable_gain_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
|
||||
cs);
|
||||
@@ -674,8 +745,7 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -688,8 +758,7 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
bool success = this->offset_pref_.save(&this->offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -705,8 +774,7 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
@@ -722,8 +790,7 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
|
||||
@@ -753,8 +820,7 @@ void ATM90E32Component::run_offset_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_power_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
@@ -827,15 +893,16 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_gain_calibrations_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
|
||||
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
|
||||
this->gain_phase_[i] = this->config_gain_phase_[i];
|
||||
}
|
||||
|
||||
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
|
||||
bool have_data = this->gain_calibration_pref_.load(&this->gain_phase_);
|
||||
|
||||
if (have_data) {
|
||||
bool all_zero = true;
|
||||
bool same_as_config = true;
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
@@ -882,12 +949,12 @@ void ATM90E32Component::restore_gain_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_offset_calibrations_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->config_offset_phase_[i] = this->offset_phase_[i];
|
||||
|
||||
bool have_data = this->offset_pref_.load(&this->offset_phase_);
|
||||
|
||||
bool all_zero = true;
|
||||
if (have_data) {
|
||||
for (auto &phase : this->offset_phase_) {
|
||||
@@ -925,12 +992,12 @@ void ATM90E32Component::restore_offset_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
|
||||
|
||||
bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_);
|
||||
|
||||
bool all_zero = true;
|
||||
if (have_data) {
|
||||
for (auto &phase : this->power_offset_phase_) {
|
||||
@@ -968,8 +1035,7 @@ void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_gain_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->using_saved_calibrations_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
@@ -1018,8 +1084,7 @@ void ATM90E32Component::clear_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->restored_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
@@ -1061,8 +1126,7 @@ void ATM90E32Component::clear_offset_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_power_offset_calibrations() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
if (!this->restored_power_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
@@ -1137,8 +1201,7 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
|
||||
}
|
||||
|
||||
bool ATM90E32Component::verify_gain_writes_() {
|
||||
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||
this->get_cs_summary_(cs);
|
||||
const char *cs = this->get_calibration_id_();
|
||||
bool success = true;
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
||||
|
||||
@@ -102,6 +102,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
void clear_gain_calibrations();
|
||||
void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; }
|
||||
void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; }
|
||||
void set_instance_id(const char *id) { instance_id_ = id; }
|
||||
int16_t calibrate_offset(uint8_t phase, bool voltage);
|
||||
int16_t calibrate_power_offset(uint8_t phase, bool reactive);
|
||||
void run_gain_calibrations();
|
||||
@@ -183,6 +184,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
bool verify_gain_writes_();
|
||||
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
||||
void log_calibration_status_();
|
||||
const char *get_calibration_id_();
|
||||
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
|
||||
|
||||
struct ATM90E32Phase {
|
||||
@@ -263,6 +265,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
bool peak_current_signed_{false};
|
||||
bool enable_offset_calibration_{false};
|
||||
bool enable_gain_calibration_{false};
|
||||
const char *instance_id_{nullptr};
|
||||
bool restored_offset_calibration_{false};
|
||||
bool restored_power_offset_calibration_{false};
|
||||
bool restored_gain_calibration_{false};
|
||||
|
||||
@@ -193,6 +193,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_instance_id(str(config[CONF_ID])))
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
|
||||
@@ -64,8 +64,7 @@ class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
# WAV defaults to True for backward compatibility; will become opt-in in a future release
|
||||
wav_support: bool = True
|
||||
wav_support: bool = False
|
||||
micro_decoder_support: bool = False
|
||||
flac: FlacOptions = field(default_factory=FlacOptions)
|
||||
mp3: Mp3Options = field(default_factory=Mp3Options)
|
||||
@@ -335,7 +334,7 @@ async def to_code(config):
|
||||
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
ref="2.0.4",
|
||||
ref="3.0.0",
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
@@ -387,7 +386,7 @@ async def to_code(config):
|
||||
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.2.0")
|
||||
_emit_memory_pair(
|
||||
data.flac.buffer_memory,
|
||||
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
|
||||
@@ -395,6 +394,7 @@ async def to_code(config):
|
||||
)
|
||||
if data.mp3_support:
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.2.0")
|
||||
_emit_memory_pair(
|
||||
data.mp3.buffer_memory,
|
||||
"CONFIG_MP3_DECODER_PREFER_PSRAM",
|
||||
@@ -402,7 +402,7 @@ async def to_code(config):
|
||||
)
|
||||
if data.opus_support:
|
||||
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.4.0")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.4.1")
|
||||
if data.opus.floating_point is not None:
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_OPUS_FLOATING_POINT", data.opus.floating_point
|
||||
@@ -427,3 +427,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_OPUS_PSEUDOSTACK_SIZE", data.opus.pseudostack.size
|
||||
)
|
||||
if data.wav_support:
|
||||
cg.add_define("USE_AUDIO_WAV_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-wav", ref="0.2.0")
|
||||
|
||||
@@ -55,8 +55,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
|
||||
case AudioFileType::OPUS:
|
||||
return "OPUS";
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
case AudioFileType::WAV:
|
||||
return "WAV";
|
||||
#endif
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
@@ -71,9 +73,11 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url)
|
||||
return AudioFileType::MP3;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
if (strcasecmp(content_type, "audio/wav") == 0) {
|
||||
return AudioFileType::WAV;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
|
||||
return AudioFileType::FLAC;
|
||||
@@ -91,9 +95,11 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url)
|
||||
|
||||
// Fallback to URL extension
|
||||
if (url != nullptr && url[0] != '\0') {
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
if (str_endswith_ignore_case(url, ".wav")) {
|
||||
return AudioFileType::WAV;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
if (str_endswith_ignore_case(url, ".mp3")) {
|
||||
return AudioFileType::MP3;
|
||||
|
||||
@@ -116,7 +116,9 @@ enum class AudioFileType : uint8_t {
|
||||
#ifdef USE_AUDIO_OPUS_SUPPORT
|
||||
OPUS,
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
WAV,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct AudioFile {
|
||||
|
||||
@@ -20,14 +20,6 @@ AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
|
||||
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
|
||||
}
|
||||
|
||||
AudioDecoder::~AudioDecoder() {
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
if (this->audio_file_type_ == AudioFileType::MP3) {
|
||||
esp_audio_libs::helix_decoder::MP3FreeDecoder(this->mp3_decoder_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
esp_err_t AudioDecoder::add_source(std::weak_ptr<RingBuffer> &input_ring_buffer) {
|
||||
auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_);
|
||||
if (source == nullptr) {
|
||||
@@ -87,18 +79,13 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
|
||||
this->flac_decoder_ = make_unique<micro_flac::FLACDecoder>();
|
||||
this->free_buffer_required_ =
|
||||
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
|
||||
this->decoder_buffers_internally_ = true;
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
case AudioFileType::MP3:
|
||||
this->mp3_decoder_ = esp_audio_libs::helix_decoder::MP3InitDecoder();
|
||||
|
||||
// MP3 always has 1152 samples per chunk
|
||||
this->free_buffer_required_ = 1152 * sizeof(int16_t) * 2; // samples * size per sample * channels
|
||||
|
||||
// Always reallocate the output transfer buffer to the smallest necessary size
|
||||
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
|
||||
this->mp3_decoder_ = make_unique<micro_mp3::Mp3Decoder>();
|
||||
this->free_buffer_required_ =
|
||||
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_OPUS_SUPPORT
|
||||
@@ -106,20 +93,18 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
|
||||
this->opus_decoder_ = make_unique<micro_opus::OggOpusDecoder>();
|
||||
this->free_buffer_required_ =
|
||||
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
|
||||
this->decoder_buffers_internally_ = true;
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
case AudioFileType::WAV:
|
||||
this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>();
|
||||
this->wav_decoder_->reset();
|
||||
|
||||
// Processing WAVs doesn't actually require a specific amount of buffer size, as it is already in PCM format.
|
||||
// Thus, we don't reallocate to a minimum size.
|
||||
this->wav_decoder_ = make_unique<micro_wav::WAVDecoder>();
|
||||
// 1 KiB suffices to always make progress while avoiding excessive CPU spinning for decoding
|
||||
this->free_buffer_required_ = 1024;
|
||||
if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) {
|
||||
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case AudioFileType::NONE:
|
||||
default:
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
@@ -190,10 +175,8 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
|
||||
// Decode more audio
|
||||
|
||||
// Only shift data on the first loop iteration to avoid unnecessary, slow moves
|
||||
// If the decoder buffers internally, then never shift
|
||||
size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS),
|
||||
first_loop_iteration && !this->decoder_buffers_internally_);
|
||||
// Never shift the input buffer; every decoder buffers internally and consumes only what it processed.
|
||||
size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
||||
|
||||
if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) {
|
||||
// Less data is available than what was processed in last iteration, so don't attempt to decode.
|
||||
@@ -237,9 +220,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
state = this->decode_opus_();
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
case AudioFileType::WAV:
|
||||
state = this->decode_wav_();
|
||||
break;
|
||||
#endif
|
||||
case AudioFileType::NONE:
|
||||
default:
|
||||
state = FileDecoderState::IDLE;
|
||||
@@ -312,51 +297,56 @@ FileDecoderState AudioDecoder::decode_flac_() {
|
||||
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
FileDecoderState AudioDecoder::decode_mp3_() {
|
||||
// Look for the next sync word
|
||||
int buffer_length = (int) this->input_buffer_->available();
|
||||
int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length);
|
||||
// microMP3's samples_decoded value is samples per channel; e.g., what ESPHome typically calls an audio frame.
|
||||
// microMP3 uses the term frame to refer to an MP3 frame: an encoded packet that contains multiple audio frames.
|
||||
size_t bytes_consumed = 0;
|
||||
size_t samples_decoded = 0;
|
||||
|
||||
if (offset < 0) {
|
||||
// New data may have the sync word
|
||||
this->input_buffer_->consume(buffer_length);
|
||||
// microMP3 buffers internally: it consumes from our input buffer at its own pace, emits MP3_STREAM_INFO_READY once
|
||||
// the first frame header is parsed, and only then produces PCM. It handles sync-word search and ID3v2 tag skipping.
|
||||
micro_mp3::Mp3Result result = this->mp3_decoder_->decode(
|
||||
this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(),
|
||||
this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded);
|
||||
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
|
||||
if (result == micro_mp3::MP3_OK) {
|
||||
if (samples_decoded > 0 && this->audio_stream_info_.has_value()) {
|
||||
this->output_transfer_buffer_->increase_buffer_length(
|
||||
this->audio_stream_info_.value().frames_to_bytes(samples_decoded));
|
||||
}
|
||||
} else if (result == micro_mp3::MP3_STREAM_INFO_READY) {
|
||||
// First successful header parse: capture stream info and resize the output buffer to fit one full frame.
|
||||
// microMP3 always outputs 16-bit PCM.
|
||||
this->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(16, this->mp3_decoder_->get_channels(), this->mp3_decoder_->get_sample_rate());
|
||||
this->free_buffer_required_ =
|
||||
this->mp3_decoder_->get_samples_per_frame() * this->mp3_decoder_->get_channels() * sizeof(int16_t);
|
||||
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
||||
return FileDecoderState::FAILED;
|
||||
}
|
||||
} else if (result == micro_mp3::MP3_NEED_MORE_DATA) {
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
} else if (result == micro_mp3::MP3_OUTPUT_BUFFER_TOO_SMALL) {
|
||||
// Reallocate to decode the frame on the next call
|
||||
if (this->mp3_decoder_->get_channels() > 0) {
|
||||
this->free_buffer_required_ =
|
||||
this->mp3_decoder_->get_samples_per_frame() * this->mp3_decoder_->get_channels() * sizeof(int16_t);
|
||||
} else {
|
||||
// Fallback to worst-case size if channel info isn't available
|
||||
this->free_buffer_required_ = this->mp3_decoder_->get_min_output_buffer_bytes();
|
||||
}
|
||||
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
||||
return FileDecoderState::FAILED;
|
||||
}
|
||||
} else if (result == micro_mp3::MP3_DECODE_ERROR) {
|
||||
// Corrupt frame skipped; recoverable, retry on next call
|
||||
ESP_LOGW(TAG, "MP3 decoder skipped a corrupt frame");
|
||||
return FileDecoderState::POTENTIALLY_FAILED;
|
||||
}
|
||||
|
||||
// Advance read pointer to match the offset for the syncword
|
||||
this->input_buffer_->consume(offset);
|
||||
const uint8_t *buffer_start = this->input_buffer_->data();
|
||||
|
||||
buffer_length = (int) this->input_buffer_->available();
|
||||
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,
|
||||
(int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0);
|
||||
|
||||
size_t consumed = this->input_buffer_->available() - buffer_length;
|
||||
this->input_buffer_->consume(consumed);
|
||||
|
||||
if (err) {
|
||||
switch (err) {
|
||||
case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY:
|
||||
[[fallthrough]];
|
||||
case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER:
|
||||
return FileDecoderState::FAILED;
|
||||
break;
|
||||
default:
|
||||
// Most errors are recoverable by moving on to the next frame, so mark as potentailly failed
|
||||
return FileDecoderState::POTENTIALLY_FAILED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
esp_audio_libs::helix_decoder::MP3FrameInfo mp3_frame_info;
|
||||
esp_audio_libs::helix_decoder::MP3GetLastFrameInfo(this->mp3_decoder_, &mp3_frame_info);
|
||||
if (mp3_frame_info.outputSamps > 0) {
|
||||
int bytes_per_sample = (mp3_frame_info.bitsPerSample / 8);
|
||||
this->output_transfer_buffer_->increase_buffer_length(mp3_frame_info.outputSamps * bytes_per_sample);
|
||||
|
||||
if (!this->audio_stream_info_.has_value()) {
|
||||
this->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(mp3_frame_info.bitsPerSample, mp3_frame_info.nChans, mp3_frame_info.samprate);
|
||||
}
|
||||
}
|
||||
// MP3_ALLOCATION_FAILED, MP3_INPUT_INVALID, or any future error -- not recoverable
|
||||
ESP_LOGE(TAG, "MP3 decoder failed: %d", static_cast<int>(result));
|
||||
return FileDecoderState::FAILED;
|
||||
}
|
||||
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
@@ -401,52 +391,42 @@ FileDecoderState AudioDecoder::decode_opus_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
FileDecoderState AudioDecoder::decode_wav_() {
|
||||
if (!this->audio_stream_info_.has_value()) {
|
||||
// Header hasn't been processed
|
||||
// microWAV's samples_decoded counts individual channel samples; e.g., for
|
||||
// 16-bit stereo, 4 input bytes results in 2 samples_decoded.
|
||||
size_t bytes_consumed = 0;
|
||||
size_t samples_decoded = 0;
|
||||
|
||||
esp_audio_libs::wav_decoder::WAVDecoderResult result =
|
||||
this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available());
|
||||
micro_wav::WAVDecoderResult result = this->wav_decoder_->decode(
|
||||
this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(),
|
||||
this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded);
|
||||
|
||||
if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) {
|
||||
this->input_buffer_->consume(this->wav_decoder_->bytes_processed());
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
|
||||
this->audio_stream_info_ = audio::AudioStreamInfo(
|
||||
this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate());
|
||||
|
||||
this->wav_bytes_left_ = this->wav_decoder_->chunk_bytes_left();
|
||||
this->wav_has_known_end_ = (this->wav_bytes_left_ > 0);
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
} else if (result == esp_audio_libs::wav_decoder::WAV_DECODER_WARNING_INCOMPLETE_DATA) {
|
||||
// Available data didn't have the full header
|
||||
return FileDecoderState::POTENTIALLY_FAILED;
|
||||
} else {
|
||||
return FileDecoderState::FAILED;
|
||||
if (result == micro_wav::WAV_DECODER_SUCCESS) {
|
||||
if (samples_decoded > 0 && this->audio_stream_info_.has_value()) {
|
||||
this->output_transfer_buffer_->increase_buffer_length(
|
||||
this->audio_stream_info_.value().samples_to_bytes(samples_decoded));
|
||||
}
|
||||
} else if (result == micro_wav::WAV_DECODER_HEADER_READY) {
|
||||
// After HEADER_READY, get_bits_per_sample() returns the output bit depth
|
||||
// (16 for A-law/mu-law, 32 for IEEE float, original value for PCM).
|
||||
this->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(this->wav_decoder_->get_bits_per_sample(), this->wav_decoder_->get_channels(),
|
||||
this->wav_decoder_->get_sample_rate());
|
||||
} else if (result == micro_wav::WAV_DECODER_NEED_MORE_DATA) {
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
} else if (result == micro_wav::WAV_DECODER_END_OF_STREAM) {
|
||||
return FileDecoderState::END_OF_FILE;
|
||||
} else {
|
||||
if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) {
|
||||
size_t bytes_to_copy = this->input_buffer_->available();
|
||||
|
||||
if (this->wav_has_known_end_) {
|
||||
bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_);
|
||||
}
|
||||
|
||||
bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free());
|
||||
|
||||
if (bytes_to_copy > 0) {
|
||||
std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy);
|
||||
this->input_buffer_->consume(bytes_to_copy);
|
||||
this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy);
|
||||
if (this->wav_has_known_end_) {
|
||||
this->wav_bytes_left_ -= bytes_to_copy;
|
||||
}
|
||||
}
|
||||
return FileDecoderState::IDLE;
|
||||
}
|
||||
ESP_LOGE(TAG, "WAV decoder failed: %d", static_cast<int>(result));
|
||||
return FileDecoderState::FAILED;
|
||||
}
|
||||
|
||||
return FileDecoderState::END_OF_FILE;
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace audio
|
||||
} // namespace esphome
|
||||
|
||||
@@ -15,22 +15,26 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
// esp-audio-libs
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
#include <mp3_decoder.h>
|
||||
#endif
|
||||
#include <wav_decoder.h>
|
||||
|
||||
// micro-flac
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
#include <micro_flac/flac_decoder.h>
|
||||
#endif
|
||||
|
||||
// micro-mp3
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
#include <micro_mp3/mp3_decoder.h>
|
||||
#endif
|
||||
|
||||
// micro-opus
|
||||
#ifdef USE_AUDIO_OPUS_SUPPORT
|
||||
#include <micro_opus/ogg_opus_decoder.h>
|
||||
#endif
|
||||
|
||||
// micro-wav
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
#include <micro_wav/wav_decoder.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace audio {
|
||||
|
||||
@@ -54,7 +58,7 @@ class AudioDecoder {
|
||||
* @brief Class that facilitates decoding an audio file.
|
||||
* The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink
|
||||
* (ring buffer, speaker component, or callback).
|
||||
* Supports wav, flac, mp3, and ogg opus formats.
|
||||
* Supports flac, mp3, ogg opus, and wav formats (each enabled independently at compile time).
|
||||
*/
|
||||
public:
|
||||
/// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source()
|
||||
@@ -62,8 +66,7 @@ class AudioDecoder {
|
||||
/// @param output_buffer_size Size of the output transfer buffer in bytes.
|
||||
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
|
||||
|
||||
/// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically)
|
||||
~AudioDecoder();
|
||||
~AudioDecoder() = default;
|
||||
|
||||
/// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr.
|
||||
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
|
||||
@@ -118,20 +121,22 @@ class AudioDecoder {
|
||||
void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; }
|
||||
|
||||
protected:
|
||||
std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_;
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
FileDecoderState decode_flac_();
|
||||
std::unique_ptr<micro_flac::FLACDecoder> flac_decoder_;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
FileDecoderState decode_mp3_();
|
||||
esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_;
|
||||
std::unique_ptr<micro_mp3::Mp3Decoder> mp3_decoder_;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_OPUS_SUPPORT
|
||||
FileDecoderState decode_opus_();
|
||||
std::unique_ptr<micro_opus::OggOpusDecoder> opus_decoder_;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
FileDecoderState decode_wav_();
|
||||
std::unique_ptr<micro_wav::WAVDecoder> wav_decoder_;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<AudioReadableBuffer> input_buffer_;
|
||||
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
|
||||
@@ -141,16 +146,12 @@ class AudioDecoder {
|
||||
|
||||
size_t input_buffer_size_{0};
|
||||
size_t free_buffer_required_{0};
|
||||
size_t wav_bytes_left_{0};
|
||||
|
||||
uint32_t potentially_failed_count_{0};
|
||||
uint32_t accumulated_frames_written_{0};
|
||||
uint32_t playback_ms_{0};
|
||||
|
||||
bool end_of_file_{false};
|
||||
bool wav_has_known_end_{false};
|
||||
|
||||
bool decoder_buffers_internally_{false};
|
||||
|
||||
bool pause_output_{false};
|
||||
};
|
||||
|
||||
@@ -193,55 +193,66 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]
|
||||
audio.request_mp3_support()
|
||||
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]):
|
||||
audio.request_opus_support()
|
||||
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["WAV"]):
|
||||
audio.request_wav_support()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def audio_files_schema() -> cv.All:
|
||||
"""Schema for a list of audio file entries.
|
||||
|
||||
Validates each entry, downloads any web files, and detects the audio file
|
||||
type while requesting codec support. Reusable by other components (e.g.
|
||||
speaker media_player) that embed audio files in firmware without going
|
||||
through the audio_file component's C++ registry.
|
||||
"""
|
||||
return cv.All(
|
||||
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
partial(download_web_files_in_config, path_for=_compute_local_file_path),
|
||||
_validate_supported_local_file,
|
||||
)
|
||||
|
||||
|
||||
def generate_audio_file_code(file_config: ConfigType) -> MockObj:
|
||||
"""Generate the progmem data, AudioFile struct, and Pvariable for one file.
|
||||
|
||||
Returns the created Pvariable. Caller is responsible for any further
|
||||
registration (the audio_file component additionally registers each file in
|
||||
its named C++ registry; other consumers may skip that).
|
||||
"""
|
||||
cache = _get_data().file_cache
|
||||
file_id = str(file_config[CONF_ID])
|
||||
if file_id in cache:
|
||||
data, media_file_type = cache[file_id]
|
||||
else:
|
||||
data, media_file_type = read_audio_file_and_type(file_config)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
media_files_struct = cg.StructInitializer(
|
||||
audio.AudioFile,
|
||||
("data", prog_arr),
|
||||
("length", len(rhs)),
|
||||
("file_type", media_file_type),
|
||||
)
|
||||
|
||||
return cg.new_Pvariable(file_config[CONF_ID], media_files_struct)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
partial(download_web_files_in_config, path_for=_compute_local_file_path),
|
||||
_validate_supported_local_file,
|
||||
audio_files_schema(),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: list[ConfigType]) -> None:
|
||||
cache = _get_data().file_cache
|
||||
|
||||
for file_config in config:
|
||||
file_id = str(file_config[CONF_ID])
|
||||
data, media_file_type = cache[file_id]
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
media_files_struct = cg.StructInitializer(
|
||||
audio.AudioFile,
|
||||
(
|
||||
"data",
|
||||
prog_arr,
|
||||
),
|
||||
(
|
||||
"length",
|
||||
len(rhs),
|
||||
),
|
||||
(
|
||||
"file_type",
|
||||
media_file_type,
|
||||
),
|
||||
)
|
||||
|
||||
cg.new_Pvariable(
|
||||
file_config[CONF_ID],
|
||||
media_files_struct,
|
||||
)
|
||||
|
||||
# Store file ID for cross-component access
|
||||
file_var = generate_audio_file_code(file_config)
|
||||
_get_data().file_ids[file_id] = file_config[CONF_ID]
|
||||
cg.add(audio_file_ns.add_named_audio_file(file_var, file_id))
|
||||
|
||||
# Register all files in the shared C++ registry
|
||||
cg.add_define("AUDIO_FILE_MAX_FILES", len(config))
|
||||
for file_config in config:
|
||||
file_id = str(file_config[CONF_ID])
|
||||
file_var = await cg.get_variable(file_config[CONF_ID])
|
||||
cg.add(audio_file_ns.add_named_audio_file(file_var, file_id))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import media_source, psram
|
||||
from esphome.components import audio, esp32, media_source, psram
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
from esphome.types import ConfigType
|
||||
@@ -13,19 +15,30 @@ AudioFileMediaSource = audio_file_ns.class_(
|
||||
"AudioFileMediaSource", cg.Component, media_source.MediaSource
|
||||
)
|
||||
|
||||
|
||||
def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
||||
audio.request_micro_decoder_support()
|
||||
return config
|
||||
|
||||
|
||||
def _validate_task_stack_in_psram(value: Any) -> bool:
|
||||
if value := cv.boolean(value):
|
||||
return cv.requires_component(psram.DOMAIN)(value)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_source.media_source_schema(
|
||||
AudioFileMediaSource,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
|
||||
cv.boolean, cv.requires_component(psram.DOMAIN)
|
||||
),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
_request_micro_decoder,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,5 +47,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await media_source.register_media_source(var, config)
|
||||
|
||||
if CONF_TASK_STACK_IN_PSRAM in config:
|
||||
cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM]))
|
||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
|
||||
@@ -2,281 +2,185 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/audio/audio_decoder.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::audio_file {
|
||||
|
||||
namespace { // anonymous namespace for internal linkage
|
||||
struct AudioSinkAdapter : public audio::AudioSinkCallback {
|
||||
media_source::MediaSource *source;
|
||||
audio::AudioStreamInfo stream_info;
|
||||
|
||||
size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override {
|
||||
return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
#if defined(USE_AUDIO_OPUS_SUPPORT)
|
||||
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024;
|
||||
#else
|
||||
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
|
||||
#endif
|
||||
|
||||
static const char *const TAG = "audio_file_media_source";
|
||||
|
||||
enum EventGroupBits : uint32_t {
|
||||
// Requests to start playback (set by play_uri, handled by loop)
|
||||
REQUEST_START = (1 << 0),
|
||||
// Commands from main loop to decode task
|
||||
COMMAND_STOP = (1 << 1),
|
||||
COMMAND_PAUSE = (1 << 2),
|
||||
// Decode task lifecycle signals (one-shot, cleared by loop)
|
||||
TASK_STARTING = (1 << 7),
|
||||
TASK_RUNNING = (1 << 8),
|
||||
TASK_STOPPING = (1 << 9),
|
||||
TASK_STOPPED = (1 << 10),
|
||||
TASK_ERROR = (1 << 11),
|
||||
// Decode task state (level-triggered, set/cleared by decode task)
|
||||
TASK_PAUSED = (1 << 12),
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50;
|
||||
static constexpr size_t DECODER_TASK_STACK_SIZE = 5120;
|
||||
static constexpr uint8_t DECODER_TASK_PRIORITY = 2;
|
||||
static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20;
|
||||
static constexpr char URI_PREFIX[] = "audio-file://";
|
||||
|
||||
namespace { // anonymous namespace for internal linkage
|
||||
|
||||
// audio::AudioFileType and micro_decoder::AudioFileType use different numeric layouts (audio's
|
||||
// values shift with USE_AUDIO_*_SUPPORT defines; micro_decoder's are fixed and guarded by
|
||||
// MICRO_DECODER_CODEC_*). The codec request flow in audio/__init__.py keeps the two sets of
|
||||
// guards aligned, so a switch with matching #ifdefs covers all reachable cases.
|
||||
micro_decoder::AudioFileType to_micro_decoder_type(audio::AudioFileType type) {
|
||||
switch (type) {
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
case audio::AudioFileType::FLAC:
|
||||
return micro_decoder::AudioFileType::FLAC;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
case audio::AudioFileType::MP3:
|
||||
return micro_decoder::AudioFileType::MP3;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_OPUS_SUPPORT
|
||||
case audio::AudioFileType::OPUS:
|
||||
return micro_decoder::AudioFileType::OPUS;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_WAV_SUPPORT
|
||||
case audio::AudioFileType::WAV:
|
||||
return micro_decoder::AudioFileType::WAV;
|
||||
#endif
|
||||
default:
|
||||
return micro_decoder::AudioFileType::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AudioFileMediaSource::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Audio File Media Source:");
|
||||
ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No");
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Audio File Media Source:\n"
|
||||
" Decoder Task Stack in PSRAM: %s",
|
||||
YESNO(this->decoder_task_stack_in_psram_));
|
||||
}
|
||||
|
||||
void AudioFileMediaSource::setup() {
|
||||
this->disable_loop();
|
||||
|
||||
this->event_group_ = xEventGroupCreate();
|
||||
if (this->event_group_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event group");
|
||||
micro_decoder::DecoderConfig config;
|
||||
config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
|
||||
config.decoder_priority = DECODER_TASK_PRIORITY;
|
||||
config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
|
||||
config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
|
||||
|
||||
this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
|
||||
if (this->decoder_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate decoder");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->decoder_->set_listener(this);
|
||||
}
|
||||
|
||||
void AudioFileMediaSource::loop() {
|
||||
EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
void AudioFileMediaSource::loop() { this->decoder_->loop(); }
|
||||
|
||||
if (event_bits & REQUEST_START) {
|
||||
xEventGroupClearBits(this->event_group_, REQUEST_START);
|
||||
this->decoding_state_ = AudioFileDecodingState::START_TASK;
|
||||
}
|
||||
|
||||
switch (this->decoding_state_) {
|
||||
case AudioFileDecodingState::START_TASK: {
|
||||
if (!this->decode_task_.is_created()) {
|
||||
xEventGroupClearBits(this->event_group_, ALL_BITS);
|
||||
if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1,
|
||||
this->task_stack_in_psram_)) {
|
||||
ESP_LOGE(TAG, "Failed to create task");
|
||||
this->status_momentary_error("task_create", 1000);
|
||||
this->set_state_(media_source::MediaSourceState::ERROR);
|
||||
this->decoding_state_ = AudioFileDecodingState::IDLE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->decoding_state_ = AudioFileDecodingState::DECODING;
|
||||
break;
|
||||
}
|
||||
case AudioFileDecodingState::DECODING: {
|
||||
if (event_bits & TASK_STARTING) {
|
||||
ESP_LOGD(TAG, "Starting");
|
||||
xEventGroupClearBits(this->event_group_, TASK_STARTING);
|
||||
}
|
||||
|
||||
if (event_bits & TASK_RUNNING) {
|
||||
ESP_LOGV(TAG, "Started");
|
||||
xEventGroupClearBits(this->event_group_, TASK_RUNNING);
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
}
|
||||
|
||||
if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) {
|
||||
this->set_state_(media_source::MediaSourceState::PAUSED);
|
||||
} else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) {
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
}
|
||||
|
||||
if (event_bits & TASK_STOPPING) {
|
||||
ESP_LOGV(TAG, "Stopping");
|
||||
xEventGroupClearBits(this->event_group_, TASK_STOPPING);
|
||||
}
|
||||
|
||||
if (event_bits & TASK_ERROR) {
|
||||
// Report error so the orchestrator knows playback failed; task will have already logged the specific error
|
||||
this->set_state_(media_source::MediaSourceState::ERROR);
|
||||
}
|
||||
|
||||
if (event_bits & TASK_STOPPED) {
|
||||
ESP_LOGD(TAG, "Stopped");
|
||||
xEventGroupClearBits(this->event_group_, ALL_BITS);
|
||||
|
||||
this->decode_task_.deallocate();
|
||||
this->set_state_(media_source::MediaSourceState::IDLE);
|
||||
this->decoding_state_ = AudioFileDecodingState::IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AudioFileDecodingState::IDLE: {
|
||||
if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) {
|
||||
this->set_state_(media_source::MediaSourceState::IDLE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((this->decoding_state_ == AudioFileDecodingState::IDLE) &&
|
||||
(this->get_state() == media_source::MediaSourceState::IDLE)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
bool AudioFileMediaSource::can_handle(const std::string &uri) const { return uri.starts_with(URI_PREFIX); }
|
||||
|
||||
// Called from the orchestrator's main loop, so no synchronization needed with loop()
|
||||
bool AudioFileMediaSource::play_uri(const std::string &uri) {
|
||||
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() ||
|
||||
xEventGroupGetBits(this->event_group_) & REQUEST_START) {
|
||||
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if source is already playing
|
||||
if (this->get_state() != media_source::MediaSourceState::IDLE) {
|
||||
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate URI starts with "audio-file://"
|
||||
if (!uri.starts_with("audio-file://")) {
|
||||
if (!uri.starts_with(URI_PREFIX)) {
|
||||
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strip "audio-file://" prefix and find the file
|
||||
const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters
|
||||
|
||||
const char *file_id = uri.c_str() + sizeof(URI_PREFIX) - 1;
|
||||
this->current_file_ = nullptr;
|
||||
for (const auto &named_file : get_named_audio_files()) {
|
||||
if (strcmp(named_file.file_id, file_id) == 0) {
|
||||
this->current_file_ = named_file.file;
|
||||
xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START);
|
||||
this->enable_loop();
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
|
||||
if (this->current_file_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
micro_decoder::AudioFileType type = to_micro_decoder_type(this->current_file_->file_type);
|
||||
if (this->decoder_->play_buffer(this->current_file_->data, this->current_file_->length, type)) {
|
||||
this->pause_.store(false, std::memory_order_relaxed);
|
||||
this->enable_loop();
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Failed to start playback of '%s'", file_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called from the orchestrator's main loop, so no synchronization needed with loop()
|
||||
void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) {
|
||||
if (this->decoding_state_ != AudioFileDecodingState::DECODING) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case media_source::MediaSourceCommand::STOP:
|
||||
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP);
|
||||
this->decoder_->stop();
|
||||
break;
|
||||
case media_source::MediaSourceCommand::PAUSE:
|
||||
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
|
||||
// Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
|
||||
// machine from getting stuck in PAUSED when no playback is active (which would block the
|
||||
// next play_uri() call via its IDLE-state precondition).
|
||||
if (this->get_state() != media_source::MediaSourceState::PLAYING)
|
||||
break;
|
||||
// PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
|
||||
// yields, which fills any internal buffering and applies back pressure that effectively
|
||||
// pauses the decoder task.
|
||||
this->set_state_(media_source::MediaSourceState::PAUSED);
|
||||
this->pause_.store(true, std::memory_order_relaxed);
|
||||
break;
|
||||
case media_source::MediaSourceCommand::PLAY:
|
||||
xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
|
||||
if (this->get_state() != media_source::MediaSourceState::PAUSED)
|
||||
break;
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
this->pause_.store(false, std::memory_order_relaxed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFileMediaSource::decode_task(void *params) {
|
||||
AudioFileMediaSource *this_source = static_cast<AudioFileMediaSource *>(params);
|
||||
// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
|
||||
// being thread-safe with respect to its own audio writer.
|
||||
size_t AudioFileMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
|
||||
if (this->pause_.load(std::memory_order_relaxed)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
|
||||
return 0;
|
||||
}
|
||||
return this->write_output(data, length, timeout_ms, this->stream_info_);
|
||||
}
|
||||
|
||||
do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break
|
||||
// Called from the decoder task before the first on_audio_write().
|
||||
void AudioFileMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
|
||||
this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING);
|
||||
|
||||
// 0 bytes for input transfer buffer makes it an inplace buffer
|
||||
std::unique_ptr<audio::AudioDecoder> decoder = make_unique<audio::AudioDecoder>(0, 4096);
|
||||
|
||||
esp_err_t err = decoder->start(this_source->current_file_->file_type);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err));
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING);
|
||||
// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
|
||||
// loop thread and it's safe to call set_state_() directly.
|
||||
void AudioFileMediaSource::on_state_change(micro_decoder::DecoderState state) {
|
||||
switch (state) {
|
||||
case micro_decoder::DecoderState::IDLE:
|
||||
this->set_state_(media_source::MediaSourceState::IDLE);
|
||||
this->disable_loop();
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the file as a const data source
|
||||
decoder->add_source(this_source->current_file_->data, this_source->current_file_->length);
|
||||
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING);
|
||||
|
||||
AudioSinkAdapter audio_sink;
|
||||
bool has_stream_info = false;
|
||||
|
||||
while (true) {
|
||||
EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_);
|
||||
|
||||
if (event_bits & EventGroupBits::COMMAND_STOP) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool paused = event_bits & EventGroupBits::COMMAND_PAUSE;
|
||||
decoder->set_pause_output_state(paused);
|
||||
if (paused) {
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
} else {
|
||||
xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
|
||||
}
|
||||
|
||||
// Will stop gracefully once finished with the current file
|
||||
audio::AudioDecoderState decoder_state = decoder->decode(true);
|
||||
|
||||
if (decoder_state == audio::AudioDecoderState::FINISHED) {
|
||||
break;
|
||||
} else if (decoder_state == audio::AudioDecoderState::FAILED) {
|
||||
ESP_LOGE(TAG, "Decoder failed");
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
|
||||
has_stream_info = true;
|
||||
|
||||
audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value();
|
||||
|
||||
ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %" PRIu32, stream_info.get_bits_per_sample(),
|
||||
stream_info.get_channels(), stream_info.get_sample_rate());
|
||||
|
||||
if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) {
|
||||
ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported");
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
|
||||
break;
|
||||
}
|
||||
|
||||
audio_sink.source = this_source;
|
||||
audio_sink.stream_info = stream_info;
|
||||
esp_err_t err = decoder->add_sink(&audio_sink);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err));
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING);
|
||||
} while (false);
|
||||
|
||||
// All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed.
|
||||
|
||||
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED);
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
case micro_decoder::DecoderState::PLAYING:
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
break;
|
||||
case micro_decoder::DecoderState::FAILED:
|
||||
this->set_state_(media_source::MediaSourceState::ERROR);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::audio_file
|
||||
|
||||
@@ -8,41 +8,48 @@
|
||||
#include "esphome/components/audio_file/audio_file.h"
|
||||
#include "esphome/components/media_source/media_source.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/static_task.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <micro_decoder/decoder_source.h>
|
||||
#include <micro_decoder/types.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::audio_file {
|
||||
|
||||
enum class AudioFileDecodingState : uint8_t {
|
||||
START_TASK,
|
||||
DECODING,
|
||||
IDLE,
|
||||
};
|
||||
|
||||
class AudioFileMediaSource : public Component, public media_source::MediaSource {
|
||||
// Inherits from two unrelated listener-style interfaces:
|
||||
// - media_source::MediaSource: this source reports state and writes audio *to* an orchestrator
|
||||
// (the orchestrator calls set_listener() on us with a MediaSourceListener*).
|
||||
// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded
|
||||
// audio and state changes (we call decoder_->set_listener(this) in setup()).
|
||||
class AudioFileMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
// MediaSource interface implementation
|
||||
bool play_uri(const std::string &uri) override;
|
||||
void handle_command(media_source::MediaSourceCommand command) override;
|
||||
bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); }
|
||||
bool can_handle(const std::string &uri) const override;
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
// DecoderListener interface implementation
|
||||
size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override;
|
||||
void on_stream_info(const micro_decoder::AudioStreamInfo &info) override;
|
||||
void on_state_change(micro_decoder::DecoderState state) override;
|
||||
|
||||
protected:
|
||||
static void decode_task(void *params);
|
||||
|
||||
std::unique_ptr<micro_decoder::DecoderSource> decoder_;
|
||||
audio::AudioStreamInfo stream_info_;
|
||||
audio::AudioFile *current_file_{nullptr};
|
||||
AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE};
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
StaticTask decode_task_;
|
||||
|
||||
bool task_stack_in_psram_{false};
|
||||
// Written from the main loop in handle_command(), read from the decoder task in
|
||||
// on_audio_write(). Must be atomic to avoid a data race.
|
||||
std::atomic<bool> pause_{false};
|
||||
bool decoder_task_stack_in_psram_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_file
|
||||
|
||||
@@ -161,13 +161,9 @@ void BL0942::received_package_(DataPacket *data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cf_cnt is only 24 bits, so track overflows
|
||||
// cf_cnt wraps at 24 bits; total_increasing on the energy sensor handles the
|
||||
// wrap (and any spurious chip resets) downstream.
|
||||
uint32_t cf_cnt = (uint24_t) data->cf_cnt;
|
||||
cf_cnt |= this->prev_cf_cnt_ & 0xff000000;
|
||||
if (cf_cnt < this->prev_cf_cnt_) {
|
||||
cf_cnt += 0x1000000;
|
||||
}
|
||||
this->prev_cf_cnt_ = cf_cnt;
|
||||
|
||||
float v_rms = (uint24_t) data->v_rms / voltage_reference_;
|
||||
float i_rms = (uint24_t) data->i_rms / current_reference_;
|
||||
|
||||
@@ -141,7 +141,6 @@ class BL0942 : public PollingComponent, public uart::UARTDevice {
|
||||
bool reset_ = false;
|
||||
LineFrequency line_freq_ = LINE_FREQUENCY_50HZ;
|
||||
optional<uint32_t> rx_start_{};
|
||||
uint32_t prev_cf_cnt_ = 0;
|
||||
|
||||
bool validate_checksum_(DataPacket *data);
|
||||
int read_reg_(uint8_t reg);
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_B_CONSTANT = "b_constant"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
|
||||
@@ -328,17 +328,28 @@ async def build_apply_lambda_action(
|
||||
Used by both `cover.control` and `cover.template.publish` (and shared
|
||||
with the template/cover platform). Constants are emitted as flash
|
||||
immediates; user lambdas are invoked inline so trigger args still flow.
|
||||
The trigger arg types are wrapped as `const T &` to match the
|
||||
`void (*)(..., const Ts &...)` ApplyFn signature.
|
||||
Trigger arg types are normalized to `const std::remove_cvref_t<T> &`
|
||||
to match the ApplyFn signature for any T (value, ref, or const-ref).
|
||||
"""
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
|
||||
# apply lambda and any inner field lambdas (generated below via
|
||||
# `process_lambda`) share one parameter spelling that's well-formed for
|
||||
# any T.
|
||||
normalized_args = [
|
||||
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n)
|
||||
for t, n in args
|
||||
]
|
||||
|
||||
fwd_args = ", ".join(name for _, name in args)
|
||||
body_lines: list[str] = []
|
||||
for field in fields:
|
||||
if (value := config.get(field.conf_key)) is None:
|
||||
continue
|
||||
if isinstance(value, Lambda):
|
||||
inner = await cg.process_lambda(value, args, return_type=field.type_)
|
||||
inner = await cg.process_lambda(
|
||||
value, normalized_args, return_type=field.type_
|
||||
)
|
||||
value_expr = f"({inner})({fwd_args})"
|
||||
else:
|
||||
value_expr = str(cg.safe_exp(value))
|
||||
@@ -346,7 +357,7 @@ async def build_apply_lambda_action(
|
||||
|
||||
apply_args = [
|
||||
*prefix_args,
|
||||
*((t.operator("const").operator("ref"), n) for t, n in args),
|
||||
*normalized_args,
|
||||
]
|
||||
apply_lambda = LambdaExpression(
|
||||
["\n".join(body_lines)],
|
||||
|
||||
@@ -51,10 +51,17 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
|
||||
// plus one parent pointer, regardless of how many fields the user set.
|
||||
// Trigger args are forwarded to the apply function so user lambdas
|
||||
// (e.g. `position: !lambda "return x;"`) keep working.
|
||||
//
|
||||
// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so
|
||||
// the codegen can emit a matching parameter list for both the apply lambda
|
||||
// and any inner field lambdas without producing invalid C++ source text
|
||||
// (e.g. `const T & &` if Ts already carries a reference, or `const const
|
||||
// T &` if Ts already carries a const). This keeps trigger args no-copy
|
||||
// regardless of whether the trigger supplies `T`, `T &`, or `const T &`.
|
||||
|
||||
template<typename... Ts> class ControlAction : public Action<Ts...> {
|
||||
public:
|
||||
using ApplyFn = void (*)(CoverCall &, const Ts &...);
|
||||
using ApplyFn = void (*)(CoverCall &, const std::remove_cvref_t<Ts> &...);
|
||||
ControlAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
@@ -70,7 +77,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
|
||||
|
||||
template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
|
||||
public:
|
||||
using ApplyFn = void (*)(Cover *, const Ts &...);
|
||||
using ApplyFn = void (*)(Cover *, const std::remove_cvref_t<Ts> &...);
|
||||
CoverPublishAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
|
||||
@@ -401,7 +401,6 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
#endif
|
||||
auto uicr = [](volatile uint32_t *data, uint8_t size) {
|
||||
std::string res;
|
||||
char buf[sizeof(uint32_t) * 2 + 1];
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
if (i > 0) {
|
||||
res += ' ';
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
@@ -2515,3 +2516,78 @@ def copy_files():
|
||||
CORE.relative_build_path(name).write_bytes(content)
|
||||
else:
|
||||
copy_file_if_changed(path, CORE.relative_build_path(name))
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
from esphome import platformio_api
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
if not idedata.addr2line_path or not idedata.firmware_elf_path:
|
||||
_LOGGER.debug("decode_pc no addr2line")
|
||||
return
|
||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
||||
try:
|
||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
||||
if "?? ??:0" in translation:
|
||||
# Nothing useful
|
||||
return
|
||||
translation = translation.replace(" at ??:?", "").replace(":?", "")
|
||||
_LOGGER.warning("Decoded %s", translation)
|
||||
|
||||
|
||||
def _parse_register(config, regex, line):
|
||||
match = regex.match(line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
|
||||
STACKTRACE_ESP32_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7}).*")
|
||||
STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_BAD_ALLOC_RE = re.compile(
|
||||
r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$"
|
||||
)
|
||||
STACKTRACE_ESP32_BACKTRACE_RE = re.compile(
|
||||
r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+"
|
||||
)
|
||||
STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
# ESP32 crash handler (stored backtrace from previous boot)
|
||||
STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})")
|
||||
|
||||
|
||||
def process_stacktrace(config, line, backtrace_state):
|
||||
line = line.strip()
|
||||
|
||||
# ESP32 PC/EXCVADDR
|
||||
_parse_register(config, STACKTRACE_ESP32_PC_RE, line)
|
||||
_parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line)
|
||||
# ESP32-C3 PC/RA
|
||||
_parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line)
|
||||
_parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line)
|
||||
|
||||
# bad alloc
|
||||
match = re.match(STACKTRACE_BAD_ALLOC_RE, line)
|
||||
if match is not None:
|
||||
_LOGGER.warning(
|
||||
"Memory allocation of %s bytes failed at %s", match.group(2), match.group(1)
|
||||
)
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
# ESP32 crash handler backtrace (from previous boot)
|
||||
match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
# ESP32 single-line backtrace
|
||||
match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line)
|
||||
if match is not None:
|
||||
_LOGGER.warning("Found stack trace! Trying to decode it")
|
||||
for addr in re.finditer(STACKTRACE_ESP32_BACKTRACE_PC_RE, line):
|
||||
_decode_pc(config, addr.group())
|
||||
|
||||
return backtrace_state
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -419,3 +420,117 @@ def copy_files() -> None:
|
||||
remove_float_scanf_file,
|
||||
CORE.relative_build_path("remove_float_scanf.py"),
|
||||
)
|
||||
|
||||
|
||||
# ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder
|
||||
ESP8266_EXCEPTION_CODES = {
|
||||
0: "Illegal instruction (Is the flash damaged?)",
|
||||
1: "SYSCALL instruction",
|
||||
2: "InstructionFetchError: Processor internal physical address or data error during "
|
||||
"instruction fetch",
|
||||
3: "LoadStoreError: Processor internal physical address or data error during load or store",
|
||||
4: "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT "
|
||||
"register",
|
||||
5: "Alloca: MOVSP instruction, if caller's registers are not in the register file",
|
||||
6: "Integer Divide By Zero",
|
||||
7: "reserved",
|
||||
8: "Privileged: Attempt to execute a privileged operation when CRING ? 0",
|
||||
9: "LoadStoreAlignmentCause: Load or store to an unaligned address",
|
||||
10: "reserved",
|
||||
11: "reserved",
|
||||
12: "InstrPIFDataError: PIF data error during instruction fetch",
|
||||
13: "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access",
|
||||
14: "InstrPIFAddrError: PIF address error during instruction fetch",
|
||||
15: "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access",
|
||||
16: "InstTLBMiss: Error during Instruction TLB refill",
|
||||
17: "InstTLBMultiHit: Multiple instruction TLB entries matched",
|
||||
18: "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level "
|
||||
"less than CRING",
|
||||
19: "reserved",
|
||||
20: "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute "
|
||||
"that does not permit instruction fetch",
|
||||
21: "reserved",
|
||||
22: "reserved",
|
||||
23: "reserved",
|
||||
24: "LoadStoreTLBMiss: Error during TLB refill for a load or store",
|
||||
25: "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store",
|
||||
26: "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less "
|
||||
"than ",
|
||||
27: "reserved",
|
||||
28: "Access to invalid address: LOAD (wild pointer?)",
|
||||
29: "Access to invalid address: STORE (wild pointer?)",
|
||||
}
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
from esphome import platformio_api
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
if not idedata.addr2line_path or not idedata.firmware_elf_path:
|
||||
_LOGGER.debug("decode_pc no addr2line")
|
||||
return
|
||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
||||
try:
|
||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
||||
if "?? ??:0" in translation:
|
||||
# Nothing useful
|
||||
return
|
||||
translation = translation.replace(" at ??:?", "").replace(":?", "")
|
||||
_LOGGER.warning("Decoded %s", translation)
|
||||
|
||||
|
||||
def _parse_register(config, regex, line):
|
||||
match = regex.match(line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
|
||||
STACKTRACE_ESP8266_EXCEPTION_TYPE_RE = re.compile(r"[eE]xception \((\d+)\):")
|
||||
STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_BAD_ALLOC_RE = re.compile(
|
||||
r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$"
|
||||
)
|
||||
STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
|
||||
|
||||
def process_stacktrace(config, line, backtrace_state):
|
||||
line = line.strip()
|
||||
# ESP8266 Exception type
|
||||
match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line)
|
||||
if match is not None:
|
||||
code = int(match.group(1))
|
||||
_LOGGER.warning(
|
||||
"Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, "unknown")
|
||||
)
|
||||
|
||||
# ESP8266 PC/EXCVADDR
|
||||
_parse_register(config, STACKTRACE_ESP8266_PC_RE, line)
|
||||
_parse_register(config, STACKTRACE_ESP8266_EXCVADDR_RE, line)
|
||||
|
||||
# bad alloc
|
||||
match = re.match(STACKTRACE_BAD_ALLOC_RE, line)
|
||||
if match is not None:
|
||||
_LOGGER.warning(
|
||||
"Memory allocation of %s bytes failed at %s", match.group(2), match.group(1)
|
||||
)
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
# ESP8266 multi-line backtrace
|
||||
if ">>>stack>>>" in line:
|
||||
# Start of backtrace
|
||||
backtrace_state = True
|
||||
_LOGGER.warning("Found stack trace! Trying to decode it")
|
||||
elif "<<<stack<<<" in line:
|
||||
# End of backtrace
|
||||
backtrace_state = False
|
||||
|
||||
if backtrace_state:
|
||||
for addr in re.finditer(STACKTRACE_ESP8266_BACKTRACE_PC_RE, line):
|
||||
_decode_pc(config, addr.group())
|
||||
|
||||
return backtrace_state
|
||||
|
||||
@@ -117,8 +117,8 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
" Partition table:\n"
|
||||
" %-12s %-4s %-8s %-10s %-10s",
|
||||
"Name", "Type", "Subtype", "Address", "Size");
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
while (it != NULL) {
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, nullptr);
|
||||
while (it != nullptr) {
|
||||
const esp_partition_t *partition = esp_partition_get(it);
|
||||
ESP_LOGCONFIG(TAG, " %-12s 0x%-2X 0x%-6X 0x%-8" PRIX32 " 0x%-8" PRIX32, partition->label, partition->type,
|
||||
partition->subtype, partition->address, partition->size);
|
||||
|
||||
@@ -70,12 +70,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
stream_ptr = std::make_unique<WiFiClient>();
|
||||
#endif // USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?)
|
||||
if (!secure) {
|
||||
ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 "
|
||||
"in your YAML, or use HTTPS");
|
||||
}
|
||||
#endif // USE_ARDUINO_VERSION_CODE
|
||||
bool status = container->client_.begin(*stream_ptr, url.c_str());
|
||||
|
||||
#elif defined(USE_RP2040)
|
||||
|
||||
@@ -13,22 +13,16 @@
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
// esp-audio-libs
|
||||
#include <gain.h>
|
||||
|
||||
namespace esphome::i2s_audio {
|
||||
|
||||
static const char *const TAG = "i2s_audio.speaker";
|
||||
|
||||
// Lists the Q15 fixed point scaling factor for volume reduction.
|
||||
// Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB.
|
||||
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
|
||||
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
|
||||
static const std::vector<int16_t> Q15_VOLUME_SCALING_FACTORS = {
|
||||
0, 116, 122, 130, 137, 146, 154, 163, 173, 183, 194, 206, 218, 231, 244,
|
||||
259, 274, 291, 308, 326, 345, 366, 388, 411, 435, 461, 488, 517, 548, 580,
|
||||
615, 651, 690, 731, 774, 820, 868, 920, 974, 1032, 1094, 1158, 1227, 1300, 1377,
|
||||
1459, 1545, 1637, 1734, 1837, 1946, 2061, 2184, 2313, 2450, 2596, 2750, 2913, 3085, 3269,
|
||||
3462, 3668, 3885, 4116, 4360, 4619, 4893, 5183, 5490, 5816, 6161, 6527, 6914, 7324, 7758,
|
||||
8218, 8706, 9222, 9770, 10349, 10963, 11613, 12302, 13032, 13805, 14624, 15491, 16410, 17384, 18415,
|
||||
19508, 20665, 21891, 23189, 24565, 26022, 27566, 29201, 30933, 32767};
|
||||
// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor.
|
||||
// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB.
|
||||
static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f;
|
||||
|
||||
void I2SAudioSpeakerBase::setup() {
|
||||
this->event_group_ = xEventGroupCreate();
|
||||
@@ -147,14 +141,16 @@ void I2SAudioSpeakerBase::set_volume(float volume) {
|
||||
} else
|
||||
#endif // USE_AUDIO_DAC
|
||||
{
|
||||
// Fallback to software volume control by using a Q15 fixed point scaling factor.
|
||||
// At maximum volume (1.0), set to INT16_MAX to completely bypass volume processing
|
||||
// Fallback to software volume control by using a Q31 fixed point scaling factor.
|
||||
// At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely
|
||||
// and avoid any floating-point precision issues that could cause slight volume reduction.
|
||||
if (volume >= 1.0f) {
|
||||
this->q15_volume_factor_ = INT16_MAX;
|
||||
this->q31_volume_factor_ = INT32_MAX;
|
||||
} else if (volume <= 0.0f) {
|
||||
this->q31_volume_factor_ = 0;
|
||||
} else {
|
||||
ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1);
|
||||
this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index];
|
||||
this->q31_volume_factor_ =
|
||||
esp_audio_libs::gain::db_to_q31(remap<float, float>(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,7 +169,7 @@ void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
|
||||
{
|
||||
if (mute_state) {
|
||||
// Fallback to software volume control and scale by 0
|
||||
this->q15_volume_factor_ = 0;
|
||||
this->q31_volume_factor_ = 0;
|
||||
} else {
|
||||
// Revert to previous volume when unmuting
|
||||
this->set_volume(this->volume_);
|
||||
@@ -309,29 +305,14 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
|
||||
if (this->q15_volume_factor_ >= INT16_MAX) {
|
||||
if (this->q31_volume_factor_ == INT32_MAX) {
|
||||
return; // Max volume, no processing needed
|
||||
}
|
||||
|
||||
const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
|
||||
const uint32_t len = bytes_read / bytes_per_sample;
|
||||
|
||||
// Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31
|
||||
int32_t shift = 15; // Q31 -> Q16
|
||||
int32_t gain_factor = this->q15_volume_factor_; // Q15
|
||||
|
||||
if (bytes_per_sample >= 3) {
|
||||
// Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31
|
||||
shift = 8; // Q31 -> Q23
|
||||
gain_factor >>= 7; // Q15 -> Q8
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < len; ++i) {
|
||||
int32_t sample = audio::unpack_audio_sample_to_q31(&data[i * bytes_per_sample], bytes_per_sample); // Q31
|
||||
sample >>= shift;
|
||||
sample *= gain_factor; // Q31
|
||||
audio::pack_q31_as_audio_sample(sample, &data[i * bytes_per_sample], bytes_per_sample);
|
||||
}
|
||||
esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample);
|
||||
}
|
||||
|
||||
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
|
||||
|
||||
@@ -151,7 +151,7 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
|
||||
|
||||
bool pause_state_{false};
|
||||
|
||||
int16_t q15_volume_factor_{INT16_MAX};
|
||||
int32_t q31_volume_factor_{INT32_MAX};
|
||||
|
||||
audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info
|
||||
|
||||
|
||||
@@ -280,6 +280,9 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
|
||||
}
|
||||
#else
|
||||
slot_cfg.slot_bit_width = this->slot_bit_width_;
|
||||
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
|
||||
slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_);
|
||||
}
|
||||
#endif // USE_ESP32_VARIANT_ESP32
|
||||
slot_cfg.slot_mask = slot_mask;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import CONF_B_CONSTANT
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
@@ -22,8 +23,6 @@ DEPENDENCIES = ["i2c"]
|
||||
|
||||
lc709203f_ns = cg.esphome_ns.namespace("lc709203f")
|
||||
|
||||
CONF_B_CONSTANT = "b_constant"
|
||||
|
||||
LC709203FBatteryVoltage = lc709203f_ns.enum("LC709203FBatteryVoltage")
|
||||
BATTERY_VOLTAGE_OPTIONS = {
|
||||
"3.7": LC709203FBatteryVoltage.LC709203F_BATTERY_VOLTAGE_3_7,
|
||||
|
||||
@@ -80,8 +80,8 @@ void Logger::pre_setup() {
|
||||
this->uart_dev_ = uart_dev;
|
||||
#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
uint32_t dtr = 0;
|
||||
uint32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs
|
||||
while (dtr == 0 && count-- != 0) {
|
||||
int32_t count = (10 * 100); // wait 10 sec for USB CDC to have early logs
|
||||
while (dtr == 0 && count-- > 0) {
|
||||
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||
delay(10);
|
||||
arch_feed_wdt();
|
||||
@@ -160,6 +160,11 @@ void Logger::dump_crash_() {
|
||||
#if defined(CONFIG_THREAD_NAME)
|
||||
ESP_LOGE(TAG, "Thread: %s", crash_buf.thread);
|
||||
#endif
|
||||
int32_t count = (2 * 100); // wait 2 sec to give a chance to print crash
|
||||
while (count-- > 0) {
|
||||
delay(10);
|
||||
arch_feed_wdt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -309,6 +309,14 @@ LV_EVENT_MAP = {
|
||||
"STYLE_CHANGE": "STYLE_CHANGED",
|
||||
"TRIPLE_CLICK": "TRIPLE_CLICKED",
|
||||
}
|
||||
|
||||
LV_PRESS_EVENTS = ("PRESS", "PRESSING", "RELEASE")
|
||||
|
||||
|
||||
def is_press_event(event: str) -> bool:
|
||||
return event.removeprefix("on_").upper() in LV_PRESS_EVENTS
|
||||
|
||||
|
||||
LV_SCREEN_EVENT_MAP = {
|
||||
"SCREEN_LOAD": "SCREEN_LOADED",
|
||||
"SCREEN_LOAD_START": "SCREEN_LOAD_START",
|
||||
|
||||
@@ -41,7 +41,7 @@ from .helpers import (
|
||||
lv_fonts_used,
|
||||
requires_component,
|
||||
)
|
||||
from .types import lv_gradient_t, lv_opa_t
|
||||
from .types import lv_coord_t, lv_gradient_t, lv_opa_t
|
||||
|
||||
LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
||||
|
||||
@@ -277,7 +277,7 @@ def pixels_or_percent_validator(value):
|
||||
|
||||
pixels_or_percent = LValidator(
|
||||
pixels_or_percent_validator,
|
||||
uint32,
|
||||
lv_coord_t,
|
||||
retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"),
|
||||
)
|
||||
|
||||
|
||||
@@ -890,7 +890,21 @@ lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) {
|
||||
int32_t offset = pos - stop1->frac;
|
||||
return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range);
|
||||
}
|
||||
#endif
|
||||
#endif // USE_LVGL_GRADIENT
|
||||
|
||||
lv_point_t LvglComponent::get_touch_relative_to_obj(lv_obj_t *obj) {
|
||||
auto *indev = lv_indev_get_act();
|
||||
if (indev == nullptr) {
|
||||
return {INT32_MAX, INT32_MAX};
|
||||
}
|
||||
lv_point_t point;
|
||||
lv_indev_get_point(indev, &point);
|
||||
lv_area_t coords;
|
||||
lv_obj_get_coords(obj, &coords);
|
||||
point.x -= coords.x1;
|
||||
point.y -= coords.y1;
|
||||
return point;
|
||||
}
|
||||
|
||||
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
|
||||
LV_TRACE_OBJ_CREATE("begin");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#endif // USE_BINARY_SENSOR
|
||||
#ifdef USE_IMAGE
|
||||
#include "esphome/components/image/image.h"
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#endif // USE_IMAGE
|
||||
#ifdef USE_LVGL_ROTARY_ENCODER
|
||||
#include "esphome/components/rotary_encoder/rotary_encoder.h"
|
||||
#endif // USE_LVGL_ROTARY_ENCODER
|
||||
@@ -32,10 +32,10 @@
|
||||
|
||||
#ifdef USE_FONT
|
||||
#include "esphome/components/font/font.h"
|
||||
#endif // USE_LVGL_FONT
|
||||
#endif // USE_FONT
|
||||
#ifdef USE_TOUCHSCREEN
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#endif // USE_LVGL_TOUCHSCREEN
|
||||
#endif // USE_TOUCHSCREEN
|
||||
|
||||
#if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD)
|
||||
#include "esphome/components/key_provider/key_provider.h"
|
||||
@@ -124,7 +124,8 @@ int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value);
|
||||
*/
|
||||
|
||||
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos);
|
||||
#endif
|
||||
#endif // USE_LVGL_GRADIENT
|
||||
|
||||
// Parent class for things that wrap an LVGL object
|
||||
class LvCompound {
|
||||
public:
|
||||
@@ -169,9 +170,9 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ObjUpdateAction(std::function<void(Ts...)> &&lamb) : lamb_(std::move(lamb)) {}
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override { this->lamb_(x...); }
|
||||
|
||||
protected:
|
||||
std::function<void(Ts...)> lamb_;
|
||||
};
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
@@ -190,6 +191,12 @@ class LvglComponent : public PollingComponent {
|
||||
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
|
||||
bool resume_on_input, bool update_when_display_idle, RotationType rotation_type);
|
||||
static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p);
|
||||
/**
|
||||
*
|
||||
* @param obj A widget
|
||||
* @return The position of the last indev point relative to the widget's origin.
|
||||
*/
|
||||
static lv_point_t get_touch_relative_to_obj(lv_obj_t *obj);
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void setup() override;
|
||||
@@ -311,9 +318,9 @@ class IdleTrigger : public Trigger<> {
|
||||
template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
|
||||
public:
|
||||
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {}
|
||||
void play(const Ts &...x) override { this->action_(this->parent_); }
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override { this->action_(this->parent_); }
|
||||
std::function<void(LvglComponent *)> action_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from .defines import (
|
||||
CONF_TIME_FORMAT,
|
||||
LV_GRAD_DIR,
|
||||
get_remapped_uses,
|
||||
is_press_event,
|
||||
)
|
||||
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||
from .layout import (
|
||||
@@ -46,6 +47,7 @@ from .types import (
|
||||
LvType,
|
||||
lv_group_t,
|
||||
lv_obj_t,
|
||||
lv_point_t,
|
||||
lv_pseudo_button_t,
|
||||
lv_style_t,
|
||||
)
|
||||
@@ -123,8 +125,8 @@ ENCODER_SCHEMA = cv.Schema(
|
||||
|
||||
POINT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_X): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_Y): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_X): lvalid.pixels_or_percent,
|
||||
cv.Required(CONF_Y): lvalid.pixels_or_percent,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -137,9 +139,13 @@ def point_schema(value):
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return POINT_SCHEMA(value)
|
||||
if isinstance(value, list):
|
||||
if len(value) != 2:
|
||||
raise cv.Invalid("Invalid point format, should be <x_int>, <y_int>")
|
||||
return POINT_SCHEMA({CONF_X: value[0], CONF_Y: value[1]})
|
||||
try:
|
||||
x, y = map(int, value.split(","))
|
||||
return {CONF_X: x, CONF_Y: y}
|
||||
x, y = str(value).split(",")
|
||||
return POINT_SCHEMA({CONF_X: x, CONF_Y: y})
|
||||
except ValueError:
|
||||
pass
|
||||
# not raising this in the catch block because pylint doesn't like it
|
||||
@@ -366,13 +372,20 @@ def automation_schema(typ: LvType):
|
||||
if typ.has_on_value:
|
||||
events = events + (CONF_ON_VALUE,)
|
||||
args = typ.get_arg_type()
|
||||
args.append(lv_event_t_ptr)
|
||||
|
||||
def get_trigger_args(event):
|
||||
result = args.copy()
|
||||
if is_press_event(event):
|
||||
result.append(lv_point_t)
|
||||
result.append(lv_event_t_ptr)
|
||||
return result
|
||||
|
||||
return {
|
||||
**{
|
||||
cv.Optional(event): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
Trigger.template(*args)
|
||||
Trigger.template(*get_trigger_args(event))
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ from .defines import (
|
||||
LV_SCREEN_EVENT_MAP,
|
||||
LV_SCREEN_EVENT_TRIGGERS,
|
||||
SWIPE_TRIGGERS,
|
||||
is_press_event,
|
||||
literal,
|
||||
)
|
||||
from .lvcode import (
|
||||
@@ -34,11 +35,10 @@ from .lvcode import (
|
||||
LvConditional,
|
||||
lv,
|
||||
lv_add,
|
||||
lv_event_t_ptr,
|
||||
lv_expr,
|
||||
lvgl_static,
|
||||
)
|
||||
from .types import LV_EVENT
|
||||
from .types import LV_EVENT, lv_point_t
|
||||
from .widgets import LvScrActType, get_screen_active, widget_map
|
||||
|
||||
|
||||
@@ -133,19 +133,24 @@ def _get_event_literal(trigger: str | MockObj) -> MockObj:
|
||||
return literal("LV_EVENT_" + TRIGGER_MAP[trigger.upper()])
|
||||
|
||||
|
||||
async def add_trigger(conf, w, *events, is_selected=None):
|
||||
async def add_trigger(conf, w, *events: str | MockObj, is_selected=None):
|
||||
is_selected = is_selected or w.is_selected()
|
||||
tid = conf[CONF_TRIGGER_ID]
|
||||
trigger = cg.new_Pvariable(tid)
|
||||
args = w.get_args() + [(lv_event_t_ptr, "event")]
|
||||
value = w.get_values()
|
||||
args = w.get_args()
|
||||
value: list = w.get_values()
|
||||
if len(events) == 1 and is_press_event(str(events[0])):
|
||||
# Make the touch point available for selected events
|
||||
args.append((lv_point_t, "point"))
|
||||
value.append(lvgl_static.get_touch_relative_to_obj(w.obj))
|
||||
args.extend(EVENT_ARG)
|
||||
await automation.build_automation(trigger, args, conf)
|
||||
async with LambdaContext(EVENT_ARG, where=tid) as context:
|
||||
with LvConditional(is_selected):
|
||||
lv_add(trigger.trigger(*value, literal("event")))
|
||||
callback = await context.get_lambda()
|
||||
event_literals = [_get_event_literal(event) for event in events]
|
||||
if isinstance(events[0], str) and events[0] in DISPLAY_TRIGGERS:
|
||||
if str(events[0]) in DISPLAY_TRIGGERS:
|
||||
assert len(events) == 1
|
||||
lv.display_add_event_cb(
|
||||
lv_expr.obj_get_display(w.obj), callback, event_literals[0], nullptr
|
||||
|
||||
@@ -70,6 +70,8 @@ lv_image_t = LvType("lv_image_t")
|
||||
lv_gradient_t = LvType("lv_grad_dsc_t")
|
||||
lv_event_t = LvType("lv_event_t")
|
||||
RotationType = lvgl_ns.enum("RotationType")
|
||||
lv_point_t = cg.global_ns.struct("lv_point_t")
|
||||
lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t")
|
||||
|
||||
LV_EVENT = MockObj(base="LV_EVENT_", op="")
|
||||
LV_STATE = MockObj(base="LV_STATE_", op="")
|
||||
|
||||
@@ -366,7 +366,7 @@ class Widget:
|
||||
|
||||
def get_args(self):
|
||||
if isinstance(self.type.w_type, LvType):
|
||||
return self.type.w_type.args
|
||||
return self.type.w_type.args.copy()
|
||||
return [(lv_obj_t_ptr, "obj")]
|
||||
|
||||
def get_value(self):
|
||||
|
||||
@@ -52,14 +52,14 @@ from ..lv_validation import (
|
||||
lv_text,
|
||||
opacity,
|
||||
pixels,
|
||||
pixels_or_percent,
|
||||
size,
|
||||
)
|
||||
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||
from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property
|
||||
from ..types import LvType, ObjUpdateAction
|
||||
from ..types import LvType, ObjUpdateAction, lv_point_precise_t
|
||||
from . import Widget, WidgetType, get_widgets
|
||||
from .img import CONF_IMAGE
|
||||
from .line import lv_point_precise_t, process_coord
|
||||
|
||||
CONF_CANVAS = "canvas"
|
||||
CONF_BUFFER_ID = "buffer_id"
|
||||
@@ -434,6 +434,13 @@ LINE_PROPS = {
|
||||
}
|
||||
|
||||
|
||||
def _validate_points(config):
|
||||
for index, point in enumerate(config[CONF_POINTS]):
|
||||
if not all(isinstance(p, int) for p in point.values()):
|
||||
raise cv.Invalid("Points must be integers", path=[CONF_POINTS, index])
|
||||
return config
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_line",
|
||||
ObjUpdateAction,
|
||||
@@ -444,12 +451,15 @@ LINE_PROPS = {
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()},
|
||||
}
|
||||
),
|
||||
).add_extra(_validate_points),
|
||||
synchronous=True,
|
||||
)
|
||||
async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
points = [
|
||||
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
|
||||
[
|
||||
await pixels.process(p[CONF_X]),
|
||||
await pixels.process(p[CONF_Y]),
|
||||
]
|
||||
for p in config[CONF_POINTS]
|
||||
]
|
||||
|
||||
@@ -470,12 +480,15 @@ async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
},
|
||||
),
|
||||
).add_extra(_validate_points),
|
||||
synchronous=True,
|
||||
)
|
||||
async def canvas_draw_polygon(config, action_id, template_arg, args):
|
||||
points = [
|
||||
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
|
||||
[
|
||||
await pixels_or_percent.process(p[CONF_X]),
|
||||
await pixels_or_percent.process(p[CONF_Y]),
|
||||
]
|
||||
for p in config[CONF_POINTS]
|
||||
]
|
||||
# Close the polygon
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_X, CONF_Y
|
||||
from esphome.core import Lambda
|
||||
|
||||
from ..defines import CONF_MAIN, call_lambda
|
||||
from ..defines import CONF_MAIN
|
||||
from ..lv_validation import pixels_or_percent
|
||||
from ..lvcode import lv_add
|
||||
from ..schemas import point_schema
|
||||
from ..types import LvCompound, LvType, lv_coord_t
|
||||
from ..types import LvCompound, LvType
|
||||
from . import Widget, WidgetType
|
||||
|
||||
CONF_LINE = "line"
|
||||
CONF_POINTS = "points"
|
||||
CONF_POINT_LIST_ID = "point_list_id"
|
||||
|
||||
lv_point_t = cg.global_ns.struct("lv_point_t")
|
||||
lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t")
|
||||
|
||||
|
||||
async def process_coord(coord):
|
||||
if isinstance(coord, Lambda):
|
||||
return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
|
||||
return cg.safe_exp(coord)
|
||||
|
||||
|
||||
class LineType(WidgetType):
|
||||
def __init__(self):
|
||||
@@ -36,7 +26,10 @@ class LineType(WidgetType):
|
||||
async def to_code(self, w: Widget, config):
|
||||
if CONF_POINTS in config:
|
||||
points = [
|
||||
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
|
||||
[
|
||||
await pixels_or_percent.process(p[CONF_X]),
|
||||
await pixels_or_percent.process(p[CONF_Y]),
|
||||
]
|
||||
for p in config[CONF_POINTS]
|
||||
]
|
||||
lv_add(w.var.set_points(points))
|
||||
|
||||
@@ -133,6 +133,7 @@ def request_codecs_for_format_configs(
|
||||
audio.request_flac_support()
|
||||
audio.request_mp3_support()
|
||||
audio.request_opus_support()
|
||||
audio.request_wav_support()
|
||||
else:
|
||||
if "FLAC" in needed_formats:
|
||||
audio.request_flac_support()
|
||||
@@ -140,6 +141,8 @@ def request_codecs_for_format_configs(
|
||||
audio.request_mp3_support()
|
||||
if "OPUS" in needed_formats:
|
||||
audio.request_opus_support()
|
||||
if "WAV" in needed_formats:
|
||||
audio.request_wav_support()
|
||||
|
||||
|
||||
# Local config key constants
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user