Public Access
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:
@@ -0,0 +1,76 @@
|
||||
#include "track.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
namespace roro::gnss {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr double kEarthRadiusM = 6371000.0;
|
||||
|
||||
double radians(double degrees) { return degrees * M_PI / 180.0; }
|
||||
|
||||
// UTC fields from seconds since 1970 (gmtime_r works on the device and the PC alike).
|
||||
struct tm utc(int64_t seconds) {
|
||||
time_t t = static_cast<time_t>(seconds);
|
||||
struct tm out;
|
||||
gmtime_r(&t, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
double distanceMeters(double lat1, double lon1, double lat2, double lon2) {
|
||||
double dLat = radians(lat2 - lat1), dLon = radians(lon2 - lon1);
|
||||
double h = std::sin(dLat / 2) * std::sin(dLat / 2) +
|
||||
std::cos(radians(lat1)) * std::cos(radians(lat2)) * std::sin(dLon / 2) * std::sin(dLon / 2);
|
||||
return 2 * kEarthRadiusM * std::asin(std::sqrt(h));
|
||||
}
|
||||
|
||||
bool TrackRule::due(uint32_t nowMs, double lat, double lon) {
|
||||
if (any_ && (nowMs - lastMs_ < kEveryMs || distanceMeters(lat_, lon_, lat, lon) < kMinMeters)) return false;
|
||||
any_ = true;
|
||||
lastMs_ = nowMs;
|
||||
lat_ = lat;
|
||||
lon_ = lon;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string trackPath(int64_t utcSeconds) {
|
||||
struct tm t = utc(utcSeconds);
|
||||
char buf[48];
|
||||
std::snprintf(buf, sizeof buf, "/gnss/tracks/%04d%02d%02d-%02d%02d%02d.gpx", t.tm_year + 1900, t.tm_mon + 1,
|
||||
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
|
||||
return buf;
|
||||
}
|
||||
|
||||
namespace gpx {
|
||||
|
||||
std::string header(const std::string& name) {
|
||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<gpx version=\"1.1\" creator=\"roro9stack\" xmlns=\"http://www.topografix.com/GPX/1/1\">\n"
|
||||
"<trk><name>" + name + "</name><trkseg>";
|
||||
}
|
||||
|
||||
std::string point(double lat, double lon, bool hasElevation, float elevationM, int64_t utcSeconds) {
|
||||
struct tm t = utc(utcSeconds);
|
||||
char buf[160], ele[32] = "";
|
||||
if (hasElevation) std::snprintf(ele, sizeof ele, "<ele>%.1f</ele>", elevationM);
|
||||
std::snprintf(buf, sizeof buf,
|
||||
"<trkpt lat=\"%.7f\" lon=\"%.7f\">%s<time>%04d-%02d-%02dT%02d:%02d:%02dZ</time></trkpt>", lat, lon,
|
||||
ele, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string footer() { return "</trkseg></trk></gpx>"; }
|
||||
|
||||
bool finished(const std::string& tail) {
|
||||
size_t end = tail.find_last_not_of(" \r\n\t");
|
||||
return end != std::string::npos && end + 1 >= 6 && tail.compare(end + 1 - 6, 6, "</gpx>") == 0;
|
||||
}
|
||||
|
||||
} // namespace gpx
|
||||
|
||||
} // namespace roro::gnss
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace roro::gnss {
|
||||
|
||||
// Great-circle distance in metres (haversine, mean Earth radius).
|
||||
double distanceMeters(double lat1, double lon1, double lat2, double lon2);
|
||||
|
||||
// When a Track takes its next point (Q63): the first one always, then at least every 5 s and only
|
||||
// after moving at least 5 m, so standing still doesn't fill the card.
|
||||
class TrackRule {
|
||||
public:
|
||||
static constexpr uint32_t kEveryMs = 5000;
|
||||
static constexpr double kMinMeters = 5.0;
|
||||
|
||||
// True if this position should be recorded (and is then remembered as the last point).
|
||||
bool due(uint32_t nowMs, double lat, double lon);
|
||||
void reset() { any_ = false; }
|
||||
|
||||
private:
|
||||
bool any_ = false;
|
||||
uint32_t lastMs_ = 0;
|
||||
double lat_ = 0, lon_ = 0;
|
||||
};
|
||||
|
||||
// "/gnss/tracks/YYYYMMDD-HHMMSS.gpx", from the UTC start time (Storage Clean-up reads its date).
|
||||
std::string trackPath(int64_t utcSeconds);
|
||||
|
||||
// GPX 1.1, written a line at a time: the header when a Track starts, a point per line, the footer
|
||||
// when it stops. A file whose footer never came (power lost) is closed at the next boot.
|
||||
namespace gpx {
|
||||
std::string header(const std::string& name);
|
||||
std::string point(double lat, double lon, bool hasElevation, float elevationM, int64_t utcSeconds);
|
||||
std::string footer();
|
||||
bool finished(const std::string& tail); // the end of a file: does it close the GPX?
|
||||
} // namespace gpx
|
||||
|
||||
} // namespace roro::gnss
|
||||
Reference in New Issue
Block a user