mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-08 01:32:42 +02:00
New Max home UI on all T-Deck Pro builds; Call Log; lock screen notifications
Home UI
- Widen the tile-grid home screen (e1300190) from Max-only to all T-Deck Pro
builds: guard flips from LilyGo_TDeck_Pro_Max to LilyGo_TDeck_Pro at five
sites in UITask.cpp (HomeIcons include, tile-flag set/reset, page-dot
offset, tile render block) and at the touch hit-test in main.cpp. Max
defines both flags, so Max behaviour is unchanged -- verified by diffing
both files preprocessed with the Max define set.
Call Log (HAS_4G_MODEM)
- New CALL_LOG subview; app menu is now Dial / SMS Inbox / Call Log.
- SMSStore gains a fixed-size CallLogRecord store in /sms/calllog.dat
(timestamp, duration, type, seen, phone), capped at 32 entries with the
oldest trimmed on append: appendCallLog, loadCallLog (newest first),
deleteCallLogEntry (rewrite via tmp + rename) and markMissedSeen
(in-place flag update). Same SD idioms as the SMS store.
- Logs incoming and outgoing calls with duration, plus missed, busy and
no-answer attempts. Calls arriving with no caller ID are logged and shown
as "Unknown" rather than dropped.
- List mirrors inbox conventions: contact name via SMSContacts, detail line
with type, duration and local date/time. Enter dials, D deletes, Q backs
out. Menu row carries an unseen-missed [n] badge matching SMS Inbox.
Lock screen (HAS_4G_MODEM)
- Show outstanding unread SMS and unseen missed calls at height()-12 with
setTextSize(1). Counts derive from the inbox read flags and call log seen
flags, so they persist across re-locks and reboots and clear only when the
conversation or the Call Log is opened. State loads at boot in
setSDReady().
Phone screen fixes
- An incoming call now drops the lock screen. Keyboard and touch input are
both blocked while locked, which left the ringing screen unable to answer
or reject. The device stays unlocked afterwards.
- gotoSMSScreen() no longer calls activate() mid-call, which was resetting
the view to the app menu and wiping the ringing screen.
- Widen the app menu footer right margin so "Ent:Open" is not clipped.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user