Compare commits

..

107 Commits

Author SHA1 Message Date
Jesse Hills 7ecdf6db2e Merge pull request #15084 from esphome/bump-2026.3.1
2026.3.1
2026-03-23 16:09:32 +13:00
J. Nick Koston 83d02c602a [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:25:04 +13:00
J. Nick Koston 6d16c57747 [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) 2026-03-23 11:23:21 +13:00
J. Nick Koston 45c0e6ef7f [logger] Fix unit test Logger constructor call (#15086) 2026-03-23 09:52:46 +13:00
Jesse Hills 320474b62d Bump version to 2026.3.1 2026-03-23 09:28:58 +13:00
Jason Kölker a3c483edf3 [pmsx003] Keep active-mode reads aligned (#14832) 2026-03-23 09:28:58 +13:00
J. Nick Koston 036be63f7b [logger] Fix race condition in task log buffer initialization (#15071)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-23 09:28:58 +13:00
Jonathan Swoboda bbfe324dd6 [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) 2026-03-23 09:28:57 +13:00
J. Nick Koston de3292c828 [light] Fix gamma LUT quantizing small brightness to zero (#15060) 2026-03-23 09:28:57 +13:00
J. Nick Koston 67ab2e143c [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) 2026-03-23 09:28:57 +13:00
J. Nick Koston 9abc112f76 [sht4x] Fix heater causing measurement jitter (#15030) 2026-03-23 09:28:50 +13:00
J. Nick Koston b5880df93c [light] Fix constant_brightness broken by gamma LUT refactor (#15048)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
J. Nick Koston 2352c732de [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) 2026-03-23 09:27:59 +13:00
Samuel Sieb 77264de3f6 [analog_threshhold] add missing header (#15058) 2026-03-23 09:27:59 +13:00
J. Nick Koston 42da281854 [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) 2026-03-23 09:27:59 +13:00
J. Nick Koston 06cc5a29a7 [core] Add copy() method to StringRef for std::string compatibility (#15028) 2026-03-23 09:27:59 +13:00
J. Nick Koston 98b4e1ea15 [web_server] Increase httpd task stack size to prevent stack overflow (#14997)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
Jonathan Swoboda 0bf6e1e839 [esp32_touch] Fix initial state never published when sensor untouched (#15032) 2026-03-23 09:27:59 +13:00
J. Nick Koston 3fe84eadef [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) 2026-03-23 09:27:59 +13:00
J. Nick Koston 12eed0d384 [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) 2026-03-23 09:27:59 +13:00
dependabot[bot] 28e8250b69 Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
Keith Roehrenbeck 0297260a57 [ld2450] Fix zone target counts including untracked ghost targets (#15026) 2026-03-23 09:27:59 +13:00
J. Nick Koston d4f7cb984c [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) 2026-03-23 09:27:59 +13:00
Clyde Stubbs 08187a01b1 [sdl] Fix get_width()/height() when rotation used (#14950) 2026-03-23 09:27:59 +13:00
J. Nick Koston daf3502e15 [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) 2026-03-23 09:27:59 +13:00
J. Nick Koston 08cab43548 [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) 2026-03-23 09:27:59 +13:00
dependabot[bot] 5cbe936256 Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
Jonathan Swoboda 729d3d4bc2 [openthread] Guard InstanceLock against uninitialized semaphore (#14940)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:27:58 +13:00
Jonathan Swoboda 8af0991590 [ble_client] Fix RSSI sensor reporting same value for all clients (#14939)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:27:58 +13:00
J. Nick Koston 99d968f80a [http_request] Prevent double update task launch (#14910) 2026-03-23 09:27:58 +13:00
dependabot[bot] 705d548435 Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 09:27:58 +13:00
Jesse Hills 609003c897 Merge pull request #14941 from esphome/bump-2026.3.0
2026.3.0
2026-03-19 17:33:27 +13:00
Jesse Hills 2c10adba85 Bump version to 2026.3.0 2026-03-19 13:09:03 +13:00
Jesse Hills 9e4e2d78dc Merge pull request #14926 from esphome/bump-2026.3.0b5
2026.3.0b5
2026-03-19 09:19:50 +13:00
Jesse Hills af9366fdd4 Bump version to 2026.3.0b5 2026-03-19 08:19:26 +13:00
J. Nick Koston 448402ca2c [http_request] Fix data race on update_info_ strings in update task (#14909) 2026-03-19 08:19:26 +13:00
Jonathan Swoboda fc67551edc [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) 2026-03-19 08:19:26 +13:00
Jonathan Swoboda 98d3dce672 [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) 2026-03-19 08:19:26 +13:00
Jonathan Swoboda 4cb93d4df8 [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) 2026-03-19 08:19:18 +13:00
Jonathan Swoboda 91e66cfd9d [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) 2026-03-19 08:17:03 +13:00
Jonathan Swoboda 6cf32af33f [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) 2026-03-19 08:17:03 +13:00
Jesse Hills 6b9be033d6 Merge pull request #14904 from esphome/bump-2026.3.0b4
2026.3.0b4
2026-03-18 16:21:28 +13:00
Jesse Hills 5cc03d9bef Bump version to 2026.3.0b4 2026-03-18 13:35:21 +13:00
J. Nick Koston 0fa96b6e1e [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-18 13:35:21 +13:00
J. Nick Koston be2e4a5278 [mqtt] Fix data race on inbound event queue (#14891)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-18 13:35:21 +13:00
J. Nick Koston 80bd6489cf [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) 2026-03-18 13:35:21 +13:00
J. Nick Koston ccf672d7ee [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) 2026-03-18 13:35:20 +13:00
J. Nick Koston 6154b673c2 [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) 2026-03-18 13:35:20 +13:00
J. Nick Koston 3bde7ec978 [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) 2026-03-18 13:35:20 +13:00
J. Nick Koston 8caa11dcf4 [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) 2026-03-18 13:35:20 +13:00
J. Nick Koston 1b70df2c1f [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) 2026-03-18 13:35:20 +13:00
J. Nick Koston 4122fa5ddd [core] Add back deprecated set_internal() for external projects (#14887) 2026-03-18 13:35:20 +13:00
Jonathan Swoboda c5d42b0569 [speaker] Fix media playlist using announcement delay (#14889) 2026-03-18 13:35:20 +13:00
J. Nick Koston 37f9541f32 [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) 2026-03-18 13:35:20 +13:00
Diorcet Yann 8bbfadb59a [core] Small improvements (#14884) 2026-03-18 13:35:20 +13:00
Jesse Hills a40d97f346 Merge pull request #14873 from esphome/bump-2026.3.0b3
2026.3.0b3
2026-03-17 14:13:29 +13:00
Jesse Hills d6c67d5c35 Bump version to 2026.3.0b3 2026-03-17 11:45:03 +13:00
Jonathan Swoboda 0816b27398 [core] Support both dot and dash separators in Version.parse (#14858) 2026-03-17 11:45:03 +13:00
Jonathan Swoboda 9133582aa0 [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) 2026-03-17 11:45:02 +13:00
Jonathan Swoboda f36b0fcb61 [am43] Fix battery update throttle using wrong type (#14864) 2026-03-17 11:45:02 +13:00
Jonathan Swoboda bb0a5dc8a8 [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) 2026-03-17 11:45:02 +13:00
Jonathan Swoboda 0c260e483e [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) 2026-03-17 11:45:02 +13:00
tomaszduda23 b8ce907976 [ble_nus] fix uart debug (#14850) 2026-03-17 11:45:02 +13:00
dependabot[bot] ffce637ea5 Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 11:45:02 +13:00
J. Nick Koston d6fba39037 [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-17 11:45:02 +13:00
J. Nick Koston 5d5c2723b2 [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) 2026-03-17 11:45:02 +13:00
guillempages 06d1498c47 [runtime_image] Update jpegdec lib version (#14726)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-17 11:45:02 +13:00
Jesse Hills 254e1f3abb Merge pull request #14834 from esphome/bump-2026.3.0b2
2026.3.0b2
2026-03-16 11:45:42 +13:00
Jesse Hills deb6b97eea Bump version to 2026.3.0b2 2026-03-16 09:25:21 +13:00
J. Nick Koston 22ea2764d4 [debug] Fix shared buffer between reset reason and wakeup cause (#14813) 2026-03-16 09:25:21 +13:00
J. Nick Koston 632dbc8fe8 [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) 2026-03-16 09:25:21 +13:00
leccelecce 98d9871620 [online_image] Log download duration in milliseconds instead of seconds (#14803) 2026-03-16 09:25:21 +13:00
J. Nick Koston a064eceb9b [template] Fix misleading 'Text value too long to save' warning (#14753) 2026-03-16 09:25:21 +13:00
J. Nick Koston 49107f2174 [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) 2026-03-16 09:25:21 +13:00
J. Nick Koston e9c2659147 [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) 2026-03-16 09:25:20 +13:00
Kjell Braden 18b54f075e [runtime_image] fix BMP parsing (#14762) 2026-03-16 09:25:20 +13:00
J. Nick Koston 45e40223ac [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) 2026-03-16 09:25:20 +13:00
J. Nick Koston 1ab1534028 [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:25:20 +13:00
J. Nick Koston 039efdb02a [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) 2026-03-16 09:25:20 +13:00
J. Nick Koston b0447dc521 [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) 2026-03-16 09:25:20 +13:00
J. Nick Koston aacbaab5f8 [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) 2026-03-16 09:25:20 +13:00
J. Nick Koston dc5032f72f [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) 2026-03-16 09:25:20 +13:00
J. Nick Koston c263c2c382 [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:25:20 +13:00
J. Nick Koston 910784ca84 [debug] Fix missing reset reason for RP2040/RP2350 (#14740) 2026-03-16 09:25:20 +13:00
J. Nick Koston 0b99e8f08d [rp2040] Use full flash for sketch in testing mode (#14747)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:25:20 +13:00
J. Nick Koston 93be539789 [light] Fix ambiguous set_effect overload for const char* (#14732) 2026-03-16 09:25:20 +13:00
Brian Kaufman 390bb0451f [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) 2026-03-16 09:25:20 +13:00
J. Nick Koston 14c3e2d9d9 [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) 2026-03-16 09:25:20 +13:00
J. Nick Koston 23c7e0f803 [uart] Allow hardware UART with single pin on RP2040 (#14725) 2026-03-16 09:25:20 +13:00
J. Nick Koston cb4d1d1b5e [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) 2026-03-16 09:25:20 +13:00
J. Nick Koston 2ba807efe8 [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) 2026-03-16 09:25:20 +13:00
J. Nick Koston c8cf9b74b1 [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) 2026-03-16 09:25:19 +13:00
J. Nick Koston 33475703da [time] Fix settimeofday() failure on ESP8266 (#14707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
J. Nick Koston 1b7d0f9c0b [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:25:19 +13:00
J. Nick Koston 1d881ef6f4 [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) 2026-03-16 09:25:19 +13:00
J. Nick Koston 3a838d897f [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) 2026-03-16 09:25:19 +13:00
Matthias König da130c900f [mqtt] Fixed permission denied error for client certificates on Windows (#13525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
Kevin Ahrendt 440734dadf [audio] Bump microOpus to v0.3.5 (#14727) 2026-03-16 09:25:19 +13:00
Brian Kaufman df2ddc47ec [web_server] use DETAIL_ALL in update_all_json_generator (#14711) 2026-03-16 09:25:19 +13:00
Keith Burzinski 4b1c4ba5c0 [ledc] Fix high-pressure crash & recovery (#14720) 2026-03-16 09:25:19 +13:00
Adam DeMuri 6002badb3c [modbus] Fix buffer overflow in modbus (#14719)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-16 09:25:19 +13:00
J. Nick Koston e8f51fec88 [rp2040] Fix crash handler design flaws (#14716) 2026-03-16 09:25:19 +13:00
Keith Burzinski 7cec2d3029 [ethernet] ESP32-S3 Ethernet compilation fix (#14717) 2026-03-16 09:25:19 +13:00
J. Nick Koston 2b0c471ed7 [esp32] Add crash handler to capture and report backtrace across reboots (#14709) 2026-03-16 09:25:19 +13:00
Keith Burzinski 064bd13ebb [ethernet] ESP32-P4 Ethernet compilation fix (#14714)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
Jonathan Swoboda 2627490a11 [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:25:19 +13:00
dependabot[bot] 4219d6d367 Bump tornado from 6.5.4 to 6.5.5 (#14704)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
144 changed files with 1573 additions and 1821 deletions
+3 -19
View File
@@ -106,7 +106,6 @@ jobs:
script/build_codeowners.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
pytest:
name: Run pytest
@@ -171,8 +170,6 @@ jobs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
@@ -213,8 +210,6 @@ jobs:
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
@@ -266,20 +261,9 @@ jobs:
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
env:
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
run: |
. venv/bin/activate
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
echo "Running all integration tests"
pytest -vv --no-cov --tb=native -n auto tests/integration/
else
# Parse JSON array into bash array to avoid shell expansion issues
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
echo "Running ${#test_files[@]} specific integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
fi
pytest -vv --no-cov --tb=native -n auto tests/integration/
cpp-unit-tests:
name: Run C++ unit tests
@@ -961,13 +945,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -10,9 +10,6 @@ name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches-ignore:
- release
- beta
permissions:
issues: write
@@ -13,9 +13,6 @@ on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
branches-ignore:
- release
- beta
permissions:
pull-requests: write
-12
View File
@@ -65,18 +65,6 @@ jobs:
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}
// Check title starts with [tag] prefix
const bracketPattern = /^\[\w+\]/;
if (!bracketPattern.test(title)) {
+1 -1
View File
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
pattern: digests-*
path: /tmp/digests
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.6
rev: v0.15.5
hooks:
# Run the linter.
- id: ruff
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.4.0-dev
PROJECT_NUMBER = 2026.3.1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+3 -53
View File
@@ -1,6 +1,6 @@
"""Memory usage analyzer for ESPHome compiled binaries."""
from collections import Counter, defaultdict
from collections import defaultdict
from dataclasses import dataclass, field
import logging
from pathlib import Path
@@ -40,15 +40,6 @@ _READELF_SECTION_PATTERN = re.compile(
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
)
# Regex for extracting call targets from objdump disassembly
# Matches direct call instructions across architectures:
# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol>
# ARM: bl/blx <addr> <symbol>
# Captures the mangled symbol name inside angle brackets.
_CALL_TARGET_PATTERN = re.compile(
r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>"
)
# Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]"
@@ -206,8 +197,6 @@ class MemoryAnalyzer:
self._lib_hash_to_name: dict[str, str] = {}
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
self._heuristic_to_lib: dict[str, str] = {}
# Function call counts: mangled_name -> call_count
self._function_call_counts: Counter[str] = Counter()
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
@@ -217,7 +206,6 @@ class MemoryAnalyzer:
self._categorize_symbols()
self._analyze_cswtch_symbols()
self._analyze_sdk_libraries()
self._analyze_function_calls()
return dict(self.components)
def _parse_sections(self) -> None:
@@ -396,9 +384,8 @@ class MemoryAnalyzer:
return
_LOGGER.info("Demangling %d symbols", len(symbols))
demangled = batch_demangle(symbols, objdump_path=self.objdump_path)
self._demangle_cache.update(demangled)
_LOGGER.info("Successfully demangled %d symbols", len(demangled))
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""
@@ -1024,43 +1011,6 @@ class MemoryAnalyzer:
total_size,
)
def _analyze_function_calls(self) -> None:
"""Count function call sites by parsing disassembly output.
Parses direct call instructions (call0/call8/bl/blx) from objdump -d
to count how many times each function is called. This helps identify
inlining candidates — frequently called small functions benefit most
from inlining.
"""
result = run_tool(
[self.objdump_path, "-d", str(self.elf_path)],
timeout=60,
)
if result is None or result.returncode != 0:
_LOGGER.debug("Failed to disassemble ELF for function call analysis")
return
self._function_call_counts = Counter(
match.group(1)
for line in result.stdout.splitlines()
if (match := _CALL_TARGET_PATTERN.search(line))
)
# Demangle any call targets not already in the cache
missing = [
name
for name in self._function_call_counts
if name not in self._demangle_cache
]
if missing:
self._batch_demangle_symbols(missing)
_LOGGER.debug(
"Function call analysis: %d unique targets, %d total calls",
len(self._function_call_counts),
sum(self._function_call_counts.values()),
)
def get_unattributed_ram(self) -> tuple[int, int, int]:
"""Get unattributed RAM sizes (SDK/framework overhead).
-109
View File
@@ -231,110 +231,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f" {size:>6,} B {sym_name}")
lines.append("")
# Number of top called functions to show
TOP_CALLS_LIMIT: int = 50
# Number of inlining candidates to show
INLINE_CANDIDATES_LIMIT: int = 25
# Maximum function size in bytes to consider for inlining
INLINE_SIZE_THRESHOLD: int = 16
def _build_symbol_sizes(self) -> dict[str, int]:
"""Build a size lookup from all component symbols: mangled_name -> size."""
return {
symbol: size
for symbols in self._component_symbols.values()
for symbol, _, size, _ in symbols
}
def _format_call_row(
self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int]
) -> str:
"""Format a single row for call frequency tables."""
demangled = self._demangle_cache.get(mangled, mangled)
if len(demangled) > 80:
demangled = f"{demangled[:77]}..."
size = symbol_sizes.get(mangled)
size_str = f"{size:>5,} B" if size is not None else " ?"
return f"{index:>3} {count:>5} {size_str} {demangled}"
def _add_call_table_header(self, lines: list[str]) -> None:
"""Add the header row for call frequency tables."""
lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function")
lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}")
def _add_function_call_analysis(self, lines: list[str]) -> None:
"""Add function call frequency analysis section.
Shows the most frequently called functions by call site count.
"""
self._add_section_header(lines, "Top Called Functions")
symbol_sizes = self._build_symbol_sizes()
# Sort by call count descending
sorted_calls = sorted(
self._function_call_counts.items(), key=lambda x: x[1], reverse=True
)
self._add_call_table_header(lines)
for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]):
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
total_calls = sum(self._function_call_counts.values())
lines.append("")
lines.append(
f"Total: {len(self._function_call_counts)} unique targets, "
f"{total_calls:,} call sites"
)
lines.append("")
def _add_inline_candidates(self, lines: list[str]) -> None:
"""Add inlining candidates section.
Shows frequently called functions that are small enough to benefit
from inlining (< 16 bytes). These are the best candidates for
reducing call overhead.
"""
self._add_section_header(
lines,
f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)",
)
symbol_sizes = self._build_symbol_sizes()
# Filter to small functions with known size, sort by call count
candidates = sorted(
(
(mangled, count)
for mangled, count in self._function_call_counts.items()
if mangled in symbol_sizes
and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD
),
key=lambda x: x[1],
reverse=True,
)
if not candidates:
lines.append("No candidates found.")
lines.append("")
return
self._add_call_table_header(lines)
for i, (mangled, count) in enumerate(
candidates[: self.INLINE_CANDIDATES_LIMIT]
):
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
lines.append("")
lines.append(
f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} "
f"of {len(candidates)} functions under "
f"{self.INLINE_SIZE_THRESHOLD} B"
)
lines.append("")
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
@@ -637,11 +533,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if self._cswtch_symbols:
self._add_cswtch_analysis(lines)
# Function call frequency analysis
if self._function_call_counts:
self._add_function_call_analysis(lines)
self._add_inline_candidates(lines)
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
+1 -2
View File
@@ -22,8 +22,7 @@ namespace adc {
#ifdef USE_ESP32
// clang-format off
#if ESP_IDF_VERSION_MAJOR >= 6 || \
(ESP_IDF_VERSION_MAJOR == 5 && \
#if (ESP_IDF_VERSION_MAJOR == 5 && \
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
(ESP_IDF_VERSION_MINOR >= 2)) \
+1 -1
View File
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
uint8_t current_sensor_;
// The AM43 often gets into a state where it spams loads of battery update
// notifications. Here we will limit to no more than every 10s.
uint8_t last_battery_update_;
uint32_t last_battery_update_;
};
} // namespace am43
@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/sensor/sensor.h"
+4 -4
View File
@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
uint8_t buf[128];
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
// The ESP's i2c driver has a limited buffer size.
// This way of retrieving the data should be wrong according to the datasheet
// but it seems to work.
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
// and advances its internal pointer after every 4th byte.
uint8_t read = std::min(32, fifo_level * 4 - pos);
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
}
if (millis() - this->gesture_start_ > 500) {
+5 -1
View File
@@ -64,7 +64,11 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS *
// A stalled handshake from a buggy client or network glitch holds a connection
// slot, which can prevent legitimate clients from reconnecting. Also hardens
// against the less likely case of intentional connection slot exhaustion.
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
//
// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak
// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to
// 28-30s. See https://github.com/esphome/esphome/issues/14999
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
+8 -5
View File
@@ -442,8 +442,12 @@ class ProtoMessage {
virtual const char *message_name() const { return "unknown"; }
#endif
#ifndef USE_HOST
protected:
#endif
// Non-virtual destructor is protected to prevent polymorphic deletion.
// On host platform, made public to allow value-initialization of std::array
// members (e.g. DeviceInfoResponse::devices) without clang errors.
~ProtoMessage() = default;
};
@@ -602,7 +606,7 @@ class ProtoSize {
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + varint(encode_zigzag32(value));
}
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
@@ -614,13 +618,13 @@ class ProtoSize {
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
return value ? field_id_size + varint(value) : 0;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return field_id_size + varint(value);
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) {
static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
@@ -638,8 +642,7 @@ class ProtoSize {
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size,
uint32_t nested_size) {
static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) {
return field_id_size + varint(nested_size) + nested_size;
}
};
+1 -1
View File
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
INT_MASK = 0xF0,
THRESH_MASK = 0x0F,
R_SPIKE_MASK = 0xF0,
ENERGY_MASK = 0xF0,
ENERGY_MASK = 0xE0,
CAP_MASK = 0xF0,
LIGHT_MASK = 0xCF,
DISTURB_MASK = 0xDF,
@@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
switch (event) {
// server response on RSSI request:
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr))
return;
if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) {
int8_t rssi = param->read_rssi_cmpl.rssi;
ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);
+4 -4
View File
@@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) {
// First, use the peek buffer if available
if (this->has_peek_) {
#ifdef USE_UART_DEBUGGER
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
#endif
data[0] = this->peek_buffer_;
this->has_peek_ = false;
data++;
if (--len == 0) { // Decrement len first, then check it...
#ifdef USE_UART_DEBUGGER
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
#endif
return true; // No more to read
return true; // No more to read
}
}
+2 -2
View File
@@ -186,8 +186,8 @@ async def to_code_base(config):
cg.add_library("SPI", None)
cg.add_library(
"BME68x Sensor library",
"1.3.40408",
"https://github.com/boschsensortec/Bosch-BME68x-Library",
None,
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
)
cg.add_library(
"BSEC2 Software Library",
-1
View File
@@ -3,7 +3,6 @@
CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_CLIMATE_ID = "climate_id"
BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian"
@@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
float DallasTemperatureSensor::get_temp_c_() {
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
if (this->scratch_pad_[7] == 0) {
return NAN;
}
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
}
switch (this->resolution_) {
+2 -1
View File
@@ -18,6 +18,7 @@ namespace debug {
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128;
// buf_append_printf is now provided by esphome/core/helpers.h
@@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent {
#endif // USE_TEXT_SENSOR
const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer);
uint32_t get_free_heap_();
size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos);
void update_platform_();
+5 -4
View File
@@ -98,7 +98,7 @@ static const char *const WAKEUP_CAUSES[] = {
"BT",
};
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
const char *wake_reason;
unsigned reason = esp_sleep_get_wakeup_cause();
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
@@ -196,9 +196,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
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 *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
char reset_buffer[RESET_REASON_BUFFER_SIZE];
char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer));
const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer));
uint8_t mac[6];
get_mac_address_raw(mac);
+1 -1
View File
@@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
return buffer.data();
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
// ESP8266 doesn't have detailed wakeup cause like ESP32
return "";
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace debug {
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
+1 -1
View File
@@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
return lt_get_reboot_reason_name(lt_get_reboot_reason());
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); }
+1 -1
View File
@@ -67,7 +67,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
return buf;
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); }
+1 -1
View File
@@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
return buf;
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
// Zephyr doesn't have detailed wakeup cause like ESP32
return "";
}
+6 -9
View File
@@ -24,7 +24,7 @@ void EE895Component::setup() {
this->read(serial_number, 20);
crc16_check = (serial_number[19] << 8) + serial_number[18];
if (crc16_check != calc_crc16_(serial_number, 19)) {
if (crc16_check != calc_crc16_(serial_number, 18)) {
this->error_code_ = CRC_CHECK_FAILED;
this->mark_failed();
return;
@@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
address[2] = addr & 0xFF;
address[3] = (reg_cnt >> 8) & 0xFF;
address[4] = reg_cnt & 0xFF;
crc16 = calc_crc16_(address, 6);
crc16 = calc_crc16_(address, 5);
address[5] = crc16 & 0xFF;
address[6] = (crc16 >> 8) & 0xFF;
this->write(address, 7);
@@ -95,7 +95,7 @@ float EE895Component::read_float_() {
uint8_t i2c_response[8];
this->read(i2c_response, 8);
crc16_check = (i2c_response[7] << 8) + i2c_response[6];
if (crc16_check != calc_crc16_(i2c_response, 7)) {
if (crc16_check != calc_crc16_(i2c_response, 6)) {
this->error_code_ = CRC_CHECK_FAILED;
this->status_set_warning();
return 0;
@@ -107,12 +107,9 @@ float EE895Component::read_float_() {
}
uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
uint8_t crc_check_buf[22];
for (int i = 0; i < len; i++) {
crc_check_buf[i + 1] = buf[i];
}
crc_check_buf[0] = this->address_;
return crc16(crc_check_buf, len);
uint8_t addr = this->address_;
uint16_t crc = crc16(&addr, 1);
return crc16(buf, len, crc);
}
} // namespace ee895
} // namespace esphome
-5
View File
@@ -1614,11 +1614,6 @@ async def to_code(config):
# This saves ~250 bytes of RAM (tag cache) and associated code
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
# Use ESP-IDF Log V2 to eliminate per-site esp_log_timestamp() macro expansions
# V2 centralizes formatting inside esp_log(), reducing flash usage
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_1", False)
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_2", True)
# Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
+2 -1
View File
@@ -575,8 +575,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
load_ble_event(event, args...);
// Push the event to the queue
// Push always succeeds: pool is sized to queue capacity (N-1), so if
// allocate() returned non-null, the queue is guaranteed to have room.
global_ble->ble_events_.push(event);
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
}
// Explicit template instantiations for the friend function
+7 -1
View File
@@ -221,7 +221,13 @@ class ESP32BLE : public Component {
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements (one slot distinguishes full from empty).
// This guarantees allocate() returns nullptr before push() can fail, which:
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
// 2. Avoids needing release() on the producer path after a failed push(),
// preserving the SPSC contract on the pool's internal free list
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_;
// 4-byte aligned members
#ifdef USE_ESP32_BLE_ADVERTISING
@@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() {
for (auto *descriptor : this->descriptors_) {
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
}
vSemaphoreDelete(this->set_value_lock_);
}
BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
this->set_value_lock_ = xSemaphoreCreateBinary();
xSemaphoreGive(this->set_value_lock_);
this->properties_ = (esp_gatt_char_prop_t) 0;
this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0);
@@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = std::move(buffer);
xSemaphoreGive(this->set_value_lock_);
}
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
@@ -16,8 +16,6 @@
#include <esp_gattc_api.h>
#include <esp_gatts_api.h>
#include <esp_bt_defs.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
namespace esphome {
namespace esp32_ble_server {
@@ -84,8 +82,6 @@ class BLECharacteristic {
uint16_t value_read_offset_{0};
std::vector<uint8_t> value_;
SemaphoreHandle_t set_value_lock_;
std::vector<BLEDescriptor *> descriptors_;
struct ClientNotificationEntry {
@@ -99,6 +99,8 @@ void ESP32RMTLEDStripLightOutput::setup() {
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
channel.flags.io_loop_back = 0;
channel.flags.io_od_mode = 0;
channel.flags.invert_out = this->invert_out_;
channel.flags.with_dma = this->use_dma_;
channel.intr_priority = 0;
@@ -360,11 +360,16 @@ void ESP32TouchComponent::loop() {
}
// Publish initial OFF state for sensors that haven't received events yet
bool all_initial_published = true;
for (auto *child : this->children_) {
this->publish_initial_state_if_needed_(child, now);
if (!child->initial_state_published_) {
all_initial_published = false;
}
}
if (!this->setup_mode_) {
// Only disable loop once all initial states are published
if (!this->setup_mode_ && all_initial_published) {
this->disable_loop();
}
}
+1 -3
View File
@@ -22,9 +22,7 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
// ESP8266 doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
// ESP8266 LwIPLock is defined inline as a no-op in helpers.h
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
wifi_get_macaddr(STATION_IF, mac);
@@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
@@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
+6 -2
View File
@@ -163,10 +163,14 @@ class ESPNowComponent : public Component {
uint8_t own_address_[ESP_NOW_ETH_ALEN]{0};
LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{};
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{};
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
// before push() can fail, preventing a pool slot leak.
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> receive_packet_pool_{};
LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{};
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{};
// Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment.
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{};
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
uint8_t wifi_channel_{0};
@@ -7,6 +7,7 @@ from esphome.const import (
CONF_OUTPUT_ID,
CONF_RGB_ORDER,
)
from esphome.core import CORE
CODEOWNERS = ["@OttoWinter"]
fastled_base_ns = cg.esphome_ns.namespace("fastled_base")
@@ -41,5 +42,9 @@ async def new_fastled_light(config):
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
cg.add_library("fastled/FastLED", "3.9.16")
if CORE.is_esp32:
from esphome.components.esp32 import include_builtin_idf_component
include_builtin_idf_component("esp_lcd")
await light.register_light(var, config)
return var
@@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
InterruptLock lock;
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
for (uint8_t i = 0; i < 64; i++) {
ret |= (uint64_t(this->read_bit_()) << i);
}
return ret;
+1 -8
View File
@@ -87,19 +87,12 @@ void GreeClimate::transmit_state() {
// Calculate the checksum
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else if (this->model_ == GREE_YAG) {
} else {
remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4);
} else {
remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4) |
(remote_state[7] & 0x0F);
}
auto transmit = this->transmitter_->transmit();
+31 -45
View File
@@ -7,50 +7,36 @@ namespace hdc2010 {
static const char *const TAG = "hdc2010";
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
static const uint8_t CONFIG = 0x0E;
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
// Register addresses
static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00;
static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01;
static constexpr uint8_t REG_HUMIDITY_LOW = 0x02;
static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03;
static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E;
static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F;
// REG_MEASUREMENT_CONF (0x0F) bit masks
static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger
static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode
static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution
static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution
// REG_RESET_DRDY_INT_CONF (0x0E) bit masks
static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode
void HDC2010Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
const uint8_t data[2] = {
0b00000000, // resolution 14bit for both humidity and temperature
0b00000000 // reserved
};
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
ESP_LOGW(TAG, "Initial config instruction error");
this->status_set_warning();
return;
}
// Set measurement mode to temperature and humidity
// Set 14-bit resolution for both sensors and measurement mode to temp + humidity
uint8_t config_contents;
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
// Set rate to manual
this->read_register(CONFIG, &config_contents, 1);
config_contents &= 0x8F;
this->write_bytes(CONFIG, &config_contents, 1);
// Set temperature resolution to 14bit
this->read_register(CONFIG, &config_contents, 1);
config_contents &= 0x3F;
this->write_bytes(CONFIG, &config_contents, 1);
// Set humidity resolution to 14bit
this->read_register(CONFIG, &config_contents, 1);
config_contents &= 0xCF;
this->write_bytes(CONFIG, &config_contents, 1);
// Set auto measurement rate to manual (on-demand via MEAS_TRIG)
this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
config_contents &= ~AMM_MASK;
this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
}
void HDC2010Component::dump_config() {
@@ -67,9 +53,9 @@ void HDC2010Component::dump_config() {
void HDC2010Component::update() {
// Trigger measurement
uint8_t config_contents;
this->read_register(CONFIG, &config_contents, 1);
config_contents |= 0x01;
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
config_contents |= MEAS_TRIG;
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
// 1ms delay after triggering the sample
set_timeout(1, [this]() {
@@ -90,8 +76,8 @@ void HDC2010Component::update() {
float HDC2010Component::read_temp() {
uint8_t byte[2];
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1);
this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1);
uint16_t temp = encode_uint16(byte[1], byte[0]);
return (float) temp * 0.0025177f - 40.0f;
@@ -100,8 +86,8 @@ float HDC2010Component::read_temp() {
float HDC2010Component::read_humidity() {
uint8_t byte[2];
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
this->read_register(REG_HUMIDITY_LOW, &byte[0], 1);
this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1);
uint16_t humidity = encode_uint16(byte[1], byte[0]);
return (float) humidity * 0.001525879f;
@@ -23,6 +23,12 @@ namespace http_request {
static const char *const TAG = "http_request.update";
// Wraps UpdateInfo + error for the task→main-loop handoff.
struct TaskResult {
update::UpdateInfo info;
const LogString *error_str{nullptr};
};
static const size_t MAX_READ_SIZE = 256;
static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
@@ -68,6 +74,10 @@ void HttpRequestUpdate::update() {
}
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
#ifdef USE_ESP32
if (this->update_task_handle_ != nullptr) {
ESP_LOGW(TAG, "Update check already in progress");
return;
}
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
#else
this->update_task(this);
@@ -77,134 +87,151 @@ void HttpRequestUpdate::update() {
void HttpRequestUpdate::update_task(void *params) {
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
// Allocate once — every path below returns via the single defer at the end.
// On failure, error_str is set; on success it is nullptr.
auto *result = new TaskResult();
auto *info = &result->info;
auto container = this_update->request_parent_->get(this_update->source_url_);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
UPDATE_RETURN;
if (container != nullptr)
container->end();
result->error_str = LOG_STR("Failed to fetch manifest");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer(
[this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
container->end();
UPDATE_RETURN;
}
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
{
RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
container->end();
result->error_str = LOG_STR("Failed to allocate memory for manifest");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
allocator.deallocate(data, container->content_length);
container->end();
UPDATE_RETURN;
}
size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
}
this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>();
this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>();
allocator.deallocate(data, container->content_length);
container->end();
result->error_str = LOG_STR("Failed to read manifest");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
for (auto build : builds_array) {
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
info->title = root[ESPHOME_F("name")].as<std::string>();
info->latest_version = root[ESPHOME_F("version")].as<std::string>();
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
for (auto build : builds_array) {
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
info->firmware_url = ota[ESPHOME_F("path")].as<std::string>();
info->md5 = ota[ESPHOME_F("md5")].as<std::string>();
if (ota[ESPHOME_F("summary")].is<const char *>())
info->summary = ota[ESPHOME_F("summary")].as<std::string>();
if (ota[ESPHOME_F("release_url")].is<const char *>())
info->release_url = ota[ESPHOME_F("release_url")].as<std::string>();
return true;
}
this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>();
this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>();
if (ota[ESPHOME_F("summary")].is<const char *>())
this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>();
if (ota[ESPHOME_F("release_url")].is<const char *>())
this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>();
return true;
}
}
return false;
});
}
allocator.deallocate(data, content_length);
return false;
});
}
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
UPDATE_RETURN;
}
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
result->error_str = LOG_STR("Failed to parse manifest JSON");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Merge source_url_ and this_update->update_info_.firmware_url
if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
std::string path = this_update->update_info_.firmware_url;
if (path[0] == '/') {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
this_update->update_info_.firmware_url = domain + path;
} else {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
this_update->update_info_.firmware_url = domain + path;
// Merge source_url_ and firmware_url
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
std::string path = info->firmware_url;
if (path[0] == '/') {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
info->firmware_url = domain + path;
} else {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
info->firmware_url = domain + path;
}
}
}
#ifdef ESPHOME_PROJECT_VERSION
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
info->current_version = ESPHOME_PROJECT_VERSION;
#else
this_update->update_info_.current_version = ESPHOME_VERSION;
info->current_version = ESPHOME_VERSION;
#endif
bool trigger_update_available = false;
if (this_update->update_info_.latest_version.empty() ||
this_update->update_info_.latest_version == this_update->update_info_.current_version) {
this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
} else {
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
trigger_update_available = true;
}
this_update->state_ = update::UPDATE_STATE_AVAILABLE;
}
// Defer to main loop to ensure thread-safe execution of:
// - status_clear_error() performs non-atomic read-modify-write on component_state_
// - publish_state() triggers API callbacks that write to the shared protobuf buffer
// which can be corrupted if accessed concurrently from task and main loop threads
// - update_available trigger to ensure consistent state when the trigger fires
this_update->defer([this_update, trigger_update_available]() {
this_update->update_info_.has_progress = false;
this_update->update_info_.progress = 0.0f;
defer:
// Release container before vTaskDelete (which doesn't call destructors)
container.reset();
// Defer to the main loop so all update_info_ and state_ writes happen on the
// same thread as readers (API, MQTT, web server). This is a single defer for
// both success and error paths to avoid multiple std::function instantiations.
// Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
this_update->defer([this_update, result]() {
#ifdef USE_ESP32
this_update->update_task_handle_ = nullptr;
#endif
if (result->error_str != nullptr) {
this_update->status_set_error(result->error_str);
delete result;
return;
}
// Determine new state on main loop (avoids extra lambda captures from task)
bool trigger_update_available = false;
update::UpdateState new_state;
if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) {
new_state = update::UPDATE_STATE_NO_UPDATE;
} else {
new_state = update::UPDATE_STATE_AVAILABLE;
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
trigger_update_available = true;
}
}
this_update->update_info_ = std::move(result->info);
this_update->state_ = new_state;
delete result; // Safe: moved-from state is valid for destruction
this_update->status_clear_error();
this_update->publish_state();
+5 -4
View File
@@ -534,10 +534,11 @@ void LD2450Component::handle_periodic_data_() {
}
#endif
// Store target info for zone target count
this->target_info_[index].x = tx;
this->target_info_[index].y = ty;
this->target_info_[index].is_moving = is_moving;
// Store target info for zone target count. Zero out untracked targets (td==0)
// so stale coordinates don't produce ghost counts in count_targets_in_zone_().
this->target_info_[index].x = (td > 0) ? tx : 0;
this->target_info_[index].y = (td > 0) ? ty : 0;
this->target_info_[index].is_moving = (td > 0) && is_moving;
} // End loop thru targets
+1 -3
View File
@@ -26,9 +26,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
// LibreTiny doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
// LibreTiny LwIPLock is defined inline as a no-op in helpers.h
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
WiFi.macAddress(mac);
+21 -7
View File
@@ -81,18 +81,32 @@ def _get_data() -> LightData:
return CORE.data[DOMAIN]
def generate_gamma_table(gamma_correct: float) -> list[HexInt]:
"""Generate a 256-entry uint16 gamma lookup table.
For gamma > 0, non-zero indices are clamped to a minimum of 1 to preserve
the invariant that non-zero input always produces non-zero output. Without
this, small brightness values (e.g. 1%) get quantized to exactly 0.0,
which breaks zero_means_zero logic in FloatOutput.
"""
if gamma_correct > 0:
return [
HexInt(
max(1, min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
if i > 0
else HexInt(0)
)
for i in range(256)
]
return [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
def _get_or_create_gamma_table(gamma_correct):
data = _get_data()
if gamma_correct in data.gamma_tables:
return data.gamma_tables[gamma_correct]
if gamma_correct > 0:
forward = [
HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535))))
for i in range(256)
]
else:
forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)]
forward = generate_gamma_table(gamma_correct)
gamma_str = f"{gamma_correct}".replace(".", "_")
fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16)
@@ -154,6 +154,16 @@ class LightColorValues {
}
/// Convert these light color values to an CWWW representation with the given parameters.
///
/// Note on gamma and constant_brightness: This method operates on the raw/internal channel
/// values stored in this object. For cold_white_ and warm_white_ specifically, these
/// may already be gamma-uncorrected when derived from a color_temperature value.
/// For constant_brightness=false, additional gamma for the output can be applied after
/// this method since gamma commutes with simple multiplication. For constant_brightness=true,
/// the caller (LightState::current_values_as_cwww) must apply gamma to the individual
/// channel values BEFORE the balancing formula, because the nonlinear max/sum ratio does
/// not commute with gamma. See LightState::current_values_as_cwww() for the correct
/// implementation.
void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const {
if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) {
const float cw_level = this->cold_white_;
+41 -6
View File
@@ -223,12 +223,11 @@ void LightState::current_values_as_rgbw(float *red, float *green, float *blue, f
}
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
bool constant_brightness) {
this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness);
this->current_values.as_rgb(red, green, blue);
*red = this->gamma_correct_lut(*red);
*green = this->gamma_correct_lut(*green);
*blue = this->gamma_correct_lut(*blue);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
this->current_values_as_cwww(cold_white, warm_white, constant_brightness);
}
void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature,
float *white_brightness) {
@@ -241,9 +240,45 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue,
*white_brightness = this->gamma_correct_lut(*white_brightness);
}
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
this->current_values.as_cwww(cold_white, warm_white, constant_brightness);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
if (!constant_brightness) {
// Without constant_brightness, gamma commutes with simple multiplication:
// gamma(white_level * cw) = gamma(white_level) * gamma(cw)
// (since gamma(a*b) = (a*b)^g = a^g * b^g = gamma(a) * gamma(b))
// so applying gamma after is mathematically equivalent and simpler.
this->current_values.as_cwww(cold_white, warm_white, false);
*cold_white = this->gamma_correct_lut(*cold_white);
*warm_white = this->gamma_correct_lut(*warm_white);
return;
}
// For constant_brightness mode, gamma MUST be applied to the individual
// channel values BEFORE the balancing formula (max/sum ratio), not after.
//
// Why: The cold_white_ and warm_white_ values stored in LightColorValues
// are gamma-uncorrected (see transform_parameters_() which applies
// gamma_uncorrect to the linear CW/WW fractions derived from color
// temperature). Applying gamma_correct here recovers the original linear
// fractions, which the constant_brightness formula then uses to distribute
// power evenly. The max/sum formula ensures cold+warm PWM output sums to
// a constant, keeping total power (and perceived brightness) the same
// across all color temperatures.
//
// Applying gamma AFTER the formula would be incorrect because gamma is
// nonlinear: gamma(a/b) != gamma(a)/gamma(b), so the carefully balanced
// ratio would be distorted, causing a severe brightness dip at mid-range
// color temperatures.
const auto &v = this->current_values;
if (!(v.get_color_mode() & ColorCapability::COLD_WARM_WHITE)) {
*cold_white = *warm_white = 0;
return;
}
const float cw_level = this->gamma_correct_lut(v.get_cold_white());
const float ww_level = this->gamma_correct_lut(v.get_warm_white());
const float white_level = this->gamma_correct_lut(v.get_state() * v.get_brightness());
const float sum = cw_level > 0 || ww_level > 0 ? cw_level + ww_level : 1; // Don't divide by zero.
*cold_white = white_level * std::max(cw_level, ww_level) * cw_level / sum;
*warm_white = white_level * std::max(cw_level, ww_level) * ww_level / sum;
}
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
auto traits = this->get_traits();
@@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
this->y_raw_max_ = this->display_->get_native_height();
}
}
}
@@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() {
}
point = buffer[5] & 0xF;
if (point > 2) {
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
point = 2;
}
if (point == 1) {
err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1);
+44 -24
View File
@@ -56,6 +56,7 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
logger_ns = cg.esphome_ns.namespace("logger")
@@ -323,19 +324,34 @@ CONFIG_SCHEMA = cv.All(
)
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
async def to_code(config):
baud_rate = config[CONF_BAUD_RATE]
@coroutine_with_priority(CoroPriority.EARLY_INIT)
async def to_code(config: ConfigType) -> None:
baud_rate: int = config[CONF_BAUD_RATE]
level = config[CONF_LEVEL]
CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
tx_buffer_size = config[CONF_TX_BUFFER_SIZE]
cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size)
log = cg.new_Pvariable(
config[CONF_ID],
baud_rate,
)
if CORE.is_esp32:
# Determine task log buffer size and define USE_ESPHOME_TASK_LOG_BUFFER early
# so the constructor can allocate the buffer immediately, preventing a race
# where another task logs before the buffer is initialized.
task_log_buffer_size = 0
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
elif CORE.is_host:
task_log_buffer_size = 64 # Fixed 64 slots for host
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
log = cg.new_Pvariable(
config[CONF_ID],
baud_rate,
task_log_buffer_size,
)
else:
log = cg.new_Pvariable(
config[CONF_ID],
baud_rate,
)
if CORE.is_esp32 or CORE.is_host:
cg.add(log.create_pthread_key())
# set_uart_selection() must be called before pre_setup() because
# pre_setup() switches on uart_ to decide which hardware to initialize
@@ -347,24 +363,28 @@ async def to_code(config):
HARDWARE_UART_TO_UART_SELECTION[config[CONF_HARDWARE_UART]]
)
)
# pre_setup() must be called before init_log_buffer() because
# init_log_buffer() calls disable_loop() which may log at VV level,
# and global_logger must be set before any logging occurs.
# pre_setup() sets global_logger and must run before any other code
# that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV).
cg.add(log.pre_setup())
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
cg.add(log.init_log_buffer(task_log_buffer_size))
if CORE.using_zephyr:
zephyr_add_prj_conf("MPSC_PBUF", True)
elif CORE.is_host:
cg.add(log.create_pthread_key())
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
cg.add(log.set_log_level(initial_level))
# Schedule the rest of logger setup at DIAGNOSTICS priority, after
# Application is constructed (CORE priority) but before most components.
CORE.add_job(_late_logger_init, config)
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
async def _late_logger_init(config: ConfigType) -> None:
"""Finish logger setup after Application is constructed."""
log = await cg.get_variable(config[CONF_ID])
level = config[CONF_LEVEL]
baud_rate: int = config[CONF_BAUD_RATE]
if CORE.using_zephyr:
task_log_buffer_size = config.get(CONF_TASK_LOG_BUFFER_SIZE, 0)
if task_log_buffer_size > 0:
zephyr_add_prj_conf("MPSC_PBUF", True)
# Enable runtime tag levels if logs are configured or explicitly enabled
logs_config = config[CONF_LOGS]
if logs_config or config[CONF_RUNTIME_TAG_LEVELS]:
+8 -12
View File
@@ -152,29 +152,25 @@ inline uint8_t Logger::level_for(const char *tag) {
return this->current_level_;
}
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
Logger::Logger(uint32_t baud_rate, size_t task_log_buffer_size) : baud_rate_(baud_rate) {
#else
Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) {
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->main_task_ = xTaskGetCurrentTaskHandle();
#elif defined(USE_ZEPHYR)
this->main_task_ = k_current_get();
#elif defined(USE_HOST)
this->main_thread_ = pthread_self();
this->main_thread_ = pthread_self();
#endif
}
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::init_log_buffer(size_t total_buffer_size) {
// Host uses slot count instead of byte size
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
// Start with loop disabled when using task buffer
// The loop will be enabled automatically when messages arrive
// Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_()
this->disable_loop_when_buffer_empty_();
this->log_buffer_ = new logger::TaskLogBuffer(task_log_buffer_size);
// Note: we don't disable loop here because the component isn't registered with App yet.
// The loop self-disables on its first iteration when it finds no messages to process.
#endif
}
#endif
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
void Logger::loop() {
+3 -2
View File
@@ -143,9 +143,10 @@ enum UARTSelection : uint8_t {
*/
class Logger final : public Component {
public:
explicit Logger(uint32_t baud_rate);
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void init_log_buffer(size_t total_buffer_size);
explicit Logger(uint32_t baud_rate, size_t task_log_buffer_size);
#else
explicit Logger(uint32_t baud_rate);
#endif
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
void loop() override;
@@ -80,6 +80,7 @@ bool StreamingModel::load_model_() {
TfLiteTensor *output = this->interpreter_->output(0);
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1.");
return false;
}
if (output->type != kTfLiteUInt8) {
-13
View File
@@ -54,17 +54,6 @@ void MIPI_DSI::setup() {
this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err);
return;
}
// clang-format off
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
auto color_format = LCD_COLOR_FMT_RGB565;
if (this->color_depth_ == display::COLOR_BITNESS_888) {
color_format = LCD_COLOR_FMT_RGB888;
}
esp_lcd_dpi_panel_config_t dpi_config = {.virtual_channel = 0,
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = this->pclk_frequency_,
.in_color_format = color_format,
#else
auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565;
if (this->color_depth_ == display::COLOR_BITNESS_888) {
pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888;
@@ -73,7 +62,6 @@ void MIPI_DSI::setup() {
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = this->pclk_frequency_,
.pixel_format = pixel_format,
#endif
.num_fbs = 1, // number of frame buffers to allocate
.video_timing =
{
@@ -89,7 +77,6 @@ void MIPI_DSI::setup() {
.flags = {
.use_dma2d = true,
}};
// clang-format on
err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_);
if (err != ESP_OK) {
this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err);
+24 -10
View File
@@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() {
void MQTTBackendESP32::loop() {
// process new events
// handle only 1 message per loop iteration
if (!mqtt_events_.empty()) {
auto &event = mqtt_events_.front();
mqtt_event_handler_(event);
mqtt_events_.pop();
Event *event = this->mqtt_event_queue_.pop();
if (event != nullptr) {
this->mqtt_event_handler_(*event);
this->mqtt_event_pool_.release(event);
}
// Log dropped inbound events (check is cheap - single atomic load in common case)
uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count();
if (inbound_dropped > 0) {
ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped);
}
#if defined(USE_MQTT_IDF_ENQUEUE)
@@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id,
void *event_data) {
MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args);
// queue event to decouple processing
// queue event to decouple processing from ESP-IDF MQTT task to main loop
if (instance) {
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
instance->mqtt_events_.emplace(event);
auto *event = instance->mqtt_event_pool_.allocate();
if (event == nullptr) {
// Pool exhausted, drop event (counted via queue's dropped counter)
instance->mqtt_event_queue_.increment_dropped_count();
return;
}
event->populate(*static_cast<esp_mqtt_event_t *>(event_data));
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
instance->mqtt_event_queue_.push(event);
// Wake main loop immediately to process MQTT event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
@@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) {
break;
}
}
this_mqtt->mqtt_event_pool_.release(elem);
this_mqtt->mqtt_outbound_pool_.release(elem);
}
}
}
bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload,
size_t len) {
auto *elem = this->mqtt_event_pool_.allocate();
auto *elem = this->mqtt_outbound_pool_.allocate();
if (!elem) {
// Queue is full - increment counter but don't log immediately.
@@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos,
// Use the helper to allocate and copy data
if (!elem->set_data(topic, payload, len)) {
// Allocation failed, return elem to pool
this->mqtt_event_pool_.release(elem);
this->mqtt_outbound_pool_.release(elem);
// Increment counter without logging to avoid cascade effect during memory pressure
this->mqtt_queue_.increment_dropped_count();
return false;
+42 -27
View File
@@ -5,7 +5,6 @@
#ifdef USE_ESP32
#include <string>
#include <queue>
#include <cstring>
#include <mqtt_client.h>
#include <freertos/FreeRTOS.h>
@@ -18,32 +17,39 @@
namespace esphome::mqtt {
struct Event {
esp_mqtt_event_id_t event_id;
esp_mqtt_event_id_t event_id{};
std::vector<char> data;
int total_data_len;
int current_data_offset;
int total_data_len{0};
int current_data_offset{0};
std::string topic;
int msg_id;
bool retain;
int qos;
bool dup;
bool session_present;
esp_mqtt_error_codes_t error_handle;
int msg_id{0};
bool retain{false};
int qos{0};
bool dup{false};
bool session_present{false};
esp_mqtt_error_codes_t error_handle{};
// Construct from esp_mqtt_event_t
// Any pointer values that are unsafe to keep are converted to safe copies
Event(const esp_mqtt_event_t &event)
: event_id(event.event_id),
data(event.data, event.data + event.data_len),
total_data_len(event.total_data_len),
current_data_offset(event.current_data_offset),
topic(event.topic, event.topic_len),
msg_id(event.msg_id),
retain(event.retain),
qos(event.qos),
dup(event.dup),
session_present(event.session_present),
error_handle(*event.error_handle) {}
// Populate from esp_mqtt_event_t
// Copies pointer-based data to owned storage for safe cross-thread transfer
void populate(const esp_mqtt_event_t &event) {
this->event_id = event.event_id;
this->data.assign(event.data, event.data + event.data_len);
this->total_data_len = event.total_data_len;
this->current_data_offset = event.current_data_offset;
this->topic.assign(event.topic, event.topic_len);
this->msg_id = event.msg_id;
this->retain = event.retain;
this->qos = event.qos;
this->dup = event.dup;
this->session_present = event.session_present;
this->error_handle = *event.error_handle;
}
// Release owned resources for pool reuse (keeps allocated capacity for efficiency)
void release() {
this->data.clear();
this->topic.clear();
}
};
enum MqttQueueTypeT : uint8_t {
@@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
static constexpr size_t TASK_STACK_SIZE = 3072;
static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations
static constexpr ssize_t TASK_PRIORITY = 5;
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
void set_client_id(const char *client_id) final { this->client_id_ = client_id; }
@@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
bool skip_cert_cn_check_{false};
#if defined(USE_MQTT_IDF_ENQUEUE)
static void esphome_mqtt_task(void *params);
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_;
// Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment.
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_;
NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_;
TaskHandle_t task_handle_{nullptr};
bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL,
@@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend {
CallbackManager<on_message_callback_t> on_message_;
CallbackManager<on_publish_user_callback_t> on_publish_;
std::string cached_topic_;
std::queue<Event> mqtt_events_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements (one slot distinguishes full from empty).
// This guarantees allocate() returns nullptr before push() can fail, which:
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
// 2. Avoids needing release() on the producer path after a failed push(),
// preserving the SPSC contract on the pool's internal free list
EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_;
LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_;
#if defined(USE_MQTT_IDF_ENQUEUE)
uint32_t last_dropped_log_time_{0};
+14 -3
View File
@@ -28,6 +28,10 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt";
// Maximum number of MQTT component resends per loop iteration.
// Limits work to avoid triggering the task watchdog on reconnect.
static constexpr uint8_t MAX_RESENDS_PER_LOOP = 8;
// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
"Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
@@ -396,9 +400,16 @@ void MQTTClientComponent::loop() {
this->resubscribe_subscriptions_();
// Process pending resends for all MQTT components centrally
// This is more efficient than each component polling in its own loop
for (MQTTComponent *component : this->children_) {
component->process_resend();
// Limit work per loop iteration to avoid triggering task WDT on reconnect
{
uint8_t resend_count = 0;
for (MQTTComponent *component : this->children_) {
if (component->is_resend_pending()) {
component->process_resend();
if (++resend_count >= MAX_RESENDS_PER_LOOP)
break;
}
}
}
}
break;
+3
View File
@@ -147,6 +147,9 @@ class MQTTComponent : public Component {
/// Internal method for the MQTT client base to schedule a resend of the state on reconnect.
void schedule_resend_state();
/// Check if a resend is pending (called by MQTTClientComponent to rate-limit work)
bool is_resend_pending() const { return this->resend_state_; }
/// Process pending resend if needed (called by MQTTClientComponent)
void process_resend();
-5
View File
@@ -13,11 +13,6 @@ const std::string &OneWireDevice::get_address_name() {
return this->address_name_;
}
void OneWireDevice::set_address(uint64_t address) {
this->address_ = address;
this->address_name_.clear();
}
bool OneWireDevice::send_command_(uint8_t cmd) {
if (!this->bus_->select(this->address_))
return false;
+1 -1
View File
@@ -15,7 +15,7 @@ class OneWireDevice {
public:
/// @brief store the address of the device
/// @param address of the device
void set_address(uint64_t address);
void set_address(uint64_t address) { this->address_ = address; }
void set_index(uint8_t index) { this->index_ = index; }
@@ -129,7 +129,7 @@ void OnlineImage::update() {
}
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
this->start_time_ = ::time(nullptr);
this->start_time_ = millis();
this->enable_loop();
}
@@ -155,8 +155,8 @@ void OnlineImage::loop() {
// Finalize decoding
this->end_decode();
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
(uint32_t) (::time(nullptr) - this->start_time_));
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 " ms", this->downloader_->get_bytes_read(),
millis() - this->start_time_);
// Save caching headers
this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
@@ -97,7 +97,7 @@ class OnlineImage : public PollingComponent,
*/
std::string last_modified_ = "";
time_t start_time_;
uint32_t start_time_{0};
};
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
@@ -11,6 +11,7 @@
#include <openthread/instance.h>
#include <openthread/thread.h>
#include <atomic>
#include <optional>
#include <vector>
@@ -28,6 +29,8 @@ class OpenThreadComponent : public Component {
float get_setup_priority() const override { return setup_priority::WIFI; }
bool is_connected() const { return this->connected_; }
/// Returns true once esp_openthread_init() has completed and the OT lock is usable.
bool is_lock_initialized() const { return this->lock_initialized_; }
network::IPAddresses get_ip_addresses();
std::optional<otIp6Address> get_omr_address();
void ot_main();
@@ -51,6 +54,7 @@ class OpenThreadComponent : public Component {
uint32_t poll_period_{0};
#endif
std::optional<int8_t> output_power_{};
std::atomic<bool> lock_initialized_{false};
bool teardown_started_{false};
bool teardown_complete_{false};
bool connected_{false};
@@ -8,6 +8,7 @@
#include "esp_openthread_lock.h"
#include "esp_task_wdt.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -81,6 +82,9 @@ void OpenThreadComponent::ot_main() {
// Initialize the OpenThread stack
// otLoggingSetLevel(OT_LOG_LEVEL_DEBG);
ESP_ERROR_CHECK(esp_openthread_init(&config));
// Mark lock as initialized so InstanceLock callers know it's safe to acquire.
// Must be set after esp_openthread_init() which creates the internal semaphore.
this->lock_initialized_ = true;
// Fetch OT instance once to avoid repeated call into OT stack
otInstance *instance = esp_openthread_get_instance();
@@ -180,7 +184,8 @@ void OpenThreadComponent::ot_main() {
esp_openthread_launch_mainloop();
// Clean up
// Clean up - reset lock flag before deinit destroys the semaphore
this->lock_initialized_ = false;
esp_openthread_deinit();
esp_openthread_netif_glue_deinit();
esp_netif_destroy(openthread_netif);
@@ -210,6 +215,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() {
otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); }
std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
if (!global_openthread_component->is_lock_initialized()) {
return {};
}
if (esp_openthread_lock_acquire(delay)) {
return InstanceLock();
}
@@ -217,6 +225,18 @@ std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
}
InstanceLock InstanceLock::acquire() {
// Wait for the lock to be created by ot_main() before attempting to acquire it.
// esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init().
constexpr uint32_t lock_init_timeout_ms = 10000;
uint32_t start = millis();
while (!global_openthread_component->is_lock_initialized()) {
if (millis() - start > lock_init_timeout_ms) {
ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms);
abort();
}
delay(10);
esp_task_wdt_reset();
}
while (!esp_openthread_lock_acquire(100)) {
esp_task_wdt_reset();
}
+9 -15
View File
@@ -57,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_,
cv.Optional(
CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1
): cv.positive_not_null_int,
): cv.int_,
}
),
cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema(
@@ -68,12 +68,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_STARTING_INTEGRAL_TERM, default=0.0): cv.float_,
cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_,
cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_,
cv.Optional(
CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1
): cv.positive_not_null_int,
cv.Optional(
CONF_OUTPUT_AVERAGING_SAMPLES, default=1
): cv.positive_not_null_int,
cv.Optional(CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1): cv.int_,
cv.Optional(CONF_OUTPUT_AVERAGING_SAMPLES, default=1): cv.int_,
}
),
}
@@ -106,15 +102,13 @@ async def to_code(config):
cg.add(var.set_starting_integral_term(params[CONF_STARTING_INTEGRAL_TERM]))
cg.add(var.set_derivative_samples(params[CONF_DERIVATIVE_AVERAGING_SAMPLES]))
output_samples = params[CONF_OUTPUT_AVERAGING_SAMPLES]
cg.add(var.set_output_samples(output_samples))
cg.add(var.set_output_samples(params[CONF_OUTPUT_AVERAGING_SAMPLES]))
if CONF_MIN_INTEGRAL in params:
cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL]))
if CONF_MAX_INTEGRAL in params:
cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL]))
deadband_output_samples = 1
if CONF_DEADBAND_PARAMETERS in config:
params = config[CONF_DEADBAND_PARAMETERS]
cg.add(var.set_threshold_low(params[CONF_THRESHOLD_LOW]))
@@ -122,11 +116,11 @@ async def to_code(config):
cg.add(var.set_kp_multiplier(params[CONF_KP_MULTIPLIER]))
cg.add(var.set_ki_multiplier(params[CONF_KI_MULTIPLIER]))
cg.add(var.set_kd_multiplier(params[CONF_KD_MULTIPLIER]))
deadband_output_samples = params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
cg.add(var.set_deadband_output_samples(deadband_output_samples))
# Single shared output buffer sized to max of both modes
cg.add(var.init_output_buffer(max(output_samples, deadband_output_samples)))
cg.add(
var.set_deadband_output_samples(
params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
)
)
cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE]))
+1 -9
View File
@@ -28,11 +28,7 @@ class PIDClimate : public climate::Climate, public Component {
void set_min_integral(float min_integral) { controller_.min_integral_ = min_integral; }
void set_max_integral(float max_integral) { controller_.max_integral_ = max_integral; }
void set_output_samples(int in) { controller_.output_samples_ = in; }
void set_derivative_samples(int in) {
controller_.derivative_samples_ = in;
if (in > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits)
controller_.derivative_window_.init(in);
}
void set_derivative_samples(int in) { controller_.derivative_samples_ = in; }
void set_threshold_low(float in) { controller_.threshold_low_ = in; }
void set_threshold_high(float in) { controller_.threshold_high_ = in; }
@@ -42,10 +38,6 @@ class PIDClimate : public climate::Climate, public Component {
void set_starting_integral_term(float in) { controller_.set_starting_integral_term(in); }
void set_deadband_output_samples(int in) { controller_.deadband_output_samples_ = in; }
void init_output_buffer(int size) {
if (size > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits)
controller_.output_window_.init(size);
}
float get_output_value() const { return output_value_; }
float get_error_value() const { return controller_.error_; }
+17 -15
View File
@@ -21,9 +21,9 @@ float PIDController::update(float setpoint, float process_value) {
// u(t) := p(t) + i(t) + d(t)
float output = proportional_term_ + integral_term_ + derivative_term_;
// smooth/sample the output using shared buffer with mode-appropriate sample count
// smooth/sample the output
int samples = in_deadband() ? deadband_output_samples_ : output_samples_;
return ring_buffer_average_(output_window_, output, samples);
return weighted_average_(output_list_, output, samples);
}
bool PIDController::in_deadband() {
@@ -83,7 +83,7 @@ void PIDController::calculate_derivative_term_(float setpoint) {
previous_setpoint_ = setpoint;
// smooth the derivative samples
derivative = ring_buffer_average_(derivative_window_, derivative, derivative_samples_);
derivative = weighted_average_(derivative_list_, derivative, derivative_samples_);
derivative_term_ = kd_ * derivative;
@@ -93,23 +93,25 @@ void PIDController::calculate_derivative_term_(float setpoint) {
}
}
float PIDController::ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples) {
// if only 1 sample needed (or invalid), clear the buffer and return
if (max_samples <= 1) {
buf.clear();
float PIDController::weighted_average_(std::deque<float> &list, float new_value, int samples) {
// if only 1 sample needed, clear the list and return
if (samples == 1) {
list.clear();
return new_value;
}
// Trim oldest entries to make room (handles mode-switching where buffer
// may have more entries than the current mode needs)
while (buf.size() >= static_cast<size_t>(max_samples))
buf.pop();
buf.push(new_value);
// add the new item to the list
list.push_front(new_value);
// keep only 'samples' readings, by popping off the back of the list
while (samples > 0 && list.size() > static_cast<size_t>(samples))
list.pop_back();
// calculate and return the average of all values in the list
float sum = 0;
for (auto val : buf)
sum += val;
return sum / buf.size();
for (auto &elem : list)
sum += elem;
return sum / list.size();
}
float PIDController::calculate_relative_time_() {
+10 -14
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <deque>
#include <cmath>
namespace esphome {
@@ -25,10 +24,10 @@ struct PIDController {
/// Differential gain K_d.
float kd_ = 0;
// smooth the derivative value using an average over X samples
int derivative_samples_ = 1;
// smooth the derivative value using a weighted average over X samples
int derivative_samples_ = 8;
/// smooth the output value using an average over X values
/// smooth the output value using a weighted average over X values
int output_samples_ = 1;
float threshold_low_ = 0.0f;
@@ -51,10 +50,7 @@ struct PIDController {
void calculate_proportional_term_();
void calculate_integral_term_();
void calculate_derivative_term_(float setpoint);
/// Ring buffer smoothing using FixedRingBuffer (single allocation at setup)
float ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples);
float weighted_average_(std::deque<float> &list, float new_value, int samples);
float calculate_relative_time_();
/// Error from previous update used for derivative term
@@ -64,12 +60,12 @@ struct PIDController {
float accumulated_integral_ = 0;
uint32_t last_time_ = 0;
// Ring buffer for derivative smoothing
FixedRingBuffer<float> derivative_window_;
// this is a list of derivative values for smoothing.
std::deque<float> derivative_list_;
// Ring buffer for output smoothing (shared between normal and deadband modes)
FixedRingBuffer<float> output_window_;
// this is a list of output values for smoothing.
std::deque<float> output_list_;
}; // Struct PIDController
}; // Struct PID Controller
} // namespace pid
} // namespace esphome
+1 -1
View File
@@ -1,6 +1,5 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_CLIMATE_ID
import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT
@@ -22,6 +21,7 @@ PID_CLIMATE_SENSOR_TYPES = {
"KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD,
}
CONF_CLIMATE_ID = "climate_id"
CONFIG_SCHEMA = (
sensor.sensor_schema(
PIDClimateSensor,
+5 -8
View File
@@ -95,10 +95,6 @@ void PMSX003Component::loop() {
// Just go ahead and read stuff
break;
}
} else if (now - this->last_update_ < this->update_interval_) {
// Otherwise just leave the sensor powered up and come back when we hit the update
// time
return;
}
if (now - this->last_transmission_ >= 500) {
@@ -114,10 +110,11 @@ void PMSX003Component::loop() {
this->read_byte(&this->data_[this->data_index_]);
auto check = this->check_byte_();
if (!check.has_value()) {
// finished
this->parse_data_();
if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) {
this->parse_data_();
this->last_update_ = now;
}
this->data_index_ = 0;
this->last_update_ = now;
} else if (!*check) {
// wrong data
this->data_index_ = 0;
@@ -138,7 +135,7 @@ optional<bool> PMSX003Component::check_byte_() {
return true;
}
ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, START_CHARACTER_1);
ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char);
return false;
}
@@ -44,6 +44,7 @@ void RemoteReceiverComponent::setup() {
channel.intr_priority = 0;
channel.flags.invert_in = 0;
channel.flags.with_dma = this->with_dma_;
channel.flags.io_loop_back = 0;
esp_err_t error = rmt_new_rx_channel(&channel, &this->channel_);
if (error != ESP_OK) {
this->error_code_ = error;
@@ -120,13 +120,11 @@ void RemoteTransmitterComponent::configure_rmt_() {
channel.gpio_num = gpio_num_t(this->pin_->get_pin());
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
channel.flags.io_loop_back = open_drain;
channel.flags.io_od_mode = open_drain;
channel.flags.invert_out = 0;
channel.flags.with_dma = this->with_dma_;
channel.intr_priority = 0;
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0)
channel.flags.io_loop_back = open_drain;
channel.flags.io_od_mode = open_drain;
#endif
error = rmt_new_tx_channel(&channel, &this->channel_);
if (error != ESP_OK) {
this->error_code_ = error;
@@ -138,13 +136,6 @@ void RemoteTransmitterComponent::configure_rmt_() {
this->mark_failed();
return;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
if (open_drain) {
gpio_num_t gpio = gpio_num_t(this->pin_->get_pin());
gpio_od_enable(gpio);
gpio_input_enable(gpio);
}
#endif
if (this->pin_->get_flags() & gpio::FLAG_PULLUP) {
gpio_pullup_en(gpio_num_t(this->pin_->get_pin()));
} else {
+1 -11
View File
@@ -6,7 +6,6 @@ Usage: python esphome/components/rp2040/generate_boards.py <arduino-pico-path>
import json
from pathlib import Path
import re
import subprocess
import sys
from jinja2 import Environment, FileSystemLoader
@@ -158,7 +157,7 @@ def generate(arduino_pico_path: Path) -> str:
board_pins, boards = load_boards(arduino_pico_path)
template = _jinja_env.get_template("boards.jinja2")
content = template.render(
return template.render(
cyw43_gpio_offset=CYW43_GPIO_OFFSET,
cyw43_max_gpio=CYW43_GPIO_OFFSET + CYW43_GPIO_COUNT - 1,
default_max_pin=DEFAULT_MAX_PIN,
@@ -166,15 +165,6 @@ def generate(arduino_pico_path: Path) -> str:
boards=sorted(boards.items()),
)
# Format output to match pre-commit ruff formatting
result = subprocess.run(
[sys.executable, "-m", "ruff", "format", "--stdin-filename", "boards.py"],
input=content.encode(),
capture_output=True,
check=True,
)
return result.stdout.decode()
def main():
if len(sys.argv) < 2:
@@ -11,6 +11,7 @@ from esphome.components.image import (
)
import esphome.config_validation as cv
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
from esphome.core import CORE
AUTO_LOAD = ["image"]
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
@@ -75,6 +76,13 @@ class JPEGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
if CORE.is_esp32:
from esphome.components.esp32 import add_idf_component
# JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level
# ARDUINO_ESP32S3_DEV define) that require esp-dsp headers.
# On Arduino this overwrites the stub; on IDF it adds the component.
add_idf_component(name="espressif/esp-dsp", ref="1.7.1")
class PNGFormat(Format):
+37
View File
@@ -5,6 +5,30 @@
namespace esphome {
namespace sdl {
int Sdl::get_width() {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
case display::DISPLAY_ROTATION_270_DEGREES:
return this->get_height_internal();
case display::DISPLAY_ROTATION_0_DEGREES:
case display::DISPLAY_ROTATION_180_DEGREES:
default:
return this->get_width_internal();
}
}
int Sdl::get_height() {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_0_DEGREES:
case display::DISPLAY_ROTATION_180_DEGREES:
return this->get_height_internal();
case display::DISPLAY_ROTATION_90_DEGREES:
case display::DISPLAY_ROTATION_270_DEGREES:
default:
return this->get_width_internal();
}
}
void Sdl::setup() {
SDL_Init(SDL_INIT_VIDEO);
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
@@ -49,6 +73,19 @@ void Sdl::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return;
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
x = this->width_ - x - 1;
y = this->height_ - y - 1;
} else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = this->width_ - y - 1;
y = tmp;
} else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = this->height_ - x - 1;
x = tmp;
}
SDL_Rect rect{x, y, 1, 1};
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
SDL_UpdateTexture(this->texture_, &rect, &data, 2);
+2 -2
View File
@@ -33,8 +33,8 @@ class Sdl : public display::Display {
this->pos_x_ = pos_x;
this->pos_y_ = pos_y;
}
int get_width() override { return this->width_; }
int get_height() override { return this->height_; }
int get_width() override;
int get_height() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void dump_config() override { LOG_DISPLAY("", "SDL", this); }
void add_key_listener(int32_t keycode, std::function<void(bool)> &&callback) {
@@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
this->sg_recv_data_state_ = FRAME_DATA_LEN_H;
break;
case FRAME_DATA_LEN_H:
if (value <= 4) {
this->sg_data_len_ = value * 256;
if (value == 0) {
this->sg_frame_buf_[4] = value;
this->sg_recv_data_state_ = FRAME_DATA_LEN_L;
} else {
this->sg_data_len_ = 0;
this->sg_recv_data_state_ = FRAME_IDLE;
ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value);
}
break;
case FRAME_DATA_LEN_L:
this->sg_data_len_ += value;
if (this->sg_data_len_ > 32) {
this->sg_data_len_ = value;
if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) {
ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value);
this->sg_data_len_ = 0;
this->sg_recv_data_state_ = FRAME_IDLE;
@@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
}
break;
case FRAME_DATA_BYTES:
this->sg_data_len_ -= 1;
this->sg_frame_buf_[this->sg_frame_len_++] = value;
if (this->sg_data_len_ <= 0) {
if (--this->sg_data_len_ == 0) {
this->sg_recv_data_state_ = FRAME_DATA_CRC;
}
break;
+17 -15
View File
@@ -406,7 +406,9 @@ QUANTILE_SCHEMA = cv.All(
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float,
cv.Optional(CONF_QUANTILE, default=0.9): cv.float_range(
min=0, min_included=False, max=1
),
}
),
validate_send_first_at,
@@ -427,9 +429,9 @@ async def quantile_filter_to_code(config, filter_id):
MEDIAN_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
}
),
validate_send_first_at,
@@ -449,9 +451,9 @@ async def median_filter_to_code(config, filter_id):
MIN_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
}
),
validate_send_first_at,
@@ -483,9 +485,9 @@ async def min_filter_to_code(config, filter_id):
MAX_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
}
),
validate_send_first_at,
@@ -509,9 +511,9 @@ async def max_filter_to_code(config, filter_id):
SLIDING_AVERAGE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
}
),
validate_send_first_at,
@@ -540,8 +542,8 @@ EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float,
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
}
),
validate_send_first_at,
+24 -10
View File
@@ -41,14 +41,26 @@ void Filter::initialize(Sensor *parent, Filter *next) {
}
// SlidingWindowFilter
SlidingWindowFilter::SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at)
: send_every_(send_every), send_at_(send_every - send_first_at) {
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
: window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
// Allocate ring buffer once at initialization
this->window_.init(window_size);
}
optional<float> SlidingWindowFilter::new_value(float value) {
// Add value to ring buffer (overwrites oldest when full)
this->window_.push_overwrite(value);
// Add value to ring buffer
if (this->window_count_ < this->window_size_) {
// Buffer not yet full - just append
this->window_.push_back(value);
this->window_count_++;
} else {
// Buffer full - overwrite oldest value (ring buffer)
this->window_[this->window_head_] = value;
this->window_head_++;
if (this->window_head_ >= this->window_size_) {
this->window_head_ = 0;
}
}
// Check if we should send a result
if (++this->send_at_ >= this->send_every_) {
@@ -65,8 +77,9 @@ FixedVector<float> SortedWindowFilter::get_window_values_() {
// Copy window without NaN values using FixedVector (no heap allocation)
// Returns unsorted values - caller will use std::nth_element for partial sorting as needed
FixedVector<float> values;
values.init(this->window_.size());
for (float v : this->window_) {
values.init(this->window_count_);
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
if (!std::isnan(v)) {
values.push_back(v);
}
@@ -137,7 +150,8 @@ float MaxFilter::compute_result() { return this->find_extremum_<std::greater<flo
float SlidingWindowMovingAverageFilter::compute_result() {
float sum = 0;
size_t valid_count = 0;
for (float v : this->window_) {
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
if (!std::isnan(v)) {
sum += v;
valid_count++;
@@ -147,7 +161,7 @@ float SlidingWindowMovingAverageFilter::compute_result() {
}
// ExponentialMovingAverageFilter
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at)
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at)
: alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {}
optional<float> ExponentialMovingAverageFilter::new_value(float value) {
if (!std::isnan(value)) {
@@ -169,7 +183,7 @@ optional<float> ExponentialMovingAverageFilter::new_value(float value) {
}
return {};
}
void ExponentialMovingAverageFilter::set_send_every(uint16_t send_every) { this->send_every_ = send_every; }
void ExponentialMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; }
// ThrottleAverageFilter
@@ -497,7 +511,7 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
}
// StreamingFilter (base class)
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
: window_size_(window_size), send_first_at_(send_first_at) {}
optional<float> StreamingFilter::new_value(float value) {
+19 -14
View File
@@ -52,7 +52,7 @@ class Filter {
*/
class SlidingWindowFilter : public Filter {
public:
SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at);
SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) final;
@@ -60,10 +60,14 @@ class SlidingWindowFilter : public Filter {
/// Called by new_value() to compute the filtered result from the current window
virtual float compute_result() = 0;
/// Sliding window ring buffer - automatically overwrites oldest values when full
FixedRingBuffer<float> window_;
uint16_t send_every_; ///< Send result every N values
uint16_t send_at_; ///< Counter for send_every
/// Access the sliding window values (ring buffer implementation)
/// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
FixedVector<float> window_;
size_t window_head_{0}; ///< Index where next value will be written
size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_)
size_t window_size_; ///< Maximum window size
size_t send_every_; ///< Send result every N values
size_t send_at_; ///< Counter for send_every
};
/** Base class for Min/Max filters.
@@ -80,7 +84,8 @@ class MinMaxFilter : public SlidingWindowFilter {
template<typename Compare> float find_extremum_() {
float result = NAN;
Compare comp;
for (float v : this->window_) {
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
if (!std::isnan(v)) {
result = std::isnan(result) ? v : (comp(v, result) ? v : result);
}
@@ -234,18 +239,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
*/
class ExponentialMovingAverageFilter : public Filter {
public:
ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at);
ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) override;
void set_send_every(uint16_t send_every);
void set_send_every(size_t send_every);
void set_alpha(float alpha);
protected:
float accumulator_{NAN};
float alpha_;
uint16_t send_every_;
uint16_t send_at_;
size_t send_every_;
size_t send_at_;
bool first_value_{true};
};
@@ -565,7 +570,7 @@ class ToNTCTemperatureFilter : public Filter {
*/
class StreamingFilter : public Filter {
public:
StreamingFilter(uint16_t window_size, uint16_t send_first_at);
StreamingFilter(size_t window_size, size_t send_first_at);
optional<float> new_value(float value) final;
@@ -579,9 +584,9 @@ class StreamingFilter : public Filter {
/// Called by new_value() to reset internal state after sending a result
virtual void reset_batch() = 0;
uint16_t window_size_;
uint16_t count_{0};
uint16_t send_first_at_;
size_t window_size_;
size_t count_{0};
size_t send_first_at_;
bool first_send_{true};
};
+23 -18
View File
@@ -1,4 +1,5 @@
#include "sht4x.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -9,14 +10,12 @@ static const char *const TAG = "sht4x";
static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
static const uint8_t SERIAL_NUMBER_COMMAND = 0x89;
void SHT4XComponent::start_heater_() {
uint8_t cmd[] = {this->heater_command_};
ESP_LOGD(TAG, "Heater turning on");
if (this->write(cmd, 1) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to turn on heater"));
}
}
// Conversion constants from SHT4x datasheet
static constexpr float TEMPERATURE_OFFSET = -45.0f;
static constexpr float TEMPERATURE_SPAN = 175.0f;
static constexpr float HUMIDITY_OFFSET = -6.0f;
static constexpr float HUMIDITY_SPAN = 125.0f;
static constexpr float RAW_MAX = 65535.0f;
void SHT4XComponent::read_serial_number_() {
uint16_t buffer[2];
@@ -39,8 +38,8 @@ void SHT4XComponent::setup() {
this->read_serial_number_();
if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) {
uint32_t heater_interval = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval);
this->heater_interval_ = static_cast<uint32_t>(static_cast<uint16_t>(this->heater_time_) / this->duty_cycle_);
ESP_LOGD(TAG, "Heater interval: %" PRIu32, this->heater_interval_);
if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) {
if (this->heater_time_ == SHT4X_HEATERTIME_LONG) {
@@ -62,8 +61,6 @@ void SHT4XComponent::setup() {
}
}
ESP_LOGD(TAG, "Heater command: %x", this->heater_command_);
this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this));
}
}
@@ -106,19 +103,27 @@ void SHT4XComponent::update() {
// Evaluate and publish measurements
if (this->temp_sensor_ != nullptr) {
// Temp is contained in the first result word
float sensor_value_temp = buffer[0];
float temp = -45 + 175 * sensor_value_temp / 65535;
float temp = TEMPERATURE_OFFSET + TEMPERATURE_SPAN * static_cast<float>(buffer[0]) / RAW_MAX;
this->temp_sensor_->publish_state(temp);
}
if (this->humidity_sensor_ != nullptr) {
// Relative humidity is in the second result word
float sensor_value_rh = buffer[1];
float rh = -6 + 125 * sensor_value_rh / 65535;
float rh = HUMIDITY_OFFSET + HUMIDITY_SPAN * static_cast<float>(buffer[1]) / RAW_MAX;
this->humidity_sensor_->publish_state(rh);
}
// Fire heater after measurement to maximize cooldown time before the next reading.
// The heater command produces a measurement that we don't need (datasheet 4.9).
if (this->heater_interval_ > 0) {
uint32_t now = millis();
if (now - this->last_heater_millis_ >= this->heater_interval_) {
ESP_LOGD(TAG, "Heater turning on");
if (this->write_command(this->heater_command_)) {
this->last_heater_millis_ = now;
}
}
}
});
}
+2 -1
View File
@@ -35,9 +35,10 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri
SHT4XHEATERTIME heater_time_;
float duty_cycle_;
void start_heater_();
void read_serial_number_();
uint8_t heater_command_;
uint32_t heater_interval_{0};
uint32_t last_heater_millis_{0};
uint32_t serial_number_;
sensor::Sensor *temp_sensor_{nullptr};
@@ -417,7 +417,7 @@ void SpeakerMediaPlayer::loop() {
this->media_playlist_.pop_front();
}
// Only delay starting playback if moving on the next playlist item or repeating the current item
timeout_ms = this->announcement_playlist_delay_ms_;
timeout_ms = this->media_playlist_delay_ms_;
}
if (!this->media_playlist_.empty()) {
PlaylistItem playlist_item = this->media_playlist_.front();
+3 -2
View File
@@ -50,8 +50,9 @@ void TC74Component::read_temperature_() {
}
}
uint8_t temperature_reg;
if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) {
int8_t temperature_reg;
if (this->read_register(TC74_REGISTER_TEMPERATURE, reinterpret_cast<uint8_t *>(&temperature_reg), 1) !=
i2c::ERROR_OK) {
this->status_set_warning();
return;
}
+8 -3
View File
@@ -59,15 +59,20 @@ _DST_RULE_TYPE_MAP = {
def _load_tzdata(iana_key: str) -> bytes | None:
# From https://tzdata.readthedocs.io/en/latest/#examples
if not iana_key:
return None
try:
package_loc, resource = iana_key.rsplit("/", 1)
except ValueError:
return None
package = "tzdata.zoneinfo." + package_loc.replace("/", ".")
# Handle top-level timezone entries like "UTC", "GMT"
package = "tzdata.zoneinfo"
resource = iana_key
else:
package = "tzdata.zoneinfo." + package_loc.replace("/", ".")
try:
return (resources.files(package) / resource).read_bytes()
except (FileNotFoundError, ModuleNotFoundError):
except (FileNotFoundError, ModuleNotFoundError, IsADirectoryError):
return None
+5
View File
@@ -30,12 +30,17 @@ enum UARTDirection {
const LogString *parity_to_str(UARTParityOptions parity);
/// Result of a flush() call.
// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro.
// Save and restore around the enum to avoid collisions with our scoped enum value.
#pragma push_macro("SUCCESS")
#undef SUCCESS
enum class FlushResult {
SUCCESS, ///< Confirmed: all bytes left the TX FIFO.
TIMEOUT, ///< Confirmed: timed out before TX completed.
FAILED, ///< Confirmed: driver or hardware error.
ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed.
};
#pragma pop_macro("SUCCESS")
class UARTComponent {
public:
@@ -7,7 +7,9 @@
#include "esphome/core/log.h"
#include "esphome/core/gpio.h"
#include "driver/gpio.h"
#include "esp_private/gpio.h"
#include "soc/gpio_num.h"
#include "soc/uart_pins.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include "esphome/core/application.h"
@@ -21,6 +23,20 @@ namespace esphome::uart {
static const char *const TAG = "uart.idf";
/// Check if a pin number matches one of the default UART0 GPIO pins.
/// These pins may have residual IOMUX state from the ROM bootloader that
/// must be cleared before UART reconfiguration.
///
/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix,
/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only
/// calls gpio_input_enable() which does NOT clear the IOMUX function select.
/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later
/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active,
/// causing TX data to loop back into RX on the same pin.
static constexpr bool is_default_uart0_pin(int8_t pin_num) {
return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM;
}
uart_config_t IDFUARTComponent::get_config_() {
uart_parity_t parity = UART_PARITY_DISABLE;
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
@@ -131,6 +147,19 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
// Clear residual IOMUX function on UART0 default pins left by the ROM bootloader.
// See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug.
if (is_default_uart0_pin(tx)) {
gpio_func_sel(static_cast<gpio_num_t>(tx), PIN_FUNC_GPIO);
}
if (is_default_uart0_pin(rx)) {
gpio_func_sel(static_cast<gpio_num_t>(rx), PIN_FUNC_GPIO);
}
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
@@ -146,10 +175,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
setup_pin_if_needed(this->tx_pin_);
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;
@@ -6,11 +6,17 @@ namespace esphome::ultrasonic {
static const char *const TAG = "ultrasonic.sensor";
static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us of each other (noise filtering)
static constexpr uint32_t START_DELAY_US = 100; // Ignore edges within 100us of trigger (filters bleed-through)
static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start
void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) {
uint32_t now = micros();
if (arg->echo_pin_isr.digital_read()) {
// Ignore edges after measurement complete or too soon after trigger pulse
if (arg->echo_end || (now - arg->measurement_start_us) <= START_DELAY_US) {
return;
}
if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) {
arg->echo_start_us = now;
arg->echo_start = true;
} else {
@@ -21,15 +27,14 @@ void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) {
void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() {
InterruptLock lock;
this->store_.echo_start_us = 0;
this->store_.echo_end_us = 0;
this->store_.echo_start = false;
this->store_.echo_end = false;
this->store_.measurement_start_us = micros();
this->trigger_pin_isr_.digital_write(true);
delayMicroseconds(this->pulse_time_us_);
this->trigger_pin_isr_.digital_write(false);
this->measurement_pending_ = true;
this->measurement_start_us_ = micros();
this->measurement_start_us_ = this->store_.measurement_start_us;
}
void UltrasonicSensorComponent::setup() {
@@ -37,7 +42,6 @@ void UltrasonicSensorComponent::setup() {
this->trigger_pin_->digital_write(false);
this->trigger_pin_isr_ = this->trigger_pin_->to_isr();
this->echo_pin_->setup();
this->store_.echo_pin_isr = this->echo_pin_->to_isr();
this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
}
@@ -77,17 +81,10 @@ void UltrasonicSensorComponent::loop() {
}
if (this->store_.echo_end) {
float result;
if (this->store_.echo_start) {
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us",
this->store_.echo_start_us - this->measurement_start_us_, pulse_duration);
result = UltrasonicSensorComponent::us_to_m(pulse_duration);
ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result);
} else {
ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str());
result = NAN;
}
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration);
float result = UltrasonicSensorComponent::us_to_m(pulse_duration);
ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result);
this->publish_state(result);
this->measurement_pending_ = false;
return;
@@ -11,8 +11,7 @@ namespace esphome::ultrasonic {
struct UltrasonicSensorStore {
static void gpio_intr(UltrasonicSensorStore *arg);
ISRInternalGPIOPin echo_pin_isr;
volatile uint32_t wait_start_us{0};
volatile uint32_t measurement_start_us{0};
volatile uint32_t echo_start_us{0};
volatile uint32_t echo_end_us{0};
volatile bool echo_start{false};
+10 -16
View File
@@ -26,16 +26,13 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) {
event->data.line_state.dtr = dtr;
event->data.line_state.rts = rts;
if (!this->event_queue_.push(event)) {
ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_);
// Return event to pool since we couldn't queue it
this->event_pool_.release(event);
} else {
// Wake main loop immediately to process event
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->event_queue_.push(event);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
App.wake_loop_threadsafe();
#endif
}
}
void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity,
@@ -53,16 +50,13 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_
event->data.line_coding.parity = parity;
event->data.line_coding.data_bits = data_bits;
if (!this->event_queue_.push(event)) {
ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_);
// Return event to pool since we couldn't queue it
this->event_pool_.release(event);
} else {
// Wake main loop immediately to process event
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->event_queue_.push(event);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
App.wake_loop_threadsafe();
#endif
}
}
void USBCDCACMInstance::process_events_() {
+5 -1
View File
@@ -102,7 +102,11 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC
LineStateCallback line_state_callback_{nullptr};
// Lock-free queue and event pool for cross-task event passing
EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
// before push() can fail, preventing both a pool slot leak and an SPSC
// violation on the pool's internal free list.
EventPool<CDCEvent, EVENT_QUEUE_SIZE - 1> event_pool_;
LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_;
};
+4 -1
View File
@@ -144,7 +144,10 @@ class USBClient : public Component {
// Lock-free event queue and pool for USB task to main loop communication
// Must be public for access from static callbacks
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
// before push() can fail, preventing a pool slot leak.
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE - 1> event_pool;
protected:
// Process USB events from the queue. Returns true if any work was done.
@@ -193,7 +193,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *
return;
}
// Push to lock-free queue (always succeeds since pool size == queue size)
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
client->event_queue.push(event);
// Re-enable component loop to process the queued event
+5 -6
View File
@@ -160,11 +160,9 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) {
size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE);
memcpy(chunk->data, data, chunk_len);
chunk->length = static_cast<uint8_t>(chunk_len);
if (!this->output_queue_.push(chunk)) {
this->output_pool_.release(chunk);
ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len);
break;
}
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->output_queue_.push(chunk);
data += chunk_len;
len -= chunk_len;
}
@@ -320,7 +318,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
this->usb_data_queue_.push(chunk);
// Re-enable component loop to process the queued data
+6 -2
View File
@@ -158,7 +158,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
// Larger structures first (8+ bytes)
RingBuffer input_buffer_;
LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_;
EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
// before push() can fail, preventing a pool slot leak.
EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1> output_pool_;
std::function<void()> rx_callback_{};
CdcEps cdc_dev_{};
StringRef debug_prefix_{};
@@ -190,7 +193,8 @@ class USBUartComponent : public usb_host::USBClient {
// Lock-free data transfer from USB task to main loop
static constexpr int USB_DATA_QUEUE_SIZE = 32;
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
// Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment.
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE - 1> chunk_pool_;
protected:
std::vector<USBUartChannel *> channels_{};
-1
View File
@@ -19,7 +19,6 @@ CONF_DELTASOL_BS_2009 = "deltasol_bs_2009"
CONF_DELTASOL_BS2 = "deltasol_bs2"
CONF_DELTASOL_C = "deltasol_c"
CONF_DELTASOL_CS2 = "deltasol_cs2"
CONF_DELTASOL_CS4 = "deltasol_cs4"
CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus"
CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
@@ -20,7 +20,6 @@ from .. import (
CONF_DELTASOL_BS_PLUS,
CONF_DELTASOL_C,
CONF_DELTASOL_CS2,
CONF_DELTASOL_CS4,
CONF_DELTASOL_CS_PLUS,
CONF_VBUS_ID,
VBus,
@@ -32,7 +31,6 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component)
DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component)
DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component)
DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component)
DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4BSensor", cg.Component)
DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component)
VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component)
VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component)
@@ -188,28 +186,6 @@ CONFIG_SCHEMA = cv.typed_schema(
),
}
),
CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaSol_CS4),
cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
),
CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
@@ -374,23 +350,6 @@ async def to_code(config):
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
cg.add(var.set_s4_error_bsensor(sens))
elif config[CONF_MODEL] == CONF_DELTASOL_CS4:
cg.add(var.set_command(0x0100))
cg.add(var.set_source(0x1122))
cg.add(var.set_dest(0x0010))
if CONF_SENSOR1_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
cg.add(var.set_s1_error_bsensor(sens))
if CONF_SENSOR2_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
cg.add(var.set_s2_error_bsensor(sens))
if CONF_SENSOR3_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
cg.add(var.set_s3_error_bsensor(sens))
if CONF_SENSOR4_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
cg.add(var.set_s4_error_bsensor(sens))
elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
cg.add(var.set_command(0x0100))
cg.add(var.set_source(0x2211))
@@ -110,25 +110,6 @@ void DeltaSolCS2BSensor::handle_message(std::vector<uint8_t> &message) {
this->s4_error_bsensor_->publish_state(message[18] & 8);
}
void DeltaSolCS4BSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Deltasol CS4:");
LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_);
LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_);
LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_);
LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_);
}
void DeltaSolCS4BSensor::handle_message(std::vector<uint8_t> &message) {
if (this->s1_error_bsensor_ != nullptr)
this->s1_error_bsensor_->publish_state(message[20] & 1);
if (this->s2_error_bsensor_ != nullptr)
this->s2_error_bsensor_->publish_state(message[20] & 2);
if (this->s3_error_bsensor_ != nullptr)
this->s3_error_bsensor_->publish_state(message[20] & 4);
if (this->s4_error_bsensor_ != nullptr)
this->s4_error_bsensor_->publish_state(message[20] & 8);
}
void DeltaSolCSPlusBSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_);
@@ -94,23 +94,6 @@ class DeltaSolCS2BSensor : public VBusListener, public Component {
void handle_message(std::vector<uint8_t> &message) override;
};
class DeltaSolCS4BSensor : public VBusListener, public Component {
public:
void dump_config() override;
void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
protected:
binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
void handle_message(std::vector<uint8_t> &message) override;
};
class DeltaSolCSPlusBSensor : public VBusListener, public Component {
public:
void dump_config() override;

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