diff --git a/docs/milestones/M2.md b/docs/milestones/M2.md index 0cbbbc1..cc6ebbf 100644 --- a/docs/milestones/M2.md +++ b/docs/milestones/M2.md @@ -8,11 +8,11 @@ | # | Decision | |---|---| -| Q58 | Settings has a GNSS On/Off switch, **On by default**. Off puts the receiver in standby if it accepts a command for it (measured in step 1); otherwise the firmware stops listening. | +| Q58 | Settings has a GNSS On/Off switch, **On by default**. Off puts the receiver in standby. *Measured:* `$PCAS12,` (CASIC) stops its output within a second, for up to at least 65535 s, and any command wakes it within a second; Off sends `PCAS12,65535` (renewed hourly), On sends a hot start, `PCAS10,0`. | | Q59 | The **GNSS App** has two views, switched with Tab. *Position*: latitude, longitude, altitude, speed, course, Fix (none / 2D / 3D), satellites used and in view, HDOP, UTC time. *Sky*: the satellites placed by azimuth and elevation, coloured by constellation, filled when used in the Fix. | | Q60 | "Radar" in M2 means the Sky view. A radar of other Nodes by distance and bearing needs the mesh: M4. | | Q61 | The **Status Bar** shows a GNSS mark: absent when off, muted while searching, normal with a 2D Fix, with the satellite count with a 3D Fix. | -| Q62 | GNSS time **sets the clock when NTP hasn't this boot**. NTP stays preferred when online. | +| Q62 | GNSS time **sets the clock once there's a Fix**, and refreshes it every 10 minutes. *Revised in step 3:* the clock's trust order from M0 (Mesh < NTP < GNSS) already ranks GNSS above NTP, which is right: GNSS time is at least as accurate. So GNSS also corrects a clock NTP set, not only an unset one. | | Q63 | A **Track** is started and stopped in the GNSS App. It's written as GPX to `/gnss/tracks/.gpx`, a point every 5 s when the position moved more than 5 m. It keeps recording with the App closed, with a Toast on start and stop and a Status Bar mark, and gets its own Storage Clean-up category. | | Q64 | Coordinates in **decimal degrees plus the Maidenhead locator**; a Settings switch for degrees, minutes and seconds. Metric units only. | | Q65 | **The position never leaves the device in M2.** Sharing it over the mesh, and at what precision, is decided in M4. | @@ -20,6 +20,8 @@ | Q67 | The receiver keeps its **defaults** (all constellations, 1 Hz). No receiver settings. Time to first fix is measured and recorded here. | | Q68 | Debug aids: `gnss status`, and `gnss nmea on/off` to stream the raw sentences to the console (USB serial and Debug Console). Raw NMEA is never written to the card. | +**Lesson (step 3):** the first probe also tried the pins swapped, driving the receiver's output line from the ESP32 for about a second. The receiver then went silent until a full power cycle (an ESP32 restart doesn't cut the Cap's power). Never drive GPIO 15. + ## Done when - The GNSS Service reads NMEA in the background whatever App is on screen, and a 3D Fix appears outdoors. diff --git a/lib/apps_model/src/settings_menu.cpp b/lib/apps_model/src/settings_menu.cpp index e82204e..bd8c019 100644 --- a/lib/apps_model/src/settings_menu.cpp +++ b/lib/apps_model/src/settings_menu.cpp @@ -20,6 +20,7 @@ const RowDef kRows[] = { {Row::Region, Kind::Choice, "Region"}, {Row::Timezone, Kind::Choice, "Timezone"}, {Row::Brightness, Kind::Slider, "Brightness"}, {Row::DimTimeout, Kind::Choice, "Dim after"}, {Row::OffTimeout, Kind::Choice, "Screen off after"}, {Row::Sound, Kind::Toggle, "Sound & LED"}, + {Row::Gnss, Kind::Toggle, "GNSS"}, {Row::Coordinates, Kind::Toggle, "Coordinates"}, {Row::ProbeMacs, Kind::Toggle, "Probe MACs"}, {Row::Wifi, Kind::Page, "Wi-Fi"}, {Row::Storage, Kind::Page, "Storage"}, {Row::Firmware, Kind::Page, "Firmware"}, @@ -79,6 +80,8 @@ std::string SettingsMenu::value(int i) const { case Row::DimTimeout: return formatSeconds(settings_.getInt(Setting::DimTimeoutS)); case Row::OffTimeout: return formatSeconds(settings_.getInt(Setting::OffTimeoutS)); case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off"; + case Row::Gnss: return settings_.getBool(Setting::GnssEnabled) ? "On" : "Off"; + case Row::Coordinates: return settings_.getBool(Setting::CoordinatesDms) ? "Deg min sec" : "Decimal"; case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised"; case Row::Wifi: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off"; default: return ""; @@ -122,6 +125,8 @@ std::string SettingsMenu::choose(int i, int c) { void SettingsMenu::toggle(int i) { if (row(i) == Row::Sound) settings_.setBool(Setting::Sound, !settings_.getBool(Setting::Sound)); + if (row(i) == Row::Gnss) settings_.setBool(Setting::GnssEnabled, !settings_.getBool(Setting::GnssEnabled)); + if (row(i) == Row::Coordinates) settings_.setBool(Setting::CoordinatesDms, !settings_.getBool(Setting::CoordinatesDms)); if (row(i) == Row::ProbeMacs) settings_.setBool(Setting::ProbeMacRaw, !settings_.getBool(Setting::ProbeMacRaw)); } diff --git a/lib/apps_model/src/settings_menu.h b/lib/apps_model/src/settings_menu.h index 26493ed..367b58c 100644 --- a/lib/apps_model/src/settings_menu.h +++ b/lib/apps_model/src/settings_menu.h @@ -11,7 +11,7 @@ namespace roro { // values, choice lists and validation messages. Rendering and navigation live in the App. class SettingsMenu { public: - enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, ProbeMacs, Wifi, Storage, Firmware, About }; + enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, Gnss, Coordinates, ProbeMacs, Wifi, Storage, Firmware, About }; enum class Kind { Text, Choice, Toggle, Slider, Page }; explicit SettingsMenu(Settings& settings) : settings_(settings) {} diff --git a/lib/gnss/src/nmea_parser.cpp b/lib/gnss/src/nmea_parser.cpp index cb4d9db..6778a9d 100644 --- a/lib/gnss/src/nmea_parser.cpp +++ b/lib/gnss/src/nmea_parser.cpp @@ -92,7 +92,10 @@ void NmeaParser::feed(const char* data, size_t len) { for (size_t i = 0; i < len; i++) { char c = data[i]; if (c == '\n') { - if (!overflow_ && !line_.empty()) sentence(line_); + if (!overflow_ && !line_.empty()) { + if (onLine) onLine(line_); + sentence(line_); + } line_.clear(); overflow_ = false; } else if (c != '\r') { diff --git a/lib/gnss/src/nmea_parser.h b/lib/gnss/src/nmea_parser.h index 3629737..aac737a 100644 --- a/lib/gnss/src/nmea_parser.h +++ b/lib/gnss/src/nmea_parser.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -53,6 +54,9 @@ class NmeaParser { // One sentence without its line end. False if malformed or its checksum is wrong. bool sentence(const std::string& line); + // Called with every complete line fed in, valid or not (`gnss nmea on` echoes them). + std::function onLine; + const GnssState& state() const { return state_; } uint32_t goodSentences() const { return good_; } uint32_t badSentences() const { return bad_; } diff --git a/lib/services/src/settings.cpp b/lib/services/src/settings.cpp index ca8cb69..c0d5224 100644 --- a/lib/services/src/settings.cpp +++ b/lib/services/src/settings.cpp @@ -29,6 +29,8 @@ const Definition kDefinitions[] = { {"sound", Kind::Bool, 1, nullptr, 0, 1}, {"probe_mac_raw", Kind::Bool, 1, nullptr, 0, 1}, {"wifi_on", Kind::Bool, 1, nullptr, 0, 1}, + {"gnss_on", Kind::Bool, 1, nullptr, 0, 1}, + {"coord_dms", Kind::Bool, 0, nullptr, 0, 1}, }; static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast(Setting::Count), "every Setting needs a definition"); diff --git a/lib/services/src/settings.h b/lib/services/src/settings.h index 8b48102..e949bc2 100644 --- a/lib/services/src/settings.h +++ b/lib/services/src/settings.h @@ -21,6 +21,8 @@ enum class Setting : uint8_t { Sound, // bool ProbeMacRaw, // bool: probe-request Logs keep raw MAC addresses WifiEnabled, // bool: the Wi-Fi Service stays Connected when a Saved Network is in range + GnssEnabled, // bool: the GNSS Service reads the receiver (M2, Q58) + CoordinatesDms, // bool: show degrees, minutes and seconds instead of decimal degrees (Q64) Count }; diff --git a/src/main.cpp b/src/main.cpp index 2fa7fb4..071a5da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -27,6 +26,7 @@ #include "services/irc_service.h" #include "services/clock_service.h" #include "services/debug_console.h" +#include "services/gnss_service.h" #include "services/power_service.h" #include "services/storage_service.h" #include "services/update_service.h" @@ -52,6 +52,7 @@ static BatteryService* battery; static StorageService* storageService; static PowerService* power; static ClockService* clockService; +static GnssService* gnssService; static SavedNetworks* savedNetworks; static WifiService* wifi; static IrcService* irc; @@ -131,6 +132,7 @@ void setup() { storageService = new StorageService(bus); power = new PowerService(settings); clockService = new ClockService(settings, bus); + gnssService = new GnssService(settings, *clockService, bus); savedNetworks = new SavedNetworks(nvs); savedNetworks->load(); wifi = new WifiService(settings, *savedNetworks, *clockService); @@ -141,6 +143,7 @@ void setup() { services.add(*power); services.add(*clockService); services.add(*battery); + services.add(*gnssService); services.add(*storageService); services.add(*wifi); services.add(*irc); @@ -325,6 +328,7 @@ static const char* const kHelp = "reboot restart\n" "boot other restart into the other app slot (manual Rollback)\n" "log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n" + "gnss status | gnss restart | gnss nmea on|off | gnss send \n" "crash the last crash: firmware, reason, task, backtrace\n" "coredump erase forget the core dump in flash\n" "key press a key: up down left right select back home del tab, or one character\n" @@ -386,33 +390,11 @@ static void runCommand(String line) { }); } if (line.startsWith("install ")) update->installFromSd(line.substring(8).c_str()); // Update from SD - if (line == "gnss probe") { // M2 step 1: which pins and baud rate carry NMEA (temporary) - struct Try { int rx, tx; uint32_t baud; } tries[] = {{15, 13, 115200}, {13, 15, 115200}, {15, 13, 9600}, {13, 15, 9600}}; - for (auto& t : tries) { - Serial1.begin(t.baud, SERIAL_8N1, t.rx, t.tx); - uint32_t start = millis(), bytes = 0, lines = 0; - std::string current, sample; - while (millis() - start < 1200) { - esp_task_wdt_reset(); // four listens outlast the 5 s loop watchdog - while (Serial1.available()) { - char c = Serial1.read(); - bytes++; - if (c == '\n') { - if (current.size() > 6 && current[0] == '$') { - lines++; - if (sample.size() < 400) sample += " " + current + "\n"; - } - current.clear(); - } else if (c != '\r' && current.size() < 120) current += c; - } - delay(5); - } - Serial1.end(); - console.printf("gnss probe: rx %d tx %d at %lu: %lu bytes, %lu NMEA lines\n", t.rx, t.tx, - (unsigned long)t.baud, (unsigned long)bytes, (unsigned long)lines); - if (lines) console.print(sample.c_str()); - } - } + if (line == "gnss status" && gnssService) gnssService->printStatus(console, millis()); + if ((line == "gnss nmea on" || line == "gnss nmea off") && gnssService) gnssService->setEcho(line.endsWith("on")); + if (line == "gnss restart" && gnssService) gnssService->restart(millis()); + if (line.startsWith("gnss send ") && gnssService) // the checksum is added + console.println(gnssService->send(line.substring(10).c_str()) ? "gnss: sent" : "gnss: off"); if (line == "crash") crash_report::print(console, nvs); if (line == "coredump erase") console.println(crash_report::erase() ? "coredump: erased" : "coredump: nothing to erase"); #ifdef RORO_DEBUG diff --git a/src/services/gnss_service.cpp b/src/services/gnss_service.cpp new file mode 100644 index 0000000..96ac751 --- /dev/null +++ b/src/services/gnss_service.cpp @@ -0,0 +1,131 @@ +#include "services/gnss_service.h" + +#include + +#include +#include + +#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(buf), std::min(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(c); + char tail[8]; + std::snprintf(tail, sizeof tail, "*%02X\r\n", sum); + std::string line = "$" + body + tail; + Serial1.write(reinterpret_cast(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(sat.system)]++; + if (sat.used) usedPerSystem[static_cast(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(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 diff --git a/src/services/gnss_service.h b/src/services/gnss_service.h new file mode 100644 index 0000000..ae72532 --- /dev/null +++ b/src/services/gnss_service.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +#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," 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 diff --git a/test/test_nmea/test_nmea.cpp b/test/test_nmea/test_nmea.cpp index 1884e0d..dee55c5 100644 --- a/test/test_nmea/test_nmea.cpp +++ b/test/test_nmea/test_nmea.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "nmea_parser.h" @@ -195,6 +196,17 @@ void test_an_overlong_line_is_dropped() { TEST_ASSERT_TRUE(p.state().positionValid); } +void test_every_complete_line_reaches_the_echo() { + NmeaParser p; + std::vector seen; + p.onLine = [&](const std::string& l) { seen.push_back(l); }; + std::string data = "$GNVTG,,,,,,,,,N*2E\r\nbroken\r\n$GN"; + p.feed(data.data(), data.size()); + TEST_ASSERT_EQUAL(2, static_cast(seen.size())); + TEST_ASSERT_EQUAL_STRING("$GNVTG,,,,,,,,,N*2E", seen[0].c_str()); + TEST_ASSERT_EQUAL_STRING("broken", seen[1].c_str()); +} + void test_constellation_names() { TEST_ASSERT_EQUAL_STRING("GPS", constellationName(Constellation::Gps)); TEST_ASSERT_EQUAL_STRING("Galileo", constellationName(Constellation::Galileo)); @@ -218,6 +230,7 @@ int main() { RUN_TEST(test_gsa_marks_the_satellites_used_per_system); RUN_TEST(test_split_feeds_and_noise_are_handled); RUN_TEST(test_an_overlong_line_is_dropped); + RUN_TEST(test_every_complete_line_reaches_the_echo); RUN_TEST(test_constellation_names); return UNITY_END(); } diff --git a/test/test_settings/test_settings.cpp b/test/test_settings/test_settings.cpp index ac6c7e2..7f2b6ea 100644 --- a/test/test_settings/test_settings.cpp +++ b/test/test_settings/test_settings.cpp @@ -26,6 +26,8 @@ void test_defaults_when_store_is_empty() { TEST_ASSERT_TRUE(s.getBool(Setting::Sound)); TEST_ASSERT_TRUE(s.getBool(Setting::ProbeMacRaw)); TEST_ASSERT_TRUE(s.getBool(Setting::WifiEnabled)); + TEST_ASSERT_TRUE(s.getBool(Setting::GnssEnabled)); // Q58: on by default + TEST_ASSERT_FALSE(s.getBool(Setting::CoordinatesDms)); // Q64: decimal degrees TEST_ASSERT_EQUAL_STRING("EU868", s.getString(Setting::Region).c_str()); TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", s.getString(Setting::Timezone).c_str()); } diff --git a/test/test_settings_menu/test_settings_menu.cpp b/test/test_settings_menu/test_settings_menu.cpp index 0a76767..1eebeb2 100644 --- a/test/test_settings_menu/test_settings_menu.cpp +++ b/test/test_settings_menu/test_settings_menu.cpp @@ -107,6 +107,19 @@ void test_names_are_text_rows_with_their_byte_limits() { TEST_ASSERT_EQUAL_STRING("RORO", f.menu.value(shortName).c_str()); } +void test_gnss_and_coordinate_rows_toggle() { + Fixture f; + int gnss = f.row(SettingsMenu::Row::Gnss), coords = f.row(SettingsMenu::Row::Coordinates); + TEST_ASSERT_EQUAL_STRING("On", f.menu.value(gnss).c_str()); + TEST_ASSERT_EQUAL_STRING("Decimal", f.menu.value(coords).c_str()); + f.menu.toggle(gnss); + f.menu.toggle(coords); + TEST_ASSERT_FALSE(f.settings.getBool(Setting::GnssEnabled)); + TEST_ASSERT_TRUE(f.settings.getBool(Setting::CoordinatesDms)); + TEST_ASSERT_EQUAL_STRING("Off", f.menu.value(gnss).c_str()); + TEST_ASSERT_EQUAL_STRING("Deg min sec", f.menu.value(coords).c_str()); +} + int main() { UNITY_BEGIN(); RUN_TEST(test_every_row_has_a_label); @@ -118,5 +131,6 @@ int main() { RUN_TEST(test_brightness_steps_with_left_right_within_bounds); RUN_TEST(test_wifi_is_a_page); RUN_TEST(test_names_are_text_rows_with_their_byte_limits); + RUN_TEST(test_gnss_and_coordinate_rows_toggle); return UNITY_END(); }