mirror of
https://github.com/esphome/esphome.git
synced 2026-10-28 00:42:11 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3278ea5a04 | ||
|
|
361a9580da | ||
|
|
43cc2a8e3d | ||
|
|
281c5fcabc | ||
|
|
79c945c76c | ||
|
|
92c54aea77 |
No files matched your search
@@ -88,7 +88,6 @@ driven, what the logs show, and the same flow on a `dev` build when a quirk coul
|
||||
| ESP32-S3, C3, C6, H2 native USB | `esphome upload --device /dev/cu.usbmodem*` | `logger: hardware_uart: USB_SERIAL_JTAG`. The USB console can wedge when the app takes over the console (0 bytes, esptool `No serial data received`); replug, or watch through `esphome logs --device <name>.local` |
|
||||
| Pico W / Pico 2 W | `esphome upload --device <port>` (1200 baud touch); first flash ever via BOOTSEL and `--device /Volumes/RPI-RP2` or `RP2350` | port re-enumerates after upload: `ports.py --wait 2e8a` then capture |
|
||||
| BW15 kit (RTL8720CF, rtl87xx) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_rtl87xx_kit.py flash write -d <port> <build>/.pioenvs/<name>/*.uf2`. Plain `esphome upload` and plain ltchiptool time out because opening the port asserts DTR/RTS and holds the chip in reset | `serial_capture.py --released`, never `esphome logs` |
|
||||
| BW12 and other AmebaZ boards (RTL8710BX, rtl87xx) on a plain USB-UART | `~/.platformio/penv/.libretiny/bin/python -m ltchiptool flash write -B 115200 -b 115200 -t 120 -d <port> <uf2>`, started first, then strap by hand: CEN to GND, TX2 to GND, release CEN, release TX2. Without `-B 115200` the system data read at 1.5 Mbaud fails on CH340s (`Data read error at 0x9000`) and nothing is written. A 1 MB image takes about 80 s | `serial_capture.py`; a reset pulse does not reach CEN on a plain adapter, so a capture starts mid-run |
|
||||
| BK7238 / CB3S (bk72xx, RTS on CEN) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_bk72xx.py flash write -d <port> <uf2>` (about 75 s) | logs are on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter. `serial_capture.py --reset` gives a chip-enable reset; a plain open may or may not reset it, check for the bootloader banner |
|
||||
| LN882H | ltchiptool forces 1 Mbaud for writes; weak adapters need `LN882H_YM_BAUDRATE` patched to 115200 in the penv's `ln882htool.py`; short BOOT to GND while the tool connects | OTA after the first flash |
|
||||
| nRF52840 Dongle (PCA10059) | Nordic `nrfutil nrf5sdk-tools dfu usb-serial` on the bootloader port, or `dfu: true` in the config so the 1200 baud touch pin-resets into the bootloader; adafruit-nrfutil is refused | app has two CDC ports, ESPHome log on the second |
|
||||
@@ -123,8 +122,8 @@ Per-topic scripts, each described in the matching `reference/recipes/<component>
|
||||
(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), `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/` (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), `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 tools), `libretiny_prefs/` (rtc preference harness header), `libretiny_crash/` (BK72xx crash probe header and
|
||||
capture decoder), `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), `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/` (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).
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
|
||||
@@ -133,18 +133,6 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
- Callbacks run on the `async_tcp` task and log lines can lose a byte. Wi-Fi connect takes tens of ms;
|
||||
radio enable 160 to 278 ms, disable about 200 ms.
|
||||
|
||||
## BW12 and other AmebaZ boards (RTL8710BX, LibreTiny rtl87xx)
|
||||
|
||||
- Download mode is a hand strap: CEN to GND, TX2 to GND, release CEN, release TX2. A plain USB-UART
|
||||
does not drive CEN, so neither ltchiptool nor `serial_capture.py --reset` can reset the chip.
|
||||
- ltchiptool links at 1.5 Mbaud and reads the system data at 0x9000 at that rate before it writes at
|
||||
115200. A CH340 drops bytes there (`Data read error at 0x9000`, `Timed out waiting for Quiet-Handshake`)
|
||||
and the run gives up without writing. `-B 115200 -b 115200` moves the link to 115200 first and works.
|
||||
Start the command with `-t 120` so it is already polling while the board is strapped.
|
||||
- After a failed run the ROM may sit at another baud rate and no longer link; strap again.
|
||||
- The device builder frontend's AmebaZ flasher follows ltchiptool (link and system read at 1.5 Mbaud),
|
||||
so it can hit the same read failure on the same adapters.
|
||||
|
||||
## BW16 kit (RTL8720DN, AmebaD)
|
||||
|
||||
- The kit's own USB-C (CH340) is on LOG_UART and flashes fine. Its two-transistor circuit resets while
|
||||
|
||||
@@ -11,15 +11,15 @@ One file per component, named after it:
|
||||
- [`espnow.md`](espnow.md): ESP8266 broadcast receive
|
||||
- [`hdmi_cec.md`](hdmi_cec.md): two-board wire
|
||||
- [`http_request.md`](http_request.md): update manifest URL changed at run time, every setter shape, a swap under a running check, non-blocking request actions with loop gap, queue, cancel and reboot checks, OTA request cases with basic auth and MD5 sources
|
||||
- [`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
|
||||
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar
|
||||
- [`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
|
||||
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx and RTL8720C crash report on the next boot
|
||||
- [`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
|
||||
- [`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.md`](ota.md): overlap and lock, timeouts, deflate, upload timing A/B per build and board with deflate and raw, on-device inflate benchmark
|
||||
- [`rtsp_server.md`](rtsp_server.md): camera streaming on the Waveshare ESP32-S3-ETH
|
||||
- [`scheduler.md`](scheduler.md): loop wake A/B
|
||||
- [`udp.md`](udp.md): datagrams, multicast, sockets, `udp.write` payload shapes
|
||||
@@ -41,9 +41,3 @@ One file per component, named after it:
|
||||
- [`xiaomi_ble.md`](xiaomi_ble.md): Xiaomi, ATC and PVVX parsing by injecting scan results into the ESP32 tracker or advertising them from a second ESP32, real AES-CCM frames, duplicate filter model
|
||||
- [`ble_terminate.md`](ble_terminate.md): proxy disconnect against a peer with peripheral latency
|
||||
- [`i2c_slave.md`](i2c_slave.md): fake I2C slave on a D1 mini, ESP32 hardware master as the control
|
||||
- [`gpio_hold.md`](gpio_hold.md): GPIO hold through the ISR pin API, pad read back with no wiring, per chip pin notes
|
||||
- [`ble_event_inject.md`](ble_event_inject.md): made up GAP and GATTC events fed into the ESP32 BLE handlers from a button
|
||||
- [`light_sleep_gpio_wake.md`](light_sleep_gpio_wake.md): GPIO wakeup from auto light sleep with an LEDC wave that keeps running in sleep
|
||||
- [`manual_ip_and_web_handlers.md`](manual_ip_and_web_handlers.md): station, AP and Ethernet manual IP, web handlers added late and web OTA with web_server, captive_portal or prometheus
|
||||
- [`ble_server_security.md`](ble_server_security.md): esp32_ble_server pairing, encrypted and MITM characteristics, subscription before and after pairing, bond reuse per ESP-IDF version
|
||||
- [`prometheus.md`](prometheus.md): date, time and datetime metrics in UTC and another time zone
|
||||
@@ -1,20 +0,0 @@
|
||||
# Injecting BLE GAP and GATTC events (#21698)
|
||||
|
||||
Some BLE paths (repeated connection parameter update failures, a disconnect in the middle of them) are hard to get
|
||||
from a real peer on demand. `scripts/ble_event_inject` feeds made up events straight into the ESP32 BLE handlers.
|
||||
|
||||
- `ESP32BLE::gap_event_handler` and `gattc_event_handler` are protected statics and the class is `final`.
|
||||
`cp_inject.hpp` reaches them through explicit template instantiation, where access checks do not apply. Its
|
||||
`cpinject::run()` sends six labelled steps (`STEP1` to `STEP6`): four peers failing five times each, a success
|
||||
followed by more failures, and disconnects between failures.
|
||||
- `cp-inject.yaml` is a package for a copy of a proxy config: it adds the include, a VERBOSE logger with the busy
|
||||
tags back at DEBUG, and an `Inject conn param events` button. Press it with `scripts/api_button.py` while a
|
||||
log capture (`esphome logs ... --device <ip>` under `timeout`) runs, then count the warning and verbose lines per
|
||||
step on the PR and on dev.
|
||||
- The handlers push into a queue and event pool that take a single writer, normally the Bluetooth task. Injecting
|
||||
from the main task while real events arrive can corrupt them, so the button stops the scan, waits for it to end,
|
||||
injects and starts it again. Run it with no active proxy connections (no Home Assistant BLE client holding one).
|
||||
- Fake disconnects use `gattc_if` 0xFE: every client and proxy connection filters on its own `gattc_if_`, so the
|
||||
queued event reaches only the shared handler. Do not use 0xFF (`ESP_GATT_IF_NONE`), which every client accepts.
|
||||
- The fake peers (`C0:FF:EE:00:00:0n`) are never connected, so a live proxy keeps serving its real connections;
|
||||
put it back on its own config afterwards.
|
||||
@@ -1,26 +0,0 @@
|
||||
# esp32_ble_server pairing and encrypted characteristics (#17562)
|
||||
|
||||
`scripts/ble_server_security`: a classic ESP32 devkit runs the secure GATT server (`server.yaml`) and a second ESP32
|
||||
(an S3 here) runs `ble_client` as the central (`client.yaml`). The server has a `read_encrypt` battery level
|
||||
(`99.9`), a plain characteristic, a `write_encrypt` characteristic that logs what it receives, a `read_encrypt_mitm`
|
||||
characteristic and a `read_encrypt` characteristic that notifies every 2 s. `run.sh TAG IO AUTH` builds both with the
|
||||
pairing mode as substitutions, pairs once, reconnects twice, and `summary.py` prints PASS or FAIL. The server needs
|
||||
`esp32_ble_server` security from #17562 or later: point `SERVER_TREE` at that checkout (the client builds from
|
||||
`TREE`, dev is fine). `summary.py` needs each refused read to show an `Error reading char` reply, so a run where the
|
||||
client never connected fails instead of passing.
|
||||
|
||||
- `ble_client` never starts pairing on its own (a phone does, after an `insufficient authentication` reply); the
|
||||
client's `Pair` button calls `BLEClientBase::pair()`. Read errors show as `Error reading char at handle N, status=5`
|
||||
(not authenticated: no bond) or `status=15` (not encrypted: bond known, link not encrypted yet).
|
||||
- The `ble_client` text sensor subscribes once, at connect, before any pairing, so a protected characteristic refuses
|
||||
it. The `Subscribe` button writes the CCCD again after pairing; `ble_client` drops its service list after discovery,
|
||||
so it writes the characteristic handle + 1 (where ESP-IDF's GATT server puts the CCCD).
|
||||
- Numeric comparison (`display_yes_no` with `sc_mitm_bond`) tells a fresh pairing from a bond reuse: the
|
||||
`numeric comparison` line only appears on a fresh pairing. Just Works cannot show it.
|
||||
- ESP-IDF 5.5.6 drops the bond on the second disconnect on both roles (`Deleting unpaired device`), so that reconnect
|
||||
pairs from scratch; 5.5.5 keeps it (`-s idf 5.5.5`).
|
||||
- The server keeps its bonds in NVS. One holding an authenticated bond for the client refuses a Just Works re-pairing
|
||||
silently; erase the devkit's flash before switching from nc to jw.
|
||||
- `summary.py` failed both modes at 58636914ab: `read_encrypt_mitm` was readable with no pairing, because
|
||||
`BLEService::create_characteristic()` took an 8 bit `esp_gatt_char_prop_t` and dropped the MITM bits (1 << 8 and
|
||||
1 << 9). Check the generated `create_characteristic` call and the property type when a flag above bit 7 is added.
|
||||
@@ -15,13 +15,3 @@ API reply and says nothing. On dev the S3 lost 6 of 12; the PR lost 0 of 16.
|
||||
and `an.py`, which prints PASS only when every round completed, every peer disconnect came within 3 s and the last
|
||||
parameter update the proxy logged before each disconnect had latency=10 (a round without it did not test peripheral
|
||||
latency and counts as inconclusive).
|
||||
|
||||
Controller build check: ESP-IDF 5.5.6 ships `libbtdm_app.a` byte for byte the same as 5.5.5 for esp32c3 and esp32s3
|
||||
(build 51d9dfd). Without hardware:
|
||||
|
||||
```bash
|
||||
strings -a <idf>/components/bt/controller/lib_esp32c3_family/<chip>/libbtdm_app.a | grep -E '^[0-9a-f]{7}$'
|
||||
```
|
||||
|
||||
On a board: a button lambda logging `btdm_controller_get_compile_version()` (declared `extern "C"`) over the API,
|
||||
since the ESP-IDF init line only reaches the UART.
|
||||
@@ -1,17 +0,0 @@
|
||||
# ESP32 GPIO hold through the ISR pin API (#21576)
|
||||
|
||||
`scripts/gpio_hold/hold_test.hpp` builds `ESP32InternalGPIOPin`s by hand, calls `to_isr()` and reads each pad back
|
||||
with `gpio_get_level`: 20 ISR writes, a raw `gpio_set_level` control (a held pad must not change, an unheld one must)
|
||||
and ISR `pin_mode` pulls (input must drop to `NONE` between pull changes, because the ISR path only applies pulls when
|
||||
`FLAG_INPUT` toggles). It needs no wiring: unconnected pins read the pulls. Each pin runs held and unheld, then the
|
||||
round logs `ROUND n PASS failures=0` or `FAIL`.
|
||||
|
||||
- `USE_GPIO_HOLD` comes from any pin with `hold_state: true`, so the configs add a `gpio` output on a spare pin.
|
||||
- `gpio-hold-esp32.yaml` (classic devkit) runs three rounds 5 s after boot and has a `rerun` button;
|
||||
`gpio-hold-esp32s3.yaml` (Waveshare ESP32-S3-ETH, where most pins go to the W5500, TF card, camera, WS2812 or
|
||||
USB) runs a round every 15 s.
|
||||
- C3: ESP-IDF 5.5 has `SOC_RTCIO_PIN_COUNT` 0, so `rtc_gpio_is_valid_gpio()` is false on every pin and only the
|
||||
digital hold path runs.
|
||||
- C6 (Waveshare ESP32-C6-GEEK, native USB): LP pads GPIO0 to 7 are RTC IO with hold. GPIO1, 2, 7, 20, 23 read high
|
||||
and GPIO6 low whatever the pull (LCD, TF card or resistors); clean pins are 0, 3, 4, 5, 18, 19, 21, 22. Dev
|
||||
already holds LP pads through `gpio_hal_hold_en`, so the PR and dev look the same there. H2 is untested.
|
||||
@@ -40,29 +40,3 @@ least 24 selects per build and read the raw bytes with `uart: debug: direction:
|
||||
id) before blaming the parser; the API log stream drops lines under that dump volume, so some misses stay
|
||||
unclassified.
|
||||
Results: https://github.com/esphome/esphome/pull/21357#issuecomment-6333244886
|
||||
|
||||
## Frame stream harness: presence hold and link loss (#21733, #21734)
|
||||
|
||||
`scripts/ld2450/stream-harness.yaml` is a loopback harness that feeds the hub one periodic frame every 100 ms from a
|
||||
mode global set by API buttons: no target, target 1 approaching, silent, or 12 garbage bytes per tick (what a radar
|
||||
at another baud rate looks like). `has_target` has `filters: []` so the hub's own hold is what gets timed, and every
|
||||
change logs `presence raw ON|OFF at <millis>`. Add `stream-connected.yaml` as a second package on builds whose ld2450
|
||||
has the link watchdog (`connected` binary sensor).
|
||||
|
||||
- `holddrive.py HOST OUT`: for `presence_timeout` 5 and 0, three seconds of target frames, then empty frames; prints
|
||||
the ms from the press to the off line, then sets 0 and 20 and reads the number back after a restart. On dev 0
|
||||
still held 5 s and snapped back to 5; with #21733 it clears on the next empty frame (under 200 ms) and stays 0.
|
||||
- `linkdrive.py HOST OUT`: steps through target frames, 10 s of silence, empty frames, 10 s of garbage, target
|
||||
frames, then the hub's radar restart followed by 3.5 s and by 15 s of silence; prints every state change and link
|
||||
log line with its time. Dev freezes every entity through silence and garbage. Run against a real radar too: a
|
||||
radar restart and a Bluetooth toggle must not report a loss (the Apollo R PRO-1 ETH did not).
|
||||
- A freshly booted ESP8266 delays API commands sent in its first seconds and then runs them back to back; the
|
||||
drivers wait 8 s after connecting before timed presses.
|
||||
|
||||
## BT MAC sensors (#21731)
|
||||
|
||||
`scripts/ld2450/macdrive.py HOST OUT` reads every LD24xx BT MAC, Bluetooth and firmware entity, turns each Bluetooth
|
||||
switch off, then on, and prints the values after each step. The MAC sensors are often `disabled_by_default` in
|
||||
vendor configs but still publish over the API. On the Apollo R PRO-1 ETH both radars (LD2450 fw 2.04, LD2412 fw
|
||||
1.26) have the multicast bit set in the first MAC byte, which dev showed as `unknown`. A radar MAC identifies a
|
||||
device: quote only its first byte in PR text.
|
||||
@@ -71,31 +71,3 @@ The `restart` after the first crash checks the report shows only once. Time each
|
||||
(the ROM fault dump to the next `ROM Version` line), not from the API port: the board is back before an API client
|
||||
notices it went away. Compare the reported PC with the `PC =` line of the ROM dump, and check the decoded lines in the
|
||||
`caseN-*.txt` files. Pass the UF2 by name (`firmware.uf2`): the build directory holds a second identically sized copy.
|
||||
|
||||
Regression rerun of the same flow (#21728): `scripts/libretiny_crash/rtl_rom_vs_report.py CAPTURE` lists, for each ROM
|
||||
fault dump in a `--ms` capture, the ROM's CFSR and PC, the ms to the next `ROM Version` banner and the Reason, CFSR, PC
|
||||
and SP the next boot reported, so two builds compare line by line. The ROM lines start with a bare CR; split the
|
||||
capture on newlines only. The imprecise bus fault's PC moves between builds (it is wherever the core was when the
|
||||
fault was raised); compare it with that run's ROM dump, not with another build. SP stays the same per fault (8 word
|
||||
frame, no FPU). On the BK7238 time the reboot from the button press to the next `BK7238_1.0.14` banner (0.6 to 0.7 s).
|
||||
|
||||
## BK7238 BLE and flash sector safety (#21735)
|
||||
|
||||
The Beken BLE bond store reads its partition at boot and erases that sector when the CRC does not match; before
|
||||
LibreTiny 1.14 the partition was unmapped and pointed at sector 0, erasing the bootloader (#18646). To prove a change
|
||||
like this safe on a board that cannot be recovered without a dump:
|
||||
|
||||
1. Take a full backup first: `scripts/bk72xx_flash/readback.sh full-before` (ltchiptool family `beken-7238`, about
|
||||
190 s for 2 MB). Keep it until the board is back on a known build.
|
||||
2. Serial flash a build without the feature, then deliver the feature build by OTA (the path users take). Use
|
||||
`bk72xx: framework: sdk_silent: none` with `logger: hardware_uart: UART1`, so the SDK's own lines (bootloader
|
||||
banner, `CRC fail, remove bonded info`) show up in the capture.
|
||||
3. Soak with Wi-Fi up, OTA again from the new build, pulse CEN a few times (`serial_capture.py --reset`), and check
|
||||
each boot comes back through the bootloader.
|
||||
4. Read the whole chip again and run `scripts/bk72xx_flash/partdiff.py full-before.bin full-after.bin
|
||||
<libretiny>/boards/_base/beken-72xx.json`: bootloader, calibration and userdata must be unchanged; app, download
|
||||
and one KVS sector change normally.
|
||||
|
||||
Check what already sits in the target partition before the first boot: on an XH-WB3S that shipped with Tuya firmware,
|
||||
0x1E1000 (the `ble_bonding` partition of the generic Beken layout) held the Tuya `diff2ya` record (magic `ba dc cd
|
||||
ab`), which the bond store erased on its first boot. Nothing depended on it there, but say so in the results.
|
||||
@@ -1,16 +0,0 @@
|
||||
# GPIO wakeup from auto light sleep (#21074)
|
||||
|
||||
`scripts/light_sleep_wake` needs no wiring. LEDC on `LEDC_USE_RC_FAST_CLK` with
|
||||
`sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE` keeps toggling GPIO10 at 2 Hz through light sleep, and an
|
||||
`ESP32InternalGPIOPin` interrupt (`ANY_EDGE`) on the same pad timestamps every edge in an IRAM handler.
|
||||
`wakeprobe::report()` logs every 30 s: edges seen and expected, the shortest and longest gap, how many edges came
|
||||
more than 20 ms off the half period, and the SLEEP share from `esp_pm_dump_locks()` (needs `profiling: true`).
|
||||
|
||||
- Native ESP-IDF builds need `include_builtin_idf_components: [esp_driver_ledc]`.
|
||||
- After `ledc_channel_config` call `gpio_input_enable(pin)`, never `gpio_set_direction(INPUT_OUTPUT)`: that routes the
|
||||
pad back to a plain GPIO output, the wave stops and the pad reads a constant 1.
|
||||
- The C3 shares one LEDC clock across timers, so a board whose own config drives LEDs through `ledc` needs a test
|
||||
config without them. Logging over the API works with `logger: baud_rate: 0`.
|
||||
- Read the reports over the API with `esphome logs ... --device <ip>` under `timeout`. Run PR, dev, then the PR
|
||||
again. On a C3 bulb dev caught every edge but handled 8 of 845 more than 20 ms late
|
||||
(up to 45 ms), the PR none; both slept 57 to 58% of the time.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Manual IP and web handler flows (#21708, #21709, #21711, #21712)
|
||||
|
||||
## Station manual IP (`scripts/manual_ip/run-sta.sh`)
|
||||
|
||||
Set `static_ip` to the board's own DHCP lease so nothing collides, and DNS2 to a server DHCP does not hand out
|
||||
(9.9.9.9), so the dump and `dns_getserver()` prove the manual values are in use. A missing SSID with its own
|
||||
`manual_ip` comes first, so the real network is a later table row and inherits the top level `manual_ip`. The probe
|
||||
logs both DNS servers and resolves a name every 20 s; `ipaddr_ntoa` uses one static buffer, so it uses
|
||||
`ipaddr_ntoa_r` to log two addresses in one line. PASS needs the address, both DNS servers, a lookup and a ping.
|
||||
|
||||
## AP manual IP (`scripts/manual_ip/run-ap.sh`)
|
||||
|
||||
The DUT has only a missing SSID, `ap_timeout: 10s` and `captive_portal:`, so its AP comes up on 10.42.7.1; the other
|
||||
board joins as a client and fetches `http://10.42.7.1/` every 10 s. A D1 mini AP drops some client requests on dev
|
||||
too, so compare failure counts with a dev run and do not expect zero.
|
||||
|
||||
## Ethernet manual IP
|
||||
|
||||
Same idea on an Ethernet board: its lease as `manual_ip` plus an outside DNS2 and the DNS probe from
|
||||
`scripts/manual_ip/dnsprobe.hpp`; restart it over the API and check again.
|
||||
|
||||
## Web handlers (`scripts/web_handlers/run.sh`)
|
||||
|
||||
`webbase.yaml` adds a GET /probe handler from `on_boot` at priority -100 (the shape of an external component adding
|
||||
a handler late) and takes a variant: `ws` (web_server), `cp` (captive_portal only, started on the station network by
|
||||
a button calling `global_captive_portal->start()`) or `prom` (captive_portal and prometheus). The runner fetches
|
||||
/probe and /, then uploads the same image through web OTA (`curl -F update=@<bin> http://ip/update`; ESP-IDF
|
||||
`build/<name>.bin`, not `firmware.ota.bin`; ESP8266 `.pioenvs/<name>/firmware.bin`) and checks over the API that the
|
||||
board is back on that build. On dev too, prometheus GET / gets no answer on ESP32 and 404 on ESP8266.
|
||||
|
||||
A background `(cd dir && cmd) &` does not change the parent shell's directory: give capture scripts absolute paths.
|
||||
@@ -54,16 +54,3 @@ over syslog and fails when a case did not reach the end of its stream.
|
||||
to `dest`); after that lands, remove the `dest_limit` line.
|
||||
|
||||
Results: https://github.com/esphome/esphome/issues/21639#issuecomment-6409786224
|
||||
|
||||
## Upload without a configuration (#21738)
|
||||
|
||||
`esphome upload --file IMAGE --device IP` with no YAML picks the OTA port from the image header and takes the password
|
||||
from `--ota-password` or `ESPHOME_OTA_PASSWORD` and the key from `ESPHOME_OTA_KEY`. Test each chip family with its own
|
||||
current image (`build/firmware.ota.bin` for ESP-IDF, `.pioenvs/<name>/firmware.bin` for ESP8266, `firmware.uf2` for
|
||||
LibreTiny) and check the build over the API; switch one board between two builds to show a new image lands.
|
||||
|
||||
Point every case that should be refused before connecting at an unroutable address (192.0.2.1), never at a board: a
|
||||
refusal prints no `Connecting to` line, and an image that slips through cannot reach a device. An ESP32-S3
|
||||
`firmware.factory.bin` slipped through on 2026-10-27 (its bootloader header at offset 0 looks like an app), the device
|
||||
reported `OTA successful` and its bootloader rejected the slot at boot; send the board its proper app image afterwards.
|
||||
A factory image has the partition table magic `AA 50` at 0x8000 and the app at 0x10000.
|
||||
@@ -1,9 +0,0 @@
|
||||
# prometheus
|
||||
|
||||
## Date, time and datetime metrics (#9941)
|
||||
|
||||
`scripts/prometheus/prom-datetime.yaml` is a D1 mini with `web_server`, `prometheus` and three optimistic template
|
||||
datetime entities; `prom-datetime-tz.yaml` adds an sntp time component with `HST10` (the zone applies without a sync).
|
||||
`datetime_metrics.py HOST` prints the rows of `/metrics`, sets the entities over the API to 2040 values (past 2038 on
|
||||
purpose) and prints them again with their UTC reading. Dates are local midnight in ms, times ms since midnight,
|
||||
datetimes ms since the epoch, so a datetime set as an instant reads the same in both zones.
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Compare two full flash dumps of a BK72xx board per partition of its LibreTiny board layout.
|
||||
|
||||
usage: partdiff.py BEFORE.bin AFTER.bin LAYOUT.json
|
||||
LAYOUT.json is a board base file from the LibreTiny platform (for example boards/_base/beken-72xx.json); its
|
||||
"flash" map ("name": "0xSTART+0xLENGTH") names the partitions. Prints the 4 KB sectors that changed in each one,
|
||||
plus any changed sector outside the map.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
before, after = (pathlib.Path(p).read_bytes() for p in sys.argv[1:3])
|
||||
if len(before) != len(after):
|
||||
sys.exit(
|
||||
f"dump sizes differ: {len(before)} and {len(after)} bytes; compare two whole chip reads"
|
||||
)
|
||||
parts = []
|
||||
for name, span in json.loads(pathlib.Path(sys.argv[3]).read_text(encoding="utf-8"))[
|
||||
"flash"
|
||||
].items():
|
||||
start, length = (int(x, 16) for x in span.split("+"))
|
||||
parts.append((start, start + length, name))
|
||||
parts.sort()
|
||||
if (end := max(e for _, e, _ in parts)) > len(before):
|
||||
sys.exit(
|
||||
f"the layout reaches {end:#x} but the dumps are {len(before):#x} bytes; read the whole chip"
|
||||
)
|
||||
SECTOR = 0x1000
|
||||
covered = set()
|
||||
for start, end, name in parts:
|
||||
changed = [
|
||||
a
|
||||
for a in range(start, end, SECTOR)
|
||||
if before[a : a + SECTOR] != after[a : a + SECTOR]
|
||||
]
|
||||
covered.update(range(start, end, SECTOR))
|
||||
listed = " ".join(hex(a) for a in changed[:4]) if 0 < len(changed) <= 4 else ""
|
||||
print(
|
||||
f"{name:12} {len(changed):4} of {(end - start) // SECTOR:4} sectors changed {listed}"
|
||||
)
|
||||
other = [
|
||||
a
|
||||
for a in range(0, len(before), SECTOR)
|
||||
if a not in covered and before[a : a + SECTOR] != after[a : a + SECTOR]
|
||||
]
|
||||
print(
|
||||
f"{'unmapped':12} {len(other):4} sectors changed {' '.join(hex(a) for a in other[:8])}"
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
# usage: readback.sh TAG [START LENGTH] read flash from a BK72xx over its adapter into TAG.bin (whole chip by default)
|
||||
# env: PORT (adapter), FAMILY (ltchiptool family, default beken-7238), FLASH_BK (path to flash_bk72xx.py),
|
||||
# LT (LibreTiny penv python, default ~/.platformio/penv/.libretiny/bin/python), SIZE (whole chip bytes, default
|
||||
# 2097152). A whole 2 MB chip takes about 190 s. The read must come back at the expected size.
|
||||
T=${1:?tag}; P=${PORT:?set PORT}; FB=${FLASH_BK:?set FLASH_BK}; LT=${LT:-$HOME/.platformio/penv/.libretiny/bin/python}
|
||||
ARGS=(); [ -n "$2" ] && ARGS=(-s "$2" -l "${3:?length}")
|
||||
WANT=$(( ${3:-${SIZE:-2097152}} ))
|
||||
: > "$T.log"
|
||||
ok=
|
||||
for i in 1 2 3; do
|
||||
echo "### try $i" >> "$T.log"
|
||||
rm -f "$T.bin"
|
||||
timeout 900 "$LT" "$FB" flash read -d "$P" "${ARGS[@]}" "${FAMILY:-beken-7238}" "$T.bin" >> "$T.log" 2>&1 &&
|
||||
[ "$(wc -c < "$T.bin" | tr -d ' ')" = "$WANT" ] && ok=1 && break
|
||||
sleep 3
|
||||
done
|
||||
[ -n "$ok" ] || { echo "FAIL: read $T (expected $WANT bytes, see $T.log)"; exit 1; }
|
||||
shasum -a 256 "$T.bin"
|
||||
@@ -1,39 +0,0 @@
|
||||
# Add to a copy of a bluetooth_proxy config: packages: {cp_inject: !include cp-inject.yaml}
|
||||
esphome:
|
||||
includes:
|
||||
- cp_inject.hpp
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
logs:
|
||||
esp32_ble_tracker: DEBUG
|
||||
bluetooth_proxy: DEBUG
|
||||
bluetooth_connection: DEBUG
|
||||
api: DEBUG
|
||||
api.service: DEBUG
|
||||
api.connection: DEBUG
|
||||
api.socket: DEBUG
|
||||
scheduler: DEBUG
|
||||
sensor: DEBUG
|
||||
component: DEBUG
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Inject conn param events
|
||||
# The handlers push into a queue that takes one writer only, the Bluetooth task. Stop the scan (and keep the
|
||||
# proxy free of real connections) so nothing else writes while the button does.
|
||||
on_press:
|
||||
- esp32_ble_tracker.stop_scan:
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return !esp32_ble_tracker::global_esp32_ble_tracker->scan_running();
|
||||
timeout: 5s
|
||||
- delay: 1s
|
||||
- lambda: |-
|
||||
if (esp32_ble_tracker::global_esp32_ble_tracker->scan_running()) {
|
||||
ESP_LOGE("cpinject", "scan still running after 5 s, nothing injected");
|
||||
return;
|
||||
}
|
||||
cpinject::run();
|
||||
- esp32_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
@@ -1,68 +0,0 @@
|
||||
#pragma once
|
||||
// Feeds fabricated conn param update and disconnect events into ESP32BLE's static handlers.
|
||||
// The event queue takes a single writer: call run() only while scanning is stopped and no connection is active.
|
||||
#include <cstring>
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace cpinject {
|
||||
using GapFn = void (*)(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
|
||||
using GattcFn = void (*)(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *);
|
||||
GapFn steal_gap();
|
||||
GattcFn steal_gattc();
|
||||
// Explicit instantiation ignores access checks, which exposes the protected static handlers.
|
||||
template<GapFn F> struct GapThief {
|
||||
friend GapFn steal_gap() { return F; }
|
||||
};
|
||||
template<GattcFn F> struct GattcThief {
|
||||
friend GattcFn steal_gattc() { return F; }
|
||||
};
|
||||
template struct GapThief<&esphome::esp32_ble::ESP32BLE::gap_event_handler>;
|
||||
template struct GattcThief<&esphome::esp32_ble::ESP32BLE::gattc_event_handler>;
|
||||
|
||||
static const char *const TAG = "cpinject";
|
||||
|
||||
inline void peer(uint8_t n, uint8_t *bda) {
|
||||
const uint8_t base[6] = {0xC0, 0xFF, 0xEE, 0x00, 0x00, 0x00};
|
||||
memcpy(bda, base, 6);
|
||||
bda[5] = n;
|
||||
}
|
||||
|
||||
inline void update(uint8_t n, int status, int times) {
|
||||
for (int i = 0; i < times; i++) {
|
||||
esp_ble_gap_cb_param_t p{};
|
||||
peer(n, p.update_conn_params.bda);
|
||||
p.update_conn_params.status = (esp_bt_status_t) status;
|
||||
p.update_conn_params.conn_int = 24;
|
||||
p.update_conn_params.timeout = 400;
|
||||
steal_gap()(ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT, &p);
|
||||
}
|
||||
}
|
||||
|
||||
inline void disconnect(uint8_t n) {
|
||||
esp_ble_gattc_cb_param_t p{};
|
||||
peer(n, p.disconnect.remote_bda);
|
||||
p.disconnect.reason = ESP_GATT_CONN_TERMINATE_PEER_USER;
|
||||
p.disconnect.conn_id = 0xEE;
|
||||
// 0xFE matches no registered client, so the queued event is ignored by every connection.
|
||||
steal_gattc()(ESP_GATTC_DISCONNECT_EVT, 0xFE, &p);
|
||||
}
|
||||
|
||||
inline void run() {
|
||||
ESP_LOGI(TAG, "STEP1 peers 1-4 fail 5x each (3 slots)");
|
||||
for (uint8_t n = 1; n <= 4; n++)
|
||||
update(n, 6, 5);
|
||||
ESP_LOGI(TAG, "STEP2 peer 1 succeeds, then fails 3x");
|
||||
update(1, 0, 1);
|
||||
update(1, 6, 3);
|
||||
ESP_LOGI(TAG, "STEP3 peer 2 disconnects, then fails 3x");
|
||||
disconnect(2);
|
||||
update(2, 6, 3);
|
||||
ESP_LOGI(TAG, "STEP4 peer 4 fails 3x, slots still full");
|
||||
update(4, 6, 3);
|
||||
ESP_LOGI(TAG, "STEP5 peer 3 disconnects, peer 4 fails 3x");
|
||||
disconnect(3);
|
||||
update(4, 6, 3);
|
||||
ESP_LOGI(TAG, "STEP6 done");
|
||||
}
|
||||
} // namespace cpinject
|
||||
@@ -1,155 +0,0 @@
|
||||
# Waveshare ESP32-S3 (any ESP32 with BLE) as the central. run.sh passes name, io, auth, idf and server (the
|
||||
# server's BLE MAC from its esp32_ble dump) with -s. ble_client never pairs by itself: the Pair button calls pair().
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: ${idf}
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
level: VERBOSE
|
||||
logs:
|
||||
esp32_ble_tracker: DEBUG
|
||||
api: DEBUG
|
||||
api.connection: DEBUG
|
||||
api.service: DEBUG
|
||||
api.socket: DEBUG
|
||||
scheduler: DEBUG
|
||||
component: DEBUG
|
||||
wifi: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
esp32_ble:
|
||||
io_capability: ${io}
|
||||
auth_req_mode: ${auth}
|
||||
|
||||
esp32_ble_tracker:
|
||||
|
||||
ble_client:
|
||||
- mac_address: ${server}
|
||||
id: cli
|
||||
on_connect:
|
||||
- logger.log: "CLI connected"
|
||||
on_disconnect:
|
||||
- logger.log: "CLI disconnected"
|
||||
on_numeric_comparison_request:
|
||||
- logger.log:
|
||||
format: "CLI numeric comparison %06u"
|
||||
args: ["(unsigned) passkey"]
|
||||
- ble_client.numeric_comparison_reply:
|
||||
id: cli
|
||||
accept: true
|
||||
|
||||
text_sensor:
|
||||
- platform: ble_client
|
||||
ble_client_id: cli
|
||||
id: secure_value
|
||||
name: Secure value
|
||||
service_uuid: "180F"
|
||||
characteristic_uuid: "2A19"
|
||||
update_interval: never
|
||||
on_value:
|
||||
- logger.log:
|
||||
format: "CLI secure read: %s"
|
||||
args: ["x.c_str()"]
|
||||
- platform: ble_client
|
||||
ble_client_id: cli
|
||||
id: open_value
|
||||
name: Open value
|
||||
service_uuid: 5ec50000-0000-4000-8000-00000000f000
|
||||
characteristic_uuid: 5ec50000-0000-4000-8000-00000000f001
|
||||
update_interval: never
|
||||
on_value:
|
||||
- logger.log:
|
||||
format: "CLI open read: %s"
|
||||
args: ["x.c_str()"]
|
||||
|
||||
- platform: ble_client
|
||||
ble_client_id: cli
|
||||
id: mitm_value
|
||||
name: Mitm value
|
||||
service_uuid: 5ec50000-0000-4000-8000-00000000f000
|
||||
characteristic_uuid: 5ec50000-0000-4000-8000-00000000f004
|
||||
update_interval: never
|
||||
on_value:
|
||||
- logger.log:
|
||||
format: "CLI mitm read: %s"
|
||||
args: ["x.c_str()"]
|
||||
- platform: ble_client
|
||||
ble_client_id: cli
|
||||
id: notify_value
|
||||
name: Notify value
|
||||
service_uuid: 5ec50000-0000-4000-8000-00000000f000
|
||||
characteristic_uuid: 5ec50000-0000-4000-8000-00000000f003
|
||||
notify: true
|
||||
on_value:
|
||||
- logger.log:
|
||||
format: "CLI notification: %s"
|
||||
args: ["x.c_str()"]
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Subscribe
|
||||
on_press:
|
||||
- lambda: |-
|
||||
// ble_client drops its service list after discovery; ESP-IDF's GATT server puts the CCCD right after
|
||||
// the characteristic value, so write handle + 1
|
||||
uint16_t chr = id(notify_value)->handle;
|
||||
if (chr == 0) { ESP_LOGW("CLI", "notify characteristic handle unknown"); return; }
|
||||
esp_ble_gattc_register_for_notify(id(cli)->get_gattc_if(), id(cli)->get_remote_bda(), chr);
|
||||
uint8_t on[2] = {1, 0};
|
||||
auto err = esp_ble_gattc_write_char_descr(id(cli)->get_gattc_if(), id(cli)->get_conn_id(), chr + 1, 2, on,
|
||||
ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
ESP_LOGI("CLI", "CCCD write to handle %u (char %u) sent, err %d", chr + 1, chr, err);
|
||||
- platform: template
|
||||
name: Pair
|
||||
on_press:
|
||||
- lambda: 'ESP_LOGI("CLI", "pair() returned %d", (int) id(cli)->pair());'
|
||||
- platform: template
|
||||
name: Read secure
|
||||
on_press:
|
||||
- component.update: secure_value
|
||||
- platform: template
|
||||
name: Read mitm
|
||||
on_press:
|
||||
- component.update: mitm_value
|
||||
- platform: template
|
||||
name: Read open
|
||||
on_press:
|
||||
- component.update: open_value
|
||||
- platform: template
|
||||
name: Write secure
|
||||
on_press:
|
||||
- ble_client.ble_write:
|
||||
id: cli
|
||||
service_uuid: 5ec50000-0000-4000-8000-00000000f000
|
||||
characteristic_uuid: 5ec50000-0000-4000-8000-00000000f002
|
||||
value: [0x68, 0x65, 0x6C, 0x6C, 0x6F]
|
||||
- platform: template
|
||||
name: Disconnect
|
||||
on_press:
|
||||
- ble_client.disconnect: cli
|
||||
- platform: template
|
||||
name: Connect
|
||||
on_press:
|
||||
- ble_client.connect: cli
|
||||
- platform: template
|
||||
name: Remove bond
|
||||
on_press:
|
||||
- ble_client.remove_bond:
|
||||
id: cli
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Press the client's buttons in order and log every client line about BLE security.
|
||||
|
||||
usage: drive.py CLIENT_HOST OUT STEP... STEP: a button name, or wait:SECONDS env: API_KEY
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import APIClient, ButtonInfo, LogLevel
|
||||
|
||||
HOST, OUT, STEPS = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
LOG = pathlib.Path(OUT).open("w", encoding="utf-8") # noqa: SIM115 open for the whole run
|
||||
PATTERN = re.compile(
|
||||
r"CLI|ble_client|ble_text_sensor|esp32_ble_client|[Aa]uth|[Ee]ncrypt|[Bb]ond|Error|status=|pair"
|
||||
)
|
||||
|
||||
|
||||
def note(s: str) -> None:
|
||||
line = f"{time.strftime('%H:%M:%S')} {s}"
|
||||
print(line)
|
||||
LOG.write(line + "\n")
|
||||
LOG.flush()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = APIClient(HOST, 6053, None, noise_psk=os.environ.get("API_KEY"))
|
||||
for attempt in range(40):
|
||||
try:
|
||||
await c.connect(login=True)
|
||||
break
|
||||
except Exception as err: # noqa: BLE001
|
||||
last_err = err
|
||||
note(f"connect attempt {attempt + 1} failed: {err!r}")
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
raise last_err
|
||||
ents, _ = await c.list_entities_services()
|
||||
btn = {e.name: e.key for e in ents if isinstance(e, ButtonInfo)}
|
||||
|
||||
def on_log(m) -> None:
|
||||
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
|
||||
if PATTERN.search(t):
|
||||
note("LOG " + t.strip().replace("\n", " | "))
|
||||
|
||||
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
|
||||
for step in STEPS:
|
||||
if step.startswith("wait:"):
|
||||
await asyncio.sleep(float(step[5:]))
|
||||
continue
|
||||
note(f"PRESS {step}")
|
||||
c.button_command(btn[step])
|
||||
await asyncio.sleep(4)
|
||||
await c.disconnect()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
# usage: run.sh TAG IO AUTH build and flash the server (serial) and the client (OTA), then pair once and reconnect twice
|
||||
# Copy this directory into a scratch directory with a secrets.yaml (wifi_ssid, wifi_password) and run it there.
|
||||
# IO/AUTH: none bond (Just Works) or display_yes_no sc_mitm_bond (numeric comparison, MITM).
|
||||
# env: TREE (esphome checkout for the client), SERVER_TREE (checkout with esp32_ble_server security, #17562 or
|
||||
# later; default TREE), SERVER_PORT (devkit serial port), CLIENT_HOST (client address), SERVER_BLE_MAC (from the
|
||||
# server's esp32_ble dump), SC (path to serial_capture.py), IDF (default recommended), PY (default $TREE/venv/bin/python)
|
||||
# A server keeps its bonds in NVS and refuses a weaker re-pairing: erase its flash before switching from nc to jw.
|
||||
D=$(cd "$(dirname "$0")" && pwd); G=${1:?tag}; IO=${2:?io}; AUTH=${3:?auth}; T=${TREE:?set TREE}; PY=${PY:-$T/venv/bin/python}
|
||||
P=${SERVER_PORT:?set SERVER_PORT}; C=${CLIENT_HOST:?set CLIENT_HOST}; MAC=${SERVER_BLE_MAC:?set SERVER_BLE_MAC}; SC=${SC:?set SC}
|
||||
E=(-s io "$IO" -s auth "$AUTH" -s idf "${IDF:-recommended}")
|
||||
ST=${SERVER_TREE:-$T}
|
||||
build() { local t=$1; shift; (cd "$t" && PYTHONUNBUFFERED=1 timeout 1800 $PY -m esphome "$@"); }
|
||||
ok=
|
||||
for i in 1 2 3; do
|
||||
build "$ST" -s name "bss-$G" "${E[@]}" run --no-logs "$D/server.yaml" --device "$P" > "$D/server-$G-upload.log" 2>&1 && ok=1 && break
|
||||
sleep 5
|
||||
done
|
||||
[ -n "$ok" ] || { echo "FAIL: server build or flash (does SERVER_TREE have #17562?)"; exit 1; }
|
||||
build "$T" -s name "bsc-$G" "${E[@]}" -s server "$MAC" run --no-logs "$D/client.yaml" --device "$C" > "$D/client-$G-upload.log" 2>&1 ||
|
||||
{ echo "FAIL: client build or upload"; exit 1; }
|
||||
rm -f "$D/server-$G.txt"
|
||||
$PY "$SC" "$P" 330 "$D/server-$G.txt" --reset > /dev/null 2>&1 & CAP=$!
|
||||
trap 'kill $CAP 2>/dev/null' EXIT
|
||||
sleep 25
|
||||
R=("Disconnect" "wait:3" "Connect" "wait:10" "Pair" "wait:5" "Read secure" "Read mitm")
|
||||
$PY "$D/drive.py" "$C" "$D/client-$G.txt" "wait:6" "Remove bond" "Disconnect" "wait:3" "Connect" "wait:10" \
|
||||
"Read secure" "Read mitm" "Write secure" "Subscribe" "wait:8" "Pair" "wait:6" "Read secure" "Read mitm" "Write secure" \
|
||||
"Subscribe" "wait:10" "${R[@]}" "${R[@]}" > /dev/null || { echo "FAIL: driver"; exit 1; }
|
||||
wait $CAP; trap - EXIT
|
||||
MODE=jw; [ "$IO" = none ] || MODE=nc
|
||||
$PY "$D/summary.py" "$MODE" "$D/client-$G.txt" "$D/server-$G.txt"
|
||||
@@ -1,85 +0,0 @@
|
||||
# Classic ESP32 devkit as the secure GATT server. run.sh passes name, io (none or display_yes_no),
|
||||
# auth (bond or sc_mitm_bond) and idf (recommended, or a version such as 5.5.5) with -s.
|
||||
# Needs esp32_ble_server security from #17562 (read_encrypt, numeric comparison); build it with SERVER_TREE.
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
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
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
esp32_ble:
|
||||
io_capability: ${io}
|
||||
auth_req_mode: ${auth}
|
||||
|
||||
esp32_ble_server:
|
||||
id: srv
|
||||
on_connect:
|
||||
- logger.log: "SRV client connected"
|
||||
on_disconnect:
|
||||
- logger.log: "SRV client disconnected"
|
||||
on_numeric_comparison_request:
|
||||
- logger.log:
|
||||
format: "SRV numeric comparison %06u"
|
||||
args: ["(unsigned) passkey"]
|
||||
- ble_server.numeric_comparison_reply:
|
||||
id: srv
|
||||
address: !lambda return address;
|
||||
accept: true
|
||||
services:
|
||||
- uuid: "180F"
|
||||
characteristics:
|
||||
- uuid: "2A19"
|
||||
read: true
|
||||
read_encrypt: true
|
||||
value:
|
||||
data: "99.9"
|
||||
- uuid: 5ec50000-0000-4000-8000-00000000f000
|
||||
characteristics:
|
||||
- uuid: 5ec50000-0000-4000-8000-00000000f001
|
||||
read: true
|
||||
value:
|
||||
data: "open"
|
||||
- uuid: 5ec50000-0000-4000-8000-00000000f002
|
||||
write: true
|
||||
write_encrypt: true
|
||||
on_write:
|
||||
- lambda: 'ESP_LOGI("SRV", "secure write received: %s", std::string(x.begin(), x.end()).c_str());'
|
||||
- uuid: 5ec50000-0000-4000-8000-00000000f004
|
||||
read: true
|
||||
read_encrypt_mitm: true
|
||||
value:
|
||||
data: "mitm"
|
||||
- uuid: 5ec50000-0000-4000-8000-00000000f003
|
||||
id: secure_notify
|
||||
read: true
|
||||
read_encrypt: true
|
||||
notify: true
|
||||
value:
|
||||
data: "n0"
|
||||
|
||||
interval:
|
||||
- interval: 2s
|
||||
then:
|
||||
- lambda: |-
|
||||
static int n = 0;
|
||||
char buf[12];
|
||||
snprintf(buf, sizeof(buf), "n%d", ++n);
|
||||
id(secure_notify)->set_value((const uint8_t *) buf, strlen(buf));
|
||||
- ble_server.characteristic.notify:
|
||||
id: secure_notify
|
||||
@@ -1,78 +0,0 @@
|
||||
"""Judge one run.sh run from the client log (drive.py output) and the server's serial capture.
|
||||
|
||||
usage: summary.py MODE CLIENT_LOG SERVER_CAPTURE MODE: jw (Just Works) or nc (numeric comparison, MITM)
|
||||
Before the first Pair: the encrypted and MITM reads must each be tried and refused (an `Error reading char` reply),
|
||||
no notification may arrive and no encrypted write may reach the server. After it: the encrypted read returns 99.9,
|
||||
the write reaches the server, the second Subscribe brings notifications, and the MITM-only characteristic is readable
|
||||
only after an nc pairing. Prints the reconnect pattern and PASS or FAIL.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
MODE = sys.argv[1]
|
||||
cli = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8").splitlines()
|
||||
srv = re.sub(
|
||||
r"\x1b\[[0-9;]*m",
|
||||
"",
|
||||
pathlib.Path(sys.argv[3]).read_bytes().decode(errors="replace"),
|
||||
)
|
||||
pair_at = next((i for i, line in enumerate(cli) if "PRESS Pair" in line), None)
|
||||
if pair_at is None:
|
||||
sys.exit("FAIL: no Pair step in the client log")
|
||||
before, after = cli[:pair_at], cli[pair_at:]
|
||||
subs = [i for i, line in enumerate(after) if "PRESS Subscribe" in line]
|
||||
after_sub = after[subs[0] :] if subs else []
|
||||
|
||||
|
||||
def count(lines: list[str], pat: str) -> int:
|
||||
return sum(1 for line in lines if re.search(pat, line))
|
||||
|
||||
|
||||
def outcome(lines: list[str], button: str, ok_pat: str) -> str:
|
||||
"""'ok', 'refused' or 'none' for the first press of BUTTON.
|
||||
|
||||
The window runs to the next read, pair or connection step, since a reply can come after a write press. A value
|
||||
wins over an error line: the error can belong to another characteristic read in the same window.
|
||||
"""
|
||||
start = next((i for i, line in enumerate(lines) if f"PRESS {button}" in line), None)
|
||||
if start is None:
|
||||
return "none"
|
||||
window = []
|
||||
for line in lines[start + 1 :]:
|
||||
if re.search(r"PRESS (Read|Pair|Disconnect|Connect|Subscribe|Remove)", line):
|
||||
break
|
||||
window.append(line)
|
||||
if any(re.search(ok_pat, line) for line in window):
|
||||
return "ok"
|
||||
return "refused" if any("Error reading char" in line for line in window) else "none"
|
||||
|
||||
|
||||
secure_before = outcome(before, "Read secure", r"secure read: 9")
|
||||
mitm_before = outcome(before, "Read mitm", r"mitm read: m")
|
||||
mitm_after = outcome(after, "Read mitm", r"mitm read: m")
|
||||
checks = {
|
||||
f"encrypted read refused before pairing ({secure_before})": secure_before
|
||||
== "refused",
|
||||
f"MITM read refused before pairing ({mitm_before})": mitm_before == "refused",
|
||||
"no notifications before pairing": count(before, r"notification: n") == 0,
|
||||
"encrypted read after pairing": outcome(after, "Read secure", r"secure read: 99\.9")
|
||||
== "ok",
|
||||
"encrypted write reached the server": "secure write received" in srv,
|
||||
"notifications after subscribing paired": count(after_sub, r"notification: n") > 0,
|
||||
}
|
||||
if MODE == "nc":
|
||||
checks[f"MITM read after nc pairing ({mitm_after})"] = mitm_after == "ok"
|
||||
else:
|
||||
checks[f"MITM read still refused after Just Works ({mitm_after})"] = (
|
||||
mitm_after == "refused"
|
||||
)
|
||||
for name, ok in checks.items():
|
||||
print(f"{'ok ' if ok else 'FAIL'} {name}")
|
||||
print(
|
||||
f"server: {srv.count('Authentication complete')} authentications, {srv.count('SRV numeric comparison')} numeric "
|
||||
f"comparisons, {srv.count('Deleting unpaired device')} bonds dropped by the stack"
|
||||
)
|
||||
print("PASS" if all(checks.values()) else "FAIL")
|
||||
sys.exit(0 if all(checks.values()) else 1)
|
||||
@@ -1,45 +0,0 @@
|
||||
# Classic ESP32 devkit: RTC pins 25 26 27 32 33 and digital pins 19 21 22 23, three rounds 5 s after boot.
|
||||
esphome:
|
||||
name: gpio-hold-esp32
|
||||
includes:
|
||||
- hold_test.hpp
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- delay: 5s
|
||||
- lambda: |-
|
||||
static const int pins[] = {25, 26, 27, 32, 33, 19, 21, 22, 23};
|
||||
for (int r = 1; r <= 3; r++) holdtest::run(pins, sizeof(pins) / sizeof(pins[0]), r);
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
# Turns on USE_GPIO_HOLD.
|
||||
output:
|
||||
- platform: gpio
|
||||
id: held_out
|
||||
pin:
|
||||
number: GPIO4
|
||||
hold_state: true
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: rerun
|
||||
on_press:
|
||||
- lambda: |-
|
||||
static const int pins[] = {25, 26, 27, 32, 33, 19, 21, 22, 23};
|
||||
holdtest::run(pins, sizeof(pins) / sizeof(pins[0]), 9);
|
||||
@@ -1,41 +0,0 @@
|
||||
# Waveshare ESP32-S3-ETH: RTC pins 16 17 and digital pin 43 are free; a round every 15 s.
|
||||
esphome:
|
||||
name: gpio-hold-esp32s3
|
||||
includes:
|
||||
- hold_test.hpp
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
# Turns on USE_GPIO_HOLD.
|
||||
output:
|
||||
- platform: gpio
|
||||
id: held_out
|
||||
pin:
|
||||
number: GPIO44
|
||||
hold_state: true
|
||||
|
||||
interval:
|
||||
- interval: 15s
|
||||
then:
|
||||
- lambda: |-
|
||||
static int round = 0;
|
||||
static const int pins[] = {16, 17, 43};
|
||||
holdtest::run(pins, sizeof(pins) / sizeof(pins[0]), ++round);
|
||||
@@ -1,93 +0,0 @@
|
||||
#pragma once
|
||||
#include "esphome/components/esp32/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/rtc_io.h>
|
||||
|
||||
// Drives held pads through the ISR pin API and reads the pad back.
|
||||
namespace holdtest {
|
||||
using esphome::esp32::ESP32InternalGPIOPin;
|
||||
namespace gpio = esphome::gpio;
|
||||
static const char *const TAG = "holdtest";
|
||||
|
||||
// One pin and one ISR argument block per GPIO and hold setting, made on first use and reused by later rounds,
|
||||
// so a test that repeats forever does not use up the heap.
|
||||
struct PinPair {
|
||||
ESP32InternalGPIOPin *pin{nullptr};
|
||||
esphome::ISRInternalGPIOPin isr;
|
||||
};
|
||||
static PinPair pairs[2][GPIO_NUM_MAX];
|
||||
|
||||
static PinPair &get(int num, bool hold) {
|
||||
PinPair &pp = pairs[hold][num];
|
||||
if (pp.pin == nullptr) {
|
||||
pp.pin = new ESP32InternalGPIOPin(); // NOLINT
|
||||
pp.pin->set_pin(static_cast<gpio_num_t>(num));
|
||||
pp.pin->set_flags(gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | (hold ? gpio::FLAG_HOLD : gpio::FLAG_NONE));
|
||||
pp.isr = pp.pin->to_isr();
|
||||
}
|
||||
pp.pin->setup(); // the previous round left the pad unheld with no mode
|
||||
return pp;
|
||||
}
|
||||
|
||||
static int lvl(int num) {
|
||||
esphome::delayMicroseconds(50);
|
||||
return gpio_get_level(static_cast<gpio_num_t>(num));
|
||||
}
|
||||
|
||||
// Returns failures for one pin.
|
||||
static int run_pin(int num, bool hold) {
|
||||
int fail = 0;
|
||||
auto &isr = get(num, hold).isr;
|
||||
isr.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_OUTPUT);
|
||||
int wr_ok = 0;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
bool v = i & 1;
|
||||
isr.digital_write(v);
|
||||
if (lvl(num) == (int) v) {
|
||||
wr_ok++;
|
||||
} else {
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
// Control: a raw register write without a hold refresh must not reach a held pad.
|
||||
int before = lvl(num);
|
||||
gpio_set_level(static_cast<gpio_num_t>(num), !before);
|
||||
int ctrl = lvl(num);
|
||||
bool ctrl_ok = hold ? (ctrl == before) : (ctrl != before);
|
||||
if (!ctrl_ok)
|
||||
fail++;
|
||||
isr.digital_write(before); // back in sync
|
||||
|
||||
// Pulls through the ISR pin_mode: input must toggle in the flags diff to apply them.
|
||||
int pd1, pu, pd2;
|
||||
isr.pin_mode(gpio::FLAG_NONE);
|
||||
isr.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN);
|
||||
pd1 = lvl(num);
|
||||
isr.pin_mode(gpio::FLAG_NONE);
|
||||
isr.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
|
||||
pu = lvl(num);
|
||||
isr.pin_mode(gpio::FLAG_NONE);
|
||||
isr.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN);
|
||||
pd2 = lvl(num);
|
||||
if (pd1 != 0 || pu != 1 || pd2 != 0)
|
||||
fail++;
|
||||
ESP_LOGI(TAG, "GPIO%d hold=%d rtc=%d write %d/20 control %s pulls pd=%d pu=%d pd=%d -> %s", num, hold,
|
||||
(int) rtc_gpio_is_valid_gpio(static_cast<gpio_num_t>(num)), wr_ok, ctrl_ok ? "ok" : "BAD", pd1, pu, pd2,
|
||||
fail ? "FAIL" : "PASS");
|
||||
// Leave the pad free for the next run.
|
||||
if (hold)
|
||||
gpio_hold_dis(static_cast<gpio_num_t>(num));
|
||||
isr.pin_mode(gpio::FLAG_NONE);
|
||||
return fail;
|
||||
}
|
||||
|
||||
static void run(const int *pins, int n, int round) {
|
||||
int fail = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
fail += run_pin(pins[i], true);
|
||||
fail += run_pin(pins[i], false);
|
||||
}
|
||||
ESP_LOGI(TAG, "ROUND %d %s failures=%d", round, fail ? "FAIL" : "PASS", fail);
|
||||
}
|
||||
} // namespace holdtest
|
||||
@@ -1,127 +0,0 @@
|
||||
"""Time the presence hold on stream-harness.yaml and check presence_timeout survives a restart.
|
||||
|
||||
usage: holddrive.py HOST OUT env: API_KEY (when the harness has one)
|
||||
For timeouts 5 and 0: target frames for 3 s, then empty frames; prints the ms from the "mode none" press to the
|
||||
has_target off line (up to one 100 ms frame of that is the harness). Then sets 0 and 20, restarts, reads the number.
|
||||
A missing press line, off line or number state prints NO-DATA and the run exits 1.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
from aioesphomeapi import APIClient, ButtonInfo, LogLevel, NumberInfo, NumberState
|
||||
|
||||
HOST, OUT = sys.argv[1], sys.argv[2]
|
||||
LOG = pathlib.Path(OUT).open("w", encoding="utf-8") # noqa: SIM115 open for the whole run
|
||||
MISSING = []
|
||||
|
||||
|
||||
def note(s: str) -> None:
|
||||
print(s)
|
||||
LOG.write(s + "\n")
|
||||
LOG.flush()
|
||||
|
||||
|
||||
async def session():
|
||||
c = APIClient(HOST, 6053, None, noise_psk=os.environ.get("API_KEY"))
|
||||
for attempt in range(60):
|
||||
try:
|
||||
await c.connect(login=True)
|
||||
break
|
||||
except Exception as err: # noqa: BLE001
|
||||
last_err = err
|
||||
note(f"connect attempt {attempt + 1} failed: {err!r}")
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
raise last_err
|
||||
ents, _ = await c.list_entities_services()
|
||||
btn = {e.name: e.key for e in ents if isinstance(e, ButtonInfo)}
|
||||
num = next(
|
||||
e.key
|
||||
for e in ents
|
||||
if isinstance(e, NumberInfo) and e.name == "Presence Timeout"
|
||||
)
|
||||
st, lines = {}, []
|
||||
|
||||
def on_state(s) -> None:
|
||||
if isinstance(s, NumberState) and s.key == num:
|
||||
st["t"] = s.state
|
||||
|
||||
def on_log(m) -> None:
|
||||
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
|
||||
if "harness" in t:
|
||||
lines.append(t)
|
||||
LOG.write("LOG " + t + "\n")
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
|
||||
for _ in range(20):
|
||||
await asyncio.sleep(1)
|
||||
if "t" in st:
|
||||
break
|
||||
# a freshly booted ESP8266 delays API commands that arrive too early
|
||||
await asyncio.sleep(8)
|
||||
return c, btn, num, st, lines
|
||||
|
||||
|
||||
def first_ms(lines: list[str], pat: str) -> int | None:
|
||||
for line in lines:
|
||||
if m := re.search(pat + r" at (\d+)", line):
|
||||
return int(m.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def number(st: dict, what: str) -> str:
|
||||
if "t" not in st:
|
||||
MISSING.append(what)
|
||||
return "NO-DATA"
|
||||
return f"{st['t']:g}"
|
||||
|
||||
|
||||
async def hold(c, btn, num, st, lines, timeout: int) -> None:
|
||||
c.number_command(num, timeout)
|
||||
await asyncio.sleep(2)
|
||||
if st.get("t") != timeout:
|
||||
# dev snaps 0 back to the saved value; the hold is then timed with what the number reads
|
||||
note(f"HOLD timeout={timeout}: the number reads {st.get('t')}, timing that")
|
||||
c.button_command(btn["Mode target"])
|
||||
await asyncio.sleep(3)
|
||||
lines.clear() # only this run's press and off lines count
|
||||
c.button_command(btn["Mode none"])
|
||||
await asyncio.sleep(timeout + 8)
|
||||
start, off = first_ms(lines, "mode none"), first_ms(lines, "presence raw OFF")
|
||||
if start is None or off is None or off < start:
|
||||
MISSING.append(f"hold {timeout}")
|
||||
note(f"HOLD timeout={timeout}: NO-DATA (press line {start}, off line {off})")
|
||||
else:
|
||||
note(
|
||||
f"HOLD timeout={timeout}: off {off - start} ms after the last target frame"
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c, btn, num, st, lines = await session()
|
||||
note(f"START number {number(st, 'start')}")
|
||||
for t in (5, 0):
|
||||
await hold(c, btn, num, st, lines, t)
|
||||
for t in (0, 20):
|
||||
st.pop("t", None)
|
||||
c.number_command(num, t)
|
||||
await asyncio.sleep(2)
|
||||
note(f"SET {t}, number reads {number(st, f'set {t}')}")
|
||||
c.button_command(btn["Restart"])
|
||||
await asyncio.sleep(1)
|
||||
await c.disconnect()
|
||||
await asyncio.sleep(15)
|
||||
c, btn, num, st, lines = await session()
|
||||
note(f"AFTER RESTART number reads {number(st, f'restart {t}')}")
|
||||
await c.disconnect()
|
||||
if MISSING:
|
||||
note(f"NO-DATA for: {', '.join(MISSING)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,78 +0,0 @@
|
||||
"""Step stream-harness.yaml through frames, silence, garbage and radar restarts; print every state change.
|
||||
|
||||
usage: linkdrive.py HOST OUT env: API_KEY (when the harness has one)
|
||||
Lines are seconds since start: PRESS steps, STATE changes of the ld2450 entities (unknown when missing), and LOG lines
|
||||
about the link or ld2450 warnings.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import APIClient, ButtonInfo, LogLevel
|
||||
|
||||
HOST, OUT = sys.argv[1], sys.argv[2]
|
||||
LOG = pathlib.Path(OUT).open("w", encoding="utf-8") # noqa: SIM115 open for the whole run
|
||||
T0 = time.time()
|
||||
STEPS = [
|
||||
("Mode none", 8), ("Mode target", 6), ("Mode silent", 10), ("Mode none", 4), ("Mode target", 4),
|
||||
("Mode garbage", 10), ("Mode target", 5),
|
||||
("Radar Restart", 0.3), ("Mode silent", 3.5), ("Mode target", 6),
|
||||
("Radar Restart", 0.3), ("Mode silent", 15), ("Mode none", 4),
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def note(s: str) -> None:
|
||||
line = f"{time.time() - T0:7.2f} {s}"
|
||||
print(line)
|
||||
LOG.write(line + "\n")
|
||||
LOG.flush()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = APIClient(HOST, 6053, None, noise_psk=os.environ.get("API_KEY"))
|
||||
await c.connect(login=True)
|
||||
ents, _ = await c.list_entities_services()
|
||||
names = {e.key: e.name for e in ents}
|
||||
btn = {e.name: e.key for e in ents if isinstance(e, ButtonInfo)}
|
||||
last = {}
|
||||
|
||||
def on_state(s) -> None:
|
||||
n = names.get(s.key)
|
||||
if n is None or n in btn:
|
||||
return
|
||||
v = (
|
||||
"unknown"
|
||||
if getattr(s, "missing_state", False)
|
||||
else getattr(s, "state", None)
|
||||
)
|
||||
if isinstance(v, float):
|
||||
v = "unknown (nan)" if math.isnan(v) else round(v, 1)
|
||||
if last.get(n) != v:
|
||||
last[n] = v
|
||||
note(f"STATE {n} = {v}")
|
||||
|
||||
def on_log(m) -> None:
|
||||
t = re.sub(r"\x1b\[[0-9;]*m", "", m.message.decode(errors="replace"))
|
||||
if re.search(r"[Ll]ink|\[W\]\[ld2450", t):
|
||||
note("LOG " + t.strip())
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
c.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
|
||||
await asyncio.sleep(3)
|
||||
for name, wait in STEPS:
|
||||
note(f"PRESS {name}")
|
||||
c.button_command(btn[name])
|
||||
await asyncio.sleep(wait)
|
||||
await c.disconnect()
|
||||
# dev freezes entities during silence but still updates them on target frames; nothing at all is a dead stream
|
||||
if not last:
|
||||
note("NO-DATA: no entity state arrived")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Read the LD24xx BT MAC text sensors, turn every LD24xx Bluetooth switch off, then on again.
|
||||
|
||||
usage: macdrive.py HOST OUT env: API_KEY (when the device has one)
|
||||
Works on any device whose MAC, Bluetooth and firmware entities have those words in their names (the BT MAC sensors
|
||||
may be disabled by default; they still publish over the API). Quote at most the first MAC byte in a PR.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import APIClient, SwitchInfo, TextSensorInfo
|
||||
|
||||
HOST, OUT = sys.argv[1], sys.argv[2]
|
||||
LOG = pathlib.Path(OUT).open("w", encoding="utf-8") # noqa: SIM115 open for the whole run
|
||||
|
||||
|
||||
def note(s: str) -> None:
|
||||
line = f"{time.strftime('%H:%M:%S')} {s}"
|
||||
print(line)
|
||||
LOG.write(line + "\n")
|
||||
LOG.flush()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
c = APIClient(HOST, 6053, None, noise_psk=os.environ.get("API_KEY"))
|
||||
await c.connect(login=True)
|
||||
ents, _ = await c.list_entities_services()
|
||||
keys = {
|
||||
e.key: e.name
|
||||
for e in ents
|
||||
if isinstance(e, (SwitchInfo, TextSensorInfo))
|
||||
and any(w in e.name for w in ("MAC", "Bluetooth", "Firmware"))
|
||||
}
|
||||
states = {}
|
||||
|
||||
def on_state(s) -> None:
|
||||
if s.key in keys and states.get(keys[s.key]) != getattr(s, "state", None):
|
||||
states[keys[s.key]] = s.state
|
||||
note(f"STATE {keys[s.key]} = {s.state}")
|
||||
|
||||
c.subscribe_states(on_state)
|
||||
await asyncio.sleep(8)
|
||||
switches = {
|
||||
e.name: e.key
|
||||
for e in ents
|
||||
if isinstance(e, SwitchInfo) and "Bluetooth" in e.name
|
||||
}
|
||||
macs = [n for n in keys.values() if "MAC" in n]
|
||||
if not macs or not switches:
|
||||
note(
|
||||
f"FAIL: need a MAC text sensor and a Bluetooth switch, found {sorted(keys.values())}"
|
||||
)
|
||||
sys.exit(1)
|
||||
missing = []
|
||||
if silent := [n for n in sorted(keys.values()) if n not in states]:
|
||||
note(f"NO-DATA after 8 s: {', '.join(silent)}")
|
||||
missing += silent
|
||||
note(
|
||||
"SNAP start: "
|
||||
+ "; ".join(f"{n}={states.get(n)}" for n in sorted(keys.values()))
|
||||
)
|
||||
for phase, on in (("off", False), ("on", True)):
|
||||
for name, key in switches.items():
|
||||
note(f"SET {name} {phase}")
|
||||
c.switch_command(key, on)
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(20)
|
||||
note(
|
||||
f"SNAP bluetooth {phase}: "
|
||||
+ "; ".join(f"{n}={states.get(n)}" for n in sorted(keys.values()))
|
||||
)
|
||||
missing += [n for n in switches if states.get(n) is not True]
|
||||
await c.disconnect()
|
||||
if missing:
|
||||
note(f"NO-DATA for: {', '.join(sorted(set(missing)))}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,5 +0,0 @@
|
||||
binary_sensor:
|
||||
- platform: ld2450
|
||||
ld2450_id: radar
|
||||
connected:
|
||||
name: Connected
|
||||
@@ -1,128 +0,0 @@
|
||||
# D1 mini, ld2450 on UART0 looped back onto itself, fed one periodic frame every 100 ms from a mode global:
|
||||
# 0 no target, 1 target 1 approaching, 2 silent, 3 garbage bytes (what a radar at another baud rate looks like).
|
||||
# Add `connected: !include stream-connected.yaml` under packages on builds whose ld2450 has the link watchdog.
|
||||
substitutions:
|
||||
name: ld2450-stream
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
// UART0 TX looped back into its own RX so injected frames reach the hub
|
||||
USC0(0) |= (1 << UCLBE);
|
||||
ESP_LOGI("harness", "UART0 loopback on");
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
logger:
|
||||
baud_rate: 0
|
||||
level: DEBUG
|
||||
|
||||
uart:
|
||||
id: radar_uart
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
baud_rate: 256000
|
||||
|
||||
ld2450:
|
||||
id: radar
|
||||
uart_id: radar_uart
|
||||
|
||||
globals:
|
||||
- id: mode
|
||||
type: int
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
- interval: 100ms
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(mode) == 1;
|
||||
then:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xE8, 0x83, 0x14, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x55, 0xCC]
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(mode) == 0;
|
||||
then:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0xAA, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x55, 0xCC]
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(mode) == 3;
|
||||
then:
|
||||
- uart.write:
|
||||
id: radar_uart
|
||||
data: [0x3F, 0x80, 0xF8, 0x00, 0x7E, 0x07, 0xC0, 0x1F, 0xE0, 0x03, 0xF0, 0x0F]
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2450
|
||||
has_target:
|
||||
name: Has Target
|
||||
# no settle filter, so the hub's own presence hold is what gets timed
|
||||
filters: []
|
||||
on_state:
|
||||
- lambda: 'ESP_LOGI("harness", "presence raw %s at %u", x ? "ON" : "OFF", (unsigned) millis());'
|
||||
|
||||
sensor:
|
||||
- platform: ld2450
|
||||
target_count:
|
||||
name: Target Count
|
||||
target_1:
|
||||
x:
|
||||
name: T1 X
|
||||
|
||||
text_sensor:
|
||||
- platform: ld2450
|
||||
target_1:
|
||||
direction:
|
||||
name: T1 Direction
|
||||
|
||||
number:
|
||||
- platform: ld2450
|
||||
presence_timeout:
|
||||
name: Presence Timeout
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Mode none
|
||||
on_press:
|
||||
- lambda: 'id(mode) = 0; ESP_LOGI("harness", "mode none at %u", (unsigned) millis());'
|
||||
- platform: template
|
||||
name: Mode target
|
||||
on_press:
|
||||
- lambda: 'id(mode) = 1; ESP_LOGI("harness", "mode target at %u", (unsigned) millis());'
|
||||
- platform: template
|
||||
name: Mode silent
|
||||
on_press:
|
||||
- lambda: 'id(mode) = 2; ESP_LOGI("harness", "mode silent at %u", (unsigned) millis());'
|
||||
- platform: template
|
||||
name: Mode garbage
|
||||
on_press:
|
||||
- lambda: 'id(mode) = 3; ESP_LOGI("harness", "mode garbage at %u", (unsigned) millis());'
|
||||
- platform: ld2450
|
||||
ld2450_id: radar
|
||||
restart:
|
||||
name: Radar Restart
|
||||
- platform: restart
|
||||
name: Restart
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Match each RTL8720C ROM fault dump in a serial capture with the report on the next boot.
|
||||
|
||||
usage: rtl_rom_vs_report.py SERIAL_CAPTURE (taken with serial_capture.py --ms)
|
||||
Per ROM dump: the ROM's CFSR and PC, ms from the dump to the next `ROM Version` banner, and the Reason, CFSR, PC and SP
|
||||
the next boot reported. The ROM lines start with a bare CR, so the capture is split on newlines only.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
RAW = pathlib.Path(sys.argv[1]).read_bytes().decode(errors="replace")
|
||||
LINES = [
|
||||
re.sub(r"\x1b\[[0-9;]*m", "", line) for line in RAW.replace("\r", "").split("\n")
|
||||
]
|
||||
|
||||
|
||||
def stamp(line: str) -> float | None:
|
||||
"""Seconds since midnight from a serial_capture.py --ms timestamp."""
|
||||
m = re.search(r"\[(\d\d):(\d\d):(\d\d\.\d{3})\]", line)
|
||||
return (
|
||||
int(m.group(1)) * 3600 + int(m.group(2)) * 60 + float(m.group(3)) if m else None
|
||||
)
|
||||
|
||||
|
||||
if not any(stamp(line) is not None for line in LINES):
|
||||
sys.exit(
|
||||
"NO-DATA: no [HH:MM:SS.mmm] timestamps; take the capture with serial_capture.py --ms"
|
||||
)
|
||||
dumps = bad = 0
|
||||
i = 0
|
||||
while i < len(LINES):
|
||||
if "Fault Handler:" not in LINES[i]:
|
||||
i += 1
|
||||
continue
|
||||
t0 = stamp(LINES[i])
|
||||
kind = LINES[i + 2].split("]", 1)[-1].strip() if i + 2 < len(LINES) else "?"
|
||||
cfsr = pc = None
|
||||
j = i
|
||||
while j < len(LINES) and "ROM Version" not in LINES[j]:
|
||||
if cfsr is None and "Configurable Fault Status Reg" in LINES[j]:
|
||||
cfsr = LINES[j].split("=")[1].strip()
|
||||
if pc is None and "PC =" in LINES[j]:
|
||||
pc = LINES[j].split("=")[1].strip()
|
||||
j += 1
|
||||
tj = next(
|
||||
(
|
||||
stamp(LINES[x])
|
||||
for x in range(j, min(j + 5, len(LINES)))
|
||||
if stamp(LINES[x]) is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
reset = (
|
||||
f"{(tj - t0) * 1000:.0f} ms"
|
||||
if tj is not None and t0 is not None
|
||||
else "no reset seen"
|
||||
)
|
||||
report = {}
|
||||
k = j + 1
|
||||
while (
|
||||
k < len(LINES)
|
||||
and "Fault Handler:" not in LINES[k]
|
||||
and "boot reason" not in LINES[k]
|
||||
):
|
||||
m = re.search(r"(Reason|CFSR|PC|SP):\s+(.*)", LINES[k])
|
||||
if m and "libretiny.crash" in LINES[k]:
|
||||
report.setdefault(m.group(1), m.group(2).split(" (")[0].strip())
|
||||
k += 1
|
||||
dumps += 1
|
||||
missing = [
|
||||
n for n, v in (("ROM CFSR", cfsr), ("ROM PC", pc), ("reset", tj)) if v is None
|
||||
]
|
||||
missing += [k for k in ("Reason", "CFSR", "PC", "SP") if k not in report]
|
||||
bad += bool(missing)
|
||||
flag = f" | MISSING {', '.join(missing)}" if missing else ""
|
||||
print(f"{kind:18} ROM CFSR {cfsr} PC {pc} | reset {reset} | report {report}{flag}")
|
||||
i = j
|
||||
if not dumps:
|
||||
sys.exit("NO-DATA: no ROM fault dump in the capture")
|
||||
sys.exit(1 if bad else 0)
|
||||
@@ -1,40 +0,0 @@
|
||||
# Any ESP32-C3 with GPIO10 free; drop ESPHome ledc outputs, the C3 shares one LEDC clock across timers.
|
||||
esphome:
|
||||
name: wake-probe-esp32c3
|
||||
includes:
|
||||
- wake_probe.hpp
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lambda: wakeprobe::start();
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
variant: esp32c3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
include_builtin_idf_components:
|
||||
- esp_driver_ledc
|
||||
|
||||
logger:
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
power_management:
|
||||
- platform: esp32_power_management
|
||||
enable_light_sleep: true
|
||||
profiling: true
|
||||
|
||||
interval:
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: wakeprobe::report();
|
||||
@@ -1,110 +0,0 @@
|
||||
#pragma once
|
||||
// LEDC on an RC_FAST clock keeps toggling GPIO10 in light sleep; an ESP32InternalGPIOPin interrupt on the same pad
|
||||
// timestamps every edge.
|
||||
#include <cstdio>
|
||||
#include "driver/ledc.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esphome/components/esp32/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace wakeprobe {
|
||||
static const char *const TAG = "wakeprobe";
|
||||
static constexpr gpio_num_t PIN = GPIO_NUM_10;
|
||||
static volatile uint32_t edges = 0;
|
||||
static volatile int64_t last_us = 0;
|
||||
static volatile int64_t gap_min = INT64_MAX, gap_max = 0;
|
||||
static volatile uint32_t late_edges = 0; // gap more than 20 ms off the half period
|
||||
static int64_t start_us = 0;
|
||||
static uint32_t half_period_us = 0;
|
||||
static uint32_t freq = 0;
|
||||
static esp_err_t err_timer = ESP_OK, err_channel = ESP_OK;
|
||||
|
||||
static void IRAM_ATTR on_edge(void *) {
|
||||
int64_t now = esp_timer_get_time();
|
||||
if (last_us != 0) {
|
||||
int64_t gap = now - last_us;
|
||||
if (gap < gap_min)
|
||||
gap_min = gap;
|
||||
if (gap > gap_max)
|
||||
gap_max = gap;
|
||||
int64_t off = gap - (int64_t) half_period_us;
|
||||
if (off > 20000 || off < -20000)
|
||||
late_edges = late_edges + 1;
|
||||
}
|
||||
last_us = now;
|
||||
edges = edges + 1;
|
||||
}
|
||||
|
||||
inline void start() {
|
||||
auto *pin = new esphome::esp32::ESP32InternalGPIOPin(); // NOLINT
|
||||
pin->set_pin(PIN);
|
||||
pin->set_flags(esphome::gpio::FLAG_INPUT);
|
||||
pin->setup();
|
||||
pin->attach_interrupt(on_edge, (void *) nullptr, esphome::gpio::INTERRUPT_ANY_EDGE);
|
||||
ledc_timer_config_t t{};
|
||||
t.speed_mode = LEDC_LOW_SPEED_MODE;
|
||||
t.duty_resolution = LEDC_TIMER_14_BIT;
|
||||
t.timer_num = LEDC_TIMER_0;
|
||||
t.freq_hz = 2;
|
||||
t.clk_cfg = LEDC_USE_RC_FAST_CLK;
|
||||
esp_err_t e1 = err_timer = ledc_timer_config(&t);
|
||||
ledc_channel_config_t c{};
|
||||
c.gpio_num = PIN;
|
||||
c.speed_mode = LEDC_LOW_SPEED_MODE;
|
||||
c.channel = LEDC_CHANNEL_0;
|
||||
c.timer_sel = LEDC_TIMER_0;
|
||||
c.duty = 1 << 13;
|
||||
c.sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE;
|
||||
esp_err_t e2 = err_channel = ledc_channel_config(&c);
|
||||
// ledc_channel_config makes the pad output only; turn its input back on so the interrupt sees the wave.
|
||||
gpio_input_enable(PIN);
|
||||
freq = ledc_get_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0);
|
||||
half_period_us = freq ? 500000 / freq : 0;
|
||||
start_us = esp_timer_get_time();
|
||||
ESP_LOGI(TAG, "LEDC timer=%s channel=%s freq=%u Hz half period %u us", esp_err_to_name(e1), esp_err_to_name(e2),
|
||||
(unsigned) freq, (unsigned) half_period_us);
|
||||
}
|
||||
|
||||
inline void report() {
|
||||
if (start_us == 0)
|
||||
return;
|
||||
char lv[21];
|
||||
for (int i = 0; i < 20; i++) {
|
||||
lv[i] = gpio_get_level(PIN) ? '1' : '0';
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
lv[20] = 0;
|
||||
// Elapsed time and edge count are read together, after the 1 s level loop, so they describe the same interval.
|
||||
float secs = (esp_timer_get_time() - start_us) / 1e6f;
|
||||
uint32_t seen = edges;
|
||||
ESP_LOGI(TAG, "LEDC timer=%s channel=%s freq=%u, level over 1 s: %s", esp_err_to_name(err_timer),
|
||||
esp_err_to_name(err_channel), (unsigned) freq, lv);
|
||||
ESP_LOGI(TAG, "%.0f s: edges=%u expected=%.0f gap min=%lld max=%lld us, off by >20 ms: %u", secs, (unsigned) seen,
|
||||
secs * 2 * freq, (long long) gap_min, (long long) gap_max, (unsigned) late_edges);
|
||||
#ifdef CONFIG_PM_PROFILING
|
||||
static char buf[1024];
|
||||
FILE *f = fmemopen(buf, sizeof(buf) - 1, "w");
|
||||
if (f == nullptr) {
|
||||
ESP_LOGW(TAG, "pm dump failed: fmemopen");
|
||||
return;
|
||||
}
|
||||
esp_pm_dump_locks(f);
|
||||
long n = ftell(f);
|
||||
fclose(f);
|
||||
buf[n < 0 ? 0 : n] = 0;
|
||||
bool sleep_seen = false;
|
||||
for (char *line = strtok(buf, "\n"); line != nullptr; line = strtok(nullptr, "\n")) {
|
||||
if (strstr(line, "SLEEP") || strstr(line, "CPU_MAX") || strstr(line, "APB_MAX") || strstr(line, "APB_MIN")) {
|
||||
sleep_seen = sleep_seen || strstr(line, "SLEEP") != nullptr;
|
||||
ESP_LOGI(TAG, "pm %s", line);
|
||||
}
|
||||
}
|
||||
if (!sleep_seen)
|
||||
ESP_LOGW(TAG, "pm dump has no SLEEP line (%ld bytes, buffer %u)", n, (unsigned) sizeof(buf));
|
||||
#else
|
||||
ESP_LOGW(TAG, "pm profiling unavailable: set power_management profiling: true");
|
||||
#endif
|
||||
}
|
||||
} // namespace wakeprobe
|
||||
@@ -1,36 +0,0 @@
|
||||
# AP manual IP: the only network is missing, so the fallback AP starts after 10 s on 10.42.7.1.
|
||||
substitutions:
|
||||
name: mip-ap
|
||||
plat: esp32
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
packages:
|
||||
plat: !include plat-${plat}.yaml
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
logs:
|
||||
scheduler: DEBUG
|
||||
component: DEBUG
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: mip-nowhere
|
||||
password: nowhere123
|
||||
manual_ip:
|
||||
static_ip: 10.9.9.9
|
||||
gateway: 10.9.9.1
|
||||
subnet: 255.255.255.0
|
||||
dns1: 10.9.9.1
|
||||
ap:
|
||||
ssid: mip-ap-test
|
||||
password: mipaptest1
|
||||
ap_timeout: 10s
|
||||
manual_ip:
|
||||
static_ip: 10.42.7.1
|
||||
gateway: 10.42.7.1
|
||||
subnet: 255.255.255.0
|
||||
|
||||
captive_portal:
|
||||
@@ -1,36 +0,0 @@
|
||||
# Joins the DUT's AP and fetches its page every 10 s.
|
||||
substitutions:
|
||||
name: mip-client
|
||||
plat: esp32
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
packages:
|
||||
plat: !include plat-${plat}.yaml
|
||||
|
||||
logger:
|
||||
|
||||
wifi:
|
||||
ssid: mip-ap-test
|
||||
password: mipaptest1
|
||||
reboot_timeout: 0s
|
||||
|
||||
http_request:
|
||||
verify_ssl: false
|
||||
|
||||
interval:
|
||||
- interval: 10s
|
||||
then:
|
||||
- if:
|
||||
condition: wifi.connected
|
||||
then:
|
||||
- http_request.get:
|
||||
url: http://10.42.7.1/
|
||||
capture_response: false
|
||||
on_response:
|
||||
then:
|
||||
- lambda: ESP_LOGI("client", "GET http://10.42.7.1/ status=%d", response->status_code);
|
||||
on_error:
|
||||
then:
|
||||
- logger.log: "GET http://10.42.7.1/ failed"
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
#include "esphome/core/log.h"
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/ip_addr.h"
|
||||
#ifdef USE_ESP8266
|
||||
#include <ESP8266WiFi.h>
|
||||
#else
|
||||
#include "lwip/netdb.h"
|
||||
#include "lwip/inet.h"
|
||||
#endif
|
||||
inline void dns_probe() {
|
||||
char b0[IPADDR_STRLEN_MAX], b1[IPADDR_STRLEN_MAX], res[16] = "-";
|
||||
ipaddr_ntoa_r(dns_getserver(0), b0, sizeof(b0));
|
||||
ipaddr_ntoa_r(dns_getserver(1), b1, sizeof(b1));
|
||||
int ok;
|
||||
#ifdef USE_ESP8266
|
||||
IPAddress ip;
|
||||
ok = WiFi.hostByName("pool.ntp.org", ip);
|
||||
if (ok)
|
||||
strncpy(res, ip.toString().c_str(), sizeof(res) - 1);
|
||||
#else
|
||||
struct addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
struct addrinfo *r = nullptr;
|
||||
ok = lwip_getaddrinfo("pool.ntp.org", nullptr, &hints, &r) == 0 && r != nullptr;
|
||||
if (ok) {
|
||||
inet_ntoa_r(((struct sockaddr_in *) r->ai_addr)->sin_addr, res, sizeof(res));
|
||||
lwip_freeaddrinfo(r);
|
||||
}
|
||||
#endif
|
||||
ESP_LOGI("dnsprobe", "dns0=%s dns1=%s lookup=%s %s", b0, b1, ok ? "ok" : "FAIL", res);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -1,2 +0,0 @@
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
# usage: run-ap.sh LABEL the DUT runs the fallback AP on 10.42.7.1, the other board joins it and fetches its page
|
||||
# Copy this directory into a scratch directory and run it there; no secrets are needed.
|
||||
# env: TREE (esphome checkout for the DUT), CLIENT_TREE (checkout for the client, default TREE), DUT_PORT, DUT_PLAT,
|
||||
# CLIENT_PORT, CLIENT_PLAT (esp32 or esp8266), SC (path to serial_capture.py), PY (default $TREE/venv/bin/python)
|
||||
D=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; T=${TREE:?set TREE}; CT=${CLIENT_TREE:-$T}
|
||||
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; DP=${DUT_PORT:?set DUT_PORT}; CP=${CLIENT_PORT:?set CLIENT_PORT}
|
||||
N=mip-ap-$L; CN=mip-client
|
||||
up() { (cd "$1" && PYTHONUNBUFFERED=1 timeout 1500 $PY -m esphome -s name "$2" -s plat "$3" run --no-logs "$D/$4" \
|
||||
--device "$5" > "$D/$2-upload.log" 2>&1) || { echo "FAIL: $2 build or upload"; exit 1; }; }
|
||||
up "$T" "$N" "${DUT_PLAT:-esp32}" ap.yaml "$DP"
|
||||
up "$CT" "$CN" "${CLIENT_PLAT:-esp8266}" client.yaml "$CP"
|
||||
rm -f "$D/$N-serial.txt" "$D/$CN-$N-serial.txt"
|
||||
$PY "$SC" "$DP" 120 "$D/$N-serial.txt" --reset > /dev/null 2>&1 & C1=$!
|
||||
$PY "$SC" "$CP" 120 "$D/$CN-$N-serial.txt" --reset > /dev/null 2>&1 & C2=$!
|
||||
wait $C1; R1=$?; wait $C2; R2=$?
|
||||
strip() { LC_ALL=C sed 's/\x1b\[[0-9;]*m//g' | sort -u -k2; }
|
||||
echo "--- DUT"; LC_ALL=C grep -a -E "Manual IP|Starting fallback|IP Address|Setting up AP|rst cause|Guru|abort|panic" \
|
||||
"$D/$N-serial.txt" | strip | head -8
|
||||
echo "--- client"; LC_ALL=C grep -a -E "IP Address:|Gateway:|Subnet:" "$D/$CN-$N-serial.txt" | strip | head -4
|
||||
ok=$(grep -a -c 'status=200' "$D/$CN-$N-serial.txt"); bad=$(grep -a -c 'GET http://10.42.7.1/ failed' "$D/$CN-$N-serial.txt")
|
||||
echo "client GET ok=$ok failed=$bad"
|
||||
fail=0
|
||||
[ "$R1" = 0 ] && [ -s "$D/$N-serial.txt" ] || { echo "FAIL: DUT capture failed or empty (rc $R1)"; fail=1; }
|
||||
[ "$R2" = 0 ] && [ -s "$D/$CN-$N-serial.txt" ] || { echo "FAIL: client capture failed or empty (rc $R2)"; fail=1; }
|
||||
grep -a -q -E "IP Address: +10\.42\.7\.1\b" "$D/$N-serial.txt" || { echo "FAIL: AP address is not 10.42.7.1"; fail=1; }
|
||||
grep -a -q -E "IP Address: +10\.42\.7\." "$D/$CN-$N-serial.txt" || { echo "FAIL: client never joined"; fail=1; }
|
||||
[ "$ok" -gt 0 ] || { echo "FAIL: no page fetched"; fail=1; }
|
||||
grep -a -q -E "Guru|abort\(\)|panic|[eE]xception \(|Soft WDT reset|wdt reset" "$D/$N-serial.txt" && { echo "FAIL: DUT crash"; fail=1; }
|
||||
[ $fail = 0 ] && echo PASS; exit $fail
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
# usage: run-sta.sh LABEL flash sta.yaml, capture 75 s, ping the manual address, check the dump and the DNS probe
|
||||
# Copy this directory into a scratch directory with a secrets.yaml (wifi_ssid, wifi_password) and run it there.
|
||||
# env: TREE (esphome checkout), PORT (board serial port), PLAT (esp32 or esp8266), IP (the board's own DHCP lease),
|
||||
# GATEWAY, SUBNET, DNS1 (the network's values), SC (path to serial_capture.py), PY (default $TREE/venv/bin/python)
|
||||
D=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; T=${TREE:?set TREE}; PY=${PY:-$T/venv/bin/python}
|
||||
P=${PORT:?set PORT}; IP=${IP:?set IP}; SC=${SC:?set SC}; N=mip-sta-$L; C=$D/$N-serial.txt
|
||||
(cd "$T" && PYTHONUNBUFFERED=1 timeout 1500 $PY -m esphome -s name "$N" -s plat "${PLAT:-esp32}" -s ip "$IP" \
|
||||
-s gateway "${GATEWAY:?set GATEWAY}" -s subnet "${SUBNET:?set SUBNET}" -s dns1 "${DNS1:?set DNS1}" \
|
||||
run --no-logs "$D/sta.yaml" --device "$P" > "$D/$N-upload.log" 2>&1) || { echo "FAIL: build or upload"; exit 1; }
|
||||
rm -f "$C"
|
||||
$PY "$SC" "$P" 75 "$C" --reset > /dev/null 2>&1 & CAP=$!
|
||||
# ping exits 0 when at least one reply came back, on macOS and Linux alike.
|
||||
sleep 50; PING=$(ping -c 10 -i 0.3 "$IP" 2>&1); PING_RC=$?; wait $CAP; CAPRC=$?
|
||||
LC_ALL=C grep -a -E "Manual IP|IP Address:|Gateway:|Subnet:|DNS1:|DNS2:|dnsprobe|rst cause|Guru|abort|panic" "$C" \
|
||||
| LC_ALL=C sed 's/\x1b\[[0-9;]*m//g' | sort -u -k2 | head -14
|
||||
echo "ping: $(echo "$PING" | grep -E 'transmitted')"
|
||||
fail=0
|
||||
[ "$CAPRC" = 0 ] && [ -s "$C" ] || { echo "FAIL: serial capture failed or empty (rc $CAPRC)"; fail=1; }
|
||||
grep -a -q -E "dns0=$DNS1 dns1=9\.9\.9\.9 lookup=ok" "$C" || { echo "FAIL: DNS servers or lookup"; fail=1; }
|
||||
grep -a -q -E "IP Address: +$IP\b" "$C" || { echo "FAIL: address is not $IP"; fail=1; }
|
||||
[ $PING_RC = 0 ] || { echo "FAIL: no ping reply"; fail=1; }
|
||||
grep -a -q -E "Guru|abort\(\)|panic|[eE]xception \(|Soft WDT reset|wdt reset" "$C" && { echo "FAIL: crash"; fail=1; }
|
||||
[ $fail = 0 ] && echo PASS; exit $fail
|
||||
@@ -1,62 +0,0 @@
|
||||
# Station manual IP. The real network is the second table row and inherits the top level manual_ip; set ip to the
|
||||
# board's own DHCP lease and dns2 to a server DHCP does not hand out, so the logs prove the manual values are used.
|
||||
# run-sta.sh passes name, plat, ip, gateway, subnet and dns1 with -s; dns2 stays 9.9.9.9, which it checks for.
|
||||
substitutions:
|
||||
name: mip-sta
|
||||
plat: esp32
|
||||
ip: 192.168.1.50
|
||||
gateway: 192.168.1.1
|
||||
subnet: 255.255.255.0
|
||||
dns1: 192.168.1.1
|
||||
dns2: 9.9.9.9
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- dnsprobe.hpp
|
||||
|
||||
packages:
|
||||
plat: !include plat-${plat}.yaml
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
logs:
|
||||
scheduler: DEBUG
|
||||
component: DEBUG
|
||||
api.service: DEBUG
|
||||
api.connection: DEBUG
|
||||
api.socket: DEBUG
|
||||
socket: DEBUG
|
||||
mdns: DEBUG
|
||||
|
||||
wifi:
|
||||
manual_ip:
|
||||
static_ip: ${ip}
|
||||
gateway: ${gateway}
|
||||
subnet: ${subnet}
|
||||
dns1: ${dns1}
|
||||
dns2: ${dns2}
|
||||
networks:
|
||||
- ssid: mip-nowhere
|
||||
password: nowhere123
|
||||
manual_ip:
|
||||
static_ip: 10.9.9.9
|
||||
gateway: 10.9.9.1
|
||||
subnet: 255.255.255.0
|
||||
dns1: 10.9.9.1
|
||||
- ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
interval:
|
||||
- interval: 20s
|
||||
then:
|
||||
- if:
|
||||
condition: wifi.connected
|
||||
then:
|
||||
- lambda: dns_probe();
|
||||
@@ -1,71 +0,0 @@
|
||||
"""Print the date, time and datetime rows of /metrics, set new values over the API, print them again.
|
||||
|
||||
usage: datetime_metrics.py HOST env: API_KEY
|
||||
Sets every date to 2040-03-04, every time to 23:59:59 and every datetime to 2040-03-04 05:06:07 UTC (past 2038 on
|
||||
purpose). Values are printed with their UTC reading: dates are local midnight, times are ms since midnight and
|
||||
datetimes are ms since the epoch.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC, datetime
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
from aioesphomeapi import APIClient, DateInfo, DateTimeInfo, TimeInfo
|
||||
|
||||
HOST = sys.argv[1]
|
||||
|
||||
|
||||
def metrics(tag: str) -> int:
|
||||
with urllib.request.urlopen(f"http://{HOST}/metrics", timeout=10) as resp: # noqa: S310
|
||||
body = resp.read().decode()
|
||||
rows = [
|
||||
line
|
||||
for line in body.splitlines()
|
||||
if re.match(r"esphome_(date|time|datetime)_", line)
|
||||
]
|
||||
print(f"--- {tag}: {len(rows)} rows")
|
||||
parsed = 0
|
||||
for line in rows:
|
||||
m = re.match(r'(esphome_\w+)\{.*name="([^"]+)"\} (-?\d+)$', line)
|
||||
if not m:
|
||||
print(f" UNPARSED {line}")
|
||||
continue
|
||||
parsed += 1
|
||||
metric, name, value = m.group(1), m.group(2), int(m.group(3))
|
||||
utc = ""
|
||||
if metric in ("esphome_date_value", "esphome_datetime_value"):
|
||||
utc = " = " + datetime.fromtimestamp(value / 1000, UTC).isoformat()
|
||||
print(f" {metric} {name} {value}{utc}")
|
||||
return parsed
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
if not metrics("initial"):
|
||||
sys.exit(
|
||||
"NO-DATA: no date, time or datetime rows (a build without them, or no such entities)"
|
||||
)
|
||||
c = APIClient(HOST, 6053, None, noise_psk=os.environ.get("API_KEY"))
|
||||
await c.connect(login=True)
|
||||
ents, _ = await c.list_entities_services()
|
||||
sent = 0
|
||||
for e in ents:
|
||||
sent += isinstance(e, (DateInfo, TimeInfo, DateTimeInfo))
|
||||
if isinstance(e, DateInfo):
|
||||
c.date_command(e.key, 2040, 3, 4)
|
||||
elif isinstance(e, TimeInfo):
|
||||
c.time_command(e.key, 23, 59, 59)
|
||||
elif isinstance(e, DateTimeInfo):
|
||||
c.datetime_command(
|
||||
e.key, int(datetime(2040, 3, 4, 5, 6, 7, tzinfo=UTC).timestamp())
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
await c.disconnect()
|
||||
if not sent:
|
||||
sys.exit("NO-DATA: the device lists no date, time or datetime entities")
|
||||
metrics("after setting 2040-03-04, 23:59:59 and 2040-03-04 05:06:07 UTC")
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,10 +0,0 @@
|
||||
# Adds a device time zone (UTC-10, no DST) to prom-datetime.yaml; the zone applies without a time sync.
|
||||
substitutions:
|
||||
name: prom-datetime-tz
|
||||
|
||||
packages:
|
||||
base: !include prom-datetime.yaml
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
timezone: HST10
|
||||
@@ -1,46 +0,0 @@
|
||||
# D1 mini with three optimistic template datetime entities. -s name NAME; build prom-datetime-tz.yaml for a time zone.
|
||||
substitutions:
|
||||
name: prom-datetime
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
logger:
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
datetime:
|
||||
- platform: template
|
||||
name: Date
|
||||
id: test_date
|
||||
type: date
|
||||
optimistic: true
|
||||
initial_value: "2000-1-2"
|
||||
- platform: template
|
||||
name: Time
|
||||
id: test_time
|
||||
type: time
|
||||
optimistic: true
|
||||
initial_value: "12:34:56am"
|
||||
- platform: template
|
||||
name: DateTime
|
||||
id: test_datetime
|
||||
type: datetime
|
||||
optimistic: true
|
||||
initial_value: "2000-1-2 12:34:56"
|
||||
|
||||
prometheus:
|
||||
@@ -1,6 +0,0 @@
|
||||
captive_portal:
|
||||
button:
|
||||
- platform: template
|
||||
name: Start portal
|
||||
on_press:
|
||||
- lambda: captive_portal::global_captive_portal->start();
|
||||
@@ -1,4 +0,0 @@
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -1,2 +0,0 @@
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
// A handler registered after setup(), the way an external component would; answers GET /probe.
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
class ProbeHandler : public AsyncWebHandler {
|
||||
public:
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
if (request->method() != HTTP_GET)
|
||||
return false;
|
||||
#ifdef USE_ESP32
|
||||
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
|
||||
return request->url_to(url_buf) == "/probe";
|
||||
#else
|
||||
return request->url() == ESPHOME_F("/probe");
|
||||
#endif
|
||||
}
|
||||
void handleRequest(AsyncWebServerRequest *request) override { request->send(200, "text/plain", "probe-ok"); }
|
||||
};
|
||||
inline ProbeHandler probe_handler;
|
||||
@@ -1,2 +0,0 @@
|
||||
captive_portal:
|
||||
prometheus:
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
# usage: run.sh LABEL VARIANT flash webbase.yaml, fetch /probe and /, then a web OTA of the same image
|
||||
# Copy this directory into a scratch directory with a secrets.yaml (wifi_ssid, wifi_password) and run it there.
|
||||
# VARIANT is ws, cp or prom (see webbase.yaml). The cp variant presses "Start portal" over the API first.
|
||||
# env: TREE (esphome checkout), PORT (board serial port), PLAT (esp32 or esp8266), IP (the board's address),
|
||||
# SC (path to serial_capture.py), PY (default $TREE/venv/bin/python), SKILL_SCRIPTS (the skill's scripts directory)
|
||||
D=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; V=${2:?variant}; T=${TREE:?set TREE}; PY=${PY:-$T/venv/bin/python}
|
||||
P=${PORT:?set PORT}; IP=${IP:?set IP}; SC=${SC:?set SC}; PL=${PLAT:-esp32}; N=web-$V-$PL-$L; C=$D/$N-serial.txt
|
||||
# api_button.py and ota_timing/apiinfo.py live in the skill's scripts directory; set SKILL_SCRIPTS when this
|
||||
# directory was copied elsewhere.
|
||||
S=${SKILL_SCRIPTS:-$(cd "$D/.." && pwd)}
|
||||
[ -f "$S/api_button.py" ] && [ -f "$S/ota_timing/apiinfo.py" ] || { echo "FAIL: set SKILL_SCRIPTS to the skill's scripts directory"; exit 1; }
|
||||
if [ "$PL" = esp32 ]; then BIN=$D/.esphome/build/$N/build/$N.bin; else BIN=$D/.esphome/build/$N/.pioenvs/$N/firmware.bin; fi
|
||||
(cd "$T" && PYTHONUNBUFFERED=1 timeout 1500 $PY -m esphome -s name "$N" -s plat "$PL" -s variant "$V" \
|
||||
run --no-logs "$D/webbase.yaml" --device "$P" > "$D/$N-upload.log" 2>&1) || { echo "FAIL: build or upload"; exit 1; }
|
||||
rm -f "$C"
|
||||
# Longer than the worst case below: 80 s to come up, 120 s of OTA, 80 s to come back.
|
||||
$PY "$SC" "$P" 300 "$C" --reset > /dev/null 2>&1 & CAP=$!
|
||||
# Wait on the API port: in the cp variant the HTTP server only starts when the portal does, also after the OTA reboot.
|
||||
up() {
|
||||
for _ in $(seq 1 40); do
|
||||
$PY -c 'import socket, sys; socket.create_connection((sys.argv[1], 6053), 1).close()' "$IP" 2> /dev/null && return 0
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
up || { echo "FAIL: board not reachable"; kill $CAP; exit 1; }; sleep 8
|
||||
fail=0
|
||||
if [ "$V" = cp ]; then
|
||||
$PY "$S/api_button.py" "$IP" - press "Start portal" || { echo "FAIL: Start portal press"; fail=1; }
|
||||
sleep 2
|
||||
# The captive portal only answers /config.json while it is active.
|
||||
CFG=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "http://$IP/config.json")
|
||||
echo "GET /config.json: $CFG"; [ "$CFG" = 200 ] || { echo "FAIL: captive portal not active"; fail=1; }
|
||||
fi
|
||||
PROBE=$(curl -s -m 10 -w ' %{http_code}' "http://$IP/probe")
|
||||
echo "GET /probe: $PROBE"
|
||||
ROOT=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "http://$IP/")
|
||||
echo "GET /: $ROOT"
|
||||
# prometheus has no page at / (no answer on ESP32, 404 on ESP8266, on dev too)
|
||||
[ "$V" = prom ] || [ "$ROOT" = 200 ] || { echo "FAIL: GET /"; fail=1; }
|
||||
if [ "$V" = prom ]; then
|
||||
MET=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "http://$IP/metrics")
|
||||
echo "GET /metrics: $MET"; [ "$MET" = 200 ] || { echo "FAIL: /metrics"; fail=1; }
|
||||
fi
|
||||
# A failed update also answers 200, with "Update Failed!" in the body.
|
||||
OTA=$(curl -s -m 120 -F "update=@$BIN" -w ' %{http_code}' "http://$IP/update" | tr -d '\r\n')
|
||||
echo "POST /update ($(wc -c < "$BIN" | tr -d ' ') B): $OTA"
|
||||
sleep 5
|
||||
if up; then
|
||||
sleep 6
|
||||
$PY "$S/ota_timing/apiinfo.py" "$IP=$D/.esphome/build/$N/build_info.json" 2>&1 | tail -1; API=${PIPESTATUS[0]}
|
||||
else
|
||||
echo "FAIL: board not back after the web OTA"; fail=1; API=1
|
||||
fi
|
||||
wait $CAP; CAPRC=$?
|
||||
[ "$CAPRC" = 0 ] && [ -s "$C" ] || { echo "FAIL: serial capture failed or empty (rc $CAPRC)"; fail=1; }
|
||||
[ "$PROBE" = "probe-ok 200" ] || { echo "FAIL: /probe"; fail=1; }
|
||||
case "$OTA" in *"Update Successful!"*" 200") ;; *) echo "FAIL: web OTA"; fail=1;; esac
|
||||
[ "$API" = 0 ] || { echo "FAIL: board not on the uploaded build after the web OTA"; fail=1; }
|
||||
# The image is the same build, so prove the reboot from the log: setup finishes again after the OTA reboot line.
|
||||
grep -a -q "Performing OTA reboot now" "$C" || { echo "FAIL: no OTA reboot in the serial log"; fail=1; }
|
||||
sed -n '/Performing OTA reboot now/,$p' "$C" | grep -a -q 'setup() finished' || { echo "FAIL: no boot after the OTA"; fail=1; }
|
||||
grep -a -q -E "Guru|abort\(\)|panic|[eE]xception \(|Soft WDT reset|wdt reset" "$C" && { echo "FAIL: crash"; fail=1; }
|
||||
[ $fail = 0 ] && echo PASS; exit $fail
|
||||
@@ -1,36 +0,0 @@
|
||||
# variant: ws (web_server), cp (captive_portal only, started from a button) or prom (captive_portal and prometheus).
|
||||
# A GET /probe handler is added after setup, the way an external component would.
|
||||
substitutions:
|
||||
name: web-handlers
|
||||
plat: esp32
|
||||
variant: ws
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- probehandler.hpp
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
web_server_base::global_web_server_base->add_handler(&probe_handler);
|
||||
ESP_LOGI("probe", "probe handler added after setup");
|
||||
|
||||
packages:
|
||||
plat: !include plat-${plat}.yaml
|
||||
variant: !include ${variant}.yaml
|
||||
|
||||
logger:
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
password: webtest123
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
- platform: web_server
|
||||
@@ -1,2 +0,0 @@
|
||||
web_server:
|
||||
port: 80
|
||||
@@ -4,11 +4,12 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack
|
||||
bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
|
||||
on this component and contain no SDK calls of their own.
|
||||
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. LibreTiny 1.14 or newer is required: earlier releases
|
||||
have no BLE bonding partition, so the Beken SDK erases the bootloader flash sector
|
||||
at boot on BK7238 (esphome#18646, libretiny-eu/libretiny#408). Known non-5.x
|
||||
families are rejected in to_code. Unknown families are capability-checked at compile time via
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE
|
||||
compiled in, the Beken SDK erases the bootloader flash sector at boot because
|
||||
LibreTiny's partition table has no BLE bonding entry (esphome#18646,
|
||||
libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in
|
||||
to_code. Unknown families are capability-checked at compile time via
|
||||
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
|
||||
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
|
||||
fails with a clear #error.
|
||||
@@ -28,6 +29,7 @@ from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_BK7251,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
@@ -52,9 +54,7 @@ BEKEN_BDK_VERSION_OPTION = "custom_versions.beken-bdk"
|
||||
# bootloader with BLE compiled in; the libretiny platform builds with this one
|
||||
MIN_BDK_VERSION = (3, 0, 78)
|
||||
# Older LibreTiny releases arm the Beken MCU sleep with wifi power save, which halts
|
||||
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414), and
|
||||
# have no BLE bond partition, so the BK7238 bond store erases flash sector 0 and
|
||||
# bricks the chip (libretiny-eu/libretiny#408)
|
||||
# the device once the station stops while BLE runs (libretiny-eu/libretiny#414)
|
||||
MIN_LIBRETINY_VERSION = (1, 14, 0)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -86,6 +86,14 @@ def _unsupported_family_message(family: str) -> str | None:
|
||||
)
|
||||
if family == FAMILY_BK7231Q:
|
||||
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
|
||||
if family == FAMILY_BK7238:
|
||||
return (
|
||||
"bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK "
|
||||
"erases the bootloader flash sector at boot and the device can no longer "
|
||||
"start (see https://github.com/esphome/esphome/issues/18646); support "
|
||||
"returns once the LibreTiny partition table fix "
|
||||
"(libretiny-eu/libretiny#408) is released"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -109,15 +117,14 @@ def check_bdk_version(value: str | None) -> None:
|
||||
|
||||
|
||||
def check_libretiny_version(version: cv.Version) -> None:
|
||||
"""Reject a LibreTiny release older than the one whose WiFi sleep and bond storage are safe with BLE."""
|
||||
"""Reject a LibreTiny release older than the one whose WiFi sleep is safe with BLE."""
|
||||
if (version.major, version.minor, version.patch) >= MIN_LIBRETINY_VERSION:
|
||||
return
|
||||
minimum = ".".join(str(part) for part in MIN_LIBRETINY_VERSION)
|
||||
raise cv.Invalid(
|
||||
f"bk72xx_ble needs LibreTiny {minimum} or newer, not {version}: older releases "
|
||||
"arm the Beken MCU sleep with wifi power save and the device halts once the "
|
||||
"station stops while BLE runs, and on BK7238 the bond store erases flash "
|
||||
"sector 0 and bricks the chip"
|
||||
"station stops while BLE runs"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ static constexpr uint8_t ENGINEERING_FRAME_MIN = OUT_PIN_SENSOR + 3 + HEADER_FOO
|
||||
// Data Header & Footer
|
||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
|
||||
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
void LD2410Component::dump_config() {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
@@ -423,7 +425,10 @@ void LD2410Component::handle_ack_data_() {
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = ld24xx::store_mac(&this->buffer_data_[10], this->mac_address_);
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
if (this->bluetooth_on_) {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
|
||||
@@ -120,6 +120,8 @@ static constexpr uint8_t ENGINEERING_FRAME_MIN = LIGHT_SENSOR + 3 + HEADER_FOOTE
|
||||
// Data Header & Footer
|
||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
|
||||
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
static constexpr uint8_t GATE_SENS_ACK_SIZE = ACK_PAYLOAD + TOTAL_GATES + HEADER_FOOTER_SIZE;
|
||||
|
||||
void LD2412Component::dump_config() {
|
||||
@@ -475,7 +477,10 @@ void LD2412Component::handle_ack_data_() {
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = ld24xx::store_mac(&this->buffer_data_[10], this->mac_address_);
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
if (this->bluetooth_on_) {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
|
||||
@@ -79,6 +79,8 @@ static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
|
||||
// 3 zones * 8 + footer (4).
|
||||
static constexpr uint8_t ZONE_ACK_FRAME_SIZE = 40;
|
||||
static constexpr uint8_t PERIODIC_FRAME_SIZE = 30; // header, three targets of 8 bytes, footer
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; };
|
||||
|
||||
@@ -190,11 +192,6 @@ void LD2450Component::dump_config() {
|
||||
for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Direction", s);
|
||||
}
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
for (text_sensor::TextSensor *s : this->target_xy_text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "TargetXY", s);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
ESP_LOGCONFIG(TAG, "Numbers:");
|
||||
@@ -298,6 +295,9 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
|
||||
if (check_millis == 0) {
|
||||
return true;
|
||||
}
|
||||
if (this->timeout_ == 0) {
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
|
||||
}
|
||||
return App.get_loop_component_start_time() - check_millis >= this->timeout_;
|
||||
}
|
||||
|
||||
@@ -468,16 +468,9 @@ void LD2450Component::handle_periodic_data_() {
|
||||
|
||||
// Store target info for zone target count. Zero out untracked targets (td==0)
|
||||
// so stale coordinates don't produce ghost counts in count_targets_in_zone_().
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
const Target previous = this->target_info_[index];
|
||||
#endif
|
||||
this->target_info_[index].x = (td > 0) ? tx : 0;
|
||||
this->target_info_[index].y = (td > 0) ? ty : 0;
|
||||
this->target_info_[index].is_moving = (td > 0) && is_moving;
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
this->update_target_xy_(index,
|
||||
previous.x != this->target_info_[index].x || previous.y != this->target_info_[index].y);
|
||||
#endif
|
||||
|
||||
} // End loop thru targets
|
||||
|
||||
@@ -628,7 +621,10 @@ void LD2450Component::handle_ack_data_() {
|
||||
break; // too short to hold a MAC address
|
||||
}
|
||||
|
||||
this->bluetooth_on_ = ld24xx::store_mac(&this->buffer_data_[10], this->mac_address_);
|
||||
this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
|
||||
if (this->bluetooth_on_) {
|
||||
std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
|
||||
}
|
||||
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
|
||||
@@ -866,27 +862,6 @@ void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::Tex
|
||||
this->direction_text_sensors_[target] = s;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
void LD2450Component::set_target_xy_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
|
||||
this->target_xy_text_sensors_[target] = s;
|
||||
}
|
||||
|
||||
// Publish the position of a target in one radar frame; the sensor's filters throttle it
|
||||
void LD2450Component::update_target_xy_(uint8_t target, bool changed) {
|
||||
text_sensor::TextSensor *s = this->target_xy_text_sensors_[target];
|
||||
if (s == nullptr || (!changed && s->has_state()))
|
||||
return;
|
||||
const Target &pos = this->target_info_[target];
|
||||
if (pos.x == 0 && pos.y == 0) {
|
||||
s->publish_state(DirectionStrings::get_progmem_str(DIRECTION_NA, DirectionStrings::LAST_INDEX));
|
||||
return;
|
||||
}
|
||||
// Longest is "-32767,-32767"
|
||||
char text[16];
|
||||
const size_t len = buf_append_printf(text, sizeof(text), 0, "%d,%d", pos.x, pos.y);
|
||||
s->publish_state(text, len);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Send Zone coordinates data to LD2450
|
||||
#ifdef USE_NUMBER
|
||||
@@ -925,14 +900,17 @@ void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number:
|
||||
// Set Presence Timeout load and save from flash
|
||||
#ifdef USE_NUMBER
|
||||
void LD2450Component::set_presence_timeout() {
|
||||
if (this->presence_timeout_number_ == nullptr)
|
||||
return;
|
||||
// Restore only while the number has no state yet: 0 is a valid presence_timeout (no hold)
|
||||
if (!this->presence_timeout_number_->has_state())
|
||||
this->presence_timeout_number_->publish_state(this->restore_from_flash_());
|
||||
const float timeout = this->presence_timeout_number_->state;
|
||||
this->save_to_flash_(timeout);
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
|
||||
if (this->presence_timeout_number_ != nullptr) {
|
||||
if (this->presence_timeout_number_->state == 0) {
|
||||
float timeout = this->restore_from_flash_();
|
||||
this->presence_timeout_number_->publish_state(timeout);
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
|
||||
}
|
||||
if (this->presence_timeout_number_->has_state()) {
|
||||
this->save_to_flash_(this->presence_timeout_number_->state);
|
||||
this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save Presence Timeout to flash
|
||||
|
||||
@@ -146,9 +146,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s);
|
||||
#endif
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
void set_target_xy_text_sensor(uint8_t target, text_sensor::TextSensor *s);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void set_zone_coordinate(number::Number *n);
|
||||
void set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, number::Number *y2);
|
||||
@@ -192,9 +189,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
void save_to_flash_(float value);
|
||||
float restore_from_flash_();
|
||||
bool get_timeout_status_(uint32_t check_millis);
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
void update_target_xy_(uint8_t target, bool changed);
|
||||
#endif
|
||||
void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving);
|
||||
|
||||
uint32_t presence_millis_ = 0;
|
||||
@@ -232,10 +226,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
|
||||
#endif
|
||||
#ifdef USE_LD2450_TARGET_XY
|
||||
// Publishes a target's position as "x,y" text, so both values always come from the same radar frame
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> target_xy_text_sensors_{};
|
||||
#endif
|
||||
|
||||
#ifdef LD2450_POLYGON_ZONE_COUNT
|
||||
StaticVector<PolygonZone *, LD2450_POLYGON_ZONE_COUNT> polygon_zones_;
|
||||
|
||||
@@ -40,8 +40,6 @@ void PolygonZone::control(const std::string &value) {
|
||||
ESP_LOGW(TAG, "'%s': could not save the polygon; keeping the previous one", LOG_STR_ARG(this->get_log_name()));
|
||||
} else {
|
||||
this->polygon_ = polygon;
|
||||
// A user-made edit is written to flash now, as wall-powered sensors lose power without warning
|
||||
global_preferences->sync();
|
||||
// A target seen inside the previous polygon must not keep the new one on
|
||||
this->last_inside_ms_ = 0;
|
||||
if (this->polygon_.empty())
|
||||
|
||||
@@ -18,10 +18,6 @@ from . import CONF_LD2450_ID, FILTER_SOURCE_FILES, LD2450Component # noqa: F401
|
||||
|
||||
DEPENDENCIES = ["ld2450"]
|
||||
|
||||
CONF_XY = "xy"
|
||||
|
||||
ICON_CROSSHAIRS_GPS = "mdi:crosshairs-gps"
|
||||
|
||||
MAX_TARGETS = 3
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
@@ -47,11 +43,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
entity_category=ENTITY_CATEGORY_NONE,
|
||||
icon=ICON_SIGN_DIRECTION,
|
||||
),
|
||||
cv.Optional(CONF_XY): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_NONE,
|
||||
icon=ICON_CROSSHAIRS_GPS,
|
||||
filters=text_sensor.timeout_throttle_filters("NA"),
|
||||
),
|
||||
}
|
||||
)
|
||||
for n in range(MAX_TARGETS)
|
||||
@@ -65,12 +56,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
|
||||
await text_sensors(CONF_MAC_ADDRESS, hub.set_mac_text_sensor)
|
||||
for n in range(MAX_TARGETS):
|
||||
if not (target_conf := config.get(f"target_{n + 1}")):
|
||||
continue
|
||||
if direction_config := target_conf.get(CONF_DIRECTION):
|
||||
if (direction_conf := config.get(f"target_{n + 1}")) and (
|
||||
direction_config := direction_conf.get(CONF_DIRECTION)
|
||||
):
|
||||
sens = await text_sensor.new_text_sensor(direction_config)
|
||||
cg.add(hub.set_direction_text_sensor(n, sens))
|
||||
if xy_config := target_conf.get(CONF_XY):
|
||||
cg.add_define("USE_LD2450_TARGET_XY")
|
||||
sens = await text_sensor.new_text_sensor(xy_config)
|
||||
cg.add(hub.set_target_xy_text_sensor(n, sens))
|
||||
@@ -41,15 +41,7 @@ template<typename T, size_t N> optional<size_t> find_index(const T (&arr)[N], T
|
||||
|
||||
// Formats the MAC address into the buffer, or "unknown" when it is not valid
|
||||
inline const char *format_mac_str(const uint8_t *mac_address, std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer) {
|
||||
// mac_address_is_valid() rejects the multicast bit, which radar Bluetooth MACs may have set; only all-00 and all-FF
|
||||
// mean no address
|
||||
uint8_t any_set = 0x00;
|
||||
uint8_t all_set = 0xFF;
|
||||
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||
any_set |= mac_address[i];
|
||||
all_set &= mac_address[i];
|
||||
}
|
||||
if (any_set != 0x00 && all_set != 0xFF) {
|
||||
if (mac_address_is_valid(mac_address)) {
|
||||
format_mac_addr_upper(mac_address, buffer.data());
|
||||
} else {
|
||||
ESPHOME_strncpy_P(buffer.data(), ESPHOME_PSTR("unknown"), buffer.size());
|
||||
@@ -57,21 +49,6 @@ inline const char *format_mac_str(const uint8_t *mac_address, std::span<char, MA
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
// MAC address the module uses when Bluetooth is disabled
|
||||
static constexpr uint8_t NO_MAC[MAC_ADDRESS_SIZE] PROGMEM = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
|
||||
|
||||
// Stores the radar's reported MAC, or clears it when the radar reports Bluetooth off; returns whether Bluetooth is on
|
||||
inline bool store_mac(const uint8_t *reported, uint8_t *mac_address) {
|
||||
uint8_t no_mac[MAC_ADDRESS_SIZE];
|
||||
progmem_memcpy(no_mac, NO_MAC, MAC_ADDRESS_SIZE);
|
||||
if (std::memcmp(reported, no_mac, MAC_ADDRESS_SIZE) == 0) {
|
||||
std::memset(mac_address, 0, MAC_ADDRESS_SIZE);
|
||||
return false;
|
||||
}
|
||||
std::memcpy(mac_address, reported, MAC_ADDRESS_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper function to format firmware version with stack allocation
|
||||
// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety)
|
||||
inline void format_version_str(const uint8_t *version, std::span<char, 20> buffer) {
|
||||
|
||||
@@ -44,6 +44,7 @@ from .const import (
|
||||
FAMILY_BK7238,
|
||||
FAMILY_COMPONENT,
|
||||
FAMILY_FRIENDLY,
|
||||
FAMILY_LN882H,
|
||||
FAMILY_RTL8710B,
|
||||
FAMILY_RTL8720C,
|
||||
KEY_BOARD,
|
||||
@@ -60,8 +61,8 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Fault handlers the crash handler wraps (BK7238, BK7231N)
|
||||
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
|
||||
# Families with a crash handler; the RTL8720C installs it at run time (no wraps).
|
||||
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_RTL8720C)
|
||||
# Families with a crash handler; the RTL8720C and LN882H install it at run time (no wraps).
|
||||
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_LN882H, FAMILY_RTL8720C)
|
||||
|
||||
CODEOWNERS = ["@kuba2k2"]
|
||||
AUTO_LOAD = ["preferences"]
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER
|
||||
|
||||
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_VARIANT_RTL8720C)
|
||||
#include "crash_handler.h"
|
||||
|
||||
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH)
|
||||
#error "The LibreTiny crash handler has no fault capture for this chip family"
|
||||
#endif
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "core.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
@@ -58,6 +59,18 @@ extern char __ram_code_text_end__[];
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8720C
|
||||
|
||||
#ifdef USE_LN882X
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
// Bounds of the code in flash and of the IRAM_ATTR code (linker symbols; the latter from patch_linker.py).
|
||||
extern char _stext[];
|
||||
extern char _etext[];
|
||||
extern char __esphome_sram_text_start[];
|
||||
extern char __esphome_sram_text_end[];
|
||||
}
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_LN882X
|
||||
|
||||
namespace esphome::libretiny {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "libretiny.crash");
|
||||
@@ -69,7 +82,7 @@ static constexpr uint32_t CRASH_MAGIC_V2 = 0xDEAD0002;
|
||||
static constexpr uint32_t CRASH_REASON_UNDEFINED = 1;
|
||||
static constexpr uint32_t CRASH_REASON_PREFETCH_ABORT = 2;
|
||||
static constexpr uint32_t CRASH_REASON_DATA_ABORT = 3;
|
||||
// RTL8720C: the Cortex-M exception number (3 to 7) plus this offset.
|
||||
// RTL8720C and LN882H: the Cortex-M exception number (3 to 7) plus this offset.
|
||||
static constexpr uint32_t CRASH_REASON_CORTEX_M_OFFSET = 1;
|
||||
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort",
|
||||
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault");
|
||||
@@ -92,6 +105,9 @@ struct CrashRecord {
|
||||
#ifdef USE_BK72XX
|
||||
// Kept in ITCM, see patch_noinit.py.script.
|
||||
#define CRASH_RECORD_SECTION ".noinit"
|
||||
#elif defined(USE_LN882X)
|
||||
// ln882hki_bsp.template.ld puts .noinit in RETENTION RAM, which startup does not clear.
|
||||
#define CRASH_RECORD_SECTION ".noinit"
|
||||
#else
|
||||
// rtl8710c_ram.ld keeps .ram.bss.noinit* out of the startup zeroing.
|
||||
#define CRASH_RECORD_SECTION ".ram.bss.noinit.esphome_crash"
|
||||
@@ -146,6 +162,9 @@ static inline bool is_code_addr(uint32_t val) {
|
||||
#ifdef USE_BK72XX
|
||||
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
|
||||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
|
||||
#elif defined(USE_LN882X)
|
||||
return (addr >= (uint32_t) _stext && addr < (uint32_t) _etext) ||
|
||||
(addr >= (uint32_t) __esphome_sram_text_start && addr < (uint32_t) __esphome_sram_text_end);
|
||||
#else
|
||||
return (addr >= (uint32_t) __xip_code_text_start__ && addr < (uint32_t) __xip_code_text_end__) ||
|
||||
(addr >= (uint32_t) __ram_entry_text_start__ && addr < (uint32_t) __ram_code_text_end__);
|
||||
@@ -257,19 +276,29 @@ extern "C" void __wrap_bk_trap_dabt(arm_registers *regs) {
|
||||
|
||||
#endif // USE_BK72XX
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
|
||||
|
||||
// The SDK points the fault vectors (3 HardFault to 7 SecureFault) of its RAM vector table (VTOR) at a
|
||||
// ROM handler that prints a register dump and then spins. crash_handler_install() puts
|
||||
// esphome_fault_entry() in front of it, as the SDK's own CONFIG_MIIO patch (app_start.c) does.
|
||||
// The SDK's RAM vector table (VTOR) points the fault vectors at a handler that prints a register dump and then
|
||||
// spins: a ROM handler on the RTL8720C, CmBacktrace (hal_interrupt.c) on the LN882H. crash_handler_install()
|
||||
// puts esphome_fault_entry() in front of it, as the RTL8720C SDK's own CONFIG_MIIO patch (app_start.c) does.
|
||||
static_assert(LT_HW_WATCHDOG, "the crash reboot relies on the hardware watchdog");
|
||||
// The ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
|
||||
// The RTL8720C ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
|
||||
// watchdog from arch_init().
|
||||
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 1000;
|
||||
static constexpr uint32_t FIRST_FAULT_VECTOR = 3;
|
||||
static constexpr uint32_t FAULT_VECTOR_COUNT = 5;
|
||||
static constexpr uint32_t FIRST_FAULT_VECTOR = 3; // HardFault, then MemManage, BusFault, UsageFault
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
|
||||
static constexpr uint32_t FAULT_VECTOR_COUNT = 5; // and SecureFault (ARMv8-M)
|
||||
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
|
||||
static constexpr uint32_t SRAM_START = 0x10000000;
|
||||
static constexpr uint32_t SRAM_END = 0x1003FA00;
|
||||
#else
|
||||
static constexpr uint32_t FAULT_VECTOR_COUNT = 4;
|
||||
// RAM0, where the stacks and heap live (ln882hki_bsp.template.ld).
|
||||
static constexpr uint32_t SRAM_START = 0x20000000;
|
||||
static constexpr uint32_t SRAM_END = 0x20049C00;
|
||||
#endif
|
||||
|
||||
// Architectural System Control Block registers (ARMv8-M).
|
||||
// Architectural System Control Block registers (ARMv7-M and ARMv8-M).
|
||||
static constexpr uintptr_t SCB_VTOR = 0xE000ED08;
|
||||
static constexpr uintptr_t SCB_CFSR = 0xE000ED28;
|
||||
static constexpr uintptr_t SCB_MMFAR = 0xE000ED34;
|
||||
@@ -277,26 +306,34 @@ static constexpr uintptr_t SCB_BFAR = 0xE000ED38;
|
||||
static constexpr uint32_t CFSR_MMARVALID = 1u << 7;
|
||||
static constexpr uint32_t CFSR_BFARVALID = 1u << 15;
|
||||
|
||||
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
|
||||
static constexpr uint32_t SRAM_START = 0x10000000;
|
||||
static constexpr uint32_t SRAM_END = 0x1003FA00;
|
||||
|
||||
// Exception frame: r0-r3, r12, lr, pc, xPSR (the RTL8720C has no FPU, so never the extended frame).
|
||||
// Exception frame: r0-r3, r12, lr, pc, xPSR, then the FPU registers when EXC_RETURN says so (LN882H only).
|
||||
static constexpr uint32_t FRAME_LR = 5;
|
||||
static constexpr uint32_t FRAME_PC = 6;
|
||||
static constexpr uint32_t FRAME_XPSR = 7;
|
||||
static constexpr uint32_t FRAME_WORDS = 8;
|
||||
static constexpr uint32_t EXTENDED_FRAME_WORDS = 26;
|
||||
static constexpr uint32_t EXC_RETURN_STANDARD_FRAME = 1u << 4;
|
||||
static constexpr uint32_t XPSR_STACK_ALIGNED = 1u << 9;
|
||||
|
||||
// The ROM handlers fault_entry() continues into, by vector.
|
||||
static uint32_t s_rom_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
// The SDK handlers fault_entry() continues into, by vector.
|
||||
static uint32_t s_sdk_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_fault_recorded = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static inline uint32_t read_reg(uintptr_t addr) {
|
||||
return *(const volatile uint32_t *) addr; // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
// Returns the ROM handler to continue into. pc, lr and sp stay 0 when the frame is outside DTCM.
|
||||
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector) {
|
||||
// Returns the SDK handler to continue into. pc, lr and sp stay 0 when the frame is outside SRAM.
|
||||
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector,
|
||||
uint32_t exc_return) {
|
||||
// A later fault, in the recording below or in the SDK handler (CmBacktrace on the LN882H walks past the end
|
||||
// of RAM), keeps the first record and waits for the watchdog instead of starting the dump again.
|
||||
if (s_fault_recorded) {
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
s_fault_recorded = true;
|
||||
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
|
||||
uint32_t cfsr = read_reg(SCB_CFSR);
|
||||
uint32_t address = 0;
|
||||
if (cfsr & CFSR_BFARVALID) {
|
||||
@@ -309,25 +346,24 @@ extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *f
|
||||
s_record.fault_address = address;
|
||||
|
||||
auto frame_addr = (uint32_t) frame;
|
||||
uint32_t frame_words = (exc_return & EXC_RETURN_STANDARD_FRAME) ? FRAME_WORDS : EXTENDED_FRAME_WORDS;
|
||||
uint32_t sp = 0;
|
||||
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - FRAME_WORDS * 4 && (frame_addr & 3) == 0) {
|
||||
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - frame_words * 4 && (frame_addr & 3) == 0) {
|
||||
s_record.pc = frame[FRAME_PC];
|
||||
s_record.lr = frame[FRAME_LR];
|
||||
sp = frame_addr + FRAME_WORDS * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
|
||||
sp = frame_addr + frame_words * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
|
||||
} else {
|
||||
s_record.pc = 0;
|
||||
s_record.lr = 0;
|
||||
}
|
||||
s_record.sp = sp;
|
||||
finish_record(sp, SRAM_START, SRAM_END);
|
||||
|
||||
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
|
||||
return s_rom_fault_vectors[vector - FIRST_FAULT_VECTOR];
|
||||
return s_sdk_fault_vectors[vector - FIRST_FAULT_VECTOR];
|
||||
}
|
||||
|
||||
// Fault vector: calls esphome_record_fault(frame, exception number), then jumps to the ROM handler it returns
|
||||
// with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the ROM handler
|
||||
// reads EXC_RETURN from lr.
|
||||
// Fault vector: calls esphome_record_fault(frame, exception number, EXC_RETURN), then jumps to the SDK handler it
|
||||
// returns with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the SDK
|
||||
// handler reads EXC_RETURN from lr.
|
||||
extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
|
||||
__asm volatile("push {r0-r3, r4, lr} \n" // r4 only keeps the stack 8-byte aligned
|
||||
"tst lr, #4 \n"
|
||||
@@ -338,8 +374,9 @@ extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
|
||||
"addeq r0, r0, #24 \n" // MSP: skip the six words pushed above
|
||||
"mrs r1, ipsr \n"
|
||||
"ubfx r1, r1, #0, #9 \n" // exception number, 3 to 7
|
||||
"mov r2, lr \n"
|
||||
"bl esphome_record_fault \n"
|
||||
"mov r12, r0 \n" // ROM handler address
|
||||
"mov r12, r0 \n" // SDK handler address
|
||||
"pop {r0-r3, r4, lr} \n"
|
||||
"bx r12 \n");
|
||||
}
|
||||
@@ -347,12 +384,12 @@ extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
|
||||
void crash_handler_install() {
|
||||
auto *vectors = (uint32_t *) read_reg(SCB_VTOR); // NOLINT(performance-no-int-to-ptr)
|
||||
for (uint32_t i = 0; i < FAULT_VECTOR_COUNT; i++) {
|
||||
s_rom_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
|
||||
s_sdk_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
|
||||
vectors[FIRST_FAULT_VECTOR + i] = (uint32_t) &esphome_fault_entry;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8720C
|
||||
#endif // USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
|
||||
|
||||
} // namespace esphome::libretiny
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER
|
||||
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(USE_LN882X)
|
||||
// Cortex-M families: the recorder goes in front of the SDK's fault handlers through the RAM vector table.
|
||||
#define USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
|
||||
#endif
|
||||
|
||||
namespace esphome::libretiny {
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
@@ -14,7 +19,7 @@ void crash_handler_log();
|
||||
/// Returns true if crash data was found this boot, reading it first if needed.
|
||||
bool crash_handler_has_data();
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
|
||||
/// Put the crash recorder in front of the SDK's fault handlers.
|
||||
void crash_handler_install();
|
||||
#endif
|
||||
|
||||
@@ -156,6 +156,7 @@ COMPONENT_CODEOWNERS = {
|
||||
# Extra imports per component; the CLI looks up process_stacktrace on the platform package.
|
||||
COMPONENT_EXTRA_IMPORTS = {
|
||||
"bk72xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
|
||||
"ln882x": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
|
||||
"rtl87xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace esphome {
|
||||
void arch_init() {
|
||||
libretiny::setup_preferences();
|
||||
lt_wdt_enable(10000L);
|
||||
#if defined(USE_LIBRETINY_CRASH_HANDLER) && defined(USE_LIBRETINY_VARIANT_RTL8720C)
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
|
||||
libretiny::crash_handler_install();
|
||||
#endif
|
||||
#ifdef USE_BK72XX
|
||||
|
||||
@@ -29,6 +29,7 @@ from esphome.components.libretiny.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import LN882X_BOARD_PINS, LN882X_BOARDS
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "prometheus_handler.h"
|
||||
#include <cinttypes>
|
||||
#if defined(USE_NETWORK) && !defined(USE_HOST)
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
@@ -101,24 +100,6 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
|
||||
this->valve_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
this->date_type_(stream);
|
||||
for (auto *obj : App.get_dates())
|
||||
this->date_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
this->time_type_(stream);
|
||||
for (auto *obj : App.get_times())
|
||||
this->time_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
this->datetime_type_(stream);
|
||||
for (auto *obj : App.get_datetimes())
|
||||
this->datetime_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
this->climate_type_(stream);
|
||||
for (auto *obj : App.get_climates())
|
||||
@@ -1115,78 +1096,6 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_DATETIME_DATE) || defined(USE_DATETIME_DATETIME) || defined(USE_DATETIME_TIME)
|
||||
void PrometheusHandler::date_time_rows_(AsyncResponseStream *stream, ProgmemStr failed_metric, ProgmemStr value_metric,
|
||||
EntityBase *obj, int64_t value_ms, std::string &area, std::string &node,
|
||||
std::string &friendly_name) {
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
this->print_metric_labels_(stream, failed_metric, obj, area, node, friendly_name);
|
||||
if (!obj->has_state()) {
|
||||
stream->print(ESPHOME_F("\"} 1\n"));
|
||||
return;
|
||||
}
|
||||
stream->print(ESPHOME_F("\"} 0\n"));
|
||||
this->print_metric_labels_(stream, value_metric, obj, area, node, friendly_name);
|
||||
// Date and datetime are ms since the epoch (what Grafana expects), time is ms since midnight
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "\"} %" PRId64 "\n", value_ms);
|
||||
stream->print(buf);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void PrometheusHandler::date_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_date_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_date_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::date_row_(AsyncResponseStream *stream, datetime::DateEntity *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
int64_t value_ms = 0;
|
||||
if (obj->has_state()) {
|
||||
// DateEntity::state_as_esptime() leaves the time fields unset, so build midnight here
|
||||
ESPTime t{};
|
||||
t.year = obj->year;
|
||||
t.month = obj->month;
|
||||
t.day_of_month = obj->day;
|
||||
t.recalc_timestamp_local();
|
||||
value_ms = static_cast<int64_t>(t.timestamp) * 1000;
|
||||
}
|
||||
this->date_time_rows_(stream, ESPHOME_F("esphome_date_failed"), ESPHOME_F("esphome_date_value"), obj, value_ms, area,
|
||||
node, friendly_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void PrometheusHandler::time_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_time_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_time_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::time_row_(AsyncResponseStream *stream, datetime::TimeEntity *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
int64_t value_ms = 0;
|
||||
if (obj->has_state())
|
||||
value_ms = (static_cast<int64_t>(obj->hour) * 3600 + obj->minute * 60 + obj->second) * 1000;
|
||||
this->date_time_rows_(stream, ESPHOME_F("esphome_time_failed"), ESPHOME_F("esphome_time_value"), obj, value_ms, area,
|
||||
node, friendly_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void PrometheusHandler::datetime_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_datetime_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_datetime_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::datetime_row_(AsyncResponseStream *stream, datetime::DateTimeEntity *obj, std::string &area,
|
||||
std::string &node, std::string &friendly_name) {
|
||||
int64_t value_ms = 0;
|
||||
if (obj->has_state())
|
||||
value_ms = static_cast<int64_t>(obj->state_as_esptime().timestamp) * 1000;
|
||||
this->date_time_rows_(stream, ESPHOME_F("esphome_datetime_failed"), ESPHOME_F("esphome_datetime_value"), obj,
|
||||
value_ms, area, node, friendly_name);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::prometheus
|
||||
|
||||
#endif
|
||||
@@ -193,36 +193,6 @@ class PrometheusHandler final : public AsyncWebHandler, public Component {
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#if defined(USE_DATETIME_DATE) || defined(USE_DATETIME_DATETIME) || defined(USE_DATETIME_TIME)
|
||||
/// Print the failed row and, when the entity has a state, the value row in milliseconds
|
||||
void date_time_rows_(AsyncResponseStream *stream, ProgmemStr failed_metric, ProgmemStr value_metric, EntityBase *obj,
|
||||
int64_t value_ms, std::string &area, std::string &node, std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
/// Return the type for prometheus
|
||||
void date_type_(AsyncResponseStream *stream);
|
||||
/// Return the date state as prometheus data point
|
||||
void date_row_(AsyncResponseStream *stream, datetime::DateEntity *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
/// Return the type for prometheus
|
||||
void time_type_(AsyncResponseStream *stream);
|
||||
/// Return the time state as prometheus data point
|
||||
void time_row_(AsyncResponseStream *stream, datetime::TimeEntity *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
/// Return the type for prometheus
|
||||
void datetime_type_(AsyncResponseStream *stream);
|
||||
/// Return the datetime state as prometheus data point
|
||||
void datetime_row_(AsyncResponseStream *stream, datetime::DateTimeEntity *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
/// Return the type for prometheus
|
||||
void climate_type_(AsyncResponseStream *stream);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_task_wdt.h"
|
||||
#endif
|
||||
@@ -94,10 +96,21 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
JpegDecoder::JpegDecoder(RuntimeImage *image)
|
||||
: ImageDecoder(image, JPEG), jpeg_(RAMAllocator<JPEGDEC>().make_unique()) {
|
||||
if (this->jpeg_ == nullptr) {
|
||||
JpegDecoder::JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {
|
||||
RAMAllocator<JPEGDEC> allocator;
|
||||
JPEGDEC *jpeg = allocator.allocate(1);
|
||||
if (jpeg == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for the JPEG decoder");
|
||||
return;
|
||||
}
|
||||
this->jpeg_ = new (jpeg) JPEGDEC();
|
||||
}
|
||||
|
||||
JpegDecoder::~JpegDecoder() {
|
||||
if (this->jpeg_ != nullptr) {
|
||||
this->jpeg_->~JPEGDEC();
|
||||
RAMAllocator<JPEGDEC> allocator;
|
||||
allocator.deallocate(this->jpeg_, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class JpegDecoder : public ImageDecoder {
|
||||
* @param image The RuntimeImage to decode the stream into.
|
||||
*/
|
||||
JpegDecoder(RuntimeImage *image);
|
||||
~JpegDecoder() override;
|
||||
|
||||
int prepare(size_t expected_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
@@ -46,8 +47,8 @@ class JpegDecoder : public ImageDecoder {
|
||||
static int32_t source_seek(JPEGFILE *file, int32_t position);
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
/// About 18 KB of decoder state: PSRAM first, nullptr if out of memory
|
||||
RAMUniquePtr<JPEGDEC> jpeg_;
|
||||
/// About 18 KB of decoder state, allocated like pngle in PngDecoder: PSRAM first, nullptr if out of memory
|
||||
JPEGDEC *jpeg_{nullptr};
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool source_failed_{false};
|
||||
|
||||
@@ -74,16 +74,9 @@ class TinyUSB final : public Component {
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
#if CFG_TUD_CDC || CFG_TUD_NCM || CFG_TUD_ECM_RNDIS
|
||||
// Interface association (IAD) only for classes that group interfaces, as esp_tinyusb's default descriptor does
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
#else
|
||||
.bDeviceClass = TUSB_CLASS_UNSPECIFIED,
|
||||
.bDeviceSubClass = 0,
|
||||
.bDeviceProtocol = 0,
|
||||
#endif
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A,
|
||||
.idProduct = 0x4001,
|
||||
|
||||
@@ -185,7 +185,6 @@
|
||||
#define MDNS_DYNAMIC_TXT_COUNT 2
|
||||
#define LD2420_LISTENER_COUNT 1
|
||||
#define LD2450_POLYGON_ZONE_COUNT 2
|
||||
#define USE_LD2450_TARGET_XY
|
||||
#define SEN6X_STATUS_SENSOR_COUNT 1
|
||||
#define UART_BINARY_SENSOR_COUNT 2
|
||||
#define MICRONOVA_LISTENER_COUNT 1
|
||||
@@ -782,7 +781,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C)
|
||||
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(USE_LN882X)
|
||||
#define USE_LIBRETINY_CRASH_HANDLER
|
||||
#endif
|
||||
#define USE_BK72XX_BLE
|
||||
|
||||
@@ -67,6 +67,7 @@ STACKTRACE_GATES: Final[dict[str, str]] = {
|
||||
),
|
||||
PLATFORM_RP2: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_BK72XX: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_LN882X: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_RTL87XX: _CRASH_HANDLER_GATE,
|
||||
PLATFORM_NRF52: r"0x[0-9a-fA-F]{3,}\b|Last crash:",
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ def _addr2line(tool: str, elf: Path, addr: str) -> str:
|
||||
def process_crash_handler_line(
|
||||
config: ConfigType, line: str, backtrace_state: bool
|
||||
) -> bool:
|
||||
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx and rtl87xx."""
|
||||
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx, rtl87xx and ln882x."""
|
||||
if CRASH_HANDLER_RE.search(line):
|
||||
_LOGGER.error("Crash detected - decoding addresses")
|
||||
return True
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.core import EsphomeError
|
||||
("test_bk7231t.yaml", "BK7231T.*BLE 4.2"),
|
||||
("test_bk7252.yaml", "BK7251.*BLE 4.2"),
|
||||
("test_bk7231q.yaml", "BK7231Q.*no BLE"),
|
||||
("test_bk7238.yaml", "BK7238.*bootloader"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_family_rejected(
|
||||
@@ -32,11 +33,9 @@ def test_unsupported_family_rejected(
|
||||
assert "cannot compile" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_file", ["test_bk7231n.yaml", "test_bk7238.yaml"])
|
||||
def test_ble5_family_generates(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
main_cpp = generate_main(component_config_path("test_bk7231n.yaml"))
|
||||
assert "bk72xx_ble::BK72xxBLE" in main_cpp
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region) and RTL8720C."""
|
||||
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region), RTL8720C and LN882H."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -19,7 +19,7 @@ from esphome.core import CORE
|
||||
("bk7231t.yaml", False, False),
|
||||
("rtl8720c.yaml", True, False), # installed at run time
|
||||
("rtl8710b.yaml", False, False),
|
||||
("ln882x.yaml", False, False),
|
||||
("ln882x.yaml", True, False), # installed at run time
|
||||
],
|
||||
)
|
||||
def test_crash_handler_family_gate(
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
bk72xx_ble: !include common.yaml
|
||||
@@ -30,18 +30,11 @@ class MockUARTComponent : public uart::UARTComponent {
|
||||
class TestableLD2450 : public LD2450Component {
|
||||
public:
|
||||
using LD2450Component::buffer_data_;
|
||||
using LD2450Component::bluetooth_on_;
|
||||
using LD2450Component::buffer_pos_;
|
||||
using LD2450Component::get_timeout_status_;
|
||||
using LD2450Component::mac_address_;
|
||||
using LD2450Component::readline_;
|
||||
using LD2450Component::timeout_;
|
||||
using LD2450Component::version_;
|
||||
using LD2450Component::zone_config_;
|
||||
using LD2450Component::zone_type_;
|
||||
#ifdef USE_NUMBER
|
||||
using LD2450Component::pref_;
|
||||
#endif
|
||||
|
||||
void feed(const std::vector<uint8_t> &data) {
|
||||
for (uint8_t byte : data) {
|
||||
|
||||
@@ -131,23 +131,12 @@ text_sensor:
|
||||
target_1:
|
||||
direction:
|
||||
name: Target-1 Direction
|
||||
xy:
|
||||
name: Target-1 XY
|
||||
target_2:
|
||||
direction:
|
||||
name: Target-2 Direction
|
||||
xy:
|
||||
name: Target-2 XY
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 200ms
|
||||
value: NA
|
||||
target_3:
|
||||
direction:
|
||||
name: Target-3 Direction
|
||||
xy:
|
||||
name: Target-3 XY
|
||||
filters: []
|
||||
|
||||
number:
|
||||
- platform: ld2450
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "common.h"
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
|
||||
namespace esphome::ld2450::testing {
|
||||
|
||||
namespace {
|
||||
// MAC query ACK (command 0xA5): header, length 10, command, result ok, 6 MAC bytes, footer.
|
||||
std::vector<uint8_t> make_mac_ack(const uint8_t (&mac)[MAC_ADDRESS_SIZE]) {
|
||||
std::vector<uint8_t> frame(std::begin(ld24xx::CMD_FRAME_HEADER), std::end(ld24xx::CMD_FRAME_HEADER));
|
||||
frame.insert(frame.end(), {0x0A, 0x00, 0xA5, 0x01, 0x00, 0x00});
|
||||
frame.insert(frame.end(), mac, mac + MAC_ADDRESS_SIZE);
|
||||
frame.insert(frame.end(), std::begin(ld24xx::CMD_FRAME_FOOTER), std::end(ld24xx::CMD_FRAME_FOOTER));
|
||||
return frame;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class LD2450MacAckTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { this->ld2450_.set_uart_parent(&this->mock_uart_); }
|
||||
std::string mac_str_() const {
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return ld24xx::format_mac_str(this->ld2450_.mac_address_, buffer);
|
||||
}
|
||||
|
||||
MockUARTComponent mock_uart_;
|
||||
TestableLD2450 ld2450_;
|
||||
};
|
||||
|
||||
TEST_F(LD2450MacAckTest, BluetoothOffAckClearsStoredMac) {
|
||||
// Captured from radar firmware 2.04.23101915; the first byte has the multicast bit set
|
||||
static constexpr uint8_t BEDROOM_MAC[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
this->ld2450_.feed(make_mac_ack(BEDROOM_MAC));
|
||||
EXPECT_TRUE(this->ld2450_.bluetooth_on_);
|
||||
EXPECT_THAT(this->ld2450_.mac_address_, ::testing::ElementsAreArray(BEDROOM_MAC));
|
||||
ASSERT_EQ(this->mac_str_(), "81:2C:62:BE:C9:96");
|
||||
|
||||
this->ld2450_.feed(make_mac_ack(ld24xx::NO_MAC));
|
||||
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
|
||||
EXPECT_FALSE(this->ld2450_.bluetooth_on_);
|
||||
EXPECT_THAT(this->ld2450_.mac_address_, ::testing::Each(0));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450::testing
|
||||
@@ -1,157 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include "common.h"
|
||||
#include "esphome/components/host/preferences.h"
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::ld2450::testing {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
TestableLD2450 *hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// What the generated apply="set_presence_timeout()", publish="before" number does
|
||||
void apply_presence_timeout(number::Number *entity, float value) {
|
||||
entity->publish_state(value);
|
||||
hub->set_presence_timeout();
|
||||
}
|
||||
using PresenceTimeoutNumber = number::ApplyNumber<apply_presence_timeout>;
|
||||
|
||||
// The presence_timeout part of LD2450Component::setup(), without its radar restart and scheduled info read
|
||||
void boot(TestableLD2450 &radar, number::Number *entity) {
|
||||
radar.set_presence_timeout_number(entity);
|
||||
if (entity != nullptr) {
|
||||
radar.pref_ = entity->make_entity_preference<float>();
|
||||
radar.set_presence_timeout();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class LD2450PresenceTimeoutTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->temp_dir_ = fs::temp_directory_path() / "esphome_ld2450_presence_timeout_test";
|
||||
fs::remove_all(this->temp_dir_);
|
||||
fs::create_directories(this->temp_dir_);
|
||||
setenv("ESPHOME_PREFDIR", this->temp_dir_.c_str(), 1);
|
||||
App.pre_setup("test_ld2450_timeout", strlen("test_ld2450_timeout"), "", 0);
|
||||
// A fresh store per test: the shared one keeps the file path of whichever test set it up first, whose directory
|
||||
// may already be gone, and its in-memory values
|
||||
host::host_preferences = &this->prefs_;
|
||||
global_preferences = &this->prefs_;
|
||||
this->ld2450_.set_uart_parent(&this->uart_);
|
||||
hub = &this->ld2450_;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
hub = nullptr;
|
||||
host::setup_preferences();
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
fs::remove_all(this->temp_dir_);
|
||||
}
|
||||
|
||||
/// Save a presence_timeout the way a previous boot did, and flush it
|
||||
void save_timeout(float seconds) {
|
||||
PresenceTimeoutNumber saved;
|
||||
ESPPreferenceObject pref = saved.make_entity_preference<float>();
|
||||
ASSERT_TRUE(pref.save(&seconds));
|
||||
ASSERT_TRUE(global_preferences->sync());
|
||||
}
|
||||
|
||||
/// Whether a frame without a target, now ms after the last one, clears presence
|
||||
bool cleared_after(uint32_t last_target_ms, uint32_t now_ms) {
|
||||
LoopBlockingGuard clock{nullptr, nullptr, now_ms};
|
||||
return this->ld2450_.get_timeout_status_(last_target_ms);
|
||||
}
|
||||
|
||||
host::HostPreferences prefs_;
|
||||
MockUARTComponent uart_;
|
||||
TestableLD2450 ld2450_;
|
||||
PresenceTimeoutNumber timeout_number_;
|
||||
fs::path temp_dir_;
|
||||
};
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, NoNumberKeepsDefaultHold) {
|
||||
boot(this->ld2450_, nullptr);
|
||||
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 5000u);
|
||||
EXPECT_FALSE(this->cleared_after(1000, 5999));
|
||||
EXPECT_TRUE(this->cleared_after(1000, 6000));
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, SetupWithNothingSavedPublishesDefault) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
ASSERT_TRUE(this->timeout_number_.has_state());
|
||||
EXPECT_EQ(this->timeout_number_.state, 5.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 5000u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, SetupWithSavedZeroPublishesZero) {
|
||||
this->save_timeout(0);
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
ASSERT_TRUE(this->timeout_number_.has_state());
|
||||
EXPECT_EQ(this->timeout_number_.state, 0.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 0u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, SetupWithSavedValuePublishesIt) {
|
||||
this->save_timeout(12);
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
EXPECT_EQ(this->timeout_number_.state, 12.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 12000u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, ControlZeroKeepsZeroAndHoldsNothing) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
ASSERT_EQ(this->timeout_number_.state, 5.0f);
|
||||
|
||||
auto call = this->timeout_number_.make_call();
|
||||
call.set_value(0);
|
||||
call.perform();
|
||||
|
||||
// The old code restored the saved 5 s as soon as the state read 0
|
||||
EXPECT_EQ(this->timeout_number_.state, 0.0f);
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 0u);
|
||||
EXPECT_TRUE(this->cleared_after(1000, 1000));
|
||||
EXPECT_TRUE(this->cleared_after(1000, 1001));
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, ControlZeroIsSavedForTheNextBoot) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
auto call = this->timeout_number_.make_call();
|
||||
call.set_value(0);
|
||||
call.perform();
|
||||
ASSERT_TRUE(global_preferences->sync());
|
||||
|
||||
TestableLD2450 rebooted;
|
||||
PresenceTimeoutNumber rebooted_number;
|
||||
rebooted.set_uart_parent(&this->uart_);
|
||||
hub = &rebooted;
|
||||
boot(rebooted, &rebooted_number);
|
||||
|
||||
ASSERT_TRUE(rebooted_number.has_state());
|
||||
EXPECT_EQ(rebooted_number.state, 0.0f);
|
||||
EXPECT_EQ(rebooted.timeout_, 0u);
|
||||
}
|
||||
|
||||
TEST_F(LD2450PresenceTimeoutTest, ControlNonZeroSetsHold) {
|
||||
boot(this->ld2450_, &this->timeout_number_);
|
||||
|
||||
auto call = this->timeout_number_.make_call();
|
||||
call.set_value(3);
|
||||
call.perform();
|
||||
|
||||
EXPECT_EQ(this->ld2450_.timeout_, 3000u);
|
||||
EXPECT_FALSE(this->cleared_after(1000, 3999));
|
||||
EXPECT_TRUE(this->cleared_after(1000, 4000));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2450::testing
|
||||
#endif // USE_HOST
|
||||
@@ -1,55 +0,0 @@
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
|
||||
namespace esphome::ld24xx::testing {
|
||||
|
||||
static std::string format_mac(const uint8_t (&mac)[MAC_ADDRESS_SIZE]) {
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return format_mac_str(mac, buffer);
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, MulticastBitMacIsShownAsIs) {
|
||||
// First byte 0x81 has the multicast bit set, and the radar reports it as its Bluetooth MAC.
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
EXPECT_EQ(format_mac(mac), "81:2C:62:BE:C9:96");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, UnicastMacIsShownAsIs) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0x80, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
EXPECT_EQ(format_mac(mac), "80:2C:62:BE:C9:96");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, AllZeroMacIsUnknown) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
|
||||
EXPECT_EQ(format_mac(mac), "unknown");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, AllFfMacIsUnknown) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
EXPECT_EQ(format_mac(mac), "unknown");
|
||||
}
|
||||
|
||||
TEST(LD24xxFormatMac, MacWithSomeZeroOrFfBytesIsShown) {
|
||||
const uint8_t mac[MAC_ADDRESS_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||
EXPECT_EQ(format_mac(mac), "00:00:00:00:00:01");
|
||||
const uint8_t ff_mac[MAC_ADDRESS_SIZE] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
|
||||
EXPECT_EQ(format_mac(ff_mac), "FF:FF:FF:FF:FF:FE");
|
||||
}
|
||||
|
||||
TEST(LD24xxStoreMac, ReportedMacIsStored) {
|
||||
const uint8_t reported[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
uint8_t mac[MAC_ADDRESS_SIZE] = {};
|
||||
EXPECT_TRUE(store_mac(reported, mac));
|
||||
EXPECT_THAT(mac, ::testing::ElementsAreArray(reported));
|
||||
}
|
||||
|
||||
TEST(LD24xxStoreMac, NoMacClearsStoredMac) {
|
||||
uint8_t mac[MAC_ADDRESS_SIZE] = {0x81, 0x2C, 0x62, 0xBE, 0xC9, 0x96};
|
||||
EXPECT_FALSE(store_mac(NO_MAC, mac));
|
||||
EXPECT_THAT(mac, ::testing::Each(0));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld24xx::testing
|
||||
@@ -182,29 +182,6 @@ climate:
|
||||
name: LG Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
datetime:
|
||||
- platform: template
|
||||
name: Date
|
||||
id: test_date
|
||||
type: date
|
||||
initial_value: "2000-1-2"
|
||||
set_action:
|
||||
- logger.log: "set_value"
|
||||
- platform: template
|
||||
name: Time
|
||||
id: test_time
|
||||
type: time
|
||||
initial_value: "12:34:56am"
|
||||
set_action:
|
||||
- logger.log: "set_value"
|
||||
- platform: template
|
||||
name: DateTime
|
||||
id: test_datetime
|
||||
type: datetime
|
||||
initial_value: "2000-1-2 12:34:56"
|
||||
set_action:
|
||||
- logger.log: "set_value"
|
||||
|
||||
prometheus:
|
||||
include_internal: true
|
||||
relabel:
|
||||
@@ -214,12 +191,3 @@ prometheus:
|
||||
template_text_sensor1:
|
||||
id: hello_text
|
||||
name: Text Substitution
|
||||
test_date:
|
||||
id: prometheus_date
|
||||
name: Prometheus Date
|
||||
test_time:
|
||||
id: prometheus_time
|
||||
name: Prometheus Time
|
||||
test_datetime:
|
||||
id: prometheus_datetime
|
||||
name: Prometheus DateTime
|
||||
@@ -3,19 +3,21 @@
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// A decoder whose JPEGDEC state is missing, as when its allocation fails. The state moves to state_, which frees it.
|
||||
// A decoder whose JPEGDEC state is missing, as when its allocation fails. The state is handed back before the base
|
||||
// destructor runs, so the teardown stays with JpegDecoder.
|
||||
class StatelessJpegDecoder : public JpegDecoder {
|
||||
public:
|
||||
explicit StatelessJpegDecoder(RuntimeImage *image) : JpegDecoder(image), state_(std::move(this->jpeg_)) {}
|
||||
explicit StatelessJpegDecoder(RuntimeImage *image) : JpegDecoder(image), state_(this->jpeg_) {
|
||||
this->jpeg_ = nullptr;
|
||||
}
|
||||
~StatelessJpegDecoder() override { this->jpeg_ = this->state_; }
|
||||
|
||||
protected:
|
||||
RAMUniquePtr<JPEGDEC> state_;
|
||||
JPEGDEC *state_;
|
||||
};
|
||||
|
||||
// The ~18 KB of JPEGDEC state lives in its own allocation, so it can come from PSRAM
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2450-target-xy
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2450's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 256000
|
||||
auto_start: false
|
||||
responses:
|
||||
# Generic ACK for any command, to unblock setup commands
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# One frame every 100 ms, as the radar sends them. Coordinates are sign-magnitude, with the high bit set for
|
||||
# positive values.
|
||||
injections:
|
||||
# Targets 1 and 2 move, one step per frame. Target 3 moves for 3 frames, then leaves while its
|
||||
# later positions are still held back by the throttle
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x58, 0x02, 0xE8, 0x83, 0x00, 0x00, 0x64, 0x00,
|
||||
0xC8, 0x80, 0xD0, 0x87, 0x00, 0x00, 0x64, 0x00,
|
||||
0x2C, 0x01, 0xB8, 0x8B, 0x00, 0x00, 0x64, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xF4, 0x01, 0x1A, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x18, 0x81, 0xEE, 0x87, 0x00, 0x00, 0x64, 0x00,
|
||||
0xFA, 0x00, 0xEA, 0x8B, 0x00, 0x00, 0x64, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x90, 0x01, 0x4C, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x68, 0x81, 0x0C, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0xC8, 0x00, 0x1C, 0x8C, 0x00, 0x00, 0x64, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x2C, 0x01, 0x7E, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0xB8, 0x81, 0x2A, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xC8, 0x00, 0xB0, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x08, 0x82, 0x48, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x64, 0x00, 0xE2, 0x84, 0x00, 0x00, 0x64, 0x00,
|
||||
0x58, 0x82, 0x66, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0x14, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0xA8, 0x82, 0x84, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x64, 0x80, 0x46, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0xF8, 0x82, 0xA2, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xC8, 0x80, 0x78, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0x48, 0x83, 0xC0, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x2C, 0x81, 0xAA, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0x98, 0x83, 0xDE, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x90, 0x81, 0xDC, 0x85, 0x00, 0x00, 0x64, 0x00,
|
||||
0xE8, 0x83, 0xFC, 0x88, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0xF4, 0x81, 0x0E, 0x86, 0x00, 0x00, 0x64, 0x00,
|
||||
0x38, 0x84, 0x1A, 0x89, 0x00, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
# Targets 1 and 2 stand still for 1.2 s (repeated frames are ignored), then leave
|
||||
- delay: 1300ms
|
||||
inject_rx:
|
||||
[
|
||||
0xAA, 0xFF, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x55, 0xCC,
|
||||
]
|
||||
|
||||
ld2450:
|
||||
id: ld2450_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
text_sensor:
|
||||
- platform: ld2450
|
||||
ld2450_id: ld2450_dev
|
||||
target_1:
|
||||
xy:
|
||||
name: "Target 1 XY"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 500ms
|
||||
value: last
|
||||
- throttle_with_priority:
|
||||
timeout: 500ms
|
||||
value: NA
|
||||
target_2:
|
||||
xy:
|
||||
name: "Target 2 XY"
|
||||
filters: []
|
||||
target_3:
|
||||
xy:
|
||||
name: "Target 3 XY"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 500ms
|
||||
value: last
|
||||
- throttle_with_priority:
|
||||
timeout: 500ms
|
||||
value: NA
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
@@ -1,87 +0,0 @@
|
||||
"""Integration test for the LD2450 target xy text sensor, with and without a throttle."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from itertools import groupby
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, TextSensorInfo, TextSensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import require_entity, subscribe_after_initial_states
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# The positions of each target in the fixture, one per radar frame
|
||||
TARGET_1 = [f"{-600 + 100 * i},{1000 + 50 * i}" for i in range(12)]
|
||||
TARGET_2 = [f"{200 + 80 * i},{2000 + 30 * i}" for i in range(12)]
|
||||
# Target 3 moves for 3 frames (300 ms) and leaves, with a 500 ms throttle
|
||||
TARGET_3 = [f"{-300 + 50 * i},{3000 + 50 * i}" for i in range(3)]
|
||||
# Frames are 100 ms apart: "NA" must come well before the 500 ms throttle ends
|
||||
TARGET_3_LEAVES_WITHIN_S = 0.45
|
||||
|
||||
|
||||
def _distinct(states: list[str]) -> list[str]:
|
||||
"""Drop the repeat that the timeout filter sends after a pause."""
|
||||
return [state for state, _ in groupby(states)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2450_target_xy(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test the target xy text sensor with and without a throttle."""
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
target_1 = require_entity(entities, "target_1_xy", TextSensorInfo)
|
||||
target_2 = require_entity(entities, "target_2_xy", TextSensorInfo)
|
||||
target_3 = require_entity(entities, "target_3_xy", TextSensorInfo)
|
||||
history: dict[int, list[str]] = {
|
||||
target.key: [] for target in (target_1, target_2, target_3)
|
||||
}
|
||||
target_3_times: list[float] = []
|
||||
loop = asyncio.get_running_loop()
|
||||
both_left = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, TextSensorState) or state.key not in history:
|
||||
return
|
||||
history[state.key].append(state.state)
|
||||
if state.key == target_3.key:
|
||||
target_3_times.append(loop.time())
|
||||
# Target 3 left long before, so only targets 1 and 2 end the scenario
|
||||
left = all(
|
||||
history[key] and history[key][-1] == "NA"
|
||||
for key in (target_1.key, target_2.key)
|
||||
)
|
||||
if left and not both_left.done():
|
||||
both_left.set_result(None)
|
||||
|
||||
await subscribe_after_initial_states(client, entities, on_state=on_state)
|
||||
|
||||
start_btn = require_entity(entities, "start_scenario", ButtonInfo)
|
||||
client.button_command(start_btn.key)
|
||||
await asyncio.wait_for(both_left, timeout=10.0)
|
||||
|
||||
# Throttled: the first position at once, then a few, in order, and the final one before the target leaves
|
||||
throttled = _distinct(history[target_1.key])
|
||||
assert throttled[-1] == "NA"
|
||||
positions = throttled[:-1]
|
||||
assert positions[0] == TARGET_1[0]
|
||||
assert positions[-1] == TARGET_1[-1], (
|
||||
"the final position was held back for good"
|
||||
)
|
||||
indexes = [TARGET_1.index(position) for position in positions]
|
||||
assert indexes == sorted(set(indexes)), f"out of order or repeated: {positions}"
|
||||
assert 3 <= len(positions) <= 6, f"expected a few updates, got {positions}"
|
||||
|
||||
# Not throttled: every change is published
|
||||
assert history[target_2.key] == [*TARGET_2, "NA"]
|
||||
|
||||
# "NA" bypasses the throttle and drops the held position: nothing follows it
|
||||
assert _distinct(history[target_3.key]) == [TARGET_3[0], "NA"]
|
||||
assert target_3_times[1] - target_3_times[0] < TARGET_3_LEAVES_WITHIN_S
|
||||
@@ -117,7 +117,6 @@ from esphome.const import (
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_HOST,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2,
|
||||
ErrorFormat,
|
||||
@@ -6304,7 +6303,7 @@ def test_run_miniterm_no_stacktrace_analyzer(
|
||||
"""Platforms without a stacktrace analyzer log an info and stream anyway."""
|
||||
mock_serial = MockSerial([b"[I][app:100]: Line 1\r\n", MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_LN882X}
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_HOST}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
|
||||
from esphome import platform_hooks
|
||||
from esphome.const import PLATFORM_ESP32, PLATFORM_LN882X, Platform
|
||||
from esphome.const import PLATFORM_ESP32, PLATFORM_HOST, Platform
|
||||
|
||||
|
||||
def test_no_unregistered_platform_exposes_a_hook() -> None:
|
||||
@@ -165,7 +165,7 @@ def test_get_stacktrace_handler_reports_missing_analyzer(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
caplog.set_level("INFO", logger="esphome.platform_hooks")
|
||||
assert platform_hooks.get_stacktrace_handler(PLATFORM_LN882X) is None
|
||||
assert platform_hooks.get_stacktrace_handler(PLATFORM_HOST) is None
|
||||
assert "no compatible analyzer" in caplog.text
|
||||
# A capability gap is ordinary; it must not warn.
|
||||
assert not any(r.levelno >= logging.WARNING for r in caplog.records)
|
||||
|
||||
@@ -18,6 +18,7 @@ from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_HOST,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2,
|
||||
@@ -71,6 +72,10 @@ CRASH_SAMPLES: dict[str, dict[str, list[str]]] = {
|
||||
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
|
||||
"addresses": ["PC: 0x9B0073A4 (fault location)"],
|
||||
},
|
||||
PLATFORM_LN882X: {
|
||||
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
|
||||
"addresses": ["PC: 0x1001A2C4 (fault location)"],
|
||||
},
|
||||
PLATFORM_NRF52: {
|
||||
"state_markers": ["Last crash:"],
|
||||
"addresses": [
|
||||
@@ -237,6 +242,7 @@ DECODER_PATTERNS: dict[str, list[str]] = {
|
||||
PLATFORM_RP2: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
|
||||
PLATFORM_BK72XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
|
||||
PLATFORM_RTL87XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
|
||||
PLATFORM_LN882X: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
|
||||
PLATFORM_NRF52: ["STACKTRACE_NRF52_PC_LR_RE"],
|
||||
}
|
||||
|
||||
@@ -554,7 +560,7 @@ def test_processor_registry_miss_disables_at_construction(
|
||||
caplog.set_level("INFO", logger="esphome.platform_hooks")
|
||||
|
||||
with patch.object(stacktrace.platform_hooks, "import_module") as mock_import:
|
||||
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_LN882X)
|
||||
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_HOST)
|
||||
processor.process_line("PC: 0x40104960")
|
||||
|
||||
mock_import.assert_not_called()
|
||||
|
||||
Reference in new issue
Block a user