// roro9stack firmware entry point: wires Services, Apps and the UI loop together. #include #include #include #include #include "app_manager.h" #include "apps/demo_app.h" #include "apps/gemini_app.h" #include "apps/gnss_app.h" #include "apps/irc_app.h" #include "apps/launcher_app.h" #include "apps/lora_scanner_app.h" #include "apps/notes_app.h" #include "apps/settings_app.h" #include "apps/storage_app.h" #include "apps/system_app.h" #include "apps/wifi_tools_app.h" #include "apps/setup_app.h" #include "event_bus.h" #include "file_receiver.h" #include "ipv4.h" #include "traffic.h" #include "key_mapper.h" #include "platform/console.h" #include "platform/crash_report.h" #include "platform/nvs_store.h" #include "platform/system_info.h" #include "sd_card_id.h" #include "sd_fault.h" #include "service_manager.h" #include "platform/identity.h" #include "file_names.h" #include "services/battery_service.h" #include "services/irc_service.h" #include "services/clock_service.h" #include "services/debug_console.h" #include "services/file_ops.h" #include "update_check.h" #include "services/gemini_service.h" #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" #include "services/wifi_service.h" #include "safe_mode.h" #include "settings.h" #include "storage_paths.h" #include "ui/notifier.h" #include "ui/screen.h" #include "version.h" using namespace roro; static EventBus bus; static NvsStore nvs; static Settings settings(nvs, bus); static ServiceManager services; static KeyMapper keyMapper; static Screen screen; // Constructed in setup(), after the hardware and Settings are ready. static BatteryService* battery; static StorageService* storageService; static FileOps* fileOps; static std::vector filesInUse(const std::string& path, bool folder); static PowerService* power; static ClockService* clockService; static GnssService* gnssService; static RadioService* radioService; static LoraCaptureService* loraCapture; static NoiseTest* noiseTest; static GeminiService* geminiService; static SavedNetworks* savedNetworks; static WifiService* wifi; static IrcService* irc; static UpdateService* update; static DebugConsole* debugConsole; static Notifier* notifier; static LauncherApp launcher; static AppManager* apps; static bool safeMode = false; // see SafeMode: only what it takes to be fixed over the air static RawKeys readKeys() { auto state = M5Cardputer.Keyboard.keysState(); RawKeys raw; raw.chars.assign(state.word.begin(), state.word.end()); raw.fn = state.fn; raw.opt = state.opt; raw.ctrl = state.ctrl; raw.alt = state.alt; raw.shift = state.shift; raw.enter = state.enter; raw.del = state.del; raw.tab = state.tab; return raw; } static StatusInfo currentStatus() { StatusInfo s; s.title = kProductName; if (apps->foregroundTitle()) s.title = apps->foregroundTitle(); if (battery->estimator().hasReading()) s.batteryPercent = battery->estimator().percent(); s.clock = clockService->displayTime(); auto storageState = storageService->state(); s.sdPresent = storageState.present; s.sdLevel = storageState.level; s.compose = keyMapper.pendingCompose(); s.unread = irc->totalUnread(); if (gnssService && gnssService->on()) { const auto& g = gnssService->state(); bool fresh = gnssService->receiving(millis()); s.gnss = !fresh || g.fix == gnss::FixType::None ? StatusInfo::Gnss::Searching : g.fix == gnss::FixType::TwoD ? StatusInfo::Gnss::TwoD : StatusInfo::Gnss::ThreeD; s.gnssSatellites = g.satellitesUsed; s.tracking = gnssService->tracking(); } if (radioService && radioService->sweeping()) s.radio = StatusInfo::Radio::Sweep; else if (radioService && radioService->listening()) { uint32_t last = radioService->lastPacketMs(); s.radio = last && millis() - last < 400 ? StatusInfo::Radio::Packet : StatusInfo::Radio::Listening; } s.capturing = loraCapture && loraCapture->capturing(); s.debug = !debugConsole->on() ? StatusInfo::Debug::Off : debugConsole->clientConnected() ? StatusInfo::Debug::Client : StatusInfo::Debug::On; using WifiState = WifiController::State; switch (wifi->state()) { case WifiState::Connected: { s.wifi = StatusInfo::Wifi::Connected; int rssi = wifi->rssi(); s.wifiBars = rssi >= -60 ? 3 : rssi >= -70 ? 2 : rssi >= -80 ? 1 : 0; break; } case WifiState::Monitoring: s.wifi = StatusInfo::Wifi::Monitoring; break; case WifiState::Off: s.wifi = StatusInfo::Wifi::None; break; default: s.wifi = StatusInfo::Wifi::Searching; break; } return s; } // Arduino-ESP32 marks a new image valid before setup() unless this returns true. Probation // (UpdateService::tick) decides instead, and an unconfirmed image stays PENDING_VERIFY. extern "C" bool verifyRollbackLater() { return true; } // C linkage, or the weak default wins // The main loop's stack: Arduino's default is 8 KB; the measured peak is 3 KB (M2). size_t getArduinoLoopTaskStackSize() { return 6144; } static void setupSafeMode(int crashes); void setup() { nvs.begin(); UpdateService::bootGuard(nvs); // first: before anything that could crash on new firmware int crashes = crash_report::noteBoot(nvs); safeMode = SafeMode::active(crashes); Serial.setRxBufferSize(2 * FileReceiver::kChunk); // before the port opens; sd put sends 1 chunk at a time Serial.setTxBufferSize(1024); // the console skips Serial when it's full: room for a burst like `tasks` auto cfg = M5.config(); M5Cardputer.begin(cfg, true); M5Cardputer.Display.setRotation(1); Serial.begin(115200); settings.load(); if (safeMode) return setupSafeMode(crashes); battery = new BatteryService(bus); storageService = new StorageService(bus); power = new PowerService(settings); clockService = new ClockService(settings, bus); geminiService = new GeminiService(nvs, *storageService, *clockService, bus); gnssService = new GnssService(settings, *clockService, *storageService, bus); radioService = new RadioService(); savedNetworks = new SavedNetworks(nvs); savedNetworks->load(); wifi = new WifiService(settings, *savedNetworks, *clockService); update = new UpdateService(nvs, *wifi, *savedNetworks, *storageService, bus, settings); fileOps = new FileOps(*storageService, filesInUse); update->enableDailyCheck(); update->holdMemory = [](bool hold) { irc->holdForUpdate(hold); }; // see UpdateService::holdMemory irc = new IrcService(nvs, "roro_" + identity::defaultShortName(), *wifi, *storageService, *clockService, bus); notifier = new Notifier(bus, settings); notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); }; services.add(*power); services.add(*clockService); services.add(*battery); services.add(*gnssService); services.add(*radioService); loraCapture = new LoraCaptureService(*radioService, *storageService, *clockService, bus); noiseTest = new NoiseTest(*radioService); services.add(*loraCapture); services.add(*storageService); services.add(*wifi); services.add(*irc); services.add(*update); debugConsole = new DebugConsole(*wifi, *storageService, settings); services.add(*debugConsole); apps = new AppManager(launcher); launcher.setManager(*apps); apps->registerApp({"irc", "IRC", false, new IrcApp(*irc, *clockService, bus)}); apps->registerApp({"wifi-tools", "Wi-Fi Tools", false, new WifiToolsApp(*wifi, *storageService, *clockService)}); apps->registerApp({"gnss", "GNSS", false, new GnssApp(*gnssService, settings)}); apps->registerApp({"gemini", "Gemini", false, new GeminiApp(*geminiService)}); apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)}); apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus)}); apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)}); // Leaving the foreground App makes it save: a note being typed, when the device is powered off. power->beforePowerOff = []() { apps->home(); }; apps->registerApp({"system", "System", false, new SystemApp(*wifi, *battery, *storageService, *radioService, *gnssService, nvs)}); apps->registerApp({"settings", "Settings", false, new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks, *update})}); apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)}); apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)}); bus.subscribe(EventType::Notification, [](const Event& e) { console.printf("notification: %s\n", e.text); }); if (!screen.begin()) console.println("frame buffer allocation failed"); debugConsole->setFrame(screen.canvas()); // for `screenshot` services.startAll(millis()); apps->begin(); if (!settings.getBool(Setting::SetupDone)) apps->openModal("setup"); console.printf("%s %s ready, free heap %u, last start: %s\n", kProductName, versionString(), ESP.getFreeHeap(), system_info::resetReason()); if (system_info::resetWasCrash()) { crash_report::print(console, nvs); bus.publish(Event::withText(EventType::Notification, crash_report::headline().c_str(), static_cast(NotificationLevel::Warning))); } // A main loop stuck for 5 s panics (and leaves a core dump) instead of hanging forever: Arduino // only watches the idle task on core 0, and the loop runs on core 1. enableLoopWDT(); } // Safe Mode: Wi-Fi, the clock, Firmware Updates and (if it's switched on) the Debug Console. No Apps, // no IRC, no SD card: whatever crashed three times in a row is most likely among them. static void setupSafeMode(int crashes) { clockService = new ClockService(settings, bus); savedNetworks = new SavedNetworks(nvs); savedNetworks->load(); wifi = new WifiService(settings, *savedNetworks, *clockService); storageService = new StorageService(bus); // never started: no card access update = new UpdateService(nvs, *wifi, *savedNetworks, *storageService, bus, settings); services.add(*clockService); services.add(*wifi); services.add(*update); debugConsole = new DebugConsole(*wifi, *storageService, settings); services.add(*debugConsole); bus.subscribe(EventType::Notification, [](const Event& e) { console.printf("notification: %s\n", e.text); }); screen.begin(); debugConsole->setFrame(screen.canvas()); services.startAll(millis()); console.printf("%s %s in SAFE MODE: %d crash restarts in a row. Only Wi-Fi and Firmware Updates run.\n", kProductName, versionString(), crashes); crash_report::print(console, nvs); enableLoopWDT(); } // Dev aid: commands to drive the UI without the keyboard, from the serial port or the Debug Console. static bool listingWanted = false; static void printListingWhenReady() { if (!listingWanted || !storageService->listingReady()) return; listingWanted = false; auto listing = storageService->listing(); for (size_t i = 0; i < listing.size(); i++) { uint64_t bytes = 0; for (auto& f : listing[i]) bytes += f.bytes; console.printf("sd list: %-20s %u files, %s\n", kCleanupCategories[i].label, (unsigned)listing[i].size(), formatBytes(bytes).c_str()); for (auto& f : listing[i]) console.printf(" %s (%u B)\n", f.path.c_str(), (unsigned)f.bytes); } } // sd put: a file sent over serial by scripts/sd_put.py. The main loop reads the bytes; every card // access runs as a Storage Service job, which reports back through uploadResult. static FileReceiver upload; static std::atomic uploadResult{-1}; // set by a job: 1 ok, 0 failed; -1 nothing new static bool uploadJobRunning = false; static void uploadJob(std::function job) { uploadJobRunning = true; storageService->runJob([job]() { uploadResult = job() ? 1 : 0; }); } static void startUpload(const String& args) { if (upload.active() || uploadJobRunning) return (void)Serial.println("sd put: error busy"); std::string error = upload.begin(args.c_str(), millis()); StorageState card = storageService->state(); if (error.empty() && !card.present) error = "no SD card"; if (error.empty() && card.totalBytes - card.usedBytes < upload.size() + 64 * 1024) error = "not enough space"; if (!error.empty()) { upload.reset(); return (void)Serial.printf("sd put: error %s\n", error.c_str()); } std::string part = upload.partPath(); uploadJob([part]() { for (size_t slash = part.find('/', 1); slash != std::string::npos; slash = part.find('/', slash + 1)) { std::string dir = part.substr(0, slash); if (!SD.exists(dir.c_str()) && !SD.mkdir(dir.c_str())) return false; } if (SD.exists(part.c_str())) SD.remove(part.c_str()); return true; }); } static void readUploadBytes() { uint8_t buf[256]; size_t want; while (!uploadJobRunning && (want = std::min(upload.wanted(), sizeof buf)) > 0 && Serial.available() > 0) { size_t n = Serial.read(buf, std::min(want, Serial.available())); upload.feed(buf, n, millis()); } } static void uploadStep() { using S = FileReceiver::State; uint32_t now = millis(); int result = uploadResult.exchange(-1); if (result >= 0) { uploadJobRunning = false; bool ok = result == 1; switch (upload.state()) { case S::Receiving: // the folder job from startUpload: the sender may start if (ok) Serial.printf("sd put: ready %u\n", (unsigned)FileReceiver::kChunk); else { upload.reset(); return (void)Serial.println("sd put: error card not writable"); } break; case S::Writing: upload.chunkWritten(ok, now); if (upload.state() == S::Receiving) Serial.printf("sd put: ok %u\n", (unsigned)upload.received()); break; case S::Finishing: upload.finished(ok); break; default: break; // a job that finished after the transfer failed } } if (!upload.active()) return; upload.tick(now); std::string path = upload.path(), part = upload.partPath(); if (upload.state() == S::Failed) { // Reported even if a job never came back (no card: the storage task drops it). Serial.printf("sd put: error %s\n", upload.error().c_str()); storageService->runJob([part]() { SD.remove(part.c_str()); }); upload.reset(); uploadJobRunning = false; return; } if (uploadJobRunning) return; if (upload.state() == S::Writing) { auto chunk = std::make_shared>(upload.chunk()); uploadJob([chunk, part]() { File f = SD.open(part.c_str(), FILE_APPEND); bool ok = f && f.write(chunk->data(), chunk->size()) == chunk->size(); if (f) f.close(); return ok; }); } else if (upload.state() == S::Finishing) { uploadJob([path, part]() { if (SD.exists(path.c_str())) SD.remove(path.c_str()); return SD.rename(part.c_str(), path.c_str()); }); } else if (upload.state() == S::Done) { Serial.printf("sd put: done %s %u B\n", path.c_str(), (unsigned)upload.size()); upload.reset(); } } // `wifi ip ... try `: a trial IP setting that reverts unless `wifi ip keep` arrives, so a // wrong address tried over Wi-Fi doesn't cut the device off for good. static struct { bool active = false; uint32_t untilMs = 0; std::string ssid; bool wasFixed = false; net::FixedIp was; } ipTrial; static void ipTrialStep() { if (!ipTrial.active || static_cast(millis() - ipTrial.untilMs) < 0) return; ipTrial.active = false; savedNetworks->setIp(ipTrial.ssid, ipTrial.wasFixed ? &ipTrial.was : nullptr); console.printf("wifi ip: trial over, %s is back to %s\n", ipTrial.ssid.c_str(), ipTrial.wasFixed ? net::formatFixed(ipTrial.was).c_str() : "Automatic (DHCP)"); wifi->ipSettingChanged(ipTrial.ssid); } // `tasks`: the first sample, and when to take the second and print. static std::vector tasksBefore; static uint32_t tasksTotal = 0, tasksDueMs = 0; static bool tasksPending = false; // What the firmware is writing right now: the Storage App and the console leave those alone (Q130). static std::vector filesInUse(const std::string& path, bool folder) { std::vector open; if (gnssService->tracking()) open.push_back(gnssService->trackPath()); if (loraCapture->capturing()) open.push_back(loraCapture->path()); if (upload.active()) open.push_back(upload.partPath()); if (irc->running() && path != "/irc" && files::isInside(path, "/irc")) { // Today's Logs, and any folder that may hold one: the IRC Service appends whenever a line comes. std::string date = clockService->localDate(); std::string today = (date.empty() ? "undated" : date) + ".log"; if (folder) open.push_back(files::joinPath(path, today)); else if (files::baseName(path) == today) open.push_back(path); } return open; } // `cp`, `mv`, `rm`, `mkdir`, `du` run as the Storage App's operations do; the result prints here. static bool consoleFileOp = false; static void fileOpsStep() { FileOps::Status s; if (!consoleFileOp || !fileOps->finished(s)) return; consoleFileOp = false; static const char* const kNames[] = {"", "ls", "du", "cp", "mv", "rm", "mkdir"}; const char* name = kNames[static_cast(s.op)]; if (!s.error.empty()) console.printf("%s: error %s\n", name, s.error.c_str()); else if (s.cancelled) console.printf("%s: cancelled\n", name); else if (s.op == FileOps::Op::Count) console.printf("du: %s, %u files, %u bytes\n", s.path.c_str(), (unsigned)s.files, (unsigned)s.bytes); else if (s.op == FileOps::Op::Copy) console.printf("cp: ok %u files, %u bytes in %.1f s\n", (unsigned)s.files, (unsigned)s.doneBytes, s.ms / 1000.0); else if (s.op == FileOps::Op::Delete) console.printf("rm: ok %u files in %.1f s\n", (unsigned)s.files, s.ms / 1000.0); else console.printf("%s: ok\n", name); } // `update ...`: the project's releases on Gitea (R1, #6). The answers come from the Update Service's // task a moment later; updateStep() prints them. static int updateWatch = 0; // 1: a check, 2: a list static void printReleases(bool list) { GiteaReleases& g = update->gitea(); if (g.status() == GiteaReleases::Status::Failed) return (void)console.printf("update: %s\n", g.error().c_str()); std::string running = update->runningVersion(); if (!list) { release::Release r; if (!g.latest(r)) return (void)console.println("update: nothing known yet"); console.printf("update: latest %s of %s, %u bytes: %s (running %s)\n", r.tag.c_str(), r.date().c_str(), (unsigned)r.otaSize, release::isNewer(r, running) ? "newer" : "not newer", running.c_str()); console.printf("update: %s\n", r.notes.c_str()); return; } for (auto& r : g.list()) console.printf("update: %-8s %s %8u B %s\n", r.tag.c_str(), r.date().c_str(), (unsigned)r.otaSize, r.notes.substr(0, 60).c_str()); console.println("update: end of list"); } static void updateStep() { if (!updateWatch || update->giteaBusy()) return; if (updateWatch == 3) { console.printf("update: probe: %s\n", update->probeResult().c_str()); updateWatch = 0; return; } printReleases(updateWatch == 2); updateWatch = 0; } static void updateCommand(const String& args) { if (args == "check" || args == "list") { if (update->giteaBusy()) return (void)console.println("update: busy"); args == "check" ? update->requestCheck() : update->requestList(); updateWatch = args == "check" ? 1 : 2; } else if (args == "status") { GiteaReleases& g = update->gitea(); console.printf("update: running %s, failed here before: %s, daily check %s, heap %u\n", update->runningVersion().c_str(), update->failedVersion().empty() ? "none" : update->failedVersion().c_str(), settings.getBool(Setting::CheckUpdates) ? "on" : "off", (unsigned)ESP.getFreeHeap()); console.printf("update: gitea %s %s\n", g.status() == GiteaReleases::Status::Done ? "done" : g.status() == GiteaReleases::Status::Failed ? "failed" : g.status() == GiteaReleases::Status::Busy ? "busy" : "never asked", g.error().c_str()); printReleases(false); } else if (args.startsWith("install ")) { String rest = args.substring(8); std::string why = update->requestInstall(rest.c_str()); console.printf("update: %s\n", why.empty() ? "installing" : why.c_str()); } else if (args.startsWith("probe ")) { // update probe [path]: is that server's certificate accepted? String rest = args.substring(6); int space = rest.indexOf(' '); update->requestProbe((space < 0 ? rest : rest.substring(0, space)).c_str(), space < 0 ? "/" : rest.substring(space + 1).c_str()); updateWatch = 3; } else if (args.startsWith("damage ")) { // update damage cut | flip : for the next download long n = args.substring(args.lastIndexOf(' ') + 1).toInt(); args.startsWith("damage cut") ? update->damageNextDownload(n, -1) : update->damageNextDownload(-1, n); console.printf("update: the next download will be %s at byte %ld\n", args.startsWith("damage cut") ? "cut" : "damaged", n); } else if (args == "daily") { // forget today's check: the daily one runs again at once, if it may update->forgetToday(); console.println("update: the daily check will run at the next tick if Wi-Fi, the clock and memory allow"); } else if (args.startsWith("pretend ")) { // update pretend v0.9.0 | off: what the comparisons take as running update->pretendVersion(args.substring(8) == "off" ? "" : args.substring(8).c_str()); console.printf("update: running %s\n", update->runningVersion().c_str()); } else { console.println("update: check | list | status | install "); } } static void fileCommand(const String& line) { int space = line.indexOf(' '); std::string command = line.substring(0, space).c_str(), rest = line.substring(space + 1).c_str(); bool force = rest.rfind("-f ", 0) == 0; // replace what's there if (force) rest = rest.substr(3); size_t split = rest.find('\t'); // two paths: a tab between them if either has a space if (split == std::string::npos) split = rest.find(' '); std::string a = rest.substr(0, split), b = split == std::string::npos ? "" : rest.substr(split + 1); std::string why; bool exists = false, folder = false; if (command == "cancel") return fileOps->cancel(); if (!storageService->state().present) why = "no SD card"; else if (command == "du") why = fileOps->count(rest); else if (command == "mkdir") why = fileOps->makeFolder(rest); else if (command == "rm") { folder = fileOps->isFolder(rest, exists); why = exists ? fileOps->remove(rest, folder) : "It isn't there"; } else if (b.empty()) why = "two paths, please"; else { folder = fileOps->isFolder(a, exists); bool intoExists = false; bool into = fileOps->isFolder(b, intoExists); // `cp a /folder` keeps the name if (!exists) why = "It isn't there"; else if (command == "cp") why = into ? fileOps->copy(a, folder, b, files::baseName(a), force) : fileOps->copy(a, folder, files::parentOf(b), files::baseName(b), force); else why = fileOps->move(a, folder, into ? files::joinPath(b, files::baseName(a)) : b, force); } if (!why.empty()) return (void)console.printf("%s: error %s\n", command.c_str(), why.c_str()); consoleFileOp = true; } static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40) static bool loopSpin = false; // `loop spin on`: no rest between passes, to compare load and radio noise static uint32_t tasksPasses = 0; static void tasksStep() { if (!tasksPending || static_cast(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().swap(tasksBefore); } static const char* const kHelp = "info firmware, uptime, memory, Wi-Fi, app slots\n" "tasks FreeRTOS tasks over the next second: state, priority, free stack, CPU share\n" "net bytes each network service has read and written since boot\n" "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" "ls [folder] | du | mkdir | rm | cp [-f] | mv [-f] | cancel the SD card, with the Storage App's rules\n" "lora probe | lora status | lora rx on|off | lora preset the LoRa radio, receive only\n" "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 [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 \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 space, or one character\n" "wifi status | wifi add \n" "wifi ip dhcp | wifi ip
/ [gateway] a Saved Network's IP setting\n" "wifi dns [b] | wifi dns always on|off | wifi ntp [b] DNS and NTP servers\n" "gemini get fetch a Gemini page and report header, size, certificate, heap\n" "irc start | irc stop | irc dump | irc say \n" "install Update from SD\n" "update check | list | status | install the project's releases on Gitea\n" "sd card | sd list | cat | log | burst | sound on|off | short | normal\n" "debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n" "debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n" "crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n" "wifi ip ... try | 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 [rssi] [snr] a packet into the LoRa Scanner as if received (nothing is sent)\n" "sd fill makes that many small files there, to test a crowded folder\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" "get | put | screenshot (Debug Console only) binary, see rdbg.py\n" "quit close the Debug Console connection\n" ; // Commands that only touch what Safe Mode starts. static bool safeModeCommand(const String& line) { return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" || line.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") || line == "wifi status" || line.startsWith("wifi add ") || line.startsWith("debug "); } // `debug off `: the console closes and comes back by itself. It can't open itself wider // that way (it was on, with the same token), and it's how its closing and reopening is tested from afar. static bool debugResumes = false; static uint32_t debugResumeMs = 0; static void debugResumeStep() { if (!debugResumes || static_cast(millis() - debugResumeMs) < 0) return; debugResumes = false; settings.setBool(Setting::DebugConsole, true); } // `debug ...`: the Debug Console's switch and token (ADR 0010). Switching it on and setting its token // are for USB serial only (Q192): whoever holds the cable holds the device anyway, and the console // can't be used to open itself wider. The token is never printed. static void debugCommand(const String& args, bool fromSerial) { if (args == "status") { console.printf("debug: %s, token %s, client %s\n", settings.getBool(Setting::DebugConsole) ? "on" : "off", settings.getString(Setting::DebugToken).empty() ? "not set" : "set", debugConsole->clientConnected() ? "connected" : "none"); } else if (args == "off") { settings.setBool(Setting::DebugConsole, false); console.println("debug: off"); } else if (args.startsWith("off ")) { // debug off : a pause, then on again as it was uint32_t seconds = constrain(args.substring(4).toInt(), 1, 600); if (!settings.getBool(Setting::DebugConsole)) return (void)console.println("debug: it is off"); settings.setBool(Setting::DebugConsole, false); debugResumeMs = millis() + seconds * 1000; debugResumes = true; console.printf("debug: off for %lu s\n", (unsigned long)seconds); } else if (!fromSerial) { console.println("debug: over USB serial only (or Settings > Debug Console)"); } else if (args == "on") { DebugConsole::switchOn(settings); console.println("debug: on (the token is in Settings > Debug Console)"); } else if (args == "token new") { settings.setString(Setting::DebugToken, DebugConsole::freshToken()); console.println("debug: a new token (it is in Settings > Debug Console)"); } else if (args.startsWith("token ")) { std::string token = debug::tidyToken(args.substring(6).c_str()); bool ok = debug::validToken(token) && settings.setString(Setting::DebugToken, token); console.println(ok ? "debug: token set" : "debug: a token is 16 to 64 characters"); } else console.println("debug: status | off [seconds] | on | token <16 to 64 characters> | token new"); } static void runCommand(String line, bool fromSerial = false) { line.trim(); if (line.isEmpty()) return; if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode"); if (line == "help") console.print(kHelp); if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial); if (line == "info") { system_info::printSystem(console); console.printf("wifi: %s, ip %s, rssi %d | sd: %s, %u write faults\n", wifi->ssid().c_str(), wifi->ip().c_str(), wifi->rssi(), storageService->state().present ? "present" : "none", (unsigned)sdLastFault().count); console.printf("update: %s | debug console: %s\n", update->onProbation() ? "on probation" : "confirmed", !debugConsole->on() ? "off" : debugConsole->clientConnected() ? "on, a client connected" : "on"); system_info::printSlots(console, nvs); } 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"); } 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) for (int i = 0; i < static_cast(net::User::Count); i++) { net::Traffic t = net::traffic(static_cast(i)); console.printf("net: %-14s in %10lu out %10lu\n", net::userName(static_cast(i)), (unsigned long)t.in, (unsigned long)t.out); } } if (line == "reboot") { console.println("restarting"); delay(300); ESP.restart(); } if (line == "boot other") console.printf("boot other: %s\n", system_info::bootOtherSlot()); if (line.startsWith("log level ")) { int level = line.substring(10).toInt(); esp_log_level_set("*", static_cast(constrain(level, 0, 5))); console.printf("log level: %d\n", constrain(level, 0, 5)); } if (line.startsWith("update ")) updateCommand(line.substring(7)); if (line.startsWith("cp ") || line.startsWith("mv ") || line.startsWith("rm ") || line.startsWith("mkdir ") || line.startsWith("du ") || line == "cancel") fileCommand(line); if (line == "ls" || line.startsWith("ls ")) { if (!storageService->state().present) return (void)console.println("sd: no card"); std::string path = line.length() > 3 ? line.substring(3).c_str() : "/"; storageService->runJob([path]() { // card access stays on the storage task File dir = SD.open(path.c_str()); if (!dir || !dir.isDirectory()) return (void)console.printf("ls: %s is not a folder\n", path.c_str()); for (File f = dir.openNextFile(); f; f = dir.openNextFile()) console.printf("%10u %-16s %s%s\n", f.isDirectory() ? 0u : (unsigned)f.size(), files::formatStamp(static_cast(f.getLastWrite())).c_str(), f.name(), f.isDirectory() ? "/" : ""); console.printf("ls: end of %s\n", path.c_str()); }); } if (line.startsWith("install ")) update->installFromSd(line.substring(8).c_str()); // Update from SD if (line.startsWith("sd fill ")) { // sd fill : small files, to test a crowded folder int space = line.lastIndexOf(' '); std::string folder = line.substring(8, space).c_str(); int count = constrain(line.substring(space + 1).toInt(), 0, 1000); storageService->runJob([folder, count]() { int made = 0; for (int i = 1; i <= count; i++) { char name[24]; snprintf(name, sizeof name, "file-%04d.txt", i); File f = SD.open(files::joinPath(folder, name).c_str(), FILE_WRITE); if (!f) break; f.printf("file %d\n", i); f.close(); made++; } console.printf("sd fill: %d files in %s\n", made, folder.c_str()); }); } 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")); 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 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 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 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)); } 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") { radioService->setSweepEcho(false); radioService->sweep(false); } else if (line.startsWith("lora sweep on")) { double from = 863, to = 870, step = 100; sscanf(line.c_str() + 13, "%lf %lf %lf", &from, &to, &step); radioService->setSweepEcho(true); radioService->sweep(true, from * 1e6 + 0.5, to * 1e6 + 0.5, step * 1e3 + 0.5); } } if (line == "lora capture start" && loraCapture) { std::string why = loraCapture->start(millis()); console.printf("lora capture: %s\n", why.empty() ? loraCapture->path().c_str() : why.c_str()); } if (line == "lora capture stop" && loraCapture) loraCapture->stop(); if (line.startsWith("lora inject ") && radioService) { // [rssi] [snr]: as if received String hex = line.substring(12); int space = hex.indexOf(' '); float rssi = -100, snr = 5; if (space > 0) sscanf(hex.c_str() + space + 1, "%f %f", &rssi, &snr), hex = hex.substring(0, space); uint8_t data[255]; size_t n = 0; for (size_t i = 0; i + 1 < hex.length() && n < sizeof data; i += 2) data[n++] = strtoul(hex.substring(i, i + 2).c_str(), nullptr, 16); radioService->inject(data, n, rssi, snr); } if (line.startsWith("lora custom ") && radioService) { double mhz = 0, bw = 0; unsigned sf = 0, cr = 0, sync = 0, preamble = 8; int n = sscanf(line.c_str() + 12, "%lf %lf %u %u %x %u", &mhz, &bw, &sf, &cr, &sync, &preamble); // Inside what the Cap's SX1262 tunes (868 to 923 MHz) and LoRa's own limits. if (n < 5 || mhz < 863 || mhz > 928 || bw < 7 || bw > 500 || sf < 5 || sf > 12 || cr < 5 || cr > 8 || sync > 0xFF) console.println("lora custom: usage: lora custom [preamble]"); else { radioService->setConfig({static_cast(mhz * 1e6 + 0.5), static_cast(bw), static_cast(sf), static_cast(cr), static_cast(sync), static_cast(preamble), "Custom"}); console.printf("lora custom: %.3f MHz, BW %.1f kHz, SF %u, CR 4/%u, sync 0x%02X, preamble %u\n", mhz, bw, sf, cr, sync, preamble); } } if (line.startsWith("lora preset ") && radioService) { const meshtastic::Preset* p = meshtastic::findPreset(line.substring(12).c_str()); 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()); if (line == "gnss track start" && gnssService) { std::string why = gnssService->startTrack(millis()); console.printf("gnss track: %s\n", why.empty() ? "recording" : why.c_str()); } if (line == "gnss track stop" && gnssService) gnssService->stopTrack(); 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"); if (line == "crash abort") abort(); if (line == "crash wdt") for (;;) {} // the main loop never yields: the task watchdog fires if (line == "burst") for (int i = 1; i <= 5; i++) bus.publish(Event::withText(EventType::Notification, ("Burst " + String(i)).c_str(), 0)); if (line.startsWith("key ")) { // key up|down|left|right|select|back|home|del|tab String k = line.substring(4); Key key = k == "up" ? Key::Up : k == "down" ? Key::Down : k == "left" ? Key::Left : k == "right" ? Key::Right : k == "back" ? Key::Back : k == "home" ? Key::Home : k == "del" ? Key::Delete : k == "tab" ? Key::Tab : Key::Select; KeyEvent ev = k == "space" ? KeyEvent::character(' ') : k.length() == 1 && k[0] > ' ' ? KeyEvent::character((unsigned char)k[0]) : KeyEvent::of(key); if (!power->onKey(millis())) apps->handleKey(ev); } if (line.startsWith("wifi add ")) { // wifi add \t: credentials never touch the repo int tab = line.indexOf('\t'); std::string ssid = (tab > 0 ? line.substring(9, tab) : line.substring(9)).c_str(); std::string pass = tab > 0 ? line.substring(tab + 1).c_str() : ""; std::string error = savedNetworks->add(ssid, pass); console.printf("wifi add: %s\n", error.empty() ? "ok" : error.c_str()); wifi->savedNetworksChanged(); } if (line.startsWith("log ") && !line.startsWith("log level ")) { // appends to a test IRC Log std::string path = storage::dailyLogPath({"irc", "dev", "#test"}, clockService->localDate()); storageService->appendLine(path, clockService->displayTime() + " " + line.substring(4).c_str()); console.printf("log: queued to %s\n", path.c_str()); } if (line.startsWith("cat ")) { std::string path = line.substring(4).c_str(); File f = SD.open(path.c_str()); if (!f) console.printf("cat: cannot open %s\n", path.c_str()); else { console.printf("--- %s (%u B) ---\n", path.c_str(), (unsigned)f.size()); int shown = 0; while (f.available() && shown < 1200) { console.write(f.read()); shown++; } console.println("\n--- end ---"); f.close(); } } if (line.startsWith("sd put ")) startUpload(line.substring(7)); // then raw bytes: see uploadStep() if (line == "sd card") { // what the card says it is, from its CID register if (!storageService->state().present) return (void)console.println("sd card: no card"); storageService->runJob([]() { uint8_t cid[16]; const char* type = SD.cardType() == CARD_SDHC ? "SDHC/SDXC" : SD.cardType() == CARD_SD ? "SDSC" : "MMC or unknown"; if (!sdReadCid(cid)) return (void)console.println("sd card: the card didn't answer"); console.printf("sd card: %s, %.1f GB, %s\n", type, SD.cardSize() / 1e9, sdCardSummary(parseSdCid(cid)).c_str()); console.printf("sd card: CID"); for (uint8_t b : cid) console.printf(" %02x", b); console.println(); }); } if (line == "sd list") { storageService->requestListing(); listingWanted = true; } if (line.startsWith("gemini trust ")) { // gemini trust : accept a changed certificate char host[96] = "", fp[80] = ""; int port = 1965; if (geminiService && sscanf(line.substring(13).c_str(), "%95s %d %79s", host, &port, fp) == 3) geminiService->trust(host, port, fp); } if (line.startsWith("gemini get ")) if (!geminiService || !geminiService->fetchToConsole(line.substring(11).c_str())) console.println("gemini: busy, or unavailable in Safe Mode"); if (line == "irc start") irc->connect(); if (line == "irc stop") irc->disconnect(); if (line.startsWith("irc say ")) { // irc say String rest = line.substring(8); int space = rest.indexOf(' '); int b = rest.substring(0, space).toInt(); std::string text = rest.substring(space + 1).c_str(); irc->withSession([&](IrcSession& s) { s.input(b, text, clockService->utcNow()); }); } if (line == "irc dump") { console.printf("irc: status %d, unread %d, heap %u min %u\n", (int)irc->status(), irc->totalUnread(), ESP.getFreeHeap(), ESP.getMinFreeHeap()); IrcConfig c = irc->draftConfig(); // which login is configured, never the secrets console.printf("irc: nick %s, sasl %s, nickserv %s, autojoin %u channels\n", c.nick.c_str(), c.saslUser.empty() || c.saslPassword.empty() ? "off" : "on", c.nickservPassword.empty() ? "off" : "on", (unsigned)c.autojoin.size()); irc->withSession([](IrcSession& s) { for (int i = 0; i < s.bufferCount(); i++) { const auto& b = s.buffer(i); console.printf("irc buffer %d %s unread %d%s\n", i, b.name.c_str(), b.unread, b.joined ? " joined" : ""); size_t from = 0; for (size_t j = from; j < b.lines.size(); j++) console.printf(" <%s> %s\n", b.lines[j].nick.c_str(), b.lines[j].text.c_str()); } }); } if (line == "wifi status") { console.printf("wifi: state %d ssid '%s' rssi %d ip %s clock %s heap %u min %u | fw %s%s\n", (int)wifi->state(), wifi->ssid().c_str(), wifi->rssi(), wifi->ip().c_str(), clockService->displayTime().c_str(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), versionString(), update->onProbation() ? " (on probation)" : ""); WifiService::Connection c = wifi->connection(); // S1, Q115: what's in use and where it came from if (c.connected) { console.printf("wifi: address %s/%d (%s), gateway %s\n", c.address.c_str(), c.prefix, c.fixed ? "fixed" : "DHCP", c.gateway.empty() ? "none" : c.gateway.c_str()); console.printf("wifi: dns %s %s (%s)\n", c.dns[0].empty() ? "none" : c.dns[0].c_str(), c.dns[1].c_str(), c.dnsFromSettings ? "Settings" : "DHCP"); console.print("wifi: ntp"); for (int i = 0; i < c.ntpCount; i++) console.printf(" %s (%s%s)", c.ntp[i].server.c_str(), c.ntp[i].fromDhcp ? "DHCP" : "Settings", c.ntp[i].answered ? ", answered" : ""); console.println(c.ntpCount ? "" : " none"); } } if (line.startsWith("wifi ip ")) { // wifi ip dhcp |
/ [gateway] (the SSID may hold spaces) std::string rest = line.substring(8).c_str(); if (rest == "keep") { // the trial setting stays console.println(ipTrial.active ? "wifi ip: kept" : "wifi ip: no trial running"); ipTrial.active = false; return; } uint32_t trialS = 0; size_t tryAt = rest.rfind(" try "); if (tryAt != std::string::npos) { trialS = strtoul(rest.c_str() + tryAt + 5, nullptr, 10); rest = rest.substr(0, tryAt); } std::string ssid, why; net::FixedIp fixed; bool dhcp = rest.size() > 5 && rest.compare(rest.size() - 5, 5, " dhcp") == 0; if (dhcp) ssid = rest.substr(0, rest.size() - 5); else { size_t slash = rest.rfind('/'), space = slash == std::string::npos ? slash : rest.rfind(' ', slash); if (space == std::string::npos) why = "usage: wifi ip dhcp |
/ [gateway]"; else { ssid = rest.substr(0, space); why = net::parseFixed(rest.substr(space + 1), fixed); } } const SavedNetwork* before = why.empty() ? savedNetworks->find(ssid) : nullptr; if (before && trialS) ipTrial = {true, millis() + trialS * 1000, ssid, before->fixed, before->ip}; if (why.empty()) why = savedNetworks->setIp(ssid, dhcp ? nullptr : &fixed); if (!why.empty()) return (void)console.printf("wifi ip: %s\n", why.c_str()); console.printf("wifi ip: %s is now %s\n", ssid.c_str(), dhcp ? "Automatic (DHCP)" : net::formatFixed(fixed).c_str()); wifi->ipSettingChanged(ssid); } if ((line.startsWith("wifi dns ") && !line.startsWith("wifi dns always")) || line.startsWith("wifi ntp ")) { // [b] bool dns = line.startsWith("wifi dns "); std::string rest = line.substring(9).c_str(); size_t space = rest.find(' '); std::string first = rest.substr(0, space), second = space == std::string::npos ? "" : rest.substr(space + 1); Setting a = dns ? Setting::Dns1 : Setting::Ntp1, b = dns ? Setting::Dns2 : Setting::Ntp2; std::string oldFirst = settings.getString(a); // Both or neither: a refused second entry leaves the first as it was. bool ok = settings.setString(a, first) && (settings.setString(b, second) || (settings.setString(a, oldFirst), false)); if (!ok) console.printf("wifi %s: %s\n", dns ? "dns" : "ntp", dns ? "an IPv4 address, then a second one if you like" : "a host name or an IPv4 address, then a second one if you like"); else { console.printf("wifi %s: %s %s\n", dns ? "dns" : "ntp", first.c_str(), second.c_str()); wifi->serversChanged(); } } if (line == "wifi dns always on" || line == "wifi dns always off") { settings.setBool(Setting::DnsAlways, line.endsWith("on")); wifi->serversChanged(); } if (line == "sound off") settings.setBool(Setting::Sound, false); if (line == "sound on") settings.setBool(Setting::Sound, true); if (line == "short") { settings.setInt(Setting::DimTimeoutS, 5); settings.setInt(Setting::OffTimeoutS, 10); } if (line == "normal") { settings.setInt(Setting::OffTimeoutS, 60); settings.setInt(Setting::DimTimeoutS, 30); } } static void serialCommands() { if (upload.active()) return readUploadBytes(); // raw file bytes, not commands static String line; while (Serial.available()) { char c = Serial.read(); if (c != '\n') { line += c; continue; } runCommand(line, true); line = ""; } } static void remoteCommands() { for (std::string remote; debugConsole->takeCommand(remote);) { console.printf("> %s\n", remote.c_str()); // so the transcript reads the same on both ends runCommand(remote.c_str()); } debugResumeStep(); for (std::string alert; debugConsole->takeAlert(alert);) bus.publish(Event::withText(EventType::Notification, alert.c_str(), static_cast(NotificationLevel::Warning))); } // After a minute up, the crash streak is over (SafeMode counts starts that crash in a row). static void noteStableOnce(uint32_t now) { static bool noted = false; if (noted || now < SafeMode::kStableAfterMs) return; noted = true; crash_report::noteStable(nvs); } static void loopSafeMode() { uint32_t now = millis(); serialCommands(); remoteCommands(); tasksStep(); services.tick(now); bus.dispatch(); noteStableOnce(now); if (update->phase() == UpdateService::Phase::Installed) { screen.renderUpdate("Restarting", "into " + update->incomingVersion(), -1); delay(800); ESP.restart(); } static uint32_t lastDraw = 0; if (update->phase() == UpdateService::Phase::Receiving) { screen.renderUpdate("Firmware update", "Receiving " + update->incomingVersion(), update->percent()); } else if (now - lastDraw > 2000 || !lastDraw) { lastDraw = now; bool up = wifi->state() == WifiController::State::Connected; screen.renderUpdate("Safe Mode", up ? "Updates: " + wifi->ip() + ":3232" : "Waiting for Wi-Fi", -1); } 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() { loopPasses++; if (safeMode) { loopSafeMode(); delay(kLoopRestScreenOnMs); return; } loopPass(); if (loopSpin) return; 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(); #endif static StatusInfo lastStatus; static uint32_t lastLog = 0; uint32_t now = millis(); serialCommands(); remoteCommands(); tasksStep(); ipTrialStep(); if (noiseTest) noiseTest->step(now); noteStableOnce(now); updateStep(); fileOpsStep(); uploadStep(); printListingWhenReady(); M5Cardputer.update(); if (M5Cardputer.Keyboard.isChange()) { bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen keyMapper.setTextEntry(apps->foreground().textEntryActive()); auto events = keyMapper.update(readKeys()); // always, so held keys are tracked if (!swallow) 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); apps->update(now); if (now - lastLog > 10000) { lastLog = now; console.printf("status: heap %u min %u\n", ESP.getFreeHeap(), ESP.getMinFreeHeap()); } // Firmware Update: its progress takes over the screen, then the device restarts when it's safe. static int shownPercent = -1; static uint32_t installedAt = 0; static bool redrawAfterUpdate = false; switch (update->phase()) { case UpdateService::Phase::Receiving: if (update->percent() != shownPercent) { shownPercent = update->percent(); screen.renderUpdate("Firmware update", "Receiving " + update->incomingVersion(), shownPercent); } redrawAfterUpdate = true; return; case UpdateService::Phase::Installed: if (!installedAt) installedAt = now; // Don't pull the rug out from under someone typing, for up to a minute. if (!apps->foreground().textEntryActive() || now - installedAt >= 60000) { screen.renderUpdate("Restarting", "into " + update->incomingVersion(), -1); delay(800); ESP.restart(); } break; default: shownPercent = -1; break; } if (power->screen() == ScreenState::Off) return; StatusInfo status = currentStatus(); const Toast* toast = notifier->toasts().current(now); bool toastChanged = notifier->toasts().takeChanged(); if (apps->takeRedraw() || toastChanged || status != lastStatus || redrawAfterUpdate) { lastStatus = status; redrawAfterUpdate = false; screen.render(*apps, status, toast); update->firstFrameDrawn(); } }