From f81de0783048b8124bf29360dce22dfc93314d2a Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:05:23 +1100 Subject: [PATCH] t5s3 - improved cardkb notes rendering; fix notes generic filename save type --- examples/companion_radio/main.cpp | 180 ++++++++++-------- .../companion_radio/ui-new/CardKBKeyboard.h | 26 ++- examples/companion_radio/ui-new/Notesscreen.h | 10 + examples/companion_radio/ui-new/UITask.cpp | 18 ++ 4 files changed, 153 insertions(+), 81 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 619249a8..278a3f77 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2025,90 +2025,118 @@ void loop() { if (ui_task.isVKBActive()) { // VKB is open — feed character into VKB text buffer ui_task.feedCardKBChar(ckb); - } else if (ckb == 0x1B) { - // ESC → back (same as 'q' on T-Deck Pro) - ui_task.injectKey('q'); } else if (ui_task.isOnHomeScreen()) { - // Home screen: letter shortcuts open tiles, arrows cycle pages - switch (ckb) { - case 'm': ui_task.gotoChannelScreen(); break; - case 'c': ui_task.gotoContactsScreen(); break; - case 'e': ui_task.gotoTextReader(); break; - case 'n': ui_task.gotoNotesScreen(); break; - case 's': ui_task.gotoSettingsScreen(); break; - case 'f': ui_task.gotoDiscoveryScreen(); break; - case 'h': ui_task.gotoLastHeardScreen(); break; + // Home screen: ESC does nothing special, letter shortcuts open tiles + if (ckb == 0x1B) { + // ESC on home — no-op (already home) + } else { + switch (ckb) { + case 'm': ui_task.gotoChannelScreen(); break; + case 'c': ui_task.gotoContactsScreen(); break; + case 'e': ui_task.gotoTextReader(); break; + case 'n': ui_task.gotoNotesScreen(); break; + case 's': ui_task.gotoSettingsScreen(); break; + case 'f': ui_task.gotoDiscoveryScreen(); break; + case 'h': ui_task.gotoLastHeardScreen(); break; #ifdef MECK_WEB_READER - case 'b': ui_task.gotoWebReader(); break; + case 'b': ui_task.gotoWebReader(); break; #endif #if HAS_GPS - case 'g': ui_task.gotoMapScreen(); break; + case 'g': ui_task.gotoMapScreen(); break; #endif - default: ui_task.injectKey(ckb); break; + default: ui_task.injectKey(ckb); break; + } } } else { - // Non-home screens: handle Enter for compose, remap arrows to WASD. - // Screens respond to w/s (scroll up/down) and a/d (prev/next channel) - // but not to KEY_LEFT/KEY_RIGHT constants. - if (ckb == '\r') { - // Enter key — screen-specific compose or select - if (ui_task.isOnChannelScreen()) { - // Open VKB for channel message compose - uint8_t chIdx = ui_task.getChannelScreenViewIdx(); - ChannelDetails ch; - if (the_mesh.getChannel(chIdx, ch)) { - char label[40]; - snprintf(label, sizeof(label), "To: %s", ch.name); - ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx); - } - } else if (ui_task.isOnContactsScreen()) { - // DM compose for chat contacts, admin for repeaters - ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); - if (cs) { - int idx = cs->getSelectedContactIdx(); - uint8_t ctype = cs->getSelectedContactType(); - if (idx >= 0 && ctype == ADV_TYPE_CHAT) { - char dname[32]; - cs->getSelectedContactName(dname, sizeof(dname)); - char label[40]; - snprintf(label, sizeof(label), "DM: %s", dname); - ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx); - } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { - ui_task.gotoRepeaterAdmin(idx); - } - } - } else if (ui_task.isOnRepeaterAdmin()) { - // Open VKB for password or CLI entry - RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen(); - if (admin) { - RepeaterAdminScreen::AdminState astate = admin->getState(); - if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) { - ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32); + // Non-home screens: context-specific routing + bool handled = false; + + // Notes editing/renaming: route ALL keys directly (no VKB). + // This gives: Enter=newline, arrows=cursor, printable=insert, ESC=save&exit + if (ui_task.isOnNotesScreen()) { + NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen(); + if (notesScr && (notesScr->isEditing() || notesScr->isRenaming())) { + handled = true; + if (ckb == 0x1B) { + // ESC: save & exit editing, or cancel rename + if (notesScr->isEditing()) { + notesScr->triggerSaveAndExit(); } else { - ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137); + ui_task.injectKey('q'); + } + } else if (notesScr->isEditing()) { + // Editing mode: arrows move cursor, everything else types directly + switch (ckb) { + case (char)0xF2: notesScr->moveCursorUp(); break; + case (char)0xF1: notesScr->moveCursorDown(); break; + case (char)0xF3: notesScr->moveCursorLeft(); break; + case (char)0xF4: notesScr->moveCursorRight(); break; + default: ui_task.injectKey(ckb); break; } - } - } else if (ui_task.isOnNotesScreen()) { - // Open VKB for note editing - NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen(); - if (notesScr && notesScr->isEditing()) { - ui_task.showVirtualKeyboard(VKB_NOTES, "Edit Note", "", 137); } else { - ui_task.injectKey('\r'); // File list: select/open + // Renaming mode: all keys go directly to rename handler + ui_task.injectKey(ckb); + } + ui_task.forceRefresh(); + } + } + + if (!handled) { + // ESC → back (same as 'q' on T-Deck Pro) for all non-notes screens + if (ckb == 0x1B) { + ui_task.injectKey('q'); + } else if (ckb == '\r') { + // Enter key — screen-specific compose or select + if (ui_task.isOnChannelScreen()) { + // Open VKB for channel message compose + uint8_t chIdx = ui_task.getChannelScreenViewIdx(); + ChannelDetails ch; + if (the_mesh.getChannel(chIdx, ch)) { + char label[40]; + snprintf(label, sizeof(label), "To: %s", ch.name); + ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx); + } + } else if (ui_task.isOnContactsScreen()) { + // DM compose for chat contacts, admin for repeaters + ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); + if (cs) { + int idx = cs->getSelectedContactIdx(); + uint8_t ctype = cs->getSelectedContactType(); + if (idx >= 0 && ctype == ADV_TYPE_CHAT) { + char dname[32]; + cs->getSelectedContactName(dname, sizeof(dname)); + char label[40]; + snprintf(label, sizeof(label), "DM: %s", dname); + ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx); + } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { + ui_task.gotoRepeaterAdmin(idx); + } + } + } else if (ui_task.isOnRepeaterAdmin()) { + // Open VKB for password or CLI entry + RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen(); + if (admin) { + RepeaterAdminScreen::AdminState astate = admin->getState(); + if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) { + ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32); + } else { + ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137); + } + } + } else { + // All other screens: pass Enter through for native handling + // (settings toggle, discovery add-contact, last heard, text reader, notes file list, etc.) + ui_task.injectKey('\r'); } } else { - // All other screens: pass Enter through for native handling - // (settings toggle, discovery add-contact, last heard, text reader file select, etc.) - ui_task.injectKey('\r'); - } - } else { - // Non-Enter keys: remap arrows to WASD, pass others through - switch (ckb) { - case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up - case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down - case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category - case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category - default: ui_task.injectKey(ckb); break; + // Non-Enter keys: remap arrows to WASD, pass others through + switch (ckb) { + case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up + case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down + case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category + case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category + default: ui_task.injectKey(ckb); break; + } } } } @@ -2843,14 +2871,6 @@ void handleKeyboardInput() { case 'n': // Open notes Serial.println("Opening notes"); - { - NotesScreen* notesScr2 = (NotesScreen*)ui_task.getNotesScreen(); - if (notesScr2) { - uint32_t ts = rtc_clock.getCurrentTime(); - int8_t utcOff = the_mesh.getNodePrefs()->utc_offset_hours; - notesScr2->setTimestamp(ts, utcOff); - } - } ui_task.gotoNotesScreen(); break; diff --git a/examples/companion_radio/ui-new/CardKBKeyboard.h b/examples/companion_radio/ui-new/CardKBKeyboard.h index e152ff75..feb3307c 100644 --- a/examples/companion_radio/ui-new/CardKBKeyboard.h +++ b/examples/companion_radio/ui-new/CardKBKeyboard.h @@ -17,6 +17,7 @@ #include #include +#include "variant.h" // For I2C_SDA, I2C_SCL (bus recovery) // I2C address (defined in variant.h, fallback here) #ifndef CARDKB_I2C_ADDR @@ -60,11 +61,31 @@ public: // Poll for a keypress. Returns 0 if no key available. // Returns raw ASCII for printable chars, or Meck KEY_* constants for nav keys. + // Throttled to avoid flooding I2C bus — polls at most every 50ms. + // On read failure, backs off 500ms and re-inits Wire to recover bus state. char readKey() { if (!_detected) return 0; + unsigned long now = millis(); + if (now - _lastPoll < _pollInterval) return 0; + _lastPoll = now; + Wire.requestFrom((uint8_t)CARDKB_I2C_ADDR, (uint8_t)1); - if (!Wire.available()) return 0; + if (!Wire.available()) { + _errorCount++; + if (_errorCount >= 3) { + // I2C bus may be stuck — re-init to recover + Wire.begin(I2C_SDA, I2C_SCL); + Wire.setClock(100000); + _pollInterval = 500; // Back off for 500ms + _errorCount = 0; + Serial.println("[CardKB] I2C error recovery — bus re-init"); + } + return 0; + } + + _errorCount = 0; + _pollInterval = 50; // Normal polling rate uint8_t raw = Wire.read(); if (raw == 0) return 0; @@ -92,6 +113,9 @@ public: private: bool _detected; + unsigned long _lastPoll = 0; + unsigned long _pollInterval = 50; // ms between polls (increases on error) + uint8_t _errorCount = 0; }; #endif // CARDKB_KEYBOARD_H diff --git a/examples/companion_radio/ui-new/Notesscreen.h b/examples/companion_radio/ui-new/Notesscreen.h index 4ab35c85..05a62972 100644 --- a/examples/companion_radio/ui-new/Notesscreen.h +++ b/examples/companion_radio/ui-new/Notesscreen.h @@ -102,6 +102,10 @@ private: uint32_t _rtcTime; // Unix timestamp (0 = unavailable) int8_t _utcOffset; // UTC offset in hours + // Callback to get fresh RTC time (set by UITask at init) + typedef uint32_t (*TimeGetterFn)(); + TimeGetterFn _getTimeFn = nullptr; + // ---- Helpers ---- String getFullPath(const String& filename) { @@ -1077,6 +1081,10 @@ private: // ---- Note Creation ---- void createNewNote() { + // Refresh timestamp at creation time for accurate filenames + if (_getTimeFn) { + _rtcTime = _getTimeFn(); + } _currentFile = generateFilename(); _buf[0] = '\0'; _bufLen = 0; @@ -1176,6 +1184,8 @@ public: _utcOffset = utcOffset; } + void setTimeGetter(TimeGetterFn fn) { _getTimeFn = fn; } + void enter(DisplayDriver& display) { initLayout(display); scanFiles(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c951a6e2..54cf96bf 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1463,6 +1463,15 @@ void UITask::loop() { gotoHomeScreen(); // file list: go home c = 0; } + } else if (isOnNotesScreen()) { + NotesScreen* notes = (NotesScreen*)notes_screen; + if (notes && notes->isEditing()) { + notes->triggerSaveAndExit(); // save and return to file list + } else { + notes->exitNotes(); + gotoHomeScreen(); + } + c = 0; } else { gotoHomeScreen(); c = 0; // consumed @@ -1616,6 +1625,11 @@ if (curr) curr->poll(); onVKBCancel(); } } else { + // Default: allow full refresh. Override for notes editing (no flash while typing). + display.setForcePartial(false); + if (isOnNotesScreen() && ((NotesScreen*)notes_screen)->isEditing()) { + display.setForcePartial(true); + } int delay_millis = curr->render(*_display); // Check if settings screen needs VKB for WiFi password entry @@ -2194,6 +2208,10 @@ void UITask::gotoNotesScreen() { if (_display != NULL) { notes->enter(*_display); } + // Set fresh timestamp and wire up time getter for note creation + notes->setTimestamp(rtc_clock.getCurrentTime(), + _node_prefs ? _node_prefs->utc_offset_hours : 0); + notes->setTimeGetter([]() -> uint32_t { return rtc_clock.getCurrentTime(); }); setCurrScreen(notes_screen); if (_display != NULL && !_display->isOn()) { _display->turnOn();