diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ec6c41e..886d14e 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1322,7 +1322,7 @@ void handleKeyboardInput() { } // Q from inbox → go home; Q from inner views is handled by SMSScreen - if ((key == 'q' || key == '\b') && smsScr->getSubView() == SMSScreen::INBOX) { + if ((key == 'q' || key == '\b') && smsScr->getSubView() == SMSScreen::APP_MENU) { Serial.println("Nav: SMS -> Home"); ui_task.gotoHomeScreen(); return; diff --git a/examples/companion_radio/ui-new/SMSScreen.h b/examples/companion_radio/ui-new/SMSScreen.h index 4117b85..9ae43d5 100644 --- a/examples/companion_radio/ui-new/SMSScreen.h +++ b/examples/companion_radio/ui-new/SMSScreen.h @@ -1,24 +1,23 @@ #pragma once // ============================================================================= -// SMSScreen - SMS messaging & Voice Calls UI for T-Deck Pro (4G variant) +// SMSScreen - SMS & Phone UI for T-Deck Pro (4G variant) // // Sub-views: -// INBOX — list of conversations (names resolved via SMSContacts) -// CONVERSATION — messages for a selected contact, scrollable -// COMPOSE — text input for new SMS -// CONTACTS — browsable contacts list, pick to compose or call -// EDIT_CONTACT — add or edit a contact name for a phone number -// DIALING — outgoing call in progress -// INCOMING_CALL — incoming call ringing (answer/reject) -// IN_CALL — active voice call (timer, DTMF, volume, hangup) +// APP_MENU — landing screen: choose Phone or SMS Inbox +// INBOX — list of conversations (names resolved via SMSContacts) +// CONVERSATION — messages for a selected contact, scrollable +// COMPOSE — text input for new SMS +// CONTACTS — browsable contacts list, pick to compose or call +// EDIT_CONTACT — add or edit a contact name for a phone number +// PHONE_DIALER — enter arbitrary phone number and call // // Navigation mirrors ChannelScreen conventions: // W/S: scroll Enter: select/send C: compose new/reply // Q: back Sh+Del: cancel compose // D: contacts (from inbox) // A: add/edit contact (from conversation) -// F: call (from conversation or contacts) +// F: call (from conversation, contacts, or phone dialer) // // Guard: HAS_4G_MODEM // ============================================================================= @@ -44,16 +43,15 @@ class UITask; // forward declaration class SMSScreen : public UIScreen { public: - enum SubView { - INBOX, CONVERSATION, COMPOSE, CONTACTS, EDIT_CONTACT, - // Voice call views - DIALING, INCOMING_CALL, IN_CALL - }; + enum SubView { APP_MENU, INBOX, CONVERSATION, COMPOSE, CONTACTS, EDIT_CONTACT, PHONE_DIALER }; private: UITask* _task; SubView _view; + // App menu state + int _menuCursor; // 0 = Phone, 1 = SMS Inbox + // Inbox state SMSConversation _conversations[SMS_MAX_CONVERSATIONS]; int _convCount; @@ -72,7 +70,7 @@ private: char _composePhone[SMS_PHONE_LEN]; bool _composeNewConversation; - // Phone input state (for new conversation) + // Phone input state (for new conversation and phone dialer) char _phoneInputBuf[SMS_PHONE_LEN]; int _phoneInputPos; bool _enteringPhone; @@ -85,14 +83,8 @@ private: char _editPhone[SMS_PHONE_LEN]; char _editNameBuf[SMS_CONTACT_NAME_LEN]; int _editNamePos; - bool _editIsNew; - SubView _editReturnView; - - // Voice call state - char _callPhone[SMS_PHONE_LEN]; // Number for current/pending call - unsigned long _callConnectedMillis; // millis() when call connected - SubView _preCallView; // View to return to after call ends - uint8_t _callVolume; // Current volume level 0-5 + bool _editIsNew; // true = adding new, false = editing existing + SubView _editReturnView; // where to return after save/cancel // Refresh debounce bool _needsRefresh; @@ -109,29 +101,20 @@ private: void refreshConversation() { _msgCount = smsStore.loadMessages(_activePhone, _msgs, SMS_MSG_PAGE_SIZE); + // Scroll to bottom (newest messages are at end now, chat-style) _msgScrollPos = (_msgCount > 3) ? _msgCount - 3 : 0; } - // Helper: initiate a call to a phone number - void startCall(const char* phone) { - strncpy(_callPhone, phone, SMS_PHONE_LEN - 1); - _callPhone[SMS_PHONE_LEN - 1] = '\0'; - _callConnectedMillis = 0; - _preCallView = _view; - modemManager.dialCall(phone); - _view = DIALING; - } - public: SMSScreen(UITask* task) - : _task(task), _view(INBOX) + : _task(task), _view(APP_MENU) + , _menuCursor(0) , _convCount(0), _inboxCursor(0), _inboxScrollTop(0) , _msgCount(0), _msgScrollPos(0) , _composePos(0), _composeNewConversation(false) , _phoneInputPos(0), _enteringPhone(false) , _contactsCursor(0), _contactsScrollTop(0) , _editNamePos(0), _editIsNew(false), _editReturnView(INBOX) - , _callConnectedMillis(0), _preCallView(INBOX), _callVolume(3) , _needsRefresh(false), _lastRefresh(0) , _sdReady(false) { @@ -141,26 +124,51 @@ public: memset(_activePhone, 0, sizeof(_activePhone)); memset(_editPhone, 0, sizeof(_editPhone)); memset(_editNameBuf, 0, sizeof(_editNameBuf)); - memset(_callPhone, 0, sizeof(_callPhone)); } void setSDReady(bool ready) { _sdReady = ready; } void activate() { - _view = INBOX; - _inboxCursor = 0; - _inboxScrollTop = 0; + _view = APP_MENU; + _menuCursor = 0; if (_sdReady) refreshInbox(); } SubView getSubView() const { return _view; } bool isComposing() const { return _view == COMPOSE; } - bool isEnteringPhone() const { return _enteringPhone; } - bool isInCallView() const { - return _view == DIALING || _view == INCOMING_CALL || _view == IN_CALL; + bool isEnteringPhone() const { return _enteringPhone || _view == PHONE_DIALER; } + bool isInCallView() const { return false; } // TODO: return true when DIALING/IN_CALL views are added + + // Handle call events from modem (incoming, connected, ended, etc.) + void onCallEvent(const CallEvent& evt) { + // TODO: implement call UI state transitions (DIALING, IN_CALL, INCOMING_CALL views) + // For now, just log the event + switch (evt.type) { + case CallEventType::INCOMING: + Serial.printf("[SMSScreen] Incoming call from %s\n", evt.phone); + break; + case CallEventType::CONNECTED: + Serial.printf("[SMSScreen] Call connected: %s\n", evt.phone); + break; + case CallEventType::ENDED: + Serial.printf("[SMSScreen] Call ended (%lus)\n", (unsigned long)evt.duration); + break; + case CallEventType::MISSED: + Serial.printf("[SMSScreen] Missed call from %s\n", evt.phone); + break; + case CallEventType::BUSY: + Serial.printf("[SMSScreen] Busy: %s\n", evt.phone); + break; + case CallEventType::NO_ANSWER: + Serial.printf("[SMSScreen] No answer: %s\n", evt.phone); + break; + case CallEventType::DIAL_FAILED: + Serial.printf("[SMSScreen] Dial failed: %s\n", evt.phone); + break; + } } - // Called from main loop when an SMS arrives + // Called from main loop when an SMS arrives (saves to store + refreshes) void onIncomingSMS(const char* phone, const char* body, uint32_t timestamp) { if (_sdReady) { smsStore.saveMessage(phone, body, false, timestamp); @@ -174,47 +182,6 @@ public: _needsRefresh = true; } - // Called from main loop when a call event arrives - void onCallEvent(const CallEvent& evt) { - switch (evt.type) { - case CallEventType::INCOMING: - // Incoming call — switch to incoming call view - strncpy(_callPhone, evt.phone, SMS_PHONE_LEN - 1); - _callPhone[SMS_PHONE_LEN - 1] = '\0'; - if (_view != INCOMING_CALL) { - _preCallView = _view; - _view = INCOMING_CALL; - } - break; - - case CallEventType::CONNECTED: - // Call connected — switch to in-call view - _callConnectedMillis = millis(); - _view = IN_CALL; - break; - - case CallEventType::ENDED: - case CallEventType::MISSED: - case CallEventType::BUSY: - case CallEventType::NO_ANSWER: - case CallEventType::DIAL_FAILED: - // Call ended — return to previous view - _callPhone[0] = '\0'; - _callConnectedMillis = 0; - // Return to pre-call view or inbox - if (_preCallView == DIALING || _preCallView == INCOMING_CALL || _preCallView == IN_CALL) { - _view = INBOX; - if (_sdReady) refreshInbox(); - } else { - _view = _preCallView; - if (_view == INBOX && _sdReady) refreshInbox(); - if (_view == CONVERSATION) refreshConversation(); - } - break; - } - _needsRefresh = true; - } - // ========================================================================= // Signal strength indicator (top-right corner) // ========================================================================= @@ -223,6 +190,7 @@ public: ModemState ms = modemManager.getState(); int bars = modemManager.getSignalBars(); + // Draw signal bars (4 bars, increasing height) int barWidth = 3; int barGap = 2; int maxBarH = 10; @@ -242,9 +210,8 @@ public: x += barWidth + barGap; } - if (ms != ModemState::READY && ms != ModemState::SENDING_SMS && - ms != ModemState::DIALING && ms != ModemState::IN_CALL && - ms != ModemState::RINGING_IN) { + // Show modem state text if not ready + if (ms != ModemState::READY && ms != ModemState::SENDING_SMS) { display.setTextSize(0); display.setColor(DisplayDriver::YELLOW); const char* label = ModemManager::stateToString(ms); @@ -266,18 +233,139 @@ public: _lastRefresh = millis(); switch (_view) { - case INBOX: return renderInbox(display); - case CONVERSATION: return renderConversation(display); - case COMPOSE: return renderCompose(display); - case CONTACTS: return renderContacts(display); - case EDIT_CONTACT: return renderEditContact(display); - case DIALING: return renderDialing(display); - case INCOMING_CALL: return renderIncomingCall(display); - case IN_CALL: return renderInCall(display); + case APP_MENU: return renderAppMenu(display); + case INBOX: return renderInbox(display); + case CONVERSATION: return renderConversation(display); + case COMPOSE: return renderCompose(display); + case CONTACTS: return renderContacts(display); + case EDIT_CONTACT: return renderEditContact(display); + case PHONE_DIALER: return renderPhoneDialer(display); } return 1000; } + // ---- App menu (landing screen) ---- + int renderAppMenu(DisplayDriver& display) { + // Header + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 0); + display.print("Phone & SMS"); + + // Signal strength at top-right + renderSignalIndicator(display, display.width() - 2, 0); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, display.width(), 1); + + // Menu items + display.setTextSize(1); + int y = 24; + int lineHeight = 16; + + // Item 0: Phone + display.setCursor(4, y); + display.setColor(_menuCursor == 0 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + if (_menuCursor == 0) display.print("> "); + else display.print(" "); + display.print("Phone"); + + y += lineHeight; + + // Item 1: SMS Inbox + display.setCursor(4, y); + display.setColor(_menuCursor == 1 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + if (_menuCursor == 1) display.print("> "); + else display.print(" "); + display.print("SMS Inbox"); + + // Show conversation count hint + if (_convCount > 0) { + char countHint[12]; + snprintf(countHint, sizeof(countHint), " [%d]", _convCount); + display.setColor(DisplayDriver::LIGHT); + display.print(countHint); + } + + // Modem status if not ready + ModemState ms = modemManager.getState(); + if (ms != ModemState::READY && ms != ModemState::SENDING_SMS) { + display.setTextSize(0); + display.setColor(DisplayDriver::YELLOW); + display.setCursor(4, y + lineHeight + 8); + char statBuf[40]; + snprintf(statBuf, sizeof(statBuf), "Modem: %s", ModemManager::stateToString(ms)); + display.print(statBuf); + 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); + display.print("Q:Back"); + const char* rt = "Ent:Open"; + display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); + display.print(rt); + + return 5000; + } + + // ---- Phone dialer ---- + int renderPhoneDialer(DisplayDriver& display) { + // Header + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 0); + display.print("Dial Number"); + + // Signal strength at top-right + renderSignalIndicator(display, display.width() - 2, 0); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, display.width(), 1); + + // Phone number input + display.setTextSize(0); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(4, 24); + display.print("Enter phone number:"); + + display.setTextSize(1); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(4, 40); + if (_phoneInputPos > 0) { + display.print(_phoneInputBuf); + } + display.print("_"); + + // Hint if empty + if (_phoneInputPos == 0) { + display.setTextSize(0); + display.setColor(DisplayDriver::DARK); + display.setCursor(4, 58); + display.print("digits, +, *, #"); + 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); + display.print("Q:Back"); + if (_phoneInputPos > 0) { + const char* rt = "Ent:Call"; + display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); + display.print(rt); + } + + return 2000; + } + // ---- Inbox ---- int renderInbox(DisplayDriver& display) { ModemState ms = modemManager.getState(); @@ -318,6 +406,7 @@ public: int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2); if (visibleCount < 1) visibleCount = 1; + // Adjust scroll to keep cursor visible if (_inboxCursor < _inboxScrollTop) _inboxScrollTop = _inboxCursor; if (_inboxCursor >= _inboxScrollTop + visibleCount) { _inboxScrollTop = _inboxCursor - visibleCount + 1; @@ -330,6 +419,7 @@ public: bool selected = (idx == _inboxCursor); + // Resolve contact name (shows name if saved, phone otherwise) char dispName[SMS_CONTACT_NAME_LEN]; smsContacts.displayName(c.phone, dispName, sizeof(dispName)); @@ -377,6 +467,7 @@ public: // ---- Conversation view ---- int renderConversation(DisplayDriver& display) { + // Header - show contact name if available, phone otherwise display.setTextSize(1); display.setColor(DisplayDriver::GREEN); display.setCursor(0, 0); @@ -384,6 +475,7 @@ public: smsContacts.displayName(_activePhone, convTitle, sizeof(convTitle)); display.print(convTitle); + // Signal icon renderSignalIndicator(display, display.width() - 2, 0); display.setColor(DisplayDriver::LIGHT); @@ -401,6 +493,7 @@ public: int headerHeight = 14; int footerHeight = 14; + // Estimate chars per line uint16_t testWidth = display.getTextWidth("MMMMMMMMMM"); int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20; if (charsPerLine < 12) charsPerLine = 12; @@ -413,13 +506,15 @@ public: SMSMessage& msg = _msgs[i]; if (!msg.valid) continue; + // Direction indicator display.setCursor(0, y); display.setColor(msg.isSent ? DisplayDriver::BLUE : DisplayDriver::YELLOW); + // Time formatting (epoch-aware) char timeStr[16]; time_t now = time(nullptr); - bool haveEpoch = (now > 1700000000); - bool msgIsEpoch = (msg.timestamp > 1700000000); + bool haveEpoch = (now > 1700000000); // system clock is set + bool msgIsEpoch = (msg.timestamp > 1700000000); // msg has real timestamp if (haveEpoch && msgIsEpoch) { uint32_t age = (uint32_t)(now - msg.timestamp); @@ -437,6 +532,7 @@ public: display.print(header); y += lineHeight; + // Message body with simple word wrap display.setColor(DisplayDriver::LIGHT); int textLen = strlen(msg.body); int pos = 0; @@ -466,16 +562,13 @@ public: display.setTextSize(1); } - // Footer — now includes F:Call + // 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); - display.print("Q:Bk A:Ct"); - const char* mid = "F:Call"; - display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); - display.print(mid); + display.print("Q:Bk A:Add Contact"); const char* rt = "C:Reply"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -495,6 +588,7 @@ public: display.print(_phoneInputBuf); display.print("_"); } else { + // Show contact name if available char dispName[SMS_CONTACT_NAME_LEN]; smsContacts.displayName(_composePhone, dispName, sizeof(dispName)); char toLabel[40]; @@ -506,6 +600,7 @@ public: display.drawRect(0, 11, display.width(), 1); if (!_enteringPhone) { + // Message body display.setCursor(0, 14); display.setColor(DisplayDriver::LIGHT); display.setTextSize(0); @@ -528,6 +623,7 @@ public: } } + // Cursor display.setCursor(x * (display.width() / charsPerLine), y); display.print("_"); display.setTextSize(1); @@ -587,6 +683,7 @@ public: int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2); if (visibleCount < 1) visibleCount = 1; + // Adjust scroll if (_contactsCursor >= cnt) _contactsCursor = cnt - 1; if (_contactsCursor < 0) _contactsCursor = 0; if (_contactsCursor < _contactsScrollTop) _contactsScrollTop = _contactsCursor; @@ -601,12 +698,14 @@ public: bool selected = (idx == _contactsCursor); + // Name display.setCursor(0, y); display.setColor(selected ? DisplayDriver::GREEN : DisplayDriver::LIGHT); if (selected) display.print("> "); display.print(ct.name); y += lineHeight; + // Phone (dimmer) display.setColor(DisplayDriver::LIGHT); display.setCursor(12, y); display.print(ct.phone); @@ -615,17 +714,14 @@ public: display.setTextSize(1); } - // Footer — now includes F:Call + // 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); display.print("Q:Back"); - const char* mid = "F:Call"; - display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); - display.print(mid); - const char* rt = "Ent:SMS"; + const char* rt = "Ent:SMS F:Call"; display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); display.print(rt); @@ -642,12 +738,14 @@ public: display.setColor(DisplayDriver::LIGHT); display.drawRect(0, 11, display.width(), 1); + // Phone number (read-only) display.setTextSize(0); display.setColor(DisplayDriver::LIGHT); display.setCursor(0, 16); display.print("Phone: "); display.print(_editPhone); + // Name input display.setCursor(0, 30); display.setColor(DisplayDriver::YELLOW); display.print("Name: "); @@ -657,6 +755,7 @@ public: display.setTextSize(1); + // Footer display.setTextSize(1); int footerY = display.height() - 12; display.drawRect(0, footerY - 2, display.width(), 1); @@ -670,214 +769,92 @@ public: return 2000; } - // ========================================================================= - // VOICE CALL RENDER VIEWS - // ========================================================================= - - // ---- Dialing (outgoing call in progress) ---- - int renderDialing(DisplayDriver& display) { - display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, 0); - display.print("Calling..."); - - renderSignalIndicator(display, display.width() - 2, 0); - - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, 11, display.width(), 1); - - // Contact name / phone number centred - int centreY = display.height() / 2 - 20; - - char dispName[SMS_CONTACT_NAME_LEN]; - smsContacts.displayName(_callPhone, dispName, sizeof(dispName)); - - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - uint16_t nameW = display.getTextWidth(dispName); - display.setCursor((display.width() - nameW) / 2, centreY); - display.print(dispName); - - // Show raw phone number below name (if name differs from phone) - if (strcmp(dispName, _callPhone) != 0) { - display.setTextSize(0); - display.setColor(DisplayDriver::LIGHT); - uint16_t phoneW = display.getTextWidth(_callPhone); - display.setCursor((display.width() - phoneW) / 2, centreY + 16); - display.print(_callPhone); - } - - // Animated dots indicator - display.setTextSize(0); - display.setColor(DisplayDriver::YELLOW); - unsigned long elapsed = millis() / 500; - int dots = (elapsed % 4); - char dotStr[5] = " "; - for (int i = 0; i < dots; i++) dotStr[i] = '.'; - dotStr[dots] = '\0'; - uint16_t dotW = display.getTextWidth("..."); - display.setCursor((display.width() - dotW) / 2, centreY + 32); - display.print(dotStr); - - // Footer - display.setTextSize(1); - int footerY = display.height() - 12; - display.drawRect(0, footerY - 2, display.width(), 1); - display.setColor(DisplayDriver::YELLOW); - const char* hangup = "Ent:Hangup"; - display.setCursor((display.width() - display.getTextWidth(hangup)) / 2, footerY); - display.print(hangup); - - return 500; // Fast refresh for animated dots - } - - // ---- Incoming call ---- - int renderIncomingCall(DisplayDriver& display) { - display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, 0); - display.print("Incoming Call"); - - renderSignalIndicator(display, display.width() - 2, 0); - - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, 11, display.width(), 1); - - int centreY = display.height() / 2 - 20; - - char dispName[SMS_CONTACT_NAME_LEN]; - if (_callPhone[0]) { - smsContacts.displayName(_callPhone, dispName, sizeof(dispName)); - } else { - strncpy(dispName, "Unknown", sizeof(dispName)); - } - - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - uint16_t nameW = display.getTextWidth(dispName); - display.setCursor((display.width() - nameW) / 2, centreY); - display.print(dispName); - - if (_callPhone[0] && strcmp(dispName, _callPhone) != 0) { - display.setTextSize(0); - display.setColor(DisplayDriver::LIGHT); - uint16_t phoneW = display.getTextWidth(_callPhone); - display.setCursor((display.width() - phoneW) / 2, centreY + 16); - display.print(_callPhone); - } - - // Ringing indicator - display.setTextSize(0); - display.setColor(DisplayDriver::YELLOW); - unsigned long elapsed = millis() / 300; - const char* ring = (elapsed % 2 == 0) ? "RINGING" : ""; - uint16_t ringW = display.getTextWidth("RINGING"); - display.setCursor((display.width() - ringW) / 2, centreY + 36); - display.print(ring); - - // 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); - display.print("Ent:Answer"); - const char* rt = "Q:Reject"; - display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); - display.print(rt); - - return 500; // Fast refresh for flashing ring indicator - } - - // ---- In-call ---- - int renderInCall(DisplayDriver& display) { - display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, 0); - display.print("In Call"); - - renderSignalIndicator(display, display.width() - 2, 0); - - display.setColor(DisplayDriver::LIGHT); - display.drawRect(0, 11, display.width(), 1); - - int centreY = 20; - - // Contact name - char dispName[SMS_CONTACT_NAME_LEN]; - smsContacts.displayName(_callPhone, dispName, sizeof(dispName)); - - display.setTextSize(1); - display.setColor(DisplayDriver::LIGHT); - uint16_t nameW = display.getTextWidth(dispName); - display.setCursor((display.width() - nameW) / 2, centreY); - display.print(dispName); - - // Phone number (if name differs) - if (strcmp(dispName, _callPhone) != 0) { - display.setTextSize(0); - display.setColor(DisplayDriver::LIGHT); - uint16_t phoneW = display.getTextWidth(_callPhone); - display.setCursor((display.width() - phoneW) / 2, centreY + 16); - display.print(_callPhone); - } - - // Call duration timer - display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); - uint32_t durSec = 0; - if (_callConnectedMillis > 0) { - durSec = (millis() - _callConnectedMillis) / 1000; - } - char timerStr[12]; - snprintf(timerStr, sizeof(timerStr), "%02d:%02d", (int)(durSec / 60), (int)(durSec % 60)); - uint16_t timerW = display.getTextWidth(timerStr); - display.setCursor((display.width() - timerW) / 2, centreY + 40); - display.print(timerStr); - - // Volume indicator - display.setTextSize(0); - display.setColor(DisplayDriver::LIGHT); - char volStr[16]; - snprintf(volStr, sizeof(volStr), "Vol: %d/5", _callVolume); - display.setCursor(0, centreY + 60); - display.print(volStr); - - // 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); - display.print("Ent:Hang"); - const char* mid = "W/S:Vol"; - display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); - display.print(mid); - const char* rt = "0-9:DTMF"; - display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); - display.print(rt); - - return 1000; // 1s refresh for call timer - } - // ========================================================================= // INPUT HANDLING // ========================================================================= bool handleInput(char c) override { switch (_view) { - case INBOX: return handleInboxInput(c); - case CONVERSATION: return handleConversationInput(c); - case COMPOSE: return handleComposeInput(c); - case CONTACTS: return handleContactsInput(c); - case EDIT_CONTACT: return handleEditContactInput(c); - case DIALING: return handleDialingInput(c); - case INCOMING_CALL: return handleIncomingCallInput(c); - case IN_CALL: return handleInCallInput(c); + case APP_MENU: return handleAppMenuInput(c); + case INBOX: return handleInboxInput(c); + case CONVERSATION: return handleConversationInput(c); + case COMPOSE: return handleComposeInput(c); + case CONTACTS: return handleContactsInput(c); + case EDIT_CONTACT: return handleEditContactInput(c); + case PHONE_DIALER: return handlePhoneDialerInput(c); } return false; } + // ---- App menu input ---- + bool handleAppMenuInput(char c) { + switch (c) { + case 'w': case 'W': + _menuCursor = 0; + return true; + + case 's': case 'S': + _menuCursor = 1; + return true; + + case '\r': // Enter - select menu item + if (_menuCursor == 0) { + // Phone dialer + _phoneInputBuf[0] = '\0'; + _phoneInputPos = 0; + _view = PHONE_DIALER; + } else { + // SMS Inbox + if (_sdReady) refreshInbox(); + _inboxCursor = 0; + _inboxScrollTop = 0; + _view = INBOX; + } + return true; + + case 'q': case 'Q': // Back to home (handled by main.cpp) + return false; + + default: + return false; + } + } + + // ---- Phone dialer input ---- + bool handlePhoneDialerInput(char c) { + switch (c) { + case '\r': // Enter - place call + if (_phoneInputPos > 0) { + _phoneInputBuf[_phoneInputPos] = '\0'; + modemManager.dialCall(_phoneInputBuf); + // TODO: transition to DIALING/IN_CALL view when implemented + } + return true; + + case '\b': // Backspace + if (_phoneInputPos > 0) { + _phoneInputPos--; + _phoneInputBuf[_phoneInputPos] = '\0'; + } + return true; + + case 'q': case 'Q': // Back to app menu + _phoneInputBuf[0] = '\0'; + _phoneInputPos = 0; + _view = APP_MENU; + return true; + + default: + // Accept phone number characters + if (_phoneInputPos < SMS_PHONE_LEN - 1 && + ((c >= '0' && c <= '9') || c == '+' || c == '*' || c == '#')) { + _phoneInputBuf[_phoneInputPos++] = c; + _phoneInputBuf[_phoneInputPos] = '\0'; + } + return true; + } + } + // ---- Inbox input ---- bool handleInboxInput(char c) { switch (c) { @@ -913,8 +890,10 @@ public: _view = CONTACTS; return true; - case 'q': case 'Q': // Back to home (handled by main.cpp) - return false; + case 'q': case 'Q': // Back to app menu + _view = APP_MENU; + _menuCursor = 0; + return true; default: return false; @@ -941,9 +920,10 @@ public: _view = COMPOSE; return true; - case 'f': case 'F': // Call this contact - if (modemManager.isReady() && _activePhone[0]) { - startCall(_activePhone); + case 'f': case 'F': // Call this number + if (_activePhone[0] != '\0') { + modemManager.dialCall(_activePhone); + // TODO: transition to DIALING/IN_CALL view when implemented } return true; @@ -1025,7 +1005,7 @@ public: } } - // ---- Phone number input ---- + // ---- Phone number input (for compose) ---- bool handlePhoneInput(char c) { switch (c) { case '\r': // Done entering phone, move to body @@ -1089,9 +1069,10 @@ public: return true; case 'f': case 'F': // Call selected contact - if (cnt > 0 && _contactsCursor < cnt && modemManager.isReady()) { + if (cnt > 0 && _contactsCursor < cnt) { const SMSContact& ct = smsContacts.get(_contactsCursor); - startCall(ct.phone); + modemManager.dialCall(ct.phone); + // TODO: transition to DIALING/IN_CALL view when implemented } return true; @@ -1146,70 +1127,6 @@ public: return true; } } - - // ========================================================================= - // VOICE CALL INPUT HANDLERS - // ========================================================================= - - // ---- Dialing input ---- - bool handleDialingInput(char c) { - switch (c) { - case '\r': // Enter — hangup / cancel dial - case 'q': case 'Q': - modemManager.hangupCall(); - return true; - - default: - return true; // Absorb all keys during dialing - } - } - - // ---- Incoming call input ---- - bool handleIncomingCallInput(char c) { - switch (c) { - case '\r': // Enter — answer call - modemManager.answerCall(); - return true; - - case 'q': case 'Q': // Reject call - modemManager.hangupCall(); - return true; - - default: - return true; // Absorb all keys - } - } - - // ---- In-call input ---- - bool handleInCallInput(char c) { - switch (c) { - case '\r': // Enter — hangup - case 'q': case 'Q': - modemManager.hangupCall(); - return true; - - case 'w': case 'W': // Volume up - if (_callVolume < 5) { - _callVolume++; - modemManager.setCallVolume(_callVolume); - } - return true; - - case 's': case 'S': // Volume down - if (_callVolume > 0) { - _callVolume--; - modemManager.setCallVolume(_callVolume); - } - return true; - - default: - // 0-9, *, # — send as DTMF - if ((c >= '0' && c <= '9') || c == '*' || c == '#') { - modemManager.sendDTMF(c); - } - return true; // Absorb all keys during call - } - } }; #endif // SMS_SCREEN_H