Milestones · M2

GNSS

The device knows where it is and what time it is without a network: a GNSS Service in the background, a GNSS App with the position and a sky view of the satellites, the clock set from satellites when there's no NTP, and Tracks…

Status: done on the device (branch m2): every "Done when" item below is met.

Measured

  • Cold start ($PCAS10,2) to a 3D Fix, by a window: 73 s, 5 satellites used of 8 in view. A restart of the ESP32 alone keeps the receiver's Fix (the Cap stays powered).

  • By a window: 3D Fix from GPS, GLONASS, Galileo and BeiDou, up to 14 of 17 satellites used, HDOP 1.0–1.3.

  • Heap, Debug Build, GNSS on, IRC on TLS (floor 40 KB; v0.2.1 had a 79 KB low):

    FreeLowest
    Start of M231 KB12.6 KB
    Stacks and buffers trimmed by measurement46 KB18 KB
    mDNS removed54 KB34 KB
    Framework rebuilt with smaller TLS buffers (ADR 0006)78 KB59 KB

    Under the heaviest combined load measured (IRC, two refused installs, a 1.6 MB put and get), the low is 46 KB. The cost had come mostly from the OTA and Debug Build work, not GNSS. Stacks were set to measured peak plus about 2 KB (loop 6 KB, update 5, storage 6, irc 6); after a TLS handshake the irc task has 1.7 KB left. IRC can now be stopped by hand (/quit in any state, irc stop), which frees its TLS memory.

Goal: the device knows where it is and what time it is without a network: a GNSS Service in the background, a GNSS App with the position and a sky view of the satellites, the clock set from satellites when there's no NTP, and Tracks recorded to the SD card.

Hardware: the Cap LoRa-1262 carries an ATGM336H-6N (AT6668), multi-constellation (GPS, BeiDou, Galileo, GLONASS, QZSS), with a ceramic antenna. NMEA over UART, 115200 8N1. Measured (step 1, gnss probe): RX GPIO 15, TX GPIO 13, 115200 8N1, as in Meshtastic's board file; M5Stack's page names GPIO 8 and 9, which are the internal I2C bus the keyboard controller sits on. Output is NMEA 4.10 style: GN RMC, VTG and GGA, one GSA per constellation with the system ID (1 GPS, 2 GLONASS, 3 Galileo, 4 BeiDou, 5 QZSS) in its last field, and a GSV sequence per constellation and signal (GP, GL, GA, …) with the signal ID last. RMC carries a time even without a Fix (status V), so only a Fix makes it trustworthy.

Decisions (design round 2026-10-04)

#Decision
Q58Settings has a GNSS On/Off switch, On by default. Off puts the receiver in standby. Measured: $PCAS12,<seconds> (CASIC) stops its output within a second, for up to at least 65535 s, and any command wakes it within a second; Off sends PCAS12,65535 (renewed hourly), On sends a hot start, PCAS10,0.
Q59The GNSS App has two views, switched with Tab. Position: latitude, longitude, altitude, speed, course, Fix (none / 2D / 3D), satellites used and in view, HDOP, UTC time. Sky: the satellites placed by azimuth and elevation, coloured by constellation, filled when used in the Fix.
Q60"Radar" in M2 means the Sky view. A radar of other Nodes by distance and bearing needs the mesh: M4.
Q61The Status Bar shows a GNSS mark: absent when off, muted while searching, normal with a 2D Fix, with the satellite count with a 3D Fix.
Q62GNSS time sets the clock once there's a Fix, and refreshes it every 10 minutes. Revised in step 3: the clock's trust order from M0 (Mesh < NTP < GNSS) already ranks GNSS above NTP, which is right: GNSS time is at least as accurate. So GNSS also corrects a clock NTP set, not only an unset one.
Q63A Track is started and stopped in the GNSS App. It's written as GPX to /gnss/tracks/<YYYYMMDD-HHMMSS>.gpx, a point every 5 s when the position moved more than 5 m. It keeps recording with the App closed, with a Toast on start and stop and a Status Bar mark, and gets its own Storage Clean-up category.
Q64Coordinates in decimal degrees plus the Maidenhead locator; a Settings switch for degrees, minutes and seconds. Metric units only.
Q65The position never leaves the device in M2. Sharing it over the mesh, and at what precision, is decided in M4.
Q66Our own NMEA parser, host-tested: RMC, GGA, GSA and GSV, with each talker ID mapped to its constellation. TinyGPSPlus (named in ADR 0001) doesn't track the satellite list across constellations, which the Sky view needs.
Q67The receiver keeps its defaults (all constellations, 1 Hz). No receiver settings. Time to first fix is measured and recorded here.
Q68Debug aids: gnss status, and gnss nmea on/off to stream the raw sentences to the console (USB serial and Debug Console). Raw NMEA is never written to the card.

Lesson (step 3): the first probe also tried the pins swapped, driving the receiver's output line from the ESP32 for about a second. The receiver then went silent until a full power cycle (an ESP32 restart doesn't cut the Cap's power). Never drive GPIO 15.

Done when

  • The GNSS Service reads NMEA in the background whatever App is on screen, and a 3D Fix appears outdoors.
  • The GNSS App shows the Position and Sky views, both live.
  • The Status Bar shows the GNSS mark per Q61.
  • With no Wi-Fi, the clock is set from GNSS after the first Fix.
  • A Track records while the App is closed, survives the screen turning off, and opens as valid GPX on the PC.
  • Settings → GNSS Off stops it (and the Status Bar mark goes away); On brings it back.
  • Free heap stays above about 40 KB with GNSS, Wi-Fi, IRC on TLS and the UI running.

Work breakdown

  1. Hardware check: a gnss probe command reads the candidate UART pins and reports which carries NMEA, at what baud rate, and which talker IDs. Then the standby command (Q58) and a first time to first fix.
  2. NMEA parser (host-tested): checksum, RMC, GGA, GSA, GSV across constellations, merged into one GNSS state (Fix, position, time, satellites).
  3. GNSS Service: UART on its own task, the parser, gnss status and gnss nmea, Settings On/Off.
  4. Clock from GNSS (Q62), and the Status Bar mark (Q61).
  5. GNSS App: Position view, Maidenhead and coordinate formats (host-tested), then the Sky view.
  6. Tracks: the 5 s / 5 m rule and GPX writing (host-tested), background recording, Clean-up category.

This page is generated from docs/milestones/M2.md in the repository. To change it, change that file and run site/tools/gen_dev_docs.py.