diff --git a/releasenotes/RELEASE_NOTES_1.17.0-zephcore.md b/releasenotes/RELEASE_NOTES_1.17.0-zephcore.md index ea8f816..9da5cc0 100644 --- a/releasenotes/RELEASE_NOTES_1.17.0-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.17.0-zephcore.md @@ -1,198 +1,181 @@ # ZephCore 1.17.0-zephcore +A **hardware release**. A new tracker board lands — the Seeed SenseCAP MeshTracker X1 — and with it the +LR2021 radio becomes a real, on-air-validated option instead of bring-up code. Plus GPS diagnostics, an +LED master switch, and a batch of receive-path hardening. + +--- + > [!IMPORTANT] > ## Before you upgrade > -> **From v1.16.5 – v1.16.8** — clean flash, no re-bond, bonds and data survive. +> **From v1.16.5 – v1.16.8** — just flash it. Bonds and data survive, no re-pairing. > -> **From v1.16.2 / v1.16.3 / v1.16.4** — clean flash, bonds and data survive. If you are on an -> **ESP32-S3 / ESP32-C board** and have *not* yet taken the v1.16.5 update, you still owe the one-time -> serial reflash: the app moved to flash offset `0x10000` in v1.16.5, and a board on the old layout -> cannot cross that update over WiFi-OTA or the browser flasher — **flash the `-merged.bin` once over -> USB/serial.** Affected boards: *Heltec V3 / V4 / V4.3, Station G2, Wireless Tracker / V2, XIAO -> ESP32-S3 / C3 / C6, LilyGo T-Lora C6, ThinkNode M9.* Identity, contacts, channels, prefs, and BLE -> bonds are preserved. nRF52, classic ESP32 (T-Beam / PICO-D4 / TTGO LoRa32), STM32WL, and native -> Linux are unaffected. +> **From v1.16.2 – v1.16.4 on an ESP32-S3 / ESP32-C board** — you still owe a one-time **USB/serial +> reflash of `-merged.bin`**. The app moved to flash offset `0x10000` in v1.16.5, and a board on the old +> layout cannot cross that gap over WiFi-OTA or the browser flasher. Affected: *Heltec V3 / V4 / V4.3, +> Station G2, Wireless Tracker / V2, XIAO ESP32-S3 / C3 / C6, LilyGo T-Lora C6, ThinkNode M9.* Your +> identity, contacts, channels, prefs and bonds are preserved. Everything else (nRF52, classic ESP32, +> STM32WL) upgrades normally. > -> **From v1.16.1 or older** — flash it; on first boot it clears BLE bonds automatically (identity, -> contacts, channels, prefs preserved). Re-bond your phone/desktop once. +> **From v1.16.1 or older** — flash it; BLE bonds are cleared on first boot, so re-pair your phone once. +> Identity, contacts, channels and prefs are kept. > -> **Coming from Arduino MeshCore** — flash it; auto-formats on first boot (new identity, clean storage). +> **Coming from Arduino MeshCore** — flash it; storage auto-formats and you get a new identity. > -> **ThinkNode M6 owners: this release stops using the external flash chip, because the board does not -> have one.** Contacts, channels and prefs move to internal flash on first boot. See the M6 section. +> **ThinkNode M6 owners** — this release stops using the external flash chip, because the board doesn't +> actually have one. Contacts, channels and prefs move to internal flash on first boot. > -> **Repeater and room-server telemetry no longer hands out your GPS position to non-admins.** -> If you were relying on public position reporting from a fixed node, set the location in prefs and -> advertise it instead. +> **Repeater / room-server telemetry no longer shares GPS position with non-admins.** If you were +> relying on that, set the location in prefs and advertise it instead. > -> **ESP32 light sleep (new in 1.16.8) is still awaiting hardware validation.** It remains on by default -> for Heltec repeaters, and their USB CLI still stops answering 10 minutes after boot as a result — -> reconnecting the terminal resets the node and gives you another window. Rebuild with `-DCONFIG_PM=n` -> to take it out. +> **ESP32 light sleep** stays on by default for Heltec repeaters. It has now been confirmed working on a +> Heltec V4 — but only that one board so far. Side effect: the USB CLI stops answering ~10 minutes after +> boot; reconnect the terminal to get another window. Rebuild with `-DCONFIG_PM=n` to remove it. --- -A **hardware and receive-path release**. A new tracker board lands (Seeed SenseCAP MeshTracker X1, the -first LR2021 target with real hardware behind it), the LR2021 gains multi-SF receive, the LR11xx and -LR20xx receive gates get the same stuck-state bound the SX126x got in 1.16.x, GPS module configuration -grew a diagnostics path and a dynamic-model setting, and the Zephyr pin moves to 4.4.2 final. +## New board — Seeed SenseCAP MeshTracker X1 -## Highlights +`meshtracker_x1` — nRF52840 + **Semtech LR2021**, 863–928 MHz / 22 dBm, dual-band L1+L5 GNSS, +barometer, haptic motor, RGB LED, buzzer, 1100 mAh battery, 8 MB external flash and an on-board RTC. +Companion, repeater and room server all build and run. -### New board — Seeed SenseCAP MeshTracker X1 +It is essentially a T1000-E with a different radio, so most of the board reuses well-travelled code. +What's new: -`meshtracker_x1` — nRF52840 + **Semtech LR2021**, 863–928 MHz / 22 dBm, dual-band L1+L5 Airoha GNSS, -SPA06-003 barometer, DRV2605L haptic motor, RGB LED, buzzer, 1100 mAh high-voltage LiPo, 8 MB QSPI -flash and an on-board RTC. Companion, repeater and room-server all build. +- **Barometer support** — a from-scratch SPA06-003 driver. Pressure and temperature are meant to show + up in normal environment telemetry. **Not yet confirmed on hardware.** +- **Haptic notifications.** The buzzer setting became a four-way notification mode: + `set buzzer 0|1|2|3` = silent / sound+vibrate / vibrate / sound, and the 3-tap button cycles them. + Boards without a motor keep the plain on/off toggle. **The X1's motor and buzzer are not yet + confirmed working** — see Known limitations. +- **8 MB external flash** for contacts, channels and the blob cache, plus the on-board RTC. -Hardware-wise it is a T1000-E with the radio swapped, so the LoRa SPI pins, button, buzzer, battery -divider and the whole GNSS control pin set map straight onto the existing T1000-E paths. What is new -on this board: +## The LR2021 radio is now validated on hardware -- **SPA06-003 barometer driver, written from scratch** (`adapters/sensors/spa06.c` plus its binding) — - no upstream Zephyr driver exists. It lives in the app tree rather than the Zephyr tree, so it needs - no patch and survives `west update`. Pressure and temperature are reported through the normal - environment-telemetry path. -- **Haptic notifications.** `helpers/ui/haptic.c` wraps Zephyr's `ti,drv2605`, and the buzzer setting - became a four-state notification mode — `set buzzer 0|1|2|3` = silent / sound+vibrate / vibrate / - sound, with the 3-tap button cycling through them. Boards with no motor keep a plain on/off toggle. - The value rides in the existing `buzzer_quiet` byte, so there is no prefs migration. -- **8 MB of QSPI flash as `/ext`** for contacts, channels and the blob cache, and the on-board - YSN8900 RTC on its own I²C bus, discovered and BCD-validated at boot. +Previously this driver was marked experimental with no board behind it. It has now had a full audit and +an on-air validation pass on the X1 — transmit, receive, listen-before-talk and RX duty cycle are all +confirmed working. Highlights of what changed to get there: -> [!NOTE] -> The **LR2021 radio path is still bring-up code.** The driver is ZephCore's own, and only one LR2021 board -> has been validated on air yet. Treat X1 radio behaviour as untested; the rest of the board (GNSS, -> sensors, storage, UI, power) is on well-travelled paths. +- **The chip's firmware patch is now loaded at boot** (Semtech "highly recommends" it; without it the + chip runs unpatched). The `PRAM loaded:` log line is proof it took. +- **Listen-before-talk runs on the chip itself** — it checks the channel and transmits in one shot with + no round-trip to the CPU. Longer transmissions automatically fall back to the classic route. +- **RX duty cycle (sniff mode) works**, with the sleep/wake timing derived from the datasheet rather + than estimated. +- **Several datasheet-conformance bugs fixed**, the most user-visible being a receive filter that was + not restored after transmitting — a node would silently stop hearing any packet longer than its own + last transmission. -### LR2021 multi-SF receive — `set extra.sf` +`promicro_lr2021` still builds but remains untested (the module was destroyed during bring-up). -The LR2021 can demodulate up to **three extra spreading factors at once**, alongside the configured -one, on the same bandwidth. One repeater can therefore serve several SF communities without splitting -the site, and which SF a packet arrived on is a chip-side readout rather than an inference. +## LR2021 multi-SF receive — `set extra.sf` + +The LR2021 can listen on **up to three extra spreading factors at once**, on the same bandwidth. One +repeater can serve several SF communities without splitting the site. ``` -set extra.sf 9 10 # receive SF9 and SF10 in addition to the configured SF +set extra.sf 9 10 # also receive SF9 and SF10 get extra.sf # -> 9,10 -set extra.sf off # back to single-SF receive +set extra.sf off # back to single-SF ``` > [!IMPORTANT] -> **Receive only, and the bridge it creates is one-way.** Transmission always uses the single -> configured `sf`, and every detector shares one bandwidth — multi-SF, not multi-channel. +> **Receive only — the bridge it creates is one-way.** Transmission always uses your configured `sf`. +> A node on `sf 7` with `extra.sf 8` hears and forwards SF8 traffic, but forwards it at SF7. So traffic +> flows **SF8 → SF7 and never back**. Adverts and flood traffic propagate fine; an SF8 node's direct +> messages get delivered but its ACKs never arrive, so it retries to its limit every time. > -> A node running `sf 7` with `extra.sf 8` hears SF8 traffic and *does* forward it, but the forward goes -> out at SF7. Traffic therefore moves **SF8 → SF7 only, and nothing comes back**. Adverts and one-way -> flood traffic propagate fine; an SF8 node's direct messages get delivered but its ACKs never arrive, -> so it retries to its limit every time. That is worth weighing before deploying it next to a busy -> slow-SF island. -> -> The direction cannot be reversed. Because every extra SF must be *greater* than `sf`, the main SF is -> always the lowest in the set, and TX always uses the main SF — so a side-detector node is always -> high-SF-in / low-SF-out. Two of them back to back point the same way; **no configuration carries -> SF7 → SF8.** Think of this as a collector that pulls slower-SF stragglers into a fast mesh, not as a -> link between two SF islands. A genuine bidirectional bridge needs two nodes joined by a non-LoRa -> link, or one node time-slicing its radio config — neither of which this feature provides. +> There is no configuration that reverses this — extra SFs must always be *higher* than your main SF, +> and TX always uses the main SF. Treat it as a collector that pulls slower-SF stragglers into a fast +> mesh, not as a link between two SF islands. -The chip's constraints are enforced in the driver, so an accepted set is a valid one: every extra SF -must be **greater** than the configured `sf`, all distinct, highest−lowest ≤ 4, and at BW ≥ 500 kHz at -most two (only one when `sf` ≥ 10). The setting is persisted and re-applied on every RX entry; a set -that no longer fits after an `sf` or `bw` change is refused at boot and logged rather than silently -half-applied. `Error: unsupported` on every non-LR2021 radio. +The chip's rules are enforced for you, so anything accepted is valid: extra SFs must be higher than +`sf`, distinct, within 4 of each other, and at BW ≥ 500 kHz at most two. A set that no longer fits +after you change `sf` or `bw` is refused at boot and logged. `Error: unsupported` on other radios. -One thing worth knowing about how this interacts with listen-before-talk: the chip's SF constraint for -**CAD** is the exact inverse of the one for **receive** — CAD wants the main SF above the side SFs, -receive wants it below. Both cannot hold at once, so the driver switches side detectors off for each -LBT CAD and restores them when RX re-arms. That costs two extra SPI commands per transmission and -needs no configuration. (Arduino MeshCore hit the same wall from the other direction, as a `-706` -error on the receive restart after a hardware CAD.) +## No more silent mute-until-reboot -### The LR11xx receive gate can no longer wedge +All four radio drivers gate transmission on "am I currently receiving?" — and on the LR11xx and LR2021 +that answer could get stuck true forever if a packet header arrived but the packet never completed. +The node would keep receiving and never transmit again, with nothing in the log pointing at it. Both +now bound that state with a generous deadline (max-length packet airtime + margin) and log a warning if +it ever fires. The SX126x got the same fix in 1.16.x. -The driver reports "we are currently receiving" from the chip's latched PREAMBLE_DETECTED and -HEADER_VALID bits, and that answer is what gates every transmission. Neither bit is self-clearing: -they are dropped only by a terminal interrupt or an RX restart, and in continuous receive there is no -symbol timer, so neither is guaranteed to arrive. A header whose packet never completes would pin the -gate true **forever** — the node keeps receiving but never transmits again, until reboot, with nothing -in the log pointing at it. +Related: an error-only interrupt on the LR2021 no longer triggers a receive restart that regenerates +the same error, which on the X1 could cost ~88 ms of deafness and a hardware reset. -This is the same class of fault the SX126x driver was hardened against earlier (`0013`), and it is now -closed on the LR11xx: +## Adaptive CAD now uses only levels the chip can distinguish -- **LR11xx (LR1110)** already had the preamble grace; it gains the payload deadline. -- **LR20xx (LR2021)** still has an unbounded gate — a bare OR of the two bits. The bound was written - for it too, then pulled back out: it landed alongside a duty-cycle rework that regressed receive on - the MeshTracker X1, and the whole group was reverted to the last configuration confirmed working on - hardware. It will return once the LR2021 receive path is settled. +The auto-tuning channel-detection threshold walks a range of levels, but where those levels fell +outside the radio's own hardware limits, several of them programmed the *identical* setting — so the +tuner was comparing physically identical rungs and reading noise as a trend. The usable window is now +derived per-driver, so every level is a distinct configuration and the `pk` value in `get cad` is what +the chip actually got. This mattered most on the LR2021. -The deadline is the airtime of a maximum-length packet at the current SF/BW at coding rate 4/8 with -LDRO pinned on, plus 25% and 100 ms — deliberately generous, because this is a stuck-state safety net -and releasing early would start a transmission on top of a packet that is still arriving. When it -fires it logs a warning, which is the tripwire that would prove the fault happens in the field; it has -never been reproduced on a bench. +## Virtual-contact fixes (companion) -### ThinkNode M6 — the external flash is gone, because the chip is not there +The built-in admin chat contact got three fixes worth knowing about: -The M6 routes a QSPI bus and its datasheet lists an MX25R1635F, so ZephCore declared the part, gave it -a power-enable regulator, and mounted it as `/ext`. Upstream Arduino MeshCore has since dropped the -same assumption after hitting it on real hardware — *"M6 doesn't have flash chip available, so -companion roles fail to start"* — which means companion builds most likely never came up on this board -here either. +- **Deleting it in the app is now temporary.** It comes back on the next app connect. Previously a + "purge all contacts" in the app permanently disabled the feature with no way back except the USB CLI. + The only durable off switch is `set v.contact off` on the node. +- **Favourite star and telemetry permissions stick** across reconnects and reboots. +- **"Last seen" no longer grows forever**, and the contact re-syncs to the app properly instead of + being sent exactly once ever. -The flash node, its partition, the enable regulator and the `/ext` mount are removed. Everything now -lives on internal flash: identity, prefs, contacts and channels all on `/lfs`. This is the second -ThinkNode M6 assumption disproved by upstream bench data in as many releases — the GPS REINIT pin was -the first — so the board is worth treating as under-verified generally. +## GPS: dynamic model and a diagnostics readout -### GPS module configuration: dynamic model, and a diagnostics readout +- **`CONFIG_ZEPHCORE_GPS_NAV_MODE`** sets the dynamic model on CASIC modules (Quectel L76K / L76KB, + Air530Z) — stationary for repeaters and room servers, automotive otherwise. Worth setting: **the + model is stored in the module** and survives reflashing the host, so a second-hand module can arrive + stuck in an airborne profile and behave oddly on a rooftop. +- **`set gps diag on` / `get gps diag`** shows what the last module-configuration attempt actually did: + which path ran, bytes sent, and tracked satellites per constellation. Not persisted. -Two additions on the GNSS side, both aimed at the thing that is hardest to see from outside: whether -the module actually accepted what the firmware sent it. - -- **`CONFIG_ZEPHCORE_GPS_NAV_MODE`** sets the dynamic model on CASIC-family modules (Quectel L76K / - L76KB, Air530Z) — stationary by default for repeaters and room servers, automotive otherwise. This - is worth setting rather than leaving alone, because **the model is stored in the module** and - survives reflashing the host: a module pulled from another device can arrive stuck in an airborne - profile and behave oddly on a rooftop. -- **`set gps diag on` / `get gps diag`** reports what the last module-configuration attempt actually - did: which path ran, how many bytes were sent, and tracked satellites per constellation. Not - persisted — it clears on reboot. - -### `set leds on|off` — one master switch +## `set leds on|off` — one master switch A single persisted switch for every LED on the node, applied live: heartbeat, unread-message and LoRa -TX-activity LEDs, plus the message and shutdown flashes. It works on every role, including headless -repeaters where the TX LED is the only one that ever lights. The display backlight is not covered — -that is the separate UI brightness setting. +TX-activity LEDs, plus the message and shutdown flashes. Works on every role, including headless +repeaters. Display backlight is separate (that's the UI brightness setting). -### Repeater and room-server GPS position is now admin-only +## Repeater / room-server GPS position is admin-only -Telemetry from a repeater or room server used to include the node's GPS position for any requester -holding the location permission. It is now released to **admins only**. A fixed node that should -publish its position can still do so through prefs and adverts, which is the deliberate, visible way -to share it. +Telemetry from a repeater or room server used to hand out its GPS position to anyone holding the +location permission. It now goes to **admins only**. A fixed node that should publish its position can +still do so through prefs and adverts. + +## ThinkNode M6 — external flash removed + +The M6's datasheet lists a flash chip that the board doesn't have, which almost certainly meant +companion builds never came up on it. The flash node, partition, power regulator and `/ext` mount are +gone; identity, prefs, contacts and channels now all live on internal flash. This is the second M6 +datasheet claim disproved by real hardware in as many releases, so treat this board as under-verified +generally. ## Other changes -- **Zephyr pinned to 4.4.2 final** (was a 4.4.2 pre-release commit). -- **ESP32: the user button is now a light-sleep wake source** on boards using the shared PM overlay, so - a sleeping node wakes on a button press rather than only on a received packet. -- **LR2021 SPI HAL resynced with RadioLib**, and the driver picked up a batch of datasheet-conformance - fixes: the TCXO start-up deadline is counted in 32 MHz periods rather than RTC ticks (the previous - unit turned a 5 ms allowance into 5 µs and guaranteed a start-up error), front-end calibration is - issued in its full three-slot form, the DIO interrupt mask is narrowed to the events the handler - actually services, the CAD detection thresholds now come from the datasheet's 4-symbol row to match - the 4-symbol window actually used, and the receive payload-length filter is restored after each - transmission — without that, a node silently stopped hearing any packet longer than its own last - transmission. -- **Adaptive-CAD and noise-floor sampling** picked up further fixes in the SX126x patch set. +- **Zephyr pinned to 4.4.2 final.** +- **ESP32: the user button now wakes the node from light sleep** on boards using the shared PM overlay + — previously only an incoming packet would. +- **`get freqerr`** (LR2021 only) reports carrier frequency error measured on received packets. Purely + diagnostic — nothing acts on it, and small values are the expected answer. +- **X1 external flash runs in quad mode**, and the LR2021 SPI layer was resynced with RadioLib. +- Further adaptive-CAD and noise-floor sampling fixes in the SX126x driver. ## Known limitations -- **LR2021 is not hardware-validated.** No LR2021 board has been confirmed on air. The X1's radio path - and everything in the multi-SF feature above are code-complete and build-verified only. -- **ESP32 light sleep is not hardware-validated** and remains on by default for Heltec repeaters. The - failure mode to rule out is a deaf repeater; check the boot log for `Pin N is not wakeup capable`. -- **ThinkNode M3 / M6 GPS fixes are not hardware-confirmed** — both were derived from upstream bench - data on their respective boards, not measured here. +- **ESP32 light sleep is validated on one board only** — a Heltec V4, where it works. It is on by + default for all Heltec repeaters, so the other variants are running it untested. The failure mode to + watch for is a deaf repeater; check the boot log for `Pin N is not wakeup capable`. +- **ThinkNode M3 / M6 GPS fixes are not confirmed here** — both came from upstream bench data on those + boards, not measured on ours. +- **`promicro_lr2021` is untested.** The X1 is the only validated LR2021 board. +- **The X1's barometer, haptic motor and buzzer are not validated.** The radio, GNSS, storage, UI and + power paths are; these three are not. The haptic driver is currently failing to come up at boot + (`haptic drv2605@5a not ready`) and is under investigation. +- **The X1's on-board RTC does not hold time across a reboot.** Its supply is switched by an MCU pin, + which goes high-impedance on reset, so the chip raises its power-loss flag and the clock is re-set + from the next GPS/app/CLI sync. Time is still correct in normal operation; it just isn't preserved + across a restart. diff --git a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts index 6f840f1..7491a1f 100644 --- a/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts +++ b/zephcore/boards/nrf52840/meshtracker_x1/meshtracker_x1_nrf52840.dts @@ -291,11 +291,24 @@ reg = <0x77>; }; + /* Deferred-init so haptic_init() owns the power-up sequence. + * + * Upstream's drv2605_init() spends DRV2605_POWER_UP_DELAY_US *before* + * drv2605_gpio_config() raises en-gpios, then reads the status register + * over I2C immediately after — so the part gets no settling time at all + * between EN rising and its first transaction, and NACKs. Deferring the + * node is what lets us assert EN and wait first; without it the driver + * has already run (and failed) by the time the app gets a say, and + * device_init() would just answer -EALREADY. + * + * Not a rail problem: the SPA06 shares this bus, this rail and this init + * priority and comes up fine. The enable pin is the only difference. */ haptic: drv2605@5a { compatible = "ti,drv2605"; reg = <0x5a>; actuator-mode = "LRA"; en-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; + zephyr,deferred-init; }; }; diff --git a/zephcore/helpers/ui/haptic.c b/zephcore/helpers/ui/haptic.c index 51cfb21..fd47389 100644 --- a/zephcore/helpers/ui/haptic.c +++ b/zephcore/helpers/ui/haptic.c @@ -14,11 +14,44 @@ LOG_MODULE_REGISTER(zephcore_haptic, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL); #if DT_HAS_COMPAT_STATUS_OKAY(ti_drv2605) +#include #include #include #define HAPTIC_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(ti_drv2605) +#if DT_PROP(HAPTIC_NODE, zephyr_deferred_init) +/* Wake a deferred-init DRV2605 and run its init here, where the part's supply + * rail has been up since boot instead of microseconds. + * + * The enable pin is asserted *before* device_init() on purpose. Upstream's + * drv2605_init() spends its DRV2605_POWER_UP_DELAY_US before it configures the + * GPIOs, so EN rises and the very next thing it does is read the status + * register over I2C — no settling time at all between the two. Raising EN + * ourselves and waiting turns that race into a plain sequence; the driver's own + * gpio_pin_configure_dt() to OUTPUT_ACTIVE afterwards is then a no-op. */ +static void haptic_deferred_init(const struct device *dev) +{ + static const struct gpio_dt_spec en = + GPIO_DT_SPEC_GET_OR(HAPTIC_NODE, en_gpios, {0}); + int rc; + + if (en.port != NULL && gpio_is_ready_dt(&en)) { + if (gpio_pin_configure_dt(&en, GPIO_OUTPUT_ACTIVE) == 0) { + /* 250 us is the datasheet figure; 1 ms costs nothing on a + * one-shot boot path and covers the rail as well. */ + k_msleep(1); + } + } + + rc = device_init(dev); + LOG_DBG("haptic deferred init: rc=%d ready=%d", rc, + (int)device_is_ready(dev)); +} +#else +static inline void haptic_deferred_init(const struct device *dev) { ARG_UNUSED(dev); } +#endif + static const struct device *haptic_dev; static bool haptic_enabled = true; @@ -36,6 +69,9 @@ int haptic_init(void) int rc; haptic_dev = DEVICE_DT_GET(HAPTIC_NODE); + if (!device_is_ready(haptic_dev)) { + haptic_deferred_init(haptic_dev); + } if (!device_is_ready(haptic_dev)) { /* The DRV2605 driver logs its own reason at debug level, so say * enough here to tell "chip absent" from "bus down". */