Public Access
Merge branch 's1': the main loop rests, and GNSS can pause for the radio
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -80,7 +80,7 @@ On a page, `b` bookmarks it, `s` saves it to the SD card to read offline (a non-
|
||||
|
||||
## LoRa Scanner
|
||||
|
||||
The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **never transmits**. The Sniffer lists what it hears, newest first: time, RSSI, SNR, and for Meshtastic packets the sender and receiver (their last 4 hex digits) and hops. Enter shows a packet's details: the Meshtastic header (which is never encrypted) and a hex dump. `p` picks one of the 7 Meshtastic presets allowed in EU868 (LongFast by default), `c` starts or stops a Capture: a pcap file with LoRaTap headers in `/captures/lora/`, for Wireshark. A Capture keeps recording with the App closed; otherwise the radio sleeps when the App isn't open. Tab switches to **Sweep**: the signal strength across 863–870 MHz in 100 kHz steps, as bars with peak hold and a waterfall, with the Sniffer's frequency marked; the Sniffer is paused meanwhile and picks up where it was. The Status Bar shows `L` while the radio listens (bright for a moment on each packet), `SW` while sweeping, and `CAP` while capturing.
|
||||
The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **never transmits**. The Sniffer lists what it hears, newest first: time, RSSI, SNR, and for Meshtastic packets the sender and receiver (their last 4 hex digits) and hops. Enter shows a packet's details: the Meshtastic header (which is never encrypted) and a hex dump. `p` picks one of the 7 Meshtastic presets allowed in EU868 (LongFast by default), `c` starts or stops a Capture: a pcap file with LoRaTap headers in `/captures/lora/`, for Wireshark. A Capture keeps recording with the App closed; otherwise the radio sleeps when the App isn't open. Tab switches to **Sweep**: the signal strength across 863–870 MHz in 100 kHz steps, as bars with peak hold and a waterfall, with the Sniffer's frequency marked; the Sniffer is paused meanwhile and picks up where it was. The GNSS receiver on the same Cap raises the radio's noise floor by 8 dB while it runs: Settings > "Pause GNSS for LoRa" (off by default) puts it in standby while the radio listens, except during a Track. The Status Bar shows `L` while the radio listens (bright for a moment on each packet), `SW` while sweeping, and `CAP` while capturing.
|
||||
|
||||
## Development aids
|
||||
|
||||
@@ -107,7 +107,7 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve
|
||||
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
||||
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from |
|
||||
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, and both app slots with their versions and OTA states |
|
||||
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, and each core's load |
|
||||
| `tasks` | FreeRTOS tasks over the next second: state, priority, lowest free stack, share of a core, each core's load, and how many passes the main loop made |
|
||||
| `net` | Bytes each network service has read and written since boot |
|
||||
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
||||
| `log level <0-5>` | ESP-IDF log level |
|
||||
@@ -119,9 +119,12 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve
|
||||
| `lora preset <name>` / `lora custom <MHz> <BW kHz> <SF> <CR> <sync hex> [preamble]` | Receive settings: a Meshtastic preset, or anything else (`lora custom 868.1 125 7 5 34 8` for LoRaWAN) |
|
||||
| `lora capture start` / `lora capture stop` | A LoRa Capture, as `c` in the App |
|
||||
| `lora sweep on [from MHz] [to MHz] [step kHz]` / `lora sweep off` / `lora sweep dump` | Sweep a band (863 870 100 by default), with a summary every 2 s (floor, strongest, peaks), or print the latest pass |
|
||||
| `gnss quiet on` / `gnss quiet off` | The "Pause GNSS for LoRa" setting |
|
||||
| `lora noise test [gnss\|quiet]` / `lora noise report` | Debug Builds: Sweep under one changed condition at a time to find what raises the noise floor (Wi-Fi goes off for a few seconds); then the result |
|
||||
| `lora inject <hex> [rssi] [snr]` | Debug Builds: a packet into the Scanner as if received (nothing is sent) |
|
||||
| `crash` | The last crash: which firmware, why, task, PC and backtrace (from the core dump in flash) |
|
||||
| `coredump erase` | Forgets the core dump |
|
||||
| `loop spin on` / `loop spin off` | Debug Builds: make the main loop spin without resting, to compare load and radio noise |
|
||||
| `crash abort` / `crash wdt` | Debug Builds: crash on purpose, or hang the main loop until the watchdog fires |
|
||||
| `help` | Lists the commands |
|
||||
|
||||
|
||||
+47
-1
@@ -1,6 +1,6 @@
|
||||
# S1 — System basics
|
||||
|
||||
**Status:** the three planned items are done: the SD driver fix in v0.6.1 (issue #21, ADR 0007), fixed IPv4 settings in v0.7.0 (issue #7), the System App in v0.8.0 (issue #11). Still open in the milestone: #39, following the SD driver upstream, and #40, the main loop's CPU use.
|
||||
**Status:** the three planned items are done: the SD driver fix in v0.6.1 (issue #21, ADR 0007), fixed IPv4 settings in v0.7.0 (issue #7), the System App in v0.8.0 (issue #11). v0.8.1 adds the resting main loop (issue #40) and the GNSS pause for the radio's noise (issue #20, still open for the 11 dB that remain). Still open in the milestone: #39, following the SD driver upstream.
|
||||
|
||||
**Goal:** the device works on any network, the card can be trusted, and you can see what the system is doing. A side milestone, like G1.
|
||||
|
||||
@@ -90,3 +90,49 @@ The App has five views, not four: Q121's network view is one of its own (Overvie
|
||||
- **`tasks` on the console** sampled twice inside one command at first, a quarter second apart, and showed the loop at 1 %: it was asleep in the command's own wait. It now samples, lets the loop run for a second, and prints.
|
||||
- **Low stack, flagged:** `IDLE0` (232 bytes left), `IDLE1` (328 to 352) and `spk_task` (256 to 264), all the framework's own tasks.
|
||||
- **Cost:** 15.6 KB of flash for the App and the counters (1,742,723 bytes, release). Nothing while it's closed; about 2 KB of history and samples while it's open.
|
||||
|
||||
## The main loop rests (issue #40)
|
||||
|
||||
The loop polled the keyboard, ticked the Services, ran the consoles and redrew when needed, then came straight back: 50,000 passes a second, and core 1 100 % busy with the device idle and the screen off.
|
||||
|
||||
Nothing needs that. The keyboard controller buffers key events; the consoles and the radio have their own tasks or interrupts; no Service asks for a tick more often than every 50 ms. So after each pass the loop now rests: **5 ms with the screen on, 20 ms with it off**, and not at all during a serial file transfer (`sd put`), which reads its bytes from the loop. Safe Mode's loop rests 5 ms too. Debug Builds have `loop spin on|off` to bring the old behaviour back for comparison.
|
||||
|
||||
### Measured (2026-10-06, Debug Build, Wi-Fi connected, GNSS on, on USB power)
|
||||
|
||||
| | Spinning | Resting |
|
||||
|---|---|---|
|
||||
| Passes a second, screen off | 50,160 | 50 |
|
||||
| Core 1 load, screen off | 100 % | 1 % |
|
||||
| Passes a second, screen on (Launcher) | 1,203 | 167 |
|
||||
| Core 1 load, screen on | 62 % | 10 % |
|
||||
| Chip temperature at rest, settled | 38.3 C | 34.3 C |
|
||||
| A 1.8 MB upload over the Debug Console | about 230 KB/s | 288 KB/s |
|
||||
|
||||
- Still working at this pace: GNSS (a 3D Fix, 22 satellites), a Gemini fetch (52 KB), the upload read back by SHA-256, the Sweep (still 606 to 610 ms a pass), the radio's DIO1 interrupt.
|
||||
- **Not measured:** the current drawn (no meter on the battery line), and how typing feels on the real keyboard: a key now waits up to 5 ms for the loop, 20 ms if it's the one that wakes the screen.
|
||||
- **The radio's noise floor didn't move** (-97 to -99 dBm at 125 kHz either way): the spinning loop wasn't the source (issue #20).
|
||||
- **Not done:** real sleep. The framework is built without power management (`CONFIG_PM_ENABLE` is off), so an idle core only halts until the next interrupt. Automatic light sleep would need the framework rebuilt with it, Wi-Fi in modem sleep, and the USB serial port's behaviour checked. A next step if battery life calls for it.
|
||||
|
||||
## The radio's noise: the GNSS receiver (issue #20)
|
||||
|
||||
M3 found the LoRa radio's noise floor about 15 dB above what the chip hears alone, and that the source travels with the device. Which part? Debug Builds got a self-test, `lora noise test`: it changes one thing at a time, Sweeps the band eight passes (568 readings), records the median as the floor, and puts the thing back. It runs on the device by itself, because one condition switches Wi-Fi off, and `lora noise report` prints the result afterwards.
|
||||
|
||||
### Measured (2026-10-06, indoors, on USB power, dBm at 125 kHz)
|
||||
|
||||
| Condition | Floor |
|
||||
|---|---|
|
||||
| Antenna switched off (the chip alone) | -117 |
|
||||
| Antenna on, GNSS in standby | -106 |
|
||||
| Antenna on, GNSS running (as shipped) | -98 |
|
||||
|
||||
- **The GNSS receiver, while it runs, raises the floor by 8 dB.** Three runs: -98 or -99 with it running, -106 in standby, every time. On LongFast (250 kHz) the Sniffer's own reading goes from about -93.5 to -101.5 dBm.
|
||||
- **It's the receiver working, not its serial line:** with one NMEA sentence a second instead of twenty (`PCAS03`), the receiver still tracking, the floor stays at -98.
|
||||
- **Nothing else moves it by more than 1 dB**, with GNSS running or in standby: the main loop spinning or resting, the CPU at 240, 160 or 80 MHz, Wi-Fi on or off, the screen on or off, the radio chip's regulator as DC-DC or LDO, its receive gain boosted or not.
|
||||
- **11 dB remain** between the antenna connected with GNSS quiet (-106) and the chip alone (-117). It comes in through the antenna and none of those switches changes it: the surroundings, or parts of the Cardputer that can't be switched off. Not separated: that needs another place, or the antenna on a cable away from the case.
|
||||
- M3's quick check had GNSS at "1 or 2 dB": it read one frequency for a few seconds, in a noisier spot. The median over the band is the better measure.
|
||||
|
||||
### What the firmware does about it
|
||||
|
||||
**Settings > Pause GNSS for LoRa**, off by default: while the LoRa radio listens or sweeps, the GNSS receiver waits in standby, and wakes when the radio goes back to sleep (a Fix again after about 7 s here). Never during a Track. The GNSS App says "GNSS is paused" meanwhile. `gnss quiet on|off` on the console.
|
||||
|
||||
It's off by default because GNSS on by default was decided in M2 (Q58), and from M4 the radio listens all the time: then "pause while listening" means GNSS mostly off, which is a decision about position, the clock and Tracks, for M4's design round (issue #23).
|
||||
|
||||
@@ -20,7 +20,8 @@ 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::Gnss, Kind::Toggle, "GNSS"}, {Row::GnssQuiet, Kind::Toggle, "Pause GNSS for LoRa"},
|
||||
{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"},
|
||||
@@ -81,6 +82,7 @@ std::string SettingsMenu::value(int i) const {
|
||||
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::GnssQuiet: return settings_.getBool(Setting::GnssQuietForLora) ? "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";
|
||||
@@ -126,6 +128,7 @@ 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::GnssQuiet) settings_.setBool(Setting::GnssQuietForLora, !settings_.getBool(Setting::GnssQuietForLora));
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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, Gnss, Coordinates, ProbeMacs, Wifi, Storage, Firmware, About };
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, Storage, Firmware, About };
|
||||
enum class Kind { Text, Choice, Toggle, Slider, Page };
|
||||
|
||||
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
||||
|
||||
@@ -39,6 +39,7 @@ const Definition kDefinitions[] = {
|
||||
{"dns_always", Kind::Bool, 0, nullptr, 0, 1},
|
||||
{"ntp1", Kind::String, 0, "pool.ntp.org", 1, 63},
|
||||
{"ntp2", Kind::String, 0, "time.cloudflare.com", 0, 63},
|
||||
{"gnss_quiet", Kind::Bool, 0, nullptr, 0, 1}, // off: GNSS stays on, as decided in M2 (Q58)
|
||||
};
|
||||
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||
"every Setting needs a definition");
|
||||
|
||||
@@ -29,6 +29,7 @@ enum class Setting : uint8_t {
|
||||
DnsAlways, // bool: use them on Automatic (DHCP) networks too, instead of DHCP's
|
||||
Ntp1, // string: the first NTP server, a host name or an IPv4 address (S1, Q110)
|
||||
Ntp2, // string: the second, or empty
|
||||
GnssQuietForLora, // bool: put the GNSS receiver in standby while the LoRa radio listens (issue #20)
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
@@ -71,10 +71,12 @@ void GnssApp::draw(Canvas& c) {
|
||||
if (!gnss_.on()) {
|
||||
c.setFont(&fonts::bold);
|
||||
c.setTextColor(theme::kText);
|
||||
c.drawString("GNSS is off", 4, area.y + 4);
|
||||
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("Settings > GNSS turns it on.", 4, area.y + 22);
|
||||
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);
|
||||
|
||||
+122
-1
@@ -38,6 +38,7 @@
|
||||
#include "services/gnss_service.h"
|
||||
#include "services/power_service.h"
|
||||
#include "services/lora_capture_service.h"
|
||||
#include "services/noise_test.h"
|
||||
#include "services/radio_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "services/update_service.h"
|
||||
@@ -66,6 +67,9 @@ static ClockService* clockService;
|
||||
static GnssService* gnssService;
|
||||
static RadioService* radioService;
|
||||
static LoraCaptureService* loraCapture;
|
||||
#ifdef RORO_DEBUG
|
||||
static NoiseTest* noiseTest;
|
||||
#endif
|
||||
static GeminiService* geminiService;
|
||||
static SavedNetworks* savedNetworks;
|
||||
static WifiService* wifi;
|
||||
@@ -180,6 +184,9 @@ void setup() {
|
||||
services.add(*gnssService);
|
||||
services.add(*radioService);
|
||||
loraCapture = new LoraCaptureService(*radioService, *storageService, *clockService, bus);
|
||||
#ifdef RORO_DEBUG
|
||||
noiseTest = new NoiseTest(*radioService);
|
||||
#endif
|
||||
services.add(*loraCapture);
|
||||
services.add(*storageService);
|
||||
services.add(*wifi);
|
||||
@@ -390,10 +397,17 @@ static std::vector<TaskSample> tasksBefore;
|
||||
static uint32_t tasksTotal = 0, tasksDueMs = 0;
|
||||
static bool tasksPending = false;
|
||||
|
||||
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
|
||||
#ifdef RORO_DEBUG
|
||||
static bool loopSpin = false; // `loop spin on`: no rest between passes, to compare load and radio noise
|
||||
#endif
|
||||
static uint32_t tasksPasses = 0;
|
||||
|
||||
static void tasksStep() {
|
||||
if (!tasksPending || static_cast<int32_t>(millis() - tasksDueMs) < 0) return;
|
||||
tasksPending = false;
|
||||
system_info::printTasks(console, tasksBefore, tasksTotal);
|
||||
console.printf("loop: %lu passes in the last second, chip %.1f C\n", (unsigned long)(loopPasses - tasksPasses), temperatureRead());
|
||||
std::vector<TaskSample>().swap(tasksBefore);
|
||||
}
|
||||
|
||||
@@ -407,6 +421,7 @@ static const char* const kHelp =
|
||||
"lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)\n"
|
||||
"lora sweep on [from MHz] [to MHz] [step kHz] | off | dump RSSI across a band (863 870 100)\n"
|
||||
"lora custom <MHz> <BW kHz> <SF> <CR 5-8> <sync hex> [preamble] e.g. 868.1 125 7 5 34 8 (LoRaWAN)\n"
|
||||
"gnss quiet on|off pause the GNSS receiver while the LoRa radio listens (it costs the radio 8 dB)\n"
|
||||
"gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>\n"
|
||||
"crash the last crash: firmware, reason, task, backtrace\n"
|
||||
"coredump erase forget the core dump in flash\n"
|
||||
@@ -421,6 +436,8 @@ static const char* const kHelp =
|
||||
#ifdef RORO_DEBUG
|
||||
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
||||
"wifi ip ... try <seconds> | wifi ip keep a trial IP setting: back to the previous one unless kept\n"
|
||||
"loop spin on|off make the main loop spin without resting, to compare load and radio noise\n"
|
||||
"lora noise test [gnss|quiet] | lora noise report Sweep under one changed condition at a time (Wi-Fi goes off for a moment)\n"
|
||||
"lora inject <hex> [rssi] [snr] a packet into the LoRa Scanner as if received (nothing is sent)\n"
|
||||
"coredump get (Debug Console only) send the raw core dump: use scripts/rdbg.py coredump\n"
|
||||
"reset (Debug Console only) restart at once, even if the main loop is stuck\n"
|
||||
@@ -448,9 +465,16 @@ static void runCommand(String line) {
|
||||
console.printf("update: %s\n", update->onProbation() ? "on probation" : "confirmed");
|
||||
system_info::printSlots(console, nvs);
|
||||
}
|
||||
#ifdef RORO_DEBUG
|
||||
if (line == "loop spin on" || line == "loop spin off") {
|
||||
loopSpin = line.endsWith("on");
|
||||
console.printf("loop: %s\n", loopSpin ? "spinning, no rest" : "resting between passes");
|
||||
}
|
||||
#endif
|
||||
if (line == "tasks") { // sampled now, printed a second later by tasksStep(): the loop must run in between
|
||||
tasksTotal = system_info::sampleTasks(tasksBefore);
|
||||
tasksDueMs = millis() + 1000;
|
||||
tasksPasses = loopPasses;
|
||||
tasksPending = true;
|
||||
}
|
||||
if (line == "net") { // bytes each service has read and written since boot (S1, Q121)
|
||||
@@ -488,6 +512,72 @@ static void runCommand(String line) {
|
||||
if (line == "lora probe" && radioService) radioService->probe(console);
|
||||
if (line == "lora status" && radioService) radioService->printStatus(console);
|
||||
if ((line == "lora rx on" || line == "lora rx off") && radioService) radioService->setEcho(line.endsWith("on"));
|
||||
#ifdef RORO_DEBUG
|
||||
if (line == "lora noise report" && noiseTest) noiseTest->printReport(console);
|
||||
if (line == "lora noise test" && noiseTest && !noiseTest->running()) {
|
||||
// One thing changed at a time, each put back before the next (issue #20). Wi-Fi off cuts the
|
||||
// Debug Console: the report prints when it's over, and `lora noise report` shows it again.
|
||||
using C = NoiseTest::Condition;
|
||||
auto radioOptions = [](bool ldo, bool boosted) { return [=]() { radioService->debugOptions(ldo, boosted); }; };
|
||||
std::vector<C> conditions = {
|
||||
{"as it is", nullptr, nullptr},
|
||||
{"loop spinning", []() { loopSpin = true; }, []() { loopSpin = false; }},
|
||||
{"GNSS in standby", []() { gnssService->send("PCAS12,30"); }, []() { gnssService->restart(millis()); }, 2500},
|
||||
{"CPU at 160 MHz", []() { setCpuFrequencyMhz(160); }, []() { setCpuFrequencyMhz(240); }},
|
||||
{"CPU at 80 MHz", []() { setCpuFrequencyMhz(80); }, []() { setCpuFrequencyMhz(240); }},
|
||||
{"radio regulator: LDO", radioOptions(true, true), radioOptions(false, true)},
|
||||
{"radio gain not boosted", radioOptions(false, false), radioOptions(false, true)},
|
||||
{"as it is, again", nullptr, nullptr},
|
||||
{"Wi-Fi off", []() { wifi->debugPause(true); }, []() { wifi->debugPause(false); }, 4000},
|
||||
{"screen on", []() { power->onKey(millis()); }, nullptr, 1000},
|
||||
{"as it is, at the end", nullptr, nullptr, 8000},
|
||||
};
|
||||
noiseTest->start(std::move(conditions));
|
||||
}
|
||||
if (line == "lora noise test quiet" && noiseTest && !noiseTest->running()) {
|
||||
// The same conditions as the first test, with the GNSS receiver in standby throughout: with
|
||||
// the loudest source out of the way, what else shows?
|
||||
using C = NoiseTest::Condition;
|
||||
auto radioOptions = [](bool ldo, bool boosted) { return [=]() { radioService->debugOptions(ldo, boosted); }; };
|
||||
std::vector<C> conditions = {
|
||||
{"GNSS on (reference)", nullptr, nullptr},
|
||||
{"GNSS in standby", []() { gnssService->send("PCAS12,300"); }, nullptr, 2500},
|
||||
{"+ loop spinning", []() { loopSpin = true; }, []() { loopSpin = false; }},
|
||||
{"+ CPU at 160 MHz", []() { setCpuFrequencyMhz(160); }, []() { setCpuFrequencyMhz(240); }},
|
||||
{"+ CPU at 80 MHz", []() { setCpuFrequencyMhz(80); }, []() { setCpuFrequencyMhz(240); }},
|
||||
{"+ radio regulator: LDO", radioOptions(true, true), radioOptions(false, true)},
|
||||
{"+ radio gain not boosted", radioOptions(false, false), radioOptions(false, true)},
|
||||
{"GNSS in standby, again", nullptr, nullptr},
|
||||
{"+ Wi-Fi off", []() { wifi->debugPause(true); }, []() { wifi->debugPause(false); }, 4000},
|
||||
{"+ screen on", []() { power->onKey(millis()); }, nullptr, 1000},
|
||||
{"+ antenna switched off", []() { radioService->debugAntenna(false); }, []() { radioService->debugAntenna(true); }},
|
||||
{"+ antenna off, gain not boosted", []() { radioService->debugAntenna(false); radioService->debugOptions(false, false); },
|
||||
[]() { radioService->debugAntenna(true); radioService->debugOptions(false, true); }},
|
||||
{"GNSS in standby, at the end", nullptr, []() { gnssService->restart(millis()); }, 8000},
|
||||
};
|
||||
noiseTest->start(std::move(conditions));
|
||||
}
|
||||
if (line == "lora noise test gnss" && noiseTest && !noiseTest->running()) {
|
||||
// The first test pointed at the GNSS receiver. Is it the receiver working, or its serial
|
||||
// line (about 20 sentences a second, next to the antenna)? Standby stops both; one sentence
|
||||
// a second quietens the line and leaves the receiver tracking. PCAS03 picks the sentences:
|
||||
// GGA, GLL, GSA, GSV, RMC, VTG, ZDA, ANT; all eight are on by default.
|
||||
using C = NoiseTest::Condition;
|
||||
auto standby = []() { gnssService->send("PCAS12,30"); };
|
||||
auto wake = []() { gnssService->restart(millis()); };
|
||||
std::vector<C> conditions = {
|
||||
{"as it is", nullptr, nullptr},
|
||||
{"GNSS in standby", standby, wake, 2500},
|
||||
{"GNSS on again", nullptr, nullptr, 5000},
|
||||
{"GNSS: RMC only", []() { gnssService->send("PCAS03,0,0,0,0,1,0,0,0,0,0,,,0,0"); },
|
||||
[]() { gnssService->send("PCAS03,1,1,1,1,1,1,1,1,0,0,,,0,0"); }, 2500},
|
||||
{"GNSS: all sentences again", nullptr, nullptr, 3000},
|
||||
{"GNSS in standby, again", standby, wake, 2500},
|
||||
{"GNSS on again", nullptr, nullptr, 5000},
|
||||
};
|
||||
noiseTest->start(std::move(conditions));
|
||||
}
|
||||
#endif
|
||||
if (line.startsWith("lora sweep") && radioService) { // on [from MHz] [to MHz] [step kHz] | off | dump
|
||||
if (line == "lora sweep dump") radioService->printSweep(console);
|
||||
else if (line == "lora sweep off") {
|
||||
@@ -535,6 +625,10 @@ static void runCommand(String line) {
|
||||
if (p) radioService->setPreset(*p);
|
||||
console.printf("lora preset: %s\n", p ? p->name : "unknown (LongFast LongSlow MediumSlow MediumFast ShortSlow ShortFast LongMod)");
|
||||
}
|
||||
if (line == "gnss quiet on" || line == "gnss quiet off") { // Settings > Pause GNSS for LoRa (issue #20)
|
||||
settings.setBool(Setting::GnssQuietForLora, line.endsWith("on"));
|
||||
console.printf("gnss quiet: %s\n", line.endsWith("on") ? "GNSS pauses while the LoRa radio listens" : "GNSS stays on");
|
||||
}
|
||||
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());
|
||||
@@ -778,8 +872,30 @@ static void loopSafeMode() {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// How long the main loop rests after a pass (issue #40). Spinning, it made 50,000 passes a
|
||||
// second and kept core 1 100 % busy for nothing: keys are buffered by the keyboard controller,
|
||||
// the consoles and the radio have their own tasks, and no Service asks for a tick more often than
|
||||
// every 50 ms. Shorter with the screen on, so a key or a redraw never waits long.
|
||||
constexpr uint32_t kLoopRestScreenOnMs = 5;
|
||||
constexpr uint32_t kLoopRestScreenOffMs = 20;
|
||||
static void loopPass();
|
||||
|
||||
void loop() {
|
||||
if (safeMode) return loopSafeMode();
|
||||
loopPasses++;
|
||||
if (safeMode) {
|
||||
loopSafeMode();
|
||||
delay(kLoopRestScreenOnMs);
|
||||
return;
|
||||
}
|
||||
loopPass();
|
||||
#ifdef RORO_DEBUG
|
||||
if (loopSpin) return;
|
||||
#endif
|
||||
if (upload.active()) return; // a serial file transfer: every byte is read promptly
|
||||
delay(power->screen() == ScreenState::Off ? kLoopRestScreenOffMs : kLoopRestScreenOnMs);
|
||||
}
|
||||
|
||||
static void loopPass() {
|
||||
#ifdef RORO_TEST_CRASH
|
||||
// Test builds only (never in a release): crash during Probation to exercise Rollback.
|
||||
if (millis() > 5000) abort();
|
||||
@@ -793,6 +909,7 @@ void loop() {
|
||||
tasksStep();
|
||||
#ifdef RORO_DEBUG
|
||||
ipTrialStep();
|
||||
if (noiseTest) noiseTest->step(now);
|
||||
#endif
|
||||
noteStableOnce(now);
|
||||
uploadStep();
|
||||
@@ -806,6 +923,10 @@ void loop() {
|
||||
for (auto& e : events) apps->handleKey(e);
|
||||
}
|
||||
|
||||
// Issue #20: the GNSS receiver costs the LoRa radio 8 dB while it runs. If the user chose so,
|
||||
// it waits in standby while the radio listens or sweeps; never while a Track is recording.
|
||||
gnssService->holdForLora(settings.getBool(Setting::GnssQuietForLora) && !gnssService->tracking() &&
|
||||
(radioService->listening() || radioService->sweeping()));
|
||||
services.tick(now);
|
||||
bus.dispatch();
|
||||
notifier->update(now);
|
||||
|
||||
@@ -66,7 +66,7 @@ void GnssService::close() {
|
||||
|
||||
void GnssService::tick(uint32_t nowMs) {
|
||||
if (!open_) open(nowMs);
|
||||
bool wanted = settings_.getBool(Setting::GnssEnabled);
|
||||
bool wanted = settings_.getBool(Setting::GnssEnabled) && !held_;
|
||||
if (wanted && !active_) wake(nowMs);
|
||||
if (!wanted && (active_ || !standbyMs_ || nowMs - standbyMs_ >= kStandbyRenewMs)) standby(nowMs);
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ class GnssService : public Service {
|
||||
void tick(uint32_t nowMs) override;
|
||||
|
||||
bool on() const { return active_; }
|
||||
// Standby while the LoRa radio listens, when the user chose that (issue #20): the receiver
|
||||
// raises the radio's noise floor by 8 dB while it runs. Never asked for during a Track.
|
||||
void holdForLora(bool hold) { held_ = hold; }
|
||||
bool heldForLora() const { return held_ && !active_; }
|
||||
bool receiving(uint32_t nowMs) const { return active_ && lastLineMs_ && nowMs - lastLineMs_ < kSilentAfterMs; }
|
||||
const gnss::GnssState& state() const { return parser_.state(); }
|
||||
uint32_t onSinceMs() const { return openedMs_; } // when the receiver was last woken
|
||||
@@ -71,7 +75,7 @@ class GnssService : public Service {
|
||||
StorageService& storage_;
|
||||
EventBus& bus_;
|
||||
gnss::NmeaParser parser_;
|
||||
bool open_ = false, active_ = false, echo_ = false;
|
||||
bool open_ = false, active_ = false, echo_ = false, held_ = false;
|
||||
uint32_t bytes_ = 0, standbyMs_ = 0;
|
||||
uint32_t openedMs_ = 0, lastLineMs_ = 0, firstFixMs_ = 0, clockSetMs_ = 0;
|
||||
gnss::FixType lastFix_ = gnss::FixType::None;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#ifdef RORO_DEBUG
|
||||
|
||||
#include "services/noise_test.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
#include "platform/console.h"
|
||||
#include "sweep_view.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
void NoiseTest::start(std::vector<Condition> conditions, int passes) {
|
||||
if (running()) return;
|
||||
conditions_ = std::move(conditions);
|
||||
results_.clear();
|
||||
report_.clear();
|
||||
passes_ = passes;
|
||||
index_ = 0;
|
||||
console.printf("noise test: %u conditions, %d passes each\n", (unsigned)conditions_.size(), passes_);
|
||||
begin(millis());
|
||||
}
|
||||
|
||||
void NoiseTest::begin(uint32_t nowMs) {
|
||||
const Condition& c = conditions_[index_];
|
||||
if (c.apply) c.apply();
|
||||
radio_.sweep(true); // sets the radio up again, with whatever the condition changed
|
||||
std::fill(std::begin(lowest_), std::end(lowest_), 0);
|
||||
std::fill(std::begin(counts_), std::end(counts_), 0);
|
||||
seen_ = 0;
|
||||
phase_ = Phase::Settle;
|
||||
phaseMs_ = nowMs;
|
||||
}
|
||||
|
||||
void NoiseTest::step(uint32_t nowMs) {
|
||||
if (phase_ == Phase::Idle) return;
|
||||
const Condition& c = conditions_[index_];
|
||||
if (phase_ == Phase::Settle) {
|
||||
if (nowMs - phaseMs_ < c.settleMs) return;
|
||||
lastSeq_ = radio_.sweeps();
|
||||
phase_ = Phase::Sweep;
|
||||
phaseMs_ = nowMs;
|
||||
return;
|
||||
}
|
||||
SweepFrame f;
|
||||
if (radio_.sweeps() != lastSeq_ && radio_.sweepFrame(f)) {
|
||||
lastSeq_ = f.seq;
|
||||
fromHz_ = f.fromHz, stepHz_ = f.stepHz, steps_ = f.steps;
|
||||
for (uint16_t i = 0; i < f.steps; i++) {
|
||||
lowest_[i] = seen_ ? std::min(lowest_[i], f.dbm[i]) : f.dbm[i];
|
||||
counts_[std::clamp<int>(-f.dbm[i], 0, 127)]++;
|
||||
}
|
||||
seen_++;
|
||||
}
|
||||
bool stuck = nowMs - phaseMs_ > 20000; // the radio never swept: don't hang the test
|
||||
if (seen_ < passes_ && !stuck) return;
|
||||
|
||||
Result r;
|
||||
r.name = c.name;
|
||||
if (seen_) {
|
||||
// The floor: the median of every reading. The top and the peaks: from the lowest reading
|
||||
// at each step, so a burst in one pass doesn't count and a steady carrier does.
|
||||
uint32_t total = 0, half = static_cast<uint32_t>(seen_) * steps_ / 2;
|
||||
for (int level = 127; level >= 0; level--) {
|
||||
total += counts_[level];
|
||||
if (total > half) {
|
||||
r.floor = -level;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lora::SweepStats st = lora::summarize(lowest_, steps_, fromHz_, stepHz_);
|
||||
r.top = st.top;
|
||||
char p[32];
|
||||
for (auto& peak : st.peaks) {
|
||||
std::snprintf(p, sizeof p, "%s%.1f %d", r.peaks.empty() ? "" : ", ", peak.hz / 1e6, peak.dbm);
|
||||
r.peaks += p;
|
||||
}
|
||||
} else {
|
||||
r.name += " (no sweep)";
|
||||
}
|
||||
results_.push_back(r);
|
||||
radio_.sweep(false);
|
||||
if (c.restore) c.restore();
|
||||
if (++index_ < conditions_.size()) begin(nowMs);
|
||||
else finish();
|
||||
}
|
||||
|
||||
void NoiseTest::finish() {
|
||||
phase_ = Phase::Idle;
|
||||
char line[160];
|
||||
report_.push_back("noise test: floor = median of every reading; top and peaks = steady (lowest of the passes), dBm at 125 kHz");
|
||||
for (auto& r : results_) {
|
||||
std::snprintf(line, sizeof line, "noise test: %-26s floor %4d top %4d %s", r.name.c_str(), r.floor, r.top,
|
||||
r.peaks.empty() ? "no steady peak" : r.peaks.c_str());
|
||||
report_.push_back(line);
|
||||
}
|
||||
report_.push_back("noise test: done");
|
||||
printReport(console);
|
||||
}
|
||||
|
||||
void NoiseTest::printReport(Print& out) const {
|
||||
if (report_.empty()) return (void)out.println(running() ? "noise test: still running" : "noise test: none run yet");
|
||||
for (auto& l : report_) out.println(l.c_str());
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
#endif // RORO_DEBUG
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef RORO_DEBUG
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "services/radio_service.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// `lora noise test` (Debug Builds, issue #20): which part of the Cardputer raises the radio's
|
||||
// noise floor? Applies one condition at a time, Sweeps the band a few passes, records the floor
|
||||
// and the steady peaks, and puts things back. It runs from the main loop on its own, because one
|
||||
// condition switches Wi-Fi off, and prints its report once the console can be reached again.
|
||||
class NoiseTest {
|
||||
public:
|
||||
struct Condition {
|
||||
std::string name;
|
||||
std::function<void()> apply, restore; // either may be empty
|
||||
uint32_t settleMs = 1500;
|
||||
};
|
||||
|
||||
explicit NoiseTest(RadioService& radio) : radio_(radio) {}
|
||||
void start(std::vector<Condition> conditions, int passes = 8);
|
||||
void step(uint32_t nowMs); // from the main loop
|
||||
bool running() const { return phase_ != Phase::Idle; }
|
||||
void printReport(Print& out) const;
|
||||
|
||||
private:
|
||||
enum class Phase { Idle, Settle, Sweep };
|
||||
struct Result {
|
||||
std::string name;
|
||||
int floor = 0, top = 0;
|
||||
std::string peaks; // steady ones: the lowest reading at each step over the passes
|
||||
};
|
||||
void begin(uint32_t nowMs);
|
||||
void finish();
|
||||
|
||||
RadioService& radio_;
|
||||
std::vector<Condition> conditions_;
|
||||
std::vector<Result> results_;
|
||||
size_t index_ = 0;
|
||||
int passes_ = 8, seen_ = 0;
|
||||
Phase phase_ = Phase::Idle;
|
||||
uint32_t phaseMs_ = 0, lastSeq_ = 0;
|
||||
int8_t lowest_[SweepFrame::kMaxSteps];
|
||||
uint16_t counts_[128]; // how often each level (in -dBm) was read: for the median
|
||||
uint32_t fromHz_ = 0, stepHz_ = 0;
|
||||
uint16_t steps_ = 0;
|
||||
std::vector<std::string> report_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
#endif // RORO_DEBUG
|
||||
@@ -71,19 +71,19 @@ bool RadioService::beginRadio(Print* report) {
|
||||
Config c = config();
|
||||
float mhz = c.frequencyHz / 1e6f;
|
||||
int16_t state = radio_->begin(mhz, c.bandwidthKHz, c.spreadingFactor, c.codingRate, c.syncWord, 0, c.preamble,
|
||||
tcxo_, false);
|
||||
tcxo_, ldo_);
|
||||
if (state != RADIOLIB_ERR_NONE && tcxo_ > 0) { // Meshtastic's TCXO_OPTIONAL: fall back to the crystal
|
||||
if (report) report->printf("lora probe: begin with TCXO %.1f V: error %d, trying the crystal\n", tcxo_, state);
|
||||
tcxo_ = 0;
|
||||
state = radio_->begin(mhz, c.bandwidthKHz, c.spreadingFactor, c.codingRate, c.syncWord, 0, c.preamble, tcxo_,
|
||||
false);
|
||||
ldo_);
|
||||
}
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
if (report) report->printf("lora probe: error %d, no SX1262 found\n", state);
|
||||
return false;
|
||||
}
|
||||
radio_->setDio2AsRfSwitch(true); // DIO2 selects TX or RX in the switch, as in Meshtastic
|
||||
radio_->setRxBoostedGainMode(true); // about 2 dB more sensitivity for about 2 mA
|
||||
radio_->setRxBoostedGainMode(boostedGain_); // about 2 dB more sensitivity for about 2 mA
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,12 @@ void RadioService::store(RadioPacket& p) {
|
||||
}
|
||||
|
||||
#ifdef RORO_DEBUG
|
||||
void RadioService::debugAntenna(bool on) {
|
||||
auto& i2c = M5.In_I2C;
|
||||
uint8_t out = i2c.readRegister8(kExpander, kOutput, kI2cFreq);
|
||||
i2c.writeRegister8(kExpander, kOutput, on ? (out | 1) : (out & ~1), kI2cFreq);
|
||||
}
|
||||
|
||||
void RadioService::inject(const uint8_t* data, size_t len, float rssi, float snr) {
|
||||
if (!listening_) return (void)console.println("lora inject: not listening");
|
||||
RadioPacket p;
|
||||
|
||||
@@ -100,6 +100,15 @@ class RadioService : public Service {
|
||||
void setEcho(bool echo);
|
||||
void probe(Print& out); // `lora probe`: runs on the radio task
|
||||
#ifdef RORO_DEBUG
|
||||
// For the noise self-test (issue #20): the chip's regulator as an LDO instead of its DC-DC
|
||||
// converter, and receive gain boosted or not. Used the next time the radio is set up.
|
||||
void debugOptions(bool ldo, bool boostedGain) {
|
||||
ldo_ = ldo;
|
||||
boostedGain_ = boostedGain;
|
||||
}
|
||||
// The antenna switch (the Cap's expander, P0), for the reference "what the chip hears alone".
|
||||
// From the main loop only, which owns the I2C bus.
|
||||
void debugAntenna(bool on);
|
||||
// `lora inject`: a packet into the ring as if received, to test the App and Captures with no
|
||||
// transmitter in range. Nothing goes on air.
|
||||
void inject(const uint8_t* data, size_t len, float rssi, float snr);
|
||||
@@ -132,6 +141,7 @@ class RadioService : public Service {
|
||||
Config config_;
|
||||
bool present_ = false, expander_ = false;
|
||||
float tcxo_ = 1.8f;
|
||||
std::atomic<bool> ldo_{false}, boostedGain_{true};
|
||||
std::atomic<uint8_t> clients_{0};
|
||||
std::atomic<bool> listening_{false}, configChanged_{false}, probeWanted_{false}, echo_{false};
|
||||
std::atomic<uint32_t> seq_{0}, packets_{0}, crcErrors_{0}, radioErrors_{0}, restarts_{0};
|
||||
|
||||
@@ -247,7 +247,7 @@ void WifiService::tick(uint32_t nowMs) {
|
||||
apply(controller_.disconnected(nowMs));
|
||||
}
|
||||
|
||||
apply(controller_.update(nowMs, settings_.getBool(Setting::WifiEnabled)));
|
||||
apply(controller_.update(nowMs, settings_.getBool(Setting::WifiEnabled) && !paused_));
|
||||
|
||||
if (controller_.state() == State::Connected && nowMs - serversCheckedMs_ >= kServersEveryMs) applyServers(Why::Check);
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ class WifiService : public Service {
|
||||
void ipSettingChanged(const std::string& ssid);
|
||||
// The DNS or NTP settings changed: use them now.
|
||||
void serversChanged() { applyServers(Why::SettingsChanged); }
|
||||
#ifdef RORO_DEBUG
|
||||
// The noise self-test switches the radio off for a few seconds. Not saved anywhere: a restart
|
||||
// during the test brings Wi-Fi back, which a changed setting wouldn't.
|
||||
void debugPause(bool paused) { paused_ = paused; }
|
||||
#endif
|
||||
|
||||
// A Saved Network was added: try it now rather than after the retry delay.
|
||||
void savedNetworksChanged() { controller_.retryNow(millis()); }
|
||||
@@ -82,6 +87,7 @@ class WifiService : public Service {
|
||||
uint32_t listScanSeq_ = 0;
|
||||
bool ntpWaiting_ = false;
|
||||
bool radioInitialised_ = false;
|
||||
bool paused_ = false; // Debug Builds: off for a moment, whatever the setting says
|
||||
bool fixed_ = false; // the network in use has a Fixed address
|
||||
bool dnsFromSettings_ = false;
|
||||
std::string ntpNames_[2]; // lwIP keeps the pointers, so the names live here
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
// Issue #20: the GNSS receiver raises the LoRa radio's noise floor by 8 dB while it runs. Pausing
|
||||
// it while the radio listens is offered, and off unless chosen (GNSS on by default: M2, Q58).
|
||||
void test_pause_gnss_for_lora_is_off_by_default() {
|
||||
Fixture f;
|
||||
int quiet = f.row(SettingsMenu::Row::GnssQuiet);
|
||||
TEST_ASSERT_EQUAL_STRING("Pause GNSS for LoRa", f.menu.label(quiet).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("Off", f.menu.value(quiet).c_str());
|
||||
f.menu.toggle(quiet);
|
||||
TEST_ASSERT_TRUE(f.settings.getBool(Setting::GnssQuietForLora));
|
||||
TEST_ASSERT_EQUAL_STRING("On", f.menu.value(quiet).c_str());
|
||||
TEST_ASSERT_TRUE(f.settings.getBool(Setting::GnssEnabled)); // the GNSS switch itself is untouched
|
||||
}
|
||||
|
||||
void test_gnss_and_coordinate_rows_toggle() {
|
||||
Fixture f;
|
||||
int gnss = f.row(SettingsMenu::Row::Gnss), coords = f.row(SettingsMenu::Row::Coordinates);
|
||||
@@ -131,6 +144,7 @@ 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_pause_gnss_for_lora_is_off_by_default);
|
||||
RUN_TEST(test_gnss_and_coordinate_rows_toggle);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user