-
Notifications
You must be signed in to change notification settings - Fork 3
10 ‐ GPS Guide
Complete reference for the u-blox M10Q GNSS subsystem: aggressive configuration for short surfacings, AssistNow, deep-idle mode (GNP52), RTC strategy as it relates to GPS, and field-debug guidance.
Applicable to all variants: Argos SMD, KIM, LoRa, RSPB — the M10Q is shared hardware. Source:
ports/nrf52840/core/hardware/m10qasync/m10qasync.cpp/hpp,core/services/gps_service.cpp/hpp.
- Part A — Why aggressive GPS
- Part B — Aggressive DTE configuration
- Part C — AssistNow
- Part D — GNSS deep-idle (GNP52)
- Part E — RTC strategy for GPS
- Part F — Surfacing sequence (warm/cold/hot)
- Part G — Per-variant tuning
- Part H — CloudLocate & REUSE_LAST
- Part I — Battery impact
- Part J — LED indicators
- Part K — Field-debug & troubleshooting
| Surfacing window | TTFF M10Q (no aid) | TTFF warm/hot (AssistNow + BBR) |
|---|---|---|
| 30 s – 2 min (green turtle) | 26 – 35 s | 1 – 5 s |
| 5 – 10 min (loggerhead) | same | same |
Objective: fix < 10 s on every surfacing.
The whole pre-warm + warm-packet + deep-idle effort exists so that warm/hot start is the common case. Cold start is the exception (first deployment, BBR drained, ANO expired).
Recommended baseline for sea-turtle deployments (works on all comm stacks):
# === Enable GPS ===
PARMW GNP01 1 # GNSS_EN = true
# === Trigger GPS at surface ===
PARMW GNP25 1 # GNSS_TRIGGER_ON_SURFACED = true
PARMW GNP28 0 # GNSS_TRIGGER_COLD_START_ON_SURFACED = false (keep ephemerides!)
# === AXL pre-trigger (LoRa: recommended; SMD/KIM: optional — Doppler compensates) ===
PARMW GNP26 1 # GNSS_TRIGGER_ON_AXL_WAKEUP = true
# === Timeouts ===
PARMW GNP05 60 # GNSS_ACQ_TIMEOUT = 60 s (warm)
PARMW GNP09 120 # GNSS_COLD_ACQ_TIMEOUT = 120 s (cold, first deploy only)
# === AssistNow (critical) ===
PARMW GNP24 1 # GNSS_ASSISTNOW_EN = true (Autonomous, ~3 days)
PARMW GNP27 1 # GNSS_ASSISTNOW_OFFLINE_EN = true (~14 days)
# === Nominal acquisition period (after first fix) ===
PARMW ARP11 1 # DLOC_ARG_NOM = index 1 → 600 s (10 min)
# For LoRa aggressive: index 11 → 60 s (1 min)
# === Filtering relaxed for fast fix ===
PARMW GNP02 1 # GNSS_HDOPFILT_EN = true
PARMW GNP03 4 # GNSS_HDOPFILT_THR = 4 (was 2 — too strict for short surface)
PARMW GNP20 1 # GNSS_HACCFILT_EN = true
PARMW GNP21 25 # GNSS_HACCFILT_THR = 25 m (was 5 m)
# === Fix mode + dynamic model ===
PARMW GNP10 3 # GNSS_FIX_MODE = AUTO (2D/3D)
PARMW GNP11 0 # GNSS_DYN_MODEL = PORTABLE (best for turtles ~2-5 km/h)
# === Constellations — all four major + augmentation ===
PARMW GNP40 0x1F # GPS + Galileo + GLONASS + BeiDou + QZSS
# Default 0x0F (4 majors). 0x1F adds QZSS for Asia-Pacific deployments.
# === Signal thresholds relaxed ===
PARMW GNP42 6 # GNSS_MIN_CNO = 6 dBHz (default 10)
PARMW GNP43 5 # GNSS_MIN_ELEV = 5° (default 10°)See 09 — Parameters for the complete GNP reference, including ANO staleness (GNP44), FastLoc mode (GNP45/46), CloudLocate format, and the deprecated GNSS_BCKP_CHARGE_* slots replaced by GNP52.
Time-to-first-fix is dominated by ephemeris availability. AssistNow provides predicted/preloaded orbital data.
Without AssistNow (cold start):
M10Q downloads ephemerides from satellites (must decode 4 subframes)
→ 26-35 s minimum
With AssistNow Autonomous (GNP24):
M10Q predicts future ephemerides from last fix
→ Valid ~3 days. TTFF: 1-5 s (hot/warm start)
With AssistNow Offline (GNP27):
Ephemerides pre-calculated, stored in flash (via BLE/USB)
→ Valid ~14 days. TTFF: 1-5 s even after long no-fix
Prerequisite: RTC must have an approximately correct time. The firmware restores LAST_KNOWN_RTC from flash at boot, which is sufficient for ANO validation. See Part E.
ANO staleness gate (GNP44 GNSS_ANO_STALE_DAYS, default 5 days, 0 = never discard): ANO data older than this threshold is rejected and the receiver falls back to cold-start timeout. Increase to 14–25 days if BLE updates are infrequent. Audit fix M2: explicit RTC ≥ year 2000 guard on the staleness math prevents virtual-RTC stamps from spuriously rejecting fresh ANO.
Replaces the deprecated periodic backup-charge cycle (GNSS_BCKP_CHARGE_* → see page 09). Designed for sealed-tortue 1-year deployments with short surface windows.
Shipped: commits
367285e6→b823620b(2026-05), hardened bycc429222,c9006b65,e322fb26,eb3f2c34,927d0837.
| Operator action | Effect |
|---|---|
Default (GNP52=0) |
No change from previous firmware. Rail cut at end of every session. |
$PARMW#GNP52=300 + $SAVEP#
|
5 min of rail-on after each session. M10Q sleeps at ~10 µA. Coin cell recharges. Next session is warm-start (~3 s TTFF). |
$PARMW#GNP52=4294967295 + $SAVEP#
|
Rail stays on forever. Maximum battery economy on sealed deploys. ~0.4 mAh/day total GPS conso vs ~7 mAh/day with periodic backup-charge. |
$GNSSBCKP#600 (DTE) |
Manual force a 10-min deep-idle session via BLE (bench tool). Same internal path. |
Before the refactor, the V_BCKP coin cell on the M10Q was recharged by a periodic cycle (GNSS_BCKP_CHARGE_INT/DUR/UW_ONLY). Every 6–24 h the firmware power-cycled the GPS rail, sync'd UART, sent UBX-RXM-PMREQ backup, waited X minutes, then cut the rail. Two problems:
- Fragile. Every cycle re-powers the M10Q cold. If UART baud-sync fails (BBR lost, capacitor discharge, contamination from listening on a floating pin pre-power-up), the recharge never happens. Field log 2026-05-23 caught this failing twice in a row — coin cell drained, next session was a 120 s cold start that missed the tortue's surface window entirely.
- Wasteful. 24 power-up cycles per day at ~30 mA × ~2 s = ~0.4 mAh/day just on RAMP, never mind the charging window itself.
GPS session ends (success or NO_FIX)
│
▼
read GNSS_DEEP_IDLE_AFTER_OFF_S (GNP52)
│
┌───────────────┼───────────────┐
▼ = 0 ▼ = 0xFFFFFFFF ▼ = X seconds
power_off() enter_deep_idle() enter_deep_idle()
(rail off) (rail ON, no timer) (rail ON, +X timer)
│ │
│ after X s, power_off()
▼
Next session:
│
┌────────────────────┼────────────────────┐
▼ rail still on ▼ rail was cut
pulse EXTINT, normal cold power-on
sync UART, (exit_shutdown,
transit to baud-sync, configure)
configure (fast)
-
Deep-idle current (M10Q in PMREQ-backup with
wakeupSources=EXTINT0): ~10–12 µA continuous (M10 datasheet typical). - Wake-up time (EXTINT pulse → first UBX response): ~30–50 ms.
- TTFF: warm-start path (~3–15 s) instead of cold-start (~30–120 s) — BBR + almanac + ephemerides + AssistNow Autonomous prediction stay alive while rail is up.
| Architecture | GPS rail conso/day | TTFF/session | Total/day |
|---|---|---|---|
| OLD (periodic backup-charge, when working) | 24 × 83 µAh recharge | + 5 × ~1 mAh warm | ~7 mAh/day |
| OLD degraded (sync fails → no recharge) | 0 | + 5 × ~3 mAh cold | ~15 mAh/day |
NEW GNP52=0 (no deep-idle) |
0 | + 5 × cold (BBR may be drained) | up to ~15 mAh/day |
NEW GNP52=300 (5 min/session) |
~12 µA × 300 s × 5 = ~5 µAh | + 5 × ~1 mAh warm | ~5–6 mAh/day |
NEW GNP52=0xFFFFFFFF (always on) |
12 µA × 24 h × ~95 % = ~0.3 mAh | + 5 × ~0.5 mAh hot | ~3 mAh/day |
For 1-year on a 8.5 Ah Saft LS33600: difference between "OLD degraded" and "NEW always-on" ≈ 12 mAh/day × 365 = 4.4 Ah saved — roughly half the battery. Single biggest battery optimization in the refactor pass.
The M10Q's deepest sleep is PMREQ-backup with wakeupSources=EXTINT0 (~10–12 µA, UART block fully powered down). Wake triggered by an edge on EXTINT.
-
Pin:
BSP::GPIO_GPS_EXT_INT= nRF52 P1.11 (LinkIt V4). Direction set toOUTPUTin BSP (2026-05 refactor). -
Pulse:
pulse_extint_wake()inm10qasync.cpp— drives HIGH for 1 ms (10× the M10's ~100 µs minimum), back to LOW, then releases to high-Z to avoid leakage when M10Q is awake. -
Power-off (
enter_shutdown): releases EXTINT to high-Z. No leakage into a powered-off M10Q input.
Why EXTINT not UART RX? UARTRX wake works (~15 µA) but the UART block stays partially powered to monitor RX edges. EXTINT-only mode is the lowest-power configuration.
Inverting "rail always off" invariant created a new failure surface. If EXTINT wake fails (HW issue, PMREQ flag mismatch, contamination), firmware would otherwise hang waiting for UART banner. Multiple defensive layers:
-
R1 — At every boot,
main()drivesGPIO_GPS_PWR_ENLOW via rawnrf_gpioAPI beforeinit_peripheralsruns. Hardware-level invariant survives even fault paths that skip clean shutdown. -
R2 —
GPSService::service_term()unconditionally callsm_device.power_off(). Cuts rail on every FSM transit out of OperationalState (ConfigurationState entry, BatteryCriticalState/ErrorState/OffState, OTA, factory_reset).
-
R4 — If
PMU::reset_cause() == WDT_RESETat boot, GPS service inhibits deep-idle fast-path for the first session post-boot. Forces a cold-cycle to prove the cold path still works before re-engaging optimization. -
R5 — Every
service_next_schedule_in_mscall checks if M10Q has been in deep-idle > 24 h. If so, forces a prophylactic rail-cycle. Daily insurance against unobserved drift.
Added m_consecutive_wake_failures (uint8) + WAKE_FAIL_FAST_FALLBACK=2. In power_on(), the backupidle branch pre-increments the counter before pulsing EXTINT, and after 2 consecutive failures forces a full cold rail-cycle (rail off, wait, exit_shutdown) instead of trusting the same fast-path that just failed. Counter resets on first valid NAV report (react(UBXCommsEventNavReport)). Prevents the failure mode where a broken EXTINT pin or BBR-lost M10Q loops the wake path forever, never trying proven cold-boot recovery.
Previously the WDT-reset inhibit flag was cleared only in react(GPSEventPVT) on a valid fix. In HAULED + REUSE_LAST/OFF, or indoor bench testing, no PVT ever arrives → inhibit stuck forever → deep-idle permanently disabled post-WDT. Now the flag is also cleared the first time the cold-off branch completes — reaching that path proves cold-power-off survived without freezing.
Mirrors the existing service_next_schedule_in_ms rate-limit gate. The Service framework's reschedule lambda calls service_initiate DIRECTLY after the delay (skipping service_next_schedule_in_ms), so without this re-check a GPS session can fire while Argos is still rate-limited, burning a full cold-acq window of GPS current for a fix that can't be transmitted.
Side-effect in service_is_enabled: if GNSS is disabled by config while deep-idle is engaged (rail on, M10Q in PMREQ-backup), the rail is cut immediately. Without this hook the Service framework stops calling service_next_schedule_in_ms (because service_is_enabled returns false), so R5's 24 h hygiene cap never fires — rail could stay on for a year, slowly draining the main battery via M10Q backup current. Documented violation of the "pure query" idiom — trade-off worth it for sealed-deploy robustness.
When CLOUDLOCATE_ONLY=true (GNP53) ends the session on raw measurement (no PVT), also: (a) reset m_cold_start_ntry=0 — capturing a raw IS the success in this mode, no NTRY_BACKOFF on next cycle; (b) clear the WDT inhibit flag — reaching the raw measurement step proves UART + nav config + cold-power survived.
-
Service::next_schedulegate: refuses to schedule while M10Q is in PMREQ-backup, otherwise the 10 scold_start_retrykept waking the module every 10 s during the GNP52 window (bench-observed: 3 s of true backup then 297 s at ~2 mA every 10 s cycle). -
try_enter_deep_idle_or_poweroff: callsservice_reschedule(false)after the auto-off timer fires, so the framework re-arms the next acquisition through the normal path. -
state_enterbackup: drivesGPIO_GPS_EXT_INTLOW before PMREQ-backup so the M10Q observes a clean stable idle on EXTINT for the entire window (floating EXTINT was being interpreted as wake edges). -
PMREQ-backup verification: probes the M10Q after PMREQ. TIMEOUT means backup confirmed; SUCCESS means M10Q still awake → retry up to
PMREQ_VERIFY_RETRIES=3. -
Wake diagnostic: includes
slept_msin EXTINT-wake INFO log so an unexpectedly short sleep duration immediately flags an unintended caller.
-
backup-chargeboots GPS at 9600 baud + clean UART teardown - Reverted "graceful fallback" — on sync failure during backup-charge, poweroff immediately rather than attempt a degraded continuation. Bounded recovery > silent partial success.
LED_FREEZE_TIMEOUT_MS in core/sm/ledsm.cpp. The 130 s ceiling covered the default GNSS_COLD_ACQ_TIMEOUT=120 s but not the param's full 10–600 s configurable range. Bumped to 650 s (600 + 50 s margin). Still well below the 15 min system watchdog, so a true freeze still recovers automatically.
If the pulse fails and UART sync doesn't succeed within BOOT_BAUD_SYNC_RETRIES × timeout (typical ~6 s), the driver bails to state_poweroff (rail cut), which the next acquisition fully re-powers cold. No scenario where the device freezes waiting for a sleeping M10Q.
# (old, no longer needed — keys silently no-op now)
# $PARMW#GNP47=0
# $PARMW#GNP48=0
# $PARMW#GNP49=false
# Set the new param:
$PARMW#GNP52=300 # 5 min deep-idle window per session (tortue recommended)
$SAVEP#
Or for max battery:
$PARMW#GNP52=4294967295 # rail always on
$SAVEP#
The $GNSSBCKP#duration_s DTE command is unchanged — same wire protocol, same arguments. It now delegates to request_deep_idle(duration_s) internally but is invisible to operator scripts.
$GNSSBCKP#600 # force a 10-min deep-idle session (bench operator charge)
When GNP53=true AND GNP45=2 (CLOUDLOCATE), the GPS session terminates immediately on first raw measurement (GPSEventCloudLocateReady) — no waiting for full PVT or GNSS_ACQ_TIMEOUT. Drops typical session 30–120 s → 5–15 s.
Trade-off: no local lat/lon (position resolved cloud-side from raw uploaded via Argos). LED double-blinks CYAN at capture moment (LEDGNSSCloudLocateReady). Off by default.
Recommended for short-surface tortues (surfacing < 30 s) where any local fix is unlikely. See Part H — CloudLocate.
- Param + DTE keys:
core/protocol/base_types.hpp(GNSS_DEEP_IDLE_AFTER_OFF_S=240,GNSS_CLOUDLOCATE_ONLY=241),core/protocol/dte_params.cpp(GNP52,GNP53) - Dispatch:
core/services/gps_service.cpp::try_enter_deep_idle_or_poweroff() - M10Q wake:
ports/.../m10qasync.cpp::pulse_extint_wake(),power_on()fast-path - Robustness R1/R4:
ports/nrf52840/main.cpp - Robustness R5:
gps_service.cpp::service_next_schedule_in_ms - LED states:
core/sm/ledsm.cpp::LEDGNSSCloudLocateReady,LEDGNSSDeepIdle,LEDGNSSPowerOff
The GPS subsystem is the single source of real UTC in the firmware (via NAV-PVT epoch). It is also the largest consumer (ANO staleness, deep-idle 24 h cap, REUSE_LAST age check). Every time-based feature in the firmware — cooldown, HAULED-mode, rate limiter, REUSE_LAST, LED 24-h window — depends on the policies described in this section.
LinKit v4 deployments target sealed wildlife trackers, often running for months to years without ever obtaining a GNSS fix. The RTC architecture below must remain meaningful regardless of:
- whether GNSS has ever synchronized the RTC
- watchdog resets mid-deployment
- brown-out (POF) interrupting flash writes
- single-bit corruption in noinit RAM or LFS param files
┌──────────────────────────────────────────────┐
│ │
Cold first boot ─────► main.cpp restore (validated, ≤ 2100) │
LAST_KNOWN_RTC=0 ──┐ ↓ │
│ ├─ if last_rtc > 0: rtc->settime() │
│ └─ if rtc still !is_set: settime(1) │
│ ↓ │
│ Virtual RTC (= 1 + uptime) │
│ ↓ │
│ Periodic config flush (every 30 min) │
│ M1a save: LAST_KNOWN_RTC = rtc->gettime()
│ ↓ │
WDT reboot ◄───────┘ ↓ │
↓ │
GNSS first fix arrives: │
m10qasync.cpp::rtc->settime(real_utc) │
↓ │
K + L hooks fire: │
HauledModeService::reset_for_rtc_sync │
RateLimiter::reset_for_rtc_sync │
↓ │
Real RTC (Unix epoch, drifts ~20 ppm) │
↓ │
M1a continues saving real values │
↓ │
More GNSS fixes: rtc resynced │
│ │
└──────────────────────────────────────┘
At every boot, LAST_KNOWN_RTC is read from LFS flash. Validation rules:
| Value | Action |
|---|---|
0 |
Skip restore. Virtual RTC fallback fires (rtc->settime(1)). |
1 to 4 102 444 800 (≤ year 2100) |
Accept and apply. |
> 4 102 444 800 |
Reject as corrupt, treat as 0. |
The lower bound is deliberately wide-open: RSPB's TPL5111 pseudo-RTC chain stores small values during the pre-GNSS phase, and sealed LinkIt V4 deployments rely on saved virtual-RTC values for cross-WDT continuity. Tighter validation would defeat both use cases.
When LAST_KNOWN_RTC = 0 (cold first boot or rejected corruption), the firmware falls back to rtc->settime(1). From this point:
-
rtc->is_set()returns true everywhere → gating logic works. -
rtc->gettime()≈ uptime seconds since boot (+1 offset). - Relative-time math (deltas) remains correct — cooldown, HAULED idle threshold, rate limiter window, REUSE_LAST age check all work.
-
Absolute UTC is wrong — features needing real UTC (PASS_PREDICTION, human-readable log timestamps) must check
rtc->gettime() >= 946684800(year 2000 sentinel) themselves.
GenTracker::periodic_config_flush() runs every 30 minutes. Before saving params, it writes the current rtc->gettime() to LAST_KNOWN_RTC (any value > 0, virtual or real). Without this:
- Sealed LinkIt V4 without GNSS would reset RTC to
1on every WDT, losing noinit-state continuity entirely. - RSPB's pseudo-RTC chain depends on this mechanism for cross-cycle continuity.
HauledModeService and other consumers detect when now < last_recorded_event_rtc (e.g. WDT reset + virtual RTC re-initializes to 1 while noinit retains last_uw_event_rtc = 3600).
Before correctifs: blind return → state machine frozen for the rest of the boot session.
After M1b: re-baseline last_event_rtc = now. Time count restarts from the new RTC frame, but state flags themselves are preserved.
if (now < s_noinit.last_event_rtc) {
DEBUG_WARN("RTC rollback detected — re-baselining");
s_noinit.last_event_rtc = now;
persist();
}Applied in: HauledModeService::evaluate, service.cpp::restore_cooldown_state (M1c), rate_limiter.cpp::restore_ring_state (M1c).
service.cpp / rate_limiter.cpp / hauled_mode_service.cpp restore validation rejects future-dated noinit timestamps — same root cause as E.5 but in reverse (real RTC at save time, virtual RTC at restore time). Ensures stale future timestamps don't permanently gate the service.
When m10qasync.cpp::on_fix transitions virtual → real RTC, it calls:
-
HauledModeService::reset_for_rtc_sync()— clearslast_uw_event_rtcstamped in virtual epoch -
RateLimiter::reset_for_rtc_sync()— clears ring buffer timestamps in virtual epoch
Without these, noinit timestamps stamped in virtual epoch (~uptime seconds) would appear billions of seconds in the past after the jump to real UTC, breaking the windowed math harmlessly but spamming WARN logs.
m10qasync.cpp has an explicit RTC ≥ year 2000 guard on the ANO staleness math (GNP44 GNSS_ANO_STALE_DAYS). Without this, a virtual-RTC stamp (~uptime seconds) would compare against last_ano_save_rtc from a previous real-RTC session, producing a "23 trillion days old" delta and spuriously rejecting fresh ANO blobs.
For the sealed-device hardening architecture (boot-fail counter, exception barriers, POF handler, WDT, defense-layers summary), see 07 — Architecture § Sealed-device hardening. For cooldown gates, see 13 — Underwater & Behavioral Modes § Part D. For the rolling rate limiter, see 11 — Satellite Communication § Part E.4.
[DIVE]
BMA400 LOW_POWER (~3 µA), GPS off, SWS periodic sampling
[APPROACH SURFACE]
T=-10 s BMA400 detects turbulence/swim → wakeup interrupt
T=-10 s GPS power on (GNP26=1)
T=-10 s M10Q acquisition starts (AssistNow ephemerides in RAM)
[SURFACE]
T=0 s SWS confirms surface (dry)
T=0 s Argos/LoRa TX rescheduled
T=1-5 s GPS fix ✅ (warm + 10 s head-start)
T=3-5 s First TX with position
T=0 s SWS confirms surface
T=0 s GPS power on
T=5-15 s GPS fix (warm, no head-start)
T=15-18 s First TX with GPS → risk of re-dive before TX
[DIVE]
GPS rail ON, M10Q in PMREQ-backup (~10 µA)
EXTINT held LOW (clean idle)
[SURFACE]
T=0 s SWS confirms surface
T=0 s Pulse EXTINT HIGH 1 ms → LOW, release high-Z
T=30-50 ms M10Q boots from PMREQ, UBX banner
T=1-5 s GPS warm fix ✅ (BBR + almanac all preserved)
T=1-5 s Service::next_schedule gate releases — TX path runs
# Doppler compensates absence of GPS fix → less aggressive OK
PARMW GNP03 2 # HDOP_THR = 2 (stricter, Doppler backup)
PARMW GNP21 10 # HACC_THR = 10 m
PARMW GNP05 120 # ACQ_TIMEOUT = 120 s
PARMW GNP26 0 # AXL pre-trigger = OFF (Doppler covers)# No Doppler → GPS is the SOLE source of position → very aggressive
PARMW GNP03 4 # HDOP_THR = 4 (relaxed)
PARMW GNP21 25 # HACC_THR = 25 m
PARMW GNP05 60 # ACQ_TIMEOUT = 60 s
PARMW GNP26 1 # AXL pre-trigger = ON (compensates no Doppler)
PARMW ARP05 120 # TR_NOM = 120 s (TX more frequent, no Doppler)Same as Argos SMD.
PARMW GNP30 1 # GNSS_SESSION_SINGLE_FIX = 1 (stop after 1 fix)
PARMW GNP09 180 # GNSS_COLD_ACQ_TIMEOUT = 180 s (3 min — battery saver)
PARMW GNP05 90 # GNSS_ACQ_TIMEOUT = 90 sThe M10 receiver captures raw GNSS measurement snapshots (MEASC12 / MEAS20 / MEAS50) alongside PVT acquisition. If timeout without valid fix and raw measurements are available:
- CloudLocate prioritizes over DEGRADED_PVT
- Raw blob stored in depth pile (overlaid on unused position fields)
- TX service emits a CloudLocate packet (type 7 Argos, type 4 LoRa)
During SURFACING_BURST: progressive CloudLocate — from the 2nd Doppler ping onwards, if raw is available, a CloudLocate packet is sent instead of Doppler.
Default behavior captures raw only on the FIRST surface acquisition of a session (cold-start path), then powers off as soon as a real fix arrives on subsequent warm surfaces — battery-friendly but no fallback if warm fix fails.
When GNP51=true, raw-meas is captured at every SURFACING_BURST surface, regardless of whether a real fix is obtained, giving the cloud a position fallback even when warm-GPS misses the 30 s timeout. Trade-off: GPS stays on the full cold_acq_timeout each surface (~30 s with fresh ANO). Useful for short-surface species (turtles surfacing < 30 s). Requires GNP45=2 AND ARGOS_MODE=SURFACING_BURST.
See D.8. Short-surface optimisation — terminate session on first raw measurement.
BaseGnssStrategy::REUSE_LAST (used by HAULED via HMP13, but also exposed as a general strategy) consumes the most recent FIX/UPDATE entry from the depth pile without powering GPS at all.
-
process_gnss_burst_from_cached()builds an Argos GNSS packet from depth-pile cached FIX/UPDATE - Falls back to Doppler when stale
- FASTLOC entries are deliberately excluded from REUSE_LAST (semantically "last known good fix")
-
GNP50 GNSS_REUSE_FIX_MAX_AGE_S(default 86400 s = 24 h) gates the age check.0disables reuse entirely.
should_promote_doppler_to_gnss promotes a fresh FastLoc/FIX in the pile to fill what would otherwise be a Doppler slot. Applied at:
- SURFACING_BURST phase 1
- LEGACY/DUTY_CYCLE/PASS_PREDICTION
!gnss_enbranch - DOPPLER mode global
Lets a real position (cached or freshly captured) take precedence over a Doppler packet whenever both are eligible.
| Scenario | TTFF | GPS current | Energy/fix |
|---|---|---|---|
| Cold start (no AssistNow) | 30 s | 25 mA | 210 µAh |
| Warm start (AssistNow) | 5 s | 25 mA | 35 µAh |
| Hot start (AXL pre-trigger) | 1-3 s | 25 mA | 14-21 µAh |
| Deep-idle wake (GNP52) | 1-5 s | 25 mA initial + ~10 µA continuous | ~5-25 µAh |
AssistNow divides GPS energy by ~6×. Most impactful single setting.
| Configuration | Annual GPS conso |
|---|---|
| Cold start every time, no AssistNow | 4 × 210 µAh × 365 = 307 mAh/yr |
| Warm start, AssistNow only | 4 × 35 µAh × 365 = 51 mAh/yr |
| Hot start, AXL pre-trigger + AssistNow | 4 × 20 µAh × 365 = 29 mAh/yr |
| Deep-idle GNP52=0xFFFFFFFF + AssistNow | (12 µA × 24 h × 365) + (4 × 20 µAh × 365) = 134 mAh/yr |
Note: deep-idle always-on is 4× more conso than aggressive AXL+AssistNow with rail cut between surfacings. But: deep-idle eliminates cold-start failures entirely on sealed devices that lose BBR power. The right trade-off depends on whether you trust the supercap V_BCKP retention vs. the cost of 100 mAh/yr extra.
GPS-related LED states emitted by ledsm.cpp (commit c9006b65, 2026-05). The end-of-session patterns were redesigned so that the operator can distinguish the sleep depth of the M10Q at a glance: double-blink = rail still on, M10Q in PMREQ-backup; fast-blink = rail cut, full power-off, next session is a cold boot.
| State | Pattern (period) | Color (fix → / no-fix →) | Duration | When |
|---|---|---|---|---|
LEDGNSSOn (legacy LEDGNSSAcquiring) |
Slow blink CYAN | Cyan | continuous | GPS acquisition in progress (until session ends) |
LEDGNSSCloudLocateReady |
Double-blink (120 ms alternate) | Cyan | ~500 ms |
CLOUDLOCATE_ONLY (GNP53) raw captured — session terminating on first raw measurement. Transit back to LEDGNSSOn if GPS still active, else LEDOff. |
LEDGNSSDeepIdle (new 2026-05) |
Double-blink (120 ms alternate) | GREEN / RED | ~500 ms |
GNSS session ended, rail kept on (GNP52) — M10Q parked in PMREQ-backup (~10 µA). Color reflects whether session captured a valid fix. Then transit to LEDOff. |
LEDGNSSPowerOff (new 2026-05) |
Fast-blink (50 ms ≈ 10 Hz) | GREEN / RED | ~500 ms | GNSS session ended, rail cut — full M10Q power-off, next session cold-boots. Color reflects whether session captured a valid fix. Visually heavier than deep-idle on purpose. |
LEDGNSSOffWithoutFix (legacy fallback) |
Solid | Red | 1.5 s (was 3 s) | Fires only when neither GNSS_OFF_DEEP_IDLE nor GNSS_OFF_POWEROFF event was emitted. Defers 500 ms if LEDGNSSCloudLocateReady is in progress, to avoid stomping the CYAN double-blink. |
LEDArgosTX |
Solid MAGENTA | Magenta | duration of TX | Argos satellite transmission in progress (also fires for SATDP cert/calibration regardless of LED_MODE, commit 143aedc9). |
Before commit c9006b65, end-of-session LED was always a solid 3 s flash GREEN (fix) or RED (no fix) via the legacy LEDGNSSOffWithFix / LEDGNSSOffWithoutFix states — and there was no way to tell whether the rail had been cut or whether the M10Q was in PMREQ-backup. With GNP52 GNSS_DEEP_IDLE_AFTER_OFF_S now controlling sleep depth per-session, the operator needs to see which sleep mode the device just entered:
| Pattern | Sleep depth | Next session TTFF |
|---|---|---|
Double-blink (LEDGNSSDeepIdle) |
M10Q PMREQ-backup, rail on (~10 µA) | Warm start ~3-15 s (BBR + almanac + ANO preserved) |
Fast-blink (LEDGNSSPowerOff) |
Full rail cut, M10Q off (0 µA) | Cold start ~30-120 s (BBR lost) |
The color (GREEN = valid fix this session, RED = no valid fix) is latched from the session result, not from the persistent BBR state — so on the second consecutive session that completes deep-idle without ever getting a fix, you'll see RED double-blink even though the M10Q is happily sleeping at 10 µA.
-
GNSS_OFF_DEEP_IDLE— emitted from all paths that leave the rail on (rail in PMREQ-backup). LED FSM dispatchesSetLEDGNSSDeepIdle. -
GNSS_OFF_POWEROFF— emitted from all paths that cut the rail (service_termR2, GNP52 auto-off timer firing viapoweroff_from_deep_idle(), R5 24 h hygiene cycle, FIX 4GNSS_EN=falserail-cut). LED FSM dispatchesSetLEDGNSSPowerOff.
WARM-path detection via the state_poweroff flag (GPIO read on GPIO_GPS_PWR_EN is broken by input-disconnect when rail is off — can't use GPIO state to infer the rail state). m_pmreq_baud tracks the actual PMREQ baud for EXTINT-wake pre-sync. service_term routes through poweroff_from_deep_idle when called while in backupidle, so even a transit out of OperationalState fires GNSS_OFF_POWEROFF correctly and the LED reflects the actual hardware state.
Every LED state arms a freeze-safety timeout (LED_FREEZE_TIMEOUT_MS = 650 s, bumped 130 s → 650 s in cc429222 to cover the full configurable range of GNSS_COLD_ACQ_TIMEOUT 10-600 s + 50 s margin). If the FSM somehow gets stuck in a single LED state past 650 s, the safety fires SetLEDOff. Still well below the 15 min system watchdog, so a true freeze still recovers automatically; the LED safety is for catastrophic FSM hangs, not for tight UX timing. The PreOperationalState / LEDOff disarm this safety (magnet activation UX must remain visible until magnet release; commit bc11660b).
- AssistNow disabled or ephemerides expired → check
GNP24=1, GNP27=1 - ANO blob older than
GNP44threshold → check$GNSSAstatus - Cold start required (BBR drained) → enable
GNP52(deep-idle) or schedule manual$GNSSBCKP
- AssistNow Offline expired (14 d) → recharge MGA-ANO via BLE
- Coin cell V_BCKP drained → enable
GNP52deep-idle (most reliable solution)
- Not enough satellites → check constellation mask (
GNP40), lowerGNP43 GNSS_MIN_ELEV - Fix mode forced to AUTO accepts 2D — change
GNP10to 3D ONLY if needed (rare)
- Antenna defective or shielded
- Check signal:
STATRGNSS section, debug log SAT level / count
- Threshold too strict → increase
GNP03to 4–6
If this happens on SMD variants: see the cold-reboot gotcha in 11 — Satellite Communication § Part D. Not GPS-related — the STM32WL state on VDD caps leaks into the next SPI session as INVALID_CMD cascades.
Bench-observed pre-e322fb26: 3 s of true backup then 297 s at ~2 mA every 10 s cycle. The Service::next_schedule gate fix prevents this — verify v4.1.8 or later is flashed.
Indoor bench: no PVT ever arrives → R4 inhibit never clears (legacy). Fixed in audit FIX 2 — the inhibit clears the first time the cold-off branch completes.
- Check
state_enterbackupwas actually reached → debug log - Verify EXTINT pin direction is OUTPUT in BSP
- Check
m_consecutive_wake_failurescounter (audit FIX 1) — if ≥ 2, the driver is forcing cold-cycles deliberately
LAST_KNOWN_RTC is refreshed every 30 min (M1a) but only saved to flash on clean shutdown or in PMU::powerdown(). POF brown-out preserves it but cannot save params (corrupts LFS). Drift between flush events is normal — ~30 min RTC drift on virtual epoch after WDT, irrelevant for relative-time math.
- 09 — Parameters § GNSS — full GNP reference
-
06 — DTE commands —
GNSSBCKP,GNSSBR,RTCW,GNSSA - 05 — Boards — power architecture (rail can stay on indefinitely with GNP52=0xFFFFFFFF)
- 13 — Underwater & Behavioral Modes — surface event triggers GPS
- 11 — Satellite Communication — SURFACING_BURST first-TX path + KIM/SMD specifics + message formats
- 07 — Architecture § Sealed-device hardening — boot-fail counter, exception barriers, defense layers summary