Drogheda/03-Architecture-Hardware/watchdog-health-telemetry.md

239 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
tags: [project-drogheda, hardware, firmware, watchdog, telemetry, nmea]
project: Project Drogheda
author: Helgen CTO
date: 2026-06-20
status: Analysis — review & bench-test before any wiring/firmware change
---
# Watchdog Health Telemetry — Feasibility & Improvement Analysis
> **The question:** can the STM32F411 watchdog transmit its health (temp, reset
> cause, link state, recovery count…) over UART so it **lands in our server
> message**, e.g. as a custom `$GPTXT`? You've seen mixed information. This
> resolves it against the **as-built wiring** and proposes a clean design.
---
## TL;DR
- **As wired today: No.** The watchdog sits on the F9P's **UART1** (it reads NMEA
and sends UBX rate commands). The data that reaches the server flows out the
F9P's **UART2 → 4G NTRIP Master → server**. The watchdog is **not on that path**,
so nothing it prints can reach the backend.
- The `$GPTXT,NET,<id>,TELCEL,<rssi>,<cell>` you see at the server is the **4G
modem's** own network-status line (the `<id>` is the modem's station ID set via
`AS+K`, which the backend treats as the `cell_id`). It is **not** produced by the
F9P or the watchdog.
- **It is achievable** — but it requires putting the watchdog **in the modem's
NMEA-uplink path** (a relay/injector) and emitting a **proprietary sentence**
(`$PHLG…`, *not* `$GPTXT` — see the collision note). This is a **wiring change**,
clean on the Phase-2 PCB, and it must be **bench-validated** against one unknown:
*does the ArduSimple 4G NTRIP Master forward arbitrary/proprietary NMEA, or only a
whitelist?* That single question is the source of the "mixed information."
- **Lower-risk interim (no rewiring):** the watchdog already has a full health
record — ship the **v2.1 parseable log** (`docs/LOGGING-SPEC.md`) over its console.
It's authoritative per-unit health, captured on pulled units; it just isn't *live
to the server*. In-band-to-server telemetry is the Phase-2 upgrade.
---
## 1. The as-built data path (why the answer is "not yet")
```text
┌──────────────── simpleRTK2B Pro (ZED-F9P) ─────────────────┐
│ │
UART1 ◄──┤ TX1 → NMEA (GGA…) ──► STM32 PA3 (RX) [watchdog: READS] │
│ RX1 ← UBX rate ◄── STM32 PA2 (TX) [watchdog: WRITES]│
│ │
UART2 ◄──┤ TX2 → NMEA/GGA @1Hz ──► 4G NTRIP Master (XBee) ──► SERVER │ ← server path
│ RX2 ← RTCM3 ◄── 4G NTRIP Master (NTRIP caster) │
└────────────────────────────────────────────────────────────┘
│
4G modem also emits its OWN $GPTXT,NET,<AS+K id>,TELCEL,…
(network status + station id = the backend's cell_id)
```
Key facts (confirmed in firmware + the 20-Jun bench analysis):
- Firmware: `HardwareSerial GNSS(PA3, PA2)` → **F9P UART1**. `parseNMEA()` only
*reads* GGA (for `fixQuality`/`satellites`/`rxCount`); `applyRate()` only *writes*
UBX. On power-off it **floats** PA2/PA3. The watchdog never transmits toward the
modem.
- The modem is on the Pro's **XBee socket = UART2** (the "XBEE↔UART2 switch"); GGA is
configured out **UART2 @ 1 Hz**. The server stream and the NTRIP GGA-uplink both
ride UART2.
- Therefore the watchdog (UART1) and the server path (UART2) are **electrically
separate**. No firmware change alone can bridge them.
---
## 2. The three theoretical ways in (and which one to use)
1. **Make the F9P emit it.** ✗ Not possible. The ZED-F9P has no command to inject an
arbitrary NMEA string into its UART2 output. It emits only its own sentences + UBX
replies.
2. **Tell the 4G modem to carry it.** ✗ No runtime path. The modem's `$GPTXT` is
network status; `AS+K` (station id) is *set-once* config, not a data channel the
MCU can stuff health into per-cycle.
3. **Put the watchdog in the uplink NMEA path (relay/injector).** ✓ **The viable
design.** Route the F9P→modem NMEA *through* the STM32:
```text
F9P TX2 (NMEA) ──► STM32 RX ──[relay verbatim]──► STM32 TX ──► modem RX
└─[inject $PHLG health @ ~1/30s in the idle gap]
modem ──► forwards EVERYTHING (GGA/RMC/GPTXT + $PHLG) ──► server
RTCM from the modem can still go straight to the F9P RX2 (corrections path intact).
```
The backend already maps **(ip,port) → cell_id** (from the modem's `$GPTXT`), so a
`$PHLG` arriving on the same socket is **auto-associated to the right unit** — no
need to put the id in the health sentence. (This reuses the CGNAT-safe identity work
shipped in `retranslator.py`.)
> **Retrofit variant (no full relay):** diode-OR / open-drain the STM32 TX onto the
> modem's RX and inject only during the F9P's idle gaps (at 115200 baud, 1 Hz NMEA
> leaves the line idle >90 % of the time). Cheaper to bolt on, but collision-prone and
> harder to guarantee — **prefer the clean relay on the PCB.**
---
## 3. The proprietary sentence (don't reuse `$GPTXT`)
**Collision warning:** the backend parses `$GPTXT` by taking **field 2 as the
`cell_id`**. A health `$GPTXT` would be mis-read as a device id. Use a **proprietary
talker** instead — `$PHLG` (`P` = proprietary, `HLG` = Helgen):
```text
$PHLG,<fw>,<state>,<link>,<fix>,<sat>,<rx>,<vbat>,<v5v>,<tc>,<tmax>,<rec>,<limp>,<nmea_age_ms>,<reset>*CS
e.g. $PHLG,2.2,RUN,DEGR,1,12,34192,3.96,5.01,58,69,0,0,180,POR*11
(checksum is the standard NMEA XOR over the chars between $ and *, computed at
runtime by buildPHLG(); -127 in tc/tmax = "n/a". The earlier *4F here was a wrong
hand-written placeholder — the correct XOR for that body is *49.)
```
Fields mirror the v2.1 `TLM` record (state, link, fix, sats, rx, battery, 5 V,
**MCU temp + session max**, recovery count, limp latch, NMEA age, **reset cause**).
`CS` = standard NMEA XOR checksum.
**Backend side** is a ~15-line addition to the hardened `retranslator.py`: a branch
that recognises `$PHLG`, verifies the checksum, and stores the fields per `cell_id`
(keyed by the existing `(ip,port)` map). Then the monitor can surface, **per unit and
live**: MCU temp / `tMax` (the thermal-hypothesis evidence, without pulling the unit),
reset cause (reboot-loop forensics), recovery count, and LIMP state — and it ships to
Loki for free.
### Backend `$PHLG` branch (concrete — review before deploy)
Drop-in for `retranslator.py`. `cell_id` is already resolved from `(ip,port)` by the
existing CGNAT-safe map; this only adds recognition + checksum + store. Tolerant of the
**extra trailing fields** the v3 board will append (air/RH/battery/fan).
```python
# --- $PHLG health branch (v2.2) — REVIEW BEFORE DEPLOY ------------------------
def _nmea_ok(s: str) -> bool:
if not s.startswith('$') or '*' not in s:
return False
body, _, cs = s[1:].partition('*')
x = 0
for ch in body:
x ^= ord(ch)
return f"{x:02X}" == cs.strip()[:2].upper()
PHLG_FIELDS = ("fw","state","link","fix","sat","rx","vbat","v5v",
"tc","tmax","rec","limp","nmea_age_ms","reset")
def handle_phlg(line: str, cell_id: str, store: dict) -> bool:
if not line.startswith('$PHLG,') or not _nmea_ok(line):
return False
body = line[1:].split('*', 1)[0]
parts = body.split(',')[1:] # drop the 'PHLG' talker
rec = dict(zip(PHLG_FIELDS, parts)) # extra v3 trailing fields are simply ignored
rec["ts"] = time.time()
store[cell_id] = rec # latest health per unit (also ship to Loki)
log.info("PHLG %s temp=%s tmax=%s reset=%s limp=%s rec=%s",
cell_id, rec.get("tc"), rec.get("tmax"), rec.get("reset"),
rec.get("limp"), rec.get("rec"))
return True
# In the inbound loop, BEFORE the GGA/position path:
# if handle_phlg(line, cell_id, PHLG_BY_CELL):
# continue # health record, NOT a position fix — don't forward to Wialon
# -----------------------------------------------------------------------------
```
---
## 4. What to bench-test before committing (resolves the "mixed info")
1. **Does the 4G NTRIP Master forward a proprietary `$P…` sentence?**
✅ **RESOLVED 2026-06-22 — YES.** Enabling `$PUBX` (a u-blox proprietary `$P…` talker)
on the F9P UART2 → it arrives at the server `udp/5010` **intact**, alongside GGA/RMC. The
modem is a transparent forwarder; **no whitelist**, so the `$GPTXT,HLT` fallback is not
needed. (Detail: `03-Architecture-Hardware/watchdog-pcb-v3-design.md` §7a.) Original note:
inject one on UART2-RX on the bench and watch the server `tcpdump`. *If it whitelists talkers:*
fall back to a `$GPTXT,HLT,…` subtype and special-case it in the backend by **field
1** (`HLT` vs `NET`) so it never collides with the cell-id parse. **This is the one
experiment that settles the whole question.**
2. **GGA integrity for NTRIP.** The modem relays the F9P's GGA *up to the caster* for
network RTK. The relay must be **byte-exact and low-latency** and only inject in
idle gaps — never delay or mangle GGA, or corrections degrade.
3. **Rate / cost.** Health at **1 / 30–60 s** keeps cellular + backend load trivial.
4. **Checksum** must be correct or the backend (and possibly the modem) drops it.
---
## 5. Other watchdog improvements (independent of the telemetry question)
### 🔴 5a. The "no-RTK ≠ dead-modem" thrash risk — *act on this now*
v2 treats `fixQuality >= 4` (RTK) as the only healthy link, and after
`NTRIP_GRACE` (120 s) of `< 4` it **power-cycles the stack**. But the 20-Jun bench
reality is **every unit is SPS (quality 1), no RTK** (caster/FLASH/sky issue, *not* a
modem hang). With the watchdog connected as-is, a unit that is **GNSS-healthy and
forwarding fine at SPS** would be **power-cycled every ~2 min + backoff** — manufacturing
the very disconnects we're trying to kill. **Fix:** only power-cycle on evidence of a
*link/modem* hang (NMEA silence is already handled); treat *stable SPS with incrementing
`rx`* as a reportable **DEGRADED** state, **not** a recovery trigger — at least until the
corrections pipeline (move off rtk2go, save F9P config to FLASH, open-sky retest) is
fixed. This is the highest-value firmware change right now.
### 🟠 5b. Ship the v2.1 parseable log (`docs/LOGGING-SPEC.md`)
The ANSI dashboard overwrites itself and has no timestamps/reset-cause → useless as a
field record. The v2.1 spec (BOOT/COLS/TLM/EVT, reset cause, ~60 additive lines) makes
the watchdog the **authoritative per-unit health record**. It's also the exact data the
`$PHLG` sentence would carry — **do this first**, so every later change is observable.
### 🟠 5c. Independent watchdog (IWDG) — watch the watcher
Nothing currently recovers the Blackpill itself if *it* hangs. Enabling the STM32 IWDG
makes an MCU lockup self-reset and show `reset=IWDG` next boot — closing the last
"silent vanish" gap for a device whose whole job is fail-safe.
### 🟠 5d. External temp sensor + persisted counters
v2 logs **MCU** temp, but the heat casualties are the **18650 / 4G modem**, not the die.
Add a small external sensor near the cell/modem (PCB brief already flags this), and
**persist** `tMax` / `recoveryCount` / reset history to RTC backup registers or flash so
a pulled unit's forensics survive the power-cycle. Directly serves the thermal RCA.
---
## 6. Recommendation (phased)
| When | Action | Risk |
|---|---|---|
| **Now** (firmware only) | 5a (stop SPS-driven power-cycling) + 5b (v2.1 log) + 5c (IWDG). Bench-test, bump v2.1, then flash. | Low — additive / behavioural guard. |
| **Bench experiment** | Test #1: does the modem forward `$P…`? Decide `$PHLG` vs `$GPTXT,HLT`. | None — measurement. |
| **Phase 2 (PCB)** | Watchdog as NMEA relay/injector on the UART2 uplink + external temp sensor; backend `$PHLG` parser + monitor/Loki surfacing. | Medium — relay must preserve GGA/NTRIP; validate before fleet. |
**Bottom line for the deck:** *Yes, we can put unit health (incl. temperature) into the
live server feed — it needs the watchdog inserted into the modem's uplink path (a
Phase-2 PCB change) and one bench test of the modem's forwarding. Until then the same
health is captured on-unit via the v2.1 log. Separately, before we connect the watchdog
we must stop it from power-cycling healthy SPS units.*
## Related
- [[03-Architecture-Hardware/watchdog-schematic|watchdog-schematic]] ·
[[03-Architecture-Hardware/firmware-review|firmware-review]] ·
[[03-Architecture-Hardware/hardware-respec-and-pcb-brief|PCB brief]] ·
`04-Source-Code/firmware/docs/LOGGING-SPEC.md` ·
`04-Source-Code/firmware/blackpillrtkcode_v2/blackpillrtkcode_v2.ino`