#include "app_keys.h" #include "gnss_app.h" #include #include #include #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); bool held = gnss_.heldForLora(); c.drawString(held ? "GNSS is paused" : "GNSS is off", 4, area.y + 4); c.setFont(&fonts::body); c.setTextColor(theme::kMuted); c.drawString(held ? "The LoRa radio is listening, and" : "Settings > GNSS turns it on.", 4, area.y + 22); if (held) c.drawString("Settings pauses GNSS for it.", 4, area.y + 22 + theme::kLineHeight); return; } sky_ ? drawSky(c) : drawPosition(c); } void GnssApp::help(std::vector& out) const { keys::add(out, keys::kGnss); } 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", 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 line[0] = 0; 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(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(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