diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 46b2255d..3a47289f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,7 +8,7 @@ #define FIRMWARE_VER_CODE 11 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "18 July 2026" +#define FIRMWARE_BUILD_DATE "25 July 2026" #endif #ifndef FIRMWARE_VERSION diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9a576aba..256dca04 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1312,8 +1312,8 @@ static void lastHeardToggleContact() { // Home screen FIRST page: tile taps (virtual coordinate hit test) if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) { -#if defined(LilyGo_TDeck_Pro_Max) - // MAX: 2-column tile grid, 6 paired rows + full-width Phone row. +#if defined(LilyGo_TDeck_Pro) + // T-Deck Pro: 2-column tile grid, 6 paired rows + full-width Phone row. // Hit-tested in RAW PHYSICAL pixels (240x320) using the untranslated // touch coords -- geometry must stay in sync with the render block in // UITask.cpp. @@ -1413,7 +1413,7 @@ static void lastHeardToggleContact() { } // Tap outside tiles — left half backward, right half forward return (vx < 64) ? (char)KEY_PREV : (char)KEY_NEXT; -#endif // LilyGo_TDeck_Pro_Max +#endif // LilyGo_TDeck_Pro } // Home screen (non-tile pages): left half taps backward, right half forward @@ -3358,6 +3358,13 @@ void loop() { ui_task.showAlert(alertBuf, 3000); ui_task.notify(UIEventType::contactMessage); + // An incoming call drops the lock screen so the call can be answered. + // Keyboard and touch input are both blocked while locked, which would + // otherwise leave the ringing screen unable to answer or reject. + // The device stays unlocked after the call ends. + if (ui_task.isLocked()) { + ui_task.unlockScreen(); + } if (!smsMode) { ui_task.gotoSMSScreen(); } diff --git a/examples/companion_radio/ui-new/SMSScreen.h b/examples/companion_radio/ui-new/SMSScreen.h index b770a6e3..0fa903ff 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 }; + DIALING_OUT, INCOMING_CALL, IN_CALL, CALL_LOG }; private: UITask* _task; @@ -56,7 +56,15 @@ private: SubView _view; // App menu state - int _menuCursor; // 0 = Phone, 1 = SMS Inbox + int _menuCursor; // 0 = Dial, 1 = SMS Inbox, 2 = Call Log + + // Call log state + CallLogRecord _callLog[SMS_CALLLOG_MAX]; + int _callLogCount; + int _callLogCursor; + int _callLogScrollTop; + int _unseenMissed; // Missed calls not yet viewed in the log (menu badge) + bool _callWasIncoming; // Direction of the active/last call, for log entries // Inbox state SMSConversation _conversations[SMS_MAX_CONVERSATIONS]; @@ -108,6 +116,14 @@ private: bool _sdReady; // Reload helpers + void refreshCallLog() { + _callLogCount = smsStore.loadCallLog(_callLog, SMS_CALLLOG_MAX); + _unseenMissed = 0; + for (int i = 0; i < _callLogCount; i++) { + if (_callLog[i].type == CALL_LOG_MISSED && _callLog[i].seen == 0) _unseenMissed++; + } + } + void refreshInbox() { _convCount = smsStore.loadConversations(_conversations, SMS_MAX_CONVERSATIONS); } @@ -129,6 +145,8 @@ public: , _contactsCursor(0), _contactsScrollTop(0) , _editNamePos(0), _editIsNew(false), _editReturnView(INBOX) , _callReturnView(APP_MENU), _callConnectTime(0), _callVolume(3), _callDotAnim(0) + , _callLogCount(0), _callLogCursor(0), _callLogScrollTop(0), _unseenMissed(0) + , _callWasIncoming(false) , _needsRefresh(false), _lastRefresh(0) , _sdReady(false) { @@ -141,12 +159,32 @@ public: memset(_callPhone, 0, sizeof(_callPhone)); } - void setSDReady(bool ready) { _sdReady = ready; } + void setSDReady(bool ready) { + _sdReady = ready; + if (_sdReady) { + // Load unread/unseen state now so the lock screen shows the right + // counts before the phone screen has ever been opened + refreshInbox(); + refreshCallLog(); + } + } + + // Live counts for the lock screen badge. Both clear only when the relevant + // conversation or the call log is actually opened. + int getUnreadSmsCount() const { + int n = 0; + for (int i = 0; i < _convCount; i++) n += _conversations[i].unreadCount; + return n; + } + int getUnseenMissedCount() const { return _unseenMissed; } void activate() { _view = APP_MENU; _menuCursor = 0; - if (_sdReady) refreshInbox(); + if (_sdReady) { + refreshInbox(); + refreshCallLog(); + } } SubView getSubView() const { return _view; } @@ -164,6 +202,7 @@ public: _callConnectTime = 0; _callVolume = 3; _callDotAnim = 0; + _callWasIncoming = false; _view = DIALING_OUT; modemManager.dialCall(phone); } @@ -177,6 +216,7 @@ public: _callPhone[SMS_PHONE_LEN - 1] = '\0'; _callConnectTime = 0; _callVolume = 3; + _callWasIncoming = true; if (!isInCallView()) { _callReturnView = _view; } @@ -193,6 +233,10 @@ public: case CallEventType::ENDED: Serial.printf("[SMSScreen] Call ended (%lus)\n", (unsigned long)evt.duration); + if (_sdReady) { + smsStore.appendCallLog(_callWasIncoming ? CALL_LOG_INCOMING : CALL_LOG_OUTGOING, + evt.phone, evt.duration, (uint32_t)time(nullptr)); + } if (_view == IN_CALL || _view == DIALING_OUT) { // Remote hangup or network drop — return to previous view // "Call Ended" alert is shown by main.cpp via showAlert() @@ -200,28 +244,41 @@ public: } _callPhone[0] = '\0'; _callConnectTime = 0; + if (_view == CALL_LOG && _sdReady) refreshCallLog(); _needsRefresh = true; break; case CallEventType::MISSED: Serial.printf("[SMSScreen] Missed call from %s\n", evt.phone); + if (_sdReady) { + smsStore.appendCallLog(CALL_LOG_MISSED, evt.phone, 0, (uint32_t)time(nullptr)); + } _view = _callReturnView; _callPhone[0] = '\0'; _callConnectTime = 0; + if (_sdReady) refreshCallLog(); _needsRefresh = true; break; case CallEventType::BUSY: Serial.printf("[SMSScreen] Busy: %s\n", evt.phone); + if (_sdReady) { + smsStore.appendCallLog(CALL_LOG_OUTGOING, evt.phone, 0, (uint32_t)time(nullptr)); + } _view = _callReturnView; _callPhone[0] = '\0'; + if (_view == CALL_LOG && _sdReady) refreshCallLog(); _needsRefresh = true; break; case CallEventType::NO_ANSWER: Serial.printf("[SMSScreen] No answer: %s\n", evt.phone); + if (_sdReady) { + smsStore.appendCallLog(CALL_LOG_OUTGOING, evt.phone, 0, (uint32_t)time(nullptr)); + } _view = _callReturnView; _callPhone[0] = '\0'; + if (_view == CALL_LOG && _sdReady) refreshCallLog(); _needsRefresh = true; break; @@ -243,9 +300,9 @@ public: smsStore.markConversationRead(_activePhone); refreshConversation(); } - if (_view == INBOX || _view == APP_MENU) { - refreshInbox(); - } + // Refresh regardless of the current view so the unread total stays + // correct for the lock screen badge + if (_sdReady) refreshInbox(); _needsRefresh = true; } @@ -301,6 +358,7 @@ public: switch (_view) { case APP_MENU: return renderAppMenu(display); + case CALL_LOG: return renderCallLog(display); case INBOX: return renderInbox(display); case CONVERSATION: return renderConversation(display); case COMPOSE: return renderCompose(display); @@ -338,7 +396,7 @@ public: display.setColor(_menuCursor == 0 ? DisplayDriver::GREEN : DisplayDriver::LIGHT); if (_menuCursor == 0) display.print("> "); else display.print(" "); - display.print("Phone"); + display.print("Dial"); y += lineHeight; @@ -359,6 +417,23 @@ 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()); @@ -387,7 +462,7 @@ public: display.setCursor(0, footerY); display.print("Q:Back"); 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); if (ms != ModemState::READY && ms != ModemState::SENDING_SMS) { @@ -533,6 +608,138 @@ public: } // ---- Inbox ---- + // ---- Call log ---- + int renderCallLog(DisplayDriver& display) { + // Header + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, 0); + display.print("Call Log"); + + // Signal strength at top-right + renderSignalIndicator(display, display.width() - 2, 0); + + display.setColor(DisplayDriver::LIGHT); + display.drawRect(0, 11, display.width(), 1); + + if (_callLogCount == 0) { + display.setTextSize(_prefs->smallTextSize()); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, 20); + display.print("No calls"); + display.setTextSize(1); + } else { + display.setTextSize(_prefs->smallTextSize()); + int lineHeight = _prefs->smallLineH() + 1; + int y = 14; + + int visibleCount = (display.height() - 14 - 14) / (lineHeight * 2 + 2); + if (visibleCount < 1) visibleCount = 1; + + // Adjust scroll to keep cursor visible + if (_callLogCursor < _callLogScrollTop) _callLogScrollTop = _callLogCursor; + if (_callLogCursor >= _callLogScrollTop + visibleCount) { + _callLogScrollTop = _callLogCursor - visibleCount + 1; + } + + for (int vi = 0; vi < visibleCount && (_callLogScrollTop + vi) < _callLogCount; vi++) { + int idx = _callLogScrollTop + vi; + CallLogRecord& e = _callLog[idx]; + + bool selected = (idx == _callLogCursor); + + // Resolve contact name (shows name if saved, phone otherwise) + char dispName[SMS_CONTACT_NAME_LEN]; + if (e.phone[0]) { + smsContacts.displayName(e.phone, dispName, sizeof(dispName)); + } else { + strncpy(dispName, "Unknown", sizeof(dispName) - 1); + dispName[sizeof(dispName) - 1] = '\0'; + } + + display.setCursor(0, y); + display.setColor(selected ? DisplayDriver::GREEN : DisplayDriver::LIGHT); + if (selected) display.print("> "); + display.print(dispName); + y += lineHeight; + + // Detail line: type, duration for connected calls, local date/time + const char* typeStr = (e.type == CALL_LOG_MISSED) ? "Missed" + : (e.type == CALL_LOG_INCOMING) ? "In" : "Out"; + int32_t local = (int32_t)e.timestamp + ((int32_t)_prefs->utc_offset_hours * 3600); + time_t lt = (time_t)local; + struct tm tmv; + gmtime_r(<, &tmv); + char detail[36]; + if (e.type != CALL_LOG_MISSED && e.duration > 0) { + snprintf(detail, sizeof(detail), "%s %lu:%02lu %02d/%02d %02d:%02d", + typeStr, (unsigned long)(e.duration / 60), (unsigned long)(e.duration % 60), + tmv.tm_mday, tmv.tm_mon + 1, tmv.tm_hour, tmv.tm_min); + } else { + snprintf(detail, sizeof(detail), "%s %02d/%02d %02d:%02d", + typeStr, tmv.tm_mday, tmv.tm_mon + 1, tmv.tm_hour, tmv.tm_min); + } + display.setColor(DisplayDriver::LIGHT); + display.setCursor(12, y); + display.print(detail); + y += lineHeight + 2; + } + 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* mid = "D:Del"; + display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY); + display.print(mid); + const char* rt = "Ent:Dial"; + display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY); + display.print(rt); + + return 5000; + } + + bool handleCallLogInput(char c) { + switch (c) { + case 'w': case 'W': + if (_callLogCursor > 0) _callLogCursor--; + return true; + + case 's': case 'S': + if (_callLogCursor < _callLogCount - 1) _callLogCursor++; + return true; + + case '\r': // Enter - dial selected entry + if (_callLogCount > 0 && _callLogCursor < _callLogCount + && _callLog[_callLogCursor].phone[0]) { + startCall(_callLog[_callLogCursor].phone); + } + return true; + + case 'd': case 'D': // Delete selected entry + if (_callLogCount > 0 && _callLogCursor < _callLogCount && _sdReady) { + smsStore.deleteCallLogEntry(_callLogCursor); + refreshCallLog(); + if (_callLogCursor >= _callLogCount && _callLogCursor > 0) _callLogCursor--; + } + return true; + + case 'q': + case KEY_CANCEL: // Back to app menu + _view = APP_MENU; + _menuCursor = 2; + return true; + + default: + return false; + } + } + int renderInbox(DisplayDriver& display) { ModemState ms = modemManager.getState(); @@ -1122,6 +1329,7 @@ public: bool handleInput(char c) override { switch (_view) { case APP_MENU: return handleAppMenuInput(c); + case CALL_LOG: return handleCallLogInput(c); case INBOX: return handleInboxInput(c); case CONVERSATION: return handleConversationInput(c); case COMPOSE: return handleComposeInput(c); @@ -1139,11 +1347,11 @@ public: bool handleAppMenuInput(char c) { switch (c) { case 'w': case 'W': - _menuCursor = 0; + if (_menuCursor > 0) _menuCursor--; return true; case 's': case 'S': - _menuCursor = 1; + if (_menuCursor < 2) _menuCursor++; return true; case '\r': // Enter - select menu item @@ -1152,12 +1360,21 @@ public: _phoneInputBuf[0] = '\0'; _phoneInputPos = 0; _view = PHONE_DIALER; - } else { + } else if (_menuCursor == 1) { // SMS Inbox if (_sdReady) refreshInbox(); _inboxCursor = 0; _inboxScrollTop = 0; _view = INBOX; + } else { + // Call Log + if (_sdReady) { + smsStore.markMissedSeen(); + refreshCallLog(); + } + _callLogCursor = 0; + _callLogScrollTop = 0; + _view = CALL_LOG; } return true; diff --git a/examples/companion_radio/ui-new/SMSStore.cpp b/examples/companion_radio/ui-new/SMSStore.cpp index 0275767c..b6b9a4e9 100644 --- a/examples/companion_radio/ui-new/SMSStore.cpp +++ b/examples/companion_radio/ui-new/SMSStore.cpp @@ -271,4 +271,149 @@ void SMSStore::migrateExistingAsRead() { digitalWrite(SDCARD_CS, HIGH); } +bool SMSStore::appendCallLog(uint8_t type, const char* phone, uint32_t duration, uint32_t timestamp) { + if (!_ready) return false; + + CallLogRecord rec; + memset(&rec, 0, sizeof(rec)); + rec.timestamp = timestamp; + rec.duration = duration; + rec.type = type; + strncpy(rec.phone, phone, SMS_PHONE_LEN - 1); + + File f = SD.open(SMS_CALLLOG_FILE, FILE_APPEND); + if (!f) { + // Try creating + f = SD.open(SMS_CALLLOG_FILE, FILE_WRITE); + if (!f) { + MESH_DEBUG_PRINTLN("[SMSStore] can't open %s", SMS_CALLLOG_FILE); + digitalWrite(SDCARD_CS, HIGH); + return false; + } + } + + size_t written = f.write((uint8_t*)&rec, sizeof(rec)); + f.close(); + + // Drop oldest records once the file exceeds the cap + trimCallLog(); + + // Release SD CS + digitalWrite(SDCARD_CS, HIGH); + + return written == sizeof(rec); +} + +void SMSStore::trimCallLog() { + File f = SD.open(SMS_CALLLOG_FILE, FILE_READ); + if (!f) return; + + int numRecords = f.size() / sizeof(CallLogRecord); + if (numRecords <= SMS_CALLLOG_MAX) { f.close(); return; } + + int startIdx = numRecords - SMS_CALLLOG_MAX; + File t = SD.open(SMS_CALLLOG_TMP, FILE_WRITE); + if (!t) { f.close(); return; } + + CallLogRecord rec; + for (int i = startIdx; i < numRecords; i++) { + f.seek((size_t)i * sizeof(CallLogRecord)); + if (f.read((uint8_t*)&rec, sizeof(CallLogRecord)) != sizeof(CallLogRecord)) continue; + t.write((uint8_t*)&rec, sizeof(CallLogRecord)); + } + f.close(); + t.close(); + + SD.remove(SMS_CALLLOG_FILE); + SD.rename(SMS_CALLLOG_TMP, SMS_CALLLOG_FILE); +} + +int SMSStore::loadCallLog(CallLogRecord* out, int maxCount) { + if (!_ready) return 0; + + File f = SD.open(SMS_CALLLOG_FILE, FILE_READ); + if (!f) return 0; + + int numRecords = f.size() / sizeof(CallLogRecord); + + // Newest first + CallLogRecord rec; + int outIdx = 0; + for (int i = numRecords - 1; i >= 0 && outIdx < maxCount; i--) { + f.seek((size_t)i * sizeof(CallLogRecord)); + if (f.read((uint8_t*)&rec, sizeof(CallLogRecord)) != sizeof(CallLogRecord)) continue; + out[outIdx++] = rec; + } + + f.close(); + + digitalWrite(SDCARD_CS, HIGH); + + return outIdx; +} + +bool SMSStore::deleteCallLogEntry(int newestFirstIdx) { + if (!_ready) return false; + + File f = SD.open(SMS_CALLLOG_FILE, FILE_READ); + if (!f) return false; + + int numRecords = f.size() / sizeof(CallLogRecord); + int fileIdx = numRecords - 1 - newestFirstIdx; + if (fileIdx < 0 || fileIdx >= numRecords) { + f.close(); + digitalWrite(SDCARD_CS, HIGH); + return false; + } + + File t = SD.open(SMS_CALLLOG_TMP, FILE_WRITE); + if (!t) { + f.close(); + digitalWrite(SDCARD_CS, HIGH); + return false; + } + + CallLogRecord rec; + for (int i = 0; i < numRecords; i++) { + if (i == fileIdx) continue; + f.seek((size_t)i * sizeof(CallLogRecord)); + if (f.read((uint8_t*)&rec, sizeof(CallLogRecord)) != sizeof(CallLogRecord)) continue; + t.write((uint8_t*)&rec, sizeof(CallLogRecord)); + } + f.close(); + t.close(); + + SD.remove(SMS_CALLLOG_FILE); + bool ok = SD.rename(SMS_CALLLOG_TMP, SMS_CALLLOG_FILE); + + digitalWrite(SDCARD_CS, HIGH); + + return ok; +} + +void SMSStore::markMissedSeen() { + if (!_ready) return; + + // In-place flag update, same approach as markFileRead() + File f = SD.open(SMS_CALLLOG_FILE, "r+"); + if (!f) return; + + int numRecords = f.size() / sizeof(CallLogRecord); + + CallLogRecord rec; + for (int i = 0; i < numRecords; i++) { + f.seek((size_t)i * sizeof(CallLogRecord)); + if (f.read((uint8_t*)&rec, sizeof(CallLogRecord)) != sizeof(CallLogRecord)) continue; + if (rec.type == CALL_LOG_MISSED && rec.seen == 0) { + rec.seen = 1; + f.seek((size_t)i * sizeof(CallLogRecord)); + f.write((uint8_t*)&rec, sizeof(CallLogRecord)); + } + } + + f.close(); + + digitalWrite(SDCARD_CS, HIGH); +} + #endif // HAS_4G_MODEM \ No newline at end of file diff --git a/examples/companion_radio/ui-new/SMSStore.h b/examples/companion_radio/ui-new/SMSStore.h index 4ce9ac81..02dbafb6 100644 --- a/examples/companion_radio/ui-new/SMSStore.h +++ b/examples/companion_radio/ui-new/SMSStore.h @@ -24,6 +24,26 @@ #define SMS_DIR "/sms" #define SMS_READ_MIGRATED "/sms/rdmig.dat" // one-time read-state migration marker +// Call log: fixed-size records appended to a single file, oldest first +#define SMS_CALLLOG_FILE "/sms/calllog.dat" +#define SMS_CALLLOG_TMP "/sms/calllog.tmp" +#define SMS_CALLLOG_MAX 32 + +// Call log entry types +#define CALL_LOG_MISSED 0 +#define CALL_LOG_INCOMING 1 +#define CALL_LOG_OUTGOING 2 + +// On-SD call log record (fixed size for random access) +struct CallLogRecord { + uint32_t timestamp; // epoch seconds when the event was logged + uint32_t duration; // call duration in seconds (0 = never connected) + uint8_t type; // CALL_LOG_MISSED / _INCOMING / _OUTGOING + uint8_t seen; // 1 = missed call has been viewed in the log + uint8_t reserved[2]; + char phone[SMS_PHONE_LEN]; +}; + // Fixed-size on-disk record (256 bytes, easy alignment) struct SMSRecord { uint32_t timestamp; // epoch seconds @@ -78,9 +98,22 @@ public: // Mark all received messages in a conversation as read (persisted to SD) void markConversationRead(const char* phone); + // --- Call log (fixed-size records in /sms/calllog.dat) --- + // Append one entry; trims oldest once past SMS_CALLLOG_MAX + bool appendCallLog(uint8_t type, const char* phone, uint32_t duration, uint32_t timestamp); + // Load entries newest-first; returns count loaded + int loadCallLog(CallLogRecord* out, int maxCount); + // Delete one entry by newest-first index (as returned by loadCallLog) + bool deleteCallLogEntry(int newestFirstIdx); + // Mark every missed-call entry as seen (called when the log is opened) + void markMissedSeen(); + private: bool _ready = false; + // Rewrite the call log keeping only the newest SMS_CALLLOG_MAX records + void trimCallLog(); + // Convert phone number to safe filename void phoneToFilename(const char* phone, char* out, size_t outLen); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 90972ce6..0362c14d 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -23,7 +23,7 @@ #if defined(LilyGo_TDeck_Pro_Max) #include "DRV2605Haptic.h" // haptic motor for "Buzzer (vibrate)" channels #endif -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT) || defined(LilyGo_TDeck_Pro_Max) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT) || defined(LilyGo_TDeck_Pro) #include "HomeIcons.h" #endif #if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) @@ -368,7 +368,7 @@ public: int render(DisplayDriver& display) override { char tmp[80]; -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro_Max) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) _task->setHomeShowingTiles(false); // Reset — only set true on FIRST page #endif @@ -453,7 +453,7 @@ public: int y = 13; // Below header #elif defined(LilyGo_T5S3_EPaper_Pro) int y = 14; // Closer to header -#elif defined(LilyGo_TDeck_Pro_Max) +#elif defined(LilyGo_TDeck_Pro) int y = 8; // Tighter under header; frees room for the MSG strip above the tile grid #else int y = 14; @@ -468,11 +468,11 @@ public: } if (_page == HomePage::FIRST) { -#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro_Max) +#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro) _task->setHomeShowingTiles(true); #endif -#if defined(LilyGo_TDeck_Pro_Max) - // ----- MAX: Touch tile grid home screen (T-Watch / P4 style) ----- +#if defined(LilyGo_TDeck_Pro) + // ----- T-Deck Pro: Touch tile grid home screen (T-Watch / P4 style) ----- // Rendered in RAW PHYSICAL pixels (240x320) via GxEPDDisplay raw // helpers, bypassing the 128x128 virtual scaling so icons stay 1:1 // and the dithered grey borders read as continuous bands. @@ -561,7 +561,7 @@ public: } display.setTextSize(1); // restore driver font state after raw text } -#else // not LilyGo_TDeck_Pro_Max +#else // not LilyGo_TDeck_Pro #if defined(LilyGo_T5S3_EPaper_Pro) #if defined(BLE_PIN_CODE) || defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION) int y = 18; // Tighter spacing — connectivity info fills gap below dots @@ -848,7 +848,7 @@ public: display.setTextSize(1); // restore #endif // LILYGO_TECHO_LITE #endif -#endif // not LilyGo_TDeck_Pro_Max +#endif // not LilyGo_TDeck_Pro } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); display.setColor(DisplayDriver::GREEN); @@ -1455,6 +1455,31 @@ public: display.drawTextCentered(display.width() / 2, 108, infoBuf); } +#ifdef HAS_4G_MODEM + // ---- Unread SMS / missed-call notification ---- + // Sourced from the SMS screen's own unread and unseen state, so it + // persists across re-locks and reboots until the conversation or the + // call log is actually opened. + { + SMSScreen* smsScr = _task->getSMSScreen(); + int sms = smsScr ? smsScr->getUnreadSmsCount() : 0; + int missed = smsScr ? smsScr->getUnseenMissedCount() : 0; + if (sms > 0 || missed > 0) { + char notifBuf[32]; + if (sms > 0 && missed > 0) { + sprintf(notifBuf, "SMS: %d Missed: %d", sms, missed); + } else if (sms > 0) { + sprintf(notifBuf, "SMS: %d", sms); + } else { + sprintf(notifBuf, "Missed: %d", missed); + } + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.drawTextCentered(display.width() / 2, display.height() - 12, notifBuf); + } + } +#endif + // ---- Unlock hint ---- #if defined(LilyGo_T5S3_EPaper_Pro) display.setTextSize(_node_prefs->smallTextSize()); @@ -3206,7 +3231,11 @@ void UITask::gotoVoiceScreen() { #ifdef HAS_4G_MODEM void UITask::gotoSMSScreen() { SMSScreen* smsScr = (SMSScreen*)sms_screen; - smsScr->activate(); + // activate() resets the view to the app menu, which would wipe the ringing + // or in-call screen that onCallEvent() has just put up. Skip it mid-call. + if (!smsScr->isInCallView()) { + smsScr->activate(); + } setCurrScreen(sms_screen); if (_display != NULL && !_display->isOn()) { _display->turnOn();