mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 03:32:50 +00:00
Merge remote-tracking branch 'origin/dev' into esp8266-entity-names-flash
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: hardware-test
|
||||
description: Test an ESPHome change on real boards over USB. Use this whenever a task asks to test, verify, reproduce, bench, A/B or soak a pull request, a branch or a crash on hardware, or mentions flashing, serial logs, esphome upload, a captive portal or fallback AP test, or results to post on a PR. Covers finding the serial ports, building from the right checkout, flashing each chip family (including the LibreTiny kits that stock tools cannot flash), bounded log capture, proven test flows and how to report results.
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Hardware testing
|
||||
|
||||
The goal of a hardware test is evidence a reviewer can trust: which build ran on which board, what was
|
||||
driven, what the logs show, and the same flow on a `dev` build when a quirk could be pre-existing.
|
||||
`$SKILL` below is this directory (`.agents/skills/hardware-test`).
|
||||
|
||||
## Ground rules
|
||||
|
||||
1. **Serial ports cannot be shared.** One bounded capture per port, stopped before any flash on that
|
||||
port, never an open-ended `esphome logs`. Stop a capture with `pkill -f "[s]erial_capture.py <port> "`,
|
||||
naming the port so a capture on another board survives; the bracket keeps the pattern from matching
|
||||
the shell that runs it. Never run `esphome logs` in a
|
||||
`while true` loop: the loop respawns it and holds the port.
|
||||
2. **Credentials stay local.** Put a `secrets.yaml` next to the scratch configs and reference it with
|
||||
`!secret`. Never paste a config with credentials into a PR, never commit scratch configs or logs.
|
||||
3. **Build from the tree under test.** `python -m esphome` puts the current directory first on
|
||||
`sys.path`, so run it with the checkout or worktree as the working directory and pass the yaml by
|
||||
its absolute path. A venv `esphome` entry point may import an installed snapshot instead of the
|
||||
checkout. Confirm with `python -c 'import esphome; print(esphome.__file__)'`, and for an A/B grep
|
||||
the build's `src/` for the change before trusting any number.
|
||||
4. **PR build and baseline build get different `name:` values.** Builds share
|
||||
`.esphome/build/<name>`, so the same name means `esphome upload` can flash the wrong binary. Saved
|
||||
Wi-Fi credentials are keyed by the name too, so a test that must start unprovisioned gets a fresh
|
||||
name every run.
|
||||
5. **Flash and RAM deltas are measured against the PR's merge base** (`git merge-base origin/dev HEAD`
|
||||
checked out in a throwaway worktree), with the baseline commit named in the table. When a delta
|
||||
looks odd, diff symbol sizes (`nm -S -C --size-sort` from the matching toolchain) before quoting it.
|
||||
6. **Prove a quirk is pre-existing before blaming the PR:** run the identical flow on a `dev` build in
|
||||
the same session and say so in the results.
|
||||
7. **Pipe safely.** Set `PYTHONUNBUFFERED=1` on any `esphome run` or `esphome logs` whose output is
|
||||
piped, or lines land minutes late. Bound every long command with `timeout` (coreutils) or
|
||||
`perl -e 'alarm shift; exec @ARGV' <seconds> <command>` on macOS without it.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Find the boards:** `python $SKILL/scripts/ports.py` (any Python with pyserial, for example the
|
||||
ESPHome venv). Port names follow the USB hub port and change on every replug, so never reuse a name
|
||||
from an earlier session. CH340 adapters (D1 mini, BW15 kit, BK7238, CB3S) all look the same; tell
|
||||
them apart with a 10 s capture using `serial_capture.py --reset` (ESP8266 prints its ROM banner at
|
||||
74880 baud, LibreTiny boards print `[LT]` and ESPHome lines, BK7238 prints its bootloader version).
|
||||
`ports.py --wait 2e8a` blocks until a Pico re-enumerates.
|
||||
2. **Make a scratch directory** outside the checkout, with `secrets.yaml` and the templates you need.
|
||||
One yaml per board per variant, file name equal to `name:`.
|
||||
3. **Compile** in the right tree:
|
||||
`(cd <worktree> && PYTHONUNBUFFERED=1 timeout 1500 python -m esphome compile <abs yaml> > <name>-compile.log 2>&1)`.
|
||||
Keep the RAM and flash lines at the end. Two ESP-IDF or two LibreTiny builds at the same time race
|
||||
over tool caches; run them one at a time.
|
||||
4. **Flash** per chip family (table below). Stop the capture on that port first.
|
||||
5. **Capture** with `serial_capture.py PORT SECONDS OUT` in the background, then poll the file with
|
||||
`grep` in an `until ... sleep 2` loop with a cap. USB CDC boards (Pico, nRF52, native USB ESP32)
|
||||
drop the port on reboot; the script reopens it until its deadline.
|
||||
6. **Drive** the test: a second board running a client config, `curl`, an aioesphomeapi script that
|
||||
presses a template button and subscribes to logs, MQTT publishes, or `interval:` and `on_boot:`
|
||||
automations in the config itself.
|
||||
7. **Analyze** with `grep -a -c` counts over the capture and keep an `-events.txt` extract of the lines
|
||||
that matter. Decode crashes with the platform's `addr2line` (see `reference/boards.md`).
|
||||
8. **Report** on the PR as a table: build SHA, board and framework, what was driven, counts, result,
|
||||
plus the `dev` control row and the exact flow in a `<details>` block. No credentials, no port names,
|
||||
no IP addresses.
|
||||
|
||||
## Flashing by chip family
|
||||
|
||||
| Board | Flash | Log |
|
||||
|---|---|---|
|
||||
| ESP8266 D1 mini (CH340) | `python -m esphome upload <yaml> --device <port>` | `serial_capture.py` at 115200; `esphome logs` on some CH340s emits garbage after the RTS reset. A failed esptool write can wedge the adapter until the board is replugged. `esptool erase_flash` before a test that needs an unprovisioned device: the preference sector survives a reflash |
|
||||
| ESP32 devkit (CP2102 or CH340) | `esphome upload --device <port>`, ESP-IDF or Arduino | `serial_capture.py`. Default Wi-Fi light sleep delays API driven actions by seconds; judge timing from device logs. `power_save_mode: none` for OTA overlap tests |
|
||||
| ESP32-S3, C3, C6, H2 native USB | `esphome upload --device /dev/cu.usbmodem*` | `logger: hardware_uart: USB_SERIAL_JTAG`. The USB console can wedge when the app takes over the console (0 bytes, esptool `No serial data received`); replug, or watch through `esphome logs --device <name>.local` |
|
||||
| Pico W / Pico 2 W | `esphome upload --device <port>` (1200 baud touch); first flash ever via BOOTSEL and `--device /Volumes/RPI-RP2` or `RP2350` | port re-enumerates after upload: `ports.py --wait 2e8a` then capture |
|
||||
| BW15 kit (RTL8720CF, rtl87xx) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_rtl87xx_kit.py flash write -d <port> <build>/.pioenvs/<name>/*.uf2`. Plain `esphome upload` and plain ltchiptool time out because opening the port asserts DTR/RTS and holds the chip in reset | `serial_capture.py --released`, never `esphome logs` |
|
||||
| BK7238 / CB3S (bk72xx, RTS on CEN) | `~/.platformio/penv/.libretiny/bin/python $SKILL/scripts/flash_bk72xx.py flash write -d <port> <uf2>` (about 75 s) | logs are on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter. Opening the port resets the chip, so keep one capture open for the whole test |
|
||||
| LN882H | ltchiptool forces 1 Mbaud for writes; weak adapters need `LN882H_YM_BAUDRATE` patched to 115200 in the penv's `ln882htool.py`; short BOOT to GND while the tool connects | OTA after the first flash |
|
||||
| nRF52840 Dongle (PCA10059) | Nordic `nrfutil nrf5sdk-tools dfu usb-serial` on the bootloader port, or `dfu: true` in the config so the 1200 baud touch pin-resets into the bootloader; adafruit-nrfutil is refused | app has two CDC ports, ESPHome log on the second |
|
||||
| Adafruit-style nRF52 (UF2 bootloader) | `UF2BOOT` volume or adafruit-nrfutil; `dfu: true` works through GPREGRET | single CDC port |
|
||||
| Any reachable device | `esphome upload <yaml> --device <ip or name.local>`; `--file <bin>` for a prebuilt image | `esphome logs --device <ip>` bounded with `timeout` |
|
||||
|
||||
ESP-IDF OTA image: `.esphome/build/<name>/build/firmware.ota.bin`. LibreTiny OTA port is 8892.
|
||||
|
||||
## Scripts in `$SKILL/scripts`
|
||||
|
||||
- `ports.py`: list ports with VID:PID, product, serial and a board guess; `--wait VID[:PID]`,
|
||||
`--product NAME`, `--serial SN` (one specific board, whatever path it comes back under) and
|
||||
`--serial-of PORT` (the serial number behind a path).
|
||||
- `serial_capture.py PORT SECONDS OUT [--baud] [--released] [--reset] [--append]`: bounded timestamped
|
||||
capture, ANSI stripped, `LOG_SECRET` text written as `<secret>`, reopens a vanished port, exits at the
|
||||
deadline. OUT is truncated unless `--append` is given, so counts over it cover one run.
|
||||
- `flash_rtl87xx_kit.py`: ltchiptool wrapper for the BW15 kit whose CH340 drives CEN (RTS) and the boot
|
||||
strap (DTR). `flash read` needs the family `realtek-ambz2`; `flash write` does not.
|
||||
- `flash_bk72xx.py`: ltchiptool wrapper for BK72xx boards whose RTS drives CEN.
|
||||
- `wifisave_hammer.sh KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE]`:
|
||||
the fallback AP plus captive portal hammer end to end (compile, flash, capture, counts) with the
|
||||
templates below. KIND is `esp`, `pico`, `bw15` or `bk`. It ends with `RESULT PASS` (exit 0),
|
||||
`RESULT FAIL` (exit 3: a crash, a reboot after the AP came up, or the device never joined the network),
|
||||
`RESULT INVALID`
|
||||
(exit 2: a precondition was missed, the stimulus was insufficient, or a capture did not last the run)
|
||||
or exits 1 when a build, flash or port step failed.
|
||||
|
||||
## Templates in `$SKILL/templates`
|
||||
|
||||
- `dut-fallback-ap.yaml` (package): a device that never finds its network, falls back to its AP and
|
||||
serves the captive portal. `example-dut.yaml` is the per-run file with the platform block.
|
||||
- `client-wifisave.yaml` (package): joins that AP, posts N bogus `/wifisave` requests, then the real
|
||||
credentials from `!secret`. `example-client.yaml` is the per-run file. `bogus_saves: "1"` gives a
|
||||
plain provisioning check.
|
||||
|
||||
## When something looks wrong
|
||||
|
||||
- A capture stopped early or a line is missing: check the timestamps before trusting it.
|
||||
- The client never joins the AP: another board may still run an AP with the same SSID, or the device
|
||||
booted with saved credentials because the name was reused. One AP device at a time on the bench.
|
||||
- Builds come out byte-identical between branches: the wrong tree was imported (rule 3).
|
||||
- Multicast from the host never arrives on some access points: test it device to device. Broadcast
|
||||
is lossy under Wi-Fi power save, unicast is not. Throttle UDP floods to a fixed packet rate.
|
||||
- `api:` reboots a board after 15 min without a client: `reboot_timeout: 0s` on `api:` and `mqtt:` for
|
||||
soak tests.
|
||||
|
||||
`reference/boards.md` has per-chip facts and crash decoding; `reference/recipes.md` has the proven
|
||||
flows. Add new findings there.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Board facts
|
||||
|
||||
Ports are not listed on purpose: they follow the USB hub port and change on every replug. Run
|
||||
`scripts/ports.py`.
|
||||
|
||||
## ESP8266 (D1 mini, NodeMCU, CH340 or CP2102)
|
||||
|
||||
- Opening the port usually resets the board through DTR, but not always; `serial_capture.py --reset`
|
||||
pulses RTS to capture from boot. 74880 baud shows the ROM boot messages (`rst cause`, `boot mode`).
|
||||
- `esphome logs` on some CH340s emits thousands of duplicated garbage lines right after the RTS reset;
|
||||
a plain pyserial capture at 115200 is reliable.
|
||||
- A failed esptool write can wedge the adapter (`termios` error 22, or an open that hangs) until replug.
|
||||
- The preference sector survives a reflash: a provisioned API key or saved Wi-Fi credentials stay
|
||||
active. `esptool erase_flash` gives a clean device.
|
||||
- `Panic core_esp8266_main.cpp:191 __yield` with `ctx: sys` means a yield from lwIP context (a web
|
||||
handler, an lwIP callback). Decode stacks with
|
||||
`xtensa-lx106-elf-addr2line -e <build>/.pioenvs/<name>/firmware.elf -f -C -i <addr...>` from the
|
||||
toolchain under `~/.platformio/packages/toolchain-xtensa/bin/` (PlatformIO builds) or the ESPHome
|
||||
cache directory (native toolchain builds).
|
||||
- Constant data lives in RAM unless marked `PROGMEM`; with 80 KB of RAM a few hundred bytes are worth a
|
||||
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.
|
||||
|
||||
## ESP32 classic devkit
|
||||
|
||||
- Takes ESP-IDF and Arduino. Default Wi-Fi light sleep delays API driven actions by up to 6 s; judge
|
||||
timings from device logs, not from the host. `power_save_mode: none` for OTA overlap tests (an IDF web
|
||||
upload of 739 KB took 107 s with power save and 45 s without).
|
||||
- Receives only a few percent of IPv6 multicast on some access points with power save on; a raw lwIP
|
||||
socket loses the same, so that is the AP or driver, not a PR.
|
||||
- Decode with `xtensa-esp32-elf-addr2line` from the IDF tools directory against
|
||||
`<build>/.pioenvs/<name>/firmware.elf` (PlatformIO) or `<build>/build/<name>.elf` (IDF native).
|
||||
- Clean reboot between runs with the capture stopped: `esptool --after hard_reset chip_id`.
|
||||
- Trigger a forced crash only after the app is marked valid (45 s after boot is plenty), never on an
|
||||
image still pending OTA rollback.
|
||||
- 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.
|
||||
|
||||
## ESP32-S3, C3, C6, H2 with native USB
|
||||
|
||||
- `/dev/cu.usbmodem*`, esptool works, `logger: hardware_uart: USB_SERIAL_JTAG`.
|
||||
- The USB console can die at the moment the app takes over the console during a reboot (log stops after
|
||||
`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 ESP32-P4 before silicon revision 3 is bricked by rev3-only firmware (boot loop at bootloader entry).
|
||||
|
||||
## RP2040 / RP2350 (Pico W, Pico 2 W)
|
||||
|
||||
- The CDC port re-enumerates after every reboot and after `esphome upload` (1200 baud touch). Re-detect
|
||||
with `ports.py --wait 2e8a` before capturing; `serial_capture.py` reopens on its own if the port
|
||||
vanishes mid-capture.
|
||||
- First flash ever: BOOTSEL, then `esphome upload --device /Volumes/RPI-RP2` (or `RP2350`).
|
||||
- arduino-pico creates the lwIP netif in `WiFi.begin()`, so any IGMP join during component setup fails;
|
||||
joins must wait for `network::is_connected()`.
|
||||
- Parallel Pico builds race on package extraction after a PlatformIO package wipe; one at a time.
|
||||
- An RTS pulse from pyserial does not reset the board; reboot with `App.safe_reboot()` or the touch.
|
||||
|
||||
## BW15 kit (RTL8720CF, LibreTiny rtl87xx)
|
||||
|
||||
- The kit's CH340 wires RTS to CEN and DTR to PA00 (boot strap). A plain pyserial open asserts both and
|
||||
holds the chip in reset, which is why `esphome upload`, plain `ltchiptool` and `esphome logs` all fail
|
||||
("Timeout while linking", or silence). `scripts/flash_rtl87xx_kit.py` sets DTR high and RTS released
|
||||
on every open, pulses RTS once so the ROM answers `ping`, and after the command drops DTR and pulses
|
||||
RTS to boot the app. Run it with the PlatformIO LibreTiny penv Python.
|
||||
- `flash write -d <port> <uf2>` takes no family argument; `flash read -d <port> -s 0 -l <len>
|
||||
realtek-ambz2 <out>` does.
|
||||
- Capture with `serial_capture.py --released`. The ESPHome log is on the flashing UART already.
|
||||
- The macOS CH340 driver only pushes DTR/RTS to the wire once a session has driven them, so the first
|
||||
plain open after a replug streams logs and the second is silent; always open with the lines released.
|
||||
- Driver quirks seen on `dev`: `WiFi.begin` can refuse the real network on the first try after bogus
|
||||
credentials (it connects after two cooldowns); right after connecting while the AP is still up the
|
||||
driver can bus-fault in `rtw_is_adapter_up` (`R0 = 0xdeadbeef`) and reboot; the AP restarts on every
|
||||
STA retry, so stations cannot stay joined; `WiFi.begin` fails synchronously for an absent network and
|
||||
lands in a cooldown at once; connecting blocks the loop 2.5 to 4.1 s.
|
||||
- Decode faults with `arm-none-eabi-addr2line -e <build>/.pioenvs/<name>/raw_firmware.elf -f -C -i <PC>`
|
||||
from `~/.platformio/packages/toolchain-gccarmnoneeabi/bin/` (`raw_firmware.elf`, not `firmware.elf`).
|
||||
Fault dumps print PC, LR and R0 to R3.
|
||||
- The AmebaZ2 SDK sets `LWIP_TCPIP_CORE_LOCKING 0`.
|
||||
|
||||
## BK7238, WB3S, CB3S (BK7231N) (LibreTiny bk72xx, CH340, RTS on CEN)
|
||||
|
||||
- Stock ltchiptool closes and reopens the port after its reset, and pyserial raises both lines on open,
|
||||
holding CEN low through the BootROM window. `scripts/flash_bk72xx.py` opens with both released and
|
||||
pulses the reset right before the link checks. A write takes about 75 s. Only a CEN or power reset
|
||||
reaches the downloader; no software reboot does.
|
||||
- ESPHome logs on UART2 by default; `logger: hardware_uart: UART1` puts them on the flashing adapter.
|
||||
- Opening the port resets the chip (bootloader banner on every open, even with DTR/RTS low), so keep one
|
||||
capture open for the whole test and never reopen mid-phase.
|
||||
- Callbacks run on the `async_tcp` task and log lines can lose a byte. Wi-Fi connect takes tens of ms;
|
||||
radio enable 160 to 278 ms, disable about 200 ms.
|
||||
|
||||
## BW16 kit (RTL8720DN, AmebaD)
|
||||
|
||||
- The kit's own USB-C (CH340) is on LOG_UART and flashes fine. Its two-transistor circuit resets while
|
||||
RTS alone is asserted and straps BURN while DTR alone is; both asserted does nothing. ROM entry is
|
||||
esptool's classic sequence (`dtr:false,rts:true` 100 ms, `dtr:true,rts:false` 400 ms, both false); the
|
||||
first pulse after a replug can be lost, retry up to three times.
|
||||
- Any reopen resets the chip, 460800 is flaky, 115200 is solid: one open per session, in-place baud
|
||||
changes only. The ROM and the loader NAK continuously while idle, so ACK scans must drain first.
|
||||
- The AmebaD core has no mDNS responder (`mdns: disabled: true`).
|
||||
|
||||
## LN882H
|
||||
|
||||
- ltchiptool forces 1 Mbaud for writes; adapters that cannot hold it fail with "Timeout while linking"
|
||||
or die mid-write. Patch `LN882H_YM_BAUDRATE` in the penv's `ltchiptool/soc/ln882h/util/ln882htool.py`
|
||||
to 115200 (lost on reprovision). Weak 3.3 V from the adapter: feed the module's 5 V pad instead. Short
|
||||
BOOT to GND while the tool connects. OTA afterwards.
|
||||
- `USE_WIFI_AP_EXCLUSIVE` (one radio mode at a time) exists only on LN882x; compile-check changes there.
|
||||
|
||||
## nRF52840
|
||||
|
||||
- Nordic PCA10059 dongle: the factory Open DFU bootloader enumerates as `1915:521F` on its own port
|
||||
name; the app as `2FE3:0100` with two CDC ports, ESPHome log on the second. Only Nordic
|
||||
`nrfutil nrf5sdk-tools` works (`pkg generate --hw-version 52 --sd-req 0x00 --application-version 1
|
||||
--application zephyr.hex app.zip`, then `dfu usb-serial -pkg app.zip -p <port>`); ESPHome downloads
|
||||
it on the first build. GPREGRET does not enter the bootloader; a pin reset through P0.19 does, which
|
||||
`dfu: true` wires up on this board. A pulsing red LED means bootloader; the side RESET button
|
||||
re-enters it.
|
||||
- Makerdiary MDK USB dongle: UF2 bootloader without SoftDevice, app at 0x1000, `UF2BOOT` volume,
|
||||
adafruit-nrfutil, GPREGRET 0x57 works. RGB LED active low (red P0.23, green P0.22, blue P0.24),
|
||||
button P0.18.
|
||||
|
||||
## ESP Thread Border Router board v1.2 (S3 host + H2 RCP)
|
||||
|
||||
- S3 host on USB JTAG serial, H2 RCP on its own USB. Spinel UART1 from the host: `rx_pin: GPIO17`,
|
||||
`tx_pin: GPIO18`, RCP reset `GPIO7` active low, 460800. The config needs `network: enable_ipv6: true`.
|
||||
- The shipped H2 firmware fails `CheckRadioCapabilities` with IDF 5.5; build `examples/openthread/ot_rcp`
|
||||
for esp32h2 with ESPHome's own IDF (`IDF_PATH` at the cached framework, its penv and tools,
|
||||
`IDF_PYTHON_CHECK_CONSTRAINTS=no`) and flash with esptool at 0x0, 0x8000 and 0x10000.
|
||||
- A crash-looping S3 pulses the H2 reset every boot, so the H2 cannot be flashed and the S3 will not
|
||||
enter its bootloader over USB. Hold S3 BOOT, tap S3 RST, flash the H2, release, then replug both.
|
||||
- `dns-sd -L <name> _meshcop._udp local.` shows the `omr` prefix; it disappears when routing is dead.
|
||||
Keep one capture per chip; never reset the H2 over USB mid-test unless that is the test (the host
|
||||
logs `Unexpected RCP reset: RESET_OTHER` and recovers).
|
||||
@@ -0,0 +1,117 @@
|
||||
# Proven test flows
|
||||
|
||||
Each flow names the pull requests it was used on, so their result tables show what to post.
|
||||
|
||||
## Captive portal `/wifisave` hammer (#20802, #20784)
|
||||
|
||||
Reproduces a `/wifisave` arriving during the STA cooldown, and proves a fix on every board.
|
||||
Device under test: `templates/example-dut.yaml` (bogus network, `ap:` with `ap_timeout: 15s`,
|
||||
`captive_portal:`). Client: `templates/example-client.yaml` on an ESP32 or a D1 mini; it joins the
|
||||
device's AP, posts 15 bogus saves 3 s apart, then the real credentials. With the portal up the cooldown
|
||||
is 30 s, so nearly every save lands inside one. Driver: `scripts/wifisave_hammer.sh`. Settle 40 s on
|
||||
ESP boards, 90 s on the BW15 (its first attempt on the real network fails, see boards.md).
|
||||
|
||||
Counts that matter: `Requested WiFi Settings Change` (saves received), `Exiting cooldown early`, crash
|
||||
markers, boots (`Log initialized`), and the client's `bogus save status 200` and `real save status 200`.
|
||||
A healthy run: every save answered 200, one cooldown exit per save (two on the BW15, whose retry enters
|
||||
the adapter restart), no panic, and the real credentials connect the device within about 10 s.
|
||||
|
||||
Fault injection for AP start failures: wrap `this->ap_setup_ = this->wifi_start_ap_(this->ap_)` in
|
||||
`setup_ap_config_()` with an `HWTEST_AP_FAIL_COUNT` build flag
|
||||
(`esphome: platformio_options: build_flags: -DHWTEST_AP_FAIL_COUNT=2`), raise the portal's "Captive
|
||||
portal started" from LOGV to LOGI, and after provisioning curl the device's STA address: connection
|
||||
refused proves the portal's server is gone.
|
||||
|
||||
## `reboot_timeout` with a fallback AP (#9958)
|
||||
|
||||
Only one board runs the AP at a time, or stations join the wrong one. AP probe: bogus network,
|
||||
`ap: {ssid, password, ap_timeout: 5s}`, `reboot_timeout: 60s`, `logger: level: VERBOSE` (ESP32 and
|
||||
ESP8266 log `AP client connected MAC=` at verbose), `on_boot: logger.log: BOOT`. Station probes:
|
||||
`reboot_timeout: 0s`, `interval: 150s` running `wifi.disable`, `delay: 20s`, `wifi.enable` (60 s cycles
|
||||
are too short to cover the due moment). Pass pattern: "Can't connect; rebooting" exactly 60 s after boot
|
||||
with no station joined, no reboot for minutes while one is, then a reboot within a second of the last
|
||||
disconnect. The BW15 restarts its AP on every STA retry and cannot be tested this way.
|
||||
|
||||
## MQTT (#20433, #17935, #20762, #20767, #20768)
|
||||
|
||||
A local mosquitto with `allow_anonymous true`, `log_type all` and `log_dest file`: the broker log gives
|
||||
each subscription's QoS, the device address (`New client connected from <ip> as <name>-<mac>`) and exact
|
||||
subscribe counts. Kill and restart mosquitto to exercise `on_disconnect` (reason 0) and resubscription.
|
||||
Keep `reboot_timeout: 0s` on `api:` and `mqtt:`. The ESP-IDF outbox swings free heap about 2 kB right
|
||||
after bulk subscribe and unsubscribe; read the heap seconds later over several rounds before calling a
|
||||
leak. Spawned subscriptions need about 2.5 s to reach the broker before the next publish.
|
||||
`esphome logs --device MQTT` only uses the MQTT transport when the config has no `api:`. Driver pattern: a
|
||||
paho script publishes and polls the serial capture file.
|
||||
|
||||
## UDP, multicast, sockets (#20556 to #20559, #20585, #20590)
|
||||
|
||||
A listener on port 9 needs no privileges on macOS. Test multicast device to device; host to device
|
||||
multicast never arrives on some access points, and `239.255.x.x` groups are unreliable even to a stock
|
||||
listener. Broadcast to boards is lossy under power save, unicast is not. Flood at a fixed packet rate
|
||||
(the host hits `ENOBUFS` unthrottled); 1500 packets per second survived on every board. A forced Wi-Fi
|
||||
reconnect on the Pico keeps IGMP membership. Set a long loop interval from `wifi.on_connect`
|
||||
(`App.set_loop_interval(20000)`), not `on_boot`, or Wi-Fi connect crawls. The `logger.log` action
|
||||
rejects `%.*s`; use a lambda.
|
||||
|
||||
## OTA overlap and lock (#20588), OTA timeouts (#19041), deflate (#19037)
|
||||
|
||||
All overlap orders (a web upload during a native OTA and the reverse) must be refused; the lock release
|
||||
after an interrupted web upload waits about 14 s (ESP8266) or 7 s (ESP-IDF) for disconnect detection. A
|
||||
refused web client on `web_server_idf` never gets the busy reply; the connection drops at reboot.
|
||||
`pkill -f "a\|b"` does not alternate; use two pkills.
|
||||
|
||||
## DHCP lease cache (#17767)
|
||||
|
||||
Cycle 20 s awake, `deep_sleep.enter 30s`, wake, 20 s, `App.safe_reboot()`, logging `millis()` at the
|
||||
`wifi.connected` condition. An RTS hardware reset must fall back to DHCP; a software reboot and a deep
|
||||
sleep wake must apply the cache. `esphome run --no-logs` silently skipped the upload on one board; use
|
||||
`upload` then `logs`.
|
||||
|
||||
## HDMI-CEC two-board wire (#9189)
|
||||
|
||||
Two boards on one wire: promiscuous mode, a 0x8F request answered with 0x90 0x00, a `destination:`
|
||||
filter on the reply handler, template buttons for each target plus lambdas calling
|
||||
`send(source, dest, span)` as another initiator. Healthy numbers: device-side turnaround 15 to 22 ms,
|
||||
round trip 144 to 156 ms. Press buttons over the API; judge timings from device logs (ESP32 light sleep
|
||||
lags the press).
|
||||
|
||||
## LD2450 polygon zones (#20154)
|
||||
|
||||
To make a target "leave" without anyone moving, set the radar's Zone-1 numbers over the target and
|
||||
switch the zone type to `Filter` (the count drops to 0 within 0.5 s); set it back to `Disabled` after.
|
||||
Writing those numbers blocks the ld2450 loop about 50 ms.
|
||||
|
||||
## Scheduler and loop wake A/B (#20356)
|
||||
|
||||
An external component whose template button arms an SDK `os_timer` (50 ms, one shot) on the ESP8266;
|
||||
the callback runs in SYS context, records `millis()` and calls `set_timeout` or `defer`; the timeout
|
||||
logs the elapsed time and loop count delta. Drive with an aioesphomeapi script (`subscribe_logs` plus
|
||||
`button_command`). Build the same yaml twice with different names, one per tree.
|
||||
|
||||
## Noise handshake and OTA encryption on the ESP8266 (#19062, #19233)
|
||||
|
||||
Host side timing is useless on a weak link; use an on-device bench with the minimum of five runs over
|
||||
two boots. Provision a key with `noise_encryption_set_key` through aioesphomeapi and upload with
|
||||
`espota2.run_ota(host, 8266, None, bin, noise_psk=key)` when the key is not in the yaml.
|
||||
|
||||
## ESP-NOW on the ESP8266 (#14407)
|
||||
|
||||
The NONOS SDK only delivers broadcast ESP-NOW frames while the station is associated or a soft-AP is
|
||||
up; a hidden, open, client-less soft-AP (60 s beacon, DHCP stopped) makes broadcasts arrive. In AP-only
|
||||
mode the device transmits from the soft-AP MAC.
|
||||
|
||||
## Wi-Fi Easy Connect (DPP) enrollee (#17934)
|
||||
|
||||
Only an Android phone configures: Settings, network details, Add device, Share Wi-Fi (the Wi-Fi list's
|
||||
QR scanner makes the phone an enrollee). Render the URI with the `qrcode` module and open the PNG.
|
||||
`esp32: framework: log_level: DEBUG` plus `sdkconfig_options: CONFIG_WPA_DEBUG_PRINT: y` prints the
|
||||
supplicant state machine.
|
||||
|
||||
## Thread border router (#18104)
|
||||
|
||||
See boards.md for the ESP Thread BR board. Check that the H2 answers Spinel before blaming a PR.
|
||||
|
||||
## nRF52840 dongle boards (#19009, #20814)
|
||||
|
||||
Test config: the two LEDs as inverted GPIO outputs, the button as an inverted pull-up binary sensor, a
|
||||
10 s alive interval. To retest the nrfutil download path, move the cached `sdk-nrf/nrfutil` aside first.
|
||||
@@ -0,0 +1,51 @@
|
||||
"""ltchiptool for boards whose RTS drives CEN: reset and link on one open port.
|
||||
|
||||
Stock ltchiptool closes and reopens the port after its DTR/RTS reset, and
|
||||
pyserial raises both lines on open, holding CEN low through the BootROM's
|
||||
listen window. Open with both lines released and pulse the reset right
|
||||
before the LinkChecks instead.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import bk7231tools.serial.main as bk_main
|
||||
from ltchiptool.__main__ import cli
|
||||
from ltchiptool.soc.bk72xx.flash import BK72XXFlash
|
||||
from serial import Serial
|
||||
|
||||
|
||||
class ReleasedSerial(Serial):
|
||||
def __init__(self, port=None, **kwargs):
|
||||
super().__init__(None, **kwargs)
|
||||
self.dtr = False
|
||||
self.rts = False
|
||||
self.port = port
|
||||
self.open()
|
||||
|
||||
|
||||
bk_main.Serial = ReleasedSerial
|
||||
|
||||
_connect = BK72XXFlash.flash_connect
|
||||
|
||||
|
||||
def flash_hw_reset(self) -> None:
|
||||
self._reset_pending = True
|
||||
|
||||
|
||||
def flash_connect(self) -> None:
|
||||
pending = getattr(self, "_reset_pending", False)
|
||||
if self.bk and self.conn.linked and not pending:
|
||||
return
|
||||
self.flash_build_protocol()
|
||||
if pending:
|
||||
self._reset_pending = False
|
||||
self.bk.hw_reset()
|
||||
self.conn.linked = False # the chip rebooted, so link again below
|
||||
_connect(self)
|
||||
|
||||
|
||||
BK72XXFlash.flash_hw_reset = flash_hw_reset
|
||||
BK72XXFlash.flash_connect = flash_connect
|
||||
|
||||
sys.argv = ["ltchiptool", *sys.argv[1:]]
|
||||
cli()
|
||||
@@ -0,0 +1,87 @@
|
||||
"""ltchiptool for the BW15 kit whose CH340 drives CEN (RTS) and PA00 (DTR).
|
||||
|
||||
A plain open asserts both lines and holds the chip in reset. Here every open keeps DTR high and RTS
|
||||
released; the first open pulses RTS so the ROM comes up; after the command DTR low plus an RTS pulse
|
||||
boots the app. The exit status is ltchiptool's; 2 means the flash succeeded but the app was not
|
||||
booted (boot pulse failed, or no -d PORT to send it to), so the board sits in the ROM until reset.
|
||||
|
||||
Usage: python flash_rtl87xx_kit.py flash write -d <port> firmware.uf2
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
|
||||
_first = [True]
|
||||
_orig_open = serial.Serial.open
|
||||
|
||||
|
||||
def _open(self):
|
||||
self.dtr = True # PA00 high: ROM download mode on the next reset
|
||||
self.rts = False # CEN released
|
||||
_orig_open(self)
|
||||
if _first[0]:
|
||||
_first[0] = False
|
||||
self.rts = True
|
||||
time.sleep(0.1)
|
||||
self.rts = False
|
||||
time.sleep(0.3) # reset into the ROM
|
||||
self.reset_input_buffer()
|
||||
|
||||
|
||||
serial.Serial.open = _open
|
||||
|
||||
from ltchiptool.__main__ import cli_entrypoint # noqa: E402
|
||||
|
||||
|
||||
def port_argument(argv: list[str]) -> str | None:
|
||||
for i, a in enumerate(argv):
|
||||
if a in ("-d", "--device") and i + 1 < len(argv):
|
||||
return argv[i + 1]
|
||||
if a.startswith(("-d=", "--device=")):
|
||||
return a.split("=", 1)[1]
|
||||
if a.startswith("-d") and len(a) > 2:
|
||||
return a[2:]
|
||||
return None
|
||||
|
||||
|
||||
def boot_app(port: str) -> None:
|
||||
s = serial.Serial(port=None)
|
||||
s.port = port
|
||||
s.dtr = False
|
||||
s.rts = False
|
||||
_orig_open(s)
|
||||
s.rts = True
|
||||
time.sleep(0.1)
|
||||
s.rts = False
|
||||
s.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rc = 1
|
||||
try:
|
||||
result = cli_entrypoint.main(args=sys.argv[1:], standalone_mode=False)
|
||||
rc = int(result) if isinstance(result, int) else 0
|
||||
finally:
|
||||
port = port_argument(sys.argv)
|
||||
if not port:
|
||||
print(
|
||||
"[flash_rtl87xx_kit] no -d PORT given, the application was not booted",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if rc == 0:
|
||||
rc = 2
|
||||
else:
|
||||
try:
|
||||
boot_app(port)
|
||||
print("[flash_rtl87xx_kit] application booted")
|
||||
except (serial.SerialException, OSError) as err:
|
||||
print(f"[flash_rtl87xx_kit] boot pulse failed: {err}", file=sys.stderr)
|
||||
if rc == 0:
|
||||
rc = 2 # flashed, but the board is still in the ROM: press reset
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,123 @@
|
||||
"""List the boards on USB, or wait for one to enumerate.
|
||||
|
||||
ports.py table: port, vid:pid, product, serial number, guess
|
||||
ports.py --wait 2e8a print the first port with VID 0x2e8a once it appears (default 60 s)
|
||||
ports.py --wait 1a86:7523 --timeout 30
|
||||
ports.py --product "USB Serial" first port whose USB product string matches (case-insensitive)
|
||||
ports.py --wait 2e8a --serial SN the port of one specific board, whatever name it comes back under
|
||||
ports.py --serial-of PORT print the USB serial number behind a port name
|
||||
|
||||
Run with a Python that has pyserial (any ESPHome venv). Port names follow the hub port on macOS, so
|
||||
re-run this after every replug instead of trusting a name from an earlier session.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from serial.tools import list_ports
|
||||
|
||||
GUESS = [
|
||||
(
|
||||
(0x1A86, 0x7523),
|
||||
"CH340: D1 mini, BW15 kit, BK7238/CB3S or LN882H adapter; identify by the boot banner",
|
||||
),
|
||||
((0x1A86, 0x55D3), "CH343: ESP32-P4 devkit console"),
|
||||
((0x1A86, 0x55D4), "CH343: ESP32 devkit"),
|
||||
((0x1A86, None), "WCH CH34x USB serial bridge"),
|
||||
((0x10C4, 0xEA60), "CP2102: classic ESP32 devkit (esp32dev)"),
|
||||
(
|
||||
(0x303A, 0x1001),
|
||||
"Espressif USB JTAG serial: S3/C3/C6/H2 native USB (logger hardware_uart USB_SERIAL_JTAG)",
|
||||
),
|
||||
(
|
||||
(0x2E8A, None),
|
||||
"Raspberry Pi Pico W / Pico 2 W CDC; the port vanishes and comes back on every reboot",
|
||||
),
|
||||
((0x1915, 0x521F), "nRF52840 Dongle (PCA10059) in the Open DFU bootloader"),
|
||||
(
|
||||
(0x2FE3, 0x0100),
|
||||
"nRF52840 Dongle running a Zephyr app; two CDC ports, ESPHome log on the second",
|
||||
),
|
||||
((0x1915, 0xCAFE), "Makerdiary nRF52840 MDK dongle running a Zephyr app"),
|
||||
((0x239A, None), "Adafruit style nRF52 board (UF2 bootloader family)"),
|
||||
]
|
||||
|
||||
|
||||
def guess(vid: int, pid: int) -> str:
|
||||
for (gv, gp), text in GUESS:
|
||||
if vid == gv and (gp is None or pid == gp):
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
def parse_id(text: str) -> tuple[int, int | None]:
|
||||
vid, _, pid = text.partition(":")
|
||||
return int(vid, 16), (int(pid, 16) if pid else None)
|
||||
|
||||
|
||||
def matches(
|
||||
port, vid: int | None, pid: int | None, product: str | None, serial: str | None
|
||||
) -> bool:
|
||||
if vid is not None and (port.vid != vid or (pid is not None and port.pid != pid)):
|
||||
return False
|
||||
if product and (port.product or "").lower() != product.lower():
|
||||
return False
|
||||
return not serial or (port.serial_number or "") == serial
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument(
|
||||
"--wait",
|
||||
metavar="VID[:PID]",
|
||||
help="block until a matching port appears, print it",
|
||||
)
|
||||
ap.add_argument("--product", help="USB product string to match")
|
||||
ap.add_argument("--serial", help="USB serial number to match")
|
||||
ap.add_argument(
|
||||
"--serial-of", metavar="PORT", help="print the USB serial number of PORT"
|
||||
)
|
||||
ap.add_argument("--timeout", type=float, default=60)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.serial_of:
|
||||
for p in list_ports.comports():
|
||||
if p.device == args.serial_of and p.serial_number:
|
||||
print(p.serial_number)
|
||||
return 0
|
||||
print(f"no USB serial number for {args.serial_of}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
vid = pid = None
|
||||
if args.wait:
|
||||
vid, pid = parse_id(args.wait)
|
||||
|
||||
if args.wait or args.product or args.serial:
|
||||
deadline = time.time() + args.timeout
|
||||
while True:
|
||||
for p in sorted(list_ports.comports(), key=lambda p: p.device):
|
||||
if p.vid and matches(p, vid, pid, args.product, args.serial):
|
||||
print(p.device)
|
||||
return 0
|
||||
if time.time() >= deadline:
|
||||
print("no matching port", file=sys.stderr)
|
||||
return 1
|
||||
time.sleep(1)
|
||||
|
||||
rows = [p for p in list_ports.comports() if p.vid]
|
||||
if not rows:
|
||||
print("no USB serial ports")
|
||||
return 1
|
||||
for p in sorted(rows, key=lambda p: p.device):
|
||||
print(
|
||||
f"{p.device:32} {p.vid:04x}:{p.pid:04x} {p.product or '?':34} "
|
||||
f"{p.serial_number or '':18} {guess(p.vid, p.pid)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Bounded, timestamped serial capture that always exits and closes the port.
|
||||
|
||||
serial_capture.py PORT SECONDS OUTFILE [--baud 115200] [--released] [--reset] [--append]
|
||||
|
||||
default open with DTR/RTS as pyserial leaves them (asserted); fine for ESP devkits and the Pico.
|
||||
--released drive DTR and RTS low before opening: the BW15 and BK7238 CH340 kits strap the boot pin
|
||||
(DTR) and reset (RTS), so a plain open holds the chip in reset.
|
||||
--reset pulse reset after opening (DTR low, RTS high 100 ms, RTS low) to capture a fresh boot.
|
||||
--baud 115200 by default; 74880 shows the ESP8266 ROM boot messages.
|
||||
--append keep what OUTFILE already holds; by default it is truncated so counts cover this run only.
|
||||
|
||||
Exit status 1 when nothing was received (wrong port name, or a port another capture still holds).
|
||||
Text ESPHome marks as secret (LOG_SECRET, the Wi-Fi password) is written as <secret>.
|
||||
Lines are written as [HH:MM:SS]text with ANSI colour stripped and flushed one by one, so a shell
|
||||
loop can grep the file while the capture runs. If the port is missing or disappears (USB CDC boards
|
||||
re-enumerate on reboot) the script retries until the deadline, and follows the board by its USB serial
|
||||
number when it comes back under another device path.
|
||||
|
||||
Never run an open-ended serial session: every capture has a deadline, and a flash on the same port
|
||||
needs the capture stopped first (pkill -f "[s]erial_capture.py" does not match the killing shell).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
ANSI = re.compile(rb"\x1b\[[0-9;]*[A-Za-z]")
|
||||
# ESPHome's LOG_SECRET span (a Wi-Fi password); a span cut off before its end marker is secret to the end
|
||||
SECRET = re.compile(rb"\x1b\[8m.*?(?:\x1b\[28m|$)", re.DOTALL)
|
||||
PORT_ERRORS = (serial.SerialException, OSError)
|
||||
|
||||
|
||||
def identify(port: str) -> tuple[str | None, int]:
|
||||
"""USB serial number of the port and its position among that device's ports (a dongle has two)."""
|
||||
ports = sorted(list_ports.comports(), key=lambda p: p.device)
|
||||
serial_number = next((p.serial_number for p in ports if p.device == port), None)
|
||||
siblings = [
|
||||
p.device for p in ports if serial_number and p.serial_number == serial_number
|
||||
]
|
||||
return serial_number, siblings.index(port) if port in siblings else 0
|
||||
|
||||
|
||||
def current_path(port: str, serial_number: str | None, index: int) -> str:
|
||||
"""The port's current device path: a USB CDC board can come back under a new name after a reboot."""
|
||||
if serial_number:
|
||||
ports = sorted(list_ports.comports(), key=lambda p: p.device)
|
||||
siblings = [p.device for p in ports if p.serial_number == serial_number]
|
||||
if index < len(siblings):
|
||||
return siblings[index]
|
||||
return port
|
||||
|
||||
|
||||
def open_port(args: argparse.Namespace, path: str) -> serial.Serial:
|
||||
s = serial.Serial(port=None, baudrate=args.baud, timeout=0.2)
|
||||
s.port = path
|
||||
if args.released:
|
||||
s.dtr = False
|
||||
s.rts = False
|
||||
s.open()
|
||||
if args.reset:
|
||||
try:
|
||||
s.dtr = False
|
||||
s.rts = True
|
||||
time.sleep(0.1)
|
||||
s.rts = False
|
||||
s.reset_input_buffer()
|
||||
except PORT_ERRORS:
|
||||
s.close()
|
||||
raise
|
||||
return s
|
||||
|
||||
|
||||
def write_line(out, tag: bytes, data: bytes) -> None:
|
||||
stamp = time.strftime("[%H:%M:%S]").encode()
|
||||
out.write(stamp + tag + ANSI.sub(b"", SECRET.sub(b"<secret>", data)) + b"\n")
|
||||
out.flush()
|
||||
|
||||
|
||||
def note(out, text: str) -> None:
|
||||
write_line(out, b"[capture] ", text.encode())
|
||||
|
||||
|
||||
def capture(args: argparse.Namespace, out) -> int:
|
||||
"""Returns the number of bytes received."""
|
||||
deadline = time.time() + args.seconds
|
||||
buf = b""
|
||||
s = None
|
||||
received = 0
|
||||
serial_number, index = None, 0
|
||||
while time.time() < deadline:
|
||||
if s is None:
|
||||
path = current_path(args.port, serial_number, index)
|
||||
try:
|
||||
s = open_port(args, path)
|
||||
args.reset = False # only reset on the first open
|
||||
if serial_number is None:
|
||||
serial_number, index = identify(path)
|
||||
if path != args.port:
|
||||
note(out, f"reopened as {path}")
|
||||
except PORT_ERRORS as err:
|
||||
note(out, f"open failed: {err}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
try:
|
||||
data = s.read(4096)
|
||||
except PORT_ERRORS as err:
|
||||
if buf: # keep the cut-off line apart from the next boot's first line
|
||||
write_line(out, b"[partial]", buf)
|
||||
buf = b""
|
||||
note(out, f"port lost: {err}")
|
||||
with contextlib.suppress(*PORT_ERRORS):
|
||||
s.close()
|
||||
s = None
|
||||
time.sleep(1)
|
||||
continue
|
||||
if not data:
|
||||
continue
|
||||
received += len(data)
|
||||
buf += data
|
||||
while b"\n" in buf:
|
||||
line, buf = buf.split(b"\n", 1)
|
||||
write_line(out, b"", line.rstrip(b"\r"))
|
||||
if buf:
|
||||
write_line(out, b"[partial]", buf)
|
||||
if s is not None:
|
||||
s.close()
|
||||
return received
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument("port")
|
||||
ap.add_argument("seconds", type=float)
|
||||
ap.add_argument("outfile")
|
||||
ap.add_argument("--baud", type=int, default=115200)
|
||||
ap.add_argument("--released", action="store_true")
|
||||
ap.add_argument("--reset", action="store_true")
|
||||
ap.add_argument("--append", action="store_true")
|
||||
args = ap.parse_args()
|
||||
with pathlib.Path(args.outfile).open("ab" if args.append else "wb") as out:
|
||||
received = capture(args, out)
|
||||
if received == 0:
|
||||
print(f"[capture] nothing received from {args.port}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
# wifisave_hammer.sh: the device under test (DUT) falls back to its AP; a client board joins it and posts
|
||||
# /wifisave repeatedly, then the real credentials. Compiles, flashes, captures both serial logs and
|
||||
# prints counts. Configs come from templates/example-dut.yaml and templates/example-client.yaml.
|
||||
#
|
||||
# Usage: wifisave_hammer.sh DUT_KIND DUT_PORT DUT_YAML DUT_TREE CLIENT_PORT CLIENT_YAML CLIENT_TREE [SETTLE_S]
|
||||
# DUT_KIND esp (esphome upload), pico (upload, then re-find the CDC port by USB serial number),
|
||||
# bw15 (flash_rtl87xx_kit.py) or bk (flash_bk72xx.py)
|
||||
# *_YAML absolute path; the file name without .yaml must equal the config's `name`
|
||||
# *_TREE checkout or worktree whose esphome code must build that yaml (cwd for python -m esphome)
|
||||
# SETTLE_S seconds to keep capturing after the client reports the real save (default 40; BW15 wants 90)
|
||||
# Env: PY (python with esphome; default DUT_TREE/venv/bin/python, else python3 on PATH; an explicit PY must exist), LTPY
|
||||
# (LibreTiny penv python with ltchiptool; default ~/.platformio/penv/.libretiny/bin/python)
|
||||
# Output next to each yaml: NAME-compile.log, NAME-upload.log, NAME-serial.log, NAME-capture.err, and DUT NAME-events.txt
|
||||
# (addresses redacted so the extract can go on a PR; the serial logs themselves stay local).
|
||||
# Exit status: 0 RESULT PASS (no crash marker or reboot after the AP came up, the device joined the network,
|
||||
# and real stimulus: saves answered and a cooldown exit); 3 RESULT FAIL (the counts show
|
||||
# a failure); 2 RESULT INVALID (a precondition or the stimulus was missed, or a capture did not last the run);
|
||||
# 1 when a build, flash or port step fails (nothing is reported).
|
||||
set -u
|
||||
SKILL=$(cd "$(dirname "$0")/.." && pwd)
|
||||
KIND=$1; DPORT=$2; DYAML=$3; DTREE=$4; CPORT=$5; CYAML=$6; CTREE=$7; SETTLE=${8:-40}
|
||||
if [ -n "${PY:-}" ]; then [ -x "$PY" ] || { echo "[hammer] FAILED: PY=$PY is not executable"; exit 1; }
|
||||
else PY=$DTREE/venv/bin/python; [ -x "$PY" ] || PY=$(command -v python3) || { echo "[hammer] FAILED: no python found, set PY"; exit 1; }; fi
|
||||
LTPY=${LTPY:-$HOME/.platformio/penv/.libretiny/bin/python}
|
||||
DDIR=$(dirname "$DYAML"); DNAME=$(basename "$DYAML" .yaml)
|
||||
CDIR=$(dirname "$CYAML"); CNAME=$(basename "$CYAML" .yaml)
|
||||
DLOG=$DDIR/$DNAME-serial.log; CLOG=$CDIR/$CNAME-serial.log
|
||||
INVALID=""
|
||||
AP_WAIT=120; UPLOAD_WAIT=300; SAVE_WAIT=240
|
||||
# Crash signatures (extended regex), not bare words: "aborting connection attempt" is a normal Wi-Fi retry line
|
||||
CRASH_RE='Panic |abort\(\)|Fatal exception|Exception \(|Guru Meditation|HardFault|Bus Fault|Backtrace:|Soft WDT reset|wdt reset|Brownout|rst cause:[24]'
|
||||
# Boot banners and port losses are counted apart: a Pico reboot shows as a port loss (and maybe a banner too),
|
||||
# while on a UART board a port loss comes from the adapter or the hub, not the chip
|
||||
BANNER_RE='Log initialized'; LOSS_RE='\[capture\] port lost'
|
||||
DUT_CAPTURE=$((AP_WAIT + UPLOAD_WAIT + SAVE_WAIT + SETTLE + 60)); CLIENT_CAPTURE=$((SAVE_WAIT + SETTLE + 60))
|
||||
|
||||
bound() { local t=$1; shift; if command -v timeout >/dev/null; then timeout "$t" "$@"; else perl -e 'alarm shift; exec @ARGV' "$t" "$@"; fi; }
|
||||
count() { LC_ALL=C grep -a -c "$1" "$2"; }
|
||||
wait_for() { # wait_for FILE PATTERN SECONDS -> prints elapsed, status 1 on timeout
|
||||
local n=0; until LC_ALL=C grep -a -q "$2" "$1"; do sleep 2; n=$((n+2)); [ $n -ge "$3" ] && { echo "$n"; return 1; }; done; echo "$n"; }
|
||||
# Only this run's captures: another capture on the host (a soak on a third board) must survive
|
||||
stop_tails() { pkill -f "[s]erial_capture.py $DPORT " 2>/dev/null; pkill -f "[s]erial_capture.py $CPORT " 2>/dev/null; sleep 1; }
|
||||
fail() { echo "[hammer] FAILED: $1"; stop_tails; exit 1; }
|
||||
port_never_opened() { # an open failure and no line that is not a capture note: the port, not the firmware
|
||||
LC_ALL=C grep -a -q '\[capture\] open failed' "$1" && ! LC_ALL=C grep -a -q -v '^\[[0-9:]*\]\[capture\]' "$1"; }
|
||||
compile() { # compile YAML TREE
|
||||
(cd "$2" && PYTHONUNBUFFERED=1 bound 1500 "$PY" -m esphome compile "$1" > "${1%.yaml}-compile.log" 2>&1); local rc=$?
|
||||
echo "[hammer] compile $(basename "$1"): rc=$rc"; return $rc; }
|
||||
esp_upload() { # esp_upload YAML PORT TREE
|
||||
(cd "$3" && bound $UPLOAD_WAIT "$PY" -m esphome upload "$1" --device "$2" > "${1%.yaml}-upload.log" 2>&1)
|
||||
local ok; ok=$(count 'Successfully uploaded' "${1%.yaml}-upload.log"); echo "[hammer] upload $(basename "$1"): ok=$ok"; [ "$ok" -ge 1 ]; }
|
||||
|
||||
stop_tails
|
||||
compile "$DYAML" "$DTREE" || fail "DUT build failed, see $DDIR/$DNAME-compile.log"
|
||||
compile "$CYAML" "$CTREE" || fail "client build failed, see $CDIR/$CNAME-compile.log"
|
||||
|
||||
TAILOPT=""
|
||||
case $KIND in
|
||||
esp) esp_upload "$DYAML" "$DPORT" "$DTREE" || fail "DUT upload failed" ;;
|
||||
pico)
|
||||
SN=$("$PY" "$SKILL/scripts/ports.py" --serial-of "$DPORT") || fail "no USB serial number for $DPORT"
|
||||
esp_upload "$DYAML" "$DPORT" "$DTREE" || fail "DUT upload failed"
|
||||
# The Pico drops its CDC port on reboot and may already be back: take whatever path this board has now;
|
||||
# the capture follows the serial number if the path changes again
|
||||
DPORT=$("$PY" "$SKILL/scripts/ports.py" --wait 2e8a --serial "$SN" --timeout 60) || fail "pico did not re-enumerate"
|
||||
echo "[hammer] pico port after upload: $DPORT"; sleep 2 ;;
|
||||
bw15|bk)
|
||||
UF2=$(ls "$DDIR/.esphome/build/$DNAME/.pioenvs/$DNAME/"*.uf2 2>/dev/null | head -1)
|
||||
[ -n "$UF2" ] || fail "no uf2 under $DDIR/.esphome/build/$DNAME"
|
||||
echo "[hammer] uf2: ${UF2##*/}"
|
||||
W=$SKILL/scripts/flash_rtl87xx_kit.py; [ "$KIND" = bk ] && W=$SKILL/scripts/flash_bk72xx.py
|
||||
(cd "$DDIR" && bound 400 "$LTPY" "$W" flash write -d "$DPORT" "$UF2" > "$DDIR/$DNAME-upload.log" 2>&1); rc=$?
|
||||
echo "[hammer] $KIND flash: rc=$rc"; [ $rc -eq 0 ] || fail "DUT flash failed, see $DDIR/$DNAME-upload.log"
|
||||
TAILOPT="--released" ;;
|
||||
*) echo "unknown DUT_KIND $KIND"; exit 1 ;;
|
||||
esac
|
||||
|
||||
: > "$DLOG"; ("$PY" "$SKILL/scripts/serial_capture.py" "$DPORT" $DUT_CAPTURE "$DLOG" $TAILOPT 2> "${DLOG%-serial.log}-capture.err" &)
|
||||
if ! n=$(wait_for "$DLOG" "Captive portal\|Starting fallback AP" $AP_WAIT); then
|
||||
[ -s "$DLOG" ] || fail "the DUT capture produced nothing (the capture itself failed), see ${DLOG%-serial.log}-capture.err"
|
||||
port_never_opened "$DLOG" && fail "could not open $DPORT (held by another process, or the wrong port), see $DLOG"
|
||||
fail "DUT never raised its AP within ${n}s, see $DLOG"
|
||||
fi
|
||||
echo "[hammer] DUT fallback AP after ${n}s"; sleep 2
|
||||
# reboots are counted from here; the first banner may predate the capture
|
||||
BANNER_BASE=$(count "$BANNER_RE" "$DLOG"); LOSS_BASE=$(count "$LOSS_RE" "$DLOG")
|
||||
|
||||
esp_upload "$CYAML" "$CPORT" "$CTREE" || fail "client upload failed"
|
||||
: > "$CLOG"; ("$PY" "$SKILL/scripts/serial_capture.py" "$CPORT" $CLIENT_CAPTURE "$CLOG" 2> "${CLOG%-serial.log}-capture.err" &)
|
||||
if n=$(wait_for "$CLOG" "real save status\|posting the real" $SAVE_WAIT); then
|
||||
echo "[hammer] client posted the real credentials after ${n}s; settling ${SETTLE}s"; sleep "$SETTLE"
|
||||
else
|
||||
[ -s "$CLOG" ] || fail "the client capture produced nothing (the capture itself failed), see ${CLOG%-serial.log}-capture.err"
|
||||
port_never_opened "$CLOG" && fail "could not open $CPORT (held by another process, or the wrong port), see $CLOG"
|
||||
INVALID="client never reached the real save within ${n}s"; echo "[hammer] WARNING: $INVALID"
|
||||
fi
|
||||
|
||||
# A verdict needs complete evidence: both captures must have lasted the run and logged no error of their own
|
||||
for spec in "$DPORT:${DLOG%-serial.log}-capture.err" "$CPORT:${CLOG%-serial.log}-capture.err"; do
|
||||
port=${spec%%:*}; err=${spec#*:}
|
||||
pgrep -f "[s]erial_capture.py $port " >/dev/null || INVALID="${INVALID:+$INVALID; }the capture on $port ended before the run did, see $err"
|
||||
[ -s "$err" ] && INVALID="${INVALID:+$INVALID; }the capture on $port reported an error, see $err"
|
||||
done
|
||||
CRASHES=$(LC_ALL=C grep -a -c -E "$CRASH_RE" "$DLOG")
|
||||
BANNERS=$(( $(count "$BANNER_RE" "$DLOG") - BANNER_BASE )); LOSSES=$(( $(count "$LOSS_RE" "$DLOG") - LOSS_BASE ))
|
||||
if [ "$KIND" = pico ]; then
|
||||
REBOOTS=$BANNERS; [ "$LOSSES" -gt "$REBOOTS" ] && REBOOTS=$LOSSES # one reboot can show as both, so take the larger
|
||||
else
|
||||
REBOOTS=$BANNERS; [ "$LOSSES" -gt 0 ] && INVALID="${INVALID:+$INVALID; }the DUT port dropped $LOSSES time(s) mid-run (adapter or hub, not the chip)"
|
||||
fi
|
||||
STA=$(LC_ALL=C grep -a 'IP Address:' "$DLOG" | grep -v -c '192\.168\.4\.'); REAL=$(LC_ALL=C grep -a -o 'real save status [-0-9]*' "$CLOG" | tail -1 | awk '{print $4}')
|
||||
SAVES=$(count 'Requested WiFi Settings Change' "$DLOG"); EXITS=$(count 'cooldown early' "$DLOG"); ANSWERED=$(count 'bogus save status 200' "$CLOG")
|
||||
echo "[hammer] $DNAME: saves requested $SAVES, cooldown exits $EXITS, crash markers $CRASHES, after the AP came up: boot banners $BANNERS, port losses $LOSSES, reboots $REBOOTS; STA addresses $STA"
|
||||
FAILED=$(count 'bogus save status -1' "$CLOG")
|
||||
echo "[hammer] $CNAME: bogus saves answered $ANSWERED, failed $FAILED, real save status ${REAL:-none}"
|
||||
# Only whole messages, matched where the message starts, so a credential line or an odd SSID never gets in
|
||||
LC_ALL=C grep -a -E "^\[[0-9:]+\](\[[A-Z]\]\[[a-z_]+:[0-9]+\]: *(Exiting cooldown early|Starting fallback AP|Captive portal|Requested WiFi Settings Change|Retry phase|Connected\$|IP Address: )|\[capture\] port lost|($CRASH_RE))" "$DLOG" | LC_ALL=C tr -cd '\11\12\15\40-\176' | sed -E 's/(IP Address: )[^ ]+/\1<redacted>/' | cut -c1-110 | head -80 > "$DDIR/$DNAME-events.txt"
|
||||
echo "[hammer] events: $DDIR/$DNAME-events.txt"
|
||||
stop_tails
|
||||
[ -z "$INVALID" ] || { echo "[hammer] RESULT INVALID: $INVALID"; exit 2; }
|
||||
# Provisioning is proven by the device joining the network; the client's answer to the real save can time
|
||||
# out because the device leaves the AP before replying, so its status is information only
|
||||
if [ "$CRASHES" -gt 0 ] || [ "$REBOOTS" -gt 0 ] || [ "$STA" -lt 1 ]; then
|
||||
echo "[hammer] RESULT FAIL: crash markers $CRASHES, reboots $REBOOTS, STA addresses $STA, real save status ${REAL:-none}"; exit 3
|
||||
fi
|
||||
# A pass needs the stimulus the flow is about: at least half the configured bogus saves answered and one cooldown exit
|
||||
CONFIGURED=$(LC_ALL=C grep -a -E '^[[:space:]]*bogus_saves:' "$CYAML" | head -1 | tr -dc '0-9'); CONFIGURED=${CONFIGURED:-15}
|
||||
if [ "$EXITS" -lt 1 ] || [ $((ANSWERED * 2)) -lt "$CONFIGURED" ]; then
|
||||
echo "[hammer] RESULT INVALID: insufficient stimulus (bogus saves answered $ANSWERED of $CONFIGURED, cooldown exits $EXITS)"; exit 2
|
||||
fi
|
||||
[ "$FAILED" -eq 0 ] || echo "[hammer] WARNING: $FAILED of $CONFIGURED bogus saves got no answer"
|
||||
echo "[hammer] RESULT PASS"; exit 0
|
||||
@@ -0,0 +1,77 @@
|
||||
# Package for the client board: it joins the device under test's AP (priority 10), posts
|
||||
# `bogus_saves` bogus /wifisave requests `save_interval` apart, then posts the real credentials once.
|
||||
# Include from a per-run yaml that holds the platform block and the substitutions (example-client.yaml);
|
||||
# real_ssid and real_psk come from the scratch directory's secrets.yaml and must be URL safe.
|
||||
# Set bogus_saves to 1 for a plain provisioning check. Log lines the driver script counts:
|
||||
# "bogus save N", "bogus save status 200", "posting the real credentials", "real save status 200"
|
||||
# The hammer is a single-mode script, so a reconnect to the AP (the device under test rebooted) does not
|
||||
# start a second series on top of the first.
|
||||
substitutions:
|
||||
ap_password: hwtestpass
|
||||
bogus_saves: "15"
|
||||
save_interval: 3s
|
||||
bogus_ssid: hwtest-no-such-network
|
||||
bogus_psk: nosuchpassword
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
http_request:
|
||||
id: http
|
||||
verify_ssl: false
|
||||
timeout: 10s
|
||||
|
||||
wifi:
|
||||
id: wifi_id
|
||||
networks:
|
||||
- ssid: ${ap_ssid}
|
||||
password: ${ap_password}
|
||||
priority: 10
|
||||
- ssid: ${real_ssid}
|
||||
password: ${real_psk}
|
||||
on_connect:
|
||||
- lambda: |-
|
||||
char b[40];
|
||||
ESP_LOGI("test", "connected, ip %s", id(wifi_id).get_ip_addresses()[0].str_to(b));
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
char b[40];
|
||||
return strncmp(id(wifi_id).get_ip_addresses()[0].str_to(b), "192.168.4.", 10) == 0;
|
||||
then:
|
||||
- script.execute: hammer
|
||||
|
||||
script:
|
||||
- id: hammer
|
||||
mode: single
|
||||
then:
|
||||
- delay: 3s
|
||||
- logger.log: "hammering /wifisave with bogus credentials"
|
||||
- repeat:
|
||||
count: ${bogus_saves}
|
||||
then:
|
||||
- lambda: ESP_LOGI("test", "bogus save %u", (unsigned) iteration + 1);
|
||||
- http_request.get:
|
||||
url: "http://192.168.4.1/wifisave?ssid=${bogus_ssid}&psk=${bogus_psk}"
|
||||
capture_response: true
|
||||
on_response:
|
||||
- lambda: ESP_LOGI("test", "bogus save status %d", response->status_code);
|
||||
on_error:
|
||||
- lambda: ESP_LOGW("test", "bogus save status -1");
|
||||
- delay: ${save_interval}
|
||||
- logger.log: "posting the real credentials"
|
||||
- http_request.get:
|
||||
url: "http://192.168.4.1/wifisave?ssid=${real_ssid}&psk=${real_psk}"
|
||||
capture_response: true
|
||||
on_response:
|
||||
- lambda: ESP_LOGI("test", "real save status %d", response->status_code);
|
||||
on_error:
|
||||
- lambda: ESP_LOGW("test", "real save status -1");
|
||||
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
|
||||
@@ -0,0 +1,31 @@
|
||||
# Package for a device under test that never finds its network, falls back to its AP and serves the
|
||||
# captive portal. Include it from a per-run yaml that holds the platform block and the substitutions
|
||||
# (see example-dut.yaml). Bump `name` on every run: saved credentials are keyed by it, and a reused
|
||||
# name boots straight onto the real network and never raises the AP.
|
||||
substitutions:
|
||||
ap_password: hwtestpass
|
||||
ap_timeout: 15s
|
||||
bogus_ssid: hwtest-no-such-network
|
||||
bogus_psk: nosuchpassword
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: ${bogus_ssid}
|
||||
password: ${bogus_psk}
|
||||
ap:
|
||||
ssid: ${ap_ssid}
|
||||
password: ${ap_password}
|
||||
ap_timeout: ${ap_timeout}
|
||||
|
||||
captive_portal:
|
||||
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- lambda: ESP_LOGI("test", "alive %u s", (unsigned) (millis() / 1000));
|
||||
@@ -0,0 +1,18 @@
|
||||
# Client board for the /wifisave hammer. Copy into a scratch directory next to a secrets.yaml;
|
||||
# ap_ssid must match the device under test, real_ssid and real_psk must be URL safe.
|
||||
substitutions:
|
||||
name: hwtest-client-1
|
||||
ap_ssid: hwtest-ap
|
||||
real_ssid: !secret wifi_ssid
|
||||
real_psk: !secret wifi_password
|
||||
bogus_saves: "15"
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
# esp8266: {board: d1_mini}
|
||||
|
||||
packages:
|
||||
hwtest: !include client-wifisave.yaml
|
||||
@@ -0,0 +1,16 @@
|
||||
# Device under test for the /wifisave hammer. Copy into a scratch directory, keep the file name equal
|
||||
# to `name`, pick one platform block, bump `name` every run.
|
||||
substitutions:
|
||||
name: hwtest-dut-1
|
||||
ap_ssid: hwtest-ap
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
# esp32: {board: esp32dev, framework: {type: esp-idf}}
|
||||
# rtl87xx: {board: bw15}
|
||||
# bk72xx: {board: generic-bk7238} # plus logger: {hardware_uart: UART1}, BK72xx logs on UART2 by default
|
||||
# rp2040: {board: rpipico2w}
|
||||
|
||||
packages:
|
||||
hwtest: !include dut-fallback-ap.yaml
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -1003,7 +1003,7 @@ jobs:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM --grep USE_OPENTHREAD
|
||||
tidy_environment: esp32s3-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 P4
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -225,6 +225,15 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
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()`.
|
||||
* **`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
|
||||
`error_code_` member exists only to pick that message. Pass the reason where setup fails instead:
|
||||
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))` for a failed bus transfer, a short specific reason
|
||||
otherwise (`LOG_STR("Wrong chip ID")`). Core also logs it at that moment, so no `ESP_LOGE` repeats it
|
||||
before `mark_failed()`; keep a separate line only when it prints a value read at run time, such as the
|
||||
chip ID that did not match. Only the first reason is kept, so do not call `status_set_error()` before
|
||||
`mark_failed()` and do not mark the component failed again after a helper already did.
|
||||
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
|
||||
runtime check of something the build already knows.
|
||||
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
|
||||
|
||||
@@ -40,6 +40,7 @@ esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apc1/* @ademuri
|
||||
esphome/components/apc_proteous/* @clydebarrow
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/apds9930/* @iyesin
|
||||
esphome/components/api/* @esphome/core
|
||||
@@ -48,6 +49,7 @@ esphome/components/aranet/* @patrickod
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/as734x/* @latonita @mrgnr
|
||||
esphome/components/async_tcp/* @esphome/core
|
||||
esphome/components/at581x/* @X-Ryl669
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
@@ -155,6 +157,7 @@ esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dew_point/* @CFlix
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dfrobot_sen0609/* @remcom
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||
@@ -257,6 +260,7 @@ esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/hdc2080/* @G-Pereira @jesserockz
|
||||
esphome/components/hdc302x/* @joshuasing
|
||||
esphome/components/hdmi_cec/* @JosVanEijndhoven
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
@@ -507,6 +511,7 @@ esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8025t/* @remcom
|
||||
esphome/components/rx8111/* @techdregs
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/samsung/* @jorofi
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.24.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -11,7 +11,7 @@ static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
|
||||
void ADS1115Component::setup() {
|
||||
uint16_t value;
|
||||
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ void ADS1115Component::setup() {
|
||||
config |= 0b0000000000000011;
|
||||
|
||||
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->prev_config_ = config;
|
||||
@@ -66,9 +66,6 @@ void ADS1115Component::setup() {
|
||||
void ADS1115Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ADS1115:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain,
|
||||
ADS1115Resolution resolution, ADS1115Samplerate samplerate) {
|
||||
|
||||
@@ -52,8 +52,7 @@ void AHT10Component::setup() {
|
||||
break;
|
||||
}
|
||||
if (error_code != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
uint8_t cal_attempts = 0;
|
||||
@@ -61,20 +60,17 @@ void AHT10Component::setup() {
|
||||
while (data & AHT10_STATUS_BUSY) {
|
||||
delay(AHT10_DEFAULT_DELAY);
|
||||
if (this->read(&data, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
++cal_attempts;
|
||||
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
|
||||
ESP_LOGE(TAG, "Initialization timed out");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Initialization timed out"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
|
||||
ESP_LOGE(TAG, "Initialization failed");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Initialization failed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -152,9 +148,6 @@ void AHT10Component::update() {
|
||||
void AHT10Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AHT10:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
|
||||
|
||||
#define ERROR_CHECK(err, msg) \
|
||||
if (!(err)) { \
|
||||
ESP_LOGE(TAG, msg); \
|
||||
this->mark_failed(); \
|
||||
this->mark_failed(LOG_STR(msg)); \
|
||||
return; \
|
||||
}
|
||||
|
||||
@@ -108,10 +107,6 @@ void AIC3204::setup() {
|
||||
void AIC3204::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AIC3204:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
bool AIC3204::set_mute_off() {
|
||||
|
||||
@@ -42,21 +42,18 @@ bool AM2315C::reset_register_(uint8_t reg) {
|
||||
data[2] = 0;
|
||||
ESP_LOGD(TAG, "Reset register: 0x%02x", reg);
|
||||
if (this->write(data, 3) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Write failed!");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
delay(5);
|
||||
if (this->read(data, 3) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read failed!");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
delay(10);
|
||||
data[0] = 0xB0 | reg;
|
||||
if (this->write(data, 3) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Write failed!");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
delay(5);
|
||||
@@ -76,8 +73,7 @@ void AM2315C::setup() {
|
||||
// get status
|
||||
uint8_t status = 0;
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read failed!");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,18 +82,9 @@ void AM2315C::setup() {
|
||||
// never required during testing
|
||||
if ((status & 0x18) != 0x18) {
|
||||
ESP_LOGD(TAG, "Resetting AM2315C registers");
|
||||
if (!this->reset_register_(0x1B)) {
|
||||
this->mark_failed();
|
||||
// reset_register_() marks the component failed with its own reason
|
||||
if (!this->reset_register_(0x1B) || !this->reset_register_(0x1C) || !this->reset_register_(0x1E))
|
||||
return;
|
||||
}
|
||||
if (!this->reset_register_(0x1C)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!this->reset_register_(0x1E)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,9 +155,6 @@ void AM2315C::update() {
|
||||
void AM2315C::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AM2315C:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -37,16 +37,13 @@ void AM2320Component::setup() {
|
||||
data[0] = 0;
|
||||
data[1] = 4;
|
||||
if (!this->read_data_(data)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
void AM2320Component::dump_config() {
|
||||
ESP_LOGD(TAG, "AM2320:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
namespace esphome::animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type, image::Transparency transparent)
|
||||
: Image(data_start, width, height, type, transparent),
|
||||
image::ImageType type, image::Transparency transparent, bool big_endian)
|
||||
: Image(data_start, width, height, type, transparent, big_endian),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::animation {
|
||||
class Animation final : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
|
||||
image::Transparency transparent);
|
||||
image::Transparency transparent, bool big_endian = false);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_LOOP
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_LOOP
|
||||
from esphome.components.file import image as file_image
|
||||
from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
@@ -90,6 +90,7 @@ async def setup_animation(config: ConfigType) -> None:
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
|
||||
)
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "apc_proteous"
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "apc_proteous_cover.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::apc_proteous {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "apc_proteous.cover");
|
||||
static constexpr const char *START_CMD = "*1\r"; // start/stop toggle
|
||||
static constexpr const char *OPEN_CMD = "*6\r";
|
||||
static constexpr const char *CLOSE_CMD = "*7\r";
|
||||
static constexpr const char *QUERY_S = "?s\r";
|
||||
static constexpr const char *QUERY_X = "?x\r";
|
||||
|
||||
// Depending on calibration the reported position may stop a little short of the physical limits
|
||||
// (e.g. 99% at the fully open reed switch), so readings this close to an end count as that end.
|
||||
static constexpr uint8_t ENDPOINT_MARGIN = 2;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void APCProteousCover::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
restore->apply(this);
|
||||
} else {
|
||||
this->position = 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::loop() {
|
||||
uint8_t data;
|
||||
while (this->available() > 0) {
|
||||
if (!this->read_byte(&data)) {
|
||||
continue;
|
||||
}
|
||||
if (data == '\r') {
|
||||
if (this->rx_len_ != 0) {
|
||||
this->rx_buffer_[this->rx_len_] = '\0';
|
||||
ESP_LOGVV(TAG, "rx: '%s'", this->rx_buffer_);
|
||||
this->parse_response_();
|
||||
this->rx_len_ = 0;
|
||||
}
|
||||
} else if (data != '\n') {
|
||||
if (this->rx_len_ >= MAX_RESPONSE_LEN) {
|
||||
ESP_LOGVV(TAG, "rx overflow, discarding");
|
||||
this->rx_len_ = 0;
|
||||
}
|
||||
this->rx_buffer_[this->rx_len_++] = static_cast<char>(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::parse_response_() {
|
||||
// Expected format: "?s=XX" or "?x=XX" where XX is a hex value
|
||||
if (this->rx_len_ < 4 || this->rx_buffer_[0] != '?' || this->rx_buffer_[2] != '=') {
|
||||
ESP_LOGV(TAG, "Invalid response: %s", this->rx_buffer_);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *hex_str = this->rx_buffer_ + 3;
|
||||
char *end_ptr;
|
||||
uint8_t value = static_cast<uint8_t>(strtol(hex_str, &end_ptr, 16));
|
||||
if (end_ptr == hex_str) {
|
||||
ESP_LOGW(TAG, "Failed to parse hex value: %s", this->rx_buffer_);
|
||||
return;
|
||||
}
|
||||
|
||||
bool state_changed = false;
|
||||
char type = this->rx_buffer_[1];
|
||||
if (type == 's') {
|
||||
// bit 0 = operating, bit 1 = direction (0 = opening, 1 = closing)
|
||||
CoverOperation new_operation = COVER_OPERATION_IDLE;
|
||||
if (value & 0x01) {
|
||||
new_operation = (value & 0x02) ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
|
||||
}
|
||||
if (this->current_operation != new_operation) {
|
||||
this->current_operation = new_operation;
|
||||
state_changed = true;
|
||||
}
|
||||
// Motion in the commanded direction acknowledges the pending command
|
||||
if ((this->pending_command_ == OPEN_CMD && new_operation == COVER_OPERATION_OPENING) ||
|
||||
(this->pending_command_ == CLOSE_CMD && new_operation == COVER_OPERATION_CLOSING)) {
|
||||
this->clear_pending_();
|
||||
}
|
||||
// A partial move is over once the acknowledged motion has stopped, wherever it stopped, so an
|
||||
// old target cannot halt a movement started from the wall button or the remote later on
|
||||
if (new_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
|
||||
this->target_position_.reset();
|
||||
}
|
||||
ESP_LOGV(TAG, "s-status: 0x%02X (operation=%d)", value, new_operation);
|
||||
} else if (type == 'x') {
|
||||
// Position percentage, snapped to the ends so is_open/is_closed can latch despite calibration offsets
|
||||
float new_position;
|
||||
if (value + ENDPOINT_MARGIN >= 100) {
|
||||
new_position = COVER_OPEN;
|
||||
} else if (value <= ENDPOINT_MARGIN) {
|
||||
new_position = COVER_CLOSED;
|
||||
} else {
|
||||
new_position = value / 100.0f;
|
||||
}
|
||||
if (this->position != new_position) {
|
||||
this->position = new_position;
|
||||
state_changed = true;
|
||||
// Until the controller has confirmed motion in the commanded direction, current_operation may
|
||||
// still show the previous one, so a reversing partial move must not be judged against it
|
||||
if (this->target_position_.has_value() && this->pending_command_ == nullptr &&
|
||||
((this->current_operation == COVER_OPERATION_OPENING && this->position >= *this->target_position_) ||
|
||||
(this->current_operation == COVER_OPERATION_CLOSING && this->position <= *this->target_position_))) {
|
||||
ESP_LOGD(TAG, "Target position %.2f reached", *this->target_position_);
|
||||
this->stop_cmd_(App.get_loop_component_start_time());
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "x-status: %d%% (position=%.2f)", value, this->position);
|
||||
}
|
||||
|
||||
if (state_changed) {
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::clear_pending_() {
|
||||
this->pending_command_ = nullptr;
|
||||
this->command_retries_ = 0;
|
||||
}
|
||||
|
||||
void APCProteousCover::retry_pending_command_(uint32_t now) {
|
||||
if (this->pending_command_ == nullptr || (now - this->pending_command_time_) < COMMAND_ACK_MS) {
|
||||
return;
|
||||
}
|
||||
bool is_open_cmd = this->pending_command_ == OPEN_CMD;
|
||||
// Nothing to resend once the gate is at the commanded end
|
||||
if ((is_open_cmd && this->position >= COVER_OPEN) || (!is_open_cmd && this->position <= COVER_CLOSED)) {
|
||||
this->clear_pending_();
|
||||
return;
|
||||
}
|
||||
const LogString *label = is_open_cmd ? LOG_STR("open") : LOG_STR("close");
|
||||
if (this->command_retries_ >= MAX_COMMAND_RETRIES) {
|
||||
ESP_LOGW(TAG, "Gate did not acknowledge %s command after %u retries", LOG_STR_ARG(label), this->command_retries_);
|
||||
this->clear_pending_();
|
||||
this->target_position_.reset();
|
||||
return;
|
||||
}
|
||||
this->command_retries_++;
|
||||
ESP_LOGD(TAG, "Gate did not respond, resending %s command (retry %u)", LOG_STR_ARG(label), this->command_retries_);
|
||||
this->pending_command_time_ = now;
|
||||
this->write_command_(this->pending_command_, now);
|
||||
}
|
||||
|
||||
void APCProteousCover::update() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->retry_pending_command_(now);
|
||||
|
||||
// Let the command and its echo clear the line before the next query
|
||||
if ((now - this->last_command_tx_) < COMMAND_QUIET_MS) {
|
||||
return;
|
||||
}
|
||||
// The line is idle, so anything left in the buffer is an unterminated echo
|
||||
if (this->rx_len_ != 0) {
|
||||
this->rx_buffer_[this->rx_len_] = '\0';
|
||||
ESP_LOGVV(TAG, "discarding stale rx '%s' before query", this->rx_buffer_);
|
||||
this->rx_len_ = 0;
|
||||
}
|
||||
|
||||
const char *query = this->query_s_next_ ? QUERY_S : QUERY_X;
|
||||
ESP_LOGVV(TAG, "tx: %s", query);
|
||||
this->write_str(query);
|
||||
this->query_s_next_ = !this->query_s_next_;
|
||||
}
|
||||
|
||||
void APCProteousCover::dump_config() {
|
||||
LOG_COVER("", "APC Proteous Cover", this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
CoverTraits APCProteousCover::get_traits() {
|
||||
auto traits = CoverTraits();
|
||||
traits.set_supports_position(true);
|
||||
traits.set_supports_stop(true);
|
||||
traits.set_supports_toggle(true);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void APCProteousCover::write_command_(const char *cmd, uint32_t now) {
|
||||
ESP_LOGVV(TAG, "tx: %s", cmd);
|
||||
this->last_command_tx_ = now;
|
||||
this->write_str(cmd);
|
||||
}
|
||||
|
||||
void APCProteousCover::send_command_(const char *cmd) {
|
||||
// The published state comes only from status reads; this just records the command so a stop
|
||||
// can cancel it before the controller reports motion, and so it can be resent if dropped.
|
||||
// The commands are idempotent, so only skip one when the gate is already moving that way.
|
||||
CoverOperation operation = cmd == OPEN_CMD ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
|
||||
if (this->current_operation == operation) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending %s command",
|
||||
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("open") : LOG_STR_LITERAL("close"));
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->pending_command_ = cmd;
|
||||
this->pending_command_time_ = now;
|
||||
this->command_retries_ = 0;
|
||||
this->write_command_(cmd, now);
|
||||
}
|
||||
|
||||
void APCProteousCover::stop_cmd_(uint32_t now) {
|
||||
// START_CMD is a start/stop toggle, so it is only sent while the gate is moving, or while a
|
||||
// movement command is in flight that the controller has not reported yet; otherwise it would start the gate.
|
||||
if (this->current_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
|
||||
ESP_LOGD(TAG, "Cover already idle, ignoring stop command");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending stop command");
|
||||
this->write_command_(START_CMD, now);
|
||||
this->clear_pending_();
|
||||
this->target_position_.reset();
|
||||
}
|
||||
|
||||
void APCProteousCover::control(const CoverCall &call) {
|
||||
if (call.get_stop()) {
|
||||
this->stop_cmd_(App.get_loop_component_start_time());
|
||||
} else if (call.get_position().has_value()) {
|
||||
float target = *call.get_position();
|
||||
ESP_LOGD(TAG, "Target position: %.2f, current: %.2f", target, this->position);
|
||||
// There is no position command: a partial target is reached by moving towards it and stopping
|
||||
// once the reported position gets there
|
||||
this->target_position_.reset();
|
||||
if (target == COVER_OPEN) {
|
||||
this->send_command_(OPEN_CMD);
|
||||
} else if (target == COVER_CLOSED) {
|
||||
this->send_command_(CLOSE_CMD);
|
||||
} else if (target != this->position) {
|
||||
this->target_position_ = target;
|
||||
this->send_command_(target > this->position ? OPEN_CMD : CLOSE_CMD);
|
||||
}
|
||||
} else if (call.get_toggle()) {
|
||||
this->clear_pending_();
|
||||
this->target_position_.reset();
|
||||
this->write_command_(START_CMD, App.get_loop_component_start_time());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::apc_proteous
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome::apc_proteous {
|
||||
|
||||
class APCProteousCover : public cover::Cover, public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
cover::CoverTraits get_traits() override;
|
||||
|
||||
protected:
|
||||
void control(const cover::CoverCall &call) override;
|
||||
void parse_response_();
|
||||
void send_command_(const char *cmd);
|
||||
void write_command_(const char *cmd, uint32_t now);
|
||||
void stop_cmd_(uint32_t now);
|
||||
void retry_pending_command_(uint32_t now);
|
||||
void clear_pending_();
|
||||
|
||||
// The longest valid frame is "?s=XX"; anything longer is line noise
|
||||
static constexpr size_t MAX_RESPONSE_LEN = 16;
|
||||
|
||||
// A movement command is acknowledged once the controller reports motion in the commanded
|
||||
// direction; resend it this often, up to MAX_COMMAND_RETRIES times, since the serial link
|
||||
// occasionally drops one. The stop toggle is never resent because a spurious toggle could start the gate.
|
||||
static constexpr uint32_t COMMAND_ACK_MS = 2000;
|
||||
static constexpr uint8_t MAX_COMMAND_RETRIES = 3;
|
||||
|
||||
// The controller echoes every command, sometimes without a terminator. A query sent too soon
|
||||
// after a command collides with that echo, so polling pauses for this long after each command.
|
||||
static constexpr uint32_t COMMAND_QUIET_MS = 250;
|
||||
|
||||
// Set only for a partial target; the gate is stopped when the reported position reaches it
|
||||
optional<float> target_position_{};
|
||||
// Last movement command sent and not yet acknowledged; lets a stop cancel it and drives the retries
|
||||
const char *pending_command_{nullptr};
|
||||
uint32_t pending_command_time_{0};
|
||||
uint32_t last_command_tx_{0};
|
||||
char rx_buffer_[MAX_RESPONSE_LEN + 1];
|
||||
uint8_t rx_len_{0};
|
||||
uint8_t command_retries_{0};
|
||||
bool query_s_next_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::apc_proteous
|
||||
@@ -0,0 +1,33 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import cover, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
apc_proteous_ns = cg.esphome_ns.namespace("apc_proteous")
|
||||
APCProteousCover = apc_proteous_ns.class_(
|
||||
"APCProteousCover", cover.Cover, cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cover.cover_schema(APCProteousCover)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.polling_component_schema("500ms"))
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await cover.new_cover(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -34,8 +34,7 @@ enum { // APDS9306 registers
|
||||
|
||||
#define APDS9306_ERROR_CHECK(func, error) \
|
||||
if (!(func)) { \
|
||||
ESP_LOGE(TAG, error); \
|
||||
this->mark_failed(); \
|
||||
this->mark_failed(LOG_STR(error)); \
|
||||
return; \
|
||||
}
|
||||
#define APDS9306_WARNING_CHECK(func, warning) \
|
||||
@@ -48,21 +47,19 @@ enum { // APDS9306 registers
|
||||
ESP_LOGV(TAG, "Writing 0x%02x to 0x%02x", value, reg); \
|
||||
if (!this->write_byte(reg, value)) { \
|
||||
ESP_LOGE(TAG, "Failed writing 0x%02x to 0x%02x", value, reg); \
|
||||
this->mark_failed(); \
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); \
|
||||
return; \
|
||||
}
|
||||
|
||||
void APDS9306::setup() {
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065
|
||||
this->error_code_ = WRONG_ID;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("APDS9306 has invalid id"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,20 +86,6 @@ void APDS9306::dump_config() {
|
||||
LOG_SENSOR("", "APDS9306", this);
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_ID:
|
||||
ESP_LOGE(TAG, "APDS9306 has invalid id!");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Setting up APDS9306 registers failed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Gain: %u\n"
|
||||
" Measurement rate: %u\n"
|
||||
|
||||
@@ -50,12 +50,6 @@ class APDS9306 final : public sensor::Sensor, public PollingComponent, public i2
|
||||
void set_ambient_light_gain(AmbientLightGain gain) { this->gain_ = gain; }
|
||||
|
||||
protected:
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
WRONG_ID,
|
||||
} error_code_{NONE};
|
||||
|
||||
MeasurementBitWidth bit_width_;
|
||||
MeasurementRate measurement_rate_;
|
||||
AmbientLightGain gain_;
|
||||
|
||||
@@ -8,7 +8,7 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
|
||||
|
||||
#define APDS9960_ERROR_CHECK(func) \
|
||||
if (!(func)) { \
|
||||
this->mark_failed(); \
|
||||
this->mark_failed(LOG_STR("Setting up APDS9960 registers failed")); \
|
||||
return; \
|
||||
}
|
||||
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
|
||||
@@ -16,14 +16,12 @@ ESPHOME_LOG_TAG(TAG, "apds9960");
|
||||
void APDS9960::setup() {
|
||||
uint8_t id;
|
||||
if (!this->read_byte(0x92, &id)) { // ID register
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
|
||||
this->error_code_ = WRONG_ID;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("APDS9960 has invalid id"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,20 +133,6 @@ void APDS9960::dump_config() {
|
||||
LOG_SENSOR(" ", "Clear channel", this->clear_sensor_);
|
||||
LOG_SENSOR(" ", "Proximity", this->proximity_sensor_);
|
||||
#endif
|
||||
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_ID:
|
||||
ESP_LOGE(TAG, "APDS9960 has invalid id!");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Setting up APDS9960 registers failed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define APDS9960_WARNING_CHECK(func, warning) \
|
||||
|
||||
@@ -58,11 +58,6 @@ class APDS9960 final : public PollingComponent, public i2c::I2CDevice {
|
||||
uint8_t gesture_gain_;
|
||||
uint8_t gesture_wait_time_;
|
||||
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
WRONG_ID,
|
||||
} error_code_{NONE};
|
||||
bool gesture_up_started_{false};
|
||||
bool gesture_down_started_{false};
|
||||
bool gesture_left_started_{false};
|
||||
|
||||
@@ -23,7 +23,7 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
|
||||
void AS5600Component::setup() {
|
||||
if (!this->read_byte(REGISTER_STATUS).has_value()) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ void AS5600Component::setup() {
|
||||
|
||||
// write config to config register
|
||||
if (!this->write_byte_16(REGISTER_CONF, config)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,10 +87,8 @@ void AS5600Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AS5600:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
if (this->is_failed())
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Watchdog: %d\n"
|
||||
|
||||
@@ -14,14 +14,13 @@ void AS7341Component::setup() {
|
||||
this->read_byte(AS7341_ID, &id);
|
||||
ESP_LOGCONFIG(TAG, " Read ID: 0x%X", id);
|
||||
if ((id & 0xFC) != (AS7341_CHIP_ID << 2)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Power on (enter IDLE state)
|
||||
if (!this->enable_power(true)) {
|
||||
ESP_LOGE(TAG, " Power on failed!");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Power on failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,9 +34,6 @@ void AS7341Component::setup() {
|
||||
void AS7341Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AS7341:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Gain: %u\n"
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CALIBRATION,
|
||||
CONF_GAIN,
|
||||
CONF_GLASS_ATTENUATION_FACTOR,
|
||||
CONF_ID,
|
||||
CONF_TYPE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@latonita", "@mrgnr"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensor"]
|
||||
MULTI_CONF = True
|
||||
DOMAIN = "as734x"
|
||||
|
||||
CONF_AS734X_ID = "as734x_id"
|
||||
CONF_ATIME = "atime"
|
||||
CONF_ASTEP = "astep"
|
||||
CONF_CHANNEL_CORRECTION = "channel_correction"
|
||||
CONF_DARK_CURRENT = "dark_current"
|
||||
CONF_NORMALIZE_BASIC_COUNTS = "normalize_basic_counts"
|
||||
|
||||
# Band order is a contract with the readout maps in as734x.cpp: the index selects the counts slot.
|
||||
BANDS_43 = (
|
||||
"f1",
|
||||
"f2",
|
||||
"fz",
|
||||
"f3",
|
||||
"f4",
|
||||
"fy",
|
||||
"f5",
|
||||
"fxl",
|
||||
"f6",
|
||||
"f7",
|
||||
"f8",
|
||||
"nir",
|
||||
"clear",
|
||||
)
|
||||
BANDS_41 = tuple(b for b in BANDS_43 if b not in ("fz", "fy", "fxl"))
|
||||
|
||||
as734x_ns = cg.esphome_ns.namespace("as734x")
|
||||
AS734XComponent = as734x_ns.class_(
|
||||
"AS734XComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
Gain = as734x_ns.enum("Gain")
|
||||
GAINS_41 = {
|
||||
f"X{name}": getattr(Gain, f"GAIN_{name.replace('.', '_')}X")
|
||||
for name in ("0.5", "1", "2", "4", "8", "16", "32", "64", "128", "256", "512")
|
||||
}
|
||||
GAINS_43 = {**GAINS_41, "X1024": Gain.GAIN_1024X, "X2048": Gain.GAIN_2048X}
|
||||
|
||||
Normalization = as734x_ns.enum("Normalization", True)
|
||||
NORMALIZATIONS = {
|
||||
"NONE": Normalization.NORMALIZATION_NONE,
|
||||
"ALL": Normalization.NORMALIZATION_ALL,
|
||||
"BANDS": Normalization.NORMALIZATION_BANDS,
|
||||
"CLEAR": Normalization.NORMALIZATION_CLEAR,
|
||||
}
|
||||
|
||||
# Per-channel correction for the golden device, measured by the manufacturer.
|
||||
DEFAULT_CHANNEL_CORRECTION_43 = [
|
||||
1.055464349,
|
||||
1.043509797,
|
||||
1.029576268,
|
||||
1.0175052,
|
||||
1.00441899,
|
||||
0.987356499,
|
||||
0.957597044,
|
||||
0.995863485,
|
||||
1.014628964,
|
||||
0.996500814,
|
||||
0.933072749,
|
||||
1.052236338,
|
||||
0.999570232,
|
||||
]
|
||||
|
||||
# One integration cycle lasts (ATIME + 1) * (ASTEP + 1) * 2.78 us
|
||||
CYCLE_STEP_SECONDS = 2.78e-6
|
||||
|
||||
|
||||
def _float_list(length: int) -> cv.All:
|
||||
"""One float per band, so the list length has to match the chip."""
|
||||
return cv.All(cv.ensure_list(cv.float_), cv.Length(min=length, max=length))
|
||||
|
||||
|
||||
def _calibration_schema(bands: tuple[str, ...]) -> cv.Schema:
|
||||
correction = (
|
||||
DEFAULT_CHANNEL_CORRECTION_43 if bands == BANDS_43 else [1.0] * len(bands)
|
||||
)
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_CHANNEL_CORRECTION, default=correction): _float_list(
|
||||
len(bands)
|
||||
),
|
||||
cv.Optional(CONF_DARK_CURRENT, default=[0.0] * len(bands)): _float_list(
|
||||
len(bands)
|
||||
),
|
||||
cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.positive_float,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_exposure(cycles: int):
|
||||
"""Reject an exposure that is empty or longer than the update interval."""
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
atime, astep = config[CONF_ATIME], config[CONF_ASTEP]
|
||||
if atime == 0 and astep == 0:
|
||||
raise cv.Invalid(f"{CONF_ATIME} and {CONF_ASTEP} must not both be 0")
|
||||
measurement = (atime + 1) * (astep + 1) * CYCLE_STEP_SECONDS * cycles
|
||||
if measurement > config[CONF_UPDATE_INTERVAL].total_seconds:
|
||||
raise cv.Invalid(
|
||||
f"A measurement takes {measurement:.1f}s with these {CONF_ATIME} and "
|
||||
f"{CONF_ASTEP} values, longer than the {CONF_UPDATE_INTERVAL}"
|
||||
)
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def _schema(
|
||||
gains: dict, address: int, cycles: int, bands: tuple[str, ...]
|
||||
) -> cv.Schema:
|
||||
return cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AS734XComponent),
|
||||
cv.Optional(CONF_GAIN, default="X8"): cv.enum(gains),
|
||||
cv.Optional(CONF_ATIME, default=29): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_ASTEP, default=599): cv.int_range(min=0, max=65534),
|
||||
cv.Optional(CONF_CALIBRATION, default={}): _calibration_schema(bands),
|
||||
cv.Optional(CONF_NORMALIZE_BASIC_COUNTS, default="NONE"): cv.enum(
|
||||
NORMALIZATIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(address)),
|
||||
_validate_exposure(cycles),
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
# two SMUX steps per update
|
||||
"AS7341": _schema(GAINS_41, 0x39, cycles=2, bands=BANDS_41),
|
||||
"AS7343": _schema(GAINS_43, 0x39, cycles=3, bands=BANDS_43),
|
||||
"TCS3448": _schema(GAINS_43, 0x59, cycles=3, bands=BANDS_43),
|
||||
},
|
||||
upper=True,
|
||||
)
|
||||
|
||||
|
||||
def _per_band(
|
||||
values: list[float], bands: tuple[str, ...], absent: float
|
||||
) -> list[float]:
|
||||
"""Spread per-band calibration over the 13 slots the C++ side indexes by band."""
|
||||
spread = [absent] * len(BANDS_43)
|
||||
for value, band in zip(values, bands, strict=True):
|
||||
spread[BANDS_43.index(band)] = value
|
||||
return spread
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
chip = config[CONF_TYPE]
|
||||
cg.add_define("USE_AS7341" if chip == "AS7341" else "USE_AS7343")
|
||||
var = cg.new_Pvariable(config[CONF_ID], cg.RawExpression(f"&as734x::{chip}_CHIP"))
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_gain(config[CONF_GAIN]))
|
||||
cg.add(var.set_atime(config[CONF_ATIME]))
|
||||
cg.add(var.set_astep(config[CONF_ASTEP]))
|
||||
cg.add(var.set_basic_counts_normalization(config[CONF_NORMALIZE_BASIC_COUNTS]))
|
||||
|
||||
bands = BANDS_41 if chip == "AS7341" else BANDS_43
|
||||
calibration = config[CONF_CALIBRATION]
|
||||
cg.add(
|
||||
var.set_channel_correction(
|
||||
_per_band(calibration[CONF_CHANNEL_CORRECTION], bands, 1.0)
|
||||
)
|
||||
)
|
||||
cg.add(var.set_dark_current(_per_band(calibration[CONF_DARK_CURRENT], bands, 0.0)))
|
||||
cg.add(var.set_glass_attenuation_factor(calibration[CONF_GLASS_ATTENUATION_FACTOR]))
|
||||
@@ -0,0 +1,671 @@
|
||||
#include "as734x.h"
|
||||
#include "color_helpers.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome::as734x {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "as734x");
|
||||
|
||||
// Registers and bits shared by both chips
|
||||
static constexpr uint8_t REG_ENABLE = 0x80;
|
||||
static constexpr uint8_t REG_ATIME = 0x81;
|
||||
static constexpr uint8_t REG_ASTATUS = 0x94;
|
||||
static constexpr uint8_t ENABLE_PON = 0x01;
|
||||
static constexpr uint8_t ENABLE_SP_EN = 0x02;
|
||||
static constexpr uint8_t ENABLE_SMUX_EN = 0x10;
|
||||
static constexpr uint8_t CFG0_REG_BANK = 0x10;
|
||||
static constexpr uint8_t BANK_SPLIT = 0x80; // registers below this need REG_BANK set
|
||||
static constexpr uint8_t STATUS2_AVALID = 0x40;
|
||||
static constexpr uint8_t ASTATUS_ASAT = 0x80;
|
||||
static constexpr uint8_t LED_ACT = 0x80; // LED driver on, in the chip's LED register
|
||||
static constexpr uint8_t CONFIG_LED_SEL = 0x08;
|
||||
|
||||
static constexpr uint8_t AS7341_CFG6 = 0xAF;
|
||||
static constexpr uint8_t AS7341_SMUX_CMD_WRITE = 0x10; // copy the SMUX image from RAM to the chain
|
||||
static constexpr uint8_t AS7341_SMUX_RAM = 0x00;
|
||||
static constexpr uint8_t AS7343_CHAIN_CMD = 0xE4;
|
||||
static constexpr uint8_t AS7343_CHAIN_SMUX = 0xE7;
|
||||
static constexpr uint8_t AS7343_CHAINS = 3;
|
||||
|
||||
// Read one field of a ChipInfo kept in flash
|
||||
template<typename T> static T pgm(const T &field) {
|
||||
T value;
|
||||
progmem_memcpy(&value, &field, sizeof(T));
|
||||
return value;
|
||||
}
|
||||
|
||||
static constexpr uint8_t NO_SLOT = 0xFF;
|
||||
static constexpr uint8_t CLEAR_BAND = NUM_BANDS - 1;
|
||||
static constexpr uint8_t MAX_SMUX_LEN = 20;
|
||||
static constexpr uint8_t MAX_ADC_SLOTS = 18;
|
||||
static constexpr uint32_t MIN_COLLECTION_TIMEOUT_MS = 30 * 1000;
|
||||
static constexpr uint32_t MAX_ADC_COUNT = 65535;
|
||||
|
||||
PROGMEM_STRING_TABLE(BandNames, "F1", "F2", "FZ", "F3", "F4", "FY", "F5", "FXL", "F6", "F7", "F8", "NIR", "Clear");
|
||||
|
||||
#ifdef USE_AS7341
|
||||
static const char AS7341_NAME[] PROGMEM = "AS7341";
|
||||
static constexpr uint8_t AS7341_ADC_SLOTS = 6;
|
||||
static constexpr uint8_t AS7341_SMUX_LEN = 20;
|
||||
static constexpr uint8_t AS7341_SMUX_STEPS = 2;
|
||||
static const RegValue AS7341_INIT[] PROGMEM = {{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
|
||||
static const uint8_t AS7341_SMUX[] PROGMEM = {
|
||||
0x30, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x50, 0x00, // step 0: F1 to F4, NIR, Clear
|
||||
0x00, 0x00, 0x20, 0x04, 0x00, 0x30, 0x01, 0x50, 0x00, 0x06, // step 0, continued
|
||||
0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x10, 0x03, 0x50, 0x10, // step 1: F5 to F8, NIR, Clear
|
||||
0x03, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x50, 0x00, 0x06, // step 1, continued
|
||||
};
|
||||
static_assert(sizeof(AS7341_SMUX) == AS7341_SMUX_STEPS * AS7341_SMUX_LEN && AS7341_SMUX_LEN <= MAX_SMUX_LEN);
|
||||
static constexpr uint8_t N = NO_SLOT;
|
||||
static const uint8_t AS7341_READOUT[] PROGMEM = {
|
||||
0, 1, N, 2, 3, N, N, N, N, N, N, N, N, // step 0
|
||||
N, N, N, N, N, N, 0, N, 1, 2, 3, 5, 4, // step 1: SMUX routes clear to ADC4 and NIR to ADC5
|
||||
};
|
||||
static_assert(sizeof(AS7341_READOUT) == AS7341_SMUX_STEPS * NUM_BANDS && AS7341_ADC_SLOTS <= MAX_ADC_SLOTS);
|
||||
// This chip has no FZ, FY or FXL band, so those slots are marked absent.
|
||||
// clang-format off
|
||||
static const uint16_t AS7341_WAVELENGTHS[] PROGMEM = {
|
||||
415, 445, ABSENT_NM, 480, 515, ABSENT_NM, 555, ABSENT_NM, 590, 630, 680, 910, WIDEBAND_NM,
|
||||
};
|
||||
// clang-format on
|
||||
static_assert(sizeof(AS7341_WAVELENGTHS) == NUM_BANDS * sizeof(uint16_t));
|
||||
// Datasheet values for the golden device: gain correction per gain step, the same for every band.
|
||||
// The last two entries stay at zero because this chip has no 1024x or 2048x gain.
|
||||
static const float AS7341_GAIN_CORRECTION[] PROGMEM = {1.0577f, 1.0491f, 1.0479f, 1.0491f, 1.0207f, 1.0158f, 1.0109f,
|
||||
1.0000f, 1.0003f, 0.9873f, 0.9593f, 0.0f, 0.0f};
|
||||
static_assert(sizeof(AS7341_GAIN_CORRECTION) == GAIN_COUNT * sizeof(float));
|
||||
// clang-format off
|
||||
// The three bands this chip does not have contribute nothing to the weighted sums.
|
||||
static const ChannelContribution AS7341_CONTRIBUTIONS[] PROGMEM = {
|
||||
{0.005261294f, 5.936230324f, 0.020518273f}, // F1
|
||||
{0.242615252f, 3.572528495f, 0.013525490f}, // F2
|
||||
{0.0f, 0.0f, 0.0f}, // FZ, absent
|
||||
{0.174482552f, 2.914113437f, 0.011585145f}, // F3
|
||||
{1.479499075f, 2.452851227f, 0.010604986f}, // F4
|
||||
{0.0f, 0.0f, 0.0f}, // FY, absent
|
||||
{1.906818555f, 1.940656433f, 0.008890531f}, // F5
|
||||
{0.0f, 0.0f, 0.0f}, // FXL, absent
|
||||
{1.162058142f, 1.533976606f, 0.007441544f}, // F6
|
||||
{0.462374845f, 1.819838250f, 0.009393590f}, // F7
|
||||
{-0.017794572f, 1.232942274f, 0.006890812f}, // F8
|
||||
{-0.260730901f, -1.102368000f, -0.004647911f}, // NIR
|
||||
{-0.022604075f, -0.033304813f, -0.000129106f}, // Clear
|
||||
};
|
||||
static const ChannelTristimulus AS7341_TRISTIMULUS[] PROGMEM = {
|
||||
{0.39814f, 0.01396f, 1.95010f}, // F1
|
||||
{1.29540f, 0.16748f, 6.45490f}, // F2
|
||||
{0.0f, 0.0f, 0.0f}, // FZ, absent
|
||||
{0.36956f, 0.23538f, 2.78010f}, // F3
|
||||
{0.10902f, 1.42750f, 0.18501f}, // F4
|
||||
{0.0f, 0.0f, 0.0f}, // FY, absent
|
||||
{0.71942f, 1.88670f, 0.15325f}, // F5
|
||||
{0.0f, 0.0f, 0.0f}, // FXL, absent
|
||||
{1.78180f, 1.14200f, 0.09539f}, // F6
|
||||
{1.10110f, 0.46497f, 0.10563f}, // F7
|
||||
{-0.03991f, -0.02702f, 0.08866f}, // F8
|
||||
{-0.27597f, -0.24468f, -0.61140f}, // NIR
|
||||
{-0.02347f, -0.01993f, -0.00938f}, // Clear
|
||||
};
|
||||
// clang-format on
|
||||
static_assert(sizeof(AS7341_CONTRIBUTIONS) == NUM_BANDS * sizeof(ChannelContribution));
|
||||
static_assert(sizeof(AS7341_TRISTIMULUS) == NUM_BANDS * sizeof(ChannelTristimulus));
|
||||
const ChipInfo AS7341_CHIP PROGMEM = {
|
||||
.name = AS7341_NAME,
|
||||
.init = AS7341_INIT,
|
||||
.smux = AS7341_SMUX,
|
||||
.readout = AS7341_READOUT,
|
||||
.wavelengths = AS7341_WAVELENGTHS,
|
||||
.gain_correction = AS7341_GAIN_CORRECTION,
|
||||
.contributions = AS7341_CONTRIBUTIONS,
|
||||
.tristimulus = AS7341_TRISTIMULUS,
|
||||
.init_count = 1,
|
||||
.astep_reg = 0xCA,
|
||||
.cfg0_reg = 0xA9,
|
||||
.cfg1_reg = 0xAA,
|
||||
.status2_reg = 0xA3,
|
||||
.id_reg = 0x92,
|
||||
.id_mask = 0xFC,
|
||||
.id_value = 0x09 << 2,
|
||||
.led_reg = 0x74,
|
||||
.led_sel_reg = 0x70, // CONFIG, whose LED_SEL bit connects the pin to the driver
|
||||
.adc_slots = AS7341_ADC_SLOTS,
|
||||
.smux_steps = AS7341_SMUX_STEPS,
|
||||
.integration_cycles = 1,
|
||||
.smux_len = AS7341_SMUX_LEN,
|
||||
.auto_smux = false,
|
||||
.gain_correction_per_band = false,
|
||||
.extra_clear_slot = NO_SLOT,
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef USE_AS7343
|
||||
static const char AS7343_NAME[] PROGMEM = "AS7343";
|
||||
static constexpr uint8_t AS7343_ADC_SLOTS = 18;
|
||||
static constexpr uint8_t AS7343_SMUX_LEN = 10;
|
||||
static const char TCS3448_NAME[] PROGMEM = "TCS3448";
|
||||
// CFG6, FD_CFG0, CFG10, CFG8, CFG20 (auto_smux, 18 channels in 3 cycles), AGC_GAIN_MAX, FD_TIME_1, FD_TIME_2
|
||||
static const RegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
|
||||
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
|
||||
static const uint8_t AS7343_SMUX[] PROGMEM = {0x00, 0x04, 0x65, 0x02, 0x00, 0x05, 0x00, 0x01, 0x00, 0x30,
|
||||
0x00, 0x00, 0x60, 0x20, 0x04, 0x50, 0x03, 0x00, 0x01, 0x05,
|
||||
0x05, 0x00, 0x60, 0x00, 0x30, 0x00, 0x40, 0x10, 0x20, 0x00};
|
||||
// Clear is the average of the cycle 1 (slot 4) and cycle 2 (slot 10) readings; cycle 3 always reads 0.
|
||||
static const uint8_t AS7343_READOUT[] PROGMEM = {12, 6, 0, 7, 8, 1, 15, 2, 9, 13, 14, 3, 10};
|
||||
static_assert(sizeof(AS7343_SMUX) == AS7343_CHAINS * AS7343_SMUX_LEN && AS7343_SMUX_LEN <= MAX_SMUX_LEN);
|
||||
static_assert(sizeof(AS7343_READOUT) == NUM_BANDS && AS7343_ADC_SLOTS <= MAX_ADC_SLOTS);
|
||||
// clang-format off
|
||||
static const uint16_t AS7343_WAVELENGTHS[] PROGMEM = {
|
||||
405, 425, 450, 475, 515, 555, 550, 600, 640, 690, 745, 855, WIDEBAND_NM,
|
||||
};
|
||||
// clang-format on
|
||||
static_assert(sizeof(AS7343_WAVELENGTHS) == NUM_BANDS * sizeof(uint16_t));
|
||||
// Datasheet values for the golden device: gain correction per gain step, per band.
|
||||
// clang-format off
|
||||
static const float AS7343_GAIN_CORRECTION[] PROGMEM = {
|
||||
1.149f, 1.100f, 1.060f, 1.070f, 1.063f, 1.051f, 1.062f, 1.056f, 1.049f, 1.040f, 1.080f, 1.038f, 1.065f, // 0.5x
|
||||
1.090f, 1.128f, 1.064f, 1.071f, 1.063f, 1.050f, 1.068f, 1.055f, 1.047f, 1.039f, 1.075f, 1.038f, 1.085f, // 1x
|
||||
1.083f, 1.086f, 1.062f, 1.070f, 1.062f, 1.049f, 1.057f, 1.053f, 1.045f, 1.038f, 1.063f, 1.037f, 1.069f, // 2x
|
||||
1.059f, 1.068f, 1.056f, 1.066f, 1.058f, 1.046f, 1.051f, 1.051f, 1.044f, 1.036f, 1.059f, 1.035f, 1.053f, // 4x
|
||||
1.100f, 1.109f, 1.096f, 1.108f, 1.099f, 1.089f, 1.091f, 1.092f, 1.082f, 1.078f, 1.100f, 1.076f, 1.088f, // 8x
|
||||
1.099f, 1.109f, 1.096f, 1.108f, 1.099f, 1.089f, 1.091f, 1.092f, 1.082f, 1.078f, 1.100f, 1.075f, 1.087f, // 16x
|
||||
1.088f, 1.096f, 1.085f, 1.097f, 1.087f, 1.078f, 1.079f, 1.080f, 1.071f, 1.067f, 1.087f, 1.064f, 1.076f, // 32x
|
||||
1.083f, 1.091f, 1.078f, 1.090f, 1.079f, 1.072f, 1.072f, 1.073f, 1.064f, 1.062f, 1.080f, 1.057f, 1.069f, // 64x
|
||||
1.076f, 1.084f, 1.072f, 1.085f, 1.074f, 1.066f, 1.062f, 1.067f, 1.055f, 1.056f, 1.074f, 1.051f, 1.061f, // 128x
|
||||
1.067f, 1.074f, 1.063f, 1.075f, 1.064f, 1.059f, 1.055f, 1.058f, 1.049f, 1.051f, 1.064f, 1.044f, 1.053f, // 256x
|
||||
1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, // 512x
|
||||
1.033303857f, 1.034763813f, 1.029855013f, 1.036911249f, 0.988150537f, 1.029003501f, 1.021363258f,
|
||||
0.98762089f, 1.021713376f, 1.040108204f, 0.987417698f, 1.019481421f, 0.987270057f, // 1024x
|
||||
0.986859143f, 0.980877221f, 0.9469769f, 0.985535324f, 0.926243961f, 0.959814787f, 1.007963896f,
|
||||
0.942685187f, 1.007630587f, 1.025460005f, 0.928696275f, 1.001296639f, 1.041275263f, // 2048x
|
||||
};
|
||||
// clang-format on
|
||||
static_assert(sizeof(AS7343_GAIN_CORRECTION) == GAIN_COUNT * NUM_BANDS * sizeof(float));
|
||||
// clang-format off
|
||||
static const ChannelContribution AS7343_CONTRIBUTIONS[] PROGMEM = {
|
||||
{-0.028559089f, 0.450293995f, 0.001266925f}, // F1
|
||||
{-0.042150948f, 3.566379984f, 0.012515925f}, // F2
|
||||
{0.000661721f, 2.877424672f, 0.010243005f}, // FZ
|
||||
{-0.108791278f, 2.273837581f, 0.009376157f}, // F3
|
||||
{-0.167172180f, 1.519298503f, 0.006017556f}, // F4
|
||||
{5.726660294f, 4.199345859f, 0.019357905f}, // FY
|
||||
{0.0f, 0.0f, 0.0f}, // F5
|
||||
{-0.241492014f, 1.185259160f, 0.006516325f}, // FXL
|
||||
{-0.047665296f, 1.759469826f, 0.009780714f}, // F6
|
||||
{-0.012516990f, 1.702006611f, 0.009950228f}, // F7
|
||||
{-0.026424974f, -1.059196137f, -0.005425656f}, // F8
|
||||
{-0.007565221f, -0.064827881f, -0.000232267f}, // NIR
|
||||
{0.0f, 0.0f, 0.0f}, // Clear
|
||||
};
|
||||
static const ChannelTristimulus AS7343_TRISTIMULUS[] PROGMEM = {
|
||||
{-0.07879f, -0.03269f, -0.31295f}, // F1
|
||||
{-0.12235f, -0.01297f, -0.57885f}, // F2
|
||||
{1.99879f, -0.04011f, 10.00197f}, // FZ
|
||||
{-0.33364f, -0.06889f, -0.31281f}, // F3
|
||||
{-0.06795f, -0.17453f, -0.19657f}, // F4
|
||||
{-0.45419f, 5.69083f, -0.11077f}, // FY
|
||||
{0.0f, 0.0f, 0.0f}, // F5
|
||||
{5.27242f, -0.22956f, -0.06132f}, // FXL
|
||||
{-0.05072f, -0.04762f, -0.03536f}, // F6
|
||||
{-0.04666f, -0.01295f, -0.06025f}, // F7
|
||||
{-0.03931f, -0.02797f, -0.12174f}, // F8
|
||||
{0.0f, 0.0f, 0.0f}, // NIR
|
||||
{0.0f, 0.0f, 0.0f}, // Clear
|
||||
};
|
||||
// clang-format on
|
||||
static_assert(sizeof(AS7343_CONTRIBUTIONS) == NUM_BANDS * sizeof(ChannelContribution));
|
||||
static_assert(sizeof(AS7343_TRISTIMULUS) == NUM_BANDS * sizeof(ChannelTristimulus));
|
||||
static constexpr ChipInfo as7343_chip(const char *name) {
|
||||
return {
|
||||
.name = name,
|
||||
.init = AS7343_INIT,
|
||||
.smux = AS7343_SMUX,
|
||||
.readout = AS7343_READOUT,
|
||||
.wavelengths = AS7343_WAVELENGTHS,
|
||||
.gain_correction = AS7343_GAIN_CORRECTION,
|
||||
.contributions = AS7343_CONTRIBUTIONS,
|
||||
.tristimulus = AS7343_TRISTIMULUS,
|
||||
.init_count = sizeof(AS7343_INIT) / sizeof(RegValue),
|
||||
.astep_reg = 0xD4,
|
||||
.cfg0_reg = 0xBF,
|
||||
.cfg1_reg = 0xC6,
|
||||
.status2_reg = 0x90,
|
||||
.id_reg = 0x5A,
|
||||
.id_mask = 0xFF,
|
||||
.id_value = 0x81,
|
||||
.led_reg = 0xCD,
|
||||
.led_sel_reg = 0, // the LED register drives the pin on its own
|
||||
.adc_slots = AS7343_ADC_SLOTS,
|
||||
.smux_steps = 1,
|
||||
.integration_cycles = 3,
|
||||
.smux_len = AS7343_SMUX_LEN,
|
||||
.auto_smux = true,
|
||||
.gain_correction_per_band = true,
|
||||
.extra_clear_slot = 4,
|
||||
};
|
||||
}
|
||||
const ChipInfo AS7343_CHIP PROGMEM = as7343_chip(AS7343_NAME);
|
||||
const ChipInfo TCS3448_CHIP PROGMEM = as7343_chip(TCS3448_NAME); // AS7343 register map, address 0x59
|
||||
#endif
|
||||
|
||||
// Registers below 0x80 are only reachable with the REG_BANK bit of CFG0 set; CFG0 itself always answers.
|
||||
bool AS734XComponent::select_bank_(uint8_t reg) {
|
||||
const bool low = reg < BANK_SPLIT;
|
||||
if (low == this->bank_low_) {
|
||||
return true;
|
||||
}
|
||||
this->bank_low_ = low;
|
||||
return this->write_byte(pgm(this->chip_->cfg0_reg), low ? CFG0_REG_BANK : 0);
|
||||
}
|
||||
|
||||
bool AS734XComponent::update_bits_(uint8_t reg, uint8_t mask, bool value) {
|
||||
uint8_t data = 0;
|
||||
if (!this->select_bank_(reg) || !this->read_byte(reg, &data)) {
|
||||
return false;
|
||||
}
|
||||
data = value ? (data | mask) : (data & ~mask);
|
||||
return this->write_byte(reg, data);
|
||||
}
|
||||
|
||||
// The exposure is applied before every measurement, so a gain, ATIME or ASTEP changed at runtime
|
||||
// takes effect on the next reading rather than only after a restart.
|
||||
bool AS734XComponent::write_exposure_() {
|
||||
const ChipInfo *chip = this->chip_;
|
||||
if (!this->select_bank_(REG_ATIME) || !this->write_byte(REG_ATIME, this->atime_) ||
|
||||
!this->write_byte_16(pgm(chip->astep_reg), byteswap(this->astep_)) ||
|
||||
!this->write_byte(pgm(chip->cfg1_reg), this->gain_)) {
|
||||
return false;
|
||||
}
|
||||
this->reading_gain_ = this->gain_;
|
||||
this->reading_atime_ = this->atime_;
|
||||
this->reading_astep_ = this->astep_;
|
||||
// One integration cycle lasts (ATIME + 1) * (ASTEP + 1) * 2.78 us.
|
||||
const uint32_t cycle_ms = (this->atime_ + 1UL) * (this->astep_ + 1UL) * 139 / 50000;
|
||||
this->integration_ms_ = cycle_ms * pgm(chip->integration_cycles);
|
||||
this->timeout_ms_ = std::max(MIN_COLLECTION_TIMEOUT_MS, 2 * this->integration_ms_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AS734XComponent::setup() {
|
||||
const ChipInfo *chip = this->chip_;
|
||||
const uint8_t id_reg = pgm(chip->id_reg);
|
||||
uint8_t id = 0;
|
||||
// The sensor keeps its bank across an MCU reset, so the first access always writes CFG0
|
||||
bool ok = this->select_bank_(REG_ENABLE) && this->write_byte(REG_ENABLE, ENABLE_PON);
|
||||
delayMicroseconds(200); // oscillator start-up
|
||||
ok = ok && this->select_bank_(id_reg) && this->read_byte(id_reg, &id);
|
||||
if (ok && (id & pgm(chip->id_mask)) != pgm(chip->id_value)) {
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
const RegValue *init = pgm(chip->init);
|
||||
for (uint8_t i = 0, n = pgm(chip->init_count); ok && i < n; i++) {
|
||||
RegValue rv;
|
||||
progmem_memcpy(&rv, &init[i], sizeof(rv));
|
||||
ok = this->select_bank_(rv.reg) && this->write_byte(rv.reg, rv.value);
|
||||
}
|
||||
// Everything from here on, including the measurement registers, is in the high bank.
|
||||
ok = ok && this->write_exposure_();
|
||||
if (pgm(chip->auto_smux)) {
|
||||
const uint8_t *smux = pgm(chip->smux);
|
||||
const uint8_t len = pgm(chip->smux_len);
|
||||
for (uint8_t i = 0; ok && i < AS7343_CHAINS * len; i++) {
|
||||
ok = this->write_byte(AS7343_CHAIN_SMUX, progmem_read_byte(&smux[i])) &&
|
||||
this->write_byte(AS7343_CHAIN_CMD, 0x46 + (i / len) * 0x10);
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void AS734XComponent::dump_config() {
|
||||
const float gain = this->gain_ == GAIN_0_5X ? 0.5f : static_cast<float>(1 << (this->gain_ - 1));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"AS734x:\n"
|
||||
" Chip: %s\n"
|
||||
" Gain: %gx\n"
|
||||
" ATIME: %u\n"
|
||||
" ASTEP: %u\n"
|
||||
" Glass attenuation factor: %.3f",
|
||||
LOG_STR_ARG(pgm(this->chip_->name)), gain, this->atime_, this->astep_, this->glass_attenuation_factor_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
char label[24];
|
||||
const char *name = LOG_STR_ARG(BandNames::get_log_str(band, BandNames::LAST_INDEX));
|
||||
snprintf(label, sizeof(label), "Counts %s", name);
|
||||
sensor::log_sensor(TAG, " ", label, this->sensors_[band]);
|
||||
snprintf(label, sizeof(label), "Basic counts %s", name);
|
||||
sensor::log_sensor(TAG, " ", label, this->basic_counts_sensors_[band]);
|
||||
}
|
||||
LOG_SENSOR(" ", "Saturation level", this->saturation_level_sensor_);
|
||||
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
||||
LOG_SENSOR(" ", "Irradiance photopic", this->irradiance_photopic_sensor_);
|
||||
LOG_SENSOR(" ", "Irradiance PAR", this->irradiance_par_sensor_);
|
||||
LOG_SENSOR(" ", "PPFD", this->ppfd_sensor_);
|
||||
LOG_SENSOR(" ", "Color temperature", this->color_temperature_sensor_);
|
||||
#ifdef USE_AS734X_RGB
|
||||
LOG_TEXT_SENSOR(" ", "RGB hex", this->rgb_hex_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AS734XComponent::enable_led(bool enable) {
|
||||
const uint8_t led_reg = pgm(this->chip_->led_reg);
|
||||
if (led_reg == 0) {
|
||||
return;
|
||||
}
|
||||
// The LED register only drives the LED while LED_SEL is set, so the pin is handed over before
|
||||
// switching on and released only after switching off. The other order would leave the LED lit
|
||||
// with nothing able to turn it off.
|
||||
const uint8_t led_sel_reg = pgm(this->chip_->led_sel_reg);
|
||||
bool ok = true;
|
||||
if (enable && led_sel_reg != 0) {
|
||||
ok = this->update_bits_(led_sel_reg, CONFIG_LED_SEL, true);
|
||||
}
|
||||
ok = this->update_bits_(led_reg, LED_ACT, enable) && ok;
|
||||
if (!enable && led_sel_reg != 0) {
|
||||
ok = this->update_bits_(led_sel_reg, CONFIG_LED_SEL, false) && ok;
|
||||
}
|
||||
// The measurement path assumes the high bank, so leave the chip there for a reading in flight
|
||||
ok = this->select_bank_(REG_ENABLE) && ok;
|
||||
if (!ok) {
|
||||
ESP_LOGW(TAG, "Failed to switch the LED");
|
||||
}
|
||||
}
|
||||
|
||||
void AS734XComponent::update() {
|
||||
if (this->state_ != State::STATE_IDLE) {
|
||||
ESP_LOGW(TAG, "Skipping update, previous measurement still running");
|
||||
return;
|
||||
}
|
||||
if (!this->write_exposure_()) {
|
||||
this->abort_measurement_(LOG_STR("Failed to apply measurement settings"));
|
||||
return;
|
||||
}
|
||||
this->step_ = 0;
|
||||
this->peak_raw_count_ = 0;
|
||||
this->raw_counts_.fill(0);
|
||||
this->enable_loop();
|
||||
if (!this->start_step_()) {
|
||||
this->abort_measurement_(LOG_STR("Failed to start measurement"));
|
||||
}
|
||||
}
|
||||
|
||||
bool AS734XComponent::start_step_() {
|
||||
this->start_ms_ = App.get_loop_component_start_time();
|
||||
if (pgm(this->chip_->auto_smux)) {
|
||||
this->state_ = State::STATE_READ_DATA;
|
||||
return this->write_byte(REG_ENABLE, ENABLE_PON | ENABLE_SP_EN);
|
||||
}
|
||||
// Load this step's image into SMUX RAM; SMUX_EN copies it to the chain and clears itself when done.
|
||||
std::array<uint8_t, MAX_SMUX_LEN> image{};
|
||||
const uint8_t len = pgm(this->chip_->smux_len);
|
||||
progmem_memcpy(image.data(), pgm(this->chip_->smux) + this->step_ * len, len);
|
||||
this->state_ = State::STATE_WAIT_SMUX;
|
||||
return this->write_byte(AS7341_CFG6, AS7341_SMUX_CMD_WRITE) &&
|
||||
this->write_bytes(AS7341_SMUX_RAM, image.data(), len) &&
|
||||
this->write_byte(REG_ENABLE, ENABLE_PON | ENABLE_SMUX_EN);
|
||||
}
|
||||
|
||||
void AS734XComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
uint8_t value = 0;
|
||||
switch (this->state_) {
|
||||
case State::STATE_WAIT_SMUX:
|
||||
// A failed read counts as busy, so the timeout below catches a chip that stops answering.
|
||||
if (this->read_byte(REG_ENABLE, &value) && !(value & ENABLE_SMUX_EN)) {
|
||||
this->start_ms_ = now;
|
||||
this->state_ = State::STATE_READ_DATA;
|
||||
if (!this->write_byte(REG_ENABLE, ENABLE_PON | ENABLE_SP_EN)) {
|
||||
this->abort_measurement_(LOG_STR("Failed to start integration"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case State::STATE_READ_DATA:
|
||||
if (now - this->start_ms_ < this->integration_ms_) {
|
||||
return;
|
||||
}
|
||||
if (this->read_byte(pgm(this->chip_->status2_reg), &value) && (value & STATUS2_AVALID)) {
|
||||
this->read_step_();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case State::STATE_IDLE:
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
if (now - this->start_ms_ > this->timeout_ms_) {
|
||||
this->abort_measurement_(LOG_STR("Measurement timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
void AS734XComponent::read_step_() {
|
||||
// Reading ASTATUS latches the data, which is only consistent within one burst starting there.
|
||||
std::array<uint8_t, 1 + 2 * MAX_ADC_SLOTS> frame{};
|
||||
if (!this->read_bytes(REG_ASTATUS, frame.data(), 1 + 2 * pgm(this->chip_->adc_slots)) ||
|
||||
!this->write_byte(REG_ENABLE, ENABLE_PON)) {
|
||||
this->abort_measurement_(LOG_STR("Failed to read channel data"));
|
||||
return;
|
||||
}
|
||||
if (frame[0] & ASTATUS_ASAT) {
|
||||
ESP_LOGV(TAG, "Step %u saturated", this->step_);
|
||||
}
|
||||
auto slot_value = [&frame](uint8_t slot) { return encode_uint16(frame[2 + 2 * slot], frame[1 + 2 * slot]); };
|
||||
const uint8_t *readout = pgm(this->chip_->readout) + this->step_ * NUM_BANDS;
|
||||
const uint8_t extra_clear = pgm(this->chip_->extra_clear_slot);
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
const uint8_t slot = progmem_read_byte(&readout[band]);
|
||||
if (slot == NO_SLOT) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t raw = slot_value(slot);
|
||||
this->peak_raw_count_ = std::max(this->peak_raw_count_, raw);
|
||||
uint16_t counts = raw;
|
||||
if (band == CLEAR_BAND && extra_clear != NO_SLOT) {
|
||||
const uint16_t extra = slot_value(extra_clear);
|
||||
this->peak_raw_count_ = std::max(this->peak_raw_count_, extra);
|
||||
counts = (static_cast<uint32_t>(raw) + extra) / 2;
|
||||
}
|
||||
this->raw_counts_[band] = counts;
|
||||
}
|
||||
if (++this->step_ < pgm(this->chip_->smux_steps)) {
|
||||
if (!this->start_step_()) {
|
||||
this->abort_measurement_(LOG_STR("Failed to configure SMUX"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
this->publish_measurement_();
|
||||
this->status_clear_warning();
|
||||
this->state_ = State::STATE_IDLE;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
// A basic count is the raw count scaled to one unit of gain and one millisecond of integration
|
||||
// time, which makes readings taken with different settings comparable. The manufacturer's
|
||||
// conversion factors are calibrated against that definition.
|
||||
void AS734XComponent::calculate_basic_counts_() {
|
||||
this->basic_counts_.fill(0.0f);
|
||||
this->max_basic_count_ = 0.0f;
|
||||
this->max_band_basic_count_ = 0.0f;
|
||||
this->clear_basic_count_ = 0.0f;
|
||||
|
||||
// The gain enum counts in powers of two starting at 0.5x, so the multiplier is 2^gain / 2.
|
||||
const float gain_x = static_cast<float>(1 << this->reading_gain_) / 2.0f;
|
||||
const float integration_ms = (1.0f + this->reading_atime_) * (1.0f + this->reading_astep_) * 2.78e-3f;
|
||||
if (gain_x <= 0.0f || integration_ms <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
const float inv_exposure = 1.0f / (gain_x * integration_ms);
|
||||
const ChipInfo *chip = this->chip_;
|
||||
const uint16_t *wavelengths = pgm(chip->wavelengths);
|
||||
const float *gain_correction = pgm(chip->gain_correction);
|
||||
const bool per_band = pgm(chip->gain_correction_per_band);
|
||||
const uint8_t correction_base = per_band ? this->reading_gain_ * NUM_BANDS : this->reading_gain_;
|
||||
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
const uint16_t wavelength = pgm(wavelengths[band]);
|
||||
if (wavelength == ABSENT_NM) {
|
||||
continue;
|
||||
}
|
||||
float basic_count = this->raw_counts_[band] * inv_exposure;
|
||||
basic_count -= this->dark_current_[band];
|
||||
basic_count = std::max(basic_count, 0.0f);
|
||||
basic_count *= pgm(gain_correction[per_band ? correction_base + band : correction_base]);
|
||||
basic_count *= this->channel_correction_[band];
|
||||
basic_count *= this->glass_attenuation_factor_;
|
||||
|
||||
this->basic_counts_[band] = basic_count;
|
||||
this->max_basic_count_ = std::max(this->max_basic_count_, basic_count);
|
||||
if (wavelength == WIDEBAND_NM) {
|
||||
this->clear_basic_count_ = basic_count;
|
||||
} else if (wavelength >= VISIBLE_MIN_NM && wavelength <= VISIBLE_MAX_NM) {
|
||||
this->max_band_basic_count_ = std::max(this->max_band_basic_count_, basic_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float AS734XComponent::normalization_divisor_() const {
|
||||
float divisor = 1.0f;
|
||||
switch (this->normalization_) {
|
||||
case Normalization::NORMALIZATION_ALL:
|
||||
divisor = this->max_basic_count_;
|
||||
break;
|
||||
case Normalization::NORMALIZATION_BANDS:
|
||||
divisor = this->max_band_basic_count_;
|
||||
break;
|
||||
case Normalization::NORMALIZATION_CLEAR:
|
||||
divisor = this->clear_basic_count_;
|
||||
break;
|
||||
case Normalization::NORMALIZATION_NONE:
|
||||
return 1.0f;
|
||||
}
|
||||
// In the dark the reference can be zero, and dividing by that would publish infinities.
|
||||
return divisor > 0.0f ? divisor : 1.0f;
|
||||
}
|
||||
|
||||
void AS734XComponent::publish_basic_counts_() {
|
||||
// Normalization describes the shape of the spectrum rather than its absolute level. Channels
|
||||
// outside the chosen reference can exceed 1.0 and are left that way rather than clipped: with
|
||||
// BANDS the near infrared channel legitimately sits well above the visible bands.
|
||||
const float scale = 1.0f / this->normalization_divisor_();
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
if (this->basic_counts_sensors_[band] != nullptr) {
|
||||
this->basic_counts_sensors_[band]->publish_state(this->basic_counts_[band] * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each band contributes a fixed amount per basic count to every integrated quantity, so the
|
||||
// totals are a weighted sum over the bands.
|
||||
// The weighted sums can go negative under light the tables were not fitted for, such as pure blue or
|
||||
// far red, so the published values are floored at zero.
|
||||
void AS734XComponent::publish_light_metrics_() {
|
||||
float irradiance_photopic = 0.0f;
|
||||
float irradiance_par = 0.0f;
|
||||
float ppfd = 0.0f;
|
||||
|
||||
const ChannelContribution *contributions = pgm(this->chip_->contributions);
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
const float basic_count = this->basic_counts_[band];
|
||||
const ChannelContribution contribution = pgm(contributions[band]);
|
||||
|
||||
irradiance_photopic += contribution.irradiance_photopic * basic_count;
|
||||
irradiance_par += contribution.irradiance_par * basic_count;
|
||||
ppfd += contribution.ppfd * basic_count;
|
||||
}
|
||||
irradiance_photopic = std::max(irradiance_photopic, 0.0f);
|
||||
irradiance_par = std::max(irradiance_par, 0.0f);
|
||||
ppfd = std::max(ppfd, 0.0f) * 1e3f;
|
||||
const float illuminance = irradiance_photopic * LUMENS_PER_WATT;
|
||||
|
||||
ESP_LOGV(TAG, "Photopic irradiance %.4f, PAR %.4f, PPFD %.4f, illuminance %.2f lx", irradiance_photopic,
|
||||
irradiance_par, ppfd, illuminance);
|
||||
if (this->illuminance_sensor_ != nullptr) {
|
||||
this->illuminance_sensor_->publish_state(illuminance);
|
||||
}
|
||||
if (this->irradiance_photopic_sensor_ != nullptr) {
|
||||
this->irradiance_photopic_sensor_->publish_state(irradiance_photopic);
|
||||
}
|
||||
if (this->irradiance_par_sensor_ != nullptr) {
|
||||
this->irradiance_par_sensor_->publish_state(irradiance_par);
|
||||
}
|
||||
if (this->ppfd_sensor_ != nullptr) {
|
||||
this->ppfd_sensor_->publish_state(ppfd);
|
||||
}
|
||||
}
|
||||
|
||||
void AS734XComponent::publish_color_() {
|
||||
float tri_x = 0.0f;
|
||||
float tri_y = 0.0f;
|
||||
float tri_z = 0.0f;
|
||||
|
||||
const ChannelTristimulus *table = pgm(this->chip_->tristimulus);
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
const float basic_count = this->basic_counts_[band];
|
||||
const ChannelTristimulus tristimulus = pgm(table[band]);
|
||||
|
||||
tri_x += tristimulus.x * basic_count;
|
||||
tri_y += tristimulus.y * basic_count;
|
||||
tri_z += tristimulus.z * basic_count;
|
||||
}
|
||||
|
||||
const float color_temperature = tristimulus_to_cct(tri_x, tri_y, tri_z);
|
||||
ESP_LOGV(TAG, "XYZ %.4f, %.4f, %.4f -> CCT %.0f K", tri_x, tri_y, tri_z, color_temperature);
|
||||
if (this->color_temperature_sensor_ != nullptr) {
|
||||
this->color_temperature_sensor_->publish_state(color_temperature);
|
||||
}
|
||||
|
||||
#ifdef USE_AS734X_RGB
|
||||
if (this->rgb_hex_sensor_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Convert to chromaticity first, so the colour describes the hue rather than the brightness.
|
||||
// Absolute tristimulus values clamp to white under anything but dim light.
|
||||
char rgb_hex[7] = "000000"; // no usable chromaticity reads as black rather than an empty string
|
||||
const float sum = tri_x + tri_y + tri_z;
|
||||
if (sum > MIN_TRISTIMULUS_SUM) {
|
||||
uint8_t rgb[3];
|
||||
tristimulus_to_rgb(tri_x / sum, tri_y / sum, tri_z / sum, rgb[0], rgb[1], rgb[2]);
|
||||
format_hex_to(rgb_hex, rgb, sizeof(rgb));
|
||||
}
|
||||
this->rgb_hex_sensor_->publish_state(rgb_hex);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AS734XComponent::publish_measurement_() {
|
||||
this->calculate_basic_counts_();
|
||||
for (uint8_t band = 0; band < NUM_BANDS; band++) {
|
||||
if (this->sensors_[band] != nullptr) {
|
||||
this->sensors_[band]->publish_state(this->raw_counts_[band]);
|
||||
}
|
||||
}
|
||||
this->publish_basic_counts_();
|
||||
this->publish_light_metrics_();
|
||||
this->publish_color_();
|
||||
if (this->saturation_level_sensor_ != nullptr) {
|
||||
const uint32_t max_adc =
|
||||
std::min<uint32_t>((this->reading_atime_ + 1UL) * (this->reading_astep_ + 1UL), MAX_ADC_COUNT);
|
||||
const float level = max_adc == 0 ? 0.0f : 100.0f * this->peak_raw_count_ / max_adc;
|
||||
ESP_LOGV(TAG, "Highest ADC count %u of %u (%.1f%%)", this->peak_raw_count_, static_cast<unsigned>(max_adc), level);
|
||||
this->saturation_level_sensor_->publish_state(level);
|
||||
}
|
||||
}
|
||||
|
||||
void AS734XComponent::abort_measurement_(const LogString *reason) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(reason));
|
||||
this->write_byte(REG_ENABLE, ENABLE_PON);
|
||||
this->status_set_warning(reason);
|
||||
this->state_ = State::STATE_IDLE;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
} // namespace esphome::as734x
|
||||
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#ifdef USE_AS734X_RGB
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::as734x {
|
||||
|
||||
enum Gain : uint8_t {
|
||||
GAIN_0_5X,
|
||||
GAIN_1X,
|
||||
GAIN_2X,
|
||||
GAIN_4X,
|
||||
GAIN_8X,
|
||||
GAIN_16X,
|
||||
GAIN_32X,
|
||||
GAIN_64X,
|
||||
GAIN_128X,
|
||||
GAIN_256X,
|
||||
GAIN_512X,
|
||||
GAIN_1024X, // AS7343 only
|
||||
GAIN_2048X, // AS7343 only
|
||||
};
|
||||
|
||||
static constexpr uint8_t GAIN_COUNT = 13; // entries in Gain, for sizing the correction tables
|
||||
static constexpr uint8_t NUM_BANDS = 13; // band order of BANDS_43 in __init__.py
|
||||
|
||||
// What the published basic counts are divided by. The clear channel is wideband, so it is roughly
|
||||
// the size of all the bands together, and the near infrared channel outgrows them under daylight.
|
||||
// Dividing by the largest of every channel therefore leaves the visible bands squeezed into the
|
||||
// bottom of the range.
|
||||
enum class Normalization : uint8_t {
|
||||
NORMALIZATION_NONE, // publish absolute basic counts
|
||||
NORMALIZATION_ALL, // largest of every channel
|
||||
NORMALIZATION_BANDS, // largest of the visible bands only
|
||||
NORMALIZATION_CLEAR, // the wideband clear channel
|
||||
};
|
||||
|
||||
// Nominal centre wavelengths in nanometres bound the visible bands. The clear channel is wideband
|
||||
// and has no single centre wavelength, so it is marked with zero instead.
|
||||
static constexpr uint16_t VISIBLE_MIN_NM = 380;
|
||||
static constexpr uint16_t VISIBLE_MAX_NM = 700;
|
||||
static constexpr uint16_t WIDEBAND_NM = 0;
|
||||
static constexpr uint16_t ABSENT_NM = 0xFFFF; // band this chip does not have
|
||||
|
||||
// Luminous efficacy at 555 nm, for turning photopic irradiance into lux.
|
||||
static constexpr float LUMENS_PER_WATT = 683.002f;
|
||||
|
||||
// How much one basic count of a band contributes to each integrated quantity. The factors come
|
||||
// from the manufacturer's golden-device calibration, convolved for a reconstructed spectrum.
|
||||
struct ChannelContribution {
|
||||
float irradiance_photopic; // weighted by the CIE 1931 V(lambda) curve
|
||||
float irradiance_par; // 400-700 nm only
|
||||
float ppfd; // 400-700 nm only, scaled by 1e3 before publishing
|
||||
};
|
||||
|
||||
// CIE 1931 tristimulus contribution of one basic count of a band.
|
||||
struct ChannelTristimulus {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
};
|
||||
|
||||
struct RegValue {
|
||||
uint8_t reg;
|
||||
uint8_t value;
|
||||
};
|
||||
|
||||
/// Per-chip constants, stored in flash. The pointed-to tables are in flash too.
|
||||
struct ChipInfo {
|
||||
const char *name;
|
||||
const RegValue *init; // written in order; the bank is selected per register address
|
||||
const uint8_t *smux; // AS7341: SMUX RAM image per step; AS7343: the three chain images
|
||||
const uint8_t *readout; // per step, the ADC slot of each band (0xFF: not read in this step)
|
||||
const uint16_t *wavelengths; // per band, WIDEBAND_NM for clear and ABSENT_NM for a missing band
|
||||
const float *gain_correction; // GAIN_COUNT entries, or GAIN_COUNT * NUM_BANDS when per band
|
||||
const ChannelContribution *contributions; // per band
|
||||
const ChannelTristimulus *tristimulus; // per band
|
||||
uint8_t init_count;
|
||||
uint8_t astep_reg;
|
||||
uint8_t cfg0_reg;
|
||||
uint8_t cfg1_reg;
|
||||
uint8_t status2_reg;
|
||||
uint8_t id_reg;
|
||||
uint8_t id_mask;
|
||||
uint8_t id_value;
|
||||
uint8_t led_reg; // LED driver register (0: this chip has no LED control)
|
||||
uint8_t led_sel_reg; // register whose LED_SEL bit hands the pin to the driver (0: not needed)
|
||||
uint8_t adc_slots; // 16-bit data registers read after ASTATUS
|
||||
uint8_t smux_steps; // measurements per update
|
||||
uint8_t integration_cycles;
|
||||
uint8_t smux_len; // bytes per SMUX image
|
||||
bool auto_smux; // the chip cycles its SMUX itself after a one-time chain setup
|
||||
bool gain_correction_per_band; // the correction table is indexed by gain and band, not gain alone
|
||||
uint8_t extra_clear_slot; // averaged into the clear band (0xFF: none)
|
||||
};
|
||||
|
||||
#ifdef USE_AS7341
|
||||
extern const ChipInfo AS7341_CHIP;
|
||||
#endif
|
||||
#ifdef USE_AS7343
|
||||
extern const ChipInfo AS7343_CHIP;
|
||||
extern const ChipInfo TCS3448_CHIP;
|
||||
#endif
|
||||
|
||||
class AS734XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
// Channel correction is multiplicative, so it starts at unity rather than zero in case no
|
||||
// calibration is configured.
|
||||
explicit AS734XComponent(const ChipInfo *chip) : chip_(chip) { this->channel_correction_.fill(1.0f); }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
void set_gain(Gain gain) { this->gain_ = gain; }
|
||||
void set_atime(uint8_t atime) { this->atime_ = atime; }
|
||||
void set_astep(uint16_t astep) { this->astep_ = astep; }
|
||||
|
||||
/// Turns the sensor's LED driver on or off. Meant for lambdas.
|
||||
void enable_led(bool enable);
|
||||
|
||||
// Calibration applied while turning raw counts into basic counts.
|
||||
void set_dark_current(const std::array<float, NUM_BANDS> &values) { this->dark_current_ = values; }
|
||||
void set_channel_correction(const std::array<float, NUM_BANDS> &values) { this->channel_correction_ = values; }
|
||||
void set_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; }
|
||||
|
||||
/// Chooses what the published basic counts are divided by.
|
||||
void set_basic_counts_normalization(Normalization normalization) { this->normalization_ = normalization; }
|
||||
|
||||
void set_counts_sensor(sensor::Sensor *sensor, uint8_t band) { this->sensors_[band] = sensor; }
|
||||
void set_basic_counts_sensor(sensor::Sensor *sensor, uint8_t band) { this->basic_counts_sensors_[band] = sensor; }
|
||||
|
||||
SUB_SENSOR(saturation_level)
|
||||
SUB_SENSOR(illuminance)
|
||||
SUB_SENSOR(irradiance_photopic)
|
||||
SUB_SENSOR(irradiance_par)
|
||||
SUB_SENSOR(ppfd)
|
||||
SUB_SENSOR(color_temperature)
|
||||
|
||||
#ifdef USE_AS734X_RGB
|
||||
void set_rgb_hex_sensor(text_sensor::TextSensor *sensor) { this->rgb_hex_sensor_ = sensor; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool select_bank_(uint8_t reg);
|
||||
bool update_bits_(uint8_t reg, uint8_t mask, bool value);
|
||||
bool write_exposure_();
|
||||
bool start_step_();
|
||||
void read_step_();
|
||||
void abort_measurement_(const LogString *reason);
|
||||
void calculate_basic_counts_();
|
||||
void publish_light_metrics_();
|
||||
void publish_color_();
|
||||
void publish_measurement_();
|
||||
void publish_basic_counts_();
|
||||
float normalization_divisor_() const;
|
||||
|
||||
std::array<sensor::Sensor *, NUM_BANDS> sensors_{};
|
||||
std::array<sensor::Sensor *, NUM_BANDS> basic_counts_sensors_{};
|
||||
std::array<float, NUM_BANDS> dark_current_{};
|
||||
std::array<float, NUM_BANDS> channel_correction_;
|
||||
std::array<float, NUM_BANDS> basic_counts_{};
|
||||
std::array<uint16_t, NUM_BANDS> raw_counts_{};
|
||||
const ChipInfo *chip_; // in flash, read field by field with progmem_memcpy
|
||||
uint32_t start_ms_{0};
|
||||
uint32_t integration_ms_{0};
|
||||
uint32_t timeout_ms_{0};
|
||||
float glass_attenuation_factor_{1.0f};
|
||||
float max_basic_count_{0.0f}; // largest of every channel
|
||||
float max_band_basic_count_{0.0f}; // largest of the visible bands only
|
||||
float clear_basic_count_{0.0f}; // the wideband clear channel
|
||||
uint16_t astep_{0};
|
||||
uint16_t peak_raw_count_{0}; // highest ADC value of this measurement, before the clear channels are averaged
|
||||
// What the running measurement was started with, so the maths matches the data even if the
|
||||
// configuration is changed while a measurement is in flight.
|
||||
uint16_t reading_astep_{0};
|
||||
Gain reading_gain_{GAIN_1X};
|
||||
uint8_t reading_atime_{0};
|
||||
enum class State : uint8_t { STATE_IDLE, STATE_WAIT_SMUX, STATE_READ_DATA } state_{State::STATE_IDLE};
|
||||
Normalization normalization_{Normalization::NORMALIZATION_NONE};
|
||||
Gain gain_{GAIN_1X};
|
||||
uint8_t atime_{0};
|
||||
uint8_t step_{0};
|
||||
bool bank_low_{true}; // unknown until setup() writes CFG0
|
||||
#ifdef USE_AS734X_RGB
|
||||
text_sensor::TextSensor *rgb_hex_sensor_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::as734x
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "color_helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::as734x {
|
||||
|
||||
namespace {
|
||||
|
||||
// Range McCamy's approximation is meaningful over.
|
||||
constexpr float CCT_MIN_K = 1000.0f;
|
||||
constexpr float CCT_MAX_K = 25000.0f;
|
||||
|
||||
#ifdef USE_AS734X_RGB
|
||||
// sRGB transfer function.
|
||||
float gamma_correct(float channel) {
|
||||
if (channel <= 0.0031308f) {
|
||||
return 12.92f * channel;
|
||||
}
|
||||
return 1.055f * std::pow(channel, 1.0f / 2.4f) - 0.055f;
|
||||
}
|
||||
|
||||
uint8_t to_byte(float channel) { return static_cast<uint8_t>(std::clamp(channel, 0.0f, 1.0f) * 255.0f); }
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
float tristimulus_to_cct(float x, float y, float z) {
|
||||
const float sum = x + y + z;
|
||||
// The conversion matrices carry negative coefficients, so a spectrum unlike the one they were
|
||||
// calibrated against can drive the sum to zero or below. Chromaticity is undefined there, and
|
||||
// McCamy's formula would still return a plausible looking number from it.
|
||||
if (sum <= MIN_TRISTIMULUS_SUM) {
|
||||
return NAN;
|
||||
}
|
||||
const float chroma_x = x / sum;
|
||||
const float chroma_y = y / sum;
|
||||
|
||||
// McCamy's approximation: cct = 437 n^3 + 3601 n^2 + 6861 n + 5517. The pole at
|
||||
// chroma_y = 0.1858 sends the cubic off to either infinity, and it stays meaningless well
|
||||
// either side of that, so the answer is only reported where the approximation holds.
|
||||
const float denominator = 0.1858f - chroma_y;
|
||||
if (std::abs(denominator) < 1e-4f) {
|
||||
return NAN;
|
||||
}
|
||||
const float n = (chroma_x - 0.3320f) / denominator;
|
||||
const float cct = ((437.0f * n + 3601.0f) * n + 6861.0f) * n + 5517.0f;
|
||||
return (cct >= CCT_MIN_K && cct <= CCT_MAX_K) ? cct : NAN;
|
||||
}
|
||||
|
||||
#ifdef USE_AS734X_RGB
|
||||
void tristimulus_to_rgb(float x, float y, float z, uint8_t &r, uint8_t &g, uint8_t &b) {
|
||||
const float r_linear = 3.2406f * x - 1.5372f * y - 0.4986f * z;
|
||||
const float g_linear = -0.9689f * x + 1.8758f * y + 0.0415f * z;
|
||||
const float b_linear = 0.0557f * x - 0.2040f * y + 1.0570f * z;
|
||||
|
||||
r = to_byte(gamma_correct(r_linear));
|
||||
g = to_byte(gamma_correct(g_linear));
|
||||
b = to_byte(gamma_correct(b_linear));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::as734x
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome::as734x {
|
||||
|
||||
// Smallest tristimulus sum that still carries a usable hue.
|
||||
constexpr float MIN_TRISTIMULUS_SUM = 0.001f;
|
||||
|
||||
// Correlated colour temperature in kelvin from CIE 1931 tristimulus values, using McCamy's
|
||||
// approximation. Returns NAN when the input carries no usable hue, or when the result falls
|
||||
// outside the range the approximation holds over.
|
||||
float tristimulus_to_cct(float x, float y, float z);
|
||||
|
||||
#ifdef USE_AS734X_RGB
|
||||
// sRGB components for CIE 1931 tristimulus values. Pass chromaticity (x + y + z == 1) rather than
|
||||
// absolute values, otherwise anything but dim light clamps to white.
|
||||
void tristimulus_to_rgb(float x, float y, float z, uint8_t &r, uint8_t &g, uint8_t &b);
|
||||
#endif
|
||||
|
||||
} // namespace esphome::as734x
|
||||
@@ -0,0 +1,144 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_COLOR_TEMPERATURE,
|
||||
CONF_COUNTS,
|
||||
CONF_ILLUMINANCE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_IRRADIANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_KELVIN,
|
||||
UNIT_LUX,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BANDS_41, BANDS_43, CONF_AS734X_ID, DOMAIN, AS734XComponent
|
||||
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
|
||||
CONF_BASIC_COUNTS = "basic_counts"
|
||||
CONF_IRRADIANCE_PAR = "irradiance_par"
|
||||
CONF_IRRADIANCE_PHOTOPIC = "irradiance_photopic"
|
||||
CONF_PPFD = "ppfd"
|
||||
CONF_SATURATION_LEVEL = "saturation_level"
|
||||
|
||||
UNIT_IRRADIANCE = "W/m\u00b2"
|
||||
UNIT_PPFD = "\u00b5mol/m\u00b2/s"
|
||||
|
||||
ICON_COUNTS = "mdi:counter"
|
||||
ICON_SATURATION = "mdi:weather-sunny-alert"
|
||||
ICON_ILLUMINANCE = "mdi:weather-sunny"
|
||||
ICON_IRRADIANCE_PHOTOPIC = "mdi:sun-wireless-outline"
|
||||
ICON_PAR = "mdi:sprout"
|
||||
ICON_PPFD = "mdi:sprout-outline"
|
||||
ICON_COLOR_TEMPERATURE = "mdi:sun-thermometer-outline"
|
||||
|
||||
_COUNTS_SENSOR = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
icon=ICON_COUNTS,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
# Basic counts are fractional, so they need decimals where raw counts do not.
|
||||
_BASIC_COUNTS_SENSOR = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
icon=ICON_COUNTS,
|
||||
accuracy_decimals=6,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
_BAND_KEYS = (CONF_COUNTS, CONF_BASIC_COUNTS)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_AS734X_ID): cv.use_id(AS734XComponent),
|
||||
cv.Optional(CONF_COUNTS, default={}): cv.Schema(
|
||||
{cv.Optional(band): _COUNTS_SENSOR for band in BANDS_43}
|
||||
),
|
||||
cv.Optional(CONF_BASIC_COUNTS, default={}): cv.Schema(
|
||||
{cv.Optional(band): _BASIC_COUNTS_SENSOR for band in BANDS_43}
|
||||
),
|
||||
cv.Optional(CONF_SATURATION_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_SATURATION,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LUX,
|
||||
icon=ICON_ILLUMINANCE,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_IRRADIANCE_PHOTOPIC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_IRRADIANCE,
|
||||
icon=ICON_IRRADIANCE_PHOTOPIC,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_IRRADIANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_IRRADIANCE_PAR): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_IRRADIANCE,
|
||||
icon=ICON_PAR,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_IRRADIANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PPFD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PPFD,
|
||||
icon=ICON_PPFD,
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_COLOR_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KELVIN,
|
||||
icon=ICON_COLOR_TEMPERATURE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
full = fv.full_config.get()
|
||||
hub = full.get_config_for_path(full.get_path_for_id(config[CONF_AS734X_ID])[:-1])
|
||||
if hub[CONF_TYPE] != "AS7341":
|
||||
return config
|
||||
for key in _BAND_KEYS:
|
||||
if extra := [band for band in config.get(key, {}) if band not in BANDS_41]:
|
||||
raise cv.Invalid(
|
||||
f"The AS7341 has no {', '.join(extra)} band", path=[key, extra[0]]
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_AS734X_ID])
|
||||
counts = config[CONF_COUNTS]
|
||||
basic_counts = config[CONF_BASIC_COUNTS]
|
||||
for i, band in enumerate(BANDS_43):
|
||||
if (conf := counts.get(band)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(hub.set_counts_sensor(sens, i))
|
||||
if (conf := basic_counts.get(band)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(hub.set_basic_counts_sensor(sens, i))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_SATURATION_LEVEL, hub.set_saturation_level_sensor)
|
||||
await sensors(CONF_ILLUMINANCE, hub.set_illuminance_sensor)
|
||||
await sensors(CONF_IRRADIANCE_PHOTOPIC, hub.set_irradiance_photopic_sensor)
|
||||
await sensors(CONF_IRRADIANCE_PAR, hub.set_irradiance_par_sensor)
|
||||
await sensors(CONF_PPFD, hub.set_ppfd_sensor)
|
||||
await sensors(CONF_COLOR_TEMPERATURE, hub.set_color_temperature_sensor)
|
||||
@@ -0,0 +1,28 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_AS734X_ID, DOMAIN, AS734XComponent
|
||||
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
|
||||
CONF_RGB_HEX = "rgb_hex"
|
||||
|
||||
ICON_PALETTE = "mdi:palette"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_AS734X_ID): cv.use_id(AS734XComponent),
|
||||
cv.Optional(CONF_RGB_HEX): text_sensor.text_sensor_schema(icon=ICON_PALETTE),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if (conf := config.get(CONF_RGB_HEX)) is None:
|
||||
return
|
||||
hub = await cg.get_variable(config[CONF_AS734X_ID])
|
||||
cg.add_define("USE_AS734X_RGB")
|
||||
sens = await text_sensor.new_text_sensor(conf)
|
||||
cg.add(hub.set_rgb_hex_sensor(sens))
|
||||
@@ -52,7 +52,7 @@ void ATM90E26Component::setup() {
|
||||
uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA);
|
||||
if (read != 0x0030) {
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
// TODO: 100 * <nominal voltage, e.g. 230> * sqrt(2) * <fraction of nominal, e.g. 0.9> / (4 * gain_voltage/32768)
|
||||
@@ -120,21 +120,18 @@ void ATM90E26Component::setup() {
|
||||
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X",
|
||||
this->read16_(ATM90E26_REGISTER_CS1));
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Checksum mismatch"));
|
||||
}
|
||||
if (sys_status & 0x3000) { // Checksum 2 Error
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X",
|
||||
this->read16_(ATM90E26_REGISTER_CS2));
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Checksum mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E26Component::dump_config() {
|
||||
ESP_LOGCONFIG("", "ATM90E26:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage A", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current A", this->current_sensor_);
|
||||
|
||||
@@ -168,7 +168,7 @@ void ATM90E32Component::setup() {
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
|
||||
if (!this->validate_spi_read_(0x55AA, "setup()")) {
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,9 +411,6 @@ void ATM90E32Component::log_calibration_status_() {
|
||||
void ATM90E32Component::dump_config() {
|
||||
ESP_LOGCONFIG("", "ATM90E32:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage A", this->phase_[PHASEA].voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_);
|
||||
|
||||
@@ -188,7 +188,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
|
||||
// Expose the next chunk of file data. Every decoder buffers internally and consumes only what it
|
||||
// processed, so the source does not need to accumulate or stitch chunks across fill() calls.
|
||||
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
||||
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
|
||||
|
||||
const size_t available_before_decode = this->input_buffer_->available();
|
||||
|
||||
|
||||
@@ -129,9 +129,8 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
|
||||
delay(READ_WRITE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
// Expose a chunk of the ring buffer's internal storage. pre_shift is ignored by RingBufferAudioSource
|
||||
// (there is no intermediate transfer buffer to compact).
|
||||
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
||||
// Expose a chunk of the ring buffer's internal storage.
|
||||
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
|
||||
|
||||
if (this->audio_source_->available() == 0) {
|
||||
// No samples available to process
|
||||
|
||||
@@ -20,16 +20,6 @@ std::unique_ptr<AudioSinkTransferBuffer> AudioSinkTransferBuffer::create(size_t
|
||||
return sink_buffer;
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
|
||||
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
|
||||
|
||||
if (!source_buffer->allocate_buffer_(buffer_size)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return source_buffer;
|
||||
}
|
||||
|
||||
size_t AudioTransferBuffer::free() const {
|
||||
if (this->buffer_size_ == 0) {
|
||||
return 0;
|
||||
@@ -132,27 +122,6 @@ void AudioTransferBuffer::deallocate_buffer_() {
|
||||
this->buffer_length_ = 0;
|
||||
}
|
||||
|
||||
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
|
||||
if (pre_shift) {
|
||||
// Shift data in buffer to start
|
||||
if (this->buffer_length_ > 0) {
|
||||
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
||||
}
|
||||
this->data_start_ = this->buffer_;
|
||||
}
|
||||
|
||||
size_t bytes_to_read = AudioTransferBuffer::free();
|
||||
size_t bytes_read = 0;
|
||||
if (bytes_to_read > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
|
||||
}
|
||||
|
||||
this->increase_buffer_length(bytes_read);
|
||||
}
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
|
||||
size_t bytes_written = 0;
|
||||
if (this->available()) {
|
||||
@@ -192,9 +161,44 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
|
||||
return (this->available() > 0);
|
||||
}
|
||||
|
||||
// AudioSourceTransferBuffer is deprecated. Remove before 2027.5.0.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
|
||||
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
|
||||
|
||||
if (!source_buffer->allocate_buffer_(buffer_size)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return source_buffer;
|
||||
}
|
||||
|
||||
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
|
||||
if (pre_shift) {
|
||||
// Shift data in buffer to start
|
||||
if (this->buffer_length_ > 0) {
|
||||
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
|
||||
}
|
||||
this->data_start_ = this->buffer_;
|
||||
}
|
||||
|
||||
size_t bytes_to_read = AudioTransferBuffer::free();
|
||||
size_t bytes_read = 0;
|
||||
if (bytes_to_read > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
|
||||
}
|
||||
|
||||
this->increase_buffer_length(bytes_read);
|
||||
}
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); }
|
||||
|
||||
bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); }
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) {
|
||||
this->data_start_ = data;
|
||||
@@ -268,6 +272,23 @@ void RingBufferAudioSource::clear_buffered_data() {
|
||||
this->ring_buffer_->reset();
|
||||
}
|
||||
|
||||
size_t RingBufferAudioSource::expose_padded_partial(size_t frame_bytes) {
|
||||
if (this->current_available_ > 0 || this->queued_length_ > 0) {
|
||||
return 0;
|
||||
}
|
||||
// Moves any trailing bytes still inside a held item into the splice buffer
|
||||
this->release_item_();
|
||||
const size_t real_bytes = this->splice_length_;
|
||||
if (real_bytes == 0 || frame_bytes == 0 || real_bytes % frame_bytes != 0) {
|
||||
return 0;
|
||||
}
|
||||
std::memset(this->splice_buffer_ + real_bytes, 0, this->alignment_bytes_ - real_bytes);
|
||||
this->splice_length_ = 0;
|
||||
this->current_data_ = this->splice_buffer_;
|
||||
this->current_available_ = this->alignment_bytes_;
|
||||
return real_bytes;
|
||||
}
|
||||
|
||||
bool RingBufferAudioSource::has_buffered_data() const {
|
||||
// splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
|
||||
// bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.
|
||||
@@ -276,7 +297,7 @@ bool RingBufferAudioSource::has_buffered_data() const {
|
||||
return (this->current_available_ > 0) || (this->queued_length_ > 0) || (this->ring_buffer_->available() > 0);
|
||||
}
|
||||
|
||||
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait, bool /*pre_shift*/) {
|
||||
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait) {
|
||||
if (this->current_available_ > 0) {
|
||||
// Caller has not finished consuming the current exposure
|
||||
return 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/components/ring_buffer/ring_buffer.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h" // for ESPDEPRECATED
|
||||
|
||||
#ifdef USE_SPEAKER
|
||||
#include "esphome/components/speaker/speaker.h"
|
||||
@@ -152,13 +153,20 @@ class AudioReadableBuffer {
|
||||
|
||||
/// @brief Refills the buffer from its source. No-op by default for read-only buffers.
|
||||
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for data
|
||||
/// @param pre_shift If true, shifts existing data to the start of the buffer before reading
|
||||
/// @return Number of bytes read
|
||||
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; }
|
||||
size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); }
|
||||
virtual size_t fill(TickType_t ticks_to_wait) { return 0; }
|
||||
|
||||
// Remove before 2027.5.0
|
||||
ESPDEPRECATED("Use fill(ticks_to_wait) instead. Removed in 2027.5.0.", "2026.11.0")
|
||||
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return this->fill(ticks_to_wait); }
|
||||
};
|
||||
|
||||
class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer {
|
||||
// Remove before 2027.5.0
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
class ESPDEPRECATED("Use RingBufferAudioSource instead. Removed in 2027.5.0.", "2026.11.0") AudioSourceTransferBuffer
|
||||
: public AudioTransferBuffer,
|
||||
public AudioReadableBuffer {
|
||||
/*
|
||||
* @brief A class that implements a transfer buffer for audio sources.
|
||||
* Supports reading audio data from a ring buffer into the transfer buffer for processing.
|
||||
@@ -189,10 +197,12 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadab
|
||||
size_t free() const override;
|
||||
void consume(size_t bytes) override { this->decrease_buffer_length(bytes); }
|
||||
bool has_buffered_data() const override;
|
||||
size_t fill(TickType_t ticks_to_wait) override { return this->transfer_data_from_source(ticks_to_wait); }
|
||||
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override {
|
||||
return this->transfer_data_from_source(ticks_to_wait, pre_shift);
|
||||
}
|
||||
};
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory).
|
||||
/// Does not allocate memory or transfer data from external sources.
|
||||
@@ -246,14 +256,21 @@ class RingBufferAudioSource : public AudioReadableBuffer {
|
||||
size_t available() const override { return this->current_available_; }
|
||||
void consume(size_t bytes) override;
|
||||
bool has_buffered_data() const override;
|
||||
/// pre_shift is ignored: there is no intermediate transfer buffer to compact, so an unconsumed
|
||||
/// exposure stays in place and fill() returns 0 until it is fully consumed.
|
||||
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override;
|
||||
/// An unconsumed exposure stays in place and fill() returns 0 until it is fully consumed.
|
||||
size_t fill(TickType_t ticks_to_wait) override;
|
||||
// Keeps the deprecated fill(ticks_to_wait, pre_shift) overload visible. Remove before 2027.5.0.
|
||||
using AudioReadableBuffer::fill;
|
||||
|
||||
/// @brief Discards all buffered audio: releases any held ring buffer item, clears the source's in-flight
|
||||
/// state, and resets the underlying ring buffer. Must be invoked from the ring buffer's consumer thread.
|
||||
void clear_buffered_data();
|
||||
|
||||
/// @brief Zero-pads held whole frames short of an alignment unit and exposes them, so a stream's tail is not
|
||||
/// stranded. A held partial frame is left to wait for its remaining bytes.
|
||||
/// @param frame_bytes Bytes per audio frame
|
||||
/// @return Real (unpadded) bytes exposed, or 0 if nothing is exposable
|
||||
size_t expose_padded_partial(size_t frame_bytes);
|
||||
|
||||
/// @brief Returns a mutable pointer to the currently exposed audio data.
|
||||
/// The pointer may reference the ring buffer's internal storage or, when exposing a stitched frame
|
||||
/// across a wrap boundary, an internal splice buffer. In either case mutations are safe but data
|
||||
|
||||
@@ -65,7 +65,7 @@ MTreg:
|
||||
void BH1750Sensor::setup() {
|
||||
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
|
||||
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->state_ = IDLE;
|
||||
@@ -74,9 +74,6 @@ void BH1750Sensor::setup() {
|
||||
void BH1750Sensor::dump_config() {
|
||||
LOG_SENSOR("", "BH1750", this);
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL_FOR, this->get_name().c_str());
|
||||
}
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ void BluedroidGattClient::loop() {
|
||||
auto ret = esp_ble_gattc_app_register(this->app_id);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
|
||||
this->mark_failed();
|
||||
this->mark_failed(
|
||||
LOG_STR("Registration failed; if the error was ESP_GATT_NO_RESOURCES, reduce the connection slots"));
|
||||
}
|
||||
// Do not wait for REG_EVT; connect() rejects until it lands.
|
||||
this->set_idle_();
|
||||
@@ -108,12 +109,7 @@ void BluedroidGattClient::ble_before_disabled_event_handler() {
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void BluedroidGattClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " Registration failed; if the error was ESP_GATT_NO_RESOURCES, reduce the connection slots");
|
||||
}
|
||||
}
|
||||
void BluedroidGattClient::dump_config() { ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_); }
|
||||
|
||||
// ---- contract ops ----
|
||||
|
||||
@@ -680,7 +676,8 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
this->gattc_if_ = esp_gattc_if;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[%d] gattc app registration failed, status=%d", this->connection_index_, param->reg.status);
|
||||
this->mark_failed();
|
||||
this->mark_failed(
|
||||
LOG_STR("Registration failed; if the error was ESP_GATT_NO_RESOURCES, reduce the connection slots"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -76,16 +76,14 @@ void RP2GattClient::setup() {
|
||||
// Pre-create every pool entry so the packet handlers' allocate() calls are
|
||||
// always a free-list pop -- the IRQ path must never reach malloc().
|
||||
if (!this->event_pool_.warm() || !this->notify_pool_.warm()) {
|
||||
ESP_LOGE(TAG, "GATT event pool warm-up failed");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("GATT event pool warm-up failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Register this engine for IRQ-context event routing.
|
||||
if (instance_count >= ESPHOME_BLE_GATT_CLIENT_COUNT) {
|
||||
// Cannot happen with codegen-sized storage; refuse loudly if it ever does.
|
||||
ESP_LOGE(TAG, "GATT client registry full");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("GATT client registry full"));
|
||||
return;
|
||||
}
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60;
|
||||
|
||||
void BM8563::setup() {
|
||||
if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,6 @@ void BM8563::update() { this->read_time(); }
|
||||
void BM8563::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BM8563:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::start_timer(uint32_t duration_s) {
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include <esphome/components/sensor/sensor.h>
|
||||
#include <esphome/core/component.h>
|
||||
|
||||
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response"
|
||||
|
||||
namespace esphome::bme280_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bme280.sensor");
|
||||
@@ -100,13 +98,11 @@ void BME280Component::setup() {
|
||||
}
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x60) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID));
|
||||
this->mark_failed(LOG_STR("Wrong chip ID or no response"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,17 +178,6 @@ void BME280Component::setup() {
|
||||
}
|
||||
void BME280Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BME280:");
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_CHIP_ID:
|
||||
ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
|
||||
@@ -101,11 +101,6 @@ class BME280Component : public PollingComponent {
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
WRONG_CHIP_ID,
|
||||
} error_code_{NONE};
|
||||
};
|
||||
|
||||
} // namespace esphome::bme280_base
|
||||
|
||||
@@ -72,22 +72,24 @@ constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.
|
||||
}
|
||||
|
||||
void BME680Component::setup() {
|
||||
if (!this->setup_registers_())
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
|
||||
bool BME680Component::setup_registers_() {
|
||||
uint8_t chip_id;
|
||||
if (!this->read_byte(BME680_REGISTER_CHIPID, &chip_id) || chip_id != 0x61) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read calibration
|
||||
uint8_t coeff1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
uint8_t coeff2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
|
||||
@@ -119,20 +121,17 @@ void BME680Component::setup() {
|
||||
|
||||
uint8_t temp_var = 0;
|
||||
if (!this->read_byte(0x02, &temp_var)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this->calibration_.res_heat_range = ((temp_var & 0x30) / 16);
|
||||
|
||||
if (!this->read_byte(0x00, &temp_var)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this->calibration_.res_heat_val = (int8_t) temp_var;
|
||||
|
||||
if (!this->read_byte(0x04, &temp_var)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this->calibration_.range_sw_err = ((int8_t) temp_var & (int8_t) 0xf0) / 16;
|
||||
|
||||
@@ -141,41 +140,35 @@ void BME680Component::setup() {
|
||||
// Config register
|
||||
uint8_t config_register;
|
||||
if (!this->read_byte(BME680_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
config_register &= ~0b00011100;
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->write_byte(BME680_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Humidity control register
|
||||
uint8_t hum_control;
|
||||
if (!this->read_byte(BME680_REGISTER_CONTROL_HUMIDITY, &hum_control)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
hum_control &= ~0b00000111;
|
||||
hum_control |= this->humidity_oversampling_ & 0b111;
|
||||
if (!this->write_byte(BME680_REGISTER_CONTROL_HUMIDITY, hum_control)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gas 1 control register
|
||||
uint8_t gas1_control;
|
||||
if (!this->read_byte(BME680_REGISTER_CONTROL_GAS1, &gas1_control)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
gas1_control &= ~0b00011111;
|
||||
gas1_control |= 1 << 4;
|
||||
gas1_control |= 0; // profile 0
|
||||
if (!this->write_byte(BME680_REGISTER_CONTROL_GAS1, gas1_control)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool heat_off = this->heater_temperature_ == 0 || this->heater_duration_ == 0;
|
||||
@@ -183,40 +176,34 @@ void BME680Component::setup() {
|
||||
// Gas 0 control register
|
||||
uint8_t gas0_control;
|
||||
if (!this->read_byte(BME680_REGISTER_CONTROL_GAS0, &gas0_control)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
gas0_control &= ~0b00001000;
|
||||
gas0_control |= heat_off << 3;
|
||||
if (!this->write_byte(BME680_REGISTER_CONTROL_GAS0, gas0_control)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!heat_off) {
|
||||
// Gas Heater Temperature
|
||||
uint8_t temperature = this->calc_heater_resistance_(this->heater_temperature_);
|
||||
if (!this->write_byte(BME680_REGISTER_HEATER_HEAT0, temperature)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gas Heater Duration
|
||||
uint8_t duration = this->calc_heater_duration_(this->heater_duration_);
|
||||
|
||||
if (!this->write_byte(BME680_REGISTER_HEATER_WAIT0, duration)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BME680Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BME680:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
/// Configure the sensor registers; false on an I2C failure.
|
||||
bool setup_registers_();
|
||||
/// Calculate the heater resistance value to send to the BME680 register.
|
||||
uint8_t calc_heater_resistance_(uint16_t temperature);
|
||||
/// Calculate the heater duration value to send to the BME680 register.
|
||||
|
||||
@@ -117,18 +117,21 @@ const uint8_t BMI160_REGISTER_DATA_TEMP_MSB = 0x21;
|
||||
const float GRAVITY_EARTH = 9.80665f;
|
||||
|
||||
void BMI160Component::internal_setup_(int stage) {
|
||||
if (!this->setup_stage_(stage))
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
|
||||
bool BMI160Component::setup_stage_(int stage) {
|
||||
switch (stage) {
|
||||
case 0:
|
||||
uint8_t chipid;
|
||||
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, " Bringing accelerometer out of sleep");
|
||||
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::ACCL_SET_PMU_MODE | (uint8_t) AcclPmuMode::NORMAL)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, " Waiting for accelerometer to wake up");
|
||||
// need to wait (max delay in datasheet) because we can't send commands while another is in progress
|
||||
@@ -139,8 +142,7 @@ void BMI160Component::internal_setup_(int stage) {
|
||||
case 1:
|
||||
ESP_LOGV(TAG, " Bringing gyroscope out of sleep");
|
||||
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, " Waiting for gyroscope to wake up");
|
||||
// wait between 51 & 81ms, doing 100 to be safe
|
||||
@@ -152,15 +154,13 @@ void BMI160Component::internal_setup_(int stage) {
|
||||
uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25;
|
||||
ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config));
|
||||
if (!this->write_byte(BMI160_REGISTER_GYRO_CONFIG, gyro_config)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, " Setting up Gyro Range");
|
||||
uint8_t gyro_range = (uint8_t) GyroRange::RANGE_2000_DPS;
|
||||
ESP_LOGV(TAG, " Output gyro_range: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_range));
|
||||
if (!this->write_byte(BMI160_REGISTER_GYRO_RANGE, gyro_range)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, " Setting up Accel Config");
|
||||
@@ -168,28 +168,24 @@ void BMI160Component::internal_setup_(int stage) {
|
||||
(uint8_t) AcclFilterMode::PERF | (uint8_t) AcclBandwidth::RES_AVG16 | (uint8_t) AccelOutputDataRate::HZ_25;
|
||||
ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config));
|
||||
if (!this->write_byte(BMI160_REGISTER_ACCEL_CONFIG, accel_config)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, " Setting up Accel Range");
|
||||
uint8_t accel_range = (uint8_t) AccelRange::RANGE_16G;
|
||||
ESP_LOGV(TAG, " Output accel_range: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_range));
|
||||
if (!this->write_byte(BMI160_REGISTER_ACCEL_RANGE, accel_range)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this->setup_complete_ = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BMI160Component::setup() { this->internal_setup_(0); }
|
||||
void BMI160Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMI160:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Acceleration X", this->accel_x_sensor_);
|
||||
LOG_SENSOR(" ", "Acceleration Y", this->accel_y_sensor_);
|
||||
|
||||
@@ -31,6 +31,8 @@ class BMI160Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
sensor::Sensor *gyro_z_sensor_{nullptr};
|
||||
|
||||
void internal_setup_(int stage);
|
||||
/// Run one setup stage; false on an I2C failure.
|
||||
bool setup_stage_(int stage);
|
||||
bool setup_complete_{false};
|
||||
|
||||
/** reads `len` 16-bit little-endian integers from the given i2c register */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "bmi270_config.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
@@ -11,6 +12,9 @@ using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device defi
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmi270");
|
||||
|
||||
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±4g", "±8g", "±16g");
|
||||
PROGMEM_STRING_TABLE(GyroRangeStrings, "±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s");
|
||||
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
static constexpr size_t MAX_I2C_BUFFER_SIZE = 32;
|
||||
#else
|
||||
@@ -76,28 +80,26 @@ void BMI270Component::setup() {
|
||||
// 1. Verify chip ID
|
||||
uint8_t chip_id = 0;
|
||||
if (!this->read_byte(BMI270_REG_CHIP_ID, &chip_id)) {
|
||||
ESP_LOGE(TAG, "Failed to read chip ID – check wiring / address");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != BMI270_CHIP_ID_VALUE) {
|
||||
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", chip_id, BMI270_CHIP_ID_VALUE);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
|
||||
|
||||
// 2. Soft-reset via CMD register (0x7E = 0xB6)
|
||||
if (!this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
delay(20);
|
||||
|
||||
// 4. Upload the configuration blob
|
||||
if (!load_config_file_()) {
|
||||
ESP_LOGE(TAG, "Config file upload failed");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Config file upload failed"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Config blob uploaded ✓");
|
||||
@@ -107,11 +109,11 @@ void BMI270Component::setup() {
|
||||
uint8_t acc_conf =
|
||||
(uint8_t) (accel_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) | BMI270_ACC_CONF_PERF_MODE;
|
||||
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,18 +122,18 @@ void BMI270Component::setup() {
|
||||
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) |
|
||||
BMI270_GYR_CONF_NOISE_PERF | BMI270_GYR_CONF_FILTER_PERF;
|
||||
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. Enable accelerometer, gyroscope, and temperature sensor
|
||||
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
|
||||
if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
delay(5);
|
||||
@@ -139,7 +141,7 @@ void BMI270Component::setup() {
|
||||
// 8. Re-enable advanced power save (optional; keeps current low between reads)
|
||||
// Disabled here for simplicity – leave in performance mode
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)) { // bit1 = fifo_self_wakeup
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -268,16 +270,13 @@ bool BMI270Component::setup_magnetometer_() {
|
||||
void BMI270Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMI270 IMU:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " Communication failed!");
|
||||
if (this->is_failed())
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
|
||||
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
|
||||
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
|
||||
ESP_LOGCONFIG(TAG, " Accel range : %s",
|
||||
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)));
|
||||
ESP_LOGCONFIG(TAG, " Gyro range : %s",
|
||||
LOG_STR_ARG(GyroRangeStrings::get_log_str(this->gyro_range_, GyroRangeStrings::LAST_INDEX)));
|
||||
#ifdef USE_BMI270_BMM150
|
||||
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
|
||||
ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)",
|
||||
@@ -315,7 +314,8 @@ bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
}
|
||||
// Scale factor: LSB/g depends on range
|
||||
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
|
||||
static constexpr float ACCEL_SCALE[] = {
|
||||
// In PROGMEM; each read is a 32-bit load, which is safe from flash on ESP8266
|
||||
alignas(4) static constexpr float ACCEL_SCALE[] PROGMEM = {
|
||||
2.0f / 32768.0f,
|
||||
4.0f / 32768.0f,
|
||||
8.0f / 32768.0f,
|
||||
@@ -329,7 +329,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
|
||||
// Gyroscope: registers 0x12–0x17 (6 bytes)
|
||||
// Scale: full-scale range / 2^15
|
||||
static constexpr float GYRO_SCALE[] = {
|
||||
alignas(4) static constexpr float GYRO_SCALE[] PROGMEM = {
|
||||
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
|
||||
};
|
||||
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
|
||||
|
||||
@@ -24,7 +24,7 @@ void BMP085Component::update() {
|
||||
void BMP085Component::setup() {
|
||||
uint8_t data[22];
|
||||
if (!this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,9 +44,6 @@ void BMP085Component::setup() {
|
||||
void BMP085Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMP085:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with BMP085 failed!");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response"
|
||||
|
||||
namespace esphome::bmp280_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bmp280.sensor");
|
||||
@@ -65,18 +63,15 @@ void BMP280Component::setup() {
|
||||
// Read the chip id twice, to work around a bug where the first read is 0.
|
||||
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x58) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID));
|
||||
this->mark_failed(LOG_STR("Wrong chip ID or no response"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,17 +125,6 @@ void BMP280Component::setup() {
|
||||
}
|
||||
void BMP280Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMP280:");
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_CHIP_ID:
|
||||
ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
|
||||
@@ -85,11 +85,6 @@ class BMP280Component : public PollingComponent {
|
||||
BMP280IIRFilter iir_filter_{BMP280_IIR_FILTER_OFF};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
WRONG_CHIP_ID,
|
||||
} error_code_{NONE};
|
||||
};
|
||||
|
||||
} // namespace esphome::bmp280_base
|
||||
|
||||
@@ -43,35 +43,27 @@ static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
}
|
||||
|
||||
void BMP3XXComponent::setup() {
|
||||
this->error_code_ = NONE;
|
||||
// Call the Device base class "initialise" function
|
||||
if (!reset()) {
|
||||
ESP_LOGE(TAG, "Failed to reset");
|
||||
this->error_code_ = ERROR_SENSOR_RESET;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to reset"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) {
|
||||
ESP_LOGE(TAG, "Can't read chip id");
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg);
|
||||
|
||||
if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) {
|
||||
ESP_LOGE(TAG, "Unknown chip id - is this really a BMP388 or BMP390?");
|
||||
this->error_code_ = ERROR_WRONG_CHIP_ID;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Wrong chip ID - please check if you are really using a BMP 388 or BMP 390"));
|
||||
return;
|
||||
}
|
||||
// set sensor in sleep mode
|
||||
stop_conversion();
|
||||
// Read the calibration parameters into the params structure
|
||||
if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) {
|
||||
ESP_LOGE(TAG, "Can't read calibration data");
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Can't read calibration data"));
|
||||
return;
|
||||
}
|
||||
compensation_float_params_.param_T1 =
|
||||
@@ -92,9 +84,7 @@ void BMP3XXComponent::setup() {
|
||||
|
||||
// Initialise the BMP388 IIR filter register
|
||||
if (!set_iir_filter(this->iir_filter_)) {
|
||||
ESP_LOGE(TAG, "Failed to set IIR filter");
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to set IIR filter"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,9 +103,7 @@ void BMP3XXComponent::setup() {
|
||||
}
|
||||
// Initialise the BMP388 oversampling register
|
||||
if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) {
|
||||
ESP_LOGE(TAG, "Failed to set oversampling register");
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to set oversampling register"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -125,23 +113,6 @@ void BMP3XXComponent::dump_config() {
|
||||
"BMP3XX:\n"
|
||||
" Type: %s (0x%X)",
|
||||
LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg);
|
||||
switch (this->error_code_) {
|
||||
case NONE:
|
||||
break;
|
||||
case ERROR_COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case ERROR_WRONG_CHIP_ID:
|
||||
ESP_LOGE(TAG, "Wrong chip ID (reported id: 0x%X) - please check if you are really using a BMP 388 or BMP 390",
|
||||
this->chip_id_.reg);
|
||||
break;
|
||||
case ERROR_SENSOR_RESET:
|
||||
ESP_LOGE(TAG, "Failed to reset");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Error code %d", (int) this->error_code_);
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_)));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->temperature_sensor_) {
|
||||
@@ -165,8 +136,7 @@ void BMP3XXComponent::update() {
|
||||
meas_time += 2.02f * oversampling_to_time(this->pressure_oversampling_) + 0.392f;
|
||||
meas_time += 0.234f;
|
||||
if (!set_mode(FORCED_MODE)) {
|
||||
ESP_LOGE(TAG, "Failed start forced mode");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed start forced mode"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,13 +124,6 @@ class BMP3XXComponent : public PollingComponent {
|
||||
OperationMode operation_mode_{FORCED_MODE};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
ERROR_COMMUNICATION_FAILED,
|
||||
ERROR_WRONG_CHIP_ID,
|
||||
ERROR_SENSOR_STATUS,
|
||||
ERROR_SENSOR_RESET,
|
||||
} error_code_{NONE};
|
||||
|
||||
struct { // The BMP388 compensation trim parameters (coefficients)
|
||||
uint16_t param_T1;
|
||||
|
||||
@@ -38,29 +38,6 @@ static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
void BMP581Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMP581:");
|
||||
|
||||
switch (this->error_code_) {
|
||||
case NONE:
|
||||
break;
|
||||
case ERROR_COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case ERROR_WRONG_CHIP_ID:
|
||||
ESP_LOGE(TAG, "Unknown chip ID");
|
||||
break;
|
||||
case ERROR_SENSOR_RESET:
|
||||
ESP_LOGE(TAG, "Reset failed");
|
||||
break;
|
||||
case ERROR_SENSOR_STATUS:
|
||||
ESP_LOGE(TAG, "Get status failed");
|
||||
break;
|
||||
case ERROR_PRIME_IIR_FAILED:
|
||||
ESP_LOGE(TAG, "IIR Filter failed to prime with initial measurement");
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Error %d", (int) this->error_code_);
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
|
||||
@@ -96,17 +73,13 @@ void BMP581Component::setup() {
|
||||
* 6) Configure and prime IIR Filter(s), if enabled
|
||||
*/
|
||||
|
||||
this->error_code_ = NONE;
|
||||
////////////////////
|
||||
// 1) Soft reboot //
|
||||
////////////////////
|
||||
|
||||
// Power-On-Reboot bit is asserted if sensor successfully reset
|
||||
if (!this->reset_()) {
|
||||
ESP_LOGE(TAG, "Reset failed");
|
||||
|
||||
this->error_code_ = ERROR_SENSOR_RESET;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -119,20 +92,14 @@ void BMP581Component::setup() {
|
||||
|
||||
// read chip id from sensor
|
||||
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
|
||||
ESP_LOGE(TAG, "Read chip ID failed");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Read chip ID failed"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// verify id
|
||||
if (chip_id != BMP581_ASIC_ID && chip_id != BMP585_ASIC_ID) {
|
||||
ESP_LOGE(TAG, "Unknown chip ID");
|
||||
|
||||
this->error_code_ = ERROR_WRONG_CHIP_ID;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Unknown chip ID"));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -142,30 +109,21 @@ void BMP581Component::setup() {
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
|
||||
ESP_LOGE(TAG, "Failed to read status register");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to read status register"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// verify status_nvm_rdy bit (it is asserted if boot was successful)
|
||||
if (!(this->status_.bit.status_nvm_rdy)) {
|
||||
ESP_LOGE(TAG, "NVM not ready");
|
||||
|
||||
this->error_code_ = ERROR_SENSOR_STATUS;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("NVM not ready"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// verify status_nvm_err bit (it is asserted if an error is detected)
|
||||
if (this->status_.bit.status_nvm_err) {
|
||||
ESP_LOGE(TAG, "NVM error detected");
|
||||
|
||||
this->error_code_ = ERROR_SENSOR_STATUS;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("NVM error detected"));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -176,10 +134,7 @@ void BMP581Component::setup() {
|
||||
|
||||
// enable the data ready interrupt source
|
||||
if (!this->write_interrupt_source_settings_(true)) {
|
||||
ESP_LOGE(TAG, "Failed to write interrupt source register");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to write interrupt source register"));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -198,10 +153,7 @@ void BMP581Component::setup() {
|
||||
|
||||
// write oversampling settings
|
||||
if (!this->write_oversampling_settings_(this->temperature_oversampling_, this->pressure_oversampling_)) {
|
||||
ESP_LOGE(TAG, "Failed to write oversampling register");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to write oversampling register"));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -218,19 +170,13 @@ void BMP581Component::setup() {
|
||||
|
||||
if ((this->iir_temperature_level_ != IIR_FILTER_OFF) || (this->iir_pressure_level_ != IIR_FILTER_OFF)) {
|
||||
if (!this->write_iir_settings_(this->iir_temperature_level_, this->iir_pressure_level_)) {
|
||||
ESP_LOGE(TAG, "Failed to write IIR configuration registers");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to write IIR configuration registers"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->prime_iir_filter_()) {
|
||||
ESP_LOGE(TAG, "Failed to prime the IIR filter with an initial measurement");
|
||||
|
||||
this->error_code_ = ERROR_PRIME_IIR_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Failed to prime the IIR filter with an initial measurement"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,15 +133,6 @@ class BMP581Component : public PollingComponent {
|
||||
// Sets the power mode on the BMP581 by writing to the ODR register
|
||||
bool write_power_mode_(OperationMode mode);
|
||||
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
ERROR_COMMUNICATION_FAILED,
|
||||
ERROR_WRONG_CHIP_ID,
|
||||
ERROR_SENSOR_STATUS,
|
||||
ERROR_SENSOR_RESET,
|
||||
ERROR_PRIME_IIR_FAILED
|
||||
} error_code_{NONE};
|
||||
|
||||
// BMP581's interrupt source register (address 0x15) to configure which interrupts are enabled (page 54 of datasheet)
|
||||
union {
|
||||
struct {
|
||||
|
||||
@@ -48,7 +48,7 @@ void BQ27220Component::setup() {
|
||||
}
|
||||
if (device_number != DEVICE_NUMBER) {
|
||||
ESP_LOGE(TAG, "Unexpected device number 0x%04X (expected 0x%04X)", device_number, DEVICE_NUMBER);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Unexpected device number"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,6 @@ void BQ27220Component::update() {
|
||||
void BQ27220Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ27220 Battery Fuel Gauge:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
|
||||
@@ -37,8 +37,7 @@ void CAP1188Component::finish_setup_() {
|
||||
this->read_byte(CAP1188_REVISION, &this->cap1188_revision_);
|
||||
|
||||
if ((this->cap1188_product_id_ != 0x50) || (this->cap1188_manufacture_id_ != 0x5D)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Product ID or Manufacture ID of the connected device does not match a known CAP1188"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,15 +69,6 @@ void CAP1188Component::dump_config() {
|
||||
this->cap1188_product_id_, this->cap1188_manufacture_id_, this->cap1188_revision_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGE(TAG, "Product ID or Manufacture ID of the connected device does not match a known CAP1188.");
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CAP1188Component::loop() {
|
||||
|
||||
@@ -59,11 +59,6 @@ class CAP1188Component final : public Component, public i2c::I2CDevice {
|
||||
uint8_t cap1188_product_id_{0};
|
||||
uint8_t cap1188_manufacture_id_{0};
|
||||
uint8_t cap1188_revision_{0};
|
||||
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
} error_code_{NONE};
|
||||
};
|
||||
|
||||
} // namespace esphome::cap1188
|
||||
|
||||
@@ -80,6 +80,9 @@ void CaptivePortal::setup() {
|
||||
this->disable_loop();
|
||||
}
|
||||
void CaptivePortal::start() {
|
||||
// Each start takes a web server reference that end() returns once
|
||||
if (this->active_)
|
||||
return;
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler_without_auth(this);
|
||||
|
||||
@@ -10,29 +10,28 @@ ESPHOME_LOG_TAG(TAG, "ccs811");
|
||||
// based on
|
||||
// - https://cdn.sparkfun.com/datasheets/BreakoutBoards/CCS811_Programming_Guide.pdf
|
||||
|
||||
#define CHECK_TRUE(f, error_code) \
|
||||
#define CHECK_TRUE(f, message) \
|
||||
if (!(f)) { \
|
||||
this->mark_failed(); \
|
||||
this->error_code_ = (error_code); \
|
||||
this->mark_failed(LOG_STR(message)); \
|
||||
return; \
|
||||
}
|
||||
|
||||
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED)
|
||||
#define CHECKED_IO(f) CHECK_TRUE(f, ESP_LOG_MSG_COMM_FAIL)
|
||||
|
||||
void CCS811Component::setup() {
|
||||
// page 9 programming guide - hwid is always 0x81
|
||||
uint8_t hw_id;
|
||||
CHECKED_IO(this->read_byte(0x20, &hw_id))
|
||||
CHECK_TRUE(hw_id == 0x81, INVALID_ID)
|
||||
CHECK_TRUE(hw_id == 0x81, "Sensor reported an invalid ID. Is this a CCS811?")
|
||||
|
||||
// software reset, page 3 - allowed to fail
|
||||
this->write_bytes(0xFF, {0x11, 0xE5, 0x72, 0x8A});
|
||||
delay(5);
|
||||
|
||||
// page 10, APP_START
|
||||
CHECK_TRUE(!this->status_has_error_(), SENSOR_REPORTED_ERROR)
|
||||
CHECK_TRUE(this->status_app_is_valid_(), APP_INVALID)
|
||||
CHECK_TRUE(this->write_bytes(0xF4, {}), APP_START_FAILED)
|
||||
CHECK_TRUE(!this->status_has_error_(), "Sensor reported internal error")
|
||||
CHECK_TRUE(this->status_app_is_valid_(), "Sensor reported invalid APP installed")
|
||||
CHECK_TRUE(this->write_bytes(0xF4, {}), "Sensor reported APP start failed")
|
||||
// App setup, wait for it to load
|
||||
delay(1);
|
||||
|
||||
@@ -160,29 +159,6 @@ void CCS811Component::dump_config() {
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Baseline: NOT SET");
|
||||
}
|
||||
if (this->is_failed()) {
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case INVALID_ID:
|
||||
ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this a CCS811?");
|
||||
break;
|
||||
case SENSOR_REPORTED_ERROR:
|
||||
ESP_LOGW(TAG, "Sensor reported internal error");
|
||||
break;
|
||||
case APP_INVALID:
|
||||
ESP_LOGW(TAG, "Sensor reported invalid APP installed.");
|
||||
break;
|
||||
case APP_START_FAILED:
|
||||
ESP_LOGW(TAG, "Sensor reported APP start failed.");
|
||||
break;
|
||||
case UNKNOWN:
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown setup error!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ccs811
|
||||
|
||||
@@ -31,15 +31,6 @@ class CCS811Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
bool status_has_data_() { return this->read_status_().value_or(0) & (1 << 3); }
|
||||
void send_env_data_();
|
||||
|
||||
enum ErrorCode {
|
||||
UNKNOWN,
|
||||
COMMUNICATION_FAILED,
|
||||
INVALID_ID,
|
||||
SENSOR_REPORTED_ERROR,
|
||||
APP_INVALID,
|
||||
APP_START_FAILED,
|
||||
} error_code_{UNKNOWN};
|
||||
|
||||
sensor::Sensor *co2_{nullptr};
|
||||
sensor::Sensor *tvoc_{nullptr};
|
||||
text_sensor::TextSensor *version_{nullptr};
|
||||
|
||||
@@ -18,7 +18,7 @@ void CH422GComponent::setup() {
|
||||
// Set mode and check for errors
|
||||
if (!this->set_mode_(this->mode_value_) || !this->read_inputs_()) {
|
||||
ESP_LOGE(TAG, "CH422G not detected at 0x%02X", this->address_);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,6 @@ void CH422GComponent::loop() {
|
||||
void CH422GComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CH422G:");
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
@@ -24,8 +24,7 @@ void CH423Component::setup() {
|
||||
success = this->read_inputs_();
|
||||
}
|
||||
if (!success) {
|
||||
ESP_LOGE(TAG, "CH423 not detected");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("CH423 not detected"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,12 +37,7 @@ void CH423Component::loop() {
|
||||
this->pin_read_flags_ = 0x00;
|
||||
}
|
||||
|
||||
void CH423Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CH423:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
void CH423Component::dump_config() { ESP_LOGCONFIG(TAG, "CH423:"); }
|
||||
|
||||
void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (pin < 8) {
|
||||
|
||||
@@ -27,8 +27,7 @@ uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
|
||||
void CM1106Component::setup() {
|
||||
uint8_t response[8] = {0};
|
||||
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,9 +138,6 @@ void CM1106Component::dump_config() {
|
||||
" ABC baseline: %uppm",
|
||||
ONOFF(this->abc_boot_logic_ == CM1106_ABC_ENABLED), this->abc_cycle_, this->abc_baseline_);
|
||||
}
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::cm1106
|
||||
|
||||
@@ -15,6 +15,7 @@ CONF_AQI = "aqi"
|
||||
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
|
||||
CONF_B_CONSTANT = "b_constant"
|
||||
CONF_BLUE_CHANNEL = "blue_channel"
|
||||
CONF_BORDER_ROUTER = "border_router"
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANNEL_COLORS = "channel_colors"
|
||||
@@ -51,6 +52,7 @@ CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_SCAN_END = "on_scan_end"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_ON_STOP = "on_stop"
|
||||
CONF_OUT_PIN_LEVEL = "out_pin_level"
|
||||
CONF_PARITY = "parity"
|
||||
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
|
||||
CONF_RECONNECT_INTERVAL = "reconnect_interval"
|
||||
|
||||
@@ -67,15 +67,12 @@ void CSE7761Component::setup() {
|
||||
ESP_LOGD(TAG, "CSE7761 found");
|
||||
this->data_.ready = true;
|
||||
} else {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
}
|
||||
|
||||
void CSE7761Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CSE7761:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Current input: %s",
|
||||
LOG_STR_ARG(this->is_current_transformer_() ? LOG_STR("current transformer") : LOG_STR("shunt")));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
@@ -23,8 +23,7 @@ void DAC7678Output::setup() {
|
||||
|
||||
// Reset device
|
||||
if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) {
|
||||
ESP_LOGE(TAG, "Reset failed");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Reset succeeded");
|
||||
@@ -35,8 +34,7 @@ void DAC7678Output::setup() {
|
||||
// Set internal reference
|
||||
if (this->internal_reference_) {
|
||||
if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) {
|
||||
ESP_LOGE(TAG, "Set internal reference failed");
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR("Set internal reference failed"));
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Internal reference enabled");
|
||||
@@ -45,9 +43,7 @@ void DAC7678Output::setup() {
|
||||
}
|
||||
|
||||
void DAC7678Output::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Setting up DAC7678 failed!");
|
||||
} else {
|
||||
if (!this->is_failed()) {
|
||||
ESP_LOGCONFIG(TAG, "DAC7678 initialised");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_MODEL
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@remcom"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "dfrobot_sen0609"
|
||||
MULTI_CONF = True
|
||||
|
||||
dfrobot_sen0609_ns = cg.esphome_ns.namespace("dfrobot_sen0609")
|
||||
DFRobotSEN0609Component = dfrobot_sen0609_ns.class_(
|
||||
"DFRobotSEN0609Component", cg.Component, uart.UARTDevice
|
||||
)
|
||||
RunMode = dfrobot_sen0609_ns.enum("RunMode", is_class=True)
|
||||
|
||||
CONF_DFROBOT_SEN0609_ID = "dfrobot_sen0609_id"
|
||||
|
||||
MODE_PRESENCE = "presence"
|
||||
MODE_SPEED_AND_DISTANCE = "speed_and_distance"
|
||||
RUN_MODES = {
|
||||
MODE_PRESENCE: RunMode.RUN_MODE_PRESENCE,
|
||||
MODE_SPEED_AND_DISTANCE: RunMode.RUN_MODE_SPEED_AND_DISTANCE,
|
||||
}
|
||||
|
||||
MODEL_SEN0609 = "SEN0609"
|
||||
MODEL_SEN0610 = "SEN0610"
|
||||
# Longest detection range in metres
|
||||
MAX_RANGES = {MODEL_SEN0609: 25.0, MODEL_SEN0610: 12.0}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_MODEL, default=MODEL_SEN0609): cv.one_of(
|
||||
*MAX_RANGES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_MODE, default=MODE_PRESENCE): cv.one_of(
|
||||
*RUN_MODES, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
def get_hub_config(root: ConfigType, hub_id: ID) -> ConfigType:
|
||||
"""Return the configuration of the hub with the given id."""
|
||||
return next(conf for conf in root[DOMAIN] if conf[CONF_ID] == hub_id)
|
||||
|
||||
|
||||
def final_validate_mode(keys: dict[str, str]) -> Callable[[ConfigType], None]:
|
||||
"""Reject entity keys that need a different hub mode; `keys` maps each key to the mode it needs."""
|
||||
|
||||
def validator(config: ConfigType) -> None:
|
||||
hub = get_hub_config(fv.full_config.get(), config[CONF_DFROBOT_SEN0609_ID])
|
||||
for key, mode in keys.items():
|
||||
if key in config and hub[CONF_MODE] != mode:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' needs the {DOMAIN} 'mode' to be '{mode}'", path=[key]
|
||||
)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], RUN_MODES[config[CONF_MODE]])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -0,0 +1,24 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HAS_TARGET, CONF_ID, DEVICE_CLASS_OCCUPANCY, ICON_ACCOUNT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
|
||||
|
||||
DEPENDENCIES = ["dfrobot_sen0609"]
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
icon=ICON_ACCOUNT,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_HAS_TARGET, hub.set_has_target_binary_sensor)
|
||||
@@ -0,0 +1,883 @@
|
||||
#include "dfrobot_sen0609.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0609");
|
||||
|
||||
static constexpr uint8_t MAX_ATTEMPTS = 3;
|
||||
// The module ignores a command that follows its last answer too closely
|
||||
static constexpr uint32_t COMMAND_GAP_MS = 100;
|
||||
// Gathers entity changes made close together into one flash write
|
||||
static constexpr uint32_t COALESCE_MS = 1000;
|
||||
static constexpr uint32_t DEFAULT_TIMEOUT_MS = 1000;
|
||||
static constexpr uint32_t LONG_TIMEOUT_MS = 2000;
|
||||
static constexpr uint32_t GIVE_UP_BACKOFF_MS = 30000;
|
||||
// Presence frames come every second (setUartOutput period); this long without one means the module lost them
|
||||
static constexpr uint32_t FRAME_TIMEOUT_MS = 10000;
|
||||
static constexpr uint8_t FRAME_FIELDS = 5;
|
||||
|
||||
static constexpr uint8_t LINE_END[] = {'\r', '\n'};
|
||||
|
||||
// Indexed by Param; empty for parameters that cannot be read
|
||||
PROGMEM_STRING_TABLE(ReadCommands, "", "", "getRange", "getTrigRange", "getSensitivity", "getLatency", "getInhibit",
|
||||
"getThrFactor", "getMicroMotion", "getGpioMode 1", "", "", "getSWV", "getHWV");
|
||||
|
||||
static_assert(PARAM_COUNT <= 16, "pending_writes_ and pending_reads_ hold one bit per parameter");
|
||||
static constexpr uint16_t param_bit(Param param) { return static_cast<uint16_t>(1U << param); }
|
||||
|
||||
// Parameters kept in the module's flash only after saveConfig
|
||||
static constexpr uint16_t PERSISTENT_PARAMS =
|
||||
param_bit(PARAM_RANGE) | param_bit(PARAM_TRIG_RANGE) | param_bit(PARAM_SENSITIVITY) | param_bit(PARAM_LATENCY) |
|
||||
param_bit(PARAM_INHIBIT) | param_bit(PARAM_THR_FACTOR) | param_bit(PARAM_MICRO_MOTION) | param_bit(PARAM_GPIO_MODE);
|
||||
// The frames depend on these, so their writes are kept until they succeed
|
||||
static constexpr uint16_t FRAME_PARAMS = param_bit(PARAM_RUN_APP) | param_bit(PARAM_UART_OUTPUT);
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
static constexpr bool is_led(Param param) { return param == PARAM_RUN_LED || param == PARAM_OUT_LED; }
|
||||
static constexpr SwitchSetting led_switch(Param param) {
|
||||
return param == PARAM_RUN_LED ? SWITCH_RUN_LED : SWITCH_OUT_LED;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
static_assert(NUMBER_COUNT <= 16, "changed_numbers_ holds one bit per number");
|
||||
static constexpr uint8_t NUMBER_PARAMS[NUMBER_COUNT] PROGMEM = {
|
||||
PARAM_RANGE, PARAM_RANGE, PARAM_TRIG_RANGE, PARAM_SENSITIVITY, PARAM_SENSITIVITY,
|
||||
PARAM_LATENCY, PARAM_LATENCY, PARAM_INHIBIT, PARAM_THR_FACTOR,
|
||||
};
|
||||
#endif
|
||||
|
||||
static bool starts_with(const char *str, ESPHOME_PGM_P prefix) {
|
||||
return ESPHOME_strncmp_P(str, prefix, ESPHOME_strlen_P(prefix)) == 0;
|
||||
}
|
||||
|
||||
static bool contains(const char *str, ESPHOME_PGM_P needle) {
|
||||
const size_t len = ESPHOME_strlen_P(needle);
|
||||
for (; *str != '\0'; str++) {
|
||||
if (ESPHOME_strncmp_P(str, needle, len) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Splits "a,b,c*" in place; returns the number of fields stored in `fields`
|
||||
static uint8_t split_fields(char *str, char **fields, uint8_t max_fields) {
|
||||
uint8_t count = 0;
|
||||
char *start = str;
|
||||
for (;; str++) {
|
||||
if (*str != ',' && *str != '*' && *str != '\0')
|
||||
continue;
|
||||
const bool last = *str != ',';
|
||||
*str = '\0';
|
||||
if (count < max_fields)
|
||||
fields[count++] = start;
|
||||
if (last)
|
||||
return count;
|
||||
start = str + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Answers that refuse a command whose effect is already in place
|
||||
static bool answer_means_done(Operation operation, const char *line) {
|
||||
switch (operation) {
|
||||
case Operation::OPERATION_STOP:
|
||||
return contains(line, ESPHOME_PSTR("stopped already"));
|
||||
case Operation::OPERATION_START:
|
||||
return contains(line, ESPHOME_PSTR("started already"));
|
||||
case Operation::OPERATION_SAVE:
|
||||
return starts_with(line, ESPHOME_PSTR("no parameter"));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool parse_float(const char *str, float &value) {
|
||||
char *end;
|
||||
value = strtof(str, &end);
|
||||
return end != str && *end == '\0';
|
||||
}
|
||||
|
||||
static const LogString *operation_to_str(Operation operation) {
|
||||
switch (operation) {
|
||||
case Operation::OPERATION_STOP:
|
||||
return LOG_STR("stop");
|
||||
case Operation::OPERATION_START:
|
||||
return LOG_STR("start");
|
||||
case Operation::OPERATION_WRITE:
|
||||
return LOG_STR("write");
|
||||
case Operation::OPERATION_SAVE:
|
||||
return LOG_STR("save");
|
||||
case Operation::OPERATION_READ:
|
||||
return LOG_STR("read");
|
||||
default:
|
||||
return LOG_STR("none");
|
||||
}
|
||||
}
|
||||
|
||||
DFRobotSEN0609Component::DFRobotSEN0609Component(RunMode mode) : mode_(mode) {
|
||||
#ifdef USE_NUMBER
|
||||
this->number_values_.fill(NAN);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::dump_config() {
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
"DFRobot SEN0609:\n"
|
||||
" Mode: %s",
|
||||
this->mode_ == RunMode::RUN_MODE_PRESENCE ? LOG_STR_LITERAL("presence") : LOG_STR_LITERAL("speed and distance"));
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "HasTarget", this->has_target_binary_sensor_);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "TargetDistance", this->target_distance_sensor_);
|
||||
LOG_SENSOR(" ", "TargetSpeed", this->target_speed_sensor_);
|
||||
LOG_SENSOR(" ", "TargetEnergy", this->target_energy_sensor_);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
|
||||
LOG_TEXT_SENSOR(" ", "HardwareVersion", this->hardware_version_text_sensor_);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
for (number::Number *n : this->numbers_) {
|
||||
LOG_NUMBER(" ", "Number", n);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
for (switch_::Switch *s : this->switches_) {
|
||||
LOG_SWITCH(" ", "Switch", s);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::setup() {
|
||||
#ifdef USE_SWITCH
|
||||
// The module forgets the LED states at power off
|
||||
for (uint8_t i = SWITCH_RUN_LED; i <= SWITCH_OUT_LED; i++) {
|
||||
switch_::Switch *sw = this->switches_[i];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
|
||||
}
|
||||
#endif
|
||||
this->queue_configuration_();
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::loop() {
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
const size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
if (!this->read_failed_) {
|
||||
ESP_LOGW(TAG, "UART read failed");
|
||||
}
|
||||
this->read_failed_ = true;
|
||||
break;
|
||||
}
|
||||
this->read_failed_ = false;
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++)
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
this->run_sequencer_();
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::readline_(uint8_t c) {
|
||||
if (c == '\r')
|
||||
return;
|
||||
if (c == '\n') {
|
||||
if (!this->line_overflow_) {
|
||||
this->line_[this->line_pos_] = '\0';
|
||||
this->handle_line_(this->line_);
|
||||
}
|
||||
this->line_overflow_ = false;
|
||||
this->line_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
if (this->line_overflow_)
|
||||
return;
|
||||
if (this->line_pos_ >= MAX_LINE_LENGTH - 1) {
|
||||
ESP_LOGW(TAG, "Line too long, dropped");
|
||||
this->line_overflow_ = true;
|
||||
return;
|
||||
}
|
||||
this->line_[this->line_pos_++] = static_cast<char>(c);
|
||||
// The prompt has no line end, and the echo of the next command follows it on the same line
|
||||
if (c == '>') {
|
||||
this->line_[this->line_pos_] = '\0';
|
||||
if (ESPHOME_strcmp_P(this->line_, ESPHOME_PSTR("DFRobot:/>")) == 0)
|
||||
this->line_pos_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::handle_line_(char *line) {
|
||||
if (*line == '\0')
|
||||
return;
|
||||
ESP_LOGV(TAG, "RX: %s", line);
|
||||
if (line[0] == '$') {
|
||||
this->handle_frame_(line);
|
||||
return;
|
||||
}
|
||||
// Version lines name their value, so they are used even when they arrive late
|
||||
const bool software = starts_with(line, ESPHOME_PSTR("SoftwareVersion:"));
|
||||
if (software || starts_with(line, ESPHOME_PSTR("HardwareVersion:"))) {
|
||||
if (this->operation_ == Operation::OPERATION_READ &&
|
||||
this->param_ == (software ? PARAM_SW_VERSION : PARAM_HW_VERSION))
|
||||
this->version_received_ = true;
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *sens = software ? this->version_text_sensor_ : this->hardware_version_text_sensor_;
|
||||
if (sens != nullptr)
|
||||
sens->publish_state(line + 16);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (this->operation_ == Operation::OPERATION_NONE) {
|
||||
ESP_LOGD(TAG, "Ignored line, no command pending: %s", line);
|
||||
return;
|
||||
}
|
||||
if (strcmp(line, this->command_) == 0) {
|
||||
this->echo_seen_ = true;
|
||||
return;
|
||||
}
|
||||
// The module echoes every command first; lines before the echo belong to an earlier command
|
||||
if (!this->echo_seen_) {
|
||||
ESP_LOGD(TAG, "Ignored line from an earlier command: %s", line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (starts_with(line, ESPHOME_PSTR("Done")) || answer_means_done(this->operation_, line)) {
|
||||
this->finish_operation_();
|
||||
} else if (starts_with(line, ESPHOME_PSTR("Response "))) {
|
||||
this->parse_response_(line + 9);
|
||||
} else if (contains(line, ESPHOME_PSTR("not stopped"))) {
|
||||
// Stop the sensor before the next attempt
|
||||
this->stopped_ = false;
|
||||
this->fail_operation_();
|
||||
} else if (starts_with(line, ESPHOME_PSTR("Error")) || contains(line, ESPHOME_PSTR("not recognized"))) {
|
||||
ESP_LOGW(TAG, "Command '%s' rejected: %s", this->command_, line);
|
||||
this->fail_operation_();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Unexpected answer to '%s': %s", this->command_, line);
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::handle_frame_(char *line) {
|
||||
// $DFHPD,<presence>, , , *
|
||||
if (starts_with(line, ESPHOME_PSTR("$DFHPD,"))) {
|
||||
const char presence = line[7];
|
||||
if (presence != '0' && presence != '1') {
|
||||
this->log_bad_frame_(LOG_STR("Malformed presence frame"), line);
|
||||
return;
|
||||
}
|
||||
this->last_frame_ms_ = App.get_loop_component_start_time();
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->has_target_binary_sensor_ != nullptr)
|
||||
this->has_target_binary_sensor_->publish_state(presence == '1');
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
// $DFDMD,<targets>,<reserved>,<distance m>,<speed m/s>,<energy>,...*
|
||||
if (!starts_with(line, ESPHOME_PSTR("$DFDMD,"))) {
|
||||
this->log_bad_frame_(LOG_STR("Unknown frame"), line);
|
||||
return;
|
||||
}
|
||||
char *fields[FRAME_FIELDS];
|
||||
float distance;
|
||||
float speed;
|
||||
float energy;
|
||||
if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
|
||||
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
|
||||
return;
|
||||
}
|
||||
const optional<uint8_t> targets = parse_number<uint8_t>(fields[0]);
|
||||
if (!targets.has_value()) {
|
||||
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
|
||||
return;
|
||||
}
|
||||
if (*targets == 0) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->has_target_binary_sensor_ != nullptr)
|
||||
this->has_target_binary_sensor_->publish_state(false);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
this->publish_target_(NAN, NAN, NAN);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
|
||||
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
|
||||
return;
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->has_target_binary_sensor_ != nullptr)
|
||||
this->has_target_binary_sensor_->publish_state(true);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
this->publish_target_(distance, speed, energy);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::log_bad_frame_(const LogString *problem, const char *line) {
|
||||
// The module sends frames several times a second, so only the first bad one is logged
|
||||
if (this->bad_frame_logged_)
|
||||
return;
|
||||
this->bad_frame_logged_ = true;
|
||||
ESP_LOGW(TAG, "%s: %s", LOG_STR_ARG(problem), line);
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
static void publish_deduplicated(sensor::Sensor *sens, Deduplicator<float> &dedup, float value) {
|
||||
if (sens == nullptr)
|
||||
return;
|
||||
if (std::isnan(value) ? dedup.next_unknown() : dedup.next(value))
|
||||
sens->publish_state(value);
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::publish_target_(float distance, float speed, float energy) {
|
||||
publish_deduplicated(this->target_distance_sensor_, this->distance_dedup_, distance);
|
||||
publish_deduplicated(this->target_speed_sensor_, this->speed_dedup_, speed);
|
||||
publish_deduplicated(this->target_energy_sensor_, this->energy_dedup_, energy);
|
||||
}
|
||||
#endif
|
||||
|
||||
void DFRobotSEN0609Component::parse_response_(const char *args) {
|
||||
this->response_count_ = 0;
|
||||
while (this->response_count_ < MAX_RESPONSE_VALUES) {
|
||||
char *end;
|
||||
const float value = strtof(args, &end);
|
||||
// strtof also takes "nan" and "inf", which no setting can hold
|
||||
if (end == args || !std::isfinite(value))
|
||||
break;
|
||||
this->response_[this->response_count_++] = value;
|
||||
args = end;
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::queue_configuration_() {
|
||||
// The mode, the UART output and the LEDs are applied at every boot instead of being saved
|
||||
uint16_t writes = param_bit(PARAM_RUN_APP);
|
||||
if (this->mode_ == RunMode::RUN_MODE_PRESENCE)
|
||||
writes |= param_bit(PARAM_UART_OUTPUT);
|
||||
#ifdef USE_SWITCH
|
||||
if (this->switches_[SWITCH_RUN_LED] != nullptr)
|
||||
writes |= param_bit(PARAM_RUN_LED);
|
||||
if (this->switches_[SWITCH_OUT_LED] != nullptr)
|
||||
writes |= param_bit(PARAM_OUT_LED);
|
||||
#endif
|
||||
this->pending_writes_ |= writes;
|
||||
this->pending_reads_ |= this->wanted_reads_();
|
||||
}
|
||||
|
||||
uint16_t DFRobotSEN0609Component::wanted_reads_() const {
|
||||
uint16_t reads = 0;
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
reads |= param_bit(PARAM_SW_VERSION);
|
||||
if (this->hardware_version_text_sensor_ != nullptr)
|
||||
reads |= param_bit(PARAM_HW_VERSION);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
|
||||
if (this->numbers_[i] != nullptr)
|
||||
reads |= param_bit(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[i])));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
if (this->switches_[SWITCH_MICRO_MOTION] != nullptr)
|
||||
reads |= param_bit(PARAM_MICRO_MOTION);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->out_pin_level_select_ != nullptr)
|
||||
reads |= param_bit(PARAM_GPIO_MODE);
|
||||
#endif
|
||||
return reads;
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::check_frames_(uint32_t now) {
|
||||
// Speed mode frames are not watched: whether the module sends them without a target is not known
|
||||
if (this->mode_ != RunMode::RUN_MODE_PRESENCE || now - this->last_frame_ms_ < FRAME_TIMEOUT_MS)
|
||||
return;
|
||||
// The module most likely restarted on its own and lost the configuration applied at boot
|
||||
ESP_LOGW(TAG, "No presence frames, applying the configuration again");
|
||||
this->status_set_warning();
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->has_target_binary_sensor_ != nullptr)
|
||||
this->has_target_binary_sensor_->invalidate_state();
|
||||
#endif
|
||||
this->last_frame_ms_ = now;
|
||||
this->queue_configuration_();
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::request_write_(Param param) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->pending_writes_ |= param_bit(param);
|
||||
this->last_change_ms_ = now;
|
||||
if (this->operation_ != Operation::OPERATION_NONE || this->stopped_)
|
||||
return;
|
||||
// Start a new sequence a little later, so changes made together end in one flash write.
|
||||
// A longer hold, such as the back-off from a module that does not answer, is not cut short.
|
||||
const uint32_t elapsed = now - this->hold_start_ms_;
|
||||
if (elapsed >= this->hold_ms_ || this->hold_ms_ - elapsed < COALESCE_MS)
|
||||
this->hold_(COALESCE_MS);
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void DFRobotSEN0609Component::control_number(NumberSetting setting, float value) {
|
||||
// The module needs min_range <= trigger_range <= max_range; a value not read yet is NAN and passes
|
||||
const auto &values = this->number_values_;
|
||||
const float min_range = setting == NUMBER_MIN_RANGE ? value : values[NUMBER_MIN_RANGE];
|
||||
const float max_range = setting == NUMBER_MAX_RANGE ? value : values[NUMBER_MAX_RANGE];
|
||||
const float trigger_range = setting == NUMBER_TRIGGER_RANGE ? value : values[NUMBER_TRIGGER_RANGE];
|
||||
if (setting <= NUMBER_TRIGGER_RANGE &&
|
||||
(min_range > max_range || trigger_range < min_range || trigger_range > max_range)) {
|
||||
ESP_LOGW(TAG, "Ranges must keep minimum <= trigger <= maximum, not written");
|
||||
if (!std::isnan(this->number_values_[setting]))
|
||||
this->publish_number_(setting, this->number_values_[setting]);
|
||||
return;
|
||||
}
|
||||
this->changed_numbers_ |= 1U << setting;
|
||||
this->publish_number_(setting, value);
|
||||
this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
|
||||
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
|
||||
if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
|
||||
continue;
|
||||
this->changed_numbers_ &= ~(1U << i);
|
||||
if (unknown)
|
||||
this->publish_number_(static_cast<NumberSetting>(i), NAN);
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::publish_number_(NumberSetting setting, float value) {
|
||||
this->number_values_[setting] = value;
|
||||
if (this->numbers_[setting] != nullptr)
|
||||
this->numbers_[setting]->publish_state(value);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void DFRobotSEN0609Component::control_switch(SwitchSetting setting, bool state) {
|
||||
this->publish_switch_(setting, state);
|
||||
switch (setting) {
|
||||
case SWITCH_MICRO_MOTION:
|
||||
this->request_write_(PARAM_MICRO_MOTION);
|
||||
break;
|
||||
case SWITCH_RUN_LED:
|
||||
this->request_write_(PARAM_RUN_LED);
|
||||
break;
|
||||
default:
|
||||
this->request_write_(PARAM_OUT_LED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::publish_switch_(SwitchSetting setting, bool state) {
|
||||
this->switch_states_[setting] = state;
|
||||
if (this->switches_[setting] != nullptr)
|
||||
this->switches_[setting]->publish_state(state);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void DFRobotSEN0609Component::control_out_pin_level(size_t index) {
|
||||
this->out_pin_level_ = static_cast<uint8_t>(index);
|
||||
this->out_pin_level_select_->publish_state(index);
|
||||
this->request_write_(PARAM_GPIO_MODE);
|
||||
}
|
||||
#endif
|
||||
|
||||
void DFRobotSEN0609Component::hold_(uint32_t duration_ms) {
|
||||
this->hold_start_ms_ = App.get_loop_component_start_time();
|
||||
this->hold_ms_ = duration_ms;
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::run_sequencer_() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->operation_ != Operation::OPERATION_NONE) {
|
||||
const bool slow = this->operation_ == Operation::OPERATION_STOP || this->operation_ == Operation::OPERATION_SAVE;
|
||||
if (now - this->sent_ms_ < (slow ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS))
|
||||
return;
|
||||
ESP_LOGD(TAG, "No answer to '%s'", this->command_);
|
||||
this->fail_operation_();
|
||||
return;
|
||||
}
|
||||
if (now - this->hold_start_ms_ < this->hold_ms_)
|
||||
return;
|
||||
|
||||
const bool work_pending = this->save_pending_ || this->pending_writes_ != 0 || this->pending_reads_ != 0;
|
||||
Operation operation;
|
||||
Param param = PARAM_COUNT;
|
||||
if (!work_pending) {
|
||||
if (!this->stopped_) {
|
||||
this->check_frames_(now);
|
||||
// Work given up on is tried again in a later sequence, so the sensor runs in between
|
||||
if ((this->deferred_writes_ != 0 || this->deferred_save_) && now - this->deferred_ms_ >= GIVE_UP_BACKOFF_MS) {
|
||||
this->pending_writes_ |= this->deferred_writes_;
|
||||
this->save_pending_ = this->save_pending_ || this->deferred_save_;
|
||||
this->deferred_writes_ = 0;
|
||||
this->deferred_save_ = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
operation = Operation::OPERATION_START;
|
||||
} else if (!this->stopped_) {
|
||||
operation = Operation::OPERATION_STOP;
|
||||
} else if (this->pending_writes_ != 0) {
|
||||
operation = Operation::OPERATION_WRITE;
|
||||
param = static_cast<Param>(__builtin_ctz(this->pending_writes_));
|
||||
} else if (this->save_pending_) {
|
||||
// Changes made within a second of each other share one flash write, also while a sequence runs
|
||||
if (now - this->last_change_ms_ < COALESCE_MS)
|
||||
return;
|
||||
operation = Operation::OPERATION_SAVE;
|
||||
} else {
|
||||
operation = Operation::OPERATION_READ;
|
||||
param = static_cast<Param>(__builtin_ctz(this->pending_reads_));
|
||||
}
|
||||
|
||||
const uint16_t bit = param_bit(param);
|
||||
bool built = this->build_command_(operation, param);
|
||||
if (!built && operation == Operation::OPERATION_WRITE) {
|
||||
// The value this write is paired with is unknown: read it first, once, and keep the write pending
|
||||
if ((this->paired_reads_ & bit) == 0)
|
||||
this->pending_reads_ |= this->wanted_reads_() & bit;
|
||||
if ((this->pending_reads_ & bit) != 0) {
|
||||
this->paired_reads_ |= bit;
|
||||
operation = Operation::OPERATION_READ;
|
||||
built = this->build_command_(operation, param);
|
||||
}
|
||||
}
|
||||
if (!built) {
|
||||
if (operation == Operation::OPERATION_WRITE) {
|
||||
ESP_LOGW(TAG, "Setting not written: the value it is paired with could not be read");
|
||||
this->pending_writes_ &= ~bit;
|
||||
this->paired_reads_ &= ~bit;
|
||||
#ifdef USE_NUMBER
|
||||
// Both values come from the read that failed, so what the module holds is unknown
|
||||
this->forget_number_changes_(param, true);
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGE(TAG, "No command for %s of parameter %u", LOG_STR_ARG(operation_to_str(operation)),
|
||||
static_cast<unsigned>(param));
|
||||
this->pending_reads_ &= ~bit;
|
||||
}
|
||||
this->sequence_failed_ = true;
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Cleared now, so a change made while the write is on its way is written again
|
||||
if (operation == Operation::OPERATION_WRITE) {
|
||||
this->pending_writes_ &= ~bit;
|
||||
this->paired_reads_ &= ~bit;
|
||||
#ifdef USE_NUMBER
|
||||
this->forget_number_changes_(param, false);
|
||||
#endif
|
||||
}
|
||||
this->operation_ = operation;
|
||||
this->param_ = param;
|
||||
this->response_count_ = 0;
|
||||
this->echo_seen_ = false;
|
||||
this->version_received_ = false;
|
||||
this->sent_ms_ = now;
|
||||
ESP_LOGV(TAG, "TX: %s", this->command_);
|
||||
this->write_str(this->command_);
|
||||
this->write_array(LINE_END, sizeof(LINE_END));
|
||||
}
|
||||
|
||||
bool DFRobotSEN0609Component::build_command_(Operation operation, Param param) {
|
||||
char *buf = this->command_;
|
||||
constexpr size_t len = MAX_COMMAND_LENGTH;
|
||||
switch (operation) {
|
||||
case Operation::OPERATION_STOP:
|
||||
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStop"), len);
|
||||
return true;
|
||||
case Operation::OPERATION_START:
|
||||
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStart"), len);
|
||||
return true;
|
||||
case Operation::OPERATION_SAVE:
|
||||
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("saveConfig"), len);
|
||||
return true;
|
||||
case Operation::OPERATION_READ:
|
||||
ESPHOME_strncpy_P(buf, reinterpret_cast<ESPHOME_PGM_P>(ReadCommands::get_progmem_str(param, 0)), len);
|
||||
return buf[0] != '\0';
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (param) {
|
||||
case PARAM_RUN_APP:
|
||||
ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setRunApp %u"), static_cast<unsigned>(this->mode_));
|
||||
return true;
|
||||
case PARAM_UART_OUTPUT:
|
||||
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("setUartOutput 1 1 1 1.0"), len);
|
||||
return true;
|
||||
#ifdef USE_SWITCH
|
||||
case PARAM_MICRO_MOTION:
|
||||
ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setMicroMotion %u"),
|
||||
static_cast<unsigned>(this->switch_states_[SWITCH_MICRO_MOTION]));
|
||||
return true;
|
||||
case PARAM_RUN_LED:
|
||||
case PARAM_OUT_LED: {
|
||||
// 0 turns the LED on
|
||||
const bool on = this->switch_states_[led_switch(param)];
|
||||
this->sent_led_on_ = on;
|
||||
ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setLedMode %u %u"), param == PARAM_RUN_LED ? 1U : 2U, on ? 0U : 1U);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
case PARAM_GPIO_MODE:
|
||||
ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setGpioMode 1 %u"), static_cast<unsigned>(this->out_pin_level_));
|
||||
return true;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
const auto &values = this->number_values_;
|
||||
float first = NAN;
|
||||
float second = 0.0f;
|
||||
ESPHOME_PGM_P format = nullptr;
|
||||
switch (param) {
|
||||
case PARAM_RANGE:
|
||||
first = values[NUMBER_MIN_RANGE];
|
||||
second = values[NUMBER_MAX_RANGE];
|
||||
format = ESPHOME_PSTR("setRange %.2f %.2f");
|
||||
break;
|
||||
case PARAM_TRIG_RANGE:
|
||||
first = values[NUMBER_TRIGGER_RANGE];
|
||||
format = ESPHOME_PSTR("setTrigRange %.2f");
|
||||
break;
|
||||
case PARAM_SENSITIVITY:
|
||||
first = values[NUMBER_HOLD_SENSITIVITY];
|
||||
second = values[NUMBER_TRIGGER_SENSITIVITY];
|
||||
format = ESPHOME_PSTR("setSensitivity %.0f %.0f");
|
||||
break;
|
||||
case PARAM_LATENCY:
|
||||
first = values[NUMBER_ON_LATENCY];
|
||||
second = values[NUMBER_OFF_LATENCY];
|
||||
format = ESPHOME_PSTR("setLatency %.2f %.1f");
|
||||
break;
|
||||
case PARAM_INHIBIT:
|
||||
first = values[NUMBER_INHIBIT_TIME];
|
||||
format = ESPHOME_PSTR("setInhibit %.2f");
|
||||
break;
|
||||
case PARAM_THR_FACTOR:
|
||||
first = values[NUMBER_THRESHOLD_FACTOR];
|
||||
format = ESPHOME_PSTR("setThrFactor %.0f");
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
// Commands that take two values need both read from the module first
|
||||
if (std::isnan(first) || std::isnan(second))
|
||||
return false;
|
||||
ESPHOME_snprintf_P(buf, len, format, first, second);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DFRobotSEN0609Component::apply_read_(Param param) {
|
||||
const float *r = this->response_;
|
||||
const uint8_t count = this->response_count_;
|
||||
// A change still waiting to be written is newer than what the module holds
|
||||
const bool write_pending = (this->pending_writes_ & param_bit(param)) != 0;
|
||||
#ifdef USE_NUMBER
|
||||
// Checked for each number: publishing runs automations, which may change the next one
|
||||
auto read_number = [this](NumberSetting setting, float value) {
|
||||
if ((this->changed_numbers_ & (1U << setting)) == 0)
|
||||
this->publish_number_(setting, value);
|
||||
};
|
||||
#endif
|
||||
switch (param) {
|
||||
#ifdef USE_NUMBER
|
||||
case PARAM_RANGE:
|
||||
if (count < 2)
|
||||
return false;
|
||||
read_number(NUMBER_MIN_RANGE, r[0]);
|
||||
read_number(NUMBER_MAX_RANGE, r[1]);
|
||||
return true;
|
||||
case PARAM_TRIG_RANGE:
|
||||
if (count < 1)
|
||||
return false;
|
||||
read_number(NUMBER_TRIGGER_RANGE, r[0]);
|
||||
return true;
|
||||
case PARAM_SENSITIVITY:
|
||||
if (count < 2)
|
||||
return false;
|
||||
read_number(NUMBER_HOLD_SENSITIVITY, r[0]);
|
||||
read_number(NUMBER_TRIGGER_SENSITIVITY, r[1]);
|
||||
return true;
|
||||
case PARAM_LATENCY:
|
||||
if (count < 2)
|
||||
return false;
|
||||
read_number(NUMBER_ON_LATENCY, r[0]);
|
||||
read_number(NUMBER_OFF_LATENCY, r[1]);
|
||||
return true;
|
||||
case PARAM_INHIBIT:
|
||||
if (count < 1)
|
||||
return false;
|
||||
read_number(NUMBER_INHIBIT_TIME, r[0]);
|
||||
return true;
|
||||
case PARAM_THR_FACTOR:
|
||||
if (count < 1)
|
||||
return false;
|
||||
read_number(NUMBER_THRESHOLD_FACTOR, r[0]);
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
case PARAM_MICRO_MOTION:
|
||||
if (count < 1)
|
||||
return false;
|
||||
if (!write_pending)
|
||||
this->publish_switch_(SWITCH_MICRO_MOTION, r[0] != 0.0f);
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
case PARAM_GPIO_MODE:
|
||||
// Response <pin> <level>
|
||||
if (count < 2 || (r[1] != 0.0f && r[1] != 1.0f))
|
||||
return false;
|
||||
if (!write_pending) {
|
||||
this->out_pin_level_ = static_cast<uint8_t>(r[1]);
|
||||
this->out_pin_level_select_->publish_state(this->out_pin_level_);
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
case PARAM_SW_VERSION:
|
||||
case PARAM_HW_VERSION:
|
||||
// The versions are published as their lines arrive
|
||||
return this->version_received_;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::finish_operation_() {
|
||||
switch (this->operation_) {
|
||||
case Operation::OPERATION_STOP:
|
||||
this->stopped_ = true;
|
||||
break;
|
||||
case Operation::OPERATION_START:
|
||||
this->stopped_ = false;
|
||||
this->last_frame_ms_ = App.get_loop_component_start_time();
|
||||
if (!this->sequence_failed_)
|
||||
this->status_clear_warning();
|
||||
this->sequence_failed_ = false;
|
||||
break;
|
||||
case Operation::OPERATION_WRITE:
|
||||
this->deferred_writes_ &= ~param_bit(this->param_);
|
||||
if (PERSISTENT_PARAMS & param_bit(this->param_))
|
||||
this->save_pending_ = true;
|
||||
#ifdef USE_SWITCH
|
||||
if (is_led(this->param_)) {
|
||||
const uint8_t bit = 1U << led_switch(this->param_);
|
||||
this->led_known_ |= bit;
|
||||
this->led_on_ = this->sent_led_on_ ? (this->led_on_ | bit) : (this->led_on_ & ~bit);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case Operation::OPERATION_SAVE:
|
||||
this->save_pending_ = false;
|
||||
this->deferred_save_ = false;
|
||||
break;
|
||||
case Operation::OPERATION_READ:
|
||||
if (!this->apply_read_(this->param_)) {
|
||||
ESP_LOGW(TAG, "Malformed answer to '%s'", this->command_);
|
||||
this->fail_operation_();
|
||||
return;
|
||||
}
|
||||
this->pending_reads_ &= ~param_bit(this->param_);
|
||||
this->paired_reads_ &= ~param_bit(this->param_);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (this->is_aux_operation_()) {
|
||||
this->aux_attempts_ = 0;
|
||||
} else {
|
||||
this->attempts_ = 0;
|
||||
}
|
||||
this->operation_ = Operation::OPERATION_NONE;
|
||||
this->hold_(COMMAND_GAP_MS);
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::fail_operation_() {
|
||||
const bool aux = this->is_aux_operation_();
|
||||
uint8_t &attempts = aux ? this->aux_attempts_ : this->attempts_;
|
||||
const Operation operation = this->operation_;
|
||||
this->operation_ = Operation::OPERATION_NONE;
|
||||
this->hold_(COMMAND_GAP_MS);
|
||||
const bool keep_write = operation == Operation::OPERATION_WRITE && (FRAME_PARAMS & param_bit(this->param_)) != 0;
|
||||
const bool retry = ++attempts < MAX_ATTEMPTS;
|
||||
if (operation == Operation::OPERATION_WRITE && (retry || keep_write))
|
||||
this->pending_writes_ |= param_bit(this->param_);
|
||||
if (retry)
|
||||
return;
|
||||
|
||||
attempts = 0;
|
||||
this->status_set_warning();
|
||||
if (aux || keep_write) {
|
||||
// Without stop, start or the configuration the sensor cannot run as set up; try again later
|
||||
ESP_LOGW(TAG, "Command '%s' failed, trying again later", this->command_);
|
||||
this->hold_ms_ = GIVE_UP_BACKOFF_MS;
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Giving up on %s '%s'", LOG_STR_ARG(operation_to_str(operation)), this->command_);
|
||||
this->sequence_failed_ = true;
|
||||
switch (operation) {
|
||||
case Operation::OPERATION_WRITE:
|
||||
#ifdef USE_SWITCH
|
||||
if (is_led(this->param_)) {
|
||||
// A newer change is written anyway
|
||||
if ((this->pending_writes_ & param_bit(this->param_)) != 0)
|
||||
break;
|
||||
// The LEDs cannot be read: show the state the module last accepted, or write this one again later
|
||||
const uint8_t bit = 1U << led_switch(this->param_);
|
||||
if ((this->led_known_ & bit) != 0) {
|
||||
this->publish_switch_(led_switch(this->param_), (this->led_on_ & bit) != 0);
|
||||
} else {
|
||||
this->deferred_writes_ |= param_bit(this->param_);
|
||||
this->deferred_ms_ = App.get_loop_component_start_time();
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
// The entity shows a value the module never got; show the module's value again
|
||||
this->pending_reads_ |= this->wanted_reads_() & param_bit(this->param_);
|
||||
break;
|
||||
case Operation::OPERATION_SAVE:
|
||||
// The entities show the changes as kept, but unsaved they are lost at power off: save in a later sequence
|
||||
this->save_pending_ = false;
|
||||
this->deferred_save_ = true;
|
||||
this->deferred_ms_ = App.get_loop_component_start_time();
|
||||
break;
|
||||
case Operation::OPERATION_READ:
|
||||
this->pending_reads_ &= ~param_bit(this->param_);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
#include "esphome/components/number/number.h"
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 80;
|
||||
static constexpr uint8_t MAX_COMMAND_LENGTH = 40;
|
||||
static constexpr uint8_t MAX_RESPONSE_VALUES = 3;
|
||||
|
||||
enum class RunMode : uint8_t {
|
||||
RUN_MODE_PRESENCE = 0,
|
||||
RUN_MODE_SPEED_AND_DISTANCE = 1,
|
||||
};
|
||||
|
||||
/// Settings shown as number entities.
|
||||
enum NumberSetting : uint8_t {
|
||||
NUMBER_MIN_RANGE,
|
||||
NUMBER_MAX_RANGE,
|
||||
NUMBER_TRIGGER_RANGE,
|
||||
NUMBER_HOLD_SENSITIVITY,
|
||||
NUMBER_TRIGGER_SENSITIVITY,
|
||||
NUMBER_ON_LATENCY,
|
||||
NUMBER_OFF_LATENCY,
|
||||
NUMBER_INHIBIT_TIME,
|
||||
NUMBER_THRESHOLD_FACTOR,
|
||||
NUMBER_COUNT,
|
||||
};
|
||||
|
||||
/// Settings shown as switch entities.
|
||||
enum SwitchSetting : uint8_t {
|
||||
SWITCH_MICRO_MOTION,
|
||||
SWITCH_RUN_LED,
|
||||
SWITCH_OUT_LED,
|
||||
SWITCH_COUNT,
|
||||
};
|
||||
|
||||
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
|
||||
enum Param : uint8_t {
|
||||
PARAM_RUN_APP,
|
||||
PARAM_UART_OUTPUT,
|
||||
PARAM_RANGE,
|
||||
PARAM_TRIG_RANGE,
|
||||
PARAM_SENSITIVITY,
|
||||
PARAM_LATENCY,
|
||||
PARAM_INHIBIT,
|
||||
PARAM_THR_FACTOR,
|
||||
PARAM_MICRO_MOTION,
|
||||
PARAM_GPIO_MODE,
|
||||
PARAM_RUN_LED,
|
||||
PARAM_OUT_LED,
|
||||
PARAM_SW_VERSION,
|
||||
PARAM_HW_VERSION,
|
||||
PARAM_COUNT,
|
||||
};
|
||||
|
||||
enum class Operation : uint8_t {
|
||||
OPERATION_NONE,
|
||||
OPERATION_STOP,
|
||||
OPERATION_START,
|
||||
OPERATION_WRITE,
|
||||
OPERATION_SAVE,
|
||||
OPERATION_READ,
|
||||
};
|
||||
|
||||
class DFRobotSEN0609Component final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(has_target)
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
SUB_SENSOR(target_distance)
|
||||
SUB_SENSOR(target_speed)
|
||||
SUB_SENSOR(target_energy)
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(version)
|
||||
SUB_TEXT_SENSOR(hardware_version)
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
SUB_SELECT(out_pin_level)
|
||||
#endif
|
||||
|
||||
public:
|
||||
explicit DFRobotSEN0609Component(RunMode mode);
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void set_number(NumberSetting setting, number::Number *number) { this->numbers_[setting] = number; }
|
||||
void control_number(NumberSetting setting, float value);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void set_switch(SwitchSetting setting, switch_::Switch *sw) { this->switches_[setting] = sw; }
|
||||
void control_switch(SwitchSetting setting, bool state);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void control_out_pin_level(size_t index);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void readline_(uint8_t c);
|
||||
void handle_line_(char *line);
|
||||
void handle_frame_(char *line);
|
||||
void log_bad_frame_(const LogString *problem, const char *line);
|
||||
void parse_response_(const char *args);
|
||||
|
||||
void queue_configuration_();
|
||||
uint16_t wanted_reads_() const;
|
||||
void request_write_(Param param);
|
||||
void run_sequencer_();
|
||||
void check_frames_(uint32_t now);
|
||||
bool build_command_(Operation operation, Param param);
|
||||
bool apply_read_(Param param);
|
||||
void finish_operation_();
|
||||
void fail_operation_();
|
||||
void hold_(uint32_t duration_ms);
|
||||
bool is_aux_operation_() const {
|
||||
return this->operation_ == Operation::OPERATION_STOP || this->operation_ == Operation::OPERATION_START;
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void publish_number_(NumberSetting setting, float value);
|
||||
void forget_number_changes_(Param param, bool unknown);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void publish_switch_(SwitchSetting setting, bool state);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void publish_target_(float distance, float speed, float energy);
|
||||
Deduplicator<float> distance_dedup_;
|
||||
Deduplicator<float> speed_dedup_;
|
||||
Deduplicator<float> energy_dedup_;
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
std::array<number::Number *, NUMBER_COUNT> numbers_{};
|
||||
std::array<float, NUMBER_COUNT> number_values_;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
std::array<switch_::Switch *, SWITCH_COUNT> switches_{};
|
||||
std::array<bool, SWITCH_COUNT> switch_states_{};
|
||||
#endif
|
||||
|
||||
char line_[MAX_LINE_LENGTH];
|
||||
char command_[MAX_COMMAND_LENGTH]{};
|
||||
float response_[MAX_RESPONSE_VALUES]{};
|
||||
uint32_t sent_ms_{0};
|
||||
uint32_t hold_start_ms_{0};
|
||||
uint32_t hold_ms_{0};
|
||||
uint32_t last_frame_ms_{0};
|
||||
uint32_t last_change_ms_{0};
|
||||
uint32_t deferred_ms_{0};
|
||||
uint16_t pending_writes_{0};
|
||||
uint16_t pending_reads_{0};
|
||||
// Reads made for a write whose paired value is unknown; the write is dropped when the read fails
|
||||
uint16_t paired_reads_{0};
|
||||
// Writes given up on that are tried again in a later sequence
|
||||
uint16_t deferred_writes_{0};
|
||||
#ifdef USE_NUMBER
|
||||
// Numbers changed since their last write was sent, one bit per NumberSetting
|
||||
uint16_t changed_numbers_{0};
|
||||
#endif
|
||||
RunMode mode_;
|
||||
Operation operation_{Operation::OPERATION_NONE};
|
||||
Param param_{PARAM_COUNT};
|
||||
uint8_t line_pos_{0};
|
||||
uint8_t response_count_{0};
|
||||
uint8_t out_pin_level_{0};
|
||||
uint8_t attempts_{0};
|
||||
uint8_t aux_attempts_{0};
|
||||
#ifdef USE_SWITCH
|
||||
// The module cannot be asked for its LED states: the ones it last accepted, one bit per SwitchSetting
|
||||
uint8_t led_known_{0};
|
||||
uint8_t led_on_{0};
|
||||
bool sent_led_on_{false};
|
||||
#endif
|
||||
bool line_overflow_{false};
|
||||
bool read_failed_{false};
|
||||
bool bad_frame_logged_{false};
|
||||
bool echo_seen_{false};
|
||||
bool version_received_{false};
|
||||
bool stopped_{false};
|
||||
bool save_pending_{false};
|
||||
bool deferred_save_{false};
|
||||
bool sequence_failed_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -0,0 +1,194 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MAX_RANGE,
|
||||
CONF_MIN_RANGE,
|
||||
CONF_MODEL,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_MOTION_SENSOR,
|
||||
ICON_RULER,
|
||||
ICON_TIMELAPSE,
|
||||
UNIT_METER,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DFROBOT_SEN0609_ID,
|
||||
MAX_RANGES,
|
||||
MODE_PRESENCE,
|
||||
MODE_SPEED_AND_DISTANCE,
|
||||
DFRobotSEN0609Component,
|
||||
dfrobot_sen0609_ns,
|
||||
final_validate_mode,
|
||||
get_hub_config,
|
||||
)
|
||||
|
||||
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
|
||||
NumberSetting = dfrobot_sen0609_ns.enum("NumberSetting")
|
||||
|
||||
CONF_HOLD_SENSITIVITY = "hold_sensitivity"
|
||||
CONF_INHIBIT_TIME = "inhibit_time"
|
||||
CONF_OFF_LATENCY = "off_latency"
|
||||
CONF_ON_LATENCY = "on_latency"
|
||||
CONF_THRESHOLD_FACTOR = "threshold_factor"
|
||||
CONF_TRIGGER_RANGE = "trigger_range"
|
||||
CONF_TRIGGER_SENSITIVITY = "trigger_sensitivity"
|
||||
|
||||
|
||||
class Setting(NamedTuple):
|
||||
key: str
|
||||
setting: cg.MockObj
|
||||
min_value: float
|
||||
max_value: float | None # None: the model's longest range
|
||||
step: float
|
||||
mode: str | None # None: both modes
|
||||
schema: cv.Schema
|
||||
|
||||
|
||||
def _distance(icon: str = ICON_RULER) -> cv.Schema:
|
||||
return number.number_schema(
|
||||
DFRobotSEN0609Number,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=icon,
|
||||
unit_of_measurement=UNIT_METER,
|
||||
)
|
||||
|
||||
|
||||
def _duration() -> cv.Schema:
|
||||
return number.number_schema(
|
||||
DFRobotSEN0609Number,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_TIMELAPSE,
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
)
|
||||
|
||||
|
||||
def _plain() -> cv.Schema:
|
||||
return number.number_schema(
|
||||
DFRobotSEN0609Number,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
)
|
||||
|
||||
|
||||
SETTINGS = (
|
||||
Setting(
|
||||
CONF_MIN_RANGE,
|
||||
NumberSetting.NUMBER_MIN_RANGE,
|
||||
0.6,
|
||||
None,
|
||||
0.1,
|
||||
None,
|
||||
_distance(),
|
||||
),
|
||||
Setting(
|
||||
CONF_MAX_RANGE,
|
||||
NumberSetting.NUMBER_MAX_RANGE,
|
||||
0.6,
|
||||
None,
|
||||
0.1,
|
||||
None,
|
||||
_distance(),
|
||||
),
|
||||
Setting(
|
||||
CONF_TRIGGER_RANGE,
|
||||
NumberSetting.NUMBER_TRIGGER_RANGE,
|
||||
0.6,
|
||||
None,
|
||||
0.1,
|
||||
MODE_PRESENCE,
|
||||
_distance(ICON_MOTION_SENSOR),
|
||||
),
|
||||
Setting(
|
||||
CONF_HOLD_SENSITIVITY,
|
||||
NumberSetting.NUMBER_HOLD_SENSITIVITY,
|
||||
0,
|
||||
9,
|
||||
1,
|
||||
MODE_PRESENCE,
|
||||
_plain(),
|
||||
),
|
||||
Setting(
|
||||
CONF_TRIGGER_SENSITIVITY,
|
||||
NumberSetting.NUMBER_TRIGGER_SENSITIVITY,
|
||||
0,
|
||||
9,
|
||||
1,
|
||||
MODE_PRESENCE,
|
||||
_plain(),
|
||||
),
|
||||
Setting(
|
||||
CONF_ON_LATENCY,
|
||||
NumberSetting.NUMBER_ON_LATENCY,
|
||||
0,
|
||||
2,
|
||||
0.01,
|
||||
MODE_PRESENCE,
|
||||
_duration(),
|
||||
),
|
||||
Setting(
|
||||
CONF_OFF_LATENCY,
|
||||
NumberSetting.NUMBER_OFF_LATENCY,
|
||||
2,
|
||||
1500,
|
||||
0.5,
|
||||
MODE_PRESENCE,
|
||||
_duration(),
|
||||
),
|
||||
Setting(
|
||||
CONF_INHIBIT_TIME,
|
||||
NumberSetting.NUMBER_INHIBIT_TIME,
|
||||
0.1,
|
||||
60,
|
||||
0.1,
|
||||
MODE_PRESENCE,
|
||||
_duration(),
|
||||
),
|
||||
Setting(
|
||||
CONF_THRESHOLD_FACTOR,
|
||||
NumberSetting.NUMBER_THRESHOLD_FACTOR,
|
||||
0,
|
||||
65535,
|
||||
1,
|
||||
MODE_SPEED_AND_DISTANCE,
|
||||
_plain(),
|
||||
),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
**{cv.Optional(s.key): s.schema for s in SETTINGS},
|
||||
}
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate_mode(
|
||||
{s.key: s.mode for s in SETTINGS if s.mode is not None}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub_id = config[CONF_DFROBOT_SEN0609_ID]
|
||||
hub = await cg.get_variable(hub_id)
|
||||
max_range = MAX_RANGES[get_hub_config(CORE.config, hub_id)[CONF_MODEL]]
|
||||
numbers = number.sub_numbers(config, parent=hub)
|
||||
for s in SETTINGS:
|
||||
await numbers(
|
||||
s.key,
|
||||
lambda n, setting=s.setting: hub.set_number(setting, n),
|
||||
s.setting,
|
||||
min_value=s.min_value,
|
||||
max_value=max_range if s.max_value is None else s.max_value,
|
||||
step=s.step,
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../dfrobot_sen0609.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
class DFRobotSEN0609Number final : public number::Number, public Parented<DFRobotSEN0609Component> {
|
||||
public:
|
||||
explicit DFRobotSEN0609Number(NumberSetting setting) : setting_(setting) {}
|
||||
|
||||
protected:
|
||||
void control(float value) override { this->parent_->control_number(this->setting_, value); }
|
||||
|
||||
NumberSetting setting_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -0,0 +1,29 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
from esphome.components.const import CONF_OUT_PIN_LEVEL
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
|
||||
|
||||
OutPinLevelSelect = dfrobot_sen0609_ns.class_("OutPinLevelSelect", select.Select)
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema(
|
||||
OutPinLevelSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
# The level the OUT pin has while a target is present; the index is the module's value
|
||||
await selects(
|
||||
CONF_OUT_PIN_LEVEL, hub.set_out_pin_level_select, options=["low", "high"]
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
#include "../dfrobot_sen0609.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
class OutPinLevelSelect final : public select::Select, public Parented<DFRobotSEN0609Component> {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) OutPinLevelSelect()` would zero-fill .bss that is already zero.
|
||||
OutPinLevelSelect() {}
|
||||
|
||||
protected:
|
||||
void control(size_t index) override { this->parent_->control_out_pin_level(index); }
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -0,0 +1,64 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_SPEED,
|
||||
ICON_SIGNAL,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_METER,
|
||||
UNIT_METER_PER_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CONF_DFROBOT_SEN0609_ID,
|
||||
MODE_SPEED_AND_DISTANCE,
|
||||
DFRobotSEN0609Component,
|
||||
final_validate_mode,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["dfrobot_sen0609"]
|
||||
|
||||
CONF_TARGET_DISTANCE = "target_distance"
|
||||
CONF_TARGET_ENERGY = "target_energy"
|
||||
CONF_TARGET_SPEED = "target_speed"
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_TARGET_DISTANCE): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_METER,
|
||||
),
|
||||
cv.Optional(CONF_TARGET_SPEED): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_SPEED,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_METER_PER_SECOND,
|
||||
),
|
||||
cv.Optional(CONF_TARGET_ENERGY): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
icon=ICON_SIGNAL,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate_mode(
|
||||
{
|
||||
CONF_TARGET_DISTANCE: MODE_SPEED_AND_DISTANCE,
|
||||
CONF_TARGET_ENERGY: MODE_SPEED_AND_DISTANCE,
|
||||
CONF_TARGET_SPEED: MODE_SPEED_AND_DISTANCE,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TARGET_DISTANCE, hub.set_target_distance_sensor)
|
||||
await sensors(CONF_TARGET_SPEED, hub.set_target_speed_sensor)
|
||||
await sensors(CONF_TARGET_ENERGY, hub.set_target_energy_sensor)
|
||||
@@ -0,0 +1,68 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_SWITCH,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_LIGHTBULB,
|
||||
ICON_MOTION_SENSOR,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DFROBOT_SEN0609_ID,
|
||||
MODE_SPEED_AND_DISTANCE,
|
||||
DFRobotSEN0609Component,
|
||||
dfrobot_sen0609_ns,
|
||||
final_validate_mode,
|
||||
)
|
||||
|
||||
DFRobotSEN0609Switch = dfrobot_sen0609_ns.class_("DFRobotSEN0609Switch", switch.Switch)
|
||||
SwitchSetting = dfrobot_sen0609_ns.enum("SwitchSetting")
|
||||
|
||||
CONF_MICRO_MOTION = "micro_motion"
|
||||
CONF_OUT_LED = "out_led"
|
||||
CONF_RUN_LED = "run_led"
|
||||
|
||||
|
||||
def _led() -> cv.Schema:
|
||||
# The module forgets the LED state at power off, so it is restored from here
|
||||
return switch.switch_schema(
|
||||
DFRobotSEN0609Switch,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_LIGHTBULB,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_MICRO_MOTION): switch.switch_schema(
|
||||
DFRobotSEN0609Switch,
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Optional(CONF_RUN_LED): _led(),
|
||||
cv.Optional(CONF_OUT_LED): _led(),
|
||||
}
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate_mode(
|
||||
{CONF_MICRO_MOTION: MODE_SPEED_AND_DISTANCE}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
for key, setting in (
|
||||
(CONF_MICRO_MOTION, SwitchSetting.SWITCH_MICRO_MOTION),
|
||||
(CONF_RUN_LED, SwitchSetting.SWITCH_RUN_LED),
|
||||
(CONF_OUT_LED, SwitchSetting.SWITCH_OUT_LED),
|
||||
):
|
||||
await switches(
|
||||
key, lambda s, setting=setting: hub.set_switch(setting, s), setting
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../dfrobot_sen0609.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
class DFRobotSEN0609Switch final : public switch_::Switch, public Parented<DFRobotSEN0609Component> {
|
||||
public:
|
||||
explicit DFRobotSEN0609Switch(SwitchSetting setting) : setting_(setting) {}
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override { this->parent_->control_switch(this->setting_, state); }
|
||||
|
||||
SwitchSetting setting_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -0,0 +1,29 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
|
||||
|
||||
DEPENDENCIES = ["dfrobot_sen0609"]
|
||||
|
||||
CONF_HARDWARE_VERSION = "hardware_version"
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
|
||||
),
|
||||
cv.Optional(CONF_HARDWARE_VERSION): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
|
||||
await text_sensors(CONF_HARDWARE_VERSION, hub.set_hardware_version_text_sensor)
|
||||
@@ -35,16 +35,13 @@ void DHT12Component::update() {
|
||||
void DHT12Component::setup() {
|
||||
uint8_t data[5];
|
||||
if (!this->read_data_(data)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
void DHT12Component::dump_config() {
|
||||
ESP_LOGD(TAG, "DHT12:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
@@ -843,7 +843,6 @@ constexpr uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19,
|
||||
void Display::test_card() {
|
||||
int w = get_width(), h = get_height(), image_w, image_h;
|
||||
this->clear();
|
||||
this->show_test_card_ = false;
|
||||
image_w = std::min(w - 20, 310);
|
||||
image_h = std::min(h - 20, 255);
|
||||
int shift_x = (w - image_w) / 2;
|
||||
|
||||
@@ -357,12 +357,17 @@ class Display : public PollingComponent {
|
||||
* \param ptr A pointer to the start of the data to be copied
|
||||
* \param order The ordering of the colors
|
||||
* \param bitness Defines the number of bits and their format for each pixel
|
||||
* \param big_endian True if 16 bit values are stored big-endian
|
||||
* \param big_endian True if 16 bit values are stored big-endian. Ignored for 888, which is always b,g,r
|
||||
* in memory for RGB order, as LVGL and ESPHome images store it.
|
||||
* \param x_offset The initial x-offset into the source buffer.
|
||||
* \param y_offset The initial y-offset into the source buffer.
|
||||
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
|
||||
*
|
||||
* The length of each source buffer line (stride) will be x_offset + w + x_pad.
|
||||
*
|
||||
* An override that copies the block as is must send any other format, byte order, a software
|
||||
* rotation or an active clipping rectangle through this base version, which draws per pixel.
|
||||
* The block must lie on the screen.
|
||||
*/
|
||||
virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
|
||||
@@ -23,9 +23,8 @@ class ColorUtil {
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
// Always the LVGL RGB888 layout, b,g,r in memory for RGB order; byte order applies to 565 only
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user