mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 00:12:11 +00:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f765a733 | ||
|
|
3a62bd2a4e | ||
|
|
7dd3a6f451 | ||
|
|
a7b5429032 | ||
|
|
3f31aeeccf | ||
|
|
b8c26bea20 | ||
|
|
1a50e1394a | ||
|
|
ae1f912a5f | ||
|
|
916798184d | ||
|
|
1e49616bc5 | ||
|
|
c66f71bb96 | ||
|
|
8db0774d67 | ||
|
|
f735c4634e | ||
|
|
fe125726ee | ||
|
|
e05f2fc74e | ||
|
|
4e68f1d324 | ||
|
|
98706c594b | ||
|
|
633c2b7f71 | ||
|
|
516511b346 | ||
|
|
f72dc7c329 | ||
|
|
f1695ac0b0 | ||
|
|
24c64a8efd | ||
|
|
d3638336ae | ||
|
|
53bffb560b | ||
|
|
2af39e6643 | ||
|
|
533a3afebd | ||
|
|
ddb7a6d9aa | ||
|
|
ae99accace | ||
|
|
474667d1a8 | ||
|
|
dbc93bb133 | ||
|
|
5486ef400f | ||
|
|
bd723f75af | ||
|
|
efba19ea3a | ||
|
|
6c68985c14 | ||
|
|
7be8a7c390 | ||
|
|
eb8cf139d7 | ||
|
|
ab6ebac628 | ||
|
|
6cb622417a | ||
|
|
960316abd6 | ||
|
|
87341dc136 | ||
|
|
cfe5809654 | ||
|
|
9c54f96520 | ||
|
|
0cc5b372fb | ||
|
|
f7c8cba8b7 | ||
|
|
79a22e9a9a | ||
|
|
103023737d | ||
|
|
6b6b70a8b6 | ||
|
|
d312bf1b73 |
No files matched your search
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -23,13 +23,13 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
if: steps.tag.outputs.push == 'true'
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -154,14 +154,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -422,7 +422,7 @@ jobs:
|
||||
version: 1.2
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore integration library cache
|
||||
@@ -441,7 +441,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -462,6 +462,7 @@ jobs:
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
id: integration
|
||||
env:
|
||||
# JSON array of test paths; parsed into a bash array below to avoid
|
||||
# shell word-splitting / glob hazards.
|
||||
@@ -477,6 +478,32 @@ jobs:
|
||||
# A hung test fails after 30 min: the 15 min shared build lock wait plus a cold compile
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal --timeout=1800 \
|
||||
--junitxml=junit-integration.xml "${test_files[@]}"
|
||||
- name: Re-run the failed tests with debug symbols
|
||||
# The first run builds without -g. A crash in the re-run prints a gdb
|
||||
# backtrace of its core; exit 1 keeps the job red when the re-run passes.
|
||||
if: failure() && steps.integration.outcome == 'failure'
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
ESPHOME_INTEGRATION_DEBUG_SYMBOLS: "1"
|
||||
ESPHOME_INTEGRATION_CORE_DIR: /tmp/esphome-cores
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
# The image's package lists usually suffice; refresh them only if not
|
||||
if ! command -v gdb >/dev/null &&
|
||||
! sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 90 apt-get install -y -qq gdb; then
|
||||
sudo timeout -k 15 300 apt-get update -qq &&
|
||||
sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 300 apt-get install -y -qq gdb ||
|
||||
echo "::warning::gdb install failed; no backtraces"
|
||||
fi
|
||||
{
|
||||
mkdir -p "$ESPHOME_INTEGRATION_CORE_DIR" &&
|
||||
echo "$ESPHOME_INTEGRATION_CORE_DIR/core.%p" | sudo tee /proc/sys/kernel/core_pattern &&
|
||||
ulimit -c unlimited
|
||||
} || echo "::warning::core dumps not enabled; no backtraces"
|
||||
# Under the step limit, so a hung test still reports before Actions kills the step
|
||||
pytest -vv --no-cov --tb=native -n auto --dist worksteal --timeout=600 \
|
||||
--lf --lfnf=none tests/integration || true
|
||||
exit 1
|
||||
- name: Upload test artifacts
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
@@ -630,7 +657,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@821a40cd6c80e3cfe387caf55668287addd808da # v5.5.0
|
||||
uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -1457,7 +1484,7 @@ jobs:
|
||||
ref: main
|
||||
path: device-builder
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Set up uv
|
||||
@@ -1466,7 +1493,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Build
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -126,12 +126,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -206,13 +206,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.17.1
|
||||
rev: v0.17.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
+17
-10
@@ -1305,10 +1305,7 @@ def upload_program(
|
||||
f"{option_string} 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_web_server(config, network_devices, _app_ota_image(args))
|
||||
|
||||
return _upload_via_native_api(config, network_devices, args)
|
||||
|
||||
@@ -1364,6 +1361,16 @@ def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
|
||||
return next(iter(esphome_ota_items(config)), None)
|
||||
|
||||
|
||||
def _app_ota_image(args: ArgsProtocol) -> Path:
|
||||
"""The file an app update sends: ``--file`` or the build output, as the
|
||||
target platform's ota_image hook picks it."""
|
||||
binary = CORE.firmware_bin
|
||||
if (file := getattr(args, "file", None)) is not None:
|
||||
binary = Path(file)
|
||||
ota_image = platform_hooks.get_platform_hook(CORE.target_platform, "ota_image")
|
||||
return ota_image(binary) if ota_image is not None else binary
|
||||
|
||||
|
||||
def _upload_via_native_api(
|
||||
config: ConfigType, network_devices: list[str], args: ArgsProtocol
|
||||
) -> tuple[int, str | None]:
|
||||
@@ -1413,27 +1420,27 @@ def _upload_via_native_api(
|
||||
f"flash a build with the option enabled, and then retry {option_string}."
|
||||
)
|
||||
|
||||
binary = CORE.firmware_bin
|
||||
ota_type = espota2.OTA_TYPE_UPDATE_APP
|
||||
file = getattr(args, "file", None)
|
||||
if getattr(args, "partition_table", False):
|
||||
# Fail fast if the resolved ESPHome OTA config does not enable allow_partition_access.
|
||||
# The device-side handshake also rejects this with "Device only supports app updates",
|
||||
# but checking here surfaces the misconfiguration before opening a network connection.
|
||||
check_partition_access("--partition-table")
|
||||
binary = CORE.partition_table_bin
|
||||
binary = Path(file) if file is not None else CORE.partition_table_bin
|
||||
ota_type = espota2.OTA_TYPE_UPDATE_PARTITION_TABLE
|
||||
elif getattr(args, "bootloader", False):
|
||||
check_partition_access("--bootloader")
|
||||
if (
|
||||
getattr(args, "file", None) is None
|
||||
file is None
|
||||
and (native := native_backend())
|
||||
and (hint := getattr(native, "missing_image_hint", lambda: None)())
|
||||
):
|
||||
raise EsphomeError(hint)
|
||||
binary = CORE.bootloader_bin
|
||||
binary = Path(file) if file is not None else CORE.bootloader_bin
|
||||
ota_type = espota2.OTA_TYPE_UPDATE_BOOTLOADER
|
||||
if getattr(args, "file", None) is not None:
|
||||
binary = Path(args.file)
|
||||
else:
|
||||
binary = _app_ota_image(args)
|
||||
|
||||
if ota_type == espota2.OTA_TYPE_UPDATE_PARTITION_TABLE:
|
||||
_validate_partition_table_binary(binary)
|
||||
|
||||
@@ -25,6 +25,7 @@ from esphome.framework_helpers import (
|
||||
get_project_link_flags,
|
||||
)
|
||||
from esphome.helpers import get_bool_env, mkdir_p, write_file_if_changed
|
||||
from esphome.platformio.library import lex_build_flags
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -154,6 +155,47 @@ def has_discovered_components() -> bool:
|
||||
return bool(get_available_components())
|
||||
|
||||
|
||||
_KEY_WARNED_BUILD_FLAGS = "warned_build_flags"
|
||||
|
||||
|
||||
def _lexed_build_flags() -> list[str]:
|
||||
"""Shell-lex the build flag entries as the other generators do, so an
|
||||
entry holding several flags is classified flag by flag. Warns once per
|
||||
entry about a flag that native ESP-IDF builds do not pass on."""
|
||||
warned: set[tuple[str, str]] = CORE.data.setdefault(KEY_ESP32, {}).setdefault(
|
||||
_KEY_WARNED_BUILD_FLAGS, set()
|
||||
)
|
||||
flags: list[str] = []
|
||||
for entry in sorted(CORE.build_flags):
|
||||
tokens = lex_build_flags(entry, "esphome build_flags")
|
||||
for token in tokens:
|
||||
if (
|
||||
(token, entry) not in warned
|
||||
and not get_project_compile_flags([token])
|
||||
and not get_project_link_flags([token])
|
||||
):
|
||||
warned.add((token, entry))
|
||||
_LOGGER.warning(
|
||||
"Build flag '%s'%s is ignored: native ESP-IDF builds only "
|
||||
"pass -D, -W and -Wl, flags",
|
||||
token,
|
||||
"" if token == entry else f" in '{entry}'",
|
||||
)
|
||||
flags += tokens
|
||||
return flags
|
||||
|
||||
|
||||
def _cmake_args(flags: list[str]) -> list[str]:
|
||||
"""Quote each flag as a CMake argument so quotes, backslashes and $
|
||||
survive."""
|
||||
return [
|
||||
'"{}"'.format(
|
||||
flag.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$")
|
||||
)
|
||||
for flag in flags
|
||||
]
|
||||
|
||||
|
||||
def _cmake_quote(value: str) -> str:
|
||||
"""Quote a cmake arg value for a set() line. add_cmake_arg rejects
|
||||
whitespace, quotes, and '$', so only backslashes need escaping."""
|
||||
@@ -294,18 +336,24 @@ def get_project_cmakelists(
|
||||
# esphome__micro-mp3) rather than just src/. Required so suppressions
|
||||
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
||||
# third-party components we don't author.
|
||||
project_compile_opts = get_project_compile_flags()
|
||||
# CMake keeps only the first copy of a repeated option; drop all but the
|
||||
# last copy instead so the last definition wins, as on the command line.
|
||||
project_compile_opts = list(
|
||||
reversed(
|
||||
dict.fromkeys(reversed(get_project_compile_flags(_lexed_build_flags())))
|
||||
)
|
||||
)
|
||||
extra_compile_options = "\n".join(
|
||||
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
||||
for flag in project_compile_opts
|
||||
f"idf_build_set_property(COMPILE_OPTIONS {flag} APPEND)"
|
||||
for flag in _cmake_args(project_compile_opts)
|
||||
)
|
||||
|
||||
# Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS
|
||||
# (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as
|
||||
# -Wno-volatile is passed on a C compile.
|
||||
cxx_compile_options = "\n".join(
|
||||
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
|
||||
for flag in get_project_cxx_compile_flags()
|
||||
f"idf_build_set_property(CXX_COMPILE_OPTIONS {flag} APPEND)"
|
||||
for flag in _cmake_args(get_project_cxx_compile_flags())
|
||||
)
|
||||
|
||||
cpp_standard_options = (
|
||||
@@ -461,7 +509,7 @@ def get_component_cmakelists() -> str:
|
||||
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
||||
# emitted project-wide via idf_build_set_property in
|
||||
# get_project_cmakelists so they reach every component, not just src/.
|
||||
link_opts = get_project_link_flags()
|
||||
link_opts = _cmake_args(get_project_link_flags(_lexed_build_flags()))
|
||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||
src_cmake = "\n".join(CORE.data[KEY_ESP32].get(KEY_SRC_CMAKE, []))
|
||||
|
||||
|
||||
@@ -33,9 +33,35 @@ _CACHE_VERSION = 1
|
||||
_LAMBDA_KEY = "__esphome_lambda__"
|
||||
|
||||
|
||||
_CACHE_SUFFIX = ".validated.json"
|
||||
|
||||
|
||||
def compiled_config_path(config_filename: str) -> Path:
|
||||
"""Path to the cached validated config alongside the storage sidecar."""
|
||||
return CORE.data_dir / "storage" / f"{config_filename}.validated.json"
|
||||
return CORE.data_dir / "storage" / f"{config_filename}{_CACHE_SUFFIX}"
|
||||
|
||||
|
||||
def invalidate_compiled_config() -> None:
|
||||
"""Drop every cache in the storage dir after a change to a file the
|
||||
mtime check cannot see, such as secrets.yaml or an include; other
|
||||
configurations may share that file."""
|
||||
storage = compiled_config_path(CORE.config_filename).parent
|
||||
try:
|
||||
caches = [p for p in storage.iterdir() if p.name.endswith(_CACHE_SUFFIX)]
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as err:
|
||||
raise EsphomeError(
|
||||
f"Could not list the validated config caches in {storage}: {err}"
|
||||
) from err
|
||||
for path in caches:
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
# A stale cache would present the old key on the next upload
|
||||
raise EsphomeError(
|
||||
f"Could not remove the validated config cache {path}: {err}"
|
||||
) from err
|
||||
|
||||
|
||||
def save_compiled_config(config: ConfigType) -> None:
|
||||
|
||||
@@ -297,9 +297,7 @@ void ADE7880::init_device_() {
|
||||
void ADE7880::reset_device_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
ESP_LOGD(TAG, "Reset device using RESET pin");
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 1});
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Reset device using SWRST command");
|
||||
this->write_u16_register16_(CONFIG, CONFIG_SWRST);
|
||||
|
||||
@@ -253,8 +253,9 @@ template<typename... Ts> class APIRespondAction final : public Action<Ts...> {
|
||||
#endif
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - it's always first for optional/only/status modes
|
||||
auto args = std::make_tuple(x...);
|
||||
// Extract call_id from first argument - it's always first for optional/only/status modes.
|
||||
// By reference: array args are non-copyable FixedVector views.
|
||||
auto args = std::forward_as_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
|
||||
bool success = this->success_.value(x...);
|
||||
@@ -308,7 +309,7 @@ template<typename... Ts> class APIUnregisterServiceCallAction final : public Act
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - same convention as APIRespondAction
|
||||
auto args = std::make_tuple(x...);
|
||||
auto args = std::forward_as_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
if (call_id != 0) {
|
||||
this->parent_->unregister_active_action_call(call_id);
|
||||
|
||||
@@ -16,13 +16,7 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a,
|
||||
}
|
||||
|
||||
void AXS15231Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(10);
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 5, .high_after_ms = 10});
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
this->interrupt_pin_->setup();
|
||||
|
||||
@@ -78,3 +78,7 @@ async def pin_to_code(config: ConfigType) -> MockObj:
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
|
||||
# Called by the OTA upload; the shared libretiny implementation.
|
||||
ota_image = libretiny.device_ota_image
|
||||
@@ -45,9 +45,8 @@ void CopyCover::control(const cover::CoverCall &call) {
|
||||
auto position = call.get_position();
|
||||
if (position.has_value())
|
||||
call2.set_position(*position);
|
||||
auto tilt2 = call.get_tilt();
|
||||
if (tilt2.has_value())
|
||||
call2.set_tilt(*tilt2);
|
||||
if (call.get_toggle().has_value())
|
||||
call2.set_command_toggle();
|
||||
call2.perform();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,6 @@ void CopyLock::control(const lock::LockCall &call) {
|
||||
call2.perform();
|
||||
}
|
||||
|
||||
void CopyLock::open_latch() { this->source_->open(); }
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -13,6 +13,7 @@ class CopyLock final : public lock::Lock, public Component {
|
||||
|
||||
protected:
|
||||
void control(const lock::LockCall &call) override;
|
||||
void open_latch() override;
|
||||
|
||||
lock::Lock *source_;
|
||||
};
|
||||
|
||||
@@ -6,12 +6,7 @@ ESPHOME_LOG_TAG(TAG, "cst226.touchscreen");
|
||||
|
||||
void CST226Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
|
||||
this->set_timeout(30, [this] { this->continue_setup_(); });
|
||||
} else {
|
||||
this->continue_setup_();
|
||||
|
||||
@@ -43,9 +43,7 @@ void CST328Touchscreen::setup() {
|
||||
}
|
||||
|
||||
void CST328Touchscreen::reset_device_() {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 5});
|
||||
this->set_timeout(CST328_TRANSITION_TIMEOUT, [this] { this->continue_setup_(); });
|
||||
}
|
||||
|
||||
|
||||
@@ -52,12 +52,7 @@ void CST816Touchscreen::continue_setup_() {
|
||||
|
||||
void CST816Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
|
||||
this->set_timeout(30, [this] { this->continue_setup_(); });
|
||||
} else {
|
||||
this->continue_setup_();
|
||||
|
||||
@@ -10,14 +10,7 @@ static constexpr uint32_t REREAD_TIMEOUT_ID = 1;
|
||||
static constexpr uint32_t REREAD_DELAY_MS = 20;
|
||||
|
||||
void CST9220Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 10});
|
||||
// Wait for the controller to leave its bootloader before talking to it.
|
||||
this->set_timeout(30, [this] { this->continue_setup_(); });
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class DemoCover final : public cover::Cover, public Component {
|
||||
}
|
||||
if (call.get_stop()) {
|
||||
this->cancel_timeout(MOVE_TIMEOUT_ID);
|
||||
this->current_operation = cover::COVER_OPERATION_IDLE;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
|
||||
@@ -42,6 +42,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_LINE_READER")
|
||||
await uart.new_uart_component(config)
|
||||
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0395");
|
||||
static constexpr char ASCII_CR = 0x0D;
|
||||
static constexpr char ASCII_LF = 0x0A;
|
||||
static constexpr float DET_RANGE_STEP_M = 0.15f;
|
||||
static constexpr float LATENCY_STEP_S = 0.025f;
|
||||
// Sent after a reset with no line end
|
||||
static constexpr char PROMPT[] PROGMEM = "leapMMW:/>";
|
||||
|
||||
// What each CommandType cannot do while the sensor runs; empty for commands that do not need it stopped
|
||||
PROGMEM_STRING_TABLE(BusyActions, "", "configure range config", "configure output latency",
|
||||
@@ -121,9 +121,9 @@ bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
|
||||
return false; // Command not done yet
|
||||
}
|
||||
|
||||
if (this->read_message_()) {
|
||||
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
|
||||
if (!this->handle_reply_(cmd))
|
||||
if (const char *message = this->read_message_()) {
|
||||
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("is not recognized as a CLI command")) == nullptr) {
|
||||
if (!this->handle_reply_(cmd, message))
|
||||
return false; // Command not done yet
|
||||
this->read_message_(); // Skip the prompt
|
||||
return true;
|
||||
@@ -146,12 +146,13 @@ bool DfrobotSen0395Component::run_command_(const Command &cmd, uint32_t now) {
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::read_state_() {
|
||||
if (!this->read_message_())
|
||||
const char *message = this->read_message_();
|
||||
if (message == nullptr)
|
||||
return;
|
||||
if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
|
||||
if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,0, , , *")) != nullptr) {
|
||||
this->set_detected_(false);
|
||||
this->set_active(true);
|
||||
} else if (ESPHOME_strstr_P(this->read_buffer_, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
|
||||
} else if (ESPHOME_strstr_P(message, ESPHOME_PSTR("$JYBSS,1, , , *")) != nullptr) {
|
||||
this->set_detected_(true);
|
||||
this->set_active(true);
|
||||
}
|
||||
@@ -206,8 +207,7 @@ void DfrobotSen0395Component::format_command(const Command &cmd, char *buf, size
|
||||
}
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
|
||||
const char *message = this->read_buffer_;
|
||||
bool DfrobotSen0395Component::handle_reply_(const Command &cmd, const char *message) {
|
||||
auto is = [message](ESPHOME_PGM_P text) { return ESPHOME_strcmp_P(message, text) == 0; };
|
||||
|
||||
// Commands with their own replies
|
||||
@@ -230,7 +230,7 @@ bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
|
||||
}
|
||||
return true;
|
||||
case CommandType::COMMAND_TYPE_RESET_SYSTEM:
|
||||
if (!is(ESPHOME_PSTR("leapMMW:/>")))
|
||||
if (!is(PROMPT))
|
||||
return false;
|
||||
ESP_LOGI(TAG, "Restarted sensor.");
|
||||
return true;
|
||||
@@ -300,34 +300,24 @@ bool DfrobotSen0395Component::handle_reply_(const Command &cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DfrobotSen0395Component::read_message_() {
|
||||
const char *DfrobotSen0395Component::read_message_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte);
|
||||
|
||||
if (byte == ASCII_CR)
|
||||
continue;
|
||||
if (byte >= 0x7F)
|
||||
byte = '?'; // needs to be valid utf8 string for log functions.
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
|
||||
if (this->read_pos_ == 9 && byte == '>')
|
||||
this->read_buffer_[++this->read_pos_] = ASCII_LF;
|
||||
|
||||
if (this->read_buffer_[this->read_pos_] == ASCII_LF) {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->read_pos_ = 0;
|
||||
ESP_LOGV(TAG, "Message: %s", this->read_buffer_);
|
||||
return true; // Full message in buffer
|
||||
const char *line = this->line_reader_.feed(byte);
|
||||
if (line == nullptr && byte == '>' && this->line_reader_.length() == sizeof(PROMPT) - 1) {
|
||||
line = this->line_reader_.partial();
|
||||
this->line_reader_.reset();
|
||||
}
|
||||
this->read_pos_++;
|
||||
if (line != nullptr) {
|
||||
ESP_LOGV(TAG, "Message: %s", line);
|
||||
return line;
|
||||
}
|
||||
this->line_reader_.log_if_overflowed(TAG);
|
||||
}
|
||||
return false; // No full message yet
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void DfrobotSen0395Component::set_detected_(bool detected) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/line_reader.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
@@ -14,7 +15,7 @@
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 255;
|
||||
static constexpr uint8_t MMWAVE_READ_BUFFER_LENGTH = 253; // largest LineReader size with a one byte position
|
||||
static constexpr uint8_t COMMAND_QUEUE_SIZE = 20;
|
||||
|
||||
// Keep in sync with the BusyActions table in dfrobot_sen0395.cpp
|
||||
@@ -121,8 +122,7 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
bool led_active_{false};
|
||||
bool uart_presence_active_{false};
|
||||
bool start_after_boot_{false};
|
||||
char read_buffer_[MMWAVE_READ_BUFFER_LENGTH];
|
||||
size_t read_pos_{0};
|
||||
LineReader<MMWAVE_READ_BUFFER_LENGTH> line_reader_;
|
||||
bool cmd_sent_{false};
|
||||
uint8_t retries_left_{2};
|
||||
StaticRingBuffer<Command, COMMAND_QUEUE_SIZE> cmd_queue_;
|
||||
@@ -131,12 +131,13 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
int16_t det_range_steps_[8]{};
|
||||
uint16_t latency_steps_[2]{};
|
||||
|
||||
bool read_message_();
|
||||
/// The next complete line, valid until the next call, or nullptr
|
||||
const char *read_message_();
|
||||
void read_state_();
|
||||
/// Run the command at the front of the queue; true when it is done
|
||||
bool run_command_(const Command &cmd, uint32_t now);
|
||||
/// Handle a reply to a sent command; true when the command is done
|
||||
bool handle_reply_(const Command &cmd);
|
||||
bool handle_reply_(const Command &cmd, const char *message);
|
||||
|
||||
void set_detected_(bool detected);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from esphome import automation, core, pins
|
||||
@@ -34,6 +35,8 @@ from esphome.types import ConfigType
|
||||
DOMAIN = "display"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
display_ns = cg.esphome_ns.namespace("display")
|
||||
Display = display_ns.class_("Display", cg.PollingComponent)
|
||||
DisplayBuffer = display_ns.class_("DisplayBuffer", Display)
|
||||
@@ -102,13 +105,21 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _validate_test_card(config):
|
||||
def _validate_test_card(config: ConfigType) -> ConfigType:
|
||||
if (
|
||||
config.get(CONF_SHOW_TEST_CARD, False)
|
||||
and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN
|
||||
and (update_interval := config.get(CONF_UPDATE_INTERVAL)) is not None
|
||||
and update_interval.total_milliseconds == SCHEDULER_DONT_RUN
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card."
|
||||
# Only a warning: something else can still update the display, for example
|
||||
# component.update, and the test card is drawn then.
|
||||
display_id = config.get(CONF_ID)
|
||||
_LOGGER.warning(
|
||||
"%s`%s: True` is used with `%s: never`, so the test card is only drawn "
|
||||
"when something updates the display (for example component.update)",
|
||||
f"{display_id.id}: " if display_id is not None and display_id.id else "",
|
||||
CONF_SHOW_TEST_CARD,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ CONF_CUSTOM_PATTERNS = "custom_patterns"
|
||||
CONF_SKIP_CRC = "skip_crc"
|
||||
CONF_DEFAULT_OBIS = "default_obis"
|
||||
|
||||
DLMS_PARSER_VERSION = "2.2.0"
|
||||
DLMS_PARSER_VERSION = "2.2.2"
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
|
||||
@@ -20,9 +20,7 @@ void EKTF2232Touchscreen::setup() {
|
||||
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
|
||||
this->reset_pin_->setup();
|
||||
|
||||
this->hard_reset_();
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 15, .high_after_ms = 15});
|
||||
if (!this->soft_reset_()) {
|
||||
ESP_LOGE(TAG, "Failed to soft reset EKT2232!");
|
||||
this->interrupt_pin_->detach_interrupt();
|
||||
@@ -96,13 +94,6 @@ bool EKTF2232Touchscreen::get_power_state() {
|
||||
return (received[1] >> 3) & 1;
|
||||
}
|
||||
|
||||
void EKTF2232Touchscreen::hard_reset_() {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(15);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(15);
|
||||
}
|
||||
|
||||
bool EKTF2232Touchscreen::soft_reset_() {
|
||||
auto err = this->write(SOFT_RESET_CMD, 4);
|
||||
if (err != i2c::ERROR_OK)
|
||||
|
||||
@@ -22,7 +22,6 @@ class EKTF2232Touchscreen final : public Touchscreen, public i2c::I2CDevice {
|
||||
bool get_power_state();
|
||||
|
||||
protected:
|
||||
void hard_reset_();
|
||||
bool soft_reset_();
|
||||
void update_touches() override;
|
||||
|
||||
|
||||
@@ -72,9 +72,7 @@ bool EPaperJD79660::reset() {
|
||||
|
||||
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
|
||||
// So do not use FSM loop, and avoid other calls/logs during pulse below.
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(SLEEP_MS_RESET1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
pulse_reset(this->reset_pin_, {.low_ms = SLEEP_MS_RESET1});
|
||||
|
||||
this->reset_duration_ = SLEEP_MS_RESET2;
|
||||
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
|
||||
|
||||
@@ -2389,7 +2389,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Optional(CONF_SOURCE): cv.git_ref,
|
||||
cv.Optional(CONF_SOURCE): cv.All(
|
||||
cv.string_strict, cv.Length(min=1)
|
||||
),
|
||||
cv.Optional(CONF_REF): cv.string,
|
||||
cv.Optional(CONF_PATH): cv.string,
|
||||
}
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
#include "hal/rtc_io_hal.h"
|
||||
#endif
|
||||
|
||||
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
|
||||
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
|
||||
// some variants don't map rtcio to gpio 1:1
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32H21)
|
||||
#define RTCIO_NEED_MAPPING 1 // NOLINT
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) || (SOC_RTCIO_PIN_COUNT > 0 && defined(USE_GPIO_HOLD))
|
||||
#define ISR_PIN_USE_RTC 1 // NOLINT
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
@@ -53,10 +58,12 @@ struct ISRPinArg {
|
||||
#ifdef USE_GPIO_HOLD
|
||||
bool hold;
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#ifdef ISR_PIN_USE_RTC
|
||||
bool use_rtc;
|
||||
#ifdef RTCIO_NEED_MAPPING
|
||||
int rtc_pin;
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_GPIO_HOLD
|
||||
@@ -69,17 +76,15 @@ static inline void refresh_hold(const ESP32InternalGPIOPin &pin) {
|
||||
static inline void IRAM_ATTR isr_refresh_hold(const ISRPinArg *arg) {
|
||||
if (!arg->hold)
|
||||
return;
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#if defined(ISR_PIN_USE_RTC) && defined(SOC_RTCIO_HOLD_SUPPORTED)
|
||||
if (arg->use_rtc) {
|
||||
#ifdef RTCIO_NEED_MAPPING
|
||||
rtcio_hal_hold_disable(arg->rtc_pin);
|
||||
rtcio_hal_hold_enable(arg->rtc_pin);
|
||||
return;
|
||||
}
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// GPIO0-21 are held from the RTC domain; the digital hold mask is zero for them
|
||||
if (arg->pin < SOC_RTCIO_PIN_COUNT) {
|
||||
#else
|
||||
rtcio_hal_hold_disable(arg->pin);
|
||||
rtcio_hal_hold_enable(arg->pin);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -96,10 +101,12 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
#ifdef USE_GPIO_HOLD
|
||||
arg->hold = this->get_hold();
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#ifdef ISR_PIN_USE_RTC
|
||||
arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num());
|
||||
#ifdef RTCIO_NEED_MAPPING
|
||||
if (arg->use_rtc)
|
||||
arg->rtc_pin = rtc_io_number_get(this->get_pin_num());
|
||||
#endif
|
||||
#endif
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
@@ -251,7 +258,8 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
if (diff & gpio::FLAG_INPUT) {
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
gpio_hal_input_enable(&GPIO_HAL, arg->pin);
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// Only ESP32 uses the RTC registers for pullup/pulldown
|
||||
if (arg->use_rtc) {
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
rtcio_hal_pullup_enable(arg->rtc_pin);
|
||||
|
||||
@@ -34,10 +34,7 @@ void FT63X6Touchscreen::setup() {
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_ANY_EDGE);
|
||||
}
|
||||
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->hard_reset_();
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 10});
|
||||
|
||||
// Get touch resolution
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
@@ -57,14 +54,6 @@ void FT63X6Touchscreen::setup() {
|
||||
this->write_byte(FT6X36_ADDR_TOUCHRATE_ACTIVE, 0x0E);
|
||||
}
|
||||
|
||||
void FT63X6Touchscreen::hard_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
|
||||
void FT63X6Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
@@ -27,7 +27,6 @@ class FT63X6Touchscreen final : public Touchscreen, public i2c::I2CDevice {
|
||||
void set_threshold(uint8_t threshold) { this->threshold_ = threshold; }
|
||||
|
||||
protected:
|
||||
void hard_reset_();
|
||||
void update_touches() override;
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
|
||||
@@ -83,13 +83,7 @@ void GSL3670Touchscreen::update_touches() {
|
||||
void GSL3670Touchscreen::clear_reg_() {
|
||||
ESP_LOGD(TAG, "clear_reg");
|
||||
|
||||
// GPIO reset pulse
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 1, .high_after_ms = 5});
|
||||
|
||||
this->write_reg8_(0x88, 0x01);
|
||||
// delay(5);
|
||||
@@ -106,12 +100,7 @@ void GSL3670Touchscreen::clear_reg_() {
|
||||
void GSL3670Touchscreen::reset_() {
|
||||
ESP_LOGD(TAG, "reset");
|
||||
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 1, .high_after_ms = 5});
|
||||
|
||||
this->write_reg8_(0xe4, 0x04);
|
||||
|
||||
|
||||
@@ -19,12 +19,7 @@ void ICNT86Touchscreen::setup() {
|
||||
}
|
||||
|
||||
// Perform reset if necessary
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 10});
|
||||
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Shared base for the infrared and radio_frequency entity components."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import remote_base
|
||||
from esphome.components.api import DOMAIN as API_DOMAIN
|
||||
from esphome.const import CONF_API
|
||||
from esphome.core import CORE
|
||||
from esphome.core.entity_helpers import queue_entity_register
|
||||
from esphome.types import ConfigType
|
||||
@@ -24,8 +23,10 @@ async def attach_transmitter(var: cg.MockObj, config: ConfigType, key: str) -> N
|
||||
does. Without this call anywhere in the build a request is answered as
|
||||
soon as the frame is handed over.
|
||||
"""
|
||||
from esphome.components import remote_base
|
||||
|
||||
await remote_base.register_transmittable(var, config, key)
|
||||
if API_DOMAIN in CORE.config:
|
||||
if CONF_API in CORE.config:
|
||||
cg.add_define("USE_IR_RF_TRANSMIT_COMPLETE")
|
||||
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ static uint8_t calc_checksum(const void *data, size_t size) {
|
||||
return xor_bytes(static_cast<const uint8_t *>(data), size);
|
||||
}
|
||||
|
||||
// Simple mode needs simple reporting from the module; Normal and Calibrate read energy frames
|
||||
static uint16_t system_mode_for(uint8_t mode) {
|
||||
return mode == OP_SIMPLE_MODE ? CMD_SYSTEM_MODE_SIMPLE : CMD_SYSTEM_MODE_ENERGY;
|
||||
}
|
||||
|
||||
static int32_t get_firmware_int(const char *version_string) {
|
||||
// Convert "v1.5.4" -> 154 by skipping 'v' and '.', accumulating digits
|
||||
const char *p = (*version_string == 'v') ? version_string + 1 : version_string;
|
||||
@@ -207,13 +212,10 @@ void LD2420Component::setup() {
|
||||
|
||||
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
|
||||
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
|
||||
this->set_operating_mode(OP_SIMPLE_MODE);
|
||||
this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
|
||||
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
|
||||
} else {
|
||||
this->set_operating_mode(OP_NORMAL_MODE);
|
||||
this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
|
||||
}
|
||||
this->set_operating_mode(OP_NORMAL_MODE); // older firmware is held in Simple
|
||||
this->set_mode_(system_mode_for(this->current_operating_mode));
|
||||
#ifdef USE_NUMBER
|
||||
this->init_gate_config_numbers();
|
||||
#endif
|
||||
@@ -244,7 +246,9 @@ void LD2420Component::apply_config_action() {
|
||||
#endif
|
||||
this->set_system_mode(this->system_mode_);
|
||||
this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm
|
||||
this->set_operating_mode(OP_NORMAL_MODE);
|
||||
// Applying the calibrated thresholds ends calibration; Normal and Simple stay as they are
|
||||
if (this->current_operating_mode == OP_CALIBRATE_MODE)
|
||||
this->set_operating_mode(OP_NORMAL_MODE);
|
||||
}
|
||||
|
||||
void LD2420Component::factory_reset_action() {
|
||||
@@ -356,6 +360,22 @@ void LD2420Component::set_operating_mode(const char *state) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::change_operating_mode(OpMode mode) {
|
||||
this->set_operating_mode(mode);
|
||||
const uint16_t system_mode = system_mode_for(this->current_operating_mode);
|
||||
if (system_mode == this->get_mode_())
|
||||
return;
|
||||
if (this->set_config_mode(true) != LD2420_ERROR_TIMEOUT) {
|
||||
this->set_system_mode(system_mode);
|
||||
this->set_config_mode(false);
|
||||
}
|
||||
if (this->get_mode_() != system_mode) {
|
||||
// The module kept its reporting mode; show the mode that matches it
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->set_operating_mode(this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE ? OP_SIMPLE_MODE : OP_NORMAL_MODE);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::set_operating_mode(OpMode mode) {
|
||||
// Older firmware only supports simple mode
|
||||
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
|
||||
|
||||
@@ -119,6 +119,7 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
void handle_cmd_error(uint16_t error);
|
||||
void set_operating_mode(const char *state);
|
||||
void set_operating_mode(OpMode mode);
|
||||
void change_operating_mode(OpMode mode);
|
||||
void auto_calibrate_sensitivity();
|
||||
void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number);
|
||||
uint8_t set_config_mode(bool enable);
|
||||
|
||||
@@ -30,6 +30,5 @@ async def to_code(config: ConfigType) -> None:
|
||||
CONF_OPERATING_MODE,
|
||||
hub.set_operating_mode_select,
|
||||
options=CONF_SELECTS,
|
||||
apply="set_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
|
||||
publish="before",
|
||||
apply="change_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
|
||||
)
|
||||
@@ -26,7 +26,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.helpers import copy_file_if_changed, write_file
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
|
||||
@@ -656,6 +656,26 @@ async def component_to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
# Called by the OTA upload through the platforms' ota_image hook
|
||||
def device_ota_image(firmware: Path) -> Path:
|
||||
"""The file to send for an app update: the UF2 without the images the
|
||||
device skips, written next to the build output, or ``firmware`` itself
|
||||
when there is nothing to drop."""
|
||||
from .ota_uf2 import device_ota_uf2
|
||||
|
||||
data = firmware.read_bytes()
|
||||
if (trimmed := device_ota_uf2(data)) is None:
|
||||
return firmware
|
||||
image = CORE.relative_pioenvs_path(CORE.name, "firmware.ota.uf2")
|
||||
write_file(image, trimmed)
|
||||
_LOGGER.info(
|
||||
"Sending the images this device writes: %s of %s bytes",
|
||||
len(trimmed),
|
||||
len(data),
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files() -> None:
|
||||
script_dir = Path(__file__).parent
|
||||
|
||||
@@ -91,6 +91,10 @@ async def pin_to_code(config: ConfigType) -> MockObj:
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
|
||||
# Called by the OTA upload; the shared libretiny implementation.
|
||||
ota_image = libretiny.device_ota_image
|
||||
'''
|
||||
|
||||
BASE_CODE_BOARDS = '''
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Trim a LibreTiny UF2 package to the images a device writes over OTA.
|
||||
|
||||
A LibreTiny UF2 holds one image per OTA scheme: the device schemes (single,
|
||||
dual slot 1, dual slot 2) and the flasher schemes used over UART. The device
|
||||
skips every block of an image whose scheme it does not run (uf2ota), so an
|
||||
image no device scheme writes is only sent to be thrown away. On single OTA
|
||||
chips (BK72xx, LN882H) that is the whole flasher image, more than half the
|
||||
package. The legacy OTA format names the same device partitions, so devices
|
||||
that read it write the same images.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
UF2_BLOCK_SIZE = 512
|
||||
UF2_MAGIC_HEAD = struct.pack("<2I", 0x0A324655, 0x9E5D5157)
|
||||
UF2_MAGIC_TAIL = struct.pack("<I", 0x0AB16F30)
|
||||
UF2_FLAG_HAS_TAGS = 0x8000
|
||||
# Header words after the magic: flags, target address, payload length, block
|
||||
# number, block count
|
||||
_HEADER_WORDS = struct.Struct("<5I")
|
||||
_SEQ_OFFSET = 20
|
||||
_DATA_START = 32
|
||||
_DATA_END = UF2_BLOCK_SIZE - len(UF2_MAGIC_TAIL)
|
||||
|
||||
# One partition index nibble per scheme, high nibble first: device single,
|
||||
# dual 1 and dual 2 are the first three, the flasher schemes the next three
|
||||
UF2_TAG_OTA_PART_INFO = 0xC0EE0C
|
||||
# Tags uf2ota acts on beyond the image they sit in; one in an image that
|
||||
# would be dropped leaves the package untrimmed
|
||||
_KEEP_TAGS = frozenset(
|
||||
{
|
||||
0x5D57D0, # OTA_FORMAT_1
|
||||
0x6C8492, # OTA_FORMAT_2
|
||||
0x6EC68A, # OTA_PART_LIST
|
||||
0xB948DE, # BINPATCH
|
||||
0x8288ED, # FAL_PTABLE
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _tags(block: bytes, length: int) -> dict[int, bytes] | None:
|
||||
"""The extension tags after a block's payload, or None if malformed."""
|
||||
tags: dict[int, bytes] = {}
|
||||
pos = _DATA_START + length
|
||||
while pos + 4 <= _DATA_END and (size := block[pos]):
|
||||
if size < 4 or pos + size > _DATA_END:
|
||||
return None
|
||||
tags[int.from_bytes(block[pos + 1 : pos + 4], "little")] = block[
|
||||
pos + 4 : pos + size
|
||||
]
|
||||
pos += (size + 3) & ~3
|
||||
return tags
|
||||
|
||||
|
||||
def device_ota_uf2(data: bytes) -> bytes | None:
|
||||
"""``data`` without the images no device scheme writes.
|
||||
|
||||
None when there is nothing to drop, or when ``data`` is not a well
|
||||
formed UF2, so the caller sends the file as it is.
|
||||
"""
|
||||
if len(data) % UF2_BLOCK_SIZE:
|
||||
return None
|
||||
count = len(data) // UF2_BLOCK_SIZE
|
||||
kept: list[bytes] = []
|
||||
keep = True
|
||||
for seq in range(count):
|
||||
block = data[seq * UF2_BLOCK_SIZE : (seq + 1) * UF2_BLOCK_SIZE]
|
||||
flags, _, length, block_seq, block_count = _HEADER_WORDS.unpack_from(block, 8)
|
||||
# A missing or repeated block must stay visible to the device, which
|
||||
# takes the update size from the block count
|
||||
if (
|
||||
block[:8] != UF2_MAGIC_HEAD
|
||||
or block[_DATA_END:] != UF2_MAGIC_TAIL
|
||||
or _DATA_START + length > _DATA_END
|
||||
or (block_seq, block_count) != (seq, count)
|
||||
):
|
||||
return None
|
||||
tags = _tags(block, length) if flags & UF2_FLAG_HAS_TAGS else {}
|
||||
if tags is None:
|
||||
return None
|
||||
if (part_info := tags.get(UF2_TAG_OTA_PART_INFO)) is not None:
|
||||
# uf2ota rejects a selector without the whole scheme map
|
||||
if len(part_info) < 3:
|
||||
return None
|
||||
# Any of the three device scheme nibbles names a partition
|
||||
keep = int.from_bytes(part_info[:2], "big") >> 4 != 0
|
||||
if keep:
|
||||
kept.append(block)
|
||||
elif not _KEEP_TAGS.isdisjoint(tags):
|
||||
# Without its image's OTA_PART_INFO the device would write this
|
||||
# block to the partition of the image before it
|
||||
return None
|
||||
if len(kept) == count:
|
||||
return None
|
||||
out = bytearray(b"".join(kept))
|
||||
for seq in range(len(kept)):
|
||||
struct.pack_into("<2I", out, seq * UF2_BLOCK_SIZE + _SEQ_OFFSET, seq, len(kept))
|
||||
return bytes(out)
|
||||
@@ -77,3 +77,7 @@ async def pin_to_code(config: ConfigType) -> MockObj:
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
|
||||
# Called by the OTA upload; the shared libretiny implementation.
|
||||
ota_image = libretiny.device_ota_image
|
||||
@@ -607,9 +607,10 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
# esp_rom_printf instead of pulling ~1.2KB esp_rom_vprintf into IRAM.
|
||||
# S2/C2 have a ROM vprintf, so IDF forces CONSTRAINED_ENV_SAFE=1 and
|
||||
# cache-off DRAM logs would reach the flash-resident wrapper; keep V1.
|
||||
if idf_version() >= cv.Version(6, 1, 0) and get_esp32_variant() not in (
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32S2,
|
||||
if (
|
||||
CORE.using_toolchain_esp_idf
|
||||
and idf_version() >= cv.Version(6, 1, 0)
|
||||
and get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32S2)
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_1", False)
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_2", True)
|
||||
|
||||
@@ -339,8 +339,8 @@ def final_validation(config_list):
|
||||
|
||||
def lv_conf_path_flag(lv_conf_h_file: Path) -> str:
|
||||
"""The ``-DLV_CONF_PATH`` build flag, decoded identically by the shell
|
||||
lexers (PlatformIO, host, ESP8266) and the quoted CMake arguments
|
||||
(ESP-IDF, Zephyr) the build flags pass through."""
|
||||
lexers (PlatformIO, host, ESP8266, and ESP-IDF before it quotes each
|
||||
token for CMake) and the raw quoted CMake argument (Zephyr)."""
|
||||
# forward slashes for Windows, then escape what shlex or a quoted CMake
|
||||
# argument would not pass through literally
|
||||
path = re.sub(r"[^\w/.:+=,-]", r"\\\g<0>", Path(lv_conf_h_file).as_posix())
|
||||
@@ -414,7 +414,8 @@ async def to_code(configs):
|
||||
"LV_COLOR_CHROMA_KEY",
|
||||
await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]),
|
||||
)
|
||||
cg.add_build_flag("-Isrc")
|
||||
if not CORE.using_toolchain_esp_idf:
|
||||
cg.add_build_flag("-Isrc")
|
||||
|
||||
cg.add_global(lvgl_ns.using)
|
||||
for font in df.get_esphome_fonts_used():
|
||||
|
||||
@@ -2,7 +2,6 @@ from collections.abc import Callable
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio
|
||||
from esphome.components.const import CONF_BITRATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -116,6 +115,8 @@ def validate_preferred_format(
|
||||
"""
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
from esphome.components import audio
|
||||
|
||||
# Inherit settings from audio device if not manually set
|
||||
inherit_property_from(CONF_NUM_CHANNELS, audio_device_key)(config)
|
||||
inherit_property_from(CONF_SAMPLE_RATE, audio_device_key)(config)
|
||||
@@ -155,6 +156,8 @@ def request_codecs_for_format_configs(
|
||||
|
||||
Public API for external components. Do not remove.
|
||||
"""
|
||||
from esphome.components import audio
|
||||
|
||||
needed_formats: set[str] = set()
|
||||
need_all = False
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -53,16 +54,25 @@ using DataCallbackManagerBase = CallbackManager<void(const AudioCapture &)>;
|
||||
|
||||
class DataCallbackManager : public DataCallbackManagerBase {
|
||||
public:
|
||||
explicit DataCallbackManager(const audio::AudioStreamInfo *stream_info) : stream_info_(stream_info) {}
|
||||
DataCallbackManager(const audio::AudioStreamInfo *stream_info, const bool *mute_state)
|
||||
: stream_info_(stream_info), mute_state_(mute_state) {}
|
||||
|
||||
using DataCallbackManagerBase::call;
|
||||
|
||||
// Remove before 2027.5.0, along with this class (use a plain CallbackManager)
|
||||
ESPDEPRECATED("Use Microphone::publish_audio_() instead. Removed in 2027.5.0", "2026.11.0")
|
||||
void call(const std::vector<uint8_t> &data) { this->call(AudioCapture{data, *this->stream_info_, 0}); }
|
||||
void call(const std::vector<uint8_t> &data) {
|
||||
if (*this->mute_state_) {
|
||||
const std::vector<uint8_t> zeros(data.size(), 0);
|
||||
this->call(AudioCapture{zeros, *this->stream_info_, 0});
|
||||
} else {
|
||||
this->call(AudioCapture{data, *this->stream_info_, 0});
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
const audio::AudioStreamInfo *stream_info_;
|
||||
const bool *mute_state_;
|
||||
};
|
||||
|
||||
class Microphone {
|
||||
@@ -72,15 +82,7 @@ class Microphone {
|
||||
|
||||
/// Callbacks take (const AudioCapture &) and run on the microphone's task.
|
||||
template<typename F> void add_data_callback(F &&data_callback) {
|
||||
this->data_callbacks_.add(
|
||||
[this, callback = as_capture_callback(std::forward<F>(data_callback))](const AudioCapture &capture) {
|
||||
if (this->mute_state_) {
|
||||
const std::vector<uint8_t> zeros(capture.data.size(), 0);
|
||||
callback(AudioCapture{zeros, capture.stream_info, capture.capture_time_us});
|
||||
} else {
|
||||
callback(capture);
|
||||
}
|
||||
});
|
||||
this->data_callbacks_.add(as_capture_callback(std::forward<F>(data_callback)));
|
||||
}
|
||||
|
||||
bool is_running() const { return this->state_ == STATE_RUNNING; }
|
||||
@@ -92,8 +94,11 @@ class Microphone {
|
||||
audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
|
||||
|
||||
protected:
|
||||
void publish_audio_(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info,
|
||||
int64_t capture_time_us = 0) {
|
||||
/// While muted, data is zeroed first, so it must be a buffer the platform owns.
|
||||
void publish_audio_(std::span<uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us = 0) {
|
||||
if (this->mute_state_) {
|
||||
std::memset(data.data(), 0, data.size());
|
||||
}
|
||||
this->data_callbacks_.call(AudioCapture{data, stream_info, capture_time_us});
|
||||
}
|
||||
|
||||
@@ -105,7 +110,7 @@ class Microphone {
|
||||
audio::AudioStreamInfo audio_stream_info_;
|
||||
|
||||
// Make private when the deprecated call(data) is removed in 2027.5.0
|
||||
DataCallbackManager data_callbacks_{&this->audio_stream_info_};
|
||||
DataCallbackManager data_callbacks_{&this->audio_stream_info_, &this->mute_state_};
|
||||
};
|
||||
|
||||
} // namespace esphome::microphone
|
||||
@@ -107,12 +107,7 @@ void MipiDsi::setup() {
|
||||
}
|
||||
#endif
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
|
||||
} else {
|
||||
esp_lcd_panel_io_tx_param(this->io_handle_, SW_RESET_CMD, nullptr, 0);
|
||||
}
|
||||
|
||||
@@ -33,14 +33,7 @@ void MipiRgb::setup_enables_() {
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
|
||||
}
|
||||
|
||||
#ifdef USE_SPI
|
||||
|
||||
@@ -151,15 +151,8 @@ class MipiSpi : public display::Display,
|
||||
pin->setup();
|
||||
pin->digital_write(true);
|
||||
}
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
// required delay after reset is already in the init sequence, don't duplicate
|
||||
}
|
||||
// required delay after reset is already in the init sequence, don't duplicate
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
|
||||
|
||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||
auto when = millis() + 120;
|
||||
|
||||
@@ -452,6 +452,16 @@ bool Nextion::remove_from_q_(bool report_empty) {
|
||||
}
|
||||
NextionComponentBase *component = nb->component;
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
if (!nb->pending_command.empty()) {
|
||||
// Not sent yet, so this response cannot belong to it. Dropping the
|
||||
// response leaves the command queued instead of losing it.
|
||||
ESP_LOGW(TAG, "Response for unsent cmd '%s' (queue %zu), ignoring", component->get_variable_name().c_str(),
|
||||
this->nextion_queue_.size());
|
||||
return false;
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
|
||||
ESP_LOGN(TAG, "Removed: %s", component->get_variable_name().c_str());
|
||||
|
||||
if (is_sleep_wake_command(component)) {
|
||||
@@ -1173,8 +1183,8 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
|
||||
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
|
||||
* sleep-safe and must bypass it.
|
||||
*
|
||||
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
|
||||
* the command is saved in the queue entry for retry rather than dropped.
|
||||
* If USE_NEXTION_COMMAND_SPACING is enabled and the command cannot go out right
|
||||
* away, it is saved in the queue entry for retry rather than dropped.
|
||||
*
|
||||
* @param variable_name Name of the variable or component associated with the command.
|
||||
* @param command The raw command string to send.
|
||||
@@ -1185,6 +1195,15 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n
|
||||
|
||||
ESP_LOGV(TAG, "Queue NORESULT '%s': %s", variable_name.c_str(), command.c_str());
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Sending now would overtake commands that are still waiting, so only send when
|
||||
// nothing is waiting. process_pending_in_queue_() transmits the rest in order.
|
||||
if (this->has_unsent_commands_()) {
|
||||
this->add_no_result_to_queue_with_pending_command_(variable_name, command);
|
||||
return;
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
|
||||
if (this->send_command_(command)) {
|
||||
this->add_no_result_to_queue_(variable_name);
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
@@ -1346,14 +1365,13 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
|
||||
std::string command = "get " + component->get_variable_name_to_send();
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Always enqueue first so the response handler is present when the command
|
||||
// is eventually sent. Store the command for retry if spacing blocked it;
|
||||
// process_pending_in_queue_() will transmit it when the pacer allows.
|
||||
nextion_queue->pending_command = command;
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
if (this->send_command_(command)) {
|
||||
nextion_queue->pending_command.clear();
|
||||
// Only send right away when nothing is waiting, otherwise this command would
|
||||
// overtake the ones still queued. Anything not sent here keeps its command for
|
||||
// process_pending_in_queue_() to transmit in order.
|
||||
if (this->has_unsent_commands_() || !this->send_command_(command)) {
|
||||
nextion_queue->pending_command = std::move(command);
|
||||
}
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
#else // USE_NEXTION_COMMAND_SPACING
|
||||
if (this->send_command_(command)) {
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
|
||||
@@ -1470,6 +1470,16 @@ class Nextion final : public NextionBase, public PollingComponent, public uart::
|
||||
* Called from loop() to retry sending commands that were delayed by spacing.
|
||||
*/
|
||||
void process_pending_in_queue_();
|
||||
|
||||
/**
|
||||
* @brief Whether the queue still holds a command that has not reached the wire.
|
||||
*
|
||||
* The queue is kept in transmit order, so anything still waiting sits at the back.
|
||||
*/
|
||||
bool has_unsent_commands_() const {
|
||||
return !this->nextion_queue_.empty() && this->nextion_queue_.back() != nullptr &&
|
||||
!this->nextion_queue_.back()->pending_command.empty();
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
|
||||
std::list<NextionQueue *> nextion_queue_;
|
||||
|
||||
@@ -226,6 +226,12 @@ def set_sdkconfig_options(config: ConfigType) -> None:
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", not border_router)
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", not border_router)
|
||||
if not border_router:
|
||||
# The DNS64 hook breaks IPv6 literal lookups; see openthread_esp.cpp.
|
||||
# Fixed upstream on ESP-IDF master, remove this once a release includes
|
||||
# "fix(openthread): bypass DNS64 synthesis for IPv6 literals",
|
||||
# https://github.com/espressif/esp-idf/commit/87b133ee1a7e91c1ee95176b4733b7f72236e5da
|
||||
# (merged in https://github.com/espressif/esp-idf/commit/83239509832bf5d427935cd365f4b62a320e8765)
|
||||
cg.add_build_flag("-Wl,--wrap=lwip_hook_dns_external_resolve")
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
|
||||
|
||||
if border_router:
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
#include "esp_vfs_eventfd.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#ifndef USE_OPENTHREAD_BORDER_ROUTER
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/ip_addr.h"
|
||||
#endif
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "openthread");
|
||||
|
||||
@@ -489,4 +493,26 @@ InstanceLock::~InstanceLock() {
|
||||
}
|
||||
|
||||
} // namespace esphome::openthread
|
||||
|
||||
#ifndef USE_OPENTHREAD_BORDER_ROUTER
|
||||
// ESP-IDF's DNS64 hook runs before lwIP's literal address check and retries every lookup as IPv4,
|
||||
// so an IPv6 literal never resolves. Let lwIP handle those; linked with --wrap from __init__.py.
|
||||
// Fixed upstream on ESP-IDF master, remove this once a release includes
|
||||
// "fix(openthread): bypass DNS64 synthesis for IPv6 literals",
|
||||
// https://github.com/espressif/esp-idf/commit/87b133ee1a7e91c1ee95176b4733b7f72236e5da
|
||||
// (merged in https://github.com/espressif/esp-idf/commit/83239509832bf5d427935cd365f4b62a320e8765)
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
int __real_lwip_hook_dns_external_resolve(const char *name, ip_addr_t *addr, dns_found_callback found,
|
||||
void *callback_arg, u8_t addrtype, err_t *err);
|
||||
int __wrap_lwip_hook_dns_external_resolve(const char *name, ip_addr_t *addr, dns_found_callback found,
|
||||
void *callback_arg, u8_t addrtype, err_t *err) {
|
||||
ip_addr_t literal;
|
||||
if (ipaddr_aton(name, &literal) && IP_IS_V6_VAL(literal))
|
||||
return 0;
|
||||
return __real_lwip_hook_dns_external_resolve(name, addr, found, callback_arg, addrtype, err);
|
||||
}
|
||||
} // extern "C"
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_OPENTHREAD_BORDER_ROUTER
|
||||
#endif
|
||||
@@ -9,23 +9,10 @@ ESPHOME_LOG_TAG(TAG, "pcd_8544");
|
||||
|
||||
void PCD8544::setup_pins_() {
|
||||
this->spi_setup();
|
||||
this->init_reset_();
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10});
|
||||
this->dc_pin_->setup();
|
||||
}
|
||||
|
||||
void PCD8544::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
|
||||
void PCD8544::initialize() {
|
||||
this->init_internal_(this->get_buffer_length_());
|
||||
|
||||
|
||||
@@ -57,8 +57,6 @@ class PCD8544 final : public display::DisplayBuffer,
|
||||
|
||||
void setup_pins_();
|
||||
|
||||
void init_reset_();
|
||||
|
||||
size_t get_buffer_length_();
|
||||
|
||||
void start_command_();
|
||||
|
||||
@@ -90,6 +90,7 @@ bool ResamplerMicrophone::init_resampler_(const audio::AudioStreamInfo &input_st
|
||||
|
||||
if (input_stream_info.get_sample_rate() == this->target_sample_rate_) {
|
||||
// The source already delivers the target sample rate, so its audio is passed through unchanged
|
||||
this->output_buffer_.reserve(input_stream_info.ms_to_bytes(BUFFER_DURATION_MS));
|
||||
this->input_stream_info_ = input_stream_info;
|
||||
this->resampler_ready_ = true;
|
||||
return true;
|
||||
@@ -137,7 +138,23 @@ void ResamplerMicrophone::process_audio_(const microphone::AudioCapture &capture
|
||||
}
|
||||
|
||||
if (this->resampler_ == nullptr) {
|
||||
this->publish_audio_(capture.data, input_stream_info, capture.capture_time_us);
|
||||
if (this->mute_state_) {
|
||||
// The source's buffer is shared with its other consumers, so publish zeros from our own buffer, in pieces that
|
||||
// fit its reserved size so muting never allocates
|
||||
const uint32_t max_frames = input_stream_info.ms_to_frames(BUFFER_DURATION_MS);
|
||||
const uint32_t total_frames = input_stream_info.bytes_to_frames(capture.data.size());
|
||||
for (uint32_t done = 0; done < total_frames;) {
|
||||
const uint32_t frames = std::min(total_frames - done, max_frames);
|
||||
this->output_buffer_.assign(input_stream_info.frames_to_bytes(frames), 0);
|
||||
const int64_t time_us =
|
||||
capture.capture_time_us == 0 ? 0 : capture.capture_time_us + input_stream_info.frames_to_microseconds(done);
|
||||
this->publish_audio_(this->output_buffer_, input_stream_info, time_us);
|
||||
done += frames;
|
||||
}
|
||||
} else {
|
||||
// Needs a protected forwarding method once data_callbacks_ is made private in 2027.5.0
|
||||
this->data_callbacks_.call(microphone::AudioCapture{capture.data, input_stream_info, capture.capture_time_us});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,3 +77,7 @@ async def pin_to_code(config: ConfigType) -> MockObj:
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
|
||||
# Called by the OTA upload; the shared libretiny implementation.
|
||||
ota_image = libretiny.device_ota_image
|
||||
@@ -122,18 +122,7 @@ void SH1122::fill(Color color) {
|
||||
const uint8_t fill = (color4 & SH1122_COLORMASK) | ((color4 & SH1122_COLORMASK) << SH1122_COLORSHIFT);
|
||||
memset(this->buffer_, fill, this->get_buffer_length_());
|
||||
}
|
||||
void SH1122::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SH1122::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
const LogString *SH1122::model_str_() {
|
||||
switch (this->model_) {
|
||||
case SH1122_MODEL_256_64:
|
||||
|
||||
@@ -355,18 +355,7 @@ void SSD1306::fill(Color color) {
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->buffer_[i] = fill;
|
||||
}
|
||||
void SSD1306::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SSD1306::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
const LogString *SSD1306::model_str_() {
|
||||
return ModelStrings::get_log_str(static_cast<uint8_t>(this->model_), ModelStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
@@ -184,18 +184,7 @@ void SSD1322::fill(Color color) {
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->buffer_[i] = fill;
|
||||
}
|
||||
void SSD1322::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SSD1322::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
const char *SSD1322::model_str_() {
|
||||
switch (this->model_) {
|
||||
case SSD1322_MODEL_256_64:
|
||||
|
||||
@@ -211,18 +211,7 @@ void SSD1325::fill(Color color) {
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->buffer_[i] = fill;
|
||||
}
|
||||
void SSD1325::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SSD1325::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
const char *SSD1325::model_str_() {
|
||||
switch (this->model_) {
|
||||
case SSD1325_MODEL_128_32:
|
||||
|
||||
@@ -160,18 +160,7 @@ void SSD1327::fill(Color color) {
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->buffer_[i] = fill;
|
||||
}
|
||||
void SSD1327::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SSD1327::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
const char *SSD1327::model_str_() {
|
||||
switch (this->model_) {
|
||||
case SSD1327_MODEL_128_128:
|
||||
|
||||
@@ -142,17 +142,6 @@ void SSD1331::fill(Color color) {
|
||||
}
|
||||
}
|
||||
}
|
||||
void SSD1331::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SSD1331::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
|
||||
} // namespace esphome::ssd1331_base
|
||||
@@ -174,18 +174,7 @@ void SSD1351::fill(Color color) {
|
||||
}
|
||||
}
|
||||
}
|
||||
void SSD1351::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void SSD1351::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
const char *SSD1351::model_str_() {
|
||||
switch (this->model_) {
|
||||
case SSD1351_MODEL_128_96:
|
||||
|
||||
@@ -9,12 +9,8 @@ ESPHOME_LOG_TAG(TAG, "st7123.touchscreen");
|
||||
|
||||
void ST7123Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false); // TP_RESX is active low, assert for at least tRSTW (2ms)
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
// TP_RESX is active low, assert for at least tRSTW (2ms)
|
||||
pulse_reset(this->reset_pin_, {.high_before_ms = 5, .low_ms = 5});
|
||||
// The controller needs up to 20ms to initialize after reset before it can be accessed.
|
||||
this->setup_time_ = millis() + 30;
|
||||
}
|
||||
|
||||
@@ -137,18 +137,7 @@ void ST7567::fill(Color color) {
|
||||
memset(buffer_, fill, this->get_buffer_length_());
|
||||
}
|
||||
|
||||
void ST7567::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(1);
|
||||
// Trigger Reset
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
// Wake up
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
}
|
||||
void ST7567::init_reset_() { pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}); }
|
||||
|
||||
const char *ST7567::model_str_() { return "ST7567 128x64"; }
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
from esphome.components.const import CONF_MODULATION
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_PIN, CONF_ID, CONF_MODEL
|
||||
from esphome.cpp_generator import MockObj
|
||||
@@ -19,6 +20,7 @@ DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_ANALOG_GAIN = "analog_gain"
|
||||
CONF_DAC_MODE = "dac_mode"
|
||||
CONF_FAULT_OUTPUT = "fault_output"
|
||||
CONF_IGNORE_ENABLE_PIN_WARNING = "ignore_enable_pin_warning"
|
||||
CONF_MIXER_MODE = "mixer_mode"
|
||||
CONF_VOLUME_MIN_DB = "volume_min_db"
|
||||
@@ -39,6 +41,11 @@ MIXER_MODES = MixerMode.options(
|
||||
"stereo", "stereo_inverse", "mono", "left", "right", prefix="MIXER_MODE_"
|
||||
)
|
||||
|
||||
Modulation = tas58xx_ns.enum("Modulation")
|
||||
MODULATIONS = Modulation.options("bd_mode", "1spw_mode", prefix="MODULATION_")
|
||||
|
||||
FAULT_OUTPUT_NONE = "none"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Model:
|
||||
@@ -49,6 +56,9 @@ class Model:
|
||||
volume_min_db: float
|
||||
volume_max_db: float
|
||||
default_address: int
|
||||
# Pins that can report FAULTZ: name -> pin number n, selected at 0x61 + n (ModelInfo::fault_output_pins)
|
||||
fault_outputs: dict[str, int]
|
||||
default_fault_output: str
|
||||
|
||||
|
||||
MODELS: dict[str, Model] = {
|
||||
@@ -58,6 +68,8 @@ MODELS: dict[str, Model] = {
|
||||
volume_min_db=-103.0,
|
||||
volume_max_db=24.0,
|
||||
default_address=0x2D,
|
||||
fault_outputs={"adr": 0},
|
||||
default_fault_output="adr",
|
||||
),
|
||||
DAC_TAS5825M: Model(
|
||||
model_info=tas58xx_ns.TAS5825M_MODEL,
|
||||
@@ -65,6 +77,8 @@ MODELS: dict[str, Model] = {
|
||||
volume_min_db=-103.0,
|
||||
volume_max_db=24.0,
|
||||
default_address=0x4C,
|
||||
fault_outputs={"gpio0": 0, "gpio1": 1, "gpio2": 2},
|
||||
default_fault_output="gpio1",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -100,9 +114,15 @@ def _model_schema(model: Model) -> cv.Schema:
|
||||
cv.Optional(CONF_DAC_MODE, default="btl"): cv.enum(
|
||||
DAC_MODES, lower=True
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_FAULT_OUTPUT, default=model.default_fault_output
|
||||
): cv.one_of(FAULT_OUTPUT_NONE, *model.fault_outputs, lower=True),
|
||||
cv.Optional(CONF_MIXER_MODE, default="stereo"): cv.enum(
|
||||
MIXER_MODES, lower=True
|
||||
),
|
||||
cv.Optional(CONF_MODULATION, default="bd_mode"): cv.enum(
|
||||
MODULATIONS, lower=True
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_VOLUME_MIN_DB, default=model.volume_min_db
|
||||
): volume_validator,
|
||||
@@ -172,7 +192,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN]))
|
||||
cg.add(var.set_dac_mode(config[CONF_DAC_MODE]))
|
||||
if (fault_output := config[CONF_FAULT_OUTPUT]) != FAULT_OUTPUT_NONE:
|
||||
cg.add(var.set_fault_output(model.fault_outputs[fault_output]))
|
||||
cg.add(var.set_mixer_mode(config[CONF_MIXER_MODE]))
|
||||
cg.add(var.set_modulation(config[CONF_MODULATION]))
|
||||
cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB]))
|
||||
cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB]))
|
||||
await cg.add_pin(var.set_enable_pin, config.get(CONF_ENABLE_PIN))
|
||||
@@ -8,11 +8,8 @@ namespace esphome::tas58xx {
|
||||
// Registers 0x46, 0x7D, 0x7E and page 1 register 0x51 are not documented in the datasheet.
|
||||
static constexpr i2c::I2CRegValue STARTUP_SEQUENCE[] PROGMEM = {
|
||||
{0x03, 0x00}, // DEVICE_CTRL_2: deep sleep
|
||||
{0x46, 0x01},
|
||||
{0x46, 0x01}, // PPC 96 kHz process flow value (its 48 kHz FIR flows write 0x11)
|
||||
{0x03, 0x02}, // DEVICE_CTRL_2: Hi-Z
|
||||
// The I2C address is latched at power up, after which the ADR pin can report faults
|
||||
{0x61, 0x0B}, // ADR_PIN_CONFIG: FAULTZ
|
||||
{0x60, 0x01}, // ADR_PIN_CTRL: output
|
||||
{0x7D, 0x11},
|
||||
{0x7E, 0xFF},
|
||||
{0x00, 0x01}, // Page 1
|
||||
@@ -25,6 +22,8 @@ static constexpr i2c::I2CRegValue STARTUP_SEQUENCE[] PROGMEM = {
|
||||
|
||||
static const LogString *model_name() { return LOG_STR("TAS5805M"); }
|
||||
|
||||
static const LogString *fault_output_name(uint8_t) { return LOG_STR("ADR"); }
|
||||
|
||||
// An if chain rather than a switch: a switch table would land in rodata, which is RAM on ESP8266.
|
||||
static const LogString *fault_name(uint8_t index) {
|
||||
if (index == 0)
|
||||
@@ -88,6 +87,8 @@ const ModelInfo TAS5805M_MODEL = {
|
||||
NO_BIT, // FAULT_SENSOR_RIGHT_CHANNEL_CBC_OVER_CURRENT_WARNING
|
||||
NO_BIT, // FAULT_SENSOR_OVER_TEMP_146C_WARNING
|
||||
},
|
||||
.fault_output_pins = 1, // ADR
|
||||
.fault_output_name = fault_output_name,
|
||||
};
|
||||
|
||||
} // namespace esphome::tas58xx
|
||||
@@ -6,7 +6,7 @@ namespace esphome::tas58xx {
|
||||
|
||||
// Remainder of the startup sequence, run after the reset in TAS58xx::init_(). Register 0x00 selects the page.
|
||||
// Based on the TI PurePath Console (PPC) export for "TAS5825M 2-Band DRC&AGL 2.0 96k".
|
||||
// Registers 0x46, 0x7D, 0x7E, page 1 register 0x51 and page 2 registers 0x1D, 0x19 are not documented in datasheet.
|
||||
// Registers 0x7D, 0x7E, page 1 register 0x51 and page 2 registers 0x1D, 0x19 are not documented in datasheet.
|
||||
static constexpr i2c::I2CRegValue STARTUP_SEQUENCE[] PROGMEM = {
|
||||
{0x03, 0x00}, // DEVICE_CTRL_2: deep sleep, the state the PPC sequence starts from
|
||||
{0x7D, 0x11},
|
||||
@@ -17,20 +17,25 @@ static constexpr i2c::I2CRegValue STARTUP_SEQUENCE[] PROGMEM = {
|
||||
{0x1D, 0x00},
|
||||
{0x19, 0x80},
|
||||
{0x00, 0x00}, // Page 0
|
||||
{0x46, 0x11},
|
||||
{0x46, 0x11}, // DSP_CTRL: 96 kHz processing (bits 4-3 = 10), use default coefficients (bit 0 = 1)
|
||||
// PPC also writes 0x02 (DEVICE_CTRL_1) and 0x54 (AGAIN) here; init_() writes both from YAML
|
||||
{0x53, 0x01}, // ANA_CTRL: in phase, 100kHz bandwidth for high audio performance with Fsw=384kHz
|
||||
{0x03, 0x02}, // DEVICE_CTRL_2: Hi-Z
|
||||
|
||||
// Additional register configuration
|
||||
// GPIO1 is a dedicated pin on TAS5825M (separate from ADR), used as FAULTZ
|
||||
{0x62, 0x0B}, // GPIO1_SEL: GPIO1 as FAULTZ
|
||||
{0x60, 0x02}, // GPIO_CTRL: GPIO1 is output
|
||||
{0x77, 0x07}, // CBC_CONTROL: enable cycle-by-cycle current limit for warnings and faults
|
||||
};
|
||||
|
||||
static const LogString *model_name() { return LOG_STR("TAS5825M"); }
|
||||
|
||||
static const LogString *fault_output_name(uint8_t pin) {
|
||||
if (pin == 0)
|
||||
return LOG_STR("GPIO0");
|
||||
if (pin == 1)
|
||||
return LOG_STR("GPIO1");
|
||||
return LOG_STR("GPIO2");
|
||||
}
|
||||
|
||||
// An if chain rather than a switch: a switch table would land in rodata, which is RAM on ESP8266.
|
||||
static const LogString *fault_name(uint8_t index) {
|
||||
if (index == 0)
|
||||
@@ -113,6 +118,8 @@ const ModelInfo TAS5825M_MODEL = {
|
||||
// 25, // FAULT_SENSOR_OVER_TEMP_122C_WARNING // not currently included
|
||||
// 24, // FAULT_SENSOR_OVER_TEMP_112C_WARNING // not currently included
|
||||
},
|
||||
.fault_output_pins = 3, // GPIO0, GPIO1, GPIO2 (ADR is an analog input only on TAS5825M)
|
||||
.fault_output_name = fault_output_name,
|
||||
};
|
||||
|
||||
} // namespace esphome::tas58xx
|
||||
@@ -20,6 +20,7 @@ static constexpr uint8_t TAS58XX_RESET_CTRL = 0x01;
|
||||
static constexpr uint8_t TAS58XX_RESET_CTRL_ALL = 0x11; // Reset DSP/control port and registers
|
||||
static constexpr uint8_t TAS58XX_DEVICE_CTRL_1 = 0x02;
|
||||
static constexpr uint8_t TAS58XX_DEVICE_CTRL_1_PBTL = (1 << 2);
|
||||
static constexpr uint8_t TAS58XX_DEVICE_CTRL_1_1SPW = 0x01; // DAMP_MOD = 01
|
||||
static constexpr uint8_t TAS58XX_DEVICE_CTRL_2 = 0x03;
|
||||
static constexpr uint8_t TAS58XX_DEVICE_CTRL_2_MUTE = (1 << 3);
|
||||
static constexpr uint8_t TAS58XX_CTRL_STATE_MASK = 0x03; // DEVICE_CTRL_2 and POWER_STATE
|
||||
@@ -33,6 +34,9 @@ static constexpr uint8_t TAS58XX_DIG_VOL_0DB = 0x30;
|
||||
static constexpr uint8_t TAS58XX_DIG_VOL_MINUS_103DB = 0xFE;
|
||||
static constexpr uint8_t TAS58XX_AGAIN = 0x54; // 0x00 = 0 dB to 0x1F = -15.5 dB in 0.5 dB steps
|
||||
static constexpr uint8_t TAS58XX_AGAIN_MINUS_15_5DB = 0x1F;
|
||||
static constexpr uint8_t TAS58XX_GPIO_CTRL = 0x60; // ADR_PIN_CTRL on TAS5805M: output enable, bit n for pin n
|
||||
static constexpr uint8_t TAS58XX_GPIO0_SEL = 0x61; // ADR_PIN_CONFIG on TAS5805M: pin n is selected at 0x61 + n
|
||||
static constexpr uint8_t TAS58XX_GPIO_SEL_FAULTZ = 0x0B; // pin function: FAULTZ output
|
||||
static constexpr uint8_t TAS58XX_POWER_STATE = 0x68;
|
||||
static constexpr uint8_t TAS58XX_CHAN_FAULT = 0x70; // GLOBAL_FAULT1, GLOBAL_FAULT2 and OT_WARNING follow
|
||||
static constexpr uint8_t TAS58XX_FAULT_REGISTER_COUNT = 4;
|
||||
@@ -98,12 +102,26 @@ bool TAS58xx::init_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setters are public, so keep the value inside the register range
|
||||
// The I2C address is latched at power up, so ADR can now be an output. The pin function is written before the
|
||||
// output enable, so the pin never drives another function.
|
||||
if (this->fault_output_pin_ != FAULT_OUTPUT_NONE) {
|
||||
// The setter is public, so only write this model's pins; YAML only allows the pins of the model
|
||||
if (this->fault_output_pin_ >= this->model_->fault_output_pins) {
|
||||
ESP_LOGW(TAG, "Invalid fault output index %u", this->fault_output_pin_);
|
||||
} else if (!this->write_byte(TAS58XX_GPIO0_SEL + this->fault_output_pin_, TAS58XX_GPIO_SEL_FAULTZ) ||
|
||||
!this->write_byte(TAS58XX_GPIO_CTRL, static_cast<uint8_t>(1 << this->fault_output_pin_))) {
|
||||
ESP_LOGE(TAG, "I2C write failed during init");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Setters are public, so keep the values inside the register ranges
|
||||
const uint8_t again =
|
||||
static_cast<uint8_t>(lroundf(clamp(-this->analog_gain_db_ * 2.0f, 0.0f, float{TAS58XX_AGAIN_MINUS_15_5DB})));
|
||||
if (!this->write_byte(TAS58XX_DEVICE_CTRL_1, this->dac_mode_ == DAC_MODE_PBTL ? TAS58XX_DEVICE_CTRL_1_PBTL : 0) ||
|
||||
!this->write_byte(TAS58XX_AGAIN, again) || !this->write_volume_() ||
|
||||
!this->write_ctrl_state_(TAS58XX_CTRL_STATE_PLAY, this->is_muted_) ||
|
||||
const uint8_t device_ctrl_1 = (this->dac_mode_ == DAC_MODE_PBTL ? TAS58XX_DEVICE_CTRL_1_PBTL : 0) |
|
||||
(this->modulation_ == MODULATION_1SPW_MODE ? TAS58XX_DEVICE_CTRL_1_1SPW : 0);
|
||||
if (!this->write_byte(TAS58XX_DEVICE_CTRL_1, device_ctrl_1) || !this->write_byte(TAS58XX_AGAIN, again) ||
|
||||
!this->write_volume_() || !this->write_ctrl_state_(TAS58XX_CTRL_STATE_PLAY, this->is_muted_) ||
|
||||
!this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
|
||||
ESP_LOGE(TAG, "I2C write failed during init");
|
||||
return false;
|
||||
@@ -263,6 +281,14 @@ void TAS58xx::dump_config() {
|
||||
} else {
|
||||
mixer_mode = LOG_STR("Stereo");
|
||||
}
|
||||
const LogString *fault_output;
|
||||
if (this->fault_output_pin_ == FAULT_OUTPUT_NONE) {
|
||||
fault_output = LOG_STR("None");
|
||||
} else if (this->fault_output_pin_ >= this->model_->fault_output_pins) {
|
||||
fault_output = LOG_STR("Invalid");
|
||||
} else {
|
||||
fault_output = this->model_->fault_output_name(this->fault_output_pin_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Audio Amplifier:\n"
|
||||
" Model: %s",
|
||||
@@ -273,11 +299,14 @@ void TAS58xx::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Analog Gain: %.1f dB\n"
|
||||
" DAC Mode: %s\n"
|
||||
" Modulation: %s\n"
|
||||
" Mixer Mode: %s\n"
|
||||
" Volume Range: %.1f dB to %.1f dB",
|
||||
" Volume Range: %.1f dB to %.1f dB\n"
|
||||
" Fault Output: %s",
|
||||
this->analog_gain_db_,
|
||||
this->dac_mode_ == DAC_MODE_PBTL ? LOG_STR_LITERAL("PBTL") : LOG_STR_LITERAL("BTL"),
|
||||
LOG_STR_ARG(mixer_mode), this->volume_min_db_, this->volume_max_db_);
|
||||
this->modulation_ == MODULATION_1SPW_MODE ? LOG_STR_LITERAL("1SPW") : LOG_STR_LITERAL("BD"),
|
||||
LOG_STR_ARG(mixer_mode), this->volume_min_db_, this->volume_max_db_, LOG_STR_ARG(fault_output));
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Any Fault", this->have_fault_binary_sensor_);
|
||||
for (auto *sensor : this->fault_binary_sensors_) {
|
||||
|
||||
@@ -22,6 +22,12 @@ enum DacMode : uint8_t {
|
||||
DAC_MODE_PBTL = 1, // Parallel bridge tied load, one speaker
|
||||
};
|
||||
|
||||
/// DEVICE_CTRL_1 DAMP_MOD values, the same on all models
|
||||
enum Modulation : uint8_t {
|
||||
MODULATION_BD_MODE = 0,
|
||||
MODULATION_1SPW_MODE = 1,
|
||||
};
|
||||
|
||||
enum MixerMode : uint8_t {
|
||||
MIXER_MODE_STEREO = 0,
|
||||
MIXER_MODE_STEREO_INVERSE,
|
||||
@@ -64,6 +70,8 @@ static_assert(
|
||||
|
||||
/// ModelInfo::fault_sensor_bits value for a FaultSensor that the model does not have.
|
||||
static constexpr uint8_t NO_BIT = 0xFF;
|
||||
/// fault_output_pin_ value for no fault output
|
||||
static constexpr uint8_t FAULT_OUTPUT_NONE = 0xFF;
|
||||
|
||||
/// Everything that differs between models of the family. One constant instance exists per model, see
|
||||
/// model_*.cpp, and each TAS58xx instance points to the one for its model.
|
||||
@@ -91,6 +99,10 @@ struct ModelInfo {
|
||||
const LogString *(*fault_name)(uint8_t index);
|
||||
/// Bit index for each FaultSensor, NO_BIT used where the model does not have that fault bit
|
||||
uint8_t fault_sensor_bits[FAULT_SENSOR_COUNT];
|
||||
/// Number of pins that can report FAULTZ: pin n is selected at 0x61 + n and enabled by bit n of 0x60
|
||||
uint8_t fault_output_pins;
|
||||
/// Name of FAULTZ pin n, for dump_config
|
||||
const LogString *(*fault_output_name)(uint8_t pin);
|
||||
};
|
||||
|
||||
#ifdef USE_TAS58XX_TAS5805M
|
||||
@@ -127,7 +139,10 @@ class TAS58xx : public audio_dac::AudioDac, public PollingComponent, public i2c:
|
||||
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
|
||||
void set_analog_gain(float analog_gain_db) { this->analog_gain_db_ = analog_gain_db; }
|
||||
void set_dac_mode(DacMode dac_mode) { this->dac_mode_ = dac_mode; }
|
||||
/// pin: 0 to 2 for GPIOn on TAS5825M, 0 for ADR on TAS5805M; leave unset for no fault output
|
||||
void set_fault_output(uint8_t pin) { this->fault_output_pin_ = pin; }
|
||||
void set_mixer_mode(MixerMode mixer_mode) { this->mixer_mode_ = mixer_mode; }
|
||||
void set_modulation(Modulation modulation) { this->modulation_ = modulation; }
|
||||
void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; }
|
||||
void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; }
|
||||
|
||||
@@ -171,7 +186,9 @@ class TAS58xx : public audio_dac::AudioDac, public PollingComponent, public i2c:
|
||||
float volume_max_db_{24.0f};
|
||||
uint32_t logged_faults_{0}; // Fault bits as last logged, packed like the fault masks
|
||||
DacMode dac_mode_{DAC_MODE_BTL};
|
||||
uint8_t fault_output_pin_{FAULT_OUTPUT_NONE}; // No fault output: the pin keeps its power on state
|
||||
MixerMode mixer_mode_{MIXER_MODE_STEREO};
|
||||
Modulation modulation_{MODULATION_BD_MODE};
|
||||
uint8_t ctrl_state_{0};
|
||||
uint8_t power_state_{0xFF}; // Last POWER_STATE seen by update(), 0xFF until the first read
|
||||
bool mixer_written_{false}; // Mixer written since the last reset
|
||||
|
||||
@@ -124,6 +124,7 @@ CoverTraits TemplateCover::get_traits() {
|
||||
Trigger<float> *TemplateCover::get_position_trigger() { return &this->position_trigger_; }
|
||||
Trigger<float> *TemplateCover::get_tilt_trigger() { return &this->tilt_trigger_; }
|
||||
void TemplateCover::stop_prev_trigger_() {
|
||||
this->position_trigger_.stop_action();
|
||||
if (this->prev_command_trigger_ != nullptr) {
|
||||
this->prev_command_trigger_->stop_action();
|
||||
this->prev_command_trigger_ = nullptr;
|
||||
|
||||
@@ -54,10 +54,7 @@ void TT21100Touchscreen::setup() {
|
||||
}
|
||||
|
||||
// Perform reset if necessary
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_();
|
||||
}
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 10, .high_after_ms = 10});
|
||||
|
||||
// Update display dimensions if they were updated during display setup
|
||||
if (this->display_ != nullptr) {
|
||||
@@ -122,15 +119,6 @@ void TT21100Touchscreen::update_touches() {
|
||||
}
|
||||
}
|
||||
|
||||
void TT21100Touchscreen::reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(10);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
void TT21100Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TT21100 Touchscreen:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
@@ -28,8 +28,6 @@ class TT21100Touchscreen final : public Touchscreen, public i2c::I2CDevice {
|
||||
void register_button_listener(TT21100ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
|
||||
protected:
|
||||
void reset_();
|
||||
|
||||
void update_touches() override;
|
||||
|
||||
InternalGPIOPin *interrupt_pin_;
|
||||
|
||||
@@ -5,25 +5,37 @@ import esphome.codegen as cg
|
||||
from esphome.components import display, network, socket
|
||||
from esphome.components.const import (
|
||||
BYTE_ORDER_LITTLE,
|
||||
CONF_BUTTONS,
|
||||
CONF_HOST,
|
||||
CONF_RECONNECT_INTERVAL,
|
||||
)
|
||||
from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTION,
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_DISPLAY,
|
||||
CONF_DISPLAY_ID,
|
||||
CONF_ID,
|
||||
CONF_KEY,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_TEXT,
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
from .keysyms import button_mask, keysym
|
||||
|
||||
CODEOWNERS = ["@MindFreeze"]
|
||||
DEPENDENCIES = ["display", "network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
AUTO_LOAD = ["socket", "unicode"]
|
||||
DOMAIN = "vnc"
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -34,11 +46,14 @@ CONF_PIXEL_FORMAT = "pixel_format"
|
||||
vnc_ns = cg.esphome_ns.namespace("vnc")
|
||||
VncClient = vnc_ns.class_("VncClient", cg.Component)
|
||||
VncPixelFormat = vnc_ns.enum("VncPixelFormat", is_class=True)
|
||||
VncKeyAction = vnc_ns.enum("VncKeyAction", is_class=True)
|
||||
|
||||
PIXEL_FORMATS = VncPixelFormat.options(
|
||||
"auto", "rgb565", "rgb332", prefix="VNC_PIXEL_FORMAT_"
|
||||
)
|
||||
|
||||
KEY_ACTIONS = VncKeyAction.options("press", "down", "up", prefix="VNC_KEY_ACTION_")
|
||||
|
||||
# Drivers that cannot take another component's pixels yet.
|
||||
UNSUPPORTED_DISPLAYS = dict.fromkeys(
|
||||
("mipi_dsi", "mipi_rgb", "mipi_spi"), "it is not supported by vnc yet"
|
||||
@@ -72,6 +87,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_PIXEL_FORMAT, default="auto"): cv.enum(
|
||||
PIXEL_FORMATS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen),
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
}
|
||||
@@ -115,6 +131,14 @@ def _final_validate(config: ConfigType) -> None:
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' is also used by LVGL", path=[CONF_DISPLAY_ID]
|
||||
)
|
||||
if (ts_id := config.get(CONF_TOUCHSCREEN_ID)) is not None:
|
||||
ts_display = fv.get_declaring_config(ts_id).get(CONF_DISPLAY)
|
||||
if ts_display is not None and ts_display.id != disp_id.id:
|
||||
raise cv.Invalid(
|
||||
f"Touchscreen '{ts_id}' belongs to display '{ts_display}', "
|
||||
f"not '{disp_id}'",
|
||||
path=[CONF_TOUCHSCREEN_ID],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
@@ -138,15 +162,89 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
|
||||
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
|
||||
cg.add(var.set_pixel_format(config[CONF_PIXEL_FORMAT]))
|
||||
if (
|
||||
await cg.add_variable(var.register_touchscreen, config.get(CONF_TOUCHSCREEN_ID))
|
||||
is not None
|
||||
):
|
||||
cg.add_define("USE_VNC_INPUT")
|
||||
cg.add_define("USE_VNC_TOUCHSCREEN")
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
VNC_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(VncClient)},
|
||||
)
|
||||
_CLIENT_ID = {cv.GenerateID(): cv.use_id(VncClient)}
|
||||
|
||||
VNC_ID_SCHEMA = automation.maybe_simple_id(_CLIENT_ID)
|
||||
|
||||
automation.register_apply_action(
|
||||
"vnc.refresh", VNC_ID_SCHEMA, automation.ApplyCall("refresh()")
|
||||
)
|
||||
|
||||
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
|
||||
|
||||
|
||||
def _register_input_action(
|
||||
name: str, schema: cv.Schema, *fields: automation.ApplyField | automation.ApplyCall
|
||||
) -> None:
|
||||
builder = automation.apply_action_builder(name, schema, *fields)
|
||||
|
||||
@automation.register_action(name, automation.ApplyAction, schema, synchronous=True)
|
||||
async def input_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_VNC_INPUT")
|
||||
return await builder(config, action_id, template_arg, args)
|
||||
|
||||
|
||||
_register_input_action(
|
||||
"vnc.send_pointer",
|
||||
cv.Schema(
|
||||
{
|
||||
**_CLIENT_ID,
|
||||
cv.Required(CONF_X): cv.templatable(cv.uint16_t),
|
||||
cv.Required(CONF_Y): cv.templatable(cv.uint16_t),
|
||||
cv.Optional(CONF_BUTTONS, default=0): cv.templatable(button_mask),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"send_pointer({}, {}, {})",
|
||||
((CONF_X, cg.uint16), (CONF_Y, cg.uint16), (CONF_BUTTONS, cg.uint8)),
|
||||
),
|
||||
)
|
||||
|
||||
_register_input_action(
|
||||
"vnc.send_key",
|
||||
automation.maybe_conf(
|
||||
CONF_KEY,
|
||||
cv.Schema(
|
||||
{
|
||||
**_CLIENT_ID,
|
||||
cv.Required(CONF_KEY): cv.templatable(keysym),
|
||||
cv.Optional(CONF_ACTION, default="press"): cv.enum(
|
||||
KEY_ACTIONS, lower=True
|
||||
),
|
||||
}
|
||||
),
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"send_key({}, {})", ((CONF_KEY, cg.uint32), (CONF_ACTION, VncKeyAction))
|
||||
),
|
||||
)
|
||||
|
||||
_register_input_action(
|
||||
"vnc.type_text",
|
||||
automation.maybe_conf(
|
||||
CONF_TEXT,
|
||||
cv.Schema(
|
||||
{
|
||||
**_CLIENT_ID,
|
||||
cv.Required(CONF_TEXT): cv.templatable(cv.string),
|
||||
}
|
||||
),
|
||||
),
|
||||
automation.ApplyField(
|
||||
CONF_TEXT, "type_text", cg.std_string, const_fn=automation.literal_with_length
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,136 @@
|
||||
"""X11 keysyms and pointer button masks for VNC input, resolved at code generation."""
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import HexInt
|
||||
|
||||
_KEYSYM_MAX = 0x1FFFFFFF
|
||||
# X11 encodes a Unicode character outside Latin-1 as this prefix plus its code point
|
||||
_UNICODE_KEYSYM = 0x01000000
|
||||
|
||||
KEYSYMS: dict[str, int] = {
|
||||
"BackSpace": 0xFF08,
|
||||
"Tab": 0xFF09,
|
||||
"Return": 0xFF0D,
|
||||
"Pause": 0xFF13,
|
||||
"Scroll_Lock": 0xFF14,
|
||||
"Escape": 0xFF1B,
|
||||
"Home": 0xFF50,
|
||||
"Left": 0xFF51,
|
||||
"Up": 0xFF52,
|
||||
"Right": 0xFF53,
|
||||
"Down": 0xFF54,
|
||||
"Page_Up": 0xFF55,
|
||||
"Page_Down": 0xFF56,
|
||||
"End": 0xFF57,
|
||||
"Print": 0xFF61,
|
||||
"Insert": 0xFF63,
|
||||
"Menu": 0xFF67,
|
||||
"Num_Lock": 0xFF7F,
|
||||
"KP_Enter": 0xFF8D,
|
||||
**{f"F{n}": 0xFFBE + n - 1 for n in range(1, 13)},
|
||||
"Shift_L": 0xFFE1,
|
||||
"Shift_R": 0xFFE2,
|
||||
"Control_L": 0xFFE3,
|
||||
"Control_R": 0xFFE4,
|
||||
"Caps_Lock": 0xFFE5,
|
||||
"Meta_L": 0xFFE7,
|
||||
"Meta_R": 0xFFE8,
|
||||
"Alt_L": 0xFFE9,
|
||||
"Alt_R": 0xFFEA,
|
||||
"Super_L": 0xFFEB,
|
||||
"Super_R": 0xFFEC,
|
||||
"Delete": 0xFFFF,
|
||||
"space": 0x20,
|
||||
"ISO_Left_Tab": 0xFE20,
|
||||
}
|
||||
|
||||
ALIASES: dict[str, str] = {
|
||||
"alt": "Alt_L",
|
||||
"ctrl": "Control_L",
|
||||
"del": "Delete",
|
||||
"enter": "Return",
|
||||
"esc": "Escape",
|
||||
"ins": "Insert",
|
||||
"next": "Page_Down",
|
||||
"pgdn": "Page_Down",
|
||||
"pgup": "Page_Up",
|
||||
"prior": "Page_Up",
|
||||
"shift": "Shift_L",
|
||||
"super": "Super_L",
|
||||
}
|
||||
|
||||
_BY_LOWER_NAME: dict[str, int] = {
|
||||
**{name.lower(): code for name, code in KEYSYMS.items()},
|
||||
**{alias: KEYSYMS[name] for alias, name in ALIASES.items()},
|
||||
}
|
||||
|
||||
BUTTONS: dict[str, int] = {
|
||||
"left": 1,
|
||||
"middle": 2,
|
||||
"right": 4,
|
||||
"wheel_up": 8,
|
||||
"wheel_down": 16,
|
||||
}
|
||||
|
||||
_BUTTON_NAMES = cv.ensure_list(cv.one_of(*BUTTONS, lower=True))
|
||||
|
||||
|
||||
_keysym_number = cv.hex_int_range(min=0, max=_KEYSYM_MAX)
|
||||
|
||||
|
||||
# Keep in step with keysym_for_codepoint() in vnc_client.cpp.
|
||||
def _character_keysym(char: str) -> HexInt:
|
||||
code = ord(char)
|
||||
if 0x20 <= code <= 0x7E or 0xA0 <= code <= 0xFF:
|
||||
return HexInt(code)
|
||||
if code <= 0x9F:
|
||||
raise cv.Invalid(
|
||||
f"Control character U+{code:04X} is not a key. Use a key name such as Return, Tab or BackSpace"
|
||||
)
|
||||
return HexInt(_UNICODE_KEYSYM | code)
|
||||
|
||||
|
||||
def keysym(value: object) -> HexInt:
|
||||
"""An X11 keysym from a key name, a single character or a number.
|
||||
|
||||
An unquoted digit 0 to 9 is read as that digit, as in tinyusb_keyboard. Names are matched
|
||||
without regard to case, but a single character is always that character, so "a" and "A" differ.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
raise cv.Invalid(
|
||||
'YAML read this key as a boolean. Put it in quotes, for example key: "y"'
|
||||
)
|
||||
if isinstance(value, int):
|
||||
if 0 <= value <= 9:
|
||||
return HexInt(ord("0") + value)
|
||||
return _keysym_number(value)
|
||||
if not isinstance(value, str):
|
||||
raise cv.Invalid(
|
||||
f"Expected a key name, a single character or a keysym number, got {value!r}"
|
||||
)
|
||||
if len(value) == 1:
|
||||
return _character_keysym(value)
|
||||
lower = value.lower()
|
||||
if lower.startswith("0x"):
|
||||
return _keysym_number(value)
|
||||
if (code := _BY_LOWER_NAME.get(lower)) is not None:
|
||||
return HexInt(code)
|
||||
raise cv.Invalid(
|
||||
f"Unknown key '{value}'. Use a single character, a keysym number such as 0xFF0D, "
|
||||
f""
|
||||
"or a key name such as Return, Escape, Tab, BackSpace, Delete, Left, Page_Up, F1 or Control_L"
|
||||
)
|
||||
|
||||
|
||||
def button_mask(value: object) -> int:
|
||||
"""A pointer button mask from a number 0 to 255, or one or more button names."""
|
||||
if isinstance(value, bool):
|
||||
raise cv.Invalid(
|
||||
f"Expected a button mask or button names ({', '.join(BUTTONS)}), got a boolean"
|
||||
)
|
||||
if isinstance(value, int):
|
||||
return cv.uint8_t(value)
|
||||
mask = 0
|
||||
for name in _BUTTON_NAMES(value):
|
||||
mask |= BUTTONS[name]
|
||||
return mask
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "vnc_client.h"
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/components/unicode/unicode.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -8,6 +9,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
@@ -53,6 +55,17 @@ static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
|
||||
static constexpr uint8_t MSG_BELL = 2;
|
||||
static constexpr uint8_t MSG_SERVER_CUT_TEXT = 3;
|
||||
|
||||
#ifdef USE_VNC_INPUT
|
||||
static constexpr uint8_t MSG_KEY_EVENT = 4;
|
||||
static constexpr uint8_t MSG_POINTER_EVENT = 5;
|
||||
|
||||
static constexpr uint32_t KEYSYM_BACKSPACE = 0xFF08;
|
||||
static constexpr uint32_t KEYSYM_TAB = 0xFF09;
|
||||
static constexpr uint32_t KEYSYM_RETURN = 0xFF0D;
|
||||
// X11 encodes a character outside Latin-1 as this prefix plus its code point.
|
||||
static constexpr uint32_t KEYSYM_UNICODE = 0x01000000;
|
||||
#endif
|
||||
|
||||
static inline uint16_t read_be16(const uint8_t *p) { return encode_uint16(p[0], p[1]); }
|
||||
static inline uint32_t read_be32(const uint8_t *p) { return encode_uint32(p[0], p[1], p[2], p[3]); }
|
||||
static inline void write_be16(uint8_t *p, uint16_t v) {
|
||||
@@ -77,8 +90,59 @@ static int parse_three_digits(const uint8_t *p) {
|
||||
return value;
|
||||
}
|
||||
|
||||
#ifdef USE_VNC_INPUT
|
||||
// Keep in step with _character_keysym() in keysyms.py.
|
||||
static uint32_t keysym_for_codepoint(uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '\n':
|
||||
return KEYSYM_RETURN;
|
||||
case '\t':
|
||||
return KEYSYM_TAB;
|
||||
case '\b':
|
||||
return KEYSYM_BACKSPACE;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (cp < 0x20 || (cp >= 0x7F && cp < 0xA0)) {
|
||||
return 0;
|
||||
}
|
||||
return cp <= 0xFF ? cp : (KEYSYM_UNICODE | cp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
static constexpr uint16_t TOUCH_SLOP_PX = 8;
|
||||
|
||||
static bool is_down(const touchscreen::TouchPoint &tp) {
|
||||
// A lifted finger lingers in the list as RELEASED or RELEASING until every finger is up.
|
||||
return (tp.state & (touchscreen::STATE_PRESSED | touchscreen::STATE_UPDATED)) != 0 &&
|
||||
(tp.state & touchscreen::STATE_RELEASING) == 0;
|
||||
}
|
||||
|
||||
// A first touch at (0, 0) matches a new point's default previous position, so only touch() reports it.
|
||||
void VncClient::touch(touchscreen::TouchPoint tp) {
|
||||
if (is_down(tp)) {
|
||||
this->handle_touch_(tp.x, tp.y);
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::update(const touchscreen::TouchPoints_t &tpoints) {
|
||||
for (const auto &tp : tpoints) {
|
||||
if (is_down(tp)) {
|
||||
this->handle_touch_(tp.x, tp.y);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::release() { this->handle_release_(); }
|
||||
#endif
|
||||
|
||||
void VncClient::setup() {
|
||||
this->setup_ms_ = App.get_loop_component_start_time();
|
||||
#ifdef USE_VNC_INPUT
|
||||
this->last_input_ms_ = this->setup_ms_ - INPUT_BOOST_MS; // no input yet
|
||||
#endif
|
||||
this->link_.begin(TAG);
|
||||
// Sized for the display now; a rotation changed later swaps the sides but keeps the cell count.
|
||||
const uint32_t wanted = static_cast<uint32_t>((this->display_->get_width() + 15) / 16) *
|
||||
@@ -121,6 +185,9 @@ void VncClient::dump_config() {
|
||||
this->max_update_delay_ms_,
|
||||
LOG_STR_ARG(PixelFormatStrings::get_log_str(static_cast<uint8_t>(this->pixel_format_), 0)),
|
||||
this->hash_capacity_);
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
ESP_LOGCONFIG(TAG, " Touchscreen: %s", this->has_touchscreen_ ? LOG_STR_LITERAL("yes") : LOG_STR_LITERAL("no"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void VncClient::on_shutdown() { this->link_.close(); }
|
||||
@@ -195,10 +262,15 @@ void VncClient::loop() {
|
||||
// A display still starting up (it8951) can go from its start-up straight into a refresh without parking.
|
||||
const bool display_starting =
|
||||
!this->display_parks_ && this->display_->has_overridden_loop() && now - this->setup_ms_ < DISPLAY_START_MS;
|
||||
uint32_t update_delay = this->update_delay_ms_;
|
||||
#ifdef USE_VNC_INPUT
|
||||
if (now - this->last_input_ms_ < INPUT_BOOST_MS) {
|
||||
update_delay = 0;
|
||||
}
|
||||
#endif
|
||||
// Wait until the screen stops changing, so an animation becomes one refresh showing where it ended.
|
||||
if (this->refresh_pending_ && !this->in_update_ && !this->display_busy_ && !display_starting &&
|
||||
(now - this->last_change_ms_ >= this->update_delay_ms_ ||
|
||||
now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
|
||||
(now - this->last_change_ms_ >= update_delay || now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
|
||||
// Synchronous displays flush inside update(), so this pass does nothing else.
|
||||
this->refresh_pending_ = false;
|
||||
this->display_->update();
|
||||
@@ -207,6 +279,9 @@ void VncClient::loop() {
|
||||
this->service_rx_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
#ifdef USE_VNC_INPUT
|
||||
this->send_pending_();
|
||||
#endif
|
||||
// A new session lays the screen out again for the new rotation and redraws all of it.
|
||||
if (static_cast<uint8_t>(this->display_->get_rotation() / 90) != this->hash_rotation_) {
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display rotation changed"));
|
||||
@@ -263,6 +338,13 @@ void VncClient::reset_session_() {
|
||||
this->refresh_pending_ = false;
|
||||
this->need_full_ = false;
|
||||
this->frame_drawn_ = false;
|
||||
#ifdef USE_VNC_INPUT
|
||||
this->pending_count_ = 0;
|
||||
this->ptr_buttons_ = 0;
|
||||
#endif
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
this->touching_ = false;
|
||||
#endif
|
||||
this->step_ = VncStep::VNC_STEP_VERSION;
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
this->high_freq_.stop();
|
||||
@@ -984,4 +1066,144 @@ bool VncClient::send_update_request_(bool incremental, uint32_t now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_VNC_INPUT
|
||||
bool VncClient::send_key_event_(uint32_t keysym, VncKeyAction action) {
|
||||
// A press is the down and the up event, sent whole or not at all.
|
||||
uint8_t msg[2 * KEY_EVENT_SIZE] = {MSG_KEY_EVENT, static_cast<uint8_t>(action != VncKeyAction::VNC_KEY_ACTION_UP)};
|
||||
msg[KEY_EVENT_SIZE] = MSG_KEY_EVENT;
|
||||
write_be32(msg + 4, keysym);
|
||||
write_be32(msg + KEY_EVENT_SIZE + 4, keysym);
|
||||
return this->send_input_(msg, action == VncKeyAction::VNC_KEY_ACTION_PRESS ? sizeof(msg) : KEY_EVENT_SIZE,
|
||||
action == VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
}
|
||||
|
||||
// Input goes out after the releases that are still waiting. A release that does not fit waits for room,
|
||||
// because a key or button left down would change every later key and click; other input is dropped.
|
||||
bool VncClient::send_input_(const uint8_t *msg, size_t len, bool release) {
|
||||
if (this->send_pending_() && this->send_(msg, len)) {
|
||||
this->note_input_();
|
||||
return true;
|
||||
}
|
||||
if (!release) {
|
||||
return false;
|
||||
}
|
||||
if (this->pending_count_ == MAX_PENDING_RELEASES) {
|
||||
// Losing a release would leave its key or button held on the server
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Too many releases waiting for the send buffer"));
|
||||
return false;
|
||||
}
|
||||
std::memcpy(this->pending_[this->pending_count_++], msg, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VncClient::send_pending_() {
|
||||
while (this->pending_count_ != 0) {
|
||||
const uint8_t *msg = this->pending_[0];
|
||||
if (!this->send_(msg, msg[0] == MSG_POINTER_EVENT ? POINTER_EVENT_SIZE : KEY_EVENT_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
this->note_input_();
|
||||
this->pending_count_--;
|
||||
std::memmove(this->pending_[0], this->pending_[1], this->pending_count_ * sizeof(this->pending_[0]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VncClient::send_pointer(uint16_t x, uint16_t y, uint8_t buttons) {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGV(TAG, "Not connected, pointer event dropped");
|
||||
return false;
|
||||
}
|
||||
x = std::min<uint16_t>(x, this->fb_w_ - 1);
|
||||
y = std::min<uint16_t>(y, this->fb_h_ - 1);
|
||||
uint8_t msg[POINTER_EVENT_SIZE] = {MSG_POINTER_EVENT, buttons};
|
||||
write_be16(msg + 2, x);
|
||||
write_be16(msg + 4, y);
|
||||
const bool release = buttons != this->ptr_buttons_ && (buttons & ~this->ptr_buttons_) == 0;
|
||||
if (!this->send_input_(msg, sizeof(msg), release)) {
|
||||
ESP_LOGW(TAG, "Send buffer full, pointer event dropped");
|
||||
return false;
|
||||
}
|
||||
this->ptr_x_ = x;
|
||||
this->ptr_y_ = y;
|
||||
this->ptr_buttons_ = buttons;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VncClient::note_input_() { this->last_input_ms_ = App.get_loop_component_start_time(); }
|
||||
|
||||
void VncClient::send_key(uint32_t keysym, VncKeyAction action) {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGV(TAG, "Not connected, key dropped");
|
||||
return;
|
||||
}
|
||||
if (!this->send_key_event_(keysym, action)) {
|
||||
ESP_LOGW(TAG, "Send buffer full, key dropped");
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::type_text(const char *text, size_t len) {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGV(TAG, "Not connected, text dropped");
|
||||
return;
|
||||
}
|
||||
size_t typed = 0;
|
||||
for (size_t i = 0; i < len;) {
|
||||
// The decoder reads up to a NUL, so give it only the bytes that belong to this text.
|
||||
char sequence[5] = {};
|
||||
std::memcpy(sequence, text + i, std::min<size_t>(4, len - i));
|
||||
size_t used;
|
||||
const uint32_t cp = unicode::extract_unicode_codepoint(sequence, &used);
|
||||
// A broken sequence or a NUL types nothing; carry on at the next byte.
|
||||
i += used == 0 ? 1 : used;
|
||||
const uint32_t keysym = keysym_for_codepoint(cp);
|
||||
if (keysym == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!this->send_key_event_(keysym, VncKeyAction::VNC_KEY_ACTION_PRESS)) {
|
||||
ESP_LOGW(TAG, "Send buffer full, text cut after %u characters", static_cast<unsigned>(typed));
|
||||
return;
|
||||
}
|
||||
typed++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
void VncClient::handle_touch_(uint16_t x, uint16_t y) {
|
||||
if (!this->is_connected()) {
|
||||
return;
|
||||
}
|
||||
if (!this->touching_) {
|
||||
if (x >= this->vis_w_ || y >= this->vis_h_) {
|
||||
return;
|
||||
}
|
||||
this->press_x_ = x;
|
||||
this->press_y_ = y;
|
||||
this->dragging_ = false;
|
||||
this->touching_ = this->send_pointer(x, y, 1);
|
||||
return;
|
||||
}
|
||||
x = std::min<uint16_t>(x, this->vis_w_ - 1);
|
||||
y = std::min<uint16_t>(y, this->vis_h_ - 1);
|
||||
// Small jitter during a tap would start a text selection in a browser.
|
||||
if (!this->dragging_) {
|
||||
if (std::abs(x - this->press_x_) <= TOUCH_SLOP_PX && std::abs(y - this->press_y_) <= TOUCH_SLOP_PX) {
|
||||
return;
|
||||
}
|
||||
this->dragging_ = true;
|
||||
}
|
||||
if (x != this->ptr_x_ || y != this->ptr_y_) {
|
||||
this->send_pointer(x, y, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::handle_release_() {
|
||||
if (this->touching_) {
|
||||
this->touching_ = false;
|
||||
this->send_pointer(this->ptr_x_, this->ptr_y_, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::vnc
|
||||
@@ -4,9 +4,13 @@
|
||||
#include "esphome/components/socket/tcp_client_link.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#endif
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
@@ -16,6 +20,12 @@ enum class VncPixelFormat : uint8_t {
|
||||
VNC_PIXEL_FORMAT_RGB332,
|
||||
};
|
||||
|
||||
enum class VncKeyAction : uint8_t {
|
||||
VNC_KEY_ACTION_PRESS,
|
||||
VNC_KEY_ACTION_DOWN,
|
||||
VNC_KEY_ACTION_UP,
|
||||
};
|
||||
|
||||
enum class VncState : uint8_t {
|
||||
VNC_STATE_IDLE,
|
||||
VNC_STATE_HANDSHAKE,
|
||||
@@ -55,7 +65,12 @@ enum class VncFailure : uint8_t {
|
||||
};
|
||||
|
||||
/// RFB (VNC) client that draws the remote framebuffer into a display.
|
||||
class VncClient : public Component {
|
||||
class VncClient : public Component
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
,
|
||||
public touchscreen::TouchListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
explicit VncClient(display::Display *display, bool big_endian = false) : display_(display), big_endian_(big_endian) {}
|
||||
|
||||
@@ -65,6 +80,15 @@ class VncClient : public Component {
|
||||
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
|
||||
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
|
||||
void set_pixel_format(VncPixelFormat format) { this->pixel_format_ = format; }
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
void register_touchscreen(touchscreen::Touchscreen *touchscreen) {
|
||||
touchscreen->register_listener(this);
|
||||
this->has_touchscreen_ = true;
|
||||
}
|
||||
void touch(touchscreen::TouchPoint tp) override;
|
||||
void update(const touchscreen::TouchPoints_t &tpoints) override;
|
||||
void release() override;
|
||||
#endif
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
@@ -76,6 +100,13 @@ class VncClient : public Component {
|
||||
/// Ask the server for the whole screen again.
|
||||
void refresh();
|
||||
|
||||
#ifdef USE_VNC_INPUT
|
||||
bool send_pointer(uint16_t x, uint16_t y, uint8_t buttons);
|
||||
void send_key(uint32_t keysym, VncKeyAction action);
|
||||
void type_text(const char *text, size_t len);
|
||||
void type_text(const std::string &text) { this->type_text(text.data(), text.size()); }
|
||||
#endif
|
||||
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
@@ -86,6 +117,13 @@ class VncClient : public Component {
|
||||
protected:
|
||||
static constexpr size_t RX_BUFFER_SIZE = 512;
|
||||
static constexpr size_t TILE_BUFFER_SIZE = 16 * 16 * 2;
|
||||
#ifdef USE_VNC_INPUT
|
||||
// For this long after input, frames are painted as soon as they arrive: the reaction matters more than a calm panel.
|
||||
static constexpr uint32_t INPUT_BOOST_MS = 2000;
|
||||
static constexpr size_t KEY_EVENT_SIZE = 8;
|
||||
static constexpr size_t POINTER_EVENT_SIZE = 6;
|
||||
static constexpr size_t MAX_PENDING_RELEASES = 4;
|
||||
#endif
|
||||
|
||||
void connect_step_(uint32_t now);
|
||||
void start_handshake_(uint32_t now);
|
||||
@@ -107,6 +145,17 @@ class VncClient : public Component {
|
||||
bool budget_exhausted_() const;
|
||||
bool send_(const uint8_t *data, size_t len);
|
||||
bool send_update_request_(bool incremental, uint32_t now);
|
||||
#ifdef USE_VNC_INPUT
|
||||
bool send_key_event_(uint32_t keysym, VncKeyAction action);
|
||||
bool send_input_(const uint8_t *msg, size_t len, bool release);
|
||||
bool send_pending_();
|
||||
void note_input_();
|
||||
#endif
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
/// Touch input in display coordinates.
|
||||
void handle_touch_(uint16_t x, uint16_t y);
|
||||
void handle_release_();
|
||||
#endif
|
||||
VncParse fail_(VncFailure failure, const LogString *reason);
|
||||
VncParse fail_send_() { return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Send failed")); }
|
||||
VncFailure lost_failure_() const;
|
||||
@@ -135,6 +184,9 @@ class VncClient : public Component {
|
||||
// The first and the latest frame drawn since the display last refreshed.
|
||||
uint32_t first_change_ms_{0};
|
||||
uint32_t last_change_ms_{0};
|
||||
#ifdef USE_VNC_INPUT
|
||||
uint32_t last_input_ms_{0};
|
||||
#endif
|
||||
uint32_t rx_total_{0};
|
||||
uint32_t update_start_bytes_{0};
|
||||
uint32_t last_request_ms_{0};
|
||||
@@ -161,7 +213,19 @@ class VncClient : public Component {
|
||||
uint16_t hash_capacity_{0};
|
||||
uint16_t hash_cols_{0};
|
||||
uint16_t hash_rows_{0};
|
||||
#ifdef USE_VNC_INPUT
|
||||
uint16_t ptr_x_{0};
|
||||
uint16_t ptr_y_{0};
|
||||
#endif
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
uint16_t press_x_{0};
|
||||
uint16_t press_y_{0};
|
||||
#endif
|
||||
|
||||
#ifdef USE_VNC_INPUT
|
||||
uint8_t ptr_buttons_{0}; // as the server will see them once the waiting releases are sent
|
||||
uint8_t pending_count_{0};
|
||||
#endif
|
||||
uint8_t tile_w_{0};
|
||||
uint8_t tile_h_{0};
|
||||
uint8_t subrects_left_{0};
|
||||
@@ -193,9 +257,18 @@ class VncClient : public Component {
|
||||
bool busy_warned_{false};
|
||||
bool refresh_retried_{false}; // one retry per refresh the driver abandoned
|
||||
bool network_was_up_{false};
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
bool has_touchscreen_{false};
|
||||
bool dragging_{false};
|
||||
bool touching_{false};
|
||||
#endif
|
||||
bool big_endian_; // the pixel order the display takes
|
||||
|
||||
uint8_t rx_[RX_BUFFER_SIZE];
|
||||
#ifdef USE_VNC_INPUT
|
||||
// Key-up and button-release messages waiting for room, oldest first.
|
||||
uint8_t pending_[MAX_PENDING_RELEASES][KEY_EVENT_SIZE];
|
||||
#endif
|
||||
alignas(4) uint8_t tile_[TILE_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
|
||||
@@ -245,6 +245,22 @@ WiFiConfigureAction = wifi_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
# The supplicant parses the bootstrapping key as a DER encoded P-256 private key in hex; these wrap the
|
||||
# 32 byte key the way the ESP-IDF enrollee example does
|
||||
DPP_KEY_DER_PREFIX = "30310201010420"
|
||||
DPP_KEY_DER_SUFFIX = "a00a06082a8648ce3d030107"
|
||||
|
||||
|
||||
def _validate_dpp_private_key(value: object) -> str:
|
||||
"""The key is 32 bytes written as 64 hexadecimal characters."""
|
||||
value = cv.string_strict(value)
|
||||
if len(value) != 64 or any(c not in "0123456789abcdefABCDEF" for c in value):
|
||||
raise cv.Invalid(
|
||||
"private_key must be 32 bytes written as 64 hexadecimal characters"
|
||||
)
|
||||
return value.lower()
|
||||
|
||||
|
||||
def _validate_dpp_device_info(value: object) -> str:
|
||||
"""The supplicant puts the text on a space separated command line and reads it up to the next space."""
|
||||
value = cv.string_strict(value)
|
||||
@@ -322,7 +338,7 @@ EAP_AUTH_SCHEMA = cv.All(
|
||||
# Wi-Fi Easy Connect (DPP) enrollee; the ESP-IDF supplicant supports the QR code method
|
||||
DPP_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PRIVATE_KEY): cv.sensitive(cv.string_strict),
|
||||
cv.Optional(CONF_PRIVATE_KEY): cv.sensitive(_validate_dpp_private_key),
|
||||
cv.Optional(CONF_DEVICE_INFO): _validate_dpp_device_info,
|
||||
cv.Optional(CONF_CHANNELS, default=[6]): cv.ensure_list(
|
||||
cv.int_range(min=1, max=14)
|
||||
@@ -904,7 +920,7 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DPP_SUPPORT", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HOST_FEAT_WIFI_EXT_DPP", True)
|
||||
if (key := dpp.get(CONF_PRIVATE_KEY)) is not None:
|
||||
cg.add(var.set_dpp_key(key))
|
||||
cg.add(var.set_dpp_key(f"{DPP_KEY_DER_PREFIX}{key}{DPP_KEY_DER_SUFFIX}"))
|
||||
if (info := dpp.get(CONF_DEVICE_INFO)) is not None:
|
||||
cg.add(var.set_dpp_device_info(info))
|
||||
cg.add(var.set_dpp_channels(",".join(str(c) for c in dpp[CONF_CHANNELS])))
|
||||
|
||||
@@ -2744,8 +2744,13 @@ def url(value):
|
||||
raise Invalid("Expected a file scheme or a URL scheme with host")
|
||||
|
||||
|
||||
def git_ref(value):
|
||||
if re.match(r"[a-zA-Z0-9_./-]+", value) is None:
|
||||
# Characters git never allows in a branch or tag name (git check-ref-format)
|
||||
GIT_REF_PATTERN = r"[^\s~^:?*\[\\]+"
|
||||
|
||||
|
||||
def git_ref(value: typing.Any) -> str:
|
||||
value = string_strict(value)
|
||||
if re.fullmatch(GIT_REF_PATTERN, value) is None:
|
||||
raise Invalid("Not a valid git ref")
|
||||
return value
|
||||
|
||||
@@ -3000,9 +3005,8 @@ def validate_source_shorthand(value):
|
||||
except Invalid:
|
||||
pass
|
||||
# Regex for GitHub repo name with optional branch/tag
|
||||
# Note: git allows other branch/tag names as well, but never seen them used before
|
||||
m = re.match(
|
||||
r"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?|pr#([0-9]+))",
|
||||
m = re.fullmatch(
|
||||
rf"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@({GIT_REF_PATTERN}))?|pr#([0-9]+))",
|
||||
value,
|
||||
)
|
||||
if m is None:
|
||||
|
||||
@@ -910,7 +910,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
CORE.add_job(_add_automations, config)
|
||||
|
||||
cg.add_build_flag("-fno-exceptions")
|
||||
if not CORE.using_toolchain_esp_idf:
|
||||
cg.add_build_flag("-fno-exceptions")
|
||||
|
||||
# Libraries
|
||||
for lib in config[CONF_LIBRARIES]:
|
||||
|
||||
@@ -536,6 +536,8 @@
|
||||
#define USE_SOCKET_TCP_CLIENT_LINK
|
||||
#define USE_SOCKET_TCP_LISTENER
|
||||
#define USE_LWIP_FAST_SELECT
|
||||
#define USE_VNC_INPUT
|
||||
#define USE_VNC_TOUCHSCREEN
|
||||
|
||||
#define USE_SPEAKER
|
||||
#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -18,4 +19,18 @@ void log_pin(const char *tag, const char *prefix, GPIOPin *pin) {
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace internal {
|
||||
void pulse_reset_ms(GPIOPin *pin, uint32_t high_before_ms, uint32_t low_ms, uint32_t high_after_ms) {
|
||||
if (pin == nullptr)
|
||||
return;
|
||||
pin->setup();
|
||||
const uint32_t waits[] = {high_before_ms, low_ms, high_after_ms};
|
||||
// Level for each step is high, low, high; a zero high_before_ms skips the first step
|
||||
for (size_t i = high_before_ms == 0; i < 3; i++) {
|
||||
pin->digital_write(i != 1);
|
||||
delay(waits[i]);
|
||||
}
|
||||
}
|
||||
} // namespace internal
|
||||
|
||||
} // namespace esphome
|
||||
@@ -144,4 +144,24 @@ void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin);
|
||||
void log_pin(const char *tag, const char *prefix, GPIOPin *pin);
|
||||
#endif
|
||||
|
||||
/// Waveform for pulse_reset(): high, low, then high again. Name the fields at the call site, e.g.
|
||||
/// pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}).
|
||||
struct ResetPulse {
|
||||
uint32_t high_before_ms{0}; ///< Drive high this long first; skipped when 0
|
||||
uint32_t low_ms{0}; ///< Hold the pin low (in reset) this long
|
||||
uint32_t high_after_ms{0}; ///< Wait this long after releasing the pin
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
/// Out-of-line body of pulse_reset(); call that instead.
|
||||
void pulse_reset_ms(GPIOPin *pin, uint32_t high_before_ms, uint32_t low_ms, uint32_t high_after_ms);
|
||||
} // namespace internal
|
||||
|
||||
/// Set up an active-low reset pin and drive it through \p pulse. Blocks for the whole sequence;
|
||||
/// does nothing when pin is null. Inline so the fields reach pulse_reset_ms() in registers
|
||||
/// (RISC-V would otherwise pass the struct by reference).
|
||||
ESPHOME_ALWAYS_INLINE inline void pulse_reset(GPIOPin *pin, ResetPulse pulse) {
|
||||
internal::pulse_reset_ms(pin, pulse.high_before_ms, pulse.low_ms, pulse.high_after_ms);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -42,20 +42,26 @@ _MIRROR_ATTEMPTS = 3
|
||||
_MIRROR_SWEEP_ATTEMPTS = NETWORK_MAX_ATTEMPTS
|
||||
|
||||
|
||||
def get_project_link_flags() -> list[str]:
|
||||
"""Return the sorted -Wl, linker flags from the current build."""
|
||||
def get_project_link_flags(flags: Iterable[str] | None = None) -> list[str]:
|
||||
"""Return the -Wl, linker flags from ``flags`` in their given order
|
||||
(default: the current build's flags, sorted)."""
|
||||
from esphome.core import CORE # local import to avoid circular dependency
|
||||
|
||||
return sorted(flag for flag in CORE.build_flags if flag.startswith("-Wl,"))
|
||||
if flags is None:
|
||||
flags = sorted(CORE.build_flags)
|
||||
return [flag for flag in flags if flag.startswith("-Wl,")]
|
||||
|
||||
|
||||
def get_project_compile_flags() -> list[str]:
|
||||
"""Return the sorted -D and -W (non-linker) flags from the current build."""
|
||||
def get_project_compile_flags(flags: Iterable[str] | None = None) -> list[str]:
|
||||
"""Return the -D and -W (non-linker) flags from ``flags`` in their given
|
||||
order (default: the current build's flags, sorted)."""
|
||||
from esphome.core import CORE # local import to avoid circular dependency
|
||||
|
||||
if flags is None:
|
||||
flags = sorted(CORE.build_flags)
|
||||
return [
|
||||
flag
|
||||
for flag in sorted(CORE.build_flags)
|
||||
for flag in flags
|
||||
if flag.startswith("-D")
|
||||
or (flag.startswith("-W") and not flag.startswith("-Wl,"))
|
||||
]
|
||||
|
||||
+5
-5
@@ -718,9 +718,9 @@ def _clone_or_update_locked(
|
||||
# compute the same cache key as this one.
|
||||
original_url = url
|
||||
if username is not None and password is not None:
|
||||
url = url.replace(
|
||||
"://", f"://{urllib.parse.quote(username)}:{urllib.parse.quote(password)}@"
|
||||
)
|
||||
user = urllib.parse.quote(username, safe="")
|
||||
token = urllib.parse.quote(password, safe="")
|
||||
url = url.replace("://", f"://{user}:{token}@")
|
||||
|
||||
hash_dir_name = _compute_destination_path(key, domain).name
|
||||
repo_dir = _repo_entry_dir(key, domain, subpath)
|
||||
@@ -1051,8 +1051,8 @@ class GitFile:
|
||||
"""Parse a git shorthand URL into its components."""
|
||||
if not isinstance(shorthand, str):
|
||||
raise ValueError("Git shorthand must be a string")
|
||||
m = re.match(
|
||||
r"(?P<domain>[a-zA-Z0-9\-]+)://(?P<owner>[a-zA-Z0-9\-]+)/(?P<repo>[a-zA-Z0-9\-\._]+)/(?P<filename>[a-zA-Z0-9\-_.\./]+)(?:@(?P<ref>[a-zA-Z0-9\-_.\./]+))?(?:\?(?P<query>[a-zA-Z0-9\-_.\./]+))?",
|
||||
m = re.fullmatch(
|
||||
rf"(?P<domain>[a-zA-Z0-9\-]+)://(?P<owner>[a-zA-Z0-9\-]+)/(?P<repo>[a-zA-Z0-9\-\._]+)/(?P<filename>[a-zA-Z0-9\-_.\./]+)(?:@(?P<ref>{cv.GIT_REF_PATTERN}))?(?:\?(?P<query>[a-zA-Z0-9\-_.\./]+))?",
|
||||
shorthand,
|
||||
)
|
||||
if m is None:
|
||||
|
||||
@@ -2,7 +2,7 @@ dependencies:
|
||||
bblanchon/arduinojson:
|
||||
version: "7.4.3"
|
||||
esphome/dlms_parser:
|
||||
version: 2.2.0
|
||||
version: 2.2.2
|
||||
esphome/esp-audio-libs:
|
||||
version: 4.1.0
|
||||
esphome/esp-micro-speech-features:
|
||||
|
||||
+7
-2
@@ -284,12 +284,17 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None:
|
||||
_COMPONENT_CACHE[domain] = manif
|
||||
return manif
|
||||
|
||||
module_name = f"esphome.components.{domain}"
|
||||
try:
|
||||
module = importlib.import_module(f"esphome.components.{domain}")
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as e:
|
||||
if exception:
|
||||
raise
|
||||
if "No module named" in str(e):
|
||||
if (
|
||||
isinstance(e, ModuleNotFoundError)
|
||||
and e.name is not None
|
||||
and f"{module_name}.".startswith(f"{e.name}.")
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Unable to import component %s: %s", domain, str(e), exc_info=False
|
||||
)
|
||||
|
||||
@@ -26,17 +26,20 @@ from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_RTL87XX,
|
||||
Platform,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Hooks whose loss only degrades diagnostics; skipping one of these is
|
||||
# logged at debug, while skipping a hook that changes what the CLI does
|
||||
# (upload method, log transport) warns. A new hook is loud by default.
|
||||
COSMETIC_HOOKS: Final = frozenset({"process_stacktrace"})
|
||||
# Hooks whose loss only degrades diagnostics or upload size; skipping one
|
||||
# of these is logged at debug, while skipping a hook that changes what the
|
||||
# CLI does (upload method, log transport) warns. A new hook is loud by
|
||||
# default.
|
||||
COSMETIC_HOOKS: Final = frozenset({"process_stacktrace", "ota_image"})
|
||||
|
||||
# Platforms whose firmware prints the crash handler report share one gate.
|
||||
_CRASH_HANDLER_GATE = r"0x[0-9a-fA-F]{3,}\b|CRASH DETECTED ON PREVIOUS BOOT"
|
||||
@@ -71,6 +74,8 @@ PLATFORM_HOOKS: Final[dict[str, frozenset[str]]] = {
|
||||
"show_logs": frozenset({PLATFORM_NRF52}),
|
||||
"upload_program": frozenset({PLATFORM_NRF52}),
|
||||
"process_stacktrace": frozenset(STACKTRACE_GATES),
|
||||
# The file an app update sends, when it is not the build output itself
|
||||
"ota_image": frozenset({PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+218
-17
@@ -9,7 +9,8 @@ from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome import compiled_config, yaml_util
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -20,6 +21,10 @@ PLAIN_SCALAR = r"[^\s#\"'>|&*!%@`\[\]{},]+"
|
||||
TRAILER = r"(?P<quote>[\"']?){value}(?P=quote)(?P<trail>(?:\s+#.*)?\s*)$"
|
||||
|
||||
|
||||
class RestoreError(EsphomeError):
|
||||
"""A rollback left at least one file as it was written."""
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
"""The file as written, line endings included; read_file would fold them."""
|
||||
try:
|
||||
@@ -30,12 +35,25 @@ def read_text(path: Path) -> str:
|
||||
|
||||
@dataclass
|
||||
class LineEdit:
|
||||
"""One line to rewrite; ``old_line`` is what it held when located."""
|
||||
"""One line to rewrite, or with ``insert_after`` a line to add right
|
||||
after it; ``old_line`` is what the line held when it was located."""
|
||||
|
||||
path: Path
|
||||
line: int
|
||||
old_line: str
|
||||
new_line: str
|
||||
insert_after: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Snapshot:
|
||||
"""A rewritten file's text before and after, and its mode before; the
|
||||
restore only puts ``original`` back over ``written``, never over the
|
||||
user's own edit."""
|
||||
|
||||
original: str
|
||||
written: str
|
||||
mode: int | None = None
|
||||
|
||||
|
||||
def field_line_re(
|
||||
@@ -63,17 +81,66 @@ def line_at(doc: Path, line_no: int) -> str:
|
||||
return lines[line_no]
|
||||
|
||||
|
||||
_DOMAIN = "yaml_edit"
|
||||
|
||||
|
||||
def own_documents() -> set[Path]:
|
||||
"""Every file the loader read for this configuration, found once per run.
|
||||
A file behind an include path with a substitution that gave nothing but
|
||||
substitutions is the one kind this misses."""
|
||||
if (documents := CORE.data.get(_DOMAIN)) is None:
|
||||
documents = CORE.data[_DOMAIN] = _own_documents()
|
||||
return documents
|
||||
|
||||
|
||||
def _own_documents() -> set[Path]:
|
||||
documents = {str(CORE.config_path)}
|
||||
|
||||
def walk(node: object) -> None:
|
||||
if (rng := getattr(node, "esp_range", None)) is not None:
|
||||
documents.add(rng.start_mark.document)
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
walk(key)
|
||||
walk(value)
|
||||
elif isinstance(node, list):
|
||||
for value in node:
|
||||
walk(value)
|
||||
|
||||
walk(CORE.raw_config or {})
|
||||
# A file that only gave substitutions leaves no node behind; the tracker
|
||||
# also lists a secrets file the loader only tried, hence the is_file check
|
||||
found = yaml_util.discover_user_yaml_files(CORE.config_path, clear_secrets=False)
|
||||
if found.load_errors: # an incomplete set would misplace a key
|
||||
raise EsphomeError(
|
||||
"Could not read every file of the configuration: "
|
||||
+ "; ".join(found.load_errors)
|
||||
)
|
||||
documents.update(map(str, found.files))
|
||||
return {p.resolve() for d in documents if (p := Path(d)).is_file()}
|
||||
|
||||
|
||||
def source_of(mapping: ConfigType, name: str) -> tuple[Path, int] | None:
|
||||
"""The file and line ``name:`` was read from, None when validation added
|
||||
it or a merge key brought it in from an anchor elsewhere. Mapping keys
|
||||
keep their range through validation, values may not."""
|
||||
"""The file and line ``name:`` was read from; None when validation added
|
||||
it or a merge key brought it in from an anchor elsewhere."""
|
||||
rng = getattr(next((k for k in mapping if k == name), None), "esp_range", None)
|
||||
if rng is None:
|
||||
return None
|
||||
# An included mapping carries the `!include` line of its parent, so only
|
||||
# a key in the same document can be placed against the mapping
|
||||
if (own := getattr(mapping, "esp_range", None)) is None:
|
||||
return None # a mapping built in code, its keys are not on its lines
|
||||
# merge_config keeps the first mapping's keys with the last one's
|
||||
# values, so only the value's range names the line that won, and
|
||||
# only when the key is on it (a block value starts on a line of its own)
|
||||
rng = getattr(mapping.get(name), "esp_range", None)
|
||||
if rng is None:
|
||||
return None
|
||||
doc, line = Path(rng.start_mark.document), rng.start_mark.line
|
||||
if doc.resolve() not in own_documents() or not field_line_re(name).match(
|
||||
line_at(doc, line)
|
||||
):
|
||||
return None
|
||||
return doc, line
|
||||
# An included mapping carries its parent's `!include` line, so only a
|
||||
# key in the same document can be checked against the mapping's lines
|
||||
if (
|
||||
rng.start_mark.document == own.start_mark.document
|
||||
and not own.start_mark.line <= rng.start_mark.line <= own.end_mark.line
|
||||
@@ -82,13 +149,46 @@ def source_of(mapping: ConfigType, name: str) -> tuple[Path, int] | None:
|
||||
return Path(rng.start_mark.document), rng.start_mark.line
|
||||
|
||||
|
||||
def write_keeping_mode(path: Path, text: str, like: Path | None = None) -> None:
|
||||
"""Write with the mode of ``like`` (default: the file itself) rather
|
||||
than write_file's 0644; a 0600 secrets file stays 0600."""
|
||||
def editable_file(path: Path) -> Path:
|
||||
"""``path`` resolved, so a symlink's target is edited; refuses a file
|
||||
outside the config dir or under the build data."""
|
||||
resolved = path.resolve()
|
||||
if (
|
||||
not path.is_file()
|
||||
or not resolved.is_relative_to(CORE.config_dir.resolve())
|
||||
or resolved.is_relative_to(CORE.data_dir.resolve())
|
||||
):
|
||||
raise EsphomeError(
|
||||
f"{path} is not an editable file in the configuration directory"
|
||||
)
|
||||
return resolved
|
||||
|
||||
|
||||
def source_line(mapping: ConfigType, name: str) -> tuple[Path, int, str]:
|
||||
"""The file, line number and text ``name:`` was read from."""
|
||||
if (source := source_of(mapping, name)) is None:
|
||||
raise EsphomeError(f"'{name}' was not read from a file")
|
||||
doc, line_no = source
|
||||
# Returned as the loader saw it: a `!secret` in a symlinked include
|
||||
# resolves beside the link
|
||||
editable_file(doc)
|
||||
return doc, line_no, line_at(doc, line_no)
|
||||
|
||||
|
||||
def mode_of(path: Path) -> int:
|
||||
try:
|
||||
mode = stat.S_IMODE((like or path).stat().st_mode)
|
||||
return stat.S_IMODE(path.stat().st_mode)
|
||||
except OSError as err:
|
||||
raise EsphomeError(f"Could not read the mode of {like or path}: {err}") from err
|
||||
raise EsphomeError(f"Could not read the mode of {path}: {err}") from err
|
||||
|
||||
|
||||
def write_keeping_mode(
|
||||
path: Path, text: str, like: Path | None = None, mode: int | None = None
|
||||
) -> None:
|
||||
"""Write with ``mode``, else the mode of ``like`` (default: the file
|
||||
itself), rather than write_file's 0644; a 0600 secrets file stays 0600."""
|
||||
if mode is None:
|
||||
mode = mode_of(like or path)
|
||||
try:
|
||||
write_file(path, text, private=True)
|
||||
except EsphomeError as err:
|
||||
@@ -103,12 +203,113 @@ def write_keeping_mode(path: Path, text: str, like: Path | None = None) -> None:
|
||||
|
||||
|
||||
def rewritten_text(original: str, edits: list[LineEdit]) -> str:
|
||||
"""``original`` with the edits applied; every edit must still find the
|
||||
line it was located on."""
|
||||
"""``original`` with the edits applied; a line that changed is refused."""
|
||||
lines = original.splitlines(keepends=True)
|
||||
for edit in edits:
|
||||
file_newline = "\r\n" if "\r\n" in original else "\n"
|
||||
# Bottom up, so an insertion never shifts a later edit; insertions under
|
||||
# one line keep their order
|
||||
order = sorted(
|
||||
enumerate(edits), key=lambda ie: (ie[1].line, ie[1].insert_after, ie[0])
|
||||
)
|
||||
for _, edit in reversed(order):
|
||||
text = lines[edit.line].rstrip("\r\n") if edit.line < len(lines) else None
|
||||
if text != edit.old_line:
|
||||
raise EsphomeError(f"{edit.path}:{edit.line + 1} changed since it was read")
|
||||
lines[edit.line] = edit.new_line + lines[edit.line][len(text) :]
|
||||
ending = lines[edit.line][len(text) :]
|
||||
if edit.insert_after:
|
||||
# A last line without a newline gets the file's own kind
|
||||
lines[edit.line] = text + (ending or file_newline)
|
||||
lines.insert(edit.line + 1, edit.new_line + ending)
|
||||
else:
|
||||
lines[edit.line] = edit.new_line + ending
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def apply_line_edits(edits: list[LineEdit]) -> dict[Path, Snapshot]:
|
||||
"""Rewrite the located lines in place; returns each file's text before
|
||||
and after for restore_files. Nothing is written while any line is stale,
|
||||
and a file that no longer loads is put back here."""
|
||||
by_path: dict[Path, list[LineEdit]] = {}
|
||||
seen_as: dict[Path, Path] = {} # the path the loader read a file by
|
||||
for edit in edits:
|
||||
resolved = editable_file(edit.path)
|
||||
by_path.setdefault(resolved, []).append(edit)
|
||||
seen_as.setdefault(resolved, edit.path)
|
||||
originals = {}
|
||||
for path, own in by_path.items():
|
||||
original = read_text(path)
|
||||
originals[path] = Snapshot(
|
||||
original, rewritten_text(original, own), mode_of(path)
|
||||
)
|
||||
try:
|
||||
for path, snapshot in originals.items():
|
||||
write_keeping_mode(path, snapshot.written)
|
||||
# Reloaded as the loader saw them: a relative !include inside a
|
||||
# symlinked file resolves beside the link
|
||||
reloads = [seen_as[path] for path in originals]
|
||||
if CORE.config_path.resolve() not in originals:
|
||||
# Its !secret tags register a value written elsewhere
|
||||
reloads.append(CORE.config_path)
|
||||
for path in reloads:
|
||||
try:
|
||||
# The secret registry stays: later output still redacts
|
||||
yaml_util.load_yaml(
|
||||
path, clear_secrets=False, track_document_range=False
|
||||
)
|
||||
except EsphomeError as err:
|
||||
raise EsphomeError(f"{path} no longer loads: {err}") from err
|
||||
compiled_config.invalidate_compiled_config()
|
||||
except BaseException as err:
|
||||
try:
|
||||
restore_files(originals)
|
||||
except RestoreError as restore_err:
|
||||
if not isinstance(err, Exception):
|
||||
err.add_note(str(restore_err)) # an interrupt stays one
|
||||
raise err from None
|
||||
raise RestoreError(f"{err}; {restore_err}") from err
|
||||
raise
|
||||
return originals
|
||||
|
||||
|
||||
def restore_files(originals: dict[Path, Snapshot]) -> None:
|
||||
"""Put back what apply_line_edits wrote, mode included; a file that
|
||||
failed or changed meanwhile is left alone and reported. Every file is
|
||||
tried whatever went wrong before; an interrupt or exit is raised again
|
||||
at the end with the report as a note."""
|
||||
failed: list[str] = []
|
||||
stop: BaseException | None = None
|
||||
|
||||
def report(what: str, err: BaseException) -> None:
|
||||
nonlocal stop
|
||||
if isinstance(err, Exception):
|
||||
why = str(err) or type(err).__name__
|
||||
else: # an interrupt or exit says what it is, not its code
|
||||
why = (
|
||||
"interrupted"
|
||||
if isinstance(err, KeyboardInterrupt)
|
||||
else type(err).__name__
|
||||
)
|
||||
stop = err
|
||||
failed.append(f"{what}: {why}")
|
||||
|
||||
for path, snapshot in originals.items():
|
||||
try:
|
||||
current = read_text(path)
|
||||
if current == snapshot.original:
|
||||
continue # never written, or already put back
|
||||
if current != snapshot.written:
|
||||
raise EsphomeError("changed since it was written, left as is")
|
||||
write_keeping_mode(path, snapshot.original, mode=snapshot.mode)
|
||||
except BaseException as err: # noqa: BLE001 # pylint: disable=broad-except
|
||||
report(str(path), err)
|
||||
try:
|
||||
compiled_config.invalidate_compiled_config()
|
||||
except BaseException as err: # noqa: BLE001 # pylint: disable=broad-except
|
||||
report("the validated caches", err)
|
||||
if not failed:
|
||||
return
|
||||
message = "Could not restore " + "; ".join(failed)
|
||||
if stop is not None:
|
||||
stop.add_note(message)
|
||||
raise stop
|
||||
raise RestoreError(message)
|
||||
+15
-4
@@ -656,9 +656,13 @@ def named_path(path: Path) -> Path:
|
||||
return path.parent.resolve() / path.name
|
||||
|
||||
|
||||
def discover_user_yaml_files(config_path: Path) -> DiscoveredYamlFiles:
|
||||
def discover_user_yaml_files(
|
||||
config_path: Path, clear_secrets: bool = True
|
||||
) -> DiscoveredYamlFiles:
|
||||
"""Fresh-re-parse ``config_path`` and report every file the YAML loader
|
||||
pulled in, plus which of them came in under a secrets filename.
|
||||
``clear_secrets`` is passed to the load; False keeps the redaction
|
||||
registry of a configuration loaded earlier in the run.
|
||||
|
||||
Does NOT run schema validation, substitutions, or package resolution — so
|
||||
component-internal YAML loaded by validators (LVGL helpers, dashboard
|
||||
@@ -702,7 +706,7 @@ def discover_user_yaml_files(config_path: Path) -> DiscoveredYamlFiles:
|
||||
load_listeners.append(_on_load)
|
||||
try:
|
||||
try:
|
||||
data = load_yaml(config_path)
|
||||
data = load_yaml(config_path, clear_secrets=clear_secrets)
|
||||
except EsphomeError as err:
|
||||
load_errors = [f"{config_path}: {err}"]
|
||||
else:
|
||||
@@ -1032,8 +1036,15 @@ class ESPHomeLoaderMixin:
|
||||
) -> OrderedDict[str, dict[str, Any]]:
|
||||
files = filter_yaml_files(find_files(self._rel_path(node.value), "*.yaml"))
|
||||
mapping = OrderedDict()
|
||||
paths: dict[str, Path] = {}
|
||||
for fname in files:
|
||||
filename = fname.stem
|
||||
if (first := paths.setdefault(filename, fname)) != fname:
|
||||
raise yaml.MarkedYAMLError(
|
||||
f"!include_dir_named: {first} and {fname} both use the key "
|
||||
f"'{filename}'; rename one of them",
|
||||
node.start_mark,
|
||||
)
|
||||
mapping[filename] = self.yaml_loader(fname)
|
||||
return mapping
|
||||
|
||||
@@ -1345,8 +1356,8 @@ def _is_file_valid(name: str) -> bool:
|
||||
def find_files(directory: Path, pattern: str) -> Iterator[Path]:
|
||||
"""Recursively find files in a directory matching *pattern*, skipping hidden entries."""
|
||||
for root, dirs, files in os.walk(directory):
|
||||
dirs[:] = [d for d in dirs if _is_file_valid(d)]
|
||||
for f in files:
|
||||
dirs[:] = sorted(d for d in dirs if _is_file_valid(d))
|
||||
for f in sorted(files):
|
||||
filename = Path(f)
|
||||
if _is_file_valid(f) and filename.match(pattern):
|
||||
filename = Path(root) / filename
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ build_unflags =
|
||||
; esp32 envs (which would double-include them).
|
||||
[common:idf-component-libs]
|
||||
lib_deps =
|
||||
esphome/dlms_parser@2.2.0 ; dlms_meter
|
||||
esphome/dlms_parser@2.2.2 ; dlms_meter
|
||||
bblanchon/ArduinoJson@7.4.3 ; json
|
||||
lvgl/lvgl@9.6.0 ; lvgl
|
||||
|
||||
|
||||
+4
-4
@@ -1,18 +1,18 @@
|
||||
# cryptography 49+ ships no Intel macOS wheels (arm64 only); esptool caps <49 there.
|
||||
# Keep 48.0.1, the last universal2 release, so esphome stays installable on Intel Macs.
|
||||
cryptography==50.0.2; platform_system != "Darwin" or platform_machine != "x86_64"
|
||||
cryptography==48.0.1; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
cryptography==50.0.2; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
voluptuous==0.16.0
|
||||
PyYAML==6.0.3
|
||||
paho-mqtt==2.1.0
|
||||
colorama==0.4.6
|
||||
tzlocal==5.4.4 # from time
|
||||
tzlocal==5.4.5 # from time
|
||||
tzdata>=2026.5 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.2.0
|
||||
esptool==5.6.0
|
||||
click==8.5.0
|
||||
aioesphomeapi==46.22.0
|
||||
aioesphomeapi==46.23.0
|
||||
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
|
||||
zeroconf==0.151.5
|
||||
puremagic==2.2.0
|
||||
@@ -28,7 +28,7 @@ bleak==3.0.2
|
||||
smpclient==7.3.0
|
||||
# smp 4.2+ needs cbor2 6+, which ships no Intel macOS wheels (arm64 only).
|
||||
smp==4.2.0; platform_system != "Darwin" or platform_machine != "x86_64"
|
||||
smp==4.1.0; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
smp==4.2.0; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
requests==2.34.2
|
||||
py7zr==1.1.4
|
||||
platformdirs==4.13.5 # native esp-idf toolchain global cache dir
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pylint==4.1.3
|
||||
ruff==0.17.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
ruff==0.17.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
pyupgrade==3.22.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
prek==0.5.6 # .github/workflows/ci.yml reads this pin
|
||||
prek==0.5.7 # .github/workflows/ci.yml reads this pin
|
||||
yamlrocks==0.7.1 # used by script/sync_dependency_versions.py
|
||||
|
||||
# Unit tests
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Tests for the show_test_card and update_interval check in the display schema."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.display import FULL_DISPLAY_SCHEMA, _validate_test_card
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
WARNING = "`show_test_card: True` is used with `update_interval: never`"
|
||||
|
||||
|
||||
def test_test_card_with_update_interval_never_warns(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""The test card is only drawn when something else updates the display."""
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.components.display"):
|
||||
FULL_DISPLAY_SCHEMA({"show_test_card": True, "update_interval": "never"})
|
||||
assert WARNING in caplog.text
|
||||
|
||||
|
||||
def test_warning_names_the_display(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""With several displays the warning says which one it is about."""
|
||||
config = {
|
||||
"id": ID("status_epaper", is_declaration=True),
|
||||
"show_test_card": True,
|
||||
"update_interval": cv.update_interval("never"),
|
||||
}
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.components.display"):
|
||||
_validate_test_card(config)
|
||||
assert f"status_epaper: {WARNING}" in caplog.text
|
||||
|
||||
|
||||
def test_warning_without_display_id_has_no_prefix(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A generated id is unset at validation time, so the warning starts with the message."""
|
||||
config = {
|
||||
"id": ID(None, is_declaration=True),
|
||||
"show_test_card": True,
|
||||
"update_interval": cv.update_interval("never"),
|
||||
}
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.components.display"):
|
||||
_validate_test_card(config)
|
||||
assert WARNING in caplog.text
|
||||
assert f": {WARNING}" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
pytest.param(
|
||||
{"show_test_card": True, "update_interval": "1s"}, id="test_card_1s"
|
||||
),
|
||||
pytest.param({"show_test_card": True}, id="test_card_default_interval"),
|
||||
pytest.param(
|
||||
{"show_test_card": False, "update_interval": "never"},
|
||||
id="no_test_card_never",
|
||||
),
|
||||
pytest.param({"update_interval": "never"}, id="never_only"),
|
||||
],
|
||||
)
|
||||
def test_other_combinations_do_not_warn(
|
||||
config: ConfigType, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Only the test card together with `update_interval: never` warns."""
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.components.display"):
|
||||
FULL_DISPLAY_SCHEMA(config)
|
||||
assert WARNING not in caplog.text
|
||||
|
||||
|
||||
def test_test_card_without_update_interval_accepted(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Platforms such as epaper_spi may leave update_interval unset at this stage."""
|
||||
config = {"show_test_card": True}
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.components.display"):
|
||||
assert _validate_test_card(config) is config
|
||||
assert WARNING not in caplog.text
|
||||
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user