Public Access
`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
213 lines
7.6 KiB
C++
213 lines
7.6 KiB
C++
#include "gnss_app.h"
|
|
|
|
#include <Arduino.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
#include "geo_format.h"
|
|
#include "ui/canvas.h"
|
|
#include "ui/fonts.h"
|
|
#include "ui/theme.h"
|
|
|
|
namespace roro {
|
|
|
|
using namespace gnss;
|
|
|
|
namespace {
|
|
|
|
// One colour per constellation, readable in the 8-bit frame buffer.
|
|
uint16_t colorOf(Constellation c) {
|
|
switch (c) {
|
|
case Constellation::Gps: return theme::kAccent;
|
|
case Constellation::Glonass: return 0xF8C8; // red
|
|
case Constellation::Galileo: return theme::kMessage;
|
|
case Constellation::BeiDou: return 0xFEA0; // yellow
|
|
case Constellation::Qzss: return 0xC99F; // violet
|
|
default: return theme::kText;
|
|
}
|
|
}
|
|
|
|
const char* fixLabel(FixType f) { return f == FixType::ThreeD ? "3D Fix" : f == FixType::TwoD ? "2D Fix" : "No Fix"; }
|
|
|
|
std::string duration(uint32_t ms) {
|
|
uint32_t s = ms / 1000;
|
|
char buf[24];
|
|
if (s < 60) std::snprintf(buf, sizeof buf, "%lu s", (unsigned long)s);
|
|
else std::snprintf(buf, sizeof buf, "%lu min %02lu s", (unsigned long)(s / 60), (unsigned long)(s % 60));
|
|
return buf;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool GnssApp::onKey(const KeyEvent& e) {
|
|
if (e.key == Key::Tab) {
|
|
sky_ = !sky_;
|
|
requestRedraw();
|
|
return true;
|
|
}
|
|
if (e.key == Key::Char && (e.ch == 'r' || e.ch == 'R')) { // record a Track, or stop it
|
|
if (gnss_.tracking()) gnss_.stopTrack();
|
|
else {
|
|
refusal_ = gnss_.startTrack(millis());
|
|
refusalMs_ = millis();
|
|
}
|
|
requestRedraw();
|
|
return true;
|
|
}
|
|
return false; // Back leaves the App
|
|
}
|
|
|
|
void GnssApp::update(uint32_t nowMs) {
|
|
if (nowMs - lastDrawMs_ >= 1000) { // the receiver reports once a second
|
|
lastDrawMs_ = nowMs;
|
|
requestRedraw();
|
|
}
|
|
}
|
|
|
|
void GnssApp::draw(Canvas& c) {
|
|
c.setTextDatum(top_left);
|
|
const auto& area = theme::kContent;
|
|
if (!gnss_.on()) {
|
|
c.setFont(&fonts::bold);
|
|
c.setTextColor(theme::kText);
|
|
c.drawString("GNSS is off", 4, area.y + 4);
|
|
c.setFont(&fonts::body);
|
|
c.setTextColor(theme::kMuted);
|
|
c.drawString("Settings > GNSS turns it on.", 4, area.y + 22);
|
|
return;
|
|
}
|
|
sky_ ? drawSky(c) : drawPosition(c);
|
|
c.setFont(&fonts::small);
|
|
c.setTextColor(theme::kMuted);
|
|
c.setTextDatum(top_right);
|
|
c.drawString(sky_ ? "Tab: position" : "Tab: sky", area.w - 3, area.y + area.h - 9);
|
|
c.setTextDatum(top_left);
|
|
}
|
|
|
|
void GnssApp::drawPosition(Canvas& c) {
|
|
const auto& area = theme::kContent;
|
|
const GnssState& s = gnss_.state();
|
|
uint32_t now = millis();
|
|
int used = 0;
|
|
for (auto& sat : s.satellites) used += sat.used;
|
|
char line[64];
|
|
|
|
c.setFont(&fonts::bold);
|
|
c.setTextColor(s.fix == FixType::None ? theme::kWarning : theme::kMessage);
|
|
if (!gnss_.receiving(now)) std::snprintf(line, sizeof line, "No data from the receiver");
|
|
else if (s.fix == FixType::None)
|
|
std::snprintf(line, sizeof line, "Searching: %u in view, %s", (unsigned)s.satellites.size(),
|
|
duration(now - gnss_.onSinceMs()).c_str());
|
|
else std::snprintf(line, sizeof line, "%s, %d of %u satellites", fixLabel(s.fix), s.satellitesUsed,
|
|
(unsigned)s.satellites.size());
|
|
c.drawString(line, 4, area.y + 2);
|
|
|
|
c.setFont(&fonts::body);
|
|
int y = area.y + 19;
|
|
auto row = [&](const char* label, const std::string& value) {
|
|
c.setTextColor(theme::kMuted);
|
|
c.drawString(label, 4, y);
|
|
c.setTextColor(theme::kText);
|
|
c.drawString(value.c_str(), 64, y);
|
|
y += theme::kLineHeight;
|
|
};
|
|
bool dms = settings_.getBool(Setting::CoordinatesDms);
|
|
if (s.positionValid) {
|
|
row("Lat", dms ? formatDms(s.latitude, true) : formatDecimal(s.latitude, true));
|
|
row("Lon", dms ? formatDms(s.longitude, false) : formatDecimal(s.longitude, false));
|
|
row("Locator", maidenhead(s.latitude, s.longitude));
|
|
std::snprintf(line, sizeof line, s.altitudeValid ? "%.0f m" : "-", s.altitudeM);
|
|
row("Altitude", line);
|
|
if (s.courseValid && s.speedKmh >= 1.0f)
|
|
std::snprintf(line, sizeof line, "%.1f km/h, %.0f\xC2\xB0", s.speedKmh, s.courseDeg);
|
|
else std::snprintf(line, sizeof line, "%.1f km/h", s.speedKmh);
|
|
row("Speed", line);
|
|
} else {
|
|
row("Lat", "-");
|
|
row("Lon", "-");
|
|
y += theme::kLineHeight * 3;
|
|
}
|
|
std::snprintf(line, sizeof line, "%.1f", s.hdop);
|
|
row("HDOP", s.fix == FixType::None ? "-" : line);
|
|
if (s.timeValid) std::snprintf(line, sizeof line, "%02d:%02d:%02d UTC", s.hour, s.minute, s.second);
|
|
row("Time", s.timeValid ? line : "-");
|
|
|
|
// The Track line, at the bottom left.
|
|
c.setFont(&fonts::small);
|
|
if (gnss_.tracking()) {
|
|
c.setTextColor(theme::kWarning);
|
|
std::snprintf(line, sizeof line, "REC %d point%s, %s r: stop", gnss_.trackPoints(),
|
|
gnss_.trackPoints() == 1 ? "" : "s", duration(now - gnss_.trackStartMs()).c_str());
|
|
} else if (!refusal_.empty() && now - refusalMs_ < 4000) {
|
|
c.setTextColor(theme::kWarning);
|
|
std::snprintf(line, sizeof line, "%s", refusal_.c_str());
|
|
} else {
|
|
c.setTextColor(theme::kMuted);
|
|
std::snprintf(line, sizeof line, "r: record a Track");
|
|
}
|
|
c.drawString(line, 4, area.y + area.h - 9);
|
|
}
|
|
|
|
void GnssApp::drawSky(Canvas& c) {
|
|
const auto& area = theme::kContent;
|
|
const GnssState& s = gnss_.state();
|
|
const int radius = (area.h - 6) / 2, cx = 4 + radius, cy = area.y + area.h / 2;
|
|
|
|
// The sky from above: horizon, 30° and 60° elevation rings, north up.
|
|
c.drawCircle(cx, cy, radius, theme::kMuted);
|
|
c.drawCircle(cx, cy, radius * 2 / 3, 0x4208);
|
|
c.drawCircle(cx, cy, radius / 3, 0x4208);
|
|
c.drawFastHLine(cx - radius, cy, 2 * radius, 0x4208);
|
|
c.drawFastVLine(cx, cy - radius, 2 * radius, 0x4208);
|
|
c.setFont(&fonts::small);
|
|
c.setTextColor(theme::kMuted);
|
|
c.setTextDatum(middle_center);
|
|
auto compass = [&](const char* letter, int x, int y) {
|
|
c.fillRect(x - 3, y - 4, 7, 9, theme::kBackground); // off the lines, so it reads
|
|
c.drawString(letter, x, y);
|
|
};
|
|
compass("N", cx, cy - radius + 5);
|
|
compass("S", cx, cy + radius - 4);
|
|
compass("E", cx + radius - 4, cy);
|
|
compass("W", cx - radius + 4, cy);
|
|
c.setTextDatum(top_left);
|
|
|
|
int inView[8] = {}, usedBy[8] = {};
|
|
for (auto& sat : s.satellites) {
|
|
int k = static_cast<int>(sat.system);
|
|
inView[k]++;
|
|
usedBy[k] += sat.used;
|
|
SkyPoint p = skyPosition(sat.azimuth, sat.elevation, cx, cy, radius - 3);
|
|
uint16_t color = colorOf(sat.system);
|
|
if (sat.used) c.fillCircle(p.x, p.y, 3, color);
|
|
else c.drawCircle(p.x, p.y, 3, sat.snr < 0 ? theme::kMuted : color);
|
|
}
|
|
|
|
// Legend: used / in view per constellation, then the Fix.
|
|
int x = cx + radius + 12, y = area.y + 4;
|
|
c.setFont(&fonts::body);
|
|
for (int k = 1; k < 7; k++) {
|
|
if (!inView[k]) continue;
|
|
auto system = static_cast<Constellation>(k);
|
|
c.fillCircle(x + 3, y + 6, 3, colorOf(system));
|
|
char line[32];
|
|
std::snprintf(line, sizeof line, "%-8s%d/%d", constellationName(system), usedBy[k], inView[k]);
|
|
c.setTextColor(theme::kText);
|
|
c.drawString(line, x + 10, y);
|
|
y += theme::kLineHeight;
|
|
}
|
|
if (s.satellites.empty()) {
|
|
c.setTextColor(theme::kMuted);
|
|
c.drawString("No satellites", x, y);
|
|
y += theme::kLineHeight;
|
|
}
|
|
c.setTextColor(s.fix == FixType::None ? theme::kWarning : theme::kMessage);
|
|
c.drawString(fixLabel(s.fix), x, y + 4);
|
|
c.setFont(&fonts::small);
|
|
c.setTextColor(theme::kMuted);
|
|
c.drawString("filled: in use", x, area.y + area.h - 20);
|
|
}
|
|
|
|
} // namespace roro
|