mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Sms app implementation phase 2 - add contact in message view screen; time of message displayed fix using 4G modem network sync - need to wait about 10-ish seconds after boot for auto network clock sync
This commit is contained in:
@@ -63,6 +63,7 @@
|
||||
#ifdef HAS_4G_MODEM
|
||||
#include "ModemManager.h"
|
||||
#include "SMSStore.h"
|
||||
#include "SMSContacts.h"
|
||||
#include "SMSScreen.h"
|
||||
static bool smsMode = false;
|
||||
#endif
|
||||
@@ -553,6 +554,7 @@ void setup() {
|
||||
#ifdef HAS_4G_MODEM
|
||||
{
|
||||
smsStore.begin();
|
||||
smsContacts.begin();
|
||||
|
||||
// Tell SMS screen that SD is ready
|
||||
SMSScreen* smsScr = (SMSScreen*)ui_task.getSMSScreen();
|
||||
@@ -560,9 +562,17 @@ void setup() {
|
||||
smsScr->setSDReady(true);
|
||||
}
|
||||
|
||||
// Start modem background task (runs on Core 0)
|
||||
modemManager.begin();
|
||||
MESH_DEBUG_PRINTLN("setup() - 4G modem manager started");
|
||||
// Start modem if enabled in config (default = enabled)
|
||||
bool modemEnabled = ModemManager::loadEnabledConfig();
|
||||
if (modemEnabled) {
|
||||
modemManager.begin();
|
||||
MESH_DEBUG_PRINTLN("setup() - 4G modem manager started");
|
||||
} else {
|
||||
// Ensure modem power is off (kills red LED too)
|
||||
pinMode(MODEM_POWER_EN, OUTPUT);
|
||||
digitalWrite(MODEM_POWER_EN, LOW);
|
||||
MESH_DEBUG_PRINTLN("setup() - 4G modem disabled by config");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "ModemManager.h"
|
||||
#include <Mesh.h> // For MESH_DEBUG_PRINTLN
|
||||
#include <SD.h> // For modem config persistence
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
// Global singleton
|
||||
ModemManager modemManager;
|
||||
@@ -100,6 +103,35 @@ const char* ModemManager::stateToString(ModemState s) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistent modem enable/disable config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define MODEM_CONFIG_FILE "/sms/modem.cfg"
|
||||
|
||||
bool ModemManager::loadEnabledConfig() {
|
||||
File f = SD.open(MODEM_CONFIG_FILE, FILE_READ);
|
||||
if (!f) {
|
||||
// No config file = enabled by default
|
||||
return true;
|
||||
}
|
||||
char c = '1';
|
||||
if (f.available()) c = f.read();
|
||||
f.close();
|
||||
return (c != '0');
|
||||
}
|
||||
|
||||
void ModemManager::saveEnabledConfig(bool enabled) {
|
||||
// Ensure /sms directory exists
|
||||
if (!SD.exists("/sms")) SD.mkdir("/sms");
|
||||
File f = SD.open(MODEM_CONFIG_FILE, FILE_WRITE);
|
||||
if (f) {
|
||||
f.print(enabled ? '1' : '0');
|
||||
f.close();
|
||||
Serial.printf("[Modem] Config saved: %s\n", enabled ? "ENABLED" : "DISABLED");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FreeRTOS Task
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -153,6 +185,9 @@ restart:
|
||||
// Enable SMS notification via +CMTI URC (new message indication)
|
||||
sendAT("AT+CNMI=2,1,0,0,0", "OK");
|
||||
|
||||
// Enable automatic time zone update from network (needed for AT+CCLK)
|
||||
sendAT("AT+CTZU=1", "OK");
|
||||
|
||||
// ---- Phase 3: Wait for network registration ----
|
||||
_state = ModemState::REGISTERING;
|
||||
MESH_DEBUG_PRINTLN("[Modem] waiting for network registration...");
|
||||
@@ -203,6 +238,51 @@ restart:
|
||||
// Initial signal query
|
||||
pollCSQ();
|
||||
|
||||
// Sync ESP32 system clock from modem network time
|
||||
// Network time may take a few seconds to arrive after registration
|
||||
bool clockSet = false;
|
||||
for (int attempt = 0; attempt < 5 && !clockSet; attempt++) {
|
||||
if (attempt > 0) vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
if (sendAT("AT+CCLK?", "OK", 3000)) {
|
||||
// Response: +CCLK: "YY/MM/DD,HH:MM:SS±TZ" (TZ in quarter-hours)
|
||||
char* p = strstr(_atBuf, "+CCLK:");
|
||||
if (p) {
|
||||
int yy = 0, mo = 0, dd = 0, hh = 0, mm = 0, ss = 0, tz = 0;
|
||||
if (sscanf(p, "+CCLK: \"%d/%d/%d,%d:%d:%d", &yy, &mo, &dd, &hh, &mm, &ss) >= 6) {
|
||||
// Skip if modem clock not synced (default is 1970 = yy 70, or yy 0)
|
||||
if (yy < 24 || yy > 50) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] CCLK not synced yet (yy=%d), retrying...", yy);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse timezone offset (e.g. "+40" = UTC+10 in quarter-hours)
|
||||
char* tzp = p + 7; // skip "+CCLK: "
|
||||
while (*tzp && *tzp != '+' && *tzp != '-') tzp++;
|
||||
if (*tzp) tz = atoi(tzp);
|
||||
|
||||
struct tm t = {};
|
||||
t.tm_year = yy + 100; // years since 1900
|
||||
t.tm_mon = mo - 1; // 0-based
|
||||
t.tm_mday = dd;
|
||||
t.tm_hour = hh;
|
||||
t.tm_min = mm;
|
||||
t.tm_sec = ss;
|
||||
time_t epoch = mktime(&t); // treats input as UTC (no TZ set on ESP32)
|
||||
epoch -= (tz * 15 * 60); // subtract local offset to get real UTC
|
||||
|
||||
struct timeval tv = { .tv_sec = epoch, .tv_usec = 0 };
|
||||
settimeofday(&tv, nullptr);
|
||||
clockSet = true;
|
||||
MESH_DEBUG_PRINTLN("[Modem] System clock set: %04d-%02d-%02d %02d:%02d:%02d (tz=%+d qh, epoch=%lu)",
|
||||
yy + 2000, mo, dd, hh, mm, ss, tz, (unsigned long)epoch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!clockSet) {
|
||||
MESH_DEBUG_PRINTLN("[Modem] WARNING: Could not sync system clock from network");
|
||||
}
|
||||
|
||||
// Delete any stale SMS on SIM to free slots
|
||||
sendAT("AT+CMGD=1,4", "OK", 5000); // Delete all read messages
|
||||
|
||||
@@ -417,7 +497,7 @@ void ModemManager::pollIncomingSMS() {
|
||||
if (bodyLen >= SMS_BODY_LEN) bodyLen = SMS_BODY_LEN - 1;
|
||||
memcpy(incoming.body, p, bodyLen);
|
||||
incoming.body[bodyLen] = '\0';
|
||||
incoming.timestamp = millis() / 1000; // Approximate; modem RTC could be used
|
||||
incoming.timestamp = (uint32_t)time(nullptr); // Real epoch from modem-synced clock
|
||||
|
||||
// Queue for main loop
|
||||
xQueueSend(_recvQueue, &incoming, 0);
|
||||
|
||||
@@ -89,6 +89,10 @@ public:
|
||||
|
||||
static const char* stateToString(ModemState s);
|
||||
|
||||
// Persistent enable/disable config (SD file /sms/modem.cfg)
|
||||
static bool loadEnabledConfig(); // returns true if enabled (default)
|
||||
static void saveEnabledConfig(bool enabled);
|
||||
|
||||
private:
|
||||
volatile ModemState _state = ModemState::OFF;
|
||||
volatile int _csq = 99; // 99 = unknown
|
||||
@@ -116,4 +120,4 @@ private:
|
||||
extern ModemManager modemManager;
|
||||
|
||||
#endif // MODEM_MANAGER_H
|
||||
#endif // HAS_4G_MODEM
|
||||
#endif // HAS_4G_MODEM
|
||||
8
examples/companion_radio/ui-new/SMSContacts.cpp
Normal file
8
examples/companion_radio/ui-new/SMSContacts.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifdef HAS_4G_MODEM
|
||||
|
||||
#include "SMSContacts.h"
|
||||
|
||||
// Global singleton
|
||||
SMSContactStore smsContacts;
|
||||
|
||||
#endif // HAS_4G_MODEM
|
||||
176
examples/companion_radio/ui-new/SMSContacts.h
Normal file
176
examples/companion_radio/ui-new/SMSContacts.h
Normal file
@@ -0,0 +1,176 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// SMSContacts - Phone-to-name lookup for SMS contacts (4G variant)
|
||||
//
|
||||
// Stores contacts in /sms/contacts.txt on SD card.
|
||||
// Format: one contact per line as "phone=Display Name"
|
||||
//
|
||||
// Completely separate from mesh ContactInfo / IdentityStore.
|
||||
//
|
||||
// Guard: HAS_4G_MODEM
|
||||
// =============================================================================
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
|
||||
#ifndef SMS_CONTACTS_H
|
||||
#define SMS_CONTACTS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
|
||||
#define SMS_CONTACT_NAME_LEN 24
|
||||
#define SMS_CONTACT_MAX 30
|
||||
#define SMS_CONTACTS_FILE "/sms/contacts.txt"
|
||||
|
||||
struct SMSContact {
|
||||
char phone[20]; // matches SMS_PHONE_LEN
|
||||
char name[SMS_CONTACT_NAME_LEN];
|
||||
bool valid;
|
||||
};
|
||||
|
||||
class SMSContactStore {
|
||||
public:
|
||||
void begin() {
|
||||
_count = 0;
|
||||
memset(_contacts, 0, sizeof(_contacts));
|
||||
load();
|
||||
}
|
||||
|
||||
// Look up a name by phone number. Returns nullptr if not found.
|
||||
const char* lookup(const char* phone) const {
|
||||
for (int i = 0; i < _count; i++) {
|
||||
if (_contacts[i].valid && strcmp(_contacts[i].phone, phone) == 0) {
|
||||
return _contacts[i].name;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Fill buf with display name if found, otherwise copy phone number.
|
||||
// Returns true if a name was found.
|
||||
bool displayName(const char* phone, char* buf, size_t bufLen) const {
|
||||
const char* name = lookup(phone);
|
||||
if (name && name[0]) {
|
||||
strncpy(buf, name, bufLen - 1);
|
||||
buf[bufLen - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
strncpy(buf, phone, bufLen - 1);
|
||||
buf[bufLen - 1] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add or update a contact. Returns true on success.
|
||||
bool set(const char* phone, const char* name) {
|
||||
// Update existing
|
||||
for (int i = 0; i < _count; i++) {
|
||||
if (_contacts[i].valid && strcmp(_contacts[i].phone, phone) == 0) {
|
||||
strncpy(_contacts[i].name, name, SMS_CONTACT_NAME_LEN - 1);
|
||||
_contacts[i].name[SMS_CONTACT_NAME_LEN - 1] = '\0';
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Add new
|
||||
if (_count >= SMS_CONTACT_MAX) return false;
|
||||
strncpy(_contacts[_count].phone, phone, sizeof(_contacts[_count].phone) - 1);
|
||||
_contacts[_count].phone[sizeof(_contacts[_count].phone) - 1] = '\0';
|
||||
strncpy(_contacts[_count].name, name, SMS_CONTACT_NAME_LEN - 1);
|
||||
_contacts[_count].name[SMS_CONTACT_NAME_LEN - 1] = '\0';
|
||||
_contacts[_count].valid = true;
|
||||
_count++;
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a contact by phone number
|
||||
bool remove(const char* phone) {
|
||||
for (int i = 0; i < _count; i++) {
|
||||
if (_contacts[i].valid && strcmp(_contacts[i].phone, phone) == 0) {
|
||||
for (int j = i; j < _count - 1; j++) {
|
||||
_contacts[j] = _contacts[j + 1];
|
||||
}
|
||||
_count--;
|
||||
memset(&_contacts[_count], 0, sizeof(SMSContact));
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Accessors for list browsing
|
||||
int count() const { return _count; }
|
||||
const SMSContact& get(int index) const { return _contacts[index]; }
|
||||
|
||||
// Check if a contact exists
|
||||
bool exists(const char* phone) const { return lookup(phone) != nullptr; }
|
||||
|
||||
private:
|
||||
SMSContact _contacts[SMS_CONTACT_MAX];
|
||||
int _count = 0;
|
||||
|
||||
void load() {
|
||||
File f = SD.open(SMS_CONTACTS_FILE, FILE_READ);
|
||||
if (!f) {
|
||||
Serial.println("[SMSContacts] No contacts file, starting fresh");
|
||||
return;
|
||||
}
|
||||
|
||||
char line[64];
|
||||
while (f.available() && _count < SMS_CONTACT_MAX) {
|
||||
int pos = 0;
|
||||
while (f.available() && pos < (int)sizeof(line) - 1) {
|
||||
char c = f.read();
|
||||
if (c == '\n' || c == '\r') break;
|
||||
line[pos++] = c;
|
||||
}
|
||||
line[pos] = '\0';
|
||||
if (pos == 0) continue;
|
||||
// Consume trailing CR/LF
|
||||
while (f.available()) {
|
||||
int pk = f.peek();
|
||||
if (pk == '\n' || pk == '\r') { f.read(); continue; }
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse "phone=name"
|
||||
char* eq = strchr(line, '=');
|
||||
if (!eq) continue;
|
||||
*eq = '\0';
|
||||
const char* phone = line;
|
||||
const char* name = eq + 1;
|
||||
if (strlen(phone) == 0 || strlen(name) == 0) continue;
|
||||
|
||||
strncpy(_contacts[_count].phone, phone, sizeof(_contacts[_count].phone) - 1);
|
||||
strncpy(_contacts[_count].name, name, SMS_CONTACT_NAME_LEN - 1);
|
||||
_contacts[_count].valid = true;
|
||||
_count++;
|
||||
}
|
||||
f.close();
|
||||
Serial.printf("[SMSContacts] Loaded %d contacts\n", _count);
|
||||
}
|
||||
|
||||
void save() {
|
||||
if (!SD.exists("/sms")) SD.mkdir("/sms");
|
||||
File f = SD.open(SMS_CONTACTS_FILE, FILE_WRITE);
|
||||
if (!f) {
|
||||
Serial.println("[SMSContacts] Failed to write contacts file");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < _count; i++) {
|
||||
if (!_contacts[i].valid) continue;
|
||||
f.print(_contacts[i].phone);
|
||||
f.print('=');
|
||||
f.println(_contacts[i].name);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Global singleton
|
||||
extern SMSContactStore smsContacts;
|
||||
|
||||
#endif // SMS_CONTACTS_H
|
||||
#endif // HAS_4G_MODEM
|
||||
@@ -3,14 +3,18 @@
|
||||
// =============================================================================
|
||||
// SMSScreen - SMS messaging UI for T-Deck Pro (4G variant)
|
||||
//
|
||||
// Three sub-views:
|
||||
// INBOX — list of conversations, sorted by most recent
|
||||
// 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
|
||||
// EDIT_CONTACT — add or edit a contact name for a phone number
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
// Guard: HAS_4G_MODEM
|
||||
// =============================================================================
|
||||
@@ -22,8 +26,10 @@
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <time.h>
|
||||
#include "ModemManager.h"
|
||||
#include "SMSStore.h"
|
||||
#include "SMSContacts.h"
|
||||
|
||||
// Limits
|
||||
#define SMS_INBOX_PAGE_SIZE 4
|
||||
@@ -34,7 +40,7 @@ class UITask; // forward declaration
|
||||
|
||||
class SMSScreen : public UIScreen {
|
||||
public:
|
||||
enum SubView { INBOX, CONVERSATION, COMPOSE };
|
||||
enum SubView { INBOX, CONVERSATION, COMPOSE, CONTACTS, EDIT_CONTACT };
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
@@ -63,6 +69,17 @@ private:
|
||||
int _phoneInputPos;
|
||||
bool _enteringPhone;
|
||||
|
||||
// Contacts list state
|
||||
int _contactsCursor;
|
||||
int _contactsScrollTop;
|
||||
|
||||
// Edit contact state
|
||||
char _editPhone[SMS_PHONE_LEN];
|
||||
char _editNameBuf[SMS_CONTACT_NAME_LEN];
|
||||
int _editNamePos;
|
||||
bool _editIsNew; // true = adding new, false = editing existing
|
||||
SubView _editReturnView; // where to return after save/cancel
|
||||
|
||||
// Refresh debounce
|
||||
bool _needsRefresh;
|
||||
unsigned long _lastRefresh;
|
||||
@@ -78,7 +95,8 @@ private:
|
||||
|
||||
void refreshConversation() {
|
||||
_msgCount = smsStore.loadMessages(_activePhone, _msgs, SMS_MSG_PAGE_SIZE);
|
||||
_msgScrollPos = 0;
|
||||
// Scroll to bottom (newest messages are at end now, chat-style)
|
||||
_msgScrollPos = (_msgCount > 3) ? _msgCount - 3 : 0;
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -88,6 +106,8 @@ public:
|
||||
, _msgCount(0), _msgScrollPos(0)
|
||||
, _composePos(0), _composeNewConversation(false)
|
||||
, _phoneInputPos(0), _enteringPhone(false)
|
||||
, _contactsCursor(0), _contactsScrollTop(0)
|
||||
, _editNamePos(0), _editIsNew(false), _editReturnView(INBOX)
|
||||
, _needsRefresh(false), _lastRefresh(0)
|
||||
, _sdReady(false)
|
||||
{
|
||||
@@ -95,6 +115,8 @@ public:
|
||||
memset(_composePhone, 0, sizeof(_composePhone));
|
||||
memset(_phoneInputBuf, 0, sizeof(_phoneInputBuf));
|
||||
memset(_activePhone, 0, sizeof(_activePhone));
|
||||
memset(_editPhone, 0, sizeof(_editPhone));
|
||||
memset(_editNameBuf, 0, sizeof(_editNameBuf));
|
||||
}
|
||||
|
||||
void setSDReady(bool ready) { _sdReady = ready; }
|
||||
@@ -115,11 +137,9 @@ public:
|
||||
if (_sdReady) {
|
||||
smsStore.saveMessage(phone, body, false, timestamp);
|
||||
}
|
||||
// If we're viewing this conversation, refresh it
|
||||
if (_view == CONVERSATION && strcmp(_activePhone, phone) == 0) {
|
||||
refreshConversation();
|
||||
}
|
||||
// If on inbox, refresh the list
|
||||
if (_view == INBOX) {
|
||||
refreshInbox();
|
||||
}
|
||||
@@ -129,26 +149,29 @@ public:
|
||||
// =========================================================================
|
||||
// Signal strength indicator (top-right corner)
|
||||
// =========================================================================
|
||||
int renderSignalIndicator(DisplayDriver& display, int rightX, int topY) {
|
||||
|
||||
int renderSignalIndicator(DisplayDriver& display, int startX, int topY) {
|
||||
ModemState ms = modemManager.getState();
|
||||
int bars = modemManager.getSignalBars();
|
||||
int iconWidth = 16;
|
||||
|
||||
// Draw signal bars (4 bars, increasing height)
|
||||
int barW = 3;
|
||||
int gap = 1;
|
||||
int startX = rightX - (4 * (barW + gap));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int barH = 2 + i * 2; // 2, 4, 6, 8
|
||||
int x = startX + i * (barW + gap);
|
||||
int y = topY + (8 - barH);
|
||||
if (i < bars) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.fillRect(x, y, barW, barH);
|
||||
} else {
|
||||
int barWidth = 3;
|
||||
int barGap = 2;
|
||||
int maxBarH = 10;
|
||||
int totalWidth = 4 * barWidth + 3 * barGap;
|
||||
int x = startX - totalWidth;
|
||||
int iconWidth = totalWidth;
|
||||
|
||||
for (int b = 0; b < 4; b++) {
|
||||
int barH = 3 + b * 2;
|
||||
int barY = topY + (maxBarH - barH);
|
||||
if (b < bars) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(x, y, barW, barH);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
display.fillRect(x, barY, barWidth, barH);
|
||||
x += barWidth + barGap;
|
||||
}
|
||||
|
||||
// Show modem state text if not ready
|
||||
@@ -157,7 +180,7 @@ public:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
const char* label = ModemManager::stateToString(ms);
|
||||
uint16_t labelW = display.getTextWidth(label);
|
||||
display.setCursor(startX - labelW - 2, topY - 3);
|
||||
display.setCursor(startX - totalWidth - labelW - 2, topY - 3);
|
||||
display.print(label);
|
||||
display.setTextSize(1);
|
||||
return iconWidth + labelW + 2;
|
||||
@@ -177,6 +200,8 @@ public:
|
||||
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);
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
@@ -204,7 +229,7 @@ public:
|
||||
display.print("No conversations");
|
||||
display.setCursor(0, 32);
|
||||
display.print("Press C for new SMS");
|
||||
|
||||
|
||||
if (ms != ModemState::READY) {
|
||||
display.setCursor(0, 48);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
@@ -234,11 +259,14 @@ public:
|
||||
|
||||
bool selected = (idx == _inboxCursor);
|
||||
|
||||
// Phone number (highlighted if selected)
|
||||
// Resolve contact name (shows name if saved, phone otherwise)
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(c.phone, dispName, sizeof(dispName));
|
||||
|
||||
display.setCursor(0, y);
|
||||
display.setColor(selected ? DisplayDriver::GREEN : DisplayDriver::LIGHT);
|
||||
if (selected) display.print("> ");
|
||||
display.print(c.phone);
|
||||
display.print(dispName);
|
||||
|
||||
// Message count at right
|
||||
char countStr[8];
|
||||
@@ -261,31 +289,31 @@ public:
|
||||
}
|
||||
|
||||
// Footer
|
||||
display.setTextSize(0); // Must be set before setCursor/getTextWidth
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
int footerY = display.height() - 10;
|
||||
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 = "W/S:Scrll";
|
||||
display.print("Q:Bk");
|
||||
const char* mid = "D:Contacts";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
const char* rt = "C:New";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
display.setTextSize(1);
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
// ---- Conversation view ----
|
||||
int renderConversation(DisplayDriver& display) {
|
||||
// Header
|
||||
// Header - show contact name if available, phone otherwise
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
display.print(_activePhone);
|
||||
char convTitle[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(_activePhone, convTitle, sizeof(convTitle));
|
||||
display.print(convTitle);
|
||||
|
||||
// Signal icon
|
||||
renderSignalIndicator(display, display.width() - 2, 0);
|
||||
@@ -322,14 +350,21 @@ public:
|
||||
display.setCursor(0, y);
|
||||
display.setColor(msg.isSent ? DisplayDriver::BLUE : DisplayDriver::YELLOW);
|
||||
|
||||
// Time formatting
|
||||
uint32_t now = millis() / 1000;
|
||||
uint32_t age = (now > msg.timestamp) ? (now - msg.timestamp) : 0;
|
||||
// Time formatting (epoch-aware)
|
||||
char timeStr[16];
|
||||
if (age < 60) snprintf(timeStr, sizeof(timeStr), "%lus", (unsigned long)age);
|
||||
else if (age < 3600) snprintf(timeStr, sizeof(timeStr), "%lum", (unsigned long)(age / 60));
|
||||
else if (age < 86400) snprintf(timeStr, sizeof(timeStr), "%luh", (unsigned long)(age / 3600));
|
||||
else snprintf(timeStr, sizeof(timeStr), "%lud", (unsigned long)(age / 86400));
|
||||
time_t now = time(nullptr);
|
||||
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);
|
||||
if (age < 60) snprintf(timeStr, sizeof(timeStr), "%lus", (unsigned long)age);
|
||||
else if (age < 3600) snprintf(timeStr, sizeof(timeStr), "%lum", (unsigned long)(age / 60));
|
||||
else if (age < 86400) snprintf(timeStr, sizeof(timeStr), "%luh", (unsigned long)(age / 3600));
|
||||
else snprintf(timeStr, sizeof(timeStr), "%lud", (unsigned long)(age / 86400));
|
||||
} else {
|
||||
strncpy(timeStr, "---", sizeof(timeStr));
|
||||
}
|
||||
|
||||
char header[32];
|
||||
snprintf(header, sizeof(header), "%s %s",
|
||||
@@ -368,20 +403,15 @@ public:
|
||||
}
|
||||
|
||||
// Footer
|
||||
display.setTextSize(0); // Must be set before setCursor/getTextWidth
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
int footerY = display.height() - 10;
|
||||
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 = "W/S:Scrll";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
display.print("Q:Bk A:Add");
|
||||
const char* rt = "C:Reply";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
display.setTextSize(1);
|
||||
|
||||
return 5000;
|
||||
}
|
||||
@@ -393,15 +423,17 @@ public:
|
||||
display.setCursor(0, 0);
|
||||
|
||||
if (_enteringPhone) {
|
||||
// Phone number input mode
|
||||
display.print("To: ");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print(_phoneInputBuf);
|
||||
display.print("_");
|
||||
} else {
|
||||
char header[40];
|
||||
snprintf(header, sizeof(header), "To: %s", _composePhone);
|
||||
display.print(header);
|
||||
// Show contact name if available
|
||||
char dispName[SMS_CONTACT_NAME_LEN];
|
||||
smsContacts.displayName(_composePhone, dispName, sizeof(dispName));
|
||||
char toLabel[40];
|
||||
snprintf(toLabel, sizeof(toLabel), "To: %s", dispName);
|
||||
display.print(toLabel);
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -438,27 +470,141 @@ public:
|
||||
}
|
||||
|
||||
// Status bar
|
||||
display.setTextSize(0); // Must be set before setCursor/getTextWidth
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
int statusY = display.height() - 10;
|
||||
display.setTextSize(1);
|
||||
int statusY = display.height() - 12;
|
||||
display.drawRect(0, statusY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, statusY);
|
||||
|
||||
if (_enteringPhone) {
|
||||
display.print("Phone# then Ent");
|
||||
const char* rt = "S+D:X";
|
||||
display.print("Phone#");
|
||||
const char* rt = "Ent S+D:X";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, statusY);
|
||||
display.print(rt);
|
||||
} else {
|
||||
char status[30];
|
||||
char status[16];
|
||||
snprintf(status, sizeof(status), "%d/%d", _composePos, SMS_COMPOSE_MAX);
|
||||
display.print(status);
|
||||
const char* rt = "Ent:Snd S+D:X";
|
||||
const char* rt = "Ent S+D:X";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, statusY);
|
||||
display.print(rt);
|
||||
}
|
||||
|
||||
return 2000;
|
||||
}
|
||||
|
||||
// ---- Contacts list ----
|
||||
int renderContacts(DisplayDriver& display) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
display.print("SMS Contacts");
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
int cnt = smsContacts.count();
|
||||
|
||||
if (cnt == 0) {
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, 25);
|
||||
display.print("No contacts saved");
|
||||
display.setCursor(0, 37);
|
||||
display.print("Open a conversation");
|
||||
display.setCursor(0, 49);
|
||||
display.print("and press A to add");
|
||||
display.setTextSize(1);
|
||||
} else {
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 10;
|
||||
int y = 14;
|
||||
|
||||
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;
|
||||
if (_contactsCursor >= _contactsScrollTop + visibleCount) {
|
||||
_contactsScrollTop = _contactsCursor - visibleCount + 1;
|
||||
}
|
||||
|
||||
for (int vi = 0; vi < visibleCount && (_contactsScrollTop + vi) < cnt; vi++) {
|
||||
int idx = _contactsScrollTop + vi;
|
||||
const SMSContact& ct = smsContacts.get(idx);
|
||||
if (!ct.valid) continue;
|
||||
|
||||
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);
|
||||
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* rt = "Ent:SMS";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
|
||||
return 5000;
|
||||
}
|
||||
|
||||
// ---- Edit contact ----
|
||||
int renderEditContact(DisplayDriver& display) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
display.print(_editIsNew ? "Add Contact" : "Edit Contact");
|
||||
|
||||
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: ");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print(_editNameBuf);
|
||||
display.print("_");
|
||||
|
||||
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("S+D:X");
|
||||
const char* rt = "Ent:Save";
|
||||
display.setCursor(display.width() - display.getTextWidth(rt) - 2, footerY);
|
||||
display.print(rt);
|
||||
|
||||
return 2000;
|
||||
}
|
||||
@@ -472,6 +618,8 @@ public:
|
||||
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);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -505,8 +653,14 @@ public:
|
||||
_view = COMPOSE;
|
||||
return true;
|
||||
|
||||
case 'd': case 'D': // Open contacts list
|
||||
_contactsCursor = 0;
|
||||
_contactsScrollTop = 0;
|
||||
_view = CONTACTS;
|
||||
return true;
|
||||
|
||||
case 'q': case 'Q': // Back to home (handled by main.cpp)
|
||||
return false; // Let main.cpp handle navigation
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
@@ -533,6 +687,26 @@ public:
|
||||
_view = COMPOSE;
|
||||
return true;
|
||||
|
||||
case 'a': case 'A': { // Add/edit contact for this number
|
||||
strncpy(_editPhone, _activePhone, SMS_PHONE_LEN - 1);
|
||||
_editPhone[SMS_PHONE_LEN - 1] = '\0';
|
||||
_editReturnView = CONVERSATION;
|
||||
|
||||
const char* existing = smsContacts.lookup(_activePhone);
|
||||
if (existing) {
|
||||
_editIsNew = false;
|
||||
strncpy(_editNameBuf, existing, SMS_CONTACT_NAME_LEN - 1);
|
||||
_editNameBuf[SMS_CONTACT_NAME_LEN - 1] = '\0';
|
||||
_editNamePos = strlen(_editNameBuf);
|
||||
} else {
|
||||
_editIsNew = true;
|
||||
_editNameBuf[0] = '\0';
|
||||
_editNamePos = 0;
|
||||
}
|
||||
_view = EDIT_CONTACT;
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'q': case 'Q': // Back to inbox
|
||||
refreshInbox();
|
||||
_view = INBOX;
|
||||
@@ -549,26 +723,18 @@ public:
|
||||
return handlePhoneInput(c);
|
||||
}
|
||||
|
||||
// Message body input
|
||||
switch (c) {
|
||||
case '\r': { // Enter - send SMS
|
||||
if (_composePos > 0) {
|
||||
_composeBuf[_composePos] = '\0';
|
||||
|
||||
// Queue for sending via modem
|
||||
bool queued = modemManager.sendSMS(_composePhone, _composeBuf);
|
||||
|
||||
// Save to store (as sent)
|
||||
if (_sdReady) {
|
||||
uint32_t ts = millis() / 1000;
|
||||
uint32_t ts = (uint32_t)time(nullptr);
|
||||
smsStore.saveMessage(_composePhone, _composeBuf, true, ts);
|
||||
}
|
||||
|
||||
Serial.printf("[SMS] %s to %s: %s\n",
|
||||
queued ? "Queued" : "Queue full", _composePhone, _composeBuf);
|
||||
}
|
||||
|
||||
// Return to inbox
|
||||
_composeBuf[0] = '\0';
|
||||
_composePos = 0;
|
||||
refreshInbox();
|
||||
@@ -583,7 +749,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
|
||||
case 0x18: // Shift+Backspace (cancel) — same as mesh compose
|
||||
case 0x18: // Shift+Backspace (cancel)
|
||||
_composeBuf[0] = '\0';
|
||||
_composePos = 0;
|
||||
refreshInbox();
|
||||
@@ -591,7 +757,6 @@ public:
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Printable character
|
||||
if (c >= 32 && c < 127 && _composePos < SMS_COMPOSE_MAX) {
|
||||
_composeBuf[_composePos++] = c;
|
||||
_composeBuf[_composePos] = '\0';
|
||||
@@ -603,7 +768,7 @@ public:
|
||||
// ---- Phone number input ----
|
||||
bool handlePhoneInput(char c) {
|
||||
switch (c) {
|
||||
case '\r': // Enter - done entering phone, move to body
|
||||
case '\r': // Done entering phone, move to body
|
||||
if (_phoneInputPos > 0) {
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
strncpy(_composePhone, _phoneInputBuf, SMS_PHONE_LEN - 1);
|
||||
@@ -613,7 +778,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
|
||||
case '\b': // Backspace
|
||||
case '\b':
|
||||
if (_phoneInputPos > 0) {
|
||||
_phoneInputPos--;
|
||||
_phoneInputBuf[_phoneInputPos] = '\0';
|
||||
@@ -629,7 +794,6 @@ public:
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Accept digits, +, *, # for phone numbers
|
||||
if (_phoneInputPos < SMS_PHONE_LEN - 1 &&
|
||||
((c >= '0' && c <= '9') || c == '+' || c == '*' || c == '#')) {
|
||||
_phoneInputBuf[_phoneInputPos++] = c;
|
||||
@@ -638,6 +802,83 @@ public:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Contacts list input ----
|
||||
bool handleContactsInput(char c) {
|
||||
int cnt = smsContacts.count();
|
||||
|
||||
switch (c) {
|
||||
case 'w': case 'W':
|
||||
if (_contactsCursor > 0) _contactsCursor--;
|
||||
return true;
|
||||
|
||||
case 's': case 'S':
|
||||
if (_contactsCursor < cnt - 1) _contactsCursor++;
|
||||
return true;
|
||||
|
||||
case '\r': // Enter - compose to selected contact
|
||||
if (cnt > 0 && _contactsCursor < cnt) {
|
||||
const SMSContact& ct = smsContacts.get(_contactsCursor);
|
||||
_composeNewConversation = true;
|
||||
_enteringPhone = false;
|
||||
strncpy(_composePhone, ct.phone, SMS_PHONE_LEN - 1);
|
||||
_composeBuf[0] = '\0';
|
||||
_composePos = 0;
|
||||
_view = COMPOSE;
|
||||
}
|
||||
return true;
|
||||
|
||||
case 'q': case 'Q': // Back to inbox
|
||||
refreshInbox();
|
||||
_view = INBOX;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Edit contact input ----
|
||||
bool handleEditContactInput(char c) {
|
||||
switch (c) {
|
||||
case '\r': // Enter - save contact
|
||||
if (_editNamePos > 0) {
|
||||
_editNameBuf[_editNamePos] = '\0';
|
||||
smsContacts.set(_editPhone, _editNameBuf);
|
||||
Serial.printf("[SMSContacts] Saved: %s = %s\n", _editPhone, _editNameBuf);
|
||||
}
|
||||
if (_editReturnView == CONVERSATION) {
|
||||
refreshConversation();
|
||||
} else {
|
||||
refreshInbox();
|
||||
}
|
||||
_view = _editReturnView;
|
||||
return true;
|
||||
|
||||
case '\b': // Backspace
|
||||
if (_editNamePos > 0) {
|
||||
_editNamePos--;
|
||||
_editNameBuf[_editNamePos] = '\0';
|
||||
}
|
||||
return true;
|
||||
|
||||
case 0x18: // Shift+Backspace (cancel without saving)
|
||||
if (_editReturnView == CONVERSATION) {
|
||||
refreshConversation();
|
||||
} else {
|
||||
refreshInbox();
|
||||
}
|
||||
_view = _editReturnView;
|
||||
return true;
|
||||
|
||||
default:
|
||||
if (c >= 32 && c < 127 && _editNamePos < SMS_CONTACT_NAME_LEN - 1) {
|
||||
_editNameBuf[_editNamePos++] = c;
|
||||
_editNameBuf[_editNamePos] = '\0';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SMS_SCREEN_H
|
||||
|
||||
@@ -138,14 +138,13 @@ int SMSStore::loadMessages(const char* phone, SMSMessage* out, int maxCount) {
|
||||
size_t fileSize = f.size();
|
||||
int numRecords = fileSize / sizeof(SMSRecord);
|
||||
|
||||
// Load from end (newest first), up to maxCount
|
||||
// Load from end of file (most recent N messages), in chronological order
|
||||
int startIdx = numRecords > maxCount ? numRecords - maxCount : 0;
|
||||
int loadCount = numRecords - startIdx;
|
||||
|
||||
// Read from startIdx and reverse order for display (newest first)
|
||||
// Read chronologically (oldest first) for chat-style display
|
||||
SMSRecord rec;
|
||||
int outIdx = 0;
|
||||
for (int i = numRecords - 1; i >= startIdx && outIdx < maxCount; i--) {
|
||||
for (int i = startIdx; i < numRecords && outIdx < maxCount; i++) {
|
||||
f.seek(i * sizeof(SMSRecord));
|
||||
if (f.read((uint8_t*)&rec, sizeof(SMSRecord)) != sizeof(SMSRecord)) continue;
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
// Load conversation list (sorted by most recent)
|
||||
int loadConversations(SMSConversation* out, int maxCount);
|
||||
|
||||
// Load messages for a specific phone number (newest first)
|
||||
// Load messages for a specific phone number (chronological, oldest first)
|
||||
int loadMessages(const char* phone, SMSMessage* out, int maxCount);
|
||||
|
||||
// Delete all messages for a phone number
|
||||
@@ -84,4 +84,4 @@ private:
|
||||
extern SMSStore smsStore;
|
||||
|
||||
#endif // SMS_STORE_H
|
||||
#endif // HAS_4G_MODEM
|
||||
#endif // HAS_4G_MODEM
|
||||
@@ -6,6 +6,10 @@
|
||||
#include <MeshCore.h>
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
#include "ModemManager.h"
|
||||
#endif
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
class MyMesh;
|
||||
@@ -56,6 +60,9 @@ enum SettingsRowType : uint8_t {
|
||||
ROW_TX_POWER, // TX power (1-20 dBm)
|
||||
ROW_UTC_OFFSET, // UTC offset (-12 to +14)
|
||||
ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle
|
||||
#ifdef HAS_4G_MODEM
|
||||
ROW_MODEM_TOGGLE, // 4G modem enable/disable toggle (4G builds only)
|
||||
#endif
|
||||
ROW_CH_HEADER, // "--- Channels ---" separator
|
||||
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
|
||||
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
|
||||
@@ -85,7 +92,7 @@ private:
|
||||
mesh::RTCClock* _rtc;
|
||||
NodePrefs* _prefs;
|
||||
|
||||
// Row table — rebuilt whenever channels change
|
||||
// Row table  rebuilt whenever channels change
|
||||
struct Row {
|
||||
SettingsRowType type;
|
||||
uint8_t param; // channel index for ROW_CHANNEL, preset index for ROW_RADIO_PRESET
|
||||
@@ -109,9 +116,14 @@ private:
|
||||
// Onboarding mode
|
||||
bool _onboarding;
|
||||
|
||||
// Dirty flag for radio params — prompt to apply
|
||||
// Dirty flag for radio params  prompt to apply
|
||||
bool _radioChanged;
|
||||
|
||||
// 4G modem state (runtime cache of config)
|
||||
#ifdef HAS_4G_MODEM
|
||||
bool _modemEnabled;
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Row table management
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -128,6 +140,9 @@ private:
|
||||
addRow(ROW_TX_POWER);
|
||||
addRow(ROW_UTC_OFFSET);
|
||||
addRow(ROW_MSG_NOTIFY);
|
||||
#ifdef HAS_4G_MODEM
|
||||
addRow(ROW_MODEM_TOGGLE);
|
||||
#endif
|
||||
addRow(ROW_CH_HEADER);
|
||||
|
||||
// Enumerate current channels
|
||||
@@ -212,11 +227,11 @@ private:
|
||||
strncpy(newCh.name, chanName, sizeof(newCh.name));
|
||||
newCh.name[31] = '\0';
|
||||
|
||||
// SHA-256 the channel name → first 16 bytes become the secret
|
||||
// SHA-256 the channel name → first 16 bytes become the secret
|
||||
uint8_t hash[32];
|
||||
mesh::Utils::sha256(hash, 32, (const uint8_t*)chanName, strlen(chanName));
|
||||
memcpy(newCh.channel.secret, hash, 16);
|
||||
// Upper 16 bytes left as zero → setChannel uses 128-bit mode
|
||||
// Upper 16 bytes left as zero → setChannel uses 128-bit mode
|
||||
|
||||
// Find next empty slot
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
@@ -289,6 +304,9 @@ public:
|
||||
_cursor = 0;
|
||||
_scrollTop = 0;
|
||||
_radioChanged = false;
|
||||
#ifdef HAS_4G_MODEM
|
||||
_modemEnabled = ModemManager::loadEnabledConfig();
|
||||
#endif
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
@@ -473,6 +491,14 @@ public:
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
case ROW_MODEM_TOGGLE:
|
||||
snprintf(tmp, sizeof(tmp), "4G Modem: %s",
|
||||
_modemEnabled ? "ON" : "OFF");
|
||||
display.print(tmp);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case ROW_CH_HEADER:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("--- Channels ---");
|
||||
@@ -838,6 +864,19 @@ public:
|
||||
Serial.printf("Settings: Msg flash notify = %s\n",
|
||||
_prefs->kb_flash_notify ? "ON" : "OFF");
|
||||
break;
|
||||
#ifdef HAS_4G_MODEM
|
||||
case ROW_MODEM_TOGGLE:
|
||||
_modemEnabled = !_modemEnabled;
|
||||
ModemManager::saveEnabledConfig(_modemEnabled);
|
||||
if (_modemEnabled) {
|
||||
modemManager.begin();
|
||||
Serial.println("Settings: 4G modem ENABLED (started)");
|
||||
} else {
|
||||
modemManager.shutdown();
|
||||
Serial.println("Settings: 4G modem DISABLED (shutdown)");
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case ROW_ADD_CHANNEL:
|
||||
startEditText("");
|
||||
break;
|
||||
@@ -861,7 +900,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Q: back — if radio changed, prompt to apply first
|
||||
// Q: back  if radio changed, prompt to apply first
|
||||
if (c == 'q' || c == 'Q') {
|
||||
if (_radioChanged) {
|
||||
_editMode = EDIT_CONFIRM;
|
||||
|
||||
Reference in New Issue
Block a user