From 444631e08b60523597d93ff99649cac6923534e8 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:05:51 +1000 Subject: [PATCH] update lock screen reference in Readme; update footers and exit behaviour so nearly all instances of Q:Exit is now Shift+Delete=Exit. Add first pass change for Meck issue 20 to dispatcher cpp and h --- README.md | 4 +- examples/companion_radio/main.cpp | 172 +++++++++--------- examples/companion_radio/ui-new/AlarmScreen.h | 28 +-- .../ui-new/AudiobookPlayerScreen.h | 4 +- .../ui-new/ChannelPickerScreen.h | 16 +- .../companion_radio/ui-new/ChannelScreen.h | 28 +-- .../companion_radio/ui-new/ContactsScreen.h | 2 +- .../companion_radio/ui-new/DiscoveryScreen.h | 4 +- examples/companion_radio/ui-new/EmojiPicker.h | 4 +- .../ui-new/Fontpreviewscreen.h | 8 +- .../companion_radio/ui-new/GamesMenuScreen.h | 6 +- .../companion_radio/ui-new/LastHeardScreen.h | 2 +- .../ui-new/MinesweeperScreen.h | 10 +- examples/companion_radio/ui-new/NotesScreen.h | 12 +- .../companion_radio/ui-new/PathEditorScreen.h | 12 +- examples/companion_radio/ui-new/RxLogScreen.h | 8 +- examples/companion_radio/ui-new/SMSScreen.h | 36 ++-- .../companion_radio/ui-new/SettingsScreen.h | 70 +++---- examples/companion_radio/ui-new/SnakeScreen.h | 12 +- .../companion_radio/ui-new/TextReaderScreen.h | 12 +- examples/companion_radio/ui-new/Tracescreen.h | 30 +-- .../ui-new/VoiceMessageScreen.h | 24 +-- .../companion_radio/ui-new/WebReaderScreen.h | 71 ++++---- src/Dispatcher.cpp | 59 +++++- src/Dispatcher.h | 9 + 25 files changed, 345 insertions(+), 298 deletions(-) diff --git a/README.md b/README.md index b13b8abb..c79f96a4 100644 --- a/README.md +++ b/README.md @@ -861,7 +861,7 @@ Packets are sent with staggered 3-second delays to avoid congesting the channel. | Enter | Send to selected contact | | Q | Back to home screen | -### Lock Screen (T-Deck Pro) +### Lock Screen (T-Deck Pro & Max) Double-click the Boot button to lock the screen. The lock screen shows the current time, battery percentage, and unread message count. The CPU drops to 40 MHz while locked to reduce power consumption. @@ -869,7 +869,7 @@ Double-click the Boot button again to unlock and return to whatever screen you w An auto-lock timer can be configured in **Settings → Auto Lock** (None / 2 / 5 / 10 / 15 / 30 minutes of idle time). -### Shutdown (T-Deck Pro) +### Shutdown (T-Deck Pro & Max) The home screen includes a **Shutdown** page. Selecting it powers the device off completely — the ESP32-S3 enters deep sleep with no wake sources, peripheral power is cut, and the LoRa module is powered down. Only a hardware reset (reset button) or USB power-on will wake the device. This is distinct from the auto-lock hibernate, which maintains wake-on-LoRa capability. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 1668e714..4e2bc97e 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -3819,7 +3819,7 @@ void loop() { } } } - // Channel picker: check if Enter/Q was handled (wantsExit) + // Channel picker: check if Enter/Shift+Del was handled (wantsExit) if (ui_task.isOnChannelPickerScreen()) { ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen(); if (pick && pick->wantsExit()) { @@ -3990,7 +3990,7 @@ void loop() { if (notesScr->isEditing()) { notesScr->triggerSaveAndExit(); } else { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); } } else if (notesScr->isEditing()) { // Editing mode: arrows move cursor, everything else types directly @@ -4011,16 +4011,16 @@ void loop() { #endif if (!handled) { - // ESC or Q → back navigation - if (ckb == 0x1B || ckb == 'q') { + // ESC -> back navigation + if (ckb == 0x1B) { if (ui_task.isOnSnakeScreen()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen(); if (ss && ss->wantsExit()) { ui_task.gotoGamesMenu(); } } else if (ui_task.isOnMinesweeperScreen()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen(); if (ms && ms->wantsExit()) { ui_task.gotoGamesMenu(); @@ -4032,9 +4032,9 @@ void loop() { } else if (ui_task.isOnChannelScreen()) { ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); if (chScr && (chScr->isReplySelectMode() || chScr->isShowingPathOverlay())) { - ui_task.injectKey('q'); // dismiss overlay/reply first + ui_task.injectKey(KEY_CANCEL); // dismiss overlay/reply first } else if (chScr && chScr->isDMConversation()) { - ui_task.injectKey('q'); // DM conversation → inbox + ui_task.injectKey(KEY_CANCEL); // DM conversation -> inbox } else { ui_task.gotoChannelPickerScreen(); } @@ -4232,19 +4232,19 @@ void loop() { ui_task.gotoPathEditor(idx); } } - } else if ((ckb == 'q' || ckb == 'Q') && ui_task.isOnPathEditor()) { - // Q on path editor → back to contacts + } else if ((ckb == 0x1B) && ui_task.isOnPathEditor()) { + // ESC on path editor -> back to contacts ui_task.gotoContactsScreen(); - } else if ((ckb == 'q' || ckb == 'Q') && ui_task.isOnChannelPickerScreen()) { - // Q on picker → home + } else if ((ckb == 0x1B) && ui_task.isOnChannelPickerScreen()) { + // ESC on picker -> home ui_task.gotoHomeScreen(); - } else if ((ckb == 'q' || ckb == 'Q') && ui_task.isOnChannelScreen()) { - // Q on channel screen → picker (unless overlay/DM conversation) + } else if ((ckb == 0x1B) && ui_task.isOnChannelScreen()) { + // ESC on channel screen -> picker (unless overlay/DM conversation) ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); if (chScr && (chScr->isReplySelectMode() || chScr->isShowingPathOverlay())) { - ui_task.injectKey('q'); // dismiss overlay/reply first + ui_task.injectKey(KEY_CANCEL); // dismiss overlay/reply first } else if (chScr && chScr->isDMConversation()) { - ui_task.injectKey('q'); // DM conversation → inbox (handled internally) + ui_task.injectKey(KEY_CANCEL); // DM conversation -> inbox (handled internally) } else { ui_task.gotoChannelPickerScreen(); } @@ -4355,6 +4355,13 @@ void handleKeyboardInput() { char key = keyboard.readKey(); if (key == 0) return; + // Shift+Backspace is the universal back/cancel token. Convert it to + // KEY_CANCEL here at the source so every screen and dispatch path sees one + // unambiguous back code -- this leaves plain Backspace as text delete and + // frees physical Q as an ordinary letter. Contexts that bind Shift+Del to a + // non-back action (notes/contacts item delete) handle KEY_CANCEL explicitly. + if (key == '\b' && keyboard.wasShiftConsumed()) key = KEY_CANCEL; + // Block all keyboard input while lock screen is active. // Still read the key above to clear the TCA8418 buffer. if (ui_task.isLocked()) return; @@ -4603,11 +4610,11 @@ void handleKeyboardInput() { AudiobookPlayerScreen* abPlayer = (AudiobookPlayerScreen*)ui_task.getAudiobookScreen(); - // Q key: behavior depends on playback state + // Shift+Del: behavior depends on playback state // - Playing: navigate home, audio continues in background // - Paused/stopped: close book, return to file list // - File list: exit player entirely - if (key == 'q') { + if (key == KEY_CANCEL) { if (abPlayer->isBookOpen()) { if (abPlayer->isAudioActive()) { // Audio is playing -- leave screen, audio continues via audioTick() @@ -4651,8 +4658,8 @@ void handleKeyboardInput() { ui_task.forceRefresh(); return; } - // Q from message list exits voice screen - if (key == 'q' && voiceScr->getMode() == VoiceMessageScreen::MESSAGE_LIST) { + // Shift+Del from message list exits voice screen + if (key == KEY_CANCEL && voiceScr->getMode() == VoiceMessageScreen::MESSAGE_LIST) { Serial.println("Exiting voice message screen"); ui_task.gotoHomeScreen(); return; @@ -4669,12 +4676,12 @@ void handleKeyboardInput() { if (readerMode) { TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen(); - // Q key: if reading, reader handles it (close book -> file list) + // Shift+Del: if reading, reader handles it (close book -> file list) // if on file list, exit reader entirely - if (key == 'q') { + if (key == KEY_CANCEL) { if (reader->isReading()) { - // Let the reader handle Q (close book, go to file list) - ui_task.injectKey('q'); + // Let the reader handle Shift+Del (close book, go to file list) + ui_task.injectKey(KEY_CANCEL); } else { // On file list - exit reader, go home reader->exitReader(); @@ -4695,15 +4702,15 @@ void handleKeyboardInput() { // ---- EDITING MODE ---- if (notes->isEditing()) { - // Shift+Backspace = save and exit + // Shift+Del (KEY_CANCEL) = save and exit + if (key == KEY_CANCEL) { + Serial.println("Notes: Shift+Del, saving..."); + notes->saveAndExit(); + ui_task.forceRefresh(); + return; + } + // Plain Backspace = delete before cursor if (key == '\b') { - if (keyboard.wasShiftConsumed()) { - Serial.println("Notes: Shift+Backspace, saving..."); - notes->saveAndExit(); - ui_task.forceRefresh(); - return; - } - // Regular backspace - delete before cursor ui_task.injectKey(key); composeNeedsRefresh = true; lastComposeKeystroke = millis(); return; @@ -4762,8 +4769,8 @@ void handleKeyboardInput() { return; } - // Shift+Backspace on a file = delete with confirmation - if (key == '\b' && keyboard.wasShiftConsumed()) { + // Shift+Del on a file = delete with confirmation + if (key == KEY_CANCEL) { if (notes->startDeleteFromList()) { ui_task.forceRefresh(); } @@ -4796,8 +4803,8 @@ void handleKeyboardInput() { return; } - // Shift+Backspace = delete note - if (key == '\b' && keyboard.wasShiftConsumed()) { + // Shift+Del = delete note + if (key == KEY_CANCEL) { Serial.println("Notes: Deleting current note"); notes->deleteCurrentNote(); ui_task.forceRefresh(); @@ -4820,8 +4827,8 @@ void handleKeyboardInput() { if (ui_task.isOnSettingsScreen()) { SettingsScreen* settings = (SettingsScreen*)ui_task.getSettingsScreen(); - // Q key: exit settings (when not editing) - if (!settings->isEditing() && (key == 'q')) { + // Shift+Del: exit settings (when not editing) + if (!settings->isEditing() && (key == KEY_CANCEL)) { if (settings->hasRadioChanges()) { // Let settings show "apply changes?" confirm dialog ui_task.injectKey(key); @@ -4834,17 +4841,17 @@ void handleKeyboardInput() { // Shift+Backspace during WiFi password entry: back to SSID selection #ifdef MECK_WIFI_COMPANION - if (settings->isInWifiPasswordEntry() && key == '\b' && keyboard.wasShiftConsumed()) { + if (settings->isInWifiPasswordEntry() && key == KEY_CANCEL) { settings->wifiPasswordBack(); ui_task.forceRefresh(); return; } #endif - // Shift+Backspace on the WiFi network picker: exit (same as Q) + // Shift+Backspace on the WiFi network picker: exit #ifdef MECK_WIFI_COMPANION - if (settings->isInWifiNetworkSelect() && key == '\b' && keyboard.wasShiftConsumed()) { - ui_task.injectKey('q'); + if (settings->isInWifiNetworkSelect() && key == KEY_CANCEL) { + ui_task.injectKey(KEY_CANCEL); return; } #endif @@ -4931,7 +4938,7 @@ void handleKeyboardInput() { if (ui_task.isOnRepeaterAdmin()) { RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen(); RepeaterAdminScreen::AdminState astate = admin->getState(); - bool shiftDel = (key == '\b' && keyboard.wasShiftConsumed()); + bool shiftDel = (key == KEY_CANCEL); // Helper: exit admin — room servers go to DM conversation if logged in, otherwise contacts auto exitAdmin = [&]() { @@ -5010,15 +5017,15 @@ void handleKeyboardInput() { return; } - // Q from app menu → go home; Q from inner views is handled by SMSScreen - if ((key == 'q' || key == '\b') && smsScr->getSubView() == SMSScreen::APP_MENU) { + // Shift+Del from app menu -> go home; Shift+Del from inner views is handled by SMSScreen + if ((key == KEY_CANCEL) && smsScr->getSubView() == SMSScreen::APP_MENU) { Serial.println("Nav: SMS -> Home"); ui_task.gotoHomeScreen(); return; } // Phone dialer: debounced refresh for digit entry, immediate render for - // view transitions (Enter=call, Q=back). This avoids the 686ms e-ink + // view transitions (Enter=call, Shift+Del=back). This avoids the 686ms e-ink // block per keypress while ensuring call/back screens render instantly. if (smsScr->getSubView() == SMSScreen::PHONE_DIALER) { smsScr->handleInput(key); @@ -5027,7 +5034,7 @@ void handleKeyboardInput() { dialerNeedsRefresh = true; lastDialerRefresh = millis(); } else { - // View changed (startCall or Q back) — render immediately + // View changed (startCall or Shift+Del back) -- render immediately dialerNeedsRefresh = false; ui_task.forceRefresh(); ui_task.loop(); @@ -5089,8 +5096,8 @@ void handleKeyboardInput() { // Not in text entry — clear flag so ui_task.loop() resumes webReaderTextEntry = false; - // Q from HOME mode exits the web reader entirely (like text reader) - if ((key == 'q' || key == 'Q') && wr && wr->isHome() && !wr->isUrlEditing() && !wr->isSearchEditing()) { + // Shift+Del from HOME mode exits the web reader entirely (like text reader) + if ((key == KEY_CANCEL) && wr && wr->isHome() && !wr->isUrlEditing() && !wr->isSearchEditing()) { Serial.println("Exiting web reader"); ui_task.gotoHomeScreen(); return; @@ -5140,18 +5147,11 @@ void handleKeyboardInput() { ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); if (cs && cs->isInSelectMode()) { switch (key) { - case 'q': - // Exit select mode (don't go home) - cs->exitSelectMode(); - ui_task.forceRefresh(); - Serial.println("Contacts: exited select mode"); - return; - - case '\b': { - // Backspace in select mode: - // Shift+Backspace = delete selected (with confirmation) - // Plain backspace = exit select mode - if (keyboard.wasShiftConsumed()) { + case KEY_CANCEL: { + // Shift+Del = delete selected (with confirmation). Select mode is + // exited via touch long-press (UITask long-press handler); plain + // Backspace is text-only and does nothing here. + { static unsigned long lastDeleteAttempt = 0; int selCount = cs->getSelectedCount(); if (selCount == 0) { @@ -5177,11 +5177,6 @@ void handleKeyboardInput() { snprintf(msg, sizeof(msg), "Delete %d? Shift+Del again", selCount); ui_task.showAlert(msg, 2500); } - } else { - // Plain backspace = exit select mode - cs->exitSelectMode(); - ui_task.forceRefresh(); - Serial.println("Contacts: exited select mode (backspace)"); } return; } @@ -5809,21 +5804,20 @@ void handleKeyboardInput() { } break; - case 'q': - case '\b': + case KEY_CANCEL: // If channel screen reply select or path overlay is showing, dismiss it if (ui_task.isOnChannelScreen()) { ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen(); if (chScr && chScr->isReplySelectMode()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); break; } if (chScr && chScr->isShowingPathOverlay()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); break; } - // DM inbox Q is handled by ChannelScreen (returns false → falls here). - // DM conversation Q is handled internally (returns true → never reaches here). + // DM inbox back is handled by ChannelScreen (returns false -> falls here). + // DM conversation back is handled internally (returns true -> never reaches here). // Normal channel view or DM inbox: go back to picker. Serial.println("Nav: Channel -> Picker"); ui_task.gotoChannelPickerScreen(); @@ -5835,12 +5829,12 @@ void handleKeyboardInput() { if (ui_task.isOnWebReader()) { WebReaderScreen* wr = (WebReaderScreen*)ui_task.getWebReaderScreen(); if (wr && !wr->isHome()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); break; } } #endif - // Contacts select mode: Q/backspace exits select mode (doesn't go home) + // Contacts: select mode handled earlier (Shift+Del = delete); normal -> home if (ui_task.isOnContactsScreen()) { ContactsScreen* csq = (ContactsScreen*)ui_task.getContactsScreen(); if (csq && csq->isInSelectMode()) { @@ -5851,27 +5845,27 @@ void handleKeyboardInput() { } // Normal mode: fall through to go home } - // Discovery screen: Q goes back to contacts (not home) + // Discovery screen: Shift+Del goes back to contacts (not home) if (ui_task.isOnDiscoveryScreen()) { the_mesh.stopDiscovery(); Serial.println("Nav: Discovery -> Contacts"); ui_task.gotoContactsScreen(); break; } - // Rx Log screen: Q goes back to settings (screen handles it) + // Rx Log screen: Shift+Del goes back to settings (screen handles it) if (ui_task.isOnRxLogScreen()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); break; } - // Path editor: Q goes back to contacts (discards unsaved changes) + // Path editor: Shift+Del goes back to contacts (discards unsaved changes) if (ui_task.isOnPathEditor()) { Serial.println("Nav: PathEditor -> Contacts"); ui_task.gotoContactsScreen(); break; } - // Trace screen: Q/wantsExit goes home + // Trace screen: Shift+Del/wantsExit goes home if (ui_task.isOnTraceScreen()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); TraceScreen* ts = (TraceScreen*)ui_task.getTraceScreen(); if (ts && ts->wantsExit()) { Serial.println("Nav: Trace -> Home"); @@ -5879,9 +5873,9 @@ void handleKeyboardInput() { } break; } - // Snake screen: Q goes back to games menu + // Snake screen: Shift+Del goes back to games menu if (ui_task.isOnSnakeScreen()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen(); if (ss && ss->wantsExit()) { Serial.println("Nav: Snake -> Games Menu"); @@ -5889,9 +5883,9 @@ void handleKeyboardInput() { } break; } - // Minesweeper screen: Q goes back to games menu + // Minesweeper screen: Shift+Del goes back to games menu if (ui_task.isOnMinesweeperScreen()) { - ui_task.injectKey('q'); + ui_task.injectKey(KEY_CANCEL); MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen(); if (ms && ms->wantsExit()) { Serial.println("Nav: Minesweeper -> Games Menu"); @@ -5899,13 +5893,13 @@ void handleKeyboardInput() { } break; } - // Games menu: Q goes back to home + // Games menu: Shift+Del goes back to home if (ui_task.isOnGamesMenu()) { Serial.println("Nav: Games Menu -> Home"); ui_task.gotoHomeScreen(); break; } - // Alarm screen: Q/backspace routing depends on sub-mode + // Alarm screen: Shift+Del/backspace routing depends on sub-mode #ifdef MECK_AUDIO_VARIANT if (ui_task.isOnAlarmScreen()) { AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen(); @@ -5913,7 +5907,7 @@ void handleKeyboardInput() { alarmScr->dismiss(); ui_task.gotoHomeScreen(); } else if (alarmScr && alarmScr->getMode() != AlarmScreen::ALARM_LIST) { - // In edit/picker/digit mode — pass to screen (Q = back to list, backspace = delete) + // In edit/picker/digit mode -- pass to screen (Shift+Del = back to list, backspace = delete) ui_task.injectKey(key); } else { // On alarm list — go home @@ -5923,13 +5917,13 @@ void handleKeyboardInput() { break; } #endif - // Last Heard: Q goes back to home + // Last Heard: Shift+Del goes back to home if (ui_task.isOnLastHeardScreen()) { Serial.println("Nav: Last Heard -> Home"); ui_task.gotoHomeScreen(); break; } - // Channel picker: Q goes back to home + // Channel picker: Shift+Del goes back to home if (ui_task.isOnChannelPickerScreen()) { Serial.println("Nav: ChannelPicker -> Home"); ui_task.gotoHomeScreen(); diff --git a/examples/companion_radio/ui-new/AlarmScreen.h b/examples/companion_radio/ui-new/AlarmScreen.h index 59b27872..ee7bf30b 100644 --- a/examples/companion_radio/ui-new/AlarmScreen.h +++ b/examples/companion_radio/ui-new/AlarmScreen.h @@ -15,11 +15,11 @@ // // Keyboard controls: // ALARM_LIST: W/S = scroll slots, Enter = edit selected alarm, -// E = toggle enable/disable, Q = exit to home +// E = toggle enable/disable, Shift+Del = exit to home // EDIT_ALARM: W/S = move between fields, A/D = adjust value, // Enter = open sound picker (on sound field) or save & exit, -// Q = cancel edit -// PICK_SOUND: W/S = scroll sounds, Enter = select, Q = cancel +// Shift+Del = cancel edit +// PICK_SOUND: W/S = scroll sounds, Enter = select, Shift+Del = cancel // RINGING: ANY key = dismiss, Z = snooze 5 minutes // // Library dependencies: ESP32-audioI2S (shared with AudiobookPlayerScreen) @@ -520,7 +520,7 @@ private: display.print(line2); } - drawFooter(display, "O:On/Off Enter:Edit", "Q:Back"); + drawFooter(display, "O:On/Off Enter:Edit", "Sh+Del:Back"); } // ---- Render: Edit alarm ---- @@ -647,9 +647,9 @@ private: display.setCursor(bx + 4, by + 16); display.print(inputDisplay); - drawFooter(display, "Type digits", "Enter:OK Q:Cancel"); + drawFooter(display, "Type digits", "Enter:OK Sh+Del:Cancel"); } else { - drawFooter(display, "A/D:Adjust Enter:Type", "Q:Save"); + drawFooter(display, "A/D:Adjust Enter:Type", "Sh+Del:Save"); } } @@ -671,7 +671,7 @@ private: display.print("Place 44kHz .mp3 in"); display.setCursor(0, 38); display.print("/alarms/ on SD card"); - drawFooter(display, "0 files", "Q:Back"); + drawFooter(display, "0 files", "Sh+Del:Back"); return; } @@ -715,7 +715,7 @@ private: char countBuf[12]; snprintf(countBuf, sizeof(countBuf), "%d files", (int)_soundFiles.size()); - drawFooter(display, countBuf, "Enter:Pick Q:Back"); + drawFooter(display, countBuf, "Enter:Pick Sh+Del:Back"); } // ---- Render: Ringing ---- @@ -837,8 +837,8 @@ private: if (f < FIELD_COUNT - 1) _editField = (EditField)(f + 1); return true; } - // Q - cancel digit entry - if (c == 'q') { + // Shift+Del - cancel digit entry + if (c == KEY_CANCEL) { _digitEntry = false; return true; } @@ -944,8 +944,8 @@ private: return true; } - // Q - save and exit edit - if (c == 'q') { + // Shift+Del - save and exit edit + if (c == KEY_CANCEL) { memcpy(&_config.slots[_editSlot], &_editCopy, sizeof(AlarmSlot)); saveConfig(); _mode = ALARM_LIST; @@ -987,8 +987,8 @@ private: return true; } - // Q - cancel - if (c == 'q') { + // Shift+Del - cancel + if (c == KEY_CANCEL) { _mode = EDIT_ALARM; return true; } diff --git a/examples/companion_radio/ui-new/AudiobookPlayerScreen.h b/examples/companion_radio/ui-new/AudiobookPlayerScreen.h index ef196ae3..a07ea0e2 100644 --- a/examples/companion_radio/ui-new/AudiobookPlayerScreen.h +++ b/examples/companion_radio/ui-new/AudiobookPlayerScreen.h @@ -1279,7 +1279,7 @@ private: display.setCursor(0, 38); display.print("/audiobooks/ on SD"); - drawFooter(display, "0 files", "Q:Back"); + drawFooter(display, "0 files", "Sh+Del:Back"); return; } @@ -1505,7 +1505,7 @@ private: // ---- Footer Nav Bar ---- { - const char* rightText = (_isPlaying && !_isPaused) ? "Q:Leave" : "Q:Close"; + const char* rightText = (_isPlaying && !_isPaused) ? "Sh+Del:Leave" : "Sh+Del:Close"; if (_playlist.size() > 1) { drawFooter(display, "A/D:Seek N:Next", rightText); } else { diff --git a/examples/companion_radio/ui-new/ChannelPickerScreen.h b/examples/companion_radio/ui-new/ChannelPickerScreen.h index 122daf44..e638f456 100644 --- a/examples/companion_radio/ui-new/ChannelPickerScreen.h +++ b/examples/companion_radio/ui-new/ChannelPickerScreen.h @@ -34,7 +34,7 @@ extern MyMesh the_mesh; // // Delete history: // Press X on a highlighted channel to enter delete confirmation mode. -// Confirmation overlay asks the user to press Enter to confirm or Q to +// Confirmation overlay asks the user to press Enter to confirm or Shift+Del to // cancel. On confirm, all messages for that channel are invalidated in // the circular buffer and persisted to SD. // @@ -379,7 +379,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) const char* hints = "Tap:Yes Boot:Cancel"; #else - const char* hints = "Enter:Yes Q:Cancel"; + const char* hints = "Enter:Yes Sh+Del:Cancel"; #endif display.setCursor(boxX + 4, boxY + 29); display.print(hints); @@ -411,9 +411,9 @@ public: display.print(rt); #else if (_confirmDelete) { - display.print("Enter:Yes Q:Cancel"); + display.print("Enter:Yes Sh+Del:Cancel"); } else { - display.print("W/S:Nav Q:Back"); + display.print("W/S:Nav Sh+Del:Back"); const char* rt = "Ent:Open"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -444,8 +444,8 @@ public: _confirmDelete = false; return true; } - // Q / backspace -- cancel - if (c == 'q' || c == 'Q' || c == '\b' || c == KEY_CANCEL) { + // Shift+Del -- cancel + if (c == KEY_CANCEL) { _confirmDelete = false; return true; } @@ -492,8 +492,8 @@ public: return true; // Consumed -- caller checks wantsExit() and navigates } - // Q / backspace -- cancel without changing channel, signal exit - if (c == 'q' || c == 'Q' || c == '\b' || c == KEY_CANCEL) { + // Shift+Del -- cancel without changing channel, signal exit + if (c == KEY_CANCEL) { _wantExit = true; return true; } diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 278e6ede..69d2e973 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -791,7 +791,7 @@ public: display.print(rtInbox); #else display.setCursor(0, footerY); - display.print("Q:Bck A/D:Ch"); + display.print("Sh+Del:Bck A/D:Ch"); const char* rtInbox = "Ent:Open"; display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY); display.print(rtInbox); @@ -992,7 +992,7 @@ public: display.print("Back"); const char* copyHint = "Tap:Dismiss"; #else - display.print("Q:Back"); + display.print("Sh+Del:Back"); // Show scroll hint if path is scrollable if (msg && (msg->path_len & 63) > _pathHopsVisible && msg->path_len != 0xFF) { const char* scrollHint = "W/S:Scrl"; @@ -1024,7 +1024,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Hold: Compose reply"); #else - display.print("Q: Back to inbox"); + display.print("Sh+Del: Back to inbox"); display.setCursor(0, 40); display.print("Ent: Compose reply"); #endif @@ -1535,21 +1535,21 @@ public: #else // Left side: abbreviated controls if (_replySelectMode) { - display.print("W/S:Sel V:Pth Q:X"); + display.print("W/S:Sel V:Pth Sh+Del:X"); const char* rightText = "Ent:Reply"; display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); display.print(rightText); } else if (_viewChannelIdx == 0xFF) { if (_dmContactPerms > 0) { - display.print("Q:Exit L:Admin"); + display.print("Sh+Del:Exit L:Admin"); } else { - display.print("Q:Exit"); + display.print("Sh+Del:Exit"); } const char* rightText = "Ent:Reply"; display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); display.print(rightText); } else { - display.print("Q:Bck A/D:Ch R:Rply"); + display.print("Sh+Del:Bck A/D:Ch R:Rply"); const char* rightText = "Ent:New"; display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); display.print(rightText); @@ -1566,7 +1566,7 @@ public: bool handleInput(char c) override { // If overlay is showing, handle scroll and dismiss if (_showPathOverlay) { - if (c == 'q' || c == 'Q' || c == '\b' || c == 'v' || c == 'V') { + if (c == KEY_CANCEL || c == 'v' || c == 'V') { _showPathOverlay = false; _pathScrollPos = 0; return true; @@ -1597,8 +1597,8 @@ public: // --- Reply select mode --- if (_replySelectMode) { - // Q - exit reply select - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - exit reply select + if (c == KEY_CANCEL) { _replySelectMode = false; _replySelectPos = -1; return true; @@ -1705,8 +1705,8 @@ public: } return true; } - // Q - let main.cpp handle (back to home) - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - let main.cpp handle (back to home) + if (c == KEY_CANCEL) { return false; } // A/D pass through to channel switching below @@ -1717,9 +1717,9 @@ public: } } - // --- DM Conversation mode: Q goes back to inbox --- + // --- DM Conversation mode: Shift+Del goes back to inbox --- if (_viewChannelIdx == 0xFF && !_dmInboxMode) { - if (c == 'q' || c == 'Q' || c == '\b') { + if (c == KEY_CANCEL) { _dmInboxMode = true; _dmFilterName[0] = '\0'; _scrollPos = 0; diff --git a/examples/companion_radio/ui-new/ContactsScreen.h b/examples/companion_radio/ui-new/ContactsScreen.h index 834faafb..bf4021c1 100644 --- a/examples/companion_radio/ui-new/ContactsScreen.h +++ b/examples/companion_radio/ui-new/ContactsScreen.h @@ -484,7 +484,7 @@ public: display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); } else { - display.print("Q:Bk A/D:Filter"); + display.print("Sh+Del:Bk A/D:Filter"); const char* right = "P:Path Ent:Sel"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); diff --git a/examples/companion_radio/ui-new/DiscoveryScreen.h b/examples/companion_radio/ui-new/DiscoveryScreen.h index 9a5a9318..238719e7 100644 --- a/examples/companion_radio/ui-new/DiscoveryScreen.h +++ b/examples/companion_radio/ui-new/DiscoveryScreen.h @@ -108,7 +108,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Long press: Rescan"); #else - display.print("F: Scan again Q: Back"); + display.print("F: Scan again Sh+Del: Back"); #endif } } else { @@ -203,7 +203,7 @@ public: display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); #else - display.print("Q:Bk F:Rescan"); + display.print("Sh+Del:Bk F:Rescan"); const char* right = "Tap/Ent:Add"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); diff --git a/examples/companion_radio/ui-new/EmojiPicker.h b/examples/companion_radio/ui-new/EmojiPicker.h index 9713e2f0..06faa055 100644 --- a/examples/companion_radio/ui-new/EmojiPicker.h +++ b/examples/companion_radio/ui-new/EmojiPicker.h @@ -2,7 +2,7 @@ // Emoji Picker with scrolling grid and scroll bar // 5 columns, 4 visible rows, scrollable through all 79 emoji -// WASD navigation, Enter to select, $/Q/Backspace to cancel +// WASD navigation, Enter to select, $/Shift+Del to cancel #include #include "EmojiSprites.h" @@ -149,7 +149,7 @@ struct EmojiPicker { case '\r': ensureVisible(); return (uint8_t)(EMOJI_ESCAPE_START + cursor); - case '\b': case 'q': case 'Q': case KB_KEY_EMOJI: + case KEY_CANCEL: case KB_KEY_EMOJI: return 0xFF; default: return 0; diff --git a/examples/companion_radio/ui-new/Fontpreviewscreen.h b/examples/companion_radio/ui-new/Fontpreviewscreen.h index 750a6ae3..9cbfe02f 100644 --- a/examples/companion_radio/ui-new/Fontpreviewscreen.h +++ b/examples/companion_radio/ui-new/Fontpreviewscreen.h @@ -17,7 +17,7 @@ extern MyMesh the_mesh; // Subscreen opened from Settings → Font Style. Shows a live preview of each // font style (Classic, Noto Sans, Montserrat) with sample body and title text. // The user cycles through styles with W/S (or swipe on T5S3), previews the -// actual rendering on-screen, and applies with Enter or cancels with Q. +// actual rendering on-screen, and applies with Enter or cancels with Shift+Del. // // The preview works by temporarily calling display.setFontStyle() for each // sample block during render(), then restoring the original style. @@ -136,7 +136,7 @@ public: display.print("Swipe:Pick"); const char* rt = "Boot:Back Tap:Apply"; #else - display.print("W/S:Pick Q:Back"); + display.print("W/S:Pick Sh+Del:Back"); const char* rt = "Enter:Apply"; #endif display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); @@ -182,8 +182,8 @@ public: return true; } - // Q / backspace — cancel, restore original style - if (c == 'q' || c == 'Q' || c == '\b' || c == KEY_CANCEL) { + // Shift+Del - cancel, restore original style + if (c == KEY_CANCEL) { _prefs->ui_font_style = _originalStyle; _wantExit = true; return true; diff --git a/examples/companion_radio/ui-new/GamesMenuScreen.h b/examples/companion_radio/ui-new/GamesMenuScreen.h index 7bce7629..c1094a69 100644 --- a/examples/companion_radio/ui-new/GamesMenuScreen.h +++ b/examples/companion_radio/ui-new/GamesMenuScreen.h @@ -3,7 +3,7 @@ // ============================================================================= // GamesMenuScreen -- Game launcher menu for Meck // -// Lists available games. W/S to navigate, Enter to launch, Q to exit. +// Lists available games. W/S to navigate, Enter to launch, Shift+Del to exit. // Uses wantsExit() and wantsLaunch() flags for navigation -- same pattern // as ChannelPickerScreen. // ============================================================================= @@ -84,7 +84,7 @@ public: _selectedGame = getGames()[_cursor].id; _wantsLaunch = true; return true; - case 'q': case 'Q': + case KEY_CANCEL: _wantsExit = true; return true; default: @@ -145,7 +145,7 @@ public: int fy = display.height() - 12; display.drawRect(0, fy - 2, display.width(), 1); display.setCursor(2, fy); - display.print("Enter:Play Q:Back"); + display.print("Enter:Play Sh+Del:Back"); #endif return 5000; // Static menu -- slow refresh diff --git a/examples/companion_radio/ui-new/LastHeardScreen.h b/examples/companion_radio/ui-new/LastHeardScreen.h index 3c2f27e7..f676308a 100644 --- a/examples/companion_radio/ui-new/LastHeardScreen.h +++ b/examples/companion_radio/ui-new/LastHeardScreen.h @@ -213,7 +213,7 @@ public: display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); #else - display.print("Q:Bk"); + display.print("Sh+Del:Bk"); const char* right = "Tap/Ent:Add/Del"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); diff --git a/examples/companion_radio/ui-new/MinesweeperScreen.h b/examples/companion_radio/ui-new/MinesweeperScreen.h index 5edb27c6..d80e3b86 100644 --- a/examples/companion_radio/ui-new/MinesweeperScreen.h +++ b/examples/companion_radio/ui-new/MinesweeperScreen.h @@ -326,7 +326,7 @@ public: _state = PLAYING; return true; } - if (c == 'q' || c == 'Q') { _wantsExit = true; return true; } + if (c == KEY_CANCEL) { _wantsExit = true; return true; } return false; case PLAYING: @@ -341,7 +341,7 @@ public: case 'f': case 'F': toggleFlag(_cursorX, _cursorY); return true; - case 'q': case 'Q': + case KEY_CANCEL: _wantsExit = true; return true; default: return false; @@ -354,7 +354,7 @@ public: _state = PLAYING; return true; } - if (c == 'q' || c == 'Q') { _wantsExit = true; return true; } + if (c == KEY_CANCEL) { _wantsExit = true; return true; } return false; } return false; @@ -431,7 +431,7 @@ public: int fy = display.height() - 12; display.drawRect(0, fy - 2, display.width(), 1); display.setCursor(2, fy); - display.print("Enter:Start Q:Back"); + display.print("Enter:Start Sh+Del:Back"); #endif return 5000; @@ -487,7 +487,7 @@ public: } ty += 16; display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(cx, ty, "Enter:Retry Q:Back"); + display.drawTextCentered(cx, ty, "Enter:Retry Sh+Del:Back"); return 5000; } diff --git a/examples/companion_radio/ui-new/NotesScreen.h b/examples/companion_radio/ui-new/NotesScreen.h index 4ef4d782..4c44513f 100644 --- a/examples/companion_radio/ui-new/NotesScreen.h +++ b/examples/companion_radio/ui-new/NotesScreen.h @@ -40,8 +40,8 @@ class UITask; // Enter = newline, Shift+WASD = cursor navigation // Shift+Backspace = save & exit // RENAMING: Type = edit filename, Backspace = delete char -// Enter = confirm rename, Q = cancel -// CONFIRM_DELETE: Enter = confirm delete, Q = cancel +// Enter = confirm rename, Shift+Del = cancel +// CONFIRM_DELETE: Enter = confirm delete, Shift+Del = cancel // // Filenames: RTC timestamp (note_YYYYMMDD_HHMM.txt) or sequential (note_001.txt) // Buffer: 16KB on PSRAM for longer notes @@ -1018,8 +1018,8 @@ private: } bool handleRenameInput(char c) { - // Q - cancel rename - if (c == 'q' || c == 'Q') { + // Shift+Del - cancel rename + if (c == KEY_CANCEL) { _mode = FILE_LIST; Serial.println("Notes: Rename cancelled"); return true; @@ -1079,8 +1079,8 @@ private: return true; } - // Q or backspace - cancel - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - cancel + if (c == KEY_CANCEL) { _deleteTarget = ""; _mode = FILE_LIST; return true; diff --git a/examples/companion_radio/ui-new/PathEditorScreen.h b/examples/companion_radio/ui-new/PathEditorScreen.h index f4a99de0..07d269b0 100644 --- a/examples/companion_radio/ui-new/PathEditorScreen.h +++ b/examples/companion_radio/ui-new/PathEditorScreen.h @@ -385,7 +385,7 @@ public: display.print(right); #else display.setCursor(0, footerY); - display.print("Q:Bk W/S:Nav"); + display.print("Sh+Del:Bk W/S:Nav"); const char* right = "Enter:Sel"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); @@ -474,7 +474,7 @@ public: display.print(right); #else display.setCursor(0, footerY); - display.print("Q:Cancel W/S:Scroll"); + display.print("Sh+Del:Cancel W/S:Scroll"); const char* right = "Enter:Add"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); @@ -596,9 +596,9 @@ public: return true; } - // Q - back (discard changes or prompt?) + // Shift+Del - back (discard changes or prompt?) // For simplicity, just go back without saving - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { // Return to contacts screen without saving // The UITask will handle this via the key falling through return false; // Let UITask handle Q as back @@ -636,8 +636,8 @@ public: return true; } - // Q - cancel picker, return to main - if (c == 'q' || c == 'Q') { + // Shift+Del - cancel picker, return to main + if (c == KEY_CANCEL) { _state = STATE_MAIN; return true; } diff --git a/examples/companion_radio/ui-new/RxLogScreen.h b/examples/companion_radio/ui-new/RxLogScreen.h index a38e461f..c8e584f3 100644 --- a/examples/companion_radio/ui-new/RxLogScreen.h +++ b/examples/companion_radio/ui-new/RxLogScreen.h @@ -13,7 +13,7 @@ extern MyMesh the_mesh; // each received packet as an app-style block: route + payload type, time, // size, hash, path, channel hash/name or From/To, the decoded line (for // decryptable channels), and SNR. Entries are shown newest-first; W/S scroll -// by entry, Q returns to Settings (where the screen is opened from). +// by entry, Shift+Del returns to Settings (where the screen is opened from). // ========================================================================== class RxLogScreen : public UIScreen { @@ -187,7 +187,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Swipe:Scroll"); #else - display.print("Q:Bk W/S:Scroll"); + display.print("Sh+Del:Bk W/S:Scroll"); #endif return 5000; // refresh every 5s to pick up newly received packets @@ -207,10 +207,10 @@ public: return false; } // Back to Settings - if (c == 'q' || c == 'Q' || c == 0x1B) { + if (c == KEY_CANCEL) { if (_task) _task->gotoSettingsScreen(); return true; } return false; } -}; +}; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/SMSScreen.h b/examples/companion_radio/ui-new/SMSScreen.h index 38646400..2963652b 100644 --- a/examples/companion_radio/ui-new/SMSScreen.h +++ b/examples/companion_radio/ui-new/SMSScreen.h @@ -17,7 +17,7 @@ // // Navigation mirrors ChannelScreen conventions: // W/S: scroll Enter: select/send C: compose new/reply -// Q: back Sh+Del: cancel compose +// Sh+Del: back Sh+Del: cancel compose // D: contacts (from inbox) // A: add/edit contact (from conversation) // F: call (from conversation, contacts, or phone dialer) @@ -385,7 +385,7 @@ public: display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); - display.print("Q:Back"); + display.print("Sh+Del:Back"); const char* rt = "Ent:Open"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -516,7 +516,7 @@ public: display.drawRect(0, footerY - 1, W, 1); display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); - display.print("Q:Bk"); + display.print("Sh+Del:Bk"); if (_phoneInputPos > 0) { const char* rt = "Ent:Call"; display.setCursor(W - display.getTextWidth(rt) - 2, footerY); @@ -620,7 +620,7 @@ public: display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); - display.print("Q:Back"); + display.print("Sh+Del:Back"); const char* mid = "D:Contacts"; display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); display.print(mid); @@ -734,7 +734,7 @@ public: display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); - display.print("Q:Bk A:Add Contact"); + display.print("Sh+Del:Bk A:Add Contact"); const char* rt = "C:Reply"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -887,7 +887,7 @@ public: display.drawRect(0, footerY - 2, display.width(), 1); display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); - display.print("Q:Back"); + display.print("Sh+Del:Back"); const char* rt = "Ent:SMS F:Call"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -986,7 +986,7 @@ public: display.drawRect(0, footerY - 2, W, 1); display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); - display.print("Ent/Q:Hang up"); + display.print("Ent/Sh+Del:Hang up"); return 800; // Fast refresh for dot animation } @@ -1042,7 +1042,7 @@ public: display.setColor(DisplayDriver::GREEN); display.setCursor(0, footerY); display.print("Ent:Answer"); - const char* rt = "Q:Reject"; + const char* rt = "Sh+Del:Reject"; display.setColor(DisplayDriver::YELLOW); display.setCursor(W - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -1161,7 +1161,7 @@ public: } return true; - case 'q': case 'Q': // Back to home (handled by main.cpp) + case KEY_CANCEL: // Back to home (handled by main.cpp) return false; default: @@ -1207,7 +1207,7 @@ public: } return true; - case 'q': // Back to app menu + case KEY_CANCEL: // Back to app menu _phoneInputBuf[0] = '\0'; _phoneInputPos = 0; _view = APP_MENU; @@ -1343,7 +1343,7 @@ public: _view = CONTACTS; return true; - case 'q': case 'Q': // Back to app menu + case KEY_CANCEL: // Back to app menu _view = APP_MENU; _menuCursor = 0; return true; @@ -1399,7 +1399,7 @@ public: return true; } - case 'q': case 'Q': // Back to inbox + case KEY_CANCEL: // Back to inbox refreshInbox(); _view = INBOX; return true; @@ -1527,7 +1527,7 @@ public: } return true; - case 'q': case 'Q': // Back to inbox + case KEY_CANCEL: // Back to inbox refreshInbox(); _view = INBOX; return true; @@ -1579,11 +1579,11 @@ public: } } - // ---- Dialing out input (Enter or Q to cancel/hang up) ---- + // ---- Dialing out input (Enter or Shift+Del to cancel/hang up) ---- bool handleDialingOutInput(char c) { switch (c) { case '\r': // Enter - hang up - case 'q': case 'Q': + case KEY_CANCEL: modemManager.hangupCall(); _view = _callReturnView; _callPhone[0] = '\0'; @@ -1594,14 +1594,14 @@ public: } } - // ---- Incoming call input (Enter to answer, Q to reject) ---- + // ---- Incoming call input (Enter to answer, Shift+Del to reject) ---- bool handleIncomingCallInput(char c) { switch (c) { case '\r': // Enter - answer call modemManager.answerCall(); return true; - case 'q': case 'Q': // Reject call + case KEY_CANCEL: // Reject call modemManager.hangupCall(); _view = _callReturnView; _callPhone[0] = '\0'; @@ -1616,7 +1616,7 @@ public: bool handleInCallInput(char c) { switch (c) { case '\r': // Enter - hang up - case 'q': case 'Q': + case KEY_CANCEL: modemManager.hangupCall(); _view = _callReturnView; _callPhone[0] = '\0'; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 15958bcd..05d34e1d 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -241,7 +241,7 @@ enum SubScreen : uint8_t { #ifdef MECK_OTA_UPDATE // OTA update phases enum OtaPhase : uint8_t { - OTA_PHASE_CONFIRM, // "Start firmware update? Enter:Yes Q:No" + OTA_PHASE_CONFIRM, // "Start firmware update? Enter:Yes Sh+Del:No" OTA_PHASE_AP_START, // Starting WiFi AP + web server OTA_PHASE_WAITING, // AP up, waiting for device to upload OTA_PHASE_RECEIVING, // File upload in progress @@ -253,7 +253,7 @@ enum OtaPhase : uint8_t { // File manager phases enum FmPhase : uint8_t { - FM_PHASE_CONFIRM, // "Start SD file manager? Enter:Yes Q:No" + FM_PHASE_CONFIRM, // "Start SD file manager? Enter:Yes Sh+Del:No" FM_PHASE_WAITING, // AP up, file browser active FM_PHASE_ERROR, // Error with message }; @@ -872,7 +872,7 @@ public: void wifiPasswordBack() { _wifiPhase = WIFI_PHASE_SELECT; } // True while the WiFi network picker is showing; UITask uses this so - // Shift+Backspace can exit the picker (same as Q). + // Shift+Backspace can exit the picker. bool isInWifiNetworkSelect() const { return _editMode == EDIT_WIFI && _wifiPhase == WIFI_PHASE_SELECT; } @@ -2358,7 +2358,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.drawTextCentered(display.width() / 2, by + bh - 14, "Tap:Yes Boot:No"); #else - display.drawTextCentered(display.width() / 2, by + bh - 14, "Enter:Yes Q:No"); + display.drawTextCentered(display.width() / 2, by + bh - 14, "Enter:Yes Sh+Del:No"); #endif display.setTextSize(1); } @@ -2440,7 +2440,7 @@ public: display.print("Tap:Pick Boot:Back"); #else display.setCursor(bx + 4, fy); - display.print("Enter:Pick Q:Back"); + display.print("Enter:Pick Sh+Del:Back"); #endif // Scroll indicator @@ -2779,7 +2779,7 @@ public: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Tap:Send Boot:Cancel"); #else - display.print("Enter:Send Q:Cancel"); + display.print("Enter:Send Sh+Del:Cancel"); #endif display.setTextSize(1); } @@ -2872,7 +2872,7 @@ public: } #elif defined(LILYGO_TECHO_LITE) if (_editMode == EDIT_TEXT) { - display.print("Ent:Ok Q:Cancel"); + display.print("Ent:Ok Sh+Del:Cancel"); } else if (_editMode == EDIT_PICKER) { display.print("A/D:Pick Ent:Ok"); } else if (_editMode == EDIT_NUMBER) { @@ -2880,19 +2880,19 @@ public: } else if (_editMode == EDIT_CONFIRM) { // overlay handles it } else { - display.print("Q:Bk"); + display.print("Sh+Del:Bk"); const char* r = "Ent:Edit"; display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); display.print(r); } #else if (_editMode == EDIT_TEXT) { - display.print("Type, Enter:Ok Q:Cancel"); + display.print("Type, Enter:Ok Sh+Del:Cancel"); #ifdef MECK_WIFI_COMPANION } else if (_editMode == EDIT_WIFI) { if (_wifiPhase == WIFI_PHASE_SELECT) { if (_wifiSSIDCount == 0) { - display.print("R/Enter:Rescan Q:Back"); + display.print("R/Enter:Rescan Sh+Del:Back"); } else { display.print("W/S:Pick Enter:Sel R:Rescan"); } @@ -2905,21 +2905,21 @@ public: #ifdef MECK_OTA_UPDATE } else if (_editMode == EDIT_OTA) { if (_otaPhase == OTA_PHASE_CONFIRM) { - display.print("Enter:Start Q:Cancel"); + display.print("Enter:Start Sh+Del:Cancel"); } else if (_otaPhase == OTA_PHASE_WAITING) { - display.print("Q:Cancel"); + display.print("Sh+Del:Cancel"); } else if (_otaPhase == OTA_PHASE_ERROR) { - display.print("Q:Back"); + display.print("Sh+Del:Back"); } else { display.print("Please wait..."); } } else if (_editMode == EDIT_FILEMGR) { if (_fmPhase == FM_PHASE_CONFIRM) { - display.print("Enter:Start Q:Cancel"); + display.print("Enter:Start Sh+Del:Cancel"); } else if (_fmPhase == FM_PHASE_WAITING) { - display.print("Q:Stop"); + display.print("Sh+Del:Stop"); } else if (_fmPhase == FM_PHASE_ERROR) { - display.print("Q:Back"); + display.print("Sh+Del:Back"); } else { display.print("Please wait..."); } @@ -2927,16 +2927,16 @@ public: } else if (_editMode == EDIT_PICKER) { display.print("A/D:Choose Enter:Ok"); } else if (_editMode == EDIT_NUMBER) { - display.print("W/S:Adj Enter:Ok Q:Cancel"); + display.print("W/S:Adj Enter:Ok Sh+Del:Cancel"); } else if (_editMode == EDIT_CONFIRM) { // Footer already covered by overlay } else { if (_subScreen == SUB_CHANNELS) { - display.print("Q:Bk C:Share"); + display.print("Sh+Del:Bk C:Share"); } else if (_subScreen != SUB_NONE) { - display.print("Q:Back"); + display.print("Sh+Del:Back"); } else { - display.print("Q:Bk"); + display.print("Sh+Del:Bk"); } const char* r = "Tap/Ent:Edit"; display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY); @@ -2982,7 +2982,7 @@ public: _confirmAction = 0; return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { if (_confirmAction == 3) { // Region nudge cancelled — scroll to Default Region row _editMode = EDIT_NONE; @@ -3036,7 +3036,7 @@ public: _editMode = EDIT_NONE; return true; } - if (c == 'q' || c == 'Q' || c == '\b') { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; return true; } @@ -3064,7 +3064,7 @@ public: _editMode = EDIT_NONE; return true; } - if (c == 'q' || c == 'Q' || c == '\b') { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; return true; } @@ -3080,7 +3080,7 @@ public: startOTAServer(); return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; return true; } @@ -3089,12 +3089,12 @@ public: if (_otaUploadOk) { return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { stopOTA(); return true; } } else if (_otaPhase == OTA_PHASE_ERROR) { - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { stopOTA(); return true; } @@ -3110,17 +3110,17 @@ public: startFileMgrServer(); return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; return true; } } else if (_fmPhase == FM_PHASE_WAITING) { - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { stopFileMgr(); return true; } } else if (_fmPhase == FM_PHASE_ERROR) { - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { stopFileMgr(); return true; } @@ -3163,7 +3163,7 @@ public: #endif return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; _wifiPhase = WIFI_PHASE_IDLE; if (_onboarding) _onboarding = false; // Skip WiFi, finish onboarding @@ -3313,7 +3313,7 @@ public: #endif return true; } - if (c == 'q' || c == 'Q' || c == 27) { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; return true; } @@ -3440,7 +3440,7 @@ public: } return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { // Revert live preview if font style picker was active if (type == ROW_FONT_STYLE) { _prefs->ui_font_style = _fontPickerOriginal; @@ -3535,7 +3535,7 @@ public: _editMode = EDIT_NONE; return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _editMode = EDIT_NONE; return true; } @@ -4000,8 +4000,8 @@ public: } #endif - // Q: back -- if in sub-screen, return to top level; else exit settings - if (c == 'q' || c == 'Q') { + // Shift+Del: back -- if in sub-screen, return to top level; else exit settings + if (c == KEY_CANCEL) { #ifdef HAS_SDCARD if (_subScreen == SUB_EXPORT_FLAGS) { // Return to Export/Import sub-screen diff --git a/examples/companion_radio/ui-new/SnakeScreen.h b/examples/companion_radio/ui-new/SnakeScreen.h index f680c8f3..854d7dbf 100644 --- a/examples/companion_radio/ui-new/SnakeScreen.h +++ b/examples/companion_radio/ui-new/SnakeScreen.h @@ -317,7 +317,7 @@ public: switch (_state) { case READY: if (c == '\r') { _state = PLAYING; _lastTick = millis(); return true; } - if (c == 'q' || c == 'Q') { _wantsExit = true; return true; } + if (c == KEY_CANCEL) { _wantsExit = true; return true; } return false; case PLAYING: switch (c) { @@ -325,12 +325,12 @@ public: case 's': case 'S': if (_dir != UP) _pendingDir = DOWN; return true; case 'a': case 'A': if (_dir != RIGHT) _pendingDir = LEFT; return true; case 'd': case 'D': if (_dir != LEFT) _pendingDir = RIGHT; return true; - case 'q': case 'Q': _wantsExit = true; return true; + case KEY_CANCEL: _wantsExit = true; return true; default: return false; } case GAME_OVER: if (c == '\r') { resetGame(); _state = PLAYING; _lastTick = millis(); return true; } - if (c == 'q' || c == 'Q') { _wantsExit = true; return true; } + if (c == KEY_CANCEL) { _wantsExit = true; return true; } return false; } return false; @@ -459,7 +459,7 @@ public: ty += 12; } display.setColor(DisplayDriver::GREEN); - display.drawTextCentered(cx, ty, "Enter:Retry Q:Back"); + display.drawTextCentered(cx, ty, "Enter:Retry Sh+Del:Back"); } } @@ -476,10 +476,10 @@ public: display.drawRect(0, fy - 2, display.width(), 1); if (_state == PLAYING) { display.setCursor(2, fy); - display.print("Q:Back"); + display.print("Sh+Del:Back"); } else if (_state == READY) { display.setCursor(2, fy); - display.print("Enter:Start Q:Back"); + display.print("Enter:Start Sh+Del:Back"); } #endif diff --git a/examples/companion_radio/ui-new/TextReaderScreen.h b/examples/companion_radio/ui-new/TextReaderScreen.h index a4612dbd..07c8d7e2 100644 --- a/examples/companion_radio/ui-new/TextReaderScreen.h +++ b/examples/companion_radio/ui-new/TextReaderScreen.h @@ -1175,7 +1175,7 @@ private: display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Open Boot: home"); #else display.setCursor(0, footerY); - display.print("Q:Bk"); + display.print("Sh+Del:Bk"); const char* right = "Tap/Ent:Open"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); @@ -1299,7 +1299,7 @@ private: display.setCursor(0, footerY); display.print(status); - const char* right = _gotoMode ? "Ent:Go Q:Cancel" : "Entr:Pg# Q:Bk"; + const char* right = _gotoMode ? "Ent:Go Sh+Del:Cancel" : "Entr:Pg# Sh+Del:Bk"; display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY); display.print(right); #endif @@ -1953,8 +1953,8 @@ public: return true; } - // Q - close book, back to file list - if (c == 'q' || c == 'Q') { + // Shift+Del - close book, back to file list + if (c == KEY_CANCEL) { closeBook(); _mode = FILE_LIST; return true; @@ -1974,8 +1974,8 @@ public: return true; } - // Q or Escape — cancel - if (c == 'q' || c == 'Q' || c == 0x1B) { + // Shift+Del - cancel + if (c == KEY_CANCEL) { _gotoMode = false; return true; } diff --git a/examples/companion_radio/ui-new/Tracescreen.h b/examples/companion_radio/ui-new/Tracescreen.h index 9d0141ea..9dbc4ab4 100644 --- a/examples/companion_radio/ui-new/Tracescreen.h +++ b/examples/companion_radio/ui-new/Tracescreen.h @@ -528,13 +528,13 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Cancel Tap:Apply"); #else - display.print("Q:Cancel Enter:Apply"); + display.print("Sh+Del:Cancel Enter:Apply"); #endif } else { #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Exit Tap:Sel"); #else - display.print("Q:Exit W/S:Nav Ent:Sel"); + display.print("Sh+Del:Exit W/S:Nav Ent:Sel"); #endif } @@ -597,7 +597,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back Tap:Add"); #else - display.print("Q:Back W/S:Scroll Ent:Add"); + display.print("Sh+Del:Back W/S:Scroll Ent:Add"); #endif return 5000; @@ -653,7 +653,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Cancel"); #else - display.print("Q:Cancel"); + display.print("Sh+Del:Cancel"); #endif return 500; // Fast refresh for elapsed timer @@ -749,7 +749,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Boot:Back Tap:New Trace"); #else - display.print("Q:Back Ent:New Trace"); + display.print("Sh+Del:Back Ent:New Trace"); #endif return 5000; @@ -787,8 +787,8 @@ private: } return true; } - // Q or Escape: cancel edit - if (c == 'q' || c == 'Q' || c == 27) { + // Shift+Del: cancel edit + if (c == KEY_CANCEL) { _editing = false; return true; } @@ -833,8 +833,8 @@ private: memset(_pathBuf, 0, sizeof(_pathBuf)); return true; } - // Q - exit - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - exit + if (c == KEY_CANCEL) { _wantExit = true; return true; } @@ -888,8 +888,8 @@ private: if (_repSel < _repCount - 1) _repSel++; return true; } - // Q - back to build - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - back to build + if (c == KEY_CANCEL) { _state = STATE_BUILD; return true; } @@ -915,8 +915,8 @@ private: } bool handleRunningInput(char c) { - // Q - cancel - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - cancel + if (c == KEY_CANCEL) { _state = STATE_BUILD; return true; } @@ -934,8 +934,8 @@ private: _resultScroll++; return true; } - // Q - back to build screen (keep path) - if (c == 'q' || c == 'Q' || c == '\b') { + // Shift+Del - back to build screen (keep path) + if (c == KEY_CANCEL) { _state = STATE_BUILD; _menuSel = 0; return true; diff --git a/examples/companion_radio/ui-new/VoiceMessageScreen.h b/examples/companion_radio/ui-new/VoiceMessageScreen.h index 266eb6a5..7649f0f4 100644 --- a/examples/companion_radio/ui-new/VoiceMessageScreen.h +++ b/examples/companion_radio/ui-new/VoiceMessageScreen.h @@ -16,9 +16,9 @@ // - Review before send: play / re-record / delete // // Keyboard controls: -// MESSAGE_LIST: W/S = scroll, Enter = play selected, D = delete, Q = exit +// MESSAGE_LIST: W/S = scroll, Enter = play selected, D = delete, Shift+Del = exit // RECORDING: Mic release or 5s timeout stops recording -// REVIEW: Enter = play, Mic = re-record, D = delete, Q = back to list +// REVIEW: Enter = play, Mic = re-record, D = delete, Shift+Del = back to list // // Guard: MECK_AUDIO_VARIANT (audio variant only — needs I2S DAC + PDM mic) // ============================================================================= @@ -1043,7 +1043,7 @@ private: int footerY = display.height() - 12; display.setTextSize(1); display.setCursor(0, footerY); - display.print("Ent:Send Q:Cancel"); + display.print("Ent:Send Sh+Del:Cancel"); // No-direct-path popup. RAW_CUSTOM voice packets are direct-route only, // so a contact with no path set cannot receive one. Drawn last so it @@ -1097,7 +1097,7 @@ private: } } break; - case 'q': case 'Q': + case KEY_CANCEL: _mode = REVIEW; break; } @@ -1172,11 +1172,11 @@ private: display.setTextSize(1); display.setCursor(0, footerY); if (_listPlaying) { - display.print("Playing... Q:Stop"); + display.print("Playing... Sh+Del:Stop"); } else if (!_fileList.empty()) { display.print("Mic:Rec Ent:Ply F:Snd D:Del"); } else { - display.print("Mic:Record Q:Exit"); + display.print("Mic:Record Sh+Del:Exit"); } // "Loading" popup while a forward-send file is read off SD and encoded. @@ -1321,11 +1321,11 @@ private: display.setTextSize(1); display.setCursor(0, footerY); if (_reviewPlaying) { - display.print("Q:Stop"); + display.print("Sh+Del:Stop"); } else if (_c2Valid) { - display.print("S:Send Ent:Play Mic:Redo Q:List"); + display.print("S:Send Ent:Play Mic:Redo Sh+Del:List"); } else { - display.print("Ent:Play Mic:Redo D:Del Q:List"); + display.print("Ent:Play Mic:Redo D:Del Sh+Del:List"); } } @@ -1674,7 +1674,7 @@ public: handleListInput(key); return true; case RECORDING: - if (key == 'q' || key == 'Q') { + if (key == KEY_CANCEL) { stopRecording(); _mode = MESSAGE_LIST; } @@ -1741,7 +1741,7 @@ private: } break; - // q/Q handled by main.cpp (exits voice screen) + // Shift+Del handled by main.cpp (exits voice screen) } } @@ -1765,7 +1765,7 @@ private: scanVoiceFolder(); break; - case 'q': case 'Q': // Back to list (keep the file) + case KEY_CANCEL: // Back to list (keep the file) stopPlayback(); _mode = MESSAGE_LIST; scanVoiceFolder(); diff --git a/examples/companion_radio/ui-new/WebReaderScreen.h b/examples/companion_radio/ui-new/WebReaderScreen.h index 0f3fcf47..62448043 100644 --- a/examples/companion_radio/ui-new/WebReaderScreen.h +++ b/examples/companion_radio/ui-new/WebReaderScreen.h @@ -2746,7 +2746,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Tap: Retry"); #else - display.print("Enter: Retry Q: Back"); + display.print("Enter: Retry Sh+Del: Back"); #endif } @@ -2762,7 +2762,7 @@ private: else display.print("Swipe: Navigate Tap: Select"); #else - display.print("Q:Back W/S:Nav Ent:Select"); + display.print("Sh+Del:Back W/S:Nav Ent:Select"); #endif } @@ -3164,11 +3164,11 @@ private: if (onBookmark && hasData) snprintf(footerBuf, sizeof(footerBuf), "Ent:Go Del:Del Bkmk X:Clr Ckies"); else if (onBookmark) - snprintf(footerBuf, sizeof(footerBuf), "Q:Bk Ent:Go Del:Del Bkmk"); + snprintf(footerBuf, sizeof(footerBuf), "Sh+Del:Bk Ent:Go Del:Del Bkmk"); else if (hasData) - snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S Ent:Go X:Clr Ckies"); + snprintf(footerBuf, sizeof(footerBuf), "Sh+Del:Bk W/S Ent:Go X:Clr Ckies"); else - snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S:Nav Ent:Go"); + snprintf(footerBuf, sizeof(footerBuf), "Sh+Del:Bk W/S:Nav Ent:Go"); #endif display.print(footerBuf); } @@ -3273,7 +3273,7 @@ private: #else display.print("Ent: Open in Reader"); display.setCursor(0, y + 16); - display.print("Q: Back to browser"); + display.print("Sh+Del: Back to browser"); #endif } else { display.setColor(DisplayDriver::YELLOW); @@ -3292,7 +3292,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Tap: Back to browser"); #else - display.print("Q: Back to browser"); + display.print("Sh+Del: Back to browser"); #endif } @@ -3305,7 +3305,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print(_downloadOk ? "Tap: Open in Reader" : "Tap: Back"); #else - display.print(_downloadOk ? "Ent:Read Q:Back" : "Q:Back"); + display.print(_downloadOk ? "Ent:Read Sh+Del:Back" : "Sh+Del:Back"); #endif } @@ -3442,13 +3442,13 @@ private: } #else } else if (_formCount > 0 && _linkCount > 0) { - hint = "L:Lnk F:Frm B:Bk Q:X"; + hint = "L:Lnk F:Frm B:Bk Sh+Del:X"; } else if (_formCount > 0) { - hint = "F:Frm B:Bk Q:X"; + hint = "F:Frm B:Bk Sh+Del:X"; } else if (_linkCount > 0) { - hint = "L:Lnk B:Bk Q:X"; + hint = "L:Lnk B:Bk Sh+Del:X"; } else { - hint = "B:Bk Q:X"; + hint = "B:Bk Sh+Del:X"; } #endif display.setCursor(display.width() - display.getTextWidth(hint) - 2, footerY); @@ -3573,8 +3573,8 @@ private: return true; } - // Q - back to home (if possible) or exit - if (c == 'q' || c == 'Q') { + // Shift+Del - back to home (if possible) or exit + if (c == KEY_CANCEL) { if (_wifiState == WIFI_ENTERING_PASS) { _wifiState = WIFI_SCAN_DONE; } else { @@ -3624,11 +3624,6 @@ private: } return true; } - if (c == 'q' && _urlLen == 0) { - // Q exits URL editing when empty - _urlEditing = false; - return true; - } // Escape URL editing mode if (c == 0x1B) { // ESC _urlEditing = false; @@ -3687,10 +3682,6 @@ private: } return true; } - if (c == 'q' && _searchLen == 0) { - _searchEditing = false; - return true; - } if (c == 0x1B) { // ESC _searchEditing = false; return true; @@ -3900,8 +3891,8 @@ private: return true; } - // Q - exit to home - if (c == 'q' || c == 'Q') { + // Shift+Del - exit to home + if (c == KEY_CANCEL) { _mode = HOME; _homeSelected = 0; return true; @@ -4068,16 +4059,16 @@ private: display.setCursor(0, footerY); if (_formFieldEditing) { - display.print("Type text Ent:Next Q:Undo"); + display.print("Type text Ent:Next Sh+Del:Undo"); } else { const char* hint; #if defined(LilyGo_T5S3_EPaper_Pro) hint = "Swipe: Navigate Tap: Edit Hold: Back"; #else if (_formCount > 1) - hint = "W/S:Nav Ent:Edit :Form Q:Back"; + hint = "W/S:Nav Ent:Edit :Form Sh+Del:Back"; else - hint = "W/S:Nav Ent:Edit/Go Q:Back"; + hint = "W/S:Nav Ent:Edit/Go Sh+Del:Back"; #endif display.print(hint); } @@ -4120,8 +4111,8 @@ private: return true; } - // Q as cancel — discard edits, restore original value - if ((c == 'q' || c == 'Q') && _formEditLen == 0) { + // Shift+Del as cancel -- discard edits, restore original value + if (c == KEY_CANCEL) { _formFieldEditing = false; _formLastCharAt = 0; return true; @@ -4196,8 +4187,8 @@ private: return true; } - // Q - back to reading - if (c == 'q' || c == 'Q') { + // Shift+Del - back to reading + if (c == KEY_CANCEL) { _mode = READING; _formFieldEditing = false; return true; @@ -4754,7 +4745,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Swipe: Navigate Tap: Edit Hold: Back"); #else - display.print("W/S:Nav Ent:Edit/Go Q:Back"); + display.print("W/S:Nav Ent:Edit/Go Sh+Del:Back"); #endif } @@ -4824,7 +4815,7 @@ private: _ircSetupBufLen = strlen(_ircSetupBuf); return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _mode = HOME; _homeSelected = 0; return true; @@ -4896,7 +4887,7 @@ private: #if defined(LilyGo_T5S3_EPaper_Pro) display.print("Tap: Compose Swipe: Scroll Hold: Back"); #else - display.print("Ent:Msg W/S:Scrl Q:Bk"); + display.print("Ent:Msg W/S:Scrl Sh+Del:Bk"); #endif } @@ -5061,8 +5052,8 @@ private: return true; } - // Q - back to home (keep connection alive) - if (c == 'q' || c == 'Q') { + // Shift+Del - back to home (keep connection alive) + if (c == KEY_CANCEL) { _mode = HOME; _homeSelected = 0; return true; @@ -5419,9 +5410,9 @@ public: case IRC_CHAT: return handleIRCChatInput(c); case FETCHING: - // Q to cancel fetch (can't actually cancel HTTP mid-stream, but + // Shift+Del to cancel fetch (can't actually cancel HTTP mid-stream, but // go back to home) - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _mode = HOME; return true; } @@ -5433,7 +5424,7 @@ public: _requestTextReader = true; return true; } - if (c == 'q' || c == 'Q') { + if (c == KEY_CANCEL) { _mode = HOME; _homeSelected = 0; return true; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 4ae8421e..7db9e8e2 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -10,6 +10,10 @@ namespace mesh { #define MAX_RX_DELAY_MILLIS 32000 // 32 seconds +// Rolling-window duty-cycle (token bucket) tuning -- see issue #20 +#define MIN_TX_BUDGET_RESERVE_MS 100 // min budget (ms) required before allowing the next TX +#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX + #ifndef NOISE_FLOOR_CALIB_INTERVAL #define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds #endif @@ -20,6 +24,13 @@ void Dispatcher::begin() { _err_flags = 0; radio_nonrx_start = _ms->getMillis(); + // Initialise the rolling-window TX budget (token bucket) to a full window's + // worth of airtime. duty_cycle = 1 / (1 + airtime_factor): factor 2.0 -> 1/3 (33%). + duty_cycle_window_ms = getDutyCycleWindowMs(); + float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); + tx_budget_ms = (unsigned long)(duty_cycle_window_ms * duty_cycle); + last_budget_update = _ms->getMillis(); + _radio->begin(); prev_isrecv_mode = _radio->isInRecvMode(); } @@ -28,6 +39,25 @@ float Dispatcher::getAirtimeBudgetFactor() const { return 2.0; // default, 33.3% (1/3rd) } +// Refill the rolling-window TX budget proportionally to idle time elapsed since +// the last update, capped at one window's worth. Cheap; safe to call often. +void Dispatcher::updateTxBudget() { + unsigned long now = _ms->getMillis(); + unsigned long elapsed = now - last_budget_update; + + float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); + unsigned long max_budget = (unsigned long)(getDutyCycleWindowMs() * duty_cycle); + unsigned long refill = (unsigned long)(elapsed * duty_cycle); + + if (refill > 0) { + tx_budget_ms += refill; + if (tx_budget_ms > max_budget) { + tx_budget_ms = max_budget; + } + last_budget_update = now; + } +} + int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { return (int) ((pow(10, 0.85f - score) - 1.0) * air_time); } @@ -82,8 +112,20 @@ void Dispatcher::loop() { total_air_time += t; // keep track of how much air time we are using //Serial.print(" airtime="); Serial.println(t); - // will need radio silence up to next_tx_time - next_tx_time = futureMillis(t * getAirtimeBudgetFactor()); + // Spend this transmission's airtime from the rolling-window budget. + updateTxBudget(); + if (t > (long)tx_budget_ms) { + tx_budget_ms = 0; + } else { + tx_budget_ms -= t; + } + if (tx_budget_ms < MIN_TX_BUDGET_RESERVE_MS) { + float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); + unsigned long needed = MIN_TX_BUDGET_RESERVE_MS - tx_budget_ms; + next_tx_time = futureMillis((unsigned long)(needed / duty_cycle)); + } else { + next_tx_time = _ms->getMillis(); + } _radio->onSendFinished(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); @@ -244,7 +286,18 @@ void Dispatcher::processRecvPacket(Packet* pkt) { void Dispatcher::checkSend() { if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return; // nothing waiting to send - if (!millisHasNowPassed(next_tx_time)) return; // still in 'radio silence' phase (from airtime budget setting) + + // Rolling-window duty-cycle gate: refill, then require enough budget for a TX. + updateTxBudget(); + uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); + if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) { + float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); + unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms; + next_tx_time = futureMillis((unsigned long)(needed / duty_cycle)); + return; + } + + if (!millisHasNowPassed(next_tx_time)) return; // CAD/retry backoff still pending if (_radio->isReceiving()) { // LBT - check if radio is currently mid-receive, or if channel activity if (cad_busy_start == 0) { cad_busy_start = _ms->getMillis(); // record when CAD busy state started diff --git a/src/Dispatcher.h b/src/Dispatcher.h index d2bac755..472841e7 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -116,6 +116,9 @@ class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; unsigned long next_tx_time; + unsigned long tx_budget_ms; // rolling-window TX budget (ms airtime) -- issue #20 + unsigned long last_budget_update; // millis() at last budget refill + unsigned long duty_cycle_window_ms; // rolling window length (default 1 hour) unsigned long cad_busy_start; unsigned long radio_nonrx_start; unsigned long next_floor_calib_time, next_agc_reset_time; @@ -139,6 +142,9 @@ protected: outbound = NULL; total_air_time = rx_air_time = 0; next_tx_time = 0; + tx_budget_ms = 0; + last_budget_update = 0; + duty_cycle_window_ms = 3600000; // 1 hour; begin() sets the real starting budget cad_busy_start = 0; next_floor_calib_time = next_agc_reset_time = 0; _err_flags = 0; @@ -158,6 +164,7 @@ protected: virtual const char* getLogDateTime() { return ""; } virtual float getAirtimeBudgetFactor() const; + virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } // rolling duty-cycle window (1 hour) -- issue #20 virtual int calcRxDelay(float score, uint32_t air_time) const; virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; @@ -178,6 +185,7 @@ public: void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0); unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds + unsigned long getRemainingTxBudget() const { return tx_budget_ms; } // rolling-window TX budget (ms) -- issue #20 unsigned long getReceiveAirTime() const {return rx_air_time; } uint32_t getNumSentFlood() const { return n_sent_flood; } uint32_t getNumSentDirect() const { return n_sent_direct; } @@ -195,6 +203,7 @@ public: private: void checkRecv(); void checkSend(); + void updateTxBudget(); // refill the rolling-window TX budget -- issue #20 }; } \ No newline at end of file