mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a36a832e3 | ||
|
|
098891bef6 |
No files matched your search
@@ -40,14 +40,6 @@ Not shared code: the component's own platform files (`esphome/components/<c>/sen
|
||||
`text_sensor/__init__.py`, ...), its new `USE_*` lines in `esphome/core/defines.h`,
|
||||
and its `CODEOWNERS` entry.
|
||||
|
||||
## YAML language freeze
|
||||
|
||||
Until configurations move to yaml.rocks (esphome/backlog#201), flag any PR that adds to the YAML language: a new tag
|
||||
in `esphome/yaml_util.py`, new behaviour in the `packages` or `substitutions` components (a new entry in the Jinja
|
||||
`SAFE_GLOBALS` in `substitutions/jinja.py`, or a custom Jinja filter, counts), or new merge, override or remove
|
||||
semantics (`esphome/config_helpers.py`, `!extend`, `!remove`). Bug fixes to the existing features are fine. This
|
||||
covers how YAML files are read, included and combined, not a component's own configuration options.
|
||||
|
||||
## Memory and embedded constraints (highest value)
|
||||
|
||||
Heap allocation after `setup()` is treated as a reliability bug, not a
|
||||
@@ -112,8 +104,6 @@ helper; `AGENTS.md` section 4 and the `component-codegen` skill have the detail.
|
||||
| --- | --- |
|
||||
| `if (this->x_sensor_ != nullptr) this->x_sensor_->publish_state(v);` | `PUBLISH_STATE(this->x_sensor_, v);` (`esphome/core/entity_base.h`) |
|
||||
| set warning / `return` / clear warning around a bus result | `if (!this->status_comm_result(ok)) return;` |
|
||||
| set warning / `return` around an earlier step of a multi-step poll | `if (!this->status_comm_check(ok)) return;` |
|
||||
| warning set, but no `status_comm_result` or `status_clear_warning` on the success path | the last step uses `status_comm_result(ok)`, or the warning never clears |
|
||||
| hand-written `set_x_sensor()` plus `x_sensor_` member | `SUB_SENSOR(x)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`, `SUB_SWITCH`, ... |
|
||||
| `if CONF_X in config: s = await sensor.new_sensor(config[CONF_X])` plus setter | `sensors = sensor.sub_sensors(config)` then `await sensors(CONF_X, hub.set_x_sensor)` |
|
||||
| hand-written `{"a": MyEnum.MY_ENUM_A, ...}` dict | `MyEnum.options("a", "b", prefix="MY_ENUM_")` |
|
||||
|
||||
@@ -236,8 +236,6 @@ Never a template per bit or a virtual listener base (a second vptr per entity).
|
||||
instead, so pass `buf` once and mark the component failed in `setup()` if it is nullptr. `align=4` emits
|
||||
`alignas(4)`, so a word-aligned byte buffer stays `uint8`. `size` may be an expression such as `MyClass.static_("buffer_size")(n)`, which renders
|
||||
`ns::MyClass::buffer_size(n)`.
|
||||
- `cg.static_storage(config[CONF_ID], T, count)` is raw aligned storage for `count` objects as a `std::span<T>`,
|
||||
for `StaticFixedVector::init()` when codegen knows the count.
|
||||
- `display.add_writer(var, config)` sets the lambda.
|
||||
- `MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict.
|
||||
- `button.restart_button_schema(cls)` (and `factory_reset_`, `query_`) are button presets.
|
||||
|
||||
@@ -88,9 +88,6 @@ Components that define buses directly are flagged as "NEEDS MIGRATION" and canno
|
||||
CI build time. `!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a
|
||||
standalone entity with its own id instead.
|
||||
|
||||
In a grouped test, set `i2c_id: i2c_bus`, `one_wire_id` and `motion_id` even with one candidate: a merged
|
||||
batch can add a second one ("Too many candidates"). `script/ci-custom.py` checks this.
|
||||
|
||||
A test that needs a shared component in a particular state (`esp32_ble` security settings, an option that
|
||||
PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
|
||||
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
|
||||
|
||||
@@ -107,8 +107,6 @@ APIs.
|
||||
This eliminates `_M_realloc_insert` and `_M_default_append` template instantiations (200-500 bytes per
|
||||
instance), needs no upper bound, and is compatible with protobuf code generation when using
|
||||
`[(fixed_vector) = true]`.
|
||||
When codegen knows the count, use `StaticFixedVector<T>` and pass `cg.static_storage(config[CONF_ID], T, count)`
|
||||
to its `init()`, so the elements live in BSS instead of the heap.
|
||||
|
||||
4. **Small datasets (1-16 elements):** use `std::vector` or `std::array` with simple structs instead of
|
||||
`std::map`/`std::set`/`std::unordered_map`.
|
||||
|
||||
@@ -56,9 +56,6 @@ driven, what the logs show, and the same flow on a `dev` build when a quirk coul
|
||||
them apart with a 10 s capture using `serial_capture.py --reset` (ESP8266 prints its ROM banner at
|
||||
74880 baud, LibreTiny boards print `[LT]` and ESPHome lines, BK7238 prints its bootloader version).
|
||||
`ports.py --wait 2e8a` blocks until a Pico re-enumerates.
|
||||
A change in validation or platform independent code needs no board: a `host:` build serves the API on
|
||||
port 6053 on every interface (reach it at 127.0.0.1, and add `api: encryption:` on a shared network); see
|
||||
recipes/homeassistant.md for one against a throwaway Home Assistant.
|
||||
2. **Make a scratch directory** outside the checkout, with `secrets.yaml` and the templates you need.
|
||||
One yaml per board per variant, file name equal to `name:`.
|
||||
3. **Compile** in the right tree:
|
||||
@@ -128,13 +125,12 @@ ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA
|
||||
(exit 2: a precondition was missed, the stimulus was insufficient, or a capture did not last the run)
|
||||
or exits 1 when a build, flash or port step failed.
|
||||
|
||||
Per-topic scripts, each described in the matching `reference/recipes/<component>.md`: `heap_alloc/` (operator new caller
|
||||
recorder, dump saver and decoder, cover cycle driver), `online_image/`
|
||||
Per-topic scripts, each described in the matching `reference/recipes/<component>.md`: `online_image/`
|
||||
(server with prefixed misbehaviour cases, PNG, JPEG, BMP and QOI generators, API drivers, first pass timer), `zwave_proxy/` (checksum verifier, device info), `usb_cdc_acm/`
|
||||
(host driver, Zephyr driver probe), `rtsp_server/` (protocol client, raw rate reader, measurement set,
|
||||
API image grab), `cse7766/` (API observer with ping), `zephyr_coredump/` (reader flow, decode with the
|
||||
Zephyr tools), `libretiny_prefs/` (rtc preference harness header), `libretiny_crash/` (BK72xx crash probe header,
|
||||
capture decoder, RTL8720C ROM dump versus report matcher), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer, frame stream harness with presence hold and link loss drivers, BT MAC driver), `api/` (raw plaintext native API client for user defined actions, store_yaml recovery client over Noise or plaintext), `http_request/` (update manifest server, update state driver, request test server, action case driver, concurrency, reboot, update check and OTA drivers, `ota_auth/` OTA request cases with basic auth), `modem/` (button and sensor driver over the plaintext API, also used for the BLE MIDI boards, AT commands for the UART passthrough), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (`ha_discovery/`, `disable/`, `retained_burst/` and `reconnect/` runners; broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board, MAC suffix runner with per-variant configs for two boards, `shutdown/` shutdown message runner and matcher), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer), `uart_sim/` (API driver and flash, capture and drive runner for the simulated UART sensors), `remote_base/` (loopback runner and API log decoder per sent frame), `xiaomi_ble/` (frame and header generator with the expected counts, injecting, advertising and receiving configs, counter and runner), `ble_terminate/` (proxy and peer configs, connect and disconnect driver, peer disconnect matcher, runner), `i2c_slave/` (D1 mini slave and BK7238 master configs), `wpa3_ap/` (PMF AP with join and leave logging, BK7238 and CB3S DUTs, runner and summary), `wisp_ap/` (WISP upstream with AP buttons, WISP and ALWAYS_ON DUTs, client, runner and per phase summary), `wifi_restart/` (WPA3 mixed WISP upstream, static IP DUTs with syslog, runner and summary), `ota_timing/` (OTA upload timer with deflate, raw and TCP modes, API build check, A/B runner and median summary, `inflate_bench/` on-device inflate benchmark with stream generator, configs and syslog runner), `gpio_hold/` (ISR pin hold self test with ESP32 and S3 configs), `ble_event_inject/` (GAP and GATTC event injector header and proxy package), `light_sleep_wake/` (LEDC wave and edge timer for light sleep wakeup on a C3, external wave generator and per-call edge detail probe for peripheral power-down on the P4), `manual_ip/` (station and AP manual IP configs, DNS probe and runners), `web_handlers/` (late handler, web_server, captive_portal and prometheus variants, web OTA runner), `bk72xx_flash/` (flash readback with retries, per partition diff of two dumps), `ble_server_security/` (secure GATT server and pairing client configs, driver, summary and runner), `prometheus/` (datetime metrics configs and driver), `zephyr_ble_server/` (nRF52 server, looping ESP32 client and UF2 flash and count runner), `ble_adv_triggers/` (advertiser with six raw frames, ESP32 and BK72xx receivers with eleven triggers, runner and checker), `sun/` (longitude sweep package, runner and checker), `sensor_filters/` (every filter shape with a fixed input sequence, per-board packages and runner), `led_driver_sniff/` (SPI slave capture header with selftest, config generator, window comparer and runner), `hub_input_sim/` (keypad, wiegand and rdm6300 simulation on a D1 mini, API log runner), `value_range/` (on_value_range sequence with a restart, runner), `ezo_fake_bus/` (fake EZO I2C bus and step config, runner), `twai_self/` (TWAI self-reception header, on_frame config and counter runner), `ota_automations/` (OTA automation package for both OTA platforms, four case runner, repeated web upload runner and per case summary), `output_button/` (pad edge timer header, button package and runner), `homeassistant_button/` (raw plaintext API client for action calls and wizard inputs, package and runner), `uart_button/` (UART0 loopback package, API driver with TX and RX counts, runner), `interval/` (plain and delayed interval package, timing summary and runner), `smp_udp_ota/` (SMP reset fault injector, OTA runner, per mode result check, MCUboot swap limit, running image check and slot blanking UF2, Thread device config), `ld2420_sim/` (fake LD2420 answering the hub's commands on a UART0 loopback, API state recorder, two-build comparer and runner), `nrf52_tcp_probe/` (TCP and UDP socket probe config, build and log check, API check over Thread), `rs485_frame/` (simulated DLE/STX/ETX bus device, frame and Line truncated
|
||||
capture decoder, RTL8720C ROM dump versus report matcher), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer, frame stream harness with presence hold and link loss drivers, BT MAC driver), `api/` (raw plaintext native API client for user defined actions, store_yaml recovery client over Noise or plaintext), `http_request/` (update manifest server, update state driver, request test server, action case driver, concurrency, reboot, update check and OTA drivers, `ota_auth/` OTA request cases with basic auth), `modem/` (button and sensor driver over the plaintext API, also used for the BLE MIDI boards), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (`ha_discovery/`, `disable/`, `retained_burst/` and `reconnect/` runners; broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board, MAC suffix runner with per-variant configs for two boards, `shutdown/` shutdown message runner and matcher), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer), `uart_sim/` (API driver and flash, capture and drive runner for the simulated UART sensors), `remote_base/` (loopback runner and API log decoder per sent frame), `xiaomi_ble/` (frame and header generator with the expected counts, injecting, advertising and receiving configs, counter and runner), `ble_terminate/` (proxy and peer configs, connect and disconnect driver, peer disconnect matcher, runner), `i2c_slave/` (D1 mini slave and BK7238 master configs), `wpa3_ap/` (PMF AP with join and leave logging, BK7238 and CB3S DUTs, runner and summary), `wisp_ap/` (WISP upstream with AP buttons, WISP and ALWAYS_ON DUTs, client, runner and per phase summary), `wifi_restart/` (WPA3 mixed WISP upstream, static IP DUTs with syslog, runner and summary), `ota_timing/` (OTA upload timer with deflate, raw and TCP modes, API build check, A/B runner and median summary, `inflate_bench/` on-device inflate benchmark with stream generator, configs and syslog runner), `gpio_hold/` (ISR pin hold self test with ESP32 and S3 configs), `ble_event_inject/` (GAP and GATTC event injector header and proxy package), `light_sleep_wake/` (LEDC wave and edge timer for light sleep wakeup on a C3, external wave generator and per-call edge detail probe for peripheral power-down on the P4), `manual_ip/` (station and AP manual IP configs, DNS probe and runners), `web_handlers/` (late handler, web_server, captive_portal and prometheus variants, web OTA runner), `bk72xx_flash/` (flash readback with retries, per partition diff of two dumps), `ble_server_security/` (secure GATT server and pairing client configs, driver, summary and runner), `prometheus/` (datetime metrics configs and driver), `zephyr_ble_server/` (nRF52 server, looping ESP32 client and UF2 flash and count runner), `ble_adv_triggers/` (advertiser with six raw frames, ESP32 and BK72xx receivers with eleven triggers, runner and checker), `sun/` (longitude sweep package, runner and checker), `sensor_filters/` (every filter shape with a fixed input sequence, per-board packages and runner), `led_driver_sniff/` (SPI slave capture header with selftest, config generator, window comparer and runner), `hub_input_sim/` (keypad, wiegand and rdm6300 simulation on a D1 mini, API log runner), `value_range/` (on_value_range sequence with a restart, runner), `ezo_fake_bus/` (fake EZO I2C bus and step config, runner), `twai_self/` (TWAI self-reception header, on_frame config and counter runner), `ota_automations/` (OTA automation package for both OTA platforms, four case runner, repeated web upload runner and per case summary), `output_button/` (pad edge timer header, button package and runner), `homeassistant_button/` (raw plaintext API client for action calls and wizard inputs, package and runner), `uart_button/` (UART0 loopback package, API driver with TX and RX counts, runner), `interval/` (plain and delayed interval package, timing summary and runner), `smp_udp_ota/` (SMP reset fault injector, OTA runner, per mode result check, MCUboot swap limit, running image check and slot blanking UF2, Thread device config), `ld2420_sim/` (fake LD2420 answering the hub's commands on a UART0 loopback, API state recorder, two-build comparer and runner), `nrf52_tcp_probe/` (TCP and UDP socket probe config, build and log check, API check over Thread), `rs485_frame/` (simulated DLE/STX/ETX bus device, frame and gate cases, runner), `hx711/` (fake HXLine truncated
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
@@ -173,9 +169,6 @@ comment quotes the exact commits. Fetch `origin dev` before computing a merge ba
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
- A harness header fails to compile on Arduino or LibreTiny: those frameworks define `millis()` and `micros()` as
|
||||
macros, so call `(esphome::millis)()`, which builds everywhere. Realtek SDK headers also define `OFF`, `ON`
|
||||
and `SUCCESS`.
|
||||
- A capture stopped early or a line is missing: check the timestamps before trusting it.
|
||||
- A script that pipes into `tee` never ends although its work is done: a background command written as
|
||||
`(cd dir && server & echo $!)` backgrounds the whole `cd && server` list, so a shell stays behind holding the
|
||||
|
||||
@@ -24,11 +24,6 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
row in the PR.
|
||||
- AsyncMqttClient drops inbound topics over 128 characters; topics of 255 or more wrap a counter and
|
||||
stall the link until the broker drops TCP.
|
||||
- `Arduino.h` already declares `_heap_start`; a lambda can use `&_heap_start` directly, and a second `extern` fails.
|
||||
- `Hardware Watchdog` with `wDev_ProcessFiq` in the stack means interrupts were held off too long for the Wi-Fi
|
||||
interrupt handler; `Software Watchdog` with `pm_send_nullfunc` means the loop was busy too long. Code that takes
|
||||
interrupt locks needs a load run to show either (`scripts/esp8266_lock_load/`, see recipes/one_wire.md).
|
||||
- `GPI` bits 16 to 31 are strapping bits (mask with 0xFFFF). GPIO16 has no interrupt; read `GP16O` for an output.
|
||||
|
||||
## ESP32 classic devkit
|
||||
|
||||
@@ -45,8 +40,7 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
- Signed OTA testing: build with `esp32.framework.advanced.signed_ota_verification.verification_keys`
|
||||
under a different name, sign with `python -m espsecure sign_data --version 2 --keyfile
|
||||
tests/components/esp32/dummy_signing_key.pem`, upload with `esphome upload --file`. The verifier
|
||||
refuses unsigned images afterwards, so restore with a signed plain build. The full accept and reject case set
|
||||
(several keys, CRC-only edits) is in `recipes/ota.md`.
|
||||
refuses unsigned images afterwards, so restore with a signed plain build.
|
||||
- An ESP32 whose bootloader has no app rollback is protected only by `safe_mode` after a bad OTA. Before sending a
|
||||
change to such a device that nobody can reach by hand, boot the same code on a devkit first and keep a fallback
|
||||
image of the merge base built.
|
||||
@@ -58,7 +52,6 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
`Disabling RNG early entropy source`, then 0 bytes, esptool reports `No serial data received`). Only a
|
||||
USB replug fixes it. Fallback: `esphome logs --device <name>.local` (API log stream, which drops lines
|
||||
while Wi-Fi is off).
|
||||
- The USB console drops a line now and then even when it stays up; do not count events by log lines alone.
|
||||
- The ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry). The
|
||||
Waveshare ESP32-P4-WIFI6 rev1.3 board is such pre-rev3 silicon.
|
||||
- S3, C3 and C6 all enumerate as `303a:1001`; confirm the chip with `esptool --port <port> chip_id` before flashing.
|
||||
@@ -154,8 +147,6 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
assuming either.
|
||||
- `flash_bk72xx.py` misses the link now and then (`Timed out attempting to link with chip`); retry up to 3
|
||||
times before suspecting the board.
|
||||
- Both flash wrappers pass ltchiptool `-L` with the newest installed LibreTiny platform (`scripts/lt_platform.py`):
|
||||
its default `~/.platformio/platforms/libretiny` can be an older copy that rejects BK7238 UF2s as unsupported.
|
||||
- Crashes: `__builtin_trap()` and a call into unmapped memory both end as an undefined-instruction trap
|
||||
(`bk_misc_get_start_type()` 6, `lt_get_reboot_reason()` Crash); reads, unaligned loads and stores to
|
||||
unmapped addresses never fault on the ARM968. Without Wi-Fi the SDK trap handler spins with interrupts off
|
||||
@@ -235,9 +226,6 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
- A 4-way handshake that never finishes after association is reported as `RTW_WRONG_PASSWORD` on AmebaZ
|
||||
even with the right password; AmebaZ2 reports a real wrong password as a 4-way handshake timeout.
|
||||
- The driver also posts a SCAN_DONE (number 0) inside every join, on AmebaZ2 too.
|
||||
- RTL8710B GPIO interrupts fire on one edge only, and LibreTiny maps CHANGE to rising. The Arduino `digitalRead` called
|
||||
from an ISR switches the pin back to GPIO and frees its interrupt (esphome#22400, #22403).
|
||||
- The FreeRTOS tick is 1/1024 s, so `millis()` on the tick ran 2.4 % fast (esphome#22404).
|
||||
|
||||
## BW16 kit (RTL8720DN, AmebaD)
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ One file per component, named after it:
|
||||
- [`ld2420.md`](ld2420.md): hub setup and sensor states with a simulated module on a UART0 loopback (energy and simple mode)
|
||||
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar, presence hold and link loss on a frame stream, BT MAC readback
|
||||
- [`ld2412.md`](ld2412.md): output pin level through the basic configuration command, held back writes
|
||||
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx and RTL8720C crash report on the next boot with a ROM dump matcher, BK7238 BLE bond store safety with a full flash backup and per partition diff, BK7238 OTA versus the board layout and bootloader, pulse counting, RTL8710B edge interrupts and millis timing
|
||||
- [`modem.md`](modem.md): PPPoS modem component boot check with no modem attached, loop blocking and disable timing, SIM7080G behaviour with no SIM and with no data plan through a raw UART passthrough
|
||||
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx and RTL8720C crash report on the next boot with a ROM dump matcher, BK7238 BLE bond store safety with a full flash backup and per partition diff, BK7238 OTA versus the board layout and bootloader
|
||||
- [`modem.md`](modem.md): PPPoS modem component boot check with no modem attached, loop blocking and disable timing
|
||||
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT, WebSocket transports with a local and a public broker, TLS trust sources (own CA, client certificate, certificate bundle, no verification) with the CLI check, MAC suffix topics and will messages on two boards, shutdown message delivery on restart, disable and deep sleep
|
||||
- [`nrf52.md`](nrf52.md): dongle board support, zephyr_ble_server connection slot under reordered callbacks, SMP over UDP OTA over Thread with reset faults, MCUboot swap limit and stuck slot recovery, TCP stack probe, sdk-nrf 3.4.1 build and USB notes
|
||||
- [`nrf52.md`](nrf52.md): dongle board support, zephyr_ble_server connection slot under reordered callbacks, SMP over UDP OTA over Thread with reset faults, MCUboot swap limit and stuck slot recovery, TCP stack probe
|
||||
- [`online_image.md`](online_image.md): stalled and malformed downloads, PNG, JPEG, BMP and QOI decode time per loop pass with large images and small buffers
|
||||
- [`openthread.md`](openthread.md): border router on the S3 host plus H2 RCP
|
||||
- [`ota.md`](ota.md): upload without a configuration and the refusal checks, overlap and lock, timeouts, deflate, upload timing A/B per build and board with deflate and raw, on-device inflate benchmark, OTA automations on two platforms
|
||||
@@ -25,14 +25,12 @@ One file per component, named after it:
|
||||
- [`scheduler.md`](scheduler.md): loop wake A/B, interval startup delay and control from lambdas
|
||||
- [`udp.md`](udp.md): datagrams, multicast, sockets, `udp.write` payload shapes
|
||||
- [`usb_cdc_acm.md`](usb_cdc_acm.md): nRF52840 dongle host driver
|
||||
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream, reboots against a WPA2/WPA3 PMF access point, WISP and ALWAYS_ON AP switching, AP restarts with WPA3 mixed and LIGHT power save, soft AP clients with the BLE tracker scanning, ESP8266 power save after captive portal provisioning
|
||||
- [`wifi.md`](wifi.md): reboot_timeout with a fallback AP, DHCP lease cache, Easy Connect (DPP), WISP bridge with an ethernet upstream, reboots against a WPA2/WPA3 PMF access point, WISP and ALWAYS_ON AP switching, AP restarts with WPA3 mixed and LIGHT power save
|
||||
- [`zephyr.md`](zephyr.md): preferences on the nRF52840 dongle
|
||||
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
|
||||
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
|
||||
- [`one_wire.md`](one_wire.md): ESP8266 interrupt locks under Wi-Fi load with a DS18B20 search and a busy-wait control
|
||||
- [`cc1101.md`](cc1101.md): sleep and wake checks read directly from the chip (CHIP_RDYn, PATABLE loss, registers per setter)
|
||||
- [`globals.md`](globals.md): restoring globals with storage rtc and flash across every reset kind
|
||||
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls, wizard-linked entities on every platform, homeassistant switch on a script with a host build
|
||||
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls
|
||||
- [`usb_audio.md`](usb_audio.md): USB audio speaker and microphone, the usb_host FIFO split, and Music Assistant playback through sendspin
|
||||
- [`dfrobot_sen0395.md`](dfrobot_sen0395.md): SEN0395 mmWave command flow, module quirks and overlong lines through its error reply
|
||||
- [`speaker.md`](speaker.md): media player pipeline tasks on the Connect AUX-2 with the `speaker` platform player
|
||||
@@ -63,7 +61,7 @@ One file per component, named after it:
|
||||
- [`rs485_frame.md`](rs485_frame.md): DLE/STX/ETX bus on an ESP32-C3 with a cross-wired simulated bus device (frame cases, gate timing)
|
||||
- [`hx711.md`](hx711.md): fake HX711 driven from the other core, every published value matched to the served conversion
|
||||
- [`automation_trace.md`](automation_trace.md): automation codegen A/B, a marker trace of chains, conditions, loops and script modes over the API
|
||||
- [`bluetooth_proxy.md`](bluetooth_proxy.md): held GATT link after a half-open API session, frozen Home Assistant stand-in, out-of-order GATT discovery responses injected on the proxy
|
||||
- [`bluetooth_proxy.md`](bluetooth_proxy.md): held GATT link after a half-open API session, frozen Home Assistant stand-in
|
||||
- [`crash_report.md`](crash_report.md): crash buttons, fetching the previous boot's crash report over the API, per-chip abort facts
|
||||
- [`https_idf.md`](https_idf.md): web_server_idf HTTPS test component, listener switch, stalled TLS clients, bad key
|
||||
- [`mdns.md`](mdns.md): legacy unicast mDNS dump, hammer and latency straight to the device
|
||||
@@ -72,14 +70,3 @@ One file per component, named after it:
|
||||
- [`microphone_http_stream.md`](microphone_http_stream.md): raw PCM over HTTP with a counter fake mic or a USB mic, multi-reader client
|
||||
- [`i2s_audio.md`](i2s_audio.md): I2S microphone fed by an I2S TX channel on the same S3 through three jumpers
|
||||
- [`voice_assistant.md`](voice_assistant.md): late TTS start on the Voice PE with a slow announcement URL
|
||||
- [`mcp251xfd.md`](mcp251xfd.md): MCP2518FD CAN FD over SPI in loopback, forced modes, chip reset and bus-off recovery, TX objects read back from chip RAM, idle SPI cost from a PCNT on CS
|
||||
- [`crc32.md`](crc32.md): crc32 vectors on every platform path and the signed OTA signature block CRC on an ESP32-C3
|
||||
- [`i2c_isr_fault.md`](i2c_isr_fault.md): software INA219 on the master's own pads with seeded bus faults, interrupt storm and stuck bus
|
||||
- [`bmp390.md`](bmp390.md): BMP390/BMP388 register readback, INT data ready rate, start write faults by software NACK or held SDA, and the IDF I2C hang after a held SDA
|
||||
- [`ipv6.md`](ipv6.md): IPv6 link-local and SLAAC boot and reconnect timing on ESP32 Wi-Fi, with an injected esp-netif delay that forces the race
|
||||
- [`mcp23017.md`](mcp23017.md): MCP23017 IOCON aliasing and INT line behavior (open drain, push-pull, polling) on an ESP8266 with PA to PB jumpers
|
||||
- [`pwm_capture.md`](pwm_capture.md): every LEDC PWM period captured on the same ESP32-C6 through RMT receive and PCNT, with and without power save
|
||||
- [`spi.md`](spi.md): SPI bus capture on the same board with no chips (ESP32 SPI3 slave, edge interrupt snapshots on C3, ESP8266 and Pico 2 W), checked and diffed between builds
|
||||
- [`heap_alloc.md`](heap_alloc.md): operator new counting, and a ring of allocation callers decoded with addr2line to tell the first run after boot from repeats
|
||||
- [`wled.md`](wled.md): WLED realtime sender on a board's own access point
|
||||
- [`combination.md`](combination.md): combination sensor NaN behaviour from the last source
|
||||
@@ -56,9 +56,3 @@ include of random base64 lines with `include_secrets: true` gives four chunks (1
|
||||
0bd3738430: `include_secrets: true` fails the build when a package in another directory uses `!secret` and has no
|
||||
`secrets.yaml` beside it; a copy next to the package works around it.
|
||||
Results: https://github.com/esphome/esphome/pull/16445#issuecomment-6335435397
|
||||
|
||||
## Allocations of a connecting client (ratgdo/esphome-ratgdo#705)
|
||||
|
||||
An API client that connects allocates about 15 times on ESP32: the connection object, then the list entities and
|
||||
initial state batches growing the `DeferredBatch` vector. A log subscription adds more per line. Connect before a
|
||||
heap measurement and subscribe to logs only afterwards (`heap_alloc.md`).
|
||||
@@ -42,7 +42,6 @@ comparison accepted automatically) and a classic ESP32 devkit runs `esp32central
|
||||
phase B reads the MITM characteristic with no numeric comparison and no `Deleting unpaired device` line.
|
||||
- On ESP-IDF 5.5.6 every other reconnect pairs again (`Deleting unpaired device` on the disconnect after a bonded
|
||||
reconnect), the regression #22081 describes.
|
||||
#22559 fixes it: the bond survived 15 reconnects and a reset of both boards with no new pairing.
|
||||
|
||||
## Bluedroid hex parsing bench (#22081)
|
||||
|
||||
|
||||
@@ -26,32 +26,3 @@ swap fails when a round never connects. An owner that never got the link also fa
|
||||
- Before #22077, case H holds the link for good: 0 adverts, and the address stays in the slot report. With #22077 the
|
||||
link is released in the same millisecond as the `replaces` warning. In case 4 the first request logs `Connection
|
||||
request ignored, state: DISCONNECTING` (connected=false at 0.2 s) and the retry connects at 0.5 to 0.7 s.
|
||||
|
||||
## Out-of-order GATT discovery responses (#22518)
|
||||
|
||||
Some peripherals answer a characteristic discovery (Read By Type) with records out of handle order. ESP-IDF 5.5.6
|
||||
rejects that (`gatt_process_read_by_type_rsp invalid handle ...`) and ends the whole discovery; 5.5.5, Android and
|
||||
BlueZ accept it. `scripts/att_reorder/` reproduces it with no such device:
|
||||
|
||||
- `server.yaml` (ESP32-S3) serves `FFF0` with six characteristics, then `1826`.
|
||||
- `proxy.yaml` (classic ESP32, `idf` substitution; keep per-build files such as `example-proxy.yaml` in the same
|
||||
directory) links `components/attswap`, which wraps Bluedroid's
|
||||
`l2c_rcv_acl_data` (`-Wl,--wrap`); `attswap.hpp` swaps the first two records of the response whose first record
|
||||
declares `FFF1`, and logs a swap count.
|
||||
- `gattget.py PROXY PEER_MAC ROUNDS` reads the services through the proxy like Home Assistant (no cache);
|
||||
`run.sh PROXY_YAML [ROUNDS]` flashes both boards and prints the services per round and the discovery errors.
|
||||
|
||||
On 5.5.6 Home Assistant gets `fff0` with no characteristics and no `1826`, while the proxy still logs
|
||||
`Service discovery complete` (the proxy ignores the error status); on 5.5.5 every service arrives. To find the order a
|
||||
real device uses, decode an Android HCI snoop: a Read By Type response lists its records in the order sent.
|
||||
|
||||
- Handles, reads and notifies after a reordered response: flash `server-notify.yaml` (`SERVER_YAML` on `run.sh`) and run
|
||||
`gattcheck.py PROXY PEER_MAC`. It prints every service, characteristic and descriptor handle, reads FFF1, FFF2 and
|
||||
FFF3 by the received handles (each holds its own value), and enables notify on FFF1, FFF2 and 2ACD. Compare the table
|
||||
with a run of a proxy built without `attswap`. On ESP-IDF 5.5.5, which accepts the unsorted response, FFF1's
|
||||
descriptor range runs over FFF2's declaration, value and CCCD, so FFF2 has no CCCD (#22576).
|
||||
- `bluetooth_gatt_start_notify` only registers the notify on the proxy; the client writes the CCCD itself, as Home
|
||||
Assistant does. Without that write no notification arrives.
|
||||
- Give the proxy an `api` encryption key (`API_KEY` for the scripts). A Home Assistant on the same network connected to
|
||||
a plaintext test proxy and took over its Bluetooth subscription (`Subscription from Home Assistant ... replaces
|
||||
aioesphomeapi`), dropping the test client's connection mid-run.
|
||||
@@ -1,35 +0,0 @@
|
||||
# BMP390 / BMP388 (bmp3xx_i2c): register readback, data ready rate, start write faults (#21632, #21633)
|
||||
|
||||
Bench: ESP32-S3 devkit (native USB console, ESP-IDF), BMP390 breakout on 3V3, SDA GPIO1, SCL GPIO2, CS and SDO open
|
||||
(address 0x77), INT to GPIO42. Some breakouts answer chip ID 0x50 (the BMP388 value); the register map is the same.
|
||||
|
||||
`scripts/bmp390/`:
|
||||
- `gen.py NAME [--rate 50Hz] [--iir 128x] [--interval 1s] [--fault --kinds ... --probe-delay MS] [--reset-at 40,80]
|
||||
[--open-line] [--bus-reset]` writes `NAME.yaml` (no Wi-Fi).
|
||||
- `bmp390probe.hpp` wraps the sensor's I2C bus (`set_i2c_bus()` from an `on_boot` priority 800 lambda), dumps
|
||||
CHIP_ID, ERR, INT_CTRL, PWR_CTRL, OSR, ODR and CONFIG before and after setup and every 10 s (`REGS` lines), turns on
|
||||
data ready on INT after setup (`INT_CTRL = 0x42`, push-pull, active high: the driver never writes INT_CTRL and its
|
||||
soft reset in setup clears it), counts rising edges (`INT edges=N in M ms`) and logs every pressure value with four
|
||||
decimals (`READ <ms> <hPa>`).
|
||||
- `run.sh NAME SECONDS [TAG]` (env `TREE`, `PORT`, `SC`, `PY`) builds, flashes, captures from a fresh boot and runs
|
||||
`stats.py CAPTURE [SKIP_S]`: read count, cadence, INT rate, detrended noise (21 sample window), repeated values.
|
||||
|
||||
Flows:
|
||||
- Register A/B: the same config on both builds; compare `REGS` after setup and between updates. Forced mode leaves
|
||||
PWR_CTRL at 0x03 (sleep, both sensors enabled), normal mode reads 0x33. ODR codes: 0x00 200 Hz, 0x02 50 Hz, 0x07
|
||||
1.5 Hz, 0x0B 0.1 Hz (period 5 ms << code).
|
||||
- Real output rate: INT edges per 10 s window. Forced mode gives one edge per update.
|
||||
- Settling: the bench cannot make a pressure step, but the soft reset in setup restarts the IIR filter with an
|
||||
offset, so the first readings after boot show its step response (50 Hz with 128x settled in about 8 s).
|
||||
- Reset behind the driver: `--reset-at` writes CMD 0x7E = 0xB6 at those seconds; a driver that handles it logs that
|
||||
the sensor was reset and restores the registers.
|
||||
- Start write faults (`--fault --kinds`, one letter per event at 30 s and then every 20 s): `N` the next start write
|
||||
NACKs in software, `M` every start write NACKs for 5 s, `T` SDA held low through the GPIO matrix for the next start
|
||||
write, `W` the same for every transfer for 5 s, `L` a continuous 5 s hold, `S` a 300 ms hold, `H` and `D` held
|
||||
transfers of the harness only. `PROBE` lines check the bus `--probe-delay` ms later.
|
||||
|
||||
Pitfall: on the S3 with ESP-IDF 5.5.6, any transfer that fails while SDA is routed to a GPIO held low (or routed away
|
||||
with the line floating high) leaves the `i2c_master` driver hung on every build, with or without the sensor: every
|
||||
later transfer logs `I2C software timeout` while the lines read idle high and the saved pad registers are back, and
|
||||
`i2c_master_bus_reset()` does not help. Use the software NACK kinds (`N`, `M`) to test what a component does after a
|
||||
failure and the recovery; use the wire kinds only to show the first failure.
|
||||
@@ -1,17 +0,0 @@
|
||||
# cc1101
|
||||
|
||||
## Sleep and wake checks read from the chip (#22519)
|
||||
|
||||
`scripts/cc1101_sleep/`: `cc1101-sleep.yaml` runs an ESP32-S3 (ESP-IDF) with an E07-M1101D module on SPI and runs
|
||||
tests T0 to T6 4 s after boot; `cctest.hpp` reads every config register and the PATABLE directly over SPI. The log
|
||||
ends with `DONE fails=N`, and T6 prints whether each setter woke a sleeping chip.
|
||||
|
||||
- Proof that the chip sleeps: with CSn low, SO (MISO) stays high until the crystal runs, so it is high after
|
||||
`power_down()` and low at once when awake. PATABLE entries 1 to 7 are also lost in SLEEP, so write marks to them
|
||||
before `power_down()` and check that they are gone.
|
||||
- Selecting the chip to check it wakes it, so after the check the chip is awake while the component still thinks it
|
||||
sleeps. Call `wake_up()` and `begin_rx()` before the next case.
|
||||
- Leave FSCAL3 to FSCAL1 (0x23 to 0x25), FSTEST (0x29) and AGCTEST (0x2B) out of register comparisons: calibration
|
||||
and the chip itself change them.
|
||||
- Results: on 7e6b90d08b, `set_frequency`, `set_channel` and `set_modulation_type` woke a sleeping chip and left it
|
||||
in RX. 9e65d0c327 fixed that; every other check passed on both, and 200 sleep and wake cycles took about 1 s.
|
||||
@@ -1,7 +0,0 @@
|
||||
# combination
|
||||
|
||||
## NaN from the last source (#22510)
|
||||
|
||||
A combination sensor defers each update and uses the value of the source that published last. A NaN from that
|
||||
source drops the whole update, so when testing NaN handling keep the last-publishing source finite, and compare
|
||||
rows one update late. Heap churn per update: see `heap_alloc.md`.
|
||||
@@ -1,13 +0,0 @@
|
||||
# crc32 helper and the signed OTA block check (esphome#22398, 2026-11-07)
|
||||
|
||||
`scripts/crc32/crctest.hpp` checks `esphome::crc32()` against Python `zlib.crc32` values: `"123456789"` = `0xCBF43926`,
|
||||
empty input returns the seed, chained calls (4 + 5, 300 one-byte calls, 97-byte chunks from an odd offset), bytes
|
||||
0..255, 32 × 0xFF, 4093 bytes from offsets 0 to 3, and 8 KiB timed with `(esphome::micros)()` (best of five).
|
||||
`common-crc.yaml` runs it at boot and every 20 s and also makes an unqualified `crc32(...)` call from a lambda, which
|
||||
catches a clash with anything the generated `main.cpp` pulls in. Paths and timings seen (8 KiB): ESP32 ROM 512 µs,
|
||||
ESP8266 PROGMEM nybble table 3072 µs, RP2350 932 µs, BW15 (FlashDB) 656 µs, BW12 2000 µs, BK7238 1026 µs.
|
||||
|
||||
The signed OTA signature block check (`block_is_valid()`), the PR's only converted caller, ran the accept and reject
|
||||
cases in `ota.md` (signed OTA with several trusted keys).
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/22398#issuecomment-6716330334 and #issuecomment-6716545831.
|
||||
@@ -13,6 +13,8 @@ replies.
|
||||
- `run.sh LABEL` builds, flashes, captures 90 s and writes `out-ezo<LABEL>.events`. Compare dev and the PR: the
|
||||
replies, callback texts, read values and the wait before each read (912 ms for reads and calibration, 304 ms for
|
||||
the rest) must match; differences in queue limits are the PR's intended change.
|
||||
- Call plain `millis()` in a fake: Arduino builds define a `millis()` macro (`esphome::millis()`), so
|
||||
`esphome::millis()` breaks there, and on ESP-IDF the plain name resolves through `using namespace esphome`.
|
||||
- Found with it: dev read past its 32-byte buffer on an unterminated reply (a custom callback got 56 bytes), and
|
||||
two queued address changes both went to the first new address.
|
||||
- Results: https://github.com/esphome/esphome/pull/21840#issuecomment-6493565259
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Heap allocation counts and callers (#22509, #22510, ratgdo/esphome-ratgdo#705)
|
||||
|
||||
## Counting
|
||||
|
||||
Replace the global `operator new` in an `esphome: includes:` header on ESP32 (Arduino or ESP-IDF) and log the
|
||||
delta from an `interval`. Compare a PR build with its base on the same flow; the rate per second shows churn.
|
||||
- The ESP8266 core defines `operator new` non-weak. `-Wl,--wrap=_Znwj` worked in a sensor-only config but
|
||||
boot-looped a neopixelbus config (alignment fault in the wrapper), so report free heap only there.
|
||||
- `vec = {}` keeps a std::vector's capacity (it is an initializer_list assignment); only
|
||||
`std::vector<T>().swap(vec)` or `clear()` plus `shrink_to_fit()` frees it. Seen as heap that did not come back
|
||||
after a WLED effect stopped.
|
||||
|
||||
## Callers
|
||||
|
||||
`scripts/heap_alloc/`:
|
||||
- `allocring.hpp` counts every `operator new` and, once armed, keeps the first 1024 with their size, time and
|
||||
five caller PCs, about 28 KB of RAM. It disarms itself when full. The frame walker
|
||||
(`esp_backtrace_get_next_frame`) exists only on Xtensa (ESP32, S2, S3); on RISC-V chips entries keep size and
|
||||
time without callers. Aligned `operator new` (`std::align_val_t`) is not replaced, so it is not counted;
|
||||
nothrow `new` goes through the replaced one.
|
||||
- `allocring.yaml` (package, substitution `alloc_arm_s`) arms it that many seconds after boot and adds a
|
||||
"Dump allocations" button that logs what was kept. The press stops recording until the next reboot, so
|
||||
reboot (or reflash) before each run; a second press returns the same entries.
|
||||
- `alloc_marker(a, b)` from a lambda writes a marker entry, for example on each cover operation change, so the
|
||||
dump splits into phases.
|
||||
- `dump.py HOST OUT` presses the button and writes the dump to a fresh OUT, pressing again until the header
|
||||
and every entry arrived (a late log subscription or dropped log lines). `decode.py LOG ELF [ADDR2LINE]` prints each entry with its callers as function and file:line, and exits 2
|
||||
when the recorder never armed, an entry was still being written, entries are missing, repeated or out of
|
||||
range, no Xtensa entry has a caller, the ELF is from another build (the buffer address in the header differs), or the buffer
|
||||
filled. A PR build and its base can share that address, so copy each build's `firmware.elf` next to its dump. On RISC-V it prints sizes and times only. It reads only the newest dump in the file. The ESP-IDF ELF is `.esphome/build/<name>/build/firmware.elf`.
|
||||
- `cycles_cover.py HOST N OUT` waits for the "Allocation recorder armed" binary sensor, then moves the first
|
||||
cover to the far end and back N times (`cover_command(position=...)`, waiting for idle each way) with a states-only API client, then saves the dump the same way. It exits 2 when a move
|
||||
never reached its position.
|
||||
|
||||
Findings:
|
||||
- Arm well after boot: the scheduler regrows its storage once after the freelist trim (see `scheduler.md`).
|
||||
- Repeat the action several times in one session and count only what comes back. On a ratgdo cover (an external
|
||||
component, ESP32 ESP-IDF) the first door open after boot allocated 18 times: four 36 B SchedulerItems,
|
||||
`to_add_` and `items_` growth, and the API `DeferredBatch` vector. Cycles 2 and 3 allocated nothing.
|
||||
- Keep the API client out of the count (see `api.md`): connect before the run, subscribe to states only, and
|
||||
subscribe to logs only for the dump.
|
||||
@@ -45,44 +45,3 @@ without the fix logs `Applying the report that arrived during the transition` an
|
||||
ends at 100% on both builds because the last call's target differs from the held report, so it is a regression
|
||||
check only, not a reproduction. Separate button presses are too slow (about 1.5 s each) to land inside the fade.
|
||||
The mirrors are not API entities, so read their state from the "Log state" button's line. Results: https://github.com/esphome/esphome/pull/21144#issuecomment-6278257695
|
||||
|
||||
## Wizard-linked homeassistant entities on every platform (#21908)
|
||||
|
||||
`scripts/homeassistant_wizard/`: `ha-all-common.yaml` adds to the buttons of `ha-button-common.yaml` one entity with a
|
||||
fixed `entity_id` and one linked to an `api: wizard:` input for each of sensor (plus one with `attribute:`),
|
||||
binary_sensor, text_sensor, number, switch, select, text and light (the wizard light is in `ha-wiz-light.yaml`), and
|
||||
logs the ESP8266 heap every 5 s (`ramchk`). `hadrive.py HOST` plays Home Assistant over a plaintext API connection: it
|
||||
records the state subscriptions the device asks for, sends states (including number and text min, max, step and mode,
|
||||
select options and light supported_color_modes), commands every writer, sets each wizard input with
|
||||
`WizardInputSetRequest` (key = FNV-1 hash of the input id), changes it to a second ID in another domain, sends an
|
||||
invalid one and reconnects. It prints SUB, STATE and ACTION lines that diff cleanly between two builds, and PASS or
|
||||
FAIL. `run.sh LABEL` (env `TREE`, `PORT`, `SC`, `PY`; `NO_LIGHT=1` for a build without #21908) flashes a D1 mini, takes
|
||||
three idle boots of heap readings and drives one boot. `lightcheck.py HOST` checks again that the wizard light follows
|
||||
only its current ID.
|
||||
|
||||
- Builds before #21908 cannot compile a wizard-linked homeassistant light (the wizard called `set_entity_id()` on the
|
||||
LightState); `NO_LIGHT=1` leaves `ha-wiz-light.yaml` out.
|
||||
- An unset wizard number or select rejects a command in the core (`> max 0`, `has no options`) before the
|
||||
`No entity ID set yet` check; nothing is sent either way.
|
||||
- Field 3 of LightStateResponse is brightness, not `missing_state`; SwitchStateResponse has no missing flag.
|
||||
- A light publishes only changes, so a report of the state it already has publishes nothing.
|
||||
|
||||
## homeassistant switch on a script, host build against a throwaway Home Assistant (#22548)
|
||||
|
||||
No board is needed when the change is in validation or platform independent code: an ESPHome `host:` build serves the
|
||||
API on 127.0.0.1:6053. `scripts/homeassistant_script_switch/`:
|
||||
|
||||
- `ha-configuration.yaml` is the Home Assistant config (port 8129, a 20 s `long_job` script that sets and clears
|
||||
`input_boolean.script_ran`, a `quick_job` toggle script). Onboard it with `scripts/mqtt/ha_discovery/onboard.py DIR`.
|
||||
- `hsw.yaml` mirrors both scripts and the flag with homeassistant switches, with template buttons that call
|
||||
`switch.turn_on` and `switch.turn_off`.
|
||||
- `haflow.py TOKENFILE` adds the device with the esphome config flow and turns on `allow_service_calls` in the options
|
||||
flow; without it the device's action calls are refused.
|
||||
- `drive.py TOKENFILE NAME` presses the buttons through the REST API and prints the script and flag states after each
|
||||
step.
|
||||
|
||||
Notes:
|
||||
- homeassistant import entities default to `internal: true`, so the mirrors are not Home Assistant entities; read
|
||||
them from the device's `Got state` log lines.
|
||||
- Turning a script off stops it mid run (the flag stays on). A script that finishes at once reports on, then off, in
|
||||
the same second.
|
||||
@@ -1,20 +0,0 @@
|
||||
# I2C master interrupt storm and stuck bus faults (esphome#22270 / #22396, 2026-11-06 to 11-07)
|
||||
|
||||
`scripts/i2c_isr_fault/inasim.hpp` is a software INA219 at 0x40 answered on the ESP32 master's own pads (SDA GPIO32,
|
||||
SCL GPIO33, nothing wired): the slave pulls a line low by switching the pad's output mux from the I2C0 signal to plain
|
||||
GPIO output 0 (a wired AND with the master), from a GPIO ISR on core 0 that spins through one transaction. A core 0
|
||||
task re-asserts long stuck lines every tick. From 20 s after boot it injects seeded faults into `p_fault` per mille of
|
||||
transactions (garbage bits, NACK, clock stretch, SCL/SDA held low up to `hold_max_ms`, glitches, vanish).
|
||||
|
||||
`i2cbase.yaml` (package): `i2c:` at `freq` with `scan: true`, an `ina219` polled every `ina_interval`, a bus voltage
|
||||
register read every `hammer`, a boot self test, and 30 s `MASTER`/`SLAVE` counter lines (ok, timeouts by kind,
|
||||
garbage, longest failed run, loop stalls). `example.yaml` shows the substitutions: framework and version (`fw`,
|
||||
`fwver`), `seed` (seed 1 at 5 % hangs dev with an interrupt watchdog on CPU0 at about 245 s, the #22270 report),
|
||||
`p_fault`, `fault_mask`. `run.sh LABEL [SECONDS]` builds, flashes and captures (640 s default); `summary.sh` prints
|
||||
crashes, watchdogs, timeout kinds and the minimum ok reads per window.
|
||||
|
||||
Findings: ESP-IDF 5.4.2 has a different failure (the driver waits for bus idle with no timeout after a NACK, so the task
|
||||
watchdog reboots in a loop with or without the guard); 5.5.5, 5.5.6 and 6.0.1 hang on the storm without the guard and
|
||||
run clean with it. ESPHome never uses the driver's async mode, so there is no asynchronous path to test.
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/22396#issuecomment-6713396173
|
||||
@@ -1,28 +0,0 @@
|
||||
# IPv6 link-local and SLAAC on ESP32 Wi-Fi: boot timing and race injection (#22364)
|
||||
|
||||
`scripts/ipv6_boot/` (ESP32-S3 and ESP32-C3, native USB console, ESP-IDF). One YAML per build
|
||||
(`example-s3.yaml`, `example-c3.yaml` show the shape) with the substitutions `min_count` (network
|
||||
`min_ipv6_addr_count`) and `cap_ms`.
|
||||
|
||||
- `v6probe.hpp` and `v6common.yaml`: `network: enable_ipv6`, VERBOSE logs, and a 50 ms interval that logs milestones
|
||||
from boot (`MS assoc|ipv4|linklocal|slaac|connected <ms>`; addresses come from `esp_netif_get_all_ip6`, so they
|
||||
count once they are preferred, after duplicate address detection), then `RESULT boot ...` with `heap_used`, then
|
||||
disables Wi-Fi for 3 s and logs `RESULT reconnect ...` the same way, then reboots. A phase ends when SLAAC and the
|
||||
connection are both seen, or after `cap_ms` (`timeout=1`).
|
||||
- `v6hf.yaml` adds a `HighFrequencyLoopRequester` (a busy main loop, as with a display or audio).
|
||||
- `v6inject.yaml` links with `-Wl,--wrap=esp_netif_action_connected` and defines `V6_INJECT`; the wrapper in
|
||||
`v6probe.hpp` sleeps 300 ms in the event task before esp-netif brings the station interface up, after the wifi
|
||||
component already queued STA_CONNECTED. That makes the "interface not up yet" race happen on every connect.
|
||||
- `run.sh NAME SECONDS` (env `TREE`, `PORT`, `SC`, `PY`) builds `NAME.yaml` as device `NAME`, flashes, captures the
|
||||
self-rebooting boots and runs `an.py CAPTURE...`: per phase the `esp_netif_create_ip6_linklocal failed` count,
|
||||
boots without link-local, SLAAC or a connection, connection timeouts, and median and worst times.
|
||||
|
||||
- Check the network first: a computer on the same network shows an `autoconf` IPv6 address when a router advertises
|
||||
a prefix. SLAAC arrived about 2 s after link-local on the test network.
|
||||
- The natural race did not show: no failure in about 160 boots on the S3 (normal and busy loop) and the C3 without
|
||||
the fix. esp_event runs handlers in registration order and the wifi component registers before esp-netif, but the
|
||||
event task reaches esp-netif's handler within microseconds. Only the injected delay reproduced the failure.
|
||||
- An extra event handler registered from `on_boot` cannot inject the race: it runs after esp-netif's handler, when
|
||||
the interface is already up. Wrap the esp-netif call instead.
|
||||
- `wifi.disable` removes the addresses, so the reconnect phase creates link-local again on the same path.
|
||||
- Count failures by the probe's `ll=0` too, not only by the warning: the USB console drops lines.
|
||||
@@ -160,21 +160,3 @@ auth frames.
|
||||
Known LibreTiny behaviour seen here: after a join with a BSSID and channel, LibreTiny's partial-scan reset leaves
|
||||
the next SSID-only join with nothing to scan (`RTW_NONE_NETWORK` in about 18 ms). That is a firmware bug, not
|
||||
the board; passing channel 0 avoids it.
|
||||
|
||||
## Pulse counting and edge interrupts on LibreTiny (esphome#22391, #22400, #22403, #22404, 2026-11-06 to 11-07)
|
||||
|
||||
`scripts/libretiny_edges/`:
|
||||
- `pulse-bk.yaml` + `pulsegen.hpp` (BK7238, generator pins `GEN_PIN_A`/`GEN_PIN_B` jumpered to the counted pins) and
|
||||
`pulse-rtl.yaml` + the same header (BW15, mbed `pwmout` on `GEN_PIN_NAME`): one pulse shape per 35 s stage, from
|
||||
BL0937-like 38 µs pulses at 1 kHz up to 50 ms, counted by `pulse_counter` with `inverted` and filters as
|
||||
substitutions; `meas` lines give count per stage. Found: Beken counted half of BL0937 pulses (readings halve, so a
|
||||
calibration note was needed), RTL8720C counted none, RTL8710B none with inverted pins, LN882H counted correctly.
|
||||
- `bw12-base.yaml` with two pin layouts (`common-bw12.yaml`, `common-bw12-b.yaml`) + `bwdbg.hpp` (BW12, RTL8710B): edges
|
||||
without wiring by toggling the pad pull on floating pins (`PAD_PullCtrl`), `-Wl,--wrap=digitalRead` to count ISR
|
||||
versus task reads, and a pin/IRQ register dump; `summ.py` summarizes. A jumper IO12 to IO13 on the jig gave a real
|
||||
driven edge.
|
||||
- `common-bw12-time.yaml`: `millis()`/`micros()` every 5 s against host time, a DWT cycle bench per call (also under
|
||||
`InterruptLock`), and a mask ROM dump of `SYSTIMER_TickGet`.
|
||||
Results: #22400 https://github.com/esphome/esphome/pull/22400#issuecomment-6712520146, #22403
|
||||
https://github.com/esphome/esphome/pull/22403#issuecomment-6712901242, #22404
|
||||
https://github.com/esphome/esphome/pull/22404#issuecomment-6713095035.
|
||||
@@ -1,17 +0,0 @@
|
||||
# MCP23017 IOCON and interrupt line (#22232)
|
||||
|
||||
Bench: an ESP8266 (Feather HUZZAH) with an MCP23017 breakout at 0x27, SDA GPIO4, SCL GPIO5, INTA to GPIO14, INTB to
|
||||
GPIO13, PA0 jumpered to PB0 and PA1 to PB1, no extra pull-ups (the breakout's were enough at 100 kHz). The esptool
|
||||
reset keeps the expander powered, so a reflash shows whether register bits from the previous build survive.
|
||||
|
||||
`scripts/mcp23017/`: the `mcptest` external component (`ext/mcptest`) reads IOCON at 0x0A and 0x0B 6 s after boot,
|
||||
toggles PA0 and PA1 20 times, samples the INT levels right after and 200 ms after each write, counts INTB edges in
|
||||
an interrupt, checks the PB binary sensors (`SUMMARY` line), then checks whether IOCONB aliases IOCONA
|
||||
(`IOCONB_TEST`). `int.yaml` wires INTA to the hub's `interrupt_pin`, open drain or push-pull by the `open_drain`
|
||||
substitution (variants `int-od` and `int-pp`); `no-int.yaml` has no interrupt pin and counts INTA edges in
|
||||
`mcptest`. `run.sh VARIANT LABEL` (env `TREE`, `PORT`, `SC`, `PY`) builds, flashes and captures 14 s.
|
||||
|
||||
- In BANK=0, 0x0A and 0x0B are the same register: a write to one reads back from the other.
|
||||
- A push-pull INT line works without a pull-up. Open drain with no pull-up stays low after the first interrupt
|
||||
(ESP8266 INPUT), so the hub loop never stops and the inputs still update by polling.
|
||||
- Builds before #22232 leave ODR and MIRROR set across reflashes.
|
||||
@@ -1,33 +0,0 @@
|
||||
# MCP2518FD CAN FD over SPI (esphome#21951, 2026-11-07)
|
||||
|
||||
Bench: ESP32-PICO-D4 (`board: pico32`, ESP-IDF) wired to an MCP2518FD module with an ATA6563 transceiver: SCK GPIO25,
|
||||
SDI GPIO26, SDO GPIO27, nCS GPIO32, INT GPIO33, module 3V3/GND, CLK pin to GPIO34. No second CAN node, so everything
|
||||
runs in `LOOPBACK`, or with the controller forced into internal loopback behind the driver.
|
||||
|
||||
- **Read the crystal.** The module's can is marked `H20.00` (20 MHz) even though listings say "8 Mbps"; the PR default
|
||||
is `40MHZ`. Loopback never leaves the chip, so a wrong `clock:` still passes at half the configured rates. Measure it:
|
||||
`pulse_counter` on the CLK pin with `internal_filter: 0us` reads SYSCLK/10 (2 MHz) at reset. Older heads wrote 0 to
|
||||
`OSC` byte 0, which set `CLKODIV` to /1 (CLKO = 20 MHz, still countable by PCNT).
|
||||
- `scripts/mcp251xfd/canlb.hpp`: stream checker (one frame in flight, burst of 8 every 64th; standard/extended IDs, FD
|
||||
with and without BRS, every 4th classic, DLC 0 to 15, sequence in the payload; starts 5 s after boot so the
|
||||
`dump_config` stall cannot trip the 50 ms in-flight timeout), a queue checker for bursts, RTR and TX objects, raw SPI
|
||||
register access on the driver's own `SPIDevice` (`rd32`, `wr8`, `chip_reset` = `0x00 0x00`), forced modes
|
||||
(`force_mode` writes `CON+3`; `force_via_config` goes through Configuration so the chip accepts it), `watch_start`
|
||||
(polls `CON` every 2 ms until `OPMOD` and `REQOP` equal the target, logs the time), `txcheck` (reads each TX object
|
||||
back from chip RAM after a frame full of 0xAA, padding included), `burst` (sends N frames in one call so the loop
|
||||
cannot drain RX: FD depth 20, CLASSIC depth 32 + one overrun) and a PCNT unit on the CS pin, which counts SPI
|
||||
transactions while the pin stays an output (`cs_mark`/`cs_report`: idle cost per second).
|
||||
- `common-can.yaml` is the package; each config adds its `canbus` block and a `phases` script: `can-loopback.yaml`
|
||||
(stream, RTR, TX objects, idle cost, chip reset, two refused `REQOP` writes), `can-classic.yaml` (CLASSIC with a
|
||||
forced internal-loopback window: force once to trigger the driver's recovery, force again right after it restores,
|
||||
then burst 48 and RTR inside the next 5 s check interval), `can-normal-busoff.yaml` (no node acknowledges, so the
|
||||
controller cycles error passive/bus-off; each forced fault and reset waits for TREC `TXBO`).
|
||||
`run.sh CONFIG LABEL [SECONDS]` builds, flashes, captures and prints the summary lines.
|
||||
- Gotchas found: a refused `REQOP` (LISTENONLY or Restricted from LOOPBACK) leaves `OPMOD` unchanged and stalls TX;
|
||||
the chip refuses LOOPBACK to CLASSIC directly (go through Configuration); after a chip reset TREC reads `0x00200000`
|
||||
(`TXBO` set in Configuration mode), so a driver that skips recovery on bus-off never recovers a reset during an error
|
||||
state. Neither a reset nor a refused request sets an interrupt flag, so detection waits for the periodic check.
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/21951#issuecomment-6715452893 (first run),
|
||||
#issuecomment-6716105991 (TX objects, idle cost), #issuecomment-6716741859 (reset during bus-off bug),
|
||||
#issuecomment-6716957543 (fixed).
|
||||
@@ -16,18 +16,3 @@ states, saves the device log). Before 8cc8df4a1f a modem only config with `api:`
|
||||
config dependent `AUTO_LOAD` added `network` after the dependency check; an explicit `network:` block was the
|
||||
workaround. Results: https://github.com/esphome/esphome/pull/6721#issuecomment-6347395159 and
|
||||
https://github.com/esphome/esphome/pull/6721#issuecomment-6348520416
|
||||
|
||||
## SIM7080G on an M5Stack Atom Lite (#6721)
|
||||
|
||||
The M5 Unit CatM (SIM7080G) on the Atom Lite's Grove port: RX GPIO32, TX GPIO26, `model: SIM7080`.
|
||||
`templates/modem/modem-passthrough.yaml` is a raw UART passthrough with both directions logged, and
|
||||
`scripts/modem/mdrive.py HOST LOGFILE text:AT+CPIN? wait:5 ...` sends AT commands through it and saves the
|
||||
replies, to check what the module does with no component in the way.
|
||||
|
||||
- With no SIM, the component sets the `SIM not ready` warning and polls `AT+CPIN?` once a second.
|
||||
- With a SIM that has no data plan, `AT+CGATT=1` blocks the AT channel for about 141 s and then answers `ERROR`.
|
||||
The module answers nothing meanwhile, so the component logs `Network attachment request refused` and then `Modem not
|
||||
responding`, and loops every 2 minutes 22 seconds.
|
||||
- `AT+CFUN=1,1` leaves the module silent for about 2.5 minutes. It comes back without a power cycle.
|
||||
- A module left in CMUX (`AT+CMUX=0`, then an ESP restart) recovered 10 of 10 times (`Autodetect mode: 5`).
|
||||
- PPP and a warm reboot during PPP need a SIM with a working data plan.
|
||||
@@ -101,14 +101,3 @@ once on a build without TCP, or `tcp ok udp ok` twice and no error with it. Add
|
||||
come back, and connect with `apicheck.py ADDR` over Thread. Measured on the ItsyBitsy: 12.9 KB less flash and 17.1 KB
|
||||
less RAM without TCP (Zephyr's default pools, 16/16 instead of 24/48), and SMP over UDP OTA still works with them.
|
||||
Results: https://github.com/esphome/esphome/pull/21910#issuecomment-6518287139
|
||||
|
||||
## sdk-nrf 3.4.1 on the ItsyBitsy (#22233, #22558)
|
||||
|
||||
- Set `framework: version: 3.4.1` and `board: adafruit_itsybitsy`.
|
||||
- Trees before #22233 include `<nrf.h>` in `logger_zephyr.cpp`, which 3.4.1 no longer has on the include path, so
|
||||
Adafruit bootloader boards fail to build (`nrf.h: No such file or directory`). To test such a tree on 3.4.1, change
|
||||
that include to `<nrfx.h>` locally.
|
||||
- On the new USB stack, `usb_cdc_acm` bulk echo ran at about half the legacy rate (67 against 121 KiB/s for 64 KiB),
|
||||
and `flush()` returned `3`. The logger, DTR, line coding and the 1200 baud touch worked.
|
||||
- The zephyr_ble_server flow above, on 3.4.1: before #22558 advertising failed to restart after the first disconnect
|
||||
(`Advertising failed to start (rc -12)`); with it the server took 39 connections in 120 s.
|
||||
@@ -1,32 +0,0 @@
|
||||
# one_wire
|
||||
|
||||
## ESP8266 interrupt locks under Wi-Fi load (#22452, #22453)
|
||||
|
||||
Code that holds interrupts off for long on the ESP8266 starves the Wi-Fi interrupt handler (reset signatures in
|
||||
`reference/boards.md`). `scripts/esp8266_lock_load/` is the shared harness for any component that takes such locks:
|
||||
|
||||
- `base.yaml` is the shared DUT base: reset reason and info logged at boot, `power_save_mode: none`, `web_server` for
|
||||
the HTTP load, `api: reboot_timeout: 0s`.
|
||||
- `one-wire.yaml` reads a DS18B20 on GPIO4 every 2 s and runs a bus search every `search_interval`; `busy_control:
|
||||
"true"` replaces the search with the same busy time (13 ms) and no 1-Wire traffic.
|
||||
- `ir-one-pin.yaml` is the remote_transmitter case (see [remote_base.md](remote_base.md)).
|
||||
- `wifi_load.py HOST SECONDS PPS` sends 512 byte UDP datagrams to port 9 at a fixed rate plus back to back GETs of
|
||||
the web_server page.
|
||||
- `run.sh` builds and flashes a DUT, captures the serial log, runs the load and prints every reset reason (usage in its
|
||||
header). A clean run has one BOOT line.
|
||||
|
||||
Make one file per build with `substitutions: {name: ...}` and `packages: {t: !include one-wire.yaml}`, with
|
||||
`secrets.yaml` next to it, and give dev and the PR different names.
|
||||
|
||||
Notes:
|
||||
- Run each build for 45 minutes at 500 packets per second. Shorter or lighter runs often show no reset on either
|
||||
build: one search every 5 s survived on both.
|
||||
- The busy-wait control separates the lock from the CPU time. On #22452 it had no Hardware Watchdog in 45 minutes,
|
||||
while the PR still had 2 (dev had 6), so the remaining resets came from the slot locks. All three builds had one
|
||||
Software Watchdog, which is the busy time plus the load.
|
||||
- A search with no sensor answering is much shorter than one with a sensor (about 190 slot locks per search with
|
||||
one DS18B20), so test with a real sensor on the bus.
|
||||
- A stale `secrets.yaml` gives `No networks found` and the run has no IP to load.
|
||||
- The 1-Wire bus code is shared by every platform, so check a lock change on an ESP32 too: the same DS18B20 search
|
||||
config on ESP-IDF with `bluetooth_proxy` scanning and the same load gave 0 resets and no missed searches on dev and
|
||||
on #22452.
|
||||
@@ -47,6 +47,7 @@ each ring size at least the encoder window, three times, and logs the best time
|
||||
`BENCHDONE`. `bench.sh CONFIG DEVICE` builds and uploads `bench-esp32s3.yaml` or `bench-bk72xx.yaml`, collects one pass
|
||||
over syslog and fails when a case did not reach the end of its stream.
|
||||
|
||||
- LibreTiny defines `micros` as a macro, so the header calls `(esphome::micros)()`.
|
||||
- Keep the streams near 130 KB on a BK7231N: an image with about 330 KB of extra incompressible data failed its
|
||||
OTA with "Writing OTA data to flash memory failed". Use one image and `--windows 12` there.
|
||||
- `bench.hpp` targets the decoder interface with `dest_limit`. #21641 drops it (the output runs into the window up
|
||||
@@ -83,24 +84,3 @@ case's fires in order with progress collapsed to a count; diff dev against the P
|
||||
- A random file through web_server gives `error 128` on the ESP32 and `error 132` on the ESP8266.
|
||||
- A SIGTERM to the uploader lets it finish the transfer; use SIGKILL for the native error path.
|
||||
- Results: https://github.com/esphome/esphome/pull/21903#issuecomment-6516529409
|
||||
|
||||
## Signed OTA with several trusted keys (`verification_keys`, #22398)
|
||||
|
||||
`scripts/signed_ota/` on an ESP32-C3 (`signed-ota-c3.yaml`, `signed_ota_verification: signing_scheme: rsa3072,
|
||||
verification_keys: [keyA.pem]`):
|
||||
1. `python -m espsecure generate-signing-key --version 2 --scheme rsa3072 keyA.pem` (and keyB).
|
||||
2. Build four tagged images with `esphome -s tag <name> compile`, serial-flash the first factory image (no on-boot
|
||||
signature check without secure boot), copy each `firmware.ota.bin`.
|
||||
3. `sign_variants.py <image> keyA.pem keyB.pem <prefix>` writes key A, key B, B then A, stored CRC flipped, byte 2
|
||||
changed (covered only by the CRC), the same with the CRC recomputed by zlib, and B then A with block 1's CRC flipped.
|
||||
4. `otaseq.sh <ip> FILE=reject... FILE=<tag>...` sends them with `esphome upload --file` and checks the first verdict
|
||||
in the device log and the upload result against the expectation (a rejection, or the `BUILDTAG` of that build);
|
||||
send the rejects first so every check runs in the same build, then the accepted ones.
|
||||
Expected: unsigned, B only and every CRC edit rejected; A, B+A and the zlib-fixed CRC accepted. The byte 2 pair isolates
|
||||
the CRC from the signature and key digest.
|
||||
|
||||
Gotchas: an ESP-IDF image can contain `E7 02` at a 4 KiB boundary by chance; find the signature sector by the CRC, as
|
||||
`sign_variants.py` does. An unsigned image sent after a signed one can be rejected with "trusted key failed to verify"
|
||||
instead of "no signature block": the partition still holds the old signature sector past the new image's end.
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/22398#issuecomment-6716330334
|
||||
@@ -1,15 +0,0 @@
|
||||
# Per period PWM capture on the same chip (#15438)
|
||||
|
||||
`scripts/pwm_capture/` (ESP32-C6): `pwmcap.hpp` routes the `ledc` pad through the GPIO matrix into RMT receive at
|
||||
80 MHz (ping pong buffers, `en_partial_rx`, `mem_block_symbols: 96`) and a PCNT unit, checks every period in the IRAM
|
||||
safe RMT interrupt and logs a `WIN` line every 10 s (high time and period histograms, minimum and maximum, level
|
||||
changes, spikes, reversals). `common.yaml` needs `include_builtin_idf_components: [esp_driver_rmt, esp_driver_pcnt]`
|
||||
and `CONFIG_RMT_ISR_IRAM_SAFE` and `CONFIG_PCNT_ISR_IRAM_SAFE`; build it with `-s name NAME -s ps light` (or `none`).
|
||||
`run.sh NAME SECONDS [--every 5 --transition 1]` (env `TREE`, `PORT`, `SC`, `PY`) resets and captures while
|
||||
`apiclient.py` holds an API connection and optionally sends light transitions; `summ.py` totals the windows.
|
||||
|
||||
- LEDC and RMT share PLL_F80M, so a 20 kHz 11 bit output reads exactly 4000 ticks per period with no jitter.
|
||||
- Result: no glitch in about 10 million periods with power save `light` or `none`; #15718 (LEDC in IRAM) changed
|
||||
nothing. Positive control: a lambda that changes the duty for a few periods is caught every time.
|
||||
- Window boundaries follow RMT chunks (about 1000 periods), so `n` alternates between 199584 and 200592; sum over
|
||||
the run.
|
||||
@@ -12,8 +12,7 @@ markers and prints what each sent frame decoded as.
|
||||
- ESP32: `remote_transmitter` and two `remote_receiver`s on the same GPIO (`allow_other_uses`), carrier 100%, no
|
||||
inversion. RMT reads its own output, every frame decodes. Second receiver `dump: [nec, raw]` shows raw runs only
|
||||
where no primary matched.
|
||||
- ESP8266 before #22453: the transmitter sends the whole frame with interrupts off, so a receiver on the same pin
|
||||
sees nothing (from #22453 on it decodes, see below). For older trees, `softtx.hpp`
|
||||
- ESP8266: the transmitter sends with interrupts off, so a receiver on the same pin sees nothing. `softtx.hpp`
|
||||
bit-bangs frames built with each protocol's `encode()` with interrupts on, and an `interval` sets the pin back to
|
||||
OUTPUT after the receiver's setup. Bit-banged NEC decodes as NEC about 2 of 3 times (timing), on every build.
|
||||
- Dish never decodes in loopback on dev either. Pronto matches every frame, so raw on `dump: all` never prints.
|
||||
@@ -29,18 +28,3 @@ Results: https://github.com/esphome/esphome/pull/21563#issuecomment-6381304387
|
||||
Retest of the moved stack with 5 rounds per build: https://github.com/esphome/esphome/pull/21563#issuecomment-6382075726.
|
||||
A D1 mini CH340 sometimes fails the 460800 baud upload; esphome retries at 115200 by itself, but a runner that reads
|
||||
only the last line sees "Failed to upload" first, so rerun before blaming the build.
|
||||
|
||||
## ESP8266 transmitter to receiver on the same board (#22453)
|
||||
|
||||
- Wire the `remote_transmitter` pin to a `remote_receiver` pin (GPIO12 to GPIO14 on a D1 mini), `inverted: false`.
|
||||
- Keep the receiver's default `idle`. `idle: 4ms` is shorter than the NEC leader space (4.5 ms), so every frame is
|
||||
split and nothing decodes.
|
||||
- Dev before #22453 sends the whole frame inside one `InterruptLock`, so a receiver on the same chip decodes 0
|
||||
frames. That is expected on old builds, not a wiring fault. With locks per edge, 3,692 of 3,695 frames decoded
|
||||
under Wi-Fi load.
|
||||
- Count with `on_nec` matching a counter in the command, and log the counts on an `interval`.
|
||||
- Without a jumper, put both on GPIO12 with `allow_other_uses: true` and call `pinMode(12, OUTPUT)` before each frame
|
||||
(the receiver's setup leaves the shared pin as an input); the receiver's edge interrupt sees the transmitter's own
|
||||
output. `scripts/esp8266_lock_load/ir-one-pin.yaml` does this, and its `run.sh` adds Wi-Fi load (see
|
||||
[one_wire.md](one_wire.md)). Under load, the build that sends the whole frame in one lock hit a Hardware Watchdog in
|
||||
`wDev_ProcessFiq` after 11 minutes.
|
||||
@@ -21,9 +21,3 @@ captures 40 s from boot and runs `ivsum.py`, which prints the run count, first r
|
||||
runs), so do not compare those offsets between builds.
|
||||
- `main.cpp` shows the class each interval gets (`IntervalTrigger` or `DelayedIntervalTrigger` on #21799).
|
||||
- Results: https://github.com/esphome/esphome/pull/21799#issuecomment-6516798398
|
||||
|
||||
## Freelist trim after boot (ratgdo/esphome-ratgdo#705)
|
||||
|
||||
10 s after setup the scheduler frees its item pool and shrinks its `to_add_` and `items_` vectors
|
||||
(`SCHEDULER_FREELIST_TRIM_DELAY_MS` in `application.cpp`). The first burst of timers after that allocates new
|
||||
items and regrows the vectors once, so heap or timing measurements should skip the first run after boot.
|
||||
@@ -1,32 +0,0 @@
|
||||
# SPI bus capture on the same board, no chips or wiring (#22376)
|
||||
|
||||
`scripts/spi_self/`: `gen.py` writes `spi<board>.yaml` (e32 classic ESP32, c3, e66 ESP8266, rp Pico 2 W) with a
|
||||
hardware bus, a software bus, several `spi_device` instances (all four modes, MSB and LSB first, different rates and
|
||||
CS pins) and a `max7219`. `spitest.hpp` drives every device from a 20 ms interval (an 8 byte transfer, write16, a 12
|
||||
bit write, read_array, write_array, back to back transfers, teardown plus setup, a second setup, a max7219 update),
|
||||
three passes, then reboots. `run.sh BOARD LABEL SECONDS` (env `TREE`, `PORT`, `SC`, `PY`, and `PORTS` for the Pico)
|
||||
builds, flashes, captures and checks; `analyze.py spi<board>.yaml BASE.txt PR.txt` checks each record against the
|
||||
expected wire data and diffs the two builds by the majority payload per record, so interrupt jitter does not count.
|
||||
|
||||
- Classic ESP32: the ESPHome bus is on SPI2 through the matrix and SPI3 runs as a slave without DMA (64 bytes at
|
||||
most), set up again in the device's mode for each operation. Its nominal CS must be a spare pad held high
|
||||
(GPIO14); the device CS is routed into `spics_in` only after `spi_slave_queue_trans()`, because the slave driver
|
||||
reconnects the nominal pad when a transaction loads. Never give the slave the device's CS pad:
|
||||
`spicommon_cs_initialize` takes over its output and `spi_slave_free` resets it. `gpio_set_direction(..., OUTPUT)`
|
||||
turns a pad back into a plain GPIO output, so only call `gpio_input_enable` on pads the master drives.
|
||||
- With that slave attached, full duplex reads on the master lose their last bytes after the first read of a boot,
|
||||
even through a MOSI to MISO loopback; a config without the slave reads fine. Do not judge reads on that layout.
|
||||
- The slave sees bit-banged mode 1 data one bit late; the interrupt captures decode it correctly.
|
||||
- C3, ESP8266 and Pico 2 W: GPIO edge interrupts on SCK and CS take snapshots of the input register. C3 SPI2 cannot
|
||||
clock below about 78 kHz; at 80 to 100 kHz the capture works with
|
||||
`gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM)`, but one build lost an SCK pair at the start
|
||||
of a frame in every run and a rebuild did not: compare two builds before calling a missing edge real. Let the log
|
||||
output drain (20 ms) before a capture that follows `spi_setup()`.
|
||||
- ESP8266: the HSPI pads (12, 13, 14) do not show in `GPI` while in SPI function; the script logs `SPI1CLK`,
|
||||
`SPI1C`, `SPI1U` and `SPI1P` after each hardware bus operation instead.
|
||||
- Pico 2 W: `IO_BANK0` INOVER on the MISO pad forces the SPI receive input low or high (reads 00 and ff).
|
||||
- Software bus loopback: MOSI and MISO on one pad with `allow_other_uses`, then set the pad back to output.
|
||||
- Seen on dev before #22376: the software bus ignores `data_rate` (`cycle_clock_()` never waits), and SCK moves to
|
||||
the new idle level only after CS is low when devices with different clock polarity share the software bus.
|
||||
- LibreTiny (BK72xx, RTL87xx, LN882x) has no `spi:` (config validation allows esp32, esp8266, nrf52 and rp2 only),
|
||||
so SPI changes cannot be run there.
|
||||
@@ -24,8 +24,3 @@ identical payloads share one table, a lambda is sent from its vector. An empty c
|
||||
(`sendto() error 22` in the log, no datagram) until it accepted `nullptr` with a zero length; the old
|
||||
`TemplatableBytes::visit` had passed a zero length stack buffer instead. Compare sizes against the stacked PR's own
|
||||
head when the PR is stacked, not against dev. Results: https://github.com/esphome/esphome/pull/21263
|
||||
|
||||
## Two sockets on one port (#22509)
|
||||
|
||||
lwIP lets two sockets bind one UDP port when `SO_REUSEADDR` is set, and unicast then goes to the socket opened
|
||||
last; without it the second bind fails with errno 112.
|
||||
@@ -93,27 +93,3 @@ connected channel starts and ends on the bench channel with another channel in b
|
||||
- Dropping the AP under a connected device gives a `Beacon Timeout` link loss.
|
||||
- The 90 s interval fires once right after boot.
|
||||
- A `Disconnected ... Association Leave` right after `Connecting to` is the aborted attempt, not a missed connect.
|
||||
|
||||
## Soft AP with the BLE tracker scanning (#22534, #22566)
|
||||
|
||||
`scripts/ble_scan_soft_ap/`: `dut.yaml` runs only `wifi: ap:` with `captive_portal:` and `bluetooth_proxy:`, and
|
||||
`client.yaml` (on a second ESP32) joins that AP and fetches the portal page every 5 s, logging `JOIN` and `STAT`
|
||||
lines. `run.sh` erases and flashes the DUT, resets the client and prints the generated scan window, the first join, the
|
||||
page counts and the client's disconnect reasons (usage in its header).
|
||||
|
||||
- A scan window equal to the scan interval (320 ms of 320 ms) leaves the radio no time for the soft AP: on dev no
|
||||
client joined on the classic ESP32, S3, C3 or C6. With #22566 the tracker uses its 30 ms connection window while
|
||||
the AP runs, and every chip took a client in about 4 s.
|
||||
- Only one AP with the test SSID may be up at a time; list the other AP boards in `PARK` so they wait in their ROM
|
||||
bootloader.
|
||||
- To check that the full window comes back after provisioning, log `esp32_ble_tracker` at VERBOSE and provision
|
||||
with `templates/client-wifisave.yaml` (set `ap_ssid` to the DUT's and `ap_password: ""`, the DUT's AP is open): `Connection or AP active, using 48 unit scan window` must stop once the AP stops.
|
||||
- Some clients do not join an ESP8266 AP at all (the C6 failed on dev too with `Too Many Associations`); use a C3.
|
||||
|
||||
## ESP8266 power save after captive portal provisioning (#22542, #22564)
|
||||
|
||||
The DUT runs `ap:` plus `captive_portal:` with no station network and logs `wifi_get_sleep_type()` every 5 s (0 is
|
||||
`NONE_SLEEP_T`, 2 is `MODEM_SLEEP_T`); an ESP32-C3 with `templates/client-wifisave.yaml` posts the real credentials.
|
||||
Erase the flash before each run so no saved credentials skip the portal. On dev the sleep type stayed 2 after
|
||||
provisioning although the config asked for none; with #22564 it read 0 from the moment the AP started, and the
|
||||
`networks:` path still read 0.
|
||||
@@ -1,9 +0,0 @@
|
||||
# wled
|
||||
|
||||
## Realtime sender on the bench (#22509)
|
||||
|
||||
When the access point stops passing traffic between clients (lwIP `sendto` on the sender fails with errno 12
|
||||
because ARP never resolves), run the sender on a board instead of the host: an ESP32-S3 runs `wifi: ap:` and a
|
||||
25 ms interval lambda sends WARLS, DRGB or DNRGB frames with lwIP `sendto` to the devices under test, which join
|
||||
that AP with `manual_ip`. Two lights on the same port: see `udp.md`. Heap after an effect stops: see
|
||||
`heap_alloc.md`.
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
// Test only: swaps the first two records of an ATT Read By Type Response whose first record declares characteristic
|
||||
// 0xFFF1, the way some peripherals answer out of handle order (esphome#22518). Count of swaps in attswap_count.
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
struct AttSwapHdr {
|
||||
uint16_t event, len, offset, layer_specific;
|
||||
};
|
||||
extern "C" void __real_l2c_rcv_acl_data(void *p_msg);
|
||||
static volatile uint32_t attswap_count = 0;
|
||||
|
||||
extern "C" void __wrap_l2c_rcv_acl_data(void *p_msg) {
|
||||
auto *h = static_cast<AttSwapHdr *>(p_msg);
|
||||
uint8_t *p = reinterpret_cast<uint8_t *>(h + 1) + h->offset;
|
||||
// HCI ACL header (4), L2CAP header (4), then ATT: opcode, record length, records
|
||||
if (h->len >= 4 + 4 + 2 + 14) {
|
||||
uint16_t cid = p[6] | (p[7] << 8);
|
||||
uint8_t *att = p + 8;
|
||||
// record: handle(2) properties(1) value handle(2) uuid16(2)
|
||||
if (cid == 0x0004 && att[0] == 0x09 && att[1] == 7 && att[2 + 5] == 0xF1 && att[2 + 6] == 0xFF) {
|
||||
uint8_t tmp[7];
|
||||
memcpy(tmp, att + 2, 7);
|
||||
memcpy(att + 2, att + 9, 7);
|
||||
memcpy(att + 9, tmp, 7);
|
||||
attswap_count = attswap_count + 1;
|
||||
}
|
||||
}
|
||||
__real_l2c_rcv_acl_data(p_msg);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
"""Test only: wrap Bluedroid's l2c_rcv_acl_data so attswap.h can reorder a Read By Type response."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_build_flag("-Wl,--wrap=l2c_rcv_acl_data")
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
name: attprx6
|
||||
idf: 5.5.6
|
||||
packages:
|
||||
c: !include proxy.yaml
|
||||
@@ -1,125 +0,0 @@
|
||||
"""Through the proxy (no cache): print the full GATT table, read FFF1/FFF2/FFF3 by handle, and enable notify on
|
||||
FFF1, FFF2 and 2ACD (writing the CCCD the way Home Assistant does), counting what arrives per handle for 6 s.
|
||||
usage: gattcheck.py PROXY PEER_MAC env: API_KEY (the proxy's api encryption key)
|
||||
Exits 1 when a characteristic or CCCD is missing, a read or notification carries another characteristic's value,
|
||||
a notify fails, or a notifying characteristic sends nothing."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from aioesphomeapi import APIClient
|
||||
|
||||
SFX = "-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
|
||||
def short(u: str) -> str:
|
||||
return u[4:8] if u.endswith(SFX) else u
|
||||
|
||||
|
||||
async def main(host: str, peer: str) -> None:
|
||||
addr = int(peer.replace(":", ""), 16)
|
||||
cli = APIClient(host, 6053, None, noise_psk=os.environ["API_KEY"])
|
||||
await cli.connect(login=True)
|
||||
seen = asyncio.Event()
|
||||
cli.subscribe_bluetooth_le_raw_advertisements(
|
||||
lambda m: (
|
||||
seen.set() if any(a.address == addr for a in m.advertisements) else None
|
||||
)
|
||||
)
|
||||
await asyncio.wait_for(seen.wait(), 30)
|
||||
state: asyncio.Queue = asyncio.Queue()
|
||||
cancel = await cli.bluetooth_device_connect(
|
||||
addr,
|
||||
lambda c, mtu, err: state.put_nowait((c, mtu, err)),
|
||||
timeout=20,
|
||||
address_type=0,
|
||||
feature_flags=0xFF,
|
||||
has_cache=False,
|
||||
)
|
||||
c, mtu, err = await asyncio.wait_for(state.get(), 25)
|
||||
if not c:
|
||||
sys.exit(f"connect failed err={err}")
|
||||
svcs = await cli.bluetooth_gatt_get_services(addr)
|
||||
chars = {}
|
||||
for s in svcs.services:
|
||||
print(f"SVC {short(s.uuid)} 0x{s.handle:04X}", flush=True)
|
||||
for ch in s.characteristics:
|
||||
chars[short(ch.uuid)] = ch
|
||||
print(
|
||||
f" CHR {short(ch.uuid)} 0x{ch.handle:04X} props=0x{ch.properties:02X}",
|
||||
flush=True,
|
||||
)
|
||||
for d in ch.descriptors:
|
||||
print(f" DSC {short(d.uuid)} 0x{d.handle:04X}", flush=True)
|
||||
wanted = ("fff1", "fff2", "fff3", "2acc", "2acd")
|
||||
problems = [f"{u} missing" for u in wanted if u not in chars]
|
||||
for u in ("fff1", "fff2", "fff3", "2acc"):
|
||||
if u in chars:
|
||||
v = bytes(await cli.bluetooth_gatt_read(addr, chars[u].handle))
|
||||
text = v.decode(errors="replace")
|
||||
print(f"READ {u} 0x{chars[u].handle:04X} -> {text}", flush=True)
|
||||
# every server value starts with its own UUID, so a mixed-up handle shows here
|
||||
if not text.startswith(f"{u}-"):
|
||||
problems.append(f"{u} read returned {text!r}")
|
||||
got: dict[int, list[str]] = {}
|
||||
|
||||
def on_notify(handle: int, data: bytearray) -> None:
|
||||
got.setdefault(handle, []).append(bytes(data).decode(errors="replace"))
|
||||
|
||||
for u in ("fff1", "fff2", "2acd"):
|
||||
if u in chars:
|
||||
try:
|
||||
await cli.bluetooth_gatt_start_notify(addr, chars[u].handle, on_notify)
|
||||
# like Home Assistant's client: write the first CCCD listed under the characteristic
|
||||
cccd = next(
|
||||
(d for d in chars[u].descriptors if short(d.uuid) == "2902"), None
|
||||
)
|
||||
if cccd is None:
|
||||
print(
|
||||
f"NOTIFY {u} 0x{chars[u].handle:04X} NO CCCD in its descriptor list",
|
||||
flush=True,
|
||||
)
|
||||
problems.append(f"{u} has no CCCD")
|
||||
continue
|
||||
await cli.bluetooth_gatt_write_descriptor(
|
||||
addr, cccd.handle, b"\x01\x00"
|
||||
)
|
||||
print(
|
||||
f"NOTIFY {u} 0x{chars[u].handle:04X} enabled, CCCD 0x{cccd.handle:04X} written",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(
|
||||
f"NOTIFY {u} 0x{chars[u].handle:04X} FAILED {type(exc).__name__}: {exc}",
|
||||
flush=True,
|
||||
)
|
||||
problems.append(f"{u} notify failed")
|
||||
await asyncio.sleep(6)
|
||||
if not got:
|
||||
print("GOT nothing", flush=True)
|
||||
for h, vals in sorted(got.items()):
|
||||
name = next((u for u, ch in chars.items() if ch.handle == h), "?")
|
||||
print(
|
||||
f"GOT 0x{h:04X} ({name}) x{len(vals)}: {vals[0]} .. {vals[-1]}", flush=True
|
||||
)
|
||||
problems.extend(
|
||||
f"0x{h:04X} ({name}) notified {v!r}"
|
||||
for v in vals
|
||||
if name == "?" or not v.startswith(f"{name}-")
|
||||
)
|
||||
# the server notifies FFF1 and 2ACD every second
|
||||
problems.extend(
|
||||
f"{u} sent nothing"
|
||||
for u in ("fff1", "2acd")
|
||||
if u in chars and chars[u].handle not in got
|
||||
)
|
||||
await cli.bluetooth_device_disconnect(addr)
|
||||
cancel()
|
||||
await cli.disconnect()
|
||||
if problems:
|
||||
sys.exit("FAIL: " + ", ".join(problems))
|
||||
print("PASS", flush=True)
|
||||
|
||||
|
||||
asyncio.run(main(sys.argv[1], sys.argv[2]))
|
||||
@@ -1,56 +0,0 @@
|
||||
"""Connect to PEER_MAC through the proxy like Home Assistant (v3 without cache) and print the services it returns.
|
||||
usage: gattget.py PROXY PEER_MAC ROUNDS env: API_KEY (the proxy's api encryption key)"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from aioesphomeapi import APIClient
|
||||
|
||||
|
||||
async def main(host: str, peer: str, rounds: int) -> None:
|
||||
addr = int(peer.replace(":", ""), 16)
|
||||
cli = APIClient(host, 6053, None, noise_psk=os.environ["API_KEY"])
|
||||
await cli.connect(login=True)
|
||||
seen = asyncio.Event()
|
||||
|
||||
def adv(msg) -> None:
|
||||
if any(a.address == addr for a in msg.advertisements):
|
||||
seen.set()
|
||||
|
||||
cli.subscribe_bluetooth_le_raw_advertisements(adv)
|
||||
await asyncio.wait_for(seen.wait(), 30)
|
||||
failed = 0
|
||||
for r in range(1, rounds + 1):
|
||||
state: asyncio.Queue = asyncio.Queue()
|
||||
cancel = await cli.bluetooth_device_connect(
|
||||
addr,
|
||||
lambda c, mtu, err, q=state: q.put_nowait((c, mtu, err)),
|
||||
timeout=20,
|
||||
address_type=0,
|
||||
feature_flags=0xFF,
|
||||
has_cache=False,
|
||||
)
|
||||
c, mtu, err = await asyncio.wait_for(state.get(), 25)
|
||||
if not c:
|
||||
print(f"R{r} connect failed err={err}", flush=True)
|
||||
failed += 1
|
||||
cancel()
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
svcs = await cli.bluetooth_gatt_get_services(addr)
|
||||
sfx = "-0000-1000-8000-00805f9b34fb"
|
||||
out = [
|
||||
f"{s.uuid[4:8] if s.uuid.endswith(sfx) else s.uuid}({len(s.characteristics)})"
|
||||
for s in svcs.services
|
||||
]
|
||||
print(f"R{r} mtu={mtu} services: {' '.join(out)}", flush=True)
|
||||
await cli.bluetooth_device_disconnect(addr)
|
||||
cancel()
|
||||
await asyncio.sleep(5)
|
||||
await cli.disconnect()
|
||||
if failed:
|
||||
sys.exit(f"{failed} of {rounds} rounds did not connect")
|
||||
|
||||
|
||||
asyncio.run(main(sys.argv[1], sys.argv[2], int(sys.argv[3])))
|
||||
@@ -1,37 +0,0 @@
|
||||
# Proxy (classic ESP32): bluetooth_proxy without service cache, encrypted API, the reorder wrapper.
|
||||
# Substitutions: name, idf.
|
||||
# Keep the per-build file in this directory: attswap.hpp and components/ are found relative to it.
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes: [attswap.hpp]
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: ${idf}
|
||||
logger:
|
||||
level: DEBUG
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
# a plaintext proxy can be taken over by any Home Assistant on the network mid-test
|
||||
encryption:
|
||||
key: !secret api_key
|
||||
ota:
|
||||
- platform: esphome
|
||||
external_components:
|
||||
- source: {type: local, path: components}
|
||||
components: [attswap]
|
||||
attswap:
|
||||
esp32_ble_tracker:
|
||||
scan_parameters:
|
||||
active: true
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
cache_services: false
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("swap", "attswap swaps=%u", (unsigned) attswap_count);'
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh PROXY_YAML [ROUNDS]: build server.yaml and PROXY_YAML from TREE, flash both, capture the proxy and read the
|
||||
# server's services through it ROUNDS times (default 3). Prints the services per round and the Bluedroid discovery
|
||||
# errors. Exit 0: every round got all services; 1: a round got a partial table (the bug); 2: setup failed, a round
|
||||
# did not connect, or the swap never fired (the result would be meaningless). PROXY_YAML must sit in this
|
||||
# directory, next to attswap.hpp and components/.
|
||||
# env: TREE, SERVER_YAML (default server.yaml), API_KEY (proxy api key), SERVER_PORT, PROXY_PORT, SERVER_BLE_MAC
|
||||
# (the server's BLE address), SC (path to serial_capture.py), PY
|
||||
D=$(cd "$(dirname "$0")" && pwd); Y=$(cd "$(dirname "${1:?proxy yaml}")" && pwd)/$(basename "$1"); R=${2:-3}
|
||||
W=${TREE:?set TREE}; SP=${SERVER_PORT:?set SERVER_PORT}; PP=${PROXY_PORT:?set PROXY_PORT}; MAC=${SERVER_BLE_MAC:?set SERVER_BLE_MAC}
|
||||
API_KEY=${API_KEY:?set API_KEY}; SC=${SC:?set SC}; PY=${PY:-$W/venv/bin/python}; N=$(basename "$Y" .yaml); O=$D/out-$N; mkdir -p "$O"
|
||||
(cd "$W" && PYTHONPATH=$W $PY -m esphome -s name attsrv run --no-logs "$D/${SERVER_YAML:-server.yaml}" --device "$SP") > "$O/server.log" 2>&1 ||
|
||||
{ echo "server build or upload failed"; exit 2; }
|
||||
(cd "$W" && PYTHONPATH=$W $PY -m esphome run --no-logs "$Y" --device "$PP") > "$O/proxy-upload.log" 2>&1 ||
|
||||
{ echo "$N build or upload failed"; exit 2; }
|
||||
rm -f "$O/proxy.txt"
|
||||
$PY "$SC" "$PP" $((R * 30 + 160)) "$O/proxy.txt" --reset > "$O/capture.log" 2>&1 &
|
||||
CAP=$!
|
||||
IP=""
|
||||
for _ in $(seq 40); do
|
||||
IP=$(LC_ALL=C grep -a -o -E "IP Address: [0-9.]+" "$O/proxy.txt" | tail -1 | cut -d' ' -f3)
|
||||
[ -n "$IP" ] && break; sleep 2
|
||||
done
|
||||
[ -n "$IP" ] || { kill "$CAP"; echo "$N no IP"; exit 2; }
|
||||
sleep 5
|
||||
timeout $((R * 30 + 60)) $PY "$D/gattget.py" "$IP" "$MAC" "$R" | tee "$O/client.txt"
|
||||
[ "${PIPESTATUS[0]}" = 0 ] || { kill "$CAP"; echo "$N client failed"; exit 2; }
|
||||
wait "$CAP" || { echo "$N capture failed (see $O/capture.log)"; exit 2; }
|
||||
echo "== $N"
|
||||
LC_ALL=C grep -a -E "invalid handle|Service discovery complete|attswap swaps=[1-9]" "$O/proxy.txt" | sed 's/^\[[0-9:]*\]//' |
|
||||
sort | uniq -c
|
||||
grep -a -q -E "attswap swaps=[1-9]" "$O/proxy.txt" || { echo "$N INVALID: the reorder never fired"; exit 2; }
|
||||
# every round must list the reordered service (fff0) and the one after it (1826) with all their characteristics
|
||||
grep -a -q "^R[0-9]* mtu=" "$O/client.txt" || { echo "$N no client output"; exit 2; }
|
||||
grep -a "^R[0-9]* mtu=" "$O/client.txt" | grep -v -q "fff0(6) 1826(2)" && exit 1
|
||||
exit 0
|
||||
@@ -1,45 +0,0 @@
|
||||
# Peripheral (ESP32-S3) for gattcheck.py: FFF1 to FFF6 each hold their own value, FFF1, FFF2 and 2ACD can notify,
|
||||
# and FFF1 and 2ACD send a changing value every second. Substitution: name.
|
||||
esphome:
|
||||
name: ${name}
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
esp32_ble:
|
||||
name: ${name}
|
||||
esp32_ble_server:
|
||||
manufacturer: ESPHome
|
||||
model: attcheck
|
||||
services:
|
||||
- uuid: FFF0
|
||||
advertise: true
|
||||
characteristics:
|
||||
- {id: c1, uuid: FFF1, read: true, notify: true, value: "fff1-v"}
|
||||
- {uuid: FFF2, read: true, notify: true, value: "fff2-v"}
|
||||
- {uuid: FFF3, read: true, value: "fff3-v"}
|
||||
- {uuid: FFF4, read: true, value: "fff4-v"}
|
||||
- {uuid: FFF5, read: true, value: "fff5-v"}
|
||||
- {uuid: FFF6, read: true, value: "fff6-v"}
|
||||
- uuid: "1826"
|
||||
characteristics:
|
||||
- {uuid: 2ACC, read: true, value: "2acc-v"}
|
||||
- {id: cd, uuid: 2ACD, read: true, notify: true, value: "2acd-0"}
|
||||
on_connect:
|
||||
- logger.log: {format: "SRV connected", level: INFO}
|
||||
globals:
|
||||
- {id: n, type: int, initial_value: "0"}
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- lambda: id(n)++;
|
||||
- ble_server.characteristic.set_value:
|
||||
id: cd
|
||||
value: !lambda 'std::string v = "2acd-" + to_string(id(n)); return std::vector<uint8_t>(v.begin(), v.end());'
|
||||
- ble_server.characteristic.notify: {id: cd}
|
||||
- ble_server.characteristic.set_value:
|
||||
id: c1
|
||||
value: !lambda 'std::string v = "fff1-n" + to_string(id(n)); return std::vector<uint8_t>(v.begin(), v.end());'
|
||||
- ble_server.characteristic.notify: {id: c1}
|
||||
@@ -1,32 +0,0 @@
|
||||
# Peripheral (ESP32-S3): a 6-characteristic service 0xFFF0, then an FTMS-like 0x1826 service. Substitution: name.
|
||||
esphome:
|
||||
name: ${name}
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
esp32_ble:
|
||||
name: ${name}
|
||||
esp32_ble_server:
|
||||
manufacturer: ESPHome
|
||||
model: g22518
|
||||
services:
|
||||
- uuid: FFF0
|
||||
advertise: true
|
||||
characteristics:
|
||||
- {uuid: FFF1, read: true, value: [1]}
|
||||
- {uuid: FFF2, read: true, value: [2]}
|
||||
- {uuid: FFF3, read: true, value: [3]}
|
||||
- {uuid: FFF4, read: true, value: [4]}
|
||||
- {uuid: FFF5, read: true, value: [5]}
|
||||
- {uuid: FFF6, read: true, value: [6]}
|
||||
- uuid: "1826"
|
||||
characteristics:
|
||||
- {uuid: 2ACC, read: true, value: [0, 0, 0, 0]}
|
||||
- {uuid: 2ACD, read: true, notify: true, value: [0, 0]}
|
||||
on_connect:
|
||||
- logger.log: {format: "SRV connected", level: INFO}
|
||||
on_disconnect:
|
||||
- logger.log: {format: "SRV disconnected", level: INFO}
|
||||
@@ -1,48 +0,0 @@
|
||||
# Joins the DUT's open AP, fetches the portal page every 5 s and logs JOIN and STAT lines. Substitutions: name, board,
|
||||
# logger_uart, ap_ssid (same as dut.yaml).
|
||||
substitutions:
|
||||
board: esp32-c3-devkitm-1
|
||||
logger_uart: USB_SERIAL_JTAG
|
||||
ap_ssid: bleaptest
|
||||
esphome:
|
||||
name: ${name}
|
||||
esp32:
|
||||
board: ${board}
|
||||
framework:
|
||||
type: esp-idf
|
||||
logger:
|
||||
hardware_uart: ${logger_uart}
|
||||
wifi:
|
||||
ssid: ${ap_ssid}
|
||||
reboot_timeout: 0s
|
||||
on_connect:
|
||||
- lambda: |-
|
||||
id(joins)++;
|
||||
ESP_LOGI("cli", "JOIN %d at %u ms", id(joins), (unsigned) millis());
|
||||
on_disconnect:
|
||||
- lambda: ESP_LOGI("cli", "LEFT at %u ms", (unsigned) millis());
|
||||
http_request:
|
||||
timeout: 5s
|
||||
globals:
|
||||
- {id: joins, type: int, initial_value: "0"}
|
||||
- {id: pages_ok, type: int, initial_value: "0"}
|
||||
- {id: pages_fail, type: int, initial_value: "0"}
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- if:
|
||||
condition: wifi.connected
|
||||
then:
|
||||
- http_request.get:
|
||||
url: http://192.168.4.1/
|
||||
on_response:
|
||||
then:
|
||||
- lambda: if (response->status_code == 200) id(pages_ok)++; else id(pages_fail)++;
|
||||
on_error:
|
||||
then:
|
||||
- lambda: id(pages_fail)++;
|
||||
- interval: 15s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("cli", "STAT up=%u joins=%d connected=%d pages_ok=%d pages_fail=%d", (unsigned) (millis() / 1000),
|
||||
id(joins), (int) wifi::global_wifi_component->is_connected(), id(pages_ok), id(pages_fail));
|
||||
@@ -1,23 +0,0 @@
|
||||
# Soft AP only (no station network) with captive_portal and bluetooth_proxy, as in #22534. Substitutions: name,
|
||||
# board, logger_uart (USB_SERIAL_JTAG on native USB chips, UART0 on a classic devkit), ap_ssid.
|
||||
substitutions:
|
||||
board: esp32dev
|
||||
logger_uart: UART0
|
||||
ap_ssid: bleaptest
|
||||
esphome:
|
||||
name: ${name}
|
||||
esp32:
|
||||
board: ${board}
|
||||
framework:
|
||||
type: esp-idf
|
||||
logger:
|
||||
hardware_uart: ${logger_uart}
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
ota:
|
||||
- platform: esphome
|
||||
wifi:
|
||||
ap:
|
||||
ssid: ${ap_ssid}
|
||||
captive_portal:
|
||||
bluetooth_proxy:
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh DUT_YAML SECONDS: erase and flash DUT_YAML from TREE, let it boot, then reset the client (already flashed with
|
||||
# client.yaml) and capture both for SECONDS. Prints the generated scan window, the client's first JOIN and last STAT
|
||||
# lines, its disconnect reasons and the DUT's resets. One AP with this SSID at a time: ports in PARK are held in their
|
||||
# ROM bootloader first. The yaml file name must equal its `name:` (the build directory is found by it).
|
||||
# Exit 0: pass; 1: the run finished and failed the checks; 2: setup or capture failed.
|
||||
# env: TREE, DUT_PORT, CLIENT_PORT, PARK (space separated ports, optional), SC (path to serial_capture.py), PY
|
||||
Y=$(cd "$(dirname "${1:?dut yaml}")" && pwd)/$(basename "$1"); T=${2:?seconds}; N=$(basename "$Y" .yaml)
|
||||
W=${TREE:?set TREE}; DUT=${DUT_PORT:?set DUT_PORT}; CLI=${CLIENT_PORT:?set CLIENT_PORT}; SC=${SC:?set SC}
|
||||
PY=${PY:-$W/venv/bin/python}; O=$(dirname "$Y")/out-$N; mkdir -p "$O"
|
||||
for p in $PARK; do $PY -m esptool --port "$p" --after no-reset chip-id > /dev/null 2>&1 || { echo "park $p failed"; exit 2; }; done
|
||||
(cd "$W" && PYTHONPATH=$W $PY -m esphome compile "$Y") > "$O/compile.log" 2>&1 || { echo "$N compile failed"; exit 2; }
|
||||
$PY -m esptool --port "$DUT" erase_flash > "$O/erase.log" 2>&1 || { echo "$N erase failed"; exit 2; }
|
||||
(cd "$W" && PYTHONPATH=$W $PY -m esphome upload "$Y" --device "$DUT") > "$O/upload.log" 2>&1 || { echo "$N upload failed"; exit 2; }
|
||||
$PY "$SC" "$DUT" $((T + 40)) "$O/dut.txt" --reset > "$O/dut-capture.log" 2>&1 &
|
||||
CAP=$!
|
||||
trap 'kill $CAP 2>/dev/null' EXIT
|
||||
sleep 30
|
||||
$PY "$SC" "$CLI" "$T" "$O/cli.txt" --reset > "$O/cli-capture.log" 2>&1 || { echo "$N client capture failed"; exit 2; }
|
||||
wait "$CAP" || { echo "$N DUT capture failed"; exit 2; }
|
||||
B=$(dirname "$Y")/.esphome/build/$N/src/main.cpp
|
||||
[ -f "$B" ] || { echo "$N: $B not found (file name differs from name:?)"; exit 2; }
|
||||
echo "== $N window=$(grep -o 'set_scan_window([0-9]*)' "$B" || echo none)"
|
||||
J=$(grep -a -m1 "JOIN 1" "$O/cli.txt" | cut -c1-100); echo "${J:-$N NO JOIN}"
|
||||
STAT=$(grep -a "STAT" "$O/cli.txt" | tail -1 | cut -c1-140); echo "$STAT"
|
||||
echo "client disconnect reasons: $(grep -a -o "reason='[^']*'" "$O/cli.txt" | sort | uniq -c | tr '\n' ' ')"
|
||||
echo "dut resets: $(grep -a -c 'rst:' "$O/dut.txt")"
|
||||
# pass: the client joined and fetched pages, and the DUT reset exactly once (the capture's own reset)
|
||||
[ -n "$J" ] && [ -n "$STAT" ] && [[ "$STAT" != *"pages_ok=0 "* ]] && [ "$(grep -a -c 'rst:' "$O/dut.txt")" = 1 ]
|
||||
@@ -1,194 +0,0 @@
|
||||
#pragma once
|
||||
// BMP390 bench helpers: register dump, INT data-ready edge counter, start write fault injection (software NACK or
|
||||
// SDA held through the GPIO matrix) and a bus probe. ESP32-S3, ESP-IDF.
|
||||
#include <cmath>
|
||||
#include "esphome/components/i2c/i2c_bus.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_rom_gpio.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "soc/gpio_reg.h"
|
||||
#include "soc/soc.h"
|
||||
#include "soc/io_mux_reg.h"
|
||||
#include "soc/gpio_periph.h"
|
||||
|
||||
namespace bmpt {
|
||||
static const char *const TAG = "bmpt";
|
||||
|
||||
inline volatile uint32_t g_int_edges = 0;
|
||||
inline void int_isr(void *) { g_int_edges = g_int_edges + 1; }
|
||||
inline void int_counter_begin(int pin) {
|
||||
gpio_config_t c{};
|
||||
c.pin_bit_mask = 1ULL << pin;
|
||||
c.mode = GPIO_MODE_INPUT;
|
||||
c.pull_down_en = GPIO_PULLDOWN_ENABLE;
|
||||
c.intr_type = GPIO_INTR_POSEDGE;
|
||||
gpio_config(&c);
|
||||
esp_err_t e = gpio_install_isr_service(0);
|
||||
if (e != ESP_OK && e != ESP_ERR_INVALID_STATE)
|
||||
ESP_LOGE(TAG, "isr service %d", e);
|
||||
gpio_isr_handler_add((gpio_num_t) pin, int_isr, nullptr);
|
||||
}
|
||||
|
||||
inline int g_sda = -1;
|
||||
inline int g_port = 0;
|
||||
inline bool g_held = false;
|
||||
inline int g_hold_level = 0; // 0: SDA shorted low, 1: SDA disconnected from the controller (open line, pulled high)
|
||||
inline bool g_bus_reset_on_probe = false;
|
||||
inline uint32_t g_out_sel = 0;
|
||||
inline bool g_saved = false;
|
||||
inline bool g_log_pad_once = true;
|
||||
inline uint32_t out_sel_reg() { return REG_READ(GPIO_FUNC0_OUT_SEL_CFG_REG + 4 * g_sda); }
|
||||
inline uint32_t pin_reg() { return REG_READ(GPIO_PIN0_REG + 4 * g_sda); }
|
||||
inline uint32_t mux_reg() { return REG_READ(GPIO_PIN_MUX_REG[g_sda]); }
|
||||
inline uint32_t en_bit() { return (REG_READ(GPIO_ENABLE_REG) >> g_sda) & 1; }
|
||||
inline uint32_t g_pin = 0, g_mux = 0, g_en = 0;
|
||||
inline void log_pad(const char *when) {
|
||||
ESP_LOGI(TAG, "PAD %s out_sel=0x%08X pin=0x%08X mux=0x%08X en=%u", when, (unsigned) out_sel_reg(),
|
||||
(unsigned) pin_reg(), (unsigned) mux_reg(), (unsigned) en_bit());
|
||||
}
|
||||
inline void sda_hold() {
|
||||
if (!g_saved) {
|
||||
g_out_sel = out_sel_reg(); // routing the i2c driver set up; restored verbatim on release
|
||||
g_pin = pin_reg();
|
||||
g_mux = mux_reg();
|
||||
g_en = en_bit();
|
||||
log_pad("before-hold");
|
||||
g_saved = true;
|
||||
ESP_LOGI(TAG, "SDA out_sel saved 0x%08X (I2CEXT0_SDA_OUT_IDX=%d port=%d)", (unsigned) g_out_sel,
|
||||
I2CEXT0_SDA_OUT_IDX, g_port);
|
||||
}
|
||||
gpio_set_level((gpio_num_t) g_sda, g_hold_level);
|
||||
esp_rom_gpio_connect_out_signal(g_sda, SIG_GPIO_OUT_IDX, false, false);
|
||||
g_held = true;
|
||||
if (g_log_pad_once) {
|
||||
log_pad("held");
|
||||
}
|
||||
}
|
||||
inline void sda_release() {
|
||||
gpio_set_level((gpio_num_t) g_sda, 1);
|
||||
REG_WRITE(GPIO_FUNC0_OUT_SEL_CFG_REG + 4 * g_sda, g_out_sel);
|
||||
REG_WRITE(GPIO_PIN0_REG + 4 * g_sda, g_pin);
|
||||
REG_WRITE(GPIO_PIN_MUX_REG[g_sda], g_mux);
|
||||
if (g_en)
|
||||
REG_WRITE(GPIO_ENABLE_W1TS_REG, 1u << g_sda);
|
||||
else
|
||||
REG_WRITE(GPIO_ENABLE_W1TC_REG, 1u << g_sda);
|
||||
g_held = false;
|
||||
if (g_log_pad_once) {
|
||||
log_pad("released");
|
||||
g_log_pad_once = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps the sensor's bus: injects the start write faults and logs every failed transfer
|
||||
class FaultBus : public esphome::i2c::I2CBus {
|
||||
public:
|
||||
explicit FaultBus(esphome::i2c::I2CBus *real) : real_(real) {}
|
||||
uint32_t hold_until{0}; // millis() deadline of a timed hold
|
||||
bool arm_start{false}; // hold SDA only during the next forced mode start write (PWR_CTRL mode bits 01)
|
||||
// until this deadline every transfer fails on its own: free-bus dummy read, hold, transfer, release
|
||||
uint32_t window_until{0};
|
||||
bool soft_arm{false}; // next start write fails with NACK in software, the wire is not touched
|
||||
uint32_t soft_until{0}; // every start write fails with NACK in software until this deadline
|
||||
uint32_t xfers{0}, fails{0}, start_writes{0}, start_fails{0}, normal_writes{0};
|
||||
esphome::i2c::ErrorCode write_readv(uint8_t address, const uint8_t *w, size_t wn, uint8_t *r, size_t rn) override {
|
||||
// mode bits of a PWR_CTRL write: 1 forced (a start), 3 normal
|
||||
int mode = wn == 2 && w[0] == 0x1B ? (w[1] >> 4) & 3 : -1;
|
||||
bool is_start = mode == 1;
|
||||
bool targeted = is_start && this->arm_start;
|
||||
if (is_start && (this->soft_arm || (int32_t) (this->soft_until - esphome::millis()) > 0)) {
|
||||
this->soft_arm = false;
|
||||
this->start_writes++;
|
||||
this->start_fails++;
|
||||
this->xfers++;
|
||||
this->fails++;
|
||||
ESP_LOGI(TAG, "SOFTFAIL start write reg=0x1B val=0x%02X -> NACK", w[1]);
|
||||
return esphome::i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
bool timed = (int32_t) (this->hold_until - esphome::millis()) > 0;
|
||||
bool window = (int32_t) (this->window_until - esphome::millis()) > 0;
|
||||
if (window) {
|
||||
// let the driver run its post-error FSM reset with free lines, so each injected failure starts from a clean bus
|
||||
uint8_t reg = 0x00, v;
|
||||
this->real_->write_readv(address, ®, 1, &v, 1);
|
||||
targeted = true;
|
||||
}
|
||||
if (timed || targeted)
|
||||
sda_hold(); // reasserted per transfer in case the driver re-routes the pin after an error
|
||||
if (is_start)
|
||||
this->start_writes++;
|
||||
if (mode == 3)
|
||||
this->normal_writes++;
|
||||
auto err = this->real_->write_readv(address, w, wn, r, rn);
|
||||
if (targeted) {
|
||||
this->arm_start = false;
|
||||
sda_release();
|
||||
} else if (!timed && g_held) {
|
||||
sda_release();
|
||||
}
|
||||
this->xfers++;
|
||||
if (err != esphome::i2c::ERROR_OK) {
|
||||
this->fails++;
|
||||
if (is_start)
|
||||
this->start_fails++;
|
||||
ESP_LOGI(TAG, "XFERFAIL addr=0x%02X reg=0x%02X wn=%u rn=%u val=0x%02X err=%d start=%d", address,
|
||||
wn > 0 ? w[0] : 0xFF, (unsigned) wn, (unsigned) rn, wn > 1 ? w[1] : 0, (int) err, is_start);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
protected:
|
||||
esphome::i2c::I2CBus *real_;
|
||||
};
|
||||
inline FaultBus *g_fb = nullptr;
|
||||
inline esphome::i2c::I2CBus *g_real = nullptr;
|
||||
|
||||
// Release a timed hold once its deadline passes even when no transfer happens.
|
||||
inline void fault_tick() {
|
||||
if (g_fb != nullptr && g_held && !g_fb->arm_start && (int32_t) (g_fb->hold_until - esphome::millis()) <= 0)
|
||||
sda_release();
|
||||
}
|
||||
|
||||
inline bool rd(uint8_t addr, uint8_t reg, uint8_t *v, size_t n = 1) {
|
||||
return g_real->write_readv(addr, ®, 1, v, n) == esphome::i2c::ERROR_OK;
|
||||
}
|
||||
inline bool wr(uint8_t addr, uint8_t reg, uint8_t v) {
|
||||
uint8_t b[2] = {reg, v};
|
||||
return g_real->write_readv(addr, b, 2, nullptr, 0) == esphome::i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
inline void dump_regs(uint8_t addr) {
|
||||
uint8_t chip = 0, err = 0, ictl = 0, pwr = 0, osr = 0, odr = 0, cfg = 0;
|
||||
bool ok = rd(addr, 0x00, &chip) && rd(addr, 0x02, &err) && rd(addr, 0x19, &ictl) && rd(addr, 0x1B, &pwr) &&
|
||||
rd(addr, 0x1C, &osr) && rd(addr, 0x1D, &odr) && rd(addr, 0x1F, &cfg);
|
||||
ESP_LOGI(TAG, "REGS ok=%d chip=0x%02X err=0x%02X int_ctrl=0x%02X pwr=0x%02X mode=%u osr=0x%02X odr=0x%02X cfg=0x%02X",
|
||||
ok, chip, err, ictl, pwr, (pwr >> 4) & 3, osr, odr, cfg);
|
||||
}
|
||||
|
||||
inline void log_heap() {
|
||||
ESP_LOGI(TAG, "HEAP internal_free=%u internal_total=%u min_free=%u",
|
||||
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
||||
(unsigned) heap_caps_get_total_size(MALLOC_CAP_INTERNAL),
|
||||
(unsigned) heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
|
||||
}
|
||||
|
||||
// Probe the chip; on failure log the line levels, reset the IDF bus once and probe again.
|
||||
inline void probe(uint8_t addr, int scl) {
|
||||
uint8_t v = 0;
|
||||
bool ok = rd(addr, 0x00, &v);
|
||||
ESP_LOGI(TAG, "PROBE %s chip=0x%02X held=%d sel=0x%08X sda=%d scl=%d", ok ? "ok" : "err", v, g_held,
|
||||
(unsigned) out_sel_reg(), gpio_get_level((gpio_num_t) g_sda), gpio_get_level((gpio_num_t) scl));
|
||||
if (ok || !g_bus_reset_on_probe)
|
||||
return;
|
||||
i2c_master_bus_handle_t h;
|
||||
esp_err_t e = i2c_master_get_bus_handle((i2c_port_num_t) g_port, &h);
|
||||
if (e == ESP_OK)
|
||||
e = i2c_master_bus_reset(h);
|
||||
ok = rd(addr, 0x00, &v);
|
||||
ESP_LOGI(TAG, "PROBE after bus reset (%d): %s chip=0x%02X", e, ok ? "ok" : "err", v);
|
||||
}
|
||||
} // namespace bmpt
|
||||
@@ -1,208 +0,0 @@
|
||||
"""Write a BMP390 bench config (ESP32-S3, no Wi-Fi) next to this script.
|
||||
|
||||
usage: gen.py NAME [--rate 50Hz] [--iir 128x|off] [--interval 1s] [--fault --kinds NMTWLSHD --probe-delay MS]
|
||||
[--reset-at 40,80] [--open-line] [--bus-reset]
|
||||
Fault kinds, one per event at 30 s and then every 20 s: N next start write NACKs in software, M every start write
|
||||
NACKs for 5 s, T SDA held low for the next start write, W the same for every transfer for 5 s, L a 5 s continuous
|
||||
hold, S a 300 ms hold, H and D harness-only held transfers.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
|
||||
FAULT = """
|
||||
- interval: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
bmpt::fault_tick();
|
||||
static constexpr char KIND[] = "KINDS";
|
||||
static uint8_t i = 0;
|
||||
static uint32_t probe_at = 0;
|
||||
uint32_t now = millis();
|
||||
if (i < sizeof(KIND) - 1 && now >= 30000u + 20000u * i) { // events at 30 s, then every 20 s
|
||||
char k = KIND[i++];
|
||||
ESP_LOGI("bmpt", "FAULT %c at %u failed=%d warn=%d", k, (unsigned) now, id(bmp)->is_failed(),
|
||||
id(bmp)->status_has_warning());
|
||||
// S and L hold SDA at once; fault_tick() releases it at the deadline
|
||||
if (k == 'S') {
|
||||
bmpt::g_fb->hold_until = now + 300;
|
||||
bmpt::sda_hold();
|
||||
}
|
||||
if (k == 'W') bmpt::g_fb->window_until = now + 5000;
|
||||
if (k == 'L' || k == 'D') bmpt::g_fb->hold_until = now + 5000;
|
||||
if (k == 'L')
|
||||
bmpt::sda_hold();
|
||||
if (k == 'N') bmpt::g_fb->soft_arm = true;
|
||||
if (k == 'M') bmpt::g_fb->soft_until = now + 5000;
|
||||
if (k == 'H') {
|
||||
uint8_t reg = 0x00, v;
|
||||
bmpt::g_fb->arm_start = false;
|
||||
bmpt::sda_hold();
|
||||
bool hok = bmpt::g_real->write_readv(ADDR, ®, 1, &v, 1) == esphome::i2c::ERROR_OK;
|
||||
bmpt::sda_release();
|
||||
ESP_LOGI("bmpt", "HARNESS held transfer ok=%d", hok);
|
||||
}
|
||||
if (k == 'D') {
|
||||
uint8_t reg = 0x00, v;
|
||||
for (int n = 0; n < 2; n++)
|
||||
bmpt::g_fb->write_readv(ADDR, ®, 1, &v, 1);
|
||||
}
|
||||
if (k == 'T') bmpt::g_fb->arm_start = true;
|
||||
probe_at = now + PROBEDELAY;
|
||||
}
|
||||
if (probe_at != 0 && now >= probe_at) {
|
||||
probe_at = 0;
|
||||
bmpt::probe(ADDR, SCLPIN);
|
||||
}
|
||||
"""
|
||||
|
||||
RESET = """
|
||||
- interval: 1s
|
||||
then:
|
||||
- lambda: |-
|
||||
static constexpr uint32_t AT[] = {RESETAT};
|
||||
static uint8_t i = 0;
|
||||
if (i < sizeof(AT) / sizeof(AT[0]) && millis() >= AT[i] * 1000) {
|
||||
i++;
|
||||
bool ok = bmpt::wr(ADDR, 0x7E, 0xB6);
|
||||
delay(10);
|
||||
ESP_LOGI("bmpt", "SOFTRESET ok=%d", ok);
|
||||
bmpt::dump_regs(ADDR);
|
||||
ESP_LOGI("bmpt", "DRDY ok=%d", bmpt::wr(ADDR, 0x19, 0x42));
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("name")
|
||||
ap.add_argument("--addr", default="0x77")
|
||||
ap.add_argument("--rate")
|
||||
ap.add_argument("--iir", default="128x")
|
||||
ap.add_argument("--interval", default="1s")
|
||||
ap.add_argument("--posr", default="8x")
|
||||
ap.add_argument("--tosr", default="none")
|
||||
ap.add_argument("--fault", action="store_true")
|
||||
ap.add_argument("--kinds", default="WTSWTSWT")
|
||||
ap.add_argument("--probe-delay", default="7000")
|
||||
ap.add_argument(
|
||||
"--open-line",
|
||||
action="store_true",
|
||||
help="fault routes SDA away (floats high) instead of low",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--bus-reset",
|
||||
action="store_true",
|
||||
help="reset the IDF bus after a failed probe",
|
||||
)
|
||||
ap.add_argument("--sda", default="1")
|
||||
ap.add_argument("--scl", default="2")
|
||||
ap.add_argument("--int-pin", default="42")
|
||||
ap.add_argument(
|
||||
"--no-drdy", action="store_true", help="do not enable data ready on INT"
|
||||
)
|
||||
ap.add_argument(
|
||||
"--reset-at",
|
||||
default="",
|
||||
help="seconds, comma separated: soft reset the chip behind the driver",
|
||||
)
|
||||
a = ap.parse_args()
|
||||
|
||||
rate = f"\n data_rate: {a.rate}" if a.rate else ""
|
||||
# data ready on INT, push-pull, active high; the driver's soft reset in setup clears it
|
||||
drdy = (
|
||||
""
|
||||
if a.no_drdy
|
||||
else f' ESP_LOGI("bmpt", "DRDY ok=%d", bmpt::wr({a.addr}, 0x19, 0x42));\n'
|
||||
)
|
||||
extra = ""
|
||||
if a.fault:
|
||||
extra += (
|
||||
FAULT.replace("KINDS", a.kinds)
|
||||
.replace("PROBEDELAY", a.probe_delay)
|
||||
.replace("SCLPIN", a.scl)
|
||||
)
|
||||
if a.reset_at:
|
||||
extra += RESET.replace("RESETAT", a.reset_at)
|
||||
yaml = f"""esphome:
|
||||
name: {a.name}
|
||||
includes:
|
||||
- bmp390probe.hpp
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: |-
|
||||
bmpt::g_real = id(bus);
|
||||
bmpt::g_sda = {a.sda};
|
||||
bmpt::g_port = id(bus)->get_port();
|
||||
static bmpt::FaultBus fb(id(bus));
|
||||
id(bmp)->set_i2c_bus(&fb);
|
||||
bmpt::g_fb = &fb;
|
||||
bmpt::g_bus_reset_on_probe = {"true" if a.bus_reset else "false"};
|
||||
bmpt::g_hold_level = {1 if a.open_line else 0};
|
||||
ESP_LOGI("bmpt", "PRESETUP");
|
||||
bmpt::dump_regs({a.addr});
|
||||
- priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("bmpt", "POSTSETUP failed=%d", id(bmp)->is_failed());
|
||||
bmpt::dump_regs({a.addr});
|
||||
{drdy} bmpt::int_counter_begin({a.int_pin});
|
||||
bmpt::dump_regs({a.addr});
|
||||
bmpt::log_heap();
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
i2c:
|
||||
id: bus
|
||||
sda: GPIO{a.sda}
|
||||
scl: GPIO{a.scl}
|
||||
scan: true
|
||||
|
||||
sensor:
|
||||
- platform: bmp3xx_i2c
|
||||
id: bmp
|
||||
address: {a.addr}
|
||||
pressure:
|
||||
name: pressure
|
||||
oversampling: {a.posr}
|
||||
on_value:
|
||||
- lambda: 'ESP_LOGI("bmpt", "READ %u %.4f", (unsigned) millis(), x);'
|
||||
temperature:
|
||||
name: temperature
|
||||
oversampling: {a.tosr}
|
||||
iir_filter: "{a.iir}"{rate}
|
||||
update_interval: {a.interval}
|
||||
|
||||
interval:
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
static uint32_t last_edges = 0, last_ms = 0;
|
||||
uint32_t e = bmpt::g_int_edges, now = millis();
|
||||
ESP_LOGI("bmpt", "INT edges=%u in %u ms failed=%d warn=%d xfers=%u fails=%u starts=%u "
|
||||
"start_fails=%u normal_writes=%u",
|
||||
(unsigned) (e - last_edges), (unsigned) (now - last_ms), id(bmp)->is_failed(),
|
||||
id(bmp)->status_has_warning(), bmpt::g_fb->xfers, bmpt::g_fb->fails, bmpt::g_fb->start_writes,
|
||||
bmpt::g_fb->start_fails, bmpt::g_fb->normal_writes);
|
||||
last_edges = e;
|
||||
last_ms = now;
|
||||
if (!bmpt::g_held) bmpt::dump_regs({a.addr});
|
||||
bmpt::log_heap();
|
||||
{extra.replace("ADDR", a.addr)}"""
|
||||
out = HERE / f"{a.name}.yaml"
|
||||
out.write_text(yaml, encoding="utf-8")
|
||||
print(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh NAME SECONDS [TAG]: build NAME.yaml (written by gen.py) from TREE, flash the ESP32-S3, capture SECONDS from
|
||||
# a fresh boot into out-NAME/TAG.txt and print stats.py's summary.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); N=${1:?name}; S=${2:?seconds}; G=${3:-run1}
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
[ -f "$R/$N.yaml" ] || { echo "no $N.yaml, run gen.py first"; exit 1; }
|
||||
OUT=$R/out-$N; mkdir -p "$OUT"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$OUT/upload.log"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
$PY "$SC" "$P" "$S" "$OUT/$G.txt" --reset > /dev/null 2>&1
|
||||
[ -s "$OUT/$G.txt" ] || { echo "serial capture got nothing"; exit 1; }
|
||||
$PY "$R/stats.py" "$OUT/$G.txt"
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Pressure statistics from a bmp390 capture.
|
||||
|
||||
usage: stats.py CAPTURE [SKIP_S]
|
||||
Prints the read count and cadence, the noise (standard deviation, of consecutive differences and detrended over a
|
||||
21 sample window), identical consecutive readings, the INT data ready rate per 10 s window and counts of errors,
|
||||
warnings and harness lines. SKIP_S drops the readings before that many seconds after boot (IIR settling).
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = pathlib.Path(sys.argv[1]).read_bytes().decode(errors="replace").splitlines()
|
||||
skip = float(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
reads = [
|
||||
(int(m[1]), float(m[2]))
|
||||
for line in lines
|
||||
if (m := re.search(r"READ (\d+) ([\d.]+)", line))
|
||||
]
|
||||
r = [p for t, p in reads if t / 1000 >= skip]
|
||||
if len(r) > 2:
|
||||
d = [b - a for a, b in zip(r, r[1:], strict=False)]
|
||||
same = sum(1 for x in d if x == 0)
|
||||
print(
|
||||
f"reads={len(reads)} used={len(r)} mean={statistics.mean(r):.4f} sd={statistics.pstdev(r) * 100:.2f}Pa "
|
||||
f"sd_diff={statistics.pstdev(d) * 100:.2f}Pa identical_consecutive={same} min={min(r):.4f} max={max(r):.4f}"
|
||||
)
|
||||
ts = [t for t, _ in reads]
|
||||
if len(ts) > 2:
|
||||
gaps = [b - a for a, b in zip(ts, ts[1:], strict=False)]
|
||||
print(f"cadence mean={statistics.mean(gaps):.0f}ms max_gap={max(gaps)}ms")
|
||||
ints = [
|
||||
(int(m[1]), int(m[2]))
|
||||
for line in lines
|
||||
if (m := re.search(r"INT edges=(\d+) in (\d+) ms", line))
|
||||
]
|
||||
# the first window starts at boot, before data ready is on
|
||||
if rates := [e * 1000 / ms for e, ms in ints[1:] if ms > 9000]:
|
||||
print(f"INT rate Hz: {' '.join(f'{x:.2f}' for x in rates)}")
|
||||
for pat in (
|
||||
r"\[E\]",
|
||||
r"\[W\]",
|
||||
"XFERFAIL",
|
||||
"SOFTFAIL",
|
||||
"FAULT",
|
||||
"PROBE",
|
||||
"marked FAILED",
|
||||
"REGS",
|
||||
):
|
||||
print(f"{pat}: {sum(1 for line in lines if re.search(pat, line))}")
|
||||
if len(r) > 30:
|
||||
k = 10
|
||||
res = [
|
||||
r[i] - statistics.mean(r[i - k : i + k + 1]) for i in range(k, len(r) - k)
|
||||
]
|
||||
print(f"detrended sd (21 sample window)={statistics.pstdev(res) * 100:.3f}Pa")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,170 +0,0 @@
|
||||
# CC1101 sleep and wake checks T0 to T6 on an ESP32-S3 (SPI on GPIO12/11/13, CS GPIO10, GDO0 GPIO9), run 4 s after
|
||||
# boot; the result is the "DONE fails=N" line. Substitution: name.
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes: [cctest.hpp]
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- delay: 4s
|
||||
- script.execute: run_tests
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
logger:
|
||||
level: DEBUG
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
spi:
|
||||
clk_pin: GPIO12
|
||||
mosi_pin: GPIO11
|
||||
miso_pin: GPIO13
|
||||
cc1101:
|
||||
id: transceiver
|
||||
cs_pin: GPIO10
|
||||
gdo0_pin: GPIO9
|
||||
frequency: 433.92MHz
|
||||
modulation_type: GFSK
|
||||
symbol_rate: 4800
|
||||
packet_mode: true
|
||||
packet_length: 8
|
||||
crc_enable: true
|
||||
sync_mode: "16/16"
|
||||
output_power: 0
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: ESP_LOGI("cct", "RX packet %s rssi %.1f", format_hex(x).c_str(), rssi);
|
||||
script:
|
||||
- id: run_tests
|
||||
then:
|
||||
- lambda: |-
|
||||
using namespace cct;
|
||||
auto *c = id(transceiver);
|
||||
Snap base, s;
|
||||
int fails = 0;
|
||||
snap(c, base);
|
||||
ESP_LOGI("cct", "T0 baseline MARCSTATE=%02X FREQ=%02X%02X%02X TEST2=%02X TEST1=%02X TEST0=%02X so_high_awake=%d",
|
||||
base.marc, base.reg[0x0D], base.reg[0x0E], base.reg[0x0F], base.reg[0x2C], base.reg[0x2D],
|
||||
base.reg[0x2E], cs_low_so_high(c));
|
||||
hexpa("T0 baseline", base.pa);
|
||||
// the sleep checks below need a probe that sees an awake chip as awake
|
||||
if (cs_low_so_high(c)) {
|
||||
ESP_LOGW("cct", "T0 FAIL: SO reads high on an awake chip, the sleep checks cannot be trusted");
|
||||
fails++;
|
||||
}
|
||||
|
||||
// T1: mark PATABLE 1-7, sleep, wake by hand: lost entries prove the chip slept
|
||||
const uint8_t marks[8] = {base.pa[0], 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
|
||||
write_pa(c, marks);
|
||||
snap(c, s); hexpa("T1 marked", s.pa);
|
||||
c->power_down();
|
||||
bool so = cs_low_so_high(c);
|
||||
snap(c, s);
|
||||
hexpa("T1 after raw wake", s.pa);
|
||||
bool lost = s.pa[1] != 0x11;
|
||||
ESP_LOGI("cct", "T1 sleep proof: so_high_on_cs=%d pa_lost=%d MARCSTATE=%02X TEST0=%02X -> %s", so, lost, s.marc,
|
||||
s.reg[0x2E], (so && lost) ? "PASS" : "FAIL");
|
||||
if (!(so && lost)) fails++;
|
||||
// the component still thinks it sleeps
|
||||
c->wake_up();
|
||||
snap(c, s);
|
||||
int d = diff(base, s, "T1 restore");
|
||||
ESP_LOGI("cct", "T1 restore after wake_up: %d differences, MARCSTATE=%02X -> %s", d, s.marc, d == 0 ? "PASS" : "FAIL");
|
||||
if (d) fails++;
|
||||
c->begin_rx();
|
||||
|
||||
// T2: change settings while asleep, compare with the same change made awake
|
||||
Snap awake;
|
||||
c->set_frequency(433.42e6); c->set_output_power(5); c->set_symbol_rate(9600);
|
||||
snap(c, awake);
|
||||
c->set_frequency(433.92e6); c->set_output_power(0); c->set_symbol_rate(4800);
|
||||
c->power_down();
|
||||
c->set_frequency(433.42e6); c->set_output_power(5); c->set_symbol_rate(9600);
|
||||
bool so2 = cs_low_so_high(c); // wakes the chip by hand; component still asleep
|
||||
c->wake_up();
|
||||
c->begin_rx();
|
||||
snap(c, s);
|
||||
awake.marc = s.marc;
|
||||
d = diff(awake, s, "T2 asleep vs awake");
|
||||
ESP_LOGI("cct", "T2 settings changed asleep: so_high=%d %d differences FREQ=%02X%02X%02X MARCSTATE=%02X -> %s", so2, d,
|
||||
s.reg[0x0D], s.reg[0x0E], s.reg[0x0F], s.marc, (so2 && d == 0) ? "PASS" : "FAIL");
|
||||
if (!(so2 && d == 0)) fails++;
|
||||
c->set_frequency(433.92e6); c->set_output_power(0); c->set_symbol_rate(4800);
|
||||
c->begin_rx();
|
||||
snap(c, base);
|
||||
|
||||
// T3: send while asleep wakes the radio by itself
|
||||
c->power_down();
|
||||
uint8_t pkt[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
int err = (int) c->transmit_packet(pkt, sizeof(pkt));
|
||||
snap(c, s);
|
||||
d = diff(base, s, "T3 after send");
|
||||
ESP_LOGI("cct", "T3 send while asleep: err=%d MARCSTATE=%02X TXBYTES=%02X %d differences -> %s", err, s.marc,
|
||||
status(c, 0x3A), d, (err == 0 && d == 0) ? "PASS" : "FAIL");
|
||||
if (!(err == 0 && d == 0)) fails++;
|
||||
|
||||
// T4: begin_rx while asleep
|
||||
c->power_down();
|
||||
c->begin_rx();
|
||||
snap(c, s);
|
||||
d = diff(base, s, "T4 begin_rx");
|
||||
ESP_LOGI("cct", "T4 begin_rx while asleep: MARCSTATE=%02X %d differences -> %s", s.marc, d,
|
||||
(s.marc == 0x0D && d == 0) ? "PASS" : "FAIL");
|
||||
if (!(s.marc == 0x0D && d == 0)) fails++;
|
||||
|
||||
// T5: 200 cycles, each one proven asleep and restored
|
||||
int bad = 0, notslept = 0;
|
||||
uint32_t t0 = micros();
|
||||
for (int i = 0; i < 200; i++) {
|
||||
write_pa(c, marks);
|
||||
c->power_down();
|
||||
if (!cs_low_so_high(c)) notslept++;
|
||||
c->wake_up();
|
||||
c->begin_rx();
|
||||
snap(c, s);
|
||||
if (diff(base, s, "T5") != 0) bad++;
|
||||
if ((i & 31) == 0) App.feed_wdt();
|
||||
}
|
||||
ESP_LOGI("cct", "T5 200 cycles: not_slept=%d restore_fail=%d in %u ms heap=%u -> %s", notslept, bad,
|
||||
(unsigned) ((micros() - t0) / 1000), (unsigned) esp_get_free_heap_size(),
|
||||
(notslept == 0 && bad == 0) ? "PASS" : "FAIL");
|
||||
if (notslept || bad) fails++;
|
||||
// T6: each setter on its own while asleep: does it wake the chip, and in which state does it leave it
|
||||
struct Case { const char *name; int kind; };
|
||||
const Case cases[] = {{"set_frequency", 0}, {"set_channel", 1}, {"set_modulation_type", 2}, {"set_symbol_rate", 3},
|
||||
{"set_output_power", 4}, {"set_filter_bandwidth", 5}, {"set_fsk_deviation", 6}};
|
||||
for (auto &k : cases) {
|
||||
c->begin_rx();
|
||||
c->power_down();
|
||||
switch (k.kind) {
|
||||
case 0: c->set_frequency(433.92e6); break;
|
||||
case 1: c->set_channel(0); break;
|
||||
case 2: c->set_modulation_type(cc1101::Modulation::MODULATION_GFSK); break;
|
||||
case 3: c->set_symbol_rate(4800); break;
|
||||
case 4: c->set_output_power(0); break;
|
||||
case 5: c->set_filter_bandwidth(203e3); break;
|
||||
case 6: c->set_fsk_deviation(1.5e3); break;
|
||||
}
|
||||
bool asleep = cs_low_so_high(c);
|
||||
uint8_t marc = status(c, 0x35) & 0x1F;
|
||||
ESP_LOGI("cct", "T6 %-20s while asleep: chip %s, MARCSTATE=%02X", k.name, asleep ? "still asleep" : "AWAKE", marc);
|
||||
if (!asleep) fails++;
|
||||
c->wake_up();
|
||||
}
|
||||
c->begin_rx();
|
||||
uint8_t agc_rx = rd(c, 0x2B);
|
||||
c->set_idle();
|
||||
uint8_t agc_idle = rd(c, 0x2B);
|
||||
ESP_LOGI("cct", "AGCTEST in RX=%02X in IDLE=%02X", agc_rx, agc_idle);
|
||||
c->begin_rx();
|
||||
ESP_LOGI("cct", "DONE fails=%d", fails);
|
||||
button:
|
||||
- platform: template
|
||||
id: yaml_actions
|
||||
name: Yaml actions
|
||||
on_press:
|
||||
- cc1101.power_down: transceiver
|
||||
- cc1101.wake_up: transceiver
|
||||
- cc1101.power_down: transceiver
|
||||
- cc1101.send_packet: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]
|
||||
- lambda: ESP_LOGI("cct", "YAML actions done MARCSTATE=%02X", cct::status(id(transceiver), 0x35) & 0x1F);
|
||||
@@ -1,70 +0,0 @@
|
||||
// Register and PATABLE snapshots plus the CHIP_RDYn sleep check for the cc1101 sleep tests in cc1101-sleep.yaml.
|
||||
#pragma once
|
||||
#include "esphome/components/cc1101/cc1101.h"
|
||||
#include "driver/gpio.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace cct {
|
||||
using esphome::cc1101::CC1101Component;
|
||||
static constexpr gpio_num_t MISO = GPIO_NUM_13; // the SPI MISO pin in cc1101-sleep.yaml
|
||||
static constexpr uint8_t NREG = 0x2F; // IOCFG2..TEST0
|
||||
|
||||
struct Snap {
|
||||
uint8_t reg[NREG];
|
||||
uint8_t pa[8];
|
||||
uint8_t marc;
|
||||
};
|
||||
|
||||
// Chip ready flag: with CSn low, SO stays high until the crystal runs (SLEEP), low at once otherwise
|
||||
inline bool cs_low_so_high(CC1101Component *c) {
|
||||
c->enable();
|
||||
bool high = gpio_get_level(MISO);
|
||||
delayMicroseconds(300);
|
||||
c->disable();
|
||||
return high;
|
||||
}
|
||||
inline uint8_t rd(CC1101Component *c, uint8_t a) {
|
||||
c->enable();
|
||||
c->write_byte(a | 0x80);
|
||||
uint8_t v = c->read_byte();
|
||||
c->disable();
|
||||
return v;
|
||||
}
|
||||
inline uint8_t status(CC1101Component *c, uint8_t a) { return rd(c, a | 0x40); }
|
||||
inline void snap(CC1101Component *c, Snap &s) {
|
||||
for (uint8_t i = 0; i < NREG; i++)
|
||||
s.reg[i] = rd(c, i);
|
||||
c->enable();
|
||||
c->write_byte(0x3E | 0xC0);
|
||||
for (auto &b : s.pa)
|
||||
b = c->read_byte();
|
||||
c->disable();
|
||||
s.marc = status(c, 0x35) & 0x1F;
|
||||
}
|
||||
inline void write_pa(CC1101Component *c, const uint8_t *pa) {
|
||||
c->enable();
|
||||
c->write_byte(0x3E | 0x40);
|
||||
for (int i = 0; i < 8; i++)
|
||||
c->write_byte(pa[i]);
|
||||
c->disable();
|
||||
}
|
||||
inline int diff(const Snap &a, const Snap &b, const char *what) {
|
||||
int n = 0;
|
||||
for (uint8_t i = 0; i < NREG; i++)
|
||||
// FSCAL3..FSCAL1 hold calibration results, FSTEST and AGCTEST are chip owned test registers
|
||||
if (i != 0x23 && i != 0x24 && i != 0x25 && i != 0x29 && i != 0x2B && a.reg[i] != b.reg[i]) {
|
||||
ESP_LOGW("cct", "%s reg 0x%02X: %02X vs %02X", what, i, a.reg[i], b.reg[i]);
|
||||
n++;
|
||||
}
|
||||
for (int i = 0; i < 8; i++)
|
||||
if (a.pa[i] != b.pa[i]) {
|
||||
ESP_LOGW("cct", "%s pa[%d]: %02X vs %02X", what, i, a.pa[i], b.pa[i]);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
inline void hexpa(const char *tag, const uint8_t *pa) {
|
||||
ESP_LOGI("cct", "%s PA %02X %02X %02X %02X %02X %02X %02X %02X", tag, pa[0], pa[1], pa[2], pa[3], pa[4], pa[5], pa[6],
|
||||
pa[7]);
|
||||
}
|
||||
} // namespace cct
|
||||
@@ -1,15 +0,0 @@
|
||||
esphome:
|
||||
includes:
|
||||
- crctest.hpp
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- delay: 3s
|
||||
- lambda: |-
|
||||
// unqualified call, as a lambda user would write it; counted in the CRCRESULT totals
|
||||
crctest::run(crc32(0, reinterpret_cast<const uint8_t *>("123456789"), 9));
|
||||
|
||||
interval:
|
||||
- interval: 20s
|
||||
then:
|
||||
- lambda: crctest::run();
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
// crc32 vectors for esphome#22398, expected values from Python zlib.crc32
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
namespace crctest {
|
||||
inline int pass_n = 0, fail_n = 0;
|
||||
inline void check(const char *name, uint32_t got, uint32_t want) {
|
||||
if (got == want)
|
||||
pass_n++;
|
||||
else
|
||||
fail_n++;
|
||||
ESP_LOGI("crctest", "%s %s got=%08X want=%08X", got == want ? "PASS" : "FAIL", name, (unsigned) got, (unsigned) want);
|
||||
}
|
||||
// unqualified: a crc32() of "123456789" computed in the caller's lambda, or -1 to skip that check
|
||||
inline void run(int64_t unqualified = -1) {
|
||||
pass_n = fail_n = 0;
|
||||
const uint8_t *s = reinterpret_cast<const uint8_t *>("123456789");
|
||||
check("check-123456789", esphome::crc32(0, s, 9), 0xCBF43926u);
|
||||
check("empty-seed0", esphome::crc32(0, s, 0), 0u);
|
||||
check("empty-seed12345678", esphome::crc32(0x12345678u, s, 0), 0x12345678u);
|
||||
check("chained-4+5", esphome::crc32(esphome::crc32(0, s, 4), s + 4, 5), 0xCBF43926u);
|
||||
uint8_t all[256];
|
||||
for (int i = 0; i < 256; i++)
|
||||
all[i] = i;
|
||||
check("bytes-0-255", esphome::crc32(0, all, 256), 0x29058C73u);
|
||||
uint8_t ff[32];
|
||||
memset(ff, 0xFF, sizeof(ff));
|
||||
check("32x0xFF", esphome::crc32(0, ff, 32), 0xFF6CAB0Bu);
|
||||
uint8_t *pat = static_cast<uint8_t *>(malloc(4100 + 8192));
|
||||
if (pat == nullptr) {
|
||||
ESP_LOGE("crctest", "malloc failed");
|
||||
fail_n++;
|
||||
ESP_LOGI("crctest", "CRCRESULT pass=%d fail=%d", pass_n, fail_n);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 4100; i++)
|
||||
pat[i] = (i * 7 + 3) & 0xFF;
|
||||
static const uint32_t EXP_OFFSET[4] = {0xDF4C2BA9u, 0x5D4B54F6u, 0x3B885695u, 0xAE989C6Eu};
|
||||
for (int o = 0; o < 4; o++) {
|
||||
char n[24];
|
||||
snprintf(n, sizeof(n), "unaligned-off%d", o);
|
||||
check(n, esphome::crc32(0, pat + o, 4093), EXP_OFFSET[o]);
|
||||
}
|
||||
uint32_t c = 0;
|
||||
for (int i = 0; i < 300; i++)
|
||||
c = esphome::crc32(c, pat + i, 1);
|
||||
check("chained-1-byte-x300", c, 0xDE0E57CEu);
|
||||
c = 0;
|
||||
for (int i = 0; i < 4093; i += 97)
|
||||
c = esphome::crc32(c, pat + 1 + i, std::min(97, 4093 - i));
|
||||
check("chained-97-byte-unaligned", c, 0x5D4B54F6u);
|
||||
uint8_t *big = pat + 4100;
|
||||
for (int i = 0; i < 8192; i++)
|
||||
big[i] = (i * 13 + 5) & 0xFF;
|
||||
uint32_t best = 0xFFFFFFFF, r = 0;
|
||||
for (int k = 0; k < 5; k++) {
|
||||
const uint32_t t0 = (esphome::micros) ();
|
||||
r = esphome::crc32(0, big, 8192);
|
||||
const uint32_t dt = (esphome::micros) () - t0;
|
||||
if (dt < best)
|
||||
best = dt;
|
||||
}
|
||||
check("8KiB", r, 0xF6950930u);
|
||||
free(pat);
|
||||
ESP_LOGI("crctest", "CRCSPEED 8192 bytes in %u us = %.1f us/KiB", (unsigned) best, best / 8.0f);
|
||||
if (unqualified >= 0)
|
||||
check("unqualified", (uint32_t) unqualified, 0xCBF43926u);
|
||||
ESP_LOGI("crctest", "CRCRESULT pass=%d fail=%d", pass_n, fail_n);
|
||||
}
|
||||
} // namespace crctest
|
||||
@@ -1,20 +0,0 @@
|
||||
# Shared ESP8266 base for the lock load DUTs: reset reason at boot, no Wi-Fi sleep, web_server for the HTTP load.
|
||||
esphome:
|
||||
name: ${name}
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: ESP_LOGI("lock", "BOOT reason=%s info=%s", ESP.getResetReason().c_str(), ESP.getResetInfo().c_str());
|
||||
esp8266:
|
||||
board: ${board}
|
||||
logger:
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
power_save_mode: none
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
ota:
|
||||
- platform: esphome
|
||||
web_server:
|
||||
port: 80
|
||||
@@ -1,39 +0,0 @@
|
||||
# remote_transmitter and remote_receiver on the same pin (GPIO12), no jumper: one NEC frame every 250 ms, each one
|
||||
# checked by the receiver's edge interrupt. Substitutions: name, board.
|
||||
substitutions:
|
||||
board: d1_mini
|
||||
packages:
|
||||
base: !include base.yaml
|
||||
globals:
|
||||
- {id: sent, type: uint32_t, initial_value: "0"}
|
||||
- {id: rx_ok, type: uint32_t, initial_value: "0"}
|
||||
- {id: rx_bad, type: uint32_t, initial_value: "0"}
|
||||
remote_transmitter:
|
||||
id: irtx
|
||||
pin:
|
||||
number: GPIO12
|
||||
allow_other_uses: true
|
||||
carrier_duty_percent: 100%
|
||||
remote_receiver:
|
||||
pin:
|
||||
number: GPIO12
|
||||
allow_other_uses: true
|
||||
on_nec:
|
||||
then:
|
||||
- lambda: |-
|
||||
if (x.address == 0x1234 && x.command == (uint16_t) (0x7800 | (id(sent) & 0xFF))) id(rx_ok)++; else id(rx_bad)++;
|
||||
interval:
|
||||
- interval: 250ms
|
||||
then:
|
||||
# The receiver's setup leaves the shared pin as an input; drive it again before each frame
|
||||
- lambda: |-
|
||||
pinMode(12, OUTPUT);
|
||||
id(sent)++;
|
||||
- remote_transmitter.transmit_nec:
|
||||
address: 0x1234
|
||||
command: !lambda return (uint16_t) (0x7800 | (id(sent) & 0xFF));
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("lock", "ALIVE up=%u sent=%u rx_ok=%u rx_bad=%u heap=%u", (unsigned) (millis() / 1000),
|
||||
(unsigned) id(sent), (unsigned) id(rx_ok), (unsigned) id(rx_bad), (unsigned) ESP.getFreeHeap());
|
||||
@@ -1,43 +0,0 @@
|
||||
# DS18B20 on GPIO4 with a bus search every ${search_interval}. Set busy_control to "true" to replace the search with
|
||||
# the same busy time and no 1-Wire traffic (the control run). Substitutions: name, board, search_interval, busy_control.
|
||||
substitutions:
|
||||
board: d1_mini
|
||||
search_interval: 100ms
|
||||
busy_control: "false"
|
||||
packages:
|
||||
base: !include base.yaml
|
||||
one_wire:
|
||||
- platform: gpio
|
||||
pin: GPIO4
|
||||
id: owbus
|
||||
globals:
|
||||
- {id: searches, type: uint32_t, initial_value: "0"}
|
||||
- {id: search_bad, type: uint32_t, initial_value: "0"}
|
||||
- {id: reads_ok, type: uint32_t, initial_value: "0"}
|
||||
- {id: reads_nan, type: uint32_t, initial_value: "0"}
|
||||
sensor:
|
||||
- platform: dallas_temp
|
||||
one_wire_id: owbus
|
||||
id: t1
|
||||
name: Temp
|
||||
update_interval: 2s
|
||||
on_raw_value:
|
||||
- lambda: if (std::isnan(x)) id(reads_nan)++; else id(reads_ok)++;
|
||||
interval:
|
||||
- interval: ${search_interval}
|
||||
then:
|
||||
- lambda: |-
|
||||
if (${busy_control}) {
|
||||
delayMicroseconds(13000); // about one search with one sensor answering
|
||||
return;
|
||||
}
|
||||
id(owbus)->search();
|
||||
id(searches)++;
|
||||
if (id(owbus)->get_devices().size() != 1)
|
||||
id(search_bad)++;
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("lock", "ALIVE up=%u ok=%u nan=%u searches=%u search_bad=%u heap=%u t=%.2f",
|
||||
(unsigned) (millis() / 1000), (unsigned) id(reads_ok), (unsigned) id(reads_nan),
|
||||
(unsigned) id(searches), (unsigned) id(search_bad), (unsigned) ESP.getFreeHeap(), id(t1).state);
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh NAME SECONDS PPS: build NAME.yaml (next to this script, or a path) from TREE, flash it over serial, capture
|
||||
# the log for SECONDS plus 160 (boot, up to 120 s for the IP, the load) and run wifi_load.py at PPS packets per second
|
||||
# (0: no load). Prints the boot count, every reset reason and the last ALIVE line. Exit 0: a clean run (exactly one
|
||||
# BOOT line); 1: the run finished and saw resets; 2: setup failed or the run is invalid (no load, capture cut short,
|
||||
# or the sensor or IR loopback never worked).
|
||||
# env: TREE (checkout under test), PORT, SC (path to serial_capture.py), PY (checkout python)
|
||||
D=$(cd "$(dirname "$0")" && pwd); N=${1:?name}; T=${2:?seconds}; P=${3:?pps}
|
||||
W=${TREE:?set TREE}; PORT=${PORT:?set PORT}; SC=${SC:?set SC}; PY=${PY:-$W/venv/bin/python}
|
||||
Y=$N; [ -f "$Y" ] || Y=$D/$N.yaml; Y=$(cd "$(dirname "$Y")" && pwd)/$(basename "$Y"); N=$(basename "$Y" .yaml)
|
||||
O=$D/out-$N; mkdir -p "$O"
|
||||
pkill -f "[s]erial_capture.py $PORT "
|
||||
rm -f "$O/serial.txt" "$O/load.txt"
|
||||
(cd "$W" && PYTHONPATH=$W $PY -m esphome compile "$Y") > "$O/compile.log" 2>&1 || { echo "$N compile failed"; exit 2; }
|
||||
(cd "$W" && PYTHONPATH=$W $PY -m esphome upload "$Y" --device "$PORT") > "$O/upload.log" 2>&1 || { echo "$N upload failed"; exit 2; }
|
||||
$PY "$SC" "$PORT" $((T + 160)) "$O/serial.txt" --reset > "$O/capture.log" 2>&1 &
|
||||
CAP=$!
|
||||
for _ in $(seq 60); do
|
||||
IP=$(LC_ALL=C grep -a -o -E "IP Address: [0-9.]+" "$O/serial.txt" | tail -1 | cut -d' ' -f3)
|
||||
[ -n "$IP" ] && break; sleep 2
|
||||
done
|
||||
if [ "$P" -gt 0 ]; then
|
||||
[ -n "$IP" ] || { kill "$CAP"; echo "$N no IP (check the secrets file)"; exit 2; }
|
||||
python3 "$D/wifi_load.py" "$IP" "$T" "$P" > "$O/load.txt" 2>&1 || { echo "$N INVALID: load failed (see $O/load.txt)"; BAD=1; }
|
||||
else
|
||||
sleep "$T"
|
||||
fi
|
||||
wait "$CAP" || { echo "$N INVALID: capture failed (see $O/capture.log)"; BAD=1; }
|
||||
BOOTS=$(LC_ALL=C grep -a -c 'BOOT reason' "$O/serial.txt")
|
||||
echo "== $N pps=$P boots=$BOOTS"
|
||||
LC_ALL=C grep -a "BOOT reason" "$O/serial.txt" | cut -c1-150
|
||||
LC_ALL=C grep -a "ALIVE" "$O/serial.txt" | tail -1 | cut -c1-200
|
||||
tail -1 "$O/load.txt" 2>/dev/null
|
||||
ALIVE=$(LC_ALL=C grep -a "ALIVE" "$O/serial.txt" | tail -1)
|
||||
# the workload must have run: a sensor answering searches, or decoded IR frames
|
||||
case "$ALIVE" in
|
||||
*" ok=0 "*|*" rx_ok=0 "*|"") echo "$N INVALID: no ALIVE line or the workload did not run"; BAD=1 ;;
|
||||
esac
|
||||
# one-wire: the searches must find the sensor (busy_control runs no searches)
|
||||
S=$(echo "$ALIVE" | sed -n 's/.* searches=\([0-9]*\) search_bad=\([0-9]*\).*/\1 \2/p')
|
||||
if [ -n "$S" ]; then
|
||||
set -- $S
|
||||
[ "$1" -gt 0 ] && [ "$2" -ge "$1" ] && { echo "$N INVALID: no search found the sensor"; BAD=1; }
|
||||
fi
|
||||
# resets are a failure even when they also broke the load or the workload
|
||||
[ "$BOOTS" -gt 1 ] && exit 1
|
||||
[ -z "$BAD" ] && [ "$BOOTS" = 1 ] || exit 2
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Wi-Fi load on an ESP8266 under test: a fixed rate UDP flood to port 9 plus back to back web_server GETs.
|
||||
|
||||
usage: wifi_load.py HOST SECONDS PPS
|
||||
Prints the counts every minute and a final "done" line.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
host, secs, pps = sys.argv[1], float(sys.argv[2]), int(sys.argv[3])
|
||||
end = time.monotonic() + secs
|
||||
stats = {"udp": 0, "udp_err": 0, "http_ok": 0, "http_err": 0, "http_gap_s": 0}
|
||||
|
||||
|
||||
last_ok = [time.monotonic()]
|
||||
|
||||
|
||||
def http_load() -> None:
|
||||
while time.monotonic() < end:
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://{host}/", timeout=5) as r:
|
||||
r.read()
|
||||
stats["http_ok"] += 1
|
||||
now = time.monotonic()
|
||||
stats["http_gap_s"] = max(stats["http_gap_s"], round(now - last_ok[0]))
|
||||
last_ok[0] = now
|
||||
except Exception: # noqa: BLE001 any failure counts; the thread must not die
|
||||
stats["http_err"] += 1
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
threading.Thread(target=http_load, daemon=True).start()
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
payload = b"x" * 512
|
||||
nxt = last = time.monotonic()
|
||||
while (now := time.monotonic()) < end:
|
||||
try:
|
||||
s.sendto(payload, (host, 9))
|
||||
stats["udp"] += 1
|
||||
except OSError: # mostly ENOBUFS when the host outruns the network
|
||||
stats["udp_err"] += 1
|
||||
time.sleep(0.01)
|
||||
nxt += 1 / pps
|
||||
if (d := nxt - time.monotonic()) > 0:
|
||||
time.sleep(d)
|
||||
if now - last > 60:
|
||||
last = now
|
||||
print(time.strftime("%H:%M:%S"), stats, flush=True)
|
||||
stats["http_gap_s"] = max(stats["http_gap_s"], round(time.monotonic() - last_ok[0]))
|
||||
print("done", stats, flush=True)
|
||||
if (
|
||||
stats["http_ok"] == 0
|
||||
or stats["http_gap_s"] > 60 # the device stopped answering for over a minute
|
||||
or stats["http_err"] > stats["http_ok"]
|
||||
or stats["udp"] < secs * pps / 2
|
||||
):
|
||||
sys.exit("the load did not reach the device; the run is invalid")
|
||||
@@ -9,7 +9,6 @@ before the LinkChecks instead.
|
||||
import sys
|
||||
|
||||
import bk7231tools.serial.main as bk_main
|
||||
from lt_platform import with_platform
|
||||
from ltchiptool.__main__ import cli
|
||||
from ltchiptool.soc.bk72xx.flash import BK72XXFlash
|
||||
from serial import Serial
|
||||
@@ -48,5 +47,5 @@ def flash_connect(self) -> None:
|
||||
BK72XXFlash.flash_hw_reset = flash_hw_reset
|
||||
BK72XXFlash.flash_connect = flash_connect
|
||||
|
||||
sys.argv = ["ltchiptool", *with_platform(sys.argv[1:])]
|
||||
sys.argv = ["ltchiptool", *sys.argv[1:]]
|
||||
cli()
|
||||
@@ -11,7 +11,6 @@ Usage: python flash_rtl87xx_kit.py flash write -d <port> firmware.uf2
|
||||
import sys
|
||||
import time
|
||||
|
||||
from lt_platform import with_platform
|
||||
import serial
|
||||
|
||||
_first = [True]
|
||||
@@ -62,9 +61,7 @@ def boot_app(port: str) -> None:
|
||||
def main() -> int:
|
||||
rc = 1
|
||||
try:
|
||||
result = cli_entrypoint.main(
|
||||
args=with_platform(sys.argv[1:]), standalone_mode=False
|
||||
)
|
||||
result = cli_entrypoint.main(args=sys.argv[1:], standalone_mode=False)
|
||||
rc = int(result) if isinstance(result, int) else 0
|
||||
finally:
|
||||
port = port_argument(sys.argv)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#ifdef __XTENSA__
|
||||
#include "esp_cpu_utils.h"
|
||||
#include "esp_debug_helpers.h"
|
||||
#endif
|
||||
#include "esp_timer.h"
|
||||
// Counts every global operator new and, once armed, keeps the first ALLOC_CAP of them with five caller PCs
|
||||
// (callers on Xtensa only; RISC-V entries keep size and time). Aligned operator new is not counted.
|
||||
// See reference/recipes/heap_alloc.md.
|
||||
struct AllocEntry {
|
||||
uint32_t ms;
|
||||
uint32_t size; // ALLOC_MARKER for a marker: pc[0] and pc[1] then hold its two values
|
||||
uint32_t pc[5];
|
||||
};
|
||||
static constexpr uint32_t ALLOC_CAP = 1024;
|
||||
static constexpr uint32_t ALLOC_MARKER = UINT32_MAX;
|
||||
AllocEntry g_allocs[ALLOC_CAP];
|
||||
std::atomic<uint32_t> g_alloc_n{0};
|
||||
std::atomic<uint32_t> g_new_count{0};
|
||||
// seq_cst with g_alloc_busy: the dump stores false then loads busy, a recorder adds busy then loads this
|
||||
std::atomic<bool> g_alloc_armed{false};
|
||||
uint32_t g_alloc_armed_ms = 0; // when recording started, 0 if it never did
|
||||
// Recorders between claiming a slot and filling it; the dump waits for 0 after disarming.
|
||||
std::atomic<uint32_t> g_alloc_busy{0};
|
||||
|
||||
// Claims the next slot, or returns nullptr and disarms once the buffer is full.
|
||||
static inline AllocEntry *alloc_claim(uint32_t size) {
|
||||
uint32_t i = g_alloc_n.fetch_add(1);
|
||||
if (i >= ALLOC_CAP) {
|
||||
g_alloc_armed = false;
|
||||
return nullptr;
|
||||
}
|
||||
AllocEntry &e = g_allocs[i];
|
||||
e.ms = (uint32_t) (esp_timer_get_time() / 1000);
|
||||
e.size = size;
|
||||
return &e;
|
||||
}
|
||||
|
||||
void *operator new(size_t size) {
|
||||
g_new_count.fetch_add(1, std::memory_order_relaxed);
|
||||
g_alloc_busy.fetch_add(1); // before the armed check, so a dump that disarmed first sees it
|
||||
AllocEntry *e = g_alloc_armed ? alloc_claim(size) : nullptr;
|
||||
if (e != nullptr) {
|
||||
e->pc[0] = e->pc[1] = e->pc[2] = e->pc[3] = e->pc[4] = 0;
|
||||
#ifdef __XTENSA__
|
||||
esp_backtrace_frame_t f;
|
||||
esp_backtrace_get_start(&f.pc, &f.sp, &f.next_pc);
|
||||
// The start frame is this function; record the five callers above it.
|
||||
// Stop at the first frame that does not unwind; the slots after it stay 0.
|
||||
for (int k = 0; k < 5 && f.next_pc != 0 && esp_backtrace_get_next_frame(&f); k++)
|
||||
e->pc[k] = esp_cpu_process_stack_pc(f.pc);
|
||||
#endif
|
||||
}
|
||||
g_alloc_busy.fetch_sub(1);
|
||||
void *p = malloc(size ? size : 1);
|
||||
if (p == nullptr)
|
||||
abort();
|
||||
return p;
|
||||
}
|
||||
void *operator new[](size_t size) { return operator new(size); }
|
||||
void operator delete(void *p) noexcept { free(p); }
|
||||
void operator delete[](void *p) noexcept { free(p); }
|
||||
void operator delete(void *p, size_t) noexcept { free(p); }
|
||||
void operator delete[](void *p, size_t) noexcept { free(p); }
|
||||
|
||||
// Splits the dump into phases, for example on each change of an entity's state.
|
||||
static inline void alloc_marker(uint32_t a, uint32_t b) {
|
||||
g_alloc_busy.fetch_add(1);
|
||||
if (AllocEntry *e = g_alloc_armed ? alloc_claim(ALLOC_MARKER) : nullptr) {
|
||||
e->pc[0] = a;
|
||||
e->pc[1] = b;
|
||||
e->pc[2] = e->pc[3] = e->pc[4] = 0;
|
||||
}
|
||||
g_alloc_busy.fetch_sub(1);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
# Package for allocring.hpp: arms it ${alloc_arm_s} s after boot, reports that on a binary sensor and adds the
|
||||
# dump button. See heap_alloc.md.
|
||||
esphome:
|
||||
includes:
|
||||
- allocring.hpp
|
||||
on_boot:
|
||||
then:
|
||||
- delay: ${alloc_arm_s}s
|
||||
- lambda: |-
|
||||
g_alloc_armed_ms = millis();
|
||||
g_alloc_armed = true;
|
||||
ESP_LOGI("allocs", "ARMED total=%u", g_new_count.load());
|
||||
|
||||
binary_sensor:
|
||||
# Drivers wait for this before the first action, so no step runs before recording starts
|
||||
- platform: template
|
||||
name: Allocation recorder armed
|
||||
lambda: return g_alloc_armed.load();
|
||||
|
||||
interval:
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("allocs", "total=%u kept=%u heap=%u", g_new_count.load(), (unsigned) g_alloc_n.load(),
|
||||
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Dump allocations
|
||||
on_press:
|
||||
# Recording stops for good; reboot before the next run, or a second press returns the same entries.
|
||||
- lambda: |-
|
||||
g_alloc_armed = false;
|
||||
// let recorders on other tasks finish the entry they claimed
|
||||
for (int w = 0; w < 100 && g_alloc_busy.load() != 0; w++)
|
||||
delay(1);
|
||||
uint32_t claimed = g_alloc_n.load();
|
||||
uint32_t n = std::min<uint32_t>(claimed, ALLOC_CAP);
|
||||
// claimed > n means the buffer filled and recording stopped early
|
||||
// armed_ms=0: the recorder never ran; busy>0: an entry may still be half written;
|
||||
// buf is the buffer's address, which decode.py matches against the ELF
|
||||
ESP_LOGI("allocs", "DUMP n=%u claimed=%u total=%u armed_ms=%u busy=%u buf=%08x", n, claimed,
|
||||
g_new_count.load(), g_alloc_armed_ms, g_alloc_busy.load(), (unsigned) (uintptr_t) g_allocs);
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
const AllocEntry &e = g_allocs[i];
|
||||
ESP_LOGI("allocs", "E %u %u %u %08x %08x %08x %08x %08x", i, e.ms, e.size, e.pc[0], e.pc[1], e.pc[2],
|
||||
e.pc[3], e.pc[4]);
|
||||
if ((i & 15) == 15) {
|
||||
delay(20); // let the log drain
|
||||
App.feed_wdt(); // a full dump over a 115200 baud UART log outlasts the task watchdog
|
||||
}
|
||||
}
|
||||
ESP_LOGI("allocs", "DUMP END");
|
||||
@@ -1,108 +0,0 @@
|
||||
"""Move the device's first cover to the far end and back N times with a states-only client, then save the dump.
|
||||
|
||||
usage: cycles_cover.py HOST CYCLES OUT env: API_KEY (unset for a plaintext API)
|
||||
Waits for the recorder's armed binary sensor first. Exits 2 when a move did not reach its position (the
|
||||
cover never moved, so the dump proves nothing).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import (
|
||||
BinarySensorInfo,
|
||||
BinarySensorState,
|
||||
CoverInfo,
|
||||
CoverOperation,
|
||||
CoverState,
|
||||
)
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||
from apiconn import connect # noqa: E402
|
||||
from heap_alloc.dump import dump # noqa: E402
|
||||
|
||||
# After each move, so allocations that come once the cover has stopped land in the same cycle
|
||||
SETTLE_S = 8
|
||||
ARM_WAIT_S = 600
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
host, cycles, out = sys.argv[1], int(sys.argv[2]), sys.argv[3]
|
||||
with pathlib.Path(out).open("w", encoding="utf-8") as f:
|
||||
|
||||
def w(s: str) -> None:
|
||||
f.write(time.strftime("%H:%M:%S ") + s + "\n")
|
||||
f.flush()
|
||||
|
||||
c = await connect(host)
|
||||
ents, _ = await c.list_entities_services()
|
||||
cover = next(e for e in ents if isinstance(e, CoverInfo))
|
||||
armed_info = next(
|
||||
e
|
||||
for e in ents
|
||||
if isinstance(e, BinarySensorInfo)
|
||||
and e.object_id == "allocation_recorder_armed"
|
||||
)
|
||||
states: asyncio.Queue = asyncio.Queue()
|
||||
armed = asyncio.Event()
|
||||
|
||||
def on_state(s) -> None:
|
||||
if isinstance(s, CoverState) and s.key == cover.key:
|
||||
states.put_nowait(s)
|
||||
elif (
|
||||
isinstance(s, BinarySensorState) and s.key == armed_info.key and s.state
|
||||
):
|
||||
armed.set()
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
latest = await asyncio.wait_for(states.get(), 10)
|
||||
# moves made before recording starts would be missing from the dump
|
||||
await asyncio.wait_for(armed.wait(), ARM_WAIT_S)
|
||||
w("ARMED")
|
||||
|
||||
def drain() -> CoverState:
|
||||
"""The newest queued state, so no stale one is read as this command's result."""
|
||||
nonlocal latest
|
||||
while not states.empty():
|
||||
latest = states.get_nowait()
|
||||
return latest
|
||||
|
||||
async def move(pos: float) -> bool:
|
||||
drain()
|
||||
w(f"CMD {pos}")
|
||||
c.cover_command(cover.key, position=pos)
|
||||
moved = False
|
||||
deadline = time.monotonic() + 60
|
||||
while (left := deadline - time.monotonic()) > 0:
|
||||
try:
|
||||
s = await asyncio.wait_for(states.get(), left)
|
||||
except TimeoutError:
|
||||
break
|
||||
idle = s.current_operation == CoverOperation.IDLE
|
||||
moved |= not idle
|
||||
if moved and idle and abs(s.position - pos) < 0.01:
|
||||
w(f"IDLE pos={s.position:.3f}")
|
||||
return True
|
||||
w("TIMEOUT")
|
||||
return False
|
||||
|
||||
timeouts = 0
|
||||
# start toward the far end, so the first command always moves the cover
|
||||
order = (0.0, 1.0) if drain().position > 0.5 else (1.0, 0.0)
|
||||
for i in range(cycles):
|
||||
w(f"CYCLE {i + 1}")
|
||||
for pos in order:
|
||||
timeouts += not await move(pos)
|
||||
await asyncio.sleep(SETTLE_S)
|
||||
if timeouts:
|
||||
# before the dump, which exits on its own failure
|
||||
print(f"{timeouts} moves timed out", file=sys.stderr)
|
||||
await dump(c, w)
|
||||
await c.disconnect()
|
||||
if timeouts:
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,159 +0,0 @@
|
||||
"""Decode an allocring dump into callers: decode.py LOG ELF [ADDR2LINE]
|
||||
|
||||
Prints each entry with its caller frames as function and file:line, and markers (size 0xFFFFFFFF, ALLOC_MARKER) as phase
|
||||
separators.
|
||||
ADDR2LINE defaults to an Xtensa one found in ESPHome's ESP-IDF tools cache, ~/.platformio or ~/.espressif.
|
||||
Exits 2 when the dump is incomplete (missing header or entries, or the buffer filled during the run).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ENTRY = re.compile(r"\bE (\d+) (\d+) (\d+) ((?:[0-9a-f]{8} ?){5})")
|
||||
HEADER = re.compile(
|
||||
r"\bDUMP n=(\d+) claimed=(\d+) total=\d+ armed_ms=(\d+) busy=(\d+) buf=([0-9a-f]{8})"
|
||||
)
|
||||
MARKER = 0xFFFFFFFF
|
||||
XTENSA = 94 # ELF e_machine; the recorder walks frames only on Xtensa
|
||||
|
||||
|
||||
def tool_roots() -> list[Path]:
|
||||
roots = [Path.home() / ".platformio", Path.home() / ".espressif"]
|
||||
try:
|
||||
from esphome.espidf.framework import get_idf_tools_path
|
||||
except ImportError:
|
||||
return roots
|
||||
return [get_idf_tools_path(), *roots]
|
||||
|
||||
|
||||
def is_xtensa(elf: str) -> bool:
|
||||
return int.from_bytes(Path(elf).read_bytes()[18:20], "little") == XTENSA
|
||||
|
||||
|
||||
def find_addr2line() -> str:
|
||||
for root in tool_roots():
|
||||
for path in root.glob("**/bin/xtensa-esp*-elf-addr2line"):
|
||||
if path.is_file():
|
||||
return str(path)
|
||||
sys.exit("no addr2line found; pass it as the third argument")
|
||||
|
||||
|
||||
def short(path: str) -> str:
|
||||
for marker in ("esphome/components/", "esphome/core/", "esp-idf/", "include/c++/"):
|
||||
if marker in path:
|
||||
return marker + path.split(marker, 1)[1]
|
||||
return path
|
||||
|
||||
|
||||
def decode(
|
||||
addr2line: str, elf: str, pcs: list[str]
|
||||
) -> dict[str, list[tuple[str, str]]]:
|
||||
"""Every PC's frames (inlined ones too); -a prints each address before its frames."""
|
||||
out = subprocess.run(
|
||||
[addr2line, "-afiC", "-e", elf, *(f"0x{pc}" for pc in pcs)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.splitlines()
|
||||
names: dict[str, list[tuple[str, str]]] = {}
|
||||
frames: list[tuple[str, str]] = []
|
||||
i = 0
|
||||
while i < len(out):
|
||||
if out[i].startswith("0x"):
|
||||
frames = names.setdefault(f"{int(out[i], 16):08x}", [])
|
||||
i += 1
|
||||
else:
|
||||
frames.append((out[i], short(out[i + 1])))
|
||||
i += 2
|
||||
return names
|
||||
|
||||
|
||||
def last_dump(text: str) -> str:
|
||||
"""The newest dump in the file (a retried press or an appended file holds several); check() rejects it if cut off."""
|
||||
start = max((m.start() for m in HEADER.finditer(text)), default=len(text))
|
||||
return text[start:]
|
||||
|
||||
|
||||
def check(dump: str, entries: list, xtensa: bool) -> list[str]:
|
||||
"""Reasons the dump is incomplete, empty when it is whole."""
|
||||
if not (header := HEADER.search(dump)):
|
||||
return ["no DUMP header (log subscription started late?)"]
|
||||
n, claimed, armed_ms, busy = (int(header[i]) for i in range(1, 5))
|
||||
problems = []
|
||||
if not armed_ms:
|
||||
problems.append("the recorder was never armed (pressed before alloc_arm_s?)")
|
||||
if busy:
|
||||
problems.append(f"{busy} recorders were still writing an entry")
|
||||
indexes = [int(e[0]) for e in entries]
|
||||
if missing := sorted(set(range(n)) - set(indexes)):
|
||||
problems.append(f"{len(missing)} of {n} entries missing, first {missing[:10]}")
|
||||
if len(indexes) != len(set(indexes)):
|
||||
problems.append("duplicate entries")
|
||||
if stray := sorted(i for i in set(indexes) if i >= n):
|
||||
problems.append(f"entries past n={n}: {stray[:10]}")
|
||||
allocs = [pcs for _, _, size, pcs in entries if size != MARKER]
|
||||
if xtensa and allocs and all(pcs[0] == "00000000" for pcs in allocs):
|
||||
problems.append("no entry has a caller (frame walk failed?)")
|
||||
if claimed > n:
|
||||
problems.append(f"buffer filled: recording stopped after {n} entries")
|
||||
if "DUMP END" not in dump:
|
||||
problems.append("no DUMP END")
|
||||
return problems
|
||||
|
||||
|
||||
def main() -> None:
|
||||
log, elf = sys.argv[1], sys.argv[2]
|
||||
text = last_dump(Path(log).read_text(encoding="utf-8", errors="replace"))
|
||||
entries = [
|
||||
(idx, int(ms), int(size), pcs.split())
|
||||
for idx, ms, size, pcs in ENTRY.findall(text)
|
||||
]
|
||||
wanted = {
|
||||
pc
|
||||
for _, _, size, pcs in entries
|
||||
if size != MARKER
|
||||
for pc in pcs
|
||||
if pc != "00000000"
|
||||
}
|
||||
# Only Xtensa dumps carry callers; RISC-V ones still print sizes and times
|
||||
names = {}
|
||||
if wanted:
|
||||
addr2line = sys.argv[3] if len(sys.argv) > 3 else find_addr2line()
|
||||
names = decode(addr2line, elf, sorted(wanted))
|
||||
for idx, ms, size, pcs in entries:
|
||||
if size == MARKER:
|
||||
print(
|
||||
f"--- marker at {ms / 1000:.3f} s: {int(pcs[0], 16)} {int(pcs[1], 16)}"
|
||||
)
|
||||
continue
|
||||
print(f"#{idx} {ms / 1000:.3f} s {size} B")
|
||||
for pc in pcs:
|
||||
if pc == "00000000":
|
||||
break
|
||||
for func, loc in names[pc]:
|
||||
print(f" {func[:110]} {loc}")
|
||||
problems = check(text, entries, is_xtensa(elf))
|
||||
# addr2line names any address, so check the ELF is the build that made the dump
|
||||
if wanted and (header := HEADER.search(text)):
|
||||
nm = addr2line[: -len("addr2line")] + "nm"
|
||||
if not Path(nm).is_file():
|
||||
problems.append(f"cannot check the ELF: no {nm} next to addr2line")
|
||||
elif not re.search(
|
||||
rf"^0*{int(header[5], 16):x} [bBdD] g_allocs$",
|
||||
subprocess.run(
|
||||
[nm, elf], capture_output=True, text=True, check=True
|
||||
).stdout,
|
||||
re.MULTILINE,
|
||||
):
|
||||
problems.append(
|
||||
"the ELF is not the build that made this dump (g_allocs address differs)"
|
||||
)
|
||||
if problems:
|
||||
print("INCOMPLETE DUMP: " + "; ".join(problems), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Press the "Dump allocations" button from allocring.yaml and save the dump.
|
||||
|
||||
usage: dump.py HOST OUT env: API_KEY (unset for a plaintext API)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import contextlib
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
from aioesphomeapi import APIClient, ButtonInfo
|
||||
from aioesphomeapi.model import LogLevel
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||
from apiconn import connect, log_text # noqa: E402
|
||||
|
||||
HEADER = re.compile(r"\bDUMP n=(\d+) claimed=\d+ total=\d+ armed_ms=(\d+) busy=(\d+)")
|
||||
ENTRY = re.compile(r"\bE (\d+) \d+ \d+ ")
|
||||
|
||||
|
||||
async def dump(c: APIClient, write: Callable[[str], None], tries: int = 3) -> None:
|
||||
"""Subscribe to logs, press the dump button and pass every line to WRITE until DUMP END.
|
||||
|
||||
subscribe_logs does not confirm the subscription and the device drops log lines when busy, so the press is
|
||||
repeated (the dump is repeatable) until the header and all n entries arrived; exits 2 after TRIES presses.
|
||||
"""
|
||||
ents, _ = await c.list_entities_services()
|
||||
button = next(
|
||||
e
|
||||
for e in ents
|
||||
if isinstance(e, ButtonInfo) and e.object_id == "dump_allocations"
|
||||
)
|
||||
seen: dict = {"n": None, "idx": set()}
|
||||
done = asyncio.Event()
|
||||
|
||||
def on_log(msg) -> None:
|
||||
text = log_text(msg)
|
||||
write(text)
|
||||
if m := HEADER.search(text):
|
||||
seen["n"], seen["idx"] = int(m[1]), set()
|
||||
seen["armed"], seen["busy"] = int(m[2]) != 0, int(m[3])
|
||||
elif m := ENTRY.search(text):
|
||||
seen["idx"].add(int(m[1]))
|
||||
elif "DUMP END" in text and seen["n"] is not None:
|
||||
# without this press's header it is a late end from an earlier press
|
||||
done.set()
|
||||
|
||||
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_INFO)
|
||||
for _ in range(tries):
|
||||
await asyncio.sleep(2)
|
||||
seen["n"] = None
|
||||
done.clear()
|
||||
c.button_command(button.key)
|
||||
with contextlib.suppress(TimeoutError):
|
||||
await asyncio.wait_for(done.wait(), 120)
|
||||
# the device drops log lines when its send buffer is full, so require every entry
|
||||
if done.is_set() and seen["n"] is not None and not seen["armed"]:
|
||||
print(
|
||||
"the recorder was never armed (pressed before alloc_arm_s?)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
if (
|
||||
done.is_set()
|
||||
and seen["n"] is not None
|
||||
and not seen["busy"]
|
||||
and seen["idx"] == set(range(seen["n"]))
|
||||
):
|
||||
return
|
||||
print(f"no whole dump after {tries} presses", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = await connect(sys.argv[1])
|
||||
with pathlib.Path(sys.argv[2]).open("w", encoding="utf-8") as f:
|
||||
await dump(c, lambda s: f.write(s + "\n"))
|
||||
await c.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -70,9 +70,7 @@ def frame(msg_type: int, payload: bytes) -> bytes:
|
||||
return b"\x00" + varint(len(payload)) + varint(msg_type) + payload
|
||||
|
||||
|
||||
def f_bytes(num: int, data: bytes | str) -> bytes:
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
def f_bytes(num: int, data: bytes) -> bytes:
|
||||
return varint((num << 3) | 2) + varint(len(data)) + data
|
||||
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Press the device's buttons through Home Assistant and print the script and flag states after each step.
|
||||
|
||||
usage: drive.py TOKENFILE NAME env: HA_URL (default http://127.0.0.1:8129)
|
||||
NAME is the device name from hsw.yaml (button entity ids are button.<NAME>_long_on and so on).
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(
|
||||
0, str(pathlib.Path(__file__).resolve().parent.parent / "mqtt" / "ha_discovery")
|
||||
)
|
||||
from ha_rest import req_or_exit # noqa: E402
|
||||
|
||||
tok = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").strip()
|
||||
name = sys.argv[2].replace("-", "_") # entity ids use underscores
|
||||
t0 = time.monotonic()
|
||||
|
||||
|
||||
def state(entity: str) -> str:
|
||||
return req_or_exit(f"/api/states/{entity}", token=tok)["state"]
|
||||
|
||||
|
||||
def show(tag: str) -> None:
|
||||
print(
|
||||
f"{time.monotonic() - t0:6.1f}s {tag:30s} long_job={state('script.long_job'):4s}"
|
||||
f" quick_job={state('script.quick_job'):4s} flag={state('input_boolean.script_ran')}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
def call(domain: str, action: str, entity: str, wait: float = 3) -> None:
|
||||
req_or_exit(f"/api/services/{domain}/{action}", {"entity_id": entity}, token=tok)
|
||||
time.sleep(wait)
|
||||
|
||||
|
||||
show("start")
|
||||
call("button", "press", f"button.{name}_long_on")
|
||||
show("device Long on")
|
||||
call("button", "press", f"button.{name}_long_off")
|
||||
show("device Long off")
|
||||
call("input_boolean", "turn_off", "input_boolean.script_ran", 1)
|
||||
call("button", "press", f"button.{name}_long_on")
|
||||
show("device Long on again")
|
||||
time.sleep(20)
|
||||
show("script finished on its own")
|
||||
call("button", "press", f"button.{name}_quick_on")
|
||||
show("device Quick on")
|
||||
call("script", "turn_on", "script.long_job")
|
||||
show("HA starts the script")
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
# Throwaway Home Assistant on port 8129: a 20 s script that sets and clears a flag, and a toggle script.
|
||||
http:
|
||||
server_port: 8129
|
||||
api:
|
||||
config:
|
||||
onboarding:
|
||||
frontend:
|
||||
input_boolean:
|
||||
script_ran:
|
||||
script:
|
||||
long_job:
|
||||
alias: Long job
|
||||
mode: single
|
||||
sequence:
|
||||
- action: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.script_ran
|
||||
- delay: 20
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.script_ran
|
||||
quick_job:
|
||||
alias: Quick job
|
||||
sequence:
|
||||
- action: input_boolean.toggle
|
||||
target:
|
||||
entity_id: input_boolean.script_ran
|
||||
logger:
|
||||
default: warning
|
||||
logs:
|
||||
homeassistant.components.esphome: debug
|
||||
homeassistant.components.script: debug
|
||||
homeassistant.helpers.script: info
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Add a host build's API to a throwaway Home Assistant and allow it to call actions.
|
||||
|
||||
usage: haflow.py TOKENFILE env: HA_URL (default http://127.0.0.1:8129)
|
||||
Runs the esphome config flow against 127.0.0.1:6053, then turns on allow_service_calls in the options flow.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(
|
||||
0, str(pathlib.Path(__file__).resolve().parent.parent / "mqtt" / "ha_discovery")
|
||||
)
|
||||
from ha_rest import req_or_exit # noqa: E402
|
||||
|
||||
tok = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").strip()
|
||||
flow = "/api/config/config_entries/flow"
|
||||
f = req_or_exit(flow, {"handler": "esphome"}, token=tok)
|
||||
r = req_or_exit(
|
||||
f"{flow}/{f['flow_id']}", {"host": "127.0.0.1", "port": 6053}, token=tok
|
||||
)
|
||||
if r.get("type") == "form" and r.get("errors"):
|
||||
sys.exit(f"config flow step {r.get('step_id')} failed: {r['errors']}")
|
||||
if r.get("type") == "form": # confirmation step
|
||||
r = req_or_exit(f"{flow}/{r['flow_id']}", {}, token=tok)
|
||||
if r.get("type") != "create_entry":
|
||||
sys.exit(
|
||||
f"config flow ended with {r.get('type')} {r.get('errors') or r.get('reason')}"
|
||||
)
|
||||
opts = "/api/config/config_entries/options/flow"
|
||||
o = req_or_exit(opts, {"handler": r["result"]["entry_id"]}, token=tok)
|
||||
o = req_or_exit(f"{opts}/{o['flow_id']}", {"allow_service_calls": True}, token=tok)
|
||||
if o.get("type") != "create_entry":
|
||||
sys.exit(
|
||||
f"options flow ended with {o.get('type')} {o.get('errors') or o.get('reason')}"
|
||||
)
|
||||
print("added", r.get("title"), "with allow_service_calls")
|
||||
@@ -1,39 +0,0 @@
|
||||
# Host build mirroring two Home Assistant scripts and the flag they set. Substitution: name.
|
||||
esphome:
|
||||
name: ${name}
|
||||
host:
|
||||
logger:
|
||||
level: DEBUG
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
switch:
|
||||
- platform: homeassistant
|
||||
id: sw_long
|
||||
name: "Long job mirror"
|
||||
entity_id: script.long_job
|
||||
on_turn_on:
|
||||
- logger.log: "EV long ON"
|
||||
on_turn_off:
|
||||
- logger.log: "EV long OFF"
|
||||
- platform: homeassistant
|
||||
id: sw_quick
|
||||
name: "Quick job mirror"
|
||||
entity_id: script.quick_job
|
||||
- platform: homeassistant
|
||||
id: sw_bool
|
||||
name: "Ran flag mirror"
|
||||
entity_id: input_boolean.script_ran
|
||||
on_turn_on:
|
||||
- logger.log: "EV flag ON"
|
||||
on_turn_off:
|
||||
- logger.log: "EV flag OFF"
|
||||
button:
|
||||
- platform: template
|
||||
name: "Long on"
|
||||
on_press: [switch.turn_on: sw_long]
|
||||
- platform: template
|
||||
name: "Long off"
|
||||
on_press: [switch.turn_off: sw_long]
|
||||
- platform: template
|
||||
name: "Quick on"
|
||||
on_press: [switch.turn_on: sw_quick]
|
||||
@@ -1,59 +0,0 @@
|
||||
# One fixed entity_id entity and one wizard-linked entity (no entity_id) per homeassistant platform that a wizard
|
||||
# input can stand for, on top of the three buttons of ha-button-common.yaml, on an ESP8266 (the ramchk interval reads
|
||||
# the ESP8266 heap). run.sh adds esphome name and board, and ha-wiz-light.yaml unless NO_LIGHT is set.
|
||||
packages:
|
||||
buttons: !include ../homeassistant_button/ha-button-common.yaml
|
||||
|
||||
api:
|
||||
wizard:
|
||||
pages:
|
||||
- inputs:
|
||||
- entity: s_wiz
|
||||
- entity: sa_wiz
|
||||
- entity: b_wiz
|
||||
- entity: t_wiz
|
||||
- entity: n_wiz
|
||||
- entity: sw_wiz
|
||||
- entity: sel_wiz
|
||||
- entity: tx_wiz
|
||||
|
||||
# Free heap every 5 s, and the heap size, for static + heap used
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: |-
|
||||
uint32_t heap_size = 0x3FFFC000u - reinterpret_cast<uint32_t>(&_heap_start);
|
||||
ESP_LOGI("ramchk", "heap_size=%u free=%u max_block=%u", heap_size, ESP.getFreeHeap(), ESP.getMaxFreeBlockSize());
|
||||
|
||||
# homeassistant entities are internal by default; internal: false lets the API client list and drive them
|
||||
sensor:
|
||||
- {platform: homeassistant, id: s_fix, internal: false, name: S fix, entity_id: sensor.outside}
|
||||
- {platform: homeassistant, id: s_wiz, internal: false, name: S wiz}
|
||||
- {platform: homeassistant, id: sa_wiz, internal: false, name: SA wiz, attribute: temperature}
|
||||
|
||||
binary_sensor:
|
||||
- {platform: homeassistant, id: b_fix, internal: false, name: B fix, entity_id: binary_sensor.door}
|
||||
- {platform: homeassistant, id: b_wiz, internal: false, name: B wiz}
|
||||
|
||||
text_sensor:
|
||||
- {platform: homeassistant, id: t_fix, internal: false, name: T fix, entity_id: sensor.weather}
|
||||
- {platform: homeassistant, id: t_wiz, internal: false, name: T wiz}
|
||||
|
||||
number:
|
||||
- {platform: homeassistant, id: n_fix, internal: false, name: N fix, entity_id: input_number.target}
|
||||
- {platform: homeassistant, id: n_wiz, internal: false, name: N wiz}
|
||||
|
||||
switch:
|
||||
- {platform: homeassistant, id: sw_fix, internal: false, name: SW fix, entity_id: switch.porch}
|
||||
- {platform: homeassistant, id: sw_wiz, internal: false, name: SW wiz}
|
||||
|
||||
select:
|
||||
- {platform: homeassistant, id: sel_fix, internal: false, name: SEL fix, entity_id: input_select.mode}
|
||||
- {platform: homeassistant, id: sel_wiz, internal: false, name: SEL wiz}
|
||||
|
||||
text:
|
||||
- {platform: homeassistant, id: tx_fix, internal: false, name: TX fix, entity_id: input_text.note}
|
||||
- {platform: homeassistant, id: tx_wiz, internal: false, name: TX wiz}
|
||||
|
||||
light:
|
||||
- {platform: homeassistant, id: l_fix, internal: false, name: L fix, entity_id: light.kitchen}
|
||||
@@ -1,9 +0,0 @@
|
||||
# The wizard-linked light, which builds before #21908 cannot compile; run.sh leaves it out when NO_LIGHT is set.
|
||||
api:
|
||||
wizard:
|
||||
pages:
|
||||
- inputs:
|
||||
- entity: l_wiz
|
||||
|
||||
light:
|
||||
- {platform: homeassistant, id: l_wiz, internal: false, name: L wiz}
|
||||
@@ -1,495 +0,0 @@
|
||||
"""Drive every homeassistant platform over a plaintext native API connection, playing Home Assistant.
|
||||
|
||||
usage: hadrive.py HOST [--no-light] (exits 1 on any failed check)
|
||||
|
||||
Matches ha-all-common.yaml: one fixed entity_id entity and one wizard-linked entity per platform. The client
|
||||
subscribes to entity states, Home Assistant actions and Home Assistant state subscriptions, then:
|
||||
A sends Home Assistant states for the fixed entities (and for a wizard ID that is not set yet), commands every
|
||||
writer, and checks that the wizard-linked ones send nothing;
|
||||
B sets every wizard input (WizardInputSetRequest), records the subscriptions the device sends for it, sends
|
||||
states for the new IDs and commands the writers;
|
||||
C changes every wizard input to a second ID: old IDs must be ignored, new ones followed;
|
||||
D sends an invalid ID, which the device must reject.
|
||||
Prints canonical lines (SUB, STATE, ACTION, CHECK) so two builds can be diffed, and PASS or FAIL at the end.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
|
||||
sys.path.insert(
|
||||
0, str(pathlib.Path(__file__).resolve().parents[1] / "homeassistant_button")
|
||||
)
|
||||
from habdrive import ( # noqa: E402
|
||||
Client as BaseClient,
|
||||
f_bytes,
|
||||
f_fixed32,
|
||||
f_varint,
|
||||
fields,
|
||||
fnv1_hash,
|
||||
varint,
|
||||
)
|
||||
|
||||
|
||||
def f_float(num: int, value: float) -> bytes:
|
||||
return varint((num << 3) | 5) + struct.pack("<f", value)
|
||||
|
||||
|
||||
# ListEntities response type -> platform
|
||||
LIST_TYPES = {
|
||||
12: "binary_sensor",
|
||||
15: "light",
|
||||
16: "sensor",
|
||||
17: "switch",
|
||||
18: "text_sensor",
|
||||
49: "number",
|
||||
52: "select",
|
||||
61: "button",
|
||||
97: "text",
|
||||
}
|
||||
STATE_TYPES = {
|
||||
21: "bool",
|
||||
24: "bool",
|
||||
25: "float",
|
||||
26: "bool",
|
||||
27: "str",
|
||||
50: "float",
|
||||
53: "str",
|
||||
98: "str",
|
||||
}
|
||||
|
||||
|
||||
class Client(BaseClient):
|
||||
def __init__(self, host: str) -> None:
|
||||
super().__init__(host)
|
||||
self.keys: dict[str, int] = {}
|
||||
self.names: dict[int, str] = {}
|
||||
self.failures: list[str] = []
|
||||
self.last: dict[
|
||||
str, str
|
||||
] = {} # the last state the device published, by entity name
|
||||
|
||||
def login(
|
||||
self, name: str, connect_wait: float = 1, list_entities: bool = True
|
||||
) -> None:
|
||||
"""Hello (API 1.14) and Connect, then ListEntities into keys and names."""
|
||||
self.send(1, f_bytes(1, name) + f_varint(2, 1) + f_varint(3, 14))
|
||||
self.pump(10, until=2)
|
||||
self.send(3)
|
||||
self.pump(connect_wait)
|
||||
if not list_entities:
|
||||
return
|
||||
self.send(11)
|
||||
for msg_type, payload in self.pump(10, until=19):
|
||||
if msg_type in LIST_TYPES:
|
||||
d = dict(fields(payload))
|
||||
key, name = struct.unpack("<I", d[2])[0], d[3].decode()
|
||||
self.keys[name] = key
|
||||
self.names[key] = name
|
||||
|
||||
def report(
|
||||
self, frames: list[tuple[int, bytes]], label: str
|
||||
) -> dict[str, list[str]]:
|
||||
"""Print SUB, STATE and ACTION lines for frames and return them by kind."""
|
||||
out = self.parse(frames)
|
||||
for line in out["STATE"]:
|
||||
name, _, value = line.partition(" = ")
|
||||
self.last[name] = value
|
||||
for kind in ("SUB", "STATE", "ACTION"):
|
||||
for line in sorted(out[kind]) if kind == "SUB" else out[kind]:
|
||||
print(f" {label} {kind} {line}")
|
||||
return out
|
||||
|
||||
def collect(
|
||||
self,
|
||||
label: str,
|
||||
done: Callable[[dict[str, list[str]]], bool] | None = None,
|
||||
timeout: float = 6.0,
|
||||
settle: float = 0.5,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Read until done(parsed) holds (or for timeout without one), then settle more seconds for strays."""
|
||||
frames: list[tuple[int, bytes]] = []
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
frames += self.pump(0.2)
|
||||
if done is not None and done(self.parse(frames)):
|
||||
break
|
||||
frames += self.pump(settle)
|
||||
return self.report(frames, label)
|
||||
|
||||
def parse(self, frames: list[tuple[int, bytes]]) -> dict[str, list[str]]:
|
||||
out: dict[str, list[str]] = {"SUB": [], "STATE": [], "ACTION": []}
|
||||
for msg_type, payload in frames:
|
||||
f = fields(payload)
|
||||
d = dict(f)
|
||||
if msg_type == 39: # SubscribeHomeAssistantStateResponse
|
||||
attr = d.get(2, b"").decode() or "-"
|
||||
line = f"{d.get(1, b'').decode()} {attr}{' once' if d.get(3) else ''}"
|
||||
out["SUB"].append(line)
|
||||
elif msg_type in STATE_TYPES:
|
||||
key = struct.unpack("<I", d[1])[0]
|
||||
kind = STATE_TYPES[msg_type]
|
||||
if kind == "float":
|
||||
value = d.get(2)
|
||||
value = (
|
||||
f"{struct.unpack('<f', value)[0]:g}"
|
||||
if value is not None
|
||||
else "0"
|
||||
)
|
||||
elif kind == "bool":
|
||||
value = "ON" if d.get(2) else "OFF"
|
||||
else:
|
||||
value = repr(d.get(2, b"").decode())
|
||||
if msg_type not in (24, 26) and d.get(
|
||||
3
|
||||
): # field 3 of a light state is brightness
|
||||
value += " missing"
|
||||
out["STATE"].append(f"{self.names.get(key, key)} = {value}")
|
||||
elif msg_type == 35: # HomeassistantActionRequest
|
||||
service, data = "", []
|
||||
for num, value in f:
|
||||
if num == 1:
|
||||
service = value.decode()
|
||||
elif num in (2, 3):
|
||||
kv = dict(fields(value))
|
||||
data.append(
|
||||
f"{kv.get(1, b'').decode()}={kv.get(2, b'').decode()}"
|
||||
)
|
||||
out["ACTION"].append(f"{service} {' '.join(data)}")
|
||||
return out
|
||||
|
||||
def check(self, label: str, ok: bool, detail: str) -> None:
|
||||
print(f" CHECK {label}: {'ok' if ok else 'FAIL'} {detail}")
|
||||
if not ok:
|
||||
self.failures.append(f"{label}: {detail}")
|
||||
|
||||
def ha_state(self, entity_id: str, state: str, attribute: str = "") -> None:
|
||||
payload = f_bytes(1, entity_id) + f_bytes(2, state)
|
||||
if attribute:
|
||||
payload += f_bytes(3, attribute)
|
||||
self.send(40, payload)
|
||||
|
||||
def wizard(self, input_id: str, entity_id: str) -> None:
|
||||
self.send(158, f_fixed32(1, fnv1_hash(input_id)) + f_bytes(2, entity_id))
|
||||
|
||||
|
||||
def feed(c: Client, platform: str, eid: str, n: int) -> None:
|
||||
"""Send what Home Assistant sends for a platform; n varies the values between rounds."""
|
||||
if platform == "sensor":
|
||||
c.ha_state(eid, f"{20 + n}.5")
|
||||
elif platform == "sensor_attr":
|
||||
c.ha_state(eid, "heat") # the state itself must not reach an attribute sensor
|
||||
c.ha_state(eid, f"{30 + n}", "temperature")
|
||||
elif platform == "binary_sensor":
|
||||
c.ha_state(eid, "on" if n % 2 else "off")
|
||||
elif platform == "text_sensor":
|
||||
c.ha_state(eid, f"text{n}")
|
||||
elif platform == "number":
|
||||
c.ha_state(eid, "0", "min")
|
||||
c.ha_state(eid, "100", "max")
|
||||
c.ha_state(eid, "1", "step")
|
||||
c.ha_state(eid, f"{40 + n}")
|
||||
elif platform == "switch":
|
||||
c.ha_state(eid, "on" if n % 2 else "off")
|
||||
elif platform == "select":
|
||||
c.ha_state(eid, "['eco', 'comfort', 'away']", "options")
|
||||
c.ha_state(eid, ["eco", "comfort", "away"][n % 3])
|
||||
elif platform == "text":
|
||||
c.ha_state(eid, "0", "min")
|
||||
c.ha_state(eid, "100", "max")
|
||||
c.ha_state(eid, "text", "mode")
|
||||
c.ha_state(eid, f"note{n}")
|
||||
elif platform == "light":
|
||||
c.ha_state(eid, "['onoff']", "supported_color_modes")
|
||||
c.ha_state(eid, "onoff", "color_mode")
|
||||
c.ha_state(eid, "on" if n % 2 else "off")
|
||||
|
||||
|
||||
def expected_state(platform: str, n: int) -> str | None:
|
||||
return {
|
||||
"sensor": f"{20 + n}.5",
|
||||
"sensor_attr": f"{30 + n}",
|
||||
"binary_sensor": "ON" if n % 2 else "OFF",
|
||||
"text_sensor": repr(f"text{n}"),
|
||||
"number": f"{40 + n}",
|
||||
"switch": "ON" if n % 2 else "OFF",
|
||||
"select": repr(["eco", "comfort", "away"][n % 3]),
|
||||
"text": repr(f"note{n}"),
|
||||
"light": "ON" if n % 2 else "OFF",
|
||||
}.get(platform)
|
||||
|
||||
|
||||
class Row(NamedTuple):
|
||||
fix: str | None # name of the fixed entity_id entity
|
||||
fix_id: str | None
|
||||
wiz: str | None # name of the wizard-linked entity
|
||||
input_id: str | None # ESPHome id of its wizard input
|
||||
platform: str
|
||||
one: str | None # the first and second entity IDs the wizard input is set to
|
||||
two: str | None
|
||||
|
||||
|
||||
# fmt: off
|
||||
PLATFORMS = [
|
||||
Row("S fix", "sensor.outside", "S wiz", "s_wiz", "sensor", "sensor.wiz_one", "sensor.wiz_two"),
|
||||
Row(None, None, "SA wiz", "sa_wiz", "sensor_attr", "climate.wiz_one", "climate.wiz_two"),
|
||||
Row("B fix", "binary_sensor.door", "B wiz", "b_wiz", "binary_sensor", "binary_sensor.wiz_one",
|
||||
"binary_sensor.wiz_two"),
|
||||
Row("T fix", "sensor.weather", "T wiz", "t_wiz", "text_sensor", "sensor.wiz_text_one", "sensor.wiz_text_two"),
|
||||
Row("N fix", "input_number.target", "N wiz", "n_wiz", "number", "input_number.wiz_one", "number.wiz_two"),
|
||||
Row("SW fix", "switch.porch", "SW wiz", "sw_wiz", "switch", "input_boolean.wiz_one", "switch.wiz_two"),
|
||||
Row("SEL fix", "input_select.mode", "SEL wiz", "sel_wiz", "select", "input_select.wiz_one", "select.wiz_two"),
|
||||
Row("TX fix", "input_text.note", "TX wiz", "tx_wiz", "text", "input_text.wiz_one", "text.wiz_two"),
|
||||
Row("L fix", "light.kitchen", "L wiz", "l_wiz", "light", "light.wiz_one", "light.wiz_two"),
|
||||
]
|
||||
# fmt: on
|
||||
# the fixed buttons come from ha-button-common.yaml
|
||||
WIZARD_BUTTON = Row(
|
||||
None,
|
||||
None,
|
||||
"HA wizard",
|
||||
"hab_wiz",
|
||||
"button",
|
||||
"input_button.wiz_one",
|
||||
"button.wiz_two",
|
||||
)
|
||||
|
||||
# The subscriptions each platform sends for one entity ID
|
||||
SUBS = {
|
||||
"button": [],
|
||||
"sensor": ["{} -"],
|
||||
"sensor_attr": ["{} temperature"],
|
||||
"binary_sensor": ["{} -"],
|
||||
"text_sensor": ["{} -"],
|
||||
"number": ["{} -", "{} max once", "{} min once", "{} step once"],
|
||||
"switch": ["{} -"],
|
||||
"select": ["{} -", "{} options"],
|
||||
"text": ["{} -", "{} max once", "{} min once", "{} mode once"],
|
||||
"light": [
|
||||
"{} -",
|
||||
"{} brightness",
|
||||
"{} color_mode",
|
||||
"{} color_temp_kelvin",
|
||||
"{} max_color_temp_kelvin",
|
||||
"{} min_color_temp_kelvin",
|
||||
"{} rgb_color",
|
||||
"{} supported_color_modes",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def command(c: Client, name: str, platform: str, n: int) -> str | None:
|
||||
"""Command a writer entity; returns the action it must send for entity ID {}, or None for a reader."""
|
||||
key = c.keys[name]
|
||||
if platform == "number":
|
||||
c.send(51, f_fixed32(1, key) + f_float(2, 10 + n))
|
||||
return "{d}.set_value entity_id={e} value=" + f"{10 + n}"
|
||||
if platform == "switch":
|
||||
state = n % 2 == 0 # the opposite of what feed() reported
|
||||
c.send(33, f_fixed32(1, key) + f_varint(2, int(state)))
|
||||
return f"homeassistant.turn_{'on' if state else 'off'} entity_id={{e}}"
|
||||
if platform == "select":
|
||||
option = ["eco", "comfort", "away"][(n + 1) % 3]
|
||||
c.send(54, f_fixed32(1, key) + f_bytes(2, option))
|
||||
return "{d}.select_option entity_id={e} option=" + option
|
||||
if platform == "text":
|
||||
c.send(99, f_fixed32(1, key) + f_bytes(2, f"cmd{n}"))
|
||||
return "{d}.set_value entity_id={e} value=" + f"cmd{n}"
|
||||
if platform == "light":
|
||||
state = n % 2 == 0
|
||||
c.send(32, f_fixed32(1, key) + f_varint(2, 1) + f_varint(3, int(state)))
|
||||
return f"light.turn_{'on' if state else 'off'} entity_id={{e}}"
|
||||
if platform == "button":
|
||||
c.send(62, f_fixed32(1, key))
|
||||
return "{d}.press entity_id={e}"
|
||||
return None
|
||||
|
||||
|
||||
def action_for(template: str, eid: str) -> str:
|
||||
# number, select, text and button call the action of the entity's own domain (input_number.set_value, ...)
|
||||
if template.startswith("{d}"):
|
||||
return template.format(d=eid.split(".", maxsplit=1)[0], e=eid)
|
||||
return template.format(e=eid)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("host")
|
||||
parser.add_argument(
|
||||
"--no-light", action="store_true", help="the config has no wizard light"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
c = Client(args.host)
|
||||
c.login("hadrive", connect_wait=2)
|
||||
print("entities", len(c.keys), sorted(c.keys))
|
||||
# only the wizard light may be left out (builds before #21908 cannot compile it)
|
||||
rows: list[Row] = []
|
||||
for row in [*PLATFORMS, WIZARD_BUTTON]:
|
||||
if row.wiz and row.wiz not in c.keys:
|
||||
c.check(
|
||||
f"{row.wiz} listed", args.no_light and row.wiz == "L wiz", "missing"
|
||||
)
|
||||
row = row._replace(wiz=None, input_id=None)
|
||||
if row.fix or row.wiz:
|
||||
rows.append(row)
|
||||
wizard = [row for row in rows if row.wiz]
|
||||
c.send(20) # SubscribeStatesRequest
|
||||
c.report(c.pump(3), "INIT")
|
||||
c.send(34) # SubscribeHomeassistantServicesRequest
|
||||
c.pump(1)
|
||||
c.send(38) # SubscribeHomeAssistantStatesRequest
|
||||
expected = sorted(
|
||||
s.format(r.fix_id) for r in rows if r.fix for s in SUBS[r.platform]
|
||||
)
|
||||
subs = c.collect("A", lambda o: len(o["SUB"]) >= len(expected), 10, 2)["SUB"]
|
||||
c.check(
|
||||
"A subscriptions are the fixed entities only",
|
||||
sorted(subs) == expected,
|
||||
f"{len(subs)} subs",
|
||||
)
|
||||
|
||||
def drive(
|
||||
label: str, entries: list[tuple[str, str | None, str]], n: int, stale: list[str]
|
||||
) -> None:
|
||||
"""entries: (name, entity ID or None if unset, platform). stale: IDs the device must ignore."""
|
||||
for eid in stale:
|
||||
c.ha_state(eid, "999")
|
||||
c.ha_state(eid, "999", "temperature")
|
||||
prev = dict(c.last)
|
||||
for _, eid, platform in entries:
|
||||
if eid:
|
||||
feed(c, platform, eid, n)
|
||||
wants = {
|
||||
f"{name} = {expected_state(p, n)}"
|
||||
for name, eid, p in entries
|
||||
if eid and expected_state(p, n)
|
||||
}
|
||||
got = c.collect(f"{label}-feed", lambda o: wants <= set(o["STATE"]), 8, 1.5)
|
||||
states = got["STATE"]
|
||||
for name, eid, platform in entries:
|
||||
want = expected_state(platform, n)
|
||||
if want is None:
|
||||
continue
|
||||
mine = [s for s in states if s.startswith(f"{name} = ")]
|
||||
if eid:
|
||||
# A light publishes only a change, so a report of the state it already has publishes nothing
|
||||
same = platform == "light" and not mine and prev.get(name) == want
|
||||
c.check(
|
||||
f"{label} {name} follows {eid}",
|
||||
f"{name} = {want}" in mine or same,
|
||||
str(mine),
|
||||
)
|
||||
else:
|
||||
c.check(f"{label} {name} unset publishes nothing", not mine, str(mine))
|
||||
c.check(
|
||||
f"{label} stale IDs ignored",
|
||||
not any("999" in s or "= 'heat'" in s for s in states),
|
||||
"",
|
||||
)
|
||||
for name, eid, platform in entries:
|
||||
if platform in ("sensor", "sensor_attr", "binary_sensor", "text_sensor"):
|
||||
continue
|
||||
template = command(c, name, platform, n)
|
||||
if eid is None:
|
||||
got = c.collect(f"{label}-cmd {name}", None, 2)
|
||||
else:
|
||||
got = c.collect(f"{label}-cmd {name}", lambda o: bool(o["ACTION"]), 6)
|
||||
actions = got["ACTION"]
|
||||
if eid is None:
|
||||
c.check(
|
||||
f"{label} {name} unset sends nothing", not actions, str(actions)
|
||||
)
|
||||
continue
|
||||
want = action_for(template, eid)
|
||||
c.check(
|
||||
f"{label} {name} action", actions == [want], f"{actions} want {want}"
|
||||
)
|
||||
if platform == "light":
|
||||
c.ha_state(
|
||||
eid, "on" if n % 2 == 0 else "off"
|
||||
) # Home Assistant echoes the change
|
||||
c.collect(f"{label}-echo {name}", None, 1.5)
|
||||
|
||||
fixed = [(r.fix, r.fix_id, r.platform) for r in rows if r.fix]
|
||||
fixed += [
|
||||
("HA button", "button.doorbell", "button"),
|
||||
("HA input", "input_button.chime", "button"),
|
||||
]
|
||||
wiz = [(r.wiz, None, r.platform) for r in wizard]
|
||||
print("== A: fixed entities, wizard inputs not set")
|
||||
drive("A", fixed + wiz, 1, ["sensor.wiz_one", "climate.wiz_one"])
|
||||
|
||||
def set_all(label: str, idx: int) -> dict[str, str]:
|
||||
ids = {}
|
||||
for row in wizard:
|
||||
name = row.wiz
|
||||
eid = row.one if idx == 1 else row.two
|
||||
ids[name] = eid
|
||||
c.wizard(row.input_id, eid)
|
||||
want = sorted(s.format(eid) for s in SUBS[row.platform])
|
||||
n_want = len(want)
|
||||
if want:
|
||||
subs = c.collect(
|
||||
f"{label}-set {name}",
|
||||
lambda o, n=n_want: len(o["SUB"]) >= n,
|
||||
6,
|
||||
0.5,
|
||||
)["SUB"]
|
||||
else:
|
||||
subs = c.collect(f"{label}-set {name}", None, 2)["SUB"]
|
||||
c.check(
|
||||
f"{label} {name} subscribes to {eid}",
|
||||
sorted(subs) == want,
|
||||
f"{sorted(subs)}",
|
||||
)
|
||||
return ids
|
||||
|
||||
print("== B: wizard inputs set to the first IDs")
|
||||
ids = set_all("B", 1)
|
||||
drive("B", [(name, ids[name], p) for name, _, p in wiz], 2, [])
|
||||
print("== C: wizard inputs changed to the second IDs")
|
||||
old = [row.one for row in wizard]
|
||||
ids = set_all("C", 2)
|
||||
drive("C", [(name, ids[name], p) for name, _, p in wiz], 3, old)
|
||||
print("== D: invalid ID rejected")
|
||||
for input_id in ("n_wiz", "hab_wiz", "s_wiz"):
|
||||
c.wizard(input_id, "not_an_entity")
|
||||
subs = c.collect(f"D-set {input_id}", None, 2)["SUB"]
|
||||
c.check(f"D {input_id} invalid set sends no subscription", not subs, str(subs))
|
||||
drive(
|
||||
"D",
|
||||
[
|
||||
(name, ids[name], p)
|
||||
for name, _, p in wiz
|
||||
if name in ("N wiz", "HA wizard", "S wiz")
|
||||
],
|
||||
4,
|
||||
[],
|
||||
)
|
||||
print("== E: a new connection gets every subscription with the current IDs")
|
||||
c.sock.close()
|
||||
c2 = Client(parser.parse_args().host)
|
||||
c2.login("hadrive2", list_entities=False)
|
||||
c2.send(38)
|
||||
expected = sorted(
|
||||
[s.format(r.fix_id) for r in rows if r.fix for s in SUBS[r.platform]]
|
||||
+ [s.format(ids[r.wiz]) for r in wizard for s in SUBS[r.platform]]
|
||||
)
|
||||
subs = c2.collect("E", lambda o: len(o["SUB"]) >= len(expected), 10, 2)["SUB"]
|
||||
c.check(
|
||||
"E subscriptions on reconnect",
|
||||
sorted(subs) == expected,
|
||||
f"{len(subs)} subs, want {len(expected)}",
|
||||
)
|
||||
if c.failures:
|
||||
print("FAIL: " + "; ".join(c.failures))
|
||||
sys.exit(1)
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,32 +0,0 @@
|
||||
"""After hadrive.py: the wizard light must follow its current entity ID (light.wiz_two) and ignore the old one.
|
||||
|
||||
usage: lightcheck.py HOST
|
||||
Sends on and off states for both IDs and prints what the device publishes after each; only light.wiz_two may move
|
||||
"L wiz". A light publishes only changes, so a repeated state shows nothing.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from hadrive import Client
|
||||
|
||||
|
||||
def main() -> None:
|
||||
c = Client(sys.argv[1])
|
||||
c.login("lightcheck")
|
||||
c.send(20) # SubscribeStatesRequest
|
||||
c.collect("init", None, 3)
|
||||
c.send(38) # SubscribeHomeAssistantStatesRequest
|
||||
c.collect("subs", None, 4)
|
||||
for eid, st in (
|
||||
("light.wiz_one", "on"),
|
||||
("light.wiz_two", "on"),
|
||||
("light.wiz_one", "off"),
|
||||
("light.wiz_two", "off"),
|
||||
):
|
||||
print(f"send {eid} = {st}")
|
||||
c.ha_state(eid, st)
|
||||
c.collect(" ->", None, 2.5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh LABEL: build ha-all-common.yaml from TREE as hawiz<LABEL> for a D1 mini, flash, take three idle boots of heap
|
||||
# readings (ramchk lines), then drive one boot with hadrive.py and print its result and the heap after the client.
|
||||
# NO_LIGHT=1 drops the wizard-linked light, which builds before #21908 cannot compile.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; N=hawiz$L
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
OUT=$R/out-$N; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
LIGHT=' light: !include ha-wiz-light.yaml\n'; NL=(); [ -n "$NO_LIGHT" ] && { LIGHT=; NL=(--no-light); }
|
||||
printf "esphome:\n name: %s\n\nesp8266:\n board: d1_mini\n\npackages:\n common: !include ha-all-common.yaml\n$LIGHT" "$N" \
|
||||
> "$R/$N.yaml"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$OUT/upload.log"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
for b in 1 2 3; do
|
||||
$PY "$SC" "$P" 45 "$OUT/boot$b.txt" --reset > /dev/null 2>&1
|
||||
echo "boot$b $(grep -a 'ramchk' "$OUT/boot$b.txt" | tail -1 | sed -E 's/.*(heap_size)/\1/')"
|
||||
done
|
||||
$PY "$SC" "$P" 150 "$OUT/serial.txt" --reset > /dev/null 2>&1 & CAP=$!
|
||||
for _ in $(seq 1 30); do
|
||||
IP=$(grep -a -o "IP Address: [0-9.]*" "$OUT/serial.txt" 2>/dev/null | tail -1 | cut -d' ' -f3)
|
||||
[ -n "$IP" ] && break; sleep 2
|
||||
done
|
||||
[ -n "$IP" ] || { echo "no IP in the capture"; kill $CAP; exit 1; }
|
||||
sleep 8
|
||||
$PY "$R/hadrive.py" "$IP" "${NL[@]}" > "$OUT/drive.txt" 2>&1; RC=$?
|
||||
sleep 6; kill $CAP
|
||||
echo "drive rc=$RC $(tail -1 "$OUT/drive.txt" | cut -c1-300)"
|
||||
echo "after-client $(grep -a 'ramchk' "$OUT/serial.txt" | tail -1 | sed -E 's/.*(heap_size)/\1/')"
|
||||
echo "crash lines: $(grep -a -c -E 'rst cause|Exception|Soft WDT' "$OUT/serial.txt")"
|
||||
exit $RC
|
||||
@@ -1,16 +0,0 @@
|
||||
# One run: p_fault is per mille of transactions, seed fixes the fault sequence (seed 1 hangs dev at about 245 s on #22270)
|
||||
substitutions:
|
||||
name: i2cfault-pr
|
||||
freq: 10kHz
|
||||
hammer: 100ms
|
||||
ina_interval: 1s
|
||||
p_fault: "50"
|
||||
fault_mask: "0xFF"
|
||||
hold_max_ms: "3000"
|
||||
diag_recover: "false"
|
||||
seed: "1"
|
||||
fw: esp-idf
|
||||
fwver: 5.5.5
|
||||
|
||||
packages:
|
||||
base: !include i2cbase.yaml
|
||||
@@ -1,132 +0,0 @@
|
||||
# Package: reporter-shaped I2C bus + INA219 on GPIO32/33, answered by the in-chip software INA219 (inasim.hpp)
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- inasim.hpp
|
||||
on_boot:
|
||||
- priority: 700
|
||||
then:
|
||||
- lambda: |-
|
||||
if (${seed} != 0) inasim::rng = ${seed};
|
||||
inasim::install();
|
||||
uint8_t w[3] = {0x05, 0x12, 0x34};
|
||||
uint8_t r = 0x05;
|
||||
for (int pass = 1; pass <= 2; pass++) {
|
||||
uint8_t b[2] = {0, 0};
|
||||
auto ew = id(bus_a)->write_readv(0x40, w, 3, nullptr, 0);
|
||||
auto er = id(bus_a)->write_readv(0x40, &r, 1, b, 2);
|
||||
ESP_LOGI("inasim", "selftest%d write=%d read=%d val=%02X%02X starts=%u", pass, ew, er, b[0], b[1],
|
||||
(unsigned) inasim::st.starts);
|
||||
}
|
||||
|
||||
esp32:
|
||||
board: pico32
|
||||
framework:
|
||||
type: ${fw}
|
||||
version: ${fwver}
|
||||
|
||||
logger:
|
||||
level: INFO
|
||||
baud_rate: 115200
|
||||
|
||||
i2c:
|
||||
id: bus_a
|
||||
sda: GPIO32
|
||||
scl: GPIO33
|
||||
frequency: ${freq}
|
||||
scan: true
|
||||
|
||||
globals:
|
||||
- id: n_ok
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_to
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_nack
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_other
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_bad
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_ina_bad
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: max_us
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: streak
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: max_streak
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
|
||||
interval:
|
||||
- interval: ${hammer}
|
||||
then:
|
||||
- lambda: |-
|
||||
if (millis() < 20000) return;
|
||||
if (!inasim::armed) {
|
||||
inasim::p_fault = ${p_fault};
|
||||
inasim::fault_mask = ${fault_mask};
|
||||
inasim::hold_max_ms = ${hold_max_ms};
|
||||
// The ISR picks faults until one is in the mask, so an empty mask would spin there forever
|
||||
if (inasim::p_fault && !(inasim::fault_mask & ((1u << inasim::F_COUNT) - 1))) {
|
||||
ESP_LOGE("inasim", "fault_mask 0x%x selects no fault, running without faults", (unsigned) inasim::fault_mask);
|
||||
inasim::p_fault = 0;
|
||||
}
|
||||
inasim::armed = true;
|
||||
ESP_LOGI("inasim", "faults armed p=%u/1000 mask=0x%x", (unsigned) inasim::p_fault, (unsigned) inasim::fault_mask);
|
||||
}
|
||||
inasim::hb = millis();
|
||||
inasim::diag_recover = ${diag_recover};
|
||||
inasim::drain();
|
||||
uint8_t r = 0x02, buf[2] = {0, 0};
|
||||
uint32_t t0 = micros();
|
||||
auto e = id(bus_a)->write_readv(0x40, &r, 1, buf, 2);
|
||||
uint32_t dt = micros() - t0;
|
||||
if (dt > id(max_us)) id(max_us) = dt;
|
||||
if (e == i2c::ERROR_OK) {
|
||||
id(streak) = 0;
|
||||
id(n_ok)++;
|
||||
if (((buf[0] << 8) | buf[1]) != ((3410 << 3) | 2)) id(n_bad)++;
|
||||
} else {
|
||||
id(streak)++;
|
||||
if (id(streak) > id(max_streak)) id(max_streak) = id(streak);
|
||||
if (e == i2c::ERROR_TIMEOUT) id(n_to)++;
|
||||
else if (e == i2c::ERROR_NOT_ACKNOWLEDGED) id(n_nack)++;
|
||||
else id(n_other)++;
|
||||
}
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("inasim", "MASTER up=%us ok=%u timeout=%u nack=%u other=%u badval=%u ina_bad=%u max_us=%u heap=%u fail_streak_now=%u max_fail_streak=%u",
|
||||
(unsigned) (millis() / 1000), (unsigned) id(n_ok), (unsigned) id(n_to), (unsigned) id(n_nack),
|
||||
(unsigned) id(n_other), (unsigned) id(n_bad), (unsigned) id(n_ina_bad), (unsigned) id(max_us),
|
||||
(unsigned) esp_get_free_heap_size(), (unsigned) id(streak), (unsigned) id(max_streak));
|
||||
id(max_us) = 0;
|
||||
inasim::log_stats();
|
||||
|
||||
sensor:
|
||||
- platform: ina219
|
||||
address: 0x40
|
||||
shunt_resistance: 0.1 ohm
|
||||
max_voltage: 32.0V
|
||||
max_current: 3.2A
|
||||
update_interval: ${ina_interval}
|
||||
bus_voltage:
|
||||
name: Starter battery
|
||||
accuracy_decimals: 2
|
||||
on_value:
|
||||
- lambda: |-
|
||||
if (fabsf(x - 13.64f) > 0.005f) {
|
||||
id(n_ina_bad)++;
|
||||
ESP_LOGW("inasim", "INA bus voltage garbage %.2f V", x);
|
||||
}
|
||||
current:
|
||||
name: Charge current
|
||||
accuracy_decimals: 2
|
||||
@@ -1,435 +0,0 @@
|
||||
#pragma once
|
||||
// Software INA219 at 0x40 answered on the master's own pads (GPIO32 SDA, GPIO33 SCL) plus fault injection.
|
||||
// The slave drives a line low by switching the pad's output mux from the I2C0 signal to the plain GPIO output
|
||||
// (level 0), which is a wired AND with the master. It runs inside a GPIO ISR on core 0 that spins through one
|
||||
// transaction. Long stuck lines are held by a task on core 0 that re-asserts every tick.
|
||||
#include <cstdint>
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soc/gpio_reg.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "esp_cpu.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
namespace inasim {
|
||||
static const char *const TAG = "inasim";
|
||||
constexpr int SDA = 32, SCL = 33;
|
||||
constexpr uint32_t SDA_BIT = 1u << (SDA - 32), SCL_BIT = 1u << (SCL - 32);
|
||||
constexpr uint32_t LOW_VAL = SIG_GPIO_OUT_IDX | GPIO_FUNC0_OEN_SEL;
|
||||
constexpr uint32_t CPU_MHZ = 240;
|
||||
|
||||
inline volatile uint32_t ®(uint32_t a) { return *reinterpret_cast<volatile uint32_t *>(a); }
|
||||
inline uint32_t out_sel_addr(int pin) { return GPIO_FUNC0_OUT_SEL_CFG_REG + 4 * pin; }
|
||||
inline uint32_t in() { return reg(GPIO_IN1_REG); }
|
||||
|
||||
enum Fault : uint8_t {
|
||||
F_GARBAGE = 0, // flip a bit in returned data
|
||||
F_NACK, // do not ack the address
|
||||
F_STRETCH, // hold SCL low 0.2..20 ms inside the transaction, then continue
|
||||
F_HOLD_SCL, // SCL stuck low 50..3000 ms (task)
|
||||
F_HOLD_SDA, // SDA stuck low 50..3000 ms (task)
|
||||
F_GLITCH_SCL, // 1..4 us low pulse on SCL while it is high
|
||||
F_GLITCH_SDA, // 1..4 us low pulse on SDA while SCL is high (false START/STOP)
|
||||
F_VANISH, // slave stops responding mid transaction
|
||||
F_COUNT
|
||||
};
|
||||
|
||||
struct Stats {
|
||||
volatile uint32_t starts, matched, stops, idle_abort, cap_abort, rd_bytes, wr_bytes;
|
||||
volatile uint32_t fault[F_COUNT];
|
||||
};
|
||||
inline Stats st{};
|
||||
inline uint32_t rel_sda = 0, rel_scl = 0;
|
||||
inline volatile bool armed = false;
|
||||
inline volatile uint32_t p_fault = 0; // per mille of matched transactions
|
||||
inline volatile uint32_t fault_mask = 0xFF; // which faults may be chosen
|
||||
inline volatile uint32_t hold_max_ms = 3000;
|
||||
inline uint16_t regs[6] = {0x399F, 0x044C, (3410 << 3) | 2, 0, 0, 0};
|
||||
inline uint8_t ptr = 0;
|
||||
inline uint32_t rng = 0x2270A5C3;
|
||||
inline uint32_t rnd() {
|
||||
rng ^= rng << 13;
|
||||
rng ^= rng >> 17;
|
||||
rng ^= rng << 5;
|
||||
return rng;
|
||||
}
|
||||
struct FaultEv {
|
||||
uint32_t ms;
|
||||
uint8_t type;
|
||||
uint32_t arg;
|
||||
};
|
||||
inline FaultEv ring[64];
|
||||
inline volatile uint32_t ring_w = 0;
|
||||
inline uint32_t ring_r = 0;
|
||||
inline void note(uint8_t type, uint32_t arg) {
|
||||
FaultEv &e = ring[ring_w & 63];
|
||||
e.ms = (uint32_t) (esp_timer_get_time() / 1000);
|
||||
e.type = type;
|
||||
e.arg = arg;
|
||||
ring_w = ring_w + 1;
|
||||
}
|
||||
inline TaskHandle_t holder = nullptr;
|
||||
inline volatile uint32_t hold_mask = 0, hold_ms = 0;
|
||||
inline volatile bool holding = false;
|
||||
|
||||
inline void drive_low(uint32_t bits) {
|
||||
reg(GPIO_OUT1_W1TC_REG) = bits;
|
||||
reg(GPIO_ENABLE1_W1TS_REG) = bits;
|
||||
if (bits & SDA_BIT)
|
||||
reg(out_sel_addr(SDA)) = LOW_VAL;
|
||||
if (bits & SCL_BIT)
|
||||
reg(out_sel_addr(SCL)) = LOW_VAL;
|
||||
}
|
||||
inline void release(uint32_t bits) {
|
||||
if (bits & SDA_BIT)
|
||||
reg(out_sel_addr(SDA)) = rel_sda;
|
||||
if (bits & SCL_BIT)
|
||||
reg(out_sel_addr(SCL)) = rel_scl;
|
||||
}
|
||||
inline void spin_us(uint32_t us) {
|
||||
uint32_t s = esp_cpu_get_cycle_count();
|
||||
while (esp_cpu_get_cycle_count() - s < us * CPU_MHZ) {
|
||||
}
|
||||
}
|
||||
|
||||
inline void update_derived() {
|
||||
// current = shunt * cal / 4096, power = current * bus / 5000 (datasheet scaling)
|
||||
int32_t shunt = (int16_t) regs[1];
|
||||
uint32_t cal = regs[5];
|
||||
int32_t cur = (int32_t) ((int64_t) shunt * cal / 4096);
|
||||
regs[4] = (uint16_t) cur;
|
||||
regs[3] = (uint16_t) ((int64_t) (cur < 0 ? -cur : cur) * (regs[2] >> 3) / 5000);
|
||||
}
|
||||
|
||||
inline void start_hold(uint32_t bits, uint32_t ms, BaseType_t *woken) {
|
||||
hold_mask = bits;
|
||||
hold_ms = ms;
|
||||
holding = true;
|
||||
drive_low(bits);
|
||||
vTaskNotifyGiveFromISR(holder, woken);
|
||||
}
|
||||
|
||||
// Spins through one transaction from a START. Returns when STOP, idle or cap hits.
|
||||
inline void run_transaction(BaseType_t *woken) {
|
||||
enum { ADDR, WDATA, RDATA, IGNORE } phase = ADDR;
|
||||
uint32_t prev = in();
|
||||
uint32_t start = esp_cpu_get_cycle_count(), last = start;
|
||||
uint8_t byte = 0, cnt = 0, out = 0, rbyte_idx = 0, wbyte_idx = 0, wmsb = 0;
|
||||
bool acked = false, mack = false, sda_driven = false;
|
||||
int fault = -1;
|
||||
uint32_t fault_at = 0, rises = 0;
|
||||
bool garbage_done = false;
|
||||
auto set_sda = [&](bool low) {
|
||||
if (low && !sda_driven) {
|
||||
drive_low(SDA_BIT);
|
||||
sda_driven = true;
|
||||
} else if (!low && sda_driven) {
|
||||
release(SDA_BIT);
|
||||
sda_driven = false;
|
||||
}
|
||||
};
|
||||
auto load_byte = [&]() {
|
||||
uint16_t v = ptr < 6 ? regs[ptr] : 0xFFFF;
|
||||
out = rbyte_idx == 0 ? (v >> 8) : (rbyte_idx == 1 ? (v & 0xFF) : 0xFF);
|
||||
if (fault == F_GARBAGE && !garbage_done) {
|
||||
out ^= 1u << (rnd() & 7);
|
||||
note(F_GARBAGE, out);
|
||||
garbage_done = true;
|
||||
}
|
||||
rbyte_idx++;
|
||||
};
|
||||
for (;;) {
|
||||
uint32_t v = in();
|
||||
uint32_t now = esp_cpu_get_cycle_count();
|
||||
if (v == prev) {
|
||||
if (now - last > 15000u * CPU_MHZ) {
|
||||
st.idle_abort++;
|
||||
break;
|
||||
}
|
||||
if (now - start > 60000u * CPU_MHZ) {
|
||||
st.cap_abort++;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
last = now;
|
||||
uint32_t ch = v ^ prev;
|
||||
prev = v;
|
||||
if (ch & SCL_BIT) {
|
||||
if (v & SCL_BIT) {
|
||||
// rising edge: sample
|
||||
cnt++;
|
||||
rises++;
|
||||
if (cnt <= 8 && (phase == ADDR || phase == WDATA))
|
||||
byte = (byte << 1) | ((v & SDA_BIT) ? 1 : 0);
|
||||
else if (cnt == 9 && phase == RDATA)
|
||||
mack = !(v & SDA_BIT);
|
||||
if (fault >= 0 && rises == fault_at) {
|
||||
if (fault == F_GLITCH_SCL || fault == F_GLITCH_SDA) {
|
||||
spin_us(2);
|
||||
uint32_t b = fault == F_GLITCH_SCL ? SCL_BIT : SDA_BIT;
|
||||
bool was = b == SDA_BIT && sda_driven;
|
||||
if (!was) {
|
||||
note(fault, rises);
|
||||
drive_low(b);
|
||||
spin_us(1 + (rnd() & 3));
|
||||
release(b);
|
||||
}
|
||||
fault = -1;
|
||||
prev = in();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// falling edge: drive
|
||||
if (fault >= 0 && rises == fault_at) {
|
||||
if (fault == F_STRETCH) {
|
||||
drive_low(SCL_BIT);
|
||||
uint32_t us = 200 + rnd() % 19800;
|
||||
note(F_STRETCH, us);
|
||||
spin_us(us);
|
||||
release(SCL_BIT);
|
||||
fault = -1;
|
||||
prev = in();
|
||||
last = esp_cpu_get_cycle_count();
|
||||
} else if (fault == F_HOLD_SCL) {
|
||||
set_sda(false);
|
||||
uint32_t ms = 50 + rnd() % hold_max_ms;
|
||||
note(F_HOLD_SCL, ms);
|
||||
start_hold(SCL_BIT, ms, woken);
|
||||
return;
|
||||
} else if (fault == F_HOLD_SDA) {
|
||||
drive_low(SDA_BIT);
|
||||
sda_driven = false;
|
||||
uint32_t ms = 50 + rnd() % hold_max_ms;
|
||||
note(F_HOLD_SDA, ms);
|
||||
start_hold(SDA_BIT, ms, woken);
|
||||
return;
|
||||
} else if (fault == F_VANISH) {
|
||||
note(F_VANISH, rises);
|
||||
set_sda(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cnt == 8) {
|
||||
if (phase == ADDR) {
|
||||
bool match = (byte >> 1) == 0x40;
|
||||
if (match) {
|
||||
st.matched++;
|
||||
if (armed && p_fault && (rnd() % 1000) < p_fault) {
|
||||
uint32_t f;
|
||||
do {
|
||||
f = rnd() % F_COUNT;
|
||||
} while (!(fault_mask & (1u << f)));
|
||||
fault = f;
|
||||
fault_at = rises + 1 + rnd() % 30;
|
||||
st.fault[f]++;
|
||||
}
|
||||
}
|
||||
if (match && fault == F_NACK)
|
||||
note(F_NACK, 0);
|
||||
if (match && fault != F_NACK) {
|
||||
set_sda(true);
|
||||
acked = true;
|
||||
phase = (byte & 1) ? RDATA : WDATA;
|
||||
rbyte_idx = 0;
|
||||
wbyte_idx = 0;
|
||||
} else {
|
||||
phase = IGNORE;
|
||||
acked = false;
|
||||
}
|
||||
} else if (phase == WDATA) {
|
||||
st.wr_bytes++;
|
||||
if (wbyte_idx == 0)
|
||||
ptr = byte;
|
||||
else if (wbyte_idx == 1)
|
||||
wmsb = byte;
|
||||
else if (wbyte_idx == 2 && ptr < 6) {
|
||||
uint16_t w = (wmsb << 8) | byte;
|
||||
if (ptr == 0 && (w & 0x8000))
|
||||
w = 0x399F;
|
||||
if (ptr == 0 || ptr == 5)
|
||||
regs[ptr] = w;
|
||||
update_derived();
|
||||
}
|
||||
wbyte_idx++;
|
||||
set_sda(true);
|
||||
acked = true;
|
||||
} else if (phase == RDATA) {
|
||||
set_sda(false); // master acks
|
||||
acked = false;
|
||||
}
|
||||
} else if (cnt == 9) {
|
||||
if (acked) {
|
||||
set_sda(false);
|
||||
acked = false;
|
||||
}
|
||||
if (phase == RDATA) {
|
||||
if (rbyte_idx == 0 || mack) {
|
||||
load_byte();
|
||||
st.rd_bytes++;
|
||||
set_sda(!(out & 0x80));
|
||||
} else {
|
||||
phase = IGNORE;
|
||||
}
|
||||
}
|
||||
cnt = 0;
|
||||
byte = 0;
|
||||
} else if (cnt >= 1 && cnt <= 7 && phase == RDATA) {
|
||||
set_sda(!(out & (0x80 >> cnt)));
|
||||
}
|
||||
}
|
||||
} else if ((ch & SDA_BIT) && (v & SCL_BIT)) {
|
||||
if (v & SDA_BIT) {
|
||||
st.stops++;
|
||||
break;
|
||||
}
|
||||
// repeated START
|
||||
st.starts++;
|
||||
set_sda(false);
|
||||
phase = ADDR;
|
||||
cnt = 0;
|
||||
byte = 0;
|
||||
acked = false;
|
||||
}
|
||||
}
|
||||
set_sda(false);
|
||||
}
|
||||
|
||||
inline void slave_isr(void *) {
|
||||
reg(GPIO_STATUS1_W1TC_REG) = SDA_BIT;
|
||||
if (holding)
|
||||
return;
|
||||
uint32_t v = in();
|
||||
if (!(v & SCL_BIT) || (v & SDA_BIT))
|
||||
return;
|
||||
st.starts++;
|
||||
BaseType_t woken = pdFALSE;
|
||||
run_transaction(&woken);
|
||||
reg(GPIO_STATUS1_W1TC_REG) = SDA_BIT;
|
||||
if (woken)
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
inline void holder_task(void *) {
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
uint32_t end = (esphome::millis) () + hold_ms;
|
||||
while ((int32_t) (end - (esphome::millis) ()) > 0) {
|
||||
drive_low(hold_mask);
|
||||
vTaskDelay(1);
|
||||
}
|
||||
release(hold_mask);
|
||||
holding = false;
|
||||
}
|
||||
}
|
||||
|
||||
inline volatile uint32_t hb = 0;
|
||||
inline volatile bool diag_recover = false;
|
||||
constexpr uint32_t I2C0 = 0x3FF53000;
|
||||
inline void dump_i2c(const char *why, uint32_t stale) {
|
||||
esp_rom_printf("DIAG %s stale=%ums ctr=%08x sr=%08x to=%08x raw=%08x ena=%08x st=%08x fifo=%08x hold=%d in=%x\n", why,
|
||||
(unsigned) stale, (unsigned) reg(I2C0 + 0x04), (unsigned) reg(I2C0 + 0x08),
|
||||
(unsigned) reg(I2C0 + 0x0c), (unsigned) reg(I2C0 + 0x20), (unsigned) reg(I2C0 + 0x28),
|
||||
(unsigned) reg(I2C0 + 0x2c), (unsigned) reg(I2C0 + 0x18), (int) holding, (unsigned) (in() & 3));
|
||||
esp_rom_printf("DIAG cmd:");
|
||||
for (int i = 0; i < 16; i++)
|
||||
esp_rom_printf(" %08x", (unsigned) reg(I2C0 + 0x58 + 4 * i));
|
||||
esp_rom_printf("\nDIAG app_intr_status %08x %08x %08x pro %08x %08x %08x\n", (unsigned) reg(0x3FF000F8),
|
||||
(unsigned) reg(0x3FF000FC), (unsigned) reg(0x3FF00100), (unsigned) reg(0x3FF000EC),
|
||||
(unsigned) reg(0x3FF000F0), (unsigned) reg(0x3FF000F4));
|
||||
}
|
||||
inline void monitor_task(void *) {
|
||||
uint32_t stage = 0;
|
||||
for (;;) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
uint32_t now = (esphome::millis) ();
|
||||
uint32_t h = hb;
|
||||
if (h == 0)
|
||||
continue;
|
||||
uint32_t stale = now - h;
|
||||
if (stale < 1000) {
|
||||
if (stage >= 3)
|
||||
esp_rom_printf("DIAG loop resumed after %u ms, stage %u\n", (unsigned) stale, (unsigned) stage);
|
||||
stage = 0;
|
||||
continue;
|
||||
}
|
||||
if (stage == 0 && stale >= 1000) {
|
||||
dump_i2c("stuck1", stale);
|
||||
stage = 1;
|
||||
} else if (stage == 1 && stale >= 1500) {
|
||||
dump_i2c("stuck2", stale);
|
||||
stage = 2;
|
||||
} else if (stage == 2 && stale >= 2500) {
|
||||
dump_i2c("stuck3", stale);
|
||||
if (diag_recover) {
|
||||
reg(I2C0 + 0x28) = 0;
|
||||
reg(I2C0 + 0x24) = 0xFFFF;
|
||||
esp_rom_printf("DIAG cleared I2C0 int_ena\n");
|
||||
}
|
||||
stage = 3;
|
||||
} else if (stage == 3 && stale >= 3500) {
|
||||
dump_i2c("stuck4", stale);
|
||||
stage = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
inline volatile bool installed = false;
|
||||
inline void install_task(void *) {
|
||||
rel_sda = reg(out_sel_addr(SDA));
|
||||
rel_scl = reg(out_sel_addr(SCL));
|
||||
// ESP_ERR_INVALID_STATE: the service is already installed, which is fine
|
||||
esp_err_t e1 = gpio_install_isr_service(0);
|
||||
esp_err_t et = gpio_set_intr_type((gpio_num_t) SDA, GPIO_INTR_NEGEDGE);
|
||||
esp_err_t e2 = gpio_isr_handler_add((gpio_num_t) SDA, slave_isr, nullptr);
|
||||
ESP_LOGI(TAG, "slave on core %d: isr service %d, intr type %d, handler %d, rel_sda 0x%x rel_scl 0x%x",
|
||||
xPortGetCoreID(), e1, et, e2, (unsigned) rel_sda, (unsigned) rel_scl);
|
||||
if ((e1 == ESP_OK || e1 == ESP_ERR_INVALID_STATE) && et == ESP_OK && e2 == ESP_OK) {
|
||||
installed = true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "slave ISR install failed: service %s, intr type %s, handler %s", esp_err_to_name(e1),
|
||||
esp_err_to_name(et), esp_err_to_name(e2));
|
||||
}
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
inline void install() {
|
||||
if (xTaskCreatePinnedToCore(holder_task, "inahold", 3072, nullptr, configMAX_PRIORITIES - 2, &holder, 0) != pdPASS ||
|
||||
xTaskCreatePinnedToCore(monitor_task, "inamon", 3072, nullptr, configMAX_PRIORITIES - 1, nullptr, 0) != pdPASS ||
|
||||
xTaskCreatePinnedToCore(install_task, "inainst", 3072, nullptr, 5, nullptr, 0) != pdPASS) {
|
||||
ESP_LOGE(TAG, "task creation failed, slave not installed");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 1000 && !installed; i++)
|
||||
vTaskDelay(1);
|
||||
if (!installed)
|
||||
ESP_LOGE(TAG, "slave install did not finish");
|
||||
}
|
||||
|
||||
inline void drain() {
|
||||
static const char *const NAMES[F_COUNT] = {"garbage", "nack", "stretch_us", "hold_scl_ms",
|
||||
"hold_sda_ms", "glitch_scl", "glitch_sda", "vanish"};
|
||||
const uint32_t w = ring_w;
|
||||
if (w - ring_r > 64) {
|
||||
ESP_LOGW(TAG, "F dropped %u events (ring overrun)", (unsigned) (w - ring_r - 64));
|
||||
ring_r = w - 64;
|
||||
}
|
||||
while (ring_r != w) {
|
||||
const FaultEv &e = ring[ring_r & 63];
|
||||
ESP_LOGI(TAG, "F (%u) %s %u", (unsigned) e.ms, NAMES[e.type], (unsigned) e.arg);
|
||||
ring_r++;
|
||||
}
|
||||
}
|
||||
inline void log_stats() {
|
||||
ESP_LOGI(TAG,
|
||||
"SLAVE starts=%u matched=%u stops=%u idle_abort=%u cap_abort=%u rd=%u wr=%u | faults garbage=%u nack=%u "
|
||||
"stretch=%u hold_scl=%u hold_sda=%u glitch_scl=%u glitch_sda=%u vanish=%u",
|
||||
(unsigned) st.starts, (unsigned) st.matched, (unsigned) st.stops, (unsigned) st.idle_abort,
|
||||
(unsigned) st.cap_abort, (unsigned) st.rd_bytes, (unsigned) st.wr_bytes, (unsigned) st.fault[0],
|
||||
(unsigned) st.fault[1], (unsigned) st.fault[2], (unsigned) st.fault[3], (unsigned) st.fault[4],
|
||||
(unsigned) st.fault[5], (unsigned) st.fault[6], (unsigned) st.fault[7]);
|
||||
}
|
||||
} // namespace inasim
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh LABEL [SECONDS]: build example.yaml from TREE as i2cfault-<LABEL> (edit its substitutions for the framework,
|
||||
# seed and fault rate), flash the ESP32, capture SECONDS (default 640) from a reset and summarize.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; S=${2:-640}; T=${TREE:?set TREE}; P=${PORT:?set PORT}
|
||||
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; NAME=i2cfault-$L; OUT=$R/out-$NAME; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
sed -E "s/^ name: .*/ name: $NAME/" "$R/example.yaml" > "$R/$NAME.yaml"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$NAME.yaml" --device "$P" > "$OUT/upload.log" 2>&1) || { echo "build or upload failed"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
$PY "$SC" "$P" "$S" "$OUT/serial.txt" --reset > /dev/null 2>&1 || { echo "serial capture got nothing"; exit 1; }
|
||||
"$R/summary.sh" "$OUT/serial.txt"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
# summary.sh CAPTURE...: crashes, watchdogs, timeout kinds and the read counters per capture
|
||||
for f in "$@"; do
|
||||
first=$(grep -a -m1 'rst:0x' "$f" | cut -c2-9); last=$(tail -1 "$f" | cut -c2-9)
|
||||
echo "== $(basename "$f") $first..$last boots=$(grep -a -c 'rst:0x' "$f") guru=$(grep -a -c Guru "$f") twdt=$(grep -a -c 'task_wdt: Task watchdog' "$f") diag=$(grep -a -c 'DIAG stuck' "$f")"
|
||||
echo " sw=$(grep -a -c 'software timeout' "$f") hw=$(grep -a -c 'hardware timeout detected' "$f") trans=$(grep -a -c 'transaction timeout detected' "$f") busy=$(grep -a -c 'still busy' "$f") ina_garbage_lines=$(grep -a -c 'INA bus voltage garbage' "$f")"
|
||||
grep -a 'MASTER' "$f" | tail -1 | sed 's/^.*MASTER/ MASTER/'
|
||||
grep -a 'SLAVE' "$f" | tail -1 | sed 's/^.*SLAVE/ SLAVE/'
|
||||
echo " ok per 30s window (min over windows after arm): $(grep -a MASTER "$f" | sed 's/.* ok=\([0-9]*\).*/\1/' | awk 'NR>2{d=$1-p; if(min==""||d<min)min=d} {p=$1} END{print (min == "" ? "n/a (fewer than 3 MASTER windows)" : min)}')"
|
||||
echo " harness: install_errors=$(grep -a -c -E 'task creation failed|slave ISR install failed|slave install did not finish|selects no fault' "$f") $(grep -a -o 'selftest1 write=[-0-9]* read=[-0-9]* val=[0-9A-F]*' "$f" | head -1)"
|
||||
done
|
||||
@@ -1,90 +0,0 @@
|
||||
"""Per phase IPv6 link-local and SLAAC statistics from v6probe captures.
|
||||
|
||||
usage: an.py CAPTURE...
|
||||
For the boot and reconnect phases: boots, boots with an esp_netif_create_ip6_linklocal failure, boots without
|
||||
link-local, SLAAC or a connection, connection timeouts, and median/worst times in seconds. Counts missing
|
||||
addresses from the RESULT line too, since the USB console can drop a warning line. Exits 1 when a boot other
|
||||
than the last of a capture has no RESULT for a phase.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
|
||||
def parse(paths: list[str]) -> list[dict]:
|
||||
boots: list[dict] = []
|
||||
cur: dict | None = None
|
||||
for p in paths:
|
||||
for line in pathlib.Path(p).read_bytes().decode(errors="replace").splitlines():
|
||||
if "BOOTMARK" in line:
|
||||
cur = {
|
||||
"boot": {},
|
||||
"reconnect": {},
|
||||
"fail": [0, 0],
|
||||
"created": [0, 0],
|
||||
"timeouts": [0, 0],
|
||||
"ph": 0,
|
||||
}
|
||||
boots.append(cur)
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
ph = cur["ph"]
|
||||
if "esp_netif_create_ip6_linklocal failed" in line:
|
||||
cur["fail"][ph] += 1
|
||||
if "IPv6 link-local address created" in line or "No lwIP netif" in line:
|
||||
cur["created"][ph] += 1
|
||||
if re.search(
|
||||
r"Connection timeout|Connection incomplete|Connection lost", line
|
||||
):
|
||||
cur["timeouts"][ph] += 1
|
||||
if "REENABLE" in line:
|
||||
cur["ph"] = 1
|
||||
if m := re.search(r"RESULT (\w+) (.*)", line):
|
||||
cur[m.group(1)] = dict(kv.split("=") for kv in m.group(2).split())
|
||||
if cur is not None:
|
||||
cur["last"] = True
|
||||
cur = None
|
||||
return boots
|
||||
|
||||
|
||||
def med_max(xs: list[int]) -> str:
|
||||
return f"{statistics.median(xs) / 1000:.1f}/{max(xs) / 1000:.1f}" if xs else "-"
|
||||
|
||||
|
||||
def summ(boots: list[dict], phase: str, idx: int) -> int:
|
||||
rs = [b for b in boots if b[phase]]
|
||||
# a capture usually ends inside its last boot
|
||||
no_result = sum(1 for b in boots if not b[phase] and "last" not in b)
|
||||
|
||||
def seen(key: str) -> list[int]:
|
||||
return [int(b[phase][key]) for b in rs if b[phase][key] != "0"]
|
||||
|
||||
def missing(key: str) -> int:
|
||||
return sum(1 for b in rs if b[phase][key] == "0")
|
||||
|
||||
print(
|
||||
f"{phase:9} n={len(rs)} no_result={no_result} esp_fail={sum(1 for b in rs if b['fail'][idx])} no_ll={missing('ll')} "
|
||||
f"no_slaac={missing('gl')} not_connected={missing('full')} timeouts={sum(b['timeouts'][idx] for b in rs)} "
|
||||
f"ll(med/max s)={med_max(seen('ll'))} slaac={med_max(seen('gl'))} connected={med_max(seen('full'))} "
|
||||
f"created_logs={sum(b['created'][idx] for b in rs)}"
|
||||
)
|
||||
return no_result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
boots = parse(sys.argv[1:])
|
||||
print("boots", len(boots))
|
||||
no_result = summ(boots, "boot", 0) + summ(boots, "reconnect", 1)
|
||||
print(
|
||||
"heap_used first 3 boots",
|
||||
[int(b["boot"]["heap_used"]) for b in boots if b["boot"]][:3],
|
||||
)
|
||||
if no_result or not boots:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,12 +0,0 @@
|
||||
substitutions:
|
||||
min_count: "0"
|
||||
cap_ms: "30000"
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
common: !include v6common.yaml
|
||||
inject: !include v6inject.yaml
|
||||
@@ -1,12 +0,0 @@
|
||||
# One file per build; run.sh passes the name. Add v6hf.yaml or v6inject.yaml to packages.
|
||||
substitutions:
|
||||
min_count: "0"
|
||||
cap_ms: "30000"
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
common: !include v6common.yaml
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
# run.sh NAME SECONDS: build NAME.yaml from TREE as device NAME, flash, then capture SECONDS of self-rebooting boots
|
||||
# (each boot is a boot phase and a reconnect phase) into out-NAME/cap.txt and summarize them with an.py.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); N=${1:?name}; S=${2:?seconds}
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
OUT=$R/out-$N; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome -s name "$N" run --no-logs "$R/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$OUT/upload.log"; exit 1; }
|
||||
$PY "$SC" "$P" "$S" "$OUT/cap.txt" --ms > /dev/null 2>&1
|
||||
[ -s "$OUT/cap.txt" ] || { echo "serial capture got nothing"; exit 1; }
|
||||
$PY "$R/an.py" "$OUT/cap.txt"
|
||||
@@ -1,28 +0,0 @@
|
||||
# IPv6 boot and reconnect probe (#22364): logs milestones per phase, then disables Wi-Fi for 3 s, measures the
|
||||
# reconnect and reboots. Substitutions: name (run.sh passes it), min_count (network min_ipv6_addr_count), cap_ms (phase time limit).
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- v6probe.hpp
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("v6probe", "BOOTMARK reason=%d", (int) esp_reset_reason());'
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
min_ipv6_addr_count: ${min_count}
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
reboot_timeout: 0s
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: "v6probe::tick(${cap_ms});"
|
||||
@@ -1,9 +0,0 @@
|
||||
# Add to v6common.yaml: keeps the main loop busy, like a display or audio config
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: |-
|
||||
static HighFrequencyLoopRequester hf;
|
||||
hf.start();
|
||||
ESP_LOGI("v6probe", "HF loop on");
|
||||
@@ -1,5 +0,0 @@
|
||||
# Add to v6common.yaml: delays esp-netif's STA_CONNECTED handler by 300 ms (wrapper in v6probe.hpp)
|
||||
esphome:
|
||||
build_flags:
|
||||
- -Wl,--wrap=esp_netif_action_connected
|
||||
- -DV6_INJECT
|
||||
@@ -1,100 +0,0 @@
|
||||
#pragma once
|
||||
// Boot and reconnect timing probe for IPv6 link-local and SLAAC (#22364). Call tick(cap_ms) every 50 ms.
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
namespace v6probe {
|
||||
static const char *const PTAG = "v6probe";
|
||||
struct Times {
|
||||
uint32_t assoc, v4, ll, gl, full;
|
||||
};
|
||||
static int phase = 0; // 0 boot, 1 wifi disabled, 2 reconnecting, 3 done
|
||||
static uint32_t start = 0;
|
||||
static Times t{};
|
||||
|
||||
inline void tick(uint32_t cap_ms) {
|
||||
using esphome::millis;
|
||||
uint32_t now = millis();
|
||||
if (phase == 3)
|
||||
return;
|
||||
if (phase == 1) {
|
||||
if (now - start >= 3000) {
|
||||
ESP_LOGI(PTAG, "REENABLE");
|
||||
t = {};
|
||||
start = now;
|
||||
phase = 2;
|
||||
esphome::wifi::global_wifi_component->enable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint32_t rel = (now - start) | 1; // never 0, 0 means not seen
|
||||
wifi_ap_record_t ap;
|
||||
if (!t.assoc && esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
|
||||
t.assoc = rel;
|
||||
ESP_LOGI(PTAG, "MS assoc %u", rel);
|
||||
}
|
||||
// a missing netif still has to reach the cap_ms timeout below
|
||||
esp_netif_t *nif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
esp_netif_ip_info_t ip;
|
||||
if (!t.v4 && nif != nullptr && esp_netif_get_ip_info(nif, &ip) == ESP_OK && ip.ip.addr != 0) {
|
||||
t.v4 = rel;
|
||||
ESP_LOGI(PTAG, "MS ipv4 %u", rel);
|
||||
}
|
||||
esp_ip6_addr_t a[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
int n = 0;
|
||||
if (nif != nullptr) {
|
||||
esphome::LwIPLock lock;
|
||||
n = esp_netif_get_all_ip6(nif, a);
|
||||
}
|
||||
bool ll = false, gl = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (esp_netif_ip6_get_addr_type(&a[i]) == ESP_IP6_ADDR_IS_LINK_LOCAL)
|
||||
ll = true;
|
||||
else
|
||||
gl = true;
|
||||
}
|
||||
if (!t.ll && ll) {
|
||||
t.ll = rel;
|
||||
ESP_LOGI(PTAG, "MS linklocal %u", rel);
|
||||
}
|
||||
if (!t.gl && gl) {
|
||||
t.gl = rel;
|
||||
ESP_LOGI(PTAG, "MS slaac %u", rel);
|
||||
}
|
||||
if (!t.full && esphome::wifi::global_wifi_component->is_connected()) {
|
||||
t.full = rel;
|
||||
ESP_LOGI(PTAG, "MS connected %u", rel);
|
||||
}
|
||||
bool done = t.gl && t.full;
|
||||
if (done || now - start >= cap_ms) {
|
||||
size_t used = heap_caps_get_total_size(MALLOC_CAP_INTERNAL) - heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
ESP_LOGI(PTAG, "RESULT %s assoc=%u v4=%u ll=%u gl=%u full=%u n6=%d timeout=%d heap_used=%u",
|
||||
phase == 0 ? "boot" : "reconnect", t.assoc, t.v4, t.ll, t.gl, t.full, n, done ? 0 : 1, (unsigned) used);
|
||||
if (phase == 0) {
|
||||
phase = 1;
|
||||
start = now;
|
||||
esphome::wifi::global_wifi_component->disable();
|
||||
} else {
|
||||
phase = 3;
|
||||
esphome::App.safe_reboot();
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace v6probe
|
||||
|
||||
#ifdef V6_INJECT
|
||||
// Fault injection: esp-netif's STA_CONNECTED handler (which brings the netif up) is delayed by 300 ms in the
|
||||
// event task, after the wifi component's own handler has already queued the event for the main loop.
|
||||
extern "C" void __real_esp_netif_action_connected(void *esp_netif, esp_event_base_t base, int32_t id, void *data);
|
||||
extern "C" void __wrap_esp_netif_action_connected(void *esp_netif, esp_event_base_t base, int32_t id, void *data) {
|
||||
ESP_LOGI("v6probe", "INJECT hold start");
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
ESP_LOGI("v6probe", "INJECT hold end");
|
||||
__real_esp_netif_action_connected(esp_netif, base, id, data);
|
||||
}
|
||||
#endif
|
||||
@@ -1,82 +0,0 @@
|
||||
# Package: BW12 heartbeat, pull-toggle edge generator, BS binary sensor on ${bs_pin}, meas lines every 10 s
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: BW12 ISR Test
|
||||
platformio_options:
|
||||
upload_speed: 115200
|
||||
build_flags:
|
||||
- -Wl,--wrap=digitalRead
|
||||
includes:
|
||||
- bwdbg.hpp
|
||||
|
||||
rtl87xx:
|
||||
board: bw12
|
||||
framework:
|
||||
version: recommended
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
baud_rate: 115200
|
||||
logs:
|
||||
sensor: WARN
|
||||
binary_sensor: WARN
|
||||
pulse_counter: WARN
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
|
||||
captive_portal:
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: PA05
|
||||
id: heartbeat_output
|
||||
|
||||
globals:
|
||||
- id: bs_changes
|
||||
type: uint32_t
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- output.turn_on: heartbeat_output
|
||||
- delay: 100ms
|
||||
- output.turn_off: heartbeat_output
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: bwdbg::tick();
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
static uint32_t lg = 0, lc = 0;
|
||||
uint32_t g = bwdbg::gen_rise + bwdbg::gen_fall, c = id(bs_changes);
|
||||
ESP_LOGI("meas", "BS changes %u gen_edges %u state %d", c - lc, g - lg, id(bs).state);
|
||||
lg = g; lc = c;
|
||||
bwdbg::report();
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: bs
|
||||
name: BS
|
||||
pin:
|
||||
number: ${bs_pin}
|
||||
mode: INPUT
|
||||
use_interrupt: true
|
||||
interrupt_type: ANY
|
||||
trigger_on_initial_state: true
|
||||
on_state:
|
||||
- lambda: id(bs_changes) += 1;
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime
|
||||
update_interval: 60s
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
// bw12isr: pull-toggle edge generator on floating pins, ISR read counters and pin/IRQ register dump (RTL8710B)
|
||||
#include <Arduino.h>
|
||||
#include "esphome/core/log.h"
|
||||
extern "C" {
|
||||
PinStatus __real_digitalRead(pin_size_t pin);
|
||||
__attribute__((long_call)) void PAD_PullCtrl(uint8_t pin, uint8_t pull);
|
||||
}
|
||||
namespace bwdbg {
|
||||
static constexpr uint8_t PINS[] = {12, 0, 14, 15, 19, 22};
|
||||
static constexpr int NP = sizeof(PINS);
|
||||
inline volatile uint32_t isr_reads[32], task_reads[32];
|
||||
inline uint32_t gen_rise = 0, gen_fall = 0, checks = 0, mismatch[NP];
|
||||
inline bool up = false, started = false;
|
||||
inline uint32_t reg(uint32_t off) { return *reinterpret_cast<volatile uint32_t *>(0x40001000 + off); }
|
||||
inline bool in_isr() {
|
||||
uint32_t r;
|
||||
__asm volatile("mrs %0, ipsr" : "=r"(r));
|
||||
return r != 0;
|
||||
}
|
||||
// Every call: check each pin follows the pull set on the previous call, then flip all pulls
|
||||
inline void tick() {
|
||||
uint32_t lvl = reg(0x50);
|
||||
if (started) {
|
||||
checks++;
|
||||
for (int i = 0; i < NP; i++)
|
||||
if (((lvl >> PINS[i]) & 1) != (up ? 1u : 0u))
|
||||
mismatch[i]++;
|
||||
}
|
||||
up = !up;
|
||||
started = true;
|
||||
for (int i = 0; i < NP; i++)
|
||||
PAD_PullCtrl(PINS[i], up ? 2 : 1); // GPIO_PuPd_UP : GPIO_PuPd_DOWN
|
||||
if (up)
|
||||
gen_rise++;
|
||||
else
|
||||
gen_fall++;
|
||||
}
|
||||
inline void report() {
|
||||
uint32_t en = reg(0x30), mask = reg(0x34), type = reg(0x38), pol = reg(0x3C), deb = reg(0x48), lvl = reg(0x50);
|
||||
ESP_LOGI("dbg", "gen rise %u fall %u checks %u", gen_rise, gen_fall, checks);
|
||||
for (int i = 0; i < NP; i++) {
|
||||
uint8_t p = PINS[i];
|
||||
PinInfo *info = pinInfo(p);
|
||||
// -1 marks a pin with no PinInfo entry
|
||||
const uint32_t e = info ? info->enabled : 0;
|
||||
const int gpio = info ? !!(e & PIN_GPIO) : -1, irq = info ? !!(e & PIN_IRQ) : -1, pwm = info ? !!(e & PIN_PWM) : -1;
|
||||
ESP_LOGI("dbg",
|
||||
"PA%02u LT 0x%x (GPIO %d IRQ %d PWM %d) HW int_en %u mask %u edge %u pol_high %u deb %u lvl %u | "
|
||||
"mismatch %u isr digitalRead %u task digitalRead %u",
|
||||
p, e, gpio, irq, pwm, (en >> p) & 1, (mask >> p) & 1, (type >> p) & 1, (pol >> p) & 1, (deb >> p) & 1,
|
||||
(lvl >> p) & 1, mismatch[i], isr_reads[p], task_reads[p]);
|
||||
}
|
||||
}
|
||||
} // namespace bwdbg
|
||||
extern "C" __attribute__((used)) PinStatus __wrap_digitalRead(pin_size_t pin) {
|
||||
if (pin < 32) {
|
||||
if (bwdbg::in_isr())
|
||||
bwdbg::isr_reads[pin] = bwdbg::isr_reads[pin] + 1;
|
||||
else
|
||||
bwdbg::task_reads[pin] = bwdbg::task_reads[pin] + 1;
|
||||
}
|
||||
return __real_digitalRead(pin);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
# Layout B: BS on PA19; F0/I0 moved to PA12/PA00
|
||||
substitutions:
|
||||
name: bw12-edges-b
|
||||
bs_pin: PA19
|
||||
|
||||
packages:
|
||||
base: !include bw12-base.yaml
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: F0
|
||||
pin:
|
||||
number: PA12
|
||||
inverted: false
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "F0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: I0
|
||||
pin:
|
||||
number: PA00
|
||||
inverted: true
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "I0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
@@ -1,104 +0,0 @@
|
||||
substitutions:
|
||||
name: bw12-time
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: BW12 Time Test
|
||||
platformio_options:
|
||||
upload_speed: 115200
|
||||
|
||||
rtl87xx:
|
||||
board: bw12
|
||||
framework:
|
||||
version: recommended
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
baud_rate: 115200
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
|
||||
captive_portal:
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: Restart
|
||||
|
||||
interval:
|
||||
- interval: 20s
|
||||
then:
|
||||
- lambda: |-
|
||||
static bool done = false;
|
||||
if (done) return;
|
||||
done = true;
|
||||
// mask ROM: SYSTIMER_TickGet at 0x33a0 (32 bytes), RTIM_GetCount at 0xb42c (72 bytes)
|
||||
const uint32_t ranges[2][2] = {{0x33a0, 32}, {0xb42c, 72}};
|
||||
for (auto &r : ranges) {
|
||||
char hex[3 * 72 + 1];
|
||||
const volatile uint8_t *p = (const volatile uint8_t *) r[0];
|
||||
for (uint32_t i = 0; i < r[1]; i++) sprintf(hex + 3 * i, "%02x ", p[i]);
|
||||
ESP_LOGI("tm", "ROM %05x: %s", (unsigned) r[0], hex);
|
||||
}
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("tm", "CLOCK ms %u us %u", millis(), micros());'
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: |-
|
||||
volatile uint32_t *demcr = (volatile uint32_t *) 0xE000EDFC, *dwt = (volatile uint32_t *) 0xE0001000;
|
||||
*demcr |= 1u << 24;
|
||||
dwt[0] |= 1;
|
||||
const float mhz = SystemCoreClock / 1e6f;
|
||||
// cycles per call, 1000 calls, loop overhead subtracted
|
||||
volatile uint32_t sink = 0;
|
||||
uint32_t c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + i;
|
||||
uint32_t base = dwt[1] - c0;
|
||||
c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + millis();
|
||||
uint32_t ms_c = dwt[1] - c0 - base;
|
||||
c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + micros();
|
||||
uint32_t us_c = dwt[1] - c0 - base;
|
||||
uint32_t isr_c;
|
||||
{
|
||||
InterruptLock lock;
|
||||
c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + micros();
|
||||
isr_c = dwt[1] - c0 - base;
|
||||
}
|
||||
ESP_LOGI("tm", "BENCH cycles per call millis %.1f micros %.1f micros_locked %.1f (core %.1f MHz)",
|
||||
ms_c / 1000.0f, us_c / 1000.0f, isr_c / 1000.0f, mhz);
|
||||
uint32_t got[6];
|
||||
const uint32_t req_us[3] = {50, 500, 5000};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t a = dwt[1];
|
||||
delayMicroseconds(req_us[i]);
|
||||
got[i] = uint32_t((dwt[1] - a) / mhz);
|
||||
}
|
||||
const uint32_t req_ms[3] = {1, 10, 100};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t a = dwt[1];
|
||||
delay(req_ms[i]);
|
||||
got[3 + i] = uint32_t((dwt[1] - a) / mhz);
|
||||
}
|
||||
ESP_LOGI("tm", "DELAY real us: delayMicroseconds 50->%u 500->%u 5000->%u, delay 1->%u 10->%u 100->%u",
|
||||
got[0], got[1], got[2], got[3], got[4], got[5]);
|
||||
uint32_t first = micros(), prev = first, maxstep = 0, back = 0, distinct = 0, mism = 0;
|
||||
for (int i = 0; i < 20000; i++) {
|
||||
uint32_t ms = millis(), m = micros(), ms2 = millis();
|
||||
if (ms == ms2 && m / 1000 != ms) mism++;
|
||||
if (m != prev) { distinct++; if (m < prev) back++; else if (m - prev > maxstep) maxstep = m - prev; }
|
||||
prev = m;
|
||||
}
|
||||
ESP_LOGI("tm", "MICROS span %u distinct %u maxstep %u backwards %u, micros/1000 != millis %u of 20000",
|
||||
prev - first, distinct, maxstep, back, mism);
|
||||
@@ -1,48 +0,0 @@
|
||||
# Layout A: BS on PA22; F13/I13 on PA12/PA00, F0/I0 on PA14/PA15, B13 on PA19 counting both edges
|
||||
substitutions:
|
||||
name: bw12-edges-a
|
||||
bs_pin: PA22
|
||||
|
||||
packages:
|
||||
base: !include bw12-base.yaml
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: F13
|
||||
pin: PA12
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "F13 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: I13
|
||||
pin:
|
||||
number: PA00
|
||||
inverted: true
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "I13 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: F0
|
||||
pin: PA14
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "F0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: I0
|
||||
pin:
|
||||
number: PA15
|
||||
inverted: true
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "I0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: B13
|
||||
pin: PA19
|
||||
count_mode:
|
||||
rising_edge: INCREMENT
|
||||
falling_edge: INCREMENT
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "B13 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
@@ -1,47 +0,0 @@
|
||||
substitutions:
|
||||
name: pulse-bk
|
||||
inv: "false"
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- -DGEN_PIN_A=26
|
||||
- -DGEN_PIN_B=8
|
||||
includes:
|
||||
- pulsegen.hpp
|
||||
|
||||
bk72xx:
|
||||
board: xh-wb3s
|
||||
|
||||
logger:
|
||||
hardware_uart: UART1
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: pulsegen::tick();
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: PC default filter
|
||||
pin:
|
||||
number: P6
|
||||
inverted: ${inv}
|
||||
update_interval: 5s
|
||||
on_raw_value:
|
||||
- lambda: 'ESP_LOGI("meas", "F13 %.1f", x);'
|
||||
- platform: pulse_counter
|
||||
name: PC filter 0
|
||||
pin:
|
||||
number: P24
|
||||
inverted: ${inv}
|
||||
internal_filter: 0us
|
||||
update_interval: 5s
|
||||
on_raw_value:
|
||||
- lambda: 'ESP_LOGI("meas", "F0 %.1f", x);'
|
||||
@@ -1,39 +0,0 @@
|
||||
substitutions:
|
||||
name: pulse-rtl
|
||||
inv: "false"
|
||||
filt: 13us
|
||||
key: PC
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- -DGEN_PIN_NAME=17
|
||||
includes:
|
||||
- pulsegen.hpp
|
||||
|
||||
rtl87xx:
|
||||
board: bw15
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: pulsegen::tick();
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: PC
|
||||
pin:
|
||||
number: PA18
|
||||
inverted: ${inv}
|
||||
internal_filter: ${filt}
|
||||
update_interval: 5s
|
||||
on_raw_value:
|
||||
- lambda: 'ESP_LOGI("meas", "${key} %.1f", x);'
|
||||
@@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
// One pulse shape per 35 s stage on GEN_PIN_A and GEN_PIN_B (BK72xx analogWrite) or GEN_PIN_NAME (AmebaZ2 pwmout)
|
||||
#include <Arduino.h>
|
||||
#ifdef GEN_PIN_NAME
|
||||
// mbed pwmout API (its header clashes with Arduino's PinMode); pwmout_t is held in an oversized buffer
|
||||
extern "C" {
|
||||
void pwmout_init(void *obj, int pin);
|
||||
void pwmout_period_us(void *obj, int us);
|
||||
void pwmout_pulsewidth_us(void *obj, int us);
|
||||
}
|
||||
#endif
|
||||
#include "esphome/core/log.h"
|
||||
namespace pulsegen {
|
||||
struct Stage {
|
||||
uint32_t period_us;
|
||||
uint32_t high_us;
|
||||
};
|
||||
static constexpr Stage STAGES[] = {
|
||||
{1000, 38}, {10000, 38}, {10000, 50}, {10000, 75}, {10000, 100}, {10000, 150},
|
||||
{10000, 200}, {1000, 200}, {10000, 300}, {10000, 500}, {10000, 1000}, {100000, 1000},
|
||||
{100000, 5000}, {100000, 50000}, {2000, 1000}, {1000, 500}, {250, 125}, {100000, 38},
|
||||
};
|
||||
static constexpr int N = sizeof(STAGES) / sizeof(STAGES[0]);
|
||||
static constexpr uint32_t STAGE_MS = 35000;
|
||||
#ifdef GEN_PIN_NAME
|
||||
// Arduino analogWrite re-init faults on the 2nd period change on AmebaZ2: keep one pwmout and retune it
|
||||
inline void apply(const Stage &st) {
|
||||
alignas(8) static uint8_t pwm[512];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
pwmout_init(pwm, GEN_PIN_NAME);
|
||||
init = true;
|
||||
}
|
||||
pwmout_period_us(pwm, st.period_us);
|
||||
pwmout_pulsewidth_us(pwm, st.high_us);
|
||||
}
|
||||
#else
|
||||
inline void apply_pin(uint8_t pin, const Stage &s) {
|
||||
pinMode(pin, INPUT); // drop PWM mode so the new period is taken
|
||||
analogWriteResolution(16);
|
||||
analogWritePeriod(s.period_us);
|
||||
analogWrite(pin, (int) (65535.0f * s.high_us / s.period_us + 0.5f));
|
||||
}
|
||||
inline void apply(const Stage &st) {
|
||||
apply_pin(GEN_PIN_A, st);
|
||||
apply_pin(GEN_PIN_B, st);
|
||||
}
|
||||
#endif
|
||||
inline void tick() {
|
||||
static int last = -1;
|
||||
int s = (millis() / STAGE_MS) % N;
|
||||
if (s == last)
|
||||
return;
|
||||
last = s;
|
||||
apply(STAGES[s]);
|
||||
ESP_LOGI("gen", "STAGE %d period %u high %u expect_per_min %.1f", s, STAGES[s].period_us, STAGES[s].high_us,
|
||||
60e6f / STAGES[s].period_us);
|
||||
}
|
||||
} // namespace pulsegen
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Sum the bw12 edge test `meas` lines per counter: counted pulses against generated rising edges."""
|
||||
|
||||
from collections import defaultdict
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
MEAS = re.compile(r"meas:\d+\]: (\w+) raw ([\d.]+) gen_rise (\d+)")
|
||||
BS = re.compile(r"BS changes (\d+) gen_edges (\d+) state (\d)")
|
||||
|
||||
|
||||
def summarize(path: str) -> None:
|
||||
counters: dict[str, list[float]] = defaultdict(lambda: [0.0, 0, 0])
|
||||
bs = [0, 0, 0]
|
||||
state = "?"
|
||||
seen: set[str] = set()
|
||||
with pathlib.Path(path).open(encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
if m := MEAS.search(line):
|
||||
key = m[1]
|
||||
if key not in seen: # the first window is partial
|
||||
seen.add(key)
|
||||
continue
|
||||
c = counters[key]
|
||||
c[0] += float(m[2]) / 6 # pulses per minute over a 10 s window
|
||||
c[1] += int(m[3])
|
||||
c[2] += 1
|
||||
elif m := BS.search(line):
|
||||
if "BS" not in seen:
|
||||
seen.add("BS")
|
||||
continue
|
||||
bs[0] += int(m[1])
|
||||
bs[1] += int(m[2])
|
||||
bs[2] += 1
|
||||
state = m[3]
|
||||
print(path.rsplit("/", 1)[-1])
|
||||
for key, (counted, rises, windows) in sorted(counters.items()):
|
||||
ratio = f"{counted / rises:.3f}" if rises else "n/a (no generated edges)"
|
||||
print(
|
||||
f" {key}: windows {windows} counted {counted:.0f} gen_rise {rises} ratio_to_rise {ratio}"
|
||||
)
|
||||
print(f" BS: windows {bs[2]} changes {bs[0]} gen_edges {bs[1]} last_state {state}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for arg in sys.argv[1:]:
|
||||
summarize(arg)
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Default ltchiptool's -L to the newest installed LibreTiny platform.
|
||||
|
||||
ltchiptool reads its chip list from ~/.platformio/platforms/libretiny, which can be an older copy than the one a
|
||||
build used (BK7238 UF2s then fail with "UF2 of unsupported family").
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def _version(platform: Path) -> tuple[int, ...]:
|
||||
try:
|
||||
text = json.loads((platform / "platform.json").read_text(encoding="utf-8"))[
|
||||
"version"
|
||||
]
|
||||
except (OSError, KeyError, ValueError):
|
||||
return ()
|
||||
# Numeric prefix of each part, so 1.9.0-dev still sorts
|
||||
return tuple(
|
||||
int(m[1]) for part in text.split(".")[:3] if (m := re.match(r"(\d+)", part))
|
||||
)
|
||||
|
||||
|
||||
def with_platform(args: list[str]) -> list[str]:
|
||||
if any(a.startswith(("-L", "--libretiny-path")) for a in args):
|
||||
return args
|
||||
found = list((Path.home() / ".platformio" / "platforms").glob("libretiny*"))
|
||||
platforms = [p for p in found if _version(p)]
|
||||
for p in found:
|
||||
if p not in platforms:
|
||||
print(
|
||||
f"lt_platform: skipped {p} (no numeric version in platform.json)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not platforms:
|
||||
print(
|
||||
f"lt_platform: no versioned LibreTiny platform in {[str(p) for p in found]}, using ltchiptool's default",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return args
|
||||
chosen = max(platforms, key=_version)
|
||||
print(f"lt_platform: using {chosen}", file=sys.stderr)
|
||||
return ["-L", str(chosen), *args]
|
||||
@@ -1,64 +0,0 @@
|
||||
# MCP23017 at 0x27 on an ESP8266 (Feather HUZZAH): SDA GPIO4, SCL GPIO5, INTA to GPIO14, INTB to GPIO13,
|
||||
# PA0 jumpered to PB0 and PA1 to PB1. The variant file adds the mcp23017 hub and mcptest. Substitution: name.
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp8266:
|
||||
board: huzzah
|
||||
|
||||
logger:
|
||||
baud_rate: 115200
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: ext
|
||||
components: [mcptest]
|
||||
|
||||
i2c:
|
||||
sda: GPIO4
|
||||
scl: GPIO5
|
||||
scan: false
|
||||
frequency: 100kHz
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: pb0
|
||||
name: PB0
|
||||
pin:
|
||||
mcp23xxx: mcp
|
||||
number: 8
|
||||
mode:
|
||||
input: true
|
||||
interrupt: CHANGE
|
||||
on_state:
|
||||
- lambda: id(tst).count_change();
|
||||
- platform: gpio
|
||||
id: pb1
|
||||
name: PB1
|
||||
pin:
|
||||
mcp23xxx: mcp
|
||||
number: 9
|
||||
mode:
|
||||
input: true
|
||||
interrupt: CHANGE
|
||||
on_state:
|
||||
- lambda: id(tst).count_change();
|
||||
|
||||
mcptest:
|
||||
id: tst
|
||||
address: 0x27
|
||||
label: ${name}
|
||||
isr_inta: ${isr_inta}
|
||||
pa0:
|
||||
mcp23xxx: mcp
|
||||
number: 0
|
||||
mode:
|
||||
output: true
|
||||
pa1:
|
||||
mcp23xxx: mcp
|
||||
number: 1
|
||||
mode:
|
||||
output: true
|
||||
pb0: pb0
|
||||
pb1: pb1
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Test only: drives PA0/PA1, reads IOCON and the INT lines, and checks the PB binary sensors."""
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "mcptest"
|
||||
mcptest_ns = cg.esphome_ns.namespace("mcptest")
|
||||
McpTest = mcptest_ns.class_("McpTest", cg.Component, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(McpTest),
|
||||
cv.Required("pa0"): pins.gpio_output_pin_schema,
|
||||
cv.Required("pa1"): pins.gpio_output_pin_schema,
|
||||
cv.Required("pb0"): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required("pb1"): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional("isr_inta", default=False): cv.boolean,
|
||||
cv.Optional("label", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x27))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(
|
||||
var.set_pins(
|
||||
await cg.gpio_pin_expression(config["pa0"]),
|
||||
await cg.gpio_pin_expression(config["pa1"]),
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
var.set_bs(
|
||||
await cg.get_variable(config["pb0"]), await cg.get_variable(config["pb1"])
|
||||
)
|
||||
)
|
||||
cg.add(var.set_opts(config["isr_inta"], config["label"]))
|
||||
Loaded 100 of 985 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user