diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 1aa70bc2..82babbab 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -72,11 +72,6 @@ /* -------------------------------------------------------------------------------------- */ -// SD-backed settings persistence (defined in main.cpp for T-Deck Pro) -#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD) - extern void backupSettingsToSD(); -#endif - #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 @@ -174,12 +169,7 @@ protected: } public: - void savePrefs() { - _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); - #if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD) - backupSettingsToSD(); - #endif - } + void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } void saveChannels() { _store->saveChannels(this); #if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 3e354c44..227f364f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -13,6 +13,7 @@ #include "TextReaderScreen.h" #include "ContactsScreen.h" #include "ChannelScreen.h" + #include "SettingsScreen.h" extern SPIClass displaySpi; // From GxEPDDisplay.cpp, shared SPI bus TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire); @@ -512,6 +513,23 @@ void setup() { } #endif + // --------------------------------------------------------------------------- + // First-boot onboarding detection + // Check if node name is still the default hex prefix (first 4 bytes of pub key) + // If so, launch onboarding wizard to set name and radio preset + // --------------------------------------------------------------------------- + #if defined(LilyGo_TDeck_Pro) + { + char defaultName[10]; + mesh::Utils::toHex(defaultName, the_mesh.self_id.pub_key, 4); + NodePrefs* prefs = the_mesh.getNodePrefs(); + if (strcmp(prefs->node_name, defaultName) == 0) { + MESH_DEBUG_PRINTLN("setup() - Default node name detected, launching onboarding"); + ui_task.gotoOnboarding(); + } + } + #endif + // GPS duty cycle — honour saved pref, default to enabled on first boot #if HAS_GPS { @@ -787,20 +805,28 @@ void handleKeyboardInput() { return; } - // C key: allow entering compose mode from reader - if (key == 'c' || key == 'C') { - composeDM = false; - composeDMContactIdx = -1; - composeMode = true; - composeBuffer[0] = '\0'; - composePos = 0; - Serial.printf("Entering compose mode from reader, channel %d\n", composeChannelIdx); - drawComposeScreen(); - lastComposeRefresh = millis(); + // All other keys pass through to the reader screen + ui_task.injectKey(key); + return; + } + + // *** SETTINGS MODE *** + if (ui_task.isOnSettingsScreen()) { + SettingsScreen* settings = (SettingsScreen*)ui_task.getSettingsScreen(); + + // Q key: exit settings (when not editing) + if (!settings->isEditing() && (key == 'q' || key == 'Q')) { + if (settings->hasRadioChanges()) { + // Let settings show "apply changes?" confirm dialog + ui_task.injectKey(key); + } else { + Serial.println("Exiting settings"); + ui_task.gotoHomeScreen(); + } return; } - - // All other keys pass through to the reader screen + + // All other keys → settings screen via injectKey ui_task.injectKey(key); return; } @@ -809,36 +835,9 @@ void handleKeyboardInput() { switch (key) { case 'c': case 'C': - // Enter compose mode - DM if on contacts screen, channel otherwise - if (ui_task.isOnContactsScreen()) { - ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); - int idx = cs->getSelectedContactIdx(); - uint8_t ctype = cs->getSelectedContactType(); - if (idx >= 0 && ctype == ADV_TYPE_CHAT) { - composeDM = true; - composeDMContactIdx = idx; - cs->getSelectedContactName(composeDMName, sizeof(composeDMName)); - composeMode = true; - composeBuffer[0] = '\0'; - composePos = 0; - Serial.printf("Entering DM compose to %s (idx %d)\n", composeDMName, idx); - drawComposeScreen(); - lastComposeRefresh = millis(); - } - } else { - composeDM = false; - composeDMContactIdx = -1; - composeMode = true; - composeBuffer[0] = '\0'; - composePos = 0; - // If on channel screen, sync compose channel with viewed channel - if (ui_task.isOnChannelScreen()) { - composeChannelIdx = ui_task.getChannelScreenViewIdx(); - } - Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx); - drawComposeScreen(); - lastComposeRefresh = millis(); - } + // Open contacts list + Serial.println("Opening contacts"); + ui_task.gotoContactsScreen(); break; case 'm': @@ -848,20 +847,24 @@ void handleKeyboardInput() { ui_task.gotoChannelScreen(); break; - case 'r': - case 'R': - // Open text reader + case 'e': + case 'E': + // Open text reader (ebooks) Serial.println("Opening text reader"); ui_task.gotoTextReader(); break; - case 'n': - case 'N': - // Open contacts list - Serial.println("Opening contacts"); - ui_task.gotoContactsScreen(); + case 's': + case 'S': + // Open settings (from home), or navigate down on channel/contacts + if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) { + ui_task.injectKey('s'); // Pass directly for channel/contacts scrolling + } else { + Serial.println("Opening settings"); + ui_task.gotoSettingsScreen(); + } break; - + case 'w': case 'W': // Navigate up/previous (scroll on channel screen) @@ -872,17 +875,6 @@ void handleKeyboardInput() { ui_task.injectKey(0xF2); // KEY_PREV } break; - - case 's': - case 'S': - // Navigate down/next (scroll on channel screen) - if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) { - ui_task.injectKey('s'); // Pass directly for channel/contacts switching - } else { - Serial.println("Nav: Next"); - ui_task.injectKey(0xF1); // KEY_NEXT - } - break; case 'a': case 'A': @@ -907,7 +899,7 @@ void handleKeyboardInput() { break; case '\r': - // Select/Enter - if on contacts screen, enter DM compose for chat contacts + // Enter = compose (only from channel or contacts screen) if (ui_task.isOnContactsScreen()) { ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); int idx = cs->getSelectedContactIdx(); @@ -923,12 +915,21 @@ void handleKeyboardInput() { drawComposeScreen(); lastComposeRefresh = millis(); } else if (idx >= 0) { - // Non-chat contact selected (repeater, room, etc.) - future use Serial.printf("Selected non-chat contact type=%d idx=%d\n", ctype, idx); } + } else if (ui_task.isOnChannelScreen()) { + composeDM = false; + composeDMContactIdx = -1; + composeChannelIdx = ui_task.getChannelScreenViewIdx(); + composeMode = true; + composeBuffer[0] = '\0'; + composePos = 0; + Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx); + drawComposeScreen(); + lastComposeRefresh = millis(); } else { - Serial.println("Nav: Enter/Select"); - ui_task.injectKey(13); // KEY_ENTER + // Other screens: pass Enter as generic select + ui_task.injectKey(13); } break; @@ -939,12 +940,6 @@ void handleKeyboardInput() { Serial.println("Nav: Back to home"); ui_task.gotoHomeScreen(); break; - - case 'u': - case 'U': - // Forward to UI for UTC offset editor on GPS page - ui_task.injectKey('u'); - break; case ' ': // Space - also acts as next/select @@ -1099,9 +1094,9 @@ void drawEmojiPicker() { void sendComposedMessage() { if (composePos == 0) return; - - cpuPower.setBoost(); // Boost CPU for crypto + radio TX + cpuPower.setBoost(); // Boost CPU for crypto + radio TX + // Convert escape bytes back to UTF-8 for mesh transmission and BLE app char utf8Buf[512]; emojiUnescape(composeBuffer, utf8Buf, sizeof(utf8Buf)); diff --git a/examples/companion_radio/ui-new/Textreaderscreen.h b/examples/companion_radio/ui-new/Textreaderscreen.h index de391fe2..40a726fb 100644 --- a/examples/companion_radio/ui-new/Textreaderscreen.h +++ b/examples/companion_radio/ui-new/Textreaderscreen.h @@ -603,7 +603,7 @@ private: _currentPage = cache->lastReadPage; } - // Already fully indexed — open immediately + // Already fully indexed — open immediately if (cache->fullyIndexed) { _totalPages = _pagePositions.size(); _mode = READING; @@ -613,7 +613,7 @@ private: return; } - // Partially indexed — finish indexing with splash + // Partially indexed — finish indexing with splash Serial.printf("TextReader: Finishing index for %s (have %d pages so far)\n", actualFilename.c_str(), (int)_pagePositions.size()); @@ -629,7 +629,7 @@ private: drawSplash("Indexing...", "Please wait", shortName); if (_pagePositions.empty()) { - // Cache had no pages (e.g. dummy entry) — full index from scratch + // Cache had no pages (e.g. dummy entry) — full index from scratch _pagePositions.push_back(0); indexPagesWordWrap(_file, 0, _pagePositions, _linesPerPage, _charsPerLine, 0); @@ -639,7 +639,7 @@ private: _linesPerPage, _charsPerLine, 0); } } else { - // No cache — full index from scratch + // No cache — full index from scratch Serial.printf("TextReader: Full index for %s\n", actualFilename.c_str()); char shortName[28]; @@ -878,9 +878,8 @@ private: display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); - char status[30]; - int pct = _totalPages > 1 ? (_currentPage * 100) / (_totalPages - 1) : 100; - sprintf(status, "%d/%d %d%%", _currentPage + 1, _totalPages, pct); + char status[20]; + sprintf(status, "%d/%d", _currentPage + 1, _totalPages); display.setCursor(0, footerY); display.print(status); @@ -997,7 +996,7 @@ public: // --- Pass 1: Fast cache load (no per-file splash screens) --- // Try to load existing .idx files from SD for every file. - // This is just SD reads — no indexing, no e-ink refreshes. + // This is just SD reads — no indexing, no e-ink refreshes. _fileCache.clear(); _fileCache.resize(_fileList.size()); // Pre-allocate slots to maintain alignment with _fileList @@ -1026,7 +1025,7 @@ public: // Skip files that loaded from cache if (_fileCache[i].filename.length() > 0) continue; - // Skip .epub files — they'll be converted on first open via openBook() + // Skip .epub files — they'll be converted on first open via openBook() if (_fileList[i].endsWith(".epub") || _fileList[i].endsWith(".EPUB")) { needsIndexCount--; // Don't count epubs in progress display continue; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 91474479..cf792209 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -34,6 +34,7 @@ #include "ChannelScreen.h" #include "ContactsScreen.h" #include "TextReaderScreen.h" +#include "SettingsScreen.h" class SplashScreen : public UIScreen { UITask* _task; @@ -372,7 +373,7 @@ public: display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; - // NMEA sentence counter — confirms baud rate and data flow + // NMEA sentence counter — confirms baud rate and data flow display.drawTextLeftAlign(0, y, "sentences"); if (gpsDuty.isHardwareOn()) { uint16_t sps = gpsStream.getSentencesPerSec(); @@ -746,6 +747,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no channel_screen = new ChannelScreen(this, &rtc_clock); contacts_screen = new ContactsScreen(this, &rtc_clock); text_reader = new TextReaderScreen(this); + settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs); setCurrScreen(splash); } @@ -1078,13 +1080,13 @@ void UITask::toggleGPS() { if (_sensors != NULL) { if (_node_prefs->gps_enabled) { - // Disable GPS — cut hardware power + // Disable GPS — cut hardware power _sensors->setSettingValue("gps", "0"); _node_prefs->gps_enabled = 0; gpsDuty.disable(); notify(UIEventType::ack); } else { - // Enable GPS — start duty cycle + // Enable GPS — start duty cycle _sensors->setSettingValue("gps", "1"); _node_prefs->gps_enabled = 1; gpsDuty.enable(); @@ -1182,6 +1184,26 @@ void UITask::gotoTextReader() { _next_refresh = 100; } +void UITask::gotoSettingsScreen() { + ((SettingsScreen *) settings_screen)->enter(); + setCurrScreen(settings_screen); + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; +} + +void UITask::gotoOnboarding() { + ((SettingsScreen *) settings_screen)->enterOnboarding(); + setCurrScreen(settings_screen); + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; +} + uint8_t UITask::getChannelScreenViewIdx() const { return ((ChannelScreen *) channel_screen)->getViewChannelIdx(); }