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

This commit is contained in:
pelgraine
2026-06-30 20:05:51 +10:00
parent cbc30e54fe
commit 444631e08b
25 changed files with 345 additions and 298 deletions
+2 -2
View File
@@ -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.
+83 -89
View File
@@ -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();
+14 -14
View File
@@ -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;
}
@@ -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 {
@@ -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;
}
+14 -14
View File
@@ -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;
@@ -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);
@@ -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);
@@ -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 <helpers/ui/DisplayDriver.h>
#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;
@@ -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;
@@ -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
@@ -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);
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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;
}
};
};
+18 -18
View File
@@ -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';
@@ -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
@@ -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
@@ -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;
}
+15 -15
View File
@@ -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;
@@ -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();
@@ -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;
+56 -3
View File
@@ -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
+9
View File
@@ -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
};
}