Compare commits

..

2 Commits

261 changed files with 1423 additions and 4657 deletions

View File

@@ -1 +1 @@
d15ae81646ac0ee76b2586716fe697f187281523ee6db566aed26542a9f98d1a d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65

View File

@@ -1,96 +0,0 @@
---
name: pr-workflow
description: Create pull requests for esphome. Use when creating PRs, submitting changes, or preparing contributions.
allowed-tools: Read, Bash, Glob, Grep
---
# ESPHome PR Workflow
When creating a pull request for esphome, follow these steps:
## 1. Create Branch from Upstream
Always base your branch on **upstream** (not origin/fork) to ensure you have the latest code:
```bash
git fetch upstream
git checkout -b <branch-name> upstream/dev
```
## 2. Read the PR Template
Before creating a PR, read `.github/PULL_REQUEST_TEMPLATE.md` to understand required fields.
## 3. Create the PR
Use `gh pr create` with the **full template** filled in. Never skip or abbreviate sections.
Required fields:
- **What does this implement/fix?**: Brief description of changes
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
- **Related issue**: Use `fixes <link>` syntax if applicable
- **Pull request in esphome-docs**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
## 4. Example PR Body
```markdown
# What does this implement/fix?
<describe your changes here>
## Types of changes
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
**Related issue or feature (if applicable):**
- fixes https://github.com/esphome/esphome/issues/XXX
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome-docs#XXX
## Test Environment
- [x] ESP32
- [x] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x
- [ ] nRF52840
## Example entry for `config.yaml`:
```yaml
# Example config.yaml
component_name:
id: my_component
option: value
```
## Checklist:
- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
```
## 5. Push and Create PR
```bash
git push -u origin <branch-name>
gh pr create --repo esphome/esphome --base dev --title "[component] Brief description"
```
Title should be prefixed with the component name in brackets, e.g. `[safe_mode] Add feature`.

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -157,7 +157,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -193,7 +193,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache - name: Restore components graph cache
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache - name: Save components graph cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis - name: Restore cached memory analysis
id: cache-memory-analysis id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache - name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -847,7 +847,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.13 rev: v0.14.11
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -1,6 +1,5 @@
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
import argparse import argparse
from collections.abc import Callable
from datetime import datetime from datetime import datetime
import functools import functools
import getpass import getpass
@@ -43,7 +42,6 @@ from esphome.const import (
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
CONF_TOPIC, CONF_TOPIC,
ENV_NOGITIGNORE, ENV_NOGITIGNORE,
KEY_NATIVE_IDF,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@@ -117,7 +115,6 @@ class ArgsProtocol(Protocol):
configuration: str configuration: str
name: str name: str
upload_speed: str | None upload_speed: str | None
native_idf: bool
def choose_prompt(options, purpose: str = None): def choose_prompt(options, purpose: str = None):
@@ -225,13 +222,8 @@ def choose_upload_log_host(
else: else:
resolved.append(device) resolved.append(device)
if not resolved: if not resolved:
if CORE.dashboard:
hint = "If you know the IP, set 'use_address' in your network config."
else:
hint = "If you know the IP, try --device <IP>"
raise EsphomeError( raise EsphomeError(
f"All specified devices {defaults} could not be resolved. " f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
f"Is the device connected to the network? {hint}"
) )
return resolved return resolved
@@ -502,15 +494,12 @@ def wrap_to_code(name, comp):
return wrapped return wrapped
def write_cpp(config: ConfigType, native_idf: bool = False) -> int: def write_cpp(config: ConfigType) -> int:
if not get_bool_env(ENV_NOGITIGNORE): if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore() writer.write_gitignore()
# Store native_idf flag so esp32 component can check it
CORE.data[KEY_NATIVE_IDF] = native_idf
generate_cpp_contents(config) generate_cpp_contents(config)
return write_cpp_file(native_idf=native_idf) return write_cpp_file()
def generate_cpp_contents(config: ConfigType) -> None: def generate_cpp_contents(config: ConfigType) -> None:
@@ -524,54 +513,32 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks() CORE.flush_tasks()
def write_cpp_file(native_idf: bool = False) -> int: def write_cpp_file() -> int:
code_s = indent(CORE.cpp_main_section) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) writer.write_cpp(code_s)
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf": from esphome.build_gen import platformio
from esphome.build_gen import espidf
espidf.write_project() platformio.write_project()
else:
from esphome.build_gen import platformio
platformio.write_project()
return 0 return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int: def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
native_idf = getattr(args, "native_idf", False) from esphome import platformio_api
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well # If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path) _LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf": if rc != 0:
from esphome import espidf_api return rc
rc = espidf_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Create factory.bin and ota.bin
espidf_api.create_factory_bin()
espidf_api.create_ota_bin()
else:
from esphome import platformio_api
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
idedata = platformio_api.get_idedata(config)
if idedata is None:
return 1
# Check if firmware was rebuilt and emit build_info + create manifest # Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info() _check_and_emit_build_info()
return 0 idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None: def _check_and_emit_build_info() -> None:
@@ -828,8 +795,7 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
native_idf = getattr(args, "native_idf", False) exit_code = write_cpp(config)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
if args.only_generate: if args.only_generate:
@@ -884,8 +850,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
native_idf = getattr(args, "native_idf", False) exit_code = write_cpp(config)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
exit_code = compile_program(args, config) exit_code = compile_program(args, config)
@@ -966,21 +931,11 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
return dashboard.start_dashboard(args) return dashboard.start_dashboard(args)
def run_multiple_configs( def command_update_all(args: ArgsProtocol) -> int | None:
files: list, command_builder: Callable[[str], list[str]]
) -> int:
"""Run a command for each configuration file in a subprocess.
Args:
files: List of configuration files to process.
command_builder: Callable that takes a file path and returns a command list.
Returns:
Number of failed files.
"""
import click import click
success = {} success = {}
files = list_yaml_files(args.configuration)
twidth = 60 twidth = 60
def print_bar(middle_text): def print_bar(middle_text):
@@ -990,19 +945,17 @@ def run_multiple_configs(
safe_print(f"{half_line}{middle_text}{half_line}") safe_print(f"{half_line}{middle_text}{half_line}")
for f in files: for f in files:
f_path = Path(f) if not isinstance(f, Path) else f safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
if any(f_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", f_path)
continue
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
safe_print("-" * twidth) safe_print("-" * twidth)
safe_print() safe_print()
if CORE.dashboard:
cmd = command_builder(f) rc = run_external_process(
rc = run_external_process(*cmd) "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
else:
rc = run_external_process(
"esphome", "run", f, "--no-logs", "--device", "OTA"
)
if rc == 0: if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}") print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
success[f] = True success[f] = True
@@ -1017,8 +970,6 @@ def run_multiple_configs(
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]") print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0 failed = 0
for f in files: for f in files:
if f not in success:
continue # Skipped file
if success[f]: if success[f]:
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}") safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else: else:
@@ -1027,17 +978,6 @@ def run_multiple_configs(
return failed return failed
def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json import json
@@ -1339,11 +1279,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.", help="Only generate source code, do not compile.",
action="store_true", action="store_true",
) )
parser_compile.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_upload = subparsers.add_parser( parser_upload = subparsers.add_parser(
"upload", "upload",
@@ -1425,11 +1360,6 @@ def parse_args(argv):
help="Reset the device before starting serial logs.", help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
) )
parser_run.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_clean = subparsers.add_parser( parser_clean = subparsers.add_parser(
"clean-mqtt", "clean-mqtt",
@@ -1598,48 +1528,38 @@ def run_esphome(argv):
_LOGGER.info("ESPHome %s", const.__version__) _LOGGER.info("ESPHome %s", const.__version__)
# Multiple configurations: use subprocesses to avoid state leakage for conf_path in args.configuration:
# between compilations (e.g., LVGL touchscreen state in module globals) conf_path = Path(conf_path)
if len(args.configuration) > 1: if any(conf_path.name == x for x in SECRETS_FILES):
# Build command by reusing argv, replacing all configs with single file _LOGGER.warning("Skipping secrets file %s", conf_path)
# argv[0] is the program path, skip it since we prefix with "esphome" continue
def build_command(f):
return (
["esphome"]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)
return run_multiple_configs(args.configuration, build_command) CORE.config_path = conf_path
CORE.dashboard = args.dashboard
# Single configuration # For logs command, skip updating external components
conf_path = Path(args.configuration[0]) skip_external = args.command == "logs"
if any(conf_path.name == x for x in SECRETS_FILES): config = read_config(
_LOGGER.warning("Skipping secrets file %s", conf_path) dict(args.substitution) if args.substitution else {},
return 0 skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
CORE.config_path = conf_path if args.command not in POST_CONFIG_ACTIONS:
CORE.dashboard = args.dashboard safe_print(f"Unknown command {args.command}")
# For logs command, skip updating external components try:
skip_external = args.command == "logs" rc = POST_CONFIG_ACTIONS[args.command](args, config)
config = read_config( except EsphomeError as e:
dict(args.substitution) if args.substitution else {}, _LOGGER.error(e, exc_info=args.verbose)
skip_external_update=skip_external, return 1
) if rc != 0:
if config is None: return rc
return 2
CORE.config = config
if args.command not in POST_CONFIG_ACTIONS: CORE.reset()
safe_print(f"Unknown command {args.command}") return 0
return 1
try:
return POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
def main(): def main():

View File

@@ -22,7 +22,7 @@ from .helpers import (
map_section_name, map_section_name,
parse_symbol_line, parse_symbol_line,
) )
from .toolchain import find_tool, resolve_tool_path, run_tool from .toolchain import find_tool, run_tool
if TYPE_CHECKING: if TYPE_CHECKING:
from esphome.platformio_api import IDEData from esphome.platformio_api import IDEData
@@ -132,12 +132,6 @@ class MemoryAnalyzer:
readelf_path = readelf_path or idedata.readelf_path readelf_path = readelf_path or idedata.readelf_path
_LOGGER.debug("Using toolchain paths from PlatformIO idedata") _LOGGER.debug("Using toolchain paths from PlatformIO idedata")
# Validate paths exist, fall back to find_tool if they don't
# This handles cases like Zephyr where cc_path doesn't include full path
# and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-)
objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path)
readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path)
self.objdump_path = objdump_path or "objdump" self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf" self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set() self.external_components = external_components or set()

View File

@@ -9,61 +9,11 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Maps standard section names to their various platform-specific variants # Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram) # Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise # because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
#
# Platform-specific sections:
# - ESP8266/ESP32: .iram*, .dram*
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
SECTION_MAPPING = { SECTION_MAPPING = {
".text": frozenset( ".text": frozenset([".text", ".iram"]),
[ ".rodata": frozenset([".rodata"]),
".text", ".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
".iram", ".data": frozenset([".data", ".dram"]),
# LibreTiny RTL87xx XIP (eXecute In Place) flash code
".xip.code",
# LibreTiny RTL87xx RAM code
".ram.code_text",
# LibreTiny BK7231 fast RAM code and vectors
".itcm.code",
".vectors",
# LibreTiny LN882X flash code
".flash_text",
".flash_copy",
# Zephyr/nRF52 sections (no leading dots)
"text",
"rom_start",
]
),
".rodata": frozenset(
[
".rodata",
# LibreTiny RTL87xx read-only data in RAM
".ram.code_rodata",
# Zephyr/nRF52 sections (no leading dots)
"rodata",
]
),
# .bss patterns - must be before .data to catch ".dram0.bss"
".bss": frozenset(
[
".bss",
# LibreTiny LN882X BSS
".bss_ram",
# Zephyr/nRF52 sections (no leading dots)
"bss",
"noinit",
]
),
".data": frozenset(
[
".data",
".dram",
# Zephyr/nRF52 sections (no leading dots)
"datas",
]
),
} }
# Section to ComponentMemory attribute mapping # Section to ComponentMemory attribute mapping

View File

@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
return None return None
# Find section, size, and name # Find section, size, and name
# Try each part as a potential section name
for i, part in enumerate(parts): for i, part in enumerate(parts):
# Skip parts that are clearly flags, addresses, or other metadata if not part.startswith("."):
# Sections start with '.' (standard ELF) or are known section names (Zephyr) continue
section = map_section_name(part) section = map_section_name(part)
if not section: if not section:
continue break
# Need at least size field after section # Need at least size field after section
if i + 1 >= len(parts): if i + 1 >= len(parts):

View File

@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -18,82 +17,10 @@ TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266 "xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32 "xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF) "xtensa-esp-elf-", # ESP32 (newer IDF)
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
"", # System default (no prefix) "", # System default (no prefix)
] ]
def _find_in_platformio_packages(tool_name: str) -> str | None:
"""Search for a tool in PlatformIO package directories.
This handles cases like Zephyr SDK where tools are installed in nested
directories that aren't in PATH.
Args:
tool_name: Name of the tool (e.g., "readelf", "objdump")
Returns:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
if not platformio_home.exists():
return None
# Search patterns for toolchains that might contain the tool
# Order matters - more specific patterns first
search_patterns = [
# Zephyr SDK deeply nested structure (4 levels)
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
f"toolchain-*/*/*/bin/*-{tool_name}",
# Zephyr SDK nested structure (3 levels)
f"toolchain-*/*/bin/*-{tool_name}",
f"toolchain-*/bin/*-{tool_name}",
# Standard PlatformIO toolchain structure
f"toolchain-*/bin/*{tool_name}",
]
for pattern in search_patterns:
matches = list(platformio_home.glob(pattern))
if matches:
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
tool_path = str(matches[0])
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
return tool_path
return None
def resolve_tool_path(
tool_name: str,
derived_path: str | None,
objdump_path: str | None = None,
) -> str | None:
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
Args:
tool_name: Name of the tool (e.g., "objdump", "readelf")
derived_path: Path derived from idedata (may not exist for some platforms)
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Resolved path to the tool, or the original derived_path if it exists
"""
if derived_path and not Path(derived_path).exists():
found = find_tool(tool_name, objdump_path)
if found:
_LOGGER.debug(
"Derived %s path %s not found, using %s",
tool_name,
derived_path,
found,
)
return found
return derived_path
def find_tool( def find_tool(
tool_name: str, tool_name: str,
objdump_path: str | None = None, objdump_path: str | None = None,
@@ -101,8 +28,7 @@ def find_tool(
"""Find a toolchain tool by name. """Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided), First tries to derive the tool path from objdump_path (if provided),
then searches PlatformIO package directories (for cross-compile toolchains), then falls back to searching for platform-specific tools.
and finally falls back to searching for platform-specific tools in PATH.
Args: Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
@@ -121,13 +47,7 @@ def find_tool(
_LOGGER.debug("Found %s at: %s", tool_name, potential_path) _LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path return potential_path
# Search in PlatformIO packages directory first (handles Zephyr SDK, etc.) # Try platform-specific tools
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
# are for the host architecture, not the target (ARM, Xtensa, etc.)
if found := _find_in_platformio_packages(tool_name):
return found
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
for prefix in TOOLCHAIN_PREFIXES: for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}" cmd = f"{prefix}{tool_name}"
try: try:

View File

@@ -1,139 +0,0 @@
"""ESP-IDF direct build generator for ESPHome."""
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
def get_available_components() -> list[str] | None:
"""Get list of available ESP-IDF components from project_description.json.
Returns only internal ESP-IDF components, excluding external/managed
components (from idf_component.yml).
"""
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
if not project_desc.exists():
return None
try:
with open(project_desc, encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})
result = []
for name, info in component_info.items():
# Exclude our own src component
if name == "src":
continue
# Exclude managed/external components
comp_dir = info.get("dir", "")
if "managed_components" in comp_dir:
continue
result.append(name)
return result
except (json.JSONDecodeError, OSError):
return None
def has_discovered_components() -> bool:
"""Check if we have discovered components from a previous configure."""
return get_available_components() is not None
def get_project_cmakelists() -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
"""
def get_component_cmakelists(minimal: bool = False) -> str:
"""Generate the main component CMakeLists.txt."""
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
# Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
idf_component_register(
SRCS ${{app_sources}}
INCLUDE_DIRS "." "esphome"
REQUIRES {requires_str}
)
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile definitions
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
{compile_defs_str}
)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
)
# ESPHome linker options
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
"""
def write_project(minimal: bool = False) -> None:
"""Write ESP-IDF project files."""
mkdir_p(CORE.build_path)
mkdir_p(CORE.relative_src_path())
# Write top-level CMakeLists.txt
write_file_if_changed(
CORE.relative_build_path("CMakeLists.txt"),
get_project_cmakelists(),
)
# Write component CMakeLists.txt in src/
write_file_if_changed(
CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(minimal=minimal),
)

View File

@@ -69,7 +69,6 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst, JsonObjectConst,
Parented, Parented,
PollingComponent, PollingComponent,
StringRef,
arduino_json_ns, arduino_json_ns,
bool_, bool_,
const_char_ptr, const_char_ptr,

View File

@@ -31,8 +31,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis(); this->last_update_ = millis();
if (state != this->current_state_) { if (state != this->current_state_) {
auto prev_state = this->current_state_; auto prev_state = this->current_state_;
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state; this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states // Single state callback - triggers check get_state() for specific states

View File

@@ -1,12 +1,21 @@
#include "am43_base.h" #include "am43_base.h"
#include "esphome/core/helpers.h"
#include <cstring> #include <cstring>
#include <cstdio>
namespace esphome { namespace esphome {
namespace am43 { namespace am43 {
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
std::string pkt_to_hex(const uint8_t *data, uint16_t len) {
char buf[64];
memset(buf, 0, 64);
for (int i = 0; i < len; i++)
sprintf(&buf[i * 2], "%02x", data[i]);
std::string ret = buf;
return ret;
}
Am43Packet *Am43Encoder::get_battery_level_request() { Am43Packet *Am43Encoder::get_battery_level_request() {
uint8_t data = 0x1; uint8_t data = 0x1;
return this->encode_(0xA2, &data, 1); return this->encode_(0xA2, &data, 1);
@@ -64,9 +73,7 @@ Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length)
memcpy(&this->packet_.data[7], data, length); memcpy(&this->packet_.data[7], data, length);
this->packet_.length = length + 7; this->packet_.length = length + 7;
this->checksum_(); this->checksum_();
char hex_buf[format_hex_size(sizeof(this->packet_.data))]; ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str());
ESP_LOGV("am43", "ENC(%d): 0x%s", this->packet_.length,
format_hex_to(hex_buf, this->packet_.data, this->packet_.length));
return &this->packet_; return &this->packet_;
} }
@@ -81,8 +88,7 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
this->has_set_state_response_ = false; this->has_set_state_response_ = false;
this->has_position_ = false; this->has_position_ = false;
this->has_pin_response_ = false; this->has_pin_response_ = false;
char hex_buf[format_hex_size(24)]; // Max expected packet size ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str());
ESP_LOGV("am43", "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length));
if (length < 2 || data[0] != 0x9a) if (length < 2 || data[0] != 0x9a)
return; return;

View File

@@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() {
AnovaPacket *AnovaCodec::get_read_device_status_request() { AnovaPacket *AnovaCodec::get_read_device_status_request() {
this->current_query_ = READ_DEVICE_STATUS; this->current_query_ = READ_DEVICE_STATUS;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DEVICE_STATUS); sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_target_temp_request() { AnovaPacket *AnovaCodec::get_read_target_temp_request() {
this->current_query_ = READ_TARGET_TEMPERATURE; this->current_query_ = READ_TARGET_TEMPERATURE;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_TARGET_TEMP); sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_current_temp_request() { AnovaPacket *AnovaCodec::get_read_current_temp_request() {
this->current_query_ = READ_CURRENT_TEMPERATURE; this->current_query_ = READ_CURRENT_TEMPERATURE;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_CURRENT_TEMP); sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_unit_request() { AnovaPacket *AnovaCodec::get_read_unit_request() {
this->current_query_ = READ_UNIT; this->current_query_ = READ_UNIT;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_UNIT); sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_data_request() { AnovaPacket *AnovaCodec::get_read_data_request() {
this->current_query_ = READ_DATA; this->current_query_ = READ_DATA;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA); sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA);
return this->clean_packet_(); return this->clean_packet_();
} }
@@ -50,25 +50,25 @@ AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
this->current_query_ = SET_TARGET_TEMPERATURE; this->current_query_ = SET_TARGET_TEMPERATURE;
if (this->fahrenheit_) if (this->fahrenheit_)
temperature = ctof(temperature); temperature = ctof(temperature);
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TARGET_TEMP, temperature); sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_set_unit_request(char unit) { AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
this->current_query_ = SET_UNIT; this->current_query_ = SET_UNIT;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TEMP_UNIT, unit); sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_start_request() { AnovaPacket *AnovaCodec::get_start_request() {
this->current_query_ = START; this->current_query_ = START;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_START); sprintf((char *) this->packet_.data, CMD_START);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_stop_request() { AnovaPacket *AnovaCodec::get_stop_request() {
this->current_query_ = STOP; this->current_query_ = STOP;
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_STOP); sprintf((char *) this->packet_.data, CMD_STOP);
return this->clean_packet_(); return this->clean_packet_();
} }

View File

@@ -1712,16 +1712,17 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
} }
// Create null-terminated state for callback (parse_number needs null-termination) // Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255 characters, but attributes can be much longer // HA state max length is 255, so 256 byte buffer covers all cases
// Use stack buffer for common case (states), heap fallback for large attributes char state_buf[256];
size_t state_len = msg.state.size(); size_t copy_len = msg.state.size();
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1); if (copy_len >= sizeof(state_buf)) {
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get()); copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
} }
state_buf[state_len] = '\0'; if (copy_len > 0) {
it.callback(StringRef(state_buf, state_len)); memcpy(state_buf, msg.state.c_str(), copy_len);
}
state_buf[copy_len] = '\0';
it.callback(StringRef(state_buf, copy_len));
} }
} }
#endif #endif

View File

@@ -3,7 +3,6 @@
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct #include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -257,30 +256,28 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send server hello // send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name(); const std::string &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE]; char mac[mac_len];
get_mac_address_into_buffer(mac); get_mac_address_into_buffer(mac);
// Calculate positions and sizes // Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator size_t name_len = name.size() + 1; // including null terminator
size_t name_offset = 1; size_t name_offset = 1;
size_t mac_offset = name_offset + name_len; size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE; size_t total_size = 1 + name_len + mac_len;
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null) auto msg = std::make_unique<uint8_t[]>(total_size);
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
uint8_t msg[max_msg_size];
// chosen proto // chosen proto
msg[0] = 0x01; msg[0] = 0x01;
// node name, terminated by null byte // node name, terminated by null byte
std::memcpy(msg + name_offset, name.c_str(), name_len); std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte // node mac, terminated by null byte
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE); std::memcpy(msg.get() + mac_offset, mac, mac_len);
aerr = write_frame_(msg, total_size); aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
@@ -356,32 +353,35 @@ APIError APINoiseFrameHelper::state_action_() {
return APIError::OK; return APIError::OK;
} }
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32];
data[0] = 0x01; // failure
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions // On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason)); size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message from PROGMEM
if (reason_len > 0) { if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len); memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
} }
#else #else
// Normal memory access // Normal memory access
const char *reason_str = LOG_STR_ARG(reason); const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str); size_t reason_len = strlen(reason_str);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message in bulk
if (reason_len > 0) { if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string std::memcpy(data.get() + 1, reason_str, reason_len);
std::memcpy(data + 1, reason_str, reason_len);
} }
#endif #endif
size_t data_size = reason_len + 1;
// temporarily remove failed state // temporarily remove failed state
auto orig_state = state_; auto orig_state = state_;
state_ = State::EXPLICIT_REJECT; state_ = State::EXPLICIT_REJECT;
write_frame_(data, data_size); write_frame_(data.get(), data_size);
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

View File

@@ -241,10 +241,8 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \ if (obj->is_internal()) \
return; \ return; \
for (auto &c : this->clients_) { \ for (auto &c : this->clients_) \
if (c->flags_.state_subscription) \ c->send_##entity_name##_state(obj); \
c->send_##entity_name##_state(obj); \
} \
} }
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@@ -323,10 +321,8 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) { void APIServer::on_event(event::Event *obj) {
if (obj->is_internal()) if (obj->is_internal())
return; return;
for (auto &c : this->clients_) { for (auto &c : this->clients_)
if (c->flags_.state_subscription) c->send_event(obj);
c->send_event(obj);
}
} }
#endif #endif
@@ -335,10 +331,8 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) { void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal()) if (obj->is_internal())
return; return;
for (auto &c : this->clients_) { for (auto &c : this->clients_)
if (c->flags_.state_subscription) c->send_update_state(obj);
c->send_update_state(obj);
}
} }
#endif #endif
@@ -558,10 +552,8 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() { void APIServer::request_time() {
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
if (!client->flags_.remove && client->is_authenticated()) { if (!client->flags_.remove && client->is_authenticated())
client->send_time_request(); client->send_time_request();
return; // Only request from one client to avoid clock conflicts
}
} }
} }
#endif #endif

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = res->as_uint32();
ptr += consumed; ptr += consumed;
if (field_length > static_cast<size_t>(end - ptr)) { if (ptr + field_length > end) {
return count; // Out of bounds return count; // Out of bounds
} }
ptr += field_length; ptr += field_length;
break; break;
} }
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (end - ptr < 4) { if (ptr + 4 > end) {
return count; return count;
} }
ptr += 4; ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = res->as_uint32();
ptr += consumed; ptr += consumed;
if (field_length > static_cast<size_t>(end - ptr)) { if (ptr + field_length > end) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return; return;
} }
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break; break;
} }
case WIRE_TYPE_FIXED32: { // 32-bit case WIRE_TYPE_FIXED32: { // 32-bit
if (end - ptr < 4) { if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return; return;
} }

View File

@@ -158,14 +158,12 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) { if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations // Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash("_offset_calibration_"); uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true); this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->restore_offset_calibrations_(); this->restore_offset_calibrations_();
// Initialize flash storage for power offset calibrations // Initialize flash storage for power offset calibrations
uint32_t po_hash = fnv1_hash("_power_offset_calibration_"); uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true); this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_(); this->restore_power_offset_calibrations_();
} else { } else {
@@ -185,8 +183,7 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) { if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration // Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash("_gain_calibration_"); uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true); this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_(); this->restore_gain_calibrations_();

View File

@@ -185,16 +185,18 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
return err; return err;
} }
if (str_endswith_ignore_case(url, ".wav")) { std::string url_string = str_lower_case(url);
if (str_endswith(url_string, ".wav")) {
file_type = AudioFileType::WAV; file_type = AudioFileType::WAV;
} }
#ifdef USE_AUDIO_MP3_SUPPORT #ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith_ignore_case(url, ".mp3")) { else if (str_endswith(url_string, ".mp3")) {
file_type = AudioFileType::MP3; file_type = AudioFileType::MP3;
} }
#endif #endif
#ifdef USE_AUDIO_FLAC_SUPPORT #ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith_ignore_case(url, ".flac")) { else if (str_endswith(url_string, ".flac")) {
file_type = AudioFileType::FLAC; file_type = AudioFileType::FLAC;
} }
#endif #endif

View File

@@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this); ControllerRegistry::notify_binary_sensor_update(this);
#endif #endif
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true; return true;
} }
return false; return false;

View File

@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state // - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) { this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop(); this->disable_loop();
} }
} }

View File

@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase web_server_base.WebServerBase
), ),
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"), cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_on( cv.only_on(

View File

@@ -152,13 +152,6 @@ void CC1101Component::setup() {
} }
} }
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, freq_offset, rssi, lqi);
}
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
}
void CC1101Component::loop() { void CC1101Component::loop() {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr || if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) { !this->gdo0_pin_->digital_read()) {
@@ -205,7 +198,7 @@ void CC1101Component::loop() {
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0; bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK; uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) { if (this->state_.CRC_EN == 0 || crc_ok) {
this->call_listeners_(this->packet_, freq_offset, rssi, lqi); this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
} }
// Return to rx // Return to rx

View File

@@ -11,11 +11,6 @@ namespace esphome::cc1101 {
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK }; enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
class CC1101Listener {
public:
virtual void on_packet(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) = 0;
};
class CC1101Component : public Component, class CC1101Component : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
@@ -78,7 +73,6 @@ class CC1101Component : public Component,
// Packet mode operations // Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet); CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; } Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
protected: protected:
@@ -95,11 +89,9 @@ class CC1101Component : public Component,
InternalGPIOPin *gdo0_pin_{nullptr}; InternalGPIOPin *gdo0_pin_{nullptr};
// Packet handling // Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{ Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()}; new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
std::vector<uint8_t> packet_; std::vector<uint8_t> packet_;
std::vector<CC1101Listener *> listeners_;
// Low-level Helpers // Low-level Helpers
uint8_t strobe_(Command cmd); uint8_t strobe_(Command cmd);

View File

@@ -81,8 +81,8 @@ void CCS811Component::setup() {
bootloader_version, application_version); bootloader_version, application_version);
if (this->version_ != nullptr) { if (this->version_ != nullptr) {
char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
buf_append_printf(version, sizeof(version), 0, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
(application_version >> 8 & 15), (application_version >> 4 & 15), application_version); (application_version >> 4 & 15), application_version);
ESP_LOGD(TAG, "publishing version state: %s", version); ESP_LOGD(TAG, "publishing version state: %s", version);
this->version_->publish_state(version); this->version_->publish_state(version);
} }

View File

@@ -133,7 +133,7 @@ bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pi
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const { size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "EXIO%u via CH422G", this->pin_); return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_);
} }
void CH422GGPIOPin::set_flags(gpio::Flags flags) { void CH422GGPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags; flags_ = flags;

View File

@@ -436,7 +436,7 @@ void Climate::save_state_() {
} }
void Climate::publish_state() { void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
auto traits = this->get_traits(); auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));

View File

@@ -153,7 +153,7 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f); this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
auto traits = this->get_traits(); auto traits = this->get_traits();
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

View File

@@ -76,6 +76,7 @@ class CS5460AComponent : public Component,
void restart() { restart_(); } void restart() { restart_(); }
void setup() override; void setup() override;
void loop() override {}
void dump_config() override; void dump_config() override;
protected: protected:

View File

@@ -207,24 +207,20 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{ {
// Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin. std::string buf = "Parsed:";
// Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999).
char buf[128];
size_t pos = buf_append_printf(buf, sizeof(buf), 0, "Parsed:");
if (have_voltage) { if (have_voltage) {
pos = buf_append_printf(buf, sizeof(buf), pos, " V=%.4fV", voltage); buf += str_sprintf(" V=%fV", voltage);
} }
if (have_current) { if (have_current) {
pos = buf_append_printf(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f, buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f);
calculated_current * 1000.0f);
} }
if (have_power) { if (have_power) {
pos = buf_append_printf(buf, sizeof(buf), pos, " P=%.4fW", power); buf += str_sprintf(" P=%fW", power);
} }
if (energy != 0.0f) { if (energy != 0.0f) {
buf_append_printf(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses); buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses);
} }
ESP_LOGVV(TAG, "%s", buf); ESP_LOGVV(TAG, "%s", buf.c_str());
} }
#endif #endif
} }

View File

@@ -258,9 +258,8 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
} }
char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0}; char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0};
size_t pos = 0;
for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) { for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) {
pos = buf_append_printf(buf, sizeof(buf), pos, "%02x ", frame[i]); sprintf(buf, "%s%02x ", buf, frame[i]);
} }
ESP_LOGD(TAG, "FRAME %s", buf); ESP_LOGD(TAG, "FRAME %s", buf);
@@ -350,9 +349,8 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true; valid_daikin_frame = true;
size_t bytes_count = data.size() / 2 / 8; size_t bytes_count = data.size() / 2 / 8;
size_t buf_size = bytes_count * 3 + 1; std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill) buf[0] = '\0';
size_t buf_pos = 0;
for (size_t i = 0; i < bytes_count; i++) { for (size_t i = 0; i < bytes_count; i++) {
uint8_t byte = 0; uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) { for (int8_t bit = 0; bit < 8; bit++) {
@@ -363,19 +361,19 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
break; break;
} }
} }
buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte); sprintf(buf.get(), "%s%02x ", buf.get(), byte);
} }
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size());
} }
if (!valid_daikin_frame) { if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1] = {0}; char sbuf[16 * 10 + 1];
size_t sbuf_pos = 0; sbuf[0] = '\0';
for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) { for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
if ((j - 2) % 16 == 0) { if ((j - 2) % 16 == 0) {
if (j > 0) { if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
} }
sbuf_pos = 0; sbuf[0] = '\0';
} }
char type_ch = ' '; char type_ch = ' ';
// debug_tolerance = 25% // debug_tolerance = 25%
@@ -403,10 +401,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
type_ch = '0'; type_ch = '0';
if (abs(data[j]) > 100000) { if (abs(data[j]) > 100000) {
sbuf_pos = buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", data[j] > 0 ? 99999 : -99999, type_ch); sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch);
} else { } else {
sbuf_pos = sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", (int) (round(data[j] / 10.) * 10), type_ch);
} }
if (j + 1 == static_cast<size_t>(data.size())) { if (j + 1 == static_cast<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);

View File

@@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() {
this->send_command_(DALLAS_COMMAND_START_CONVERSION); this->send_command_(DALLAS_COMMAND_START_CONVERSION);
this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] { this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] {
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) { if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
this->publish_state(NAN); this->publish_state(NAN);
return; return;

View File

@@ -30,7 +30,7 @@ void DateEntity::publish_state() {
return; return;
} }
this->set_has_state(true); this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this); ControllerRegistry::notify_date_update(this);
@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); }; DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
DateCall &DateCall::set_date(const char *date, size_t len) { DateCall &DateCall::set_date(const std::string &date) {
ESPTime val{}; ESPTime val{};
if (!ESPTime::strptime(date, len, val)) { if (!ESPTime::strptime(date, val)) {
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object"); ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
return *this; return *this;
} }

View File

@@ -67,9 +67,7 @@ class DateCall {
void perform(); void perform();
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day); DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
DateCall &set_date(ESPTime time); DateCall &set_date(ESPTime time);
DateCall &set_date(const char *date, size_t len); DateCall &set_date(const std::string &date);
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
DateCall &set_year(uint16_t year) { DateCall &set_year(uint16_t year) {
this->year_ = year; this->year_ = year;

View File

@@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() {
return; return;
} }
this->set_has_state(true); this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->day_, this->hour_, this->minute_, this->second_); this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this); ControllerRegistry::notify_datetime_update(this);
@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
datetime.second); datetime.second);
}; };
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) { DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
ESPTime val{}; ESPTime val{};
if (!ESPTime::strptime(datetime, len, val)) { if (!ESPTime::strptime(datetime, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object"); ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this; return *this;
} }

View File

@@ -71,11 +71,7 @@ class DateTimeCall {
void perform(); void perform();
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
DateTimeCall &set_datetime(ESPTime datetime); DateTimeCall &set_datetime(ESPTime datetime);
DateTimeCall &set_datetime(const char *datetime, size_t len); DateTimeCall &set_datetime(const std::string &datetime);
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
DateTimeCall &set_datetime(const std::string &datetime) {
return this->set_datetime(datetime.c_str(), datetime.size());
}
DateTimeCall &set_datetime(time_t epoch_seconds); DateTimeCall &set_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) { DateTimeCall &set_year(uint16_t year) {

View File

@@ -26,7 +26,8 @@ void TimeEntity::publish_state() {
return; return;
} }
this->set_has_state(true); this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this); ControllerRegistry::notify_time_update(this);
@@ -74,9 +75,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); }; TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
TimeCall &TimeCall::set_time(const char *time, size_t len) { TimeCall &TimeCall::set_time(const std::string &time) {
ESPTime val{}; ESPTime val{};
if (!ESPTime::strptime(time, len, val)) { if (!ESPTime::strptime(time, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object"); ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this; return *this;
} }

View File

@@ -69,9 +69,7 @@ class TimeCall {
void perform(); void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second); TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time); TimeCall &set_time(ESPTime time);
TimeCall &set_time(const char *time, size_t len); TimeCall &set_time(const std::string &time);
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
TimeCall &set_hour(uint8_t hour) { TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour; this->hour_ = hour;

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION); size_t pos = buf_append(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
this->free_heap_ = get_free_heap_(); this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);

View File

@@ -5,6 +5,12 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include <span> #include <span>
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
@@ -19,7 +25,40 @@ namespace debug {
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
// buf_append_printf is now provided by esphome/core/helpers.h #ifdef USE_ESP8266
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
// Format strings must be wrapped with PSTR() macro
inline size_t buf_append_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf_P(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#define buf_append(buf, size, pos, fmt, ...) buf_append_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__)
#else
/// Safely append formatted string to buffer, returning new position (capped at size)
__attribute__((format(printf, 4, 5))) inline size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt,
...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#endif
class DebugComponent : public PollingComponent { class DebugComponent : public PollingComponent {
public: public:
@@ -35,11 +74,8 @@ class DebugComponent : public PollingComponent {
#ifdef USE_SENSOR #ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) #if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; }
#endif #endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -61,11 +97,8 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr};
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) #if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
sensor::Sensor *fragmentation_sensor_{nullptr}; sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
sensor::Sensor *min_free_sensor_{nullptr};
#endif #endif
sensor::Sensor *loop_time_sensor_{nullptr}; sensor::Sensor *loop_time_sensor_{nullptr};
#ifdef USE_ESP32 #ifdef USE_ESP32

View File

@@ -173,8 +173,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode); flash_mode);
#endif #endif
esp_chip_info_t info; esp_chip_info_t info;
@@ -182,71 +182,60 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT; const char *model = ESPHOME_VARIANT;
// Build features string // Build features string
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model); pos = buf_append(buf, size, pos, "|Chip: %s Features:", model);
bool first_feature = true; bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) { for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) { if (info.features & feature.bit) {
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name); pos = buf_append(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
first_feature = false; first_feature = false;
info.features &= ~feature.bit; info.features &= ~feature.bit;
} }
} }
if (info.features != 0) { if (info.features != 0) {
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); pos = buf_append(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
} }
ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision);
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); pos = buf_append(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
// Framework detection // Framework detection
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino"); ESP_LOGD(TAG, "Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); pos = buf_append(buf, size, pos, "|Framework: Arduino");
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF"); ESP_LOGD(TAG, "Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); pos = buf_append(buf, size, pos, "|Framework: ESP-IDF");
#else #else
ESP_LOGW(TAG, "Framework: UNKNOWN"); ESP_LOGW(TAG, "Framework: UNKNOWN");
pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); pos = buf_append(buf, size, pos, "|Framework: UNKNOWN");
#endif #endif
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); pos = buf_append(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
uint8_t mac[6]; uint8_t mac[6];
get_mac_address_raw(mac); get_mac_address_raw(mac);
ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[4], mac[5]); mac[5]);
char reason_buffer[RESET_REASON_BUFFER_SIZE]; char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos; return pos;
} }
void DebugComponent::update_platform_() { void DebugComponent::update_platform_() {
#ifdef USE_SENSOR #ifdef USE_SENSOR
uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
if (this->block_sensor_ != nullptr) { if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(max_alloc); this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
}
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
}
if (this->fragmentation_sensor_ != nullptr) {
uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
if (free_heap > 0) {
float fragmentation = 100.0f - (100.0f * max_alloc / free_heap);
this->fragmentation_sensor_->publish_state(fragmentation);
}
} }
if (this->psram_sensor_ != nullptr) { if (this->psram_sensor_ != nullptr) {
this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

View File

@@ -3,80 +3,21 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <Esp.h> #include <Esp.h>
extern "C" {
#include <user_interface.h>
// Global reset info struct populated by SDK at boot
extern struct rst_info resetInfo;
// Core version - either a string pointer or a version number to format as hex
extern uint32_t core_version;
extern const char *core_release;
}
namespace esphome { namespace esphome {
namespace debug { namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
// Get core version string (no heap allocation)
// Returns either core_release directly or formats core_version as hex into provided buffer
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
if (core_release != nullptr) {
return core_release;
}
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
return buffer.data();
}
// Size for reset info buffer
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
// Get detailed reset info string (no heap allocation)
// For watchdog/exception resets, includes detailed exception info
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
resetInfo.excvaddr, resetInfo.depc);
return buffer.data();
}
return LOG_STR_ARG(get_reset_reason_str(reason));
}
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
// Copy from flash to provided buffer char *buf = buffer.data();
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1); #if !defined(CLANG_TIDY)
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0'; String reason = ESP.getResetReason(); // NOLINT
return buffer.data(); snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
return buf;
#else
buf[0] = '\0';
return buf;
#endif
} }
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
@@ -92,42 +33,37 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data(); char *buf = buffer.data();
const LogString *flash_mode; const char *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO: case FM_QIO:
flash_mode = LOG_STR("QIO"); flash_mode = "QIO";
break; break;
case FM_QOUT: case FM_QOUT:
flash_mode = LOG_STR("QOUT"); flash_mode = "QOUT";
break; break;
case FM_DIO: case FM_DIO:
flash_mode = LOG_STR("DIO"); flash_mode = "DIO";
break; break;
case FM_DOUT: case FM_DOUT:
flash_mode = LOG_STR("DOUT"); flash_mode = "DOUT";
break; break;
default: default:
flash_mode = LOG_STR("UNKNOWN"); flash_mode = "UNKNOWN";
} }
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
LOG_STR_ARG(flash_mode)); pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, flash_mode);
LOG_STR_ARG(flash_mode));
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE]; char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(reason_buffer); const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
// NOLINTBEGIN(readability-static-accessed-through-instance)
uint32_t chip_id = ESP.getChipId(); uint32_t chip_id = ESP.getChipId();
uint8_t boot_version = ESP.getBootVersion(); uint8_t boot_version = ESP.getBootVersion();
uint8_t boot_mode = ESP.getBootMode(); uint8_t boot_mode = ESP.getBootMode();
uint8_t cpu_freq = ESP.getCpuFreqMHz(); uint8_t cpu_freq = ESP.getCpuFreqMHz();
uint32_t flash_chip_id = ESP.getFlashChipId(); uint32_t flash_chip_id = ESP.getFlashChipId();
const char *sdk_version = ESP.getSdkVersion();
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG, ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n" "Chip ID: 0x%08" PRIX32 "\n"
@@ -138,18 +74,19 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
"Flash Chip ID=0x%08" PRIX32 "\n" "Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n" "Reset Reason: %s\n"
"Reset Info: %s", "Reset Info: %s",
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq, chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason)); reset_reason, ESP.getResetInfo().c_str());
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id); pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version); pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer)); pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version); pos = buf_append(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode); pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq); pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id); pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason)); pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
#endif
return pos; return pos;
} }

View File

@@ -36,12 +36,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason); lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10); pos = buf_append(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason); pos = buf_append(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name()); pos = buf_append(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id); pos = buf_append(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib); pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib); pos = buf_append(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
return pos; return pos;
} }
@@ -51,9 +51,6 @@ void DebugComponent::update_platform_() {
if (this->block_sensor_ != nullptr) { if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(lt_heap_get_max_alloc()); this->block_sensor_->publish_state(lt_heap_get_max_alloc());
} }
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(lt_heap_get_min_free());
}
#endif #endif
} }

View File

@@ -19,7 +19,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t cpu_freq = rp2040.f_cpu(); uint32_t cpu_freq = rp2040.f_cpu();
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
return pos; return pos;
} }

View File

@@ -20,9 +20,9 @@ static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set,
return pos; return pos;
} }
if (pos > 0) { if (pos > 0) {
pos = buf_append_printf(buf, size, pos, ", "); pos = buf_append(buf, size, pos, ", ");
} }
return buf_append_printf(buf, size, pos, "%s", reason); return buf_append(buf, size, pos, "%s", reason);
} }
static inline uint32_t read_mem_u32(uintptr_t addr) { static inline uint32_t read_mem_u32(uintptr_t addr) {
@@ -140,7 +140,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status = const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage."; (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status); ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status); pos = buf_append(buf, size, pos, "|Main supply status: %s", supply_status);
// Regulator stage 0 // Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) { if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
@@ -172,16 +172,16 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
reg0_voltage = "???V"; reg0_voltage = "???V";
} }
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage); ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage); pos = buf_append(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
} else { } else {
ESP_LOGD(TAG, "Regulator stage 0: disabled"); ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled"); pos = buf_append(buf, size, pos, "|Regulator stage 0: disabled");
} }
// Regulator stage 1 // Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO"; const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type); ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type); pos = buf_append(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
// USB power state // USB power state
const char *usb_state; const char *usb_state;
@@ -195,7 +195,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected"; usb_state = "disconnected";
} }
ESP_LOGD(TAG, "USB power state: %s", usb_state); ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state); pos = buf_append(buf, size, pos, "|USB power state: %s", usb_state);
// Power-fail comparator // Power-fail comparator
bool enabled; bool enabled;
@@ -300,14 +300,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break; break;
} }
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); pos = buf_append(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
} else { } else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage); ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage); pos = buf_append(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
} }
} else { } else {
ESP_LOGD(TAG, "Power-fail comparator: disabled"); ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled"); pos = buf_append(buf, size, pos, "|Power-fail comparator: disabled");
} }
auto package = [](uint32_t value) { auto package = [](uint32_t value) {

View File

@@ -11,24 +11,16 @@ from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_DIAGNOSTIC,
ICON_COUNTER, ICON_COUNTER,
ICON_TIMER, ICON_TIMER,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
UNIT_BYTES, UNIT_BYTES,
UNIT_HERTZ, UNIT_HERTZ,
UNIT_MILLISECOND, UNIT_MILLISECOND,
UNIT_PERCENT, UNIT_PERCENT,
) )
from . import ( # noqa: F401 pylint: disable=unused-import from . import CONF_DEBUG_ID, DebugComponent
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"] DEPENDENCIES = ["debug"]
CONF_MIN_FREE = "min_free"
CONF_PSRAM = "psram" CONF_PSRAM = "psram"
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
@@ -46,14 +38,8 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
), ),
cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any( cv.only_on_esp8266,
cv.All( cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32,
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
),
sensor.sensor_schema( sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,
icon=ICON_COUNTER, icon=ICON_COUNTER,
@@ -61,19 +47,6 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
), ),
), ),
cv.Optional(CONF_MIN_FREE): cv.All(
cv.Any(
cv.only_on_esp32,
cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]),
msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND, unit_of_measurement=UNIT_MILLISECOND,
icon=ICON_TIMER, icon=ICON_TIMER,
@@ -116,10 +89,6 @@ async def to_code(config):
sens = await sensor.new_sensor(fragmentation_conf) sens = await sensor.new_sensor(fragmentation_conf)
cg.add(debug_component.set_fragmentation_sensor(sens)) cg.add(debug_component.set_fragmentation_sensor(sens))
if min_free_conf := config.get(CONF_MIN_FREE):
sens = await sensor.new_sensor(min_free_conf)
cg.add(debug_component.set_min_free_sensor(sens))
if loop_time_conf := config.get(CONF_LOOP_TIME): if loop_time_conf := config.get(CONF_LOOP_TIME):
sens = await sensor.new_sensor(loop_time_conf) sens = await sensor.new_sensor(loop_time_conf)
cg.add(debug_component.set_loop_time_sensor(sens)) cg.add(debug_component.set_loop_time_sensor(sens))

View File

@@ -8,11 +8,7 @@ from esphome.const import (
ICON_RESTART, ICON_RESTART,
) )
from . import ( # noqa: F401 pylint: disable=unused-import from . import CONF_DEBUG_ID, DebugComponent
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"] DEPENDENCIES = ["debug"]

View File

@@ -127,9 +127,7 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1; this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) { } else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -137,10 +135,7 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max2_ = max2 = round(max2 / 0.15) * 0.15; this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15);
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) { } else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -150,10 +145,9 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max3_ = max3 = round(max3 / 0.15) * 0.15; this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = this->max4_ = max4 = -1; this->min4_ = min4 = this->max4_ = max4 = -1;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null this->cmd_ = str_sprintf("detRangeCfg -1 "
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, "%.0f %.0f %.0f %.0f %.0f %.0f",
max2 / 0.15, min3 / 0.15, max3 / 0.15); min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15);
this->cmd_ = buf;
} else { } else {
this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -164,10 +158,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min4_ = min4 = round(min4 / 0.15) * 0.15; this->min4_ = min4 = round(min4 / 0.15) * 0.15;
this->max4_ = max4 = round(max4 / 0.15) * 0.15; this->max4_ = max4 = round(max4 / 0.15) * 0.15;
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null this->cmd_ = str_sprintf("detRangeCfg -1 "
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, "%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f",
min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15); min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15,
this->cmd_ = buf; max4 / 0.15);
} }
this->min1_ = min1; this->min1_ = min1;
@@ -209,10 +203,7 @@ SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_af
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f; delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f); this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f); this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32 this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
char buf[32];
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
this->cmd_ = buf;
}; };
uint8_t SetLatencyCommand::on_message(std::string &message) { uint8_t SetLatencyCommand::on_message(std::string &message) {

View File

@@ -75,8 +75,8 @@ class SetLatencyCommand : public Command {
class SensorCfgStartCommand : public Command { class SensorCfgStartCommand : public Command {
public: public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) { SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17 char tmp_cmd[20] = {0};
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode); sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd); cmd_ = std::string(tmp_cmd);
} }
uint8_t on_message(std::string &message) override; uint8_t on_message(std::string &message) override;
@@ -142,8 +142,8 @@ class SensitivityCommand : public Command {
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) { SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9) if (sensitivity > 9)
sensitivity_ = sensitivity = 9; sensitivity_ = sensitivity = 9;
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17 char tmp_cmd[20] = {0};
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity); sprintf(tmp_cmd, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd); cmd_ = std::string(tmp_cmd);
}; };
uint8_t on_message(std::string &message) override; uint8_t on_message(std::string &message) override;

View File

@@ -25,13 +25,29 @@ dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
def _validate_key(value):
value = cv.string_strict(value)
parts = [value[i : i + 2] for i in range(0, len(value), 2)]
if len(parts) != 16:
raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers")
parts_int = []
if any(len(part) != 2 for part in parts):
raise cv.Invalid("Decryption key must be format XX")
for part in parts:
try:
parts_int.append(int(part, 16))
except ValueError:
# pylint: disable=raise-missing-from
raise cv.Invalid("Decryption key must be hex values from 00 to FF")
return "".join(f"{part:02X}" for part in parts_int)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(Dsmr), cv.GenerateID(): cv.declare_id(Dsmr),
cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key( cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
value, name="Decryption key"
),
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,

View File

@@ -1,5 +1,4 @@
#include "dsmr.h" #include "dsmr.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <AES.h> #include <AES.h>
@@ -295,8 +294,8 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
} }
void Dsmr::set_decryption_key(const char *decryption_key) { void Dsmr::set_decryption_key(const std::string &decryption_key) {
if (decryption_key == nullptr || decryption_key[0] == '\0') { if (decryption_key.empty()) {
ESP_LOGI(TAG, "Disabling decryption"); ESP_LOGI(TAG, "Disabling decryption");
this->decryption_key_.clear(); this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) { if (this->crypt_telegram_ != nullptr) {
@@ -306,15 +305,21 @@ void Dsmr::set_decryption_key(const char *decryption_key) {
return; return;
} }
if (!parse_hex(decryption_key, this->decryption_key_, 16)) { if (decryption_key.length() != 32) {
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters"); ESP_LOGE(TAG, "Error, decryption key must be 32 character long");
this->decryption_key_.clear();
return; return;
} }
this->decryption_key_.clear();
ESP_LOGI(TAG, "Decryption key is set"); ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key // Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key); ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str());
char temp[3] = {0};
for (int i = 0; i < 16; i++) {
strncpy(temp, &(decryption_key.c_str()[i * 2]), 2);
this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16));
}
if (this->crypt_telegram_ == nullptr) { if (this->crypt_telegram_ == nullptr) {
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT

View File

@@ -63,7 +63,7 @@ class Dsmr : public Component, public uart::UARTDevice {
void dump_config() override; void dump_config() override;
void set_decryption_key(const char *decryption_key); void set_decryption_key(const std::string &decryption_key);
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }

View File

@@ -34,7 +34,6 @@ from esphome.const import (
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
KEY_NAME, KEY_NAME,
KEY_NATIVE_IDF,
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_ESP32, PLATFORM_ESP32,
@@ -54,7 +53,6 @@ from .const import ( # noqa
KEY_COMPONENTS, KEY_COMPONENTS,
KEY_ESP32, KEY_ESP32,
KEY_EXTRA_BUILD_FILES, KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_PATH, KEY_PATH,
KEY_REF, KEY_REF,
KEY_REPO, KEY_REPO,
@@ -201,7 +199,6 @@ def set_core_data(config):
) )
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
CORE.data[KEY_ESP32][KEY_VARIANT] = variant CORE.data[KEY_ESP32][KEY_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@@ -965,54 +962,12 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
async def to_code(config): async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] cg.add_platformio_option("board", config[CONF_BOARD])
conf = config[CONF_FRAMEWORK] cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
# Check if using native ESP-IDF build (--native-idf) "board_upload.maximum_size",
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False) int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
if use_platformio: )
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
# but keep them when using --native-idf for native ESP-IDF builds
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
else:
cg.add_build_flag("-Wno-error=format")
cg.set_cpp_standard("gnu++20") cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32") cg.add_build_flag("-DUSE_ESP32")
cg.add_build_flag("-Wl,-z,noexecstack") cg.add_build_flag("-Wl,-z,noexecstack")
@@ -1022,49 +977,79 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant]) cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
# Set the location of the IDF component manager cache # Set the location of the IDF component manager cache
os.environ["IDF_COMPONENT_CACHE_PATH"] = str( os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
CORE.relative_internal_path(".espressif") CORE.relative_internal_path(".espressif")
) )
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
else: else:
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
if use_platformio:
cg.add_platformio_option("framework", "arduino, espidf")
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages",
[_format_framework_espidf_version(idf_ver, None)],
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_define( cg.add_define(
"USE_ARDUINO_VERSION_CODE", "USE_ARDUINO_VERSION_CODE",
cg.RawExpression( cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-Wno-nonnull-compare") cg.add_build_flag("-Wno-nonnull-compare")
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
@@ -1211,8 +1196,7 @@ async def to_code(config):
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] "CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
) )
if use_platformio: cg.add_platformio_option("board_build.partitions", "partitions.csv")
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config: if CONF_PARTITIONS in config:
add_extra_build_file( add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
@@ -1377,16 +1361,19 @@ def copy_files():
_write_idf_component_yml() _write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino: if CORE.using_arduino:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(flash_size), get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
) )
else: else:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(flash_size), get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
) )
# IDF build scripts look for version string to put in the build. # IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo, # However, if the build path does not have an initialized git repo,

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
KEY_ESP32 = "esp32" KEY_ESP32 = "esp32"
KEY_BOARD = "board" KEY_BOARD = "board"
KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant" KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components" KEY_COMPONENTS = "components"

View File

@@ -181,8 +181,7 @@ class ESP32Preferences : public ESPPreferences {
if (actual_len != to_save.len) { if (actual_len != to_save.len) {
return true; return true;
} }
// Most preferences are small, use stack buffer with heap fallback for large ones auto stored_data = std::make_unique<uint8_t[]>(actual_len);
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));

View File

@@ -46,8 +46,6 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const; esp_bt_uuid_t get_uuid() const;
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const; std::string to_string() const;
const char *to_str(std::span<char, UUID_STR_LEN> output) const; const char *to_str(std::span<char, UUID_STR_LEN> output) const;

View File

@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
this->set_state(espbt::ClientState::INIT); this->set_state(espbt::ClientState::INIT);
return; return;
} }
if (this->state() == espbt::ClientState::INIT) { if (this->state_ == espbt::ClientState::INIT) {
auto ret = esp_ble_gattc_app_register(this->app_id); auto ret = esp_ble_gattc_app_register(this->app_id);
if (ret) { if (ret) {
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
} }
// If idle, we can disable the loop as connect() // If idle, we can disable the loop as connect()
// will enable it again when a connection is needed. // will enable it again when a connection is needed.
else if (this->state() == espbt::ClientState::IDLE) { else if (this->state_ == espbt::ClientState::IDLE) {
this->disable_loop(); this->disable_loop();
} }
} }
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false; return false;
if (this->address_ == 0 || device.address_uint64() != this->address_) if (this->address_ == 0 || device.address_uint64() != this->address_)
return false; return false;
if (this->state() != espbt::ClientState::IDLE) if (this->state_ != espbt::ClientState::IDLE)
return false; return false;
this->log_event_("Found device"); this->log_event_("Found device");
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
void BLEClientBase::connect() { void BLEClientBase::connect() {
// Prevent duplicate connection attempts // Prevent duplicate connection attempts
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
this->state() == espbt::ClientState::ESTABLISHED) { this->state_ == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state())); espbt::client_state_to_string(this->state_));
return; return;
} }
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
void BLEClientBase::disconnect() { void BLEClientBase::disconnect() {
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) { if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_, ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state())); espbt::client_state_to_string(this->state_));
return; return;
} }
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_); this->address_str_);
this->want_disconnect_ = true; this->want_disconnect_ = true;
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
void BLEClientBase::unconditional_disconnect() { void BLEClientBase::unconditional_disconnect() {
// Disconnect without checking the state. // Disconnect without checking the state.
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_); ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
if (this->state() == espbt::ClientState::DISCONNECTING) { if (this->state_ == espbt::ClientState::DISCONNECTING) {
this->log_error_("Already disconnecting"); this->log_error_("Already disconnecting");
return; return;
} }
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
this->log_gattc_warning_("esp_ble_gattc_close", err); this->log_gattc_warning_("esp_ble_gattc_close", err);
} }
if (this->state() == espbt::ClientState::DISCOVERED) { if (this->state_ == espbt::ClientState::DISCOVERED) {
this->set_address(0); this->set_address(0);
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
} else { } else {
@@ -193,18 +193,10 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name);
} }
void BLEClientBase::log_gattc_lifecycle_event_(const char *name) { void BLEClientBase::log_gattc_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
} }
void BLEClientBase::log_gattc_data_event_(const char *name) {
// Data transfer events are logged at VERBOSE level because logging to UART creates
// delays that cause timing issues during time-sensitive BLE operations. This is
// especially problematic during pairing or firmware updates which require rapid
// writes to many characteristics - the log spam can cause these operations to fail.
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status);
} }
@@ -288,25 +280,25 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT: { case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr(param->open.remote_bda)) if (!this->check_addr(param->open.remote_bda))
return false; return false;
this->log_gattc_lifecycle_event_("OPEN"); this->log_gattc_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT // conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0; this->service_count_ = 0;
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event // error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state. // arriving after we've already transitioned to IDLE state.
if (this->state() == espbt::ClientState::IDLE) { if (this->state_ == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_, param->open.status); this->address_str_, param->open.status);
break; break;
} }
if (this->state() != espbt::ClientState::CONNECTING) { if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does // This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the // because it means we have a bad assumption about how the
// ESP BT stack works. // ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status); this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
} }
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status); this->log_gattc_warning_("Connection open", param->open.status);
@@ -327,7 +319,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Cached connections already connected with medium parameters, no update needed // Cached connections already connected with medium parameters, no update needed
// only set our state, subclients might have more stuff to do yet. // only set our state, subclients might have more stuff to do yet.
this->set_state_internal_(espbt::ClientState::ESTABLISHED); this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
} }
// For V3_WITHOUT_CACHE, we already set fast params before connecting // For V3_WITHOUT_CACHE, we already set fast params before connecting
@@ -339,7 +331,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CONNECT_EVT: { case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr(param->connect.remote_bda)) if (!this->check_addr(param->connect.remote_bda))
return false; return false;
this->log_gattc_lifecycle_event_("CONNECT"); this->log_gattc_event_("CONNECT");
this->conn_id_ = param->connect.conn_id; this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples // Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
@@ -356,7 +348,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
return false; return false;
// Check if we were disconnected while waiting for service discovery // Check if we were disconnected while waiting for service discovery
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state() == espbt::ClientState::CONNECTED) { this->state_ == espbt::ClientState::CONNECTED) {
this->log_warning_("Remote closed during discovery"); this->log_warning_("Remote closed during discovery");
} else { } else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
@@ -384,7 +376,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CLOSE_EVT: { case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id) if (this->conn_id_ != param->close.conn_id)
return false; return false;
this->log_gattc_lifecycle_event_("CLOSE"); this->log_gattc_event_("CLOSE");
this->release_services(); this->release_services();
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID; this->conn_id_ = UNSET_CONN_ID;
@@ -412,7 +404,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_SEARCH_CMPL_EVT: { case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id) if (this->conn_id_ != param->search_cmpl.conn_id)
return false; return false;
this->log_gattc_lifecycle_event_("SEARCH_CMPL"); this->log_gattc_event_("SEARCH_CMPL");
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase // This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
@@ -433,41 +425,41 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
#endif #endif
} }
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
this->set_state_internal_(espbt::ClientState::ESTABLISHED); this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
} }
case ESP_GATTC_READ_DESCR_EVT: { case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id) if (this->conn_id_ != param->write.conn_id)
return false; return false;
this->log_gattc_data_event_("READ_DESCR"); this->log_gattc_event_("READ_DESCR");
break; break;
} }
case ESP_GATTC_WRITE_DESCR_EVT: { case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id) if (this->conn_id_ != param->write.conn_id)
return false; return false;
this->log_gattc_data_event_("WRITE_DESCR"); this->log_gattc_event_("WRITE_DESCR");
break; break;
} }
case ESP_GATTC_WRITE_CHAR_EVT: { case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id) if (this->conn_id_ != param->write.conn_id)
return false; return false;
this->log_gattc_data_event_("WRITE_CHAR"); this->log_gattc_event_("WRITE_CHAR");
break; break;
} }
case ESP_GATTC_READ_CHAR_EVT: { case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id) if (this->conn_id_ != param->read.conn_id)
return false; return false;
this->log_gattc_data_event_("READ_CHAR"); this->log_gattc_event_("READ_CHAR");
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id) if (this->conn_id_ != param->notify.conn_id)
return false; return false;
this->log_gattc_data_event_("NOTIFY"); this->log_gattc_event_("NOTIFY");
break; break;
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_gattc_data_event_("REG_FOR_NOTIFY"); this->log_gattc_event_("REG_FOR_NOTIFY");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value // Client is responsible for flipping the descriptor value
@@ -499,7 +491,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_err_t status = esp_err_t status =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); (uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) { if (status) {
this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status); this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
} }
@@ -507,13 +499,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
this->log_gattc_data_event_("UNREG_FOR_NOTIFY"); this->log_gattc_event_("UNREG_FOR_NOTIFY");
break; break;
} }
default: default:
// Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations // ideally would check all other events for matching conn_id
ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
break; break;
} }
return true; return true;

View File

@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void unconditional_disconnect(); void unconditional_disconnect();
void release_services(); void release_services();
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; } bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
@@ -127,8 +127,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding // 6 bytes used, 2 bytes padding
void log_event_(const char *name); void log_event_(const char *name);
void log_gattc_lifecycle_event_(const char *name); void log_gattc_event_(const char *name);
void log_gattc_data_event_(const char *name);
void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type); const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,

View File

@@ -105,13 +105,15 @@ void ESP32BLETracker::loop() {
} }
// Check for scan timeout - moved here from scheduler to avoid false reboots // Check for scan timeout - moved here from scheduler to avoid false reboots
// when the loop is blocked. This must run every iteration for safety. // when the loop is blocked
if (this->scanner_state_ == ScannerState::RUNNING) { if (this->scanner_state_ == ScannerState::RUNNING) {
switch (this->scan_timeout_state_) { switch (this->scan_timeout_state_) {
case ScanTimeoutState::MONITORING: { case ScanTimeoutState::MONITORING: {
uint32_t now = App.get_loop_component_start_time();
uint32_t timeout_ms = this->scan_duration_ * 2000;
// Robust time comparison that handles rollover correctly // Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably // This works because unsigned arithmetic wraps around predictably
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) { if ((now - this->scan_start_time_) > timeout_ms) {
// First time we've seen the timeout exceeded - wait one more loop iteration // First time we've seen the timeout exceeded - wait one more loop iteration
// This ensures all components have had a chance to process pending events // This ensures all components have had a chance to process pending events
// This is because esp32_ble may not have run yet and called // This is because esp32_ble may not have run yet and called
@@ -126,31 +128,13 @@ void ESP32BLETracker::loop() {
ESP_LOGE(TAG, "Scan never terminated, rebooting"); ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot(); App.reboot();
break; break;
case ScanTimeoutState::INACTIVE: case ScanTimeoutState::INACTIVE:
// This case should be unreachable - scanner and timeout states are always synchronized
break; break;
} }
} }
// Fast path: skip expensive client state counting and processing
// if no state has changed since last loop iteration.
//
// How state changes ensure we reach the code below:
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
// STARTING, not RUNNING, so version is always incremented before this condition is true)
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
//
// All conditions that affect the logic below are tied to state changes that increment
// state_version_, so the fast path is safe.
if (this->state_version_ == this->last_processed_version_) {
return;
}
this->last_processed_version_ = this->state_version_;
// State changed - do full processing
ClientStateCounts counts = this->count_client_states_(); ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) { if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts; this->client_state_counts_ = counts;
@@ -158,7 +142,6 @@ void ESP32BLETracker::loop() {
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
} }
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
if (this->scanner_state_ == ScannerState::FAILED || if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_(); this->handle_scanner_failure_();
@@ -177,8 +160,6 @@ void ESP32BLETracker::loop() {
*/ */
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// all clients are idle (their state changes increment version when they finish)
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false); this->update_coex_preference_(false);
@@ -187,9 +168,8 @@ void ESP32BLETracker::loop() {
this->start_scan_(false); // first = false this->start_scan_(false); // first = false
} }
} }
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()), // If there is a discovered client and no connecting
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE). // clients, then promote the discovered client to ready to connect.
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
// We check both RUNNING and IDLE states because: // We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler) // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
@@ -256,7 +236,6 @@ void ESP32BLETracker::start_scan_(bool first) {
// Start timeout monitoring in loop() instead of using scheduler // Start timeout monitoring in loop() instead of using scheduler
// This prevents false reboots when the loop is blocked // This prevents false reboots when the loop is blocked
this->scan_start_time_ = App.get_loop_component_start_time(); this->scan_start_time_ = App.get_loop_component_start_time();
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
this->scan_timeout_state_ = ScanTimeoutState::MONITORING; this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
@@ -274,10 +253,6 @@ void ESP32BLETracker::start_scan_(bool first) {
void ESP32BLETracker::register_client(ESPBTClient *client) { void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_; client->app_id = ++this->app_id_;
// Give client a pointer to our state_version_ so it can notify us of state changes.
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
client->set_tracker_state_version(&this->state_version_);
this->clients_.push_back(client); this->clients_.push_back(client);
this->recalculate_advertisement_parser_types(); this->recalculate_advertisement_parser_types();
#endif #endif
@@ -407,7 +382,6 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
void ESP32BLETracker::set_scanner_state_(ScannerState state) { void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state; this->scanner_state_ = state;
this->state_version_++;
for (auto *listener : this->scanner_state_listeners_) { for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state); listener->on_scanner_state(state);
} }

View File

@@ -216,19 +216,6 @@ enum class ConnectionType : uint8_t {
V3_WITHOUT_CACHE V3_WITHOUT_CACHE
}; };
/// Base class for BLE GATT clients that connect to remote devices.
///
/// State Change Tracking Design:
/// -----------------------------
/// ESP32BLETracker::loop() needs to know when client states change to avoid
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
/// we use a version counter owned by ESP32BLETracker that clients increment on
/// state changes. The tracker compares versions to skip work when nothing changed.
///
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
/// pointer (tracker_state_version_) set during register_client(). Clients
/// increment the counter through this pointer when their state changes.
/// The pointer may be null if the client is not registered with a tracker.
class ESPBTClient : public ESPBTDeviceListener { class ESPBTClient : public ESPBTDeviceListener {
public: public:
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -238,49 +225,26 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void disconnect() = 0; virtual void disconnect() = 0;
bool disconnect_pending() const { return this->want_disconnect_; } bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; } void cancel_pending_disconnect() { this->want_disconnect_ = false; }
/// Set the client state with IDLE handling (clears want_disconnect_).
/// Notifies the tracker of state change for loop optimization.
virtual void set_state(ClientState st) { virtual void set_state(ClientState st) {
this->set_state_internal_(st); this->state_ = st;
if (st == ClientState::IDLE) { if (st == ClientState::IDLE) {
this->want_disconnect_ = false; this->want_disconnect_ = false;
} }
} }
ClientState state() const { return this->state_; } ClientState state() const { return state_; }
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
/// The pointer must remain valid for the lifetime of the client (guaranteed since
/// ESP32BLETracker is a singleton that outlives all clients).
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
// Memory optimized layout // Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially uint8_t app_id; // App IDs are small integers assigned sequentially
protected: protected:
/// Set state without IDLE handling - use for direct state transitions. // Group 1: 1-byte types
/// Increments the tracker's state version counter to signal that loop() ClientState state_{ClientState::INIT};
/// should do full processing on the next iteration.
void set_state_internal_(ClientState st) {
this->state_ = st;
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
if (this->tracker_state_version_ != nullptr) {
(*this->tracker_state_version_)++;
}
}
// want_disconnect_ is set to true when a disconnect is requested // want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the // while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the // client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event. // ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false}; bool want_disconnect_{false};
// 2 bytes used, 2 bytes padding
private:
ClientState state_{ClientState::INIT};
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
/// state changes, we increment the tracker's counter to signal that loop()
/// should perform full processing. Null if client not registered with tracker.
uint8_t *tracker_state_version_{nullptr};
}; };
class ESP32BLETracker : public Component, class ESP32BLETracker : public Component,
@@ -416,16 +380,6 @@ class ESP32BLETracker : public Component,
// Group 4: 1-byte types (enums, uint8_t, bool) // Group 4: 1-byte types (enums, uint8_t, bool)
uint8_t app_id_{0}; uint8_t app_id_{0};
uint8_t scan_start_fail_count_{0}; uint8_t scan_start_fail_count_{0};
/// Version counter for loop() fast-path optimization. Incremented when:
/// - Scanner state changes (via set_scanner_state_())
/// - Any registered client's state changes (clients hold pointer to this counter)
/// Owned by this class; clients receive non-owning pointer via register_client().
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
/// client state counting and takes the fast path (just timeout check + return).
uint8_t state_version_{0};
/// Last state_version_ value when loop() did full processing. Compared against
/// state_version_ to detect if any state changed since last iteration.
uint8_t last_processed_version_{0};
ScannerState scanner_state_{ScannerState::IDLE}; ScannerState scanner_state_{ScannerState::IDLE};
bool scan_continuous_; bool scan_continuous_;
bool scan_active_; bool scan_active_;
@@ -442,8 +396,6 @@ class ESP32BLETracker : public Component,
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
}; };
uint32_t scan_start_time_{0}; uint32_t scan_start_time_{0};
/// Precomputed timeout value: scan_duration_ * 2000
uint32_t scan_timeout_ms_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
}; };

View File

@@ -69,10 +69,7 @@ void Esp32HostedUpdate::setup() {
// Get coprocessor version // Get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info; esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
// 16 bytes: "255.255.255" (11 chars) + null + safety margin this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
this->update_info_.current_version = buf;
} else { } else {
this->update_info_.current_version = "unknown"; this->update_info_.current_version = "unknown";
} }
@@ -297,7 +294,8 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
} }
// Stream firmware to coprocessor while computing SHA256 // Stream firmware to coprocessor while computing SHA256
sha256::SHA256 hasher; // Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init(); hasher.init();
uint8_t buffer[CHUNK_SIZE]; uint8_t buffer[CHUNK_SIZE];
@@ -354,7 +352,8 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
} }
// Verify SHA256 before writing // Verify SHA256 before writing
sha256::SHA256 hasher; // Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init(); hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_); hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate(); hasher.calculate();

View File

@@ -6,11 +6,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "preferences.h" #include "preferences.h"
#include <Arduino.h> #include <Arduino.h>
#include <core_esp8266_features.h> #include <Esp.h>
extern "C" {
#include <user_interface.h>
}
namespace esphome { namespace esphome {
@@ -20,19 +16,23 @@ void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() { void arch_restart() {
system_restart(); ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
// restart() doesn't always end execution // restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code) while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield(); yield();
} }
} }
void arch_init() {} void arch_init() {}
void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } void IRAM_ATTR HOT arch_feed_wdt() {
ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance)
}
uint8_t progmem_read_byte(const uint8_t *addr) { uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT return pgm_read_byte(addr); // NOLINT
} }
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() {
return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance)
}
uint32_t arch_get_cpu_freq_hz() { return F_CPU; } uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
void force_link_symbols() { void force_link_symbols() {

View File

@@ -99,7 +99,7 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) {
} }
size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const { size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "GPIO%u", this->pin_); return snprintf(buffer, len, "GPIO%u", this->pin_);
} }
bool ESP8266GPIOPin::digital_read() { bool ESP8266GPIOPin::digital_read() {

View File

@@ -563,9 +563,11 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function. // (no passing to other functions). All hash operations must happen in this function.
sha256::SHA256 hasher; // NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
const size_t hex_size = hasher.get_size() * 2; const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4; const size_t nonce_len = hasher.get_size() / 4;
@@ -637,9 +639,11 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
const char *cnonce = nonce + hex_size; const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size; const char *response = cnonce + hex_size;
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function. // (no passing to other functions). All hash operations must happen in this function.
sha256::SHA256 hasher; // NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
hasher.init(); hasher.init();
hasher.add(this->password_.c_str(), this->password_.length()); hasher.add(this->password_.c_str(), this->password_.length());

View File

@@ -90,7 +90,9 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []): for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) await automation.build_automation(
trigger, [(cg.std_string, "event_type")], conf
)
cg.add(var.set_event_types(event_types)) cg.add(var.set_event_types(event_types))

View File

@@ -14,10 +14,10 @@ template<typename... Ts> class TriggerEventAction : public Action<Ts...>, public
void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); }
}; };
class EventTrigger : public Trigger<StringRef> { class EventTrigger : public Trigger<std::string> {
public: public:
EventTrigger(Event *event) { EventTrigger(Event *event) {
event->add_on_event_callback([this](StringRef event_type) { this->trigger(event_type); }); event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); });
} }
}; };

View File

@@ -22,8 +22,8 @@ void Event::trigger(const std::string &event_type) {
return; return;
} }
this->last_event_type_ = found; this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(StringRef(found)); this->event_callback_.call(event_type);
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this); ControllerRegistry::notify_event(this);
#endif #endif
@@ -45,7 +45,7 @@ void Event::set_event_types(const std::vector<const char *> &event_types) {
this->last_event_type_ = nullptr; // Reset when types change this->last_event_type_ = nullptr; // Reset when types change
} }
void Event::add_on_event_callback(std::function<void(StringRef event_type)> &&callback) { void Event::add_on_event_callback(std::function<void(const std::string &event_type)> &&callback) {
this->event_callback_.add(std::move(callback)); this->event_callback_.add(std::move(callback));
} }

View File

@@ -70,10 +70,10 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Check if an event has been triggered. /// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; } bool has_event() const { return this->last_event_type_ != nullptr; }
void add_on_event_callback(std::function<void(StringRef event_type)> &&callback); void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
protected: protected:
LazyCallbackManager<void(StringRef event_type)> event_callback_; LazyCallbackManager<void(const std::string &event_type)> event_callback_;
FixedVector<const char *> types_; FixedVector<const char *> types_;
private: private:

View File

@@ -160,7 +160,7 @@ void EZOSensor::loop() {
this->commands_.pop_front(); this->commands_.pop_front();
} }
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) { void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand); std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = command; ezo_command->command = command;
ezo_command->command_type = command_type; ezo_command->command_type = command_type;
@@ -169,17 +169,13 @@ void EZOSensor::add_command_(const char *command, EzoCommandType command_type, u
} }
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) { void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
char payload[24];
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
} }
void EZOSensor::set_address(uint8_t address) { void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) { if (address > 0 && address < 128) {
// max 8: "I2C,"(4) + uint8(3) + null std::string payload = str_sprintf("I2C,%u", address);
char payload[8];
snprintf(payload, sizeof(payload), "I2C,%u", address);
this->new_address_ = address; this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C); this->add_command_(payload, EzoCommandType::EZO_I2C);
} else { } else {
@@ -198,9 +194,7 @@ void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_
void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); } void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) { void EZOSensor::set_t(float value) {
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment std::string payload = str_sprintf("T,%0.2f", value);
char payload[16];
snprintf(payload, sizeof(payload), "T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T); this->add_command_(payload, EzoCommandType::EZO_T);
} }
@@ -221,9 +215,7 @@ void EZOSensor::set_calibration_point_high(float value) {
} }
void EZOSensor::set_calibration_generic(float value) { void EZOSensor::set_calibration_generic(float value) {
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16 std::string payload = str_sprintf("Cal,%0.2f", value);
char payload[16];
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
} }
@@ -231,11 +223,13 @@ void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommand
void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); } void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); }
void EZOSensor::set_led_state(bool on) { this->add_command_(on ? "L,1" : "L,0", EzoCommandType::EZO_LED); } void EZOSensor::set_led_state(bool on) {
std::string to_send = "L,";
void EZOSensor::send_custom(const std::string &to_send) { to_send += on ? "1" : "0";
this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM); this->add_command_(to_send, EzoCommandType::EZO_LED);
} }
void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); }
} // namespace ezo } // namespace ezo
} // namespace esphome } // namespace esphome

View File

@@ -92,7 +92,7 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
std::deque<std::unique_ptr<EzoCommand>> commands_; std::deque<std::unique_ptr<EzoCommand>> commands_;
int new_address_; int new_address_;
void add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms = 300); void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300);
void set_calibration_point_(EzoCalibrationType type, float value); void set_calibration_point_(EzoCalibrationType type, float value);

View File

@@ -318,93 +318,90 @@ void EzoPMP::send_next_command_() {
switch (this->next_command_) { switch (this->next_command_) {
// Read Commands // Read Commands
case EZO_PMP_COMMAND_READ_DOSING: // Page 54 case EZO_PMP_COMMAND_READ_DOSING: // Page 54
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,?"); command_buffer_length = sprintf((char *) command_buffer, "D,?");
break; break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "R"); command_buffer_length = sprintf((char *) command_buffer, "R");
break; break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE:
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,?"); command_buffer_length = sprintf((char *) command_buffer, "DC,?");
break; break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS: case EZO_PMP_COMMAND_READ_PAUSE_STATUS:
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P,?"); command_buffer_length = sprintf((char *) command_buffer, "P,?");
break; break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED:
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "TV,?"); command_buffer_length = sprintf((char *) command_buffer, "TV,?");
break; break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED:
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "ATV,?"); command_buffer_length = sprintf((char *) command_buffer, "ATV,?");
break; break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS:
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,?"); command_buffer_length = sprintf((char *) command_buffer, "Cal,?");
break; break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE:
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "PV,?"); command_buffer_length = sprintf((char *) command_buffer, "PV,?");
break; break;
// Non-Read Commands // Non-Read Commands
case EZO_PMP_COMMAND_FIND: // Find (page 52) case EZO_PMP_COMMAND_FIND: // Find (page 52)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Find"); command_buffer_length = sprintf((char *) command_buffer, "Find");
wait_time_for_command = 60000; // This command will block all updates for a minute wait_time_for_command = 60000; // This command will block all updates for a minute
break; break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,*"); command_buffer_length = sprintf((char *) command_buffer, "D,*");
break; break;
case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Clear"); command_buffer_length = sprintf((char *) command_buffer, "Clear");
break; break;
case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,clear"); command_buffer_length = sprintf((char *) command_buffer, "Cal,clear");
break; break;
case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P"); command_buffer_length = sprintf((char *) command_buffer, "P");
break; break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "X"); command_buffer_length = sprintf((char *) command_buffer, "X");
break; break;
// Non-Read commands with parameters // Non-Read commands with parameters
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
command_buffer_length = command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_);
snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f", this->next_command_volume_);
break; break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f,%i", command_buffer_length =
this->next_command_volume_, this->next_command_duration_); sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
break; break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,%0.1f,%i", command_buffer_length =
this->next_command_volume_, this->next_command_duration_); sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
break; break;
case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65)
command_buffer_length = command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_);
snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,%0.2f", this->next_command_volume_);
break; break;
case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73) case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73)
command_buffer_length = command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_);
snprintf((char *) command_buffer, sizeof(command_buffer), "I2C,%i", this->next_command_duration_);
break; break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_); command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_);
ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer);
break; break;

View File

@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_) "FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
) )
FanPresetSetTrigger = fan_ns.class_( FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef) "FanPresetSetTrigger", automation.Trigger.template(cg.std_string)
) )
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
await automation.build_automation(trigger, [(cg.int_, "x")], conf) await automation.build_automation(trigger, [(cg.int_, "x")], conf)
for conf in config.get(CONF_ON_PRESET_SET, []): for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.StringRef, "x")], conf) await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
async def register_fan(var, config): async def register_fan(var, config):

View File

@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
int last_speed_; int last_speed_;
}; };
class FanPresetSetTrigger : public Trigger<StringRef> { class FanPresetSetTrigger : public Trigger<std::string> {
public: public:
FanPresetSetTrigger(Fan *state) { FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() { state->add_on_state_callback([this, state]() {
@@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<StringRef> {
auto should_trigger = preset_mode != this->last_preset_mode_; auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode; this->last_preset_mode_ = preset_mode;
if (should_trigger) { if (should_trigger) {
this->trigger(preset_mode); this->trigger(std::string(preset_mode));
} }
}); });
this->last_preset_mode_ = state->get_preset_mode(); this->last_preset_mode_ = state->get_preset_mode();

View File

@@ -201,7 +201,7 @@ void Fan::publish_state() {
auto traits = this->get_traits(); auto traits = this->get_traits();
ESP_LOGD(TAG, ESP_LOGD(TAG,
"'%s' >>\n" "'%s' - Sending state:\n"
" State: %s", " State: %s",
this->name_.c_str(), ONOFF(this->state)); this->name_.c_str(), ONOFF(this->state));
if (traits.supports_speed()) { if (traits.supports_speed()) {

View File

@@ -163,10 +163,9 @@ bool GDK101Component::read_fw_version_(uint8_t *data) {
return false; return false;
} }
// max 8: "255.255" (7 chars) + null const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]);
char buf[8];
snprintf(buf, sizeof(buf), "%d.%d", data[0], data[1]); this->fw_version_text_sensor_->publish_state(fw_version_str);
this->fw_version_text_sensor_->publish_state(buf);
} }
#endif // USE_TEXT_SENSOR #endif // USE_TEXT_SENSOR
return true; return true;

View File

@@ -1,3 +1,4 @@
#include <cstdio>
#include <cstring> #include <cstring>
#include "hmac_sha256.h" #include "hmac_sha256.h"
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
@@ -25,7 +26,9 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
void HmacSHA256::get_hex(char *output) { void HmacSHA256::get_hex(char *output) {
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE); for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
sprintf(output + (i * 2), "%02x", this->digest_[i]);
}
} }
bool HmacSHA256::equals_bytes(const uint8_t *expected) { bool HmacSHA256::equals_bytes(const uint8_t *expected) {

View File

@@ -97,7 +97,7 @@ void HomeassistantNumber::control(float value) {
entity_value.key = VALUE_KEY; entity_value.key = VALUE_KEY;
// Stack buffer - no heap allocation; %g produces shortest representation // Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16]; char value_buf[16];
buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value); snprintf(value_buf, sizeof(value_buf), "%g", value);
entity_value.value = StringRef(value_buf); entity_value.value = StringRef(value_buf);
api::global_api_server->send_homeassistant_action(resp); api::global_api_server->send_homeassistant_action(resp);

View File

@@ -242,7 +242,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
return; return;
} }
size_t max_length = this->max_response_buffer_size_; size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
#ifdef USE_HTTP_REQUEST_RESPONSE #ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) { if (this->capture_response_.value(x...)) {
std::string response_body; std::string response_body;

View File

@@ -213,12 +213,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->feed_wdt(); int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt(); if (bufsize == 0) {
if (read_len > 0) { this->duration_ms += (millis() - start);
this->bytes_read_ += read_len; return 0;
} }
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start); this->duration_ms += (millis() - start);
return read_len; return read_len;

View File

@@ -1,4 +1,3 @@
import logging
from typing import Any from typing import Any
from esphome import automation, pins from esphome import automation, pins
@@ -19,16 +18,13 @@ from esphome.const import (
CONF_ROTATION, CONF_ROTATION,
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
) )
from esphome.core import ID, EnumValue from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.helpers import add_class_to_obj
from esphome.types import ConfigType from esphome.types import ConfigType
from . import boards, hub75_ns from . import boards, hub75_ns
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@stuartparmenter"] CODEOWNERS = ["@stuartparmenter"]
@@ -124,51 +120,13 @@ PANEL_LAYOUTS = {
} }
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True) Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_WIRINGS = { SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
} }
# Deprecated scan wiring names - mapped to new names
DEPRECATED_SCAN_WIRINGS = {
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
}
def _validate_scan_wiring(value):
"""Validate scan_wiring with deprecation warnings for old names."""
value = cv.string(value).upper().replace(" ", "_")
# Check if using deprecated name
# Remove deprecated names in 2026.7.0
if value in DEPRECATED_SCAN_WIRINGS:
new_name = DEPRECATED_SCAN_WIRINGS[value]
_LOGGER.warning(
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
"Please use '%s' instead.",
value,
new_name,
)
value = new_name
# Validate against allowed values
if value not in SCAN_WIRINGS:
raise cv.Invalid(
f"Unknown scan wiring '{value}'. "
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
)
# Return as EnumValue like cv.enum does
result = add_class_to_obj(value, EnumValue)
result.enum_value = SCAN_WIRINGS[value]
return result
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
CLOCK_SPEEDS = { CLOCK_SPEEDS = {
"8MHZ": Hub75ClockSpeed.HZ_8M, "8MHZ": Hub75ClockSpeed.HZ_8M,
@@ -424,7 +382,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int, cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"), cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
# Panel hardware configuration # Panel hardware configuration
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring, cv.Optional(CONF_SCAN_WIRING): cv.enum(
SCAN_PATTERNS, upper=True, space="_"
),
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True), cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
# Display configuration # Display configuration
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean, cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
@@ -587,7 +547,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
add_idf_component( add_idf_component(
name="esphome/esp-hub75", name="esphome/esp-hub75",
ref="0.3.0", ref="0.2.2",
) )
# Set compile-time configuration via build flags (so external library sees them) # Set compile-time configuration via build flags (so external library sees them)

View File

@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
} }
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get(); uint8_t *buffer = buffer_alloc.get(len + 1);
buffer[0] = a_register; buffer[0] = a_register;
std::copy(data, data + len, buffer + 1); std::copy(data, data + len, buffer + 1);
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
} }
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get(); uint8_t *buffer = buffer_alloc.get(len + 2);
buffer[0] = a_register >> 8; buffer[0] = a_register >> 8;
buffer[1] = a_register; buffer[1] = a_register;

View File

@@ -11,6 +11,22 @@
namespace esphome { namespace esphome {
namespace i2c { namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> heap_buffer_;
};
/// @brief Error codes returned by I2CBus and I2CDevice methods /// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode { enum ErrorCode {
NO_ERROR = 0, ///< No error found during execution of method NO_ERROR = 0, ///< No error found during execution of method
@@ -76,8 +92,8 @@ class I2CBus {
total_len += read_buffers[i].len; total_len += read_buffers[i].len;
} }
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get(); uint8_t *buffer = buffer_alloc.get(total_len);
auto err = this->write_readv(address, nullptr, 0, buffer, total_len); auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
if (err != ERROR_OK) if (err != ERROR_OK)
@@ -100,8 +116,8 @@ class I2CBus {
total_len += write_buffers[i].len; total_len += write_buffers[i].len;
} }
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get(); uint8_t *buffer = buffer_alloc.get(total_len);
size_t pos = 0; size_t pos = 0;
for (size_t i = 0; i != count; i++) { for (size_t i = 0; i != count; i++) {

View File

@@ -665,10 +665,15 @@ async def write_image(config, all_frames=False):
if is_svg_file(path): if is_svg_file(path):
import resvg_py import resvg_py
resize = resize or (None, None) if resize:
image_data = resvg_py.svg_to_bytes( width, height = resize
svg_path=str(path), width=resize[0], height=resize[1], dpi=100 # resvg-py allows rendering by width/height directly
) image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
# Convert bytes to Pillow Image # Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data)) image = Image.open(io.BytesIO(image_data))

View File

@@ -18,15 +18,7 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) { InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings; this->raw_timings_ = &timings;
this->packed_data_ = nullptr; this->packed_data_ = nullptr; // Clear packed if vector is set
this->base64url_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return *this; return *this;
} }
@@ -34,8 +26,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
this->packed_data_ = data; this->packed_data_ = data;
this->packed_length_ = length; this->packed_length_ = length;
this->packed_count_ = count; this->packed_count_ = count;
this->raw_timings_ = nullptr; this->raw_timings_ = nullptr; // Clear vector if packed is set
this->base64url_ptr_ = nullptr;
return *this; return *this;
} }
@@ -101,23 +92,6 @@ void Infrared::control(const InfraredCall &call) {
call.get_packed_count()); call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
call.get_repeat_count()); call.get_repeat_count());
} else if (call.is_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
call.get_repeat_count());
} else { } else {
// From vector (lambdas/automations) // From vector (lambdas/automations)
transmit_data->set_data(call.get_raw_timings()); transmit_data->set_data(call.get_raw_timings());

View File

@@ -28,29 +28,12 @@ class InfraredCall {
/// Set the carrier frequency in Hz /// Set the carrier frequency in Hz
InfraredCall &set_carrier_frequency(uint32_t frequency); InfraredCall &set_carrier_frequency(uint32_t frequency);
/// Set the raw timings (positive = mark, negative = space)
// ===== Raw Timings Methods ===== /// Note: The timings vector must outlive the InfraredCall (zero-copy reference)
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings); InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
/// Set the raw timings from base64url-encoded little-endian int32 data /// Note: The data must outlive the InfraredCall
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.) /// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
InfraredCall &set_repeat_count(uint32_t count); InfraredCall &set_repeat_count(uint32_t count);
@@ -59,18 +42,12 @@ class InfraredCall {
/// Get the carrier frequency /// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; } const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
/// Get the raw timings (only valid if set via set_raw_timings) /// Get the raw timings (only valid if set via set_raw_timings, not packed)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; } const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (any format) /// Check if raw timings have been set (either vector or packed)
bool has_raw_timings() const { bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; }
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format /// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; } bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed) /// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; } const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; } uint16_t get_packed_length() const { return this->packed_length_; }
@@ -82,11 +59,9 @@ class InfraredCall {
uint32_t repeat_count_{1}; uint32_t repeat_count_{1};
Infrared *parent_; Infrared *parent_;
optional<uint32_t> carrier_frequency_; optional<uint32_t> carrier_frequency_;
// Pointer to vector-based timings (caller-owned, must outlive perform()) // Vector-based timings (for lambdas/automations)
const std::vector<int32_t> *raw_timings_{nullptr}; const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base64url-encoded string (caller-owned, must outlive perform()) // Packed protobuf timings (for API zero-copy)
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr}; const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0}; uint16_t packed_length_{0};
uint16_t packed_count_{0}; uint16_t packed_count_{0};

View File

@@ -1,6 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core import CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
json_ns = cg.esphome_ns.namespace("json") json_ns = cg.esphome_ns.namespace("json")
@@ -12,11 +12,6 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(CoroPriority.BUS) @coroutine_with_priority(CoroPriority.BUS)
async def to_code(config): async def to_code(config):
if CORE.is_esp32: cg.add_library("bblanchon/ArduinoJson", "7.4.2")
from esphome.components.esp32 import add_idf_component
add_idf_component(name="bblanchon/arduinojson", ref="7.4.2")
else:
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
cg.add_define("USE_JSON") cg.add_define("USE_JSON")
cg.add_global(json_ns.using) cg.add_global(json_ns.using)

View File

@@ -382,11 +382,4 @@ async def component_to_code(config):
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS "custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
) )
# Disable LWIP statistics to save RAM - not needed in production
# Must explicitly disable all sub-stats to avoid redefinition warnings
cg.add_platformio_option(
"custom_options.lwip",
["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"],
)
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -166,8 +166,8 @@ class LibreTinyPreferences : public ESPPreferences {
return true; return true;
} }
// Most preferences are small, use stack buffer with heap fallback for large ones // Allocate buffer on heap to avoid stack allocation for large data
SmallBufferWithHeapFallback<256> stored_data(kv.value_len); auto stored_data = std::make_unique<uint8_t[]>(kv.value_len);
fdb_blob_make(&this->blob, stored_data.get(), kv.value_len); fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob); size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
if (actual_len != kv.value_len) { if (actual_len != kv.value_len) {

View File

@@ -1,5 +1,4 @@
#include "light_json_schema.h" #include "light_json_schema.h"
#include "color_mode.h"
#include "light_output.h" #include "light_output.h"
#include "esphome/core/progmem.h" #include "esphome/core/progmem.h"
@@ -9,32 +8,29 @@ namespace esphome::light {
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
// Get JSON string for color mode. // Get JSON string for color mode using linear search (avoids large switch jump table)
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would static const char *get_color_mode_json_str(ColorMode mode) {
// generate a large jump table. Converting to bit index (0-9) allows a compact switch. // Parallel arrays: mode values and their corresponding strings
static ProgmemStr get_color_mode_json_str(ColorMode mode) { // Uses less RAM than a switch jump table on sparse enum values
switch (ColorModeBitPolicy::to_bit(mode)) { static constexpr ColorMode MODES[] = {
case 1: ColorMode::ON_OFF,
return ESPHOME_F("onoff"); ColorMode::BRIGHTNESS,
case 2: ColorMode::WHITE,
return ESPHOME_F("brightness"); ColorMode::COLOR_TEMPERATURE,
case 3: ColorMode::COLD_WARM_WHITE,
return ESPHOME_F("white"); ColorMode::RGB,
case 4: ColorMode::RGB_WHITE,
return ESPHOME_F("color_temp"); ColorMode::RGB_COLOR_TEMPERATURE,
case 5: ColorMode::RGB_COLD_WARM_WHITE,
return ESPHOME_F("cwww"); };
case 6: static constexpr const char *STRINGS[] = {
return ESPHOME_F("rgb"); "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww",
case 7: };
return ESPHOME_F("rgbw"); for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) {
case 8: if (MODES[i] == mode)
return ESPHOME_F("rgbct"); return STRINGS[i];
case 9:
return ESPHOME_F("rgbww");
default:
return nullptr;
} }
return nullptr;
} }
void LightJSONSchema::dump_json(LightState &state, JsonObject root) { void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
@@ -48,7 +44,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
auto values = state.remote_values; auto values = state.remote_values;
const auto color_mode = values.get_color_mode(); const auto color_mode = values.get_color_mode();
const auto *mode_str = get_color_mode_json_str(color_mode); const char *mode_str = get_color_mode_json_str(color_mode);
if (mode_str != nullptr) { if (mode_str != nullptr) {
root[ESPHOME_F("color_mode")] = mode_str; root[ESPHOME_F("color_mode")] = mode_str;
} }

View File

@@ -1,4 +1,3 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP8266 #ifdef USE_ESP8266
@@ -45,16 +44,13 @@ void LightWaveRF::send_rx(const std::vector<uint8_t> &msg, uint8_t repeats, bool
} }
void LightWaveRF::print_msg_(uint8_t *msg, uint8_t len) { void LightWaveRF::print_msg_(uint8_t *msg, uint8_t len) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG char buffer[65];
char buffer[65]; // max 10 entries * 6 chars + null
ESP_LOGD(TAG, " Received code (len:%i): ", len); ESP_LOGD(TAG, " Received code (len:%i): ", len);
size_t pos = 0;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
pos = buf_append_printf(buffer, sizeof(buffer), pos, "0x%02x, ", msg[i]); sprintf(&buffer[i * 6], "0x%02x, ", msg[i]);
} }
ESP_LOGD(TAG, "[%s]", buffer); ESP_LOGD(TAG, "[%s]", buffer);
#endif
} }
void LightWaveRF::dump_config() { void LightWaveRF::dump_config() {

View File

@@ -28,14 +28,16 @@ const LogString *lock_state_to_string(LockState state) {
Lock::Lock() : state(LOCK_STATE_NONE) {} Lock::Lock() : state(LOCK_STATE_NONE) {}
LockCall Lock::make_call() { return LockCall(this); } LockCall Lock::make_call() { return LockCall(this); }
void Lock::set_state_(LockState state) { void Lock::lock() {
auto call = this->make_call(); auto call = this->make_call();
call.set_state(state); call.set_state(LOCK_STATE_LOCKED);
this->control(call);
}
void Lock::unlock() {
auto call = this->make_call();
call.set_state(LOCK_STATE_UNLOCKED);
this->control(call); this->control(call);
} }
void Lock::lock() { this->set_state_(LOCK_STATE_LOCKED); }
void Lock::unlock() { this->set_state_(LOCK_STATE_UNLOCKED); }
void Lock::open() { void Lock::open() {
if (traits.get_supports_open()) { if (traits.get_supports_open()) {
ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str()); ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str());
@@ -50,7 +52,7 @@ void Lock::publish_state(LockState state) {
this->state = state; this->state = state;
this->rtc_.save(&this->state); this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_lock_update(this); ControllerRegistry::notify_lock_update(this);

View File

@@ -156,9 +156,6 @@ class Lock : public EntityBase {
protected: protected:
friend LockCall; friend LockCall;
/// Helper for lock/unlock convenience methods
void set_state_(LockState state);
/** Perform the open latch action with hardware. This method is optional to implement /** Perform the open latch action with hardware. This method is optional to implement
* when creating a new lock. * when creating a new lock.
* *

View File

@@ -1,5 +1,8 @@
#include "logger.h" #include "logger.h"
#include <cinttypes> #include <cinttypes>
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#include <memory> // For unique_ptr
#endif
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@@ -196,8 +199,7 @@ inline uint8_t Logger::level_for(const char *tag) {
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) {
// add 1 to buffer size for null terminator // add 1 to buffer size for null terminator
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->main_task_ = xTaskGetCurrentTaskHandle(); this->main_task_ = xTaskGetCurrentTaskHandle();
#elif defined(USE_ZEPHYR) #elif defined(USE_ZEPHYR)
@@ -210,14 +212,11 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
void Logger::init_log_buffer(size_t total_buffer_size) { void Logger::init_log_buffer(size_t total_buffer_size) {
#ifdef USE_HOST #ifdef USE_HOST
// Host uses slot count instead of byte size // Host uses slot count instead of byte size
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferHost>(total_buffer_size);
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size);
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
#elif defined(USE_LIBRETINY) #elif defined(USE_LIBRETINY)
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferLibreTiny>(total_buffer_size);
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)

View File

@@ -412,11 +412,11 @@ class Logger : public Component {
#endif #endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST #ifdef USE_HOST
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed std::unique_ptr<logger::TaskLogBufferHost> log_buffer_; // Will be initialized with init_log_buffer
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
#elif defined(USE_LIBRETINY) #elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed std::unique_ptr<logger::TaskLogBufferLibreTiny> log_buffer_; // Will be initialized with init_log_buffer
#endif #endif
#endif #endif

View File

@@ -2,7 +2,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes>
#include <map> #include <map>
#include <string> #include <string>
@@ -44,17 +43,8 @@ template<typename K, typename V> class Mapping {
esph_log_e(TAG, "Key '%p' not found in mapping", key); esph_log_e(TAG, "Key '%p' not found in mapping", key);
} else if constexpr (std::is_same_v<K, std::string>) { } else if constexpr (std::is_same_v<K, std::string>) {
esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str()); esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str());
} else if constexpr (std::is_integral_v<K>) {
char buf[24]; // enough for 64-bit integer
if constexpr (std::is_unsigned_v<K>) {
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(key));
} else {
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, static_cast<int64_t>(key));
}
esph_log_e(TAG, "Key '%s' not found in mapping", buf);
} else { } else {
// All supported key types are handled above - this should never be reached esph_log_e(TAG, "Key '%s' not found in mapping", to_string(key).c_str());
static_assert(sizeof(K) == 0, "Unsupported key type for Mapping error logging");
} }
return {}; return {};
} }

View File

@@ -162,7 +162,7 @@ void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const { size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "%u via Max6956", this->pin_); return snprintf(buffer, len, "%u via Max6956", this->pin_);
} }
} // namespace max6956 } // namespace max6956

View File

@@ -100,7 +100,7 @@ void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this
bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const { size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "%u via MCP23016", this->pin_); return snprintf(buffer, len, "%u via MCP23016", this->pin_);
} }
} // namespace mcp23016 } // namespace mcp23016

View File

@@ -17,7 +17,7 @@ template<uint8_t N> void MCP23XXXGPIOPin<N>::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_); this->parent_->digital_write(this->pin_, value != this->inverted_);
} }
template<uint8_t N> size_t MCP23XXXGPIOPin<N>::dump_summary(char *buffer, size_t len) const { template<uint8_t N> size_t MCP23XXXGPIOPin<N>::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "%u via MCP23XXX", this->pin_); return snprintf(buffer, len, "%u via MCP23XXX", this->pin_);
} }
template class MCP23XXXGPIOPin<8>; template class MCP23XXXGPIOPin<8>;

Some files were not shown because too many files have changed in this diff Show More