# Firmware Review β€” `blackpillrtkcode.ino` (v1 β†’ v2) Review of the as-deployed watchdog firmware, with the fixes shipped in `04-Source-Code/firmware/blackpillrtkcode_v2/blackpillrtkcode_v2.ino`. Severity: πŸ”΄ likely field-impacting Β· 🟠 robustness Β· 🟑 cleanup. UBX command checksums and ADC divider math were **verified programmatically** (see end of this doc) β€” those parts of v1 are correct. --- ## Issues found ### πŸ”΄ 1. Loss-of-corrections recovery targets the wrong subsystem **v1:** after `SOFT_RESET_DELAY` (5 min) of `fixQuality < 4`, it sends `UBX_SOFT_RESET` (a GNSS hot-reset) to the ZED-F9P. **Problem:** corrections arrive over the **4G NTRIP modem**, not from the GNSS. Hot-resetting the receiver does nothing for a hung modem β€” the dominant field failure per the client. Five minutes are spent on a no-op before the only effective action (a full power-cycle at 10 min). **v2:** drops the GNSS hot-reset; recovery = **power-cycle the whole stack** (which restarts the 4G module), gated by a grace window and backoff. ### πŸ”΄ 2. Hard-reset has no backoff β†’ reboot-loop / Wialon flapping **v1:** after `HARD_RESET_DELAY` (10 min) it power-cycles, returns to BROWNOUT, immediately powers back on, and β€” if the fault persists β€” does it **again every ~10 min indefinitely**. Each cycle is a multi-minute disconnect. **Problem:** a genuinely dead 4G card, or a unit parked without sky view, makes the unit **flap off Wialon forever**. This is a prime suspect for "desconexiones recurrentes." **v2:** **exponential backoff** (1β†’2β†’4β†’8β†’16 min, capped 20 min) then a **latched LIMP mode** that keeps the unit **reporting standalone GPS** and only retries recovery every 30 min. The unit never silently vanishes. ### πŸ”΄ 3. No detection of a fully hung stack (NMEA silence) **v1:** only reacts via `fixQuality`. If the GNSS/4G stack hangs and stops emitting NMEA entirely, `fixQuality` simply freezes at its last value β€” the watchdog may not act at all, or acts on stale data. **v2:** tracks `lastNmeaMs`; **no NMEA for 60 s while powered β‡’ power-cycle now**. Catches hung hardware far faster than the fix-quality ladder. ### 🟠 4. No hysteresis / debounce on vehicle-power & ignition **v1:** `pa4_on = (v5vF > 4.0)` is a single hard threshold; ignition is a raw `digitalRead`. Around the threshold (alternator noise, ignition flicker), the rate mode (1 Hz↔30 s) and state can chatter. **v2:** hysteresis band (ON >4.2 V, OFF <3.8 V) + 750 ms debounce on both vehicle-power and ignition. ### 🟠 5. Brownout exit has no debounce (oscillation risk) **v1:** entry to BROWNOUT is debounced 3 s, but the **exit** powers on the instant `pa4_on || pa5_on || vBat>3.7`. A brief blip near the threshold can start a power-on it can't sustain β†’ drain β†’ repeat. **v2:** wake is driven by the same debounced/hysteretic signals; brownout entry uses a 5 s confirmation. ### 🟠 6. No thermal awareness (the actual field failure mode) **v1:** nothing measures temperature, despite "died from heat." **v2:** reads the STM32 internal temp sensor (F411 factory cal), logs session **min/max**, **derates to 30 s** above 75 Β°C to cut self-heat/power, and shows it on the dashboard. Firmware can't cool the box, but a pulled unit's `tMax` now **proves or kills the thermal hypothesis**. (Disable with `ENABLE_MCU_THERMAL 0` if your core lacks `ATEMP`/`AVREF`.) ### 🟑 7. `String` heap churn on an ARM MCU **v1:** comments claim `const char*` avoids fragmentation, but `nmeaBuffer` is a `String`, and `drawDashboard` builds a `String paddedMsg` every refresh. **v2:** fixed `char nmeaBuf[110]` parser (no heap) and `snprintf` padding. ### 🟑 8. Doc/firmware mismatch: boot delay The LED table says "waiting 25 s"; v1 `BOOT_STABILIZE_TIME` is **15 s**. **v2:** set to **25 s** to match the documented SOP (gives the 4G modem more time to attach before the supervisor starts judging the link). Tune on the bench. ### 🟑 9. Minor: redundant `pinMode` before `GNSS.begin()` Harmless (the serial peripheral reconfigures the pins); left as explicit intent in v2. --- ## What v1 got *right* (kept in v2) - **Float is not treated as failure.** `fixQuality >= 4` correctly counts both RTK **fixed (4)** and **float (5)** as healthy. (Earlier verbal claim that FLOAT triggered resets was wrong β€” corrected here.) - Floating the TX pin on power-off to avoid back-powering the rail through a GPIO. - Low-pass filtering the ADC readings. - Hysteresis between `BAT_CRITICAL` (3.4 V) and `BAT_RECOVERY` (3.7 V). - The UBX rate commands and their checksums (verified below). --- ## v2 recovery state machine (summary) ``` SLEEP ──(vehicle pwr | ignition | Vbat>3.7)──► BOOTING ──(25 s)──► RUNNING β–² β”‚ β”‚ Vbat<3.4 & no pwr & no ign (5 s) β”‚ └───────────────────────────────────────────────────────── RUNNING supervises: β€’ no NMEA 60 s ........................β–Ί POWER_CYCLE (gated by backoff) β€’ RTK lost > 120 s grace .............β–Ί POWER_CYCLE (gated by backoff) β€’ RTK healthy 5 min .................β–Ί clear backoff + LIMP latch POWER_CYCLE (6 s rail off) ──► BOOTING ; after 5 fails ──► LIMP (keep reporting) ``` --- ## Verification performed (2026-06-19) - **UBX checksums** recomputed (8-bit Fletcher over class…payload): `UBX_1HZ` β†’ `01 39` βœ“, `UBX_30S` β†’ `1E AD` βœ“, `UBX_SOFT_RESET` β†’ `0F 66` βœ“. - **CFG-RATE decode:** 1 Hz = measRate 1000 ms Γ— navRate 1; 30 s = 1000 ms Γ— 30 βœ“. - **ADC dividers (12-bit, 3.3 V, Γ·2):** LiPo 3.4–4.2 V β†’ 1.70–2.10 V at pin (2110–2606 counts); 5 V rail β†’ 2.50 V (3102 counts). No ADC clipping βœ“. - **Backoff ladder:** 1, 2, 4, 8, 16 min β†’ LIMP @ 30 min, as designed βœ“. - **Compile:** v2 builds clean under `g++ -std=c++14 -Wall -Wextra` against a hardware stub (logic/types/flow only). > ⚠️ The stub compile validates **logic**, not the STM32 target. **Bench-flash and > test v2 on real hardware** (Arduino IDE / PlatformIO, STM32duino core) before > any field deployment β€” especially the thermal read and the power-cycle timing.