// roro9stack firmware entry point: wires Services, Apps and the UI loop together. #include #include #include "app_manager.h" #include "apps/demo_app.h" #include "apps/irc_app.h" #include "apps/launcher_app.h" #include "apps/settings_app.h" #include "apps/wifi_tools_app.h" #include "apps/setup_app.h" #include "event_bus.h" #include "key_mapper.h" #include "platform/nvs_store.h" #include "service_manager.h" #include "platform/identity.h" #include "services/battery_service.h" #include "services/irc_service.h" #include "services/clock_service.h" #include "services/power_service.h" #include "services/storage_service.h" #include "services/wifi_service.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 PowerService* power; static ClockService* clockService; static SavedNetworks* savedNetworks; static WifiService* wifi; static IrcService* irc; static Notifier* notifier; static LauncherApp launcher; static AppManager* apps; 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(); 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; } void setup() { auto cfg = M5.config(); M5Cardputer.begin(cfg, true); M5Cardputer.Display.setRotation(1); Serial.begin(115200); nvs.begin(); settings.load(); battery = new BatteryService(bus); storageService = new StorageService(bus); power = new PowerService(settings); clockService = new ClockService(settings, bus); savedNetworks = new SavedNetworks(nvs); savedNetworks->load(); wifi = new WifiService(settings, *savedNetworks, *clockService); 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(*storageService); services.add(*wifi); services.add(*irc); 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({"settings", "Settings", false, new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks})}); 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) { Serial.printf("notification: %s\n", e.text); }); if (!screen.begin()) Serial.println("frame buffer allocation failed"); services.startAll(millis()); apps->begin(); if (!settings.getBool(Setting::SetupDone)) apps->openModal("setup"); Serial.printf("%s %s ready, free heap %u\n", kProductName, versionString(), ESP.getFreeHeap()); } // Dev aid: serial commands to drive the UI without the keyboard. 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; Serial.printf("sd list: %-20s %u files, %s\n", kCleanupCategories[i].label, (unsigned)listing[i].size(), formatBytes(bytes).c_str()); for (auto& f : listing[i]) Serial.printf(" %s (%u B)\n", f.path.c_str(), (unsigned)f.bytes); } } static void serialCommands() { static String line; while (Serial.available()) { char c = Serial.read(); if (c != '\n') { line += c; continue; } line.trim(); 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 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 : Key::Select; KeyEvent ev = 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); Serial.printf("wifi add: %s\n", error.empty() ? "ok" : error.c_str()); wifi->savedNetworksChanged(); } if (line.startsWith("log ")) { // 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()); Serial.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) Serial.printf("cat: cannot open %s\n", path.c_str()); else { Serial.printf("--- %s (%u B) ---\n", path.c_str(), (unsigned)f.size()); int shown = 0; while (f.available() && shown < 1200) { Serial.write(f.read()); shown++; } Serial.println("\n--- end ---"); f.close(); } } if (line == "sd list") { storageService->requestListing(); listingWanted = true; } if (line == "irc start") irc->connect(); 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") { Serial.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 Serial.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); Serial.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++) Serial.printf(" <%s> %s\n", b.lines[j].nick.c_str(), b.lines[j].text.c_str()); } }); } if (line == "wifi status") Serial.printf("wifi: state %d ssid '%s' rssi %d clock %s heap %u min %u\n", (int)wifi->state(), wifi->ssid().c_str(), wifi->rssi(), clockService->displayTime().c_str(), ESP.getFreeHeap(), ESP.getMinFreeHeap()); 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); } line = ""; } } void loop() { static StatusInfo lastStatus; static uint32_t lastLog = 0; uint32_t now = millis(); serialCommands(); 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); } services.tick(now); bus.dispatch(); notifier->update(now); apps->update(now); if (now - lastLog > 10000) { lastLog = now; Serial.printf("status: heap %u min %u\n", ESP.getFreeHeap(), ESP.getMinFreeHeap()); } 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) { lastStatus = status; screen.render(*apps, status, toast); } }