M2 step 3: GNSS Service, with standby and the clock from a Fix

The GNSS Service reads the receiver (UART 15/13, 115200, 1 KB buffer)
from its 50 ms tick and feeds the NMEA parser. With a Fix it sets the
clock (GNSS is the most trusted TimeSource) and refreshes it every 10
min. Settings gets GNSS On/Off and the coordinate format (Q64).

Off puts the receiver in standby with $PCAS12,65535, renewed hourly; On
wakes it with a hot start, $PCAS10,0. Both measured on the device: the
output stops within a second, and a command wakes it within a second.

Commands: gnss status (Fix, satellites per constellation, bytes and
sentences), gnss nmea on|off, gnss send <sentence>, gnss restart. The
probe is gone; never drive GPIO 15 (the receiver's output): the first
probe's swapped-pin attempt silenced it until a power cycle.

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:26:53 +02:00
co-authored by Claude Opus 5.5
parent c9b1ecb772
commit 988253ef02
13 changed files with 257 additions and 32 deletions
+131
View File
@@ -0,0 +1,131 @@
#include "services/gnss_service.h"
#include <Arduino.h>
#include <algorithm>
#include <cstdio>
#include "platform/console.h"
namespace roro {
namespace {
const char* fixName(gnss::FixType f) {
return f == gnss::FixType::ThreeD ? "3D" : f == gnss::FixType::TwoD ? "2D" : "none";
}
} // namespace
GnssService::GnssService(const Settings& settings, ClockService& clock, EventBus& bus)
: settings_(settings), clock_(clock), bus_(bus) {
parser_.onLine = [this](const std::string& line) {
lastLineMs_ = millis();
if (echo_) console.printf("nmea: %s\n", line.c_str());
};
}
void GnssService::start() { open(millis()); }
void GnssService::stop() { close(); }
void GnssService::open(uint32_t nowMs) {
Serial1.setRxBufferSize(1024); // before begin(): over 2 s of NMEA
Serial1.begin(kBaud, SERIAL_8N1, kRxPin, kTxPin);
open_ = true;
active_ = false; // tick() wakes it or puts it to sleep, per the setting
standbyMs_ = 0;
}
void GnssService::wake(uint32_t nowMs) {
send("PCAS10,0"); // hot start: ends a standby, keeps what the receiver knows
active_ = true;
openedMs_ = nowMs;
firstFixMs_ = 0;
lastLineMs_ = 0;
lastFix_ = gnss::FixType::None;
console.println("gnss: on");
}
void GnssService::standby(uint32_t nowMs) {
send("PCAS12,65535");
if (active_) console.println("gnss: off (receiver in standby)");
active_ = false;
standbyMs_ = nowMs;
}
void GnssService::close() {
if (!open_) return;
Serial1.end();
open_ = active_ = false;
lastLineMs_ = 0;
}
void GnssService::tick(uint32_t nowMs) {
if (!open_) open(nowMs);
bool wanted = settings_.getBool(Setting::GnssEnabled);
if (wanted && !active_) wake(nowMs);
if (!wanted && (active_ || !standbyMs_ || nowMs - standbyMs_ >= kStandbyRenewMs)) standby(nowMs);
char buf[128];
for (int avail; (avail = Serial1.available()) > 0;) {
size_t n = Serial1.read(reinterpret_cast<uint8_t*>(buf), std::min<size_t>(avail, sizeof buf));
if (n == 0) break;
bytes_ += n;
if (active_) parser_.feed(buf, n); // in standby: the last lines before it, discarded
}
if (!active_) return;
const gnss::GnssState& s = parser_.state();
if (s.fix != lastFix_) {
if (s.fix != gnss::FixType::None && !firstFixMs_) {
firstFixMs_ = nowMs - openedMs_;
console.printf("gnss: first Fix after %lu s\n", (unsigned long)(firstFixMs_ / 1000));
}
console.printf("gnss: Fix %s, %d satellites used\n", fixName(s.fix), s.satellitesUsed);
lastFix_ = s.fix;
}
// GNSS time is the most trusted source (ClockModel): set it with a Fix, then now and then.
if (s.timeValid && s.fix != gnss::FixType::None && (!clockSetMs_ || nowMs - clockSetMs_ >= kClockEveryMs)) {
if (clock_.set(s.utcSeconds(), TimeSource::Gnss) && !clockSetMs_) console.println("gnss: clock set");
clockSetMs_ = nowMs;
}
}
bool GnssService::send(const std::string& body) {
if (!open_) return false;
uint8_t sum = 0;
for (char c : body) sum ^= static_cast<uint8_t>(c);
char tail[8];
std::snprintf(tail, sizeof tail, "*%02X\r\n", sum);
std::string line = "$" + body + tail;
Serial1.write(reinterpret_cast<const uint8_t*>(line.data()), line.size());
return true;
}
void GnssService::printStatus(Print& out, uint32_t nowMs) const {
if (!active_) return (void)out.println("gnss: off (receiver in standby)");
const gnss::GnssState& s = parser_.state();
out.printf("gnss: %s, Fix %s, %d used, %u in view, HDOP %.1f\n",
receiving(nowMs) ? "receiving" : "silent", fixName(s.fix), s.satellitesUsed,
(unsigned)s.satellites.size(), s.hdop);
if (s.positionValid)
out.printf("gnss: %.6f %.6f, %s%.0f m, %.1f km/h\n", s.latitude, s.longitude,
s.altitudeValid ? "" : "altitude unknown ", s.altitudeM, s.speedKmh);
if (s.timeValid)
out.printf("gnss: %04d-%02d-%02d %02d:%02d:%02d UTC\n", s.year, s.month, s.day, s.hour, s.minute, s.second);
int perSystem[8] = {}, usedPerSystem[8] = {};
for (auto& sat : s.satellites) {
perSystem[static_cast<int>(sat.system)]++;
if (sat.used) usedPerSystem[static_cast<int>(sat.system)]++;
}
out.print("gnss: in view (used)");
for (int c = 1; c < 7; c++)
if (perSystem[c])
out.printf(" %s %d (%d)", gnss::constellationName(static_cast<gnss::Constellation>(c)), perSystem[c],
usedPerSystem[c]);
out.println();
out.printf("gnss: up %lu s, first Fix %s, %lu bytes, %lu sentences, %lu bad\n", (unsigned long)((nowMs - openedMs_) / 1000),
firstFixMs_ ? (std::to_string(firstFixMs_ / 1000) + " s").c_str() : "not yet", (unsigned long)bytes_,
(unsigned long)parser_.goodSentences(), (unsigned long)parser_.badSentences());
}
} // namespace roro
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <Print.h>
#include <string>
#include "event_bus.h"
#include "nmea_parser.h"
#include "service.h"
#include "services/clock_service.h"
#include "settings.h"
namespace roro {
// The GNSS receiver on the Cap LoRa-1262 (see CONTEXT.md and docs/milestones/M2.md): reads its
// NMEA from the main loop's tick (about 450 bytes/s, so a 1 KB UART buffer covers any stall),
// holds the current Fix, and sets the clock from it. Settings → GNSS switches it on and off.
class GnssService : public Service {
public:
static constexpr int kRxPin = 15, kTxPin = 13; // measured: M2 step 1
static constexpr uint32_t kBaud = 115200;
static constexpr uint32_t kSilentAfterMs = 3000; // no sentence for this long: receiver silent
static constexpr uint32_t kClockEveryMs = 600000; // refresh the clock from the Fix this often
// Off (Q58, measured): "$PCAS12,<s>" puts the receiver in standby for that long, and any
// 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);
const char* name() const override { return "gnss"; }
uint32_t tickIntervalMs() const override { return 50; }
void start() override;
void stop() override;
void tick(uint32_t nowMs) override;
bool on() const { return active_; }
bool receiving(uint32_t nowMs) const { return active_ && lastLineMs_ && nowMs - lastLineMs_ < kSilentAfterMs; }
const gnss::GnssState& state() const { return parser_.state(); }
// Debug aids (Q68): raw sentences to the console, a status report, and sending a command
// ("PCAS10,0" becomes "$PCAS10,0*1C\r\n").
void setEcho(bool echo) { echo_ = echo; }
void printStatus(Print& out, uint32_t nowMs) const;
bool send(const std::string& body);
void restart(uint32_t nowMs) {
close();
open(nowMs);
}
private:
void open(uint32_t nowMs); // the UART; the receiver's state follows the setting in tick()
void close();
void wake(uint32_t nowMs);
void standby(uint32_t nowMs);
const Settings& settings_;
ClockService& clock_;
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;
};
} // namespace roro