mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 09:36:04 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b5f99a60e | ||
|
|
e7c066b923 | ||
|
|
1add061d94 | ||
|
|
6ba3c6b03b | ||
|
|
b55460af99 | ||
|
|
f25d66d7df | ||
|
|
ad6b83dce4 | ||
|
|
62d9f9ef26 | ||
|
|
b3e75be525 | ||
|
|
d9797c2efb | ||
|
|
805392e9e2 | ||
|
|
9c350d0bc1 | ||
|
|
5f0e2db058 | ||
|
|
bd75639765 | ||
|
|
a4f844335d | ||
|
|
12117419f6 | ||
|
|
b9c5899ad4 | ||
|
|
6cdf238568 | ||
|
|
1424c9b74b | ||
|
|
62828cb550 | ||
|
|
6cc96fab0d | ||
|
|
ee60953e67 | ||
|
|
ac0fed53a4 | ||
|
|
a769d6309b | ||
|
|
a330714cf6 | ||
|
|
428883ed80 | ||
|
|
62cef67191 | ||
|
|
ba423ff170 | ||
|
|
2610a6a03a | ||
|
|
380660dcdb | ||
|
|
be0550b1e8 | ||
|
|
935befcbe5 | ||
|
|
3325b16c56 | ||
|
|
d9cac9a88f | ||
|
|
86442c8816 |
No files matched your search
@@ -104,6 +104,8 @@ helper; `AGENTS.md` section 4 and the `component-codegen` skill have the detail.
|
||||
| --- | --- |
|
||||
| `if (this->x_sensor_ != nullptr) this->x_sensor_->publish_state(v);` | `PUBLISH_STATE(this->x_sensor_, v);` (`esphome/core/entity_base.h`) |
|
||||
| set warning / `return` / clear warning around a bus result | `if (!this->status_comm_result(ok)) return;` |
|
||||
| set warning / `return` around an earlier step of a multi-step poll | `if (!this->status_comm_check(ok)) return;` |
|
||||
| warning set, but no `status_comm_result` or `status_clear_warning` on the success path | the last step uses `status_comm_result(ok)`, or the warning never clears |
|
||||
| hand-written `set_x_sensor()` plus `x_sensor_` member | `SUB_SENSOR(x)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`, `SUB_SWITCH`, ... |
|
||||
| `if CONF_X in config: s = await sensor.new_sensor(config[CONF_X])` plus setter | `sensors = sensor.sub_sensors(config)` then `await sensors(CONF_X, hub.set_x_sensor)` |
|
||||
| hand-written `{"a": MyEnum.MY_ENUM_A, ...}` dict | `MyEnum.options("a", "b", prefix="MY_ENUM_")` |
|
||||
|
||||
@@ -236,6 +236,8 @@ Never a template per bit or a virtual listener base (a second vptr per entity).
|
||||
instead, so pass `buf` once and mark the component failed in `setup()` if it is nullptr. `align=4` emits
|
||||
`alignas(4)`, so a word-aligned byte buffer stays `uint8`. `size` may be an expression such as `MyClass.static_("buffer_size")(n)`, which renders
|
||||
`ns::MyClass::buffer_size(n)`.
|
||||
- `cg.static_storage(config[CONF_ID], T, count)` is raw aligned storage for `count` objects as a `std::span<T>`,
|
||||
for `StaticFixedVector::init()` when codegen knows the count.
|
||||
- `display.add_writer(var, config)` sets the lambda.
|
||||
- `MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict.
|
||||
- `button.restart_button_schema(cls)` (and `factory_reset_`, `query_`) are button presets.
|
||||
|
||||
@@ -88,6 +88,9 @@ Components that define buses directly are flagged as "NEEDS MIGRATION" and canno
|
||||
CI build time. `!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a
|
||||
standalone entity with its own id instead.
|
||||
|
||||
In a grouped test, set `i2c_id: i2c_bus`, `one_wire_id` and `motion_id` even with one candidate: a merged
|
||||
batch can add a second one ("Too many candidates"). `script/ci-custom.py` checks this.
|
||||
|
||||
A test that needs a shared component in a particular state (`esp32_ble` security settings, an option that
|
||||
PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
|
||||
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
|
||||
|
||||
@@ -107,6 +107,8 @@ APIs.
|
||||
This eliminates `_M_realloc_insert` and `_M_default_append` template instantiations (200-500 bytes per
|
||||
instance), needs no upper bound, and is compatible with protobuf code generation when using
|
||||
`[(fixed_vector) = true]`.
|
||||
When codegen knows the count, use `StaticFixedVector<T>` and pass `cg.static_storage(config[CONF_ID], T, count)`
|
||||
to its `init()`, so the elements live in BSS instead of the heap.
|
||||
|
||||
4. **Small datasets (1-16 elements):** use `std::vector` or `std::array` with simple structs instead of
|
||||
`std::map`/`std::set`/`std::unordered_map`.
|
||||
|
||||
@@ -130,7 +130,7 @@ Per-topic scripts, each described in the matching `reference/recipes/<component>
|
||||
(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/` (`ha_discovery/`, `disable/`, `retained_burst/` and `reconnect/` runners; broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board, MAC suffix runner with per-variant configs for two boards, `shutdown/` shutdown message runner and matcher), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer), `uart_sim/` (API driver and flash, capture and drive runner for the simulated UART sensors), `remote_base/` (loopback runner and API log decoder per sent frame), `xiaomi_ble/` (frame and header generator with the expected counts, injecting, advertising and receiving configs, counter and runner), `ble_terminate/` (proxy and peer configs, connect and disconnect driver, peer disconnect matcher, runner), `i2c_slave/` (D1 mini slave and BK7238 master configs), `wpa3_ap/` (PMF AP with join and leave logging, BK7238 and CB3S DUTs, runner and summary), `wisp_ap/` (WISP upstream with AP buttons, WISP and ALWAYS_ON DUTs, client, runner and per phase summary), `wifi_restart/` (WPA3 mixed WISP upstream, static IP DUTs with syslog, runner and summary), `ota_timing/` (OTA upload timer with deflate, raw and TCP modes, API build check, A/B runner and median summary, `inflate_bench/` on-device inflate benchmark with stream generator, configs and syslog runner), `gpio_hold/` (ISR pin hold self test with ESP32 and S3 configs), `ble_event_inject/` (GAP and GATTC event injector header and proxy package), `light_sleep_wake/` (LEDC wave and edge timer for light sleep wakeup on a C3, external wave generator and per-call edge detail probe for peripheral power-down on the P4), `manual_ip/` (station and AP manual IP configs, DNS probe and runners), `web_handlers/` (late handler, web_server, captive_portal and prometheus variants, web OTA runner), `bk72xx_flash/` (flash readback with retries, per partition diff of two dumps), `ble_server_security/` (secure GATT server and pairing client configs, driver, summary and runner), `prometheus/` (datetime metrics configs and driver), `zephyr_ble_server/` (nRF52 server, looping ESP32 client and UF2 flash and count runner), `ble_adv_triggers/` (advertiser with six raw frames, ESP32 and BK72xx receivers with eleven triggers, runner and checker), `sun/` (longitude sweep package, runner and checker), `sensor_filters/` (every filter shape with a fixed input sequence, per-board packages and runner), `led_driver_sniff/` (SPI slave capture header with selftest, config generator, window comparer and runner), `hub_input_sim/` (keypad, wiegand and rdm6300 simulation on a D1 mini, API log runner), `value_range/` (on_value_range sequence with a restart, runner), `ezo_fake_bus/` (fake EZO I2C bus and step config, runner), `twai_self/` (TWAI self-reception header, on_frame config and counter runner), `ota_automations/` (OTA automation package for both OTA platforms, four case runner, repeated web upload runner and per case summary), `output_button/` (pad edge timer header, button package and runner), `homeassistant_button/` (raw plaintext API client for action calls and wizard inputs, package and runner), `uart_button/` (UART0 loopback package, API driver with TX and RX counts, runner), `interval/` (plain and delayed interval package, timing summary and runner), `smp_udp_ota/` (SMP reset fault injector, OTA runner, per mode result check, MCUboot swap limit, running image check and slot blanking UF2, Thread device config), `ld2420_sim/` (fake LD2420 answering the hub's commands on a UART0 loopback, API state recorder, two-build comparer and runner), `nrf52_tcp_probe/` (TCP and UDP socket probe config, build and log check, API check over Thread), `rs485_frame/` (simulated DLE/STX/ETX bus device, frame and gate cases, runner), `hx711/` (fake HXLine truncated
|
||||
capture decoder, RTL8720C ROM dump versus report matcher), `api_outgoing/` (wake cycle and Wi-Fi drop timing readers), `ld2450/` (zone number driver, loopback harness driver with number and binary sensor states and TX frame summarizer, frame stream harness with presence hold and link loss drivers, BT MAC driver), `api/` (raw plaintext native API client for user defined actions, store_yaml recovery client over Noise or plaintext), `http_request/` (update manifest server, update state driver, request test server, action case driver, concurrency, reboot, update check and OTA drivers, `ota_auth/` OTA request cases with basic auth), `modem/` (button and sensor driver over the plaintext API, also used for the BLE MIDI boards), `dfrobot_sen0395/` (API driver and case runner), `homeassistant_light/` (direct bulb driver, held-report case driver), `usb_audio/` (URL player), `speaker_media_player/` (API player driver, pipeline task case runner), `web_server/` (SSE client, request mix, SSE and HTTPD case runners), `mqtt/` (`ha_discovery/`, `disable/`, `retained_burst/` and `reconnect/` runners; broker config with TLS, client-certificate and WebSocket listeners, test CA with server and client certificates, transport case runner, flash-and-drive runner, CLI TLS check without a board, MAC suffix runner with per-variant configs for two boards, `shutdown/` shutdown message runner and matcher), `udp/` (datagram recorder, `udp.write` payload checker and case driver), `entity_icons/` (icon and device class readback over the API, MQTT discovery, web_server and dump_config, with a comparer), `uart_sim/` (API driver and flash, capture and drive runner for the simulated UART sensors), `remote_base/` (loopback runner and API log decoder per sent frame), `xiaomi_ble/` (frame and header generator with the expected counts, injecting, advertising and receiving configs, counter and runner), `ble_terminate/` (proxy and peer configs, connect and disconnect driver, peer disconnect matcher, runner), `i2c_slave/` (D1 mini slave and BK7238 master configs), `wpa3_ap/` (PMF AP with join and leave logging, BK7238 and CB3S DUTs, runner and summary), `wisp_ap/` (WISP upstream with AP buttons, WISP and ALWAYS_ON DUTs, client, runner and per phase summary), `wifi_restart/` (WPA3 mixed WISP upstream, static IP DUTs with syslog, runner and summary), `ota_timing/` (OTA upload timer with deflate, raw and TCP modes, API build check, A/B runner and median summary, `inflate_bench/` on-device inflate benchmark with stream generator, configs and syslog runner), `gpio_hold/` (ISR pin hold self test with ESP32 and S3 configs), `ble_event_inject/` (GAP and GATTC event injector header and proxy package), `light_sleep_wake/` (LEDC wave and edge timer for light sleep wakeup on a C3, external wave generator and per-call edge detail probe for peripheral power-down on the P4), `manual_ip/` (station and AP manual IP configs, DNS probe and runners), `web_handlers/` (late handler, web_server, captive_portal and prometheus variants, web OTA runner), `bk72xx_flash/` (flash readback with retries, per partition diff of two dumps), `ble_server_security/` (secure GATT server and pairing client configs, driver, summary and runner), `prometheus/` (datetime metrics configs and driver), `zephyr_ble_server/` (nRF52 server, looping ESP32 client and UF2 flash and count runner), `ble_adv_triggers/` (advertiser with six raw frames, ESP32 and BK72xx receivers with eleven triggers, runner and checker), `sun/` (longitude sweep package, runner and checker), `sensor_filters/` (every filter shape with a fixed input sequence, per-board packages and runner), `led_driver_sniff/` (SPI slave capture header with selftest, config generator, window comparer and runner), `hub_input_sim/` (keypad, wiegand and rdm6300 simulation on a D1 mini, API log runner), `value_range/` (on_value_range sequence with a restart, runner), `ezo_fake_bus/` (fake EZO I2C bus and step config, runner), `twai_self/` (TWAI self-reception header, on_frame config and counter runner), `ota_automations/` (OTA automation package for both OTA platforms, four case runner, repeated web upload runner and per case summary), `output_button/` (pad edge timer header, button package and runner), `homeassistant_button/` (raw plaintext API client for action calls and wizard inputs, package and runner), `uart_button/` (UART0 loopback package, API driver with TX and RX counts, runner), `interval/` (plain and delayed interval package, timing summary and runner), `smp_udp_ota/` (SMP reset fault injector, OTA runner, per mode result check, MCUboot swap limit, running image check and slot blanking UF2, Thread device config), `ld2420_sim/` (fake LD2420 answering the hub's commands on a UART0 loopback, API state recorder, two-build comparer and runner), `nrf52_tcp_probe/` (TCP and UDP socket probe config, build and log check, API check over Thread), `rs485_frame/` (simulated DLE/STX/ETX bus device, frame and gate cases, runner), `hx711/` (fake HXLine truncated
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
@@ -169,6 +169,9 @@ comment quotes the exact commits. Fetch `origin dev` before computing a merge ba
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
- A harness header fails to compile on Arduino or LibreTiny: those frameworks define `millis()` and `micros()` as
|
||||
macros, so call `(esphome::millis)()`, which builds everywhere. Realtek SDK headers also define `OFF`, `ON`
|
||||
and `SUCCESS`.
|
||||
- A capture stopped early or a line is missing: check the timestamps before trusting it.
|
||||
- A script that pipes into `tee` never ends although its work is done: a background command written as
|
||||
`(cd dir && server & echo $!)` backgrounds the whole `cd && server` list, so a shell stays behind holding the
|
||||
|
||||
@@ -24,6 +24,8 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
row in the PR.
|
||||
- AsyncMqttClient drops inbound topics over 128 characters; topics of 255 or more wrap a counter and
|
||||
stall the link until the broker drops TCP.
|
||||
- `Arduino.h` already declares `_heap_start`; a lambda can use `&_heap_start` directly, and a second `extern` fails.
|
||||
- `GPI` bits 16 to 31 are strapping bits (mask with 0xFFFF). GPIO16 has no interrupt; read `GP16O` for an output.
|
||||
|
||||
## ESP32 classic devkit
|
||||
|
||||
@@ -40,7 +42,8 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
- Signed OTA testing: build with `esp32.framework.advanced.signed_ota_verification.verification_keys`
|
||||
under a different name, sign with `python -m espsecure sign_data --version 2 --keyfile
|
||||
tests/components/esp32/dummy_signing_key.pem`, upload with `esphome upload --file`. The verifier
|
||||
refuses unsigned images afterwards, so restore with a signed plain build.
|
||||
refuses unsigned images afterwards, so restore with a signed plain build. The full accept and reject case set
|
||||
(several keys, CRC-only edits) is in `recipes/ota.md`.
|
||||
- An ESP32 whose bootloader has no app rollback is protected only by `safe_mode` after a bad OTA. Before sending a
|
||||
change to such a device that nobody can reach by hand, boot the same code on a devkit first and keep a fallback
|
||||
image of the merge base built.
|
||||
@@ -52,6 +55,7 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
`Disabling RNG early entropy source`, then 0 bytes, esptool reports `No serial data received`). Only a
|
||||
USB replug fixes it. Fallback: `esphome logs --device <name>.local` (API log stream, which drops lines
|
||||
while Wi-Fi is off).
|
||||
- The USB console drops a line now and then even when it stays up; do not count events by log lines alone.
|
||||
- The ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry). The
|
||||
Waveshare ESP32-P4-WIFI6 rev1.3 board is such pre-rev3 silicon.
|
||||
- S3, C3 and C6 all enumerate as `303a:1001`; confirm the chip with `esptool --port <port> chip_id` before flashing.
|
||||
@@ -147,6 +151,8 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
assuming either.
|
||||
- `flash_bk72xx.py` misses the link now and then (`Timed out attempting to link with chip`); retry up to 3
|
||||
times before suspecting the board.
|
||||
- Both flash wrappers pass ltchiptool `-L` with the newest installed LibreTiny platform (`scripts/lt_platform.py`):
|
||||
its default `~/.platformio/platforms/libretiny` can be an older copy that rejects BK7238 UF2s as unsupported.
|
||||
- Crashes: `__builtin_trap()` and a call into unmapped memory both end as an undefined-instruction trap
|
||||
(`bk_misc_get_start_type()` 6, `lt_get_reboot_reason()` Crash); reads, unaligned loads and stores to
|
||||
unmapped addresses never fault on the ARM968. Without Wi-Fi the SDK trap handler spins with interrupts off
|
||||
@@ -226,6 +232,9 @@ Ports are not listed on purpose: they follow the USB hub port and change on ever
|
||||
- A 4-way handshake that never finishes after association is reported as `RTW_WRONG_PASSWORD` on AmebaZ
|
||||
even with the right password; AmebaZ2 reports a real wrong password as a 4-way handshake timeout.
|
||||
- The driver also posts a SCAN_DONE (number 0) inside every join, on AmebaZ2 too.
|
||||
- RTL8710B GPIO interrupts fire on one edge only, and LibreTiny maps CHANGE to rising. The Arduino `digitalRead` called
|
||||
from an ISR switches the pin back to GPIO and frees its interrupt (esphome#22400, #22403).
|
||||
- The FreeRTOS tick is 1/1024 s, so `millis()` on the tick ran 2.4 % fast (esphome#22404).
|
||||
|
||||
## BW16 kit (RTL8720DN, AmebaD)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ One file per component, named after it:
|
||||
- [`ld2420.md`](ld2420.md): hub setup and sensor states with a simulated module on a UART0 loopback (energy and simple mode)
|
||||
- [`ld2450.md`](ld2450.md): polygon zones, zone number round trip through the radar's readback, ESP8266 loopback harness without a radar, presence hold and link loss on a frame stream, BT MAC readback
|
||||
- [`ld2412.md`](ld2412.md): output pin level through the basic configuration command, held back writes
|
||||
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx and RTL8720C crash report on the next boot with a ROM dump matcher, BK7238 BLE bond store safety with a full flash backup and per partition diff, BK7238 OTA versus the board layout and bootloader
|
||||
- [`libretiny.md`](libretiny.md): rtc preference emulation, BK72xx and RTL8720C crash report on the next boot with a ROM dump matcher, BK7238 BLE bond store safety with a full flash backup and per partition diff, BK7238 OTA versus the board layout and bootloader, pulse counting, RTL8710B edge interrupts and millis timing
|
||||
- [`modem.md`](modem.md): PPPoS modem component boot check with no modem attached, loop blocking and disable timing
|
||||
- [`mqtt.md`](mqtt.md): broker bench, QoS, reconnect, crash log over MQTT, WebSocket transports with a local and a public broker, TLS trust sources (own CA, client certificate, certificate bundle, no verification) with the CLI check, MAC suffix topics and will messages on two boards, shutdown message delivery on restart, disable and deep sleep
|
||||
- [`nrf52.md`](nrf52.md): dongle board support, zephyr_ble_server connection slot under reordered callbacks, SMP over UDP OTA over Thread with reset faults, MCUboot swap limit and stuck slot recovery, TCP stack probe
|
||||
@@ -30,7 +30,7 @@ One file per component, named after it:
|
||||
- [`zephyr_coredump.md`](zephyr_coredump.md): crash and decode on nRF52 boards
|
||||
- [`zwave_proxy.md`](zwave_proxy.md): frame checksums on a live proxy
|
||||
- [`globals.md`](globals.md): restoring globals with storage rtc and flash across every reset kind
|
||||
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls
|
||||
- [`homeassistant.md`](homeassistant.md): light platform mirrored from a local Home Assistant, held reports versus later calls, wizard-linked entities on every platform
|
||||
- [`usb_audio.md`](usb_audio.md): USB audio speaker and microphone, the usb_host FIFO split, and Music Assistant playback through sendspin
|
||||
- [`dfrobot_sen0395.md`](dfrobot_sen0395.md): SEN0395 mmWave command flow, module quirks and overlong lines through its error reply
|
||||
- [`speaker.md`](speaker.md): media player pipeline tasks on the Connect AUX-2 with the `speaker` platform player
|
||||
@@ -70,3 +70,11 @@ One file per component, named after it:
|
||||
- [`microphone_http_stream.md`](microphone_http_stream.md): raw PCM over HTTP with a counter fake mic or a USB mic, multi-reader client
|
||||
- [`i2s_audio.md`](i2s_audio.md): I2S microphone fed by an I2S TX channel on the same S3 through three jumpers
|
||||
- [`voice_assistant.md`](voice_assistant.md): late TTS start on the Voice PE with a slow announcement URL
|
||||
- [`mcp251xfd.md`](mcp251xfd.md): MCP2518FD CAN FD over SPI in loopback, forced modes, chip reset and bus-off recovery, TX objects read back from chip RAM, idle SPI cost from a PCNT on CS
|
||||
- [`crc32.md`](crc32.md): crc32 vectors on every platform path and the signed OTA signature block CRC on an ESP32-C3
|
||||
- [`i2c_isr_fault.md`](i2c_isr_fault.md): software INA219 on the master's own pads with seeded bus faults, interrupt storm and stuck bus
|
||||
- [`bmp390.md`](bmp390.md): BMP390/BMP388 register readback, INT data ready rate, start write faults by software NACK or held SDA, and the IDF I2C hang after a held SDA
|
||||
- [`ipv6.md`](ipv6.md): IPv6 link-local and SLAAC boot and reconnect timing on ESP32 Wi-Fi, with an injected esp-netif delay that forces the race
|
||||
- [`mcp23017.md`](mcp23017.md): MCP23017 IOCON aliasing and INT line behavior (open drain, push-pull, polling) on an ESP8266 with PA to PB jumpers
|
||||
- [`pwm_capture.md`](pwm_capture.md): every LEDC PWM period captured on the same ESP32-C6 through RMT receive and PCNT, with and without power save
|
||||
- [`spi.md`](spi.md): SPI bus capture on the same board with no chips (ESP32 SPI3 slave, edge interrupt snapshots on C3, ESP8266 and Pico 2 W), checked and diffed between builds
|
||||
@@ -0,0 +1,35 @@
|
||||
# BMP390 / BMP388 (bmp3xx_i2c): register readback, data ready rate, start write faults (#21632, #21633)
|
||||
|
||||
Bench: ESP32-S3 devkit (native USB console, ESP-IDF), BMP390 breakout on 3V3, SDA GPIO1, SCL GPIO2, CS and SDO open
|
||||
(address 0x77), INT to GPIO42. Some breakouts answer chip ID 0x50 (the BMP388 value); the register map is the same.
|
||||
|
||||
`scripts/bmp390/`:
|
||||
- `gen.py NAME [--rate 50Hz] [--iir 128x] [--interval 1s] [--fault --kinds ... --probe-delay MS] [--reset-at 40,80]
|
||||
[--open-line] [--bus-reset]` writes `NAME.yaml` (no Wi-Fi).
|
||||
- `bmp390probe.hpp` wraps the sensor's I2C bus (`set_i2c_bus()` from an `on_boot` priority 800 lambda), dumps
|
||||
CHIP_ID, ERR, INT_CTRL, PWR_CTRL, OSR, ODR and CONFIG before and after setup and every 10 s (`REGS` lines), turns on
|
||||
data ready on INT after setup (`INT_CTRL = 0x42`, push-pull, active high: the driver never writes INT_CTRL and its
|
||||
soft reset in setup clears it), counts rising edges (`INT edges=N in M ms`) and logs every pressure value with four
|
||||
decimals (`READ <ms> <hPa>`).
|
||||
- `run.sh NAME SECONDS [TAG]` (env `TREE`, `PORT`, `SC`, `PY`) builds, flashes, captures from a fresh boot and runs
|
||||
`stats.py CAPTURE [SKIP_S]`: read count, cadence, INT rate, detrended noise (21 sample window), repeated values.
|
||||
|
||||
Flows:
|
||||
- Register A/B: the same config on both builds; compare `REGS` after setup and between updates. Forced mode leaves
|
||||
PWR_CTRL at 0x03 (sleep, both sensors enabled), normal mode reads 0x33. ODR codes: 0x00 200 Hz, 0x02 50 Hz, 0x07
|
||||
1.5 Hz, 0x0B 0.1 Hz (period 5 ms << code).
|
||||
- Real output rate: INT edges per 10 s window. Forced mode gives one edge per update.
|
||||
- Settling: the bench cannot make a pressure step, but the soft reset in setup restarts the IIR filter with an
|
||||
offset, so the first readings after boot show its step response (50 Hz with 128x settled in about 8 s).
|
||||
- Reset behind the driver: `--reset-at` writes CMD 0x7E = 0xB6 at those seconds; a driver that handles it logs that
|
||||
the sensor was reset and restores the registers.
|
||||
- Start write faults (`--fault --kinds`, one letter per event at 30 s and then every 20 s): `N` the next start write
|
||||
NACKs in software, `M` every start write NACKs for 5 s, `T` SDA held low through the GPIO matrix for the next start
|
||||
write, `W` the same for every transfer for 5 s, `L` a continuous 5 s hold, `S` a 300 ms hold, `H` and `D` held
|
||||
transfers of the harness only. `PROBE` lines check the bus `--probe-delay` ms later.
|
||||
|
||||
Pitfall: on the S3 with ESP-IDF 5.5.6, any transfer that fails while SDA is routed to a GPIO held low (or routed away
|
||||
with the line floating high) leaves the `i2c_master` driver hung on every build, with or without the sensor: every
|
||||
later transfer logs `I2C software timeout` while the lines read idle high and the saved pad registers are back, and
|
||||
`i2c_master_bus_reset()` does not help. Use the software NACK kinds (`N`, `M`) to test what a component does after a
|
||||
failure and the recovery; use the wire kinds only to show the first failure.
|
||||
@@ -0,0 +1,13 @@
|
||||
# crc32 helper and the signed OTA block check (esphome#22398, 2026-11-07)
|
||||
|
||||
`scripts/crc32/crctest.hpp` checks `esphome::crc32()` against Python `zlib.crc32` values: `"123456789"` = `0xCBF43926`,
|
||||
empty input returns the seed, chained calls (4 + 5, 300 one-byte calls, 97-byte chunks from an odd offset), bytes
|
||||
0..255, 32 × 0xFF, 4093 bytes from offsets 0 to 3, and 8 KiB timed with `(esphome::micros)()` (best of five).
|
||||
`common-crc.yaml` runs it at boot and every 20 s and also makes an unqualified `crc32(...)` call from a lambda, which
|
||||
catches a clash with anything the generated `main.cpp` pulls in. Paths and timings seen (8 KiB): ESP32 ROM 512 µs,
|
||||
ESP8266 PROGMEM nybble table 3072 µs, RP2350 932 µs, BW15 (FlashDB) 656 µs, BW12 2000 µs, BK7238 1026 µs.
|
||||
|
||||
The signed OTA signature block check (`block_is_valid()`), the PR's only converted caller, ran the accept and reject
|
||||
cases in `ota.md` (signed OTA with several trusted keys).
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/22398#issuecomment-6716330334 and #issuecomment-6716545831.
|
||||
@@ -13,8 +13,6 @@ replies.
|
||||
- `run.sh LABEL` builds, flashes, captures 90 s and writes `out-ezo<LABEL>.events`. Compare dev and the PR: the
|
||||
replies, callback texts, read values and the wait before each read (912 ms for reads and calibration, 304 ms for
|
||||
the rest) must match; differences in queue limits are the PR's intended change.
|
||||
- Call plain `millis()` in a fake: Arduino builds define a `millis()` macro (`esphome::millis()`), so
|
||||
`esphome::millis()` breaks there, and on ESP-IDF the plain name resolves through `using namespace esphome`.
|
||||
- Found with it: dev read past its 32-byte buffer on an unterminated reply (a custom callback got 56 bytes), and
|
||||
two queued address changes both went to the first new address.
|
||||
- Results: https://github.com/esphome/esphome/pull/21840#issuecomment-6493565259
|
||||
|
||||
@@ -45,3 +45,24 @@ without the fix logs `Applying the report that arrived during the transition` an
|
||||
ends at 100% on both builds because the last call's target differs from the held report, so it is a regression
|
||||
check only, not a reproduction. Separate button presses are too slow (about 1.5 s each) to land inside the fade.
|
||||
The mirrors are not API entities, so read their state from the "Log state" button's line. Results: https://github.com/esphome/esphome/pull/21144#issuecomment-6278257695
|
||||
|
||||
## Wizard-linked homeassistant entities on every platform (#21908)
|
||||
|
||||
`scripts/homeassistant_wizard/`: `ha-all-common.yaml` adds to the buttons of `ha-button-common.yaml` one entity with a
|
||||
fixed `entity_id` and one linked to an `api: wizard:` input for each of sensor (plus one with `attribute:`),
|
||||
binary_sensor, text_sensor, number, switch, select, text and light (the wizard light is in `ha-wiz-light.yaml`), and
|
||||
logs the ESP8266 heap every 5 s (`ramchk`). `hadrive.py HOST` plays Home Assistant over a plaintext API connection: it
|
||||
records the state subscriptions the device asks for, sends states (including number and text min, max, step and mode,
|
||||
select options and light supported_color_modes), commands every writer, sets each wizard input with
|
||||
`WizardInputSetRequest` (key = FNV-1 hash of the input id), changes it to a second ID in another domain, sends an
|
||||
invalid one and reconnects. It prints SUB, STATE and ACTION lines that diff cleanly between two builds, and PASS or
|
||||
FAIL. `run.sh LABEL` (env `TREE`, `PORT`, `SC`, `PY`; `NO_LIGHT=1` for a build without #21908) flashes a D1 mini, takes
|
||||
three idle boots of heap readings and drives one boot. `lightcheck.py HOST` checks again that the wizard light follows
|
||||
only its current ID.
|
||||
|
||||
- Builds before #21908 cannot compile a wizard-linked homeassistant light (the wizard called `set_entity_id()` on the
|
||||
LightState); `NO_LIGHT=1` leaves `ha-wiz-light.yaml` out.
|
||||
- An unset wizard number or select rejects a command in the core (`> max 0`, `has no options`) before the
|
||||
`No entity ID set yet` check; nothing is sent either way.
|
||||
- Field 3 of LightStateResponse is brightness, not `missing_state`; SwitchStateResponse has no missing flag.
|
||||
- A light publishes only changes, so a report of the state it already has publishes nothing.
|
||||
@@ -0,0 +1,20 @@
|
||||
# I2C master interrupt storm and stuck bus faults (esphome#22270 / #22396, 2026-11-06 to 11-07)
|
||||
|
||||
`scripts/i2c_isr_fault/inasim.hpp` is a software INA219 at 0x40 answered on the ESP32 master's own pads (SDA GPIO32,
|
||||
SCL GPIO33, nothing wired): the slave pulls a line low by switching the pad's output mux from the I2C0 signal to plain
|
||||
GPIO output 0 (a wired AND with the master), from a GPIO ISR on core 0 that spins through one transaction. A core 0
|
||||
task re-asserts long stuck lines every tick. From 20 s after boot it injects seeded faults into `p_fault` per mille of
|
||||
transactions (garbage bits, NACK, clock stretch, SCL/SDA held low up to `hold_max_ms`, glitches, vanish).
|
||||
|
||||
`i2cbase.yaml` (package): `i2c:` at `freq` with `scan: true`, an `ina219` polled every `ina_interval`, a bus voltage
|
||||
register read every `hammer`, a boot self test, and 30 s `MASTER`/`SLAVE` counter lines (ok, timeouts by kind,
|
||||
garbage, longest failed run, loop stalls). `example.yaml` shows the substitutions: framework and version (`fw`,
|
||||
`fwver`), `seed` (seed 1 at 5 % hangs dev with an interrupt watchdog on CPU0 at about 245 s, the #22270 report),
|
||||
`p_fault`, `fault_mask`. `run.sh LABEL [SECONDS]` builds, flashes and captures (640 s default); `summary.sh` prints
|
||||
crashes, watchdogs, timeout kinds and the minimum ok reads per window.
|
||||
|
||||
Findings: ESP-IDF 5.4.2 has a different failure (the driver waits for bus idle with no timeout after a NACK, so the task
|
||||
watchdog reboots in a loop with or without the guard); 5.5.5, 5.5.6 and 6.0.1 hang on the storm without the guard and
|
||||
run clean with it. ESPHome never uses the driver's async mode, so there is no asynchronous path to test.
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/22396#issuecomment-6713396173
|
||||
@@ -0,0 +1,28 @@
|
||||
# IPv6 link-local and SLAAC on ESP32 Wi-Fi: boot timing and race injection (#22364)
|
||||
|
||||
`scripts/ipv6_boot/` (ESP32-S3 and ESP32-C3, native USB console, ESP-IDF). One YAML per build
|
||||
(`example-s3.yaml`, `example-c3.yaml` show the shape) with the substitutions `min_count` (network
|
||||
`min_ipv6_addr_count`) and `cap_ms`.
|
||||
|
||||
- `v6probe.hpp` and `v6common.yaml`: `network: enable_ipv6`, VERBOSE logs, and a 50 ms interval that logs milestones
|
||||
from boot (`MS assoc|ipv4|linklocal|slaac|connected <ms>`; addresses come from `esp_netif_get_all_ip6`, so they
|
||||
count once they are preferred, after duplicate address detection), then `RESULT boot ...` with `heap_used`, then
|
||||
disables Wi-Fi for 3 s and logs `RESULT reconnect ...` the same way, then reboots. A phase ends when SLAAC and the
|
||||
connection are both seen, or after `cap_ms` (`timeout=1`).
|
||||
- `v6hf.yaml` adds a `HighFrequencyLoopRequester` (a busy main loop, as with a display or audio).
|
||||
- `v6inject.yaml` links with `-Wl,--wrap=esp_netif_action_connected` and defines `V6_INJECT`; the wrapper in
|
||||
`v6probe.hpp` sleeps 300 ms in the event task before esp-netif brings the station interface up, after the wifi
|
||||
component already queued STA_CONNECTED. That makes the "interface not up yet" race happen on every connect.
|
||||
- `run.sh NAME SECONDS` (env `TREE`, `PORT`, `SC`, `PY`) builds `NAME.yaml` as device `NAME`, flashes, captures the
|
||||
self-rebooting boots and runs `an.py CAPTURE...`: per phase the `esp_netif_create_ip6_linklocal failed` count,
|
||||
boots without link-local, SLAAC or a connection, connection timeouts, and median and worst times.
|
||||
|
||||
- Check the network first: a computer on the same network shows an `autoconf` IPv6 address when a router advertises
|
||||
a prefix. SLAAC arrived about 2 s after link-local on the test network.
|
||||
- The natural race did not show: no failure in about 160 boots on the S3 (normal and busy loop) and the C3 without
|
||||
the fix. esp_event runs handlers in registration order and the wifi component registers before esp-netif, but the
|
||||
event task reaches esp-netif's handler within microseconds. Only the injected delay reproduced the failure.
|
||||
- An extra event handler registered from `on_boot` cannot inject the race: it runs after esp-netif's handler, when
|
||||
the interface is already up. Wrap the esp-netif call instead.
|
||||
- `wifi.disable` removes the addresses, so the reconnect phase creates link-local again on the same path.
|
||||
- Count failures by the probe's `ll=0` too, not only by the warning: the USB console drops lines.
|
||||
@@ -160,3 +160,21 @@ auth frames.
|
||||
Known LibreTiny behaviour seen here: after a join with a BSSID and channel, LibreTiny's partial-scan reset leaves
|
||||
the next SSID-only join with nothing to scan (`RTW_NONE_NETWORK` in about 18 ms). That is a firmware bug, not
|
||||
the board; passing channel 0 avoids it.
|
||||
|
||||
## Pulse counting and edge interrupts on LibreTiny (esphome#22391, #22400, #22403, #22404, 2026-11-06 to 11-07)
|
||||
|
||||
`scripts/libretiny_edges/`:
|
||||
- `pulse-bk.yaml` + `pulsegen.hpp` (BK7238, generator pins `GEN_PIN_A`/`GEN_PIN_B` jumpered to the counted pins) and
|
||||
`pulse-rtl.yaml` + the same header (BW15, mbed `pwmout` on `GEN_PIN_NAME`): one pulse shape per 35 s stage, from
|
||||
BL0937-like 38 µs pulses at 1 kHz up to 50 ms, counted by `pulse_counter` with `inverted` and filters as
|
||||
substitutions; `meas` lines give count per stage. Found: Beken counted half of BL0937 pulses (readings halve, so a
|
||||
calibration note was needed), RTL8720C counted none, RTL8710B none with inverted pins, LN882H counted correctly.
|
||||
- `bw12-base.yaml` with two pin layouts (`common-bw12.yaml`, `common-bw12-b.yaml`) + `bwdbg.hpp` (BW12, RTL8710B): edges
|
||||
without wiring by toggling the pad pull on floating pins (`PAD_PullCtrl`), `-Wl,--wrap=digitalRead` to count ISR
|
||||
versus task reads, and a pin/IRQ register dump; `summ.py` summarizes. A jumper IO12 to IO13 on the jig gave a real
|
||||
driven edge.
|
||||
- `common-bw12-time.yaml`: `millis()`/`micros()` every 5 s against host time, a DWT cycle bench per call (also under
|
||||
`InterruptLock`), and a mask ROM dump of `SYSTIMER_TickGet`.
|
||||
Results: #22400 https://github.com/esphome/esphome/pull/22400#issuecomment-6712520146, #22403
|
||||
https://github.com/esphome/esphome/pull/22403#issuecomment-6712901242, #22404
|
||||
https://github.com/esphome/esphome/pull/22404#issuecomment-6713095035.
|
||||
@@ -0,0 +1,17 @@
|
||||
# MCP23017 IOCON and interrupt line (#22232)
|
||||
|
||||
Bench: an ESP8266 (Feather HUZZAH) with an MCP23017 breakout at 0x27, SDA GPIO4, SCL GPIO5, INTA to GPIO14, INTB to
|
||||
GPIO13, PA0 jumpered to PB0 and PA1 to PB1, no extra pull-ups (the breakout's were enough at 100 kHz). The esptool
|
||||
reset keeps the expander powered, so a reflash shows whether register bits from the previous build survive.
|
||||
|
||||
`scripts/mcp23017/`: the `mcptest` external component (`ext/mcptest`) reads IOCON at 0x0A and 0x0B 6 s after boot,
|
||||
toggles PA0 and PA1 20 times, samples the INT levels right after and 200 ms after each write, counts INTB edges in
|
||||
an interrupt, checks the PB binary sensors (`SUMMARY` line), then checks whether IOCONB aliases IOCONA
|
||||
(`IOCONB_TEST`). `int.yaml` wires INTA to the hub's `interrupt_pin`, open drain or push-pull by the `open_drain`
|
||||
substitution (variants `int-od` and `int-pp`); `no-int.yaml` has no interrupt pin and counts INTA edges in
|
||||
`mcptest`. `run.sh VARIANT LABEL` (env `TREE`, `PORT`, `SC`, `PY`) builds, flashes and captures 14 s.
|
||||
|
||||
- In BANK=0, 0x0A and 0x0B are the same register: a write to one reads back from the other.
|
||||
- A push-pull INT line works without a pull-up. Open drain with no pull-up stays low after the first interrupt
|
||||
(ESP8266 INPUT), so the hub loop never stops and the inputs still update by polling.
|
||||
- Builds before #22232 leave ODR and MIRROR set across reflashes.
|
||||
@@ -0,0 +1,33 @@
|
||||
# MCP2518FD CAN FD over SPI (esphome#21951, 2026-11-07)
|
||||
|
||||
Bench: ESP32-PICO-D4 (`board: pico32`, ESP-IDF) wired to an MCP2518FD module with an ATA6563 transceiver: SCK GPIO25,
|
||||
SDI GPIO26, SDO GPIO27, nCS GPIO32, INT GPIO33, module 3V3/GND, CLK pin to GPIO34. No second CAN node, so everything
|
||||
runs in `LOOPBACK`, or with the controller forced into internal loopback behind the driver.
|
||||
|
||||
- **Read the crystal.** The module's can is marked `H20.00` (20 MHz) even though listings say "8 Mbps"; the PR default
|
||||
is `40MHZ`. Loopback never leaves the chip, so a wrong `clock:` still passes at half the configured rates. Measure it:
|
||||
`pulse_counter` on the CLK pin with `internal_filter: 0us` reads SYSCLK/10 (2 MHz) at reset. Older heads wrote 0 to
|
||||
`OSC` byte 0, which set `CLKODIV` to /1 (CLKO = 20 MHz, still countable by PCNT).
|
||||
- `scripts/mcp251xfd/canlb.hpp`: stream checker (one frame in flight, burst of 8 every 64th; standard/extended IDs, FD
|
||||
with and without BRS, every 4th classic, DLC 0 to 15, sequence in the payload; starts 5 s after boot so the
|
||||
`dump_config` stall cannot trip the 50 ms in-flight timeout), a queue checker for bursts, RTR and TX objects, raw SPI
|
||||
register access on the driver's own `SPIDevice` (`rd32`, `wr8`, `chip_reset` = `0x00 0x00`), forced modes
|
||||
(`force_mode` writes `CON+3`; `force_via_config` goes through Configuration so the chip accepts it), `watch_start`
|
||||
(polls `CON` every 2 ms until `OPMOD` and `REQOP` equal the target, logs the time), `txcheck` (reads each TX object
|
||||
back from chip RAM after a frame full of 0xAA, padding included), `burst` (sends N frames in one call so the loop
|
||||
cannot drain RX: FD depth 20, CLASSIC depth 32 + one overrun) and a PCNT unit on the CS pin, which counts SPI
|
||||
transactions while the pin stays an output (`cs_mark`/`cs_report`: idle cost per second).
|
||||
- `common-can.yaml` is the package; each config adds its `canbus` block and a `phases` script: `can-loopback.yaml`
|
||||
(stream, RTR, TX objects, idle cost, chip reset, two refused `REQOP` writes), `can-classic.yaml` (CLASSIC with a
|
||||
forced internal-loopback window: force once to trigger the driver's recovery, force again right after it restores,
|
||||
then burst 48 and RTR inside the next 5 s check interval), `can-normal-busoff.yaml` (no node acknowledges, so the
|
||||
controller cycles error passive/bus-off; each forced fault and reset waits for TREC `TXBO`).
|
||||
`run.sh CONFIG LABEL [SECONDS]` builds, flashes, captures and prints the summary lines.
|
||||
- Gotchas found: a refused `REQOP` (LISTENONLY or Restricted from LOOPBACK) leaves `OPMOD` unchanged and stalls TX;
|
||||
the chip refuses LOOPBACK to CLASSIC directly (go through Configuration); after a chip reset TREC reads `0x00200000`
|
||||
(`TXBO` set in Configuration mode), so a driver that skips recovery on bus-off never recovers a reset during an error
|
||||
state. Neither a reset nor a refused request sets an interrupt flag, so detection waits for the periodic check.
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/21951#issuecomment-6715452893 (first run),
|
||||
#issuecomment-6716105991 (TX objects, idle cost), #issuecomment-6716741859 (reset during bus-off bug),
|
||||
#issuecomment-6716957543 (fixed).
|
||||
@@ -47,7 +47,6 @@ each ring size at least the encoder window, three times, and logs the best time
|
||||
`BENCHDONE`. `bench.sh CONFIG DEVICE` builds and uploads `bench-esp32s3.yaml` or `bench-bk72xx.yaml`, collects one pass
|
||||
over syslog and fails when a case did not reach the end of its stream.
|
||||
|
||||
- LibreTiny defines `micros` as a macro, so the header calls `(esphome::micros)()`.
|
||||
- Keep the streams near 130 KB on a BK7231N: an image with about 330 KB of extra incompressible data failed its
|
||||
OTA with "Writing OTA data to flash memory failed". Use one image and `--windows 12` there.
|
||||
- `bench.hpp` targets the decoder interface with `dest_limit`. #21641 drops it (the output runs into the window up
|
||||
@@ -84,3 +83,24 @@ case's fires in order with progress collapsed to a count; diff dev against the P
|
||||
- A random file through web_server gives `error 128` on the ESP32 and `error 132` on the ESP8266.
|
||||
- A SIGTERM to the uploader lets it finish the transfer; use SIGKILL for the native error path.
|
||||
- Results: https://github.com/esphome/esphome/pull/21903#issuecomment-6516529409
|
||||
|
||||
## Signed OTA with several trusted keys (`verification_keys`, #22398)
|
||||
|
||||
`scripts/signed_ota/` on an ESP32-C3 (`signed-ota-c3.yaml`, `signed_ota_verification: signing_scheme: rsa3072,
|
||||
verification_keys: [keyA.pem]`):
|
||||
1. `python -m espsecure generate-signing-key --version 2 --scheme rsa3072 keyA.pem` (and keyB).
|
||||
2. Build four tagged images with `esphome -s tag <name> compile`, serial-flash the first factory image (no on-boot
|
||||
signature check without secure boot), copy each `firmware.ota.bin`.
|
||||
3. `sign_variants.py <image> keyA.pem keyB.pem <prefix>` writes key A, key B, B then A, stored CRC flipped, byte 2
|
||||
changed (covered only by the CRC), the same with the CRC recomputed by zlib, and B then A with block 1's CRC flipped.
|
||||
4. `otaseq.sh <ip> FILE=reject... FILE=<tag>...` sends them with `esphome upload --file` and checks the first verdict
|
||||
in the device log and the upload result against the expectation (a rejection, or the `BUILDTAG` of that build);
|
||||
send the rejects first so every check runs in the same build, then the accepted ones.
|
||||
Expected: unsigned, B only and every CRC edit rejected; A, B+A and the zlib-fixed CRC accepted. The byte 2 pair isolates
|
||||
the CRC from the signature and key digest.
|
||||
|
||||
Gotchas: an ESP-IDF image can contain `E7 02` at a 4 KiB boundary by chance; find the signature sector by the CRC, as
|
||||
`sign_variants.py` does. An unsigned image sent after a signed one can be rejected with "trusted key failed to verify"
|
||||
instead of "no signature block": the partition still holds the old signature sector past the new image's end.
|
||||
|
||||
Results: https://github.com/esphome/esphome/pull/22398#issuecomment-6716330334
|
||||
@@ -0,0 +1,15 @@
|
||||
# Per period PWM capture on the same chip (#15438)
|
||||
|
||||
`scripts/pwm_capture/` (ESP32-C6): `pwmcap.hpp` routes the `ledc` pad through the GPIO matrix into RMT receive at
|
||||
80 MHz (ping pong buffers, `en_partial_rx`, `mem_block_symbols: 96`) and a PCNT unit, checks every period in the IRAM
|
||||
safe RMT interrupt and logs a `WIN` line every 10 s (high time and period histograms, minimum and maximum, level
|
||||
changes, spikes, reversals). `common.yaml` needs `include_builtin_idf_components: [esp_driver_rmt, esp_driver_pcnt]`
|
||||
and `CONFIG_RMT_ISR_IRAM_SAFE` and `CONFIG_PCNT_ISR_IRAM_SAFE`; build it with `-s name NAME -s ps light` (or `none`).
|
||||
`run.sh NAME SECONDS [--every 5 --transition 1]` (env `TREE`, `PORT`, `SC`, `PY`) resets and captures while
|
||||
`apiclient.py` holds an API connection and optionally sends light transitions; `summ.py` totals the windows.
|
||||
|
||||
- LEDC and RMT share PLL_F80M, so a 20 kHz 11 bit output reads exactly 4000 ticks per period with no jitter.
|
||||
- Result: no glitch in about 10 million periods with power save `light` or `none`; #15718 (LEDC in IRAM) changed
|
||||
nothing. Positive control: a lambda that changes the duty for a few periods is caught every time.
|
||||
- Window boundaries follow RMT chunks (about 1000 periods), so `n` alternates between 199584 and 200592; sum over
|
||||
the run.
|
||||
@@ -0,0 +1,32 @@
|
||||
# SPI bus capture on the same board, no chips or wiring (#22376)
|
||||
|
||||
`scripts/spi_self/`: `gen.py` writes `spi<board>.yaml` (e32 classic ESP32, c3, e66 ESP8266, rp Pico 2 W) with a
|
||||
hardware bus, a software bus, several `spi_device` instances (all four modes, MSB and LSB first, different rates and
|
||||
CS pins) and a `max7219`. `spitest.hpp` drives every device from a 20 ms interval (an 8 byte transfer, write16, a 12
|
||||
bit write, read_array, write_array, back to back transfers, teardown plus setup, a second setup, a max7219 update),
|
||||
three passes, then reboots. `run.sh BOARD LABEL SECONDS` (env `TREE`, `PORT`, `SC`, `PY`, and `PORTS` for the Pico)
|
||||
builds, flashes, captures and checks; `analyze.py spi<board>.yaml BASE.txt PR.txt` checks each record against the
|
||||
expected wire data and diffs the two builds by the majority payload per record, so interrupt jitter does not count.
|
||||
|
||||
- Classic ESP32: the ESPHome bus is on SPI2 through the matrix and SPI3 runs as a slave without DMA (64 bytes at
|
||||
most), set up again in the device's mode for each operation. Its nominal CS must be a spare pad held high
|
||||
(GPIO14); the device CS is routed into `spics_in` only after `spi_slave_queue_trans()`, because the slave driver
|
||||
reconnects the nominal pad when a transaction loads. Never give the slave the device's CS pad:
|
||||
`spicommon_cs_initialize` takes over its output and `spi_slave_free` resets it. `gpio_set_direction(..., OUTPUT)`
|
||||
turns a pad back into a plain GPIO output, so only call `gpio_input_enable` on pads the master drives.
|
||||
- With that slave attached, full duplex reads on the master lose their last bytes after the first read of a boot,
|
||||
even through a MOSI to MISO loopback; a config without the slave reads fine. Do not judge reads on that layout.
|
||||
- The slave sees bit-banged mode 1 data one bit late; the interrupt captures decode it correctly.
|
||||
- C3, ESP8266 and Pico 2 W: GPIO edge interrupts on SCK and CS take snapshots of the input register. C3 SPI2 cannot
|
||||
clock below about 78 kHz; at 80 to 100 kHz the capture works with
|
||||
`gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM)`, but one build lost an SCK pair at the start
|
||||
of a frame in every run and a rebuild did not: compare two builds before calling a missing edge real. Let the log
|
||||
output drain (20 ms) before a capture that follows `spi_setup()`.
|
||||
- ESP8266: the HSPI pads (12, 13, 14) do not show in `GPI` while in SPI function; the script logs `SPI1CLK`,
|
||||
`SPI1C`, `SPI1U` and `SPI1P` after each hardware bus operation instead.
|
||||
- Pico 2 W: `IO_BANK0` INOVER on the MISO pad forces the SPI receive input low or high (reads 00 and ff).
|
||||
- Software bus loopback: MOSI and MISO on one pad with `allow_other_uses`, then set the pad back to output.
|
||||
- Seen on dev before #22376: the software bus ignores `data_rate` (`cycle_clock_()` never waits), and SCK moves to
|
||||
the new idle level only after CS is low when devices with different clock polarity share the software bus.
|
||||
- LibreTiny (BK72xx, RTL87xx, LN882x) has no `spi:` (config validation allows esp32, esp8266, nrf52 and rp2 only),
|
||||
so SPI changes cannot be run there.
|
||||
@@ -0,0 +1,194 @@
|
||||
#pragma once
|
||||
// BMP390 bench helpers: register dump, INT data-ready edge counter, start write fault injection (software NACK or
|
||||
// SDA held through the GPIO matrix) and a bus probe. ESP32-S3, ESP-IDF.
|
||||
#include <cmath>
|
||||
#include "esphome/components/i2c/i2c_bus.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_rom_gpio.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "soc/gpio_reg.h"
|
||||
#include "soc/soc.h"
|
||||
#include "soc/io_mux_reg.h"
|
||||
#include "soc/gpio_periph.h"
|
||||
|
||||
namespace bmpt {
|
||||
static const char *const TAG = "bmpt";
|
||||
|
||||
inline volatile uint32_t g_int_edges = 0;
|
||||
inline void int_isr(void *) { g_int_edges = g_int_edges + 1; }
|
||||
inline void int_counter_begin(int pin) {
|
||||
gpio_config_t c{};
|
||||
c.pin_bit_mask = 1ULL << pin;
|
||||
c.mode = GPIO_MODE_INPUT;
|
||||
c.pull_down_en = GPIO_PULLDOWN_ENABLE;
|
||||
c.intr_type = GPIO_INTR_POSEDGE;
|
||||
gpio_config(&c);
|
||||
esp_err_t e = gpio_install_isr_service(0);
|
||||
if (e != ESP_OK && e != ESP_ERR_INVALID_STATE)
|
||||
ESP_LOGE(TAG, "isr service %d", e);
|
||||
gpio_isr_handler_add((gpio_num_t) pin, int_isr, nullptr);
|
||||
}
|
||||
|
||||
inline int g_sda = -1;
|
||||
inline int g_port = 0;
|
||||
inline bool g_held = false;
|
||||
inline int g_hold_level = 0; // 0: SDA shorted low, 1: SDA disconnected from the controller (open line, pulled high)
|
||||
inline bool g_bus_reset_on_probe = false;
|
||||
inline uint32_t g_out_sel = 0;
|
||||
inline bool g_saved = false;
|
||||
inline bool g_log_pad_once = true;
|
||||
inline uint32_t out_sel_reg() { return REG_READ(GPIO_FUNC0_OUT_SEL_CFG_REG + 4 * g_sda); }
|
||||
inline uint32_t pin_reg() { return REG_READ(GPIO_PIN0_REG + 4 * g_sda); }
|
||||
inline uint32_t mux_reg() { return REG_READ(GPIO_PIN_MUX_REG[g_sda]); }
|
||||
inline uint32_t en_bit() { return (REG_READ(GPIO_ENABLE_REG) >> g_sda) & 1; }
|
||||
inline uint32_t g_pin = 0, g_mux = 0, g_en = 0;
|
||||
inline void log_pad(const char *when) {
|
||||
ESP_LOGI(TAG, "PAD %s out_sel=0x%08X pin=0x%08X mux=0x%08X en=%u", when, (unsigned) out_sel_reg(),
|
||||
(unsigned) pin_reg(), (unsigned) mux_reg(), (unsigned) en_bit());
|
||||
}
|
||||
inline void sda_hold() {
|
||||
if (!g_saved) {
|
||||
g_out_sel = out_sel_reg(); // routing the i2c driver set up; restored verbatim on release
|
||||
g_pin = pin_reg();
|
||||
g_mux = mux_reg();
|
||||
g_en = en_bit();
|
||||
log_pad("before-hold");
|
||||
g_saved = true;
|
||||
ESP_LOGI(TAG, "SDA out_sel saved 0x%08X (I2CEXT0_SDA_OUT_IDX=%d port=%d)", (unsigned) g_out_sel,
|
||||
I2CEXT0_SDA_OUT_IDX, g_port);
|
||||
}
|
||||
gpio_set_level((gpio_num_t) g_sda, g_hold_level);
|
||||
esp_rom_gpio_connect_out_signal(g_sda, SIG_GPIO_OUT_IDX, false, false);
|
||||
g_held = true;
|
||||
if (g_log_pad_once) {
|
||||
log_pad("held");
|
||||
}
|
||||
}
|
||||
inline void sda_release() {
|
||||
gpio_set_level((gpio_num_t) g_sda, 1);
|
||||
REG_WRITE(GPIO_FUNC0_OUT_SEL_CFG_REG + 4 * g_sda, g_out_sel);
|
||||
REG_WRITE(GPIO_PIN0_REG + 4 * g_sda, g_pin);
|
||||
REG_WRITE(GPIO_PIN_MUX_REG[g_sda], g_mux);
|
||||
if (g_en)
|
||||
REG_WRITE(GPIO_ENABLE_W1TS_REG, 1u << g_sda);
|
||||
else
|
||||
REG_WRITE(GPIO_ENABLE_W1TC_REG, 1u << g_sda);
|
||||
g_held = false;
|
||||
if (g_log_pad_once) {
|
||||
log_pad("released");
|
||||
g_log_pad_once = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps the sensor's bus: injects the start write faults and logs every failed transfer
|
||||
class FaultBus : public esphome::i2c::I2CBus {
|
||||
public:
|
||||
explicit FaultBus(esphome::i2c::I2CBus *real) : real_(real) {}
|
||||
uint32_t hold_until{0}; // millis() deadline of a timed hold
|
||||
bool arm_start{false}; // hold SDA only during the next forced mode start write (PWR_CTRL mode bits 01)
|
||||
// until this deadline every transfer fails on its own: free-bus dummy read, hold, transfer, release
|
||||
uint32_t window_until{0};
|
||||
bool soft_arm{false}; // next start write fails with NACK in software, the wire is not touched
|
||||
uint32_t soft_until{0}; // every start write fails with NACK in software until this deadline
|
||||
uint32_t xfers{0}, fails{0}, start_writes{0}, start_fails{0}, normal_writes{0};
|
||||
esphome::i2c::ErrorCode write_readv(uint8_t address, const uint8_t *w, size_t wn, uint8_t *r, size_t rn) override {
|
||||
// mode bits of a PWR_CTRL write: 1 forced (a start), 3 normal
|
||||
int mode = wn == 2 && w[0] == 0x1B ? (w[1] >> 4) & 3 : -1;
|
||||
bool is_start = mode == 1;
|
||||
bool targeted = is_start && this->arm_start;
|
||||
if (is_start && (this->soft_arm || (int32_t) (this->soft_until - esphome::millis()) > 0)) {
|
||||
this->soft_arm = false;
|
||||
this->start_writes++;
|
||||
this->start_fails++;
|
||||
this->xfers++;
|
||||
this->fails++;
|
||||
ESP_LOGI(TAG, "SOFTFAIL start write reg=0x1B val=0x%02X -> NACK", w[1]);
|
||||
return esphome::i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
bool timed = (int32_t) (this->hold_until - esphome::millis()) > 0;
|
||||
bool window = (int32_t) (this->window_until - esphome::millis()) > 0;
|
||||
if (window) {
|
||||
// let the driver run its post-error FSM reset with free lines, so each injected failure starts from a clean bus
|
||||
uint8_t reg = 0x00, v;
|
||||
this->real_->write_readv(address, ®, 1, &v, 1);
|
||||
targeted = true;
|
||||
}
|
||||
if (timed || targeted)
|
||||
sda_hold(); // reasserted per transfer in case the driver re-routes the pin after an error
|
||||
if (is_start)
|
||||
this->start_writes++;
|
||||
if (mode == 3)
|
||||
this->normal_writes++;
|
||||
auto err = this->real_->write_readv(address, w, wn, r, rn);
|
||||
if (targeted) {
|
||||
this->arm_start = false;
|
||||
sda_release();
|
||||
} else if (!timed && g_held) {
|
||||
sda_release();
|
||||
}
|
||||
this->xfers++;
|
||||
if (err != esphome::i2c::ERROR_OK) {
|
||||
this->fails++;
|
||||
if (is_start)
|
||||
this->start_fails++;
|
||||
ESP_LOGI(TAG, "XFERFAIL addr=0x%02X reg=0x%02X wn=%u rn=%u val=0x%02X err=%d start=%d", address,
|
||||
wn > 0 ? w[0] : 0xFF, (unsigned) wn, (unsigned) rn, wn > 1 ? w[1] : 0, (int) err, is_start);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
protected:
|
||||
esphome::i2c::I2CBus *real_;
|
||||
};
|
||||
inline FaultBus *g_fb = nullptr;
|
||||
inline esphome::i2c::I2CBus *g_real = nullptr;
|
||||
|
||||
// Release a timed hold once its deadline passes even when no transfer happens.
|
||||
inline void fault_tick() {
|
||||
if (g_fb != nullptr && g_held && !g_fb->arm_start && (int32_t) (g_fb->hold_until - esphome::millis()) <= 0)
|
||||
sda_release();
|
||||
}
|
||||
|
||||
inline bool rd(uint8_t addr, uint8_t reg, uint8_t *v, size_t n = 1) {
|
||||
return g_real->write_readv(addr, ®, 1, v, n) == esphome::i2c::ERROR_OK;
|
||||
}
|
||||
inline bool wr(uint8_t addr, uint8_t reg, uint8_t v) {
|
||||
uint8_t b[2] = {reg, v};
|
||||
return g_real->write_readv(addr, b, 2, nullptr, 0) == esphome::i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
inline void dump_regs(uint8_t addr) {
|
||||
uint8_t chip = 0, err = 0, ictl = 0, pwr = 0, osr = 0, odr = 0, cfg = 0;
|
||||
bool ok = rd(addr, 0x00, &chip) && rd(addr, 0x02, &err) && rd(addr, 0x19, &ictl) && rd(addr, 0x1B, &pwr) &&
|
||||
rd(addr, 0x1C, &osr) && rd(addr, 0x1D, &odr) && rd(addr, 0x1F, &cfg);
|
||||
ESP_LOGI(TAG, "REGS ok=%d chip=0x%02X err=0x%02X int_ctrl=0x%02X pwr=0x%02X mode=%u osr=0x%02X odr=0x%02X cfg=0x%02X",
|
||||
ok, chip, err, ictl, pwr, (pwr >> 4) & 3, osr, odr, cfg);
|
||||
}
|
||||
|
||||
inline void log_heap() {
|
||||
ESP_LOGI(TAG, "HEAP internal_free=%u internal_total=%u min_free=%u",
|
||||
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
||||
(unsigned) heap_caps_get_total_size(MALLOC_CAP_INTERNAL),
|
||||
(unsigned) heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
|
||||
}
|
||||
|
||||
// Probe the chip; on failure log the line levels, reset the IDF bus once and probe again.
|
||||
inline void probe(uint8_t addr, int scl) {
|
||||
uint8_t v = 0;
|
||||
bool ok = rd(addr, 0x00, &v);
|
||||
ESP_LOGI(TAG, "PROBE %s chip=0x%02X held=%d sel=0x%08X sda=%d scl=%d", ok ? "ok" : "err", v, g_held,
|
||||
(unsigned) out_sel_reg(), gpio_get_level((gpio_num_t) g_sda), gpio_get_level((gpio_num_t) scl));
|
||||
if (ok || !g_bus_reset_on_probe)
|
||||
return;
|
||||
i2c_master_bus_handle_t h;
|
||||
esp_err_t e = i2c_master_get_bus_handle((i2c_port_num_t) g_port, &h);
|
||||
if (e == ESP_OK)
|
||||
e = i2c_master_bus_reset(h);
|
||||
ok = rd(addr, 0x00, &v);
|
||||
ESP_LOGI(TAG, "PROBE after bus reset (%d): %s chip=0x%02X", e, ok ? "ok" : "err", v);
|
||||
}
|
||||
} // namespace bmpt
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Write a BMP390 bench config (ESP32-S3, no Wi-Fi) next to this script.
|
||||
|
||||
usage: gen.py NAME [--rate 50Hz] [--iir 128x|off] [--interval 1s] [--fault --kinds NMTWLSHD --probe-delay MS]
|
||||
[--reset-at 40,80] [--open-line] [--bus-reset]
|
||||
Fault kinds, one per event at 30 s and then every 20 s: N next start write NACKs in software, M every start write
|
||||
NACKs for 5 s, T SDA held low for the next start write, W the same for every transfer for 5 s, L a 5 s continuous
|
||||
hold, S a 300 ms hold, H and D harness-only held transfers.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
|
||||
FAULT = """
|
||||
- interval: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
bmpt::fault_tick();
|
||||
static constexpr char KIND[] = "KINDS";
|
||||
static uint8_t i = 0;
|
||||
static uint32_t probe_at = 0;
|
||||
uint32_t now = millis();
|
||||
if (i < sizeof(KIND) - 1 && now >= 30000u + 20000u * i) { // events at 30 s, then every 20 s
|
||||
char k = KIND[i++];
|
||||
ESP_LOGI("bmpt", "FAULT %c at %u failed=%d warn=%d", k, (unsigned) now, id(bmp)->is_failed(),
|
||||
id(bmp)->status_has_warning());
|
||||
// S and L hold SDA at once; fault_tick() releases it at the deadline
|
||||
if (k == 'S') {
|
||||
bmpt::g_fb->hold_until = now + 300;
|
||||
bmpt::sda_hold();
|
||||
}
|
||||
if (k == 'W') bmpt::g_fb->window_until = now + 5000;
|
||||
if (k == 'L' || k == 'D') bmpt::g_fb->hold_until = now + 5000;
|
||||
if (k == 'L')
|
||||
bmpt::sda_hold();
|
||||
if (k == 'N') bmpt::g_fb->soft_arm = true;
|
||||
if (k == 'M') bmpt::g_fb->soft_until = now + 5000;
|
||||
if (k == 'H') {
|
||||
uint8_t reg = 0x00, v;
|
||||
bmpt::g_fb->arm_start = false;
|
||||
bmpt::sda_hold();
|
||||
bool hok = bmpt::g_real->write_readv(ADDR, ®, 1, &v, 1) == esphome::i2c::ERROR_OK;
|
||||
bmpt::sda_release();
|
||||
ESP_LOGI("bmpt", "HARNESS held transfer ok=%d", hok);
|
||||
}
|
||||
if (k == 'D') {
|
||||
uint8_t reg = 0x00, v;
|
||||
for (int n = 0; n < 2; n++)
|
||||
bmpt::g_fb->write_readv(ADDR, ®, 1, &v, 1);
|
||||
}
|
||||
if (k == 'T') bmpt::g_fb->arm_start = true;
|
||||
probe_at = now + PROBEDELAY;
|
||||
}
|
||||
if (probe_at != 0 && now >= probe_at) {
|
||||
probe_at = 0;
|
||||
bmpt::probe(ADDR, SCLPIN);
|
||||
}
|
||||
"""
|
||||
|
||||
RESET = """
|
||||
- interval: 1s
|
||||
then:
|
||||
- lambda: |-
|
||||
static constexpr uint32_t AT[] = {RESETAT};
|
||||
static uint8_t i = 0;
|
||||
if (i < sizeof(AT) / sizeof(AT[0]) && millis() >= AT[i] * 1000) {
|
||||
i++;
|
||||
bool ok = bmpt::wr(ADDR, 0x7E, 0xB6);
|
||||
delay(10);
|
||||
ESP_LOGI("bmpt", "SOFTRESET ok=%d", ok);
|
||||
bmpt::dump_regs(ADDR);
|
||||
ESP_LOGI("bmpt", "DRDY ok=%d", bmpt::wr(ADDR, 0x19, 0x42));
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("name")
|
||||
ap.add_argument("--addr", default="0x77")
|
||||
ap.add_argument("--rate")
|
||||
ap.add_argument("--iir", default="128x")
|
||||
ap.add_argument("--interval", default="1s")
|
||||
ap.add_argument("--posr", default="8x")
|
||||
ap.add_argument("--tosr", default="none")
|
||||
ap.add_argument("--fault", action="store_true")
|
||||
ap.add_argument("--kinds", default="WTSWTSWT")
|
||||
ap.add_argument("--probe-delay", default="7000")
|
||||
ap.add_argument(
|
||||
"--open-line",
|
||||
action="store_true",
|
||||
help="fault routes SDA away (floats high) instead of low",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--bus-reset",
|
||||
action="store_true",
|
||||
help="reset the IDF bus after a failed probe",
|
||||
)
|
||||
ap.add_argument("--sda", default="1")
|
||||
ap.add_argument("--scl", default="2")
|
||||
ap.add_argument("--int-pin", default="42")
|
||||
ap.add_argument(
|
||||
"--no-drdy", action="store_true", help="do not enable data ready on INT"
|
||||
)
|
||||
ap.add_argument(
|
||||
"--reset-at",
|
||||
default="",
|
||||
help="seconds, comma separated: soft reset the chip behind the driver",
|
||||
)
|
||||
a = ap.parse_args()
|
||||
|
||||
rate = f"\n data_rate: {a.rate}" if a.rate else ""
|
||||
# data ready on INT, push-pull, active high; the driver's soft reset in setup clears it
|
||||
drdy = (
|
||||
""
|
||||
if a.no_drdy
|
||||
else f' ESP_LOGI("bmpt", "DRDY ok=%d", bmpt::wr({a.addr}, 0x19, 0x42));\n'
|
||||
)
|
||||
extra = ""
|
||||
if a.fault:
|
||||
extra += (
|
||||
FAULT.replace("KINDS", a.kinds)
|
||||
.replace("PROBEDELAY", a.probe_delay)
|
||||
.replace("SCLPIN", a.scl)
|
||||
)
|
||||
if a.reset_at:
|
||||
extra += RESET.replace("RESETAT", a.reset_at)
|
||||
yaml = f"""esphome:
|
||||
name: {a.name}
|
||||
includes:
|
||||
- bmp390probe.hpp
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: |-
|
||||
bmpt::g_real = id(bus);
|
||||
bmpt::g_sda = {a.sda};
|
||||
bmpt::g_port = id(bus)->get_port();
|
||||
static bmpt::FaultBus fb(id(bus));
|
||||
id(bmp)->set_i2c_bus(&fb);
|
||||
bmpt::g_fb = &fb;
|
||||
bmpt::g_bus_reset_on_probe = {"true" if a.bus_reset else "false"};
|
||||
bmpt::g_hold_level = {1 if a.open_line else 0};
|
||||
ESP_LOGI("bmpt", "PRESETUP");
|
||||
bmpt::dump_regs({a.addr});
|
||||
- priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("bmpt", "POSTSETUP failed=%d", id(bmp)->is_failed());
|
||||
bmpt::dump_regs({a.addr});
|
||||
{drdy} bmpt::int_counter_begin({a.int_pin});
|
||||
bmpt::dump_regs({a.addr});
|
||||
bmpt::log_heap();
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
i2c:
|
||||
id: bus
|
||||
sda: GPIO{a.sda}
|
||||
scl: GPIO{a.scl}
|
||||
scan: true
|
||||
|
||||
sensor:
|
||||
- platform: bmp3xx_i2c
|
||||
id: bmp
|
||||
address: {a.addr}
|
||||
pressure:
|
||||
name: pressure
|
||||
oversampling: {a.posr}
|
||||
on_value:
|
||||
- lambda: 'ESP_LOGI("bmpt", "READ %u %.4f", (unsigned) millis(), x);'
|
||||
temperature:
|
||||
name: temperature
|
||||
oversampling: {a.tosr}
|
||||
iir_filter: "{a.iir}"{rate}
|
||||
update_interval: {a.interval}
|
||||
|
||||
interval:
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
static uint32_t last_edges = 0, last_ms = 0;
|
||||
uint32_t e = bmpt::g_int_edges, now = millis();
|
||||
ESP_LOGI("bmpt", "INT edges=%u in %u ms failed=%d warn=%d xfers=%u fails=%u starts=%u "
|
||||
"start_fails=%u normal_writes=%u",
|
||||
(unsigned) (e - last_edges), (unsigned) (now - last_ms), id(bmp)->is_failed(),
|
||||
id(bmp)->status_has_warning(), bmpt::g_fb->xfers, bmpt::g_fb->fails, bmpt::g_fb->start_writes,
|
||||
bmpt::g_fb->start_fails, bmpt::g_fb->normal_writes);
|
||||
last_edges = e;
|
||||
last_ms = now;
|
||||
if (!bmpt::g_held) bmpt::dump_regs({a.addr});
|
||||
bmpt::log_heap();
|
||||
{extra.replace("ADDR", a.addr)}"""
|
||||
out = HERE / f"{a.name}.yaml"
|
||||
out.write_text(yaml, encoding="utf-8")
|
||||
print(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# run.sh NAME SECONDS [TAG]: build NAME.yaml (written by gen.py) from TREE, flash the ESP32-S3, capture SECONDS from
|
||||
# a fresh boot into out-NAME/TAG.txt and print stats.py's summary.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); N=${1:?name}; S=${2:?seconds}; G=${3:-run1}
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
[ -f "$R/$N.yaml" ] || { echo "no $N.yaml, run gen.py first"; exit 1; }
|
||||
OUT=$R/out-$N; mkdir -p "$OUT"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$OUT/upload.log"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
$PY "$SC" "$P" "$S" "$OUT/$G.txt" --reset > /dev/null 2>&1
|
||||
[ -s "$OUT/$G.txt" ] || { echo "serial capture got nothing"; exit 1; }
|
||||
$PY "$R/stats.py" "$OUT/$G.txt"
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Pressure statistics from a bmp390 capture.
|
||||
|
||||
usage: stats.py CAPTURE [SKIP_S]
|
||||
Prints the read count and cadence, the noise (standard deviation, of consecutive differences and detrended over a
|
||||
21 sample window), identical consecutive readings, the INT data ready rate per 10 s window and counts of errors,
|
||||
warnings and harness lines. SKIP_S drops the readings before that many seconds after boot (IIR settling).
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = pathlib.Path(sys.argv[1]).read_bytes().decode(errors="replace").splitlines()
|
||||
skip = float(sys.argv[2]) if len(sys.argv) > 2 else 0
|
||||
reads = [
|
||||
(int(m[1]), float(m[2]))
|
||||
for line in lines
|
||||
if (m := re.search(r"READ (\d+) ([\d.]+)", line))
|
||||
]
|
||||
r = [p for t, p in reads if t / 1000 >= skip]
|
||||
if len(r) > 2:
|
||||
d = [b - a for a, b in zip(r, r[1:], strict=False)]
|
||||
same = sum(1 for x in d if x == 0)
|
||||
print(
|
||||
f"reads={len(reads)} used={len(r)} mean={statistics.mean(r):.4f} sd={statistics.pstdev(r) * 100:.2f}Pa "
|
||||
f"sd_diff={statistics.pstdev(d) * 100:.2f}Pa identical_consecutive={same} min={min(r):.4f} max={max(r):.4f}"
|
||||
)
|
||||
ts = [t for t, _ in reads]
|
||||
if len(ts) > 2:
|
||||
gaps = [b - a for a, b in zip(ts, ts[1:], strict=False)]
|
||||
print(f"cadence mean={statistics.mean(gaps):.0f}ms max_gap={max(gaps)}ms")
|
||||
ints = [
|
||||
(int(m[1]), int(m[2]))
|
||||
for line in lines
|
||||
if (m := re.search(r"INT edges=(\d+) in (\d+) ms", line))
|
||||
]
|
||||
# the first window starts at boot, before data ready is on
|
||||
if rates := [e * 1000 / ms for e, ms in ints[1:] if ms > 9000]:
|
||||
print(f"INT rate Hz: {' '.join(f'{x:.2f}' for x in rates)}")
|
||||
for pat in (
|
||||
r"\[E\]",
|
||||
r"\[W\]",
|
||||
"XFERFAIL",
|
||||
"SOFTFAIL",
|
||||
"FAULT",
|
||||
"PROBE",
|
||||
"marked FAILED",
|
||||
"REGS",
|
||||
):
|
||||
print(f"{pat}: {sum(1 for line in lines if re.search(pat, line))}")
|
||||
if len(r) > 30:
|
||||
k = 10
|
||||
res = [
|
||||
r[i] - statistics.mean(r[i - k : i + k + 1]) for i in range(k, len(r) - k)
|
||||
]
|
||||
print(f"detrended sd (21 sample window)={statistics.pstdev(res) * 100:.3f}Pa")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
includes:
|
||||
- crctest.hpp
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- delay: 3s
|
||||
- lambda: |-
|
||||
// unqualified call, as a lambda user would write it; counted in the CRCRESULT totals
|
||||
crctest::run(crc32(0, reinterpret_cast<const uint8_t *>("123456789"), 9));
|
||||
|
||||
interval:
|
||||
- interval: 20s
|
||||
then:
|
||||
- lambda: crctest::run();
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
// crc32 vectors for esphome#22398, expected values from Python zlib.crc32
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
namespace crctest {
|
||||
inline int pass_n = 0, fail_n = 0;
|
||||
inline void check(const char *name, uint32_t got, uint32_t want) {
|
||||
if (got == want)
|
||||
pass_n++;
|
||||
else
|
||||
fail_n++;
|
||||
ESP_LOGI("crctest", "%s %s got=%08X want=%08X", got == want ? "PASS" : "FAIL", name, (unsigned) got, (unsigned) want);
|
||||
}
|
||||
// unqualified: a crc32() of "123456789" computed in the caller's lambda, or -1 to skip that check
|
||||
inline void run(int64_t unqualified = -1) {
|
||||
pass_n = fail_n = 0;
|
||||
const uint8_t *s = reinterpret_cast<const uint8_t *>("123456789");
|
||||
check("check-123456789", esphome::crc32(0, s, 9), 0xCBF43926u);
|
||||
check("empty-seed0", esphome::crc32(0, s, 0), 0u);
|
||||
check("empty-seed12345678", esphome::crc32(0x12345678u, s, 0), 0x12345678u);
|
||||
check("chained-4+5", esphome::crc32(esphome::crc32(0, s, 4), s + 4, 5), 0xCBF43926u);
|
||||
uint8_t all[256];
|
||||
for (int i = 0; i < 256; i++)
|
||||
all[i] = i;
|
||||
check("bytes-0-255", esphome::crc32(0, all, 256), 0x29058C73u);
|
||||
uint8_t ff[32];
|
||||
memset(ff, 0xFF, sizeof(ff));
|
||||
check("32x0xFF", esphome::crc32(0, ff, 32), 0xFF6CAB0Bu);
|
||||
uint8_t *pat = static_cast<uint8_t *>(malloc(4100 + 8192));
|
||||
if (pat == nullptr) {
|
||||
ESP_LOGE("crctest", "malloc failed");
|
||||
fail_n++;
|
||||
ESP_LOGI("crctest", "CRCRESULT pass=%d fail=%d", pass_n, fail_n);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 4100; i++)
|
||||
pat[i] = (i * 7 + 3) & 0xFF;
|
||||
static const uint32_t EXP_OFFSET[4] = {0xDF4C2BA9u, 0x5D4B54F6u, 0x3B885695u, 0xAE989C6Eu};
|
||||
for (int o = 0; o < 4; o++) {
|
||||
char n[24];
|
||||
snprintf(n, sizeof(n), "unaligned-off%d", o);
|
||||
check(n, esphome::crc32(0, pat + o, 4093), EXP_OFFSET[o]);
|
||||
}
|
||||
uint32_t c = 0;
|
||||
for (int i = 0; i < 300; i++)
|
||||
c = esphome::crc32(c, pat + i, 1);
|
||||
check("chained-1-byte-x300", c, 0xDE0E57CEu);
|
||||
c = 0;
|
||||
for (int i = 0; i < 4093; i += 97)
|
||||
c = esphome::crc32(c, pat + 1 + i, std::min(97, 4093 - i));
|
||||
check("chained-97-byte-unaligned", c, 0x5D4B54F6u);
|
||||
uint8_t *big = pat + 4100;
|
||||
for (int i = 0; i < 8192; i++)
|
||||
big[i] = (i * 13 + 5) & 0xFF;
|
||||
uint32_t best = 0xFFFFFFFF, r = 0;
|
||||
for (int k = 0; k < 5; k++) {
|
||||
const uint32_t t0 = (esphome::micros) ();
|
||||
r = esphome::crc32(0, big, 8192);
|
||||
const uint32_t dt = (esphome::micros) () - t0;
|
||||
if (dt < best)
|
||||
best = dt;
|
||||
}
|
||||
check("8KiB", r, 0xF6950930u);
|
||||
free(pat);
|
||||
ESP_LOGI("crctest", "CRCSPEED 8192 bytes in %u us = %.1f us/KiB", (unsigned) best, best / 8.0f);
|
||||
if (unqualified >= 0)
|
||||
check("unqualified", (uint32_t) unqualified, 0xCBF43926u);
|
||||
ESP_LOGI("crctest", "CRCRESULT pass=%d fail=%d", pass_n, fail_n);
|
||||
}
|
||||
} // namespace crctest
|
||||
@@ -9,6 +9,7 @@ before the LinkChecks instead.
|
||||
import sys
|
||||
|
||||
import bk7231tools.serial.main as bk_main
|
||||
from lt_platform import with_platform
|
||||
from ltchiptool.__main__ import cli
|
||||
from ltchiptool.soc.bk72xx.flash import BK72XXFlash
|
||||
from serial import Serial
|
||||
@@ -47,5 +48,5 @@ def flash_connect(self) -> None:
|
||||
BK72XXFlash.flash_hw_reset = flash_hw_reset
|
||||
BK72XXFlash.flash_connect = flash_connect
|
||||
|
||||
sys.argv = ["ltchiptool", *sys.argv[1:]]
|
||||
sys.argv = ["ltchiptool", *with_platform(sys.argv[1:])]
|
||||
cli()
|
||||
@@ -11,6 +11,7 @@ Usage: python flash_rtl87xx_kit.py flash write -d <port> firmware.uf2
|
||||
import sys
|
||||
import time
|
||||
|
||||
from lt_platform import with_platform
|
||||
import serial
|
||||
|
||||
_first = [True]
|
||||
@@ -61,7 +62,9 @@ def boot_app(port: str) -> None:
|
||||
def main() -> int:
|
||||
rc = 1
|
||||
try:
|
||||
result = cli_entrypoint.main(args=sys.argv[1:], standalone_mode=False)
|
||||
result = cli_entrypoint.main(
|
||||
args=with_platform(sys.argv[1:]), standalone_mode=False
|
||||
)
|
||||
rc = int(result) if isinstance(result, int) else 0
|
||||
finally:
|
||||
port = port_argument(sys.argv)
|
||||
|
||||
@@ -70,7 +70,9 @@ def frame(msg_type: int, payload: bytes) -> bytes:
|
||||
return b"\x00" + varint(len(payload)) + varint(msg_type) + payload
|
||||
|
||||
|
||||
def f_bytes(num: int, data: bytes) -> bytes:
|
||||
def f_bytes(num: int, data: bytes | str) -> bytes:
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
return varint((num << 3) | 2) + varint(len(data)) + data
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# One fixed entity_id entity and one wizard-linked entity (no entity_id) per homeassistant platform that a wizard
|
||||
# input can stand for, on top of the three buttons of ha-button-common.yaml, on an ESP8266 (the ramchk interval reads
|
||||
# the ESP8266 heap). run.sh adds esphome name and board, and ha-wiz-light.yaml unless NO_LIGHT is set.
|
||||
packages:
|
||||
buttons: !include ../homeassistant_button/ha-button-common.yaml
|
||||
|
||||
api:
|
||||
wizard:
|
||||
pages:
|
||||
- inputs:
|
||||
- entity: s_wiz
|
||||
- entity: sa_wiz
|
||||
- entity: b_wiz
|
||||
- entity: t_wiz
|
||||
- entity: n_wiz
|
||||
- entity: sw_wiz
|
||||
- entity: sel_wiz
|
||||
- entity: tx_wiz
|
||||
|
||||
# Free heap every 5 s, and the heap size, for static + heap used
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: |-
|
||||
uint32_t heap_size = 0x3FFFC000u - reinterpret_cast<uint32_t>(&_heap_start);
|
||||
ESP_LOGI("ramchk", "heap_size=%u free=%u max_block=%u", heap_size, ESP.getFreeHeap(), ESP.getMaxFreeBlockSize());
|
||||
|
||||
# homeassistant entities are internal by default; internal: false lets the API client list and drive them
|
||||
sensor:
|
||||
- {platform: homeassistant, id: s_fix, internal: false, name: S fix, entity_id: sensor.outside}
|
||||
- {platform: homeassistant, id: s_wiz, internal: false, name: S wiz}
|
||||
- {platform: homeassistant, id: sa_wiz, internal: false, name: SA wiz, attribute: temperature}
|
||||
|
||||
binary_sensor:
|
||||
- {platform: homeassistant, id: b_fix, internal: false, name: B fix, entity_id: binary_sensor.door}
|
||||
- {platform: homeassistant, id: b_wiz, internal: false, name: B wiz}
|
||||
|
||||
text_sensor:
|
||||
- {platform: homeassistant, id: t_fix, internal: false, name: T fix, entity_id: sensor.weather}
|
||||
- {platform: homeassistant, id: t_wiz, internal: false, name: T wiz}
|
||||
|
||||
number:
|
||||
- {platform: homeassistant, id: n_fix, internal: false, name: N fix, entity_id: input_number.target}
|
||||
- {platform: homeassistant, id: n_wiz, internal: false, name: N wiz}
|
||||
|
||||
switch:
|
||||
- {platform: homeassistant, id: sw_fix, internal: false, name: SW fix, entity_id: switch.porch}
|
||||
- {platform: homeassistant, id: sw_wiz, internal: false, name: SW wiz}
|
||||
|
||||
select:
|
||||
- {platform: homeassistant, id: sel_fix, internal: false, name: SEL fix, entity_id: input_select.mode}
|
||||
- {platform: homeassistant, id: sel_wiz, internal: false, name: SEL wiz}
|
||||
|
||||
text:
|
||||
- {platform: homeassistant, id: tx_fix, internal: false, name: TX fix, entity_id: input_text.note}
|
||||
- {platform: homeassistant, id: tx_wiz, internal: false, name: TX wiz}
|
||||
|
||||
light:
|
||||
- {platform: homeassistant, id: l_fix, internal: false, name: L fix, entity_id: light.kitchen}
|
||||
@@ -0,0 +1,9 @@
|
||||
# The wizard-linked light, which builds before #21908 cannot compile; run.sh leaves it out when NO_LIGHT is set.
|
||||
api:
|
||||
wizard:
|
||||
pages:
|
||||
- inputs:
|
||||
- entity: l_wiz
|
||||
|
||||
light:
|
||||
- {platform: homeassistant, id: l_wiz, internal: false, name: L wiz}
|
||||
@@ -0,0 +1,495 @@
|
||||
"""Drive every homeassistant platform over a plaintext native API connection, playing Home Assistant.
|
||||
|
||||
usage: hadrive.py HOST [--no-light] (exits 1 on any failed check)
|
||||
|
||||
Matches ha-all-common.yaml: one fixed entity_id entity and one wizard-linked entity per platform. The client
|
||||
subscribes to entity states, Home Assistant actions and Home Assistant state subscriptions, then:
|
||||
A sends Home Assistant states for the fixed entities (and for a wizard ID that is not set yet), commands every
|
||||
writer, and checks that the wizard-linked ones send nothing;
|
||||
B sets every wizard input (WizardInputSetRequest), records the subscriptions the device sends for it, sends
|
||||
states for the new IDs and commands the writers;
|
||||
C changes every wizard input to a second ID: old IDs must be ignored, new ones followed;
|
||||
D sends an invalid ID, which the device must reject.
|
||||
Prints canonical lines (SUB, STATE, ACTION, CHECK) so two builds can be diffed, and PASS or FAIL at the end.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
|
||||
sys.path.insert(
|
||||
0, str(pathlib.Path(__file__).resolve().parents[1] / "homeassistant_button")
|
||||
)
|
||||
from habdrive import ( # noqa: E402
|
||||
Client as BaseClient,
|
||||
f_bytes,
|
||||
f_fixed32,
|
||||
f_varint,
|
||||
fields,
|
||||
fnv1_hash,
|
||||
varint,
|
||||
)
|
||||
|
||||
|
||||
def f_float(num: int, value: float) -> bytes:
|
||||
return varint((num << 3) | 5) + struct.pack("<f", value)
|
||||
|
||||
|
||||
# ListEntities response type -> platform
|
||||
LIST_TYPES = {
|
||||
12: "binary_sensor",
|
||||
15: "light",
|
||||
16: "sensor",
|
||||
17: "switch",
|
||||
18: "text_sensor",
|
||||
49: "number",
|
||||
52: "select",
|
||||
61: "button",
|
||||
97: "text",
|
||||
}
|
||||
STATE_TYPES = {
|
||||
21: "bool",
|
||||
24: "bool",
|
||||
25: "float",
|
||||
26: "bool",
|
||||
27: "str",
|
||||
50: "float",
|
||||
53: "str",
|
||||
98: "str",
|
||||
}
|
||||
|
||||
|
||||
class Client(BaseClient):
|
||||
def __init__(self, host: str) -> None:
|
||||
super().__init__(host)
|
||||
self.keys: dict[str, int] = {}
|
||||
self.names: dict[int, str] = {}
|
||||
self.failures: list[str] = []
|
||||
self.last: dict[
|
||||
str, str
|
||||
] = {} # the last state the device published, by entity name
|
||||
|
||||
def login(
|
||||
self, name: str, connect_wait: float = 1, list_entities: bool = True
|
||||
) -> None:
|
||||
"""Hello (API 1.14) and Connect, then ListEntities into keys and names."""
|
||||
self.send(1, f_bytes(1, name) + f_varint(2, 1) + f_varint(3, 14))
|
||||
self.pump(10, until=2)
|
||||
self.send(3)
|
||||
self.pump(connect_wait)
|
||||
if not list_entities:
|
||||
return
|
||||
self.send(11)
|
||||
for msg_type, payload in self.pump(10, until=19):
|
||||
if msg_type in LIST_TYPES:
|
||||
d = dict(fields(payload))
|
||||
key, name = struct.unpack("<I", d[2])[0], d[3].decode()
|
||||
self.keys[name] = key
|
||||
self.names[key] = name
|
||||
|
||||
def report(
|
||||
self, frames: list[tuple[int, bytes]], label: str
|
||||
) -> dict[str, list[str]]:
|
||||
"""Print SUB, STATE and ACTION lines for frames and return them by kind."""
|
||||
out = self.parse(frames)
|
||||
for line in out["STATE"]:
|
||||
name, _, value = line.partition(" = ")
|
||||
self.last[name] = value
|
||||
for kind in ("SUB", "STATE", "ACTION"):
|
||||
for line in sorted(out[kind]) if kind == "SUB" else out[kind]:
|
||||
print(f" {label} {kind} {line}")
|
||||
return out
|
||||
|
||||
def collect(
|
||||
self,
|
||||
label: str,
|
||||
done: Callable[[dict[str, list[str]]], bool] | None = None,
|
||||
timeout: float = 6.0,
|
||||
settle: float = 0.5,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Read until done(parsed) holds (or for timeout without one), then settle more seconds for strays."""
|
||||
frames: list[tuple[int, bytes]] = []
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
frames += self.pump(0.2)
|
||||
if done is not None and done(self.parse(frames)):
|
||||
break
|
||||
frames += self.pump(settle)
|
||||
return self.report(frames, label)
|
||||
|
||||
def parse(self, frames: list[tuple[int, bytes]]) -> dict[str, list[str]]:
|
||||
out: dict[str, list[str]] = {"SUB": [], "STATE": [], "ACTION": []}
|
||||
for msg_type, payload in frames:
|
||||
f = fields(payload)
|
||||
d = dict(f)
|
||||
if msg_type == 39: # SubscribeHomeAssistantStateResponse
|
||||
attr = d.get(2, b"").decode() or "-"
|
||||
line = f"{d.get(1, b'').decode()} {attr}{' once' if d.get(3) else ''}"
|
||||
out["SUB"].append(line)
|
||||
elif msg_type in STATE_TYPES:
|
||||
key = struct.unpack("<I", d[1])[0]
|
||||
kind = STATE_TYPES[msg_type]
|
||||
if kind == "float":
|
||||
value = d.get(2)
|
||||
value = (
|
||||
f"{struct.unpack('<f', value)[0]:g}"
|
||||
if value is not None
|
||||
else "0"
|
||||
)
|
||||
elif kind == "bool":
|
||||
value = "ON" if d.get(2) else "OFF"
|
||||
else:
|
||||
value = repr(d.get(2, b"").decode())
|
||||
if msg_type not in (24, 26) and d.get(
|
||||
3
|
||||
): # field 3 of a light state is brightness
|
||||
value += " missing"
|
||||
out["STATE"].append(f"{self.names.get(key, key)} = {value}")
|
||||
elif msg_type == 35: # HomeassistantActionRequest
|
||||
service, data = "", []
|
||||
for num, value in f:
|
||||
if num == 1:
|
||||
service = value.decode()
|
||||
elif num in (2, 3):
|
||||
kv = dict(fields(value))
|
||||
data.append(
|
||||
f"{kv.get(1, b'').decode()}={kv.get(2, b'').decode()}"
|
||||
)
|
||||
out["ACTION"].append(f"{service} {' '.join(data)}")
|
||||
return out
|
||||
|
||||
def check(self, label: str, ok: bool, detail: str) -> None:
|
||||
print(f" CHECK {label}: {'ok' if ok else 'FAIL'} {detail}")
|
||||
if not ok:
|
||||
self.failures.append(f"{label}: {detail}")
|
||||
|
||||
def ha_state(self, entity_id: str, state: str, attribute: str = "") -> None:
|
||||
payload = f_bytes(1, entity_id) + f_bytes(2, state)
|
||||
if attribute:
|
||||
payload += f_bytes(3, attribute)
|
||||
self.send(40, payload)
|
||||
|
||||
def wizard(self, input_id: str, entity_id: str) -> None:
|
||||
self.send(158, f_fixed32(1, fnv1_hash(input_id)) + f_bytes(2, entity_id))
|
||||
|
||||
|
||||
def feed(c: Client, platform: str, eid: str, n: int) -> None:
|
||||
"""Send what Home Assistant sends for a platform; n varies the values between rounds."""
|
||||
if platform == "sensor":
|
||||
c.ha_state(eid, f"{20 + n}.5")
|
||||
elif platform == "sensor_attr":
|
||||
c.ha_state(eid, "heat") # the state itself must not reach an attribute sensor
|
||||
c.ha_state(eid, f"{30 + n}", "temperature")
|
||||
elif platform == "binary_sensor":
|
||||
c.ha_state(eid, "on" if n % 2 else "off")
|
||||
elif platform == "text_sensor":
|
||||
c.ha_state(eid, f"text{n}")
|
||||
elif platform == "number":
|
||||
c.ha_state(eid, "0", "min")
|
||||
c.ha_state(eid, "100", "max")
|
||||
c.ha_state(eid, "1", "step")
|
||||
c.ha_state(eid, f"{40 + n}")
|
||||
elif platform == "switch":
|
||||
c.ha_state(eid, "on" if n % 2 else "off")
|
||||
elif platform == "select":
|
||||
c.ha_state(eid, "['eco', 'comfort', 'away']", "options")
|
||||
c.ha_state(eid, ["eco", "comfort", "away"][n % 3])
|
||||
elif platform == "text":
|
||||
c.ha_state(eid, "0", "min")
|
||||
c.ha_state(eid, "100", "max")
|
||||
c.ha_state(eid, "text", "mode")
|
||||
c.ha_state(eid, f"note{n}")
|
||||
elif platform == "light":
|
||||
c.ha_state(eid, "['onoff']", "supported_color_modes")
|
||||
c.ha_state(eid, "onoff", "color_mode")
|
||||
c.ha_state(eid, "on" if n % 2 else "off")
|
||||
|
||||
|
||||
def expected_state(platform: str, n: int) -> str | None:
|
||||
return {
|
||||
"sensor": f"{20 + n}.5",
|
||||
"sensor_attr": f"{30 + n}",
|
||||
"binary_sensor": "ON" if n % 2 else "OFF",
|
||||
"text_sensor": repr(f"text{n}"),
|
||||
"number": f"{40 + n}",
|
||||
"switch": "ON" if n % 2 else "OFF",
|
||||
"select": repr(["eco", "comfort", "away"][n % 3]),
|
||||
"text": repr(f"note{n}"),
|
||||
"light": "ON" if n % 2 else "OFF",
|
||||
}.get(platform)
|
||||
|
||||
|
||||
class Row(NamedTuple):
|
||||
fix: str | None # name of the fixed entity_id entity
|
||||
fix_id: str | None
|
||||
wiz: str | None # name of the wizard-linked entity
|
||||
input_id: str | None # ESPHome id of its wizard input
|
||||
platform: str
|
||||
one: str | None # the first and second entity IDs the wizard input is set to
|
||||
two: str | None
|
||||
|
||||
|
||||
# fmt: off
|
||||
PLATFORMS = [
|
||||
Row("S fix", "sensor.outside", "S wiz", "s_wiz", "sensor", "sensor.wiz_one", "sensor.wiz_two"),
|
||||
Row(None, None, "SA wiz", "sa_wiz", "sensor_attr", "climate.wiz_one", "climate.wiz_two"),
|
||||
Row("B fix", "binary_sensor.door", "B wiz", "b_wiz", "binary_sensor", "binary_sensor.wiz_one",
|
||||
"binary_sensor.wiz_two"),
|
||||
Row("T fix", "sensor.weather", "T wiz", "t_wiz", "text_sensor", "sensor.wiz_text_one", "sensor.wiz_text_two"),
|
||||
Row("N fix", "input_number.target", "N wiz", "n_wiz", "number", "input_number.wiz_one", "number.wiz_two"),
|
||||
Row("SW fix", "switch.porch", "SW wiz", "sw_wiz", "switch", "input_boolean.wiz_one", "switch.wiz_two"),
|
||||
Row("SEL fix", "input_select.mode", "SEL wiz", "sel_wiz", "select", "input_select.wiz_one", "select.wiz_two"),
|
||||
Row("TX fix", "input_text.note", "TX wiz", "tx_wiz", "text", "input_text.wiz_one", "text.wiz_two"),
|
||||
Row("L fix", "light.kitchen", "L wiz", "l_wiz", "light", "light.wiz_one", "light.wiz_two"),
|
||||
]
|
||||
# fmt: on
|
||||
# the fixed buttons come from ha-button-common.yaml
|
||||
WIZARD_BUTTON = Row(
|
||||
None,
|
||||
None,
|
||||
"HA wizard",
|
||||
"hab_wiz",
|
||||
"button",
|
||||
"input_button.wiz_one",
|
||||
"button.wiz_two",
|
||||
)
|
||||
|
||||
# The subscriptions each platform sends for one entity ID
|
||||
SUBS = {
|
||||
"button": [],
|
||||
"sensor": ["{} -"],
|
||||
"sensor_attr": ["{} temperature"],
|
||||
"binary_sensor": ["{} -"],
|
||||
"text_sensor": ["{} -"],
|
||||
"number": ["{} -", "{} max once", "{} min once", "{} step once"],
|
||||
"switch": ["{} -"],
|
||||
"select": ["{} -", "{} options"],
|
||||
"text": ["{} -", "{} max once", "{} min once", "{} mode once"],
|
||||
"light": [
|
||||
"{} -",
|
||||
"{} brightness",
|
||||
"{} color_mode",
|
||||
"{} color_temp_kelvin",
|
||||
"{} max_color_temp_kelvin",
|
||||
"{} min_color_temp_kelvin",
|
||||
"{} rgb_color",
|
||||
"{} supported_color_modes",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def command(c: Client, name: str, platform: str, n: int) -> str | None:
|
||||
"""Command a writer entity; returns the action it must send for entity ID {}, or None for a reader."""
|
||||
key = c.keys[name]
|
||||
if platform == "number":
|
||||
c.send(51, f_fixed32(1, key) + f_float(2, 10 + n))
|
||||
return "{d}.set_value entity_id={e} value=" + f"{10 + n}"
|
||||
if platform == "switch":
|
||||
state = n % 2 == 0 # the opposite of what feed() reported
|
||||
c.send(33, f_fixed32(1, key) + f_varint(2, int(state)))
|
||||
return f"homeassistant.turn_{'on' if state else 'off'} entity_id={{e}}"
|
||||
if platform == "select":
|
||||
option = ["eco", "comfort", "away"][(n + 1) % 3]
|
||||
c.send(54, f_fixed32(1, key) + f_bytes(2, option))
|
||||
return "{d}.select_option entity_id={e} option=" + option
|
||||
if platform == "text":
|
||||
c.send(99, f_fixed32(1, key) + f_bytes(2, f"cmd{n}"))
|
||||
return "{d}.set_value entity_id={e} value=" + f"cmd{n}"
|
||||
if platform == "light":
|
||||
state = n % 2 == 0
|
||||
c.send(32, f_fixed32(1, key) + f_varint(2, 1) + f_varint(3, int(state)))
|
||||
return f"light.turn_{'on' if state else 'off'} entity_id={{e}}"
|
||||
if platform == "button":
|
||||
c.send(62, f_fixed32(1, key))
|
||||
return "{d}.press entity_id={e}"
|
||||
return None
|
||||
|
||||
|
||||
def action_for(template: str, eid: str) -> str:
|
||||
# number, select, text and button call the action of the entity's own domain (input_number.set_value, ...)
|
||||
if template.startswith("{d}"):
|
||||
return template.format(d=eid.split(".", maxsplit=1)[0], e=eid)
|
||||
return template.format(e=eid)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("host")
|
||||
parser.add_argument(
|
||||
"--no-light", action="store_true", help="the config has no wizard light"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
c = Client(args.host)
|
||||
c.login("hadrive", connect_wait=2)
|
||||
print("entities", len(c.keys), sorted(c.keys))
|
||||
# only the wizard light may be left out (builds before #21908 cannot compile it)
|
||||
rows: list[Row] = []
|
||||
for row in [*PLATFORMS, WIZARD_BUTTON]:
|
||||
if row.wiz and row.wiz not in c.keys:
|
||||
c.check(
|
||||
f"{row.wiz} listed", args.no_light and row.wiz == "L wiz", "missing"
|
||||
)
|
||||
row = row._replace(wiz=None, input_id=None)
|
||||
if row.fix or row.wiz:
|
||||
rows.append(row)
|
||||
wizard = [row for row in rows if row.wiz]
|
||||
c.send(20) # SubscribeStatesRequest
|
||||
c.report(c.pump(3), "INIT")
|
||||
c.send(34) # SubscribeHomeassistantServicesRequest
|
||||
c.pump(1)
|
||||
c.send(38) # SubscribeHomeAssistantStatesRequest
|
||||
expected = sorted(
|
||||
s.format(r.fix_id) for r in rows if r.fix for s in SUBS[r.platform]
|
||||
)
|
||||
subs = c.collect("A", lambda o: len(o["SUB"]) >= len(expected), 10, 2)["SUB"]
|
||||
c.check(
|
||||
"A subscriptions are the fixed entities only",
|
||||
sorted(subs) == expected,
|
||||
f"{len(subs)} subs",
|
||||
)
|
||||
|
||||
def drive(
|
||||
label: str, entries: list[tuple[str, str | None, str]], n: int, stale: list[str]
|
||||
) -> None:
|
||||
"""entries: (name, entity ID or None if unset, platform). stale: IDs the device must ignore."""
|
||||
for eid in stale:
|
||||
c.ha_state(eid, "999")
|
||||
c.ha_state(eid, "999", "temperature")
|
||||
prev = dict(c.last)
|
||||
for _, eid, platform in entries:
|
||||
if eid:
|
||||
feed(c, platform, eid, n)
|
||||
wants = {
|
||||
f"{name} = {expected_state(p, n)}"
|
||||
for name, eid, p in entries
|
||||
if eid and expected_state(p, n)
|
||||
}
|
||||
got = c.collect(f"{label}-feed", lambda o: wants <= set(o["STATE"]), 8, 1.5)
|
||||
states = got["STATE"]
|
||||
for name, eid, platform in entries:
|
||||
want = expected_state(platform, n)
|
||||
if want is None:
|
||||
continue
|
||||
mine = [s for s in states if s.startswith(f"{name} = ")]
|
||||
if eid:
|
||||
# A light publishes only a change, so a report of the state it already has publishes nothing
|
||||
same = platform == "light" and not mine and prev.get(name) == want
|
||||
c.check(
|
||||
f"{label} {name} follows {eid}",
|
||||
f"{name} = {want}" in mine or same,
|
||||
str(mine),
|
||||
)
|
||||
else:
|
||||
c.check(f"{label} {name} unset publishes nothing", not mine, str(mine))
|
||||
c.check(
|
||||
f"{label} stale IDs ignored",
|
||||
not any("999" in s or "= 'heat'" in s for s in states),
|
||||
"",
|
||||
)
|
||||
for name, eid, platform in entries:
|
||||
if platform in ("sensor", "sensor_attr", "binary_sensor", "text_sensor"):
|
||||
continue
|
||||
template = command(c, name, platform, n)
|
||||
if eid is None:
|
||||
got = c.collect(f"{label}-cmd {name}", None, 2)
|
||||
else:
|
||||
got = c.collect(f"{label}-cmd {name}", lambda o: bool(o["ACTION"]), 6)
|
||||
actions = got["ACTION"]
|
||||
if eid is None:
|
||||
c.check(
|
||||
f"{label} {name} unset sends nothing", not actions, str(actions)
|
||||
)
|
||||
continue
|
||||
want = action_for(template, eid)
|
||||
c.check(
|
||||
f"{label} {name} action", actions == [want], f"{actions} want {want}"
|
||||
)
|
||||
if platform == "light":
|
||||
c.ha_state(
|
||||
eid, "on" if n % 2 == 0 else "off"
|
||||
) # Home Assistant echoes the change
|
||||
c.collect(f"{label}-echo {name}", None, 1.5)
|
||||
|
||||
fixed = [(r.fix, r.fix_id, r.platform) for r in rows if r.fix]
|
||||
fixed += [
|
||||
("HA button", "button.doorbell", "button"),
|
||||
("HA input", "input_button.chime", "button"),
|
||||
]
|
||||
wiz = [(r.wiz, None, r.platform) for r in wizard]
|
||||
print("== A: fixed entities, wizard inputs not set")
|
||||
drive("A", fixed + wiz, 1, ["sensor.wiz_one", "climate.wiz_one"])
|
||||
|
||||
def set_all(label: str, idx: int) -> dict[str, str]:
|
||||
ids = {}
|
||||
for row in wizard:
|
||||
name = row.wiz
|
||||
eid = row.one if idx == 1 else row.two
|
||||
ids[name] = eid
|
||||
c.wizard(row.input_id, eid)
|
||||
want = sorted(s.format(eid) for s in SUBS[row.platform])
|
||||
n_want = len(want)
|
||||
if want:
|
||||
subs = c.collect(
|
||||
f"{label}-set {name}",
|
||||
lambda o, n=n_want: len(o["SUB"]) >= n,
|
||||
6,
|
||||
0.5,
|
||||
)["SUB"]
|
||||
else:
|
||||
subs = c.collect(f"{label}-set {name}", None, 2)["SUB"]
|
||||
c.check(
|
||||
f"{label} {name} subscribes to {eid}",
|
||||
sorted(subs) == want,
|
||||
f"{sorted(subs)}",
|
||||
)
|
||||
return ids
|
||||
|
||||
print("== B: wizard inputs set to the first IDs")
|
||||
ids = set_all("B", 1)
|
||||
drive("B", [(name, ids[name], p) for name, _, p in wiz], 2, [])
|
||||
print("== C: wizard inputs changed to the second IDs")
|
||||
old = [row.one for row in wizard]
|
||||
ids = set_all("C", 2)
|
||||
drive("C", [(name, ids[name], p) for name, _, p in wiz], 3, old)
|
||||
print("== D: invalid ID rejected")
|
||||
for input_id in ("n_wiz", "hab_wiz", "s_wiz"):
|
||||
c.wizard(input_id, "not_an_entity")
|
||||
subs = c.collect(f"D-set {input_id}", None, 2)["SUB"]
|
||||
c.check(f"D {input_id} invalid set sends no subscription", not subs, str(subs))
|
||||
drive(
|
||||
"D",
|
||||
[
|
||||
(name, ids[name], p)
|
||||
for name, _, p in wiz
|
||||
if name in ("N wiz", "HA wizard", "S wiz")
|
||||
],
|
||||
4,
|
||||
[],
|
||||
)
|
||||
print("== E: a new connection gets every subscription with the current IDs")
|
||||
c.sock.close()
|
||||
c2 = Client(parser.parse_args().host)
|
||||
c2.login("hadrive2", list_entities=False)
|
||||
c2.send(38)
|
||||
expected = sorted(
|
||||
[s.format(r.fix_id) for r in rows if r.fix for s in SUBS[r.platform]]
|
||||
+ [s.format(ids[r.wiz]) for r in wizard for s in SUBS[r.platform]]
|
||||
)
|
||||
subs = c2.collect("E", lambda o: len(o["SUB"]) >= len(expected), 10, 2)["SUB"]
|
||||
c.check(
|
||||
"E subscriptions on reconnect",
|
||||
sorted(subs) == expected,
|
||||
f"{len(subs)} subs, want {len(expected)}",
|
||||
)
|
||||
if c.failures:
|
||||
print("FAIL: " + "; ".join(c.failures))
|
||||
sys.exit(1)
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
"""After hadrive.py: the wizard light must follow its current entity ID (light.wiz_two) and ignore the old one.
|
||||
|
||||
usage: lightcheck.py HOST
|
||||
Sends on and off states for both IDs and prints what the device publishes after each; only light.wiz_two may move
|
||||
"L wiz". A light publishes only changes, so a repeated state shows nothing.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from hadrive import Client
|
||||
|
||||
|
||||
def main() -> None:
|
||||
c = Client(sys.argv[1])
|
||||
c.login("lightcheck")
|
||||
c.send(20) # SubscribeStatesRequest
|
||||
c.collect("init", None, 3)
|
||||
c.send(38) # SubscribeHomeAssistantStatesRequest
|
||||
c.collect("subs", None, 4)
|
||||
for eid, st in (
|
||||
("light.wiz_one", "on"),
|
||||
("light.wiz_two", "on"),
|
||||
("light.wiz_one", "off"),
|
||||
("light.wiz_two", "off"),
|
||||
):
|
||||
print(f"send {eid} = {st}")
|
||||
c.ha_state(eid, st)
|
||||
c.collect(" ->", None, 2.5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# run.sh LABEL: build ha-all-common.yaml from TREE as hawiz<LABEL> for a D1 mini, flash, take three idle boots of heap
|
||||
# readings (ramchk lines), then drive one boot with hadrive.py and print its result and the heap after the client.
|
||||
# NO_LIGHT=1 drops the wizard-linked light, which builds before #21908 cannot compile.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; N=hawiz$L
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
OUT=$R/out-$N; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
LIGHT=' light: !include ha-wiz-light.yaml\n'; NL=(); [ -n "$NO_LIGHT" ] && { LIGHT=; NL=(--no-light); }
|
||||
printf "esphome:\n name: %s\n\nesp8266:\n board: d1_mini\n\npackages:\n common: !include ha-all-common.yaml\n$LIGHT" "$N" \
|
||||
> "$R/$N.yaml"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$OUT/upload.log"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
for b in 1 2 3; do
|
||||
$PY "$SC" "$P" 45 "$OUT/boot$b.txt" --reset > /dev/null 2>&1
|
||||
echo "boot$b $(grep -a 'ramchk' "$OUT/boot$b.txt" | tail -1 | sed -E 's/.*(heap_size)/\1/')"
|
||||
done
|
||||
$PY "$SC" "$P" 150 "$OUT/serial.txt" --reset > /dev/null 2>&1 & CAP=$!
|
||||
for _ in $(seq 1 30); do
|
||||
IP=$(grep -a -o "IP Address: [0-9.]*" "$OUT/serial.txt" 2>/dev/null | tail -1 | cut -d' ' -f3)
|
||||
[ -n "$IP" ] && break; sleep 2
|
||||
done
|
||||
[ -n "$IP" ] || { echo "no IP in the capture"; kill $CAP; exit 1; }
|
||||
sleep 8
|
||||
$PY "$R/hadrive.py" "$IP" "${NL[@]}" > "$OUT/drive.txt" 2>&1; RC=$?
|
||||
sleep 6; kill $CAP
|
||||
echo "drive rc=$RC $(tail -1 "$OUT/drive.txt" | cut -c1-300)"
|
||||
echo "after-client $(grep -a 'ramchk' "$OUT/serial.txt" | tail -1 | sed -E 's/.*(heap_size)/\1/')"
|
||||
echo "crash lines: $(grep -a -c -E 'rst cause|Exception|Soft WDT' "$OUT/serial.txt")"
|
||||
exit $RC
|
||||
@@ -0,0 +1,16 @@
|
||||
# One run: p_fault is per mille of transactions, seed fixes the fault sequence (seed 1 hangs dev at about 245 s on #22270)
|
||||
substitutions:
|
||||
name: i2cfault-pr
|
||||
freq: 10kHz
|
||||
hammer: 100ms
|
||||
ina_interval: 1s
|
||||
p_fault: "50"
|
||||
fault_mask: "0xFF"
|
||||
hold_max_ms: "3000"
|
||||
diag_recover: "false"
|
||||
seed: "1"
|
||||
fw: esp-idf
|
||||
fwver: 5.5.5
|
||||
|
||||
packages:
|
||||
base: !include i2cbase.yaml
|
||||
@@ -0,0 +1,132 @@
|
||||
# Package: reporter-shaped I2C bus + INA219 on GPIO32/33, answered by the in-chip software INA219 (inasim.hpp)
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- inasim.hpp
|
||||
on_boot:
|
||||
- priority: 700
|
||||
then:
|
||||
- lambda: |-
|
||||
if (${seed} != 0) inasim::rng = ${seed};
|
||||
inasim::install();
|
||||
uint8_t w[3] = {0x05, 0x12, 0x34};
|
||||
uint8_t r = 0x05;
|
||||
for (int pass = 1; pass <= 2; pass++) {
|
||||
uint8_t b[2] = {0, 0};
|
||||
auto ew = id(bus_a)->write_readv(0x40, w, 3, nullptr, 0);
|
||||
auto er = id(bus_a)->write_readv(0x40, &r, 1, b, 2);
|
||||
ESP_LOGI("inasim", "selftest%d write=%d read=%d val=%02X%02X starts=%u", pass, ew, er, b[0], b[1],
|
||||
(unsigned) inasim::st.starts);
|
||||
}
|
||||
|
||||
esp32:
|
||||
board: pico32
|
||||
framework:
|
||||
type: ${fw}
|
||||
version: ${fwver}
|
||||
|
||||
logger:
|
||||
level: INFO
|
||||
baud_rate: 115200
|
||||
|
||||
i2c:
|
||||
id: bus_a
|
||||
sda: GPIO32
|
||||
scl: GPIO33
|
||||
frequency: ${freq}
|
||||
scan: true
|
||||
|
||||
globals:
|
||||
- id: n_ok
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_to
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_nack
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_other
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_bad
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: n_ina_bad
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: max_us
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: streak
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: max_streak
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
|
||||
interval:
|
||||
- interval: ${hammer}
|
||||
then:
|
||||
- lambda: |-
|
||||
if (millis() < 20000) return;
|
||||
if (!inasim::armed) {
|
||||
inasim::p_fault = ${p_fault};
|
||||
inasim::fault_mask = ${fault_mask};
|
||||
inasim::hold_max_ms = ${hold_max_ms};
|
||||
// The ISR picks faults until one is in the mask, so an empty mask would spin there forever
|
||||
if (inasim::p_fault && !(inasim::fault_mask & ((1u << inasim::F_COUNT) - 1))) {
|
||||
ESP_LOGE("inasim", "fault_mask 0x%x selects no fault, running without faults", (unsigned) inasim::fault_mask);
|
||||
inasim::p_fault = 0;
|
||||
}
|
||||
inasim::armed = true;
|
||||
ESP_LOGI("inasim", "faults armed p=%u/1000 mask=0x%x", (unsigned) inasim::p_fault, (unsigned) inasim::fault_mask);
|
||||
}
|
||||
inasim::hb = millis();
|
||||
inasim::diag_recover = ${diag_recover};
|
||||
inasim::drain();
|
||||
uint8_t r = 0x02, buf[2] = {0, 0};
|
||||
uint32_t t0 = micros();
|
||||
auto e = id(bus_a)->write_readv(0x40, &r, 1, buf, 2);
|
||||
uint32_t dt = micros() - t0;
|
||||
if (dt > id(max_us)) id(max_us) = dt;
|
||||
if (e == i2c::ERROR_OK) {
|
||||
id(streak) = 0;
|
||||
id(n_ok)++;
|
||||
if (((buf[0] << 8) | buf[1]) != ((3410 << 3) | 2)) id(n_bad)++;
|
||||
} else {
|
||||
id(streak)++;
|
||||
if (id(streak) > id(max_streak)) id(max_streak) = id(streak);
|
||||
if (e == i2c::ERROR_TIMEOUT) id(n_to)++;
|
||||
else if (e == i2c::ERROR_NOT_ACKNOWLEDGED) id(n_nack)++;
|
||||
else id(n_other)++;
|
||||
}
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("inasim", "MASTER up=%us ok=%u timeout=%u nack=%u other=%u badval=%u ina_bad=%u max_us=%u heap=%u fail_streak_now=%u max_fail_streak=%u",
|
||||
(unsigned) (millis() / 1000), (unsigned) id(n_ok), (unsigned) id(n_to), (unsigned) id(n_nack),
|
||||
(unsigned) id(n_other), (unsigned) id(n_bad), (unsigned) id(n_ina_bad), (unsigned) id(max_us),
|
||||
(unsigned) esp_get_free_heap_size(), (unsigned) id(streak), (unsigned) id(max_streak));
|
||||
id(max_us) = 0;
|
||||
inasim::log_stats();
|
||||
|
||||
sensor:
|
||||
- platform: ina219
|
||||
address: 0x40
|
||||
shunt_resistance: 0.1 ohm
|
||||
max_voltage: 32.0V
|
||||
max_current: 3.2A
|
||||
update_interval: ${ina_interval}
|
||||
bus_voltage:
|
||||
name: Starter battery
|
||||
accuracy_decimals: 2
|
||||
on_value:
|
||||
- lambda: |-
|
||||
if (fabsf(x - 13.64f) > 0.005f) {
|
||||
id(n_ina_bad)++;
|
||||
ESP_LOGW("inasim", "INA bus voltage garbage %.2f V", x);
|
||||
}
|
||||
current:
|
||||
name: Charge current
|
||||
accuracy_decimals: 2
|
||||
@@ -0,0 +1,435 @@
|
||||
#pragma once
|
||||
// Software INA219 at 0x40 answered on the master's own pads (GPIO32 SDA, GPIO33 SCL) plus fault injection.
|
||||
// The slave drives a line low by switching the pad's output mux from the I2C0 signal to the plain GPIO output
|
||||
// (level 0), which is a wired AND with the master. It runs inside a GPIO ISR on core 0 that spins through one
|
||||
// transaction. Long stuck lines are held by a task on core 0 that re-asserts every tick.
|
||||
#include <cstdint>
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soc/gpio_reg.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "esp_cpu.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
namespace inasim {
|
||||
static const char *const TAG = "inasim";
|
||||
constexpr int SDA = 32, SCL = 33;
|
||||
constexpr uint32_t SDA_BIT = 1u << (SDA - 32), SCL_BIT = 1u << (SCL - 32);
|
||||
constexpr uint32_t LOW_VAL = SIG_GPIO_OUT_IDX | GPIO_FUNC0_OEN_SEL;
|
||||
constexpr uint32_t CPU_MHZ = 240;
|
||||
|
||||
inline volatile uint32_t ®(uint32_t a) { return *reinterpret_cast<volatile uint32_t *>(a); }
|
||||
inline uint32_t out_sel_addr(int pin) { return GPIO_FUNC0_OUT_SEL_CFG_REG + 4 * pin; }
|
||||
inline uint32_t in() { return reg(GPIO_IN1_REG); }
|
||||
|
||||
enum Fault : uint8_t {
|
||||
F_GARBAGE = 0, // flip a bit in returned data
|
||||
F_NACK, // do not ack the address
|
||||
F_STRETCH, // hold SCL low 0.2..20 ms inside the transaction, then continue
|
||||
F_HOLD_SCL, // SCL stuck low 50..3000 ms (task)
|
||||
F_HOLD_SDA, // SDA stuck low 50..3000 ms (task)
|
||||
F_GLITCH_SCL, // 1..4 us low pulse on SCL while it is high
|
||||
F_GLITCH_SDA, // 1..4 us low pulse on SDA while SCL is high (false START/STOP)
|
||||
F_VANISH, // slave stops responding mid transaction
|
||||
F_COUNT
|
||||
};
|
||||
|
||||
struct Stats {
|
||||
volatile uint32_t starts, matched, stops, idle_abort, cap_abort, rd_bytes, wr_bytes;
|
||||
volatile uint32_t fault[F_COUNT];
|
||||
};
|
||||
inline Stats st{};
|
||||
inline uint32_t rel_sda = 0, rel_scl = 0;
|
||||
inline volatile bool armed = false;
|
||||
inline volatile uint32_t p_fault = 0; // per mille of matched transactions
|
||||
inline volatile uint32_t fault_mask = 0xFF; // which faults may be chosen
|
||||
inline volatile uint32_t hold_max_ms = 3000;
|
||||
inline uint16_t regs[6] = {0x399F, 0x044C, (3410 << 3) | 2, 0, 0, 0};
|
||||
inline uint8_t ptr = 0;
|
||||
inline uint32_t rng = 0x2270A5C3;
|
||||
inline uint32_t rnd() {
|
||||
rng ^= rng << 13;
|
||||
rng ^= rng >> 17;
|
||||
rng ^= rng << 5;
|
||||
return rng;
|
||||
}
|
||||
struct FaultEv {
|
||||
uint32_t ms;
|
||||
uint8_t type;
|
||||
uint32_t arg;
|
||||
};
|
||||
inline FaultEv ring[64];
|
||||
inline volatile uint32_t ring_w = 0;
|
||||
inline uint32_t ring_r = 0;
|
||||
inline void note(uint8_t type, uint32_t arg) {
|
||||
FaultEv &e = ring[ring_w & 63];
|
||||
e.ms = (uint32_t) (esp_timer_get_time() / 1000);
|
||||
e.type = type;
|
||||
e.arg = arg;
|
||||
ring_w = ring_w + 1;
|
||||
}
|
||||
inline TaskHandle_t holder = nullptr;
|
||||
inline volatile uint32_t hold_mask = 0, hold_ms = 0;
|
||||
inline volatile bool holding = false;
|
||||
|
||||
inline void drive_low(uint32_t bits) {
|
||||
reg(GPIO_OUT1_W1TC_REG) = bits;
|
||||
reg(GPIO_ENABLE1_W1TS_REG) = bits;
|
||||
if (bits & SDA_BIT)
|
||||
reg(out_sel_addr(SDA)) = LOW_VAL;
|
||||
if (bits & SCL_BIT)
|
||||
reg(out_sel_addr(SCL)) = LOW_VAL;
|
||||
}
|
||||
inline void release(uint32_t bits) {
|
||||
if (bits & SDA_BIT)
|
||||
reg(out_sel_addr(SDA)) = rel_sda;
|
||||
if (bits & SCL_BIT)
|
||||
reg(out_sel_addr(SCL)) = rel_scl;
|
||||
}
|
||||
inline void spin_us(uint32_t us) {
|
||||
uint32_t s = esp_cpu_get_cycle_count();
|
||||
while (esp_cpu_get_cycle_count() - s < us * CPU_MHZ) {
|
||||
}
|
||||
}
|
||||
|
||||
inline void update_derived() {
|
||||
// current = shunt * cal / 4096, power = current * bus / 5000 (datasheet scaling)
|
||||
int32_t shunt = (int16_t) regs[1];
|
||||
uint32_t cal = regs[5];
|
||||
int32_t cur = (int32_t) ((int64_t) shunt * cal / 4096);
|
||||
regs[4] = (uint16_t) cur;
|
||||
regs[3] = (uint16_t) ((int64_t) (cur < 0 ? -cur : cur) * (regs[2] >> 3) / 5000);
|
||||
}
|
||||
|
||||
inline void start_hold(uint32_t bits, uint32_t ms, BaseType_t *woken) {
|
||||
hold_mask = bits;
|
||||
hold_ms = ms;
|
||||
holding = true;
|
||||
drive_low(bits);
|
||||
vTaskNotifyGiveFromISR(holder, woken);
|
||||
}
|
||||
|
||||
// Spins through one transaction from a START. Returns when STOP, idle or cap hits.
|
||||
inline void run_transaction(BaseType_t *woken) {
|
||||
enum { ADDR, WDATA, RDATA, IGNORE } phase = ADDR;
|
||||
uint32_t prev = in();
|
||||
uint32_t start = esp_cpu_get_cycle_count(), last = start;
|
||||
uint8_t byte = 0, cnt = 0, out = 0, rbyte_idx = 0, wbyte_idx = 0, wmsb = 0;
|
||||
bool acked = false, mack = false, sda_driven = false;
|
||||
int fault = -1;
|
||||
uint32_t fault_at = 0, rises = 0;
|
||||
bool garbage_done = false;
|
||||
auto set_sda = [&](bool low) {
|
||||
if (low && !sda_driven) {
|
||||
drive_low(SDA_BIT);
|
||||
sda_driven = true;
|
||||
} else if (!low && sda_driven) {
|
||||
release(SDA_BIT);
|
||||
sda_driven = false;
|
||||
}
|
||||
};
|
||||
auto load_byte = [&]() {
|
||||
uint16_t v = ptr < 6 ? regs[ptr] : 0xFFFF;
|
||||
out = rbyte_idx == 0 ? (v >> 8) : (rbyte_idx == 1 ? (v & 0xFF) : 0xFF);
|
||||
if (fault == F_GARBAGE && !garbage_done) {
|
||||
out ^= 1u << (rnd() & 7);
|
||||
note(F_GARBAGE, out);
|
||||
garbage_done = true;
|
||||
}
|
||||
rbyte_idx++;
|
||||
};
|
||||
for (;;) {
|
||||
uint32_t v = in();
|
||||
uint32_t now = esp_cpu_get_cycle_count();
|
||||
if (v == prev) {
|
||||
if (now - last > 15000u * CPU_MHZ) {
|
||||
st.idle_abort++;
|
||||
break;
|
||||
}
|
||||
if (now - start > 60000u * CPU_MHZ) {
|
||||
st.cap_abort++;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
last = now;
|
||||
uint32_t ch = v ^ prev;
|
||||
prev = v;
|
||||
if (ch & SCL_BIT) {
|
||||
if (v & SCL_BIT) {
|
||||
// rising edge: sample
|
||||
cnt++;
|
||||
rises++;
|
||||
if (cnt <= 8 && (phase == ADDR || phase == WDATA))
|
||||
byte = (byte << 1) | ((v & SDA_BIT) ? 1 : 0);
|
||||
else if (cnt == 9 && phase == RDATA)
|
||||
mack = !(v & SDA_BIT);
|
||||
if (fault >= 0 && rises == fault_at) {
|
||||
if (fault == F_GLITCH_SCL || fault == F_GLITCH_SDA) {
|
||||
spin_us(2);
|
||||
uint32_t b = fault == F_GLITCH_SCL ? SCL_BIT : SDA_BIT;
|
||||
bool was = b == SDA_BIT && sda_driven;
|
||||
if (!was) {
|
||||
note(fault, rises);
|
||||
drive_low(b);
|
||||
spin_us(1 + (rnd() & 3));
|
||||
release(b);
|
||||
}
|
||||
fault = -1;
|
||||
prev = in();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// falling edge: drive
|
||||
if (fault >= 0 && rises == fault_at) {
|
||||
if (fault == F_STRETCH) {
|
||||
drive_low(SCL_BIT);
|
||||
uint32_t us = 200 + rnd() % 19800;
|
||||
note(F_STRETCH, us);
|
||||
spin_us(us);
|
||||
release(SCL_BIT);
|
||||
fault = -1;
|
||||
prev = in();
|
||||
last = esp_cpu_get_cycle_count();
|
||||
} else if (fault == F_HOLD_SCL) {
|
||||
set_sda(false);
|
||||
uint32_t ms = 50 + rnd() % hold_max_ms;
|
||||
note(F_HOLD_SCL, ms);
|
||||
start_hold(SCL_BIT, ms, woken);
|
||||
return;
|
||||
} else if (fault == F_HOLD_SDA) {
|
||||
drive_low(SDA_BIT);
|
||||
sda_driven = false;
|
||||
uint32_t ms = 50 + rnd() % hold_max_ms;
|
||||
note(F_HOLD_SDA, ms);
|
||||
start_hold(SDA_BIT, ms, woken);
|
||||
return;
|
||||
} else if (fault == F_VANISH) {
|
||||
note(F_VANISH, rises);
|
||||
set_sda(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cnt == 8) {
|
||||
if (phase == ADDR) {
|
||||
bool match = (byte >> 1) == 0x40;
|
||||
if (match) {
|
||||
st.matched++;
|
||||
if (armed && p_fault && (rnd() % 1000) < p_fault) {
|
||||
uint32_t f;
|
||||
do {
|
||||
f = rnd() % F_COUNT;
|
||||
} while (!(fault_mask & (1u << f)));
|
||||
fault = f;
|
||||
fault_at = rises + 1 + rnd() % 30;
|
||||
st.fault[f]++;
|
||||
}
|
||||
}
|
||||
if (match && fault == F_NACK)
|
||||
note(F_NACK, 0);
|
||||
if (match && fault != F_NACK) {
|
||||
set_sda(true);
|
||||
acked = true;
|
||||
phase = (byte & 1) ? RDATA : WDATA;
|
||||
rbyte_idx = 0;
|
||||
wbyte_idx = 0;
|
||||
} else {
|
||||
phase = IGNORE;
|
||||
acked = false;
|
||||
}
|
||||
} else if (phase == WDATA) {
|
||||
st.wr_bytes++;
|
||||
if (wbyte_idx == 0)
|
||||
ptr = byte;
|
||||
else if (wbyte_idx == 1)
|
||||
wmsb = byte;
|
||||
else if (wbyte_idx == 2 && ptr < 6) {
|
||||
uint16_t w = (wmsb << 8) | byte;
|
||||
if (ptr == 0 && (w & 0x8000))
|
||||
w = 0x399F;
|
||||
if (ptr == 0 || ptr == 5)
|
||||
regs[ptr] = w;
|
||||
update_derived();
|
||||
}
|
||||
wbyte_idx++;
|
||||
set_sda(true);
|
||||
acked = true;
|
||||
} else if (phase == RDATA) {
|
||||
set_sda(false); // master acks
|
||||
acked = false;
|
||||
}
|
||||
} else if (cnt == 9) {
|
||||
if (acked) {
|
||||
set_sda(false);
|
||||
acked = false;
|
||||
}
|
||||
if (phase == RDATA) {
|
||||
if (rbyte_idx == 0 || mack) {
|
||||
load_byte();
|
||||
st.rd_bytes++;
|
||||
set_sda(!(out & 0x80));
|
||||
} else {
|
||||
phase = IGNORE;
|
||||
}
|
||||
}
|
||||
cnt = 0;
|
||||
byte = 0;
|
||||
} else if (cnt >= 1 && cnt <= 7 && phase == RDATA) {
|
||||
set_sda(!(out & (0x80 >> cnt)));
|
||||
}
|
||||
}
|
||||
} else if ((ch & SDA_BIT) && (v & SCL_BIT)) {
|
||||
if (v & SDA_BIT) {
|
||||
st.stops++;
|
||||
break;
|
||||
}
|
||||
// repeated START
|
||||
st.starts++;
|
||||
set_sda(false);
|
||||
phase = ADDR;
|
||||
cnt = 0;
|
||||
byte = 0;
|
||||
acked = false;
|
||||
}
|
||||
}
|
||||
set_sda(false);
|
||||
}
|
||||
|
||||
inline void slave_isr(void *) {
|
||||
reg(GPIO_STATUS1_W1TC_REG) = SDA_BIT;
|
||||
if (holding)
|
||||
return;
|
||||
uint32_t v = in();
|
||||
if (!(v & SCL_BIT) || (v & SDA_BIT))
|
||||
return;
|
||||
st.starts++;
|
||||
BaseType_t woken = pdFALSE;
|
||||
run_transaction(&woken);
|
||||
reg(GPIO_STATUS1_W1TC_REG) = SDA_BIT;
|
||||
if (woken)
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
inline void holder_task(void *) {
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
uint32_t end = (esphome::millis) () + hold_ms;
|
||||
while ((int32_t) (end - (esphome::millis) ()) > 0) {
|
||||
drive_low(hold_mask);
|
||||
vTaskDelay(1);
|
||||
}
|
||||
release(hold_mask);
|
||||
holding = false;
|
||||
}
|
||||
}
|
||||
|
||||
inline volatile uint32_t hb = 0;
|
||||
inline volatile bool diag_recover = false;
|
||||
constexpr uint32_t I2C0 = 0x3FF53000;
|
||||
inline void dump_i2c(const char *why, uint32_t stale) {
|
||||
esp_rom_printf("DIAG %s stale=%ums ctr=%08x sr=%08x to=%08x raw=%08x ena=%08x st=%08x fifo=%08x hold=%d in=%x\n", why,
|
||||
(unsigned) stale, (unsigned) reg(I2C0 + 0x04), (unsigned) reg(I2C0 + 0x08),
|
||||
(unsigned) reg(I2C0 + 0x0c), (unsigned) reg(I2C0 + 0x20), (unsigned) reg(I2C0 + 0x28),
|
||||
(unsigned) reg(I2C0 + 0x2c), (unsigned) reg(I2C0 + 0x18), (int) holding, (unsigned) (in() & 3));
|
||||
esp_rom_printf("DIAG cmd:");
|
||||
for (int i = 0; i < 16; i++)
|
||||
esp_rom_printf(" %08x", (unsigned) reg(I2C0 + 0x58 + 4 * i));
|
||||
esp_rom_printf("\nDIAG app_intr_status %08x %08x %08x pro %08x %08x %08x\n", (unsigned) reg(0x3FF000F8),
|
||||
(unsigned) reg(0x3FF000FC), (unsigned) reg(0x3FF00100), (unsigned) reg(0x3FF000EC),
|
||||
(unsigned) reg(0x3FF000F0), (unsigned) reg(0x3FF000F4));
|
||||
}
|
||||
inline void monitor_task(void *) {
|
||||
uint32_t stage = 0;
|
||||
for (;;) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
uint32_t now = (esphome::millis) ();
|
||||
uint32_t h = hb;
|
||||
if (h == 0)
|
||||
continue;
|
||||
uint32_t stale = now - h;
|
||||
if (stale < 1000) {
|
||||
if (stage >= 3)
|
||||
esp_rom_printf("DIAG loop resumed after %u ms, stage %u\n", (unsigned) stale, (unsigned) stage);
|
||||
stage = 0;
|
||||
continue;
|
||||
}
|
||||
if (stage == 0 && stale >= 1000) {
|
||||
dump_i2c("stuck1", stale);
|
||||
stage = 1;
|
||||
} else if (stage == 1 && stale >= 1500) {
|
||||
dump_i2c("stuck2", stale);
|
||||
stage = 2;
|
||||
} else if (stage == 2 && stale >= 2500) {
|
||||
dump_i2c("stuck3", stale);
|
||||
if (diag_recover) {
|
||||
reg(I2C0 + 0x28) = 0;
|
||||
reg(I2C0 + 0x24) = 0xFFFF;
|
||||
esp_rom_printf("DIAG cleared I2C0 int_ena\n");
|
||||
}
|
||||
stage = 3;
|
||||
} else if (stage == 3 && stale >= 3500) {
|
||||
dump_i2c("stuck4", stale);
|
||||
stage = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
inline volatile bool installed = false;
|
||||
inline void install_task(void *) {
|
||||
rel_sda = reg(out_sel_addr(SDA));
|
||||
rel_scl = reg(out_sel_addr(SCL));
|
||||
// ESP_ERR_INVALID_STATE: the service is already installed, which is fine
|
||||
esp_err_t e1 = gpio_install_isr_service(0);
|
||||
esp_err_t et = gpio_set_intr_type((gpio_num_t) SDA, GPIO_INTR_NEGEDGE);
|
||||
esp_err_t e2 = gpio_isr_handler_add((gpio_num_t) SDA, slave_isr, nullptr);
|
||||
ESP_LOGI(TAG, "slave on core %d: isr service %d, intr type %d, handler %d, rel_sda 0x%x rel_scl 0x%x",
|
||||
xPortGetCoreID(), e1, et, e2, (unsigned) rel_sda, (unsigned) rel_scl);
|
||||
if ((e1 == ESP_OK || e1 == ESP_ERR_INVALID_STATE) && et == ESP_OK && e2 == ESP_OK) {
|
||||
installed = true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "slave ISR install failed: service %s, intr type %s, handler %s", esp_err_to_name(e1),
|
||||
esp_err_to_name(et), esp_err_to_name(e2));
|
||||
}
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
inline void install() {
|
||||
if (xTaskCreatePinnedToCore(holder_task, "inahold", 3072, nullptr, configMAX_PRIORITIES - 2, &holder, 0) != pdPASS ||
|
||||
xTaskCreatePinnedToCore(monitor_task, "inamon", 3072, nullptr, configMAX_PRIORITIES - 1, nullptr, 0) != pdPASS ||
|
||||
xTaskCreatePinnedToCore(install_task, "inainst", 3072, nullptr, 5, nullptr, 0) != pdPASS) {
|
||||
ESP_LOGE(TAG, "task creation failed, slave not installed");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 1000 && !installed; i++)
|
||||
vTaskDelay(1);
|
||||
if (!installed)
|
||||
ESP_LOGE(TAG, "slave install did not finish");
|
||||
}
|
||||
|
||||
inline void drain() {
|
||||
static const char *const NAMES[F_COUNT] = {"garbage", "nack", "stretch_us", "hold_scl_ms",
|
||||
"hold_sda_ms", "glitch_scl", "glitch_sda", "vanish"};
|
||||
const uint32_t w = ring_w;
|
||||
if (w - ring_r > 64) {
|
||||
ESP_LOGW(TAG, "F dropped %u events (ring overrun)", (unsigned) (w - ring_r - 64));
|
||||
ring_r = w - 64;
|
||||
}
|
||||
while (ring_r != w) {
|
||||
const FaultEv &e = ring[ring_r & 63];
|
||||
ESP_LOGI(TAG, "F (%u) %s %u", (unsigned) e.ms, NAMES[e.type], (unsigned) e.arg);
|
||||
ring_r++;
|
||||
}
|
||||
}
|
||||
inline void log_stats() {
|
||||
ESP_LOGI(TAG,
|
||||
"SLAVE starts=%u matched=%u stops=%u idle_abort=%u cap_abort=%u rd=%u wr=%u | faults garbage=%u nack=%u "
|
||||
"stretch=%u hold_scl=%u hold_sda=%u glitch_scl=%u glitch_sda=%u vanish=%u",
|
||||
(unsigned) st.starts, (unsigned) st.matched, (unsigned) st.stops, (unsigned) st.idle_abort,
|
||||
(unsigned) st.cap_abort, (unsigned) st.rd_bytes, (unsigned) st.wr_bytes, (unsigned) st.fault[0],
|
||||
(unsigned) st.fault[1], (unsigned) st.fault[2], (unsigned) st.fault[3], (unsigned) st.fault[4],
|
||||
(unsigned) st.fault[5], (unsigned) st.fault[6], (unsigned) st.fault[7]);
|
||||
}
|
||||
} // namespace inasim
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# run.sh LABEL [SECONDS]: build example.yaml from TREE as i2cfault-<LABEL> (edit its substitutions for the framework,
|
||||
# seed and fault rate), flash the ESP32, capture SECONDS (default 640) from a reset and summarize.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); L=${1:?label}; S=${2:-640}; T=${TREE:?set TREE}; P=${PORT:?set PORT}
|
||||
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; NAME=i2cfault-$L; OUT=$R/out-$NAME; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
sed -E "s/^ name: .*/ name: $NAME/" "$R/example.yaml" > "$R/$NAME.yaml"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$NAME.yaml" --device "$P" > "$OUT/upload.log" 2>&1) || { echo "build or upload failed"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
$PY "$SC" "$P" "$S" "$OUT/serial.txt" --reset > /dev/null 2>&1 || { echo "serial capture got nothing"; exit 1; }
|
||||
"$R/summary.sh" "$OUT/serial.txt"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# summary.sh CAPTURE...: crashes, watchdogs, timeout kinds and the read counters per capture
|
||||
for f in "$@"; do
|
||||
first=$(grep -a -m1 'rst:0x' "$f" | cut -c2-9); last=$(tail -1 "$f" | cut -c2-9)
|
||||
echo "== $(basename "$f") $first..$last boots=$(grep -a -c 'rst:0x' "$f") guru=$(grep -a -c Guru "$f") twdt=$(grep -a -c 'task_wdt: Task watchdog' "$f") diag=$(grep -a -c 'DIAG stuck' "$f")"
|
||||
echo " sw=$(grep -a -c 'software timeout' "$f") hw=$(grep -a -c 'hardware timeout detected' "$f") trans=$(grep -a -c 'transaction timeout detected' "$f") busy=$(grep -a -c 'still busy' "$f") ina_garbage_lines=$(grep -a -c 'INA bus voltage garbage' "$f")"
|
||||
grep -a 'MASTER' "$f" | tail -1 | sed 's/^.*MASTER/ MASTER/'
|
||||
grep -a 'SLAVE' "$f" | tail -1 | sed 's/^.*SLAVE/ SLAVE/'
|
||||
echo " ok per 30s window (min over windows after arm): $(grep -a MASTER "$f" | sed 's/.* ok=\([0-9]*\).*/\1/' | awk 'NR>2{d=$1-p; if(min==""||d<min)min=d} {p=$1} END{print (min == "" ? "n/a (fewer than 3 MASTER windows)" : min)}')"
|
||||
echo " harness: install_errors=$(grep -a -c -E 'task creation failed|slave ISR install failed|slave install did not finish|selects no fault' "$f") $(grep -a -o 'selftest1 write=[-0-9]* read=[-0-9]* val=[0-9A-F]*' "$f" | head -1)"
|
||||
done
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Per phase IPv6 link-local and SLAAC statistics from v6probe captures.
|
||||
|
||||
usage: an.py CAPTURE...
|
||||
For the boot and reconnect phases: boots, boots with an esp_netif_create_ip6_linklocal failure, boots without
|
||||
link-local, SLAAC or a connection, connection timeouts, and median/worst times in seconds. Counts missing
|
||||
addresses from the RESULT line too, since the USB console can drop a warning line. Exits 1 when a boot other
|
||||
than the last of a capture has no RESULT for a phase.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
|
||||
def parse(paths: list[str]) -> list[dict]:
|
||||
boots: list[dict] = []
|
||||
cur: dict | None = None
|
||||
for p in paths:
|
||||
for line in pathlib.Path(p).read_bytes().decode(errors="replace").splitlines():
|
||||
if "BOOTMARK" in line:
|
||||
cur = {
|
||||
"boot": {},
|
||||
"reconnect": {},
|
||||
"fail": [0, 0],
|
||||
"created": [0, 0],
|
||||
"timeouts": [0, 0],
|
||||
"ph": 0,
|
||||
}
|
||||
boots.append(cur)
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
ph = cur["ph"]
|
||||
if "esp_netif_create_ip6_linklocal failed" in line:
|
||||
cur["fail"][ph] += 1
|
||||
if "IPv6 link-local address created" in line or "No lwIP netif" in line:
|
||||
cur["created"][ph] += 1
|
||||
if re.search(
|
||||
r"Connection timeout|Connection incomplete|Connection lost", line
|
||||
):
|
||||
cur["timeouts"][ph] += 1
|
||||
if "REENABLE" in line:
|
||||
cur["ph"] = 1
|
||||
if m := re.search(r"RESULT (\w+) (.*)", line):
|
||||
cur[m.group(1)] = dict(kv.split("=") for kv in m.group(2).split())
|
||||
if cur is not None:
|
||||
cur["last"] = True
|
||||
cur = None
|
||||
return boots
|
||||
|
||||
|
||||
def med_max(xs: list[int]) -> str:
|
||||
return f"{statistics.median(xs) / 1000:.1f}/{max(xs) / 1000:.1f}" if xs else "-"
|
||||
|
||||
|
||||
def summ(boots: list[dict], phase: str, idx: int) -> int:
|
||||
rs = [b for b in boots if b[phase]]
|
||||
# a capture usually ends inside its last boot
|
||||
no_result = sum(1 for b in boots if not b[phase] and "last" not in b)
|
||||
|
||||
def seen(key: str) -> list[int]:
|
||||
return [int(b[phase][key]) for b in rs if b[phase][key] != "0"]
|
||||
|
||||
def missing(key: str) -> int:
|
||||
return sum(1 for b in rs if b[phase][key] == "0")
|
||||
|
||||
print(
|
||||
f"{phase:9} n={len(rs)} no_result={no_result} esp_fail={sum(1 for b in rs if b['fail'][idx])} no_ll={missing('ll')} "
|
||||
f"no_slaac={missing('gl')} not_connected={missing('full')} timeouts={sum(b['timeouts'][idx] for b in rs)} "
|
||||
f"ll(med/max s)={med_max(seen('ll'))} slaac={med_max(seen('gl'))} connected={med_max(seen('full'))} "
|
||||
f"created_logs={sum(b['created'][idx] for b in rs)}"
|
||||
)
|
||||
return no_result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
boots = parse(sys.argv[1:])
|
||||
print("boots", len(boots))
|
||||
no_result = summ(boots, "boot", 0) + summ(boots, "reconnect", 1)
|
||||
print(
|
||||
"heap_used first 3 boots",
|
||||
[int(b["boot"]["heap_used"]) for b in boots if b["boot"]][:3],
|
||||
)
|
||||
if no_result or not boots:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
substitutions:
|
||||
min_count: "0"
|
||||
cap_ms: "30000"
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
common: !include v6common.yaml
|
||||
inject: !include v6inject.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
# One file per build; run.sh passes the name. Add v6hf.yaml or v6inject.yaml to packages.
|
||||
substitutions:
|
||||
min_count: "0"
|
||||
cap_ms: "30000"
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
common: !include v6common.yaml
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# run.sh NAME SECONDS: build NAME.yaml from TREE as device NAME, flash, then capture SECONDS of self-rebooting boots
|
||||
# (each boot is a boot phase and a reconnect phase) into out-NAME/cap.txt and summarize them with an.py.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); N=${1:?name}; S=${2:?seconds}
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
OUT=$R/out-$N; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome -s name "$N" run --no-logs "$R/$N.yaml" --device "$P" > "$OUT/upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$OUT/upload.log"; exit 1; }
|
||||
$PY "$SC" "$P" "$S" "$OUT/cap.txt" --ms > /dev/null 2>&1
|
||||
[ -s "$OUT/cap.txt" ] || { echo "serial capture got nothing"; exit 1; }
|
||||
$PY "$R/an.py" "$OUT/cap.txt"
|
||||
@@ -0,0 +1,28 @@
|
||||
# IPv6 boot and reconnect probe (#22364): logs milestones per phase, then disables Wi-Fi for 3 s, measures the
|
||||
# reconnect and reboots. Substitutions: name (run.sh passes it), min_count (network min_ipv6_addr_count), cap_ms (phase time limit).
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- v6probe.hpp
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("v6probe", "BOOTMARK reason=%d", (int) esp_reset_reason());'
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
min_ipv6_addr_count: ${min_count}
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
reboot_timeout: 0s
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: "v6probe::tick(${cap_ms});"
|
||||
@@ -0,0 +1,9 @@
|
||||
# Add to v6common.yaml: keeps the main loop busy, like a display or audio config
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 800
|
||||
then:
|
||||
- lambda: |-
|
||||
static HighFrequencyLoopRequester hf;
|
||||
hf.start();
|
||||
ESP_LOGI("v6probe", "HF loop on");
|
||||
@@ -0,0 +1,5 @@
|
||||
# Add to v6common.yaml: delays esp-netif's STA_CONNECTED handler by 300 ms (wrapper in v6probe.hpp)
|
||||
esphome:
|
||||
build_flags:
|
||||
- -Wl,--wrap=esp_netif_action_connected
|
||||
- -DV6_INJECT
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
// Boot and reconnect timing probe for IPv6 link-local and SLAAC (#22364). Call tick(cap_ms) every 50 ms.
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
namespace v6probe {
|
||||
static const char *const PTAG = "v6probe";
|
||||
struct Times {
|
||||
uint32_t assoc, v4, ll, gl, full;
|
||||
};
|
||||
static int phase = 0; // 0 boot, 1 wifi disabled, 2 reconnecting, 3 done
|
||||
static uint32_t start = 0;
|
||||
static Times t{};
|
||||
|
||||
inline void tick(uint32_t cap_ms) {
|
||||
using esphome::millis;
|
||||
uint32_t now = millis();
|
||||
if (phase == 3)
|
||||
return;
|
||||
if (phase == 1) {
|
||||
if (now - start >= 3000) {
|
||||
ESP_LOGI(PTAG, "REENABLE");
|
||||
t = {};
|
||||
start = now;
|
||||
phase = 2;
|
||||
esphome::wifi::global_wifi_component->enable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint32_t rel = (now - start) | 1; // never 0, 0 means not seen
|
||||
wifi_ap_record_t ap;
|
||||
if (!t.assoc && esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
|
||||
t.assoc = rel;
|
||||
ESP_LOGI(PTAG, "MS assoc %u", rel);
|
||||
}
|
||||
// a missing netif still has to reach the cap_ms timeout below
|
||||
esp_netif_t *nif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
esp_netif_ip_info_t ip;
|
||||
if (!t.v4 && nif != nullptr && esp_netif_get_ip_info(nif, &ip) == ESP_OK && ip.ip.addr != 0) {
|
||||
t.v4 = rel;
|
||||
ESP_LOGI(PTAG, "MS ipv4 %u", rel);
|
||||
}
|
||||
esp_ip6_addr_t a[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
int n = 0;
|
||||
if (nif != nullptr) {
|
||||
esphome::LwIPLock lock;
|
||||
n = esp_netif_get_all_ip6(nif, a);
|
||||
}
|
||||
bool ll = false, gl = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (esp_netif_ip6_get_addr_type(&a[i]) == ESP_IP6_ADDR_IS_LINK_LOCAL)
|
||||
ll = true;
|
||||
else
|
||||
gl = true;
|
||||
}
|
||||
if (!t.ll && ll) {
|
||||
t.ll = rel;
|
||||
ESP_LOGI(PTAG, "MS linklocal %u", rel);
|
||||
}
|
||||
if (!t.gl && gl) {
|
||||
t.gl = rel;
|
||||
ESP_LOGI(PTAG, "MS slaac %u", rel);
|
||||
}
|
||||
if (!t.full && esphome::wifi::global_wifi_component->is_connected()) {
|
||||
t.full = rel;
|
||||
ESP_LOGI(PTAG, "MS connected %u", rel);
|
||||
}
|
||||
bool done = t.gl && t.full;
|
||||
if (done || now - start >= cap_ms) {
|
||||
size_t used = heap_caps_get_total_size(MALLOC_CAP_INTERNAL) - heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
ESP_LOGI(PTAG, "RESULT %s assoc=%u v4=%u ll=%u gl=%u full=%u n6=%d timeout=%d heap_used=%u",
|
||||
phase == 0 ? "boot" : "reconnect", t.assoc, t.v4, t.ll, t.gl, t.full, n, done ? 0 : 1, (unsigned) used);
|
||||
if (phase == 0) {
|
||||
phase = 1;
|
||||
start = now;
|
||||
esphome::wifi::global_wifi_component->disable();
|
||||
} else {
|
||||
phase = 3;
|
||||
esphome::App.safe_reboot();
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace v6probe
|
||||
|
||||
#ifdef V6_INJECT
|
||||
// Fault injection: esp-netif's STA_CONNECTED handler (which brings the netif up) is delayed by 300 ms in the
|
||||
// event task, after the wifi component's own handler has already queued the event for the main loop.
|
||||
extern "C" void __real_esp_netif_action_connected(void *esp_netif, esp_event_base_t base, int32_t id, void *data);
|
||||
extern "C" void __wrap_esp_netif_action_connected(void *esp_netif, esp_event_base_t base, int32_t id, void *data) {
|
||||
ESP_LOGI("v6probe", "INJECT hold start");
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
ESP_LOGI("v6probe", "INJECT hold end");
|
||||
__real_esp_netif_action_connected(esp_netif, base, id, data);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,82 @@
|
||||
# Package: BW12 heartbeat, pull-toggle edge generator, BS binary sensor on ${bs_pin}, meas lines every 10 s
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: BW12 ISR Test
|
||||
platformio_options:
|
||||
upload_speed: 115200
|
||||
build_flags:
|
||||
- -Wl,--wrap=digitalRead
|
||||
includes:
|
||||
- bwdbg.hpp
|
||||
|
||||
rtl87xx:
|
||||
board: bw12
|
||||
framework:
|
||||
version: recommended
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
baud_rate: 115200
|
||||
logs:
|
||||
sensor: WARN
|
||||
binary_sensor: WARN
|
||||
pulse_counter: WARN
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
|
||||
captive_portal:
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: PA05
|
||||
id: heartbeat_output
|
||||
|
||||
globals:
|
||||
- id: bs_changes
|
||||
type: uint32_t
|
||||
initial_value: "0"
|
||||
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- output.turn_on: heartbeat_output
|
||||
- delay: 100ms
|
||||
- output.turn_off: heartbeat_output
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: bwdbg::tick();
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
static uint32_t lg = 0, lc = 0;
|
||||
uint32_t g = bwdbg::gen_rise + bwdbg::gen_fall, c = id(bs_changes);
|
||||
ESP_LOGI("meas", "BS changes %u gen_edges %u state %d", c - lc, g - lg, id(bs).state);
|
||||
lg = g; lc = c;
|
||||
bwdbg::report();
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: bs
|
||||
name: BS
|
||||
pin:
|
||||
number: ${bs_pin}
|
||||
mode: INPUT
|
||||
use_interrupt: true
|
||||
interrupt_type: ANY
|
||||
trigger_on_initial_state: true
|
||||
on_state:
|
||||
- lambda: id(bs_changes) += 1;
|
||||
|
||||
sensor:
|
||||
- platform: uptime
|
||||
name: Uptime
|
||||
update_interval: 60s
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
// bw12isr: pull-toggle edge generator on floating pins, ISR read counters and pin/IRQ register dump (RTL8710B)
|
||||
#include <Arduino.h>
|
||||
#include "esphome/core/log.h"
|
||||
extern "C" {
|
||||
PinStatus __real_digitalRead(pin_size_t pin);
|
||||
__attribute__((long_call)) void PAD_PullCtrl(uint8_t pin, uint8_t pull);
|
||||
}
|
||||
namespace bwdbg {
|
||||
static constexpr uint8_t PINS[] = {12, 0, 14, 15, 19, 22};
|
||||
static constexpr int NP = sizeof(PINS);
|
||||
inline volatile uint32_t isr_reads[32], task_reads[32];
|
||||
inline uint32_t gen_rise = 0, gen_fall = 0, checks = 0, mismatch[NP];
|
||||
inline bool up = false, started = false;
|
||||
inline uint32_t reg(uint32_t off) { return *reinterpret_cast<volatile uint32_t *>(0x40001000 + off); }
|
||||
inline bool in_isr() {
|
||||
uint32_t r;
|
||||
__asm volatile("mrs %0, ipsr" : "=r"(r));
|
||||
return r != 0;
|
||||
}
|
||||
// Every call: check each pin follows the pull set on the previous call, then flip all pulls
|
||||
inline void tick() {
|
||||
uint32_t lvl = reg(0x50);
|
||||
if (started) {
|
||||
checks++;
|
||||
for (int i = 0; i < NP; i++)
|
||||
if (((lvl >> PINS[i]) & 1) != (up ? 1u : 0u))
|
||||
mismatch[i]++;
|
||||
}
|
||||
up = !up;
|
||||
started = true;
|
||||
for (int i = 0; i < NP; i++)
|
||||
PAD_PullCtrl(PINS[i], up ? 2 : 1); // GPIO_PuPd_UP : GPIO_PuPd_DOWN
|
||||
if (up)
|
||||
gen_rise++;
|
||||
else
|
||||
gen_fall++;
|
||||
}
|
||||
inline void report() {
|
||||
uint32_t en = reg(0x30), mask = reg(0x34), type = reg(0x38), pol = reg(0x3C), deb = reg(0x48), lvl = reg(0x50);
|
||||
ESP_LOGI("dbg", "gen rise %u fall %u checks %u", gen_rise, gen_fall, checks);
|
||||
for (int i = 0; i < NP; i++) {
|
||||
uint8_t p = PINS[i];
|
||||
PinInfo *info = pinInfo(p);
|
||||
// -1 marks a pin with no PinInfo entry
|
||||
const uint32_t e = info ? info->enabled : 0;
|
||||
const int gpio = info ? !!(e & PIN_GPIO) : -1, irq = info ? !!(e & PIN_IRQ) : -1, pwm = info ? !!(e & PIN_PWM) : -1;
|
||||
ESP_LOGI("dbg",
|
||||
"PA%02u LT 0x%x (GPIO %d IRQ %d PWM %d) HW int_en %u mask %u edge %u pol_high %u deb %u lvl %u | "
|
||||
"mismatch %u isr digitalRead %u task digitalRead %u",
|
||||
p, e, gpio, irq, pwm, (en >> p) & 1, (mask >> p) & 1, (type >> p) & 1, (pol >> p) & 1, (deb >> p) & 1,
|
||||
(lvl >> p) & 1, mismatch[i], isr_reads[p], task_reads[p]);
|
||||
}
|
||||
}
|
||||
} // namespace bwdbg
|
||||
extern "C" __attribute__((used)) PinStatus __wrap_digitalRead(pin_size_t pin) {
|
||||
if (pin < 32) {
|
||||
if (bwdbg::in_isr())
|
||||
bwdbg::isr_reads[pin] = bwdbg::isr_reads[pin] + 1;
|
||||
else
|
||||
bwdbg::task_reads[pin] = bwdbg::task_reads[pin] + 1;
|
||||
}
|
||||
return __real_digitalRead(pin);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Layout B: BS on PA19; F0/I0 moved to PA12/PA00
|
||||
substitutions:
|
||||
name: bw12-edges-b
|
||||
bs_pin: PA19
|
||||
|
||||
packages:
|
||||
base: !include bw12-base.yaml
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: F0
|
||||
pin:
|
||||
number: PA12
|
||||
inverted: false
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "F0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: I0
|
||||
pin:
|
||||
number: PA00
|
||||
inverted: true
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "I0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
@@ -0,0 +1,104 @@
|
||||
substitutions:
|
||||
name: bw12-time
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: BW12 Time Test
|
||||
platformio_options:
|
||||
upload_speed: 115200
|
||||
|
||||
rtl87xx:
|
||||
board: bw12
|
||||
framework:
|
||||
version: recommended
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
baud_rate: 115200
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
ap:
|
||||
|
||||
captive_portal:
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: Restart
|
||||
|
||||
interval:
|
||||
- interval: 20s
|
||||
then:
|
||||
- lambda: |-
|
||||
static bool done = false;
|
||||
if (done) return;
|
||||
done = true;
|
||||
// mask ROM: SYSTIMER_TickGet at 0x33a0 (32 bytes), RTIM_GetCount at 0xb42c (72 bytes)
|
||||
const uint32_t ranges[2][2] = {{0x33a0, 32}, {0xb42c, 72}};
|
||||
for (auto &r : ranges) {
|
||||
char hex[3 * 72 + 1];
|
||||
const volatile uint8_t *p = (const volatile uint8_t *) r[0];
|
||||
for (uint32_t i = 0; i < r[1]; i++) sprintf(hex + 3 * i, "%02x ", p[i]);
|
||||
ESP_LOGI("tm", "ROM %05x: %s", (unsigned) r[0], hex);
|
||||
}
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: 'ESP_LOGI("tm", "CLOCK ms %u us %u", millis(), micros());'
|
||||
- interval: 30s
|
||||
then:
|
||||
- lambda: |-
|
||||
volatile uint32_t *demcr = (volatile uint32_t *) 0xE000EDFC, *dwt = (volatile uint32_t *) 0xE0001000;
|
||||
*demcr |= 1u << 24;
|
||||
dwt[0] |= 1;
|
||||
const float mhz = SystemCoreClock / 1e6f;
|
||||
// cycles per call, 1000 calls, loop overhead subtracted
|
||||
volatile uint32_t sink = 0;
|
||||
uint32_t c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + i;
|
||||
uint32_t base = dwt[1] - c0;
|
||||
c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + millis();
|
||||
uint32_t ms_c = dwt[1] - c0 - base;
|
||||
c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + micros();
|
||||
uint32_t us_c = dwt[1] - c0 - base;
|
||||
uint32_t isr_c;
|
||||
{
|
||||
InterruptLock lock;
|
||||
c0 = dwt[1];
|
||||
for (int i = 0; i < 1000; i++) sink = sink + micros();
|
||||
isr_c = dwt[1] - c0 - base;
|
||||
}
|
||||
ESP_LOGI("tm", "BENCH cycles per call millis %.1f micros %.1f micros_locked %.1f (core %.1f MHz)",
|
||||
ms_c / 1000.0f, us_c / 1000.0f, isr_c / 1000.0f, mhz);
|
||||
uint32_t got[6];
|
||||
const uint32_t req_us[3] = {50, 500, 5000};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t a = dwt[1];
|
||||
delayMicroseconds(req_us[i]);
|
||||
got[i] = uint32_t((dwt[1] - a) / mhz);
|
||||
}
|
||||
const uint32_t req_ms[3] = {1, 10, 100};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t a = dwt[1];
|
||||
delay(req_ms[i]);
|
||||
got[3 + i] = uint32_t((dwt[1] - a) / mhz);
|
||||
}
|
||||
ESP_LOGI("tm", "DELAY real us: delayMicroseconds 50->%u 500->%u 5000->%u, delay 1->%u 10->%u 100->%u",
|
||||
got[0], got[1], got[2], got[3], got[4], got[5]);
|
||||
uint32_t first = micros(), prev = first, maxstep = 0, back = 0, distinct = 0, mism = 0;
|
||||
for (int i = 0; i < 20000; i++) {
|
||||
uint32_t ms = millis(), m = micros(), ms2 = millis();
|
||||
if (ms == ms2 && m / 1000 != ms) mism++;
|
||||
if (m != prev) { distinct++; if (m < prev) back++; else if (m - prev > maxstep) maxstep = m - prev; }
|
||||
prev = m;
|
||||
}
|
||||
ESP_LOGI("tm", "MICROS span %u distinct %u maxstep %u backwards %u, micros/1000 != millis %u of 20000",
|
||||
prev - first, distinct, maxstep, back, mism);
|
||||
@@ -0,0 +1,48 @@
|
||||
# Layout A: BS on PA22; F13/I13 on PA12/PA00, F0/I0 on PA14/PA15, B13 on PA19 counting both edges
|
||||
substitutions:
|
||||
name: bw12-edges-a
|
||||
bs_pin: PA22
|
||||
|
||||
packages:
|
||||
base: !include bw12-base.yaml
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: F13
|
||||
pin: PA12
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "F13 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: I13
|
||||
pin:
|
||||
number: PA00
|
||||
inverted: true
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "I13 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: F0
|
||||
pin: PA14
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "F0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: I0
|
||||
pin:
|
||||
number: PA15
|
||||
inverted: true
|
||||
internal_filter: 0us
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "I0 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
- platform: pulse_counter
|
||||
name: B13
|
||||
pin: PA19
|
||||
count_mode:
|
||||
rising_edge: INCREMENT
|
||||
falling_edge: INCREMENT
|
||||
update_interval: 10s
|
||||
on_raw_value:
|
||||
- lambda: 'static uint32_t l = 0; uint32_t g = bwdbg::gen_rise; ESP_LOGI("meas", "B13 raw %.0f gen_rise %u", x, g - l); l = g;'
|
||||
@@ -0,0 +1,47 @@
|
||||
substitutions:
|
||||
name: pulse-bk
|
||||
inv: "false"
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- -DGEN_PIN_A=26
|
||||
- -DGEN_PIN_B=8
|
||||
includes:
|
||||
- pulsegen.hpp
|
||||
|
||||
bk72xx:
|
||||
board: xh-wb3s
|
||||
|
||||
logger:
|
||||
hardware_uart: UART1
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: pulsegen::tick();
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: PC default filter
|
||||
pin:
|
||||
number: P6
|
||||
inverted: ${inv}
|
||||
update_interval: 5s
|
||||
on_raw_value:
|
||||
- lambda: 'ESP_LOGI("meas", "F13 %.1f", x);'
|
||||
- platform: pulse_counter
|
||||
name: PC filter 0
|
||||
pin:
|
||||
number: P24
|
||||
inverted: ${inv}
|
||||
internal_filter: 0us
|
||||
update_interval: 5s
|
||||
on_raw_value:
|
||||
- lambda: 'ESP_LOGI("meas", "F0 %.1f", x);'
|
||||
@@ -0,0 +1,39 @@
|
||||
substitutions:
|
||||
name: pulse-rtl
|
||||
inv: "false"
|
||||
filt: 13us
|
||||
key: PC
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- -DGEN_PIN_NAME=17
|
||||
includes:
|
||||
- pulsegen.hpp
|
||||
|
||||
rtl87xx:
|
||||
board: bw15
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
interval:
|
||||
- interval: 50ms
|
||||
then:
|
||||
- lambda: pulsegen::tick();
|
||||
|
||||
sensor:
|
||||
- platform: pulse_counter
|
||||
name: PC
|
||||
pin:
|
||||
number: PA18
|
||||
inverted: ${inv}
|
||||
internal_filter: ${filt}
|
||||
update_interval: 5s
|
||||
on_raw_value:
|
||||
- lambda: 'ESP_LOGI("meas", "${key} %.1f", x);'
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
// One pulse shape per 35 s stage on GEN_PIN_A and GEN_PIN_B (BK72xx analogWrite) or GEN_PIN_NAME (AmebaZ2 pwmout)
|
||||
#include <Arduino.h>
|
||||
#ifdef GEN_PIN_NAME
|
||||
// mbed pwmout API (its header clashes with Arduino's PinMode); pwmout_t is held in an oversized buffer
|
||||
extern "C" {
|
||||
void pwmout_init(void *obj, int pin);
|
||||
void pwmout_period_us(void *obj, int us);
|
||||
void pwmout_pulsewidth_us(void *obj, int us);
|
||||
}
|
||||
#endif
|
||||
#include "esphome/core/log.h"
|
||||
namespace pulsegen {
|
||||
struct Stage {
|
||||
uint32_t period_us;
|
||||
uint32_t high_us;
|
||||
};
|
||||
static constexpr Stage STAGES[] = {
|
||||
{1000, 38}, {10000, 38}, {10000, 50}, {10000, 75}, {10000, 100}, {10000, 150},
|
||||
{10000, 200}, {1000, 200}, {10000, 300}, {10000, 500}, {10000, 1000}, {100000, 1000},
|
||||
{100000, 5000}, {100000, 50000}, {2000, 1000}, {1000, 500}, {250, 125}, {100000, 38},
|
||||
};
|
||||
static constexpr int N = sizeof(STAGES) / sizeof(STAGES[0]);
|
||||
static constexpr uint32_t STAGE_MS = 35000;
|
||||
#ifdef GEN_PIN_NAME
|
||||
// Arduino analogWrite re-init faults on the 2nd period change on AmebaZ2: keep one pwmout and retune it
|
||||
inline void apply(const Stage &st) {
|
||||
alignas(8) static uint8_t pwm[512];
|
||||
static bool init = false;
|
||||
if (!init) {
|
||||
pwmout_init(pwm, GEN_PIN_NAME);
|
||||
init = true;
|
||||
}
|
||||
pwmout_period_us(pwm, st.period_us);
|
||||
pwmout_pulsewidth_us(pwm, st.high_us);
|
||||
}
|
||||
#else
|
||||
inline void apply_pin(uint8_t pin, const Stage &s) {
|
||||
pinMode(pin, INPUT); // drop PWM mode so the new period is taken
|
||||
analogWriteResolution(16);
|
||||
analogWritePeriod(s.period_us);
|
||||
analogWrite(pin, (int) (65535.0f * s.high_us / s.period_us + 0.5f));
|
||||
}
|
||||
inline void apply(const Stage &st) {
|
||||
apply_pin(GEN_PIN_A, st);
|
||||
apply_pin(GEN_PIN_B, st);
|
||||
}
|
||||
#endif
|
||||
inline void tick() {
|
||||
static int last = -1;
|
||||
int s = (millis() / STAGE_MS) % N;
|
||||
if (s == last)
|
||||
return;
|
||||
last = s;
|
||||
apply(STAGES[s]);
|
||||
ESP_LOGI("gen", "STAGE %d period %u high %u expect_per_min %.1f", s, STAGES[s].period_us, STAGES[s].high_us,
|
||||
60e6f / STAGES[s].period_us);
|
||||
}
|
||||
} // namespace pulsegen
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Sum the bw12 edge test `meas` lines per counter: counted pulses against generated rising edges."""
|
||||
|
||||
from collections import defaultdict
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
MEAS = re.compile(r"meas:\d+\]: (\w+) raw ([\d.]+) gen_rise (\d+)")
|
||||
BS = re.compile(r"BS changes (\d+) gen_edges (\d+) state (\d)")
|
||||
|
||||
|
||||
def summarize(path: str) -> None:
|
||||
counters: dict[str, list[float]] = defaultdict(lambda: [0.0, 0, 0])
|
||||
bs = [0, 0, 0]
|
||||
state = "?"
|
||||
seen: set[str] = set()
|
||||
with pathlib.Path(path).open(encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
if m := MEAS.search(line):
|
||||
key = m[1]
|
||||
if key not in seen: # the first window is partial
|
||||
seen.add(key)
|
||||
continue
|
||||
c = counters[key]
|
||||
c[0] += float(m[2]) / 6 # pulses per minute over a 10 s window
|
||||
c[1] += int(m[3])
|
||||
c[2] += 1
|
||||
elif m := BS.search(line):
|
||||
if "BS" not in seen:
|
||||
seen.add("BS")
|
||||
continue
|
||||
bs[0] += int(m[1])
|
||||
bs[1] += int(m[2])
|
||||
bs[2] += 1
|
||||
state = m[3]
|
||||
print(path.rsplit("/", 1)[-1])
|
||||
for key, (counted, rises, windows) in sorted(counters.items()):
|
||||
ratio = f"{counted / rises:.3f}" if rises else "n/a (no generated edges)"
|
||||
print(
|
||||
f" {key}: windows {windows} counted {counted:.0f} gen_rise {rises} ratio_to_rise {ratio}"
|
||||
)
|
||||
print(f" BS: windows {bs[2]} changes {bs[0]} gen_edges {bs[1]} last_state {state}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for arg in sys.argv[1:]:
|
||||
summarize(arg)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Default ltchiptool's -L to the newest installed LibreTiny platform.
|
||||
|
||||
ltchiptool reads its chip list from ~/.platformio/platforms/libretiny, which can be an older copy than the one a
|
||||
build used (BK7238 UF2s then fail with "UF2 of unsupported family").
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def _version(platform: Path) -> tuple[int, ...]:
|
||||
try:
|
||||
text = json.loads((platform / "platform.json").read_text(encoding="utf-8"))[
|
||||
"version"
|
||||
]
|
||||
except (OSError, KeyError, ValueError):
|
||||
return ()
|
||||
# Numeric prefix of each part, so 1.9.0-dev still sorts
|
||||
return tuple(
|
||||
int(m[1]) for part in text.split(".")[:3] if (m := re.match(r"(\d+)", part))
|
||||
)
|
||||
|
||||
|
||||
def with_platform(args: list[str]) -> list[str]:
|
||||
if any(a.startswith(("-L", "--libretiny-path")) for a in args):
|
||||
return args
|
||||
found = list((Path.home() / ".platformio" / "platforms").glob("libretiny*"))
|
||||
platforms = [p for p in found if _version(p)]
|
||||
for p in found:
|
||||
if p not in platforms:
|
||||
print(
|
||||
f"lt_platform: skipped {p} (no numeric version in platform.json)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not platforms:
|
||||
print(
|
||||
f"lt_platform: no versioned LibreTiny platform in {[str(p) for p in found]}, using ltchiptool's default",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return args
|
||||
chosen = max(platforms, key=_version)
|
||||
print(f"lt_platform: using {chosen}", file=sys.stderr)
|
||||
return ["-L", str(chosen), *args]
|
||||
@@ -0,0 +1,64 @@
|
||||
# MCP23017 at 0x27 on an ESP8266 (Feather HUZZAH): SDA GPIO4, SCL GPIO5, INTA to GPIO14, INTB to GPIO13,
|
||||
# PA0 jumpered to PB0 and PA1 to PB1. The variant file adds the mcp23017 hub and mcptest. Substitution: name.
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
esp8266:
|
||||
board: huzzah
|
||||
|
||||
logger:
|
||||
baud_rate: 115200
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: ext
|
||||
components: [mcptest]
|
||||
|
||||
i2c:
|
||||
sda: GPIO4
|
||||
scl: GPIO5
|
||||
scan: false
|
||||
frequency: 100kHz
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: pb0
|
||||
name: PB0
|
||||
pin:
|
||||
mcp23xxx: mcp
|
||||
number: 8
|
||||
mode:
|
||||
input: true
|
||||
interrupt: CHANGE
|
||||
on_state:
|
||||
- lambda: id(tst).count_change();
|
||||
- platform: gpio
|
||||
id: pb1
|
||||
name: PB1
|
||||
pin:
|
||||
mcp23xxx: mcp
|
||||
number: 9
|
||||
mode:
|
||||
input: true
|
||||
interrupt: CHANGE
|
||||
on_state:
|
||||
- lambda: id(tst).count_change();
|
||||
|
||||
mcptest:
|
||||
id: tst
|
||||
address: 0x27
|
||||
label: ${name}
|
||||
isr_inta: ${isr_inta}
|
||||
pa0:
|
||||
mcp23xxx: mcp
|
||||
number: 0
|
||||
mode:
|
||||
output: true
|
||||
pa1:
|
||||
mcp23xxx: mcp
|
||||
number: 1
|
||||
mode:
|
||||
output: true
|
||||
pb0: pb0
|
||||
pb1: pb1
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Test only: drives PA0/PA1, reads IOCON and the INT lines, and checks the PB binary sensors."""
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "mcptest"
|
||||
mcptest_ns = cg.esphome_ns.namespace("mcptest")
|
||||
McpTest = mcptest_ns.class_("McpTest", cg.Component, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(McpTest),
|
||||
cv.Required("pa0"): pins.gpio_output_pin_schema,
|
||||
cv.Required("pa1"): pins.gpio_output_pin_schema,
|
||||
cv.Required("pb0"): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Required("pb1"): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional("isr_inta", default=False): cv.boolean,
|
||||
cv.Optional("label", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x27))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(
|
||||
var.set_pins(
|
||||
await cg.gpio_pin_expression(config["pa0"]),
|
||||
await cg.gpio_pin_expression(config["pa1"]),
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
var.set_bs(
|
||||
await cg.get_variable(config["pb0"]), await cg.get_variable(config["pb1"])
|
||||
)
|
||||
)
|
||||
cg.add(var.set_opts(config["isr_inta"], config["label"]))
|
||||
@@ -0,0 +1,164 @@
|
||||
#pragma once
|
||||
// MCP23017 IOCON and INT line checks on an ESP8266: INTB on GPIO13, INTA on GPIO14, PA0/PA1 jumpered to PB0/PB1.
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace esphome::mcptest {
|
||||
|
||||
static const char *const TAG = "mcptest";
|
||||
static volatile uint32_t isr13_count = 0;
|
||||
static volatile uint32_t isr14_count = 0;
|
||||
static void IRAM_ATTR isr13() { isr13_count++; }
|
||||
static void IRAM_ATTR isr14() { isr14_count++; }
|
||||
inline bool pin_level(uint8_t pin) { return digitalRead(pin); } // NOLINT
|
||||
|
||||
class McpTest : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_pins(GPIOPin *a0, GPIOPin *a1) {
|
||||
this->pa_[0] = a0;
|
||||
this->pa_[1] = a1;
|
||||
}
|
||||
void set_bs(binary_sensor::BinarySensor *b0, binary_sensor::BinarySensor *b1) {
|
||||
this->pb_[0] = b0;
|
||||
this->pb_[1] = b1;
|
||||
}
|
||||
void set_opts(bool isr_inta, const char *label) {
|
||||
this->isr_inta_ = isr_inta;
|
||||
this->label_ = label;
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::LATE; }
|
||||
|
||||
void setup() override {
|
||||
pinMode(13, INPUT); // NOLINT
|
||||
attachInterrupt(digitalPinToInterrupt(13), isr13, CHANGE);
|
||||
if (this->isr_inta_) {
|
||||
pinMode(14, INPUT); // NOLINT
|
||||
attachInterrupt(digitalPinToInterrupt(14), isr14, CHANGE);
|
||||
}
|
||||
this->pa_[0]->setup();
|
||||
this->pa_[1]->setup();
|
||||
this->pa_[0]->digital_write(false);
|
||||
this->pa_[1]->digital_write(false);
|
||||
}
|
||||
|
||||
uint8_t rd(uint8_t reg) {
|
||||
uint8_t v = 0xEE;
|
||||
if (!this->read_byte(reg, &v)) {
|
||||
ESP_LOGE(TAG, "read 0x%02X failed", reg);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// A failed write reads back unchanged and would look like no aliasing; run.sh fails on the [E] line.
|
||||
void wr(uint8_t reg, uint8_t v) {
|
||||
if (!this->write_byte(reg, v)) {
|
||||
ESP_LOGE(TAG, "write 0x%02X failed", reg);
|
||||
}
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
uint32_t now = millis();
|
||||
if (this->phase_ == 0) {
|
||||
if (now < 6000)
|
||||
return;
|
||||
this->iocon_a_ = this->rd(0x0A);
|
||||
this->iocon_b_ = this->rd(0x0B);
|
||||
ESP_LOGI(TAG, "[%s] IOCON 0x0A=0x%02X 0x0B=0x%02X GPINTENB=0x%02X IODIRA=0x%02X IODIRB=0x%02X INT13=%d INT14=%d",
|
||||
this->label_, this->iocon_a_, this->iocon_b_, this->rd(0x05), this->rd(0x00), this->rd(0x01),
|
||||
pin_level(13), pin_level(14));
|
||||
this->isr13_start_ = isr13_count;
|
||||
this->isr14_start_ = isr14_count;
|
||||
this->phase_ = 1;
|
||||
this->next_ = now + 200;
|
||||
return;
|
||||
}
|
||||
if (this->phase_ == 1) {
|
||||
if (now < this->next_)
|
||||
return;
|
||||
if (this->step_ > 0 && this->pending_check_) {
|
||||
// 200 ms after the toggle: INT released? loopback seen?
|
||||
int p = (this->step_ - 1) / 10;
|
||||
bool want = this->level_[p];
|
||||
if (!pin_level(13))
|
||||
this->intb_low_late_++;
|
||||
if (!pin_level(14))
|
||||
this->inta_low_late_++;
|
||||
if (this->pb_[p]->has_state() && this->pb_[p]->state == want)
|
||||
this->loop_ok_++;
|
||||
this->pending_check_ = false;
|
||||
}
|
||||
if (this->step_ >= 20) {
|
||||
this->phase_ = 2;
|
||||
return;
|
||||
}
|
||||
int p = this->step_ / 10;
|
||||
this->level_[p] = !this->level_[p];
|
||||
this->pa_[p]->digital_write(this->level_[p]);
|
||||
if (!pin_level(13))
|
||||
this->intb_low_now_++;
|
||||
if (!pin_level(14))
|
||||
this->inta_low_now_++;
|
||||
this->step_++;
|
||||
this->pending_check_ = true;
|
||||
this->next_ = now + 200;
|
||||
return;
|
||||
}
|
||||
if (this->phase_ == 2) {
|
||||
ESP_LOGI(
|
||||
TAG,
|
||||
"[%s] SUMMARY iocon_a=0x%02X iocon_b=0x%02X toggles=20 inta_low_now=%u intb_low_now=%u inta_low_200ms=%u "
|
||||
"intb_low_200ms=%u loopback_ok=%u/20 isr_intb=%u isr_inta=%s%u bs_changes=%u heap_free=%u",
|
||||
this->label_, this->iocon_a_, this->iocon_b_, this->inta_low_now_, this->intb_low_now_, this->inta_low_late_,
|
||||
this->intb_low_late_, this->loop_ok_, isr13_count - this->isr13_start_, this->isr_inta_ ? "" : "n/a ",
|
||||
isr14_count - this->isr14_start_, this->changes_, ESP.getFreeHeap());
|
||||
this->ioconb_test_();
|
||||
this->phase_ = 3;
|
||||
}
|
||||
}
|
||||
|
||||
void ioconb_test_() {
|
||||
uint8_t a0 = this->rd(0x0A), b0 = this->rd(0x0B);
|
||||
const uint8_t bit = 0x08; // HAEN, ignored by the I2C part
|
||||
this->wr(0x0B, b0 ^ bit);
|
||||
uint8_t a1 = this->rd(0x0A), b1 = this->rd(0x0B);
|
||||
this->wr(0x0B, b0);
|
||||
uint8_t a2 = this->rd(0x0A), b2 = this->rd(0x0B);
|
||||
this->wr(0x0A, a0 ^ bit);
|
||||
uint8_t a3 = this->rd(0x0A), b3 = this->rd(0x0B);
|
||||
this->wr(0x0A, a0);
|
||||
uint8_t a4 = this->rd(0x0A), b4 = this->rd(0x0B);
|
||||
ESP_LOGI(TAG,
|
||||
"[%s] IOCONB_TEST start A=0x%02X B=0x%02X | write B^0x08 -> A=0x%02X B=0x%02X | restore -> A=0x%02X "
|
||||
"B=0x%02X | write A^0x08 -> A=0x%02X B=0x%02X | restore -> A=0x%02X B=0x%02X",
|
||||
this->label_, a0, b0, a1, b1, a2, b2, a3, b3, a4, b4);
|
||||
// Second probe on a bit that matters (MIRROR) to rule out HAEN being read-only
|
||||
this->wr(0x0B, b0 ^ 0x40);
|
||||
uint8_t a5 = this->rd(0x0A), b5 = this->rd(0x0B);
|
||||
this->wr(0x0B, b0);
|
||||
uint8_t a6 = this->rd(0x0A), b6 = this->rd(0x0B);
|
||||
ESP_LOGI(TAG, "[%s] IOCONB_TEST2 write B^0x40 -> A=0x%02X B=0x%02X | restore -> A=0x%02X B=0x%02X", this->label_,
|
||||
a5, b5, a6, b6);
|
||||
}
|
||||
|
||||
void count_change() { this->changes_++; }
|
||||
|
||||
protected:
|
||||
GPIOPin *pa_[2];
|
||||
binary_sensor::BinarySensor *pb_[2];
|
||||
bool isr_inta_{false};
|
||||
const char *label_{""};
|
||||
int phase_{0};
|
||||
uint32_t next_{0};
|
||||
int step_{0};
|
||||
bool pending_check_{false};
|
||||
bool level_[2]{false, false};
|
||||
uint8_t iocon_a_{0}, iocon_b_{0};
|
||||
uint32_t isr13_start_{0}, isr14_start_{0};
|
||||
uint32_t intb_low_now_{0}, inta_low_now_{0}, intb_low_late_{0}, inta_low_late_{0}, loop_ok_{0}, changes_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::mcptest
|
||||
@@ -0,0 +1,16 @@
|
||||
# INTA drives the hub's interrupt_pin; run.sh sets open_drain to true (int-od) or false (int-pp)
|
||||
substitutions:
|
||||
isr_inta: "false"
|
||||
open_drain: "true"
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
mcp23017:
|
||||
- id: mcp
|
||||
address: 0x27
|
||||
interrupt_pin:
|
||||
number: GPIO14
|
||||
mode:
|
||||
input: true
|
||||
open_drain_interrupt: ${open_drain}
|
||||
@@ -0,0 +1,11 @@
|
||||
# No interrupt_pin on the hub (polling); mcptest counts INTA edges itself
|
||||
substitutions:
|
||||
isr_inta: "true"
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
mcp23017:
|
||||
- id: mcp
|
||||
address: 0x27
|
||||
open_drain_interrupt: false
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# run.sh VARIANT LABEL: build VARIANT (int-od, int-pp or no-int) from TREE as mcp<VARIANT><LABEL>, flash the
|
||||
# ESP8266 and capture 14 s into out-mcp<VARIANT><LABEL>.txt, then print the mcptest SUMMARY and IOCONB_TEST lines.
|
||||
# The esptool reset keeps the expander powered, so a reflash shows whether register bits from the last build survive.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); V=${1:?variant}; L=${2:?label}
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; N=mcp${V//-/}$L
|
||||
case $V in
|
||||
int-od) F=int.yaml; X=(-s open_drain true) ;;
|
||||
int-pp) F=int.yaml; X=(-s open_drain false) ;;
|
||||
no-int) F=no-int.yaml; X=() ;;
|
||||
*) echo "VARIANT is int-od, int-pp or no-int"; exit 1 ;;
|
||||
esac
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome -s name "$N" "${X[@]}" run --no-logs "$R/$F" --device "$P" > "$R/out-$N-upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$R/out-$N-upload.log"; exit 1; }
|
||||
$PY "$SC" "$P" 14 "$R/out-$N.txt" > /dev/null 2>&1
|
||||
[ -s "$R/out-$N.txt" ] || { echo "serial capture got nothing"; exit 1; }
|
||||
grep -a "mcptest\|\[E\]\|\[W\]\|rst cause" "$R/out-$N.txt" | cut -c1-400
|
||||
grep -a -q "SUMMARY" "$R/out-$N.txt" || { echo "FAIL: no SUMMARY line"; exit 1; }
|
||||
grep -a -q "SUMMARY.*loopback_ok=20/20" "$R/out-$N.txt" || { echo "FAIL: loopback_ok is not 20/20"; exit 1; }
|
||||
! grep -a -q "\[E\]" "$R/out-$N.txt" || { echo "FAIL: [E] line in the capture"; exit 1; }
|
||||
# the boot after the upload may show one ROM line; a second one is a reset during the test
|
||||
[ "$(grep -a -c "rst cause" "$R/out-$N.txt")" -le 1 ] || { echo "FAIL: reset during the capture"; exit 1; }
|
||||
@@ -0,0 +1,49 @@
|
||||
# CLASSIC with a forced internal loopback window: the first force triggers the driver's recovery, the second lands
|
||||
# inside the next 5 s check interval, long enough for a 48-frame burst and the RTR check; then a chip reset
|
||||
substitutions:
|
||||
name: canlb-classic
|
||||
classic: "true"
|
||||
|
||||
packages:
|
||||
common: !include common-can.yaml
|
||||
|
||||
canbus:
|
||||
- platform: mcp251xfd
|
||||
id: can0
|
||||
cs_pin: GPIO32
|
||||
interrupt_pin: GPIO33
|
||||
can_id: 0x7FF
|
||||
mode: CLASSIC
|
||||
clock: 20MHZ
|
||||
bit_rate: 1000KBPS
|
||||
|
||||
script:
|
||||
- id: phases
|
||||
then:
|
||||
- delay: 20s
|
||||
- lambda: canlb::phase = canlb::PAUSED;
|
||||
- lambda: canlb::force_via_config(2, "lb1");
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return canlb::opmod() == 6;
|
||||
timeout: 8s
|
||||
- lambda: 'ESP_LOGI("canlb", "RECOVERY1 ok=%d", canlb::opmod() == 6);'
|
||||
- lambda: canlb::regs("after-lb1");
|
||||
- lambda: canlb::force_via_config(2, "lb2");
|
||||
- lambda: canlb::burst(48, "classic48");
|
||||
- delay: 300ms
|
||||
- lambda: canlb::queue_report("classic48");
|
||||
- lambda: canlb::rtr_test();
|
||||
- delay: 300ms
|
||||
- lambda: canlb::queue_report("rtr-classic");
|
||||
- lambda: canlb::phase = canlb::PAUSED;
|
||||
- delay: 8s
|
||||
- lambda: canlb::regs("after-window");
|
||||
- lambda: canlb::timing("pre-reset");
|
||||
- lambda: canlb::chip_reset();
|
||||
- lambda: canlb::watch_start(6);
|
||||
- delay: 10s
|
||||
- lambda: canlb::timing("reset+10s");
|
||||
- lambda: canlb::regs("reset+10s");
|
||||
- lambda: canlb::report();
|
||||
- lambda: ESP_LOGI("canlb", "PHASES DONE");
|
||||
@@ -0,0 +1,57 @@
|
||||
# Stream, RTR, TX objects, idle SPI cost, chip reset and two refused REQOP writes in LOOPBACK
|
||||
substitutions:
|
||||
name: canlb-loopback
|
||||
classic: "false"
|
||||
|
||||
packages:
|
||||
common: !include common-can.yaml
|
||||
|
||||
canbus:
|
||||
- platform: mcp251xfd
|
||||
id: can0
|
||||
cs_pin: GPIO32
|
||||
interrupt_pin: GPIO33
|
||||
can_id: 0x7FF
|
||||
mode: LOOPBACK
|
||||
clock: 20MHZ
|
||||
bit_rate: 1000KBPS
|
||||
data_bit_rate: 5000KBPS
|
||||
|
||||
script:
|
||||
- id: phases
|
||||
then:
|
||||
- delay: 20s
|
||||
- lambda: canlb::report();
|
||||
# Pause the stream and let the frame in flight drain before the queue checks
|
||||
- lambda: canlb::phase = canlb::PAUSED;
|
||||
- delay: 200ms
|
||||
- lambda: canlb::rtr_test();
|
||||
- delay: 500ms
|
||||
- lambda: canlb::queue_report("rtr");
|
||||
- lambda: canlb::txcheck_start();
|
||||
- delay: 3s
|
||||
- lambda: canlb::queue_report("txcheck");
|
||||
- lambda: canlb::phase = canlb::PAUSED; canlb::cs_mark();
|
||||
- delay: 20s
|
||||
- lambda: canlb::cs_report("idle");
|
||||
- lambda: canlb::resume();
|
||||
- delay: 5s
|
||||
- lambda: canlb::timing("pre-reset");
|
||||
- lambda: canlb::chip_reset();
|
||||
- lambda: canlb::watch_start(2);
|
||||
- delay: 10s
|
||||
- lambda: canlb::timing("reset+10s");
|
||||
- lambda: canlb::regs("reset+10s");
|
||||
- lambda: canlb::report();
|
||||
- repeat:
|
||||
count: 2
|
||||
then:
|
||||
- lambda: canlb::force_mode(3);
|
||||
- lambda: canlb::watch_start(2);
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return !canlb::watching;
|
||||
timeout: 15s
|
||||
- delay: 10s
|
||||
- lambda: canlb::report();
|
||||
- lambda: ESP_LOGI("canlb", "PHASES DONE");
|
||||
@@ -0,0 +1,60 @@
|
||||
# NORMAL with no node acknowledging, so the controller cycles error passive and bus-off; three forced REQOP faults
|
||||
# and three chip resets, each sent while TREC shows bus-off
|
||||
substitutions:
|
||||
name: canlb-normal
|
||||
classic: "false"
|
||||
|
||||
packages:
|
||||
common: !include common-can.yaml
|
||||
|
||||
canbus:
|
||||
- platform: mcp251xfd
|
||||
id: can0
|
||||
cs_pin: GPIO32
|
||||
interrupt_pin: GPIO33
|
||||
can_id: 0x7FF
|
||||
mode: NORMAL
|
||||
clock: 20MHZ
|
||||
bit_rate: 1000KBPS
|
||||
data_bit_rate: 2000KBPS
|
||||
|
||||
script:
|
||||
- id: phases
|
||||
then:
|
||||
- delay: 10s
|
||||
- repeat:
|
||||
count: 3
|
||||
then:
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return canlb::busoff();
|
||||
timeout: 30s
|
||||
- lambda: 'if (!canlb::busoff()) ESP_LOGW("canlb", "NOTBUSOFF before fault");'
|
||||
- lambda: 'ESP_LOGI("canlb", "FAULTINBUSOFF TREC=%08X", (unsigned) canlb::rd32(0x034));'
|
||||
- lambda: canlb::force_mode(3);
|
||||
- lambda: canlb::watch_start(0);
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return !canlb::watching;
|
||||
timeout: 20s
|
||||
- delay: 3s
|
||||
- repeat:
|
||||
count: 3
|
||||
then:
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return canlb::busoff();
|
||||
timeout: 30s
|
||||
- lambda: 'if (!canlb::busoff()) ESP_LOGW("canlb", "NOTBUSOFF before reset");'
|
||||
- lambda: 'ESP_LOGI("canlb", "RESETINBUSOFF TREC=%08X", (unsigned) canlb::rd32(0x034));'
|
||||
- lambda: canlb::chip_reset();
|
||||
- lambda: canlb::watch_start(0);
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return !canlb::watching;
|
||||
timeout: 20s
|
||||
- lambda: canlb::timing("reset-after");
|
||||
- lambda: canlb::regs("reset-after");
|
||||
- delay: 3s
|
||||
- lambda: canlb::report();
|
||||
- lambda: ESP_LOGI("canlb", "PHASES DONE");
|
||||
@@ -0,0 +1,483 @@
|
||||
#pragma once
|
||||
// canlb: MCP251xFD loopback checker. Stream frames carry a sequence number; other phases check against a queue.
|
||||
#include <cstring>
|
||||
#include <driver/pulse_cnt.h>
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/canbus/canbus.h"
|
||||
#include "esphome/components/mcp251xfd/mcp251xfd.h"
|
||||
namespace canlb {
|
||||
using esphome::canbus::CanFrame;
|
||||
static constexpr uint8_t DLC_LEN[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64};
|
||||
struct Stats {
|
||||
uint32_t sent, send_err, rx, lost, bad_id, bad_len, bad_flags, bad_data, lat_sum, lat_max, lat_n;
|
||||
};
|
||||
inline Stats st{};
|
||||
inline esphome::canbus::Canbus *bus = nullptr;
|
||||
inline esphome::mcp251xfd::MCP251XFD *chip = nullptr;
|
||||
inline uint32_t seq = 0, expect = 0, t_sent = 0, inflight = 0;
|
||||
inline bool classic = false;
|
||||
enum Phase { STREAM, PAUSED, QUEUE };
|
||||
inline Phase phase = STREAM;
|
||||
|
||||
inline void set_fd(CanFrame &f, bool fd, bool brs) {
|
||||
#ifdef USE_CANBUS_FD
|
||||
f.fd = fd;
|
||||
f.bit_rate_switch = brs;
|
||||
#endif
|
||||
}
|
||||
inline bool is_fd(const CanFrame &f) {
|
||||
#ifdef USE_CANBUS_FD
|
||||
return f.fd;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
inline bool is_brs(const CanFrame &f) {
|
||||
#ifdef USE_CANBUS_FD
|
||||
return f.bit_rate_switch;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
inline void fill(CanFrame &f, uint32_t s) {
|
||||
f = CanFrame{};
|
||||
f.use_extended_id = s & 1;
|
||||
f.can_id = f.use_extended_id ? (0x1ABC000 | (s & 0xFFF)) : (0x100 | (s & 0xFF));
|
||||
const bool fd = !classic && (s % 4) != 3;
|
||||
set_fd(f, fd, fd && (s & 2));
|
||||
uint8_t dlc = s % (fd ? 16 : 9);
|
||||
f.can_data_length_code = dlc;
|
||||
uint8_t len = fd ? DLC_LEN[dlc] : dlc;
|
||||
for (uint8_t i = 0; i < len; i++)
|
||||
f.data[i] = uint8_t(s * 31 + i * 7);
|
||||
if (len >= 4) {
|
||||
f.data[0] = s >> 24;
|
||||
f.data[1] = s >> 16;
|
||||
f.data[2] = s >> 8;
|
||||
f.data[3] = s;
|
||||
}
|
||||
}
|
||||
// returns 0 if equal, else 1 id, 2 len, 3 flags, 4 data
|
||||
inline int compare(const CanFrame &got, const CanFrame &want) {
|
||||
if (got.can_id != want.can_id || got.use_extended_id != want.use_extended_id)
|
||||
return 1;
|
||||
if (got.can_data_length_code != want.can_data_length_code)
|
||||
return 2;
|
||||
if (is_fd(got) != is_fd(want) || is_brs(got) != is_brs(want) ||
|
||||
got.remote_transmission_request != want.remote_transmission_request)
|
||||
return 3;
|
||||
if (!want.remote_transmission_request && memcmp(got.data, want.data, want.payload_length()) != 0)
|
||||
return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// expected queue for the non-stream phases
|
||||
inline CanFrame q[64];
|
||||
inline uint32_t q_rtr = 0, q_rtr_dirty = 0, q_send_err = 0;
|
||||
inline uint32_t q_head = 0, q_tail = 0, q_rx = 0, q_bad = 0, q_stray = 0, q_tx_bad = 0, q_tx_ok = 0;
|
||||
inline void q_reset() {
|
||||
q_head = q_tail = q_rx = q_bad = q_stray = q_tx_bad = q_tx_ok = 0;
|
||||
q_rtr = q_rtr_dirty = q_send_err = 0;
|
||||
}
|
||||
|
||||
inline void on_frame(const CanFrame &got) {
|
||||
if (phase == QUEUE || phase == PAUSED) {
|
||||
if (q_head == q_tail) {
|
||||
q_stray++;
|
||||
return;
|
||||
}
|
||||
const CanFrame &w = q[q_head++ % 64];
|
||||
q_rx++;
|
||||
if (got.remote_transmission_request) {
|
||||
q_rtr++;
|
||||
bool zero = true;
|
||||
for (uint8_t k = 0; k < got.can_data_length_code && k < 8; k++)
|
||||
zero = zero && got.data[k] == 0;
|
||||
if (!zero) {
|
||||
q_rtr_dirty++;
|
||||
ESP_LOGW("canlb", "RTRDIRTY dlc=%u data=%02X %02X %02X %02X %02X %02X %02X %02X", got.can_data_length_code,
|
||||
got.data[0], got.data[1], got.data[2], got.data[3], got.data[4], got.data[5], got.data[6],
|
||||
got.data[7]);
|
||||
}
|
||||
}
|
||||
if (compare(got, w) != 0) {
|
||||
if (q_bad < 5)
|
||||
ESP_LOGW("canlb", "QBAD id=%x ext=%d dlc=%u/%u rtr=%d/%d fd=%d/%d code=%d", (unsigned) got.can_id,
|
||||
got.use_extended_id, got.can_data_length_code, w.can_data_length_code, got.remote_transmission_request,
|
||||
w.remote_transmission_request, is_fd(got), is_fd(w), compare(got, w));
|
||||
q_bad++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
st.rx++;
|
||||
CanFrame want;
|
||||
fill(want, expect);
|
||||
switch (compare(got, want)) {
|
||||
case 1:
|
||||
st.bad_id++;
|
||||
break;
|
||||
case 2:
|
||||
st.bad_len++;
|
||||
break;
|
||||
case 3:
|
||||
st.bad_flags++;
|
||||
break;
|
||||
case 4:
|
||||
st.bad_data++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
uint32_t lat = esphome::micros() - t_sent;
|
||||
st.lat_sum += lat;
|
||||
st.lat_n++;
|
||||
if (lat > st.lat_max)
|
||||
st.lat_max = lat;
|
||||
expect++;
|
||||
if (inflight)
|
||||
inflight--;
|
||||
}
|
||||
|
||||
// chip select counter on the CS GPIO, which stays an output
|
||||
inline pcnt_unit_handle_t cs_unit = nullptr;
|
||||
inline void cs_init(int gpio) {
|
||||
pcnt_unit_config_t uc = {};
|
||||
uc.low_limit = -30000;
|
||||
uc.high_limit = 30000;
|
||||
uc.flags.accum_count = true;
|
||||
pcnt_chan_config_t cc = {};
|
||||
cc.edge_gpio_num = gpio;
|
||||
cc.level_gpio_num = -1;
|
||||
pcnt_channel_handle_t ch = nullptr;
|
||||
esp_err_t e = pcnt_new_unit(&uc, &cs_unit);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_new_channel(cs_unit, &cc, &ch);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_channel_set_edge_action(ch, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_unit_add_watch_point(cs_unit, 30000);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_unit_add_watch_point(cs_unit, -30000);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_unit_enable(cs_unit);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_unit_clear_count(cs_unit);
|
||||
if (e == ESP_OK)
|
||||
e = pcnt_unit_start(cs_unit);
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGE("canlb", "CS counter setup failed: %s", esp_err_to_name(e));
|
||||
cs_unit = nullptr;
|
||||
}
|
||||
}
|
||||
// False when there is no counter or the read failed
|
||||
inline bool cs_count(int &c) {
|
||||
c = 0;
|
||||
if (cs_unit == nullptr)
|
||||
return false;
|
||||
const esp_err_t e = pcnt_unit_get_count(cs_unit, &c);
|
||||
if (e != ESP_OK)
|
||||
ESP_LOGW("canlb", "CS counter read failed: %s", esp_err_to_name(e));
|
||||
return e == ESP_OK;
|
||||
}
|
||||
inline int cs_mark_count = 0;
|
||||
inline bool cs_mark_ok = false;
|
||||
inline uint32_t cs_mark_time = 0;
|
||||
inline void cs_mark() {
|
||||
cs_mark_ok = cs_count(cs_mark_count);
|
||||
cs_mark_time = esphome::millis();
|
||||
}
|
||||
inline void cs_report(const char *tag) {
|
||||
int c;
|
||||
if (!cs_mark_ok || !cs_count(c)) {
|
||||
ESP_LOGW("canlb", "CSRATE %s unavailable (no CS counter or a failed read)", tag);
|
||||
return;
|
||||
}
|
||||
const uint32_t ms = esphome::millis() - cs_mark_time;
|
||||
ESP_LOGI("canlb", "CSRATE %s transactions=%d in %u ms = %.2f/s", tag, c - cs_mark_count, (unsigned) ms,
|
||||
(c - cs_mark_count) * 1000.0f / ms);
|
||||
}
|
||||
|
||||
inline uint32_t start_ms = 0;
|
||||
inline void start(esphome::mcp251xfd::MCP251XFD *b) {
|
||||
bus = b;
|
||||
chip = b;
|
||||
start_ms = esphome::millis();
|
||||
b->add_frame_callback([](const CanFrame &f) { on_frame(f); });
|
||||
}
|
||||
inline void txcheck_step();
|
||||
inline bool watching = false;
|
||||
inline uint8_t watch_target = 0;
|
||||
inline uint32_t watch_t0 = 0;
|
||||
inline void watch_check();
|
||||
inline void step() {
|
||||
if (bus == nullptr)
|
||||
return;
|
||||
if (watching)
|
||||
watch_check();
|
||||
if (phase == QUEUE) {
|
||||
txcheck_step();
|
||||
return;
|
||||
}
|
||||
// Hold the stream until the boot dump_config is out, or a frame stalled past the 50 ms timeout shifts the count
|
||||
if (phase != STREAM || esphome::millis() - start_ms < 5000)
|
||||
return;
|
||||
const uint32_t now = esphome::micros();
|
||||
if (inflight) {
|
||||
if (now - t_sent < 50000)
|
||||
return;
|
||||
st.lost += inflight;
|
||||
expect = seq;
|
||||
inflight = 0;
|
||||
}
|
||||
uint32_t n = (seq % 64 == 0) ? 8 : 1;
|
||||
t_sent = now;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
CanFrame f;
|
||||
fill(f, seq);
|
||||
if (bus->send_data(f) != esphome::canbus::ERROR_OK) {
|
||||
st.send_err++;
|
||||
break;
|
||||
}
|
||||
st.sent++;
|
||||
seq++;
|
||||
inflight++;
|
||||
}
|
||||
}
|
||||
inline void resume() {
|
||||
phase = STREAM;
|
||||
expect = seq;
|
||||
inflight = 0;
|
||||
}
|
||||
inline void report() {
|
||||
ESP_LOGI(
|
||||
"canlb",
|
||||
"STATS sent=%u send_err=%u rx=%u lost=%u bad_id=%u bad_len=%u bad_flags=%u bad_data=%u lat_avg=%uus lat_max=%uus",
|
||||
st.sent, st.send_err, st.rx, st.lost, st.bad_id, st.bad_len, st.bad_flags, st.bad_data,
|
||||
st.lat_n ? st.lat_sum / st.lat_n : 0, st.lat_max);
|
||||
}
|
||||
|
||||
// raw register access, bypassing the driver
|
||||
inline void rdbuf(uint16_t a, uint8_t *out, size_t n) {
|
||||
uint8_t b[2 + 80] = {uint8_t(0x30 | (a >> 8)), uint8_t(a)};
|
||||
memset(b + 2, 0, n);
|
||||
chip->enable();
|
||||
chip->transfer_array(b, 2 + n);
|
||||
chip->disable();
|
||||
memcpy(out, b + 2, n);
|
||||
}
|
||||
inline uint32_t rd32(uint16_t a) {
|
||||
uint8_t b[4];
|
||||
rdbuf(a, b, 4);
|
||||
return b[0] | (b[1] << 8) | (b[2] << 16) | (uint32_t(b[3]) << 24);
|
||||
}
|
||||
inline void wr8(uint16_t a, uint8_t v) {
|
||||
uint8_t b[3] = {uint8_t(0x20 | (a >> 8)), uint8_t(a), v};
|
||||
chip->enable();
|
||||
chip->write_array(b, 3);
|
||||
chip->disable();
|
||||
}
|
||||
// TREC TXBO: bus-off, also set while the controller sits in Configuration mode
|
||||
inline bool busoff() { return (rd32(0x034) & 0x00200000) != 0; }
|
||||
inline uint8_t opmod() { return (rd32(0x000) >> 21) & 7; }
|
||||
inline bool wait_opmod(uint8_t m) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if (opmod() == m)
|
||||
return true;
|
||||
esphome::delay(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
inline void regs(const char *tag) {
|
||||
ESP_LOGI("canlb", "REGS %s CON=%08X OSC=%08X TXFIFOCON=%08X TXFIFOSTA=%08X RXFIFOCON=%08X RXFIFOSTA=%08X", tag,
|
||||
(unsigned) rd32(0x000), (unsigned) rd32(0xE00), (unsigned) rd32(0x05C), (unsigned) rd32(0x060),
|
||||
(unsigned) rd32(0x068), (unsigned) rd32(0x06C));
|
||||
}
|
||||
inline void force_mode(uint8_t m) {
|
||||
wr8(0x003, m);
|
||||
ESP_LOGI("canlb", "FORCE REQOP %u, CON now %08X", m, (unsigned) rd32(0x000));
|
||||
}
|
||||
// through Configuration, so the chip accepts it
|
||||
inline void force_via_config(uint8_t m, const char *tag) {
|
||||
wr8(0x003, 4);
|
||||
const bool c = wait_opmod(4);
|
||||
wr8(0x003, m);
|
||||
const bool l = wait_opmod(m);
|
||||
ESP_LOGI("canlb", "FORCEVIA %s config=%d mode%u=%d CON=%08X", tag, c, m, l, (unsigned) rd32(0x000));
|
||||
}
|
||||
|
||||
// send n frames without letting the loop drain RX
|
||||
inline void burst(uint32_t n, const char *tag) {
|
||||
phase = QUEUE;
|
||||
q_reset();
|
||||
const uint32_t before = bus->get_status().rx_overrun_count;
|
||||
uint32_t sent = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
CanFrame f = CanFrame{};
|
||||
f.use_extended_id = i & 1;
|
||||
f.can_id = f.use_extended_id ? (0x1F000000 | i) : (0x600 | i);
|
||||
set_fd(f, !classic, false);
|
||||
f.can_data_length_code = classic ? 8 : 15;
|
||||
for (uint8_t k = 0; k < f.payload_length(); k++)
|
||||
f.data[k] = uint8_t(i * 13 + k);
|
||||
uint32_t t0 = esphome::micros();
|
||||
esphome::canbus::Error e;
|
||||
while ((e = bus->send_data(f)) == esphome::canbus::ERROR_ALLTXBUSY && esphome::micros() - t0 < 20000) {
|
||||
}
|
||||
if (e != esphome::canbus::ERROR_OK) {
|
||||
q_send_err++;
|
||||
ESP_LOGW("canlb", "BURST %s send error %d at frame %u", tag, (int) e, (unsigned) i);
|
||||
break;
|
||||
}
|
||||
q[q_tail++ % 64] = f;
|
||||
sent++;
|
||||
}
|
||||
esphome::delay(20);
|
||||
ESP_LOGI("canlb", "BURST %s sent=%u overrun_before=%u", tag, sent, before);
|
||||
}
|
||||
inline void queue_report(const char *tag) {
|
||||
ESP_LOGI(
|
||||
"canlb",
|
||||
"QUEUE %s rx=%u bad=%u stray=%u missing=%u tx_obj_ok=%u tx_obj_bad=%u rtr=%u rtr_dirty=%u send_err=%u overrun=%u",
|
||||
tag, q_rx, q_bad, q_stray, q_tail - q_head, q_tx_ok, q_tx_bad, q_rtr, q_rtr_dirty, q_send_err,
|
||||
bus->get_status().rx_overrun_count);
|
||||
}
|
||||
|
||||
// send one frame and check the TX object the driver wrote into chip RAM, including the padding
|
||||
inline void tx_one(const CanFrame &f) {
|
||||
const uint32_t ua = rd32(0x064) & 0xFFF;
|
||||
if (bus->send_data(f) != esphome::canbus::ERROR_OK) {
|
||||
q_tx_bad++;
|
||||
ESP_LOGW("canlb", "TXSEND failed");
|
||||
return;
|
||||
}
|
||||
q[q_tail++ % 64] = f;
|
||||
esphome::delay(2);
|
||||
uint8_t obj[72];
|
||||
rdbuf(0x400 + ua, obj, 72);
|
||||
uint32_t id = f.use_extended_id ? ((f.can_id >> 18) | ((f.can_id & 0x3FFFF) << 11)) : f.can_id;
|
||||
uint32_t flags = f.can_data_length_code | (f.use_extended_id << 4) | (f.remote_transmission_request << 5) |
|
||||
(is_brs(f) << 6) | (is_fd(f) << 7);
|
||||
const uint32_t got_id = obj[0] | (obj[1] << 8) | (obj[2] << 16) | (uint32_t(obj[3]) << 24);
|
||||
const uint32_t got_flags = obj[4] | (obj[5] << 8);
|
||||
const size_t len = f.remote_transmission_request ? 0 : f.payload_length();
|
||||
const size_t padded = (len + 3) & ~size_t{3};
|
||||
bool ok = got_id == id && (got_flags & 0xFF) == flags && memcmp(obj + 8, f.data, len) == 0;
|
||||
for (size_t k = len; k < padded; k++)
|
||||
ok = ok && obj[8 + k] == 0;
|
||||
if (ok)
|
||||
q_tx_ok++;
|
||||
else {
|
||||
q_tx_bad++;
|
||||
// A 64-byte payload fills the object, so there are no pad bytes to show
|
||||
auto pad = [&](size_t k) -> unsigned { return 8 + len + k < sizeof(obj) ? obj[8 + len + k] : 0; };
|
||||
ESP_LOGW("canlb", "TXOBJ bad id=%08X/%08X flags=%02X/%02X len=%u pad=%02X %02X %02X", (unsigned) got_id,
|
||||
(unsigned) id, (unsigned) (got_flags & 0xFF), (unsigned) flags, (unsigned) len, pad(0), pad(1), pad(2));
|
||||
}
|
||||
}
|
||||
inline void tx_dirty(bool ext) {
|
||||
CanFrame d = CanFrame{};
|
||||
d.use_extended_id = ext;
|
||||
d.can_id = ext ? 0x1EEEEEE : 0x5AA;
|
||||
set_fd(d, !classic, false);
|
||||
d.can_data_length_code = classic ? 8 : 15;
|
||||
memset(d.data, 0xAA, sizeof(d.data));
|
||||
tx_one(d);
|
||||
}
|
||||
// every DLC, classic and FD, RTR, both ID types; each case right after a frame full of 0xAA. Stepped from step().
|
||||
inline int tc_ext = 0, tc_kind = 0, tc_dlc = 0;
|
||||
inline bool tc_active = false;
|
||||
inline uint32_t tc_cases = 0;
|
||||
inline void txcheck_start() {
|
||||
phase = QUEUE;
|
||||
q_reset();
|
||||
tc_ext = tc_kind = tc_dlc = 0;
|
||||
tc_cases = 0;
|
||||
tc_active = true;
|
||||
}
|
||||
inline void txcheck_step() {
|
||||
if (!tc_active)
|
||||
return;
|
||||
if (classic && tc_kind >= 2) {
|
||||
tc_kind = 0;
|
||||
tc_ext++;
|
||||
}
|
||||
if (tc_ext >= 2) {
|
||||
tc_active = false;
|
||||
ESP_LOGI("canlb", "TXCHECK cases=%u", tc_cases);
|
||||
return;
|
||||
}
|
||||
tx_dirty(tc_ext);
|
||||
CanFrame f = CanFrame{};
|
||||
f.use_extended_id = tc_ext;
|
||||
f.can_id = tc_ext ? (0x1C000000 | (tc_kind << 8) | tc_dlc) : (0x700 | (tc_kind << 4) | tc_dlc);
|
||||
f.remote_transmission_request = tc_kind == 1;
|
||||
set_fd(f, tc_kind >= 2, tc_kind == 3);
|
||||
f.can_data_length_code = tc_dlc;
|
||||
if (!f.remote_transmission_request)
|
||||
for (uint8_t k = 0; k < f.payload_length(); k++)
|
||||
f.data[k] = uint8_t(0x30 + tc_dlc + k);
|
||||
tx_one(f);
|
||||
tc_cases++;
|
||||
if (++tc_dlc > (tc_kind >= 2 ? 15 : 8)) {
|
||||
tc_dlc = 0;
|
||||
if (++tc_kind >= 4) {
|
||||
tc_kind = 0;
|
||||
tc_ext++;
|
||||
}
|
||||
}
|
||||
}
|
||||
inline void watch_start(uint8_t target) {
|
||||
watch_target = target;
|
||||
watch_t0 = esphome::micros();
|
||||
watching = true;
|
||||
}
|
||||
inline void watch_check() {
|
||||
const uint32_t con = rd32(0x000);
|
||||
const uint8_t op = (con >> 21) & 7, req = (con >> 24) & 7;
|
||||
const uint32_t ms = (esphome::micros() - watch_t0) / 1000;
|
||||
if (op == watch_target && req == watch_target) {
|
||||
ESP_LOGI("canlb", "RESTORED to %u after %u ms", watch_target, (unsigned) ms);
|
||||
watching = false;
|
||||
} else if (ms > 12000) {
|
||||
ESP_LOGW("canlb", "NOT RESTORED after %u ms, CON=%08X", (unsigned) ms, (unsigned) con);
|
||||
watching = false;
|
||||
}
|
||||
}
|
||||
// data frame with distinct bytes, then a classic RTR, for DLC 4 to 8
|
||||
inline void rtr_test() {
|
||||
phase = QUEUE;
|
||||
q_reset();
|
||||
for (uint8_t dlc = 4; dlc <= 8; dlc++) {
|
||||
CanFrame d = CanFrame{};
|
||||
d.can_id = 0x321;
|
||||
d.can_data_length_code = 8;
|
||||
for (uint8_t k = 0; k < 8; k++)
|
||||
d.data[k] = 0x11 * (k + 1);
|
||||
CanFrame r = CanFrame{};
|
||||
r.can_id = 0x322;
|
||||
r.remote_transmission_request = true;
|
||||
r.can_data_length_code = dlc;
|
||||
for (const CanFrame &f : {d, r}) {
|
||||
const auto e = bus->send_data(f);
|
||||
if (e == esphome::canbus::ERROR_OK) {
|
||||
q[q_tail++ % 64] = f;
|
||||
} else {
|
||||
q_send_err++;
|
||||
ESP_LOGW("canlb", "RTR test send failed: %d", (int) e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
inline void chip_reset() {
|
||||
uint8_t b[2] = {0, 0};
|
||||
chip->enable();
|
||||
chip->write_array(b, 2);
|
||||
chip->disable();
|
||||
ESP_LOGI("canlb", "CHIPRESET sent, CON now %08X", (unsigned) rd32(0x000));
|
||||
}
|
||||
inline void timing(const char *tag) {
|
||||
ESP_LOGI("canlb", "TIMING %s NBTCFG=%08X DBTCFG=%08X TDC=%08X heap_free=%u", tag, (unsigned) rd32(0x004),
|
||||
(unsigned) rd32(0x008), (unsigned) rd32(0x00C), (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
|
||||
}
|
||||
} // namespace canlb
|
||||
@@ -0,0 +1,46 @@
|
||||
# Package: ESP32-PICO-D4 wired to the MCP2518FD (SCK 25, SDI 26, SDO 27, nCS 32, INT 33, CLK 34); each config adds
|
||||
# its canbus block and a `phases` script, run once the stream has started
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- canlb.hpp
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("ram", "BOOTFREE %u TOTAL %u", (unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
|
||||
(unsigned) heap_caps_get_total_size(MALLOC_CAP_INTERNAL));
|
||||
canlb::classic = ${classic};
|
||||
canlb::start(id(can0));
|
||||
canlb::cs_init(32);
|
||||
canlb::regs("boot");
|
||||
- script.execute: phases
|
||||
|
||||
esp32:
|
||||
board: pico32
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO25
|
||||
mosi_pin: GPIO26
|
||||
miso_pin: GPIO27
|
||||
|
||||
interval:
|
||||
- interval: 2ms
|
||||
then:
|
||||
- lambda: canlb::step();
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: canlb::report();
|
||||
|
||||
sensor:
|
||||
# CLKO: SYSCLK / 10 at reset, so 2 MHz with a 20 MHz crystal
|
||||
- platform: pulse_counter
|
||||
pin: GPIO34
|
||||
name: clko
|
||||
internal_filter: 0us
|
||||
update_interval: 60s
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# run.sh CONFIG LABEL [SECONDS]: build CONFIG (can-loopback, can-classic or can-normal-busoff) from TREE as
|
||||
# <CONFIG>-<LABEL>, flash the PICO-D4, capture SECONDS (default 140) plus two more resets for the boot heap line.
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
R=$(cd "$(dirname "$0")" && pwd); C=${1:?config}; L=${2:?label}; S=${3:-140}; T=${TREE:?set TREE}; P=${PORT:?set PORT}
|
||||
PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; NAME=$C-$L; OUT=$R/out-$NAME; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
sed -E "s/^ name: .*/ name: $NAME/" "$R/$C.yaml" > "$R/$NAME.yaml"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome run --no-logs "$R/$NAME.yaml" --device "$P" > "$OUT/upload.log" 2>&1) || { echo "build or upload failed"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$OUT/upload.log"
|
||||
$PY "$SC" "$P" "$S" "$OUT/serial.txt" --reset > /dev/null 2>&1 || { echo "serial capture got nothing"; exit 1; }
|
||||
for b in 2 3; do $PY "$SC" "$P" 8 "$OUT/boot$b.txt" --reset > /dev/null 2>&1 || echo "boot$b capture got nothing"; done
|
||||
grep -a -h -o "BOOTFREE [0-9]* TOTAL [0-9]*" "$OUT/serial.txt" "$OUT/boot2.txt" "$OUT/boot3.txt"
|
||||
grep -a -E "canlb|mcp251xfd" "$OUT/serial.txt" | grep -a -v "failed with error" | sed 's/^.*\]\[/[/' | cut -c1-200
|
||||
grep -a -q "PHASES DONE" "$OUT/serial.txt" || { echo "FAIL: phases did not finish within $S s"; exit 1; }
|
||||
echo "spi_device lines: $(grep -a -c spi_device "$OUT/serial.txt") failed sends: $(grep -a -c 'failed with error' "$OUT/serial.txt")"
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Hold a plaintext API connection to the device; optionally send light transitions.
|
||||
|
||||
usage: apiclient.py HOST SECONDS [--every S] [--transition S]
|
||||
With --every, alternates the first light between 30% and 70% brightness every S seconds with the given transition.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aioesphomeapi import LightInfo
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
||||
from apiconn import connect # noqa: E402
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"{time.strftime('%H:%M:%S')} {msg}", flush=True)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("host")
|
||||
ap.add_argument("seconds", type=float)
|
||||
ap.add_argument("--every", type=float, default=0)
|
||||
ap.add_argument("--transition", type=float, default=1.0)
|
||||
a = ap.parse_args()
|
||||
cli = await connect(a.host)
|
||||
ents, _ = await cli.list_entities_services()
|
||||
key = next(e.key for e in ents if isinstance(e, LightInfo))
|
||||
states = 0
|
||||
|
||||
def on_state(_state: object) -> None:
|
||||
nonlocal states
|
||||
states += 1
|
||||
|
||||
cli.subscribe_states(on_state)
|
||||
log("connected")
|
||||
end = time.monotonic() + a.seconds
|
||||
cmds = 0
|
||||
while time.monotonic() < end:
|
||||
if not a.every:
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
b = (0.3, 0.7)[cmds % 2]
|
||||
cli.light_command(key, state=True, brightness=b, transition_length=a.transition)
|
||||
log(f"cmd brightness={b} transition={a.transition}")
|
||||
cmds += 1
|
||||
await asyncio.sleep(a.every)
|
||||
log(f"done states={states} cmds={cmds}")
|
||||
await cli.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
# ESP32-C6: a 20 kHz ledc output on GPIO14 behind a monochromatic light, captured period by period on the same pad
|
||||
# by pwmcap.hpp. Substitutions: name, ps (wifi power_save_mode).
|
||||
esphome:
|
||||
name: ${name}
|
||||
includes:
|
||||
- pwmcap.hpp
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lambda: pwmcap::start(14);
|
||||
- light.turn_on:
|
||||
id: lamp
|
||||
brightness: 50%
|
||||
transition_length: 0s
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
board: esp32-c6-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
include_builtin_idf_components: [esp_driver_rmt, esp_driver_pcnt]
|
||||
sdkconfig_options:
|
||||
CONFIG_RMT_ISR_IRAM_SAFE: y
|
||||
CONFIG_PCNT_ISR_IRAM_SAFE: y
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
power_save_mode: ${ps}
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO14
|
||||
id: pwm_output
|
||||
frequency: 20000 Hz
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
id: lamp
|
||||
name: Lamp
|
||||
output: pwm_output
|
||||
default_transition_length: 1000ms
|
||||
|
||||
interval:
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: pwmcap::report();
|
||||
@@ -0,0 +1,296 @@
|
||||
#pragma once
|
||||
// Captures every period of the LEDC pin on the same chip: RMT RX at 80 MHz (12.5 ns ticks) through the GPIO
|
||||
// matrix, analysed in the IRAM-safe RMT ISR, plus PCNT counting rising edges as an independent period count.
|
||||
// Assumes LEDC timer 0 channel 0 drives the pad; report() logs one WIN line per call.
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
#include "driver/rmt_rx.h"
|
||||
#include "driver/pulse_cnt.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "hal/gpio_ll.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_attr.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "soc/ledc_struct.h"
|
||||
#include "soc/pcr_struct.h"
|
||||
#include "driver/ledc.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace pwmcap {
|
||||
static const char *const TAG = "pwmcap";
|
||||
static constexpr int TOL = 3; // ticks; one LEDC step at 11 bits is about 1.95 ticks
|
||||
static constexpr int HBINS = 4200;
|
||||
static constexpr int PBASE = 3800, PBINS = 400;
|
||||
static constexpr size_t RXBUF = 1024;
|
||||
static constexpr size_t CHG = 2048;
|
||||
|
||||
struct Win {
|
||||
uint32_t hist_h[HBINS];
|
||||
uint32_t hist_p[PBINS];
|
||||
uint32_t p_under, p_over, h_over;
|
||||
uint32_t n, zero_dur, chunks;
|
||||
uint32_t pmin, pmax, hmin, hmax;
|
||||
// level changes: |h - level| > TOL
|
||||
uint32_t chg_n;
|
||||
uint32_t chg_idx[CHG];
|
||||
uint16_t chg_h[CHG];
|
||||
};
|
||||
|
||||
static DRAM_ATTR Win wins[2];
|
||||
static DRAM_ATTR volatile int cur = 0;
|
||||
static DRAM_ATTR rmt_symbol_word_t rxbuf[RXBUF];
|
||||
static DRAM_ATTR volatile uint32_t total_idx = 0;
|
||||
static DRAM_ATTR volatile int level = -1;
|
||||
static DRAM_ATTR volatile bool skip_first = true;
|
||||
static DRAM_ATTR volatile uint32_t idle_ends = 0;
|
||||
static rmt_channel_handle_t rx = nullptr;
|
||||
static pcnt_unit_handle_t pcnt = nullptr;
|
||||
static rmt_receive_config_t rcfg{};
|
||||
static int64_t win_start = 0;
|
||||
static int last_pcnt = 0;
|
||||
static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static volatile bool need_rearm = false;
|
||||
|
||||
static void reset_win(Win &w) {
|
||||
memset(&w, 0, sizeof(w));
|
||||
w.pmin = UINT32_MAX;
|
||||
w.hmin = UINT32_MAX;
|
||||
}
|
||||
|
||||
static bool IRAM_ATTR on_rx(rmt_channel_handle_t, const rmt_rx_done_event_data_t *ev, void *) {
|
||||
Win &w = wins[cur];
|
||||
w.chunks++;
|
||||
size_t n = ev->num_symbols;
|
||||
const rmt_symbol_word_t *s = ev->received_symbols;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint32_t d0 = s[i].duration0, d1 = s[i].duration1;
|
||||
if (skip_first) {
|
||||
skip_first = false;
|
||||
continue;
|
||||
}
|
||||
if (d0 == 0 || d1 == 0) {
|
||||
w.zero_dur++;
|
||||
continue;
|
||||
}
|
||||
uint32_t h = s[i].level0 ? d0 : d1;
|
||||
uint32_t p = d0 + d1;
|
||||
uint32_t idx = total_idx++;
|
||||
w.n++;
|
||||
if (h < HBINS)
|
||||
w.hist_h[h]++;
|
||||
else
|
||||
w.h_over++;
|
||||
if (p < PBASE)
|
||||
w.p_under++;
|
||||
else if (p >= PBASE + PBINS)
|
||||
w.p_over++;
|
||||
else
|
||||
w.hist_p[p - PBASE]++;
|
||||
if (p < w.pmin)
|
||||
w.pmin = p;
|
||||
if (p > w.pmax)
|
||||
w.pmax = p;
|
||||
if (h < w.hmin)
|
||||
w.hmin = h;
|
||||
if (h > w.hmax)
|
||||
w.hmax = h;
|
||||
int lv = level;
|
||||
int dh = (int) h - lv;
|
||||
if (lv < 0 || dh > TOL || dh < -TOL) {
|
||||
level = (int) h;
|
||||
if (w.chg_n < CHG) {
|
||||
w.chg_idx[w.chg_n] = idx;
|
||||
w.chg_h[w.chg_n] = (uint16_t) h;
|
||||
}
|
||||
w.chg_n++;
|
||||
}
|
||||
}
|
||||
if (ev->flags.is_last) {
|
||||
idle_ends++;
|
||||
need_rearm = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mode of a histogram whose bin i counts value base + i, the count more than TOL from the mode and the largest
|
||||
// such distance.
|
||||
struct HistStats {
|
||||
uint32_t mode, out, dev;
|
||||
};
|
||||
|
||||
static HistStats hist_stats(const uint32_t *hist, int bins, int base) {
|
||||
HistStats st{0, 0, 0};
|
||||
uint32_t best = 0;
|
||||
for (int i = 0; i < bins; i++) {
|
||||
if (hist[i] > best) {
|
||||
best = hist[i];
|
||||
st.mode = i + base;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < bins; i++) {
|
||||
int d = i + base - (int) st.mode;
|
||||
uint32_t ad = d < 0 ? -d : d;
|
||||
if (ad > (uint32_t) TOL && hist[i]) {
|
||||
st.out += hist[i];
|
||||
if (ad > st.dev)
|
||||
st.dev = ad;
|
||||
}
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
||||
// Logs LABEL, PREFIX and up to 12 bins more than TOL from mode.
|
||||
static void log_outliers(const char *label, const char *prefix, const uint32_t *hist, int bins, int base,
|
||||
uint32_t mode) {
|
||||
char buf[200];
|
||||
int pos = snprintf(buf, sizeof(buf), "%s", prefix);
|
||||
for (int i = 0, k = 0; i < bins && k < 12 && pos < (int) sizeof(buf); i++) {
|
||||
int d = i + base - (int) mode;
|
||||
if ((d > TOL || d < -TOL) && hist[i]) {
|
||||
pos += snprintf(buf + pos, sizeof(buf) - pos, " %d:%" PRIu32, i + base, hist[i]);
|
||||
k++;
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "%s%s", label, buf);
|
||||
}
|
||||
|
||||
static bool check(esp_err_t e, const char *what) {
|
||||
if (e != ESP_OK)
|
||||
ESP_LOGE(TAG, "%s: %s", what, esp_err_to_name(e));
|
||||
return e == ESP_OK;
|
||||
}
|
||||
|
||||
static void arm() {
|
||||
skip_first = true;
|
||||
esp_err_t e = rmt_receive(rx, rxbuf, sizeof(rxbuf), &rcfg);
|
||||
if (e != ESP_OK)
|
||||
ESP_LOGE(TAG, "rmt_receive: %s", esp_err_to_name(e));
|
||||
}
|
||||
|
||||
static void dump_ledc() {
|
||||
auto &t = LEDC.timer_group[0].timer[0].conf;
|
||||
auto &c = LEDC.channel_group[0].channel[0];
|
||||
ESP_LOGI(TAG,
|
||||
"LEDC sclk_sel=%u (1=PLL_F80M 2=RC_FAST 3=XTAL) timer0 duty_res=%u clk_div=0x%05x (%.4f) freq=%" PRIu32
|
||||
" Hz duty_r=%u (x1/16) hpoint=%u",
|
||||
(unsigned) PCR.ledc_sclk_conf.ledc_sclk_sel, (unsigned) t.duty_res, (unsigned) t.clk_div, t.clk_div / 256.0f,
|
||||
ledc_get_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0), (unsigned) c.duty_rd.duty_r, (unsigned) c.hpoint.hpoint);
|
||||
}
|
||||
|
||||
inline void start(int pin) {
|
||||
reset_win(wins[0]);
|
||||
reset_win(wins[1]);
|
||||
rmt_rx_channel_config_t cc{};
|
||||
cc.gpio_num = (gpio_num_t) pin;
|
||||
cc.clk_src = RMT_CLK_SRC_DEFAULT;
|
||||
cc.resolution_hz = 80000000;
|
||||
cc.mem_block_symbols = 96;
|
||||
cc.intr_priority = 3;
|
||||
esp_err_t e = rmt_new_rx_channel(&cc, &rx);
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGE(TAG, "rmt_new_rx_channel: %s", esp_err_to_name(e));
|
||||
return;
|
||||
}
|
||||
rmt_rx_event_callbacks_t cbs{};
|
||||
cbs.on_recv_done = on_rx;
|
||||
if (!check(rmt_rx_register_event_callbacks(rx, &cbs, nullptr), "rmt callbacks") ||
|
||||
!check(rmt_enable(rx), "rmt_enable")) {
|
||||
rmt_del_channel(rx);
|
||||
rx = nullptr;
|
||||
return;
|
||||
}
|
||||
rcfg.signal_range_min_ns = 100;
|
||||
rcfg.signal_range_max_ns = 300000;
|
||||
rcfg.flags.en_partial_rx = 1;
|
||||
|
||||
pcnt_unit_config_t uc{};
|
||||
uc.low_limit = -1;
|
||||
uc.high_limit = 30000;
|
||||
uc.flags.accum_count = 1;
|
||||
// a PCNT failure leaves pcnt=0 in every WIN line, which summ.py flags against n
|
||||
pcnt_chan_config_t pc{};
|
||||
pc.edge_gpio_num = pin;
|
||||
pc.level_gpio_num = -1;
|
||||
pcnt_channel_handle_t ch;
|
||||
if (check(pcnt_new_unit(&uc, &pcnt), "pcnt_new_unit") && check(pcnt_new_channel(pcnt, &pc, &ch), "pcnt channel")) {
|
||||
check(pcnt_channel_set_edge_action(ch, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD),
|
||||
"pcnt edge action");
|
||||
check(pcnt_unit_add_watch_point(pcnt, 30000), "pcnt watch point");
|
||||
check(pcnt_unit_enable(pcnt), "pcnt_unit_enable");
|
||||
check(pcnt_unit_clear_count(pcnt), "pcnt_unit_clear_count");
|
||||
check(pcnt_unit_start(pcnt), "pcnt_unit_start");
|
||||
}
|
||||
// The drivers only add input; make sure LEDC still drives the pad.
|
||||
gpio_ll_output_enable(&GPIO, pin);
|
||||
gpio_pullup_dis((gpio_num_t) pin);
|
||||
win_start = esp_timer_get_time();
|
||||
arm();
|
||||
dump_ledc();
|
||||
}
|
||||
|
||||
// Called from the main loop every 10 s.
|
||||
inline void report() {
|
||||
if (rx == nullptr) {
|
||||
ESP_LOGE(TAG, "WIN capture=off");
|
||||
return;
|
||||
}
|
||||
if (need_rearm) {
|
||||
need_rearm = false;
|
||||
arm();
|
||||
}
|
||||
portENTER_CRITICAL(&mux);
|
||||
int done = cur;
|
||||
cur = done ^ 1;
|
||||
portEXIT_CRITICAL(&mux);
|
||||
int64_t now = esp_timer_get_time();
|
||||
int pc = 0;
|
||||
pcnt_unit_get_count(pcnt, &pc);
|
||||
int pc_delta = pc - last_pcnt;
|
||||
last_pcnt = pc;
|
||||
Win &w = wins[done];
|
||||
HistStats hs = hist_stats(w.hist_h, HBINS, 0);
|
||||
HistStats ps = hist_stats(w.hist_p, PBINS, PBASE);
|
||||
uint32_t h_out = w.h_over + hs.out, p_out = w.p_under + w.p_over + ps.out;
|
||||
// spikes: a level change that returns to the previous level within 64 periods; reversals: direction flips
|
||||
uint32_t spikes = 0, reversals = 0;
|
||||
uint32_t nchg = w.chg_n < CHG ? w.chg_n : CHG;
|
||||
int last_dir = 0;
|
||||
for (uint32_t i = 1; i < nchg; i++) {
|
||||
int d = (int) w.chg_h[i] - (int) w.chg_h[i - 1];
|
||||
int dir = d > 0 ? 1 : -1;
|
||||
if (i >= 2) {
|
||||
int back = (int) w.chg_h[i] - (int) w.chg_h[i - 2];
|
||||
if (back <= TOL && back >= -TOL && w.chg_idx[i] - w.chg_idx[i - 1] <= 64)
|
||||
spikes++;
|
||||
}
|
||||
if (last_dir && dir != last_dir && w.chg_idx[i] - w.chg_idx[i - 1] <= 2000)
|
||||
reversals++;
|
||||
last_dir = dir;
|
||||
}
|
||||
ESP_LOGI(TAG,
|
||||
"WIN dt=%" PRId64 "ms n=%" PRIu32 " pcnt=%d chunks=%" PRIu32 " zero=%" PRIu32 " idle=%" PRIu32
|
||||
" h[min=%" PRIu32 " mode=%" PRIu32 " max=%" PRIu32 " out=%" PRIu32 " dev=%" PRIu32 "] p[min=%" PRIu32
|
||||
" mode=%" PRIu32 " max=%" PRIu32 " out=%" PRIu32 " dev=%" PRIu32 "] chg=%" PRIu32 " spikes=%" PRIu32
|
||||
" rev=%" PRIu32 " duty_r=%u",
|
||||
(now - win_start) / 1000, w.n, pc_delta, w.chunks, w.zero_dur, idle_ends, w.hmin == UINT32_MAX ? 0 : w.hmin,
|
||||
hs.mode, w.hmax, h_out, hs.dev, w.pmin == UINT32_MAX ? 0 : w.pmin, ps.mode, w.pmax, p_out, ps.dev, w.chg_n,
|
||||
spikes, reversals, (unsigned) LEDC.channel_group[0].channel[0].duty_rd.duty_r);
|
||||
// print outlier bins (up to 12) for the static cases
|
||||
if (h_out && w.chg_n < 40)
|
||||
log_outliers("HOUT", "", w.hist_h, HBINS, 0, hs.mode);
|
||||
if (p_out) {
|
||||
char prefix[48];
|
||||
snprintf(prefix, sizeof(prefix), " under=%" PRIu32 " over=%" PRIu32, w.p_under, w.p_over);
|
||||
log_outliers("POUT", prefix, w.hist_p, PBINS, PBASE, ps.mode);
|
||||
}
|
||||
if (spikes || reversals) {
|
||||
char buf[240];
|
||||
int pos = 0;
|
||||
for (uint32_t i = 0; i < nchg && pos < 200; i++)
|
||||
pos += snprintf(buf + pos, sizeof(buf) - pos, " %u", w.chg_h[i]);
|
||||
ESP_LOGI(TAG, "CHG%s", buf);
|
||||
}
|
||||
win_start = now;
|
||||
reset_win(w);
|
||||
}
|
||||
} // namespace pwmcap
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# run.sh NAME SECONDS [apiclient.py options]: reset the already flashed C6 and capture SECONDS into out-NAME/cap.txt
|
||||
# while apiclient.py holds an API connection (optionally sending light transitions), then print summ.py's totals.
|
||||
# Build and flash first with: esphome -s name NAME -s ps light|none run --no-logs common.yaml
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (python with aioesphomeapi)
|
||||
R=$(cd "$(dirname "$0")" && pwd); N=${1:?name}; S=${2:?seconds}; shift 2
|
||||
# the API client holds SECONDS - 25 (boot and Wi-Fi take up to 25 s)
|
||||
[ "$S" -ge 40 ] || { echo "SECONDS must be at least 40"; exit 1; }
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}
|
||||
OUT=$R/out-$N; rm -rf "$OUT"; mkdir -p "$OUT"
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
$PY "$SC" "$P" "$S" "$OUT/cap.txt" --reset > /dev/null 2>&1 < /dev/null & CAP=$!
|
||||
for _ in $(seq 1 60); do
|
||||
IP=$(grep -a -o "IP Address: [0-9.]*" "$OUT/cap.txt" 2>/dev/null | head -1 | awk '{print $3}')
|
||||
[ -n "$IP" ] && break; sleep 1
|
||||
done
|
||||
[ -n "$IP" ] || { echo "no IP in the capture"; kill $CAP; exit 1; }
|
||||
$PY "$R/apiclient.py" "$IP" $((S - 25)) "$@" > "$OUT/api.txt" 2>&1
|
||||
API_RC=$?
|
||||
wait $CAP
|
||||
tail -1 "$OUT/api.txt"
|
||||
$PY "$R/summ.py" "$OUT/cap.txt"
|
||||
SUMM_RC=$?
|
||||
[ $API_RC = 0 ] || { echo "FAIL: API driver exited $API_RC"; exit 1; }
|
||||
[ $SUMM_RC = 0 ] || exit 1
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Total the pwmcap WIN lines of one or more captures.
|
||||
|
||||
usage: summ.py CAPTURE...
|
||||
Sums periods, PCNT edges, out-of-tolerance high times and periods, level changes, spikes and reversals over every
|
||||
full 10 s window, and prints the high time and period ranges. Window boundaries follow RMT chunks, so sum over the
|
||||
run instead of comparing single windows. Exits 1 when a capture has no full window, no periods, or RMT and PCNT
|
||||
period totals more than 1% apart.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
WIN = re.compile(
|
||||
r"WIN dt=(\d+)ms n=(\d+) pcnt=(-?\d+) chunks=\d+ zero=(\d+) idle=(\d+) "
|
||||
r"h\[min=(\d+) mode=(\d+) max=(\d+) out=(\d+) dev=(\d+)\] "
|
||||
r"p\[min=(\d+) mode=(\d+) max=(\d+) out=(\d+) dev=(\d+)\] chg=(\d+) spikes=(\d+) rev=(\d+)"
|
||||
)
|
||||
|
||||
|
||||
def summarize(path: str) -> bool:
|
||||
tot = dict.fromkeys(
|
||||
("n", "pcnt", "h_out", "p_out", "spikes", "rev", "chg", "zero"), 0
|
||||
)
|
||||
hmin, hmax, pmin, pmax, hdev, pdev, wins, idle = 10**9, 0, 10**9, 0, 0, 0, 0, 0
|
||||
modes: set[int] = set()
|
||||
for line in pathlib.Path(path).read_bytes().decode(errors="replace").splitlines():
|
||||
m = WIN.search(line)
|
||||
if not m or int(m[1]) < 9000:
|
||||
continue
|
||||
v = [int(x) for x in m.groups()]
|
||||
wins += 1
|
||||
tot["n"] += v[1]
|
||||
tot["pcnt"] += v[2]
|
||||
tot["zero"] += v[3]
|
||||
idle = v[4]
|
||||
hmin, hmax, hdev = min(hmin, v[5]), max(hmax, v[7]), max(hdev, v[9])
|
||||
modes.add(v[6])
|
||||
tot["h_out"] += v[8]
|
||||
pmin, pmax, pdev = min(pmin, v[10]), max(pmax, v[12]), max(pdev, v[14])
|
||||
tot["p_out"] += v[13]
|
||||
tot["chg"] += v[15]
|
||||
tot["spikes"] += v[16]
|
||||
tot["rev"] += v[17]
|
||||
print(
|
||||
path,
|
||||
f"windows={wins} ({wins * 10}s)",
|
||||
tot,
|
||||
f"h {hmin}..{hmax} modes={sorted(modes)[:6]} hdev={hdev} p {pmin}..{pmax} pdev={pdev} idle={idle}",
|
||||
)
|
||||
if wins and tot["n"] and abs(tot["n"] - tot["pcnt"]) * 100 <= tot["n"]:
|
||||
return True
|
||||
print(f"FAIL: {path} windows={wins} n={tot['n']} pcnt={tot['pcnt']}")
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
results = [summarize(path) for path in sys.argv[1:]]
|
||||
if not results or not all(results):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# otaseq.sh DEVICE-IP FILE=EXPECT...: send each image with a config-less `esphome upload --file` while capturing the
|
||||
# device log over USB. EXPECT is `reject` (a Signature check rejection and a failed upload) or the build tag the image
|
||||
# must boot with (its `BUILDTAG <tag>` line and a successful upload). Exits 1 on any mismatch or missing verdict.
|
||||
# Send the rejects first so every check runs in the same build.
|
||||
# env: TREE (esphome checkout), PORT (device USB serial port), SC (path to serial_capture.py), PY (checkout python)
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; SC=${SC:?set SC}; PY=${PY:-$T/venv/bin/python}; IP=${1:?ip}; shift
|
||||
for a in "$@"; do [ "${a%=*}" != "$a" ] || { echo "$a: give FILE=reject or FILE=<tag>"; exit 2; }; done
|
||||
W=$(pwd); LOG=$W/ota-log.txt
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
: > "$LOG"
|
||||
$PY "$SC" "$P" 1800 "$LOG" > /dev/null 2>&1 &
|
||||
CAP=$!
|
||||
fail=0
|
||||
# first verdict line logged after line $1 of the capture
|
||||
verdict() { tail -n +"$(($1 + 1))" "$LOG" | grep -a -o -E "Signature check: (image|no trusted)|BUILDTAG [A-Za-z0-9_.-]+" | head -1; }
|
||||
for a in "$@"; do
|
||||
f=${a%=*}; want=${a##*=}
|
||||
kill -0 $CAP 2>/dev/null || { echo "serial capture died, stopping"; exit 1; }
|
||||
up=0; for i in $(seq 1 40); do nc -z -w2 "$IP" 3232 && { up=1; break; }; sleep 3; done
|
||||
[ $up = 1 ] || { echo "$f: device unreachable, stopping; not sent: $*"; fail=1; break; }
|
||||
start=$(wc -l < "$LOG")
|
||||
(cd "$T" && $PY -m esphome upload --file "$W/$f" --device "$IP" > "$W/up-$f.log" 2>&1)
|
||||
rc=$?
|
||||
shift
|
||||
v=""; for i in $(seq 1 60); do v=$(verdict "$start"); [ -n "$v" ] && break; sleep 1; done
|
||||
if [ "$want" = reject ]; then
|
||||
[[ "$v" == "Signature check"* && $rc != 0 ]] && res=ok || res=MISMATCH
|
||||
else
|
||||
[[ "$v" == "BUILDTAG $want" && $rc = 0 ]] && res=ok || res=MISMATCH
|
||||
fi
|
||||
[ $res = ok ] || fail=1
|
||||
echo "$f want=$want rc=$rc verdict=${v:-none} $res $(grep -a -E 'ERROR|OTA successful' "$W/up-$f.log" | tail -1 | cut -c1-100)"
|
||||
done
|
||||
kill $CAP; wait
|
||||
grep -a -E "BUILDTAG|Signature check" "$LOG"
|
||||
exit $fail
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Sign an ESP-IDF app image and write the signed OTA cases: key A, key B, B then A, and CRC-only edits."""
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
BLOCK = 1216
|
||||
CRC_AT = 1196
|
||||
|
||||
|
||||
def sig_sector(data: bytes) -> int:
|
||||
# The image can hold E7 02 at a 4 KiB boundary by chance; the real block has a matching CRC
|
||||
for off in range(0, len(data) - BLOCK, 4096):
|
||||
b = data[off : off + BLOCK]
|
||||
if (
|
||||
b[0] == 0xE7
|
||||
and b[1] == 0x02
|
||||
and struct.unpack("<I", b[CRC_AT : CRC_AT + 4])[0] == zlib.crc32(b[:CRC_AT])
|
||||
):
|
||||
return off
|
||||
raise SystemExit("no signature block")
|
||||
|
||||
|
||||
def sign(image: str, out: str, *keys: str) -> bytes:
|
||||
args = [sys.executable, "-m", "espsecure", "sign-data", "-v", "2"]
|
||||
for k in keys:
|
||||
args += ["-k", k]
|
||||
result = subprocess.run(
|
||||
[*args, "-o", out, image], check=False, capture_output=True, text=True
|
||||
)
|
||||
if result.returncode:
|
||||
raise SystemExit(f"espsecure failed for {out}:\n{result.stderr}")
|
||||
return pathlib.Path(out).read_bytes()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("image", help="firmware.ota.bin")
|
||||
ap.add_argument("key_a", help="trusted key (in verification_keys)")
|
||||
ap.add_argument("key_b", help="untrusted key")
|
||||
ap.add_argument("prefix")
|
||||
a = ap.parse_args()
|
||||
signed_a = sign(a.image, f"{a.prefix}.A.bin", a.key_a)
|
||||
sign(a.image, f"{a.prefix}.B.bin", a.key_b)
|
||||
signed_ba = sign(a.image, f"{a.prefix}.BA.bin", a.key_b, a.key_a)
|
||||
o = sig_sector(signed_a)
|
||||
pad = bytearray(signed_a)
|
||||
pad[o + 2] ^= 0x5A # covered only by the CRC
|
||||
pathlib.Path(f"{a.prefix}.A.pad.bin").write_bytes(pad)
|
||||
pad[o + CRC_AT : o + CRC_AT + 4] = struct.pack(
|
||||
"<I", zlib.crc32(bytes(pad[o : o + CRC_AT]))
|
||||
)
|
||||
pathlib.Path(f"{a.prefix}.A.padfix.bin").write_bytes(pad)
|
||||
crc = bytearray(signed_a)
|
||||
crc[o + CRC_AT] ^= 0x01
|
||||
pathlib.Path(f"{a.prefix}.A.crc.bin").write_bytes(crc)
|
||||
o2 = sig_sector(signed_ba) + BLOCK
|
||||
ba = bytearray(signed_ba)
|
||||
ba[o2 + CRC_AT] ^= 0x01
|
||||
pathlib.Path(f"{a.prefix}.BAcrc.bin").write_bytes(ba)
|
||||
print(f"signature sector at {o:#x}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: sigc3
|
||||
on_boot:
|
||||
- priority: -100
|
||||
then:
|
||||
- logger.log:
|
||||
format: "BUILDTAG ${tag}"
|
||||
level: INFO
|
||||
|
||||
substitutions:
|
||||
tag: first
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
signing_scheme: rsa3072
|
||||
verification_keys:
|
||||
- keyA.pem
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
power_save_mode: none
|
||||
|
||||
api:
|
||||
reboot_timeout: 0s
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,370 @@
|
||||
"""Decode spitest captures, check every record against the expected wire data and diff two builds.
|
||||
|
||||
usage: analyze.py spi<board>.yaml CAPTURE [CAPTURE2]
|
||||
With two captures (base, PR) also diffs every record, by the majority payload per record so ISR jitter does not
|
||||
count, and prints the median time per operation. Failures seen in most passes are SYSTEMATIC, the rest sporadic.
|
||||
"""
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
import pathlib
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
|
||||
LINE = re.compile(r"\[spitest(?::\d+)?\]: (.*)")
|
||||
TX0 = [0x01, 0x80, 0xA5, 0x3C, 0xF0, 0x0F, 0x55]
|
||||
TX4 = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||
ANS = [0xC3, 0x5A, 0x81, 0x7E, 0x0F, 0xF0, 0x33, 0xCC]
|
||||
|
||||
|
||||
def rev(b: int) -> int:
|
||||
return int(f"{b:08b}"[::-1], 2)
|
||||
|
||||
|
||||
def parse_devs(yaml_path: str) -> dict:
|
||||
text = pathlib.Path(yaml_path).read_text(encoding="utf-8")
|
||||
devs = {}
|
||||
add = r'spitest::add\("(\w+)", id\(\w+\), (\d), (true|false), \d+, \d+, \d+, \d+, (true|false)\)'
|
||||
for m in re.finditer(add, text):
|
||||
name, mode, lsb, hw = m.groups()
|
||||
devs[name] = {
|
||||
"mode": int(mode),
|
||||
"lsb": lsb == "true",
|
||||
"hw": hw == "true",
|
||||
"idx": len(devs),
|
||||
}
|
||||
for d in devs.values():
|
||||
d["tag"] = 0x10 * (d["idx"] + 1)
|
||||
return devs
|
||||
|
||||
|
||||
def bits_of(bytes_: list[int], lsb: bool) -> list[int]:
|
||||
out = []
|
||||
for b in bytes_:
|
||||
s = f"{b:08b}"
|
||||
out.extend(int(c) for c in (s[::-1] if lsb else s))
|
||||
return out
|
||||
|
||||
|
||||
def sample(edges: str, idle: int, mode: int) -> list[int] | None:
|
||||
cpol, cpha = mode >> 1, mode & 1
|
||||
lvl = idle
|
||||
seen = False
|
||||
out = []
|
||||
for c in edges:
|
||||
if c == "-":
|
||||
continue
|
||||
new = 1 if c in "rR" else 0
|
||||
if new == lvl:
|
||||
return None # an edge was lost
|
||||
lvl = new
|
||||
bit = 1 if c in "RF" else 0
|
||||
if new != cpol: # leading edge
|
||||
seen = True
|
||||
if cpha == 0:
|
||||
out.append(bit)
|
||||
elif cpha == 1 and seen:
|
||||
out.append(bit)
|
||||
return out
|
||||
|
||||
|
||||
def load(path: str) -> tuple[list[tuple[int, str]], int, list[dict]]:
|
||||
recs = []
|
||||
boots = 0
|
||||
heap = []
|
||||
for raw in pathlib.Path(path).read_bytes().decode(errors="replace").splitlines():
|
||||
m = LINE.search(raw)
|
||||
if not m:
|
||||
continue
|
||||
s = m.group(1).strip()
|
||||
if s.startswith("START"):
|
||||
boots += 1
|
||||
if s.startswith("HEAP boot") and recs and recs[-1][1].startswith("START"):
|
||||
heap.append(dict(kv.split("=") for kv in s.split()[2:]))
|
||||
recs.append((boots, s))
|
||||
return recs, boots, heap
|
||||
|
||||
|
||||
def key_and_payload(s: str) -> tuple[tuple | None, str | None]:
|
||||
"""Split a record into a key and a payload without timing fields."""
|
||||
p = s.split()
|
||||
if len(p) < 5 or not p[1].isdigit():
|
||||
return None, None
|
||||
kind = p[0]
|
||||
if kind in ("T", "S", "N"):
|
||||
key = (kind, p[1], p[2], p[3])
|
||||
rest = [x for x in p[4:] if not x.startswith(("us=", "per10=", "ev="))]
|
||||
return key, " ".join(rest)
|
||||
if kind == "F":
|
||||
key = (kind, p[1], p[2], p[3], p[4])
|
||||
rest = [x for x in p[5:] if not x.startswith(("per10=", "t1x10="))]
|
||||
return key, " ".join(rest)
|
||||
return None, None
|
||||
|
||||
|
||||
def fields(s: str) -> dict[str, str]:
|
||||
return dict(x.split("=", 1) for x in s.split() if "=" in x)
|
||||
|
||||
|
||||
def check(
|
||||
devs: dict, recs: list[tuple[int, str]], board: str
|
||||
) -> tuple[int, list[str], dict, dict]:
|
||||
"""Return (n_checks, failures, us per op, clock period x10 per op), both keyed by (device, step)."""
|
||||
fails = []
|
||||
n = 0
|
||||
timing = defaultdict(list)
|
||||
per = defaultdict(list)
|
||||
capture = any(s.startswith("S ") for _, s in recs)
|
||||
frames = defaultdict(list)
|
||||
for boot, s in recs:
|
||||
p = s.split()
|
||||
if p[0] == "F" and "UNTERMINATED" not in s:
|
||||
frames[(boot, p[1], p[2], p[3])].append(s)
|
||||
for boot, s in recs:
|
||||
p = s.split()
|
||||
if not p or p[0] not in ("S", "T", "N", "F"):
|
||||
continue
|
||||
f = fields(s)
|
||||
# the USB console can splice or truncate lines; a damaged record must not pass as clean
|
||||
if len(p) < 4 or not p[1].isdigit() or (p[0] == "T" and "us" not in f):
|
||||
fails.append(f"boot{boot}: malformed record {s}")
|
||||
continue
|
||||
if p[0] == "T":
|
||||
timing[(p[2], p[3])].append(int(f["us"]))
|
||||
if p[0] == "F" and "per10" in f and int(f["per10"]) > 0:
|
||||
per[(p[2], p[3])].append(int(f["per10"]))
|
||||
if "UNTERMINATED" in s:
|
||||
fails.append(f"boot{boot}: {s}")
|
||||
dev = p[2] if len(p) > 2 else None
|
||||
if dev not in devs and dev != "mx":
|
||||
continue
|
||||
step = int(p[3]) if len(p) > 3 and p[3].isdigit() else None
|
||||
d = devs.get(dev)
|
||||
if p[0] == "S" and dev != "mx":
|
||||
n += 1
|
||||
tag = d["tag"]
|
||||
lsb = d["lsb"]
|
||||
exp_bits = {0: 64, 5: 64, 6: 64, 8: 64, 9: 64, 1: 16, 2: 12, 3: 32, 4: 40}[
|
||||
step
|
||||
]
|
||||
|
||||
def fx(bs: list[int], lsb: bool = lsb) -> str:
|
||||
return "".join(f"{(rev(b) if lsb else b):02x}" for b in bs)
|
||||
|
||||
errs = []
|
||||
if int(f["bits"]) != exp_bits:
|
||||
errs.append(f"bits {f['bits']} != {exp_bits}")
|
||||
if step in (0, 5, 6, 8, 9) and f["srx"] != fx(TX0 + [tag]):
|
||||
errs.append(f"capture got {f['srx']} want {fx(TX0 + [tag])}")
|
||||
if step == 4 and f["srx"] != fx(TX4 + [tag]):
|
||||
errs.append(f"capture got {f['srx']} want {fx(TX4 + [tag])}")
|
||||
if f["cs"] != "2/0":
|
||||
errs.append(f"cs edges {f['cs']} != 2/0")
|
||||
if errs:
|
||||
fails.append(f"boot{boot}: {s} :: {'; '.join(errs)}")
|
||||
if p[0] == "S" and dev == "mx":
|
||||
n += 1
|
||||
if f["bits"] != "128" or f["cs"] != "16/0":
|
||||
fails.append(f"boot{boot}: {s} :: want 128 bits and cs 16/0")
|
||||
else:
|
||||
regs = [int(f["srx"][i : i + 2], 16) for i in range(0, 32, 4)]
|
||||
if regs != list(range(8, 0, -1)):
|
||||
fails.append(f"boot{boot}: {s} :: registers {regs}")
|
||||
if p[0] == "T" and capture and dev != "mx" and step in (8, 9):
|
||||
n += 1
|
||||
want = "".join(f"{b:02x}" for b in TX0 + [d["tag"]])
|
||||
if f["rx"] != want:
|
||||
fails.append(f"boot{boot}: {s} :: loopback read {f['rx']} want {want}")
|
||||
if p[0] == "T" and capture and dev != "mx" and step in (0, 3, 5, 6):
|
||||
n += 1
|
||||
tag = d["tag"]
|
||||
want = [(b ^ tag) for b in ANS][: 8 if step != 3 else 4]
|
||||
wanth = "".join(f"{(rev(b) if d['lsb'] else b):02x}" for b in want)
|
||||
if f["rx"] != wanth:
|
||||
fails.append(f"boot{boot}: {s} :: master read {f['rx']} want {wanth}")
|
||||
if p[0] == "N":
|
||||
n += 1
|
||||
nf = int(f["frames"])
|
||||
want = 8 if dev == "mx" else 1
|
||||
if nf != want or f["drop"] != "0":
|
||||
fails.append(f"boot{boot}: {s} :: frames {nf} want {want}")
|
||||
fl = frames.get((boot, p[1], p[2], p[3]), [])
|
||||
if dev == "mx":
|
||||
for fr in fl:
|
||||
ff = fields(fr)
|
||||
bits = sample(ff["e"], int(ff["idle"]), 0)
|
||||
if bits is None or len(bits) != 16 or ff["ov"] != "0":
|
||||
fails.append(f"boot{boot}: {fr} :: mx frame decode {bits}")
|
||||
continue
|
||||
if not fl and nf:
|
||||
fails.append(f"boot{boot}: {s} :: no F record")
|
||||
if len(fl) != 1:
|
||||
continue
|
||||
ff = fields(fl[0])
|
||||
if ff["ov"] != "0":
|
||||
fails.append(f"boot{boot}: {fl[0]} :: another CS was low")
|
||||
exp = None
|
||||
if step in (0, 5, 6):
|
||||
exp = TX0 + [d["tag"]]
|
||||
elif step == 4:
|
||||
exp = TX4 + [d["tag"]]
|
||||
elif step == 1:
|
||||
v = 0x1234 ^ d["tag"]
|
||||
exp = [v >> 8, v & 0xFF] if not d["lsb"] else None
|
||||
if board == "e66" and d["hw"]:
|
||||
continue # HSPI pads cannot be read back; the T line carries the SPI registers instead
|
||||
bits = sample(ff["e"], int(ff["idle"]), d["mode"])
|
||||
if bits is None:
|
||||
fails.append(f"boot{boot}: {fl[0]} :: lost an edge")
|
||||
elif exp is not None:
|
||||
n += 1
|
||||
want = bits_of(exp, d["lsb"])
|
||||
if bits != want:
|
||||
fails.append(f"boot{boot}: {fl[0]} :: decoded {bits} want {want}")
|
||||
if p[0] == "T" and not capture and dev != "mx" and step in (0, 5, 6):
|
||||
# software bus loopback and the C3 matrix loopback read back what was sent
|
||||
loop = (not d["hw"]) or board == "c3"
|
||||
if loop:
|
||||
n += 1
|
||||
want = "".join(f"{b:02x}" for b in TX0 + [d["tag"]])
|
||||
if f["rx"] != want:
|
||||
fails.append(
|
||||
f"boot{boot}: {s} :: loopback read {f['rx']} want {want}"
|
||||
)
|
||||
elif board == "rp":
|
||||
n += 1
|
||||
if f["rx"] != "00" * 8:
|
||||
fails.append(f"boot{boot}: {s} :: forced-low MISO read {f['rx']}")
|
||||
if (
|
||||
p[0] == "T"
|
||||
and not capture
|
||||
and dev != "mx"
|
||||
and step == 3
|
||||
and board == "rp"
|
||||
and d["hw"]
|
||||
):
|
||||
n += 1
|
||||
if f["rx"] != "ff" * 4:
|
||||
fails.append(f"boot{boot}: {s} :: forced-high MISO read {f['rx']}")
|
||||
return n, fails, timing, per
|
||||
|
||||
|
||||
def completeness(recs: list[tuple[int, str]]) -> list[str]:
|
||||
"""Every boot but the last must reach DONE with one S (wire capture) or N record per scheduled item."""
|
||||
kind = "S" if any(s.startswith("S ") for _, s in recs) else "N"
|
||||
items: dict[int, int] = {}
|
||||
done: set[int] = set()
|
||||
count: Counter = Counter()
|
||||
for boot, s in recs:
|
||||
p = s.split()
|
||||
if p and p[0] == "START":
|
||||
items[boot] = int(fields(s).get("items", -1))
|
||||
elif p and p[0] == "DONE":
|
||||
done.add(boot)
|
||||
elif len(p) > 1 and p[0] == kind and p[1].isdigit():
|
||||
count[boot] += 1
|
||||
fails = []
|
||||
last = max(items, default=0)
|
||||
for boot, want in items.items():
|
||||
if boot in done and count[boot] != want:
|
||||
fails.append(f"boot{boot}: {count[boot]} {kind} records for {want} items")
|
||||
elif boot not in done and boot != last:
|
||||
fails.append(
|
||||
f"boot{boot}: restarted before DONE after {count[boot]} of {want} items"
|
||||
)
|
||||
if not done:
|
||||
fails.append("no boot reached DONE")
|
||||
return fails
|
||||
|
||||
|
||||
def canon(devs: dict, k: tuple | None, pl: str) -> str:
|
||||
"""F records: compare what a device on the wire would latch, not ISR timing."""
|
||||
if k is None or k[0] != "F":
|
||||
return pl
|
||||
f = fields(pl)
|
||||
mode = 0 if k[2] == "mx" else devs[k[2]]["mode"]
|
||||
e = f.get("e", "-").replace("-", "")
|
||||
bits = sample(e, int(f["idle"]), mode)
|
||||
b = "lost" if bits is None else "".join(map(str, bits))
|
||||
return f"idle={f['idle']} edges={len(e)} ov={f['ov']} bits={b}"
|
||||
|
||||
|
||||
def variants(devs: dict, recs: list[tuple[int, str]]) -> dict[tuple, Counter]:
|
||||
v = defaultdict(Counter)
|
||||
for _, s in recs:
|
||||
k, pl = key_and_payload(s)
|
||||
if k is not None:
|
||||
v[k][canon(devs, k, pl)] += 1
|
||||
return v
|
||||
|
||||
|
||||
def majority(c: Counter | None) -> str | None:
|
||||
return c.most_common(1)[0][0] if c else None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
yaml_path = sys.argv[1]
|
||||
board = re.search(r"spi(e32|c3|e66|rp)\.yaml$", yaml_path).group(1)
|
||||
devs = parse_devs(yaml_path)
|
||||
caps = sys.argv[2:]
|
||||
results = []
|
||||
failed = False
|
||||
for c in caps:
|
||||
recs, boots, heap = load(c)
|
||||
n, fails, timing, per = check(devs, recs, board)
|
||||
fails += completeness(recs)
|
||||
failed |= n == 0 or bool(fails)
|
||||
v = variants(devs, recs)
|
||||
occ = Counter()
|
||||
for _, s in recs:
|
||||
k, _pl = key_and_payload(s)
|
||||
if k is not None:
|
||||
occ[k] += 1
|
||||
fk = defaultdict(list)
|
||||
for fl in fails:
|
||||
body = fl.split(": ", 1)[-1].split(" :: ")[0]
|
||||
k, _pl = key_and_payload(body)
|
||||
fk[k].append(fl)
|
||||
systematic = {k: f for k, f in fk.items() if k is None or len(f) * 2 > occ[k]}
|
||||
sporadic = sum(len(f) for k, f in fk.items() if k not in systematic)
|
||||
unstable = {k: pl for k, pl in v.items() if len(pl) > 1}
|
||||
done = sum(1 for _, s in recs if s.startswith("DONE"))
|
||||
print(
|
||||
f"== {c}\n boots={boots} done={done} records={len(recs)} checks={n} failed={len(fails)} "
|
||||
f"systematic_keys={len(systematic)} sporadic_fails={sporadic} keys_with_minority_variants={len(unstable)}"
|
||||
)
|
||||
for k, f in list(systematic.items())[:12]:
|
||||
print(" SYSTEMATIC", len(f), "of", occ[k], f[0][:400])
|
||||
print(" heap at boot:", [(h.get("free"), h.get("used")) for h in heap])
|
||||
results.append((v, timing, per, fails))
|
||||
if len(results) == 2:
|
||||
(va, ta, pa, _), (vb, tb, pb, _) = results
|
||||
keys = set(va) | set(vb)
|
||||
diff = [k for k in sorted(keys) if majority(va.get(k)) != majority(vb.get(k))]
|
||||
print(
|
||||
f"== diff base vs PR (majority payload per key): {len(keys)} keys, {len(diff)} differ"
|
||||
)
|
||||
for k in diff[:20]:
|
||||
print(
|
||||
" DIFF",
|
||||
k,
|
||||
"base:",
|
||||
dict(va.get(k) or {}),
|
||||
"pr:",
|
||||
dict(vb.get(k) or {}),
|
||||
)
|
||||
print("== timing (median us per op, base -> PR; clock period from edges)")
|
||||
for k in sorted(set(ta) | set(tb)):
|
||||
a = statistics.median(ta[k]) if ta.get(k) else None
|
||||
b = statistics.median(tb[k]) if tb.get(k) else None
|
||||
pa_ = statistics.median(pa[k]) / 10 if pa.get(k) else None
|
||||
pb_ = statistics.median(pb[k]) / 10 if pb.get(k) else None
|
||||
if k[1] in ("0", "7"):
|
||||
print(f" {k[0]} step{k[1]}: {a} -> {b} us period {pa_} -> {pb_} us")
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Write spi<board>.yaml for each board: a hardware and a software SPI bus, several spi_device instances and a max7219.
|
||||
|
||||
usage: gen.py then run.sh BOARD LABEL SECONDS builds one of them with the name spi<board><LABEL>.
|
||||
Pins are (clk, mosi, miso, ...); a device entry is (id, bus, spi_mode, bit_order, data_rate, cs_pin). A software bus
|
||||
with mosi == miso is the loopback. Classic ESP32 also uses GPIO14 as the capture peripheral's spare CS (spitest.hpp).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
BOARDS = {
|
||||
"e32": {
|
||||
"platform": "esp32:\n board: pico32\n framework:\n type: esp-idf\n",
|
||||
"logger": "logger:\n level: DEBUG\n",
|
||||
"hw": {"clk": 25, "mosi": 26, "miso": 27, "interface": "spi2"},
|
||||
"sw": {"clk": 18, "mosi": 19, "miso": 21},
|
||||
"devs": [
|
||||
("hA", "hw", 0, "msb_first", "1MHz", 32),
|
||||
("hB", "hw", 3, "lsb_first", "4MHz", 33),
|
||||
("hE", "hw", 1, "lsb_first", "2MHz", 4),
|
||||
("hF", "hw", 2, "msb_first", "8MHz", 13),
|
||||
("sC", "sw", 1, "msb_first", "500kHz", 5),
|
||||
("sD", "sw", 2, "lsb_first", "200kHz", 23),
|
||||
],
|
||||
"mx": (22, "1MHz"),
|
||||
},
|
||||
"c3": {
|
||||
"platform": "esp32:\n variant: esp32c3\n board: esp32-c3-devkitm-1\n framework:\n type: esp-idf\n",
|
||||
"logger": "logger:\n level: DEBUG\n hardware_uart: USB_SERIAL_JTAG\n",
|
||||
"hw": {"clk": 4, "mosi": 5, "miso": 6, "interface": "spi2"},
|
||||
"sw": {"clk": 0, "mosi": 1, "miso": 1},
|
||||
"devs": [
|
||||
("hA", "hw", 0, "msb_first", "80kHz", 7),
|
||||
("hB", "hw", 3, "lsb_first", "100kHz", 10),
|
||||
("hE", "hw", 1, "lsb_first", "90kHz", 3),
|
||||
("hF", "hw", 2, "msb_first", "80kHz", 9),
|
||||
("sC", "sw", 1, "msb_first", "10kHz", 20),
|
||||
("sD", "sw", 2, "lsb_first", "5kHz", 21),
|
||||
],
|
||||
"mx": (8, "80kHz"),
|
||||
},
|
||||
"e66": {
|
||||
"platform": "esp8266:\n board: huzzah\n",
|
||||
"logger": "logger:\n level: DEBUG\n",
|
||||
"hw": {"clk": 14, "mosi": 13, "miso": 12, "interface": "hardware"},
|
||||
"sw": {"clk": 4, "mosi": 5, "miso": 5},
|
||||
"devs": [
|
||||
("hA", "hw", 0, "msb_first", "10kHz", 15),
|
||||
("hB", "hw", 3, "lsb_first", "20kHz", 0),
|
||||
("sC", "sw", 1, "msb_first", "10kHz", 2),
|
||||
("sD", "sw", 2, "lsb_first", "5kHz", 16),
|
||||
],
|
||||
"mx": None,
|
||||
},
|
||||
"rp": {
|
||||
"platform": "rp2040:\n board: rpipico2w\n",
|
||||
"logger": "logger:\n level: DEBUG\n",
|
||||
"hw": {"clk": 18, "mosi": 19, "miso": 16, "interface": "hardware"},
|
||||
"sw": {"clk": 10, "mosi": 11, "miso": 11},
|
||||
"devs": [
|
||||
("hA", "hw", 0, "msb_first", "10kHz", 17),
|
||||
("hB", "hw", 3, "lsb_first", "20kHz", 26),
|
||||
("hE", "hw", 1, "lsb_first", "15kHz", 21),
|
||||
("hF", "hw", 2, "msb_first", "10kHz", 22),
|
||||
("sC", "sw", 1, "msb_first", "10kHz", 12),
|
||||
("sD", "sw", 2, "lsb_first", "5kHz", 13),
|
||||
],
|
||||
"mx": (20, "10kHz"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def pin(n: int, other: bool = False) -> str:
|
||||
if other:
|
||||
return f"\n number: {n}\n allow_other_uses: true"
|
||||
return f" {n}"
|
||||
|
||||
|
||||
def gen(board: str) -> str:
|
||||
b = BOARDS[board]
|
||||
hw, sw = b["hw"], b["sw"]
|
||||
shared = sw["mosi"] == sw["miso"]
|
||||
out = [
|
||||
(
|
||||
"esphome:\n name: ${name}\n includes:\n - spitest.hpp\n"
|
||||
" on_boot:\n - priority: -100\n then:\n"
|
||||
" - lambda: |-\n"
|
||||
' spitest::log_heap("boot");\n'
|
||||
),
|
||||
]
|
||||
lines = []
|
||||
for nm, bus, mode, order, _rate, cs in b["devs"]:
|
||||
p = hw if bus == "hw" else sw
|
||||
lines.append(
|
||||
f' spitest::add("{nm}", id({nm}), {mode}, {"true" if order == "lsb_first" else "false"}, '
|
||||
f"{cs}, {p['clk']}, {p['mosi']}, {p['miso']}, {'true' if bus == 'hw' else 'false'});"
|
||||
)
|
||||
if b["mx"]:
|
||||
lines.append(
|
||||
f" spitest::add_mx(id(mx), {b['mx'][0]}, {hw['clk']}, {hw['mosi']});"
|
||||
)
|
||||
out.append("\n".join(lines) + "\n")
|
||||
out.append(" - delay: 2s\n - lambda: spitest::start();\n\n")
|
||||
out.append(b["platform"] + "\n" + b["logger"] + "\n")
|
||||
out.append(
|
||||
"interval:\n - interval: 20ms\n then:\n - lambda: spitest::tick();\n\n"
|
||||
)
|
||||
out.append(
|
||||
"spi:\n"
|
||||
f" - id: hwbus\n interface: {hw['interface']}\n clk_pin: {hw['clk']}\n"
|
||||
f" mosi_pin: {hw['mosi']}\n miso_pin: {hw['miso']}\n"
|
||||
f" - id: swbus\n interface: software\n clk_pin: {sw['clk']}\n"
|
||||
f" mosi_pin:{pin(sw['mosi'], shared)}\n miso_pin:{pin(sw['miso'], shared)}\n\n"
|
||||
)
|
||||
out.append("spi_device:\n")
|
||||
for nm, bus, mode, order, rate, cs in b["devs"]:
|
||||
out.append(
|
||||
f" - id: {nm}\n spi_id: {'hwbus' if bus == 'hw' else 'swbus'}\n cs_pin: {cs}\n"
|
||||
f" spi_mode: {mode}\n bit_order: {order}\n data_rate: {rate}\n"
|
||||
)
|
||||
if b["mx"]:
|
||||
out.append(
|
||||
"\ndisplay:\n - platform: max7219\n id: mx\n spi_id: hwbus\n"
|
||||
f" cs_pin: {b['mx'][0]}\n data_rate: {b['mx'][1]}\n num_chips: 1\n"
|
||||
' update_interval: never\n lambda: it.print("01234567");\n'
|
||||
)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for board in BOARDS:
|
||||
(HERE / f"spi{board}.yaml").write_text(gen(board), encoding="utf-8")
|
||||
print(HERE / f"spi{board}.yaml")
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# run.sh BOARD LABEL SECONDS: build spi<BOARD>.yaml (e32, c3, e66 or rp; gen.py writes them) from TREE as
|
||||
# spi<BOARD><LABEL>, flash, capture SECONDS (three passes, then the board reboots) into out-spi<BOARD><LABEL>.txt and
|
||||
# check it with analyze.py. Compare two builds with: analyze.py spi<BOARD>.yaml out-<base>.txt out-<pr>.txt
|
||||
# env: TREE (esphome checkout), PORT (serial port), SC (path to serial_capture.py), PY (checkout python),
|
||||
# PORTS (path to ports.py, Pico only: the port comes back after the upload)
|
||||
R=$(cd "$(dirname "$0")" && pwd); B=${1:?board}; L=${2:?label}; S=${3:?seconds}
|
||||
T=${TREE:?set TREE}; P=${PORT:?set PORT}; PY=${PY:-$T/venv/bin/python}; SC=${SC:?set SC}; N=spi$B$L
|
||||
[ -f "$R/spi$B.yaml" ] || $PY "$R/gen.py" > /dev/null
|
||||
[ -f "$R/spi$B.yaml" ] || { echo "no spi$B.yaml"; exit 1; }
|
||||
pkill -f "[s]erial_capture.py $P"
|
||||
(cd "$T" && $PY -m esphome -s name "$N" run --no-logs "$R/spi$B.yaml" --device "$P" > "$R/out-$N-upload.log" 2>&1) ||
|
||||
{ echo "build or upload failed"; tail -5 "$R/out-$N-upload.log"; exit 1; }
|
||||
grep -a -E '^(RAM|Flash):' "$R/out-$N-upload.log"
|
||||
RESET=--reset
|
||||
if [ "$B" = rp ]; then $PY "${PORTS:?set PORTS}" --wait 2e8a > /dev/null; sleep 1; RESET=; fi
|
||||
$PY "$SC" "$P" "$S" "$R/out-$N.txt" $RESET > /dev/null 2>&1
|
||||
[ -s "$R/out-$N.txt" ] || { echo "serial capture got nothing"; exit 1; }
|
||||
$PY "$R/analyze.py" "$R/spi$B.yaml" "$R/out-$N.txt"
|
||||
@@ -0,0 +1,608 @@
|
||||
#pragma once
|
||||
// SPI capture on the same board, no chips or wiring (#22376). Call add() per device, start() once, tick() every 20 ms.
|
||||
// Classic ESP32: SPI3 as a non-DMA slave fed from the master's pads through the GPIO matrix, answering on MISO.
|
||||
// Other boards: GPIO edge interrupts on CLK and CS take snapshots of the input register (low data rates only).
|
||||
#include <cstring>
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/spi_device/spi_device.h"
|
||||
#ifdef USE_ESP32
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_rom_gpio.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "soc/spi_periph.h"
|
||||
#include "soc/gpio_reg.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_cpu.h"
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#define SPITEST_SLAVE 1
|
||||
#include "driver/spi_slave.h"
|
||||
#endif
|
||||
#if defined(USE_ESP8266) || defined(USE_RP2)
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
#ifdef USE_RP2
|
||||
#include "hardware/structs/sio.h"
|
||||
#include "hardware/structs/io_bank0.h"
|
||||
#include "hardware/timer.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_RP2
|
||||
#define SPITEST_ISR
|
||||
#else
|
||||
#define SPITEST_ISR IRAM_ATTR
|
||||
#endif
|
||||
|
||||
namespace spitest {
|
||||
using esphome::spi_device::SPIDeviceComponent;
|
||||
static const char *const TAG = "spitest";
|
||||
|
||||
struct Dev {
|
||||
const char *name;
|
||||
SPIDeviceComponent *d;
|
||||
uint8_t mode;
|
||||
bool lsb;
|
||||
int cs, clk, mosi, miso;
|
||||
bool hw;
|
||||
uint8_t tag;
|
||||
};
|
||||
static Dev devs[8];
|
||||
static int ndev = 0;
|
||||
static esphome::PollingComponent *mx_ = nullptr;
|
||||
static int mx_cs_ = -1, mx_clk_ = -1, mx_mosi_ = -1;
|
||||
static int hw_mosi_ = -1, hw_miso_ = -1; // for the read path tricks
|
||||
|
||||
inline void add(const char *name, SPIDeviceComponent *d, int mode, bool lsb, int cs, int clk, int mosi, int miso,
|
||||
bool hw) {
|
||||
devs[ndev] = Dev{name, d, (uint8_t) mode, lsb, cs, clk, mosi, miso, hw, (uint8_t) (0x10 * (ndev + 1))};
|
||||
if (hw) {
|
||||
hw_mosi_ = mosi;
|
||||
hw_miso_ = miso;
|
||||
}
|
||||
ndev++;
|
||||
}
|
||||
inline void add_mx(esphome::PollingComponent *mx, int cs, int clk, int mosi) {
|
||||
mx_ = mx;
|
||||
mx_cs_ = cs;
|
||||
mx_clk_ = clk;
|
||||
mx_mosi_ = mosi;
|
||||
}
|
||||
|
||||
inline void hex(const uint8_t *b, size_t n, char *out) {
|
||||
for (size_t i = 0; i < n; i++)
|
||||
sprintf(out + 2 * i, "%02x", b[i]);
|
||||
out[2 * n] = 0;
|
||||
if (n == 0)
|
||||
strcpy(out, "-");
|
||||
}
|
||||
|
||||
static char boot_heap_[96];
|
||||
inline void log_heap(const char *what) {
|
||||
char *b = strcmp(what, "boot") == 0 ? boot_heap_ : nullptr;
|
||||
char tmp[96];
|
||||
if (b == nullptr)
|
||||
b = tmp;
|
||||
#ifdef USE_ESP32
|
||||
snprintf(b, 96, "HEAP %s free=%u total=%u used=%u minfree=%u", what,
|
||||
(unsigned) heap_caps_get_free_size(MALLOC_CAP_DEFAULT),
|
||||
(unsigned) heap_caps_get_total_size(MALLOC_CAP_DEFAULT),
|
||||
(unsigned) (heap_caps_get_total_size(MALLOC_CAP_DEFAULT) - heap_caps_get_free_size(MALLOC_CAP_DEFAULT)),
|
||||
(unsigned) heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT));
|
||||
#elif defined(USE_ESP8266)
|
||||
uint32_t total = 0x3FFFC000u - (uint32_t) &_heap_start[0];
|
||||
uint32_t fr = ESP.getFreeHeap();
|
||||
snprintf(b, 96, "HEAP %s free=%u total=%u used=%u", what, (unsigned) fr, (unsigned) total, (unsigned) (total - fr));
|
||||
#elif defined(USE_RP2)
|
||||
snprintf(b, 96, "HEAP %s free=%u total=%u used=%u", what, (unsigned) rp2040.getFreeHeap(),
|
||||
(unsigned) rp2040.getTotalHeap(), (unsigned) rp2040.getUsedHeap());
|
||||
#endif
|
||||
ESP_LOGI(TAG, "%s", b);
|
||||
}
|
||||
|
||||
// One operation on a device. Returns elapsed us; rx/rxn get what the master read.
|
||||
static int clk_before_ = -1, clk_after_ = -1;
|
||||
static char regs_[64] = "";
|
||||
inline int read_level(int pin);
|
||||
inline uint32_t do_op(Dev &v, int step, uint8_t *rx, size_t &rxn) {
|
||||
auto *d = v.d;
|
||||
uint8_t buf[8] = {0x01, 0x80, 0xA5, 0x3C, 0xF0, 0x0F, 0x55, v.tag};
|
||||
rxn = 0;
|
||||
uint32_t t0 = (esphome::micros) ();
|
||||
d->enable();
|
||||
clk_before_ = read_level(v.clk);
|
||||
switch (step) {
|
||||
case 0:
|
||||
case 5:
|
||||
case 6:
|
||||
case 8:
|
||||
d->transfer_array(buf, 8);
|
||||
memcpy(rx, buf, 8);
|
||||
rxn = 8;
|
||||
break;
|
||||
#ifdef USE_ESP32
|
||||
case 9: { // same loopback read into a word aligned DMA capable buffer
|
||||
static uint8_t *dma = (uint8_t *) heap_caps_malloc(8, MALLOC_CAP_DMA);
|
||||
memcpy(dma, buf, 8);
|
||||
d->transfer_array(dma, 8);
|
||||
memcpy(rx, dma, 8);
|
||||
rxn = 8;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case 1:
|
||||
d->write_byte16(0x1234 ^ v.tag);
|
||||
break;
|
||||
case 2:
|
||||
d->write(0x5A5, 12);
|
||||
break;
|
||||
case 3:
|
||||
memset(rx, 0x99, 4);
|
||||
d->read_array(rx, 4);
|
||||
rxn = 4;
|
||||
break;
|
||||
case 4: {
|
||||
uint8_t w[5] = {0xDE, 0xAD, 0xBE, 0xEF, v.tag};
|
||||
d->write_array(w, 5);
|
||||
break;
|
||||
}
|
||||
}
|
||||
clk_after_ = read_level(v.clk);
|
||||
d->disable();
|
||||
uint32_t t1 = (esphome::micros) ();
|
||||
#ifdef USE_ESP8266
|
||||
// The HSPI pads are not readable, so record what beginTransaction() programmed; endTransaction() leaves it.
|
||||
if (v.hw)
|
||||
snprintf(regs_, sizeof(regs_), " clk=%08x c=%08x u=%08x p=%08x", (unsigned) SPI1CLK, (unsigned) SPI1C,
|
||||
(unsigned) SPI1U, (unsigned) SPI1P);
|
||||
#endif
|
||||
return t1 - t0;
|
||||
}
|
||||
|
||||
inline void lifecycle(Dev &v, int step) {
|
||||
if (step == 5) {
|
||||
v.d->spi_teardown();
|
||||
v.d->spi_setup();
|
||||
} else if (step == 6) {
|
||||
v.d->spi_setup(); // already registered: must keep the existing delegate
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
(esphome::delay)(20); // let the log lines go out before the capture
|
||||
}
|
||||
|
||||
#ifdef SPITEST_SLAVE
|
||||
// ---------------- classic ESP32: SPI3 slave ----------------
|
||||
static constexpr int SCS = 14; // nominal slave CS, held high; driven low only for the max7219 window
|
||||
static WORD_ALIGNED_ATTR uint8_t s_rx[64];
|
||||
static WORD_ALIGNED_ATTR uint8_t s_tx[64];
|
||||
static bool s_init = false;
|
||||
static volatile uint16_t cs_edges[40];
|
||||
inline int read_level(int pin) { return gpio_get_level((gpio_num_t) pin); }
|
||||
|
||||
static void IRAM_ATTR cs_isr(void *arg) { cs_edges[(int) (intptr_t) arg]++; }
|
||||
|
||||
inline void arm_cs(int pin) {
|
||||
gpio_input_enable((gpio_num_t) pin);
|
||||
gpio_set_intr_type((gpio_num_t) pin, GPIO_INTR_ANYEDGE);
|
||||
gpio_isr_handler_add((gpio_num_t) pin, cs_isr, (void *) (intptr_t) pin);
|
||||
gpio_intr_enable((gpio_num_t) pin);
|
||||
}
|
||||
|
||||
inline void prep() {
|
||||
gpio_install_isr_service(0);
|
||||
for (int i = 0; i < ndev; i++) {
|
||||
gpio_input_enable((gpio_num_t) devs[i].clk);
|
||||
gpio_input_enable((gpio_num_t) devs[i].mosi);
|
||||
arm_cs(devs[i].cs);
|
||||
}
|
||||
if (mx_cs_ >= 0)
|
||||
arm_cs(mx_cs_);
|
||||
}
|
||||
|
||||
inline void slave_setup(int mode) {
|
||||
if (s_init)
|
||||
spi_slave_free(SPI3_HOST);
|
||||
spi_bus_config_t b{};
|
||||
b.mosi_io_num = -1;
|
||||
b.miso_io_num = -1;
|
||||
b.sclk_io_num = -1;
|
||||
b.quadwp_io_num = -1;
|
||||
b.quadhd_io_num = -1;
|
||||
b.max_transfer_sz = 64;
|
||||
spi_slave_interface_config_t s{};
|
||||
s.mode = mode;
|
||||
s.spics_io_num = SCS;
|
||||
s.queue_size = 1;
|
||||
ESP_ERROR_CHECK(spi_slave_initialize(SPI3_HOST, &b, &s, SPI_DMA_DISABLED));
|
||||
s_init = true;
|
||||
gpio_set_direction((gpio_num_t) SCS, GPIO_MODE_INPUT_OUTPUT); // plain GPIO output, input still enabled
|
||||
gpio_set_level((gpio_num_t) SCS, 1);
|
||||
}
|
||||
|
||||
inline void route(int clk, int mosi, int miso) {
|
||||
gpio_input_enable((gpio_num_t) clk);
|
||||
gpio_input_enable((gpio_num_t) mosi);
|
||||
esp_rom_gpio_connect_in_signal(clk, spi_periph_signal[SPI3_HOST].spiclk_in, false);
|
||||
esp_rom_gpio_connect_in_signal(mosi, spi_periph_signal[SPI3_HOST].spid_in, false);
|
||||
if (miso >= 0) {
|
||||
gpio_set_direction((gpio_num_t) miso, GPIO_MODE_INPUT_OUTPUT);
|
||||
esp_rom_gpio_connect_out_signal(miso, spi_periph_signal[SPI3_HOST].spiq_out, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
inline bool queue(int len, uint8_t tag) {
|
||||
static const uint8_t ans[8] = {0xC3, 0x5A, 0x81, 0x7E, 0x0F, 0xF0, 0x33, 0xCC};
|
||||
memset(s_rx, 0, sizeof(s_rx));
|
||||
for (int i = 0; i < 64; i++)
|
||||
s_tx[i] = ans[i % 8] ^ tag;
|
||||
static spi_slave_transaction_t t;
|
||||
t = {};
|
||||
t.length = len * 8;
|
||||
t.tx_buffer = s_tx;
|
||||
t.rx_buffer = s_rx;
|
||||
if (spi_slave_queue_trans(SPI3_HOST, &t, pdMS_TO_TICKS(100)) != ESP_OK)
|
||||
return false;
|
||||
esp_rom_delay_us(300); // let the slave ISR load it (it reconnects CS from SCS)
|
||||
return true;
|
||||
}
|
||||
|
||||
inline int result() {
|
||||
spi_slave_transaction_t *r;
|
||||
if (spi_slave_get_trans_result(SPI3_HOST, &r, pdMS_TO_TICKS(200)) == ESP_OK)
|
||||
return r->trans_len;
|
||||
// flush a transaction that never saw CS: pulse the slave CS
|
||||
esp_rom_gpio_connect_in_signal(SCS, spi_periph_signal[SPI3_HOST].spics_in, false);
|
||||
gpio_set_level((gpio_num_t) SCS, 0);
|
||||
esp_rom_delay_us(20);
|
||||
gpio_set_level((gpio_num_t) SCS, 1);
|
||||
spi_slave_get_trans_result(SPI3_HOST, &r, pdMS_TO_TICKS(200));
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline void cs_summary(int own, int &o, int &oth) {
|
||||
o = cs_edges[own];
|
||||
oth = 0;
|
||||
for (int p = 0; p < 40; p++)
|
||||
if (p != own)
|
||||
oth += cs_edges[p];
|
||||
}
|
||||
|
||||
inline void run_step(int pass, int di, int step) {
|
||||
char h1[140], h2[140];
|
||||
if (di < 0) { // max7219 window on the hardware bus
|
||||
slave_setup(0);
|
||||
route(mx_clk_, mx_mosi_, -1);
|
||||
bool q = queue(64, 0);
|
||||
memset((void *) cs_edges, 0, sizeof(cs_edges));
|
||||
gpio_set_level((gpio_num_t) SCS, 0);
|
||||
esp_rom_delay_us(10);
|
||||
uint32_t t0 = (esphome::micros) ();
|
||||
mx_->update();
|
||||
uint32_t us = (esphome::micros) () - t0;
|
||||
esp_rom_delay_us(10);
|
||||
gpio_set_level((gpio_num_t) SCS, 1);
|
||||
int bits = q ? result() : -2;
|
||||
int o, oth;
|
||||
cs_summary(mx_cs_, o, oth);
|
||||
hex(s_rx, bits > 0 ? bits / 8 : 0, h1);
|
||||
ESP_LOGI(TAG, "S %d mx 7 bits=%d srx=%s cs=%d/%d us=%u", pass, bits, h1, o, oth, (unsigned) us);
|
||||
return;
|
||||
}
|
||||
Dev &v = devs[di];
|
||||
lifecycle(v, step);
|
||||
if (step == 5 || step == 6)
|
||||
prep(); // a new delegate re-runs the CS pin setup
|
||||
slave_setup(v.mode);
|
||||
route(v.clk, v.mosi, v.miso);
|
||||
bool q = queue(64, v.tag);
|
||||
esp_rom_gpio_connect_in_signal(v.cs, spi_periph_signal[SPI3_HOST].spics_in, false);
|
||||
if (step == 8 || step == 9) // master reads its own MOSI pad
|
||||
esp_rom_gpio_connect_in_signal(v.mosi, spi_periph_signal[SPI2_HOST].spiq_in, false);
|
||||
memset((void *) cs_edges, 0, sizeof(cs_edges));
|
||||
uint8_t rx[8];
|
||||
size_t rxn;
|
||||
uint32_t us = do_op(v, step, rx, rxn);
|
||||
int bits = q ? result() : -2;
|
||||
gpio_set_direction((gpio_num_t) v.miso, GPIO_MODE_INPUT);
|
||||
if (step == 8 || step == 9)
|
||||
esp_rom_gpio_connect_in_signal(v.miso, spi_periph_signal[SPI2_HOST].spiq_in, false);
|
||||
int o, oth;
|
||||
cs_summary(v.cs, o, oth);
|
||||
hex(s_rx, bits > 0 ? (bits + 7) / 8 : 0, h1);
|
||||
hex(rx, rxn, h2);
|
||||
ESP_LOGI(TAG, "S %d %s %d bits=%d srx=%s cs=%d/%d c0=%d c1=%d", pass, v.name, step, bits, h1, o, oth, clk_before_,
|
||||
clk_after_);
|
||||
ESP_LOGI(TAG, "T %d %s %d us=%u rx=%s", pass, v.name, step, (unsigned) us, h2);
|
||||
}
|
||||
|
||||
#else
|
||||
// ---------------- edge interrupt snapshots ----------------
|
||||
struct Ev {
|
||||
uint32_t t;
|
||||
uint32_t in;
|
||||
};
|
||||
static constexpr int NEV = 1100;
|
||||
static Ev evs[NEV];
|
||||
static volatile int nev = 0;
|
||||
static volatile uint32_t ndrop = 0;
|
||||
|
||||
static inline __attribute__((always_inline)) uint32_t raw_in() {
|
||||
#if defined(USE_ESP8266)
|
||||
return (GPI & 0xFFFF) | ((GP16O & 1) << 16); // GPIO16 is only ever a CS output here
|
||||
#elif defined(USE_ESP32)
|
||||
return REG_READ(GPIO_IN_REG);
|
||||
#elif defined(USE_RP2)
|
||||
return sio_hw->gpio_in;
|
||||
#endif
|
||||
}
|
||||
static inline __attribute__((always_inline)) uint32_t raw_t() {
|
||||
#if defined(USE_ESP8266)
|
||||
return esp_get_cycle_count();
|
||||
#elif defined(USE_ESP32)
|
||||
return esp_cpu_get_cycle_count();
|
||||
#elif defined(USE_RP2)
|
||||
return time_us_32();
|
||||
#endif
|
||||
}
|
||||
static inline uint32_t ticks_per_us() {
|
||||
#if defined(USE_RP2)
|
||||
return 1;
|
||||
#else
|
||||
return esphome::arch_get_cpu_freq_hz() / 1000000;
|
||||
#endif
|
||||
}
|
||||
static void SPITEST_ISR isr_rec() {
|
||||
int i = nev;
|
||||
if (i < NEV) {
|
||||
evs[i].t = raw_t();
|
||||
evs[i].in = raw_in();
|
||||
nev = i + 1;
|
||||
} else {
|
||||
ndrop = ndrop + 1;
|
||||
}
|
||||
}
|
||||
#ifdef USE_ESP32
|
||||
static void SPITEST_ISR isr_rec_arg(void *) { isr_rec(); }
|
||||
#endif
|
||||
inline int read_level(int pin) { return (raw_in() >> pin) & 1; }
|
||||
|
||||
inline void arm(int pin) {
|
||||
#ifdef USE_ESP32
|
||||
gpio_input_enable((gpio_num_t) pin);
|
||||
gpio_set_intr_type((gpio_num_t) pin, GPIO_INTR_ANYEDGE);
|
||||
gpio_isr_handler_add((gpio_num_t) pin, isr_rec_arg, nullptr);
|
||||
gpio_intr_enable((gpio_num_t) pin);
|
||||
#else
|
||||
#ifdef USE_ESP8266
|
||||
if (pin == 16)
|
||||
return; // no interrupt on GPIO16; its level is still in every snapshot
|
||||
#endif
|
||||
attachInterrupt(digitalPinToInterrupt(pin), isr_rec, CHANGE);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool loop_done_ = false;
|
||||
inline void prep() {
|
||||
#ifdef USE_ESP32
|
||||
static bool svc = false;
|
||||
if (!svc) {
|
||||
svc = true;
|
||||
esp_err_t e = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM);
|
||||
ESP_LOGI(TAG, "isr service: %s", esp_err_to_name(e));
|
||||
}
|
||||
#endif
|
||||
for (int i = 0; i < ndev; i++) {
|
||||
arm(devs[i].clk);
|
||||
arm(devs[i].cs);
|
||||
#ifdef USE_ESP32
|
||||
gpio_input_enable((gpio_num_t) devs[i].mosi);
|
||||
#endif
|
||||
}
|
||||
if (mx_cs_ >= 0)
|
||||
arm(mx_cs_);
|
||||
if (loop_done_)
|
||||
return;
|
||||
loop_done_ = true;
|
||||
// software bus: MOSI and MISO share one pad, so the bit-banged read sees what was written
|
||||
for (int i = 0; i < ndev; i++) {
|
||||
if (devs[i].hw || devs[i].mosi != devs[i].miso)
|
||||
continue;
|
||||
#ifdef USE_ESP32
|
||||
gpio_set_direction((gpio_num_t) devs[i].mosi, GPIO_MODE_INPUT_OUTPUT);
|
||||
#else
|
||||
pinMode(devs[i].mosi, OUTPUT);
|
||||
#endif
|
||||
}
|
||||
#if defined(USE_ESP32)
|
||||
// C3 hardware bus: feed the SPI2 MISO input from the MOSI pad
|
||||
if (hw_mosi_ >= 0) {
|
||||
gpio_input_enable((gpio_num_t) hw_mosi_);
|
||||
esp_rom_gpio_connect_in_signal(hw_mosi_, spi_periph_signal[SPI2_HOST].spiq_in, false);
|
||||
}
|
||||
#elif defined(USE_ESP8266)
|
||||
if (hw_miso_ >= 0)
|
||||
GPF(hw_miso_) |= (1 << GPFPU); // pull-up, function unchanged: reads give 0xff
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_RP2
|
||||
// INOVER on the MISO pad: 2 forces the SPI input low, 3 high, 0 normal
|
||||
inline void rp2_miso(int pin, uint32_t v) {
|
||||
hw_write_masked(&io_bank0_hw->io[pin].ctrl, v << IO_BANK0_GPIO0_CTRL_INOVER_LSB, IO_BANK0_GPIO0_CTRL_INOVER_BITS);
|
||||
}
|
||||
#endif
|
||||
|
||||
static char es_[420];
|
||||
inline void decode(int pass, const char *name, int step, int cs, int clk, int mosi) {
|
||||
int n = nev;
|
||||
uint32_t csm = 1u << cs, clkm = 1u << clk, mom = 1u << mosi, othm = 0;
|
||||
for (int i = 0; i < ndev; i++)
|
||||
if (devs[i].cs != cs)
|
||||
othm |= 1u << devs[i].cs;
|
||||
if (mx_cs_ >= 0 && mx_cs_ != cs)
|
||||
othm |= 1u << mx_cs_;
|
||||
bool in = false, ov = false;
|
||||
int frames = 0, el = 0, idle = 0, nr = 0;
|
||||
uint32_t t_cs = 0, t_e1 = 0;
|
||||
uint32_t fr = 0, lr = 0;
|
||||
uint32_t prev = evs[0].in;
|
||||
for (int i = 1; i < n; i++) {
|
||||
uint32_t cur = evs[i].in;
|
||||
bool low = !(cur & csm);
|
||||
bool chg = (cur ^ prev) & clkm;
|
||||
if (!in && low) {
|
||||
in = true;
|
||||
el = 0;
|
||||
ov = false;
|
||||
nr = 0;
|
||||
idle = chg ? !!(prev & clkm) : !!(cur & clkm);
|
||||
t_cs = evs[i].t;
|
||||
t_e1 = 0;
|
||||
}
|
||||
if (in) {
|
||||
if (chg && low) {
|
||||
if (t_e1 == 0)
|
||||
t_e1 = evs[i].t;
|
||||
bool rise = cur & clkm;
|
||||
bool mo = cur & mom;
|
||||
if (el < (int) sizeof(es_) - 2)
|
||||
es_[el++] = rise ? (mo ? 'R' : 'r') : (mo ? 'F' : 'f');
|
||||
if (rise) {
|
||||
if (nr == 0)
|
||||
fr = evs[i].t;
|
||||
lr = evs[i].t;
|
||||
nr++;
|
||||
}
|
||||
}
|
||||
if ((~cur) & othm)
|
||||
ov = true;
|
||||
if (!low) {
|
||||
es_[el] = 0;
|
||||
uint32_t per10 = nr > 1 ? (uint32_t) ((uint64_t) (lr - fr) * 10 / (nr - 1) / ticks_per_us()) : 0;
|
||||
uint32_t t1 = t_e1 ? (uint32_t) ((uint64_t) (t_e1 - t_cs) * 10 / ticks_per_us()) : 0;
|
||||
ESP_LOGI(TAG, "F %d %s %d %d idle=%d ov=%d per10=%u t1x10=%u e=%s", pass, name, step, frames, idle, ov,
|
||||
(unsigned) per10, (unsigned) t1, el ? es_ : "-");
|
||||
frames++;
|
||||
in = false;
|
||||
}
|
||||
}
|
||||
prev = cur;
|
||||
}
|
||||
if (in) {
|
||||
es_[el] = 0;
|
||||
ESP_LOGW(TAG, "F %d %s %d %d UNTERMINATED e=%s", pass, name, step, frames, es_);
|
||||
}
|
||||
ESP_LOGI(TAG, "N %d %s %d frames=%d ev=%d drop=%u", pass, name, step, frames, n, (unsigned) ndrop);
|
||||
}
|
||||
|
||||
inline void run_step(int pass, int di, int step) {
|
||||
char h2[40];
|
||||
if (di < 0) {
|
||||
nev = 0;
|
||||
ndrop = 0;
|
||||
isr_rec();
|
||||
uint32_t t0 = (esphome::micros) ();
|
||||
mx_->update();
|
||||
uint32_t us = (esphome::micros) () - t0;
|
||||
isr_rec();
|
||||
decode(pass, "mx", 7, mx_cs_, mx_clk_, mx_mosi_);
|
||||
ESP_LOGI(TAG, "T %d mx 7 us=%u rx=-", pass, (unsigned) us);
|
||||
return;
|
||||
}
|
||||
Dev &v = devs[di];
|
||||
lifecycle(v, step);
|
||||
if (step == 5 || step == 6)
|
||||
prep();
|
||||
#ifdef USE_RP2
|
||||
if (v.hw)
|
||||
rp2_miso(v.miso, step == 3 ? 3 : 2);
|
||||
#endif
|
||||
nev = 0;
|
||||
ndrop = 0;
|
||||
isr_rec();
|
||||
uint8_t rx[8];
|
||||
size_t rxn;
|
||||
uint32_t us = do_op(v, step, rx, rxn);
|
||||
isr_rec();
|
||||
#ifdef USE_RP2
|
||||
if (v.hw)
|
||||
rp2_miso(v.miso, 0);
|
||||
#endif
|
||||
decode(pass, v.name, step, v.cs, v.clk, v.mosi);
|
||||
hex(rx, rxn, h2);
|
||||
ESP_LOGI(TAG, "T %d %s %d us=%u rx=%s c0=%d c1=%d%s", pass, v.name, step, (unsigned) us, h2, clk_before_, clk_after_,
|
||||
regs_);
|
||||
regs_[0] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------- schedule ----------------
|
||||
struct Item {
|
||||
int8_t dev;
|
||||
uint8_t step;
|
||||
};
|
||||
static Item sched_[200];
|
||||
static int nsched_ = 0, pos_ = 0;
|
||||
static constexpr int PASSES = 3;
|
||||
static uint32_t done_at_ = 0;
|
||||
static bool started_ = false;
|
||||
|
||||
inline void build() {
|
||||
for (int p = 0; p < PASSES; p++) {
|
||||
for (int i = 0; i < ndev; i++)
|
||||
for (int s = 0; s <= 4; s++)
|
||||
sched_[nsched_++] = Item{(int8_t) i, (uint8_t) s};
|
||||
for (int i = 0; i < ndev; i++) // back to back, all devices, then the first again
|
||||
sched_[nsched_++] = Item{(int8_t) i, 0};
|
||||
sched_[nsched_++] = Item{0, 0};
|
||||
for (int i = 0; i < ndev; i++) // teardown + setup on every device
|
||||
sched_[nsched_++] = Item{(int8_t) i, 5};
|
||||
sched_[nsched_++] = Item{1, 6}; // second spi_setup() on a registered device
|
||||
sched_[nsched_++] = Item{(int8_t) (ndev - 1), 6};
|
||||
#ifdef SPITEST_SLAVE
|
||||
for (int i = 0; i < ndev; i++) // hardware bus read through a MOSI to MISO loopback
|
||||
if (devs[i].hw) {
|
||||
sched_[nsched_++] = Item{(int8_t) i, 8};
|
||||
sched_[nsched_++] = Item{(int8_t) i, 9};
|
||||
}
|
||||
#endif
|
||||
if (mx_ != nullptr) {
|
||||
sched_[nsched_++] = Item{0, 0}; // leave the hardware bus clock at mode 0 idle before the max7219 window
|
||||
sched_[nsched_++] = Item{-1, 7};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void start() {
|
||||
prep();
|
||||
build();
|
||||
started_ = true;
|
||||
ESP_LOGI(TAG, "START items=%d devs=%d", nsched_, ndev);
|
||||
ESP_LOGI(TAG, "%s", boot_heap_);
|
||||
}
|
||||
|
||||
inline void tick() {
|
||||
if (!started_)
|
||||
return;
|
||||
if (pos_ < nsched_) {
|
||||
int per_pass = nsched_ / PASSES;
|
||||
Item it = sched_[pos_];
|
||||
run_step(pos_ / per_pass, it.dev, it.step);
|
||||
pos_++;
|
||||
if (pos_ == nsched_) {
|
||||
log_heap("after");
|
||||
ESP_LOGI(TAG, "DONE items=%d", nsched_);
|
||||
done_at_ = (esphome::millis) ();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (done_at_ != 0 && (esphome::millis) () - done_at_ > 2000) {
|
||||
done_at_ = 0;
|
||||
esphome::App.safe_reboot();
|
||||
}
|
||||
}
|
||||
} // namespace spitest
|
||||
@@ -246,7 +246,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
|
||||
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning; in `setup()`,
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning; an earlier step of a multi-step
|
||||
poll uses `status_comm_check(ok)`, which only sets it, so the last step must clear it; in `setup()`,
|
||||
`if (!this->setup_check(ok)) return;` marks the component failed on error.
|
||||
* **A hub's pointer to a child entity** comes from `SUB_SENSOR(name)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`,
|
||||
`SUB_SWITCH`, `SUB_NUMBER`, `SUB_SELECT` or `SUB_BUTTON`, which declare the `set_<name>_<type>()` setter and
|
||||
@@ -264,7 +265,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
(`cg.add_define("USE_SELEC_METER_EM4M")`), so the other models' builds leave it out; the runtime model check
|
||||
stays inside the `#ifdef`.
|
||||
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
|
||||
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
|
||||
sets `status_set_warning()` and the next success of the last step clears it; writes skip work when
|
||||
`is_failed()`.
|
||||
* **`dump_config()` already prints the failure reason:** for a failed component, core prints
|
||||
`<component> is marked FAILED: <reason>` right after the component's `dump_config()`, using the reason
|
||||
passed to `mark_failed()`. So `dump_config()` never checks `is_failed()` to log a failure, and no
|
||||
|
||||
+21
-145
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Awaitable, Callable, Iterator, Sequence
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
@@ -18,10 +18,8 @@ from esphome.const import (
|
||||
CONF_CONDITION,
|
||||
CONF_COUNT,
|
||||
CONF_ELSE,
|
||||
CONF_FOR,
|
||||
CONF_ID,
|
||||
CONF_IF,
|
||||
CONF_LAMBDA,
|
||||
CONF_THEN,
|
||||
CONF_TIME,
|
||||
CONF_TIMEOUT,
|
||||
@@ -1462,112 +1460,14 @@ async def choose_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
# Body actions that run code, which may change anything the condition reads
|
||||
_WHILE_CODE_ACTIONS = frozenset((CONF_LAMBDA, "script.execute", "component.update"))
|
||||
|
||||
|
||||
def _is_id_reference(id_: Any) -> bool:
|
||||
# An id generated later is still None during validation
|
||||
return isinstance(id_, ID) and not id_.is_declaration and id_.id is not None
|
||||
|
||||
|
||||
def _action_ids(actions: list[tuple[str, Any]]) -> set[str]:
|
||||
"""The ids the actions act on: their ``id`` option or shorthand id, and every id a lambda action names."""
|
||||
ids: set[str] = set()
|
||||
for key, options in actions:
|
||||
if key == CONF_LAMBDA:
|
||||
ids |= _referenced_ids(options)
|
||||
elif _is_id_reference(
|
||||
target := options.get(CONF_ID) if isinstance(options, dict) else options
|
||||
):
|
||||
ids.add(target.id)
|
||||
return ids
|
||||
|
||||
|
||||
def _referenced_ids(value: Any) -> set[str]:
|
||||
# Lazy: importing esphome.config here would load it for every entity component
|
||||
from esphome.config import iter_ids
|
||||
|
||||
return {id_.id for id_, _ in iter_ids(value) if _is_id_reference(id_)}
|
||||
|
||||
|
||||
def _condition_has_code(value: Any) -> bool:
|
||||
"""Whether a condition runs a lambda or depends on time (``for:``), so it can change on its own."""
|
||||
if isinstance(value, Lambda):
|
||||
return True
|
||||
if isinstance(value, dict):
|
||||
return CONF_FOR in value or any(_condition_has_code(v) for v in value.values())
|
||||
if isinstance(value, list):
|
||||
return any(_condition_has_code(v) for v in value)
|
||||
return False
|
||||
|
||||
|
||||
# The fix for a while whose body never waits, for the error and the warning
|
||||
_WHILE_FIX = "Add '- delay: 100ms' as the last action under this loop's 'then:'."
|
||||
|
||||
|
||||
def _condition_name(condition: ConfigType) -> str:
|
||||
"""The condition's name, with its place in the file when known.
|
||||
|
||||
A list condition is wrapped in an `and:` that has no place in the file, so its first entry names it.
|
||||
"""
|
||||
key = _registry_key(condition)
|
||||
if (
|
||||
not hasattr(key, "esp_range")
|
||||
and isinstance(items := condition[key], list)
|
||||
and items
|
||||
):
|
||||
key = _registry_key(items[0])
|
||||
if (location := getattr(key, "esp_range", None)) is None:
|
||||
return f"'{key}'"
|
||||
mark = location.start_mark
|
||||
return f"'{key}' ({mark.document}:{mark.line + 1}:{mark.column + 1})"
|
||||
|
||||
|
||||
def _validate_while_waits(config: ConfigType) -> ConfigType:
|
||||
"""Reject or warn about a while whose body never waits and cannot change its condition.
|
||||
|
||||
Such a loop runs its body again from inside the previous pass and never lets other components run, so
|
||||
a condition only they can change stays true until the device runs out of stack or the watchdog resets it.
|
||||
"""
|
||||
body = config[CONF_THEN]
|
||||
# An empty body ends the loop at once
|
||||
if not body or has_non_synchronous_actions(body):
|
||||
return config
|
||||
actions = list(_iter_actions(body))
|
||||
condition = config[CONF_CONDITION]
|
||||
# An action on an id the condition (or its lambda) reads, or a lambda action naming one, may change it
|
||||
if (read := _referenced_ids(condition)) and read & _action_ids(actions):
|
||||
return config
|
||||
if not _condition_has_code(condition) and all(
|
||||
action not in _WHILE_CODE_ACTIONS for action, _ in actions
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"The body of this 'while' loop on {_condition_name(condition)} never waits and "
|
||||
"cannot change the condition, so the condition never becomes false and the device "
|
||||
f"would crash. {_WHILE_FIX}",
|
||||
[CONF_THEN],
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"The body of the 'while' loop on %s never waits, so nothing else runs until the body "
|
||||
"changes the condition; if it does not, the device crashes. %s",
|
||||
_condition_name(condition),
|
||||
_WHILE_FIX,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@register_action(
|
||||
"while",
|
||||
WhileAction,
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
_validate_while_waits,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
@@ -1794,14 +1694,10 @@ def _check_collected(result: MockObj, collector: _InlineCollector) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _registry_key(full_config: ConfigType) -> str:
|
||||
"""The action or condition name of a validated entry."""
|
||||
return next(key for key in full_config if key != CONF_TYPE_ID)
|
||||
|
||||
|
||||
def _builder_name(full_config: ConfigType) -> str:
|
||||
"""For an error: the id is usually generated, so the registry key tells the author where to look."""
|
||||
return f"'{_registry_key(full_config)}' ({full_config[CONF_TYPE_ID]})"
|
||||
name = next(key for key in full_config if key != CONF_TYPE_ID)
|
||||
return f"'{name}' ({full_config[CONF_TYPE_ID]})"
|
||||
|
||||
|
||||
async def _build_condition_or_inline(
|
||||
@@ -1934,44 +1830,24 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
|
||||
option that shares a name with an action, such as the ``delay:`` option of
|
||||
``speaker_source.set_playlist_delay``, is not mistaken for that action.
|
||||
"""
|
||||
return any(_is_deferring(key) for key, _ in _iter_actions(actions))
|
||||
|
||||
|
||||
def _is_deferring(key: str) -> bool:
|
||||
return key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous
|
||||
|
||||
|
||||
def _iter_actions(actions: Any) -> Iterator[tuple[str, Any]]:
|
||||
"""Yield ``(key, options)`` of every action in an action list, a single action or an automation.
|
||||
|
||||
Nested action lists (``then:``, ``else:``, ...) are searched; other options, including conditions, are not
|
||||
taken for actions.
|
||||
"""
|
||||
if isinstance(actions, list):
|
||||
for item in actions:
|
||||
yield from _iter_actions(item)
|
||||
elif isinstance(actions, dict):
|
||||
return any(has_non_synchronous_actions(item) for item in actions)
|
||||
if isinstance(actions, dict):
|
||||
for key, options in actions.items():
|
||||
yield key, options
|
||||
if key == CONF_CHOOSE:
|
||||
# A list of choices, not of actions
|
||||
for choice in options:
|
||||
yield from _iter_option_actions(choice)
|
||||
else:
|
||||
yield from _iter_option_actions(options)
|
||||
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
|
||||
return True
|
||||
if _options_have_non_synchronous_actions(options):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Options that hold conditions, not actions, though and:/or: are lists
|
||||
_CONDITION_OPTIONS = frozenset((CONF_CONDITION, CONF_ANY, CONF_ALL, CONF_IF))
|
||||
|
||||
|
||||
def _iter_option_actions(options: Any) -> Iterator[tuple[str, Any]]:
|
||||
def _options_have_non_synchronous_actions(options: Any) -> bool:
|
||||
"""Search an action's options for nested action lists, without treating option keys as actions."""
|
||||
if isinstance(options, list):
|
||||
yield from _iter_actions(options)
|
||||
elif isinstance(options, dict):
|
||||
for key, value in options.items():
|
||||
if key not in _CONDITION_OPTIONS:
|
||||
yield from _iter_option_actions(value)
|
||||
return has_non_synchronous_actions(options)
|
||||
if isinstance(options, dict):
|
||||
return any(_options_have_non_synchronous_actions(v) for v in options.values())
|
||||
return False
|
||||
|
||||
|
||||
def owning_args(args: TemplateArgsType, conf: ConfigType) -> TemplateArgsType:
|
||||
|
||||
@@ -49,6 +49,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
static_buffer,
|
||||
static_const_array,
|
||||
static_function,
|
||||
static_storage,
|
||||
templatable,
|
||||
unique_global_id,
|
||||
variable,
|
||||
@@ -92,6 +93,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
StringRef,
|
||||
arduino_json_ns,
|
||||
bool_,
|
||||
byte_span,
|
||||
char,
|
||||
const_char_ptr,
|
||||
double,
|
||||
|
||||
@@ -87,10 +87,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
|
||||
}
|
||||
|
||||
if (!this->continuous_mode_ || this->prev_config_ != config) {
|
||||
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
|
||||
this->status_set_warning();
|
||||
if (!this->status_comm_check(this->write_byte_16(ADS1115_REGISTER_CONFIG, config)))
|
||||
return NAN;
|
||||
}
|
||||
this->prev_config_ = config;
|
||||
|
||||
// Delay calculated as: ceil((1000/SPS)+.5)
|
||||
@@ -159,10 +157,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
|
||||
}
|
||||
|
||||
uint16_t raw_conversion;
|
||||
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &raw_conversion)) {
|
||||
this->status_set_warning();
|
||||
if (!this->status_comm_result(this->read_byte_16(ADS1115_REGISTER_CONVERSION, &raw_conversion)))
|
||||
return NAN;
|
||||
}
|
||||
|
||||
if (resolution == ADS1015_12_BITS) {
|
||||
// ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend
|
||||
@@ -196,7 +192,6 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
|
||||
millivolts = NAN;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
return millivolts / 1e3f;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,9 +102,7 @@ void AHT10Component::read_data_() {
|
||||
ESP_LOGV(TAG, "Invalid humidity (reading not required)");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Invalid humidity, retrying");
|
||||
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
this->status_comm_check(this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) == i2c::ERROR_OK);
|
||||
this->restart_read_();
|
||||
return;
|
||||
}
|
||||
@@ -130,10 +128,8 @@ void AHT10Component::update() {
|
||||
if (this->read_count_ != 0)
|
||||
return;
|
||||
this->start_time_ = millis();
|
||||
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
if (!this->status_comm_check(this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) == i2c::ERROR_OK))
|
||||
return;
|
||||
}
|
||||
this->restart_read_();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,8 @@ void AM2320Component::update() {
|
||||
uint8_t data[8];
|
||||
data[0] = 0;
|
||||
data[1] = 4;
|
||||
if (!this->read_data_(data)) {
|
||||
this->status_set_warning();
|
||||
if (!this->status_comm_check(this->read_data_(data)))
|
||||
return;
|
||||
}
|
||||
|
||||
float temperature = (((data[4] & 0x7F) << 8) + data[5]) / 10.0f;
|
||||
temperature = (data[4] & 0x80) ? -temperature : temperature;
|
||||
|
||||
@@ -10,10 +10,8 @@ ESPHOME_LOG_TAG(TAG, "ax15231.touchscreen");
|
||||
constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, 0x0, 0x0, 0x0, 0x8};
|
||||
|
||||
#define ERROR_CHECK(err) \
|
||||
if ((err) != i2c::ERROR_OK) { \
|
||||
this->status_set_warning(LOG_STR("Failed to communicate")); \
|
||||
return; \
|
||||
}
|
||||
if (!this->status_comm_check((err) == i2c::ERROR_OK)) \
|
||||
return;
|
||||
|
||||
void AXS15231Touchscreen::setup() {
|
||||
pulse_reset(this->reset_pin_, {.low_ms = 5, .high_after_ms = 10});
|
||||
|
||||
@@ -194,10 +194,6 @@ void BangBangClimate::change_away_(bool away) {
|
||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
||||
this->normal_config_ = normal_config;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
||||
this->supports_away_ = true;
|
||||
this->away_config_ = away_config;
|
||||
|
||||
@@ -27,7 +27,7 @@ class BangBangClimate final : public climate::Climate, public Component {
|
||||
|
||||
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; }
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
Trigger<> *get_idle_trigger();
|
||||
|
||||
@@ -159,17 +159,7 @@ void spi_dma_tx_finish_callback(unsigned int param) {
|
||||
}
|
||||
|
||||
void BekenSPILEDStripLightOutput::setup() {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
if (!this->setup_check(this->internal_accessor_.allocate(allocator, this->num_leds_),
|
||||
LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
|
||||
return;
|
||||
|
||||
const size_t dma_buffer_size = (this->get_buffer_size_() * 8) + (2 * 64);
|
||||
this->dma_buf_ = allocator.allocate(dma_buffer_size);
|
||||
if (!this->setup_check(this->dma_buf_ != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
|
||||
return;
|
||||
|
||||
memset(this->dma_buf_, 0, dma_buffer_size);
|
||||
const size_t dma_buffer_size = dma_buffer_bytes(this->get_buffer_size_());
|
||||
|
||||
uint32_t value = PCLK_POSI_SPI;
|
||||
sddev_control(ICU_DEV_NAME, CMD_CONF_PCLK_26M, &value);
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
namespace esphome::beken_spi_led_strip {
|
||||
|
||||
/// 8 SPI bytes per color byte plus 2 * 64 bytes of padding
|
||||
static constexpr size_t dma_buffer_bytes(size_t color_bytes) { return color_bytes * 8 + 2 * 64; }
|
||||
|
||||
class BekenSPILEDStripLightOutput final : public light::AddressableLight {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -26,6 +29,12 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
|
||||
this->internal_accessor_.channel_colors = channel_colors;
|
||||
}
|
||||
|
||||
/// Call after set_num_leds() and set_channel_colors(); `dma_buf` is a word aligned dma_buffer_bytes() block.
|
||||
void set_buffers(uint8_t *led_data, uint8_t *dma_buf) {
|
||||
this->internal_accessor_.set_buffer(led_data, this->num_leds_);
|
||||
this->dma_buf_ = dma_buf;
|
||||
}
|
||||
|
||||
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
|
||||
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
|
||||
|
||||
|
||||
@@ -118,3 +118,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(
|
||||
var.set_channel_colors(light.channel_colors_struct(config[CONF_CHANNEL_COLORS]))
|
||||
)
|
||||
num_leds = config[CONF_NUM_LEDS]
|
||||
channel_colors = config[CONF_CHANNEL_COLORS]
|
||||
led_data = light.static_led_buffer(
|
||||
config[CONF_OUTPUT_ID], num_leds, len(channel_colors)
|
||||
)
|
||||
dma_buf = cg.static_buffer(
|
||||
config[CONF_OUTPUT_ID],
|
||||
cg.uint8,
|
||||
beken_spi_led_strip_ns.dma_buffer_bytes(num_leds * len(channel_colors)),
|
||||
align=4,
|
||||
)
|
||||
cg.add(var.set_buffers(led_data, dma_buf))
|
||||
@@ -122,10 +122,8 @@ void BH1745Component::update() {
|
||||
|
||||
// Reading MODE_CONTROL2 clears VALID, so read it only once per poll.
|
||||
uint8_t mode_control2;
|
||||
if (!this->read_byte(REG_MODE_CONTROL2, &mode_control2)) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
if (!this->status_comm_check(this->read_byte(REG_MODE_CONTROL2, &mode_control2)))
|
||||
return;
|
||||
}
|
||||
if ((mode_control2 & MODE_CONTROL2_VALID) == 0)
|
||||
return;
|
||||
|
||||
@@ -153,9 +151,7 @@ void BH1745Component::update() {
|
||||
void BH1745Component::set_interrupt_state(bool on) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (!this->write_byte(REG_INTERRUPT, on ? INTERRUPT_ENABLE : 0x00)) {
|
||||
this->status_set_warning();
|
||||
}
|
||||
this->status_comm_check(this->write_byte(REG_INTERRUPT, on ? INTERRUPT_ENABLE : 0x00));
|
||||
}
|
||||
|
||||
} // namespace esphome::bh1745
|
||||
@@ -30,11 +30,6 @@ from esphome.const import (
|
||||
CONF_DURATION,
|
||||
CONF_ID,
|
||||
CONF_INTERVAL,
|
||||
CONF_MANUFACTURER_ID,
|
||||
CONF_ON_BLE_ADVERTISE,
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||
CONF_SERVICE_UUID,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
@@ -55,9 +50,6 @@ BK72xxBLETracker = bk72xx_ble_tracker_ns.class_(
|
||||
|
||||
StartScanAction = bk72xx_ble_tracker_ns.class_("StartScanAction", automation.Action)
|
||||
|
||||
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
|
||||
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
|
||||
BLEManufacturerDataAdvertiseTrigger = ble_automation.BLEManufacturerDataAdvertiseTrigger
|
||||
BLEEndOfScanTrigger = ble_automation.BLEEndOfScanTrigger
|
||||
|
||||
|
||||
@@ -71,21 +63,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(): cv.declare_id(BK72xxBLETracker),
|
||||
cv.GenerateID(CONF_BK72XX_BLE_ID): cv.use_id(bk72xx_ble.BK72xxBLE),
|
||||
cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA,
|
||||
cv.Optional(CONF_ON_BLE_ADVERTISE): ble_automation.advertise_trigger_schema(
|
||||
ESPBTAdvertiseTrigger
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE
|
||||
): ble_automation.uuid_trigger_schema(
|
||||
BLEServiceDataAdvertiseTrigger,
|
||||
{cv.Required(CONF_SERVICE_UUID): ble_device_base.bt_uuid},
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE
|
||||
): ble_automation.uuid_trigger_schema(
|
||||
BLEManufacturerDataAdvertiseTrigger,
|
||||
{cv.Required(CONF_MANUFACTURER_ID): ble_device_base.bt_uuid},
|
||||
),
|
||||
**ble_automation.ADVERTISE_TRIGGERS_SCHEMA,
|
||||
cv.Optional(CONF_ON_SCAN_END): ble_automation.scan_end_trigger_schema(
|
||||
BLEEndOfScanTrigger
|
||||
),
|
||||
@@ -158,21 +136,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_configured_continuous(scan[CONF_CONTINUOUS]))
|
||||
cg.add(var.set_scan_active(scan[CONF_ACTIVE]))
|
||||
|
||||
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
||||
await ble_automation.advertise_trigger_to_code(conf, var)
|
||||
|
||||
for trigger_key, uuid_key, setter_prefix in (
|
||||
(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, "set_service_uuid"),
|
||||
(
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_MANUFACTURER_ID,
|
||||
"set_manufacturer_uuid",
|
||||
),
|
||||
):
|
||||
for conf in config.get(trigger_key, []):
|
||||
await ble_automation.uuid_trigger_to_code(
|
||||
conf, var, uuid_key, setter_prefix
|
||||
)
|
||||
await ble_automation.advertise_triggers_to_code(config, var)
|
||||
|
||||
for conf in config.get(CONF_ON_SCAN_END, []):
|
||||
await ble_automation.scan_end_trigger_to_code(conf, var)
|
||||
@@ -70,8 +70,8 @@ class BLEServiceDataAdvertiseTrigger final : public Trigger<const adv_data_t &>,
|
||||
}
|
||||
|
||||
protected:
|
||||
ESPBTUUID uuid_{};
|
||||
uint64_t address_{0};
|
||||
ESPBTUUID uuid_{};
|
||||
bool has_address_{false};
|
||||
};
|
||||
|
||||
@@ -104,8 +104,8 @@ class BLEManufacturerDataAdvertiseTrigger final : public Trigger<const adv_data_
|
||||
}
|
||||
|
||||
protected:
|
||||
ESPBTUUID uuid_{};
|
||||
uint64_t address_{0};
|
||||
ESPBTUUID uuid_{};
|
||||
bool has_address_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@ from typing import Any
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MAC_ADDRESS, CONF_TRIGGER_ID
|
||||
from esphome.const import (
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_MANUFACTURER_ID,
|
||||
CONF_ON_BLE_ADVERTISE,
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||
CONF_SERVICE_UUID,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -18,6 +26,7 @@ from . import (
|
||||
as_hex,
|
||||
as_reversed_hex_array,
|
||||
ble_device_base_ns,
|
||||
bt_uuid,
|
||||
)
|
||||
|
||||
adv_data_t = cg.std_vector.template(cg.uint8)
|
||||
@@ -51,20 +60,15 @@ _UUID_WIDTHS = {
|
||||
}
|
||||
|
||||
|
||||
def uuid_trigger_schema(
|
||||
trigger_class: MockObjClass, extra: dict[Any, Any] | None = None
|
||||
def _uuid_trigger_schema(
|
||||
trigger_class: MockObjClass, uuid_key: str
|
||||
) -> Callable[[Any], Any]:
|
||||
"""Schema for a UUID-filtered trigger — pairs with uuid_trigger_to_code().
|
||||
|
||||
`extra` carries the required UUID key (a cv marker, so a dict rather than
|
||||
**kwargs); the optional single-mac filter is what uuid_trigger_to_code()
|
||||
reads back.
|
||||
"""
|
||||
"""Schema for a UUID-filtered trigger — pairs with _uuid_trigger_to_code()."""
|
||||
return automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class),
|
||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
**(extra or {}),
|
||||
cv.Required(uuid_key): bt_uuid,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -80,12 +84,12 @@ def _filter_mac(value: Any) -> Any:
|
||||
MAC_FILTER_LIST = cv.ensure_list(_filter_mac)
|
||||
|
||||
|
||||
def advertise_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any]:
|
||||
def _advertise_trigger_schema() -> Callable[[Any], Any]:
|
||||
"""on_ble_advertise schema: multi-mac list filter, unlike the single-mac
|
||||
uuid_trigger_schema() — pairs with advertise_trigger_to_code()."""
|
||||
_uuid_trigger_schema() — pairs with _advertise_trigger_to_code()."""
|
||||
return automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger),
|
||||
cv.Optional(CONF_MAC_ADDRESS): MAC_FILTER_LIST,
|
||||
}
|
||||
)
|
||||
@@ -112,7 +116,7 @@ def mac_filter_table(macs: list) -> cg.MockObj:
|
||||
)
|
||||
|
||||
|
||||
async def advertise_trigger_to_code(conf: ConfigType, var: cg.MockObj) -> None:
|
||||
async def _advertise_trigger_to_code(conf: ConfigType, var: cg.MockObj) -> None:
|
||||
"""Build an on_ble_advertise trigger (optional multi-mac filter)."""
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
if macs := conf.get(CONF_MAC_ADDRESS):
|
||||
@@ -128,7 +132,7 @@ async def scan_end_trigger_to_code(conf: ConfigType, var: cg.MockObj) -> None:
|
||||
_count_listener()
|
||||
|
||||
|
||||
async def uuid_trigger_to_code(
|
||||
async def _uuid_trigger_to_code(
|
||||
conf: ConfigType, var: cg.MockObj, key: str, setter_prefix: str
|
||||
) -> None:
|
||||
"""Build a UUID-filtered advertise trigger.
|
||||
@@ -145,3 +149,34 @@ async def uuid_trigger_to_code(
|
||||
cg.add(trigger.set_address(mac.as_hex))
|
||||
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
||||
_count_listener()
|
||||
|
||||
|
||||
# (trigger key, uuid key, setter prefix) of the UUID-filtered advertise triggers.
|
||||
_UUID_TRIGGERS = (
|
||||
(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, "set_service_uuid"),
|
||||
(
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_MANUFACTURER_ID,
|
||||
"set_manufacturer_uuid",
|
||||
),
|
||||
)
|
||||
|
||||
# Advertise trigger keys shared by every tracker; pairs with advertise_triggers_to_code().
|
||||
ADVERTISE_TRIGGERS_SCHEMA = {
|
||||
cv.Optional(CONF_ON_BLE_ADVERTISE): _advertise_trigger_schema(),
|
||||
cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): _uuid_trigger_schema(
|
||||
BLEServiceDataAdvertiseTrigger, CONF_SERVICE_UUID
|
||||
),
|
||||
cv.Optional(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE): _uuid_trigger_schema(
|
||||
BLEManufacturerDataAdvertiseTrigger, CONF_MANUFACTURER_ID
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def advertise_triggers_to_code(config: ConfigType, var: cg.MockObj) -> None:
|
||||
"""Build every ADVERTISE_TRIGGERS_SCHEMA trigger in a tracker config on hub `var`."""
|
||||
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
||||
await _advertise_trigger_to_code(conf, var)
|
||||
for trigger_key, uuid_key, setter_prefix in _UUID_TRIGGERS:
|
||||
for conf in config.get(trigger_key, []):
|
||||
await _uuid_trigger_to_code(conf, var, uuid_key, setter_prefix)
|
||||
@@ -35,7 +35,7 @@ CONF_VELOCITY = "velocity"
|
||||
ble_midi_ns = cg.esphome_ns.namespace("ble_midi")
|
||||
BLEMidi = ble_midi_ns.class_("BLEMidi", cg.Component, ble_client.BLEClientNode)
|
||||
# The message bytes as a view: data.size() and data[i] work, and nothing is copied
|
||||
ByteSpan = cg.std_span.template(cg.uint8.operator("const"))
|
||||
ByteSpan = cg.byte_span
|
||||
|
||||
# MIDI data bytes are 7-bit and channels are 4-bit, matching what goes on the wire and what the triggers report.
|
||||
# Channel 0 is the channel that MIDI hardware usually labels "1".
|
||||
|
||||
@@ -157,18 +157,14 @@ inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (
|
||||
void BME280Component::update() {
|
||||
// Enable sensor
|
||||
ESP_LOGV(TAG, "Sending conversion request");
|
||||
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, this->humidity_oversampling_ & 0b111)) {
|
||||
this->status_set_warning();
|
||||
if (!this->status_comm_check(this->write_byte(BME280_REGISTER_CONTROLHUMID, this->humidity_oversampling_ & 0b111)))
|
||||
return;
|
||||
}
|
||||
uint8_t meas_value = 0;
|
||||
meas_value |= (this->temperature_oversampling_ & 0b111) << 5;
|
||||
meas_value |= (this->pressure_oversampling_ & 0b111) << 2;
|
||||
meas_value |= BME280_MODE_FORCED;
|
||||
if (!this->write_byte(BME280_REGISTER_CONTROL, meas_value)) {
|
||||
this->status_set_warning();
|
||||
if (!this->status_comm_check(this->write_byte(BME280_REGISTER_CONTROL, meas_value)))
|
||||
return;
|
||||
}
|
||||
|
||||
float meas_time = 1.5f;
|
||||
meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_);
|
||||
@@ -280,15 +276,6 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
|
||||
|
||||
return h / 1024.0f;
|
||||
}
|
||||
void BME280Component::set_temperature_oversampling(BME280Oversampling temperature_over_sampling) {
|
||||
this->temperature_oversampling_ = temperature_over_sampling;
|
||||
}
|
||||
void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over_sampling) {
|
||||
this->pressure_oversampling_ = pressure_over_sampling;
|
||||
}
|
||||
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
|
||||
this->humidity_oversampling_ = humidity_over_sampling;
|
||||
}
|
||||
uint8_t BME280Component::read_u8_(uint8_t a_register) {
|
||||
uint8_t data = 0;
|
||||
this->read_byte(a_register, &data);
|
||||
|
||||
@@ -63,11 +63,17 @@ class BME280Component : public PollingComponent {
|
||||
|
||||
public:
|
||||
/// Set the oversampling value for the temperature sensor. Default is 16x.
|
||||
void set_temperature_oversampling(BME280Oversampling temperature_over_sampling);
|
||||
void set_temperature_oversampling(BME280Oversampling temperature_over_sampling) {
|
||||
this->temperature_oversampling_ = temperature_over_sampling;
|
||||
}
|
||||
/// Set the oversampling value for the pressure sensor. Default is 16x.
|
||||
void set_pressure_oversampling(BME280Oversampling pressure_over_sampling);
|
||||
void set_pressure_oversampling(BME280Oversampling pressure_over_sampling) {
|
||||
this->pressure_oversampling_ = pressure_over_sampling;
|
||||
}
|
||||
/// Set the oversampling value for the humidity sensor. Default is 16x.
|
||||
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
|
||||
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
|
||||
this->humidity_oversampling_ = humidity_over_sampling;
|
||||
}
|
||||
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
|
||||
void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
|
||||
|
||||
@@ -175,10 +175,8 @@ void BME680Component::update() {
|
||||
meas_control |= (this->temperature_oversampling_ & 0b111) << 5;
|
||||
meas_control |= (this->pressure_oversampling_ & 0b111) << 2;
|
||||
meas_control |= 0b01; // forced mode
|
||||
if (!this->write_byte(BME680_REGISTER_CONTROL_MEAS, meas_control)) {
|
||||
this->status_set_warning();
|
||||
if (!this->status_comm_check(this->write_byte(BME680_REGISTER_CONTROL_MEAS, meas_control)))
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(DATA_TIMEOUT_ID, this->calc_meas_duration_(), [this]() { this->read_data_(); });
|
||||
}
|
||||
@@ -423,15 +421,6 @@ uint32_t BME680Component::calc_meas_duration_() {
|
||||
|
||||
return tph_dur;
|
||||
}
|
||||
void BME680Component::set_temperature_oversampling(BME680Oversampling temperature_oversampling) {
|
||||
this->temperature_oversampling_ = temperature_oversampling;
|
||||
}
|
||||
void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_oversampling) {
|
||||
this->pressure_oversampling_ = pressure_oversampling;
|
||||
}
|
||||
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
|
||||
this->humidity_oversampling_ = humidity_oversampling;
|
||||
}
|
||||
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
|
||||
this->heater_temperature_ = heater_temperature;
|
||||
this->heater_duration_ = heater_duration;
|
||||
|
||||
Loaded 100 of 300 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user