Compare commits

..
Author SHA1 Message Date
J. Nick Koston 1e9499d87d Merge remote-tracking branch 'origin/dev' into esp8266-native-build-surgery 2026-08-25 13:20:50 -05:00
J. Nick Koston 3426eb2733 Merge remote-tracking branch 'origin/dev' into esp8266-native-build-surgery 2026-08-25 12:38:49 -05:00
J. Nick Koston 94b47496c8 Merge remote-tracking branch 'origin/dev' into esp8266-native-build-surgery 2026-08-25 12:37:36 -05:00
J. Nick Koston 515aa96844 Merge remote-tracking branch 'origin/dev' into esp8266-native-build-surgery 2026-08-25 10:47:44 -05:00
J. Nick Koston 802a14f90a Hoist the test imports to module level 2026-08-24 18:53:36 -05:00
J. Nick Koston 69a3ab6fb1 Error instead of substituting a state-destroying layout on legacy cores, extract the ld choice, regroup KEY_LDSCRIPT, document segment_length's None 2026-08-24 18:51:10 -05:00
J. Nick Koston 2cd5db4854 Merge remote-tracking branch 'origin/esp8266-native-build-surgery' into esp8266-native-build-surgery 2026-08-24 18:47:58 -05:00
J. Nick Koston 767b60a914 Merge remote-tracking branch 'origin/dev' into esp8266-native-build-surgery 2026-08-24 18:46:55 -05:00
J. Nick Koston 7ca1b79544 Name the ldscript key, warn when a pre-2.4.2 core cannot honor it, pin the rule
KEY_LDSCRIPT replaces the bare literal at both the table entry and the
lookup, so a typo cannot silently fall back to the layout-moving
default; the regeneration recipe notes that overrides must survive a
refresh; a board pinning an override on an Arduino core <= 2.4.2 warns
that the layout cannot be honored; and board_ld_script gains the test
pinning d1_wroom_02's shipped layout and the size defaults.
2026-08-24 18:46:47 -05:00
J. Nick Koston a25509bda0 Share the per-board ldscript rule via boards.board_ld_script
One source of truth for the PlatformIO pinning and the native
generator's fallback, so the per-board rule cannot drift.
2026-08-24 18:46:47 -05:00
J. Nick Koston 04825d3a58 Reconcile BOARDS with the generated table: d1_wroom_02 joins with its shipped flash layout 2026-08-24 18:46:47 -05:00
J. Nick Koston e714848fb9 Trim comment essays and hoist function-local test imports 2026-08-24 18:46:47 -05:00
J. Nick Koston d7ee824228 Phrase the board-table regen comment imperatively 2026-08-24 18:46:47 -05:00
J. Nick Koston 862b7f7c3e Pin fingerprint stability and sensitivity behaviorally, note the platform pairing 2026-08-24 18:46:47 -05:00
J. Nick Koston 77c41d2579 State what the fingerprint test actually pins 2026-08-24 18:46:47 -05:00
J. Nick Koston 4d92741e69 Pin the decoy anchor, guard unselected segments, fingerprint the module source 2026-08-24 18:46:47 -05:00
J. Nick Koston b2ac49d8a8 Anchor the segment regex, reject unknown segments, and pin real-script fixtures 2026-08-24 18:46:47 -05:00
J. Nick Koston 8fb6496583 Export a surgery fingerprint for linker-script cache stamps 2026-08-24 18:46:47 -05:00
J. Nick Koston 5f3d73720e Patch exactly the named segments and share the segment-line pattern 2026-08-24 18:46:47 -05:00
J. Nick Koston cdd8a16cb9 Add the ESP8266 linker-script surgery and board build metadata 2026-08-24 18:46:47 -05:00
J. Nick Koston 4efc3ecb70 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-24 16:00:08 -05:00
J. Nick Koston 4457158e3b Merge remote-tracking branch 'origin/dev' into esp8266-native-shared-helpers 2026-08-24 15:59:59 -05:00
J. Nick Koston ad437cbd15 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-24 15:58:30 -05:00
J. Nick Koston 7e236bc762 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-24 15:58:30 -05:00
J. Nick Koston 6eed2e5d2b Merge remote-tracking branch 'origin/dev' into esp8266-native-library-converter 2026-08-24 15:58:21 -05:00
J. Nick Koston 454e380057 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-24 14:38:58 -05:00
J. Nick Koston cd1e5aad67 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-24 14:38:58 -05:00
J. Nick Koston a8c4018536 Merge remote-tracking branch 'origin/dev' into esp8266-native-library-converter 2026-08-24 14:38:49 -05:00
J. Nick Koston 200ee0c9c3 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-24 12:32:54 -05:00
J. Nick Koston 81c375af0b Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-24 12:32:53 -05:00
J. Nick Koston 3ca65ffb0d Cover the file-like backoff tick, settle the batch worker return shape 2026-08-24 12:32:38 -05:00
J. Nick Koston 68b474fd00 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-24 12:20:18 -05:00
J. Nick Koston efa7fae845 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-24 12:20:18 -05:00
J. Nick Koston 29c290166b Apply simplify pass: flatten the batch worker, drop a dead guard, skip the unread backoff probe 2026-08-24 12:20:14 -05:00
J. Nick Koston d348284d9e Hoist the manifest shape gate into a helper 2026-08-24 12:17:05 -05:00
J. Nick Koston 3a248d526d Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 22:25:37 -05:00
J. Nick Koston 2f49405740 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 22:25:36 -05:00
J. Nick Koston d418472a8f Merge remote-tracking branch 'origin/dev' into esp8266-native-library-converter
# Conflicts:
#	esphome/platformio/library.py
#	tests/unit_tests/test_platformio_library.py
2026-08-23 22:25:22 -05:00
J. Nick Koston 7a715ee583 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 22:02:12 -05:00
J. Nick Koston d8dc1dc70e Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 22:02:12 -05:00
J. Nick Koston 5479752da6 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 22:02:12 -05:00
J. Nick Koston b0e4711cc3 Merge remote-tracking branch 'origin/esp8266-native-converter-hardening' into esp8266-native-converter-hardening 2026-08-23 22:01:59 -05:00
J. Nick Koston d744a98f5b Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 22:00:34 -05:00
J. Nick Koston a03945bbfb Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 22:00:34 -05:00
J. Nick Koston bc2b4bd381 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 22:00:34 -05:00
J. Nick Koston f8acaab6c1 Merge remote-tracking branch 'origin/dev' into esp8266-native-converter-hardening
# Conflicts:
#	script/determine-jobs.py
#	tests/script/test_determine_jobs.py
2026-08-23 22:00:22 -05:00
J. Nick Koston 5f2b8f7251 Type LINK_FLAGS in the gate, skip malformed transitive deps, escape quotes in zephyr CMake entries 2026-08-23 21:59:22 -05:00
J. Nick Koston d80d2c7034 Tolerate malformed repository/description metadata instead of crashing 2026-08-23 21:59:22 -05:00
J. Nick Koston 1f5152892c Give the fake env real membership, widen the manifest gate, route zephyr through the shared lexer 2026-08-23 21:59:22 -05:00
J. Nick Koston 89671f9e0d Normalize captured CPPDEFINES to a CppDefine named tuple at capture time 2026-08-23 21:59:22 -05:00
J. Nick Koston c6f9153e63 Route captured LINKFLAGS to the link line and make Prepend a real front-insert 2026-08-23 21:59:22 -05:00
J. Nick Koston aa3268a6d8 Match SCons' remaining CPPDEFINES spellings, drop dead test scaffolding
A bare 2-tuple is one name=value pair (not -DFOO -D1), a dict maps
names to values, and a None value is a bare define, per processDefines.
The unused caplog_at_info helper and the progress kwarg the download
stubs declared ahead of their PR are gone.
2026-08-23 21:59:22 -05:00
J. Nick Koston 879cbf6264 Cover env access warnings and the unmodelled-subscript degrade 2026-08-23 21:59:22 -05:00
J. Nick Koston 4da8868510 Quote CPPDEFINES, warn on env access, degrade unmodelled subscripts, realify InvalidLibrary
CPPDEFINES joins the quoted buckets (a spaced define no longer splits
across tokens) and its tuple branch validates the pair elements. The
fake env warns on attribute access rather than call, so hasattr/
truthiness branches are diagnosable, with dunder probes excluded; an
unmodelled subscript warns and returns '' instead of a KeyError
discarding the whole capture. Malformed platforms/frameworks values
raise plain InvalidLibrary, giving the non-platform warning branches a
real producer, and their tests use real manifests instead of
monkeypatched raisers. The version-less dependency drop moves to debug:
bundled names (Wire, SPI) made it per-build noise nobody can act on.
2026-08-23 21:59:22 -05:00
J. Nick Koston df76fc6fba Trim comments and docstrings 2026-08-23 21:59:22 -05:00
J. Nick Koston 0d75f597b3 Harden the shared library converter
Split out of #18662: the converter/manifest hardening without the
download parallelization, which stays there.

The PIO extra-script shim moves from espidf/ to platformio/ and gains
type checks for every manifest field, capture of the Append variants and
CPPPATH, warn-once diagnostics for unmodelled env reads, and uniform
shlex quoting so captured values survive the lex_build_flags round-trip.
The dependency walk normalizes every spelling PIO accepts, splits the
routine cross-platform skip (IncompatiblePlatform) from real manifest
problems, validates manifest shape once, and the shared flag lexer
warns and drops trailing or empty glued arguments.
2026-08-23 21:59:22 -05:00
J. Nick Koston 936bc708af Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 21:10:46 -05:00
J. Nick Koston 13dcf54a43 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 21:10:46 -05:00
J. Nick Koston 5bccc41ca3 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 21:10:46 -05:00
J. Nick Koston 5ba31dcccb Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 21:10:45 -05:00
J. Nick Koston 3fe4e255d8 Merge remote-tracking branch 'origin/dev' into esp8266-native-idedata-helpers 2026-08-23 21:10:35 -05:00
J. Nick Koston 67608f9386 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 19:38:32 -05:00
J. Nick Koston 5aaf5e8150 Parametrize the build-info staleness test 2026-08-23 19:33:08 -05:00
J. Nick Koston 5ce8c68df5 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 19:30:46 -05:00
J. Nick Koston 412b7a8b99 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 19:30:46 -05:00
J. Nick Koston e594888176 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter
# Conflicts:
#	esphome/platformio/library.py
2026-08-23 19:30:35 -05:00
J. Nick Koston 6bdd8b1163 Type LINK_FLAGS in the gate, skip malformed transitive deps, escape quotes in zephyr CMake entries 2026-08-23 19:30:02 -05:00
J. Nick Koston 06e3525f82 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 18:58:59 -05:00
J. Nick Koston 476ec16a3d Split the AS and ASPP source kinds to match SCons 2026-08-23 18:58:48 -05:00
J. Nick Koston d9b3536e9f Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 18:45:14 -05:00
J. Nick Koston 0ed990e311 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 18:45:14 -05:00
J. Nick Koston 69d997521f Guard the bar writes, narrow the cache probe, make a total prefetch failure loud 2026-08-23 18:45:02 -05:00
J. Nick Koston 4affd574ac Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 18:31:08 -05:00
J. Nick Koston 4840740032 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers
# Conflicts:
#	esphome/writer.py
2026-08-23 18:31:05 -05:00
J. Nick Koston ae5e5b3011 Merge remote-tracking branch 'origin/dev' into esp8266-native-idedata-helpers 2026-08-23 18:30:06 -05:00
J. Nick Koston 3ab464c345 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 18:30:06 -05:00
J. Nick Koston b74de2dfa2 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 18:30:06 -05:00
J. Nick Koston 7ea8892e0a Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 18:20:45 -05:00
J. Nick Koston dce979245d Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 18:20:45 -05:00
J. Nick Koston 6e65634408 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 18:20:45 -05:00
J. Nick Koston 6300e9b72e Tolerate malformed repository/description metadata instead of crashing 2026-08-23 18:20:43 -05:00
J. Nick Koston cb4dd32e19 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 18:15:04 -05:00
J. Nick Koston 6381ed97b9 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 18:15:04 -05:00
J. Nick Koston 786e9ef278 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter
# Conflicts:
#	esphome/platformio/library.py
2026-08-23 18:14:53 -05:00
J. Nick Koston 24ce2195da Merge remote-tracking branch 'origin/dev' into esp8266-native-idedata-helpers 2026-08-23 18:12:27 -05:00
J. Nick Koston d5fe45db66 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 18:12:27 -05:00
J. Nick Koston a027d06231 Give the fake env real membership, widen the manifest gate, route zephyr through the shared lexer 2026-08-23 18:12:14 -05:00
J. Nick Koston 8f7534f39a Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 17:27:06 -05:00
J. Nick Koston 6bac1ed950 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 17:27:05 -05:00
J. Nick Koston 57241e40b8 Make failure_reason public now that two modules import it, cover interrupt() on a finished bar 2026-08-23 17:26:47 -05:00
J. Nick Koston 76ddbe5ebd Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 17:17:59 -05:00
J. Nick Koston 81d9b943aa Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 17:17:59 -05:00
J. Nick Koston 28f0a7b155 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 17:17:59 -05:00
J. Nick Koston 531bd8ec62 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 17:17:59 -05:00
J. Nick Koston 40e6e406d7 Merge remote-tracking branch 'origin/dev' into esp8266-native-idedata-helpers 2026-08-23 17:17:52 -05:00
J. Nick Koston 6cfbe5b166 Normalize captured CPPDEFINES to a CppDefine named tuple at capture time 2026-08-23 17:15:29 -05:00
J. Nick Koston b738b36a39 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 17:08:16 -05:00
J. Nick Koston ac66e3a96c Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 17:08:16 -05:00
J. Nick Koston e7fc3937bf Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 17:08:16 -05:00
J. Nick Koston 70f4555ec8 Route captured LINKFLAGS to the link line and make Prepend a real front-insert 2026-08-23 17:08:04 -05:00
J. Nick Koston 5f20ddb630 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:54:56 -05:00
J. Nick Koston 6cfb915aeb Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:54:56 -05:00
J. Nick Koston 04a44d6e63 Let ProgressBar own the mid-row interrupt, share the part-file naming, fold duplicate except arms 2026-08-23 16:54:33 -05:00
J. Nick Koston cb10904fa3 Keep the combined bar steady during a backoff sleep 2026-08-23 16:49:54 -05:00
J. Nick Koston 89e5ba00de Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:35:58 -05:00
J. Nick Koston 81b1678498 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:35:56 -05:00
J. Nick Koston ba0d9e77db Never log a blank prefetch reason, keep the download error over a tick failure
Both prefetch warning loops format through _failure_reason so a
message-less exception cannot log blank. A bar-frame write failure in
the failure path cannot displace the download error, the failure list
is annotated BaseException (cancellations are entries too), the Ctrl-C
docstring notes an in-progress extraction runs to completion, and the
wave-prefetch docstring carries the superseded-sibling caveat.
2026-08-23 16:35:48 -05:00
J. Nick Koston 330f070921 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:31:12 -05:00
J. Nick Koston f6ae5e4892 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:31:10 -05:00
J. Nick Koston b0b174f3e2 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 16:31:08 -05:00
J. Nick Koston d8857080f0 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 16:31:06 -05:00
J. Nick Koston c285af3857 Cover the Flash line through the shared bar 2026-08-23 16:31:03 -05:00
J. Nick Koston d7d236c6bc Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:25:22 -05:00
J. Nick Koston de00530c0e Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:25:20 -05:00
J. Nick Koston acf5ce718c Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 16:25:18 -05:00
J. Nick Koston 5904928f0c Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 16:25:16 -05:00
J. Nick Koston dceeee69d3 Restore the size summary to its dev shape
The defensive spiral (#18668, closed) guarded inputs that cannot occur:
gen_esp32part validates partitions.csv before a build can succeed, and
esp_idf_size.json's realistic failures do not produce the handled
shapes. This PR's real change stands alone again: the bar moves to
build_helpers as print_size_line, with dev behavior everywhere else.
2026-08-23 16:25:11 -05:00
J. Nick Koston 03c7d3b343 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:17:47 -05:00
J. Nick Koston d405db1268 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:17:45 -05:00
J. Nick Koston 8cac10d319 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 16:17:43 -05:00
J. Nick Koston 030a3b7ab3 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 16:17:41 -05:00
J. Nick Koston dfc26e9f09 Mirror the named-file arm and tightened checks 2026-08-23 16:17:37 -05:00
J. Nick Koston 4f9941decb Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:12:01 -05:00
J. Nick Koston 385681121f Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:11:59 -05:00
J. Nick Koston c395861fa3 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 16:11:57 -05:00
J. Nick Koston 30b2bbfe3a Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 16:11:55 -05:00
J. Nick Koston 3af17d5c92 Mirror the named missing-table raise 2026-08-23 16:11:52 -05:00
J. Nick Koston 72d6864938 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:09:48 -05:00
J. Nick Koston 89ffacaffb Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:09:46 -05:00
J. Nick Koston 6eff668dba Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 16:09:44 -05:00
J. Nick Koston 350202efa6 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 16:09:42 -05:00
J. Nick Koston 14388c859a Mirror the backstop-ownership trim from the standalone 2026-08-23 16:09:26 -05:00
J. Nick Koston 7061bd7e70 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 16:06:48 -05:00
J. Nick Koston fe274653bd Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 16:06:46 -05:00
J. Nick Koston 7beb0078c2 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 16:06:44 -05:00
J. Nick Koston e26dbb6d75 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 16:06:42 -05:00
J. Nick Koston 7aa0f30ceb Mirror the unreachable-arm removal from the standalone 2026-08-23 16:06:38 -05:00
J. Nick Koston 438a4ddc14 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 15:59:01 -05:00
J. Nick Koston 5a749c4b5d Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 15:58:59 -05:00
J. Nick Koston 3de58dae99 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 15:58:57 -05:00
J. Nick Koston fd2971bd70 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 15:58:55 -05:00
J. Nick Koston 8a9bb7741a Mirror the truncated-size-report classification 2026-08-23 15:58:52 -05:00
J. Nick Koston 393dabadf8 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 15:52:30 -05:00
J. Nick Koston 56dc9124ab Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 15:52:28 -05:00
J. Nick Koston 92b655c5a0 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 15:52:26 -05:00
J. Nick Koston c474421399 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 15:52:24 -05:00
J. Nick Koston ca611a3159 Mirror the size-summary visibility refinements from the standalone
Corrupt shapes at any level warn as malformed while a simply-absent
DRAM/DIRAM region stays at debug, the backstop warning carries its
traceback, and the image_size warning names its file.
2026-08-23 15:51:30 -05:00
J. Nick Koston 6dbef0d0bb Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 15:45:46 -05:00
J. Nick Koston 832c6feecc Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 15:45:44 -05:00
J. Nick Koston 41b0fe6747 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 15:45:42 -05:00
J. Nick Koston e580e267ce Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 15:45:40 -05:00
J. Nick Koston d3590ffd7c Port the standalone size-summary hardening into the chain copy
The full #18668 state, adapted to the shared bar: _ram_bar/_flash_bar
return validated (used, total) pairs printed through print_size_line,
the partition parser returns None for legitimate absence and raises
only for a broken table, skips follow the warn/debug visibility rule,
and run_compile passes the table path unconditionally. Ported now so
the merge-time conflict resolves itself; the chain-only exact-format
and RAM-vs-Flash isolation tests stay.
2026-08-23 15:45:36 -05:00
J. Nick Koston 54429bcdfb Name the ldscript key, warn when a pre-2.4.2 core cannot honor it, pin the rule
KEY_LDSCRIPT replaces the bare literal at both the table entry and the
lookup, so a typo cannot silently fall back to the layout-moving
default; the regeneration recipe notes that overrides must survive a
refresh; a board pinning an override on an Arduino core <= 2.4.2 warns
that the layout cannot be honored; and board_ld_script gains the test
pinning d1_wroom_02's shipped layout and the size defaults.
2026-08-23 15:27:36 -05:00
J. Nick Koston 8018a0d930 Cover the bar-less logging guard and the uncancelled backoff slice 2026-08-23 15:25:29 -05:00
J. Nick Koston 20920e0971 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 15:09:05 -05:00
J. Nick Koston 81be569a85 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 15:09:03 -05:00
J. Nick Koston 641c80b193 Use the caplog fixture where the deleted helper was 2026-08-23 15:09:00 -05:00
J. Nick Koston 8170cd3d96 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 15:06:44 -05:00
J. Nick Koston e2eae002e6 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 15:06:42 -05:00
J. Nick Koston 065867f815 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 15:06:40 -05:00
J. Nick Koston 5aebd7cb41 Match SCons' remaining CPPDEFINES spellings, drop dead test scaffolding
A bare 2-tuple is one name=value pair (not -DFOO -D1), a dict maps
names to values, and a None value is a bare define, per processDefines.
The unused caplog_at_info helper and the progress kwarg the download
stubs declared ahead of their PR are gone.
2026-08-23 15:05:43 -05:00
J. Nick Koston 6497ca1496 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 14:42:19 -05:00
J. Nick Koston 759ddfc91d Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 14:42:17 -05:00
J. Nick Koston 062c542c36 Give worker log records their own line, harden the cancellation sentinel
A logging filter installed for the batch's duration ends a partial bar
row (under the bar's lock) before any record is emitted, so a mirror
retry warning from a worker is no longer overwritten by the next CR
frame. The Ctrl-C sentinel derives from BaseException so a broad
except Exception in the download layers cannot convert an abort into a
retry, an abandoned job is reported like a failure instead of reading
as a completed download, and the mirror-sweep backoff sleeps through
the cancellation tick so a Ctrl-C during a retry window aborts at the
boundary instead of after it. The docstring states the parked-socket
worst case.
2026-08-23 14:42:00 -05:00
J. Nick Koston d23e9a205a Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 14:31:26 -05:00
J. Nick Koston 03a443c2b7 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 14:31:24 -05:00
J. Nick Koston 0c97e2b354 Route single-archive waves through the batch runner
A dependency chain (api -> noise-c -> libsodium) discovers one archive
per wave, so the <2 gate sent every chained download through the legacy
per-file path. Sized registry archives now always use the batch runner:
one download method, one bar; the per-file path remains only for
unknown-size, git, and local sources.
2026-08-23 14:31:18 -05:00
J. Nick Koston 4803a92554 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 13:57:11 -05:00
J. Nick Koston 3a35c75ec1 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 13:57:09 -05:00
J. Nick Koston 8536d54998 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 13:57:07 -05:00
J. Nick Koston 10bccf3fc0 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-converter-hardening 2026-08-23 13:57:05 -05:00
J. Nick Koston b070d1631c Merge remote-tracking branch 'origin/dev' into esp8266-native-idedata-helpers 2026-08-23 13:57:03 -05:00
J. Nick Koston abcfaa1810 Share the per-board ldscript rule via boards.board_ld_script
One source of truth for the PlatformIO pinning and the native
generator's fallback, so the per-board rule cannot drift.
2026-08-23 13:54:22 -05:00
J. Nick Koston c822b80c95 Cover env access warnings and the unmodelled-subscript degrade 2026-08-23 13:49:03 -05:00
J. Nick Koston ae8aa3fee9 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 13:36:08 -05:00
J. Nick Koston ce0a0da42a Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 13:36:06 -05:00
J. Nick Koston 2199fcc851 Restore the malformed-manifest raises lost in the merge resolution 2026-08-23 13:35:52 -05:00
J. Nick Koston 554e19ccdb Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 13:35:09 -05:00
J. Nick Koston 9662fea8b9 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 13:35:07 -05:00
J. Nick Koston b4031dc53a Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter
# Conflicts:
#	esphome/platformio/library.py
2026-08-23 13:35:04 -05:00
J. Nick Koston eb958bd0a0 Quote CPPDEFINES, warn on env access, degrade unmodelled subscripts, realify InvalidLibrary
CPPDEFINES joins the quoted buckets (a spaced define no longer splits
across tokens) and its tuple branch validates the pair elements. The
fake env warns on attribute access rather than call, so hasattr/
truthiness branches are diagnosable, with dunder probes excluded; an
unmodelled subscript warns and returns '' instead of a KeyError
discarding the whole capture. Malformed platforms/frameworks values
raise plain InvalidLibrary, giving the non-platform warning branches a
real producer, and their tests use real manifests instead of
monkeypatched raisers. The version-less dependency drop moves to debug:
bundled names (Wire, SPI) made it per-build noise nobody can act on.
2026-08-23 13:34:19 -05:00
J. Nick Koston 2da0319eaa Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 13:17:14 -05:00
J. Nick Koston c376d32db4 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 13:17:12 -05:00
J. Nick Koston 59fc108aa7 Cover the prefetch blanket guard 2026-08-23 13:17:08 -05:00
J. Nick Koston a272ca9e17 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 13:09:53 -05:00
J. Nick Koston e40537cc34 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 13:09:53 -05:00
J. Nick Koston 9d761102e5 Size the prefetch from the registry, fold the batch API, guard the prefetch
The registry response already carries each package file's size, so the
combined bar is sized from URLSource instead of a HEAD sweep; the probe,
its all-or-nothing fallback, and its duplicate Content-Length parsing are
gone, and archives without a reported size simply stay on the sequential
path. run_batch_downloads now takes (name, size, fetch) jobs and builds
the (now private) progress bar itself, collecting failures from future
results instead of a shared list; both call sites use functools.partial
instead of lambda factories. The prefetch touches only URLSource, so the
progress parameter comes off Source/Git/Local/ConvertedLibrary, and the
wave prefetch is wrapped best-effort like its ESP-IDF twin, which also
dedupes by dest only after the verifiability check. ProgressBar.done()
itself now skips the newline when no frame was drawn or the 100% frame
already ended the line, collapsing the batch wrapper's guard and fixing
the stray blank line after OTA uploads.
2026-08-23 13:08:32 -05:00
J. Nick Koston 1372ec8ad2 Trim the shape-key comment 2026-08-23 13:07:33 -05:00
J. Nick Koston c4ddad0237 Trim comments and docstrings 2026-08-23 12:57:59 -05:00
J. Nick Koston 6d4d9aa1ce Trim comments and docstrings 2026-08-23 12:57:17 -05:00
J. Nick Koston caff71c17a Trim comments and docstrings 2026-08-23 12:56:31 -05:00
J. Nick Koston 8e4c9c31e6 Merge branch 'esp8266-native-converter-hardening' into esp8266-native-library-converter 2026-08-23 12:55:57 -05:00
J. Nick Koston a478ae4a46 Trim comments and docstrings 2026-08-23 12:55:52 -05:00
J. Nick Koston bd69eab7fb Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 12:33:55 -05:00
J. Nick Koston 5adaee5408 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 12:33:53 -05:00
J. Nick Koston b903c884fd Absorb the converter-hardening split (#18685); this PR keeps only the download parallelization 2026-08-23 12:33:21 -05:00
J. Nick Koston 9731c9527a Harden the shared library converter
Split out of #18662: the converter/manifest hardening without the
download parallelization, which stays there.

The PIO extra-script shim moves from espidf/ to platformio/ and gains
type checks for every manifest field, capture of the Append variants and
CPPPATH, warn-once diagnostics for unmodelled env reads, and uniform
shlex quoting so captured values survive the lex_build_flags round-trip.
The dependency walk normalizes every spelling PIO accepts, splits the
routine cross-platform skip (IncompatiblePlatform) from real manifest
problems, validates manifest shape once, and the shared flag lexer
warns and drops trailing or empty glued arguments.
2026-08-23 12:32:44 -05:00
J. Nick Koston f595a32434 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 12:15:19 -05:00
J. Nick Koston 1959913347 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 12:15:18 -05:00
J. Nick Koston 035555b24c Drop an empty glued flag argument at the lex funnel
shlex.split('-D ""') left a bare -D that gcc resolves by eating the
next flag; the funnel now warns and drops it for every consumer, which
also retires raise_on_empty_arg_flags, whose staged ESP8266 caller no
longer has anything to catch.
2026-08-23 12:15:04 -05:00
J. Nick Koston f8de6fe0d1 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 12:05:07 -05:00
J. Nick Koston 5137c3f16b Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 12:05:05 -05:00
J. Nick Koston ea3ff0e372 Abort in-flight batch downloads on Ctrl-C, make the deferral test hermetic
cancel_futures only drops queued jobs, and the non-daemon workers are
joined at interpreter exit, so a Ctrl-C during the ESP-IDF toolchain
fetch waited out every in-flight archive. The tracker each job reports
through now checks a cancellation flag set when the wait raises, so
downloads abort at their next progress tick and the resumable .part
files keep the bytes already fetched.

The wave-deferral test stubs _content_lengths instead of issuing real
HEAD requests that passed only by failing, the dead hasattr branch in
the HEAD-probe test is gone, and the .C suffix comment names the SCons
divergence instead of claiming parity.
2026-08-23 12:04:49 -05:00
J. Nick Koston 1157189b36 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 11:36:56 -05:00
J. Nick Koston dd7a25b302 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 11:36:54 -05:00
J. Nick Koston 7c0e03f309 Capture the Append variants, warn on unmodelled env reads, quote every lexed bucket
Prepend/AppendUnique/PrependUnique write the same captured keys and now
alias Append instead of no-op'ing away a link directory. env.get on an
unmodelled var warns once so a script taking the default branch is
diagnosable. LIBS/LINKFLAGS/CPPFLAGS entries are shlex-quoted like the
path buckets, since each is one argv token in SCons and re-lexing must
not split a spaced value. The HEAD probes follow the happy-eyeballs
convention, and the tool-prefetch backstop keeps its traceback at debug.
2026-08-23 11:36:36 -05:00
J. Nick Koston 4e172a5774 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 10:48:53 -05:00
J. Nick Koston e4a54b27ed Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 10:48:53 -05:00
J. Nick Koston 7b3330d5fe Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-library-converter 2026-08-23 10:48:53 -05:00
J. Nick Koston e681c441d9 Close the residual idedata and size-summary silent-failure findings
A list-of-non-dicts compile DB now raises by name inside the best-effort
tuple. The launcher rejection moved into parse_entry itself so no caller
can record ccache as the compiler, and the shape key includes the entry's
directory since relative includes resolve against it. format_bar raises
on a non-positive total so every consumer must handle the meaningless
bar; the espidf summary validates the RAM region by name (a malformed
region can no longer suppress a computable Flash line) and the blanket
backstop logs the traceback at debug. A new test pins that the CI
trigger paths exist on disk, so a rename fails loudly.
2026-08-23 10:48:39 -05:00
J. Nick Koston 8cbb83a06d Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 10:37:16 -05:00
J. Nick Koston bac6530203 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 10:37:14 -05:00
J. Nick Koston 355d4788b8 Assert lexed tokens, not raw flag strings
shlex.quote wraps backslash paths on Windows, so the raw emitted strings
are platform-varying; lexing like the consumer does pins the behavior
that matters and passes on both separators.
2026-08-23 10:37:11 -05:00
J. Nick Koston b8dac2dc89 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 10:33:02 -05:00
J. Nick Koston 585d818e7a Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 10:33:02 -05:00
J. Nick Koston 80b492f638 Quote emitted -I/-L paths so spaced directories survive re-lexing
lex_build_flags shlex-splits build.flags, so an unquoted spaced path
became two garbage tokens; shlex.quote at the emit site round-trips it
back to one.
2026-08-23 10:31:59 -05:00
J. Nick Koston 692bb2a810 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-23 10:16:07 -05:00
J. Nick Koston 4a874a374a Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-23 10:16:06 -05:00
J. Nick Koston 54cd17591d Type-check extraScript, capture CPPPATH, surface version-less dependency drops
A non-string extraScript now fails naming the library instead of an opaque
TypeError, matching the hardening on every other manifest field. CPPPATH
joins the captured keys and translates to -I flags anchored like LIBPATH,
since the pipeline already routes -I to include dirs. Version-less
dependency drops log at INFO until the arduino-backend reconciliation
lands, and the wave-deferral comment now claims only what the guard
actually saves.
2026-08-23 10:15:52 -05:00
J. Nick Koston 98b595c95d Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-library-converter 2026-08-23 10:13:22 -05:00
J. Nick Koston e16cfded5d Merge remote-tracking branch 'origin/dev' into esp8266-native-idedata-helpers 2026-08-23 10:12:45 -05:00
J. Nick Koston 746b294ed7 Harden idedata against non-list compile DBs and stale caches, blanket the size summary
A compile DB that parses as valid JSON but is not a list now raises
EsphomeError inside IDEDATA_BEST_EFFORT_ERRORS instead of a TypeError that
fails an already-linked build. Cached idedata is validated on read against
the write path's guarantees (dict shape, no launcher as compiler, include
union present) so caches from older versions regenerate instead of serving
degraded data, and the served copy is re-stamped with the current ELF path.
Response-file shape sentinels key on the output object rather than the
source file, so two objects built from one source keep both include sets.

The size summary mirrors the standalone hardening PR: a blank partition
size cell raises by name, and a blanket guard keeps unexpected nested
shapes from raising past a build that already linked.
2026-08-23 10:12:40 -05:00
J. Nick Koston ec6123e0ef Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 23:41:57 -05:00
J. Nick Koston da3c6dec7f Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 23:41:56 -05:00
J. Nick Koston cfac36c647 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-library-converter 2026-08-22 23:41:56 -05:00
J. Nick Koston d179f43e7c Correct the zero-partition comment: the skip fails CI extraction loudly, it does not feed it 2026-08-22 23:41:55 -05:00
J. Nick Koston 327630523c Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 23:23:40 -05:00
J. Nick Koston 0e4f4237fd Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 23:23:40 -05:00
J. Nick Koston 96a74cd217 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-library-converter 2026-08-22 23:23:40 -05:00
J. Nick Koston dc64c93f39 [core] Register the OTA component in dummy_main like its siblings 2026-08-22 23:23:39 -05:00
J. Nick Koston 1b109518f7 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 23:21:06 -05:00
J. Nick Koston 5793a5f08a Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 23:21:06 -05:00
J. Nick Koston 9d0d1b42d5 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-library-converter 2026-08-22 23:21:06 -05:00
J. Nick Koston 8230c16f12 Order the launcher strip before the empty guard, never dedupe rsp commands, harden the size summary 2026-08-22 23:21:04 -05:00
J. Nick Koston 905c806009 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 22:58:47 -05:00
J. Nick Koston 42f59f94c9 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 22:58:47 -05:00
J. Nick Koston 35bb8388a2 Defer superseded wave downloads, dedupe tool dests, validate captured buckets, right-size the log levels 2026-08-22 22:58:44 -05:00
J. Nick Koston 6fda5b9a07 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 22:39:48 -05:00
J. Nick Koston 5ac6e2b3c6 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 22:39:47 -05:00
J. Nick Koston b08026e413 Trigger the IDF infra job on the extraScript shim now that it exists 2026-08-22 22:39:34 -05:00
J. Nick Koston f6c6708f24 Merge branch 'esp8266-native-idedata-helpers' into esp8266-native-library-converter 2026-08-22 22:39:33 -05:00
J. Nick Koston 48c6948ae8 Guard empty compile commands, refuse a compile DB with no ESPHome TU, share the launcher rejection 2026-08-22 22:39:16 -05:00
J. Nick Koston e714ef4dfc Reconcile BOARDS with the generated table: d1_wroom_02 joins with its shipped flash layout 2026-08-22 22:32:26 -05:00
J. Nick Koston e9466ff738 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 22:05:03 -05:00
J. Nick Koston 4d6300b400 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 22:05:03 -05:00
J. Nick Koston 030e79fa5a Keep the empty-argument flag check next to the lexer that leaves the bare token 2026-08-22 22:04:47 -05:00
J. Nick Koston da9c0bdd14 Merge branch 'esp8266-native-library-converter' into esp8266-native-shared-helpers 2026-08-22 21:59:57 -05:00
J. Nick Koston a1e6370e8b [core] Harden the shared library converter and download registry archives in parallel 2026-08-22 21:59:19 -05:00
J. Nick Koston dada0f2c2b [core] Extract the shared idedata and size-summary helpers into build_helpers 2026-08-22 21:58:41 -05:00
J. Nick Koston a22c2a9768 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 21:41:27 -05:00
J. Nick Koston 18206967a1 Validate captured CPPDEFINES, warn on empty idedata includes, name the idedata consumers 2026-08-22 21:40:57 -05:00
J. Nick Koston 791e2b8204 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 21:27:39 -05:00
J. Nick Koston d9e18ee982 Support subscript env reads in extra scripts; make the INFO-capture fixture real 2026-08-22 21:27:31 -05:00
J. Nick Koston d26d3fdc33 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 21:24:06 -05:00
J. Nick Koston 227f5c67e2 Merge remote-tracking branch 'origin/dev' into esp8266-native-shared-helpers
# Conflicts:
#	tests/unit_tests/test_main.py
2026-08-22 21:23:40 -05:00
J. Nick Koston 044d593f09 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 21:12:36 -05:00
J. Nick Koston d6d80ee323 Consolidate the batch-download scaffold, share the idedata error set, dedupe compdb tokenizing 2026-08-22 21:12:20 -05:00
J. Nick Koston 149ca8c954 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 20:01:31 -05:00
J. Nick Koston 2acba9b364 Materialize the tracker mock before the prefetch threads race its creation 2026-08-22 20:01:13 -05:00
J. Nick Koston 36b0b62a8f Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 19:55:33 -05:00
J. Nick Koston f4bc7dda35 Fault the cache read directly so the unreadable-cache test holds on Windows 2026-08-22 19:55:16 -05:00
J. Nick Koston cbbc0a8fa9 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 19:46:45 -05:00
J. Nick Koston 7d246893be Surface prefetch failures, harden the idedata and build-info cache reads 2026-08-22 19:46:42 -05:00
J. Nick Koston 27aef88077 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 17:10:23 -05:00
J. Nick Koston bff8bea83e Degrade garbage Content-Length probes; never prefetch an unverifiable tool archive 2026-08-22 17:10:19 -05:00
J. Nick Koston bc98ba3b46 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 16:41:52 -05:00
J. Nick Koston 49d060fdfd Correct the _content_lengths contract docs and pin the unknown-size fallback 2026-08-22 16:41:40 -05:00
J. Nick Koston 2acde317d2 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 16:10:08 -05:00
J. Nick Koston f2ff1cc986 Keep per-file bars when sizes are unknown; harden the prefetch probes
An unknown Content-Length (probe failure, non-2xx, or a CDN omitting it
on HEAD) now skips the parallel prefetch so the sequential downloads
keep their per-file bars, instead of a silent multi-MB batch; the HEAD
helper returns None for unknown, distinct from a genuine zero, and
catches only RequestException. The cache probe logs its failure at
debug. The espidf tool prefetch reads sizes with .get so a missing size
only drops the bar instead of aborting the resumable prefetch. The
idedata warning logs its traceback at debug for parser regressions, and
framework_helpers joins the ESP-IDF infra CI triggers it is imported
by.
2026-08-22 16:10:03 -05:00
J. Nick Koston 4e6a8e297e Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 15:51:41 -05:00
J. Nick Koston 86ac8f661b Fold the dependency-entry validation into one helper
The name and version checks were duplicated across the dict-shorthand
and list spellings with four identical warnings; _valid_dependency_entry
carries them once.
2026-08-22 15:51:38 -05:00
J. Nick Koston 689861ffc9 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 15:46:47 -05:00
J. Nick Koston eec01ea793 Diagnosable prefetch failures, Ctrl-C-safe pool, typed dependency versions
The library prefetch pool now cancels queued futures on interrupt like
the espidf pool it mirrors, prefetch and HEAD failures log their cause
at debug (a suppressed bar is traceable), and non-2xx HEAD statuses read
as unknown size. normalize_dependencies rejects a non-string version in
both spellings (a container would raise from set.add(); an int fails
opaquely in the registry), and the walk's version-less skip logs the
dependency at debug until the arduino-backend reconciliation lands.
2026-08-22 15:46:38 -05:00
J. Nick Koston b3f058613c Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 15:33:40 -05:00
J. Nick Koston 1eda5a8534 Skip already-cached archives in the download prefetch
The wave logged 'Downloading N libraries' before checking the
extraction markers, so a warm build announced downloads it never made.
URLSource gains an is_cached probe (best-effort in the prefetch) and a
fully warm wave stays silent.
2026-08-22 15:33:37 -05:00
J. Nick Koston 520bf3f63b Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 15:20:42 -05:00
J. Nick Koston 1cf5df3d59 Merge the espidf parallel-prefetch branch and give library downloads its combined bar
Brings in #18513 (BatchDownloadProgress and the download_with_resume
progress callback) and extends the plumbing one level:
download_from_mirrors passes a progress callback to both target kinds,
Source/ConvertedLibrary.download thread it through, and the library
wave prefetch draws one combined bar sized by parallel HEAD requests
(no sizes, no bar). The thread-local bar suppression this replaces is
removed; a batch caller passing a tracker also owns the messaging, so
the per-file Downloading line only prints for solo downloads.
2026-08-22 15:19:58 -05:00
J. Nick Koston 91836a8c69 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 15:03:20 -05:00
J. Nick Koston f4482275cb Pin sortedness on a nested fixture in the comment-order test 2026-08-22 15:03:07 -05:00
J. Nick Koston f599e37375 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:54:57 -05:00
J. Nick Koston fd7e340e81 Type-hint the to_code test stub 2026-08-22 14:54:39 -05:00
J. Nick Koston 84a8ac3772 Use a docstring instead of pass in the to_code test stub 2026-08-22 14:54:24 -05:00
J. Nick Koston 910147139a Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:52:39 -05:00
J. Nick Koston 4a459e3d0d Lift the remaining function-local imports in test_main to top level 2026-08-22 14:52:38 -05:00
J. Nick Koston 35a1136bea Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:49:41 -05:00
J. Nick Koston 3b091676bf Drop the unreachable nameless-dependency check in the walk
normalize_dependencies guarantees every entry carries a non-empty
string name, so only the version can be absent.
2026-08-22 14:49:39 -05:00
J. Nick Koston d7af5f37b6 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:44:17 -05:00
J. Nick Koston 121795fa8d Trim comment and docstring essays 2026-08-22 14:44:12 -05:00
J. Nick Koston a2267eb686 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:41:33 -05:00
J. Nick Koston dbd6f323c0 Extract the build-info staleness check so its branches are testable
copy_src_tree is always mocked in the writer tests, so the new
missing/stale/unreadable debug branches were unreachable; the decision
moves into _build_info_stale with a test covering every branch.
2026-08-22 14:41:30 -05:00
J. Nick Koston 3a9a6edc0e Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:37:46 -05:00
J. Nick Koston 11875ec040 Dump the main.cpp config comment with sorted keys
voluptuous fills schema defaults in set-iteration order, so the
validated dict's key order changes with the process hash seed; the
unsorted dump churned the comment block in main.cpp and relinked the
firmware on every esphome run for any config using a defaults-heavy
action (logger.log in a button's on_press was enough). Affects the
PlatformIO path identically.
2026-08-22 14:37:42 -05:00
J. Nick Koston a97b836012 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:31:38 -05:00
J. Nick Koston afeb1a5ea1 Name the changed source when the writer flags a rebuild
Every sources_changed site now logs which file (or which build-info
staleness) triggered it, so a config that relinks on every run can be
diagnosed from one -v compile instead of diffing the build tree.
2026-08-22 14:31:36 -05:00
J. Nick Koston 803eef7de6 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 14:11:02 -05:00
J. Nick Koston fb06b4d631 Download a wave of registry libraries in parallel
The walk now drains its frontier, prefetches the wave's registry
archives with a small thread pool (deduped by URL, progress bars
suppressed per thread since parallel bars would interleave), and then
processes the wave sequentially; the sequential download() call stays
authoritative so failures surface with resume exactly as before. Same
approach as the espidf tool prefetch in #18513.
2026-08-22 14:10:40 -05:00
J. Nick Koston a6ca775a48 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 12:46:29 -05:00
J. Nick Koston 65609bb94b Move the backend-agnostic library hardening down from the arduino backend PR
The typed IncompatiblePlatform exception, dependency_is_usable,
warn_properties_depends, the lex_build_flags helper (espidf switches to
it), the normalize_dependencies validation, the manifest shape check,
the component-level drop-warning split, and the SCons case-sensitive
.C/.C++ suffixes all harden the shared converter independently of the
arduino backend, so they belong in this PR; the provides hook and the
version-less reconciliation stay with the backend that needs them.
2026-08-22 12:46:16 -05:00
J. Nick Koston 60cf44de3b Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 12:15:38 -05:00
J. Nick Koston f71867ed2d Merge remote-tracking branch 'origin/dev' into esp8266-native-shared-helpers 2026-08-22 12:15:30 -05:00
J. Nick Koston f08e06f574 Trim comment essays and hoist function-local test imports 2026-08-22 11:35:35 -05:00
J. Nick Koston 27f4db8458 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-22 11:34:57 -05:00
J. Nick Koston e99e0cc8b8 Trim comment essays and hoist function-local test imports 2026-08-22 11:34:32 -05:00
J. Nick Koston 4d63564cdd Phrase the board-table regen comment imperatively 2026-08-21 20:56:27 -05:00
J. Nick Koston d47dc0d992 Pin fingerprint stability and sensitivity behaviorally, note the platform pairing 2026-08-20 22:08:41 -05:00
J. Nick Koston 9624abcd57 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 21:09:42 -05:00
J. Nick Koston 2c229debe8 Best-effort bad encodings, dedupe append warnings, tolerate structurally odd compile DBs 2026-08-20 21:09:38 -05:00
J. Nick Koston 6a0ce046fd Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 20:53:31 -05:00
J. Nick Koston 6b6344b454 Cover the healthy post-compile idedata path 2026-08-20 20:53:30 -05:00
J. Nick Koston 09fd3fb53b Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 20:48:32 -05:00
J. Nick Koston 73ab04330e Treat sys.exit(0) as script success, fail on an unreadable declared script 2026-08-20 20:48:30 -05:00
J. Nick Koston b8bb4453e0 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 20:46:30 -05:00
J. Nick Koston 91eed262c8 Repr zero-arg script exceptions, surface a None idedata after an IDF build 2026-08-20 20:46:15 -05:00
J. Nick Koston 3063b9067a State what the fingerprint test actually pins 2026-08-20 20:34:24 -05:00
J. Nick Koston 62c27826ee Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 19:51:27 -05:00
J. Nick Koston c95e0b90f5 Raise on a traversing extraScript, survive sys.exit in vendored scripts, fix the caveats wording 2026-08-20 19:51:26 -05:00
J. Nick Koston 84a3c39562 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 19:29:28 -05:00
J. Nick Koston 5cf2135038 Cover the warn-once dedup branch 2026-08-20 19:29:27 -05:00
J. Nick Koston 7ad97697b2 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 19:25:45 -05:00
J. Nick Koston 5bd328e6c4 Fail on a declared-but-absent extraScript, warn once per unsupported env method, split and widen tests 2026-08-20 19:25:33 -05:00
J. Nick Koston 5cec72020f Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 19:02:46 -05:00
J. Nick Koston ef2f16713a Widen the post-compile idedata guard to every failure the comment promises 2026-08-20 19:02:35 -05:00
J. Nick Koston 12f8b14418 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 18:40:46 -05:00
J. Nick Koston 790c6d6353 Drop the never-hitting parse memo, guard the post-compile idedata refresh, pin the kind map 2026-08-20 18:40:29 -05:00
J. Nick Koston 77fa411f04 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 18:18:07 -05:00
J. Nick Koston bc6b997350 Restore the lazy shlex import, pin the probe convention, assert the shim diagnostics 2026-08-20 18:17:56 -05:00
J. Nick Koston e20c43520b Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 17:52:57 -05:00
J. Nick Koston 9b12934307 Reject a launcher compile DB by name before the probe, discard partial script captures, log dropped env keys 2026-08-20 17:52:54 -05:00
J. Nick Koston f93d46e684 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 17:29:59 -05:00
J. Nick Koston d33a097152 Name every skipped size line, best-effort script compile, unusable idedata returns None 2026-08-20 17:29:47 -05:00
J. Nick Koston 8008eda57f Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 17:12:31 -05:00
J. Nick Koston 466f890e1f Clean errors for non-string flags, visible size-summary skips, partial extra-script capture 2026-08-20 17:12:19 -05:00
J. Nick Koston 57b7f03e42 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 16:54:39 -05:00
J. Nick Koston 4ab2a7ba98 Join flag args per entry like ParseFlags, pin the launcher strip log, record the asm merge decision 2026-08-20 16:54:28 -05:00
J. Nick Koston 65f5c302c5 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 16:30:26 -05:00
J. Nick Koston a0f0ad8729 Restore the underscore in test_parse_library_json 2026-08-20 16:30:13 -05:00
J. Nick Koston 51af18cdfb Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 16:25:48 -05:00
J. Nick Koston 57a9c71da8 Cover the malformed build.flags rejection 2026-08-20 16:25:47 -05:00
J. Nick Koston 3d8a3c6527 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 16:24:32 -05:00
J. Nick Koston 1483b7c10b Detect launchers by a closed denylist instead of guessing compiler names 2026-08-20 16:24:21 -05:00
J. Nick Koston 37de984df5 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 16:13:20 -05:00
J. Nick Koston 7fe0847e5c Update espidf test to the public parse_library_json name 2026-08-20 16:13:19 -05:00
J. Nick Koston 80815f1dc7 Make parse_library_json public alongside its properties sibling 2026-08-20 16:12:52 -05:00
J. Nick Koston 8f5f349e1b Pin the decoy anchor, guard unselected segments, fingerprint the module source 2026-08-20 16:01:06 -05:00
J. Nick Koston 83b4da1777 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 15:59:10 -05:00
J. Nick Koston 12c1b15578 Reject malformed build.flags by name and pin the idedata cache-hit path 2026-08-20 15:59:03 -05:00
J. Nick Koston 6a84d67786 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 15:55:01 -05:00
J. Nick Koston 7a3f700206 Act on bad-compiler detection, atomic logged idedata cache, memoized entry parsing, callable board_mcu 2026-08-20 15:41:39 -05:00
J. Nick Koston 7d9c18a330 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 15:26:38 -05:00
J. Nick Koston 5a4337c240 Share the size line format, derive source extensions from a kind map, close_fds on the ccache probe 2026-08-20 15:21:31 -05:00
J. Nick Koston 375cf95240 Anchor the segment regex, reject unknown segments, and pin real-script fixtures 2026-08-20 14:38:25 -05:00
J. Nick Koston 18e3eac419 Export a surgery fingerprint for linker-script cache stamps 2026-08-20 14:14:32 -05:00
J. Nick Koston 0620ce81c3 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 13:57:51 -05:00
J. Nick Koston 2564ad0deb Warn once per non-compiler path, accept versioned compiler names, and pin the trigger table 2026-08-20 13:57:30 -05:00
J. Nick Koston e3936f500f Patch exactly the named segments and share the segment-line pattern 2026-08-20 13:44:12 -05:00
J. Nick Koston cea86989c9 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 13:42:37 -05:00
J. Nick Koston e82453e80e Complete the unit-test package chain so test modules import fully qualified 2026-08-20 13:42:15 -05:00
J. Nick Koston 3eec5085da Give the format_bar tests a unique basename 2026-08-20 13:39:46 -05:00
J. Nick Koston 57c8648210 Drop the test package init that collides with script/build_helpers.py on the CI path 2026-08-20 13:38:54 -05:00
J. Nick Koston 46fa42fa33 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-build-surgery 2026-08-20 13:35:42 -05:00
J. Nick Koston 9bd548ba92 Move the extraScript machinery into the platformio package and tidy the extraction 2026-08-20 13:35:07 -05:00
J. Nick Koston 72da2b1ce2 Add the ESP8266 linker-script surgery and board build metadata 2026-08-20 12:53:50 -05:00
J. Nick Koston ab58f1080a Cover the tidy-project idedata assembly and hoist its helper import 2026-08-20 12:50:56 -05:00
J. Nick Koston 47942fb166 Note the upstream spelling in the format_bar citation 2026-08-20 12:47:54 -05:00
J. Nick Koston f326c3f04d Move the shared helpers into a neutral build_helpers package 2026-08-20 12:43:34 -05:00
J. Nick Koston 3b51ec2ae7 Extract the shared native-build helpers from the ESP-IDF backend 2026-08-20 12:36:42 -05:00
J. Nick Koston 28384673f6 Merge branch 'dev' into espidf_parallel_tool_prefetch 2026-08-19 16:32:48 -05:00
J. Nick Koston b361342bde Do not end a progress bar that never drew a frame 2026-08-19 15:13:04 -05:00
J. Nick Koston d34eed6d2d Merge branch 'dev' into espidf_parallel_tool_prefetch 2026-08-19 14:41:44 -05:00
J. Nick Koston 12238eec3b Log prefetch failures after the bar, assert on the shared progress instance 2026-08-19 13:57:15 -05:00
J. Nick Koston fe53ff6b6b Finish the batch bar, cancel queued archives on Ctrl-C, report known sizes 2026-08-19 13:28:42 -05:00
J. Nick Koston d0f38d7644 [espidf] Download tool archives in parallel during the install prefetch 2026-08-19 12:03:39 -05:00
229 changed files with 10363 additions and 17192 deletions
-16
View File
@@ -1,16 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "d=\"${CLAUDE_PROJECT_DIR:-.}\"; [ -x \"$d/venv/bin/python\" ] || { mkdir -p \"$d/.temp\"; env -u VIRTUAL_ENV \"$d/script/setup\" >\"$d/.temp/setup.log\" 2>&1 || echo '{\"systemMessage\":\"script/setup failed; see .temp/setup.log\"}'; }",
"statusMessage": "Setting up dev environment (script/setup)...",
"timeout": 900
}
]
}
]
}
}
+9 -13
View File
@@ -119,22 +119,16 @@ jobs:
# pushed image) keeps it working for fork PRs, which never push to ghcr.io.
- name: Export image for compile-test
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
# zstd over gzip: docker save is on the critical path for every
# compile-test job, and zstd -T0 is multithreaded (export 50s -> 9s).
# docker load auto-detects the format; its time is layer extraction,
# not decompression, so it is unchanged. shell: bash adds pipefail so
# a failed docker save cannot upload a truncated artifact.
shell: bash
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | zstd -T0 -3 > compile-test-image.tar.zst
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
# (the `name` input is ignored in that mode).
path: compile-test-image.tar.zst
# The tar is already gzipped, so upload it as-is. archive: false skips
# the redundant zip and makes the file name the artifact name (the
# `name` input is ignored in that mode).
path: compile-test-image.tar.gz
retention-days: 1
archive: false
@@ -188,6 +182,8 @@ jobs:
contents: read # actions/checkout to load the test configs
strategy:
fail-fast: false
# Modest cap so this smoke test leaves room on the shared runner pool.
max-parallel: 8
matrix:
# One entry per distinct toolchain. ESP32 variants (c3/c6/s2/s3/p4)
# share a toolchain bundle, so esp32 is exercised on the base variant
@@ -212,9 +208,9 @@ jobs:
- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: compile-test-image.tar.zst
name: compile-test-image.tar.gz
- name: Load image
run: docker load --input compile-test-image.tar.zst
run: docker load --input compile-test-image.tar.gz
- name: Compile ${{ matrix.id }}
run: |
docker run --rm \
+2 -1
View File
@@ -536,7 +536,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
with:
run: |
. venv/bin/activate
@@ -946,6 +946,7 @@ jobs:
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
strategy:
fail-fast: false
max-parallel: ${{ needs.determine-jobs.outputs.release-pr == 'true' && 32 || 16 }}
matrix:
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps:
-1
View File
@@ -577,7 +577,6 @@ esphome/components/tuya/select/* @bearpawmaxim
esphome/components/tuya/sensor/* @jesserockz
esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/tuya/water_heater/* @iago-veiga
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.0
RUN \
platformio settings set enable_telemetry No \
+8 -9
View File
@@ -2734,14 +2734,10 @@ def run_esphome(argv):
# Skipped when -s overrides are passed, since the cache was written
# against the previous substitution set.
config: ConfigType | None = None
cache_write_eligible = (
cache_eligible = (
args.command in ("upload", "logs") and not command_line_substitutions
)
# An explicit --toolchain must re-run the per-platform validators, so
# gate only the cache read; the refresh below saves the result unless
# the sidecar records a different toolchain.
cache_read_eligible = cache_write_eligible and args.toolchain is None
if cache_read_eligible:
if cache_eligible:
from esphome.compiled_config import load_compiled_config
config = load_compiled_config(conf_path)
@@ -2765,14 +2761,17 @@ def run_esphome(argv):
return 2
CORE.config = config
# The cache fast path skips validation, and legacy sidecars lack the
# toolchain field. Must run before the cache refresh below.
# Fallback for platforms whose validators didn't set the toolchain
# (only the esp32 component reads esp32.framework.toolchain). All
# other platforms only support PlatformIO today. Must run before the
# cache refresh below so its sidecar records the same toolchain a
# compile would.
if CORE.toolchain is None:
CORE.toolchain = Toolchain.PLATFORMIO
# Refresh the cache so the next upload/logs hits the fast path
# instead of re-running read_config.
if cache_write_eligible and cache_missed:
if cache_eligible and cache_missed:
from esphome.compiled_config import save_compiled_config_and_sidecar
save_compiled_config_and_sidecar(config)
-9
View File
@@ -1,9 +0,0 @@
"""Native (PlatformIO-free) build support for the ESP8266 Arduino core.
This package downloads the Arduino ESP8266 core and the xtensa-lx106
toolchain, generates a ninja build for them plus the ESPHome sources, and
drives the build directly — the ESP8266 equivalent of ``esphome.espidf``.
Deliberately importable without the esp8266 component to avoid circular
imports; the component wires these modules in via lazy imports.
"""
-164
View File
@@ -1,164 +0,0 @@
"""Download and install the Arduino ESP8266 core, toolchain, and ninja.
Artifacts land in a machine-global cache (shared across projects, like the
ESP-IDF install in ``esphome.espidf.framework``):
<cache>/arduino8266/frameworks/<version>/ framework-arduinoespressif8266
<cache>/arduino8266/toolchains/<version>/ toolchain-xtensa (gcc 10.3)
Packages come from the PlatformIO registry (identical bits to the PlatformIO
backend); ``ESPHOME_ARDUINO8266_*_MIRRORS`` overrides the URLs. ninja comes
from PATH or the ninja PyPI wheel.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import NamedTuple
from esphome.build_helpers.ccache import ccache_defaults_env
from esphome.build_helpers.ninja import find_ninja
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
from esphome.core import EsphomeError, Version
from esphome.framework_helpers import str_to_lst_of_str
from esphome.platformio.registry import install_package, prefetch_packages
FRAMEWORK_PACKAGE = "framework-arduinoespressif8266"
TOOLCHAIN_PACKAGE = "toolchain-xtensa"
# gcc 10.3, the toolchain Arduino core 3.x builds with; the build
# generator's compile flags are tuned to it.
TOOLCHAIN_VERSION = "2.100300.220621"
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS = str_to_lst_of_str(
os.environ.get("ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS", "")
)
ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
os.environ.get("ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS", "")
)
def get_arduino8266_tools_path() -> Path:
# Machine-global so all projects share one install; see
# espidf.framework.get_idf_tools_path for the location rationale.
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
def framework_package_version(ver: Version) -> str:
"""Map an Arduino core version to its registry package version (3.1.2 ->
3.30102.0; the leading 3 is the package major).
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
at MIN_FRAMEWORK_VERSION.
"""
if ver.major > 3:
raise EsphomeError(
f"Arduino core {ver} is not supported yet; "
"the newest known core series is 3.x"
)
if ver <= Version(2, 6, 2):
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
# boundary as _format_framework_arduino_version's era guard)
raise EsphomeError(
f"Arduino core {ver} uses an older package encoding than this "
"helper implements (newer than 2.6.2)"
)
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
def get_framework_path(package_version: str) -> Path:
return get_arduino8266_tools_path() / "frameworks" / package_version
def get_toolchain_path() -> Path:
return get_arduino8266_tools_path() / "toolchains" / TOOLCHAIN_VERSION
class InstalledPaths(NamedTuple):
"""Locations of the installed framework, toolchain, and ninja binary."""
framework: Path
toolchain: Path
ninja: Path
def check_and_install(framework_version: Version) -> InstalledPaths:
"""Ensure framework, toolchain, and ninja are installed; return their paths."""
if framework_version < MIN_FRAMEWORK_VERSION:
# Config validation enforces this too; keep the module honest when
# called directly.
raise EsphomeError(
f"The native toolchain requires the Arduino core "
f">= {MIN_FRAMEWORK_VERSION}, got {framework_version}"
)
# Probe the cheap local dependency before ~110 MB of downloads
ninja_path = find_ninja()
package_version = framework_package_version(framework_version)
framework_path = get_framework_path(package_version)
downloads_dir = get_arduino8266_tools_path() / "downloads"
toolchain_path = get_toolchain_path()
# One spec per package: the prefetch and the installs must agree
specs = (
(
FRAMEWORK_PACKAGE,
package_version,
framework_path,
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
("cores/esp8266", "tools/sdk", "libraries"),
),
(
TOOLCHAIN_PACKAGE,
TOOLCHAIN_VERSION,
toolchain_path,
ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS,
# xtensa-lx106-elf pins the target: every gcc package has a bin/
("bin", "xtensa-lx106-elf"),
),
)
# Fetch both archives at once; the installs below verify and extract
prefetch_packages([spec[:4] for spec in specs], downloads_dir)
for name, version, dest, mirrors, expect in specs:
install_package(name, version, dest, mirrors, downloads_dir, expect=expect)
return InstalledPaths(
framework=framework_path, toolchain=toolchain_path, ninja=ninja_path
)
def toolchain_tool(toolchain_path: Path, name: str) -> Path:
"""Path to one toolchain tool (gcc, g++, ar, size, addr2line, ...).
The single owner of the ``bin/xtensa-lx106-elf-<name>`` layout and the
Windows suffix, so a toolchain package bump touches one spot.
"""
suffix = ".exe" if os.name == "nt" else ""
return toolchain_path / "bin" / f"xtensa-lx106-elf-{name}{suffix}"
def get_build_env(toolchain_path: Path, ccache: str | None) -> dict[str, str]:
env = os.environ.copy()
# Drop empty entries: a trailing separator from an absent PATH would
# make the shell search the current directory for tools
parts = [
str(toolchain_path / "bin"),
*filter(None, env.get("PATH", "").split(os.pathsep)),
]
env["PATH"] = os.pathsep.join(parts)
env.update(ccache_env(ccache))
return env
def ccache_env(ccache: str | None) -> dict[str, str]:
"""Return ccache settings for the build subprocess (not os.environ).
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None
when disabled. Values the user already set in the environment are
respected.
"""
if ccache is None:
return {}
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
+5 -8
View File
@@ -90,10 +90,9 @@ def get_project_cmakelists(
"""
idf_target = variant_to_idf_target(get_esp32_variant())
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
# total_size, hence the ELF fallback in espidf/size_summary.py; both
# go away together when 1.x support is dropped.
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
# --format=raw because the legacy mode doesn't support it.
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
# Project-wide compile options: -D defines and -W warning flags (skip
@@ -212,12 +211,10 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
# Emit per-memory-type JSON size data for ESPHome to read post-build.
# json2 stays small; raw dumps every symbol (~2s on a large map) and
# this command runs inside the link edge, blocking everything downstream.
# Emit raw JSON size data for ESPHome to read post-build.
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
-92
View File
@@ -1,92 +0,0 @@
"""Shared ccache policy for build backends: env-knob parsing, binary
resolution, and default ``CCACHE_*`` values."""
from __future__ import annotations
import logging
import os
from pathlib import Path
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
_LOGGER = logging.getLogger(__name__)
def _ccache_runs(ccache: str) -> bool:
"""Return True when the ``ccache`` found on PATH actually runs."""
return tool_version_runs(
ccache,
"Ignoring ccache at %s because it failed to run; compiling without ccache",
)
def parse_enable_env(name: str) -> bool | None:
"""Strictly parse an on/off environment knob; None when unset or invalid.
``bool(str)`` truthiness would flip ``no``/``off`` to enabled, so only
1/true/yes/on and 0/false/no/off count; anything else warns and reads
as unset so the caller's default policy applies.
"""
raw = os.environ.get(name)
if raw is None:
return None
lowered = raw.strip().lower()
if not lowered:
# ENV KNOB= (Docker/CI) has always read as a disable
return False
if lowered in TRUTHY_ENV_STRINGS:
return True
if lowered in FALSY_ENV_STRINGS:
return False
_LOGGER.warning("Ignoring unrecognized %s=%r; use 1 or 0", name, raw)
return None
def resolve_ccache_path() -> str | None:
"""The ccache binary to wrap compiles with, or None when disabled.
An explicit ``ESPHOME_CCACHE_ENABLE=1`` skips the runnability probe; the
Windows extended-length prefix is stripped before probing (#18399).
"""
import shutil
explicit = parse_enable_env("ESPHOME_CCACHE_ENABLE")
if explicit is False:
return None
ccache = shutil.which("ccache")
if ccache is None:
if explicit:
_LOGGER.warning(
"ESPHOME_CCACHE_ENABLE is set but no ccache binary is on PATH; "
"compiling without ccache"
)
return None
ccache = strip_win_long_path_prefix(ccache)
if not explicit and not _ccache_runs(ccache):
return None
return ccache
def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
"""Default ``CCACHE_*`` values for a build subprocess (not os.environ).
Values the user already set in the environment are respected. Depend
mode is on: both native backends emit depfiles (-MMD / CMake), which
keeps cache-miss overhead low.
"""
from esphome.core import CORE
# An unset build_path means the env was built before preload; fail loudly
# rather than silently drop CCACHE_BASEDIR.
if CORE.build_path is None:
raise ValueError(
"CORE.build_path must be set before constructing the build environment"
)
defaults = {
"CCACHE_DIR": str(cache_dir),
"CCACHE_NOHASHDIR": "true",
"CCACHE_DEPEND": "1",
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
}
return {k: v for k, v in defaults.items() if k not in os.environ}
-92
View File
@@ -1,92 +0,0 @@
"""Platform-neutral helpers for ninja-driven native builds."""
from __future__ import annotations
import logging
import os
from pathlib import Path
import re
import shutil
from esphome.core import EsphomeError
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
_LOGGER = logging.getLogger(__name__)
def _ninja_runs(binary: str) -> bool:
"""Whether the ninja found on PATH actually runs (see tool_version_runs)."""
return tool_version_runs(
binary,
"Ignoring ninja at %s because it failed to run; "
"falling back to the bundled wheel",
)
def find_ninja() -> Path:
"""Locate the ninja binary: a runnable PATH hit first, else the ninja
PyPI wheel."""
if binary := shutil.which("ninja"):
binary = strip_win_long_path_prefix(binary)
if _ninja_runs(binary):
return Path(binary)
import_error: ImportError | None = None
try:
import ninja
except ImportError as err:
import_error = err
wheel_binary = None
else:
wheel_binary = Path(ninja.BIN_DIR) / (
"ninja.exe" if os.name == "nt" else "ninja"
)
if wheel_binary is None or not wheel_binary.is_file():
raise EsphomeError(
"ninja not found on PATH or in the ninja package; reinstall the "
"esphome Python environment"
) from import_error
return wheel_binary
def escape(value: Path | str) -> str:
"""Escape a path or token for a ninja file."""
return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
def quote_arg(tok: str) -> str:
"""Quote with the CreateProcess argv rule (as ``subprocess.list2cmdline``):
backslash runs double only before a quote. Windows-only; ``$`` must
already be doubled for ninja.
"""
quoted = re.sub(r'(\\*)"', lambda m: m.group(1) * 2 + '\\"', tok)
quoted = re.sub(r"(\\+)\Z", lambda m: m.group(1) * 2, quoted)
return f'"{quoted}"'
# Force-quote any token containing a character outside the shlex.quote-style
# safe set: ninja hands POSIX commands to /bin/sh -c, so bare (, ;, <, *, `
# and friends would be re-parsed as shell syntax.
_NEEDS_QUOTE = re.compile(r"[^\w@%+=:,./-]")
def shell_token(tok: str, force: bool = False) -> str:
"""Re-quote a lexed token for the platform shell; ``force`` always quotes.
Single quotes on POSIX (/bin/sh), the argv rule on Windows
(CreateProcess). ``$`` is doubled first because ninja expands it before
the command reaches the shell.
"""
tok = tok.replace("$", "$$") # ninja would expand a bare $ to nothing
if not (force or not tok or _NEEDS_QUOTE.search(tok)):
return tok
# An empty token must become '' / "" or it vanishes from the argv
if os.name == "nt":
return quote_arg(tok)
# shlex.quote's rule; inlined because the $-doubled token must not be
# re-examined for safe characters
return "'" + tok.replace("'", "'\"'\"'") + "'"
def quote_path(value: Path | str) -> str:
"""Force-quote a path for the ninja command line (shell/CreateProcess)."""
return shell_token(str(value), force=True)
-36
View File
@@ -1,36 +0,0 @@
"""Machine-global tools cache location shared by the native backends."""
from __future__ import annotations
from pathlib import Path
def tools_cache_path(env_var: str, subdir: str) -> Path:
"""A backend's machine-global tools directory, with an env override.
A blank/whitespace override is treated as unset: ``Path("")`` resolves
to the CWD, which ``clean-all`` would then delete.
"""
import platformdirs
from esphome.helpers import get_str_env
if prefix := get_str_env(env_var, "").strip():
# resolve(): symlinked prefixes otherwise trip idf.py's
# venv-mismatch warning on every build
return Path(prefix).expanduser().resolve()
# appauthor=False keeps the Windows path short (no vendor segment);
# deep IDF trees run into MAX_PATH otherwise
return (
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / subdir
).resolve()
# (env override, cache subdir) per native backend. writer.clean_all wipes
# every entry via tools_cache_path, so listing a cache here is the single
# step that registers it for removal; the backends' own path getters use
# the same named pairs so the two cannot drift.
IDF_TOOLS_CACHE = ("ESPHOME_ESP_IDF_PREFIX", "idf")
SDK_NRF_TOOLS_CACHE = ("ESPHOME_SDK_NRF_PREFIX", "sdk-nrf")
ARDUINO8266_TOOLS_CACHE = ("ESPHOME_ARDUINO8266_PREFIX", "arduino8266")
TOOLS_CACHE_SPECS = (IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE)
-15
View File
@@ -100,21 +100,6 @@ def _refresh_sidecar() -> bool:
)
return False
if old is not None and old.can_apply_to_core():
if (
old.toolchain is not None
and CORE.toolchain is not None
and old.toolchain != CORE.toolchain.value
):
# Platforms normalize toolchain-sensitive keys differently;
# never cache a config validated under a different toolchain
# than the compile's
_LOGGER.debug(
"Not caching: config validated with toolchain %r but the "
"last compile used %r",
CORE.toolchain.value,
old.toolchain,
)
return False
# Compile-written; nothing to refresh.
return True
if CORE.build_path is not None and CORE.build_path.exists():
+4 -41
View File
@@ -1,5 +1,4 @@
import logging
import re
from typing import Any
from esphome import automation
@@ -500,40 +499,6 @@ async def to_code(config: ConfigType) -> None:
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
_ID_CALL_PROG = re.compile(r"\bid\s*\(")
# Remove before 2027.3.0: untagged strings that look like lambda source keep
# being compiled as lambdas during the deprecation window
def _coerce_implicit_lambda(value: Any) -> Any:
if not isinstance(value, str):
return value
if cv.looks_like_returning_lambda(value):
_LOGGER.warning(
"[api] The 'variables' value '%s' looks like a lambda but is "
"missing the !lambda tag. It is compiled as a lambda for now but "
"will be sent as literal text from 2027.3.0. Add !lambda to keep "
"it evaluated; literal text belongs under 'data:'.",
value,
)
# cv.templatable runs returning_lambda on the coerced Lambda
return cv.lambda_(value)
if _ID_CALL_PROG.search(value):
# lambda source without a return: issue 5394's mistake class
_LOGGER.warning(
"[api] The 'variables' value '%s' is sent as literal text; wrap "
"it in !lambda 'return ...;' to evaluate it instead.",
value,
)
return value
# Static strings or !lambda values. cv.templatable stays introspectable for
# schema tooling; removing the shim leaves KEY_VALUE_SCHEMA.
VARIABLES_SCHEMA = cv.Schema(
{cv.string: cv.All(_coerce_implicit_lambda, cv.templatable(cv.string_strict))}
)
def _validate_response_config(config: ConfigType) -> ConfigType:
# Validate dependencies:
@@ -570,7 +535,9 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
),
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): VARIABLES_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda}
),
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
@@ -631,8 +598,6 @@ async def homeassistant_service_to_code(
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
if on_error := config.get(CONF_ON_ERROR):
@@ -687,7 +652,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
cv.Required(CONF_EVENT): validate_homeassistant_event,
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): VARIABLES_SCHEMA,
cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA,
}
)
@@ -733,8 +698,6 @@ async def homeassistant_event_to_code(
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
return var
+1 -2
View File
@@ -1654,8 +1654,7 @@ message ListEntitiesMediaPlayerResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
// Deprecated in ESPHome 2026.9.0; use feature_flags instead.
bool supports_pause = 8 [deprecated = true];
bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
+2 -9
View File
@@ -1,20 +1,13 @@
#include "api_buffer.h"
#include <new>
namespace esphome::api {
bool APIBuffer::grow_(size_t n) {
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
// (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF).
// RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory.
std::unique_ptr<uint8_t[]> new_data(new (std::nothrow) uint8_t[n]);
if (new_data == nullptr)
return false;
void APIBuffer::grow_(size_t n) {
auto new_data = make_buffer(n);
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
this->capacity_ = n;
return true;
}
} // namespace esphome::api
+21 -11
View File
@@ -9,6 +9,16 @@
namespace esphome::api {
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
return std::make_unique<uint8_t[]>(n);
#else
return std::make_unique_for_overwrite<uint8_t[]>(n);
#endif
}
/// Byte buffer that skips zero-initialization on resize().
///
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
@@ -26,23 +36,23 @@ namespace esphome::api {
class APIBuffer {
public:
void clear() { this->size_ = 0; }
/// Returns false if allocation fails; the buffer is left unchanged.
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
/// Returns false if allocation fails; the buffer is left unchanged. No zero-fill.
[[nodiscard]] inline bool resize(size_t n) ESPHOME_ALWAYS_INLINE { return this->reserve_and_resize(n, n); }
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
if (n > this->capacity_)
this->grow_(n);
}
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
this->reserve(n);
this->size_ = n; // no zero-fill
}
/// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size.
/// Single grow_ check regardless of argument order.
/// Returns false if allocation fails; the buffer is left unchanged.
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
if (!this->reserve(std::max(reserve_size, new_size)))
return false;
inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
this->reserve(std::max(reserve_size, new_size));
this->size_ = new_size;
return true;
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
size_t capacity() const { return this->capacity_; }
bool empty() const { return this->size_ == 0; }
uint8_t &operator[](size_t i) { return this->data_[i]; }
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
@@ -54,7 +64,7 @@ class APIBuffer {
}
protected:
bool grow_(size_t n);
void grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
+12 -38
View File
@@ -1,6 +1,6 @@
#include "api_connection.h"
#ifdef USE_API
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
#include "api_connection_buffer.h" // for encode_to_buffer / get_batch_delay_ms_ inlines
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
@@ -1099,6 +1099,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
ListEntitiesMediaPlayerResponse msg;
auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause();
msg.feature_flags = traits.get_feature_flags();
for (auto &supported_format : traits.get_supported_formats()) {
msg.supported_formats.emplace_back();
@@ -2239,17 +2240,10 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type,
this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
}
#endif
if (!this->prepare_first_message_buffer(payload_size)) [[unlikely]] {
this->fatal_out_of_memory_();
return false;
}
auto &shared_buf = this->parent_->get_shared_buffer_ref();
this->prepare_first_message_buffer(shared_buf, payload_size);
size_t write_start = shared_buf.size();
#ifdef ESPHOME_DEBUG_API
assert(shared_buf.capacity() >= write_start + payload_size);
#endif
// Capacity reserved above, cannot fail
(void) shared_buf.resize(write_start + payload_size);
shared_buf.resize(write_start + payload_size);
ProtoWriteBuffer buffer{&shared_buf, write_start};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
@@ -2285,9 +2279,6 @@ void APIConnection::on_no_setup_connection() {
this->on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::fatal_out_of_memory_() {
this->fatal_error_with_log_(LOG_STR("Out of memory"), APIError::OUT_OF_MEMORY);
}
void APIConnection::on_fatal_error() {
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
@@ -2302,25 +2293,16 @@ bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_t message
bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
uint8_t aux_data_index) {
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// No local for the shared buffer here: keeping it live across
// dispatch_message_ costs a register and spills message_type into the
// batching path's dedup loop (measured on x86 GCC -Os)
if (!this->prepare_first_message_buffer(estimated_size)) [[unlikely]] {
this->fatal_out_of_memory_();
return false;
}
auto &shared_buf = this->parent_->get_shared_buffer_ref();
this->prepare_first_message_buffer(shared_buf, estimated_size);
DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index};
if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_batch_item_(item);
#endif
return true;
}
// An OOM during the immediate attempt marks the connection for removal;
// don't queue more work (schedule_message_'s push_back may allocate again)
if (this->flags_.remove) [[unlikely]]
return false;
}
return this->schedule_message_(entity, message_type, estimated_size, aux_data_index);
}
@@ -2370,11 +2352,7 @@ void APIConnection::process_batch_() {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
if (!this->prepare_first_message_buffer(header_padding, total_estimated_size)) [[unlikely]] {
this->fatal_out_of_memory_();
this->clear_batch_();
return;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
// Fast path for single message - buffer already allocated above
if (num_items == 1) {
@@ -2389,10 +2367,8 @@ void APIConnection::process_batch_() {
#endif
this->clear_batch_();
} else if (payload_size == 0) {
// payload_size == 0 with remove set means encoding hit OOM and the
// connection is being dropped; warn only for a genuinely oversized message
if (!this->flags_.remove)
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
// Message too large to fit in available space
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
this->clear_batch_();
}
return;
@@ -2455,10 +2431,8 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
if (items_processed > 0) {
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0 && !shared_buf.resize(shared_buf.size() + footer_size)) [[unlikely]] {
this->fatal_out_of_memory_();
this->clear_batch_();
return;
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
+14 -8
View File
@@ -352,13 +352,22 @@ class APIConnection final : public APIServerConnectionBase {
}
}
/// Clear the shared write buffer and reserve space for the first message.
/// Returns false if the allocation fails (out of memory).
/// Defined in api_connection_buffer.h (needs APIServer complete).
[[nodiscard]] bool prepare_first_message_buffer(size_t header_padding, size_t total_size);
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
// Reserve full size but only set initial size to header padding
// so message encoding starts at the correct position
shared_buf.reserve_and_resize(total_size, header_padding);
}
// Convenience overload - computes frame overhead internally
[[nodiscard]] bool prepare_first_message_buffer(size_t payload_size);
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size();
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
}
bool try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove)
@@ -844,9 +853,6 @@ class APIConnection final : public APIServerConnectionBase {
this->on_fatal_error();
this->log_warning_(message, err);
}
// Shared cold path for buffer allocation failures — noinline keeps the
// OOM handling out of the hot send paths
void __attribute__((noinline)) fatal_out_of_memory_();
};
} // namespace esphome::api
+3 -23
View File
@@ -3,8 +3,8 @@
#include "esphome/core/defines.h"
#ifdef USE_API
// Inline APIConnection members that need APIServer complete. Include this
// instead of api_connection.h when calling them.
// Inline APIConnection methods that need APIServer complete. Include this
// instead of api_connection.h when calling encode_to_buffer or get_batch_delay_ms_.
#include "api_connection.h"
#include "api_server.h"
@@ -41,10 +41,7 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
return 0;
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
if (!shared_buf.resize(shared_buf.size() + to_add)) [[unlikely]] {
conn->fatal_out_of_memory_();
return 0;
}
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
@@ -53,22 +50,5 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
inline uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
inline bool APIConnection::prepare_first_message_buffer(size_t header_padding, size_t total_size) {
auto &shared_buf = this->parent_->get_shared_buffer_ref();
shared_buf.clear();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
// Reserve full size but only set initial size to header padding
// so message encoding starts at the correct position
return shared_buf.reserve_and_resize(total_size, header_padding);
}
inline bool APIConnection::prepare_first_message_buffer(size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size();
return this->prepare_first_message_buffer(header_padding, payload_size + header_padding + footer_size);
}
} // namespace esphome::api
#endif
+1 -1
View File
@@ -172,7 +172,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
@@ -68,10 +68,7 @@ APIError APINoiseFrameHelper::init() {
// init prologue
size_t old_size = prologue_.size();
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
prologue_.resize(old_size + PROLOGUE_INIT_LEN);
#ifdef USE_ESP8266
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#else
@@ -205,10 +202,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
// During handshake, rx_buf_.size() is used in prologue construction, so
// the buffer must be exactly msg_size to avoid prologue mismatch.)
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
if (!this->rx_buf_.resize(alloc_size)) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
this->rx_buf_.resize(alloc_size);
if (rx_buf_len_ < msg_size) {
// more data to read
@@ -275,10 +269,7 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
size_t rx_size = this->rx_buf_.size();
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
this->prologue_.resize(old_size + 2 + rx_size);
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
if (rx_size > 0) {
@@ -486,15 +477,13 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
assert(this->state_ == State::DATA);
#endif
APIBuffer *buf = buffer.get_buffer();
// Resize buffer to include footer space for Noise MAC
if (this->frame_footer_size_ && !buf->resize(buf->size() + this->frame_footer_size_)) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
if (this->frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
uint16_t payload_size = static_cast<uint16_t>(buf->size() - HEADER_PADDING - this->frame_footer_size_);
uint8_t *buf_start = buf->data();
uint16_t payload_size =
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
uint8_t *buf_start = buffer.get_buffer()->data();
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
if (aerr != APIError::OK)
@@ -172,10 +172,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// Reserve space for body (+ null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (!this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR)) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
+2 -14
View File
@@ -1,7 +1,6 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
#include <new>
namespace esphome::api {
@@ -62,18 +61,9 @@ bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_
return false;
uint16_t buffer_size = total_len - skip;
// nothrow: a failed allocation returns nullptr so the connection is dropped
// cleanly instead of plain new's crash or abort on OOM
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *data = new (std::nothrow) uint8_t[buffer_size];
if (data == nullptr)
return false;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
if (entry == nullptr) {
delete[] data;
return false;
}
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
this->queue_[this->tail_] = entry;
uint16_t to_skip = skip;
uint16_t write_pos = 0;
@@ -90,8 +80,6 @@ bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_
}
}
// Publish only after the copy completes so a half-built entry is never reachable
this->queue_[this->tail_] = entry;
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
+1 -1
View File
@@ -61,7 +61,7 @@ class APIOverflowBuffer {
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full or allocation fails (caller should fail the connection).
/// Returns false if the queue is full (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
protected:
+2
View File
@@ -2323,6 +2323,7 @@ uint8_t *ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer PROTO_
#endif
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category));
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supports_pause);
for (auto &it : this->supported_formats) {
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 9, it);
}
@@ -2342,6 +2343,7 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const {
#endif
size += ProtoSize::calc_bool(1, this->disabled_by_default);
size += this->entity_category ? 2 : 0;
size += ProtoSize::calc_bool(1, this->supports_pause);
if (!this->supported_formats.empty()) {
for (const auto &it : this->supported_formats) {
size += ProtoSize::calc_message_force(1, it.calculate_size());
+2 -1
View File
@@ -1911,10 +1911,11 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 63;
static constexpr uint8_t ESTIMATED_SIZE = 78;
static constexpr uint8_t ESTIMATED_SIZE = 80;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_media_player_response"); }
#endif
bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
uint32_t feature_flags{0};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
+1
View File
@@ -1962,6 +1962,7 @@ const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const {
#endif
dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default);
dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, ESPHOME_PSTR("supports_pause"), this->supports_pause);
for (const auto &it : this->supported_formats) {
out.append(4, ' ').append_p(ESPHOME_PSTR("supported_formats")).append(": ");
it.dump_to(out);
+137 -137
View File
@@ -7,146 +7,146 @@ namespace esphome::captive_portal {
#ifdef USE_CAPTIVE_PORTAL_GZIP
constexpr uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x56, 0x6d, 0x8f, 0xdb, 0x36, 0x0c, 0xfe, 0xbe,
0x5f, 0xa1, 0x79, 0xdd, 0x6a, 0xaf, 0xb1, 0xfc, 0x92, 0x4b, 0xda, 0x3a, 0x96, 0x8b, 0xee, 0xd6, 0x62, 0x03, 0xd6,
0xad, 0xc0, 0xdd, 0xba, 0x0f, 0x45, 0x01, 0x2b, 0x32, 0x1d, 0xab, 0x27, 0x4b, 0x9e, 0xa4, 0xbc, 0x35, 0xc8, 0x7e,
0xfb, 0x20, 0xdb, 0xc9, 0xe5, 0x8a, 0x16, 0xd8, 0x10, 0xc4, 0xa0, 0x44, 0xf2, 0xe1, 0x8b, 0x28, 0x52, 0xf9, 0xb7,
0x95, 0x62, 0x76, 0xdf, 0x01, 0x6a, 0x6c, 0x2b, 0x8a, 0xdc, 0x7d, 0x91, 0xa0, 0x72, 0x45, 0x40, 0x16, 0x79, 0x03,
0xb4, 0x2a, 0xf2, 0x16, 0x2c, 0x45, 0xac, 0xa1, 0xda, 0x80, 0x25, 0x7f, 0xde, 0xbe, 0x0e, 0x9f, 0x15, 0xb9, 0xe0,
0xf2, 0x0e, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0xa8,
0x22, 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0xc4, 0xdb, 0xf2, 0xca, 0x36,
0xa4, 0x82, 0x0d, 0x67, 0x10, 0xf6, 0x8b, 0x09, 0x97, 0xdc, 0x72, 0x2a, 0x42, 0xc3, 0xa8, 0x00, 0x92, 0x4c, 0xd6,
0x06, 0x74, 0xbf, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0xaf, 0xc8, 0x0d, 0xd3, 0xbc, 0xb3, 0xc8, 0xb9, 0x4a, 0x5a, 0x55,
0xad, 0x05, 0x20, 0xa6, 0x95, 0x31, 0x4a, 0xf3, 0x15, 0x97, 0x45, 0xa5, 0xd8, 0xba, 0x05, 0x69, 0xb1, 0x50, 0x8c,
0x5a, 0xae, 0x24, 0x36, 0x40, 0x35, 0x6b, 0x08, 0x21, 0xe5, 0x0b, 0x43, 0x37, 0x50, 0xfe, 0xf0, 0x83, 0x7f, 0x16,
0x5a, 0x81, 0x7d, 0x25, 0xc0, 0x91, 0xe6, 0xa7, 0xfd, 0x2d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x4b, 0x6a, 0x78, 0x05,
0x65, 0xf0, 0x3e, 0xfe, 0x80, 0x8d, 0xdd, 0x0b, 0xc0, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x52, 0x2e, 0x85, 0x62, 0x77,
0x65, 0xb0, 0xa8, 0xd7, 0x92, 0x39, 0x70, 0x04, 0x3e, 0x04, 0x07, 0x01, 0x16, 0x49, 0xf2, 0x86, 0xda, 0x06, 0xb7,
0x74, 0xe7, 0x0f, 0x04, 0x97, 0x7e, 0xfa, 0xa3, 0x0f, 0x4f, 0x92, 0x38, 0x0e, 0x26, 0xfd, 0x27, 0x0e, 0xa2, 0x24,
0x8e, 0x17, 0x1a, 0xec, 0x5a, 0x4b, 0x64, 0xfd, 0x32, 0xef, 0xa8, 0x6d, 0x50, 0x45, 0xbc, 0x37, 0x49, 0x8a, 0x92,
0xe7, 0x38, 0x9d, 0xfd, 0x86, 0x9f, 0xa2, 0x2b, 0x9c, 0xce, 0xd8, 0xd3, 0x70, 0x86, 0x92, 0xab, 0x70, 0x86, 0xd2,
0x14, 0xcf, 0x50, 0xfc, 0xc9, 0x43, 0x35, 0x17, 0x82, 0x78, 0x52, 0x49, 0xf0, 0x90, 0xb1, 0x5a, 0xdd, 0x01, 0xf1,
0xd8, 0x5a, 0x6b, 0x90, 0xf6, 0x5a, 0x09, 0xa5, 0xbd, 0xa8, 0xf8, 0xe6, 0x7f, 0x01, 0x5a, 0x4d, 0xa5, 0xa9, 0x95,
0x6e, 0x89, 0xd7, 0xa7, 0xdb, 0x7f, 0x74, 0x90, 0x47, 0xe4, 0x3e, 0xc1, 0x05, 0x33, 0x1c, 0xf2, 0x4a, 0x3c, 0x87,
0xf8, 0xcc, 0x8b, 0x8a, 0x32, 0x38, 0x9e, 0xa3, 0xb7, 0x2e, 0xfa, 0x31, 0x1e, 0xed, 0xbf, 0x2f, 0x73, 0xb3, 0x59,
0xa1, 0x5d, 0x2b, 0xa4, 0x21, 0x5e, 0x63, 0x6d, 0x97, 0x45, 0xd1, 0x76, 0xbb, 0xc5, 0xdb, 0x29, 0x56, 0x7a, 0x15,
0xa5, 0x71, 0x1c, 0x47, 0x66, 0xb3, 0xf2, 0xd0, 0x70, 0xf2, 0x5e, 0x7a, 0xe5, 0xa1, 0x06, 0xf8, 0xaa, 0xb1, 0x3d,
0x5d, 0x3c, 0x3a, 0xc0, 0x31, 0x77, 0x12, 0x45, 0xf9, 0xe1, 0xc2, 0x8a, 0xbc, 0xb0, 0x02, 0x2f, 0x2e, 0xf2, 0xf6,
0xb8, 0x0f, 0xf3, 0x29, 0x4d, 0x51, 0x8a, 0xe2, 0xfe, 0x97, 0x86, 0x8e, 0x1e, 0x57, 0xe1, 0x67, 0x2b, 0x74, 0xb1,
0x72, 0x54, 0x3b, 0x0f, 0x9f, 0x9f, 0x75, 0x13, 0xb7, 0xb3, 0x49, 0xe2, 0xfb, 0x0d, 0xa7, 0xf0, 0xcb, 0xfc, 0x72,
0x1d, 0xa6, 0xef, 0x2e, 0x05, 0x9c, 0xb5, 0x26, 0x79, 0x37, 0xa7, 0x33, 0x34, 0x1b, 0x77, 0x66, 0xa1, 0xa3, 0xcf,
0x2b, 0x34, 0xdb, 0xa4, 0x4d, 0xd2, 0x86, 0xf3, 0x70, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xda,
0xcc, 0xdf, 0xcd, 0x2f, 0xf7, 0xc2, 0xe9, 0xa7, 0xc7, 0x2e, 0xb9, 0x59, 0x59, 0xde, 0x47, 0xae, 0x2f, 0x23, 0xc7,
0x1f, 0x15, 0x97, 0x7e, 0xe9, 0xf2, 0x0f, 0x96, 0x35, 0x7e, 0x19, 0x31, 0x25, 0x6b, 0xbe, 0xc2, 0x1f, 0x8d, 0x92,
0x65, 0x80, 0x6d, 0x03, 0xd2, 0x3f, 0xa9, 0xfa, 0x36, 0x38, 0xd8, 0x9e, 0xe3, 0x7f, 0x81, 0x73, 0xae, 0x7f, 0xcb,
0xad, 0x00, 0x62, 0xb1, 0xbb, 0xa1, 0x93, 0x2f, 0xdc, 0x8a, 0x9f, 0xf6, 0xbf, 0x56, 0x7e, 0xd9, 0x52, 0x56, 0x06,
0x98, 0x4b, 0x09, 0xfa, 0x16, 0x76, 0x96, 0x94, 0x6f, 0x5e, 0x5e, 0xa3, 0x97, 0x55, 0xa5, 0xc1, 0x98, 0x0c, 0x95,
0x4f, 0x2c, 0x6e, 0x29, 0xfb, 0xba, 0x7a, 0x93, 0x3c, 0xd4, 0xfe, 0x8b, 0xbf, 0xe6, 0xe8, 0x77, 0xb0, 0x5b, 0xa5,
0xef, 0x46, 0x7d, 0x67, 0x7f, 0xe1, 0xae, 0x91, 0x26, 0x5f, 0x85, 0x91, 0x60, 0xcb, 0x60, 0xc2, 0xbf, 0x2e, 0x60,
0x0c, 0xaf, 0xca, 0x60, 0x42, 0xbf, 0x2e, 0xd1, 0x19, 0x77, 0x79, 0x2d, 0xa6, 0x9d, 0xc1, 0x46, 0x70, 0x06, 0x7e,
0x12, 0xe0, 0x5a, 0xe9, 0x57, 0x94, 0x35, 0x0f, 0x12, 0xe4, 0x5c, 0x51, 0xf7, 0x38, 0x4c, 0x03, 0xb5, 0x30, 0x42,
0xf9, 0x65, 0xc5, 0x37, 0x65, 0xb0, 0x50, 0x98, 0x09, 0x6a, 0x8c, 0x6b, 0x19, 0xc4, 0x39, 0xe7, 0xc2, 0x29, 0x27,
0x6a, 0x88, 0xf4, 0x97, 0xdb, 0x37, 0xbf, 0x91, 0x32, 0xa7, 0x43, 0x47, 0xf4, 0xbe, 0xf3, 0x50, 0x2f, 0x4c, 0xbc,
0x51, 0x30, 0x14, 0x50, 0xdb, 0xbe, 0xe2, 0x7d, 0x8b, 0xb5, 0x31, 0x3c, 0x38, 0xe6, 0xa6, 0xa3, 0xf2, 0x73, 0x31,
0x17, 0x93, 0x57, 0xe4, 0x91, 0xe3, 0x15, 0x79, 0x44, 0x8b, 0x47, 0x07, 0xe9, 0xf7, 0xcd, 0xed, 0x2e, 0x38, 0x3a,
0x6b, 0x7f, 0xaf, 0x41, 0xef, 0x6f, 0x40, 0x00, 0xb3, 0x4a, 0xfb, 0x25, 0xbe, 0x54, 0x74, 0x45, 0x01, 0x3b, 0x7b,
0x3d, 0x36, 0x5c, 0x8b, 0xdd, 0xe6, 0x44, 0x61, 0x25, 0x99, 0xe0, 0xec, 0x8e, 0x9c, 0x23, 0x0e, 0x0e, 0x1c, 0x6f,
0xa8, 0x58, 0xc3, 0x49, 0x86, 0xe2, 0x5a, 0xb1, 0xb5, 0xf1, 0x83, 0xe3, 0x44, 0x63, 0xda, 0x75, 0x20, 0xab, 0xeb,
0x86, 0x8b, 0xca, 0x57, 0xc1, 0x31, 0xb8, 0x3f, 0xe9, 0xcf, 0x8c, 0xbb, 0x59, 0xf0, 0x5e, 0x83, 0xf8, 0x87, 0x3c,
0x76, 0xd3, 0xe0, 0xf1, 0x87, 0x32, 0xc0, 0x7d, 0xfc, 0xe5, 0xfd, 0x48, 0x70, 0xd7, 0xfb, 0xc9, 0xae, 0x15, 0x13,
0x17, 0x7a, 0x38, 0x9f, 0x05, 0xc7, 0xf2, 0x18, 0x1c, 0x83, 0x45, 0x1e, 0x0d, 0x8d, 0xbd, 0xc8, 0xfb, 0x96, 0xdb,
0x8f, 0x94, 0x9e, 0x32, 0x0d, 0x80, 0x7d, 0xd0, 0xe2, 0x7f, 0x3c, 0x2c, 0xd5, 0x2e, 0x34, 0xfc, 0x13, 0x97, 0xab,
0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0x26, 0x5c, 0x76, 0x6b, 0x7b, 0xe8, 0x68, 0x55, 0x39, 0xce, 0xac,
0xdb, 0x2d, 0x6a, 0x25, 0xad, 0x93, 0x84, 0x2c, 0x81, 0xf6, 0x38, 0xf0, 0xfb, 0xe6, 0x93, 0x3d, 0x9f, 0x7d, 0x7f,
0x5c, 0xaa, 0x6a, 0x7f, 0x70, 0x19, 0x0a, 0xa9, 0xe0, 0x2b, 0x99, 0x31, 0x90, 0x16, 0xf4, 0xa0, 0x54, 0xd3, 0x96,
0x8b, 0x7d, 0x66, 0xa8, 0x34, 0xa1, 0x01, 0xcd, 0xeb, 0xe3, 0x72, 0x6d, 0xad, 0x92, 0x07, 0xe6, 0x9a, 0x6d, 0xf6,
0x5d, 0x5d, 0xd7, 0x0b, 0xb6, 0xd6, 0x46, 0xe9, 0xac, 0x53, 0xbc, 0xd7, 0x5b, 0x52, 0x76, 0xb7, 0xd2, 0x6a, 0x2d,
0xab, 0x70, 0x14, 0x4a, 0x6a, 0x3a, 0x05, 0xb6, 0x58, 0x2a, 0x5d, 0x81, 0xce, 0xe2, 0x91, 0x08, 0x35, 0xad, 0xf8,
0xda, 0x64, 0x78, 0xaa, 0xa1, 0x5d, 0x0c, 0xee, 0x24, 0x71, 0xfc, 0xfd, 0xe2, 0xe4, 0x79, 0x7c, 0xe9, 0x37, 0x4e,
0x9d, 0x94, 0xe0, 0x12, 0xc2, 0xa1, 0x57, 0x66, 0x29, 0xbe, 0xd2, 0xd0, 0x1e, 0x5b, 0xca, 0xe5, 0xa5, 0xf7, 0xae,
0xa2, 0x16, 0x2d, 0x97, 0xc3, 0x28, 0xcd, 0xd2, 0x79, 0xdc, 0xed, 0x16, 0xe3, 0xe4, 0xca, 0xb8, 0xec, 0x11, 0xfa,
0xf9, 0x75, 0x3c, 0x15, 0xc9, 0xe1, 0xe3, 0xda, 0x58, 0x5e, 0xef, 0xc3, 0x71, 0x24, 0x67, 0xa6, 0xa3, 0x0c, 0xc2,
0x25, 0xd8, 0x2d, 0x80, 0x5c, 0xf4, 0xb0, 0x21, 0xb7, 0xd0, 0x9a, 0x53, 0x6a, 0x4e, 0x70, 0xb5, 0x80, 0xdd, 0x19,
0xa6, 0xaf, 0xe5, 0xc3, 0x7f, 0x96, 0x76, 0x05, 0x76, 0x68, 0xa9, 0x5e, 0x71, 0x19, 0x2e, 0x95, 0xb5, 0xaa, 0xcd,
0xc2, 0xa7, 0xdd, 0x6e, 0x31, 0x6e, 0x39, 0xb0, 0x2c, 0x89, 0xbb, 0xdd, 0xb1, 0x1f, 0xc3, 0xa7, 0x7c, 0x5f, 0xd5,
0xcf, 0x68, 0x12, 0x7f, 0x21, 0xc7, 0x55, 0x5d, 0xa7, 0xcb, 0xfa, 0x94, 0xe3, 0xa4, 0xdb, 0x21, 0xa3, 0x04, 0xaf,
0x46, 0xb8, 0x1e, 0x09, 0xc5, 0xe7, 0xd4, 0x26, 0xb3, 0x6e, 0x87, 0x92, 0xcb, 0xcc, 0xb8, 0x89, 0xea, 0xa6, 0x8e,
0xab, 0xb5, 0x22, 0x8f, 0x86, 0x97, 0x8e, 0xab, 0x8c, 0x22, 0x77, 0x19, 0x2e, 0xf2, 0x26, 0x41, 0xbc, 0x22, 0x2d,
0x65, 0xc5, 0x45, 0xdb, 0xcb, 0xa3, 0x26, 0x39, 0xb1, 0x9a, 0xa4, 0x78, 0xd0, 0xd2, 0x06, 0x5e, 0xef, 0x7d, 0x71,
0xad, 0xa4, 0x04, 0x66, 0xb9, 0x5c, 0x21, 0xab, 0xd0, 0x98, 0x02, 0x8c, 0x71, 0xbe, 0xd4, 0xc5, 0x5b, 0x01, 0xd4,
0x00, 0xda, 0x52, 0x6e, 0x71, 0x1e, 0x0d, 0xf2, 0x43, 0x13, 0xe0, 0x15, 0x91, 0x60, 0xcf, 0xd7, 0xbe, 0x99, 0x0e,
0x06, 0x6e, 0xc0, 0x3a, 0x24, 0x67, 0x60, 0x5a, 0xe4, 0x6e, 0x3a, 0x23, 0xda, 0x5f, 0x60, 0x12, 0x6d, 0x79, 0xcd,
0xdd, 0xeb, 0xa6, 0xc8, 0xfb, 0x22, 0x77, 0x08, 0x2e, 0xcf, 0xc3, 0xd3, 0xab, 0xa7, 0x04, 0xc8, 0x95, 0x6d, 0xc8,
0x34, 0x45, 0x9d, 0xa0, 0x0c, 0x1a, 0x25, 0x2a, 0xd0, 0xe4, 0xe6, 0xe6, 0xd7, 0x9f, 0x0b, 0xe7, 0xcc, 0xbd, 0x5e,
0x67, 0xee, 0x06, 0x35, 0x47, 0x8c, 0x5a, 0xf3, 0xab, 0xe1, 0xc1, 0xd5, 0x51, 0x63, 0xb6, 0x4a, 0x57, 0x0f, 0x30,
0xde, 0x8e, 0x9b, 0x03, 0x4e, 0xff, 0xef, 0xaf, 0x4a, 0x71, 0x43, 0x37, 0x90, 0x47, 0xe3, 0x22, 0x8f, 0x9c, 0xc3,
0x03, 0xbf, 0x19, 0xe5, 0x9a, 0xa4, 0xf8, 0xe3, 0xf6, 0x25, 0xfa, 0xb3, 0xab, 0xa8, 0x85, 0x21, 0x6d, 0x7d, 0x54,
0x2d, 0xd8, 0x46, 0x55, 0xe4, 0xed, 0x1f, 0x37, 0xb7, 0xe7, 0x08, 0xd7, 0xbd, 0x10, 0x02, 0xc9, 0x86, 0xa7, 0xdf,
0x5a, 0x58, 0xde, 0x51, 0x6d, 0x7b, 0xd8, 0xd0, 0x35, 0x98, 0x53, 0x0c, 0x3d, 0xbf, 0xe6, 0x02, 0x86, 0x30, 0x06,
0xc5, 0x02, 0x9d, 0xbc, 0x3a, 0x59, 0xfb, 0xcc, 0xaf, 0x68, 0x38, 0xed, 0x68, 0x38, 0xfa, 0xa8, 0x7f, 0x05, 0xff,
0x0b, 0x54, 0xcf, 0x54, 0x8b, 0x15, 0x0b, 0x00, 0x00};
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7f,
0x05, 0x8f, 0x4d, 0x1b, 0xa9, 0xb1, 0xa8, 0x87, 0xd7, 0xde, 0x44, 0x96, 0x54, 0xa4, 0x7b, 0x2d, 0x5a, 0xa0, 0x69,
0x03, 0xec, 0x36, 0xf7, 0x21, 0x08, 0xb0, 0x34, 0x39, 0xb2, 0x98, 0xa5, 0x48, 0x1d, 0x49, 0xbf, 0x62, 0xf8, 0x7e,
0xfb, 0x81, 0x92, 0xec, 0xf5, 0x2e, 0x9a, 0x03, 0x0e, 0x86, 0x85, 0x19, 0xce, 0x7b, 0x38, 0x0f, 0x16, 0xff, 0xe0,
0x9a, 0xb9, 0x7d, 0x07, 0xa8, 0x71, 0xad, 0xac, 0x0a, 0xff, 0x45, 0x92, 0xaa, 0x55, 0x09, 0xaa, 0x2a, 0x1a, 0xa0,
0xbc, 0x2a, 0x5a, 0x70, 0x14, 0xb1, 0x86, 0x1a, 0x0b, 0xae, 0xfc, 0xeb, 0xee, 0x97, 0xe8, 0x75, 0x55, 0x48, 0xa1,
0x1e, 0x90, 0x01, 0x59, 0x0a, 0xa6, 0x15, 0x6a, 0x0c, 0xd4, 0x25, 0xa7, 0x8e, 0xe6, 0xa2, 0xa5, 0x2b, 0x18, 0x45,
0x14, 0x6d, 0xa1, 0xdc, 0x08, 0xd8, 0x76, 0xda, 0x38, 0xc4, 0xb4, 0x72, 0xa0, 0x5c, 0x89, 0xb7, 0x82, 0xbb, 0xa6,
0xe4, 0xb0, 0x11, 0x0c, 0xa2, 0x1e, 0x99, 0x08, 0x25, 0x9c, 0xa0, 0x32, 0xb2, 0x8c, 0x4a, 0x28, 0xd3, 0xc9, 0xda,
0x82, 0xe9, 0x11, 0xba, 0x94, 0x50, 0x2a, 0x8d, 0xab, 0xc2, 0x32, 0x23, 0x3a, 0x87, 0xbc, 0xab, 0x65, 0xab, 0xf9,
0x5a, 0x42, 0x15, 0xc7, 0xd4, 0x5a, 0x70, 0x36, 0x16, 0x8a, 0xc3, 0x8e, 0xd0, 0x6b, 0xb8, 0xa6, 0x2c, 0x4d, 0xc8,
0x67, 0xfb, 0x0d, 0xd7, 0x6c, 0xdd, 0x82, 0x72, 0x44, 0x6a, 0x46, 0x9d, 0xd0, 0x8a, 0x58, 0xa0, 0x86, 0x35, 0x65,
0x59, 0xe2, 0x1f, 0x2d, 0xdd, 0x00, 0xfe, 0xfe, 0xfb, 0xe0, 0xcc, 0xb4, 0x02, 0xf7, 0xb3, 0x04, 0x0f, 0xda, 0x9f,
0xf6, 0x77, 0x74, 0xf5, 0x07, 0x6d, 0x21, 0xc0, 0xd4, 0x0a, 0x0e, 0x38, 0xfc, 0x98, 0x7c, 0x22, 0xd6, 0xed, 0x25,
0x10, 0x2e, 0x6c, 0x27, 0xe9, 0xbe, 0xc4, 0x4b, 0xa9, 0xd9, 0x03, 0x0e, 0x17, 0xf5, 0x5a, 0x31, 0xaf, 0x1c, 0xe9,
0x00, 0xc2, 0x83, 0x04, 0x87, 0x5c, 0xf9, 0x8e, 0xba, 0x86, 0xb4, 0x74, 0x17, 0x0c, 0x80, 0x50, 0x41, 0xf6, 0x43,
0x00, 0xaf, 0xd2, 0x24, 0x09, 0x27, 0xfd, 0x27, 0x09, 0xe3, 0x34, 0x49, 0x16, 0x06, 0xdc, 0xda, 0x28, 0x44, 0x83,
0xfb, 0xa2, 0xa3, 0xae, 0x41, 0xbc, 0xc4, 0xef, 0xd2, 0x0c, 0xa5, 0x6f, 0x48, 0x36, 0xfb, 0x9d, 0x5c, 0xa3, 0x2b,
0x92, 0xcd, 0xd8, 0x75, 0x34, 0x43, 0xe9, 0x55, 0x34, 0x43, 0x59, 0x46, 0x66, 0x28, 0xf9, 0x82, 0x51, 0x2d, 0xa4,
0x2c, 0xb1, 0xd2, 0x0a, 0x30, 0xb2, 0xce, 0xe8, 0x07, 0x28, 0x31, 0x5b, 0x1b, 0x03, 0xca, 0xdd, 0x68, 0xa9, 0x0d,
0x8e, 0xab, 0x6f, 0xfe, 0x2f, 0x85, 0xce, 0x50, 0x65, 0x6b, 0x6d, 0xda, 0x12, 0xf7, 0xd9, 0x0f, 0x5e, 0x1c, 0xdc,
0x11, 0xf9, 0x4f, 0x78, 0x41, 0x8c, 0xb4, 0x11, 0x2b, 0xa1, 0x4a, 0xec, 0x35, 0xbe, 0xc6, 0x71, 0x75, 0x1f, 0x1e,
0xcf, 0xd1, 0x53, 0x1f, 0xfd, 0x18, 0x0f, 0x0f, 0x3e, 0xde, 0x17, 0x76, 0xb3, 0x42, 0xbb, 0x56, 0x2a, 0x5b, 0xe2,
0xc6, 0xb9, 0x2e, 0x8f, 0xe3, 0xed, 0x76, 0x4b, 0xb6, 0x53, 0xa2, 0xcd, 0x2a, 0xce, 0x92, 0x24, 0x89, 0xed, 0x66,
0x85, 0xd1, 0x50, 0x08, 0x38, 0xbb, 0xc2, 0xa8, 0x01, 0xb1, 0x6a, 0x5c, 0x0f, 0x57, 0x2f, 0x0e, 0x70, 0x2c, 0x3c,
0x47, 0x75, 0xff, 0xe9, 0xc2, 0x8a, 0xb9, 0xb0, 0x02, 0x3f, 0xd2, 0x00, 0x9f, 0xc2, 0x7c, 0xd9, 0x87, 0x79, 0x4d,
0x33, 0x94, 0xa1, 0xa4, 0xff, 0x65, 0x91, 0x87, 0x47, 0x2c, 0x7a, 0x86, 0xa1, 0x0b, 0xcc, 0x43, 0xed, 0x3c, 0x7a,
0x73, 0x96, 0x4d, 0xfd, 0xc9, 0x26, 0x4d, 0x1e, 0x0f, 0xbc, 0xc0, 0xaf, 0xf3, 0x4b, 0x3c, 0xca, 0x3e, 0x5c, 0x32,
0x78, 0x6b, 0x4d, 0xfa, 0x61, 0x4e, 0x67, 0x68, 0x36, 0x9e, 0xcc, 0x22, 0x0f, 0x9f, 0x31, 0x34, 0xdb, 0x64, 0x4d,
0xda, 0x46, 0xf3, 0x68, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xd6, 0xcc, 0x3f, 0xcc, 0x2f, 0xcf,
0xa2, 0xe9, 0x97, 0x97, 0x71, 0x85, 0xc3, 0x1c, 0xe3, 0xc7, 0xc8, 0xf9, 0x65, 0xe4, 0xe4, 0xb3, 0x16, 0x2a, 0xc0,
0x38, 0x3c, 0xd6, 0xe0, 0x58, 0x13, 0xe0, 0x98, 0x69, 0x55, 0x8b, 0x15, 0xf9, 0x6c, 0xb5, 0xc2, 0x21, 0x71, 0x0d,
0xa8, 0xe0, 0x24, 0xea, 0x05, 0xa1, 0xa7, 0x04, 0xcf, 0x29, 0x2e, 0x3c, 0x9c, 0xeb, 0xdf, 0x09, 0x27, 0xa1, 0x74,
0xc4, 0x37, 0xec, 0xe4, 0x6f, 0xba, 0xe2, 0xa7, 0xfd, 0x6f, 0x3c, 0xc0, 0x2d, 0x65, 0x38, 0x24, 0x42, 0x29, 0x30,
0x77, 0xb0, 0x73, 0x25, 0x7e, 0xf7, 0xf6, 0x06, 0xbd, 0xe5, 0xdc, 0x80, 0xb5, 0x39, 0xc2, 0xaf, 0x1c, 0x69, 0x29,
0xfb, 0xba, 0x78, 0x93, 0x3e, 0x95, 0xfe, 0x97, 0xf8, 0x45, 0xa0, 0x3f, 0xc0, 0x6d, 0xb5, 0x79, 0x18, 0xe5, 0xbd,
0xfd, 0x85, 0x6f, 0x23, 0x56, 0x7e, 0x55, 0x8d, 0x02, 0x87, 0xc3, 0x89, 0xf8, 0x3a, 0x83, 0xb5, 0x82, 0xe3, 0x70,
0x22, 0xbf, 0xce, 0xd1, 0x59, 0xdf, 0xbc, 0x8e, 0xd0, 0xce, 0x12, 0x2b, 0x05, 0x83, 0x20, 0x0d, 0x49, 0xad, 0xcd,
0xcf, 0x94, 0x35, 0x8f, 0x09, 0xb2, 0x43, 0x47, 0xab, 0x47, 0x3d, 0xcc, 0x00, 0x75, 0x30, 0xaa, 0x0a, 0x30, 0x17,
0x1b, 0x1c, 0x2e, 0x14, 0x61, 0x92, 0x5a, 0xeb, 0x47, 0x46, 0xe9, 0x9d, 0xf3, 0xe1, 0xe0, 0x89, 0x1a, 0x22, 0xfd,
0xf5, 0xee, 0xdd, 0xef, 0xe5, 0x7d, 0x41, 0x87, 0x01, 0x89, 0xbf, 0xc5, 0xa8, 0x67, 0x3e, 0x33, 0x46, 0x12, 0x6a,
0xe7, 0x2b, 0x5e, 0x07, 0x96, 0x18, 0x6b, 0x45, 0x78, 0x2c, 0x6c, 0x47, 0xd5, 0x73, 0xb6, 0x3e, 0xa6, 0xaa, 0x88,
0x3d, 0xad, 0x2a, 0x62, 0x5a, 0xbd, 0x38, 0x98, 0xc0, 0xfa, 0xe1, 0xf6, 0x10, 0x1e, 0xef, 0x27, 0x8a, 0xfc, 0x7b,
0x0d, 0x66, 0x7f, 0x0b, 0x12, 0x98, 0xd3, 0x26, 0xc0, 0xe4, 0x89, 0x60, 0x48, 0x1c, 0xec, 0xdc, 0xcd, 0x38, 0x7f,
0x2d, 0xf1, 0x87, 0x13, 0x45, 0xb4, 0x62, 0x52, 0xb0, 0x87, 0xf2, 0x1c, 0x71, 0x78, 0x10, 0x64, 0x43, 0xe5, 0x1a,
0x4e, 0x3c, 0x92, 0xd4, 0x9a, 0xad, 0x6d, 0x10, 0x1e, 0x27, 0x8c, 0xd0, 0xae, 0x03, 0xc5, 0x6f, 0x1a, 0x21, 0x79,
0xa0, 0xc2, 0x63, 0xf8, 0x78, 0xd3, 0xcf, 0x8c, 0xfb, 0xd5, 0xf0, 0xd1, 0x80, 0xfc, 0x4f, 0xf9, 0xd2, 0x2f, 0x87,
0x97, 0x9f, 0x70, 0x48, 0xfa, 0xf8, 0xef, 0x1f, 0x37, 0x84, 0x6f, 0xef, 0x57, 0xbb, 0x56, 0x4e, 0x7c, 0xe8, 0xd1,
0x7c, 0x16, 0x1e, 0xef, 0x8f, 0xe1, 0x31, 0x5c, 0x14, 0xf1, 0x30, 0xe7, 0xab, 0xa2, 0x1f, 0xb9, 0xd5, 0x0f, 0x87,
0xa5, 0xde, 0x45, 0x56, 0x7c, 0x11, 0x6a, 0x95, 0x0b, 0xd5, 0x80, 0x11, 0xee, 0xc8, 0xc5, 0x66, 0x22, 0x54, 0xb7,
0x76, 0x87, 0x8e, 0x72, 0xee, 0x29, 0xb3, 0x6e, 0xb7, 0xa8, 0xb5, 0x72, 0x9e, 0x13, 0xf2, 0x14, 0xda, 0xe3, 0x40,
0xef, 0x27, 0x4c, 0xfe, 0x66, 0xf6, 0xdd, 0x71, 0xa9, 0xf9, 0xfe, 0xe0, 0xd3, 0x10, 0x51, 0x29, 0x56, 0x2a, 0x67,
0xa0, 0x1c, 0x98, 0x41, 0xa8, 0xa6, 0xad, 0x90, 0xfb, 0xdc, 0x52, 0x65, 0x23, 0x0b, 0x46, 0xd4, 0xc7, 0xe5, 0xda,
0x39, 0xad, 0x0e, 0x4b, 0x6d, 0x38, 0x98, 0x3c, 0x59, 0x0c, 0x40, 0x64, 0x28, 0x17, 0x6b, 0x9b, 0x93, 0xa9, 0x81,
0x76, 0xb1, 0xa4, 0xec, 0x61, 0x65, 0xf4, 0x5a, 0xf1, 0x88, 0xf9, 0xc9, 0x9b, 0x7f, 0x9b, 0xd6, 0x74, 0x0a, 0x6c,
0x31, 0x62, 0x75, 0x5d, 0x2f, 0xa4, 0x50, 0x10, 0x0d, 0xb3, 0x2d, 0xcf, 0xc8, 0x95, 0x17, 0xbb, 0x70, 0x93, 0x64,
0xfe, 0x60, 0xf0, 0x31, 0x4d, 0x92, 0xef, 0x16, 0xa7, 0x70, 0x92, 0x05, 0x5b, 0x1b, 0xab, 0x4d, 0xde, 0x69, 0xe1,
0xdd, 0x3c, 0xb6, 0x54, 0xa8, 0x4b, 0xef, 0x7d, 0xd9, 0x2c, 0xc6, 0x75, 0x94, 0x0b, 0xd5, 0x9b, 0xe9, 0x97, 0xd2,
0xa2, 0x15, 0x6a, 0xd8, 0xa9, 0x79, 0x36, 0x4f, 0xba, 0xdd, 0xf1, 0x54, 0x09, 0x87, 0x13, 0x77, 0x2d, 0x61, 0xb7,
0xf8, 0xbc, 0xb6, 0x4e, 0xd4, 0xfb, 0x68, 0xdc, 0xc9, 0xb9, 0xed, 0x28, 0x83, 0x68, 0x09, 0x6e, 0x0b, 0xa0, 0x16,
0xbd, 0x8d, 0x48, 0x38, 0x68, 0xed, 0x98, 0xa7, 0xb3, 0x9a, 0xbe, 0x60, 0x9f, 0xea, 0xfa, 0x5f, 0xdc, 0xbe, 0x8a,
0x0e, 0x2d, 0x35, 0x2b, 0xa1, 0xa2, 0xa5, 0x76, 0x4e, 0xb7, 0x79, 0x74, 0xdd, 0xed, 0x16, 0xe3, 0x91, 0x57, 0x96,
0xa7, 0xde, 0xcd, 0x7e, 0xd7, 0x9e, 0xf2, 0x9d, 0x76, 0x3b, 0x64, 0xb5, 0x14, 0x7c, 0xe4, 0xeb, 0x59, 0x50, 0x72,
0x4e, 0x4f, 0x3a, 0xeb, 0x76, 0xc8, 0x9f, 0x9d, 0x52, 0x7d, 0x55, 0xbf, 0xa6, 0x69, 0xf2, 0x37, 0x37, 0xc2, 0xeb,
0x3a, 0x5b, 0xd6, 0xe7, 0x4c, 0xf9, 0xb5, 0xe9, 0x57, 0x4b, 0x5f, 0x5a, 0x45, 0x3c, 0xbc, 0x6e, 0x7c, 0x65, 0x54,
0x85, 0xcf, 0x70, 0x55, 0x34, 0x29, 0x12, 0xbc, 0x6c, 0x29, 0xab, 0x2e, 0x66, 0x5b, 0x11, 0x37, 0xe9, 0x89, 0xd4,
0xa4, 0xd5, 0x93, 0xb9, 0x35, 0xd0, 0x7a, 0xef, 0xab, 0x1b, 0xad, 0x14, 0x30, 0x27, 0xd4, 0x0a, 0x39, 0x8d, 0xc6,
0x14, 0x10, 0x42, 0x8a, 0xa5, 0xa9, 0xde, 0x4b, 0xa0, 0x16, 0xd0, 0x96, 0x0a, 0x47, 0x8a, 0x78, 0xe0, 0x1f, 0x3a,
0x5d, 0xf0, 0x52, 0x81, 0x3b, 0xf7, 0x76, 0x33, 0x1d, 0x0c, 0xdc, 0x82, 0xf3, 0x9a, 0xbc, 0x81, 0x69, 0x55, 0xf8,
0x15, 0x8c, 0x68, 0xdf, 0xa5, 0x65, 0xbc, 0x15, 0xb5, 0xf0, 0x4f, 0x98, 0xaa, 0xe8, 0x8b, 0xdc, 0x6b, 0xf0, 0x79,
0x1e, 0x9e, 0x5b, 0x3d, 0x24, 0x41, 0xad, 0x5c, 0x53, 0x4e, 0x33, 0xd4, 0x49, 0xca, 0xa0, 0xd1, 0x92, 0x83, 0x29,
0x6f, 0x6f, 0x7f, 0xfb, 0x67, 0xe5, 0x9d, 0x79, 0x94, 0xeb, 0xec, 0xc3, 0x20, 0xe6, 0x81, 0x51, 0x6a, 0x7e, 0x35,
0x3c, 0xb2, 0x3a, 0x6a, 0xed, 0x56, 0x1b, 0xfe, 0x44, 0xc7, 0xfb, 0xf1, 0x70, 0xd0, 0xd3, 0xff, 0xfb, 0x56, 0xa9,
0x6e, 0xe9, 0x06, 0x8a, 0x78, 0x44, 0x8a, 0xd8, 0x3b, 0x3c, 0xd0, 0x9b, 0x91, 0xaf, 0x49, 0xab, 0x3f, 0xef, 0xde,
0xa2, 0xbf, 0x3a, 0x4e, 0x1d, 0x0c, 0x69, 0xeb, 0xa3, 0x6a, 0xc1, 0x35, 0x9a, 0x97, 0xef, 0xff, 0xbc, 0xbd, 0x3b,
0x47, 0xb8, 0xee, 0x99, 0x10, 0x28, 0x36, 0x3c, 0xf7, 0xd6, 0xd2, 0x89, 0x8e, 0x1a, 0xd7, 0xab, 0x8d, 0xfc, 0x14,
0x39, 0xc5, 0xd0, 0xd3, 0x6b, 0x21, 0x61, 0x08, 0x63, 0x10, 0xac, 0xd0, 0xc9, 0xab, 0x93, 0xb5, 0x67, 0x7e, 0xc5,
0xc3, 0x6d, 0xc7, 0xc3, 0xd5, 0xc7, 0xfd, 0xcb, 0xf7, 0xbf, 0x81, 0xdb, 0x13, 0xb5, 0x09, 0x0b, 0x00, 0x00};
#else // Brotli (default, smaller)
constexpr uint8_t INDEX_BR[] PROGMEM = {
0x1b, 0x14, 0x0b, 0x00, 0xe4, 0x6f, 0xcd, 0xfc, 0x7b, 0x2e, 0x27, 0xd2, 0x21, 0x2b, 0x58, 0xea, 0x16, 0xf8, 0xa5,
0xb6, 0x47, 0x14, 0xb3, 0x4c, 0x56, 0xcc, 0x20, 0x5b, 0x1d, 0xfe, 0x7d, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6, 0x48,
0x84, 0xa2, 0x21, 0x69, 0x0d, 0x77, 0x33, 0xf3, 0x77, 0xcf, 0x4c, 0x21, 0xf1, 0xde, 0xee, 0x7d, 0x44, 0xbc, 0xd3,
0xc4, 0x33, 0x89, 0x1a, 0x69, 0x68, 0x48, 0x57, 0xa7, 0x17, 0x0b, 0x4d, 0x91, 0xac, 0x30, 0x48, 0x21, 0x4d, 0x4c,
0x10, 0x57, 0x46, 0x14, 0x17, 0xd5, 0x21, 0x0e, 0xb4, 0x50, 0x87, 0xad, 0x62, 0xda, 0x11, 0x68, 0xc3, 0x20, 0x7f,
0x2d, 0xdc, 0x57, 0xeb, 0x2c, 0x36, 0xdb, 0xc4, 0x84, 0x0f, 0xac, 0xec, 0x08, 0x91, 0xa3, 0x92, 0x27, 0x45, 0xc4,
0x1e, 0xa4, 0x9e, 0x72, 0x3b, 0xc2, 0xe3, 0x20, 0x7d, 0xf8, 0x05, 0x09, 0xea, 0xe3, 0xa6, 0x3f, 0x11, 0x8b, 0x16,
0x11, 0x6b, 0x26, 0x91, 0x1a, 0x2c, 0x19, 0x24, 0xf7, 0x5d, 0x44, 0x82, 0x49, 0x44, 0x15, 0xce, 0x39, 0xdc, 0xcb,
0x8f, 0x5b, 0xe1, 0xe2, 0x42, 0x96, 0xa2, 0x10, 0x3c, 0x20, 0xa5, 0x65, 0x85, 0x49, 0x6a, 0x06, 0x08, 0xd1, 0xcb,
0x41, 0xb8, 0x49, 0x20, 0x73, 0x71, 0xfe, 0x55, 0x61, 0x45, 0xc6, 0x95, 0x72, 0xc8, 0xf0, 0xa5, 0xe9, 0xe6, 0x3a,
0xb9, 0xc3, 0x8a, 0x9f, 0xb5, 0xc1, 0xc9, 0x15, 0x56, 0x93, 0x38, 0x8a, 0x48, 0xf0, 0x4f, 0x38, 0x22, 0xe1, 0x8d,
0x55, 0xbb, 0x8c, 0xc3, 0xb0, 0x68, 0xcc, 0x7f, 0x6f, 0xf8, 0xc9, 0x9b, 0x38, 0x41, 0xf1, 0x94, 0x25, 0xf9, 0x6b,
0x56, 0xa2, 0xec, 0xa7, 0xbd, 0x2e, 0x69, 0x8e, 0xe2, 0xec, 0x36, 0x94, 0x24, 0x2c, 0x12, 0x1d, 0x4e, 0x76, 0x7e,
0x23, 0xcc, 0xf2, 0x6e, 0x35, 0x1a, 0x9c, 0xed, 0x6f, 0x15, 0x3f, 0xc9, 0x72, 0xdc, 0xfd, 0x13, 0x0f, 0x16, 0x8a,
0x23, 0x4f, 0xf9, 0x2e, 0x63, 0x44, 0x91, 0x77, 0xe0, 0xb3, 0xd1, 0x78, 0x74, 0xdb, 0x4a, 0x2c, 0xd8, 0xe8, 0xf9,
0x2c, 0x03, 0xbe, 0xae, 0xac, 0x4e, 0x42, 0x01, 0xc4, 0x4b, 0x40, 0xe7, 0x94, 0x34, 0x85, 0x2c, 0xfe, 0xf5, 0x74,
0x75, 0xd8, 0xdc, 0xec, 0x6a, 0x00, 0x3f, 0x3f, 0x81, 0x77, 0xa8, 0xcd, 0xde, 0x6d, 0x5a, 0x47, 0xa1, 0x99, 0x36,
0x87, 0xb6, 0x78, 0x35, 0x3c, 0x9a, 0x64, 0x15, 0x7c, 0x56, 0x76, 0x22, 0xce, 0x46, 0xe5, 0x17, 0x57, 0x05, 0xfc,
0x09, 0x69, 0xae, 0x39, 0xaa, 0xee, 0xc9, 0x4e, 0x7f, 0x99, 0x2a, 0x65, 0x82, 0x7e, 0xeb, 0x23, 0x4f, 0x42, 0xd5,
0xca, 0xcb, 0x42, 0x74, 0x2e, 0xd2, 0xa2, 0x62, 0x57, 0xd0, 0xa9, 0x7b, 0x4b, 0xac, 0x7b, 0x65, 0x13, 0x47, 0x0f,
0x2d, 0x3d, 0xf6, 0xbc, 0x78, 0x5c, 0xa3, 0xc9, 0x57, 0x4b, 0x10, 0x62, 0x68, 0x19, 0x7f, 0xfd, 0x1a, 0xcb, 0x51,
0x1e, 0x41, 0x39, 0x55, 0xf3, 0x97, 0x30, 0xca, 0x37, 0xb6, 0x42, 0x1d, 0x2d, 0x8c, 0xc6, 0x65, 0x8a, 0xd2, 0x89,
0x88, 0xa6, 0x28, 0xbd, 0x56, 0x7c, 0x2d, 0x5e, 0x0d, 0x2f, 0x9e, 0xc3, 0xa5, 0x80, 0xaf, 0xcc, 0x00, 0xde, 0xe6,
0x5a, 0x8b, 0xda, 0xff, 0x1f, 0x16, 0xc8, 0x83, 0x5e, 0xe5, 0xea, 0x25, 0x86, 0x70, 0x55, 0x25, 0x01, 0x14, 0x0c,
0x77, 0xd8, 0x31, 0x21, 0xcc, 0x79, 0x15, 0x3b, 0x32, 0x3a, 0xd3, 0xc5, 0x30, 0xaf, 0x03, 0xd8, 0x3e, 0x44, 0xb8,
0x63, 0xb5, 0x74, 0x4f, 0x41, 0x4b, 0xf0, 0x24, 0x74, 0xb2, 0x06, 0xb2, 0x7b, 0x06, 0x60, 0xdc, 0xc1, 0x3e, 0x0e,
0x6f, 0x1e, 0x3c, 0x42, 0xa0, 0xdb, 0x36, 0x43, 0x30, 0x71, 0xcc, 0xd6, 0x1a, 0xbd, 0x38, 0x61, 0x19, 0x3f, 0xf2,
0xdf, 0xf4, 0x53, 0x3d, 0xc1, 0x14, 0xbe, 0x50, 0x1c, 0x2c, 0xf3, 0xaa, 0x74, 0x36, 0xcb, 0xbd, 0x7a, 0x4b, 0xa3,
0x1c, 0x90, 0x40, 0x5b, 0x4a, 0x0f, 0x83, 0x6e, 0x9e, 0x96, 0x28, 0x3d, 0x77, 0x43, 0x05, 0x0e, 0x39, 0x26, 0x15,
0xb8, 0x6b, 0x4e, 0x3a, 0x62, 0xc2, 0xda, 0xde, 0x8e, 0x29, 0x29, 0x0b, 0x09, 0xa2, 0xb3, 0xa7, 0x1e, 0x9a, 0x0c,
0x8f, 0xa1, 0x46, 0x6f, 0x92, 0xf3, 0x9e, 0xb5, 0xc5, 0x7e, 0x0e, 0x8d, 0xeb, 0x55, 0x08, 0xfa, 0xc9, 0xd8, 0x4e,
0xe3, 0xd0, 0xca, 0x2a, 0x96, 0x11, 0x7e, 0xb1, 0xd4, 0x7f, 0x8f, 0x1d, 0xb3, 0xc3, 0xa0, 0x89, 0x6f, 0x93, 0x99,
0x55, 0x48, 0x97, 0x08, 0x79, 0x66, 0xe9, 0x4a, 0x6b, 0xa0, 0x29, 0xf2, 0x10, 0x1f, 0x22, 0xa2, 0x1b, 0x41, 0xdf,
0xa3, 0xde, 0x62, 0x60, 0x8e, 0xa1, 0x60, 0xc4, 0xd5, 0xce, 0x81, 0x47, 0x84, 0x3b, 0x66, 0x60, 0x74, 0xa7, 0x74,
0xcc, 0x48, 0xe0, 0xe1, 0xd5, 0x0c, 0x15, 0x98, 0x3d, 0xa7, 0x9c, 0xb2, 0xec, 0xba, 0x0f, 0x2c, 0x52, 0x14, 0x7b,
0xe2, 0x49, 0x6e, 0x0f, 0x8c, 0x00, 0xe0, 0x81, 0xf6, 0x57, 0xe4, 0x3f, 0xbf, 0x7c, 0xa9, 0x5e, 0xfe, 0x01, 0xc2,
0x64, 0xb0, 0x05, 0x60, 0x01, 0xaa, 0x78, 0x65, 0xb2, 0xeb, 0x56, 0x41, 0xf2, 0xbf, 0xa3, 0x45, 0x4e, 0x3c, 0x78,
0xe2, 0xa1, 0x0d, 0xa9, 0x2a, 0xc0, 0x8a, 0xc0, 0x6f, 0xe4, 0x66, 0xbe, 0x72, 0x35, 0x5e, 0xf7, 0x3b, 0x42, 0x53,
0xd4, 0xe6, 0x66, 0xb6, 0x78, 0xcd, 0xaa, 0x6f, 0xf4, 0x26, 0x80, 0x3a, 0x4e, 0x74, 0x80, 0x17, 0x88, 0x44, 0x23,
0xa6, 0x3a, 0x6f, 0x87, 0xb8, 0xd0, 0x4d, 0xd3, 0xfc, 0x7c, 0xd6, 0x38, 0x2a, 0x00, 0x28, 0x01, 0xa2, 0x40, 0x94,
0x6c, 0x1e, 0x8a, 0xed, 0xe3, 0x92, 0x9d, 0x90, 0xe3, 0x0d, 0x02, 0x4e, 0x15, 0x30, 0xed, 0x8f, 0x5b, 0x99, 0xaa,
0x7a, 0x4e, 0xb9, 0xec, 0x91, 0xe2, 0x9f, 0xd4, 0xca, 0x46, 0xaf, 0x87, 0x19, 0x4b, 0xad, 0xea, 0xe6, 0x6c, 0x8d,
0x53, 0x4b, 0x29, 0xee, 0x1e, 0x96, 0xd8, 0x94, 0x30, 0x3a, 0x9c, 0xb0, 0x4c, 0xdb, 0xe2, 0xa1, 0x7f, 0xc7, 0x11,
0xdd, 0xe3, 0x9d, 0x36, 0x44, 0xd3, 0x93, 0x14, 0x9c, 0x4c, 0x9d, 0xdd, 0x3a, 0x7c, 0x41, 0xb1, 0x8f, 0x14, 0xd9,
0x4e, 0x61, 0xd9, 0x3a, 0xe3, 0x0d, 0x76, 0xca, 0x6c, 0xac, 0x73, 0xaf, 0xad, 0x94, 0x87, 0x30, 0xf1, 0x30, 0x2f,
0x8b, 0xed, 0x4a, 0xed, 0xbc, 0xc2, 0xf2, 0x7c, 0x30, 0xa3, 0x0b, 0x28, 0x64, 0x3b, 0x8c, 0xd4, 0xc3, 0x42, 0xb9,
0xa3, 0x44, 0x51, 0x80, 0x07, 0x5a, 0x3d, 0x14, 0x33, 0x99, 0xbf, 0x2a, 0x6b, 0x2b, 0x19, 0x47, 0x72, 0x9e, 0xd4,
0xb4, 0x6d, 0x72, 0xdd, 0x8a, 0x4b, 0x33, 0x55, 0xbc, 0xb4, 0xcd, 0xc8, 0x2b, 0x17, 0x2f, 0x74, 0xeb, 0x22, 0x17,
0x94, 0x08, 0x27, 0x27, 0xc2, 0x5b, 0x17, 0xb4, 0xa9, 0x22, 0x16, 0x9d, 0xd4, 0xfc, 0xc7, 0x15, 0xa3, 0x9b, 0x86,
0x1f, 0xad, 0x45, 0xd3, 0x87, 0x94, 0x5b, 0x31, 0x36, 0xaa, 0xe4, 0x66, 0x8d, 0xcc, 0x31, 0x05, 0x5b, 0xc4, 0x40,
0xc0, 0xb8, 0xeb, 0x91, 0x18, 0x22, 0x8c, 0x31, 0x1e, 0xad, 0xd0, 0x3a, 0x98, 0x07, 0xb5, 0x6f, 0x11, 0xba, 0x11,
0xa6, 0x14, 0x35, 0x5a, 0xe7, 0x55, 0xdf, 0xb7, 0x4c, 0x03, 0x61, 0xa3, 0x74, 0x23, 0xdf, 0x55, 0x1f, 0x02, 0x51,
0x09, 0xb7, 0xba, 0xd5, 0x0c, 0x67, 0xab, 0x98, 0x70, 0x14, 0x64, 0x8d, 0xf4, 0x8b, 0x54, 0x44, 0x07, 0x6f, 0xe0,
0x69, 0x32, 0xca, 0x48, 0xe5, 0xd3, 0xa7, 0x17, 0x8f, 0x45, 0x84, 0x04, 0xb7, 0xd1, 0xbb, 0xe1, 0xf6, 0x01, 0x0a,
0xf6, 0xee, 0x2b, 0x32, 0xd2, 0xc5, 0xf8, 0xa6, 0xec, 0x0f, 0x1b, 0x09, 0x1d, 0xfc, 0xa2, 0xbf, 0x54, 0x5d, 0x2c,
0x62, 0xf4, 0x77, 0xde, 0xad, 0xa0, 0x50, 0x6a, 0xb4, 0xe3, 0x5f, 0xda, 0xff, 0x6b, 0xb1, 0x78, 0xf7, 0xf9, 0xc1,
0xa6, 0xa8, 0x93, 0xe8, 0xe4, 0x11, 0x56, 0xa0, 0x5b, 0x85, 0xa7, 0x92, 0x7a, 0x58, 0x45, 0x95, 0xa9, 0x63, 0x83,
0xb4, 0x1f, 0x18, 0x31, 0x7a, 0x6d, 0xa1, 0x8d, 0x8c, 0xdc, 0x91, 0x02, 0x3c, 0x9c, 0x92, 0x42, 0x8e, 0x03, 0x02,
0xc5, 0x0c, 0x43, 0x54, 0xf9, 0xb2, 0x85, 0x39, 0x2e, 0x77, 0xad, 0x00};
0x1b, 0x08, 0x0b, 0x00, 0xe4, 0x7f, 0x9b, 0xad, 0xbb, 0x97, 0x53, 0xde, 0xb7, 0x25, 0x0e, 0x69, 0xd4, 0x69, 0x89,
0xba, 0xa5, 0x55, 0x22, 0x04, 0x27, 0xeb, 0x00, 0x52, 0xac, 0xbc, 0xec, 0x5f, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6,
0x48, 0x84, 0x4a, 0x48, 0x5a, 0xcb, 0xbd, 0xb7, 0x72, 0x66, 0x4e, 0x62, 0xd8, 0xbd, 0x7f, 0x88, 0x68, 0x86, 0x28,
0xd6, 0xf1, 0xda, 0x2c, 0xa2, 0x32, 0x5c, 0xdd, 0xab, 0xb2, 0x15, 0xbc, 0x24, 0x13, 0xe6, 0xbd, 0x0c, 0x52, 0x63,
0x82, 0x88, 0x6d, 0x74, 0x71, 0x51, 0x9c, 0xe2, 0x40, 0x56, 0xea, 0xb0, 0x5c, 0xb7, 0x1d, 0x81, 0x3a, 0x0c, 0xf2,
0xd7, 0xa8, 0x5c, 0x35, 0x2d, 0x43, 0xb3, 0x42, 0x37, 0x7c, 0x60, 0xd8, 0xc1, 0x95, 0x91, 0x94, 0x3c, 0x29, 0x20,
0x96, 0x20, 0xf6, 0x24, 0xb7, 0x83, 0x4a, 0x06, 0xf1, 0xc3, 0x2f, 0x88, 0x50, 0x55, 0x35, 0x2d, 0xe8, 0xb6, 0x21,
0x38, 0x61, 0x8e, 0x44, 0x2a, 0xac, 0x39, 0xcf, 0x74, 0xaa, 0x31, 0x7f, 0x62, 0x32, 0x9b, 0x99, 0x42, 0x0a, 0xf6,
0x6f, 0xd8, 0x89, 0x3f, 0x49, 0xb1, 0xa2, 0x52, 0x0a, 0x8e, 0xb3, 0x27, 0x0b, 0x07, 0x07, 0x58, 0xb0, 0x8f, 0xaa,
0xdc, 0x68, 0x12, 0x0f, 0x95, 0xbf, 0xc7, 0x36, 0xd5, 0x60, 0x5a, 0xc7, 0x80, 0xac, 0x52, 0xf7, 0xa7, 0x2d, 0xb6,
0x64, 0xda, 0xda, 0x11, 0x8d, 0x6a, 0xe5, 0xba, 0xe9, 0xda, 0xdc, 0x62, 0xc3, 0xcb, 0xae, 0xc1, 0xe1, 0x11, 0xb6,
0x33, 0x29, 0x04, 0x09, 0xfe, 0x09, 0x08, 0xc2, 0x1f, 0x98, 0x16, 0x26, 0x0d, 0xce, 0xd7, 0x75, 0xfb, 0xd7, 0xa5,
0x82, 0xd7, 0x32, 0x44, 0x72, 0xc1, 0xc2, 0xe4, 0x15, 0xcb, 0x50, 0xfc, 0xd3, 0x58, 0x64, 0x34, 0x41, 0x32, 0xbe,
0x1f, 0x08, 0x43, 0x16, 0x14, 0xf7, 0x70, 0x2c, 0x1c, 0x61, 0x09, 0x78, 0x73, 0xd1, 0x30, 0xf6, 0xed, 0x85, 0x55,
0x50, 0x02, 0xf0, 0x68, 0x50, 0x5c, 0x1a, 0xd7, 0x3b, 0x8e, 0xf2, 0x23, 0xc8, 0x88, 0x39, 0xd0, 0x84, 0xf7, 0xa6,
0xd1, 0xa3, 0xfb, 0xe5, 0x44, 0xc0, 0x4c, 0x2f, 0x6f, 0x19, 0x70, 0x75, 0xf6, 0x1c, 0xb8, 0xce, 0x89, 0x4f, 0x01,
0x8d, 0xa1, 0x71, 0xf2, 0x97, 0xf8, 0xe7, 0xd3, 0xf1, 0xe1, 0xfa, 0xfc, 0xc8, 0x03, 0x78, 0x14, 0xa0, 0x3d, 0x28,
0xb7, 0xeb, 0x26, 0x52, 0x59, 0xa8, 0xb5, 0x0d, 0x5c, 0x8a, 0x6b, 0xe5, 0xf6, 0x30, 0xd6, 0xf7, 0x71, 0x31, 0xe8,
0xbd, 0xc9, 0xfa, 0xf5, 0x71, 0x9d, 0xff, 0xf6, 0x69, 0x62, 0x5f, 0xb5, 0xc7, 0x06, 0x43, 0x54, 0xb5, 0x87, 0xf1,
0xcc, 0x84, 0xe8, 0xb7, 0x56, 0x38, 0x43, 0x6a, 0xa6, 0x2f, 0x53, 0xd1, 0x89, 0x48, 0x8d, 0x72, 0x75, 0x4a, 0x17,
0xf6, 0x8d, 0xb2, 0xeb, 0xb1, 0x6b, 0x29, 0x1e, 0xd5, 0x74, 0xc7, 0xb3, 0xf4, 0xf1, 0x04, 0x0d, 0xbf, 0x08, 0x81,
0x8f, 0xfe, 0x8d, 0xfc, 0xf2, 0x35, 0x92, 0xa0, 0x24, 0x88, 0x12, 0x6a, 0xe6, 0x2f, 0x01, 0x94, 0x5c, 0x4b, 0xf9,
0x6b, 0x9a, 0xf6, 0x1a, 0x35, 0x11, 0x8a, 0xc6, 0x04, 0x8d, 0x50, 0x64, 0x48, 0x2d, 0x8b, 0xdf, 0xdf, 0xa1, 0xd1,
0xfd, 0x21, 0xd7, 0x40, 0x96, 0x00, 0xb1, 0x9f, 0x54, 0xc2, 0xe1, 0x00, 0x79, 0x15, 0x80, 0xf8, 0xca, 0x8e, 0xc5,
0x06, 0x03, 0xdf, 0x72, 0x49, 0xc0, 0x08, 0x27, 0x90, 0xe3, 0x14, 0xc2, 0xac, 0xc3, 0x83, 0xc9, 0xf6, 0x9d, 0x12,
0xab, 0x46, 0xae, 0x03, 0x74, 0x1d, 0x27, 0xce, 0x91, 0x1d, 0x4e, 0xb4, 0xbe, 0x80, 0xe7, 0x6a, 0x6d, 0x0a, 0x20,
0x47, 0x6b, 0x00, 0x4e, 0x25, 0x52, 0xe6, 0xf5, 0xe9, 0x43, 0x84, 0xc1, 0x0d, 0x4b, 0x04, 0xb3, 0x91, 0x49, 0xf5,
0xe9, 0xc4, 0x2e, 0x1b, 0xf9, 0x98, 0xf9, 0xea, 0x9e, 0xb8, 0xf6, 0x53, 0xf8, 0x42, 0xc2, 0x60, 0x5c, 0xa9, 0xd2,
0xfe, 0x0a, 0xe5, 0xd4, 0x7d, 0x36, 0x76, 0x04, 0x12, 0x38, 0xa1, 0xc4, 0x30, 0xb8, 0xf2, 0x69, 0x86, 0x5b, 0xe7,
0xe5, 0xa0, 0xc0, 0xfe, 0x91, 0x19, 0x03, 0x1e, 0xa9, 0x93, 0x26, 0x49, 0x58, 0xd5, 0xf6, 0x33, 0x4e, 0x0a, 0x89,
0x64, 0x1e, 0xb4, 0x7a, 0x5d, 0x0d, 0x12, 0xcf, 0x2b, 0xdd, 0x35, 0x90, 0x55, 0xc3, 0x0e, 0xcd, 0x0e, 0xad, 0x6b,
0x8f, 0x43, 0xd0, 0xb4, 0x6a, 0xdb, 0x4b, 0xe5, 0xa4, 0x9b, 0x09, 0x23, 0x1a, 0x43, 0xa9, 0xff, 0x61, 0x8b, 0x07,
0xd6, 0x0f, 0x83, 0x23, 0xbe, 0x8a, 0x66, 0xa2, 0x10, 0x2f, 0x11, 0x01, 0x0d, 0xc6, 0x96, 0xba, 0x87, 0xaa, 0xa8,
0x44, 0x7c, 0x1e, 0x34, 0xdb, 0xba, 0x41, 0x90, 0xf0, 0x7a, 0xdb, 0x63, 0x60, 0x96, 0x8d, 0x64, 0xcb, 0x2f, 0x28,
0x96, 0x04, 0xd5, 0xc0, 0x7a, 0xe8, 0xec, 0xd1, 0x61, 0x1b, 0x09, 0x4b, 0x4c, 0x6e, 0x1b, 0x31, 0x98, 0x9c, 0x6d,
0xdb, 0xd0, 0xec, 0xa4, 0x0f, 0x0a, 0xe0, 0xc8, 0x35, 0xc4, 0x93, 0xdc, 0xee, 0x19, 0x00, 0x80, 0x07, 0xf5, 0xcf,
0xe0, 0x7f, 0x75, 0xf8, 0x52, 0x3a, 0xfc, 0x0d, 0x84, 0xa5, 0xc1, 0xb2, 0x1c, 0x25, 0x40, 0xc5, 0x8b, 0xb3, 0xdb,
0x7a, 0x1b, 0x44, 0xff, 0x79, 0x9a, 0x26, 0xc4, 0xe7, 0x9e, 0x78, 0x4c, 0xa5, 0x94, 0xab, 0x78, 0x34, 0x9d, 0xb5,
0xb7, 0x24, 0x26, 0xe7, 0x9a, 0xf3, 0xe5, 0x2a, 0x35, 0x4b, 0xbe, 0x74, 0xd7, 0x01, 0xbc, 0x71, 0x72, 0x03, 0x5c,
0x40, 0x24, 0x17, 0x61, 0x5b, 0x7b, 0x19, 0xc2, 0x7f, 0x4e, 0x5b, 0x24, 0xfb, 0x8b, 0xc3, 0x51, 0x00, 0x3d, 0x09,
0x04, 0x05, 0x72, 0x64, 0xf6, 0xf0, 0x6b, 0x99, 0x38, 0x93, 0x5b, 0xac, 0x0c, 0xff, 0x40, 0x7b, 0x53, 0xba, 0xab,
0x61, 0xc9, 0xa2, 0xde, 0xd6, 0x2b, 0x0c, 0xbd, 0x85, 0xac, 0x4c, 0x64, 0x8b, 0xe6, 0x69, 0x2b, 0x56, 0x10, 0x1b,
0x08, 0x59, 0x6c, 0x55, 0x0a, 0xaa, 0x93, 0x85, 0x1d, 0x45, 0xda, 0xc6, 0x39, 0xe6, 0x6a, 0xab, 0x71, 0x73, 0x46,
0x0f, 0xf7, 0xab, 0x4e, 0x88, 0xae, 0x8c, 0xe0, 0x91, 0xda, 0x35, 0x8c, 0xd3, 0x18, 0x92, 0x3d, 0xab, 0x2f, 0x0d,
0xd6, 0xc9, 0x06, 0xdb, 0xc2, 0x62, 0xcb, 0x8a, 0x23, 0x5b, 0x28, 0x2e, 0x9b, 0x96, 0xc4, 0xc1, 0x42, 0xa9, 0x8d,
0x69, 0xe5, 0x8f, 0x89, 0x12, 0x11, 0x9a, 0x56, 0x3b, 0x3c, 0x2b, 0xb4, 0xb2, 0x7b, 0xfd, 0xdb, 0x80, 0x92, 0xb4,
0xca, 0x44, 0x37, 0x8c, 0x94, 0x2f, 0x4a, 0xb4, 0xc4, 0x28, 0x83, 0x8a, 0x78, 0xcb, 0xd2, 0x5c, 0x5c, 0x35, 0x29,
0x95, 0x35, 0x2f, 0x99, 0x28, 0x4f, 0x22, 0x18, 0x70, 0x85, 0xee, 0x2c, 0xb9, 0xef, 0x14, 0x57, 0x73, 0x23, 0x45,
0xae, 0xdc, 0x54, 0x56, 0x55, 0x78, 0x56, 0xad, 0x48, 0x26, 0x27, 0xbf, 0xcb, 0xd7, 0x54, 0xa9, 0xa8, 0xd7, 0xb5,
0x71, 0x9c, 0xe7, 0x79, 0x89, 0x5c, 0xa9, 0x6a, 0x53, 0xe8, 0xfa, 0x0d, 0x69, 0x36, 0xed, 0xad, 0x33, 0xc9, 0x75,
0x17, 0xe9, 0x8f, 0x31, 0x58, 0xa6, 0x27, 0xfc, 0x79, 0xc6, 0xb6, 0xd5, 0x65, 0x90, 0x31, 0xc6, 0x9d, 0x29, 0x95,
0x83, 0xe5, 0x4e, 0xbb, 0xd7, 0xdc, 0x8e, 0xd0, 0x3a, 0x54, 0x27, 0xce, 0xf5, 0xdb, 0xbd, 0x89, 0x3c, 0x61, 0xb3,
0x71, 0x23, 0xc7, 0x55, 0x9e, 0xea, 0x50, 0xe4, 0x37, 0xae, 0x72, 0x34, 0x66, 0xb9, 0x6e, 0x2c, 0x0a, 0x69, 0x8d,
0x94, 0x8b, 0x98, 0x08, 0x05, 0x3c, 0x45, 0x45, 0xe1, 0x6c, 0x22, 0xc5, 0x8f, 0x1f, 0x9f, 0x3f, 0xd2, 0x01, 0x12,
0xec, 0x86, 0x2f, 0x87, 0x8b, 0x47, 0x30, 0xd0, 0x77, 0x77, 0x1a, 0x13, 0x2d, 0xc6, 0x31, 0x65, 0x77, 0xd8, 0x8c,
0xe7, 0xe0, 0x16, 0xf9, 0x4b, 0xd4, 0xc5, 0xa8, 0x67, 0x79, 0xe7, 0xc3, 0x07, 0x94, 0x46, 0xa3, 0x8c, 0x67, 0xd3,
0xff, 0x5f, 0x96, 0xfa, 0xed, 0xa7, 0xd3, 0xdd, 0x4f, 0x27, 0x49, 0x27, 0xcf, 0xa4, 0x02, 0xc3, 0x27, 0x3c, 0x96,
0x64, 0x24, 0x55, 0xc8, 0x36, 0x45, 0x68, 0x90, 0xea, 0x03, 0x0b, 0x46, 0xa7, 0x8d, 0xb4, 0x26, 0x91, 0x07, 0x4c,
0x80, 0x7b, 0x93, 0xa8, 0x10, 0xcb, 0x5e, 0x8d, 0x42, 0x86, 0x3e, 0x2a, 0x7c, 0x99, 0x79, 0x8e, 0xcb, 0x43, 0x28};
// Backwards compatibility alias
#define INDEX_GZ INDEX_BR
@@ -6,9 +6,6 @@
#include "esphome/core/string_ref.h"
#include "esphome/components/wifi/scan_list.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_PROVISIONING
#include "esphome/components/provisioning/provisioning.h"
#endif
#include "captive_index.h"
namespace esphome::captive_portal {
@@ -81,20 +78,6 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
void CaptivePortal::setup() {
// Disable loop by default - will be enabled when captive portal starts
this->disable_loop();
#ifdef USE_PROVISIONING
// The captive portal is a provisioning surface: once the provisioning window
// has closed, stop serving it. WiFi's own closed-callback shuts down the
// access point the portal runs on, and the gated fallback in WiFiComponent's
// loop() ensures neither is started again afterwards.
if (provisioning::global_provisioning_manager != nullptr) {
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
if (this->active_) {
ESP_LOGD(TAG, "Provisioning window closed; stopping captive portal");
this->end();
}
});
}
#endif
}
void CaptivePortal::start() {
this->base_->init();
+1 -8
View File
@@ -551,14 +551,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
void ClimateDeviceRestoreState::apply(Climate *climate) {
auto traits = climate->get_traits();
// A saved mode the device no longer offers cannot be selected again, so skip it and leave the
// entity on the mode it already has. The other saved fields are still restored.
if (traits.supports_mode(this->mode)) {
climate->mode = this->mode;
} else {
ESP_LOGW(TAG, "'%s' - Saved mode %s is no longer supported, keeping %s", climate->get_name().c_str(),
LOG_STR_ARG(climate_mode_to_string(this->mode)), LOG_STR_ARG(climate_mode_to_string(climate->mode)));
}
climate->mode = this->mode;
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
climate->target_temperature_low = this->target_temperature_low;
@@ -13,11 +13,9 @@ CONF_HEADER_LOW = "header_low"
CONF_BIT_HIGH = "bit_high"
CONF_BIT_ONE_LOW = "bit_one_low"
CONF_BIT_ZERO_LOW = "bit_zero_low"
CONF_ADVANCED_COMMANDS_SUPPORT = "advanced_commands_support"
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
{
cv.Optional(CONF_ADVANCED_COMMANDS_SUPPORT, default=False): cv.boolean,
cv.Optional(
CONF_HEADER_HIGH, default="8000us"
): cv.positive_time_period_microseconds,
@@ -40,7 +38,6 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
async def to_code(config: ConfigType) -> None:
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_advanced_commands_support(config[CONF_ADVANCED_COMMANDS_SUPPORT]))
cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
cg.add(var.set_header_low(config[CONF_HEADER_LOW]))
cg.add(var.set_bit_high(config[CONF_BIT_HIGH]))
@@ -5,85 +5,11 @@ namespace esphome::climate_ir_lg {
static const char *const TAG = "climate.climate_ir_lg";
// All codes provided here are missing the checksum (last 4 bits)
// this checksum needs to be calculated before sending (look at `calc_checksum_()`)
const uint32_t LG_HEADER = 0x8800000;
// Commands
const uint32_t COMMAND_HEADER_MASK = 0xFF000;
const uint32_t COMMAND_DATA_MASK = 0x00FF0;
const uint32_t CHECKSUM_MASK = 0xF;
const uint32_t COMMAND_MASK = 0xFF000;
const uint32_t COMMAND_OFF = 0xC0000;
const uint32_t COMMAND_SWING = 0x10000;
enum CommandBasic : uint32_t {
HEADER_BASIC = 0x10000,
BASIC_SWING_TOGGLE = 0x000,
// JET MODE (only for cooling/drying/heating modes)
// For 30 minutes: max airflow (stronger than F5 aka FAN_MAX) + PO (min/min/max temperature respectively)
// After 30 minutes: F5 aka FAN_MAX + min/min/max temperature respectively
BASIC_JET = 0x080,
};
enum CommandSys : uint32_t {
HEADER_SYS = 0xC0000,
COMMAND_OFF = 0x050,
// Also known as 'auto-dry'
AUTO_CLEAN_ON = 0x0B0,
AUTO_CLEAN_OFF = 0x0C0,
PURIFY_ON = 0x000, // From either OFF or Mode -> Purify
PURIFY_OFF = 0x080, // From Mode + Purify -> Mode
QUIET_OUTDOOR_ON = 0xA60,
QUIET_OUTDOOR_OFF = 0xA70,
// ENERGY CTRL (only in Cooling mode)
COOL_ENERG_CTRL_80 = 0x7D0, // 80%
COOL_ENERG_CTRL_60 = 0x7E0, // 60%
COOL_ENERG_CTRL_40 = 0x800, // 40%
COOL_ENERG_CTRL_OFF = 0x7F0, // OFF
DISPLAY_KW = 0x460,
LIGHT_ON_OFF = 0x0A0,
TEMP_UNIT_F = 0x170,
TEMP_UNIT_C = 0x160,
};
enum CommandAdvSwing : uint32_t {
HEADER_ADV_SWING = 0x13000,
// Only 5 bits are relevant, I got 0x13952 once - not sure what is the 8th bit so ignoring that.
ADV_SWING_DATA_MASK = 0x1F0,
// Commands for Advanced Vertical Control: Swing + 6 fixed positions
VERT_FIX_1 = 0x040, // Down
VERT_FIX_2 = 0x050,
VERT_FIX_3 = 0x060,
VERT_FIX_4 = 0x070,
VERT_FIX_5 = 0x080,
VERT_FIX_6 = 0x090, // Up
VERT_SWING_ON = 0x140, // Swing between 1 and 6
VERT_SWING_OFF = 0x150, // Stops immediately
// Commands for Advanced Horizontal Control: Swing (3 modes) + 5 fixed positions
HORI_FIX_1 = 0x0B0, // Left
HORI_FIX_2 = 0x0C0,
HORI_FIX_3 = 0x0D0,
HORI_FIX_4 = 0x0E0,
HORI_FIX_5 = 0x0F0, // Right
HORI_SWING_ON_LEFT = 0x100, // Swing between 1 and 3
HORI_SWING_ON_RIGHT = 0x110, // Swing between 3 and 5
HORI_SWING_ON_FULL = 0x160, // Swing between 1 and 5
HORI_SWING_OFF = 0x170, // Stops immediately
};
// Following commands contain mode, fan speed and temperature
// Modes
const uint32_t COMMAND_ON_COOL = 0x00000;
const uint32_t COMMAND_ON_DRY = 0x01000;
const uint32_t COMMAND_ON_FAN_ONLY = 0x02000;
@@ -97,13 +23,11 @@ const uint32_t COMMAND_AI = 0x0B000;
const uint32_t COMMAND_HEAT = 0x0C000;
// Fan speed
const uint32_t FAN_SPEED_MASK = 0xF0;
const uint32_t FAN_MASK = 0xF0;
const uint32_t FAN_AUTO = 0x50;
const uint32_t FAN_MIN = 0x00; // AKA F1
const uint32_t FAN_F2 = 0x90;
const uint32_t FAN_MED = 0x20; // AKA F3
const uint32_t FAN_F4 = 0xA0;
const uint32_t FAN_MAX = 0x40; // AKA F5
const uint32_t FAN_MIN = 0x00;
const uint32_t FAN_MED = 0x20;
const uint32_t FAN_MAX = 0x40;
// Temperature
const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1;
@@ -113,37 +37,16 @@ const uint32_t TEMP_SHIFT = 8;
const uint16_t BITS = 28;
void LgIrClimate::transmit_state() {
uint32_t remote_state = LG_HEADER;
uint32_t remote_state = 0x8800000;
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", this->modeBefore_);
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
// Set command
if (this->send_swing_cmd_) {
this->send_swing_cmd_ = false;
if (this->advanced_commands_support_) {
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
ESP_LOGD(TAG, "setting swing vertical");
remote_state |= CommandAdvSwing::HEADER_ADV_SWING;
remote_state |= CommandAdvSwing::VERT_SWING_ON;
break;
case climate::CLIMATE_SWING_OFF:
ESP_LOGD(TAG, "setting swing off");
remote_state |= CommandAdvSwing::HEADER_ADV_SWING;
remote_state |= CommandAdvSwing::VERT_SWING_OFF;
break;
default:
return;
}
this->transmit_(remote_state);
this->publish_state();
return;
} else { // just toggle swing when advanced_commands_support is not set
remote_state |= HEADER_BASIC;
remote_state |= BASIC_SWING_TOGGLE;
}
} else { // Mode commands
const bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
remote_state |= COMMAND_SWING;
} else {
bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
@@ -162,8 +65,8 @@ void LgIrClimate::transmit_state() {
break;
case climate::CLIMATE_MODE_OFF:
default:
remote_state |= CommandSys::HEADER_SYS;
remote_state |= CommandSys::COMMAND_OFF;
remote_state |= COMMAND_OFF;
break;
}
}
@@ -172,8 +75,9 @@ void LgIrClimate::transmit_state() {
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
// Set fan speed
if (this->mode !=
climate::CLIMATE_MODE_OFF) { // https://github.com/esphome/esphome/pull/10875#issuecomment-5042765948
if (this->mode == climate::CLIMATE_MODE_OFF) {
remote_state |= FAN_AUTO;
} else {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_HIGH:
remote_state |= FAN_MAX;
@@ -191,20 +95,10 @@ void LgIrClimate::transmit_state() {
}
}
uint8_t temp;
switch (this->mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
if (!this->advanced_commands_support_) { // Keep previous behavior
break;
}
[[fallthrough]];
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_HEAT:
temp = static_cast<uint8_t>(roundf(clamp<float>(this->target_temperature, TEMP_MIN, TEMP_MAX)));
remote_state |= (temp - 15) << TEMP_SHIFT;
break;
default:
break;
// Set temperature
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
auto temp = (uint8_t) roundf(clamp<float>(this->target_temperature, TEMP_MIN, TEMP_MAX));
remote_state |= ((temp - 15) << TEMP_SHIFT);
}
this->transmit_(remote_state);
@@ -230,134 +124,62 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
}
}
ESP_LOGD(TAG, "Received 0x%02" PRIX32, remote_state);
if ((remote_state & 0xFF00000) != LG_HEADER)
ESP_LOGD(TAG, "Decoded 0x%02" PRIX32, remote_state);
if ((remote_state & 0xFF00000) != 0x8800000)
return false;
// Decode commands
switch (remote_state & COMMAND_HEADER_MASK) {
case CommandSys::HEADER_SYS:
ESP_LOGD(TAG, "Got system command! With data: 0x%02" PRIX32, remote_state & COMMAND_DATA_MASK);
if ((remote_state & COMMAND_DATA_MASK) == CommandSys::COMMAND_OFF) {
this->mode = climate::CLIMATE_MODE_OFF;
} else {
return false;
}
break;
case CommandAdvSwing::HEADER_ADV_SWING:
ESP_LOGD(TAG, "Got advanced swing command! With data: 0x%02" PRIX32,
remote_state & CommandAdvSwing::ADV_SWING_DATA_MASK);
switch (remote_state & CommandAdvSwing::ADV_SWING_DATA_MASK) {
case CommandAdvSwing::VERT_SWING_ON:
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
break;
case CommandAdvSwing::VERT_SWING_OFF:
case CommandAdvSwing::VERT_FIX_1:
case CommandAdvSwing::VERT_FIX_2:
case CommandAdvSwing::VERT_FIX_3:
case CommandAdvSwing::VERT_FIX_4:
case CommandAdvSwing::VERT_FIX_5:
case CommandAdvSwing::VERT_FIX_6:
this->swing_mode = climate::CLIMATE_SWING_OFF;
break;
default:
return false; // Ignore all other (horizontal) swing commands
}
// Get command
if ((remote_state & COMMAND_MASK) == COMMAND_OFF) {
this->mode = climate::CLIMATE_MODE_OFF;
} else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) {
this->swing_mode =
this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
} else {
switch (remote_state & COMMAND_MASK) {
case COMMAND_DRY:
case COMMAND_ON_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case COMMAND_FAN_ONLY:
case COMMAND_ON_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case COMMAND_AI:
case COMMAND_ON_AI:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case COMMAND_HEAT:
case COMMAND_ON_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case COMMAND_COOL:
case COMMAND_ON_COOL:
default:
this->mode = climate::CLIMATE_MODE_COOL;
break;
}
this->publish_state();
return true;
case HEADER_BASIC:
if ((remote_state & COMMAND_DATA_MASK) == BASIC_JET) {
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_HEAT:
case climate::CLIMATE_MODE_DRY:
this->target_temperature =
this->mode == climate::CLIMATE_MODE_HEAT ? this->maximum_temperature_ : this->minimum_temperature_;
this->fan_mode = climate::CLIMATE_FAN_HIGH;
// When enabling PO(WER) also known as JET mode, swing is set to VERT_3, but after 30 mins it will switch
// back to what it was before, so let's just not change it here it at all
this->publish_state();
return true;
default:
ESP_LOGD(TAG, "Got jet command, but current mode does not support it! Ignoring.");
return false;
}
}
// Keep previous behavior in case of other BASIC command
if (this->swing_mode == climate::CLIMATE_SWING_OFF) { // Just flip between vertical and off
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
this->publish_state();
return true;
// Following commands also contain fan speed and temperature, so no 'return' in these cases
case COMMAND_DRY:
case COMMAND_ON_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case COMMAND_FAN_ONLY:
case COMMAND_ON_FAN_ONLY:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case COMMAND_AI:
case COMMAND_ON_AI:
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
case COMMAND_HEAT:
case COMMAND_ON_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case COMMAND_COOL:
case COMMAND_ON_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
default:
ESP_LOGD(TAG, "Got unknown command! Ignoring!");
return false;
}
// Decode fan speed
switch (remote_state & FAN_SPEED_MASK) {
case FAN_AUTO:
// Get fan speed
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
case FAN_MIN:
case FAN_F2:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case FAN_MED:
case FAN_F4:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
break;
case FAN_MAX:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
break;
default:
ESP_LOGD(TAG, "Got unknown fan speed! Ignoring!");
return false;
}
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_HEAT) {
if ((remote_state & FAN_MASK) == FAN_AUTO) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
} else if ((remote_state & FAN_MASK) == FAN_MIN) {
this->fan_mode = climate::CLIMATE_FAN_LOW;
} else if ((remote_state & FAN_MASK) == FAN_MED) {
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
} else if ((remote_state & FAN_MASK) == FAN_MAX) {
this->fan_mode = climate::CLIMATE_FAN_HIGH;
}
}
// Keep previous behavior
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL && !(this->advanced_commands_support_)) {
this->fan_mode = climate::CLIMATE_FAN_AUTO;
}
// Decode temperature for modes that support it
switch (this->mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_HEAT:
// Get temperature
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
break;
default:
break;
}
}
this->mode_before_ = this->mode;
this->publish_state();
return true;
@@ -385,14 +207,14 @@ void LgIrClimate::transmit_(uint32_t value) {
data->mark(this->bit_high_);
transmit.perform();
}
void LgIrClimate::calc_checksum_(uint32_t &value) {
uint32_t mask = 0xF;
uint32_t sum = 0;
for (uint8_t i = 1; i < 8; i++) {
sum += (value & (CHECKSUM_MASK << (i * 4))) >> (i * 4);
sum += (value & (mask << (i * 4))) >> (i * 4);
}
value |= (sum & CHECKSUM_MASK);
value |= (sum & mask);
}
} // namespace esphome::climate_ir_lg
@@ -21,13 +21,12 @@ class LgIrClimate final : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override {
this->send_swing_cmd_ = call.get_swing_mode().has_value();
// swing resets after unit powered off, except when advanced_commands_support_ is set
// swing resets after unit powered off
auto mode = call.get_mode();
if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF && !(this->advanced_commands_support_))
if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF)
this->swing_mode = climate::CLIMATE_SWING_OFF;
climate_ir::ClimateIR::control(call);
}
void set_advanced_commands_support(bool value) { this->advanced_commands_support_ = value; }
void set_header_high(uint32_t header_high) { this->header_high_ = header_high; }
void set_header_low(uint32_t header_low) { this->header_low_ = header_low; }
void set_bit_high(uint32_t bit_high) { this->bit_high_ = bit_high; }
@@ -45,7 +44,6 @@ class LgIrClimate final : public climate_ir::ClimateIR {
void calc_checksum_(uint32_t &value);
void transmit_(uint32_t value);
bool advanced_commands_support_{false};
uint32_t header_high_;
uint32_t header_low_;
uint32_t bit_high_;
+3 -5
View File
@@ -116,16 +116,14 @@ _CALLBACK_AUTOMATIONS = (
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
# Initialize sensor storage with count from final_validate before any
# await, so platform to_code() calls always see it initialized
# regardless of YAML key order.
# Initialize sensor storage with count from final_validate
sensor_count = _get_data().sensor_counts.get(str(config[CONF_ID]), 0)
if sensor_count > 0:
cg.add(var.init_sensors(sensor_count))
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
+13 -5
View File
@@ -1105,11 +1105,19 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
return config
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ESP_IDF)
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
# Runs before _detect_variant so downstream validators can rely on
# CORE.toolchain instead of re-resolving it from the config dict.
_resolve_toolchain = cv.resolve_toolchain("ESP32", _TOOLCHAINS, Toolchain.ESP_IDF)
def _validate_toolchain(value) -> Toolchain:
return Toolchain(
cv.one_of(Toolchain.PLATFORMIO, Toolchain.ESP_IDF, lower=True)(value)
)
def _resolve_toolchain(value: ConfigType) -> ConfigType:
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
# Runs before _detect_variant so downstream validators can rely on
# CORE.toolchain instead of re-resolving it from the config dict.
if CORE.toolchain is None:
CORE.toolchain = value.get(CONF_TOOLCHAIN, Toolchain.ESP_IDF)
return value
def _check_versions(config: ConfigType) -> ConfigType:
-8
View File
@@ -2,7 +2,6 @@
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -30,13 +29,6 @@ void loop_task(void *pv_params) {
}
extern "C" void app_main() {
// Apply the custom eFuse MAC (if burned and valid) as the base MAC before any
// interface (Wi-Fi, Ethernet, Bluetooth, 802.15.4) derives its address from it.
// The logger does not exist yet, so only log-free helpers may be used here.
uint8_t mac[MAC_ADDRESS_SIZE];
if (get_custom_mac_address(mac)) {
set_mac_address(mac);
}
initArduino();
esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE
+8 -17
View File
@@ -71,32 +71,23 @@ static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK &
static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits
// Must not use the ESPHome logger (may run before it exists, e.g. from app_main()).
bool get_custom_mac_address(uint8_t *mac) {
// has_custom_mac_address() checks the raw eFuse field, while the reads below select their
// method differently and may still fail (CRC), so the result must be validated again.
if (!has_custom_mac_address())
return false;
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
return read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS));
#else
return read_valid_mac(mac, esp_efuse_mac_get_custom(mac));
#endif
}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
if (get_custom_mac_address(mac)) {
return;
}
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
// returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
// This already reads raw eFuse bytes, so there is no CRC-bypass fallback
// Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback
// (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks).
if (has_custom_mac_address() &&
read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) {
return;
}
if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) {
return;
}
#else
if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) {
return;
}
if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) {
return;
}
@@ -1,7 +1,5 @@
#include "esp32_improv_component.h"
#include <array>
#include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble_server/ble_2902.h"
@@ -21,13 +19,7 @@ using namespace bytebuffer;
static const char *const TAG = "esp32_improv.component";
static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
// command + data length + trailing byte
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
// Reserves the ESPHOME_MY_LINK entry; a maximal next URL displaces only the
// lower value web server URL
static constexpr size_t MAX_NEXT_URL_LEN =
improv::RPC_RESPONSE_MAX_SIZE - RPC_RESPONSE_OVERHEAD - 1 - sizeof(ESPHOME_MY_LINK);
static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
static constexpr uint16_t STOP_ADVERTISING_DELAY =
10000; // Delay (ms) before stopping service to allow BLE clients to read the final state
static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds
@@ -293,9 +285,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
}
}
void ESP32ImprovComponent::send_response_(std::span<const uint8_t> response) {
// The BLE characteristic owns its value, so one exact-size copy is required here
this->rpc_response_->set_value(std::vector<uint8_t>(response.begin(), response.end()));
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
this->rpc_response_->set_value(std::move(response));
if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify();
}
@@ -439,35 +430,40 @@ void ESP32ImprovComponent::check_wifi_connection_() {
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
// Build the URL list directly into a stack buffer with no heap allocation
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS);
// Build URL list with minimal allocations
// Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
std::string url_strings[3];
size_t url_count = 0;
#ifdef USE_ESP32_IMPROV_NEXT_URL
// Add next_url if configured (should be first per Improv BLE spec)
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
{
char url_buffer[384];
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
if (len > 0) {
url_strings[url_count++] = std::string(url_buffer, len);
}
}
#endif
// Add default URLs for backward compatibility; MAX_NEXT_URL_LEN reserves this
// entry's space, so it always fits
builder.add_string(ESPHOME_MY_LINK, sizeof(ESPHOME_MY_LINK) - 1);
// Add default URLs for backward compatibility
url_strings[url_count++] = ESPHOME_MY_LINK;
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
size_t len =
buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
// "http://" (7) + IPv4 max (15) + ":" (1) + port max (5) + null = 29
char url_buffer[32];
memcpy(url_buffer, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates
ip.str_to(url_buffer + 7);
size_t len = strlen(url_buffer);
snprintf(url_buffer + len, sizeof(url_buffer) - len, ":%d", USE_WEBSERVER_PORT);
url_strings[url_count++] = url_buffer;
break;
}
}
#endif
this->send_response_(builder.finish());
this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
std::vector<std::string>(url_strings, url_strings + url_count)));
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally");
}
@@ -22,7 +22,6 @@
#include "esphome/components/output/binary_output.h"
#endif
#include <span>
#include <vector>
#ifdef USE_ESP32
@@ -110,7 +109,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
void set_state_(improv::State state, bool update_advertising = true);
void set_error_(improv::Error error);
improv::State get_initial_state_() const;
void send_response_(std::span<const uint8_t> response);
void send_response_(std::vector<uint8_t> &&response);
void process_incoming_data_();
void on_wifi_connect_timeout_();
void check_wifi_connection_();
+1 -13
View File
@@ -137,16 +137,7 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if ver <= cv.Version(2, 6, 2):
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# Same encoding the native toolchain uses for its package download, so a
# version bump cannot drift between the two paths.
from esphome.arduino8266.framework import framework_package_version
try:
return f"~{framework_package_version(ver)}"
except EsphomeError as err:
# Anchor the 4.x rejection to the framework version line instead of
# aborting with a bare traceback-level error
raise cv.Invalid(str(err), path=[CONF_VERSION]) from err
return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# NOTE: Keep this in mind when updating the recommended version:
@@ -256,9 +247,6 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
}
),
# Until the native toolchain lands, PlatformIO is the only backend;
# reject a --toolchain this platform cannot serve yet.
cv.require_platformio_toolchain("ESP8266"),
set_core_data,
)
@@ -71,33 +71,6 @@ def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> P
return components_dir
def _log_overridden_components(
conf: dict[str, Any], component_names: list[str]
) -> None:
overridden = [
name
for name in component_names
if (loader.CORE_COMPONENTS_PATH / name / "__init__.py").is_file()
]
if not overridden:
return
if conf[CONF_TYPE] == TYPE_GIT:
source = conf[CONF_URL]
if ref := conf.get(CONF_REF):
source = f"{source}@{ref}"
if path := conf.get(CONF_PATH):
source = f"{source} ({path})"
else:
source = conf[CONF_PATH]
_LOGGER.info(
"External components are overriding built-in components:\n"
" source: %s\n"
" components: %s",
source,
", ".join(sorted(overridden)),
)
def _process_single_config(config: dict[str, Any]) -> None:
conf = config[CONF_SOURCE]
if conf[CONF_TYPE] == TYPE_GIT:
@@ -111,8 +84,8 @@ def _process_single_config(config: dict[str, Any]) -> None:
raise NotImplementedError
if config[CONF_COMPONENTS] == "all":
component_names = [p.parent.name for p in components_dir.glob("*/__init__.py")]
if len(component_names) > 100:
num_components = len(list(components_dir.glob("*/__init__.py")))
if num_components > 100:
# Prevent accidentally including all components from an esphome fork/branch
# In this case force the user to manually specify which components they want to include
raise cv.Invalid(
@@ -129,9 +102,6 @@ def _process_single_config(config: dict[str, Any]) -> None:
[CONF_COMPONENTS, i],
)
allowed_components = config[CONF_COMPONENTS]
component_names = allowed_components
_log_overridden_components(conf, component_names)
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
@@ -1,43 +0,0 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import ICON_AIR_FILTER
from esphome.types import ConfigType
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
DEPENDENCIES = ["hoermann_hcp"]
CONF_HALF_OPEN = "half_open"
CONF_VENT = "vent"
ICON_GARAGE_OPEN_VARIANT = "mdi:garage-open-variant"
HoermannHcpVentButton = hoermann_hcp_ns.class_("HoermannHcpVentButton", button.Button)
HoermannHcpHalfOpenButton = hoermann_hcp_ns.class_(
"HoermannHcpHalfOpenButton", button.Button
)
BUTTON_KEYS = (CONF_VENT, CONF_HALF_OPEN)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
cv.Optional(CONF_VENT): button.button_schema(
HoermannHcpVentButton, icon=ICON_AIR_FILTER
),
cv.Optional(CONF_HALF_OPEN): button.button_schema(
HoermannHcpHalfOpenButton, icon=ICON_GARAGE_OPEN_VARIANT
),
}
),
cv.has_at_least_one_key(*BUTTON_KEYS),
)
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
for key in BUTTON_KEYS:
if (conf := config.get(key)) is not None:
await button.new_button(conf, parent)
@@ -1,34 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../hoermann_hcp.h"
namespace esphome::hoermann_hcp {
// The door commands the cover has no equivalent for. A refused command is already reported by the hub and
// leaves nothing to correct here, because a button carries no state of its own.
class HoermannHcpButton : public button::Button {
public:
explicit HoermannHcpButton(HoermannHcp *parent) : parent_(parent) {}
protected:
HoermannHcp *const parent_;
};
class HoermannHcpVentButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->vent_door(); }
};
class HoermannHcpHalfOpenButton final : public HoermannHcpButton {
public:
using HoermannHcpButton::HoermannHcpButton;
protected:
void press_action() override { this->parent_->half_open_door(); }
};
} // namespace esphome::hoermann_hcp
@@ -22,9 +22,6 @@ static constexpr uint8_t MAX_LIGHT_TOGGLES_IN_FLIGHT = 4;
static constexpr HoermannHcpCommand COMMAND_OPEN{"open", 0x0210, 0x0110};
static constexpr HoermannHcpCommand COMMAND_CLOSE{"close", 0x0220, 0x0120};
static constexpr HoermannHcpCommand COMMAND_IMPULSE{"impulse", 0x0240, 0x0140};
// The intermediate positions are named in the second register, so the first only carries the phase.
static constexpr HoermannHcpCommand COMMAND_VENT{"vent", 0x0200, 0x0100, 0x4000, 0x4000};
static constexpr HoermannHcpCommand COMMAND_HALF_OPEN{"half open", 0x0200, 0x0100, 0x0400, 0x0400};
// The lamp is named in the second register, but its phase bytes follow no scheme the door commands share.
static constexpr HoermannHcpCommand COMMAND_TOGGLE_LAMP{"toggle light", 0x0100, 0x0800, 0x0200, 0x0200, false};
@@ -289,8 +286,6 @@ bool HoermannHcp::queue_command_(const HoermannHcpCommand &command) {
bool HoermannHcp::open_door() { return this->queue_command_(COMMAND_OPEN); }
bool HoermannHcp::close_door() { return this->queue_command_(COMMAND_CLOSE); }
bool HoermannHcp::impulse_door() { return this->queue_command_(COMMAND_IMPULSE); }
bool HoermannHcp::vent_door() { return this->queue_command_(COMMAND_VENT); }
bool HoermannHcp::half_open_door() { return this->queue_command_(COMMAND_HALF_OPEN); }
bool HoermannHcp::toggle_light() {
if (this->light_toggles_in_flight_ >= MAX_LIGHT_TOGGLES_IN_FLIGHT) {
ESP_LOGW(TAG, "Too many lamp toggles are still waiting to be confirmed, dropping this one");
@@ -22,8 +22,7 @@ enum class DoorState : uint8_t {
};
// A HCP command is a simulated key press: the pressed value is presented to the bus controller, then after a
// short delay the released value. Each half also carries a second register, which names the buttons that do
// not fit into the first.
// short delay the released value. Each half also carries a second register, which only the lamp command uses.
struct HoermannHcpCommand {
const char *name;
uint16_t pressed_value;
@@ -55,9 +54,6 @@ class HoermannHcp : public PollingComponent, public modbus::ModbusServerDevice {
bool open_door();
bool close_door();
bool impulse_door();
// The door drives to these intermediate positions on its own, so neither takes a target to be stopped at.
bool vent_door();
bool half_open_door();
bool stop_door();
bool set_position(float position);
bool toggle_light();
-2
View File
@@ -37,7 +37,6 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
}
),
cv.require_platformio_toolchain("host"),
set_core_data,
)
@@ -50,7 +49,6 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host")
cg.add_define("ESPHOME_VARIANT", "HOST")
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off")
+1 -33
View File
@@ -17,14 +17,12 @@ from esphome.const import (
CONF_TIMEOUT,
CONF_URL,
CONF_WATCHDOG_TIMEOUT,
PLATFORM_ESP32,
PLATFORM_HOST,
PlatformFramework,
__version__,
)
from esphome.core import CORE, ID, Lambda, TimePeriodMilliseconds
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import IS_MACOS
from esphome.types import ConfigType
@@ -96,34 +94,6 @@ def validate_ssl_verification(config: ConfigType) -> ConfigType:
return config
# esp_http_client_open() runs DNS, TCP connect and the TLS handshake with no
# watchdog feed in between; each can take up to `timeout` on ESP-IDF.
WATCHDOG_TIMEOUT_MULTIPLIER = 3
# Headroom over the exact worst case so a fully stalled open does not land on
# the watchdog deadline.
WATCHDOG_TIMEOUT_MARGIN_MS = 1000
def default_watchdog_timeout(config: ConfigType) -> None:
"""Arm the request watchdog on ESP32 when the user did not set it.
The default never goes below the platform task watchdog, so a user who
widened `esp32.watchdog_timeout` keeps that window during requests.
"""
if not CORE.is_esp32 or CONF_WATCHDOG_TIMEOUT in config:
return
derived_ms = (
config[CONF_TIMEOUT].total_milliseconds * WATCHDOG_TIMEOUT_MULTIPLIER
+ WATCHDOG_TIMEOUT_MARGIN_MS
)
platform_ms = fv.full_config.get()[PLATFORM_ESP32][
CONF_WATCHDOG_TIMEOUT
].total_milliseconds
config[CONF_WATCHDOG_TIMEOUT] = TimePeriodMilliseconds(
milliseconds=max(derived_ms, platform_ms)
)
def _declare_request_class(value: Any) -> ID:
if CORE.is_host:
return cv.declare_id(HttpRequestHost)(value)
@@ -183,8 +153,6 @@ CONFIG_SCHEMA = cv.All(
validate_ssl_verification,
)
FINAL_VALIDATE_SCHEMA = default_watchdog_timeout
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -142,13 +142,12 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
const char *buf = body.c_str();
while (write_left > 0) {
int written = esp_http_client_write(client, buf + write_index, write_left);
if (written <= 0) {
if (written < 0) {
err = ESP_FAIL;
break;
}
write_left -= written;
write_index += written;
container->feed_wdt();
}
}
+1 -1
View File
@@ -43,4 +43,4 @@ async def setup_improv_core(var: MockObj, config: ConfigType, component: str) ->
cg.add(var.set_next_url(_process_next_url(next_url)))
cg.add_define(f"USE_{component.upper()}_NEXT_URL")
cg.add_library("improv/Improv", "1.2.7")
cg.add_library("improv/Improv", "1.2.6")
@@ -4,13 +4,10 @@
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
namespace esphome::improv_base {
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
static const char *const TAG = "improv_base";
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
@@ -65,21 +62,6 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) {
*out = '\0';
return out - buffer;
}
void ImprovBase::add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len) {
// The builder rejects strings above 254 bytes, so anything longer than this
// buffer could never be sent anyway
char url_buffer[256];
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
if (len == 0) {
return;
}
// max_len is the transport's budget for this entry; skipping an over-long URL
// here keeps the rest of the response sendable instead of oversizing the frame
if (len > max_len || !builder.add_string(url_buffer, len)) {
ESP_LOGW(TAG, "Next URL too long; skipping");
}
}
#endif
} // namespace esphome::improv_base
@@ -3,10 +3,6 @@
#include <cstddef>
#include "esphome/core/defines.h"
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#include <improv.h>
#endif
namespace esphome::improv_base {
class ImprovBase {
@@ -19,8 +15,6 @@ class ImprovBase {
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
/// Format next_url_ into buffer, replacing placeholders. Returns length written.
size_t get_formatted_next_url_(char *buffer, size_t buffer_size);
/// Append the formatted next_url to the RPC response, warning if it does not fit.
void add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len);
const char *next_url_{nullptr};
#endif
};
+5 -36
View File
@@ -1,15 +1,9 @@
import esphome.codegen as cg
from esphome.components import improv_base, uart
from esphome.components import improv_base
from esphome.components.esp32 import VARIANT_ESP32S3, get_esp32_variant
from esphome.components.logger import USB_CDC
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_HARDWARE_UART,
CONF_ID,
CONF_LOGGER,
CONF_UART_ID,
)
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -23,35 +17,13 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial")
ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ImprovSerialComponent),
# YAML only: rewiring Improv onto another UART is not a knob for a
# visual editor and the device builder must not expose it
cv.Optional(CONF_UART_ID, visibility=cv.Visibility.YAML_ONLY): cv.use_id(
uart.UARTComponent
),
}
)
cv.Schema({cv.GenerateID(): cv.declare_id(ImprovSerialComponent)})
.extend(improv_base.IMPROV_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
_UART_FINAL_VALIDATE = uart.final_validate_device_schema(
"improv_serial", require_tx=True, require_rx=True
)
def validate_transport(config: ConfigType) -> None:
if CONF_UART_ID in config:
# A dedicated UART bus is used; the logger's serial settings are irrelevant,
# but the bus itself must be bidirectional and not claimed by another device
_UART_FINAL_VALIDATE(config)
return
# The host logger has no serial port for Improv to share
if CORE.is_host:
raise cv.Invalid("improv_serial on the host platform requires uart_id")
def validate_logger(config: ConfigType) -> None:
logger_conf = fv.full_config.get()[CONF_LOGGER]
if logger_conf[CONF_BAUD_RATE] == 0:
raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
@@ -64,7 +36,7 @@ def validate_transport(config: ConfigType) -> None:
)
FINAL_VALIDATE_SCHEMA = validate_transport
FINAL_VALIDATE_SCHEMA = validate_logger
async def to_code(config: ConfigType) -> None:
@@ -72,6 +44,3 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await improv_base.setup_improv_core(var, config, "improv_serial")
cg.add_define("USE_IMPROV_SERIAL")
if (uart_id := config.get(CONF_UART_ID)) is not None:
cg.add(var.set_uart(await cg.get_variable(uart_id)))
cg.add_define("USE_IMPROV_SERIAL_UART")
@@ -9,17 +9,13 @@
#include "esphome/components/logger/logger.h"
#include "esphome/components/wifi/scan_list.h"
#include <array>
namespace esphome::improv_serial {
static const char *const TAG = "improv_serial";
void ImprovSerialComponent::setup() {
global_improv_serial_component = this;
#ifdef USE_IMPROV_SERIAL_UART
// Transport is a dedicated UART bus set via set_uart() in generated code
#elif defined(USE_ESP32)
#ifdef USE_ESP32
this->uart_num_ = logger::global_logger->get_uart_num();
this->uart_selection_ = logger::global_logger->get_uart();
#elif defined(USE_ARDUINO)
@@ -63,7 +59,8 @@ void ImprovSerialComponent::loop() {
this->cancel_timeout("wifi-connect-timeout");
this->set_state_(improv::STATE_PROVISIONED);
this->send_settings_response_(improv::WIFI_SETTINGS);
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
this->send_response_(url);
}
}
}
@@ -92,13 +89,7 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
}
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
#ifdef USE_IMPROV_SERIAL_UART
this->uart_->write_array(this->tx_header_, header_tx_len);
if (there_is_data) {
this->uart_->write_array(data, size);
this->uart_->write_array(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
}
#elif defined(USE_ESP32)
#ifdef USE_ESP32
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
@@ -143,11 +134,16 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
#endif
}
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
std::vector<std::string> urls;
#ifdef USE_IMPROV_SERIAL_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
{
char url_buffer[384];
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
if (len > 0) {
urls.emplace_back(url_buffer, len);
}
}
#endif
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
@@ -156,63 +152,25 @@ void ImprovSerialComponent::send_settings_response_(improv::Command command) {
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
// buf_append_printf keeps the format string in flash on ESP8266
size_t len =
buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
snprintf(webserver_url, sizeof(webserver_url), "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
urls.emplace_back(webserver_url);
break;
}
}
#endif
this->send_response_(builder.finish(false));
std::vector<uint8_t> data = improv::build_rpc_response(command, urls, false);
return data;
}
void ImprovSerialComponent::send_version_info_() {
// Entry cost per field is sizeof(lit): a length byte plus the string
std::vector<uint8_t> ImprovSerialComponent::build_version_info_() {
#ifdef ESPHOME_PROJECT_NAME
static constexpr size_t INFO_ENTRIES_LEN =
sizeof(ESPHOME_PROJECT_NAME) + sizeof(ESPHOME_PROJECT_VERSION) + sizeof(ESPHOME_VARIANT);
std::vector<std::string> infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()};
#else
static constexpr size_t INFO_ENTRIES_LEN = sizeof("ESPHome") + sizeof(ESPHOME_VERSION) + sizeof(ESPHOME_VARIANT);
std::vector<std::string> infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()};
#endif
static_assert(INFO_ENTRIES_LEN < MAX_SERIAL_PAYLOAD,
"esphome project name and version too long for the improv_serial device info frame");
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, improv::GET_DEVICE_INFO);
#ifdef USE_ESP8266
// Keep each literal in flash and copy it through an exact size stack buffer,
// so a long project name or version can never be truncated
#define IMPROV_ADD_INFO(lit) \
do { \
static const char progmem_str[] PROGMEM = lit; \
char tmp[sizeof(lit)]; \
progmem_memcpy(tmp, progmem_str, sizeof(lit)); \
builder.add_string(tmp, sizeof(lit) - 1); \
} while (0)
#else
// Literals are directly flash mapped on all other platforms
#define IMPROV_ADD_INFO(lit) builder.add_string(lit, sizeof(lit) - 1)
#endif
#ifdef ESPHOME_PROJECT_NAME
IMPROV_ADD_INFO(ESPHOME_PROJECT_NAME);
IMPROV_ADD_INFO(ESPHOME_PROJECT_VERSION);
#else
IMPROV_ADD_INFO("ESPHome");
IMPROV_ADD_INFO(ESPHOME_VERSION);
#endif
IMPROV_ADD_INFO(ESPHOME_VARIANT);
#undef IMPROV_ADD_INFO
// Only the device name length is unknown at compile time
const auto &name = App.get_name();
if (INFO_ENTRIES_LEN + 1 + name.size() <= MAX_SERIAL_PAYLOAD) {
builder.add_string(name.c_str(), name.size());
} else {
ESP_LOGW(TAG, "Response full; device name dropped");
}
this->send_response_(builder.finish(false));
}
std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false);
return data;
};
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
size_t at = this->rx_buffer_.size();
@@ -263,35 +221,32 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
}
this->set_state_(this->state_);
if (this->state_ == improv::STATE_PROVISIONED) {
this->send_settings_response_(improv::GET_CURRENT_STATE);
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE);
this->send_response_(url);
}
return true;
case improv::GET_DEVICE_INFO: {
this->send_version_info_();
std::vector<uint8_t> info = this->build_version_info_();
this->send_response_(info);
return true;
}
case improv::GET_WIFI_NETWORKS: {
const auto &results = wifi::global_wifi_component->get_scan_result();
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
for (const auto &scan : results) {
bool with_auth = false;
if (!wifi::should_show_scan_entry(results, scan, with_auth))
continue;
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
char *rssi_end = int8_to_str(rssi_buf, scan.get_rssi());
*rssi_end = '\0';
improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS);
// SSID(32) + RSSI(4) + YESNO(3) entries always fit the payload
const auto &ssid = scan.get_ssid();
builder.add_string(ssid.c_str(), ssid.size());
builder.add_string(rssi_buf, rssi_end - rssi_buf);
builder.add_string(YESNO(with_auth));
this->send_response_(builder.finish(false));
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data = improv::build_rpc_response(
improv::GET_WIFI_NETWORKS, {scan.get_ssid().str(), rssi_buf, YESNO(with_auth)}, false);
this->send_response_(data);
}
// Send empty response to signify the end of the list.
improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS);
this->send_response_(builder.finish(false));
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector<std::string>{}, false);
this->send_response_(data);
return true;
}
default: {
@@ -319,14 +274,7 @@ void ImprovSerialComponent::set_error_(improv::Error error) {
this->write_data_();
}
void ImprovSerialComponent::send_response_(std::span<const uint8_t> response) {
// The serial frame length field is a single byte
if (response.size() > MAX_SERIAL_RESPONSE) {
ESP_LOGE(TAG, "Response too long");
// Fail fast instead of leaving the client to wait out its timeout
this->set_error_(improv::ERROR_UNKNOWN);
return;
}
void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
this->write_data_(response.data(), response.size());
}
@@ -8,12 +8,9 @@
#include "esphome/core/helpers.h"
#ifdef USE_WIFI
#include <improv.h>
#include <span>
#include <vector>
#ifdef USE_IMPROV_SERIAL_UART
#include "esphome/components/uart/uart_component.h"
#elif defined(USE_ESP32)
#ifdef USE_ESP32
#include <driver/uart.h>
#ifdef USE_LOGGER_USB_SERIAL_JTAG
#include <driver/usb_serial_jtag.h>
@@ -48,22 +45,6 @@ enum ImprovSerialType : uint8_t {
static const uint16_t IMPROV_SERIAL_TIMEOUT = 100;
static const uint8_t IMPROV_SERIAL_VERSION = 1;
// The serial frame length field is one byte
static constexpr size_t MAX_SERIAL_RESPONSE = 255;
// command + data length + trailing byte
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD;
#ifdef USE_WEBSERVER
// length byte + "http://" + IPv4 + ":" + port
static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5;
#else
static constexpr size_t WEBSERVER_URL_RESERVE = 0;
#endif
// Entry budget minus its own length byte
static constexpr size_t MAX_NEXT_URL_LEN = MAX_SERIAL_PAYLOAD - WEBSERVER_URL_RESERVE - 1;
static_assert(MAX_SERIAL_RESPONSE <= improv::RPC_RESPONSE_MAX_SIZE, "builder buffer too small for the frame");
class ImprovSerialComponent final : public Component, public improv_base::ImprovBase {
public:
void setup() override;
@@ -72,10 +53,6 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
#ifdef USE_IMPROV_SERIAL_UART
void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; }
#endif
protected:
bool parse_improv_serial_byte_(uint8_t byte);
bool parse_improv_payload_(improv::ImprovCommand &command);
@@ -83,20 +60,16 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
void set_state_(improv::State state);
void send_current_state_(improv::State state);
void set_error_(improv::Error error);
void send_response_(std::span<const uint8_t> response);
void send_response_(std::vector<uint8_t> &response);
void on_wifi_connect_timeout_();
void send_settings_response_(improv::Command command);
void send_version_info_();
std::vector<uint8_t> build_rpc_settings_response_(improv::Command command);
std::vector<uint8_t> build_version_info_();
ESPHOME_ALWAYS_INLINE optional<uint8_t> read_byte_() {
optional<uint8_t> byte;
uint8_t data = 0;
#ifdef USE_IMPROV_SERIAL_UART
if (this->uart_->available() && this->uart_->read_byte(&data)) {
byte = data;
}
#elif defined(USE_ESP32)
#ifdef USE_ESP32
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
@@ -156,9 +129,7 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
'\n',
};
#ifdef USE_IMPROV_SERIAL_UART
uart::UARTComponent *uart_{nullptr};
#elif defined(USE_ESP32)
#ifdef USE_ESP32
uart_port_t uart_num_;
logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0};
#elif defined(USE_ARDUINO)
+1 -2
View File
@@ -300,7 +300,7 @@ FRAMEWORK_SCHEMA = cv.All(
_check_debug_order,
)
CONFIG_SCHEMA = cv.All(_notify_old_style, cv.require_platformio_toolchain("LibreTiny"))
CONFIG_SCHEMA = cv.All(_notify_old_style)
BASE_SCHEMA = cv.Schema(
{
@@ -314,7 +314,6 @@ BASE_SCHEMA = cv.Schema(
)
BASE_SCHEMA.add_extra(_detect_variant)
BASE_SCHEMA.add_extra(cv.require_platformio_toolchain("LibreTiny"))
BASE_SCHEMA.add_extra(_update_core_data)
+4 -50
View File
@@ -525,52 +525,6 @@ void IndicatorLine::update_length_() {
}
#endif
#ifdef USE_LVGL_TABLE
uint32_t lv_table_get_selected_row(lv_obj_t *obj) {
uint32_t row;
uint32_t column;
lv_table_get_selected_cell(obj, &row, &column);
return row;
}
uint32_t lv_table_get_selected_column(lv_obj_t *obj) {
uint32_t row;
uint32_t column;
lv_table_get_selected_cell(obj, &row, &column);
return column;
}
void LvTableType::set_obj(lv_obj_t *lv_obj) {
LvCompound::set_obj(lv_obj);
lv_obj_add_event_cb(
lv_obj,
[](lv_event_t *e) {
auto *table = static_cast<LvTableType *>(lv_event_get_user_data(e));
table->update_column_widths_();
},
LV_EVENT_SIZE_CHANGED, this);
}
void LvTableType::add_column_width_pct(uint32_t col, uint8_t pct) {
for (auto &i : this->column_pct_) {
if (i.col == col) {
i.pct = pct;
this->update_column_widths_();
return;
}
}
this->column_pct_.push_back({col, pct});
this->update_column_widths_();
}
void LvTableType::update_column_widths_() {
auto content_width = lv_obj_get_content_width(this->obj);
for (const auto &col : this->column_pct_) {
lv_table_set_column_width(this->obj, col.col, content_width * col.pct / 100);
}
}
#endif // USE_LVGL_TABLE
#ifdef USE_LVGL_KEY_LISTENER
LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time) {
this->drv_ = lv_indev_create();
@@ -597,21 +551,21 @@ std::string LvSelectable::get_selected_text() {
return this->options_[selected];
}
static std::string join_string(const FixedVector<const char *> &options) {
static std::string join_string(std::vector<std::string> options) {
return std::accumulate(
options.begin(), options.end(), std::string(),
[](const std::string &a, const char *b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; });
[](const std::string &a, const std::string &b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; });
}
void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) {
auto *index = std::find(this->options_.begin(), this->options_.end(), text);
auto index = std::find(this->options_.begin(), this->options_.end(), text);
if (index != this->options_.end()) {
this->set_selected_index(index - this->options_.begin(), anim);
lv_obj_send_event(this->obj, lv_update_event, nullptr);
}
}
void LvSelectable::set_options(FixedVector<const char *> options) {
void LvSelectable::set_options(std::vector<std::string> options) {
auto index = this->get_selected_index();
if (index >= options.size())
index = options.size() - 1;
+3 -28
View File
@@ -58,10 +58,6 @@ lv_obj_t *lv_container_create(lv_obj_t *parent);
void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start,
lv_color_t color_end, int width, bool local);
#endif
#ifdef USE_LVGL_TABLE
uint32_t lv_table_get_selected_row(lv_obj_t *obj);
uint32_t lv_table_get_selected_column(lv_obj_t *obj);
#endif
#if LV_COLOR_DEPTH == 16
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
#elif LV_COLOR_DEPTH == 32
@@ -515,27 +511,6 @@ class LvLineType : public LvCompound {
FixedVector<lv_point_precise_t> points_{};
};
#endif
#ifdef USE_LVGL_TABLE
// Unlike most size properties, lv_table_set_column_width() only accepts a literal pixel
// count, so percentage column widths must be recomputed by hand whenever the table's own
// content width changes.
class LvTableType : public LvCompound {
public:
void set_obj(lv_obj_t *lv_obj) override;
// count is the number of percentage-width columns, known at code-generation time.
void init_column_pct(size_t count) { this->column_pct_.init(count); }
void add_column_width_pct(uint32_t col, uint8_t pct);
protected:
void update_column_widths_();
struct ColumnPct {
uint32_t col;
uint8_t pct;
};
FixedVector<ColumnPct> column_pct_{};
};
#endif // USE_LVGL_TABLE
#if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER)
class LvSelectable : public LvCompound {
public:
@@ -543,12 +518,12 @@ class LvSelectable : public LvCompound {
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
void set_selected_text(const std::string &text, lv_anim_enable_t anim);
std::string get_selected_text();
const FixedVector<const char *> &get_options() { return this->options_; }
void set_options(FixedVector<const char *> options);
const std::vector<std::string> &get_options() { return this->options_; }
void set_options(std::vector<std::string> options);
protected:
virtual void set_option_string(const char *options) = 0;
FixedVector<const char *> options_{};
std::vector<std::string> options_{};
};
#ifdef USE_LVGL_DROPDOWN
+12 -3
View File
@@ -50,10 +50,19 @@ class LVGLSelect final : public select::Select, public Component {
protected:
void control(size_t index) override {
this->widget_->set_selected_index(index, this->anim_);
// The update event fires the widget's on_value/on_update triggers
lv_obj_send_event(this->widget_->obj, lv_update_event, nullptr);
this->publish();
}
void set_options_() {
// Widget uses std::vector<std::string>, SelectTraits uses FixedVector<const char*>
// Convert by extracting c_str() pointers
const auto &opts = this->widget_->get_options();
FixedVector<const char *> opt_ptrs;
opt_ptrs.init(opts.size());
for (const auto &opt : opts) {
opt_ptrs.push_back(opt.c_str());
}
this->traits.set_options(opt_ptrs);
}
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
LvSelectable *widget_;
lv_anim_enable_t anim_;
-3
View File
@@ -3,8 +3,6 @@ from esphome.const import CONF_TEXT, CONF_VALUE
from esphome.cpp_generator import MockObj
from esphome.cpp_types import Component, esphome_ns
from .defines import CONF_SELECTED_INDEX
class LvType(cg.MockObjClass):
def __init__(self, *args, **kwargs):
@@ -114,4 +112,3 @@ class LvSelect(LvType):
parents=parens,
**kwargs,
)
self.value_property = CONF_SELECTED_INDEX
-280
View File
@@ -1,280 +0,0 @@
from contextlib import ExitStack
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_ROWS
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROW, CONF_TEXT, CONF_WIDTH
from esphome.core import ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.schema_extractors import SCHEMA_EXTRACT
from esphome.types import ConfigFragmentType, ConfigType, SafeExpType
from ..automation import action_to_code
from ..defines import CONF_COLUMN, CONF_MAIN, LValidator, literal
from ..lv_validation import lv_int, lv_text, pixels_or_percent, pixels_validator
from ..lvcode import LocalVariable, lv, lv_add, lv_expr
from ..types import LvCompound, LvType, ObjUpdateAction, lv_coord_t
from . import Widget, WidgetType, get_widgets
from .label import CONF_LABEL
CONF_TABLE = "table"
CONF_CELLS = "cells"
CONF_COLUMNS = "columns"
CONF_ROW_COUNT = "row_count"
CONF_COLUMN_COUNT = "column_count"
CONF_MERGE_RIGHT = "merge_right"
CONF_TEXT_CROP = "text_crop"
CONF_SELECTED_ROW = "selected_row"
CONF_SELECTED_COLUMN = "selected_column"
CELL_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT, default=""): lv_text,
# Not templatable: the value selects between two different LVGL calls
# (set/clear cell ctrl), so a runtime lambda can't be mapped to a single call.
cv.Optional(CONF_MERGE_RIGHT): cv.boolean,
cv.Optional(CONF_TEXT_CROP): cv.boolean,
}
)
# A cell can be given as a bare piece of text, or a dict for more control
TABLE_CELL_SCHEMA = cv.maybe_simple_value(CELL_SCHEMA, key=CONF_TEXT)
# A row can be given as a bare list of cells, or a dict for future extension
ROW_SCHEMA = cv.maybe_simple_value(
cv.Schema({cv.Required(CONF_CELLS): cv.ensure_list(TABLE_CELL_SCHEMA)}),
key=CONF_CELLS,
)
def _column_width_validator(value: ConfigFragmentType) -> int | float | list[str]:
"""Like pixels_or_percent, but rejects negative widths, which would
defeat the 100%-total check and wrap around in the generated uint8_t pct."""
if value == SCHEMA_EXTRACT:
return ["pixels", "..%"]
return cv.Any(pixels_validator, cv.percentage)(value)
column_width = LValidator(
_column_width_validator,
lv_coord_t,
retmapper=pixels_or_percent.retmapper,
animatable=True,
)
COLUMN_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH): column_width,
}
)
def _validate_table(config: ConfigType) -> ConfigType:
rows = config.get(CONF_ROWS)
min_row_count = len(rows) if rows else 0
min_column_count = max(len(row[CONF_CELLS]) for row in rows) if rows else 0
row_count = config.get(CONF_ROW_COUNT)
if row_count is not None and row_count < min_row_count:
raise cv.Invalid(
f"{CONF_ROW_COUNT} must be at least {min_row_count} to hold all the given rows",
path=[CONF_ROW_COUNT],
)
column_count = config.get(CONF_COLUMN_COUNT)
if column_count is not None and column_count < min_column_count:
raise cv.Invalid(
f"{CONF_COLUMN_COUNT} must be at least {min_column_count} to hold all the cells in a row",
path=[CONF_COLUMN_COUNT],
)
column_count = column_count if column_count is not None else min_column_count
columns = config.get(CONF_COLUMNS)
if columns and column_count and len(columns) > column_count:
raise cv.Invalid(
f"{CONF_COLUMNS} defines {len(columns)} columns, but the table has only {column_count}",
path=[CONF_COLUMNS],
)
total_pct = sum(
width
for column in columns or ()
if isinstance((width := column.get(CONF_WIDTH)), float)
)
if total_pct > 1.0:
raise cv.Invalid(
f"{CONF_COLUMNS} percentage widths add up to {total_pct * 100:.0f}%, which exceeds 100%",
path=[CONF_COLUMNS],
)
return config
TABLE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ROWS): cv.ensure_list(ROW_SCHEMA),
cv.Optional(CONF_ROW_COUNT): cv.positive_int,
cv.Optional(CONF_COLUMN_COUNT): cv.positive_int,
cv.Optional(CONF_COLUMNS): cv.ensure_list(COLUMN_SCHEMA),
cv.Optional(CONF_SELECTED_ROW): lv_int,
cv.Optional(CONF_SELECTED_COLUMN): lv_int,
}
).add_extra(_validate_table)
lv_table_t = LvType(
"LvTableType",
parents=(LvCompound,),
largs=[(cg.uint32, "row"), (cg.uint32, "column")],
lvalue=lambda w: [
lv_expr.table_get_selected_row(w.obj),
lv_expr.table_get_selected_column(w.obj),
],
has_on_value=True,
)
async def set_cell_ctrl(
w: Widget, row: SafeExpType, column: SafeExpType, cell: ConfigType
) -> None:
for key, ctrl in (
(CONF_MERGE_RIGHT, "LV_TABLE_CELL_CTRL_MERGE_RIGHT"),
(CONF_TEXT_CROP, "LV_TABLE_CELL_CTRL_TEXT_CROP"),
):
if key not in cell:
continue
if cell[key]:
lv.table_set_cell_ctrl(w.obj, row, column, literal(ctrl))
else:
lv.table_clear_cell_ctrl(w.obj, row, column, literal(ctrl))
async def set_selected_cell(w: Widget, config: ConfigType) -> None:
selected_row = config.get(CONF_SELECTED_ROW)
selected_column = config.get(CONF_SELECTED_COLUMN)
if selected_row is None and selected_column is None:
return
# LV_TABLE_CELL_NONE selects the whole column/row when only one index is given
row_value = (
await lv_int.process(selected_row)
if selected_row is not None
else literal("LV_TABLE_CELL_NONE")
)
column_value = (
await lv_int.process(selected_column)
if selected_column is not None
else literal("LV_TABLE_CELL_NONE")
)
lv.table_set_selected_cell(w.obj, row_value, column_value)
TABLE_MODIFY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SELECTED_ROW): lv_int,
cv.Optional(CONF_SELECTED_COLUMN): lv_int,
}
)
class TableType(WidgetType):
def __init__(self):
super().__init__(
CONF_TABLE,
lv_table_t,
(CONF_MAIN, CONF_ITEMS),
TABLE_SCHEMA,
modify_schema=TABLE_MODIFY_SCHEMA,
)
def get_uses(self) -> tuple[str]:
return (CONF_LABEL,)
async def to_code(self, w: Widget, config: dict) -> None:
rows = config.get(CONF_ROWS)
row_count = config.get(CONF_ROW_COUNT)
column_count = config.get(CONF_COLUMN_COUNT)
if rows is not None:
if row_count is None:
row_count = len(rows)
if column_count is None:
column_count = max((len(row[CONF_CELLS]) for row in rows), default=0)
if row_count is not None:
lv.table_set_row_count(w.obj, row_count)
if column_count is not None:
lv.table_set_column_count(w.obj, column_count)
columns = config.get(CONF_COLUMNS, ())
pct_column_count = sum(
1 for column in columns if isinstance(column.get(CONF_WIDTH), float)
)
if pct_column_count:
lv_add(w.var.init_column_pct(pct_column_count))
for index, column in enumerate(columns):
if (width := column.get(CONF_WIDTH)) is None:
continue
if isinstance(width, float):
# A percentage: column_width validation leaves it as a 0.0-1.0
# fraction. LVGL's table widget only accepts a literal pixel width, so
# the actual width is recomputed at runtime from the table's own size.
lv_add(w.var.add_column_width_pct(index, round(width * 100)))
else:
lv.table_set_column_width(
w.obj, index, await column_width.process(width)
)
for row_index, row in enumerate(rows or ()):
for column_index, cell in enumerate(row[CONF_CELLS]):
lv.table_set_cell_value(
w.obj,
row_index,
column_index,
await lv_text.process(cell[CONF_TEXT]),
)
await set_cell_ctrl(w, row_index, column_index, cell)
await set_selected_cell(w, config)
table_spec = TableType()
@automation.register_action(
"lvgl.table.cell.update",
ObjUpdateAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(lv_table_t),
cv.Required(CONF_ROW): lv_int,
cv.Required(CONF_COLUMN): lv_int,
cv.Optional(CONF_TEXT): lv_text,
cv.Optional(CONF_MERGE_RIGHT): cv.boolean,
cv.Optional(CONF_TEXT_CROP): cv.boolean,
}
).add_extra(cv.has_at_least_one_key(CONF_TEXT, CONF_MERGE_RIGHT, CONF_TEXT_CROP)),
synchronous=True,
)
async def table_cell_update_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
widgets = await get_widgets(config)
async def do_update(w: Widget):
row = await lv_int.process(config[CONF_ROW])
column = await lv_int.process(config[CONF_COLUMN])
fields_set = sum(
key in config for key in (CONF_TEXT, CONF_MERGE_RIGHT, CONF_TEXT_CROP)
)
with ExitStack() as stack:
if fields_set > 1:
# row/column feed more than one generated call below: cache them in
# local variables so a !lambda value is only evaluated once.
row = stack.enter_context(
LocalVariable("row", cg.int_, row, modifier="")
)
column = stack.enter_context(
LocalVariable("column", cg.int_, column, modifier="")
)
if CONF_TEXT in config:
lv.table_set_cell_value(
w.obj, row, column, await lv_text.process(config[CONF_TEXT])
)
await set_cell_ctrl(w, row, column, config)
return await action_to_code(
widgets, do_update, action_id, template_arg, args, config
)
+1 -13
View File
@@ -41,19 +41,7 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() {
#ifdef USE_MDNS_WIFI_LISTENER
// MDNS.update() can suspend the loop in UdpContext::sendTimeout() while a send is
// failing (radio off-channel during a roam scan, or mid reconnect); an incoming
// packet then re-enters LEAmDNS from lwIP and corrupts shared UdpContext state.
// Skip the tick while the radio cannot transmit (#18760), but keep polling while
// the AP is serving clients (AP-only or fallback AP with the STA down).
auto *wifi = wifi::global_wifi_component;
if (wifi->is_roaming() || (!wifi->is_connected() && !wifi->is_ap_active()))
return;
#endif
MDNS.update();
});
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
#endif
+2
View File
@@ -266,6 +266,8 @@ DriverChip(
"JC3636W518V2",
height=360,
width=360,
offset_height=1,
draw_rounding=1,
cs_pin=10,
reset_pin=47,
invert_colors=True,
-3
View File
@@ -3,9 +3,6 @@ import esphome.codegen as cg
modbus_ns = cg.esphome_ns.namespace("modbus")
modbus_helpers_ns = modbus_ns.namespace("helpers")
RegisterValues = modbus_ns.class_("RegisterValues")
PduBuffer = modbus_helpers_ns.class_("PduBuffer")
FunctionCode_ns = modbus_ns.namespace("FunctionCode")
FunctionCode = FunctionCode_ns.enum("FunctionCode")
@@ -20,9 +20,7 @@ const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E
enum class FunctionCode : uint8_t {
INVALID = 0x00, // 0x00 is not a valid function code (even for custom functions).
// Remove before 2027.3.0
CUSTOM ESPDEPRECATED("0x00 is not a function code; use FunctionCode::INVALID for the sentinel. Removed in 2027.3.0",
"2026.9.0") = 0x00,
CUSTOM = 0x00, // The CUSTOM alias should be removed in future.
READ_COILS = 0x01,
READ_DISCRETE_INPUTS = 0x02,
READ_HOLDING_REGISTERS = 0x03,
+28 -44
View File
@@ -153,50 +153,6 @@ inline std::span<const uint8_t> server_pdu_payload(std::span<const uint8_t> pdu)
inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; }
/** Extract data from modbus response buffer
* @param T one of supported integer data types int_8,int_16,int_32,int_64
* @param data modbus response buffer (uint8_t)
* @param buffer_offset offset in bytes.
* @return value of type T extracted from buffer
*/
template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
if (sizeof(T) == sizeof(uint8_t)) {
return T(data[buffer_offset]);
}
if (sizeof(T) == sizeof(uint16_t)) {
return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
}
if (sizeof(T) == sizeof(uint32_t)) {
return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
}
if (sizeof(T) == sizeof(uint64_t)) {
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
(static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
}
static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
sizeof(T) == sizeof(uint64_t),
"Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
return T{};
}
/// Function code of a PDU, exception flag masked; 0 for an empty PDU.
inline uint8_t pdu_function_code(std::span<const uint8_t> pdu) {
return pdu.empty() ? 0 : (pdu[0] & FUNCTION_CODE_MASK);
}
/// Start address of a standard client request PDU ([fc, addr_hi, addr_lo, ...]). Empty when the PDU is
/// too short or its function code has no known layout - custom-frame bytes are not misread as an address.
inline std::optional<uint16_t> client_pdu_start_address(std::span<const uint8_t> pdu) {
if (pdu.size() < 3 || is_function_code_unknown_length(pdu[0]))
return std::nullopt;
const auto fc = static_cast<FunctionCode>(pdu[0]);
// The file-record PDUs are known-length but carry a byte count, not a start address.
if (fc == FunctionCode::READ_FILE_RECORD || fc == FunctionCode::WRITE_FILE_RECORD)
return std::nullopt;
return get_data<uint16_t>(pdu.data(), 1);
}
enum class SensorValueType : uint8_t {
RAW = 0x00, // variable length
U_WORD = 0x1, // 1 Register unsigned
@@ -300,6 +256,34 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
}
// Extract data from modbus response buffer
/** Extract data from modbus response buffer
* @param T one of supported integer data types int_8,int_16,int_32,int_64
* @param data modbus response buffer (uint8_t)
* @param buffer_offset offset in bytes.
* @return value of type T extracted from buffer
*/
template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
if (sizeof(T) == sizeof(uint8_t)) {
return T(data[buffer_offset]);
}
if (sizeof(T) == sizeof(uint16_t)) {
return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
}
if (sizeof(T) == sizeof(uint32_t)) {
return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
}
if (sizeof(T) == sizeof(uint64_t)) {
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
(static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
}
static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
sizeof(T) == sizeof(uint64_t),
"Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
return T{};
}
template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
return get_data<T>(data.data(), buffer_offset);
}
@@ -191,7 +191,7 @@ ModbusItemBaseSchema = cv.Schema(
)
def validate_modbus_register(config: ConfigType) -> ConfigType:
def validate_modbus_register(config):
# custom_command is the deprecated alias for custom_pdu (migrated later in final validate); treat
# either as "a custom frame is configured" so the address/register_type rules match.
has_custom = CONF_CUSTOM_PDU in config or CONF_CUSTOM_COMMAND in config
@@ -278,22 +278,7 @@ def _final_validate(config: ConfigType) -> None:
FINAL_VALIDATE_SCHEMA = _final_validate
def reject_odd_holding_write_offset(config: ConfigType) -> ConfigType:
"""Reject an odd byte offset on a holding-register write entity.
A 16-bit register write cannot target half a register, so the residual byte is inexpressible.
"""
key = CONF_BYTE_OFFSET if CONF_BYTE_OFFSET in config else CONF_OFFSET
if config.get(key, 0) % 2:
raise cv.Invalid(
f"An odd '{key}' cannot be used with holding-register writes: a 16-bit register "
"write cannot target half a register. Use an even offset, or fold it into 'address'",
path=[key],
)
return config
def modbus_calc_properties(config: ConfigType) -> tuple[int, int]:
def modbus_calc_properties(config):
byte_offset = 0
reg_count = 0
if CONF_OFFSET in config:
@@ -322,12 +307,8 @@ def modbus_calc_properties(config: ConfigType) -> tuple[int, int]:
async def add_modbus_base_properties(
var: cg.MockObj,
config: ConfigType,
sensor_type: cg.MockObjClass,
lambda_param_type: cg.MockObj = cg.float_,
lambda_return_type: Any = float,
) -> None:
var, config, sensor_type, lambda_param_type=cg.float_, lambda_return_type=float
):
if CONF_CUSTOM_PDU in config:
cg.add(var.set_custom_pdu(config[CONF_CUSTOM_PDU]))
@@ -366,11 +347,8 @@ _CALLBACK_AUTOMATIONS = (
)
async def to_code(config: ConfigType) -> None:
# Await the hub first, so no entity can bind to a controller that doesn't have one yet.
hub = await cg.get_variable(config[modbus.CONF_MODBUS_ID])
var = cg.new_Pvariable(config[CONF_ID], hub, config[CONF_ADDRESS])
await cg.register_component(var, config)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
cg.add(
@@ -378,22 +356,17 @@ async def to_code(config: ConfigType) -> None:
modbus.command_options_expression(config, direction="read")
)
)
await register_modbus_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
async def register_modbus_device(var: cg.MockObj, config: ConfigType) -> cg.MockObj:
# Remove before 2027.3.0
_LOGGER.warning(
"'modbus_controller.register_modbus_device' is deprecated, use "
"'modbus.register_modbus_client_device' and set the address on your own "
"class instead. Will be removed in 2027.3.0"
)
async def register_modbus_device(var, config):
cg.add(var.set_address(config[CONF_ADDRESS]))
await cg.register_component(var, config)
return await modbus.register_modbus_client_device(var, config)
def function_code_to_register(function_code: str) -> cg.MockObj:
def function_code_to_register(function_code):
FUNCTION_CODE_TYPE_MAP = {
"read_coils": EntityType.COIL,
"read_discrete_inputs": EntityType.DISCRETE_INPUT,
@@ -10,119 +10,6 @@ static const char *const TAG = "modbus_controller";
void ModbusController::setup() { this->create_polling_commands_(); }
void WriterDevice::warn_write_buffer_deprecated(const LogString *platform, uint16_t address) {
if (this->write_buffer_deprecated_warned_)
return;
this->write_buffer_deprecated_warned_ = true;
ESP_LOGW(TAG,
"Modbus %s (address 0x%X): filling the write_lambda buffer parameter is deprecated; call a write helper / "
"queue_pdu() on the entity (item) instead. The buffer parameter is removed in 2027.3.0",
LOG_STR_ARG(platform), address);
}
bool WriterDevice::send_raw_frame_deprecated(std::span<const uint8_t> frame) {
if (frame.empty())
return false;
return this->parent_->queue_pdu(frame[0], frame.subspan(1), this);
}
void ControllerDevice::set_controller(ModbusController *controller) {
this->controller_ = controller;
this->set_parent(controller->hub());
this->set_address(controller->device_address());
}
// A request whose layout carries no start address (a custom PDU) reports -1; 0 stays a real address.
static int trigger_address(std::span<const uint8_t> request_pdu) {
const auto addr = modbus::helpers::client_pdu_start_address(request_pdu);
return addr.has_value() ? *addr : -1;
}
void ControllerDevice::notify_online_(std::span<const uint8_t> request_pdu) {
if (this->controller_ != nullptr) {
this->controller_->set_online(true, modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu));
}
}
void ControllerDevice::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
this->notify_online_(request_pdu);
}
void ControllerDevice::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) {
ESP_LOGW(TAG, "Modbus error function code: 0x%X register %d exception: %d",
modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu),
static_cast<uint8_t>(exception_code));
this->notify_online_(request_pdu); // an exception is still a legitimate reply -> device is online
}
void WriterDevice::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
ControllerDevice::on_response(request_pdu, response_pdu);
this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
}
void WriterDevice::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) {
ControllerDevice::on_error(request_pdu, exception_code);
this->dispatch_response_(request_pdu, {}, exception_code);
}
// Fired once per wire transmission (including hub re-queues from a retry), so the on_command_sent trigger
// reflects when the frame actually went out, not when it was queued.
void ControllerDevice::on_sent(std::span<const uint8_t> request_pdu) {
if (this->controller_ != nullptr) {
this->controller_->command_sent(modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu));
}
}
void ControllerDevice::on_not_sent(std::span<const uint8_t> request_pdu) {
const uint8_t fc = modbus::helpers::pdu_function_code(request_pdu);
const int addr = trigger_address(request_pdu);
// Only the offline teardown reaches this (a supersede retires silently), so the frame is genuinely
// lost; a dropped write was already published optimistically, so surface it.
if (modbus::helpers::is_function_code_write(fc)) {
ESP_LOGW(TAG, "Write not sent: function 0x%X register %d", fc, addr);
} else {
ESP_LOGD(TAG, "Request not sent: function 0x%X register %d", fc, addr);
}
}
bool ControllerDevice::on_no_response(std::span<const uint8_t> request_pdu) {
if (this->controller_ == nullptr)
return false;
this->controller_->increment_non_response_count();
if (this->controller_->can_send())
return true; // the hub re-queues the frame it is holding; on_sent fires again on the retry
this->controller_->set_online(false, modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu));
return false;
}
PollingDevice::PollingDevice(ModbusController &controller, RegisterRange &&range)
: ControllerDevice(&controller), range_(std::move(range)) {}
bool PollingDevice::queue(modbus::CommandOptions options) {
bool accepted;
if (this->range_.custom_pdu != nullptr) {
accepted = this->queue_pdu(std::span<const uint8_t>(*this->range_.custom_pdu), options);
} else {
accepted = this->read_entities(this->range_.register_type, this->range_.start_address, this->range_.register_count,
options);
}
if (accepted) {
ESP_LOGV(TAG, "Poll queued type=%u 0x%X %d", static_cast<uint8_t>(this->range_.register_type),
this->range_.start_address, this->range_.register_count);
}
return accepted;
}
void PollingDevice::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
this->notify_online_(request_pdu);
auto data = modbus::helpers::server_pdu_payload(response_pdu);
for (auto *sensor : this->range_.sensors)
sensor->parse_and_publish(data);
}
// ModbusCommandItem's machinery stays as-is until its removal in 2027.3.0; silence its self-references.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
ModbusCommandItem::ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address,
RegisterRange &&range)
: modbus::ModbusClientDevice(parent, address),
@@ -253,8 +140,6 @@ bool ModbusCommandItem::on_no_response(std::span<const uint8_t> request_pdu) {
return false;
}
#pragma GCC diagnostic pop
void ModbusController::set_online(bool online, int function_code, int register_address) {
if (online) {
this->cmd_non_responses_ = 0;
@@ -276,8 +161,6 @@ void ModbusController::set_online(bool online, int function_code, int register_a
}
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
void ModbusController::queue_command(ModbusCommandItem command) {
this->sweep_completed_one_shots_(); // reclaim finished one-shots before adding a new one
// Duplicates are the caller's to manage; the controller only holds the item until its terminal callback.
@@ -312,8 +195,6 @@ void ModbusController::sweep_completed_one_shots_() {
[](const std::unique_ptr<ModbusCommandItem> &item) { return item->pending_removal; });
}
#pragma GCC diagnostic pop
void ModbusController::update() {
this->sweep_completed_one_shots_(); // reclaim one-shots deferred out of their own callbacks
if (this->module_offline_) {
@@ -322,11 +203,11 @@ void ModbusController::update() {
if (offline_retry_due(this->update_counter_, this->module_offline_at_, this->offline_skip_updates_)) {
ESP_LOGV(TAG, "Module offline - retrying");
this->cmd_non_responses_ = 0; // allow the probe through can_send()
for (auto &poll : this->polling_devices_) {
for (auto &cmd : this->polling_command_items_) {
// Probes carry the read-side options too, so a recovering device resumes streaming on the
// probe itself rather than waiting for the next update_interval.
if (!poll.queue(this->read_options_)) {
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", poll.register_address());
if (!cmd.send(this->read_options_)) {
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address());
}
}
} else {
@@ -337,12 +218,12 @@ void ModbusController::update() {
}
if (this->can_send()) {
for (auto &poll : this->polling_devices_) {
ESP_LOGVV(TAG, "Updating range 0x%X", poll.register_address());
for (auto &cmd : this->polling_command_items_) {
ESP_LOGVV(TAG, "Updating range 0x%X", cmd.register_address());
// read_options_ carries the controller's continuous flag (the offline probe above sends it too).
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
if (!poll.queue(this->read_options_)) {
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address());
if (!cmd.send(this->read_options_)) {
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address());
}
}
}
@@ -360,10 +241,6 @@ void ModbusController::create_polling_commands_() {
// force_new_range ahead of the rest, then address - so the walk is not purely address-ordered.
// Each keeps the address it was configured with; what is resolved here is its `offset`, the position
// of its data within the response of whichever range it ends up in.
// One range per sensor is a strict upper bound: each walk step closes at most one range, plus one
// closed after the walk. Sized to that bound so no push is ever silently dropped, then handed on by move.
FixedVector<RegisterRange> ranges;
ranges.init(this->sensorset_.size());
RegisterRange r = {};
bool have_range = false;
// Set while the open range belongs to a force_new_range sensor: a range the user asked to keep
@@ -446,7 +323,7 @@ void ModbusController::create_polling_commands_() {
if (!join) {
if (have_range) {
ESP_LOGV(TAG, "Add range 0x%X %d", r.start_address, r.register_count);
ranges.push_back(std::move(r));
this->create_polling_command_(std::move(r));
}
r = {};
range_bytes = curr->get_register_size();
@@ -457,8 +334,6 @@ void ModbusController::create_polling_commands_() {
r.start_address = curr->start_address;
r.register_count = curr->register_count;
r.register_type = curr->register_type;
if (curr->register_type == modbus::EntityType::CUSTOM)
r.custom_pdu = &curr->custom_pdu;
have_range = true;
}
@@ -470,13 +345,11 @@ void ModbusController::create_polling_commands_() {
}
if (have_range) {
ESP_LOGV(TAG, "Add last range 0x%X %d", r.start_address, r.register_count);
ranges.push_back(std::move(r));
}
// Staged in a setup-time vector so the device storage can be sized exactly (see polling_devices_).
this->polling_devices_.init(ranges.size());
for (auto &range : ranges) {
this->polling_devices_.emplace_back(*this, std::move(range));
this->create_polling_command_(std::move(r));
}
// Reclaim growth slack; safe here because nothing has registered with the hub yet (see the
// lifetime note on polling_command_items_).
this->polling_command_items_.shrink_to_fit();
}
void ModbusController::dump_config() {
@@ -495,15 +368,13 @@ void ModbusController::dump_config() {
it->get_register_size());
}
ESP_LOGCONFIG(TAG, "ranges");
for (auto &it : this->polling_devices_) {
for (auto &it : this->polling_command_items_) {
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d", static_cast<uint8_t>(it.register_type()),
it.register_address(), it.register_count());
}
#endif
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
void ModbusController::on_write_register_response(EntityType register_type, uint16_t start_address,
std::span<const uint8_t> data) {
// A well-formed write ACK echoes address and value, but a truncated PDU yields a short/empty span.
@@ -660,6 +531,5 @@ bool ModbusCommandItem::send(modbus::CommandOptions options) {
}
return accepted;
}
#pragma GCC diagnostic pop
} // namespace esphome::modbus_controller
@@ -4,7 +4,6 @@
#include "esphome/components/modbus/modbus.h"
#include "esphome/components/modbus/modbus_helpers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/automation.h"
#include <list>
@@ -231,147 +230,12 @@ struct RegisterRange {
modbus::EntityType register_type;
uint8_t register_count;
SensorSet sensors; // all sensors of this range
/// A custom range polls this PDU, referenced from the sensor that opened the range.
const SmallInlineBuffer<8> *custom_pdu{nullptr};
};
/// The shared feedback half of a controller-owned hub device: online/offline tracking, retry counting
/// and the on_command_sent trigger all route to the controller from here. The hub base is inherited
/// protected, so a subclass chooses exactly what request API it exposes.
class ControllerDevice : protected modbus::ModbusClientDevice {
public:
// Public: only the owner can reach this instance, so reachability is the access gate.
void set_controller(ModbusController *controller);
protected:
ControllerDevice() = default; // WriterEntity's member is wired later via set_controller()
explicit ControllerDevice(ModbusController *controller) { this->set_controller(controller); }
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override;
void on_sent(std::span<const uint8_t> request_pdu) override;
void on_not_sent(std::span<const uint8_t> request_pdu) override;
bool on_no_response(std::span<const uint8_t> request_pdu) override;
void notify_online_(std::span<const uint8_t> request_pdu);
/// Write-path state owned by WriterEntity's forwarders, stored here so both bools land in the base's
/// tail padding instead of adding a word to every writer entity. The warn flag leaves in 2027.3.0.
bool dispatched_{false};
bool write_buffer_deprecated_warned_{false};
ModbusController *controller_{nullptr};
};
/// The write side of a ControllerDevice, owned by the writer entities through WriterEntity, whose
/// forwarders re-expose exactly the request API a write lambda may use and record every dispatch.
class WriterDevice final : public ControllerDevice {
public:
using modbus::ModbusClientDevice::clear_tx_queue_for_device;
using modbus::ModbusClientDevice::queue_pdu;
using modbus::ModbusClientDevice::write_multiple_coils;
using modbus::ModbusClientDevice::write_multiple_registers;
using modbus::ModbusClientDevice::write_single_coil;
using modbus::ModbusClientDevice::write_single_register;
/// Send a legacy raw frame (address + function code + data) to the frame's own address.
/// Serves only the deprecated write_lambda buffer path. Remove before 2027.3.0.
bool send_raw_frame_deprecated(std::span<const uint8_t> frame);
bool dispatched() const { return this->dispatched_; }
void set_dispatched() { this->dispatched_ = true; }
void clear_dispatched() { this->dispatched_ = false; }
/// Warn once per entity that filling the write_lambda buffer parameter is deprecated (the entity is now the
/// command - call a write helper / queue_pdu() on `item` instead). The buffer parameter is removed in 2027.3.0.
void warn_write_buffer_deprecated(const LogString *platform, uint16_t address);
protected:
// Only the write side forwards to the typed callbacks (for item->queue_pdu() replies): a poll parses
// its own response, and dispatching its errors would trip the base unhandled-custom-response warning.
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override;
};
/// Gives a writer entity the write API of the WriterDevice it owns. The device is a member, not a base:
/// the mixin declares no virtual function, so an entity mixing it in gains no second vtable and all the
/// writer platforms share the single WriterDevice vtable instead of each emitting its own copy.
/// The forwarders keep `item->write_*()` working unchanged inside a write_lambda, and record every
/// dispatch, so the write path can tell "the lambda sent it itself" from "use the default write".
class WriterEntity {
public:
/// Whether the lambda called a request helper since the last clear_dispatched_(). Deliberately records
/// the call, not the hub's accept/refuse: a refused lambda write must not fall through to the default write.
bool dispatched() const { return this->device_.dispatched(); }
bool write_single_register(uint16_t address, uint16_t value) {
this->device_.set_dispatched();
return this->device_.write_single_register(address, value);
}
bool write_single_coil(uint16_t address, bool value) {
this->device_.set_dispatched();
return this->device_.write_single_coil(address, value);
}
bool write_multiple_registers(uint16_t address, std::span<const uint16_t> values) {
this->device_.set_dispatched();
return this->device_.write_multiple_registers(address, values);
}
bool write_multiple_coils(uint16_t address, std::span<const bool> values) {
this->device_.set_dispatched();
return this->device_.write_multiple_coils(address, values);
}
bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) {
this->device_.set_dispatched();
return this->device_.write_multiple_coils(address, bits);
}
bool queue_pdu(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
this->device_.set_dispatched();
return this->device_.queue_pdu(pdu, options);
}
void clear_tx_queue_for_device() { this->device_.clear_tx_queue_for_device(); }
protected:
bool send_raw_frame_deprecated_(std::span<const uint8_t> frame) {
this->device_.set_dispatched();
return this->device_.send_raw_frame_deprecated(frame);
}
void set_controller_(ModbusController *controller) { this->device_.set_controller(controller); }
void clear_dispatched_() { this->device_.clear_dispatched(); }
void warn_write_buffer_deprecated_(const LogString *platform, uint16_t address) {
this->device_.warn_write_buffer_deprecated(platform, address);
}
private:
// Private so a derived entity cannot reach the device except through the recording forwarders above.
WriterDevice device_;
};
/// A persistent hub device that polls one register range - the read-side mirror of WriterDevice.
/// Owned by the controller, one per range; the response is parsed straight to the range's sensors.
class PollingDevice final : public ControllerDevice {
public:
PollingDevice(ModbusController &controller, RegisterRange &&range);
/// Queue this range's read (or its sensor's custom PDU) on the hub. False = refused, no callback follows.
bool queue(modbus::CommandOptions options = {});
uint16_t register_address() const { return this->range_.start_address; }
uint16_t register_count() const { return this->range_.register_count; }
EntityType register_type() const { return this->range_.register_type; }
protected:
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
RegisterRange range_;
};
/// A single modbus command. Each command is its own ModbusClientDevice: it sends its frame to the hub
/// and the hub routes the response back to this object's on_modbus_* callbacks, so the controller no
/// longer has to match responses to a FIFO queue.
// The deprecated class references other deprecated names. Remove before 2027.3.0.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
class ESPDEPRECATED(
"One-shot writes go through the entity write helpers (WriterDevice) or the modbus_client actions, and "
"polling runs through PollingDevice. Removed in 2027.3.0",
"2026.9.0") ModbusCommandItem : public modbus::ModbusClientDevice {
class ModbusCommandItem : public modbus::ModbusClientDevice {
public:
/// Empty command with no controller connection (kept for source compatibility with value-type usage).
ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address)
@@ -516,7 +380,6 @@ class ESPDEPRECATED(
const SmallInlineBuffer<8> *custom_pdu_{nullptr};
ModbusController *controller_{nullptr};
};
#pragma GCC diagnostic pop
/// Whether an offline probe is due this update cycle: every offline_skip_updates + 1 cycles,
/// anchored at the cycle the device went offline. Pure so the cadence (including update_counter
@@ -535,39 +398,30 @@ inline bool offline_retry_due(uint16_t update_counter, uint16_t module_offline_a
class ModbusController final : public PollingComponent {
public:
// The controller is not itself a modbus device - its commands and writer entities send as their own
// devices, built against this hub + address.
ModbusController(modbus::ModbusClientHub *hub, uint8_t address) : hub_(hub), address_(address) {}
void dump_config() override;
// No loop() override: the hub owns transmit/receive timing and each command routes its own
// response, so the controller never joins the looping components at all.
void setup() override;
void update() override;
// The controller is not itself a modbus device - its commands and writer entities send as their own
// devices. It only owns the hub + address so those senders can be built against them.
void set_parent(modbus::ModbusClientHub *hub) { this->hub_ = hub; }
void set_address(uint8_t address) { this->address_ = address; }
/// The hub and modbus address this controller talks to. Used to build commands/entities that send as
/// their own device.
modbus::ModbusClientHub *hub() const { return this->hub_; }
uint8_t device_address() const { return this->address_; }
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
/// Queues a one-shot modbus command (writes, custom commands); taken by value, so std::move to avoid a copy.
/// Remove with ModbusCommandItem before 2027.3.0.
ESPDEPRECATED("Use the entity write helpers or the modbus_client actions instead. Removed in 2027.3.0", "2026.9.0")
void queue_command(ModbusCommandItem command);
/// Flags a finished one-shot command for removal. Called by the command as the last action of its own
/// callback, so the item is not destroyed here (send() and the hub still touch it) but swept later.
/// Remove with ModbusCommandItem before 2027.3.0.
ESPDEPRECATED("Serves only ModbusCommandItem's own callbacks. Removed in 2027.3.0", "2026.9.0")
void unqueue_command(const ModbusCommandItem *command);
#pragma GCC diagnostic pop
/// Registers a sensor with the controller. Called by esphomes code generator
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
/// Handles a write command acknowledgement (used by write command on_data_func handlers).
/// Remove with ModbusCommandItem before 2027.3.0.
ESPDEPRECATED("Write acknowledgements are handled by the writing entity's own device. Removed in 2027.3.0",
"2026.9.0")
void on_write_register_response(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data);
/// Update the online/offline state after a response or a run of timeouts, firing the callbacks.
void set_online(bool online, int function_code, int register_address);
@@ -606,22 +460,32 @@ class ModbusController final : public PollingComponent {
const modbus::CommandOptions &read_options() const { return this->read_options_; }
protected:
/// Group the registered sensors into contiguous ranges and create one PollingDevice per range.
/// parse sensormap_ and create range of sequential addresses
/// Group the registered sensors into contiguous ranges and create one polling command per range.
void create_polling_commands_();
/// build one persistent polling command from a range and add it to polling_command_items_
void create_polling_command_(RegisterRange &&range) {
// A custom range polls the first sensor's custom_pdu (referenced, not copied); the sensor constructor
// decodes the real function code. The response still dispatches to every sensor in the range.
if (range.register_type == EntityType::CUSTOM && !range.sensors.empty()) {
auto &cmd = this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, *range.sensors.begin());
cmd.sensors = std::move(range.sensors);
} else {
this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, std::move(range));
}
}
/// The hub this controller's commands/entities send through, and the modbus address they target.
modbus::ModbusClientHub *hub_{nullptr};
uint8_t address_{0};
/// Collection of all sensors for this component
SensorSet sensorset_;
/// One persistent PollingDevice per register range. Built once in setup() with the exact count
/// (FixedVector never reallocates), so the hub's device pointers stay valid once polls start sending.
FixedVector<PollingDevice> polling_devices_;
/// One persistent command per register range, each its own ModbusClientDevice. Built once in setup()
/// (create_polling_commands_ feeds each range straight in; the vector may reallocate as it grows, which
/// is safe because no command has registered with the hub yet) and never appended to afterward, so the
/// hub's device pointers stay valid once commands start sending.
std::vector<ModbusCommandItem> polling_command_items_{};
/// Dynamically queued one-shot commands (writes, custom commands). std::list keeps stable addresses.
/// Remove with ModbusCommandItem before 2027.3.0.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
std::list<std::unique_ptr<ModbusCommandItem>> one_shot_command_items_;
#pragma GCC diagnostic pop
/// Erases one-shot commands flagged by unqueue_command(). Safe even when reached from inside a hub
/// callback (via an on_online/on_offline/on_command_sent automation that queues a command): the
/// destructor detaches via clear_tx_queue_for_device(), which the hub allows from callbacks, and the
@@ -3,7 +3,6 @@ from esphome.components import number
from esphome.components.modbus.helpers import (
MODBUS_WRITE_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
RegisterValues,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -14,7 +13,6 @@ from esphome.const import (
CONF_MULTIPLY,
CONF_STEP,
)
from esphome.types import ConfigType
from .. import (
ModbusItemBaseSchema,
@@ -45,7 +43,7 @@ ModbusNumber = modbus_controller_ns.class_(
)
def validate_min_max(config: ConfigType) -> ConfigType:
def validate_min_max(config):
if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]:
raise cv.Invalid("max_value must be greater than min_value")
if config[CONF_MIN_VALUE] < -16777215:
@@ -55,7 +53,7 @@ def validate_min_max(config: ConfigType) -> ConfigType:
return config
def validate_modbus_number(config: ConfigType) -> ConfigType:
def validate_modbus_number(config):
# custom_command is the deprecated alias for custom_pdu (migrated later in final validate).
has_custom = CONF_CUSTOM_PDU in config or CONF_CUSTOM_COMMAND in config
if not has_custom and CONF_ADDRESS not in config:
@@ -91,7 +89,7 @@ CONFIG_SCHEMA = cv.All(
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
async def to_code(config: ConfigType) -> None:
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
var = cg.new_Pvariable(
config[CONF_ID],
@@ -126,7 +124,7 @@ async def to_code(config: ConfigType) -> None:
[
(ModbusNumber.operator("ptr"), "item"),
(cg.float_, "x"),
(RegisterValues.operator("ref"), "payload"),
(cg.std_vector.template(cg.uint16).operator("ref"), "payload"),
],
return_type=cg.optional.template(float),
)
@@ -1,3 +1,4 @@
#include <vector>
#include "modbus_number.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -28,73 +29,62 @@ void ModbusNumber::parse_and_publish(std::span<const uint8_t> data) {
}
void ModbusNumber::control(float value) {
this->clear_dispatched_();
// A new write supersedes this entity's own not-yet-sent writes: drop them (and detach any in-flight one)
// so a rapidly-changing value writes the latest, not every intermediate.
this->clear_tx_queue_for_device();
modbus::RegisterValues data;
optional<ModbusCommandItem> write_cmd;
std::vector<uint16_t> data;
float write_value = value;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// The lambda may drive the write itself via item->write_*(), override the value (return a value), or
// (deprecated) fill `data` with the register words to write.
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, value, data);
if (this->dispatched()) {
this->publish_state(value);
return;
}
if (!data.empty()) {
// Deprecated buffer path (frozen): the lambda filled a legacy raw frame as words; pack it big-endian.
this->warn_write_buffer_deprecated_(LOG_STR("number"), this->start_address);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_uint16_size(MODBUS_NUMBER_MAX_LOG_REGISTERS)];
#endif
ESP_LOGV(TAG, "Modbus Number write raw: %s",
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
// Sized to hold RegisterValues at capacity, so a full buffer can never truncate into a valid frame.
StaticVector<uint8_t, modbus::MAX_NUM_OF_REGISTERS_TO_READ * 2> bytes;
for (uint16_t word : data) {
const auto word_bytes = decode_value(word);
bytes.push_back(word_bytes[0]);
bytes.push_back(word_bytes[1]);
}
if (!this->send_raw_frame_deprecated_(std::span<const uint8_t>(bytes.data(), bytes.size()))) {
ESP_LOGW(TAG, "Modbus write for '%s' was refused by the hub; state not published", this->get_name().c_str());
return;
}
this->publish_state(value);
return;
}
if (!val.has_value()) {
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
write_value = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
ESP_LOGV(TAG, "Value overwritten by lambda");
write_value = val.value();
} else {
write_value = this->multiply_by_ * write_value;
}
modbus::helpers::float_to_payload(data, write_value, this->sensor_value_type);
// float_to_payload() appends nothing for RAW, so an empty payload must be caught before data[0] below.
if (data.empty()) {
ESP_LOGW(TAG, "No payload was created for updating number");
return;
}
ESP_LOGD(TAG,
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
bool queued;
if (this->register_count == 1 && !this->use_write_multiple_) {
queued = this->write_single_register(this->write_address(), data[0]);
if (!data.empty()) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_uint16_size(MODBUS_NUMBER_MAX_LOG_REGISTERS)];
#endif
ESP_LOGV(TAG, "Modbus Number write raw: %s",
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
write_cmd.emplace(ModbusCommandItem::create_custom_command(
this->parent_, data,
[this](modbus::EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
this->parent_->on_write_register_response(register_type, this->start_address, data);
}));
} else {
queued = this->write_multiple_registers(this->write_address(), data);
}
if (!queued) {
ESP_LOGW(TAG, "Modbus write for '%s' was refused by the hub; state not published", this->get_name().c_str());
return;
std::vector<uint16_t> payload;
modbus::helpers::float_to_payload(payload, write_value, this->sensor_value_type);
ESP_LOGD(TAG,
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
// Create and send the write command
if (this->register_count == 1 && !this->use_write_multiple_) {
write_cmd.emplace(
ModbusCommandItem::create_write_single_command(this->parent_, this->write_address(), payload[0]));
} else {
write_cmd.emplace(ModbusCommandItem::create_write_multiple_command(this->parent_, this->write_address(),
this->register_count, payload));
}
// publish new value
write_cmd->on_data_func = [this, value](modbus::EntityType register_type, uint16_t start_address,
std::span<const uint8_t> data) {
// gets called when the write command is ack'd from the device
this->parent_->on_write_register_response(register_type, start_address, data);
this->publish_state(value);
};
}
this->parent_->queue_command(std::move(*write_cmd));
this->publish_state(value);
}
void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); }
@@ -10,7 +10,7 @@ namespace esphome::modbus_controller {
using value_to_data_t = std::function<float>(float);
class ModbusNumber final : public number::Number, public Component, public SensorItem, public WriterEntity {
class ModbusNumber final : public number::Number, public Component, public SensorItem {
public:
ModbusNumber(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
SensorValueType value_type, int register_count, bool force_new_range) {
@@ -26,11 +26,11 @@ class ModbusNumber final : public number::Number, public Component, public Senso
void dump_config() override;
void parse_and_publish(std::span<const uint8_t> data) override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void set_parent(ModbusController *parent) { this->set_controller_(parent); }
void set_parent(ModbusController *parent) { this->parent_ = parent; }
void set_write_multiply(float factor) { this->multiply_by_ = factor; }
using transform_func_t = optional<float> (*)(ModbusNumber *, float, std::span<const uint8_t>);
using write_transform_func_t = optional<float> (*)(ModbusNumber *, float, modbus::RegisterValues &);
using write_transform_func_t = optional<float> (*)(ModbusNumber *, float, std::vector<uint16_t> &);
void set_template(transform_func_t f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
@@ -39,6 +39,7 @@ class ModbusNumber final : public number::Number, public Component, public Senso
void control(float value) override;
optional<transform_func_t> transform_func_{nullopt};
optional<write_transform_func_t> write_transform_func_{nullopt};
ModbusController *parent_{nullptr};
float multiply_by_{1.0};
bool use_write_multiple_{false};
};
@@ -1,20 +1,14 @@
import esphome.codegen as cg
from esphome.components import output
from esphome.components.modbus.helpers import (
SENSOR_VALUE_TYPE,
PduBuffer,
RegisterValues,
)
from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY
from esphome.types import ConfigType
from .. import (
ModbusItemBaseSchema,
SensorItem,
modbus_calc_properties,
modbus_controller_ns,
reject_odd_holding_write_offset,
)
from ..const import (
CONF_CUSTOM_COMMAND,
@@ -54,26 +48,23 @@ CONFIG_SCHEMA = cv.typed_schema(
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
"holding": cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusFloatOutput),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_PDU): cv.invalid(
"custom_pdu is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid(
"custom_command is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SENSOR_VALUE_TYPE
),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
reject_odd_holding_write_offset,
"holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusFloatOutput),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_PDU): cv.invalid(
"custom_pdu is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid(
"custom_command is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SENSOR_VALUE_TYPE
),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
},
lower=True,
@@ -82,7 +73,7 @@ CONFIG_SCHEMA = cv.typed_schema(
)
async def to_code(config: ConfigType) -> None:
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
# Binary Output
write_template = None
@@ -98,7 +89,7 @@ async def to_code(config: ConfigType) -> None:
[
(ModbusBinaryOutput.operator("ptr"), "item"),
(cg.bool_, "x"),
(PduBuffer.operator("ref"), "payload"),
(cg.std_vector.template(cg.uint8).operator("ref"), "payload"),
],
return_type=cg.optional.template(bool),
)
@@ -118,7 +109,7 @@ async def to_code(config: ConfigType) -> None:
[
(ModbusFloatOutput.operator("ptr"), "item"),
(cg.float_, "x"),
(RegisterValues.operator("ref"), "payload"),
(cg.std_vector.template(cg.uint16).operator("ref"), "payload"),
],
return_type=cg.optional.template(float),
)
@@ -2,8 +2,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <array>
namespace esphome::modbus_controller {
static const char *const TAG = "modbus_controller.output";
@@ -15,33 +13,25 @@ static constexpr size_t MODBUS_OUTPUT_MAX_LOG_BYTES = 64;
*
*/
void ModbusFloatOutput::write_state(float value) {
this->clear_dispatched_();
// A new write supersedes this entity's own not-yet-sent writes: drop them (and detach any in-flight one)
// so a rapidly-changing value writes the latest, not every intermediate.
this->clear_tx_queue_for_device();
modbus::RegisterValues data;
std::vector<uint16_t> data;
auto original_value = value;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// The lambda may drive the write itself via item->write_*(), override the value (return a value), or
// (deprecated) fill `data` with the register words to write.
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, value, data);
if (this->dispatched()) {
return;
}
if (!data.empty()) {
// Deprecated buffer path (frozen): the lambda supplied the register words for the shared write below.
this->warn_write_buffer_deprecated_(LOG_STR("float output"), this->start_address);
} else if (!val.has_value()) {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
} else {
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
value = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
} else {
value = this->multiply_by_ * value;
}
// lambda didn't set payload
if (data.empty()) {
modbus::helpers::float_to_payload(data, value, this->sensor_value_type);
}
@@ -67,15 +57,16 @@ void ModbusFloatOutput::write_state(float value) {
return;
}
bool queued;
// Create and send the write command
optional<ModbusCommandItem> write_cmd;
if (this->register_count == 1 && !this->use_write_multiple_) {
queued = this->write_single_register(this->write_address(), data[0]);
write_cmd.emplace(
ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset, data[0]));
} else {
queued = this->write_multiple_registers(this->write_address(), data);
}
if (!queued) {
ESP_LOGW(TAG, "Modbus output write (address 0x%X) was refused by the hub", this->write_address());
write_cmd.emplace(ModbusCommandItem::create_write_multiple_command(
this->parent_, this->start_address + this->offset, data.size(), data));
}
this->parent_->queue_command(std::move(*write_cmd));
}
void ModbusFloatOutput::dump_config() {
@@ -90,52 +81,50 @@ void ModbusFloatOutput::dump_config() {
// ModbusBinaryOutput
void ModbusBinaryOutput::write_state(bool state) {
this->clear_dispatched_();
// A new write supersedes this entity's own not-yet-sent writes: drop them (and detach any in-flight one)
// so a rapidly-changing value writes the latest, not every intermediate.
this->clear_tx_queue_for_device();
modbus::helpers::PduBuffer data;
// This will be called every time the user requests a state change.
optional<ModbusCommandItem> cmd;
std::vector<uint8_t> data;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// The lambda may drive the write itself via item->write_*/queue_pdu(), override the value (return a value),
// or (deprecated) fill `data` with a custom PDU.
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, state, data);
if (this->dispatched()) {
return;
}
if (!data.empty()) {
this->warn_write_buffer_deprecated_(LOG_STR("binary output"), this->start_address);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_OUTPUT_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus binary output write raw: %s",
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
// The lambda filled a legacy raw frame (device address + function code + data).
if (!this->send_raw_frame_deprecated_(data)) {
ESP_LOGW(TAG, "Modbus output write (address 0x%X) was refused by the hub", this->write_address());
}
return;
}
if (!val.has_value()) {
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
state = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
ESP_LOGV(TAG, "Value overwritten by lambda");
state = val.value();
}
ESP_LOGV(TAG, "Write new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state),
(int) this->register_type, this->start_address, this->offset);
// offset for coil and discrete inputs is the coil/register number not bytes
bool queued;
if (this->use_write_multiple_) {
std::array<bool, 1> states{state};
queued = this->write_multiple_coils(this->write_address(), states);
if (!data.empty()) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_OUTPUT_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus binary output write raw: %s",
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
cmd.emplace(ModbusCommandItem::create_custom_command(
this->parent_, data,
[this](modbus::EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
this->parent_->on_write_register_response(register_type, this->start_address, data);
}));
} else {
queued = this->write_single_coil(this->write_address(), state);
}
if (!queued) {
ESP_LOGW(TAG, "Modbus output write (address 0x%X) was refused by the hub", this->write_address());
ESP_LOGV(TAG, "Write new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state),
(int) this->register_type, this->start_address, this->offset);
// offset for coil and discrete inputs is the coil/register number not bytes
if (this->use_write_multiple_) {
std::vector<bool> states{state};
cmd.emplace(
ModbusCommandItem::create_write_multiple_coils(this->parent_, this->start_address + this->offset, states));
} else {
cmd.emplace(
ModbusCommandItem::create_write_single_coil(this->parent_, this->start_address + this->offset, state));
}
}
this->parent_->queue_command(std::move(*cmd));
}
void ModbusBinaryOutput::dump_config() {
@@ -8,25 +8,26 @@
namespace esphome::modbus_controller {
class ModbusFloatOutput final : public output::FloatOutput, public Component, public SensorItem, public WriterEntity {
class ModbusFloatOutput final : public output::FloatOutput, public Component, public SensorItem {
public:
ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) {
this->register_type = modbus::EntityType::HOLDING;
// A byte offset folds into the address as whole registers; odd offsets are rejected at validation.
this->set_address(start_address + offset / 2);
this->set_offset_from_start_address(0);
this->set_address(start_address);
this->set_offset_from_start_address(offset);
this->bitmask = 0xFFFFFFFF;
this->register_count = register_count;
this->sensor_value_type = value_type;
this->set_address(this->start_address + offset);
this->set_offset_from_start_address(0);
}
void dump_config() override;
void set_parent(ModbusController *parent) { this->set_controller_(parent); }
void set_parent(ModbusController *parent) { this->parent_ = parent; }
void set_write_multiply(float factor) { this->multiply_by_ = factor; }
// Do nothing
void parse_and_publish(std::span<const uint8_t> data) override{};
using write_transform_func_t = optional<float> (*)(ModbusFloatOutput *, float, modbus::RegisterValues &);
using write_transform_func_t = optional<float> (*)(ModbusFloatOutput *, float, std::vector<uint16_t> &);
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
@@ -34,28 +35,29 @@ class ModbusFloatOutput final : public output::FloatOutput, public Component, pu
void write_state(float value) override;
optional<write_transform_func_t> write_transform_func_{nullopt};
ModbusController *parent_{nullptr};
float multiply_by_{1.0};
bool use_write_multiple_{false};
};
class ModbusBinaryOutput final : public output::BinaryOutput, public Component, public SensorItem, public WriterEntity {
class ModbusBinaryOutput final : public output::BinaryOutput, public Component, public SensorItem {
public:
ModbusBinaryOutput(uint16_t start_address, uint8_t offset) {
this->register_type = modbus::EntityType::COIL;
// A coil offset is a coil count; fold it into the address.
this->set_address(start_address + offset);
this->set_address(start_address);
this->bitmask = 0xFFFFFFFF;
this->sensor_value_type = SensorValueType::BIT;
this->register_count = 1;
this->set_address(this->start_address + offset);
this->set_offset_from_start_address(0);
}
void dump_config() override;
void set_parent(ModbusController *parent) { this->set_controller_(parent); }
void set_parent(ModbusController *parent) { this->parent_ = parent; }
// Do nothing
void parse_and_publish(std::span<const uint8_t> data) override{};
using write_transform_func_t = optional<bool> (*)(ModbusBinaryOutput *, bool, modbus::helpers::PduBuffer &);
using write_transform_func_t = optional<bool> (*)(ModbusBinaryOutput *, bool, std::vector<uint8_t> &);
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
@@ -63,6 +65,7 @@ class ModbusBinaryOutput final : public output::BinaryOutput, public Component,
void write_state(bool state) override;
optional<write_transform_func_t> write_transform_func_{nullopt};
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
};
@@ -1,16 +1,8 @@
from collections.abc import Callable
from typing import Any
import esphome.codegen as cg
from esphome.components import select
from esphome.components.modbus.helpers import (
SENSOR_VALUE_TYPE,
TYPE_REGISTER_MAP,
RegisterValues,
)
from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, TYPE_REGISTER_MAP
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
from esphome.types import ConfigType
from .. import (
ModbusController,
@@ -37,8 +29,8 @@ ModbusSelect = modbus_controller_ns.class_(
)
def ensure_option_map() -> Callable[[Any], dict[str, int]]:
def validator(value: Any) -> dict[str, int]:
def ensure_option_map():
def validator(value):
cv.check_not_templatable(value)
option = cv.All(cv.string_strict)
mapping = cv.All(cv.int_range(-(2**63), 2**63 - 1))
@@ -55,7 +47,7 @@ def ensure_option_map() -> Callable[[Any], dict[str, int]]:
return validator
def register_count_value_type_min(value: ConfigType) -> ConfigType:
def register_count_value_type_min(value):
reg_count = value.get(CONF_REGISTER_COUNT)
if reg_count is not None:
value_type = value[CONF_VALUE_TYPE]
@@ -95,7 +87,7 @@ CONFIG_SCHEMA = cv.All(
)
async def to_code(config: ConfigType) -> None:
async def to_code(config):
value_type = config[CONF_VALUE_TYPE]
reg_count = config.get(CONF_REGISTER_COUNT)
if reg_count is None:
@@ -140,7 +132,7 @@ async def to_code(config: ConfigType) -> None:
(ModbusSelect.operator("const_ptr"), "item"),
(cg.std_string.operator("const").operator("ref"), "x"),
(cg.int64, "value"),
(RegisterValues.operator("ref"), "payload"),
(cg.std_vector.template(cg.uint16).operator("ref"), "payload"),
],
return_type=cg.optional.template(cg.int64),
)
@@ -46,43 +46,35 @@ void ModbusSelect::control(size_t index) {
const char *option = this->option_at(index);
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, option);
this->clear_dispatched_();
// A new write supersedes this entity's own not-yet-sent writes: drop them (and detach any in-flight one)
// so a rapidly-changing value writes the latest, not every intermediate.
this->clear_tx_queue_for_device();
modbus::RegisterValues data;
std::vector<uint16_t> data;
if (this->write_transform_func_.has_value()) {
// The lambda may drive the write itself via item->write_*(), override the mapping value (return a value),
// or (deprecated) fill `data` with the register words to write. Transform func requires string parameter
// for backward compatibility.
// Transform func requires string parameter for backward compatibility
auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data);
if (this->dispatched()) {
if (this->optimistic_)
this->publish_state(index);
return;
}
if (!data.empty()) {
// Deprecated buffer path (frozen): the lambda supplied the register words for the shared write below.
this->warn_write_buffer_deprecated_(LOG_STR("select"), this->start_address);
} else if (!val.has_value()) {
ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control");
return;
} else {
if (val.has_value()) {
mapval = val;
ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval);
} else {
ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control");
return;
}
}
if (data.empty()) {
modbus::helpers::number_to_payload(data, *mapval, this->sensor_value_type);
// number_to_payload() appends nothing for RAW.
if (data.empty()) {
ESP_LOGW(TAG, "No payload was created for updating select");
return;
}
} else {
ESP_LOGV(TAG, "Using payload from write lambda");
}
if (data.empty()) {
ESP_LOGW(TAG, "No payload was created for updating select");
return;
}
// The command declares register_count registers, so the payload must be exactly that many words:
// a value type narrower than the declared width is zero-padded (the config deliberately allows
// register_count larger than the value type). Anything else would put a byte count on the wire
// that disagrees with the quantity field, which conformant devices reject.
// register_count declares the READ range width - it may pull neighboring registers into one poll -
// so a write covers exactly the registers the value occupies: the quantity comes from the payload,
// never from register_count (padding to it would zero registers the user only declared for reading).
@@ -94,17 +86,16 @@ void ModbusSelect::control(size_t index) {
}
const uint16_t write_address = this->write_address();
bool queued;
optional<ModbusCommandItem> write_cmd;
if ((this->register_count == 1) && (!this->use_write_multiple_)) {
queued = this->write_single_register(write_address, data[0]);
write_cmd.emplace(ModbusCommandItem::create_write_single_command(this->parent_, write_address, data[0]));
} else {
queued = this->write_multiple_registers(write_address, data);
write_cmd.emplace(
ModbusCommandItem::create_write_multiple_command(this->parent_, write_address, data.size(), data));
}
if (!queued) {
ESP_LOGW(TAG, "Modbus write for '%s' was refused by the hub; state not published", this->get_name().c_str());
return;
}
this->parent_->queue_command(std::move(*write_cmd));
if (this->optimistic_)
this->publish_state(index);
}
@@ -9,7 +9,7 @@
namespace esphome::modbus_controller {
class ModbusSelect final : public Component, public select::Select, public SensorItem, public WriterEntity {
class ModbusSelect final : public Component, public select::Select, public SensorItem {
public:
ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, bool force_new_range,
std::vector<int64_t> mapping) {
@@ -26,9 +26,9 @@ class ModbusSelect final : public Component, public select::Select, public Senso
using transform_func_t = optional<std::string> (*)(ModbusSelect *const, int64_t, std::span<const uint8_t>);
using write_transform_func_t = optional<int64_t> (*)(ModbusSelect *const, const std::string &, int64_t,
modbus::RegisterValues &);
std::vector<uint16_t> &);
void set_parent(ModbusController *const parent) { this->set_controller_(parent); }
void set_parent(ModbusController *const parent) { this->parent_ = parent; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_template(transform_func_t f) { this->transform_func_ = f; }
@@ -40,6 +40,7 @@ class ModbusSelect final : public Component, public select::Select, public Senso
protected:
std::vector<int64_t> mapping_{};
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
bool optimistic_{false};
optional<transform_func_t> transform_func_{nullopt};
@@ -1,9 +1,8 @@
import esphome.codegen as cg
from esphome.components import switch
from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE, PduBuffer
from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID
from esphome.types import ConfigType
from .. import (
ModbusItemBaseSchema,
@@ -11,7 +10,6 @@ from .. import (
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
reject_odd_holding_write_offset,
validate_custom_pdu_item,
validate_modbus_register,
)
@@ -32,14 +30,6 @@ ModbusSwitch = modbus_controller_ns.class_(
"ModbusSwitch", cg.Component, switch.Switch, SensorItem
)
def _validate_holding_offset(config: ConfigType) -> ConfigType:
# Only a holding-register switch folds the byte offset into a 16-bit register write.
if config.get(CONF_REGISTER_TYPE) == "holding":
reject_odd_holding_write_offset(config)
return config
CONFIG_SCHEMA = cv.All(
switch.switch_schema(ModbusSwitch, default_restore_mode="DISABLED")
.extend(cv.COMPONENT_SCHEMA)
@@ -53,13 +43,12 @@ CONFIG_SCHEMA = cv.All(
}
),
validate_modbus_register,
_validate_holding_offset,
)
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
async def to_code(config: ConfigType) -> None:
async def to_code(config):
byte_offset, _ = modbus_calc_properties(config)
var = cg.new_Pvariable(
config[CONF_ID],
@@ -85,7 +74,7 @@ async def to_code(config: ConfigType) -> None:
[
(ModbusSwitch.operator("ptr"), "item"),
(cg.bool_, "x"),
(PduBuffer.operator("ref"), "payload"),
(cg.std_vector.template(cg.uint8).operator("ref"), "payload"),
],
return_type=cg.optional.template(bool),
)
@@ -3,8 +3,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <array>
namespace esphome::modbus_controller {
static const char *const TAG = "modbus_controller.switch";
@@ -60,68 +58,57 @@ void ModbusSwitch::parse_and_publish(std::span<const uint8_t> data) {
}
void ModbusSwitch::write_state(bool state) {
this->clear_dispatched_();
// A new write supersedes this entity's own not-yet-sent writes: drop them (and detach any in-flight one)
// so a rapidly-changing value writes the latest, not every intermediate.
this->clear_tx_queue_for_device();
modbus::helpers::PduBuffer data;
bool write_value = state;
// This will be called every time the user requests a state change.
optional<ModbusCommandItem> cmd;
std::vector<uint8_t> data;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// The lambda may drive the write itself via item->write_*/queue_pdu(), override the written value (return a
// value), or (deprecated) fill `data` with a custom PDU.
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, state, data);
if (this->dispatched()) {
this->publish_state(state);
return;
}
if (!data.empty()) {
this->warn_write_buffer_deprecated_(LOG_STR("switch"), this->start_address);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_SWITCH_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus Switch write raw: %s",
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
// The lambda filled a legacy raw frame (device address + function code + data).
if (!this->send_raw_frame_deprecated_(data)) {
ESP_LOGW(TAG, "Modbus write for '%s' was refused by the hub; state not published", this->get_name().c_str());
return;
}
this->publish_state(state);
return;
}
if (!val.has_value()) {
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
state = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
// The returned bool is the wire value only; the entity still reports the requested state. A polled
// entity needs the read lambda inverted to match, or the next poll flips the display back.
ESP_LOGV(TAG, "Value overwritten by lambda");
write_value = val.value();
}
ESP_LOGV(TAG, "write_state '%s': new value = %s (wire = %s) type = %d address = %X offset = %x",
this->get_name().c_str(), ONOFF(state), ONOFF(write_value), (int) this->register_type, this->start_address,
this->offset);
bool queued;
if (this->register_type == EntityType::COIL) {
// offset for coil and discrete inputs is the coil/register number not bytes
if (this->use_write_multiple_) {
std::array<bool, 1> states{write_value};
queued = this->write_multiple_coils(this->write_address(), states);
} else {
queued = this->write_single_coil(this->write_address(), write_value);
}
if (!data.empty()) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_SWITCH_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus Switch write raw: %s",
format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
cmd.emplace(ModbusCommandItem::create_custom_command(
this->parent_, data,
[this](modbus::EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
this->parent_->on_write_register_response(register_type, this->start_address, data);
}));
} else {
if (this->use_write_multiple_) {
std::array<uint16_t, 1> states{static_cast<uint16_t>(write_value ? (0xFFFF & this->bitmask) : 0)};
queued = this->write_multiple_registers(this->write_address(), states);
ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(),
ONOFF(state), (int) this->register_type, this->start_address, this->offset);
if (this->register_type == modbus::EntityType::COIL) {
// offset for coil and discrete inputs is the coil/register number not bytes
if (this->use_write_multiple_) {
std::vector<bool> states{state};
cmd.emplace(ModbusCommandItem::create_write_multiple_coils(this->parent_, this->write_address(), states));
} else {
cmd.emplace(ModbusCommandItem::create_write_single_coil(this->parent_, this->write_address(), state));
}
} else {
queued = this->write_single_register(this->write_address(), write_value ? 0xFFFF & this->bitmask : 0u);
if (this->use_write_multiple_) {
std::vector<uint16_t> bool_states(1, state ? (0xFFFF & this->bitmask) : 0);
cmd.emplace(
ModbusCommandItem::create_write_multiple_command(this->parent_, this->write_address(), 1, bool_states));
} else {
cmd.emplace(ModbusCommandItem::create_write_single_command(this->parent_, this->write_address(),
state ? 0xFFFF & this->bitmask : 0u));
}
}
}
if (!queued) {
ESP_LOGW(TAG, "Modbus write for '%s' was refused by the hub; state not published", this->get_name().c_str());
return;
}
this->parent_->queue_command(std::move(*cmd));
this->publish_state(state);
}
// ModbusSwitch end
@@ -8,7 +8,7 @@
namespace esphome::modbus_controller {
class ModbusSwitch final : public Component, public switch_::Switch, public SensorItem, public WriterEntity {
class ModbusSwitch final : public Component, public switch_::Switch, public SensorItem {
public:
ModbusSwitch(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
bool force_new_range) {
@@ -18,13 +18,8 @@ class ModbusSwitch final : public Component, public switch_::Switch, public Sens
this->bitmask = bitmask;
this->sensor_value_type = SensorValueType::BIT;
this->register_count = 1;
// A holding byte offset folds into the address as whole registers (odd offsets are rejected at
// validation: a 16-bit register write cannot target half a register); a coil offset is a coil count.
if (register_type == modbus::EntityType::HOLDING) {
this->set_address(start_address + offset / 2);
this->set_offset_from_start_address(0);
} else if (register_type == modbus::EntityType::COIL) {
this->set_address(start_address + offset);
if (register_type == modbus::EntityType::HOLDING || register_type == modbus::EntityType::COIL) {
this->set_address(this->start_address + offset);
this->set_offset_from_start_address(0);
}
this->force_new_range = force_new_range;
@@ -35,16 +30,17 @@ class ModbusSwitch final : public Component, public switch_::Switch, public Sens
void set_assumed_state(bool assumed_state);
void set_state(bool state) { this->state = state; }
void parse_and_publish(std::span<const uint8_t> data) override;
void set_parent(ModbusController *parent) { this->set_controller_(parent); }
void set_parent(ModbusController *parent) { this->parent_ = parent; }
using transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, std::span<const uint8_t>);
using write_transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, modbus::helpers::PduBuffer &);
using write_transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, std::vector<uint8_t> &);
void set_template(transform_func_t f) { this->publish_transform_func_ = f; }
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
bool assumed_state() override;
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
optional<transform_func_t> publish_transform_func_{nullopt};
optional<write_transform_func_t> write_transform_func_{nullopt};
+1 -5
View File
@@ -41,11 +41,7 @@ MQTTClientComponent::MQTTClientComponent() {
global_mqtt_client = this;
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_addr);
const StringRef &name = App.get_name();
char client_id[MAX_NAME_WITH_SUFFIX_SIZE];
size_t len = make_name_with_suffix_to(client_id, sizeof(client_id), name.c_str(), name.size(), '-', mac_addr,
MAC_ADDRESS_BUFFER_SIZE - 1);
this->credentials_.client_id.assign(client_id, len);
this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr, MAC_ADDRESS_BUFFER_SIZE - 1);
}
// Connection
-2
View File
@@ -188,8 +188,6 @@ struct IPAddress {
}
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
bool is_ip4() const { return true; }
bool is_ip6() const { return false; }
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
char *str_to(char *buf) const {
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
+8 -3
View File
@@ -125,8 +125,10 @@ def set_core_data(config: ConfigType) -> ConfigType:
return config
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF)
_resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF)
def _resolve_toolchain(config: ConfigType) -> ConfigType:
if CORE.toolchain is None:
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.SDK_NRF)
return config
def set_framework(config: ConfigType) -> ConfigType:
@@ -168,7 +170,10 @@ BOOTLOADERS = [
]
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
def _validate_toolchain(value) -> Toolchain:
return Toolchain(
cv.one_of(Toolchain.PLATFORMIO, Toolchain.SDK_NRF, lower=True)(value)
)
def _detect_bootloader(config: ConfigType) -> ConfigType:
+12 -4
View File
@@ -6,7 +6,8 @@ import platform
import shutil
import sys
from esphome.build_helpers.tools_cache import SDK_NRF_TOOLS_CACHE, tools_cache_path
import platformdirs
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE, EsphomeError
@@ -18,6 +19,7 @@ from esphome.framework_helpers import (
run_command_ok,
str_to_lst_of_str,
)
from esphome.helpers import get_str_env
_LOGGER = logging.getLogger(__name__)
@@ -47,9 +49,15 @@ SDK_NG_MINIMAL_MIRRORS = str_to_lst_of_str(
def get_sdk_nrf_tools_path() -> Path:
# Machine-global (OS user cache dir) so all projects share one install;
# see espidf.framework.get_idf_tools_path for the location rationale.
return tools_cache_path(*SDK_NRF_TOOLS_CACHE)
# A blank ESPHOME_SDK_NRF_PREFIX must be treated as unset: Path("")
# resolves to the CWD, which clean-all would then delete.
if prefix := get_str_env("ESPHOME_SDK_NRF_PREFIX", "").strip():
path = Path(prefix).expanduser()
else:
# Machine-global (OS user cache dir) so all projects share one install;
# see espidf.framework.get_idf_tools_path for the location rationale.
path = Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "sdk-nrf"
return path.resolve()
def _needs_venv_rebuild(
+19 -37
View File
@@ -4,7 +4,6 @@
#include <openthread/cli.h>
#include <openthread/instance.h>
#include <openthread/ip6.h>
#include <openthread/logging.h>
#include <openthread/netdata.h>
#include <openthread/tasklet.h>
@@ -230,43 +229,26 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) {
void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; }
bool OpenThreadComponent::teardown() {
switch (this->teardown_stage_) {
case TeardownStage::TEARDOWN_STAGE_NOT_STARTED: {
auto lock = InstanceLock::try_acquire(100);
if (!lock) {
// Try again on next teardown loop
ESP_LOGV(TAG, "Failed to acquire OpenThread lock during teardown");
return false;
}
// Start tearing down
this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_STOP_IN_PROCESS;
ESP_LOGV(TAG, "Clear SRP");
otInstance *instance = lock.get_instance();
otSrpClientClearHostAndServices(instance);
otSrpClientBuffersFreeAllServices(instance);
if (otThreadSetEnabled(instance, false) != OT_ERROR_NONE) {
ESP_LOGW(TAG, "Failed to disable Thread during teardown");
}
if (otIp6SetEnabled(instance, false) != OT_ERROR_NONE) {
ESP_LOGW(TAG, "Failed to disable IPv6 during teardown");
}
// Stop OpenThread
global_openthread_component = nullptr;
ESP_LOGV(TAG, "Stop OpenThread");
int error = this->openthread_stop_();
if (error != 0) {
ESP_LOGW(TAG, "Failed attempt to stop OpenThread %d", error);
this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
}
} break;
case TeardownStage::TEARDOWN_STAGE_STOP_IN_PROCESS:
// Waiting on OpenThread stop
break;
case TeardownStage::TEARDOWN_STAGE_COMPLETED:
ESP_LOGV(TAG, "OpenThreadComponent Teardown Complete");
break;
if (!this->teardown_started_) {
this->teardown_started_ = true;
ESP_LOGD(TAG, "Clear Srp");
auto lock = InstanceLock::try_acquire(100);
if (!lock) {
ESP_LOGW(TAG, "Failed to acquire OpenThread lock during teardown, leaking memory");
return true;
}
otInstance *instance = lock.get_instance();
otSrpClientClearHostAndServices(instance);
otSrpClientBuffersFreeAllServices(instance);
global_openthread_component = nullptr;
ESP_LOGD(TAG, "Exit main loop ");
int error = this->openthread_stop_();
if (error != 0) {
ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error);
this->teardown_complete_ = true;
}
}
return this->teardown_stage_ == TeardownStage::TEARDOWN_STAGE_COMPLETED;
return this->teardown_complete_;
}
void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
+3 -8
View File
@@ -19,12 +19,6 @@ namespace esphome::openthread {
class InstanceLock;
enum class TeardownStage : uint8_t {
TEARDOWN_STAGE_NOT_STARTED = 0,
TEARDOWN_STAGE_STOP_IN_PROCESS,
TEARDOWN_STAGE_COMPLETED,
};
template<typename... Ts> class OpenThreadComponentPollPeriodAction;
class OpenThreadComponent final : public Component {
@@ -77,8 +71,9 @@ class OpenThreadComponent final : public Component {
#endif
std::optional<int8_t> output_power_{};
std::atomic<bool> lock_initialized_{false};
std::atomic<TeardownStage> teardown_stage_{TeardownStage::TEARDOWN_STAGE_NOT_STARTED};
std::atomic<bool> connected_{false};
bool teardown_started_{false};
bool teardown_complete_{false};
bool connected_{false};
private:
// Stores a pointer to a string literal (static storage duration).
@@ -168,7 +168,7 @@ void OpenThreadComponent::ot_main() {
esp_netif_destroy(openthread_netif);
esp_vfs_eventfd_unregister();
this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
this->teardown_complete_ = true;
vTaskDelete(NULL);
}
@@ -90,8 +90,10 @@ void OpenThreadComponent::ot_main() {}
otInstance *OpenThreadComponent::get_openthread_instance_() { return openthread_get_default_instance(); }
int OpenThreadComponent::openthread_stop_() {
// Zephyr has no stack-stop API, so stop is synchronous here; mark complete immediately.
this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED;
// OT stack is intentionally left running — no Zephyr stop API. The state callback stays
// registered but is safe (null-checks global_openthread_component). nRF52840 never
// re-enters setup() after teardown so this is functionally correct.
this->teardown_complete_ = true;
return 0;
}
@@ -23,8 +23,6 @@ class ProvisioningData:
sources: set[str] = field(default_factory=set)
# Names of source components that have their credentials set in the config.
hardcoded_credentials: set[str] = field(default_factory=set)
# True when WiFi is configured with an access point but no station credentials.
ap_without_sta: bool = False
def _get_data() -> ProvisioningData:
@@ -58,17 +56,6 @@ def report_hardcoded_credentials(name: str) -> None:
_get_data().hardcoded_credentials.add(name)
def report_ap_without_sta() -> None:
"""Record that WiFi runs an access point with no station credentials.
The access point (and captive portal) shut down when the provisioning window
closes. On a device where that access point is the only network connection,
closing the window makes the device unreachable until it is power-cycled, so
`provisioning:` warns about this combination.
"""
_get_data().ap_without_sta = True
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ProvisioningManager),
@@ -102,13 +89,6 @@ def _final_validate(config: ConfigType) -> None:
"hardcoding them makes the window pointless.",
", ".join(sorted(data.hardcoded_credentials)),
)
if data.ap_without_sta:
_LOGGER.warning(
"'provisioning' is configured with a WiFi access point and no station "
"credentials. The access point shuts down when the provisioning window "
"closes, so if it is the device's only network connection, the device "
"will be unreachable until it is power-cycled."
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -3,12 +3,6 @@ import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base
from esphome.components.libretiny import get_libretiny_family
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7238,
FAMILY_RTL8720C,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -49,21 +43,6 @@ DigitalWriteAction = remote_transmitter_ns.class_(
)
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238)
def _validate_non_blocking_platform(value: bool) -> bool:
# non_blocking requires hardware transmission: RMT on ESP32, a hardware timer
# envelope chain on the listed LibreTiny families. Reject elsewhere at config time.
if CORE.is_esp32:
return cv.boolean(value)
if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES:
return cv.boolean(value)
raise cv.Invalid(
"non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238"
)
MULTI_CONF = True
CONFIG_SCHEMA = (
cv.Schema(
@@ -97,7 +76,7 @@ CONFIG_SCHEMA = (
esp32_s2=64,
esp32_s3=48,
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
cv.Optional(CONF_NON_BLOCKING): _validate_non_blocking_platform,
cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean),
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
}
@@ -185,8 +164,6 @@ async def to_code(config: ConfigType) -> None:
)
else:
var = cg.new_Pvariable(config[CONF_ID], pin)
if (non_blocking := config.get(CONF_NON_BLOCKING)) is not None:
cg.add(var.set_non_blocking(non_blocking))
await cg.register_component(var, config)
cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT]))
@@ -211,13 +188,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
"remote_transmitter_rtl87xx.cpp": {
PlatformFramework.RTL87XX_ARDUINO,
},
"remote_transmitter_bk72xx.cpp": {
PlatformFramework.BK72XX_ARDUINO,
},
"remote_transmitter_libretiny_isr.cpp": {
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
},
"remote_transmitter.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
@@ -2,8 +2,8 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#if (defined(USE_LIBRETINY) && !defined(USE_RTL87XX) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || \
defined(USE_ESP8266) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
#if (defined(USE_LIBRETINY) && !defined(USE_RTL87XX)) || defined(USE_ESP8266) || defined(USE_RP2) || \
(defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
namespace esphome::remote_transmitter {
@@ -12,13 +12,6 @@
#endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven
// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate.
// See remote_transmitter_bk72xx.cpp.
#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238)
#define REMOTE_TRANSMITTER_BK_PWM
#endif
namespace esphome::remote_transmitter {
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
@@ -63,32 +56,19 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
#endif
#if (defined(USE_ESP32) && SOC_RMT_SUPPORTED) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || \
defined(REMOTE_TRANSMITTER_BK_PWM)
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
#endif
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
void loop() override;
// called from the envelope timer ISR trampoline; not part of the public API
void advance_envelope_isr();
// same, for trampolines whose SDK callback carries no user argument
static void advance_active_isr();
#endif
Trigger<> *get_transmit_trigger() { return &this->transmit_trigger_; }
Trigger<> *get_complete_trigger() { return &this->complete_trigger_; }
protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override;
#if defined(USE_ESP8266) || \
(defined(USE_LIBRETINY) && !defined(USE_LIBRETINY_VARIANT_RTL8720C) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || \
defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
void await_target_time_();
uint32_t target_time_{0};
#endif
#if defined(USE_ESP8266) || \
(defined(USE_LIBRETINY) && !defined(USE_RTL87XX) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || defined(USE_RP2) || \
#if defined(USE_ESP8266) || (defined(USE_LIBRETINY) && !defined(USE_RTL87XX)) || defined(USE_RP2) || \
(defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period);
@@ -101,43 +81,6 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
uint32_t current_carrier_frequency_{0};
void *pwm_{nullptr}; // pwmout_t*, opaque here to keep the SDK header out of this shared header
#endif
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
// Envelope chain, shared by every family that paces transmission from a hardware timer
// (remote_transmitter_libretiny_isr.cpp)
void start_isr_item_(size_t index);
void arm_envelope_timer_(uint32_t duration_us);
void abort_stalled_chain_();
void deliver_completion_();
void wait_until_idle_();
void arm_chain_(uint32_t send_times, uint32_t send_wait);
// Hooks implemented per family: everything the chain needs from the hardware
bool envelope_ready_() const; // PWM claimed successfully in setup()
void prepare_carrier_(uint32_t carrier_frequency); // retune period, stage mark/space levels
void write_envelope_level_(bool mark); // drive carrier (mark) or idle (space)
void arm_one_shot_(uint32_t duration_us); // fire advance_envelope_isr after duration_us
void stop_envelope_timer_();
std::vector<int32_t> isr_data_; // owned copy of the frame; temp_ may be re-encoded mid-flight
volatile size_t isr_index_{0};
volatile uint32_t isr_repeats_left_{0};
uint32_t isr_send_wait_{0};
volatile uint32_t isr_wait_remaining_{0}; // remainder of a duration chained across one-shots
volatile bool isr_in_gap_{false};
volatile bool transmitting_{false};
bool non_blocking_{false};
bool complete_pending_{false};
bool stall_aborted_{false}; // this transmission ended via abort; blocks warning clear
#endif
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
float isr_mark_duty_{0.0f};
float isr_space_duty_{0.0f};
#endif
#ifdef REMOTE_TRANSMITTER_BK_PWM
void write_pwm_t1_(uint32_t t1_counts);
uint32_t isr_mark_t1_{0};
uint32_t isr_space_t1_{0};
uint32_t isr_period_t4_{684}; // 26MHz counts; ~38kHz default until a send sets the real carrier
int8_t pwm_channel_{-1};
#endif
#if defined(USE_ESP32) && SOC_RMT_SUPPORTED
void configure_rmt_();
@@ -1,187 +0,0 @@
#include "remote_transmitter.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
// clang-tidy cannot parse the Beken SDK headers pulled in via ArduinoPrivate.h
#if defined(USE_BK72XX) && !defined(CLANG_TIDY)
// ArduinoPrivate.h = Arduino.h + the BDK SDK headers (pwm_pub.h, bk_timer_pub.h, icu_pub.h)
// with the core's fixes for type-name collisions between the two
#include <ArduinoPrivate.h>
// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang
// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing.
// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h.
namespace esphome::remote_transmitter {
static const char *const TAG = "remote_transmitter";
#ifdef REMOTE_TRANSMITTER_BK_PWM
// PWM peripheral carrier (26MHz block), envelope paced by a BKTIMER1 interrupt chain: each
// interrupt writes the next duty through the shadow registers (T1..T4 + CFG_UPDATA hardware
// load, glitch-free at the next carrier period). Direct register writes beat the driver's
// pwm_update_param() (~19us vs ~26us edge error) and have no shared state to race against.
// BKTIMER1 is the only free channel: TIMER0 = FreeRTOS tick, TIMER2 = SDK cal, TIMER4 = wdt.
static constexpr uint32_t REG_PWM_BASE = 0x00802B00UL;
static constexpr uint32_t REG_PWM_GROUP_STRIDE = 0x40; // one register group per channel pair
static constexpr uint32_t REG_PWM_T_REGS[2] = {0x04, 0x14}; // T1..T4 offsets within a group
static constexpr uint32_t PWM_INT_STATUS_MASK = 3UL << 30; // write-1-clear -- always write as zero
static constexpr uint8_t ENVELOPE_TIMER = BKTIMER1;
// The bk_timer handler receives only the channel number, so the chain resolves the instance
// that owns the timer. No IRAM_ATTR: hal.h makes it a no-op on BK72xx (the SDK masks IRQs
// around flash writes).
static void envelope_timer_isr(UINT8 channel) { RemoteTransmitterComponent::advance_active_isr(); }
// Channel <-> pin comes from the board variant's own PIN_PWMn defines rather than a
// family-wide assumption, so an unusual pinout maps correctly instead of silently
// driving another pad
struct PwmPinChannel {
uint8_t pin;
int8_t channel;
};
static constexpr PwmPinChannel PWM_PIN_CHANNELS[] = {
#ifdef PIN_PWM0
{PIN_PWM0, 0},
#endif
#ifdef PIN_PWM1
{PIN_PWM1, 1},
#endif
#ifdef PIN_PWM2
{PIN_PWM2, 2},
#endif
#ifdef PIN_PWM3
{PIN_PWM3, 3},
#endif
#ifdef PIN_PWM4
{PIN_PWM4, 4},
#endif
#ifdef PIN_PWM5
{PIN_PWM5, 5},
#endif
};
static int8_t pwm_channel_for_pin(uint8_t pin) {
for (const auto &entry : PWM_PIN_CHANNELS) {
if (entry.pin == pin)
return entry.channel;
}
return -1;
}
void RemoteTransmitterComponent::setup() {
// Deliberately no pin_->setup(): the pin must belong to the PWM function, not GPIO
const int8_t channel = pwm_channel_for_pin(this->pin_->get_pin());
if (channel < 0) {
ESP_LOGE(TAG, "Pin %u is not PWM-capable", this->pin_->get_pin());
this->mark_failed();
return;
}
this->pwm_channel_ = channel;
const uint32_t idle_t1 = this->pin_->is_inverted() ? this->isr_period_t4_ : 0;
pwm_param_st param{};
param.chan = channel;
param.t1 = idle_t1;
param.t4 = this->isr_period_t4_;
param.init_level = idle_t1 ? 1 : 0;
if (pwm_init_param(&param) != 0 || pwm_start(channel) != 0) {
ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin());
this->pwm_channel_ = -1;
this->mark_failed();
return;
}
this->disable_loop(); // loop() is only needed while a non-blocking completion is pending
}
void RemoteTransmitterComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Transmitter:\n"
" Carrier Duty: %u%%\n"
" Non-blocking: %s",
this->carrier_duty_percent_, YESNO(this->non_blocking_));
LOG_PIN(" Pin: ", this->pin_);
}
// Writes the duty compare registers and sets the hardware CFG_UPDATA shadow-load bit;
// the new duty latches glitch-free at the next carrier period. ISR-safe: registers only.
// The group control word is shared with the paired channel, but every SDK write to it runs
// under GLOBAL_INT_DISABLE (bk_pwm), so it cannot be torn by this interrupt.
void RemoteTransmitterComponent::write_pwm_t1_(uint32_t t1_counts) {
const uint32_t group = this->pwm_channel_ / 2;
const uint32_t post = this->pwm_channel_ % 2;
const uint32_t group_base = REG_PWM_BASE + REG_PWM_GROUP_STRIDE * group;
auto *t_regs = (volatile uint32_t *) (group_base + REG_PWM_T_REGS[post]);
auto *ctrl = (volatile uint32_t *) group_base;
const uint32_t init_level_bit = 1UL << (8 * post + 6); // output level while the counter is stopped
const uint32_t cfg_updata_bit = 1UL << (8 * post + 7); // 0->1 latches T1..T4 at the next period
t_regs[0] = t1_counts; // T1: high time
t_regs[1] = 0; // T2
t_regs[2] = 0; // T3
t_regs[3] = this->isr_period_t4_; // T4: period
uint32_t cfg = *ctrl;
cfg &= ~(PWM_INT_STATUS_MASK | init_level_bit | cfg_updata_bit);
if (t1_counts != 0)
cfg |= init_level_bit;
*ctrl = cfg;
*ctrl = cfg | cfg_updata_bit;
}
// --- envelope chain hooks (see remote_transmitter_libretiny_isr.cpp) ---
bool RemoteTransmitterComponent::envelope_ready_() const { return this->pwm_channel_ >= 0; }
// Recomputes the carrier period in 26MHz counts and stages the per-item duties;
// unmodulated protocols drive the pin constantly during marks
void RemoteTransmitterComponent::prepare_carrier_(uint32_t carrier_frequency) {
if (carrier_frequency > 0) {
this->isr_period_t4_ = std::max(uint32_t(2), (26000000UL + carrier_frequency / 2) / carrier_frequency);
}
uint32_t mark_t1 = (carrier_frequency > 0 && this->carrier_duty_percent_ < 100)
? std::max(uint32_t(1), this->isr_period_t4_ * this->carrier_duty_percent_ / 100)
: this->isr_period_t4_;
uint32_t space_t1 = 0;
if (this->pin_->is_inverted()) {
mark_t1 = this->isr_period_t4_ - mark_t1;
space_t1 = this->isr_period_t4_;
}
this->isr_mark_t1_ = mark_t1;
this->isr_space_t1_ = space_t1;
}
void RemoteTransmitterComponent::write_envelope_level_(bool mark) {
this->write_pwm_t1_(mark ? this->isr_mark_t1_ : this->isr_space_t1_);
}
// The driver's microsecond init path is register writes under a nested interrupt guard,
// so it is safe to call from the chain's own interrupt
void RemoteTransmitterComponent::arm_one_shot_(uint32_t duration_us) {
timer_param_t param{};
param.channel = ENVELOPE_TIMER;
param.div = 1;
param.period = duration_us;
param.t_Int_Handler = envelope_timer_isr;
sddev_control((char *) TIMER_DEV_NAME, CMD_TIMER_INIT_PARAM_US, &param);
}
void RemoteTransmitterComponent::stop_envelope_timer_() {
UINT32 channel = ENVELOPE_TIMER;
sddev_control((char *) TIMER_DEV_NAME, CMD_TIMER_UNIT_DISABLE, &channel);
}
void RemoteTransmitterComponent::digital_write(bool value) {
if (this->pwm_channel_ < 0)
return;
// serialize behind an in-flight chain, matching the ESP32/RMT non-blocking behavior
this->wait_until_idle_();
this->write_pwm_t1_((value != this->pin_->is_inverted()) ? this->isr_period_t4_ : 0);
}
#endif // REMOTE_TRANSMITTER_BK_PWM
} // namespace esphome::remote_transmitter
#endif // USE_BK72XX && !CLANG_TIDY
@@ -1,224 +0,0 @@
#include "remote_transmitter.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware
// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything
// platform-specific sits behind five hooks implemented in the per-family files -- carrier
// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep
// the generic bit-bang implementation and compile none of this.
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
namespace esphome::remote_transmitter {
static const char *const TAG = "remote_transmitter";
// Margin past a transmission's expected duration before the chain is declared stalled
static constexpr uint32_t STALL_MARGIN_MS = 1000;
// Longest single one-shot armed; longer durations are chained. Both families need the cap:
// the Beken driver computes period_us * 26 in 32 bits (overflows past ~165s) and the Realtek
// us->tick conversion lives in mask ROM with unverified headroom.
static constexpr uint32_t MAX_ONE_SHOT_US = 50000;
// One hardware timer is shared by all instances (MULTI_CONF), so they serialize on this
// token; the deadline always describes whichever chain currently owns it.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
static RemoteTransmitterComponent *volatile s_active_transmitter = nullptr;
static uint32_t s_expected_end_ms = 0;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
// Entry point for trampolines whose SDK callback carries no user argument
void IRAM_ATTR RemoteTransmitterComponent::advance_active_isr() {
auto *transmitter = s_active_transmitter;
if (transmitter != nullptr)
transmitter->advance_envelope_isr();
}
// Arms the envelope timer, chaining durations longer than MAX_ONE_SHOT_US. ISR-safe.
void IRAM_ATTR RemoteTransmitterComponent::arm_envelope_timer_(uint32_t duration_us) {
// clamp to 1us (a zero-length one-shot never fires); the remainder must not underflow
const uint32_t chunk = std::max(uint32_t(1), std::min(duration_us, MAX_ONE_SHOT_US));
this->isr_wait_remaining_ = duration_us > chunk ? duration_us - chunk : 0;
this->arm_one_shot_(chunk);
}
// Writes the level for one envelope item and arms the timer for its duration.
// Runs in ISR context (and once from arm_chain_ to kick the chain): no logging, no allocation.
void IRAM_ATTR RemoteTransmitterComponent::start_isr_item_(size_t index) {
const int32_t item = this->isr_data_[index];
this->write_envelope_level_(item > 0);
this->arm_envelope_timer_(uint32_t(item > 0 ? item : -item));
}
void IRAM_ATTR RemoteTransmitterComponent::advance_envelope_isr() {
if (!this->transmitting_)
return; // chain was aborted; this is a stale one-shot that was already latched
if (this->isr_wait_remaining_ > 0) {
// continue a duration longer than one hardware one-shot
this->arm_envelope_timer_(this->isr_wait_remaining_);
return;
}
if (this->isr_in_gap_) {
// inter-repeat gap elapsed; restart the item chain
this->isr_in_gap_ = false;
this->isr_index_ = 0;
this->start_isr_item_(0);
return;
}
this->isr_index_ = this->isr_index_ + 1;
if (this->isr_index_ < this->isr_data_.size()) {
this->start_isr_item_(this->isr_index_);
return;
}
// end of one repetition
this->write_envelope_level_(false);
if (this->isr_repeats_left_ > 1) {
this->isr_repeats_left_ = this->isr_repeats_left_ - 1;
this->isr_index_ = 0;
if (this->isr_send_wait_ > 0) {
this->isr_in_gap_ = true;
this->arm_envelope_timer_(this->isr_send_wait_);
} else {
this->start_isr_item_(0);
}
return;
}
// required on Beken (its timer reloads); on Realtek this only clears the enable bit of a
// one-shot that has already fired
this->stop_envelope_timer_();
this->transmitting_ = false;
s_active_transmitter = nullptr;
}
// Aborts a chain that stopped advancing: stop the timer, idle the pin, release the token.
// Every step is a no-op if the chain completed meanwhile. Task context only.
void RemoteTransmitterComponent::abort_stalled_chain_() {
// cleared first so a straggler one-shot bails at the ISR entry check
this->transmitting_ = false;
this->stop_envelope_timer_();
this->write_envelope_level_(false);
s_active_transmitter = nullptr;
this->stall_aborted_ = true;
this->status_set_warning("envelope timer stalled");
ESP_LOGE(TAG, "Envelope timer stalled; transmission aborted");
delay(1); // let any already-latched interrupt land while the chain state is safe
}
// Delivers one deferred completion with its status bookkeeping
void RemoteTransmitterComponent::deliver_completion_() {
if (!this->stall_aborted_)
this->status_clear_warning();
this->complete_pending_ = false;
this->complete_trigger_.trigger();
}
// Waits until no chain is in flight, delivering any deferred completions; a completion
// automation may start a new send, so repeat until truly idle. Bounded by the stall deadline.
void RemoteTransmitterComponent::wait_until_idle_() {
while (true) {
while (true) {
// snapshot: the final ISR can clear the volatile pointer between a check and a use
auto *active = s_active_transmitter;
if (active == nullptr)
break;
if ((int32_t) (millis() - s_expected_end_ms) > 0) {
active->abort_stalled_chain_();
break;
}
App.feed_wdt();
delay(1);
}
if (!this->complete_pending_)
break;
this->deliver_completion_();
}
}
// Stages the repeat schedule and stall deadline, then starts the interrupt chain
void RemoteTransmitterComponent::arm_chain_(uint32_t send_times, uint32_t send_wait) {
this->isr_repeats_left_ = send_times;
this->isr_send_wait_ = send_wait;
this->isr_index_ = 0;
this->isr_in_gap_ = false;
this->stall_aborted_ = false;
uint64_t frame_us = 0;
for (int32_t item : this->isr_data_)
frame_us += uint32_t(item > 0 ? item : -item);
const uint64_t total_us = frame_us * send_times + uint64_t(send_wait) * (send_times - 1);
s_expected_end_ms = millis() + uint32_t(total_us / 1000) + STALL_MARGIN_MS;
this->transmitting_ = true;
s_active_transmitter = this;
this->start_isr_item_(0);
}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (!this->envelope_ready_()) {
// both triggers still fire, so an on_complete-sequenced automation does not stall
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
this->transmit_trigger_.trigger();
this->deliver_completion_();
return;
}
this->wait_until_idle_();
if (send_times == 0) {
// parity with the loop-based implementations: transmit nothing, but both triggers
// still fire so an on_complete-sequenced automation does not stall
this->transmit_trigger_.trigger();
this->deliver_completion_();
return;
}
ESP_LOGD(TAG, "Sending remote code");
this->prepare_carrier_(this->temp_.get_carrier_frequency());
// own copy: with non_blocking the caller may re-encode temp_ while this frame is in flight
this->isr_data_.assign(this->temp_.get_data().begin(), this->temp_.get_data().end());
if (this->isr_data_.empty()) {
ESP_LOGW(TAG, "Empty data");
this->transmit_trigger_.trigger();
this->deliver_completion_();
return;
}
// trigger first: the deadline computed in arm_chain_ must not be charged for user code
this->transmit_trigger_.trigger();
// the automation may have started a send on another instance; let it finish before
// claiming the shared timer (a same-instance send remains unsupported here)
this->wait_until_idle_();
this->arm_chain_(send_times, send_wait);
if (this->non_blocking_) {
this->complete_pending_ = true;
this->enable_loop();
return;
}
// blocking mode: wait out the chain, bounded by the stall deadline
while (this->transmitting_) {
if ((int32_t) (millis() - s_expected_end_ms) > 0) {
this->abort_stalled_chain_();
break;
}
App.feed_wdt();
delay(1);
}
this->deliver_completion_();
}
void RemoteTransmitterComponent::loop() {
if (!this->complete_pending_) {
this->disable_loop();
return;
}
if (this->transmitting_) {
// non-blocking stall recovery: without this, a dead chain would leave the carrier
// driven and on_complete unfired until the next send happened to abort it
if ((int32_t) (millis() - s_expected_end_ms) <= 0)
return;
this->abort_stalled_chain_();
}
// release the loop before user code runs: the automation may start a new non-blocking
// send, and its enable_loop() must be the last writer or its completion would strand
this->disable_loop();
this->deliver_completion_();
}
} // namespace esphome::remote_transmitter
#endif // USE_LIBRETINY_VARIANT_RTL8720C || REMOTE_TRANSMITTER_BK_PWM
@@ -5,42 +5,31 @@
// clang-tidy cannot parse the Realtek SDK headers pulled in via ArduinoPrivate.h
#if defined(USE_RTL87XX) && !defined(CLANG_TIDY)
// ArduinoPrivate.h = Arduino.h + the SDK's mbed HAL (pwmout, gtimer) with the core's fixes for
// ArduinoPrivate.h = Arduino.h + the SDK's mbed HAL (pwmout etc.) with the core's fixes for
// type-name collisions between the two (e.g. PinMode)
#include <ArduinoPrivate.h>
#ifndef USE_LIBRETINY_VARIANT_RTL8720C
#include <FreeRTOS.h>
#include <task.h>
#endif
namespace esphome::remote_transmitter {
static const char *const TAG = "remote_transmitter";
// PWM peripheral carrier, envelope paced by a gtimer interrupt chain. Bit-banging would need
// interrupts disabled for the whole frame, but this core's micros() derives from the FreeRTOS
// tick and freezes then. The SDK pwmout HAL is driven directly: the Arduino wiring layer's
// GPIO/PWM mode round-trip use-after-frees LibreTiny's per-pin state.
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
static constexpr uint32_t ENVELOPE_TIMER_ID = TIMER6; // GTimer7
// One envelope timer for all instances: a second gtimer_init on the same id fails silently,
// so the chain serializes them (remote_transmitter_libretiny_isr.cpp)
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
static uint8_t s_pwm_tick_sources[] = {GTimer1, GTimer2, GTimer3, GTimer4, GTimer5, GTimer6, 0xff};
static gtimer_t s_envelope_timer;
static bool s_envelope_timer_ready = false;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static void IRAM_ATTR envelope_timer_isr(uint32_t arg) {
reinterpret_cast<RemoteTransmitterComponent *>(arg)->advance_envelope_isr();
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
// The carrier is generated by the PWM peripheral instead of bit-banging the pin: software carrier
// generation requires disabling interrupts for the whole frame, but this core's micros() is derived
// from the FreeRTOS tick and freezes while interrupts are off, so the timing loop never advances and
// the watchdog resets the chip. With hardware PWM, software only times the mark/space envelope and
// interrupts can stay enabled.
//
// The PWM is driven through the SDK's pwmout HAL directly rather than the Arduino wiring layer:
// changing the carrier frequency via the wiring requires a GPIO/PWM pin mode round-trip, which
// use-after-frees the core's per-pin state (pinRemoveMode() frees without nulling) and corrupts the
// heap. pwmout_period_us() changes the frequency with no mode transitions.
void RemoteTransmitterComponent::setup() {
// no pin_->setup(): a GPIO claim in the SDK's pin management blocks pwmout_init from
// owning the pad
// Deliberately no pin_->setup(): registering the pin as GPIO claims it in the SDK's pin
// management, and the pad is then never handed over to the PWM peripheral -- pwmout_init()
// must own the pin from the start.
PinInfo *info = pinInfo(this->pin_->get_pin());
if (info == nullptr || !pinSupported(info, PIN_PWM)) {
// checked here because the AmebaZ (RTL8710B) SDK does not report PWM init failure
@@ -51,7 +40,7 @@ void RemoteTransmitterComponent::setup() {
auto *pwm = new pwmout_t();
this->pwm_ = pwm;
pwmout_init(pwm, static_cast<PinName>(info->gpio));
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
#if LT_RTL8720C
// only the AmebaZ2 SDK's pwmout_s reports init success
if (!pwm->is_init) {
ESP_LOGE(TAG, "PWM init failed on pin %u", this->pin_->get_pin());
@@ -60,19 +49,9 @@ void RemoteTransmitterComponent::setup() {
this->mark_failed();
return;
}
// Shrink the PWM tick-source pool before the period claim below so GTimer7 stays free
// for the envelope; pwmout_init just registered the full pool.
hal_pwm_comm_tick_source_list(s_pwm_tick_sources);
#endif
pwmout_period_us(pwm, 26); // placeholder; the real carrier period is set per transmission
pwmout_write(pwm, this->pin_->is_inverted() ? 1.0f : 0.0f);
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
if (!s_envelope_timer_ready) {
gtimer_init(&s_envelope_timer, ENVELOPE_TIMER_ID);
s_envelope_timer_ready = true;
}
this->disable_loop(); // loop() is only needed while a non-blocking completion is pending
#endif
}
void RemoteTransmitterComponent::dump_config() {
@@ -80,59 +59,9 @@ void RemoteTransmitterComponent::dump_config() {
"Remote Transmitter:\n"
" Carrier Duty: %u%%",
this->carrier_duty_percent_);
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
ESP_LOGCONFIG(TAG, " Non-blocking: %s", YESNO(this->non_blocking_));
#endif
LOG_PIN(" Pin: ", this->pin_);
}
void RemoteTransmitterComponent::digital_write(bool value) {
if (this->pwm_ == nullptr)
return;
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
// serialize behind an in-flight chain, matching the ESP32/RMT non-blocking behavior
this->wait_until_idle_();
#endif
pwmout_write(static_cast<pwmout_t *>(this->pwm_), (value != this->pin_->is_inverted()) ? 1.0f : 0.0f);
}
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
// --- envelope chain hooks (see remote_transmitter_libretiny_isr.cpp) ---
bool RemoteTransmitterComponent::envelope_ready_() const { return this->pwm_ != nullptr; }
// Retunes the PWM period when the carrier changes and stages the per-item duties;
// unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
void RemoteTransmitterComponent::prepare_carrier_(uint32_t carrier_frequency) {
float mark_duty =
(carrier_frequency > 0 && this->carrier_duty_percent_ < 100) ? this->carrier_duty_percent_ / 100.0f : 1.0f;
float space_duty = 0.0f;
if (this->pin_->is_inverted()) {
mark_duty = 1.0f - mark_duty;
space_duty = 1.0f;
}
this->isr_mark_duty_ = mark_duty;
this->isr_space_duty_ = space_duty;
if (carrier_frequency == 0 || carrier_frequency == this->current_carrier_frequency_)
return;
// round(1000000/freq), clamped so a bad lambda can't hand the SDK a zero period
const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency);
pwmout_period_us(static_cast<pwmout_t *>(this->pwm_), period);
this->current_carrier_frequency_ = carrier_frequency;
}
void IRAM_ATTR RemoteTransmitterComponent::write_envelope_level_(bool mark) {
pwmout_write(static_cast<pwmout_t *>(this->pwm_), mark ? this->isr_mark_duty_ : this->isr_space_duty_);
}
void IRAM_ATTR RemoteTransmitterComponent::arm_one_shot_(uint32_t duration_us) {
gtimer_start_one_shout(&s_envelope_timer, duration_us, (void *) envelope_timer_isr, (uint32_t) this);
}
void IRAM_ATTR RemoteTransmitterComponent::stop_envelope_timer_() { gtimer_stop(&s_envelope_timer); }
#else // !USE_LIBRETINY_VARIANT_RTL8720C -- AmebaZ (RTL8710B): spin-based envelope, per-frame priority boost
void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
@@ -143,8 +72,15 @@ void RemoteTransmitterComponent::await_target_time_() {
}
}
void RemoteTransmitterComponent::digital_write(bool value) {
if (this->pwm_ == nullptr)
return;
pwmout_write(static_cast<pwmout_t *>(this->pwm_), (value != this->pin_->is_inverted()) ? 1.0f : 0.0f);
}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->pwm_ == nullptr) {
auto *pwm = static_cast<pwmout_t *>(this->pwm_);
if (pwm == nullptr) {
ESP_LOGW(TAG, "Cannot send: PWM not initialized");
return;
}
@@ -158,7 +94,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
mark_duty = 1.0f - mark_duty;
space_duty = 1.0f;
}
auto *pwm = static_cast<pwmout_t *>(this->pwm_);
if (carrier_frequency > 0 && carrier_frequency != this->current_carrier_frequency_) {
// round(1000000/freq), clamped like the bit-bang path so a bad lambda can't hand the SDK a zero period
const uint32_t period = std::max(uint32_t(1), (1000000UL + carrier_frequency / 2) / carrier_frequency);
@@ -197,8 +132,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
this->complete_trigger_.trigger();
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
} // namespace esphome::remote_transmitter
#endif // USE_RTL87XX && !CLANG_TIDY
-1
View File
@@ -312,7 +312,6 @@ CONFIG_SCHEMA = cv.All(
),
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
_detect_variant,
cv.require_platformio_toolchain("RP2"),
set_core_data,
)
+1 -26
View File
@@ -10,7 +10,6 @@ from esphome.components.image import (
validate_transparency,
validate_type,
)
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
from esphome.core import CORE
@@ -28,7 +27,6 @@ ImageDecoder = runtime_image_ns.class_("ImageDecoder")
BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder)
JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder)
PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder)
QoiDecoder = runtime_image_ns.class_("QoiDecoder", ImageDecoder)
# Runtime image class
RuntimeImage = runtime_image_ns.class_(
@@ -38,10 +36,9 @@ RuntimeImage = runtime_image_ns.class_(
# Image format enum
ImageFormat = runtime_image_ns.enum("ImageFormat")
IMAGE_FORMAT_AUTO = ImageFormat.AUTO
IMAGE_FORMAT_BMP = ImageFormat.BMP
IMAGE_FORMAT_JPEG = ImageFormat.JPEG
IMAGE_FORMAT_PNG = ImageFormat.PNG
IMAGE_FORMAT_QOI = ImageFormat.QOI
IMAGE_FORMAT_BMP = ImageFormat.BMP
# Export enum for decode errors
DecodeError = runtime_image_ns.enum("DecodeError")
@@ -117,38 +114,16 @@ class PNGFormat(Format):
cg.add_library("pngle", "1.1.0")
class QOIFormat(Format):
"""QOI format decoder configuration."""
def __init__(self):
super().__init__("QOI", QoiDecoder)
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_QOI")
# Decodable formats only; platforms that support runtime detection accept
# "AUTO" in their own schema and get_format() resolves it
_JPEG_FORMAT = JPEGFormat()
# Registry of available formats
IMAGE_FORMATS = {
"BMP": BMPFormat(),
"JPEG": _JPEG_FORMAT,
"JPG": _JPEG_FORMAT, # Alias for JPEG
"PNG": PNGFormat(),
"QOI": QOIFormat(),
}
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"bmp_decoder.cpp": "USE_RUNTIME_IMAGE_BMP",
"jpeg_decoder.cpp": "USE_RUNTIME_IMAGE_JPEG",
"png_decoder.cpp": "USE_RUNTIME_IMAGE_PNG",
"qoi_decoder.cpp": "USE_RUNTIME_IMAGE_QOI",
}
)
AUTO_FORMAT = AUTOFormat()
@@ -80,10 +80,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
this->width_ = encode_uint32(buffer[21], buffer[20], buffer[19], buffer[18]);
this->height_ = encode_uint32(buffer[25], buffer[24], buffer[23], buffer[22]);
if (this->width_ <= 0 || this->height_ <= 0) {
ESP_LOGE(TAG, "Invalid image dimensions: (%zdx%zd)", this->width_, this->height_);
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
this->bits_per_pixel_ = encode_uint16(buffer[29], buffer[28]);
this->compression_method_ = encode_uint32(buffer[33], buffer[32], buffer[31], buffer[30]);
this->image_data_size_ = encode_uint32(buffer[37], buffer[36], buffer[35], buffer[34]);
@@ -21,9 +21,6 @@ static constexpr MimeLookup MIME_LOOKUP_TABLE[] = {
#ifdef USE_RUNTIME_IMAGE_PNG
{"image/png", ImageFormat::PNG}, {"image/x-png", ImageFormat::PNG},
#endif
#ifdef USE_RUNTIME_IMAGE_QOI
{"image/qoi", ImageFormat::QOI}, {"image/x-qoi", ImageFormat::QOI},
#endif
};
const char *get_mime_type_for_format(ImageFormat format) {

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