added extra phone screen to allow phone or sms inbox selection to enabling dialing of non-contact numbers

This commit is contained in:
pelgraine
2026-02-25 20:26:05 +11:00
parent 049017cd2d
commit 7f03d6fbea
2 changed files with 296 additions and 379 deletions

View File

@@ -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;

View File

@@ -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
// CONTACTSbrowsable 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
// COMPOSEtext 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