M2 step 6: Tracks, recorded to GPX in the background

`r` in the GNSS App (or `gnss track start|stop`) records a Track to
/gnss/tracks/YYYYMMDD-HHMMSS.gpx: a point every 5 s once moved 5 m
(lib/gnss/track, 7 tests: haversine, the rule, GPX text, the file name).
It keeps recording with the App closed, shows REC in the Status Bar, and
announces start and stop with a Toast. It needs a card and the time;
switching GNSS off stops it. Tracks are written like Captures: past 90 %
card usage, until full. Storage Clean-up gets a GNSS tracks category.

A Track cut short by a reset or power loss has no GPX footer; the GNSS
Service closes such files at the next boot.

Verified on the device: a recorded Track and one interrupted by a reset
both parse as GPX 1.1 on the PC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-04 22:46:57 +02:00
co-authored by Claude Opus 5.5
parent 463ed2dda8
commit d7952612dd
13 changed files with 335 additions and 11 deletions
+19 -1
View File
@@ -8,7 +8,9 @@
#include "nmea_parser.h"
#include "service.h"
#include "services/clock_service.h"
#include "services/storage_service.h"
#include "settings.h"
#include "track.h"
namespace roro {
@@ -25,7 +27,7 @@ class GnssService : public Service {
// command wakes it. Standby is renewed while off, in case the duration is clamped.
static constexpr uint32_t kStandbyRenewMs = 3600000;
GnssService(const Settings& settings, ClockService& clock, EventBus& bus);
GnssService(const Settings& settings, ClockService& clock, StorageService& storage, EventBus& bus);
const char* name() const override { return "gnss"; }
uint32_t tickIntervalMs() const override { return 50; }
void start() override;
@@ -42,6 +44,14 @@ class GnssService : public Service {
void setEcho(bool echo) { echo_ = echo; }
void printStatus(Print& out, uint32_t nowMs) const;
bool send(const std::string& body);
// Tracks (Q63): GPX on the SD card, a point every 5 s once moved 5 m, recorded in the
// background whatever App is open. startTrack() returns why it can't, or "".
std::string startTrack(uint32_t nowMs);
void stopTrack();
bool tracking() const { return !trackPath_.empty(); }
int trackPoints() const { return trackPoints_; }
uint32_t trackStartMs() const { return trackStartMs_; }
void restart(uint32_t nowMs) {
close();
open(nowMs);
@@ -51,16 +61,24 @@ class GnssService : public Service {
void open(uint32_t nowMs); // the UART; the receiver's state follows the setting in tick()
void close();
void wake(uint32_t nowMs);
void recordPoint(uint32_t nowMs);
void closeUnfinishedTracks(); // on the storage task
void notify(const std::string& text);
void standby(uint32_t nowMs);
const Settings& settings_;
ClockService& clock_;
StorageService& storage_;
EventBus& bus_;
gnss::NmeaParser parser_;
bool open_ = false, active_ = false, echo_ = false;
uint32_t bytes_ = 0, standbyMs_ = 0;
uint32_t openedMs_ = 0, lastLineMs_ = 0, firstFixMs_ = 0, clockSetMs_ = 0;
gnss::FixType lastFix_ = gnss::FixType::None;
std::string trackPath_;
int trackPoints_ = 0;
uint32_t trackStartMs_ = 0;
gnss::TrackRule trackRule_;
};
} // namespace roro