mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 00:12:11 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f01ab32fa | ||
|
|
3ca116aff5 | ||
|
|
f29745408a | ||
|
|
ea28b66e8a | ||
|
|
cd06a02ff8 | ||
|
|
fcb52d286c | ||
|
|
cb943c4492 |
No files matched your search
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -462,7 +462,6 @@ 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.
|
||||
@@ -478,32 +477,6 @@ 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.
|
||||
@@ -657,7 +630,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0
|
||||
uses: CodSpeedHQ/action@821a40cd6c80e3cfe387caf55668287addd808da # v5.5.0
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -1484,7 +1457,7 @@ jobs:
|
||||
ref: main
|
||||
path: device-builder
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Set up uv
|
||||
@@ -1493,7 +1466,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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
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@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
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@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
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@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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.2
|
||||
rev: v0.17.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -25,7 +25,6 @@ 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__)
|
||||
|
||||
@@ -155,47 +154,6 @@ 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."""
|
||||
@@ -336,24 +294,18 @@ 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.
|
||||
# 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())))
|
||||
)
|
||||
)
|
||||
project_compile_opts = get_project_compile_flags()
|
||||
extra_compile_options = "\n".join(
|
||||
f"idf_build_set_property(COMPILE_OPTIONS {flag} APPEND)"
|
||||
for flag in _cmake_args(project_compile_opts)
|
||||
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
||||
for flag in 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 _cmake_args(get_project_cxx_compile_flags())
|
||||
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
|
||||
for flag in get_project_cxx_compile_flags()
|
||||
)
|
||||
|
||||
cpp_standard_options = (
|
||||
@@ -509,7 +461,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 = _cmake_args(get_project_link_flags(_lexed_build_flags()))
|
||||
link_opts = get_project_link_flags()
|
||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||
src_cmake = "\n".join(CORE.data[KEY_ESP32].get(KEY_SRC_CMAKE, []))
|
||||
|
||||
|
||||
@@ -253,9 +253,8 @@ 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.
|
||||
// By reference: array args are non-copyable FixedVector views.
|
||||
auto args = std::forward_as_tuple(x...);
|
||||
// Extract call_id from first argument - it's always first for optional/only/status modes
|
||||
auto args = std::make_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
|
||||
bool success = this->success_.value(x...);
|
||||
@@ -309,7 +308,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::forward_as_tuple(x...);
|
||||
auto args = std::make_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
if (call_id != 0) {
|
||||
this->parent_->unregister_active_action_call(call_id);
|
||||
|
||||
@@ -45,8 +45,9 @@ void CopyCover::control(const cover::CoverCall &call) {
|
||||
auto position = call.get_position();
|
||||
if (position.has_value())
|
||||
call2.set_position(*position);
|
||||
if (call.get_toggle().has_value())
|
||||
call2.set_command_toggle();
|
||||
auto tilt2 = call.get_tilt();
|
||||
if (tilt2.has_value())
|
||||
call2.set_tilt(*tilt2);
|
||||
call2.perform();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,4 @@ void CopyLock::control(const lock::LockCall &call) {
|
||||
call2.perform();
|
||||
}
|
||||
|
||||
void CopyLock::open_latch() { this->source_->open(); }
|
||||
|
||||
} // namespace esphome::copy
|
||||
@@ -13,7 +13,6 @@ class CopyLock final : public lock::Lock, public Component {
|
||||
|
||||
protected:
|
||||
void control(const lock::LockCall &call) override;
|
||||
void open_latch() override;
|
||||
|
||||
lock::Lock *source_;
|
||||
};
|
||||
|
||||
@@ -57,7 +57,6 @@ 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();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from esphome import automation, core, pins
|
||||
@@ -35,8 +34,6 @@ 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)
|
||||
@@ -105,21 +102,13 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _validate_test_card(config: ConfigType) -> ConfigType:
|
||||
def _validate_test_card(config):
|
||||
if (
|
||||
config.get(CONF_SHOW_TEST_CARD, False)
|
||||
and (update_interval := config.get(CONF_UPDATE_INTERVAL)) is not None
|
||||
and update_interval.total_milliseconds == SCHEDULER_DONT_RUN
|
||||
and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN
|
||||
):
|
||||
# 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,
|
||||
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."
|
||||
)
|
||||
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.2"
|
||||
DLMS_PARSER_VERSION = "2.2.0"
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
|
||||
@@ -2389,9 +2389,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Optional(CONF_SOURCE): cv.All(
|
||||
cv.string_strict, cv.Length(min=1)
|
||||
),
|
||||
cv.Optional(CONF_SOURCE): cv.git_ref,
|
||||
cv.Optional(CONF_REF): cv.string,
|
||||
cv.Optional(CONF_PATH): cv.string,
|
||||
}
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
#include "hal/rtc_io_hal.h"
|
||||
#endif
|
||||
|
||||
// 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
|
||||
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
|
||||
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
@@ -58,12 +53,10 @@ struct ISRPinArg {
|
||||
#ifdef USE_GPIO_HOLD
|
||||
bool hold;
|
||||
#endif
|
||||
#ifdef ISR_PIN_USE_RTC
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
bool use_rtc;
|
||||
#ifdef RTCIO_NEED_MAPPING
|
||||
int rtc_pin;
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_GPIO_HOLD
|
||||
@@ -76,15 +69,17 @@ 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(ISR_PIN_USE_RTC) && defined(SOC_RTCIO_HOLD_SUPPORTED)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
if (arg->use_rtc) {
|
||||
#ifdef RTCIO_NEED_MAPPING
|
||||
rtcio_hal_hold_disable(arg->rtc_pin);
|
||||
rtcio_hal_hold_enable(arg->rtc_pin);
|
||||
#else
|
||||
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) {
|
||||
rtcio_hal_hold_disable(arg->pin);
|
||||
rtcio_hal_hold_enable(arg->pin);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -101,12 +96,10 @@ ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
|
||||
#ifdef USE_GPIO_HOLD
|
||||
arg->hold = this->get_hold();
|
||||
#endif
|
||||
#ifdef ISR_PIN_USE_RTC
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
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);
|
||||
}
|
||||
@@ -258,8 +251,7 @@ 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);
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
// Only ESP32 uses the RTC registers for pullup/pulldown
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
if (arg->use_rtc) {
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
rtcio_hal_pullup_enable(arg->rtc_pin);
|
||||
|
||||
@@ -138,11 +138,6 @@ 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;
|
||||
@@ -212,10 +207,13 @@ 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
|
||||
@@ -246,9 +244,7 @@ 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
|
||||
// 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);
|
||||
this->set_operating_mode(OP_NORMAL_MODE);
|
||||
}
|
||||
|
||||
void LD2420Component::factory_reset_action() {
|
||||
@@ -360,22 +356,6 @@ 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,7 +119,6 @@ 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,5 +30,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
CONF_OPERATING_MODE,
|
||||
hub.set_operating_mode_select,
|
||||
options=CONF_SELECTS,
|
||||
apply="change_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
|
||||
apply="set_operating_mode(static_cast<ld2420::OpMode>(index + 1))",
|
||||
publish="before",
|
||||
)
|
||||
@@ -45,6 +45,7 @@ from .const import (
|
||||
FAMILY_COMPONENT,
|
||||
FAMILY_FRIENDLY,
|
||||
FAMILY_RTL8710B,
|
||||
FAMILY_RTL8720C,
|
||||
KEY_BOARD,
|
||||
KEY_COMPONENT,
|
||||
KEY_COMPONENT_DATA,
|
||||
@@ -59,6 +60,8 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Fault handlers the crash handler wraps (BK7238, BK7231N)
|
||||
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
|
||||
# Families with a crash handler; the RTL8720C installs it at run time (no wraps).
|
||||
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_RTL8720C)
|
||||
|
||||
CODEOWNERS = ["@kuba2k2"]
|
||||
AUTO_LOAD = ["preferences"]
|
||||
@@ -554,10 +557,11 @@ async def component_to_code(config):
|
||||
# RAM-executable output section and prints a post-link placement summary.
|
||||
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
|
||||
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
|
||||
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
|
||||
# Crash handler (crash_handler.cpp, patch_noinit.py.script)
|
||||
if config[CONF_FAMILY] in CRASH_HANDLER_FAMILIES:
|
||||
# Crash handler (crash_handler.cpp)
|
||||
cg.add_define("USE_LIBRETINY_CRASH_HANDLER")
|
||||
cg.add_define("USE_CRASH_HANDLER")
|
||||
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
|
||||
for trap in BK_TRAP_HANDLERS:
|
||||
cg.add_build_flag(f"-Wl,--wrap={trap}")
|
||||
cg.add_platformio_option("extra_scripts", ["pre:patch_noinit.py"])
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#ifdef USE_BK72XX
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER
|
||||
|
||||
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_VARIANT_RTL8720C)
|
||||
#error "The LibreTiny crash handler has no fault capture for this chip family"
|
||||
#endif
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "core.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -13,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
extern "C" {
|
||||
#include <arm_mcu_pub.h> // MCU_REG_BACKUP_SP_*
|
||||
#include <intc.h> // struct arm_registers
|
||||
@@ -39,28 +44,138 @@ extern char _itcmcode_ram_begin[];
|
||||
extern char _itcmcode_ram_end[];
|
||||
}
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_BK72XX
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
// Bounds of the code in flash (XIP) and in RAM (linker symbols, rtl8710c_ram.ld).
|
||||
extern char __xip_code_text_start__[];
|
||||
extern char __xip_code_text_end__[];
|
||||
extern char __ram_entry_text_start__[];
|
||||
extern char __ram_code_text_end__[];
|
||||
}
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8720C
|
||||
|
||||
namespace esphome::libretiny {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "libretiny.crash");
|
||||
|
||||
// 0xDEAD sentinel, then the record layout version.
|
||||
static constexpr uint32_t CRASH_MAGIC_V1 = 0xDEAD0001;
|
||||
static constexpr uint32_t CRASH_MAGIC_V2 = 0xDEAD0002;
|
||||
|
||||
// BK72xx: the ARM9 exception taken.
|
||||
static constexpr uint32_t CRASH_REASON_UNDEFINED = 1;
|
||||
static constexpr uint32_t CRASH_REASON_PREFETCH_ABORT = 2;
|
||||
static constexpr uint32_t CRASH_REASON_DATA_ABORT = 3;
|
||||
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort");
|
||||
// RTL8720C: the Cortex-M exception number (3 to 7) plus this offset.
|
||||
static constexpr uint32_t CRASH_REASON_CORTEX_M_OFFSET = 1;
|
||||
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort",
|
||||
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault");
|
||||
|
||||
static constexpr size_t MAX_BACKTRACE = 8;
|
||||
// Words scanned above sp for return addresses.
|
||||
static constexpr size_t STACK_SCAN_WORDS = 64;
|
||||
|
||||
struct CrashRecord {
|
||||
uint32_t magic;
|
||||
uint32_t reason;
|
||||
uint32_t pc;
|
||||
uint32_t lr;
|
||||
uint32_t sp;
|
||||
uint32_t fault_status; // Cortex-M CFSR, 0 on BK72xx
|
||||
uint32_t fault_address; // Cortex-M BFAR or MMFAR when valid, else 0
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
};
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
// Kept in ITCM, see patch_noinit.py.script.
|
||||
#define CRASH_RECORD_SECTION ".noinit"
|
||||
#else
|
||||
// rtl8710c_ram.ld keeps .ram.bss.noinit* out of the startup zeroing.
|
||||
#define CRASH_RECORD_SECTION ".ram.bss.noinit.esphome_crash"
|
||||
#endif
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
__attribute__((section(CRASH_RECORD_SECTION))) static CrashRecord s_record;
|
||||
|
||||
static bool s_record_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_record_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool crash_handler_has_data() {
|
||||
if (!s_record_read) {
|
||||
s_record_read = true;
|
||||
s_record_valid = s_record.magic == CRASH_MAGIC_V2;
|
||||
// Clear the magic so the record is reported once.
|
||||
s_record.magic = 0;
|
||||
}
|
||||
return s_record_valid;
|
||||
}
|
||||
|
||||
// One ESP_LOGE per line: the CLI decodes per line and partial output still shows.
|
||||
void crash_handler_log() {
|
||||
if (!crash_handler_has_data())
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(CrashReasonStrings::get_log_str(s_record.reason, 0)));
|
||||
if (s_record.fault_status != 0) {
|
||||
ESP_LOGE(TAG, " CFSR: 0x%08" PRIX32 " Address: 0x%08" PRIX32, s_record.fault_status, s_record.fault_address);
|
||||
}
|
||||
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_record.pc);
|
||||
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_record.lr);
|
||||
ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_record.sp);
|
||||
uint8_t count = 0;
|
||||
for (; count < MAX_BACKTRACE && s_record.backtrace[count] != 0; count++) {
|
||||
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", count, s_record.backtrace[count]);
|
||||
}
|
||||
// addr2line hint with every address
|
||||
char hint[160];
|
||||
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e raw_firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
|
||||
s_record.pc, s_record.lr);
|
||||
for (uint8_t i = 0; i < count && pos < (int) sizeof(hint) - 12; i++) {
|
||||
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_record.backtrace[i]);
|
||||
}
|
||||
ESP_LOGE(TAG, "%s", hint);
|
||||
}
|
||||
|
||||
// --- Crash time: exception mode, a few dozen bytes of stack: no printing, no large locals.
|
||||
|
||||
static inline bool is_code_addr(uint32_t val) {
|
||||
uint32_t addr = val & ~1u; // clear the Thumb bit of a return address
|
||||
#ifdef USE_BK72XX
|
||||
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
|
||||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
|
||||
#else
|
||||
return (addr >= (uint32_t) __xip_code_text_start__ && addr < (uint32_t) __xip_code_text_end__) ||
|
||||
(addr >= (uint32_t) __ram_entry_text_start__ && addr < (uint32_t) __ram_code_text_end__);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Return addresses above sp give the backtrace; then the magic marks the record complete.
|
||||
static void finish_record(uint32_t sp, uint32_t sram_start, uint32_t sram_end) {
|
||||
uint32_t count = 0;
|
||||
if (sp >= sram_start && sp < sram_end && (sp & 3) == 0) {
|
||||
auto *scan = (const uint32_t *) sp; // NOLINT(performance-no-int-to-ptr)
|
||||
auto *end = (const uint32_t *) sram_end; // NOLINT(performance-no-int-to-ptr)
|
||||
for (size_t i = 0; i < STACK_SCAN_WORDS && scan < end && count < MAX_BACKTRACE; i++, scan++) {
|
||||
uint32_t val = *scan;
|
||||
if (is_code_addr(val) && val != s_record.lr && (val & ~1u) != s_record.pc)
|
||||
s_record.backtrace[count++] = val;
|
||||
}
|
||||
}
|
||||
if (count < MAX_BACKTRACE)
|
||||
s_record.backtrace[count] = 0; // terminator, the reader stops at the first 0
|
||||
s_record.magic = CRASH_MAGIC_V2;
|
||||
}
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
|
||||
// With Wi-Fi up the boot-time watchdog no longer fires: any interrupts-off spin hangs until power is
|
||||
// cycled (BK7238 and BK7231N; cause not in the open SDK). Re-armed here, long enough for the SDK's
|
||||
// register dump when sdk_silent is off.
|
||||
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 500;
|
||||
|
||||
static constexpr size_t MAX_BACKTRACE = 8;
|
||||
// Words scanned above sp for return addresses.
|
||||
static constexpr size_t STACK_SCAN_WORDS = 64;
|
||||
|
||||
// SRAM: 192 KB on the BK7231N, 256 KB on the BK7238.
|
||||
static constexpr uint32_t SRAM_START = 0x00400000;
|
||||
#ifdef USE_LIBRETINY_VARIANT_BK7231N
|
||||
@@ -79,65 +194,6 @@ static constexpr uint32_t CPSR_MODE_UND = 0x1B;
|
||||
static constexpr uint32_t CPSR_MODE_SYS = 0x1F;
|
||||
static constexpr uint32_t CPSR_THUMB = 1u << 5;
|
||||
|
||||
struct CrashRecord {
|
||||
uint32_t magic;
|
||||
uint32_t reason;
|
||||
uint32_t pc;
|
||||
uint32_t lr;
|
||||
uint32_t sp;
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
};
|
||||
|
||||
// Kept in ITCM, see patch_noinit.py.script.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
__attribute__((section(".noinit"))) static CrashRecord s_record;
|
||||
|
||||
static bool s_record_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_record_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool crash_handler_has_data() {
|
||||
if (!s_record_read) {
|
||||
s_record_read = true;
|
||||
s_record_valid = s_record.magic == CRASH_MAGIC_V1;
|
||||
// Clear the magic so the record is reported once.
|
||||
s_record.magic = 0;
|
||||
}
|
||||
return s_record_valid;
|
||||
}
|
||||
|
||||
// One ESP_LOGE per line: the CLI decodes per line and partial output still shows.
|
||||
void crash_handler_log() {
|
||||
if (!crash_handler_has_data())
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(CrashReasonStrings::get_log_str(s_record.reason, 0)));
|
||||
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_record.pc);
|
||||
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_record.lr);
|
||||
ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_record.sp);
|
||||
uint8_t count = 0;
|
||||
for (; count < MAX_BACKTRACE && s_record.backtrace[count] != 0; count++) {
|
||||
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", count, s_record.backtrace[count]);
|
||||
}
|
||||
// addr2line hint with every address
|
||||
char hint[160];
|
||||
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e raw_firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
|
||||
s_record.pc, s_record.lr);
|
||||
for (uint8_t i = 0; i < count && pos < (int) sizeof(hint) - 12; i++) {
|
||||
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_record.backtrace[i]);
|
||||
}
|
||||
ESP_LOGE(TAG, "%s", hint);
|
||||
}
|
||||
|
||||
// --- Crash time: exception mode, interrupts masked, a few dozen bytes of stack: no printing, no
|
||||
// large locals.
|
||||
|
||||
static inline bool is_code_addr(uint32_t val) {
|
||||
uint32_t addr = val & ~1u; // clear the Thumb bit of a return address
|
||||
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
|
||||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
|
||||
}
|
||||
|
||||
// PUSH_ALL_ARM_REG (boot_handlers.S) saves every mode's registers at the arm_mcu_pub.h addresses
|
||||
// before bk_trap_*; lr (r14) follows sp (r13).
|
||||
struct SavedBank {
|
||||
@@ -174,21 +230,9 @@ static void record_crash(arm_registers *regs, uint32_t reason, uint32_t pc_offse
|
||||
s_record.pc = regs->pc - pc_offset;
|
||||
s_record.lr = lr;
|
||||
s_record.sp = sp;
|
||||
|
||||
// Return addresses above sp give the backtrace.
|
||||
uint32_t count = 0;
|
||||
if (sp >= SRAM_START && sp < SRAM_END && (sp & 3) == 0) {
|
||||
auto *scan = (const uint32_t *) sp; // NOLINT(performance-no-int-to-ptr)
|
||||
auto *end = (const uint32_t *) SRAM_END; // NOLINT(performance-no-int-to-ptr)
|
||||
for (size_t i = 0; i < STACK_SCAN_WORDS && scan < end && count < MAX_BACKTRACE; i++, scan++) {
|
||||
uint32_t val = *scan;
|
||||
if (is_code_addr(val) && val != lr && (val & ~1u) != s_record.pc)
|
||||
s_record.backtrace[count++] = val;
|
||||
}
|
||||
}
|
||||
if (count < MAX_BACKTRACE)
|
||||
s_record.backtrace[count] = 0; // terminator, the reader stops at the first 0
|
||||
s_record.magic = CRASH_MAGIC_V1;
|
||||
s_record.fault_status = 0;
|
||||
s_record.fault_address = 0;
|
||||
finish_record(sp, SRAM_START, SRAM_END);
|
||||
|
||||
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
|
||||
}
|
||||
@@ -211,7 +255,106 @@ extern "C" void __wrap_bk_trap_dabt(arm_registers *regs) {
|
||||
}
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
|
||||
#endif // USE_BK72XX
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
|
||||
|
||||
// The SDK points the fault vectors (3 HardFault to 7 SecureFault) of its RAM vector table (VTOR) at a
|
||||
// ROM handler that prints a register dump and then spins. crash_handler_install() puts
|
||||
// esphome_fault_entry() in front of it, as the SDK's own CONFIG_MIIO patch (app_start.c) does.
|
||||
static_assert(LT_HW_WATCHDOG, "the crash reboot relies on the hardware watchdog");
|
||||
// The ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
|
||||
// watchdog from arch_init().
|
||||
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 1000;
|
||||
static constexpr uint32_t FIRST_FAULT_VECTOR = 3;
|
||||
static constexpr uint32_t FAULT_VECTOR_COUNT = 5;
|
||||
|
||||
// Architectural System Control Block registers (ARMv8-M).
|
||||
static constexpr uintptr_t SCB_VTOR = 0xE000ED08;
|
||||
static constexpr uintptr_t SCB_CFSR = 0xE000ED28;
|
||||
static constexpr uintptr_t SCB_MMFAR = 0xE000ED34;
|
||||
static constexpr uintptr_t SCB_BFAR = 0xE000ED38;
|
||||
static constexpr uint32_t CFSR_MMARVALID = 1u << 7;
|
||||
static constexpr uint32_t CFSR_BFARVALID = 1u << 15;
|
||||
|
||||
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
|
||||
static constexpr uint32_t SRAM_START = 0x10000000;
|
||||
static constexpr uint32_t SRAM_END = 0x1003FA00;
|
||||
|
||||
// Exception frame: r0-r3, r12, lr, pc, xPSR (the RTL8720C has no FPU, so never the extended frame).
|
||||
static constexpr uint32_t FRAME_LR = 5;
|
||||
static constexpr uint32_t FRAME_PC = 6;
|
||||
static constexpr uint32_t FRAME_XPSR = 7;
|
||||
static constexpr uint32_t FRAME_WORDS = 8;
|
||||
static constexpr uint32_t XPSR_STACK_ALIGNED = 1u << 9;
|
||||
|
||||
// The ROM handlers fault_entry() continues into, by vector.
|
||||
static uint32_t s_rom_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static inline uint32_t read_reg(uintptr_t addr) {
|
||||
return *(const volatile uint32_t *) addr; // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
// Returns the ROM handler to continue into. pc, lr and sp stay 0 when the frame is outside DTCM.
|
||||
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector) {
|
||||
uint32_t cfsr = read_reg(SCB_CFSR);
|
||||
uint32_t address = 0;
|
||||
if (cfsr & CFSR_BFARVALID) {
|
||||
address = read_reg(SCB_BFAR);
|
||||
} else if (cfsr & CFSR_MMARVALID) {
|
||||
address = read_reg(SCB_MMFAR);
|
||||
}
|
||||
s_record.reason = vector + CRASH_REASON_CORTEX_M_OFFSET;
|
||||
s_record.fault_status = cfsr;
|
||||
s_record.fault_address = address;
|
||||
|
||||
auto frame_addr = (uint32_t) frame;
|
||||
uint32_t sp = 0;
|
||||
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - FRAME_WORDS * 4 && (frame_addr & 3) == 0) {
|
||||
s_record.pc = frame[FRAME_PC];
|
||||
s_record.lr = frame[FRAME_LR];
|
||||
sp = frame_addr + FRAME_WORDS * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
|
||||
} else {
|
||||
s_record.pc = 0;
|
||||
s_record.lr = 0;
|
||||
}
|
||||
s_record.sp = sp;
|
||||
finish_record(sp, SRAM_START, SRAM_END);
|
||||
|
||||
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
|
||||
return s_rom_fault_vectors[vector - FIRST_FAULT_VECTOR];
|
||||
}
|
||||
|
||||
// Fault vector: calls esphome_record_fault(frame, exception number), then jumps to the ROM handler it returns
|
||||
// with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the ROM handler
|
||||
// reads EXC_RETURN from lr.
|
||||
extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
|
||||
__asm volatile("push {r0-r3, r4, lr} \n" // r4 only keeps the stack 8-byte aligned
|
||||
"tst lr, #4 \n"
|
||||
"ite eq \n"
|
||||
"mrseq r0, msp \n"
|
||||
"mrsne r0, psp \n"
|
||||
"it eq \n"
|
||||
"addeq r0, r0, #24 \n" // MSP: skip the six words pushed above
|
||||
"mrs r1, ipsr \n"
|
||||
"ubfx r1, r1, #0, #9 \n" // exception number, 3 to 7
|
||||
"bl esphome_record_fault \n"
|
||||
"mov r12, r0 \n" // ROM handler address
|
||||
"pop {r0-r3, r4, lr} \n"
|
||||
"bx r12 \n");
|
||||
}
|
||||
|
||||
void crash_handler_install() {
|
||||
auto *vectors = (uint32_t *) read_reg(SCB_VTOR); // NOLINT(performance-no-int-to-ptr)
|
||||
for (uint32_t i = 0; i < FAULT_VECTOR_COUNT; i++) {
|
||||
s_rom_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
|
||||
vectors[FIRST_FAULT_VECTOR + i] = (uint32_t) &esphome_fault_entry;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8720C
|
||||
|
||||
} // namespace esphome::libretiny
|
||||
|
||||
#endif // USE_LIBRETINY_CRASH_HANDLER
|
||||
#endif // USE_BK72XX
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
@@ -14,7 +14,12 @@ void crash_handler_log();
|
||||
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||
bool crash_handler_has_data();
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
|
||||
/// Put the crash recorder in front of the SDK's fault handlers.
|
||||
void crash_handler_install();
|
||||
#endif
|
||||
|
||||
} // namespace esphome::libretiny
|
||||
|
||||
#endif // USE_LIBRETINY_CRASH_HANDLER
|
||||
#endif // USE_BK72XX
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -156,6 +156,7 @@ COMPONENT_CODEOWNERS = {
|
||||
# Extra imports per component; the CLI looks up process_stacktrace on the platform package.
|
||||
COMPONENT_EXTRA_IMPORTS = {
|
||||
"bk72xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
|
||||
"rtl87xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "core.h"
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "preferences.h"
|
||||
|
||||
@@ -27,6 +28,9 @@ namespace esphome {
|
||||
void arch_init() {
|
||||
libretiny::setup_preferences();
|
||||
lt_wdt_enable(10000L);
|
||||
#if defined(USE_LIBRETINY_CRASH_HANDLER) && defined(USE_LIBRETINY_VARIANT_RTL8720C)
|
||||
libretiny::crash_handler_install();
|
||||
#endif
|
||||
#ifdef USE_BK72XX
|
||||
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
|
||||
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
|
||||
|
||||
@@ -607,10 +607,9 @@ 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 (
|
||||
CORE.using_toolchain_esp_idf
|
||||
and idf_version() >= cv.Version(6, 1, 0)
|
||||
and get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32S2)
|
||||
if 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 ESP-IDF before it quotes each
|
||||
token for CMake) and the raw quoted CMake argument (Zephyr)."""
|
||||
lexers (PlatformIO, host, ESP8266) and the quoted CMake arguments
|
||||
(ESP-IDF, Zephyr) the build flags pass through."""
|
||||
# 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,8 +414,7 @@ async def to_code(configs):
|
||||
"LV_COLOR_CHROMA_KEY",
|
||||
await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]),
|
||||
)
|
||||
if not CORE.using_toolchain_esp_idf:
|
||||
cg.add_build_flag("-Isrc")
|
||||
cg.add_build_flag("-Isrc")
|
||||
|
||||
cg.add_global(lvgl_ns.using)
|
||||
for font in df.get_esphome_fonts_used():
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -54,25 +53,16 @@ using DataCallbackManagerBase = CallbackManager<void(const AudioCapture &)>;
|
||||
|
||||
class DataCallbackManager : public DataCallbackManagerBase {
|
||||
public:
|
||||
DataCallbackManager(const audio::AudioStreamInfo *stream_info, const bool *mute_state)
|
||||
: stream_info_(stream_info), mute_state_(mute_state) {}
|
||||
explicit DataCallbackManager(const audio::AudioStreamInfo *stream_info) : stream_info_(stream_info) {}
|
||||
|
||||
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) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
void call(const std::vector<uint8_t> &data) { this->call(AudioCapture{data, *this->stream_info_, 0}); }
|
||||
|
||||
protected:
|
||||
const audio::AudioStreamInfo *stream_info_;
|
||||
const bool *mute_state_;
|
||||
};
|
||||
|
||||
class Microphone {
|
||||
@@ -82,7 +72,15 @@ 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(as_capture_callback(std::forward<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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool is_running() const { return this->state_ == STATE_RUNNING; }
|
||||
@@ -94,11 +92,8 @@ class Microphone {
|
||||
audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
|
||||
|
||||
protected:
|
||||
/// 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());
|
||||
}
|
||||
void publish_audio_(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info,
|
||||
int64_t capture_time_us = 0) {
|
||||
this->data_callbacks_.call(AudioCapture{data, stream_info, capture_time_us});
|
||||
}
|
||||
|
||||
@@ -110,7 +105,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_, &this->mute_state_};
|
||||
DataCallbackManager data_callbacks_{&this->audio_stream_info_};
|
||||
};
|
||||
|
||||
} // namespace esphome::microphone
|
||||
@@ -452,16 +452,6 @@ 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)) {
|
||||
@@ -1183,8 +1173,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 command cannot go out right
|
||||
* away, it is saved in the queue entry for retry rather than dropped.
|
||||
* 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.
|
||||
*
|
||||
* @param variable_name Name of the variable or component associated with the command.
|
||||
* @param command The raw command string to send.
|
||||
@@ -1195,15 +1185,6 @@ 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
|
||||
@@ -1365,13 +1346,14 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
|
||||
std::string command = "get " + component->get_variable_name_to_send();
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// 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);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
#else // USE_NEXTION_COMMAND_SPACING
|
||||
if (this->send_command_(command)) {
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
|
||||
@@ -1470,16 +1470,6 @@ 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_;
|
||||
|
||||
@@ -90,7 +90,6 @@ 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;
|
||||
@@ -138,23 +137,7 @@ void ResamplerMicrophone::process_audio_(const microphone::AudioCapture &capture
|
||||
}
|
||||
|
||||
if (this->resampler_ == nullptr) {
|
||||
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});
|
||||
}
|
||||
this->publish_audio_(capture.data, input_stream_info, capture.capture_time_us);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from esphome.components.libretiny.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -20,7 +19,6 @@ 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"
|
||||
@@ -41,11 +39,6 @@ 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:
|
||||
@@ -56,9 +49,6 @@ 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] = {
|
||||
@@ -68,8 +58,6 @@ 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,
|
||||
@@ -77,8 +65,6 @@ 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",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -114,15 +100,9 @@ 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,
|
||||
@@ -192,10 +172,7 @@ 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,8 +8,11 @@ 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}, // PPC 96 kHz process flow value (its 48 kHz FIR flows write 0x11)
|
||||
{0x46, 0x01},
|
||||
{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
|
||||
@@ -22,8 +25,6 @@ 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)
|
||||
@@ -87,8 +88,6 @@ 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 0x7D, 0x7E, page 1 register 0x51 and page 2 registers 0x1D, 0x19 are not documented in datasheet.
|
||||
// Registers 0x46, 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,25 +17,20 @@ static constexpr i2c::I2CRegValue STARTUP_SEQUENCE[] PROGMEM = {
|
||||
{0x1D, 0x00},
|
||||
{0x19, 0x80},
|
||||
{0x00, 0x00}, // Page 0
|
||||
{0x46, 0x11}, // DSP_CTRL: 96 kHz processing (bits 4-3 = 10), use default coefficients (bit 0 = 1)
|
||||
{0x46, 0x11},
|
||||
// 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)
|
||||
@@ -118,8 +113,6 @@ 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,7 +20,6 @@ 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
|
||||
@@ -34,9 +33,6 @@ 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;
|
||||
@@ -102,26 +98,12 @@ bool TAS58xx::init_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Setters are public, so keep the value inside the register range
|
||||
const uint8_t again =
|
||||
static_cast<uint8_t>(lroundf(clamp(-this->analog_gain_db_ * 2.0f, 0.0f, float{TAS58XX_AGAIN_MINUS_15_5DB})));
|
||||
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_) ||
|
||||
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_) ||
|
||||
!this->write_byte(TAS58XX_FAULT_CLEAR, TAS58XX_FAULT_CLEAR_ANALOG)) {
|
||||
ESP_LOGE(TAG, "I2C write failed during init");
|
||||
return false;
|
||||
@@ -281,14 +263,6 @@ 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",
|
||||
@@ -299,14 +273,11 @@ 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\n"
|
||||
" Fault Output: %s",
|
||||
" Volume Range: %.1f dB to %.1f dB",
|
||||
this->analog_gain_db_,
|
||||
this->dac_mode_ == DAC_MODE_PBTL ? LOG_STR_LITERAL("PBTL") : LOG_STR_LITERAL("BTL"),
|
||||
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));
|
||||
LOG_STR_ARG(mixer_mode), this->volume_min_db_, this->volume_max_db_);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Any Fault", this->have_fault_binary_sensor_);
|
||||
for (auto *sensor : this->fault_binary_sensors_) {
|
||||
|
||||
@@ -22,12 +22,6 @@ 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,
|
||||
@@ -70,8 +64,6 @@ 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.
|
||||
@@ -99,10 +91,6 @@ 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
|
||||
@@ -139,10 +127,7 @@ 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; }
|
||||
|
||||
@@ -186,9 +171,7 @@ 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,7 +124,6 @@ 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;
|
||||
|
||||
@@ -5,37 +5,25 @@ 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, TemplateArgsType
|
||||
|
||||
from .keysyms import button_mask, keysym
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@MindFreeze"]
|
||||
DEPENDENCIES = ["display", "network"]
|
||||
AUTO_LOAD = ["socket", "unicode"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
DOMAIN = "vnc"
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -46,14 +34,11 @@ 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"
|
||||
@@ -87,7 +72,6 @@ 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({}),
|
||||
}
|
||||
@@ -131,14 +115,6 @@ 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
|
||||
@@ -162,89 +138,15 @@ 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)
|
||||
|
||||
|
||||
_CLIENT_ID = {cv.GenerateID(): cv.use_id(VncClient)}
|
||||
|
||||
VNC_ID_SCHEMA = automation.maybe_simple_id(_CLIENT_ID)
|
||||
VNC_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(VncClient)},
|
||||
)
|
||||
|
||||
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
|
||||
),
|
||||
)
|
||||
@@ -1,136 +0,0 @@
|
||||
"""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,7 +1,6 @@
|
||||
#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"
|
||||
@@ -9,7 +8,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
@@ -55,17 +53,6 @@ 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) {
|
||||
@@ -90,59 +77,8 @@ 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) *
|
||||
@@ -185,9 +121,6 @@ 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(); }
|
||||
@@ -262,15 +195,10 @@ 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_ >= update_delay || now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
|
||||
(now - this->last_change_ms_ >= this->update_delay_ms_ ||
|
||||
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();
|
||||
@@ -279,9 +207,6 @@ 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"));
|
||||
@@ -338,13 +263,6 @@ 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();
|
||||
@@ -1066,144 +984,4 @@ 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,13 +4,9 @@
|
||||
#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 {
|
||||
|
||||
@@ -20,12 +16,6 @@ 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,
|
||||
@@ -65,12 +55,7 @@ enum class VncFailure : uint8_t {
|
||||
};
|
||||
|
||||
/// RFB (VNC) client that draws the remote framebuffer into a display.
|
||||
class VncClient : public Component
|
||||
#ifdef USE_VNC_TOUCHSCREEN
|
||||
,
|
||||
public touchscreen::TouchListener
|
||||
#endif
|
||||
{
|
||||
class VncClient : public Component {
|
||||
public:
|
||||
explicit VncClient(display::Display *display, bool big_endian = false) : display_(display), big_endian_(big_endian) {}
|
||||
|
||||
@@ -80,15 +65,6 @@ 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;
|
||||
@@ -100,13 +76,6 @@ 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));
|
||||
}
|
||||
@@ -117,13 +86,6 @@ 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);
|
||||
@@ -145,17 +107,6 @@ 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;
|
||||
@@ -184,9 +135,6 @@ 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};
|
||||
@@ -213,19 +161,7 @@ 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};
|
||||
@@ -257,18 +193,9 @@ 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,22 +245,6 @@ 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)
|
||||
@@ -338,7 +322,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(_validate_dpp_private_key),
|
||||
cv.Optional(CONF_PRIVATE_KEY): cv.sensitive(cv.string_strict),
|
||||
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)
|
||||
@@ -920,7 +904,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(f"{DPP_KEY_DER_PREFIX}{key}{DPP_KEY_DER_SUFFIX}"))
|
||||
cg.add(var.set_dpp_key(key))
|
||||
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,13 +2744,8 @@ def url(value):
|
||||
raise Invalid("Expected a file scheme or a URL scheme with host")
|
||||
|
||||
|
||||
# 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:
|
||||
def git_ref(value):
|
||||
if re.match(r"[a-zA-Z0-9_./-]+", value) is None:
|
||||
raise Invalid("Not a valid git ref")
|
||||
return value
|
||||
|
||||
@@ -3005,8 +3000,9 @@ def validate_source_shorthand(value):
|
||||
except Invalid:
|
||||
pass
|
||||
# Regex for GitHub repo name with optional branch/tag
|
||||
m = re.fullmatch(
|
||||
rf"github://(?:([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@({GIT_REF_PATTERN}))?|pr#([0-9]+))",
|
||||
# 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]+))",
|
||||
value,
|
||||
)
|
||||
if m is None:
|
||||
|
||||
@@ -910,8 +910,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
CORE.add_job(_add_automations, config)
|
||||
|
||||
if not CORE.using_toolchain_esp_idf:
|
||||
cg.add_build_flag("-fno-exceptions")
|
||||
cg.add_build_flag("-fno-exceptions")
|
||||
|
||||
# Libraries
|
||||
for lib in config[CONF_LIBRARIES]:
|
||||
|
||||
@@ -536,8 +536,6 @@
|
||||
#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
|
||||
@@ -776,7 +774,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
#ifdef USE_BK72XX
|
||||
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C)
|
||||
#define USE_LIBRETINY_CRASH_HANDLER
|
||||
#endif
|
||||
#define USE_BK72XX_BLE
|
||||
|
||||
@@ -42,26 +42,20 @@ _MIRROR_ATTEMPTS = 3
|
||||
_MIRROR_SWEEP_ATTEMPTS = NETWORK_MAX_ATTEMPTS
|
||||
|
||||
|
||||
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)."""
|
||||
def get_project_link_flags() -> list[str]:
|
||||
"""Return the sorted -Wl, linker flags from the current build."""
|
||||
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 flags if flag.startswith("-Wl,")]
|
||||
return sorted(flag for flag in CORE.build_flags if flag.startswith("-Wl,"))
|
||||
|
||||
|
||||
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)."""
|
||||
def get_project_compile_flags() -> list[str]:
|
||||
"""Return the sorted -D and -W (non-linker) flags from the current build."""
|
||||
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 flags
|
||||
for flag in sorted(CORE.build_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:
|
||||
user = urllib.parse.quote(username, safe="")
|
||||
token = urllib.parse.quote(password, safe="")
|
||||
url = url.replace("://", f"://{user}:{token}@")
|
||||
url = url.replace(
|
||||
"://", f"://{urllib.parse.quote(username)}:{urllib.parse.quote(password)}@"
|
||||
)
|
||||
|
||||
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.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\-_.\./]+))?",
|
||||
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\-_.\./]+))?",
|
||||
shorthand,
|
||||
)
|
||||
if m is None:
|
||||
|
||||
@@ -2,7 +2,7 @@ dependencies:
|
||||
bblanchon/arduinojson:
|
||||
version: "7.4.3"
|
||||
esphome/dlms_parser:
|
||||
version: 2.2.2
|
||||
version: 2.2.0
|
||||
esphome/esp-audio-libs:
|
||||
version: 4.1.0
|
||||
esphome/esp-micro-speech-features:
|
||||
|
||||
+2
-7
@@ -284,17 +284,12 @@ 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(module_name)
|
||||
module = importlib.import_module(f"esphome.components.{domain}")
|
||||
except ImportError as e:
|
||||
if exception:
|
||||
raise
|
||||
if (
|
||||
isinstance(e, ModuleNotFoundError)
|
||||
and e.name is not None
|
||||
and f"{module_name}.".startswith(f"{e.name}.")
|
||||
):
|
||||
if "No module named" in str(e):
|
||||
_LOGGER.info(
|
||||
"Unable to import component %s: %s", domain, str(e), exc_info=False
|
||||
)
|
||||
|
||||
@@ -67,6 +67,7 @@ STACKTRACE_GATES: Final[dict[str, str]] = {
|
||||
),
|
||||
PLATFORM_RP2: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_BK72XX: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_RTL87XX: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_NRF52: r"0x[0-9a-fA-F]{3,}\b|Last crash:",
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ def _addr2line(tool: str, elf: Path, addr: str) -> str:
|
||||
def process_crash_handler_line(
|
||||
config: ConfigType, line: str, backtrace_state: bool
|
||||
) -> bool:
|
||||
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2 and bk72xx."""
|
||||
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx and rtl87xx."""
|
||||
if CRASH_HANDLER_RE.search(line):
|
||||
_LOGGER.error("Crash detected - decoding addresses")
|
||||
return True
|
||||
|
||||
@@ -1036,15 +1036,8 @@ 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
|
||||
|
||||
@@ -1356,8 +1349,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[:] = sorted(d for d in dirs if _is_file_valid(d))
|
||||
for f in sorted(files):
|
||||
dirs[:] = [d for d in dirs if _is_file_valid(d)]
|
||||
for f in 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.2 ; dlms_meter
|
||||
esphome/dlms_parser@2.2.0 ; 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==50.0.2; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
cryptography==48.0.1; 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.5 # from time
|
||||
tzlocal==5.4.4 # from time
|
||||
tzdata>=2026.5 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.2.0
|
||||
esptool==5.6.0
|
||||
click==8.5.0
|
||||
aioesphomeapi==46.23.0
|
||||
aioesphomeapi==46.22.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.2.0; platform_system == "Darwin" and platform_machine == "x86_64"
|
||||
smp==4.1.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.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
ruff==0.17.1 # .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.7 # .github/workflows/ci.yml reads this pin
|
||||
prek==0.5.6 # .github/workflows/ci.yml reads this pin
|
||||
yamlrocks==0.7.1 # used by script/sync_dependency_versions.py
|
||||
|
||||
# Unit tests
|
||||
|
||||
@@ -57,6 +57,7 @@ PLATFORM_BUILD_SECONDS = {
|
||||
"esp32-idf": 55,
|
||||
"esp32-p4-idf": 55,
|
||||
"ln882x-ard": 55,
|
||||
"rtl8720c-ard": 55,
|
||||
"esp32-s3-idf": 60,
|
||||
"bk72xx-ard": 70,
|
||||
"bk7238-ard": 70,
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"""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
|
||||
@@ -0,0 +1,5 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rtl87xx:
|
||||
board: generic-rtl8710bn-2mb-788k
|
||||
@@ -0,0 +1,5 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rtl87xx:
|
||||
board: bw15
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Only BK7238 and BK7231N, the BK chips with an ITCM linker region, get the crash handler."""
|
||||
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region) and RTL8720C."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,12 +12,14 @@ from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "enabled"),
|
||||
("config", "enabled", "bk_hooks"),
|
||||
[
|
||||
("bk7238.yaml", True),
|
||||
("bk72xx.yaml", True), # BK7231N
|
||||
("bk7231t.yaml", False),
|
||||
("ln882x.yaml", False),
|
||||
("bk7238.yaml", True, True),
|
||||
("bk72xx.yaml", True, True), # BK7231N
|
||||
("bk7231t.yaml", False, False),
|
||||
("rtl8720c.yaml", True, False), # installed at run time
|
||||
("rtl8710b.yaml", False, False),
|
||||
("ln882x.yaml", False, False),
|
||||
],
|
||||
)
|
||||
def test_crash_handler_family_gate(
|
||||
@@ -25,13 +27,14 @@ def test_crash_handler_family_gate(
|
||||
component_config_path: Callable[[str], Path],
|
||||
config: str,
|
||||
enabled: bool,
|
||||
bk_hooks: bool,
|
||||
) -> None:
|
||||
generate_main(component_config_path(config))
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_LIBRETINY_CRASH_HANDLER" in defines) is enabled
|
||||
assert ("USE_CRASH_HANDLER" in defines) is enabled
|
||||
for trap in BK_TRAP_HANDLERS:
|
||||
assert (f"-Wl,--wrap={trap}" in CORE.build_flags) is enabled
|
||||
assert (f"-Wl,--wrap={trap}" in CORE.build_flags) is bk_hooks
|
||||
assert (
|
||||
"pre:patch_noinit.py" in CORE.platformio_options["extra_scripts"]
|
||||
) is enabled
|
||||
) is bk_hooks
|
||||
@@ -138,26 +138,3 @@ def test_task_log_buffer_size_unaligned_allowed_on_libretiny(
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.BK72XX_ARDUINO)
|
||||
assert validate_task_log_buffer_alignment(641) == 641
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "log_v2"),
|
||||
[
|
||||
pytest.param("test_logger_idf6_esp_idf.yaml", True, id="native"),
|
||||
pytest.param("test_logger_idf6_platformio.yaml", False, id="platformio"),
|
||||
],
|
||||
)
|
||||
def test_log_v2_only_on_native_toolchain(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
config: str,
|
||||
log_v2: bool,
|
||||
) -> None:
|
||||
"""PlatformIO links the bootloader with the app's build flags, so the wrap has no target there."""
|
||||
from esphome.components.esp32 import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
|
||||
generate_main(f"tests/component_tests/logger/{config}")
|
||||
|
||||
assert ("-Wl,--wrap=esp_log_format" in CORE.build_flags) is log_v2
|
||||
assert ("USE_ESP32_LOG_V2" in {d.name for d in CORE.defines}) is log_v2
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert ("CONFIG_LOG_VERSION_2" in sdkconfig) is log_v2
|
||||
@@ -1,11 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
toolchain: esp-idf
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: 6.1.0
|
||||
|
||||
logger:
|
||||
@@ -1,12 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
toolchain: platformio
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: 6.1.0
|
||||
platform_version: 61.4.0-RC1
|
||||
|
||||
logger:
|
||||
@@ -1,9 +1,8 @@
|
||||
"""The ``-DLV_CONF_PATH`` build flag carries a path. PlatformIO, the host
|
||||
build and the native ESP8266 build shell-lex build flags; the ESP-IDF build
|
||||
shell-lexes them and then quotes each token as a CMake argument; the Zephyr
|
||||
build writes each flag into a quoted CMake argument as is. All must decode it
|
||||
to one ``-DLV_CONF_PATH="..."`` argument, including when the project path
|
||||
contains a space."""
|
||||
build and the native ESP8266 build shell-lex build flags, while the ESP-IDF
|
||||
and Zephyr builds write each flag into a quoted CMake argument; both must
|
||||
decode it to one ``-DLV_CONF_PATH="..."`` argument, including when the
|
||||
project path contains a space."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -13,9 +12,7 @@ import shlex
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.build_gen.espidf import _cmake_args
|
||||
from esphome.components.lvgl import lv_conf_path_flag
|
||||
from esphome.platformio.library import lex_build_flags
|
||||
|
||||
|
||||
def _cmake_quoted_argument(flag: str) -> str:
|
||||
@@ -40,7 +37,3 @@ def test_flag_is_one_argument_everywhere(build_path: str) -> None:
|
||||
|
||||
assert shlex.split(flag) == [expected]
|
||||
assert _cmake_quoted_argument(flag) == expected
|
||||
assert [
|
||||
_cmake_quoted_argument(arg.removeprefix('"').removesuffix('"'))
|
||||
for arg in _cmake_args(lex_build_flags([flag], "lvgl"))
|
||||
] == [expected]
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Tests that the TAS58xx fault_output and modulation defaults keep the existing behaviour."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_tas58xx_defaults_keep_the_fault_pin_and_bd_modulation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""Without fault_output or modulation each model keeps its usual fault pin and BD modulation."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/tas58xx/test_tas58xx_fault_output_modulation.yaml"
|
||||
)
|
||||
|
||||
assert "amp_5805_default->set_fault_output(0);" in main_cpp
|
||||
assert "amp_5825_default->set_fault_output(1);" in main_cpp
|
||||
for amp in ("amp_5805_default", "amp_5825_default", "amp_5805_none"):
|
||||
assert f"{amp}->set_modulation(tas58xx::MODULATION_BD_MODE);" in main_cpp
|
||||
|
||||
|
||||
def test_tas58xx_fault_output_none_leaves_the_pin_alone(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""fault_output: none does not configure a fault pin."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/tas58xx/test_tas58xx_fault_output_modulation.yaml"
|
||||
)
|
||||
|
||||
assert "amp_5805_none->set_fault_output(" not in main_cpp
|
||||
@@ -1,26 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
id: i2c_bus
|
||||
sda: 21
|
||||
scl: 22
|
||||
|
||||
audio_dac:
|
||||
- platform: tas58xx
|
||||
model: tas5805m
|
||||
id: amp_5805_default
|
||||
ignore_enable_pin_warning: true
|
||||
- platform: tas58xx
|
||||
model: tas5825m
|
||||
id: amp_5825_default
|
||||
ignore_enable_pin_warning: true
|
||||
- platform: tas58xx
|
||||
model: tas5805m
|
||||
id: amp_5805_none
|
||||
address: 0x2C
|
||||
ignore_enable_pin_warning: true
|
||||
fault_output: none
|
||||
@@ -56,17 +56,6 @@ EPAPER_SPI = """
|
||||
reset_pin: GPIO25
|
||||
"""
|
||||
|
||||
TOUCHSCREEN = """
|
||||
i2c:
|
||||
sda: GPIO32
|
||||
scl: GPIO33
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
id: touch
|
||||
display: {display}
|
||||
"""
|
||||
|
||||
|
||||
S3_BASE = """
|
||||
esphome:
|
||||
@@ -166,10 +155,6 @@ def config_error(
|
||||
return validate
|
||||
|
||||
|
||||
def _defines() -> set[str]:
|
||||
return {define.name for define in CORE.defines}
|
||||
|
||||
|
||||
def test_it8951_owned_by_vnc(
|
||||
generate_main: Callable[[str | Path], str], write_yaml: Callable[..., Path]
|
||||
) -> None:
|
||||
@@ -278,30 +263,6 @@ vnc:
|
||||
assert TEST_CARD not in main_cpp
|
||||
|
||||
|
||||
def test_no_touchscreen_no_forwarding(
|
||||
generate_main: Callable[[str | Path], str], write_yaml: Callable[..., Path]
|
||||
) -> None:
|
||||
main_cpp = generate_main(write_yaml(IT8951, VNC))
|
||||
|
||||
assert "USE_VNC_TOUCHSCREEN" not in _defines()
|
||||
assert "register_touchscreen" not in main_cpp
|
||||
|
||||
|
||||
def test_touchscreen_forwarding(
|
||||
generate_main: Callable[[str | Path], str], write_yaml: Callable[..., Path]
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
write_yaml(
|
||||
IT8951,
|
||||
TOUCHSCREEN.format(display="epd"),
|
||||
"\nvnc:\n - host: 192.168.1.20\n touchscreen_id: touch\n",
|
||||
)
|
||||
)
|
||||
|
||||
assert "USE_VNC_TOUCHSCREEN" in _defines()
|
||||
assert re.search(r"->register_touchscreen\(touch\);", main_cpp)
|
||||
|
||||
|
||||
def test_action_codegen(
|
||||
generate_main: Callable[[str | Path], str], write_yaml: Callable[..., Path]
|
||||
) -> None:
|
||||
@@ -316,30 +277,11 @@ vnc:
|
||||
interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- vnc.send_pointer:
|
||||
x: 10
|
||||
y: 20
|
||||
buttons: [left, wheel_down]
|
||||
- vnc.send_key: Return
|
||||
- vnc.send_key:
|
||||
key: "A"
|
||||
action: down
|
||||
- vnc.send_key: 7
|
||||
- vnc.type_text: "é€"
|
||||
- vnc.refresh
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
assert "remote->send_pointer(10, 20, 17);" in main_cpp
|
||||
assert (
|
||||
"remote->send_key(0xFF0D, vnc::VncKeyAction::VNC_KEY_ACTION_PRESS);" in main_cpp
|
||||
)
|
||||
assert "remote->send_key(0x41, vnc::VncKeyAction::VNC_KEY_ACTION_DOWN);" in main_cpp
|
||||
assert (
|
||||
"remote->send_key(0x37, vnc::VncKeyAction::VNC_KEY_ACTION_PRESS);" in main_cpp
|
||||
)
|
||||
assert r'remote->type_text("\303\251\342\202\254", 5);' in main_cpp
|
||||
assert "remote->refresh();" in main_cpp
|
||||
|
||||
|
||||
@@ -463,20 +405,6 @@ display:
|
||||
assert "use epaper_spi" in out
|
||||
|
||||
|
||||
def test_touchscreen_on_other_display_rejected(
|
||||
config_error: Callable[..., str],
|
||||
) -> None:
|
||||
out = config_error(
|
||||
IT8951,
|
||||
EPAPER_SPI,
|
||||
TOUCHSCREEN.format(display="other"),
|
||||
"\nvnc:\n - host: 192.168.1.20\n display_id: epd\n"
|
||||
" touchscreen_id: touch\n",
|
||||
)
|
||||
|
||||
assert "Touchscreen 'touch' belongs to display 'other', not 'epd'" in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vnc_options", "error"),
|
||||
[
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
dpp:
|
||||
private_key: 00112233445566778899AABBCCDDEEFF00112233445566778899aabbccddeeff
|
||||
@@ -1,42 +0,0 @@
|
||||
"""The DPP bootstrapping key is given as 32 raw bytes in hex and handed to the supplicant DER encoded."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import wifi
|
||||
import esphome.config_validation as cv
|
||||
|
||||
RAW_KEY = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
RAW_KEY[:-2],
|
||||
RAW_KEY + "00",
|
||||
"zz" + RAW_KEY[2:],
|
||||
wifi.DPP_KEY_DER_PREFIX + RAW_KEY + wifi.DPP_KEY_DER_SUFFIX,
|
||||
],
|
||||
)
|
||||
def test_key_must_be_64_hex_digits(value: str) -> None:
|
||||
with pytest.raises(cv.Invalid, match="64 hexadecimal characters"):
|
||||
wifi._validate_dpp_private_key(value)
|
||||
|
||||
|
||||
def test_key_is_lower_cased() -> None:
|
||||
assert wifi._validate_dpp_private_key(RAW_KEY.upper()) == RAW_KEY
|
||||
|
||||
|
||||
def test_generated_key_is_der_encoded(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The ESP-IDF supplicant expects the P-256 DER header around the raw key."""
|
||||
main_cpp = generate_main(component_config_path("dpp_key.yaml"))
|
||||
|
||||
assert (
|
||||
f'set_dpp_key("{wifi.DPP_KEY_DER_PREFIX}{RAW_KEY}{wifi.DPP_KEY_DER_SUFFIX}")'
|
||||
in main_cpp
|
||||
)
|
||||
@@ -400,37 +400,6 @@ api:
|
||||
root["items"][0] = "item1";
|
||||
root["items"][1] = "item2";
|
||||
|
||||
# ==========================================================================
|
||||
# Array args in actions that respond. The default array args are
|
||||
# non-copyable FixedVector views, so api.respond and the action that
|
||||
# unregisters the call must read call_id without copying the other args.
|
||||
# ==========================================================================
|
||||
- action: test_respond_only_array
|
||||
supports_response: only
|
||||
variables:
|
||||
float_arr: float[]
|
||||
string_arr: string[]
|
||||
then:
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["floats"] = float_arr.size();
|
||||
root["strings"] = string_arr.size();
|
||||
|
||||
- action: test_respond_status_array
|
||||
variables:
|
||||
int_arr: int[]
|
||||
then:
|
||||
- api.respond:
|
||||
success: !lambda 'return !int_arr.empty();'
|
||||
|
||||
- action: test_respond_optional_array
|
||||
variables:
|
||||
bool_arr: bool[]
|
||||
then:
|
||||
- api.respond:
|
||||
data: !lambda |-
|
||||
root["bools"] = bool_arr.size();
|
||||
|
||||
# ==========================================================================
|
||||
# supports_response: none (no api.respond action)
|
||||
# No call_id or return_response - just user variables
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/demo/demo_cover.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::demo::testing {
|
||||
|
||||
using cover::COVER_CLOSED;
|
||||
using cover::COVER_OPERATION_IDLE;
|
||||
using cover::COVER_OPERATION_OPENING;
|
||||
|
||||
// Type 4 is the demo cover with a stop button. It starts closed.
|
||||
TEST(DemoCoverTest, StopEndsMovement) {
|
||||
DemoCover cover;
|
||||
cover.set_type(DemoCoverType::TYPE_4);
|
||||
cover.setup();
|
||||
|
||||
cover.make_call().set_command_open().perform();
|
||||
EXPECT_EQ(cover.current_operation, COVER_OPERATION_OPENING);
|
||||
|
||||
cover.make_call().set_command_stop().perform();
|
||||
EXPECT_EQ(cover.current_operation, COVER_OPERATION_IDLE);
|
||||
EXPECT_EQ(cover.position, COVER_CLOSED);
|
||||
// MOVE_TIMEOUT_ID (0) is gone, so the move cannot finish 2 s later
|
||||
EXPECT_FALSE(App.scheduler.cancel_timeout(&cover, uint32_t{0}));
|
||||
}
|
||||
|
||||
} // namespace esphome::demo::testing
|
||||
@@ -17,4 +17,3 @@ sensor:
|
||||
sensor: integration_my_sensor
|
||||
name: Integration Sensor
|
||||
time_unit: s
|
||||
state_class: total_increasing
|
||||
@@ -6,4 +6,3 @@ sensor:
|
||||
sensor: integration_my_sensor
|
||||
name: Integration Sensor
|
||||
time_unit: s
|
||||
state_class: total_increasing
|
||||
@@ -6,4 +6,3 @@ sensor:
|
||||
sensor: integration_my_sensor
|
||||
name: Integration Sensor
|
||||
time_unit: s
|
||||
state_class: total_increasing
|
||||
@@ -0,0 +1,13 @@
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- lambda: |-
|
||||
int x = 100;
|
||||
x = clamp(x, 50, 90);
|
||||
assert(x == 90);
|
||||
x = clamp_at_least(x, 95);
|
||||
assert(x == 95);
|
||||
x = clamp_at_most(x, 40);
|
||||
assert(x == 40);
|
||||
@@ -17,7 +17,7 @@ class TestMicrophone : public Microphone {
|
||||
void start() override {}
|
||||
void stop() override {}
|
||||
|
||||
void publish(std::span<uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us) {
|
||||
void publish(std::span<const uint8_t> data, const audio::AudioStreamInfo &stream_info, int64_t capture_time_us) {
|
||||
this->publish_audio_(data, stream_info, capture_time_us);
|
||||
}
|
||||
bool has_callbacks() const { return this->has_data_callbacks_(); }
|
||||
@@ -56,24 +56,20 @@ TEST(MicrophoneDataCallbacks, CallbackGetsDataFormatAndCaptureTime) {
|
||||
});
|
||||
EXPECT_TRUE(mic.has_callbacks());
|
||||
|
||||
std::vector<uint8_t> buffer = SAMPLES;
|
||||
mic.publish(buffer, STEREO_48K, 123456);
|
||||
mic.publish(SAMPLES, STEREO_48K, 123456);
|
||||
EXPECT_EQ(data, SAMPLES);
|
||||
EXPECT_EQ(stream_info, STEREO_48K);
|
||||
EXPECT_EQ(capture_time_us, 123456);
|
||||
}
|
||||
|
||||
TEST(MicrophoneDataCallbacks, MuteZeroesTheBufferInPlace) {
|
||||
TEST(MicrophoneDataCallbacks, MutedCallbackGetsZeros) {
|
||||
TestMicrophone mic;
|
||||
std::vector<uint8_t> data;
|
||||
mic.add_data_callback([&](const AudioCapture &capture) { data.assign(capture.data.begin(), capture.data.end()); });
|
||||
|
||||
mic.set_mute_state(true);
|
||||
std::vector<uint8_t> buffer = SAMPLES;
|
||||
mic.publish(buffer, STEREO_48K, 0);
|
||||
const std::vector<uint8_t> zeros(SAMPLES.size(), 0);
|
||||
EXPECT_EQ(data, zeros);
|
||||
EXPECT_EQ(buffer, zeros);
|
||||
mic.publish(SAMPLES, STEREO_48K, 0);
|
||||
EXPECT_EQ(data, std::vector<uint8_t>(SAMPLES.size(), 0));
|
||||
}
|
||||
|
||||
TEST(MicrophoneDataCallbacks, DataTriggerPassesACopy) {
|
||||
@@ -119,17 +115,4 @@ TEST(MicrophoneDataCallbacks, DeprecatedCallUsesMicrophoneFormat) {
|
||||
EXPECT_EQ(capture_time_us, 0);
|
||||
}
|
||||
|
||||
// Remove in 2027.5.0 with the deprecated DataCallbackManager::call(data)
|
||||
TEST(MicrophoneDataCallbacks, DeprecatedCallDeliversZerosWhileMuted) {
|
||||
TestMicrophone mic;
|
||||
std::vector<uint8_t> data;
|
||||
mic.add_data_callback([&](const AudioCapture &capture) { data.assign(capture.data.begin(), capture.data.end()); });
|
||||
|
||||
mic.set_mute_state(true);
|
||||
const std::vector<uint8_t> buffer = SAMPLES;
|
||||
mic.publish_deprecated(buffer);
|
||||
EXPECT_EQ(data, std::vector<uint8_t>(SAMPLES.size(), 0));
|
||||
EXPECT_EQ(buffer, SAMPLES);
|
||||
}
|
||||
|
||||
} // namespace esphome::microphone::testing
|
||||
@@ -1,5 +1,3 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
@@ -12,12 +10,3 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
if dep not in deps:
|
||||
deps.append(dep)
|
||||
manifest.dependencies = deps
|
||||
|
||||
# The command queue ordering code under test is gated on
|
||||
# USE_NEXTION_COMMAND_SPACING, which the component only defines when
|
||||
# command_spacing is configured. Define it here so the C++ unit test build
|
||||
# compiles those paths.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
cg.add_define("USE_NEXTION_COMMAND_SPACING")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -8,48 +8,33 @@
|
||||
|
||||
#include "esphome/components/nextion/nextion.h"
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::nextion::testing {
|
||||
|
||||
// Sets the loop clock the component reads through App.get_loop_component_start_time(), so spacing can be
|
||||
// stepped forward deterministically. The guard only publishes the time; it does not restore it on destruction.
|
||||
static void set_loop_time(uint32_t now) { LoopBlockingGuard clock(nullptr, LOG_STR("test"), now); }
|
||||
|
||||
// Explicit instantiation skips access checks, which lets the test reach the queue and the response
|
||||
// handlers without making them public on the component.
|
||||
template<std::list<NextionQueue *> Nextion::*Q, void (Nextion::*N)(int), void (Nextion::*S)(const std::string &),
|
||||
void (Nextion::*R)(NextionQueue *), bool NextionBase::*U, bool (Nextion::*RQ)(bool), void (Nextion::*PP)()>
|
||||
void (Nextion::*R)(NextionQueue *), bool NextionBase::*U>
|
||||
struct NextionAccess {
|
||||
friend std::list<NextionQueue *> &queue(Nextion &nextion) { return nextion.*Q; }
|
||||
friend void numeric_return(Nextion &nextion, int value) { (nextion.*N)(value); }
|
||||
friend void string_return(Nextion &nextion, const std::string &value) { (nextion.*S)(value); }
|
||||
friend void release_queue_entry(Nextion &nextion, NextionQueue *entry) { (nextion.*R)(entry); }
|
||||
friend void mark_setup(Nextion &nextion) { nextion.*U = true; }
|
||||
friend bool remove_from_q(Nextion &nextion, bool report_empty) { return (nextion.*RQ)(report_empty); }
|
||||
friend void process_pending(Nextion &nextion) { (nextion.*PP)(); }
|
||||
};
|
||||
std::list<NextionQueue *> &queue(Nextion &nextion);
|
||||
void numeric_return(Nextion &nextion, int value);
|
||||
void string_return(Nextion &nextion, const std::string &value);
|
||||
void release_queue_entry(Nextion &nextion, NextionQueue *entry);
|
||||
void mark_setup(Nextion &nextion);
|
||||
bool remove_from_q(Nextion &nextion, bool report_empty);
|
||||
void process_pending(Nextion &nextion);
|
||||
template struct NextionAccess<&Nextion::nextion_queue_, &Nextion::set_numeric_return_, &Nextion::set_string_return_,
|
||||
&Nextion::release_queue_entry_, &Nextion::is_setup_, &Nextion::remove_from_q_,
|
||||
&Nextion::process_pending_in_queue_>;
|
||||
&Nextion::release_queue_entry_, &Nextion::is_setup_>;
|
||||
|
||||
/// Accepts the commands the component writes; the display never answers
|
||||
class FakeUART : public uart::UARTComponent {
|
||||
public:
|
||||
/// Every byte written, so tests can check what reached the wire and in what order
|
||||
std::string sent;
|
||||
|
||||
void write_array(const uint8_t *data, size_t len) override {
|
||||
this->sent.append(reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
void write_array(const uint8_t *data, size_t len) override {}
|
||||
bool peek_byte(uint8_t *data) override { return false; }
|
||||
bool read_array(uint8_t *data, size_t len) override { return false; }
|
||||
size_t available() override { return 0; }
|
||||
@@ -125,19 +110,6 @@ class DeliverQueueResponse : public ::testing::Test {
|
||||
return names;
|
||||
}
|
||||
|
||||
/// Commands that reached the wire, in order. send_command_() ends each with the 0xFF 0xFF 0xFF terminator.
|
||||
std::vector<std::string> sent_commands_() {
|
||||
std::vector<std::string> commands;
|
||||
const std::string terminator(3, static_cast<char>(0xFF));
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
while ((end = this->uart_.sent.find(terminator, start)) != std::string::npos) {
|
||||
commands.push_back(this->uart_.sent.substr(start, end - start));
|
||||
start = end + terminator.size();
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
FakeUART uart_;
|
||||
Nextion nextion_;
|
||||
std::vector<std::unique_ptr<MockComponent>> components_;
|
||||
@@ -268,55 +240,4 @@ TEST_F(DeliverQueueResponse, QueryNamedRawGetIsNotRawGet) {
|
||||
EXPECT_TRUE(this->queued_().empty());
|
||||
}
|
||||
|
||||
// With command spacing, an unrelated ACK or error must not consume a front entry whose command is still
|
||||
// unsent: the response cannot belong to it, so remove_from_q_() leaves it queued instead of losing it.
|
||||
TEST_F(DeliverQueueResponse, AckKeepsUnsentFrontEntry) {
|
||||
this->nextion_.set_command_spacing(50);
|
||||
auto *unsent = this->add_query_("temp", NextionQueueType::SENSOR, false);
|
||||
|
||||
EXPECT_FALSE(remove_from_q(this->nextion_, false));
|
||||
|
||||
EXPECT_FALSE(unsent->received);
|
||||
EXPECT_EQ(this->queued_(), std::vector<std::string>{"temp"});
|
||||
}
|
||||
|
||||
// Once a command is waiting for spacing, a later write must not go out ahead of it, even after the spacing
|
||||
// interval has elapsed. process_pending_in_queue_() then drains the queue in the original transmit order.
|
||||
TEST_F(DeliverQueueResponse, WriteDoesNotOvertakePendingCommand) {
|
||||
this->nextion_.set_command_spacing(50);
|
||||
|
||||
set_loop_time(1000);
|
||||
this->nextion_.send_command("cmd1"); // nothing waiting, goes out now
|
||||
this->nextion_.send_command("cmd2"); // spacing not elapsed, kept pending
|
||||
|
||||
set_loop_time(1100); // spacing elapsed
|
||||
this->nextion_.send_command("cmd3"); // must queue behind cmd2, not overtake it
|
||||
EXPECT_EQ(this->sent_commands_(), std::vector<std::string>{"cmd1"});
|
||||
|
||||
process_pending(this->nextion_); // sends cmd2
|
||||
set_loop_time(1200);
|
||||
process_pending(this->nextion_); // sends cmd3
|
||||
EXPECT_EQ(this->sent_commands_(), (std::vector<std::string>{"cmd1", "cmd2", "cmd3"}));
|
||||
}
|
||||
|
||||
// A GET follows the same rule: while an earlier command is still waiting, the query is kept pending and only
|
||||
// transmitted once the queue drains in order, never ahead of the command that came before it.
|
||||
TEST_F(DeliverQueueResponse, GetDoesNotOvertakePendingCommand) {
|
||||
this->nextion_.set_command_spacing(50);
|
||||
auto *sensor = this->components_.emplace_back(std::make_unique<MockComponent>("bri", NextionQueueType::SENSOR)).get();
|
||||
|
||||
set_loop_time(1000);
|
||||
this->nextion_.send_command("cmd1"); // goes out now
|
||||
this->nextion_.send_command("cmd2"); // kept pending by spacing
|
||||
|
||||
set_loop_time(1100); // spacing elapsed
|
||||
this->nextion_.add_to_get_queue(sensor); // must queue behind cmd2
|
||||
EXPECT_EQ(this->sent_commands_(), std::vector<std::string>{"cmd1"});
|
||||
|
||||
process_pending(this->nextion_); // sends cmd2
|
||||
set_loop_time(1200);
|
||||
process_pending(this->nextion_); // sends "get bri"
|
||||
EXPECT_EQ(this->sent_commands_(), (std::vector<std::string>{"cmd1", "cmd2", "get bri"}));
|
||||
}
|
||||
|
||||
} // namespace esphome::nextion::testing
|
||||
@@ -7,9 +7,7 @@ audio_dac:
|
||||
enable_pin: GPIO12
|
||||
analog_gain: -15.5dB
|
||||
dac_mode: btl
|
||||
fault_output: adr
|
||||
mixer_mode: stereo
|
||||
modulation: 1SPW_MODE
|
||||
volume_min_db: -60dB
|
||||
volume_max_db: 0dB
|
||||
update_interval: 1s
|
||||
|
||||
@@ -9,4 +9,3 @@ audio_dac:
|
||||
dac_mode: PBTL
|
||||
mixer_mode: mono
|
||||
analog_gain: -3.5dB
|
||||
fault_output: none
|
||||
@@ -1,6 +1,5 @@
|
||||
# This file's presence makes pytest treat this directory as a package named
|
||||
# "vnc"; required for cpp unit testing.
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import socket as socket_component
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
@@ -9,11 +8,8 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# MULTI_CONF components get no config in a unit test build, so to_code would never run.
|
||||
manifest.multi_conf = False
|
||||
manifest.dependencies = [*manifest.dependencies, "touchscreen"]
|
||||
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
socket_component.require_tcp_client_link()
|
||||
cg.add_define("USE_VNC_INPUT")
|
||||
cg.add_define("USE_VNC_TOUCHSCREEN")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -16,60 +16,6 @@ interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- vnc.refresh: vnc_client
|
||||
- vnc.send_pointer:
|
||||
id: vnc_client
|
||||
x: 10
|
||||
y: 20
|
||||
- vnc.send_pointer:
|
||||
id: vnc_client
|
||||
x: 10
|
||||
y: 20
|
||||
buttons: 1
|
||||
- vnc.send_pointer:
|
||||
id: vnc_client
|
||||
x: 10
|
||||
y: 20
|
||||
buttons: [left, wheel_down]
|
||||
- vnc.send_pointer:
|
||||
id: vnc_client
|
||||
x: 10
|
||||
y: 20
|
||||
buttons: right
|
||||
- vnc.send_pointer:
|
||||
id: vnc_client
|
||||
x: !lambda return 30;
|
||||
y: !lambda return 40;
|
||||
buttons: !lambda return 4;
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: Return
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: "a"
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: 0xFF1B
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: !lambda return 0xFF09;
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: Shift_L
|
||||
action: down
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: Shift_L
|
||||
action: up
|
||||
- vnc.send_key:
|
||||
id: vnc_client
|
||||
key: "é"
|
||||
action: press
|
||||
- vnc.type_text:
|
||||
id: vnc_client
|
||||
text: "Hello\n"
|
||||
- vnc.type_text:
|
||||
id: vnc_client
|
||||
text: !lambda return std::string("world");
|
||||
- if:
|
||||
condition:
|
||||
vnc.is_connected: vnc_client
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
vnc: !include common.yaml
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
display:
|
||||
- platform: test_display
|
||||
id: vnc_touch_display
|
||||
dimensions: 320x240
|
||||
|
||||
touchscreen:
|
||||
- platform: ft5x06
|
||||
i2c_id: i2c_bus
|
||||
id: vnc_touchscreen
|
||||
display: vnc_touch_display
|
||||
|
||||
vnc:
|
||||
- id: vnc_touch_client
|
||||
display_id: vnc_touch_display
|
||||
touchscreen_id: vnc_touchscreen
|
||||
host: 192.0.2.11
|
||||
@@ -1,575 +0,0 @@
|
||||
#include "vnc_test_helpers.h"
|
||||
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
namespace esphome::vnc::testing {
|
||||
|
||||
static constexpr uint32_t KEYSYM_RETURN = 0xFF0D;
|
||||
static constexpr uint32_t KEYSYM_SHIFT_L = 0xFFE1;
|
||||
static constexpr uint32_t KEYSYM_CONTROL_L = 0xFFE3;
|
||||
|
||||
static touchscreen::TouchPoint point(uint8_t id, uint16_t x, uint16_t y, uint8_t state) {
|
||||
touchscreen::TouchPoint tp{};
|
||||
tp.id = id;
|
||||
tp.x = x;
|
||||
tp.y = y;
|
||||
tp.state = static_cast<int8_t>(state);
|
||||
return tp;
|
||||
}
|
||||
|
||||
/// One finger, reported to the listener the way Touchscreen does: update() then touch() on first contact
|
||||
/// (no update() when it lands on (0, 0), the new point's previous position), update() while it moves, and on
|
||||
/// lift update() with the point RELEASING, then release().
|
||||
class Finger {
|
||||
public:
|
||||
explicit Finger(VncClient &client) : client_(client) {}
|
||||
void at(uint16_t x, uint16_t y) {
|
||||
if (!this->down_) {
|
||||
this->tp_ = point(0, 0, 0, touchscreen::STATE_PRESSED);
|
||||
} else {
|
||||
this->tp_.state = touchscreen::STATE_UPDATED;
|
||||
}
|
||||
this->tp_.x_prev = this->tp_.x;
|
||||
this->tp_.y_prev = this->tp_.y;
|
||||
this->tp_.x = x;
|
||||
this->tp_.y = y;
|
||||
if (x != this->tp_.x_prev || y != this->tp_.y_prev) {
|
||||
this->client_.update({this->tp_});
|
||||
}
|
||||
if (!this->down_) {
|
||||
this->client_.touch(this->tp_);
|
||||
}
|
||||
this->down_ = true;
|
||||
}
|
||||
void lift() {
|
||||
ASSERT_TRUE(this->down_);
|
||||
this->tp_.state |= touchscreen::STATE_RELEASING;
|
||||
this->client_.update({this->tp_});
|
||||
this->client_.release();
|
||||
this->down_ = false;
|
||||
}
|
||||
|
||||
protected:
|
||||
VncClient &client_;
|
||||
touchscreen::TouchPoint tp_{};
|
||||
bool down_{false};
|
||||
};
|
||||
|
||||
class VncInput : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { this->begin(64, 48, 64, 48); }
|
||||
void begin(int disp_w, int disp_h, uint16_t fb_w, uint16_t fb_h) {
|
||||
this->f_.reset();
|
||||
this->s_.reset();
|
||||
this->d_ = std::make_unique<FakeDisplay>(disp_w, disp_h);
|
||||
this->s_ = std::make_unique<Session>(this->d_.get());
|
||||
this->s_->start(fb_w, fb_h);
|
||||
this->f_ = std::make_unique<Finger>(this->s_->client);
|
||||
}
|
||||
Session &s() { return *this->s_; }
|
||||
TestVncClient &c() { return this->s_->client; }
|
||||
Finger &f() { return *this->f_; }
|
||||
std::unique_ptr<FakeDisplay> d_;
|
||||
std::unique_ptr<Session> s_;
|
||||
std::unique_ptr<Finger> f_;
|
||||
};
|
||||
|
||||
TEST_F(VncInput, PointerEventBytes) {
|
||||
c().send_pointer(10, 20, 1);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 10, 20)));
|
||||
c().send_pointer(63, 47, 8 | 16);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(24, 63, 47)));
|
||||
c().send_pointer(1000, 1000, 0);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(0, 63, 47)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, KeyEventBytes) {
|
||||
c().send_key(KEYSYM_RETURN, VncKeyAction::VNC_KEY_ACTION_DOWN);
|
||||
EXPECT_EQ(hex(s().take()), hex({4, 1, 0, 0, 0, 0, 0xFF, 0x0D}));
|
||||
c().send_key(KEYSYM_RETURN, VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
EXPECT_EQ(hex(s().take()), hex({4, 0, 0, 0, 0, 0, 0xFF, 0x0D}));
|
||||
c().send_key(0x01000000 | 0x20AC, VncKeyAction::VNC_KEY_ACTION_PRESS);
|
||||
EXPECT_EQ(hex(s().take()), hex(press(0x010020AC)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TypeText) {
|
||||
c().type_text("aB\n", 3);
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press('a'), press('B'), press(KEYSYM_RETURN)})));
|
||||
c().type_text(std::string("\t\b"));
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press(0xFF09), press(0xFF08)})));
|
||||
// Other control characters have no key.
|
||||
c().type_text(std::string("x\x01y\x7F"));
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press('x'), press('y')})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TypeTextUtf8) {
|
||||
c().type_text(std::string("\xC3\xA9\xE2\x82\xAC"));
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press(0xE9), press(0x010020AC)})));
|
||||
c().type_text(std::string("\xF0\x9F\x98\x80"));
|
||||
EXPECT_EQ(hex(s().take()), hex(press(0x0101F600)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TypeTextSkipsBrokenUtf8) {
|
||||
// A lead byte without its continuation, a stray continuation, and a cut-off sequence.
|
||||
c().type_text(std::string("\xC3(\x80"
|
||||
"a\xE2\x82"));
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press('('), press('a')})));
|
||||
}
|
||||
|
||||
// A cut-off sequence ends at the first byte that is not a continuation byte, so "\xE2a" still types the 'a'.
|
||||
TEST_F(VncInput, TruncatedUtf8KeepsTheNextCharacter) {
|
||||
c().type_text(std::string("\xE2"
|
||||
"a"));
|
||||
EXPECT_EQ(hex(s().take()), hex(press('a')));
|
||||
c().type_text(std::string("x\xF0"
|
||||
"yz"));
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press('x'), press('y'), press('z')})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, InvalidUtf8ValuesAreSkipped) {
|
||||
// Overlong 'A', a surrogate, and a value above U+10FFFF; U+10FFFF itself is kept.
|
||||
c().type_text(std::string("\xC1\x81"
|
||||
"a\xED\xA0\x80"
|
||||
"b\xF4\x90\x80\x80"
|
||||
"c\xF4\x8F\xBF\xBF"));
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press('a'), press('b'), press('c'), press(0x0110FFFF)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, LengthCutsAMultiByteCharacter) {
|
||||
c().type_text("a\xC3\xA9", 2);
|
||||
EXPECT_EQ(hex(s().take()), hex(press('a')));
|
||||
c().type_text("\xC3\xA9", 1);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(VncInput, EmbeddedNulIsSkipped) {
|
||||
c().type_text("a\0b", 3);
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({press('a'), press('b')})));
|
||||
}
|
||||
|
||||
// --- A full send buffer never gets a partial message.
|
||||
|
||||
class VncInputFull : public VncInput {
|
||||
protected:
|
||||
/// Fills the socket, then the client's buffer up to `room` free bytes.
|
||||
void fill(size_t room) {
|
||||
uint8_t junk[1024];
|
||||
std::memset(junk, 0xEE, sizeof(junk));
|
||||
for (;;) {
|
||||
ssize_t n = ::write(s().client_fd, junk, sizeof(junk));
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
this->expected_.insert(this->expected_.end(), junk, junk + n);
|
||||
}
|
||||
while (::write(s().client_fd, junk, 1) == 1) {
|
||||
this->expected_.push_back(0xEE);
|
||||
}
|
||||
ASSERT_GE(c().link().tx_free(), room);
|
||||
Bytes filler(c().link().tx_free() - room, 0xDD);
|
||||
ASSERT_EQ(c().link().queue(filler.data(), filler.size()), filler.size());
|
||||
this->expected_.insert(this->expected_.end(), filler.begin(), filler.end());
|
||||
ASSERT_EQ(c().link().tx_free(), room);
|
||||
}
|
||||
/// Everything that reaches the server once it reads again.
|
||||
Bytes drain() {
|
||||
Bytes all;
|
||||
for (int idle = 0; idle < 3;) {
|
||||
Bytes more = s().take();
|
||||
all.insert(all.end(), more.begin(), more.end());
|
||||
idle = more.empty() ? idle + 1 : 0;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
Bytes expected_;
|
||||
};
|
||||
|
||||
TEST_F(VncInputFull, PointerDroppedWhole) {
|
||||
this->fill(5);
|
||||
c().send_pointer(1, 2, 1);
|
||||
c().send_key('a', VncKeyAction::VNC_KEY_ACTION_DOWN);
|
||||
EXPECT_EQ(hex(this->drain()), hex(this->expected_));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, PressNeedsRoomForBothHalves) {
|
||||
this->fill(15);
|
||||
c().send_key('a', VncKeyAction::VNC_KEY_ACTION_PRESS);
|
||||
c().send_key('b', VncKeyAction::VNC_KEY_ACTION_DOWN);
|
||||
EXPECT_EQ(hex(this->drain()), hex(cat({this->expected_, key(true, 'b')})));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, DroppedKeyUpIsSentOnceThereIsRoom) {
|
||||
this->fill(5);
|
||||
c().send_key(KEYSYM_SHIFT_L, VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
EXPECT_EQ(hex(this->drain()), hex(this->expected_));
|
||||
s().loop();
|
||||
EXPECT_EQ(hex(s().take()), hex(key(false, KEYSYM_SHIFT_L)));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, LaterInputWaitsForTheDroppedKeyUp) {
|
||||
this->fill(5);
|
||||
c().send_key(KEYSYM_SHIFT_L, VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
EXPECT_EQ(hex(this->drain()), hex(this->expected_));
|
||||
c().send_key('a', VncKeyAction::VNC_KEY_ACTION_PRESS);
|
||||
c().send_pointer(3, 4, 0);
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({key(false, KEYSYM_SHIFT_L), press('a'), pointer(0, 3, 4)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, ClickReleaseWaitsForRoom) {
|
||||
this->fill(6);
|
||||
c().send_pointer(5, 5, 1);
|
||||
c().send_pointer(5, 5, 0);
|
||||
EXPECT_EQ(hex(this->drain()), hex(cat({this->expected_, pointer(1, 5, 5)})));
|
||||
s().loop();
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(0, 5, 5)));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, ReleaseOverflowEndsTheSession) {
|
||||
this->fill(5);
|
||||
for (uint32_t k = 0; k < 4; k++) {
|
||||
c().send_key('a' + k, VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
}
|
||||
EXPECT_TRUE(c().is_connected());
|
||||
c().send_key('e', VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
EXPECT_FALSE(c().is_connected());
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, BothHeldModifiersAreReleased) {
|
||||
this->fill(5);
|
||||
c().send_key(KEYSYM_SHIFT_L, VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
c().send_key(KEYSYM_CONTROL_L, VncKeyAction::VNC_KEY_ACTION_UP);
|
||||
EXPECT_EQ(hex(this->drain()), hex(this->expected_));
|
||||
s().loop();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({key(false, KEYSYM_SHIFT_L), key(false, KEYSYM_CONTROL_L)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, TextStopsAtAWholeCharacter) {
|
||||
this->fill(40);
|
||||
c().type_text(std::string("abcd"));
|
||||
EXPECT_EQ(hex(this->drain()), hex(cat({this->expected_, press('a'), press('b')})));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, DroppedReleaseIsSentOnceThereIsRoom) {
|
||||
f().at(10, 10);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 10, 10)));
|
||||
this->fill(3);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(this->drain()), hex(this->expected_));
|
||||
s().loop();
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(0, 10, 10)));
|
||||
}
|
||||
|
||||
TEST_F(VncInputFull, TouchPressRetriedWhenDropped) {
|
||||
this->fill(3);
|
||||
f().at(10, 10);
|
||||
Bytes all = this->drain();
|
||||
EXPECT_EQ(hex(all), hex(this->expected_));
|
||||
f().at(11, 11);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 11, 11)));
|
||||
}
|
||||
|
||||
TEST(VncInputState, DroppedWhenNotRunning) {
|
||||
FakeDisplay d;
|
||||
Session s(&d);
|
||||
Finger f(s.client);
|
||||
s.attach();
|
||||
s.deliver(Rfb().version());
|
||||
s.take();
|
||||
s.client.send_pointer(1, 1, 1);
|
||||
s.client.send_key('a', VncKeyAction::VNC_KEY_ACTION_PRESS);
|
||||
s.client.type_text(std::string("abc"));
|
||||
f.at(5, 5);
|
||||
f.lift();
|
||||
s.loop();
|
||||
EXPECT_EQ(s.take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST(VncInputState, DroppedAfterTheSessionEnds) {
|
||||
FakeDisplay d;
|
||||
Session s(&d);
|
||||
Finger f(s.client);
|
||||
s.start(64, 48);
|
||||
f.at(5, 5);
|
||||
s.take();
|
||||
s.close_peer();
|
||||
s.loop();
|
||||
f.lift();
|
||||
s.client.send_pointer(1, 1, 1);
|
||||
|
||||
// The next session starts with no button held: the first touch is a press again.
|
||||
s.start(64, 48);
|
||||
f.at(7, 8);
|
||||
EXPECT_EQ(hex(s.take()), hex(pointer(1, 7, 8)));
|
||||
}
|
||||
|
||||
// --- Touch
|
||||
|
||||
TEST_F(VncInput, TouchPressIsSentAtOnce) {
|
||||
f().at(10, 12);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 10, 12)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchMovesInsideTheSlopAreSuppressed) {
|
||||
f().at(20, 20);
|
||||
s().take();
|
||||
f().at(25, 24);
|
||||
f().at(28, 12);
|
||||
f().at(12, 28);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
f().at(29, 20);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 29, 20)));
|
||||
// Once dragging, every change goes out, even back inside the slop.
|
||||
f().at(21, 20);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 21, 20)));
|
||||
f().at(21, 20);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TapReleasesWhereItPressed) {
|
||||
f().at(10, 10);
|
||||
f().at(13, 12);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 10, 10), pointer(0, 10, 10)})));
|
||||
c().release();
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(VncInput, DragReleasesAtTheLastPosition) {
|
||||
f().at(10, 10);
|
||||
f().at(30, 30);
|
||||
f().at(40, 35);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()),
|
||||
hex(cat({pointer(1, 10, 10), pointer(1, 30, 30), pointer(1, 40, 35), pointer(0, 40, 35)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, EachTapStartsANewSlop) {
|
||||
f().at(10, 10);
|
||||
f().at(40, 10);
|
||||
f().lift();
|
||||
s().take();
|
||||
f().at(50, 10);
|
||||
f().at(52, 10);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 50, 10), pointer(0, 50, 10)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchIgnoresButtonsLeftByAnAutomation) {
|
||||
c().send_pointer(5, 5, 8 | 4);
|
||||
s().take();
|
||||
f().at(10, 10);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 10, 10), pointer(0, 10, 10)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchPressesEvenWhileAnAutomationHoldsLeft) {
|
||||
c().send_pointer(5, 5, 1);
|
||||
s().take();
|
||||
f().at(3, 3);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 3, 3)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, PressOutsideTheVisibleAreaIsIgnored) {
|
||||
this->begin(64, 48, 100, 80);
|
||||
f().at(64, 10);
|
||||
f().at(10, 48);
|
||||
f().lift();
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
f().at(63, 47);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 63, 47)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, PressOutsideASmallerServerScreenIsIgnored) {
|
||||
this->begin(64, 48, 32, 20);
|
||||
f().at(40, 10);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
f().at(31, 19);
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 31, 19)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, DragIsClampedToTheVisibleArea) {
|
||||
this->begin(64, 48, 100, 80);
|
||||
f().at(50, 30);
|
||||
f().at(200, 300);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 50, 30), pointer(1, 63, 47), pointer(0, 63, 47)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, ClampedMoveInsideTheSlopStaysATap) {
|
||||
this->begin(64, 48, 100, 80);
|
||||
f().at(60, 40);
|
||||
f().at(200, 300);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 60, 40), pointer(0, 60, 40)})));
|
||||
}
|
||||
|
||||
// The client never scales: a touch at display x,y is a pointer event at framebuffer x,y.
|
||||
TEST_F(VncInput, TouchCoordinatesAreNotScaled) {
|
||||
this->begin(960, 540, 960, 540);
|
||||
const uint16_t points[][2] = {{0, 0}, {192, 108}, {480, 270}, {767, 431}, {900, 500}, {959, 539}, {959, 0}, {0, 539}};
|
||||
for (const auto &p : points) {
|
||||
f().at(p[0], p[1]);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, p[0], p[1]), pointer(0, p[0], p[1])}))) << p[0] << "," << p[1];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchCoordinatesAreNotScaledOnALargerServer) {
|
||||
this->begin(960, 540, 1280, 720);
|
||||
f().at(900, 500);
|
||||
f().at(950, 530);
|
||||
f().lift();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 900, 500), pointer(1, 950, 530), pointer(0, 950, 530)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, FirstTouchAtTheOriginComesOnlyThroughTouch) {
|
||||
c().touch(point(0, 0, 0, touchscreen::STATE_PRESSED));
|
||||
c().release();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 0, 0), pointer(0, 0, 0)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchSkipsALiftedPoint) {
|
||||
c().touch(point(0, 5, 5, touchscreen::STATE_PRESSED | touchscreen::STATE_RELEASING));
|
||||
c().touch(point(0, 5, 5, touchscreen::STATE_RELEASED));
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchListenerSkipsStalePoints) {
|
||||
// Finger 0 was lifted earlier and lingers as RELEASED.
|
||||
c().update({point(0, 1, 1, touchscreen::STATE_RELEASED), point(1, 20, 21, touchscreen::STATE_PRESSED)});
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 20, 21)));
|
||||
// Finger 0 is being lifted now (still flagged PRESSED as well).
|
||||
c().update({point(0, 5, 5, touchscreen::STATE_PRESSED | touchscreen::STATE_RELEASING),
|
||||
point(1, 40, 21, touchscreen::STATE_UPDATED)});
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(1, 40, 21)));
|
||||
// A point from an uncalibrated touchscreen.
|
||||
c().update({point(0, 3, 3, touchscreen::STATE_CALIBRATE)});
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
c().update({});
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
c().release();
|
||||
EXPECT_EQ(hex(s().take()), hex(pointer(0, 40, 21)));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchListenerUsesTheFirstActivePoint) {
|
||||
c().update({point(0, 10, 10, touchscreen::STATE_PRESSED), point(1, 50, 40, touchscreen::STATE_PRESSED)});
|
||||
c().update({point(0, 10, 11, touchscreen::STATE_UPDATED), point(1, 60, 40, touchscreen::STATE_UPDATED)});
|
||||
c().release();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 10, 10), pointer(0, 10, 10)})));
|
||||
}
|
||||
|
||||
TEST_F(VncInput, TouchListenerPassesCoordinatesThrough) {
|
||||
this->begin(960, 540, 960, 540);
|
||||
c().update({point(0, 767, 431, touchscreen::STATE_PRESSED)});
|
||||
c().update({point(0, 192, 108, touchscreen::STATE_UPDATED)});
|
||||
c().release();
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 767, 431), pointer(1, 192, 108), pointer(0, 192, 108)})));
|
||||
}
|
||||
|
||||
// --- Through the real touchscreen state machine: raw controller points to pointer events.
|
||||
|
||||
class FakeTouchscreen : public touchscreen::Touchscreen {
|
||||
public:
|
||||
struct Raw {
|
||||
uint8_t id;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
};
|
||||
void attach(display::Display *display) {
|
||||
this->set_display(display);
|
||||
// What Touchscreen::call_setup() does.
|
||||
this->display_width_ = display->get_width();
|
||||
this->display_height_ = display->get_height();
|
||||
}
|
||||
/// One controller read with these fingers down, then the deferred listener calls.
|
||||
void report(std::vector<Raw> points) {
|
||||
this->points_ = std::move(points);
|
||||
this->store_.touched = true;
|
||||
this->loop();
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
protected:
|
||||
void update_touches() override {
|
||||
this->skip_update_ = false;
|
||||
for (const auto &p : this->points_) {
|
||||
this->add_raw_touch_position_(p.id, p.x, p.y);
|
||||
}
|
||||
}
|
||||
std::vector<Raw> points_;
|
||||
};
|
||||
|
||||
/// The display position the touchscreen component computes for a raw value.
|
||||
static uint16_t mapped(int raw, int raw_max, int size) {
|
||||
const int normalized = raw <= 0 ? 0 : (raw >= raw_max ? 0xfff : 0xfff * raw / raw_max);
|
||||
return static_cast<uint16_t>(normalized * size / 0x1000);
|
||||
}
|
||||
|
||||
class VncTouchscreen : public VncInput {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// The test main does not construct App; the touchscreen defers its listener calls to the scheduler.
|
||||
static const bool APP_CONSTRUCTED = (new (&App) Application(), true);
|
||||
(void) APP_CONSTRUCTED;
|
||||
this->begin(960, 540, 960, 540);
|
||||
this->ts_.set_calibration(0, 960, 0, 540);
|
||||
this->ts_.attach(this->d_.get());
|
||||
this->c().register_touchscreen(&this->ts_);
|
||||
}
|
||||
FakeTouchscreen ts_;
|
||||
};
|
||||
|
||||
TEST_F(VncTouchscreen, TapsLandWhereTheFingerIs) {
|
||||
const int16_t taps[][2] = {{10, 10}, {100, 50}, {480, 270}, {700, 400}, {900, 500}, {955, 535}};
|
||||
for (const auto &t : taps) {
|
||||
this->ts_.report({{0, t[0], t[1]}});
|
||||
this->ts_.report({});
|
||||
const uint16_t x = mapped(t[0], 960, 960);
|
||||
const uint16_t y = mapped(t[1], 540, 540);
|
||||
EXPECT_LE(std::abs(x - t[0]), 1);
|
||||
EXPECT_LE(std::abs(y - t[1]), 1);
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, x, y), pointer(0, x, y)}))) << t[0] << "," << t[1];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VncTouchscreen, TapAtTheOrigin) {
|
||||
this->ts_.report({{0, 0, 0}});
|
||||
this->ts_.report({});
|
||||
EXPECT_EQ(hex(s().take()), hex(cat({pointer(1, 0, 0), pointer(0, 0, 0)})));
|
||||
}
|
||||
|
||||
TEST_F(VncTouchscreen, DragFollowsTheFinger) {
|
||||
this->ts_.report({{0, 100, 100}});
|
||||
this->ts_.report({{0, 104, 103}});
|
||||
this->ts_.report({{0, 300, 200}});
|
||||
this->ts_.report({{0, 600, 400}});
|
||||
this->ts_.report({});
|
||||
const uint16_t x0 = mapped(100, 960, 960), y0 = mapped(100, 540, 540);
|
||||
const uint16_t x1 = mapped(300, 960, 960), y1 = mapped(200, 540, 540);
|
||||
const uint16_t x2 = mapped(600, 960, 960), y2 = mapped(400, 540, 540);
|
||||
EXPECT_EQ(hex(s().take()),
|
||||
hex(cat({pointer(1, x0, y0), pointer(1, x1, y1), pointer(1, x2, y2), pointer(0, x2, y2)})));
|
||||
}
|
||||
|
||||
TEST_F(VncTouchscreen, LiftedFirstFingerDoesNotPullThePointerBack) {
|
||||
this->ts_.report({{0, 100, 100}});
|
||||
this->ts_.report({{0, 100, 100}, {1, 500, 300}});
|
||||
// Finger 0 lifts; it lingers in the list as RELEASING, then RELEASED.
|
||||
this->ts_.report({{1, 510, 300}});
|
||||
this->ts_.report({{1, 520, 300}});
|
||||
this->ts_.report({});
|
||||
Bytes tx = s().take();
|
||||
ASSERT_GE(tx.size(), 12u);
|
||||
const Bytes release(tx.end() - 6, tx.end());
|
||||
EXPECT_EQ(hex(release), hex(pointer(0, mapped(520, 960, 960), mapped(300, 540, 540))));
|
||||
for (size_t i = 0; i + 6 <= tx.size(); i += 6) {
|
||||
const uint16_t x = (tx[i + 2] << 8) | tx[i + 3];
|
||||
EXPECT_TRUE(x == mapped(100, 960, 960) || x >= mapped(500, 960, 960)) << "event at x=" << x;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::vnc::testing
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -125,59 +125,6 @@ TEST(VncPacingDelays, ZeroDelayRefreshesOnTheNextPass) {
|
||||
EXPECT_EQ(d.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, InputPaintsWithoutWaitingForTheScreenToSettle) {
|
||||
set_now(1100);
|
||||
s().client.send_pointer(5, 5, 1);
|
||||
s().deliver(frame());
|
||||
s().loop();
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
// Every frame during the boost is painted on the next pass.
|
||||
set_now(1150);
|
||||
s().deliver(frame(10, 0x4321));
|
||||
s().loop();
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, KeysAndTextAlsoBoost) {
|
||||
set_now(1100);
|
||||
s().client.send_key(0xFF0D, VncKeyAction::VNC_KEY_ACTION_PRESS);
|
||||
s().deliver(frame());
|
||||
s().loop();
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
set_now(4000);
|
||||
s().client.type_text("a", 1);
|
||||
s().deliver(frame(10, 0x4321));
|
||||
s().loop();
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, BoostEndsTwoSecondsAfterTheLastInput) {
|
||||
set_now(1100);
|
||||
s().client.send_pointer(5, 5, 0);
|
||||
set_now(3100);
|
||||
s().deliver(frame());
|
||||
s().loop();
|
||||
EXPECT_EQ(d_.updates(), 0);
|
||||
s().at(3299);
|
||||
EXPECT_EQ(d_.updates(), 0);
|
||||
s().at(3300);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, DroppedInputDoesNotBoost) {
|
||||
s().close_peer();
|
||||
s().loop(2);
|
||||
ASSERT_FALSE(s().client.is_connected());
|
||||
set_now(1100);
|
||||
s().client.send_pointer(5, 5, 1);
|
||||
s().start(64, 48);
|
||||
s().deliver(frame());
|
||||
s().loop();
|
||||
EXPECT_EQ(d_.updates(), 0);
|
||||
s().at(1300);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, NextRequestRightAfterEachFrameWithA50msFloor) {
|
||||
set_now(1010);
|
||||
s().deliver(frame());
|
||||
|
||||
@@ -8,15 +8,6 @@ interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- vnc.refresh
|
||||
- vnc.send_pointer:
|
||||
x: 10
|
||||
y: 20
|
||||
- vnc.send_key: Return
|
||||
- vnc.send_key: 0xFF1B
|
||||
- vnc.send_key:
|
||||
key: Shift_L
|
||||
action: down
|
||||
- vnc.type_text: "Hello\n"
|
||||
- if:
|
||||
condition: vnc.is_connected
|
||||
then:
|
||||
|
||||
@@ -365,9 +365,6 @@ inline Bytes handshake_38(uint16_t w, uint16_t h) {
|
||||
inline Bytes fbur(bool incremental, uint16_t w, uint16_t h) {
|
||||
return Rfb().u8(3).u8(incremental ? 1 : 0).u16(0).u16(0).u16(w).u16(h).bytes;
|
||||
}
|
||||
inline Bytes pointer(uint8_t mask, uint16_t x, uint16_t y) { return Rfb().u8(5).u8(mask).u16(x).u16(y).bytes; }
|
||||
inline Bytes key(bool down, uint32_t keysym) { return Rfb().u8(4).u8(down ? 1 : 0).u16(0).u32(keysym).bytes; }
|
||||
inline Bytes press(uint32_t keysym) { return cat({key(true, keysym), key(false, keysym)}); }
|
||||
inline Bytes set_pixel_format_565() { return {0, 0, 0, 0, 16, 16, 0, 1, 0, 31, 0, 63, 0, 31, 11, 5, 0, 0, 0, 0}; }
|
||||
inline Bytes set_pixel_format_332() { return {0, 0, 0, 0, 8, 8, 0, 1, 0, 7, 0, 7, 0, 3, 5, 2, 0, 0, 0, 0}; }
|
||||
inline Bytes set_encodings() { return {2, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x11}; }
|
||||
|
||||
@@ -2,7 +2,6 @@ wifi:
|
||||
dpp:
|
||||
channels: [1, 6, 11]
|
||||
device_info: ESPHome
|
||||
private_key: 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff
|
||||
timeout: 30s
|
||||
on_dpp_uri:
|
||||
- logger.log:
|
||||
|
||||
@@ -16,7 +16,6 @@ import re
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
@@ -29,7 +28,6 @@ import pytest_asyncio
|
||||
import esphome.config
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import (
|
||||
get_bool_env,
|
||||
get_usable_cpu_count,
|
||||
read_file,
|
||||
rmtree,
|
||||
@@ -41,7 +39,6 @@ from esphome.host.toolchain import get_elf_path
|
||||
from .const import (
|
||||
API_CONNECTION_TIMEOUT,
|
||||
DEFAULT_API_PORT,
|
||||
GDB_TIMEOUT,
|
||||
LOCALHOST,
|
||||
PORT_POLL_INTERVAL,
|
||||
PORT_WAIT_TIMEOUT,
|
||||
@@ -83,13 +80,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
# CI caches parts of this path; keep in sync with ci.yml integration-tests.
|
||||
INTEGRATION_TESTS_ROOT = Path.home() / ".esphome-integration-tests"
|
||||
|
||||
# Opt-in -g and gdb core backtraces; CI enables both only to re-run failed tests
|
||||
DEBUG_SYMBOLS = get_bool_env("ESPHOME_INTEGRATION_DEBUG_SYMBOLS")
|
||||
CORE_DIR = os.environ.get("ESPHOME_INTEGRATION_CORE_DIR")
|
||||
_CRASH_SIGNALS = frozenset(
|
||||
{signal.SIGABRT, signal.SIGBUS, signal.SIGFPE, signal.SIGILL, signal.SIGSEGV}
|
||||
)
|
||||
|
||||
|
||||
def _get_build_env() -> dict[str, str]:
|
||||
"""Environment for an ``esphome compile`` subprocess."""
|
||||
@@ -237,12 +227,9 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
|
||||
" build_flags:\n"
|
||||
' - "-DDEBUG" # Enable assert() statements\n'
|
||||
' - "-DESPHOME_DEBUG" # Enable ESPHOME_DEBUG_ASSERT checks\n'
|
||||
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts'
|
||||
+ ('\n - "-g" # Add debug symbols' if DEBUG_SYMBOLS else ""),
|
||||
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n'
|
||||
' - "-g" # Add debug symbols',
|
||||
)
|
||||
elif DEBUG_SYMBOLS and " build_flags:\n" in content:
|
||||
# A fixture with its own flags still gets symbols for a debug re-run
|
||||
content = content.replace(" build_flags:\n", ' build_flags:\n - "-g"\n', 1)
|
||||
|
||||
# Replace external component path placeholder if present
|
||||
if "EXTERNAL_COMPONENT_PATH" in content:
|
||||
@@ -785,46 +772,16 @@ async def _read_stream_lines(
|
||||
line_callback(decoded_line.rstrip())
|
||||
|
||||
|
||||
def _print_crash_backtrace(binary_path: Path, returncode: int, pid: int) -> None:
|
||||
"""Print a gdb backtrace from the core of a binary that died from a crash signal."""
|
||||
if CORE_DIR is None or -returncode not in _CRASH_SIGNALS:
|
||||
return
|
||||
core = Path(CORE_DIR) / f"core.{pid}"
|
||||
if not core.is_file() or (gdb := shutil.which("gdb")) is None:
|
||||
print(f"No core or gdb for the crash of {binary_path} (looked for {core})")
|
||||
return
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[gdb, "-batch", "-iex", "set debuginfod enabled off"]
|
||||
+ ["-ex", "thread apply all bt", str(binary_path), str(core)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
errors="replace",
|
||||
timeout=GDB_TIMEOUT,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as err:
|
||||
# Best effort: never replace the test's own failure
|
||||
print(f"gdb could not read {core}: {err}")
|
||||
return
|
||||
print(f"--- Backtrace of {binary_path} ({core}) ---")
|
||||
print(result.stdout + result.stderr)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def run_binary(
|
||||
binary_path: Path,
|
||||
line_callback: Callable[[str], None] | None = None,
|
||||
symbols_path: Path | None = None,
|
||||
) -> AsyncGenerator[tuple[asyncio.subprocess.Process, list[str]]]:
|
||||
"""Run a binary under a PTY, capture log output, and clean up on exit.
|
||||
|
||||
Yields the running ``Process`` and a live list of captured log lines.
|
||||
No port wait -- callers that need that should use
|
||||
``run_binary_and_wait_for_port``.
|
||||
``symbols_path`` is the unstripped build when ``binary_path`` is a stripped
|
||||
copy; a crash backtrace reads its symbols from it.
|
||||
"""
|
||||
``run_binary_and_wait_for_port``."""
|
||||
# Create a pseudo-terminal to make the binary think it's running interactively
|
||||
# This is needed because the ESPHome host logger checks isatty()
|
||||
controller_fd, device_fd = pty.openpty()
|
||||
@@ -853,9 +810,6 @@ async def run_binary(
|
||||
)
|
||||
|
||||
if process.returncode is not None:
|
||||
_print_crash_backtrace(
|
||||
symbols_path or binary_path, process.returncode, process.pid
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Process died immediately with return code {process.returncode}. "
|
||||
"Ensure the binary is valid and can run successfully."
|
||||
@@ -897,9 +851,6 @@ async def run_binary(
|
||||
# Last resort: SIGKILL
|
||||
process.kill()
|
||||
await process.wait()
|
||||
_print_crash_backtrace(
|
||||
symbols_path or binary_path, process.returncode, process.pid
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
@@ -19,4 +19,3 @@ KEY_ACTIVATION_DELAY = 0.5 # seconds
|
||||
# Process shutdown timeouts
|
||||
SIGINT_TIMEOUT = 5.0 # seconds
|
||||
SIGTERM_TIMEOUT = 2.0 # seconds
|
||||
GDB_TIMEOUT = 60.0 # seconds, reading a crashed binary's core
|
||||
@@ -1,26 +0,0 @@
|
||||
esphome:
|
||||
name: copy-cover-toggle-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
cover:
|
||||
- platform: template
|
||||
name: "Source Cover"
|
||||
id: source_cover
|
||||
toggle_action:
|
||||
- cover.template.publish:
|
||||
id: source_cover
|
||||
position: 0.5
|
||||
- platform: copy
|
||||
name: "Copy Cover"
|
||||
id: copy_cover
|
||||
source_id: source_cover
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Toggle Copy"
|
||||
id: toggle_copy
|
||||
on_press:
|
||||
- cover.toggle: copy_cover
|
||||
@@ -1,27 +0,0 @@
|
||||
esphome:
|
||||
name: copy-lock-open-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
lock:
|
||||
- platform: template
|
||||
name: "Source Lock"
|
||||
id: source_lock
|
||||
lock_action:
|
||||
- lock.template.publish:
|
||||
id: source_lock
|
||||
state: LOCKED
|
||||
unlock_action:
|
||||
- lock.template.publish:
|
||||
id: source_lock
|
||||
state: UNLOCKED
|
||||
open_action:
|
||||
- lock.template.publish:
|
||||
id: source_lock
|
||||
state: OPEN
|
||||
- platform: copy
|
||||
name: "Copy Lock"
|
||||
id: copy_lock
|
||||
source_id: source_lock
|
||||
@@ -1,27 +0,0 @@
|
||||
esphome:
|
||||
name: cover-position-cancel-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
cover:
|
||||
- platform: template
|
||||
name: "Test Cover"
|
||||
id: test_cover
|
||||
position_action:
|
||||
- delay: 1s
|
||||
- cover.template.publish:
|
||||
id: test_cover
|
||||
position: !lambda "return pos;"
|
||||
open_action:
|
||||
- cover.template.publish:
|
||||
id: test_cover
|
||||
state: OPEN
|
||||
close_action:
|
||||
- cover.template.publish:
|
||||
id: test_cover
|
||||
state: CLOSED
|
||||
# Only there so that the cover supports toggle
|
||||
toggle_action:
|
||||
- logger.log: "Toggle"
|
||||
@@ -1,96 +0,0 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-select-mode
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: false
|
||||
responses:
|
||||
# Firmware v2.0.0 (1.5.4 or newer), so setup picks energy reporting
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Generic ACK for every other command
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Sent after the select switched the module to Simple: "ON Range 0035\r\n" is only
|
||||
# parsed while the component expects simple frames.
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x30, 0x33, 0x35,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
# A changed timeout gets apply_config_action() past its no-change check
|
||||
- platform: template
|
||||
name: "Apply Changed Config"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
id(ld2420_dev).new_config.timeout++;
|
||||
id(ld2420_dev).apply_config_action();
|
||||
- platform: template
|
||||
name: "Inject Simple Frame"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
// "ON Range 0040\r\n"
|
||||
id(mock_uart).inject_to_rx_buffer(std::vector<uint8_t>{
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20, 0x30, 0x30, 0x34, 0x30, 0x0D, 0x0A});
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
|
||||
select:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
operating_mode:
|
||||
name: "Operating Mode"
|
||||
@@ -1,73 +0,0 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-mode-nack
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
responses:
|
||||
# Firmware v2.0.0 (1.5.4 or newer), so setup picks energy reporting
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# The simple system mode write is refused (error word 1)
|
||||
- expect_tx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00,
|
||||
0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x01, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Generic ACK for every other command
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
select:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
operating_mode:
|
||||
name: "Operating Mode"
|
||||
@@ -1,41 +0,0 @@
|
||||
esphome:
|
||||
name: uart-mock-pipsolar-switches
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy pipsolar's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 2400
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 2400
|
||||
auto_start: false
|
||||
|
||||
# No polling, so only the switch writes reach the command queue and no
|
||||
# polled status publishes the switch states
|
||||
pipsolar:
|
||||
id: inverter0
|
||||
uart_id: mock_uart
|
||||
update_interval: never
|
||||
|
||||
switch:
|
||||
- platform: pipsolar
|
||||
pipsolar_id: inverter0
|
||||
output_source_priority_utility:
|
||||
name: Priority Utility
|
||||
input_voltage_range:
|
||||
name: Input Voltage Range
|
||||
pv_power_balance:
|
||||
name: PV Power Balance
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user