From 213e7d6b9e611338abc7b34fdf1f231d46b2ddaa Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:23:34 +1000 Subject: [PATCH] Footer fixes when the N was truncated. Added new Contacts screen for phone contacts for Pro 4G and Max. Added touch recognition to phone screen app menu. --- .../ui-new/ChannelPickerScreen.h | 2 +- .../companion_radio/ui-new/ChannelScreen.h | 2 +- .../ui-new/RepeaterAdminScreen.h | 4 +- examples/companion_radio/ui-new/SMSScreen.h | 322 ++++++++++++++++-- variants/lilygo_tdeck_max/TDeckProMaxBoard.h | 2 +- variants/lilygo_tdeck_max/variant.h | 4 +- 6 files changed, 293 insertions(+), 43 deletions(-) diff --git a/examples/companion_radio/ui-new/ChannelPickerScreen.h b/examples/companion_radio/ui-new/ChannelPickerScreen.h index 564d4790..e42587ca 100644 --- a/examples/companion_radio/ui-new/ChannelPickerScreen.h +++ b/examples/companion_radio/ui-new/ChannelPickerScreen.h @@ -415,7 +415,7 @@ public: } else { display.print("W/S:Nav Q:X"); const char* rt = "Ent:Open"; - display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); + display.setCursor(display.width() - display.getTextWidth(rt) - 6, footerY); display.print(rt); } #endif diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index f0ec7668..12c66fb4 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -866,7 +866,7 @@ public: display.setCursor(0, footerY); display.print("Q:Bck"); const char* rtInbox = "Ent:Open"; - display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY); + display.setCursor(display.width() - display.getTextWidth(rtInbox) - 6, footerY); display.print(rtInbox); #endif diff --git a/examples/companion_radio/ui-new/RepeaterAdminScreen.h b/examples/companion_radio/ui-new/RepeaterAdminScreen.h index dbae5729..7321dbc5 100644 --- a/examples/companion_radio/ui-new/RepeaterAdminScreen.h +++ b/examples/companion_radio/ui-new/RepeaterAdminScreen.h @@ -746,14 +746,14 @@ private: // --- Footer helpers --- void renderFooterRight(DisplayDriver& display, int footerY, const char* text) { - display.setCursor(display.width() - display.getTextWidth(text) - 2, footerY); + display.setCursor(display.width() - display.getTextWidth(text) - 6, footerY); display.print(text); } void renderFooterMidRight(DisplayDriver& display, int footerY, const char* left, const char* right, const char* mid) { int leftEnd = display.getTextWidth(left) + 2; - int rightStart = display.width() - display.getTextWidth(right) - 2; + int rightStart = display.width() - display.getTextWidth(right) - 6; int midX = leftEnd + (rightStart - leftEnd - display.getTextWidth(mid)) / 2; display.setCursor(midX, footerY); display.print(mid); diff --git a/examples/companion_radio/ui-new/SMSScreen.h b/examples/companion_radio/ui-new/SMSScreen.h index 0fa903ff..fcec8170 100644 --- a/examples/companion_radio/ui-new/SMSScreen.h +++ b/examples/companion_radio/ui-new/SMSScreen.h @@ -48,7 +48,7 @@ class UITask; // forward declaration class SMSScreen : public UIScreen { public: enum SubView { APP_MENU, INBOX, CONVERSATION, COMPOSE, CONTACTS, EDIT_CONTACT, PHONE_DIALER, - DIALING_OUT, INCOMING_CALL, IN_CALL, CALL_LOG }; + DIALING_OUT, INCOMING_CALL, IN_CALL, CALL_LOG, CONTACT_PAGE }; private: UITask* _task; @@ -92,6 +92,18 @@ private: // Contacts list state int _contactsCursor; int _contactsScrollTop; + SubView _contactsReturnView; // where Q returns to from the contacts list + + // Contact page (view / add / edit a single contact) + char _cpName[SMS_CONTACT_NAME_LEN]; + char _cpPhone[SMS_PHONE_LEN]; + char _cpOrigPhone[SMS_PHONE_LEN]; // key before an edit, for rename + int _cpCursor; // 0 = Name, 1 = Number, 2 = Save + bool _cpIsNew; + bool _cpEditing; // true = typing into the selected field + char _cpEditBuf[SMS_CONTACT_NAME_LEN]; + int _cpEditPos; + bool _cpNeedNumber; // set when Save is pressed with no number // Edit contact state char _editPhone[SMS_PHONE_LEN]; @@ -142,7 +154,9 @@ public: , _msgCount(0), _msgScrollPos(0) , _composePos(0), _composeNewConversation(false) , _phoneInputPos(0), _enteringPhone(false) - , _contactsCursor(0), _contactsScrollTop(0) + , _contactsCursor(0), _contactsScrollTop(0), _contactsReturnView(INBOX) + , _cpCursor(0), _cpIsNew(false), _cpEditing(false), _cpEditPos(0) + , _cpNeedNumber(false) , _editNamePos(0), _editIsNew(false), _editReturnView(INBOX) , _callReturnView(APP_MENU), _callConnectTime(0), _callVolume(3), _callDotAnim(0) , _callLogCount(0), _callLogCursor(0), _callLogScrollTop(0), _unseenMissed(0) @@ -154,6 +168,10 @@ public: memset(_composePhone, 0, sizeof(_composePhone)); memset(_phoneInputBuf, 0, sizeof(_phoneInputBuf)); memset(_activePhone, 0, sizeof(_activePhone)); + memset(_cpName, 0, sizeof(_cpName)); + memset(_cpPhone, 0, sizeof(_cpPhone)); + memset(_cpOrigPhone, 0, sizeof(_cpOrigPhone)); + memset(_cpEditBuf, 0, sizeof(_cpEditBuf)); memset(_editPhone, 0, sizeof(_editPhone)); memset(_editNameBuf, 0, sizeof(_editNameBuf)); memset(_callPhone, 0, sizeof(_callPhone)); @@ -359,6 +377,7 @@ public: switch (_view) { case APP_MENU: return renderAppMenu(display); case CALL_LOG: return renderCallLog(display); + case CONTACT_PAGE: return renderContactPage(display); case INBOX: return renderInbox(display); case CONVERSATION: return renderConversation(display); case COMPOSE: return renderCompose(display); @@ -400,11 +419,37 @@ public: y += lineHeight; - // Item 1: SMS Inbox + // Item 1: Call Log display.setCursor(4, y); display.setColor(_menuCursor == 1 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); if (_menuCursor == 1) display.print("> "); else display.print(" "); + display.print("Call Log"); + + // Show unseen missed-call count (hidden when there are none) + if (_unseenMissed > 0) { + char countHint[12]; + snprintf(countHint, sizeof(countHint), " [%d]", _unseenMissed); + display.setColor(DisplayDriver::LIGHT); + display.print(countHint); + } + + y += lineHeight; + + // Item 2: Contacts + display.setCursor(4, y); + display.setColor(_menuCursor == 2 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + if (_menuCursor == 2) display.print("> "); + else display.print(" "); + display.print("Contacts"); + + y += lineHeight; + + // Item 3: SMS Inbox + display.setCursor(4, y); + display.setColor(_menuCursor == 3 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + if (_menuCursor == 3) display.print("> "); + else display.print(" "); display.print("SMS Inbox"); // Show unread count hint (hidden when there are none) @@ -417,23 +462,6 @@ public: display.print(countHint); } - y += lineHeight; - - // Item 2: Call Log - display.setCursor(4, y); - display.setColor(_menuCursor == 2 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); - if (_menuCursor == 2) display.print("> "); - else display.print(" "); - display.print("Call Log"); - - // Show unseen missed-call count (hidden when there are none) - if (_unseenMissed > 0) { - char countHint[12]; - snprintf(countHint, sizeof(countHint), " [%d]", _unseenMissed); - display.setColor(DisplayDriver::LIGHT); - display.print(countHint); - } - // Modem status indicator ModemState ms = modemManager.getState(); display.setTextSize(_prefs->smallTextSize()); @@ -1028,6 +1056,203 @@ public: } // ---- Contacts list ---- + // ---- Contact page: view / add / edit one contact ---- + void openContactPage(const char* name, const char* phone, bool isNew) { + strncpy(_cpName, name, SMS_CONTACT_NAME_LEN - 1); + _cpName[SMS_CONTACT_NAME_LEN - 1] = '\0'; + strncpy(_cpPhone, phone, SMS_PHONE_LEN - 1); + _cpPhone[SMS_PHONE_LEN - 1] = '\0'; + strncpy(_cpOrigPhone, phone, SMS_PHONE_LEN - 1); + _cpOrigPhone[SMS_PHONE_LEN - 1] = '\0'; + _cpIsNew = isNew; + _cpCursor = 0; + _cpEditing = false; + _cpEditPos = 0; + _cpEditBuf[0] = '\0'; + _cpNeedNumber = false; + _view = CONTACT_PAGE; + } + + int renderContactPage(DisplayDriver& display) { + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 0); + display.print(_cpIsNew ? "New Contact" : "Contact"); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, display.width(), 1); + + display.setTextSize(1); + int lineHeight = 16; + int y = 24; + char rowBuf[SMS_CONTACT_NAME_LEN + 16]; + + // Row 0: Name + display.setCursor(4, y); + display.setColor(_cpCursor == 0 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + display.print(_cpCursor == 0 ? "> " : " "); + if (_cpEditing && _cpCursor == 0) { + snprintf(rowBuf, sizeof(rowBuf), "Name: %s_", _cpEditBuf); + } else { + snprintf(rowBuf, sizeof(rowBuf), "Name: %s", _cpName); + } + display.print(rowBuf); + y += lineHeight; + + // Row 1: Number + display.setCursor(4, y); + display.setColor(_cpCursor == 1 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + display.print(_cpCursor == 1 ? "> " : " "); + if (_cpEditing && _cpCursor == 1) { + snprintf(rowBuf, sizeof(rowBuf), "Number: %s_", _cpEditBuf); + } else { + snprintf(rowBuf, sizeof(rowBuf), "Number: %s", _cpPhone); + } + display.print(rowBuf); + y += lineHeight; + + // Row 2: Save + display.setCursor(4, y); + display.setColor(_cpCursor == 2 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + display.print(_cpCursor == 2 ? "> " : " "); + display.print("Save"); + y += lineHeight; + + if (_cpNeedNumber) { + display.setTextSize(_prefs->smallTextSize()); + display.setColor(DisplayDriver::YELLOW); + display.setCursor(4, y + 4); + display.print("A number is required"); + display.setTextSize(1); + } + + // Footer + display.setTextSize(1); + int footerY = display.height() - 12; + display.drawRect(0, footerY - 2, display.width(), 1); + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, footerY); + if (_cpEditing) { + display.print("Sh+Del:Cancel"); + const char* rt = "Ent:Done"; + display.setCursor(display.width() - display.getTextWidth(rt) - 6, footerY); + display.print(rt); + } else { + display.print("Q:Back"); + if (!_cpIsNew) { + const char* mid = "F:Dial D:Del"; + display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); + display.print(mid); + } + const char* rt = "Ent:Sel"; + display.setCursor(display.width() - display.getTextWidth(rt) - 6, footerY); + display.print(rt); + } + + return 5000; + } + + bool handleContactPageInput(char c) { + // --- Field editing --- + if (_cpEditing) { + if (c == '\r') { + _cpEditBuf[_cpEditPos] = '\0'; + if (_cpCursor == 0) { + strncpy(_cpName, _cpEditBuf, SMS_CONTACT_NAME_LEN - 1); + _cpName[SMS_CONTACT_NAME_LEN - 1] = '\0'; + } else { + strncpy(_cpPhone, _cpEditBuf, SMS_PHONE_LEN - 1); + _cpPhone[SMS_PHONE_LEN - 1] = '\0'; + } + _cpEditing = false; + return true; + } + if (c == 0x18) { // Shift+Backspace -- abandon this field edit + _cpEditing = false; + return true; + } + if (c == '\b') { + if (_cpEditPos > 0) { + _cpEditPos--; + _cpEditBuf[_cpEditPos] = '\0'; + } + return true; + } + int limit = (_cpCursor == 0) ? SMS_CONTACT_NAME_LEN - 1 : SMS_PHONE_LEN - 1; + if (c >= 32 && c < 127 && _cpEditPos < limit) { + _cpEditBuf[_cpEditPos++] = c; + _cpEditBuf[_cpEditPos] = '\0'; + } + return true; // consume all keys while editing + } + + // --- Navigation --- + switch (c) { + case 'w': case 'W': + if (_cpCursor > 0) _cpCursor--; + _cpNeedNumber = false; + return true; + + case 's': case 'S': + if (_cpCursor < 2) _cpCursor++; + _cpNeedNumber = false; + return true; + + case '\r': + if (_cpCursor == 2) { + saveContactPage(); + } else { + const char* src = (_cpCursor == 0) ? _cpName : _cpPhone; + strncpy(_cpEditBuf, src, sizeof(_cpEditBuf) - 1); + _cpEditBuf[sizeof(_cpEditBuf) - 1] = '\0'; + _cpEditPos = strlen(_cpEditBuf); + _cpEditing = true; + _cpNeedNumber = false; + } + return true; + + case 'f': case 'F': // Dial this contact + if (!_cpIsNew && _cpPhone[0]) startCall(_cpPhone); + return true; + + case 'd': case 'D': // Delete this contact + if (!_cpIsNew && _cpOrigPhone[0]) { + smsContacts.remove(_cpOrigPhone); + Serial.printf("[SMSContacts] Deleted: %s\n", _cpOrigPhone); + returnToContacts(); + } + return true; + + case 'q': + case KEY_CANCEL: // Back without saving + returnToContacts(); + return true; + + default: + return false; + } + } + + void saveContactPage() { + if (_cpPhone[0] == '\0') { + _cpNeedNumber = true; // number is the storage key, so it cannot be blank + return; + } + // Renaming the number is a remove-then-add, since contacts are keyed on it + if (!_cpIsNew && _cpOrigPhone[0] && strcmp(_cpOrigPhone, _cpPhone) != 0) { + smsContacts.remove(_cpOrigPhone); + } + smsContacts.set(_cpPhone, _cpName); + Serial.printf("[SMSContacts] Saved: %s = %s\n", _cpPhone, _cpName); + returnToContacts(); + } + + void returnToContacts() { + int cnt = smsContacts.count(); + if (_contactsCursor >= cnt) _contactsCursor = (cnt > 0) ? cnt - 1 : 0; + _view = CONTACTS; + } + int renderContacts(DisplayDriver& display) { display.setTextSize(1); display.setColor(DisplayDriver::GREEN); @@ -1045,9 +1270,7 @@ public: display.setCursor(0, 25); display.print("No contacts saved"); display.setCursor(0, 37); - display.print("Open a conversation"); - display.setCursor(0, 49); - display.print("and press A to add"); + display.print("Press A to add one"); display.setTextSize(1); } else { display.setTextSize(_prefs->smallTextSize()); @@ -1095,8 +1318,11 @@ public: display.setColor(DisplayDriver::YELLOW); display.setCursor(0, footerY); display.print("Q:Back"); - const char* rt = "Ent:SMS F:Call"; - display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); + const char* mid = "A:Add"; + display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); + display.print(mid); + const char* rt = "Ent:Open"; + display.setCursor(display.width() - display.getTextWidth(rt) - 6, footerY); display.print(rt); return 5000; @@ -1330,6 +1556,7 @@ public: switch (_view) { case APP_MENU: return handleAppMenuInput(c); case CALL_LOG: return handleCallLogInput(c); + case CONTACT_PAGE: return handleContactPageInput(c); case INBOX: return handleInboxInput(c); case CONVERSATION: return handleConversationInput(c); case COMPOSE: return handleComposeInput(c); @@ -1351,7 +1578,7 @@ public: return true; case 's': case 'S': - if (_menuCursor < 2) _menuCursor++; + if (_menuCursor < 3) _menuCursor++; return true; case '\r': // Enter - select menu item @@ -1361,12 +1588,6 @@ public: _phoneInputPos = 0; _view = PHONE_DIALER; } else if (_menuCursor == 1) { - // SMS Inbox - if (_sdReady) refreshInbox(); - _inboxCursor = 0; - _inboxScrollTop = 0; - _view = INBOX; - } else { // Call Log if (_sdReady) { smsStore.markMissedSeen(); @@ -1375,6 +1596,18 @@ public: _callLogCursor = 0; _callLogScrollTop = 0; _view = CALL_LOG; + } else if (_menuCursor == 2) { + // Contacts + _contactsCursor = 0; + _contactsScrollTop = 0; + _contactsReturnView = APP_MENU; + _view = CONTACTS; + } else { + // SMS Inbox + if (_sdReady) refreshInbox(); + _inboxCursor = 0; + _inboxScrollTop = 0; + _view = INBOX; } return true; @@ -1557,6 +1790,7 @@ public: case 'd': case 'D': // Open contacts list _contactsCursor = 0; _contactsScrollTop = 0; + _contactsReturnView = INBOX; _view = CONTACTS; return true; @@ -1727,7 +1961,18 @@ public: if (_contactsCursor < cnt - 1) _contactsCursor++; return true; - case '\r': // Enter - compose to selected contact + case '\r': // Enter - open the contact page + if (cnt > 0 && _contactsCursor < cnt) { + const SMSContact& ct = smsContacts.get(_contactsCursor); + openContactPage(ct.name, ct.phone, false); + } + return true; + + case 'a': case 'A': // Add a new contact + openContactPage("", "", true); + return true; + + case 'm': case 'M': // Compose an SMS to the selected contact if (cnt > 0 && _contactsCursor < cnt) { const SMSContact& ct = smsContacts.get(_contactsCursor); _composeNewConversation = true; @@ -1747,9 +1992,14 @@ public: return true; case 'q': - case KEY_CANCEL: // Back to inbox - refreshInbox(); - _view = INBOX; + case KEY_CANCEL: // Back to wherever the list was opened from + if (_contactsReturnView == INBOX) { + refreshInbox(); + _view = INBOX; + } else { + _view = APP_MENU; + _menuCursor = 2; + } return true; default: diff --git a/variants/lilygo_tdeck_max/TDeckProMaxBoard.h b/variants/lilygo_tdeck_max/TDeckProMaxBoard.h index 9660aac7..02f71386 100644 --- a/variants/lilygo_tdeck_max/TDeckProMaxBoard.h +++ b/variants/lilygo_tdeck_max/TDeckProMaxBoard.h @@ -39,7 +39,7 @@ #define BQ27220_I2C_ADDR 0x55 #ifndef BQ27220_DESIGN_CAPACITY_MAH -#define BQ27220_DESIGN_CAPACITY_MAH 1400 +#define BQ27220_DESIGN_CAPACITY_MAH 2500 #endif class TDeckProMaxBoard : public ESP32Board { diff --git a/variants/lilygo_tdeck_max/variant.h b/variants/lilygo_tdeck_max/variant.h index 07149c09..20522716 100644 --- a/variants/lilygo_tdeck_max/variant.h +++ b/variants/lilygo_tdeck_max/variant.h @@ -239,8 +239,8 @@ #define BQ27220_I2C_ADDR 0x55 #define BQ27220_I2C_SDA I2C_SDA #define BQ27220_I2C_SCL I2C_SCL -#define BQ27220_DESIGN_CAPACITY 1400 // -#define BQ27220_DESIGN_CAPACITY_MAH 1400 // Alias used by TDeckBoard.h +#define BQ27220_DESIGN_CAPACITY 2500 // +#define BQ27220_DESIGN_CAPACITY_MAH 2500 // Alias used by TDeckBoard.h #define HAS_PPM 1 // This board uses the SY6970 charger @ 0x6A (newer revision -- confirmed by I2C