mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-07 22:04:49 +02:00
Add apn database to enable modem to connect to network without wifi, same with updates to modem manager; adustments to settings screen to show imei, carrier, apn information; updated new no-ble 4G standalone env
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// ApnDatabase.h - Embedded APN Lookup Table
|
||||
//
|
||||
// Maps MCC/MNC (Mobile Country Code / Mobile Network Code) to default APN
|
||||
// settings for common carriers worldwide. Compiled directly into flash (~3KB)
|
||||
// so users never need to manually install a lookup file.
|
||||
//
|
||||
// The modem queries IMSI via AT+CIMI to extract MCC (3 digits) + MNC (2-3
|
||||
// digits), then looks up the APN here. If not found, falls back to the
|
||||
// modem's existing PDP context (AT+CGDCONT?) or user-configured APN.
|
||||
//
|
||||
// To add a carrier: append to APN_DATABASE[] with the MCC+MNC as a single
|
||||
// integer. MNC can be 2 or 3 digits:
|
||||
// MCC=310, MNC=260 → mccmnc = 310260
|
||||
// MCC=505, MNC=01 → mccmnc = 50501
|
||||
//
|
||||
// Guard: HAS_4G_MODEM
|
||||
// =============================================================================
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
|
||||
#ifndef APN_DATABASE_H
|
||||
#define APN_DATABASE_H
|
||||
|
||||
struct ApnEntry {
|
||||
uint32_t mccmnc; // MCC+MNC as integer (e.g. 310260 for T-Mobile US)
|
||||
const char* apn; // APN string
|
||||
const char* carrier; // Human-readable carrier name (for debug/display)
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// APN Database — sorted by MCC for binary search potential (not required)
|
||||
//
|
||||
// Sources: carrier documentation, GSMA databases, community wikis.
|
||||
// This covers ~120 major carriers across key regions. Users with less
|
||||
// common carriers can set APN manually in Settings.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const ApnEntry APN_DATABASE[] = {
|
||||
// =========================================================================
|
||||
// Australia (MCC 505)
|
||||
// =========================================================================
|
||||
{ 50501, "telstra.internet", "Telstra" },
|
||||
{ 50502, "yesinternet", "Optus" },
|
||||
{ 50503, "vfinternet.au", "Vodafone AU" },
|
||||
{ 50506, "3netaccess", "Three AU" },
|
||||
{ 50507, "telstra.internet", "Vodafone AU (MVNO)" }, // Many MVNOs on Telstra
|
||||
{ 50510, "telstra.internet", "Norfolk Tel" },
|
||||
{ 50512, "3netaccess", "Amaysim" }, // Optus MVNO
|
||||
{ 50514, "yesinternet", "Aussie Broadband" }, // Optus MVNO
|
||||
{ 50590, "yesinternet", "Optus MVNO" },
|
||||
|
||||
// =========================================================================
|
||||
// New Zealand (MCC 530)
|
||||
// =========================================================================
|
||||
{ 53001, "internet", "Vodafone NZ" },
|
||||
{ 53005, "internet", "Spark NZ" },
|
||||
{ 53024, "internet", "2degrees" },
|
||||
|
||||
// =========================================================================
|
||||
// United States (MCC 310, 311, 312, 313, 316)
|
||||
// =========================================================================
|
||||
{ 310012, "fast.t-mobile.com", "Verizon (old)" },
|
||||
{ 310026, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310030, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310032, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310060, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310160, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310200, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310210, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310220, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310230, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310240, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310250, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310260, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310270, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310310, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310490, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310530, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310580, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310660, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 310800, "fast.t-mobile.com", "T-Mobile US" },
|
||||
{ 311480, "vzwinternet", "Verizon" },
|
||||
{ 311481, "vzwinternet", "Verizon" },
|
||||
{ 311482, "vzwinternet", "Verizon" },
|
||||
{ 311483, "vzwinternet", "Verizon" },
|
||||
{ 311484, "vzwinternet", "Verizon" },
|
||||
{ 311489, "vzwinternet", "Verizon" },
|
||||
{ 310410, "fast.t-mobile.com", "AT&T (migrated)" },
|
||||
{ 310120, "att.mvno", "AT&T (Sprint)" },
|
||||
{ 312530, "iot.1nce.net", "1NCE IoT" },
|
||||
{ 310120, "tfdata", "Tracfone" },
|
||||
|
||||
// =========================================================================
|
||||
// Canada (MCC 302)
|
||||
// =========================================================================
|
||||
{ 30220, "internet.com", "Rogers" },
|
||||
{ 30221, "internet.com", "Rogers" },
|
||||
{ 30237, "internet.com", "Rogers" },
|
||||
{ 30272, "internet.com", "Rogers" },
|
||||
{ 30234, "sp.telus.com", "Telus" },
|
||||
{ 30286, "sp.telus.com", "Telus" },
|
||||
{ 30236, "sp.telus.com", "Telus" },
|
||||
{ 30261, "sp.bell.ca", "Bell" },
|
||||
{ 30263, "sp.bell.ca", "Bell" },
|
||||
{ 30267, "sp.bell.ca", "Bell" },
|
||||
{ 30268, "fido-core-appl1.apn", "Fido" },
|
||||
{ 30278, "internet.com", "SaskTel" },
|
||||
{ 30266, "sp.mb.com", "MTS" },
|
||||
|
||||
// =========================================================================
|
||||
// United Kingdom (MCC 234, 235)
|
||||
// =========================================================================
|
||||
{ 23410, "o2-internet", "O2 UK" },
|
||||
{ 23415, "three.co.uk", "Vodafone UK" },
|
||||
{ 23420, "three.co.uk", "Three UK" },
|
||||
{ 23430, "everywhere", "EE" },
|
||||
{ 23431, "everywhere", "EE" },
|
||||
{ 23432, "everywhere", "EE" },
|
||||
{ 23433, "everywhere", "EE" },
|
||||
{ 23450, "data.lycamobile.co.uk","Lycamobile UK" },
|
||||
{ 23486, "three.co.uk", "Three UK" },
|
||||
|
||||
// =========================================================================
|
||||
// Germany (MCC 262)
|
||||
// =========================================================================
|
||||
{ 26201, "internet.t-mobile", "Telekom DE" },
|
||||
{ 26202, "web.vodafone.de", "Vodafone DE" },
|
||||
{ 26203, "internet", "O2 DE" },
|
||||
{ 26207, "internet", "O2 DE" },
|
||||
|
||||
// =========================================================================
|
||||
// France (MCC 208)
|
||||
// =========================================================================
|
||||
{ 20801, "orange", "Orange FR" },
|
||||
{ 20810, "sl2sfr", "SFR" },
|
||||
{ 20815, "free", "Free Mobile" },
|
||||
{ 20820, "ofnew.fr", "Bouygues" },
|
||||
|
||||
// =========================================================================
|
||||
// Italy (MCC 222)
|
||||
// =========================================================================
|
||||
{ 22201, "mobile.vodafone.it", "TIM" },
|
||||
{ 22210, "mobile.vodafone.it", "Vodafone IT" },
|
||||
{ 22250, "internet.it", "Iliad IT" },
|
||||
{ 22288, "internet.wind", "WindTre" },
|
||||
{ 22299, "internet.wind", "WindTre" },
|
||||
|
||||
// =========================================================================
|
||||
// Spain (MCC 214)
|
||||
// =========================================================================
|
||||
{ 21401, "internet", "Vodafone ES" },
|
||||
{ 21403, "internet", "Orange ES" },
|
||||
{ 21404, "internet", "Yoigo" },
|
||||
{ 21407, "internet", "Movistar" },
|
||||
|
||||
// =========================================================================
|
||||
// Netherlands (MCC 204)
|
||||
// =========================================================================
|
||||
{ 20404, "internet", "Vodafone NL" },
|
||||
{ 20408, "internet", "KPN" },
|
||||
{ 20412, "internet", "Telfort" },
|
||||
{ 20416, "internet", "T-Mobile NL" },
|
||||
{ 20420, "internet", "T-Mobile NL" },
|
||||
|
||||
// =========================================================================
|
||||
// Sweden (MCC 240)
|
||||
// =========================================================================
|
||||
{ 24001, "internet.telia.se", "Telia SE" },
|
||||
{ 24002, "tre.se", "Three SE" },
|
||||
{ 24007, "internet.telenor.se", "Telenor SE" },
|
||||
|
||||
// =========================================================================
|
||||
// Norway (MCC 242)
|
||||
// =========================================================================
|
||||
{ 24201, "internet.telenor.no", "Telenor NO" },
|
||||
{ 24202, "internet.netcom.no", "Telia NO" },
|
||||
|
||||
// =========================================================================
|
||||
// Denmark (MCC 238)
|
||||
// =========================================================================
|
||||
{ 23801, "internet", "TDC" },
|
||||
{ 23802, "internet", "Telenor DK" },
|
||||
{ 23806, "internet", "Three DK" },
|
||||
{ 23820, "internet", "Telia DK" },
|
||||
|
||||
// =========================================================================
|
||||
// Switzerland (MCC 228)
|
||||
// =========================================================================
|
||||
{ 22801, "gprs.swisscom.ch", "Swisscom" },
|
||||
{ 22802, "internet", "Sunrise" },
|
||||
{ 22803, "internet", "Salt" },
|
||||
|
||||
// =========================================================================
|
||||
// Austria (MCC 232)
|
||||
// =========================================================================
|
||||
{ 23201, "a1.net", "A1" },
|
||||
{ 23203, "web.one.at", "Three AT" },
|
||||
{ 23205, "web", "T-Mobile AT" },
|
||||
|
||||
// =========================================================================
|
||||
// Japan (MCC 440, 441)
|
||||
// =========================================================================
|
||||
{ 44010, "spmode.ne.jp", "NTT Docomo" },
|
||||
{ 44020, "plus.4g", "SoftBank" },
|
||||
{ 44051, "au.au-net.ne.jp", "KDDI au" },
|
||||
|
||||
// =========================================================================
|
||||
// South Korea (MCC 450)
|
||||
// =========================================================================
|
||||
{ 45005, "lte.sktelecom.com", "SK Telecom" },
|
||||
{ 45006, "lte.ktfwing.com", "KT" },
|
||||
{ 45008, "lte.lguplus.co.kr", "LG U+" },
|
||||
|
||||
// =========================================================================
|
||||
// India (MCC 404, 405)
|
||||
// =========================================================================
|
||||
{ 40445, "airtelgprs.com", "Airtel" },
|
||||
{ 40410, "airtelgprs.com", "Airtel" },
|
||||
{ 40411, "www", "Vodafone IN (Vi)" },
|
||||
{ 40413, "www", "Vodafone IN (Vi)" },
|
||||
{ 40486, "www", "Vodafone IN (Vi)" },
|
||||
{ 40553, "jionet", "Jio" },
|
||||
{ 40554, "jionet", "Jio" },
|
||||
{ 40512, "bsnlnet", "BSNL" },
|
||||
|
||||
// =========================================================================
|
||||
// Singapore (MCC 525)
|
||||
// =========================================================================
|
||||
{ 52501, "internet", "Singtel" },
|
||||
{ 52503, "internet", "M1" },
|
||||
{ 52505, "internet", "StarHub" },
|
||||
|
||||
// =========================================================================
|
||||
// Hong Kong (MCC 454)
|
||||
// =========================================================================
|
||||
{ 45400, "internet", "CSL" },
|
||||
{ 45406, "internet", "SmarTone" },
|
||||
{ 45412, "internet", "CMHK" },
|
||||
|
||||
// =========================================================================
|
||||
// Brazil (MCC 724)
|
||||
// =========================================================================
|
||||
{ 72405, "claro.com.br", "Claro BR" },
|
||||
{ 72406, "wap.oi.com.br", "Vivo" },
|
||||
{ 72410, "wap.oi.com.br", "Vivo" },
|
||||
{ 72411, "wap.oi.com.br", "Vivo" },
|
||||
{ 72415, "internet.tim.br", "TIM BR" },
|
||||
{ 72431, "gprs.oi.com.br", "Oi" },
|
||||
|
||||
// =========================================================================
|
||||
// Mexico (MCC 334)
|
||||
// =========================================================================
|
||||
{ 33402, "internet.itelcel.com","Telcel" },
|
||||
{ 33403, "internet.movistar.mx","Movistar MX" },
|
||||
{ 33404, "internet.att.net.mx", "AT&T MX" },
|
||||
|
||||
// =========================================================================
|
||||
// South Africa (MCC 655)
|
||||
// =========================================================================
|
||||
{ 65501, "internet", "Vodacom" },
|
||||
{ 65502, "internet", "Telkom ZA" },
|
||||
{ 65507, "internet", "Cell C" },
|
||||
{ 65510, "internet", "MTN ZA" },
|
||||
|
||||
// =========================================================================
|
||||
// Philippines (MCC 515)
|
||||
// =========================================================================
|
||||
{ 51502, "internet.globe.com.ph","Globe" },
|
||||
{ 51503, "internet", "Smart" },
|
||||
{ 51505, "internet", "Sun Cellular" },
|
||||
|
||||
// =========================================================================
|
||||
// Thailand (MCC 520)
|
||||
// =========================================================================
|
||||
{ 52001, "internet", "AIS" },
|
||||
{ 52004, "internet", "TrueMove" },
|
||||
{ 52005, "internet", "dtac" },
|
||||
|
||||
// =========================================================================
|
||||
// Indonesia (MCC 510)
|
||||
// =========================================================================
|
||||
{ 51001, "internet", "Telkomsel" },
|
||||
{ 51010, "internet", "Telkomsel" },
|
||||
{ 51011, "3gprs", "XL Axiata" },
|
||||
{ 51028, "3gprs", "XL Axiata (Axis)" },
|
||||
|
||||
// =========================================================================
|
||||
// Malaysia (MCC 502)
|
||||
// =========================================================================
|
||||
{ 50212, "celcom3g", "Celcom" },
|
||||
{ 50213, "celcom3g", "Celcom" },
|
||||
{ 50216, "internet", "Digi" },
|
||||
{ 50219, "celcom3g", "Celcom" },
|
||||
|
||||
// =========================================================================
|
||||
// Czech Republic (MCC 230)
|
||||
// =========================================================================
|
||||
{ 23001, "internet.t-mobile.cz","T-Mobile CZ" },
|
||||
{ 23002, "internet", "O2 CZ" },
|
||||
{ 23003, "internet.vodafone.cz","Vodafone CZ" },
|
||||
|
||||
// =========================================================================
|
||||
// Poland (MCC 260)
|
||||
// =========================================================================
|
||||
{ 26001, "internet", "Plus PL" },
|
||||
{ 26002, "internet", "T-Mobile PL" },
|
||||
{ 26003, "internet", "Orange PL" },
|
||||
{ 26006, "internet", "Play" },
|
||||
|
||||
// =========================================================================
|
||||
// Portugal (MCC 268)
|
||||
// =========================================================================
|
||||
{ 26801, "internet", "Vodafone PT" },
|
||||
{ 26803, "internet", "NOS" },
|
||||
{ 26806, "internet", "MEO" },
|
||||
|
||||
// =========================================================================
|
||||
// Ireland (MCC 272)
|
||||
// =========================================================================
|
||||
{ 27201, "internet", "Vodafone IE" },
|
||||
{ 27202, "open.internet", "Three IE" },
|
||||
{ 27205, "three.ie", "Three IE" },
|
||||
|
||||
// =========================================================================
|
||||
// IoT / Global SIMs
|
||||
// =========================================================================
|
||||
{ 901028, "iot.1nce.net", "1NCE (IoT)" },
|
||||
{ 90143, "hologram", "Hologram" },
|
||||
};
|
||||
|
||||
#define APN_DATABASE_SIZE (sizeof(APN_DATABASE) / sizeof(APN_DATABASE[0]))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lookup function — returns nullptr if not found
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
inline const ApnEntry* apnLookup(uint32_t mccmnc) {
|
||||
for (int i = 0; i < (int)APN_DATABASE_SIZE; i++) {
|
||||
if (APN_DATABASE[i].mccmnc == mccmnc) {
|
||||
return &APN_DATABASE[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parse IMSI string into MCC+MNC. Tries 3-digit MNC first (6-digit mccmnc),
|
||||
// falls back to 2-digit MNC (5-digit mccmnc) if not found.
|
||||
inline const ApnEntry* apnLookupFromIMSI(const char* imsi) {
|
||||
if (!imsi || strlen(imsi) < 5) return nullptr;
|
||||
|
||||
// Extract MCC (always 3 digits)
|
||||
uint32_t mcc = (imsi[0] - '0') * 100 + (imsi[1] - '0') * 10 + (imsi[2] - '0');
|
||||
|
||||
// Try 3-digit MNC first (more specific)
|
||||
if (strlen(imsi) >= 6) {
|
||||
uint32_t mnc3 = (imsi[3] - '0') * 100 + (imsi[4] - '0') * 10 + (imsi[5] - '0');
|
||||
uint32_t mccmnc6 = mcc * 1000 + mnc3;
|
||||
const ApnEntry* entry = apnLookup(mccmnc6);
|
||||
if (entry) return entry;
|
||||
}
|
||||
|
||||
// Fall back to 2-digit MNC
|
||||
uint32_t mnc2 = (imsi[3] - '0') * 10 + (imsi[4] - '0');
|
||||
uint32_t mccmnc5 = mcc * 100 + mnc2;
|
||||
return apnLookup(mccmnc5);
|
||||
}
|
||||
|
||||
#endif // APN_DATABASE_H
|
||||
#endif // HAS_4G_MODEM
|
||||
@@ -14,7 +14,7 @@
|
||||
// Maximum messages to store in history
|
||||
#define CHANNEL_MSG_HISTORY_SIZE 300
|
||||
#define CHANNEL_MSG_TEXT_LEN 160
|
||||
#define MSG_PATH_MAX 20 // Max repeater hops stored per message
|
||||
#define MSG_PATH_MAX 8 // Max repeater hops stored per message
|
||||
|
||||
#ifndef MAX_GROUP_CHANNELS
|
||||
#define MAX_GROUP_CHANNELS 20
|
||||
@@ -24,7 +24,7 @@
|
||||
// On-disk format for message persistence (SD card)
|
||||
// ---------------------------------------------------------------------------
|
||||
#define MSG_FILE_MAGIC 0x4D434853 // "MCHS" - MeshCore History Store
|
||||
#define MSG_FILE_VERSION 3
|
||||
#define MSG_FILE_VERSION 2
|
||||
#define MSG_FILE_PATH "/meshcore/messages.bin"
|
||||
|
||||
struct __attribute__((packed)) MsgFileHeader {
|
||||
@@ -44,7 +44,7 @@ struct __attribute__((packed)) MsgFileRecord {
|
||||
uint8_t reserved;
|
||||
uint8_t path[MSG_PATH_MAX]; // Repeater hop hashes (first byte of pub key)
|
||||
char text[CHANNEL_MSG_TEXT_LEN];
|
||||
// 188 bytes total
|
||||
// 176 bytes total
|
||||
};
|
||||
|
||||
class UITask; // Forward declaration
|
||||
@@ -74,17 +74,23 @@ private:
|
||||
uint8_t _viewChannelIdx; // Which channel we're currently viewing
|
||||
bool _sdReady; // SD card is available for persistence
|
||||
bool _showPathOverlay; // Show path detail overlay for last received msg
|
||||
int _pathOverlayScroll; // Scroll offset for hop list in path overlay
|
||||
|
||||
// Per-channel unread message counts (standalone mode)
|
||||
// Index 0..MAX_GROUP_CHANNELS-1 for channel messages
|
||||
// Index MAX_GROUP_CHANNELS for DMs (channel_idx == 0xFF)
|
||||
int _unread[MAX_GROUP_CHANNELS + 1];
|
||||
|
||||
public:
|
||||
ChannelScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0),
|
||||
_msgsPerPage(6), _viewChannelIdx(0), _sdReady(false), _showPathOverlay(false), _pathOverlayScroll(0) {
|
||||
_msgsPerPage(6), _viewChannelIdx(0), _sdReady(false), _showPathOverlay(false) {
|
||||
// Initialize all messages as invalid
|
||||
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
|
||||
_messages[i].valid = false;
|
||||
memset(_messages[i].path, 0, MSG_PATH_MAX);
|
||||
}
|
||||
// Initialize unread counts
|
||||
memset(_unread, 0, sizeof(_unread));
|
||||
}
|
||||
|
||||
void setSDReady(bool ready) { _sdReady = ready; }
|
||||
@@ -119,7 +125,15 @@ public:
|
||||
// Reset scroll to show newest message
|
||||
_scrollPos = 0;
|
||||
_showPathOverlay = false; // Dismiss overlay on new message
|
||||
_pathOverlayScroll = 0;
|
||||
|
||||
// Track unread count for this channel (only for received messages, not sent)
|
||||
// path_len == 0 means locally sent
|
||||
if (path_len != 0) {
|
||||
int unreadSlot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
|
||||
if (unreadSlot >= 0 && unreadSlot <= MAX_GROUP_CHANNELS) {
|
||||
_unread[unreadSlot]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist to SD card
|
||||
saveToSD();
|
||||
@@ -139,9 +153,42 @@ public:
|
||||
int getMessageCount() const { return _msgCount; }
|
||||
|
||||
uint8_t getViewChannelIdx() const { return _viewChannelIdx; }
|
||||
void setViewChannelIdx(uint8_t idx) { _viewChannelIdx = idx; _scrollPos = 0; _showPathOverlay = false; _pathOverlayScroll = 0; }
|
||||
void setViewChannelIdx(uint8_t idx) {
|
||||
_viewChannelIdx = idx;
|
||||
_scrollPos = 0;
|
||||
_showPathOverlay = false;
|
||||
markChannelRead(idx);
|
||||
}
|
||||
bool isShowingPathOverlay() const { return _showPathOverlay; }
|
||||
|
||||
// --- Unread message tracking (standalone mode) ---
|
||||
|
||||
// Mark all messages for a channel as read
|
||||
void markChannelRead(uint8_t channel_idx) {
|
||||
int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
|
||||
if (slot >= 0 && slot <= MAX_GROUP_CHANNELS) {
|
||||
_unread[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Get unread count for a specific channel
|
||||
int getUnreadForChannel(uint8_t channel_idx) const {
|
||||
int slot = (channel_idx == 0xFF) ? MAX_GROUP_CHANNELS : channel_idx;
|
||||
if (slot >= 0 && slot <= MAX_GROUP_CHANNELS) {
|
||||
return _unread[slot];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get total unread across all channels
|
||||
int getTotalUnread() const {
|
||||
int total = 0;
|
||||
for (int i = 0; i <= MAX_GROUP_CHANNELS; i++) {
|
||||
total += _unread[i];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Find the newest RECEIVED message for the current channel
|
||||
// (path_len != 0 means received, path_len 0 = locally sent)
|
||||
ChannelMessage* getNewestReceivedMsg() {
|
||||
@@ -162,7 +209,7 @@ public:
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Save the entire message buffer to SD card.
|
||||
// File: /meshcore/messages.bin (~56 KB for 300 messages)
|
||||
// File: /meshcore/messages.bin (~50 KB for 300 messages)
|
||||
void saveToSD() {
|
||||
#if defined(HAS_SDCARD) && defined(ESP32)
|
||||
if (!_sdReady) return;
|
||||
@@ -362,25 +409,12 @@ public:
|
||||
}
|
||||
y += lineH + 2;
|
||||
|
||||
// Show each hop resolved against contacts (scrollable)
|
||||
// Show each hop resolved against contacts
|
||||
if (plen > 0 && plen != 0xFF) {
|
||||
int displayHops = plen < MSG_PATH_MAX ? plen : MSG_PATH_MAX;
|
||||
int footerHeight = 14;
|
||||
int scrollBarW = 4;
|
||||
int maxY = display.height() - footerHeight;
|
||||
int maxY = display.height() - 26;
|
||||
|
||||
// Calculate how many hops fit in the visible area
|
||||
int hopsAreaTop = y;
|
||||
int visibleHops = (maxY - y) / lineH;
|
||||
if (visibleHops < 1) visibleHops = 1;
|
||||
|
||||
// Clamp scroll position
|
||||
int maxScroll = displayHops > visibleHops ? displayHops - visibleHops : 0;
|
||||
if (_pathOverlayScroll > maxScroll) _pathOverlayScroll = maxScroll;
|
||||
|
||||
int startHop = _pathOverlayScroll;
|
||||
|
||||
for (int h = startHop; h < displayHops && y + lineH <= maxY; h++) {
|
||||
for (int h = 0; h < displayHops && y + lineH <= maxY; h++) {
|
||||
uint8_t hopHash = msg->path[h];
|
||||
display.setCursor(0, y);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -423,24 +457,6 @@ public:
|
||||
}
|
||||
y += lineH;
|
||||
}
|
||||
|
||||
// --- Scroll bar for hop list ---
|
||||
if (displayHops > visibleHops) {
|
||||
int sbX = display.width() - scrollBarW;
|
||||
int sbTop = hopsAreaTop;
|
||||
int sbHeight = maxY - hopsAreaTop;
|
||||
|
||||
// Draw track outline
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(sbX, sbTop, scrollBarW, sbHeight);
|
||||
|
||||
// Draw proportional thumb
|
||||
int thumbH = (visibleHops * sbHeight) / displayHops;
|
||||
if (thumbH < 4) thumbH = 4;
|
||||
int thumbY = sbTop + (_pathOverlayScroll * (sbHeight - thumbH)) / maxScroll;
|
||||
for (int ty = thumbY + 1; ty < thumbY + thumbH - 1; ty++)
|
||||
display.drawRect(sbX + 1, ty, scrollBarW - 2, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +466,7 @@ public:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setCursor(0, footerY);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("Q:Back W/S:Scroll");
|
||||
display.print("Q:Back");
|
||||
|
||||
#if AUTO_OFF_MILLIS == 0
|
||||
return 5000;
|
||||
@@ -709,18 +725,6 @@ public:
|
||||
_showPathOverlay = false;
|
||||
return true;
|
||||
}
|
||||
// W - scroll up in hop list
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_pathOverlayScroll > 0) {
|
||||
_pathOverlayScroll--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// S - scroll down in hop list
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
_pathOverlayScroll++; // Clamped during render
|
||||
return true;
|
||||
}
|
||||
return true; // Consume all keys while overlay is up
|
||||
}
|
||||
|
||||
@@ -730,7 +734,6 @@ public:
|
||||
if (c == 'v' || c == 'V') {
|
||||
if (getNewestReceivedMsg() != nullptr) {
|
||||
_showPathOverlay = true;
|
||||
_pathOverlayScroll = 0;
|
||||
return true;
|
||||
}
|
||||
return false; // No received messages to show
|
||||
@@ -767,6 +770,7 @@ public:
|
||||
}
|
||||
}
|
||||
_scrollPos = 0;
|
||||
markChannelRead(_viewChannelIdx);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -780,6 +784,7 @@ public:
|
||||
_viewChannelIdx = 0;
|
||||
}
|
||||
_scrollPos = 0;
|
||||
markChannelRead(_viewChannelIdx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ ModemManager modemManager;
|
||||
#define AT_BUF_SIZE 512
|
||||
static char _atBuf[AT_BUF_SIZE];
|
||||
|
||||
// Config file paths
|
||||
#define MODEM_CONFIG_FILE "/sms/modem.cfg"
|
||||
#define APN_CONFIG_FILE "/sms/apn.cfg"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API - SMS (unchanged)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -30,6 +34,10 @@ void ModemManager::begin() {
|
||||
_callPhone[0] = '\0';
|
||||
_callStartTime = 0;
|
||||
_urcPos = 0;
|
||||
_imei[0] = '\0';
|
||||
_imsi[0] = '\0';
|
||||
_apn[0] = '\0';
|
||||
strcpy(_apnSource, "none");
|
||||
|
||||
// Create FreeRTOS primitives
|
||||
_sendQueue = xQueueCreate(MODEM_SEND_QUEUE_SIZE, sizeof(SMSOutgoing));
|
||||
@@ -192,8 +200,6 @@ 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) {
|
||||
@@ -217,6 +223,112 @@ void ModemManager::saveEnabledConfig(bool enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// APN Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ModemManager::setAPN(const char* apn) {
|
||||
strncpy(_apn, apn, sizeof(_apn) - 1);
|
||||
_apn[sizeof(_apn) - 1] = '\0';
|
||||
strcpy(_apnSource, "user");
|
||||
saveAPNConfig(apn);
|
||||
MESH_DEBUG_PRINTLN("[Modem] APN set by user: %s", _apn);
|
||||
}
|
||||
|
||||
bool ModemManager::loadAPNConfig(char* apnOut, int maxLen) {
|
||||
File f = SD.open(APN_CONFIG_FILE, FILE_READ);
|
||||
if (!f) { return false; }
|
||||
String line = f.readStringUntil('\n');
|
||||
f.close();
|
||||
line.trim();
|
||||
if (line.length() == 0) return false;
|
||||
strncpy(apnOut, line.c_str(), maxLen - 1);
|
||||
apnOut[maxLen - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModemManager::saveAPNConfig(const char* apn) {
|
||||
if (!SD.exists("/sms")) SD.mkdir("/sms");
|
||||
File f = SD.open(APN_CONFIG_FILE, FILE_WRITE);
|
||||
if (f) {
|
||||
f.println(apn);
|
||||
f.close();
|
||||
Serial.printf("[Modem] APN config saved: %s\n", apn);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// APN Resolution — called during init after network registration
|
||||
//
|
||||
// Priority:
|
||||
// 1. User-configured APN (from /sms/apn.cfg)
|
||||
// 2. Network-provisioned APN (AT+CGDCONT? — modem already has one)
|
||||
// 3. Auto-detected from IMSI via embedded ApnDatabase
|
||||
// 4. Blank (some carriers work with empty APN)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void ModemManager::resolveAPN() {
|
||||
// 1. Check for user-configured APN on SD card
|
||||
char userApn[64];
|
||||
if (loadAPNConfig(userApn, sizeof(userApn))) {
|
||||
strncpy(_apn, userApn, sizeof(_apn) - 1);
|
||||
strcpy(_apnSource, "user");
|
||||
MESH_DEBUG_PRINTLN("[Modem] APN from user config: %s", _apn);
|
||||
|
||||
// Apply to modem
|
||||
char cmd[80];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CGDCONT=1,\"IP\",\"%s\"", _apn);
|
||||
sendAT(cmd, "OK", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check if modem already has a network-provisioned APN
|
||||
if (sendAT("AT+CGDCONT?", "OK", 3000)) {
|
||||
// Response: +CGDCONT: 1,"IP","telstra.internet",,0,0
|
||||
char* p = strstr(_atBuf, "+CGDCONT:");
|
||||
if (p) {
|
||||
char* q1 = strchr(p, '"'); // first quote (before IP)
|
||||
if (q1) q1 = strchr(q1 + 1, '"'); // close quote of IP
|
||||
if (q1) q1 = strchr(q1 + 1, '"'); // open quote of APN
|
||||
if (q1) {
|
||||
q1++;
|
||||
char* q2 = strchr(q1, '"');
|
||||
if (q2 && q2 > q1) {
|
||||
int len = q2 - q1;
|
||||
if (len > 0 && len < (int)sizeof(_apn)) {
|
||||
memcpy(_apn, q1, len);
|
||||
_apn[len] = '\0';
|
||||
strcpy(_apnSource, "network");
|
||||
MESH_DEBUG_PRINTLN("[Modem] APN from network/modem: %s", _apn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Auto-detect from IMSI using embedded database
|
||||
if (_imsi[0]) {
|
||||
const ApnEntry* entry = apnLookupFromIMSI(_imsi);
|
||||
if (entry) {
|
||||
strncpy(_apn, entry->apn, sizeof(_apn) - 1);
|
||||
strcpy(_apnSource, "auto");
|
||||
MESH_DEBUG_PRINTLN("[Modem] APN auto-detected: %s (%s)", _apn, entry->carrier);
|
||||
|
||||
// Apply to modem
|
||||
char cmd[80];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CGDCONT=1,\"IP\",\"%s\"", _apn);
|
||||
sendAT(cmd, "OK", 3000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. No APN found — leave blank
|
||||
_apn[0] = '\0';
|
||||
strcpy(_apnSource, "none");
|
||||
MESH_DEBUG_PRINTLN("[Modem] APN: none detected (IMSI=%s)", _imsi[0] ? _imsi : "unknown");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// URC (Unsolicited Result Code) Handling
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -537,6 +649,28 @@ restart:
|
||||
// Disable echo
|
||||
sendAT("ATE0", "OK");
|
||||
|
||||
// --- Query device identity ---
|
||||
// IMEI (International Mobile Equipment Identity)
|
||||
if (sendAT("AT+GSN", "OK", 3000)) {
|
||||
// Response is just the IMEI number on its own line
|
||||
char* p = _atBuf;
|
||||
while (*p && !isdigit(*p)) p++; // skip to first digit
|
||||
int i = 0;
|
||||
while (isdigit(p[i]) && i < 19) { _imei[i] = p[i]; i++; }
|
||||
_imei[i] = '\0';
|
||||
MESH_DEBUG_PRINTLN("[Modem] IMEI: %s", _imei);
|
||||
}
|
||||
|
||||
// IMSI (International Mobile Subscriber Identity) — for APN auto-detection
|
||||
if (sendAT("AT+CIMI", "OK", 3000)) {
|
||||
char* p = _atBuf;
|
||||
while (*p && !isdigit(*p)) p++;
|
||||
int i = 0;
|
||||
while (isdigit(p[i]) && i < 19) { _imsi[i] = p[i]; i++; }
|
||||
_imsi[i] = '\0';
|
||||
MESH_DEBUG_PRINTLN("[Modem] IMSI: %s", _imsi);
|
||||
}
|
||||
|
||||
// Set SMS text mode
|
||||
sendAT("AT+CMGF=1", "OK");
|
||||
|
||||
@@ -589,6 +723,10 @@ restart:
|
||||
}
|
||||
|
||||
// Query operator name
|
||||
// AT+COPS=3,0 sets the format to "long alphanumeric" so AT+COPS?
|
||||
// returns "Optus" instead of "50502"
|
||||
sendAT("AT+COPS=3,0", "OK", 2000);
|
||||
|
||||
if (sendAT("AT+COPS?", "OK", 5000)) {
|
||||
char* p = strchr(_atBuf, '"');
|
||||
if (p) {
|
||||
@@ -604,9 +742,28 @@ restart:
|
||||
}
|
||||
}
|
||||
|
||||
// If operator is still numeric (all digits), look up friendly name from IMSI
|
||||
if (_operator[0] && isdigit(_operator[0])) {
|
||||
bool allDigits = true;
|
||||
for (int i = 0; _operator[i]; i++) {
|
||||
if (!isdigit(_operator[i])) { allDigits = false; break; }
|
||||
}
|
||||
if (allDigits && _imsi[0]) {
|
||||
const ApnEntry* entry = apnLookupFromIMSI(_imsi);
|
||||
if (entry && entry->carrier) {
|
||||
strncpy(_operator, entry->carrier, sizeof(_operator) - 1);
|
||||
_operator[sizeof(_operator) - 1] = '\0';
|
||||
MESH_DEBUG_PRINTLN("[Modem] operator (from IMSI lookup): %s", _operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial signal query
|
||||
pollCSQ();
|
||||
|
||||
// Resolve APN (user config → network provisioned → IMSI auto-detect)
|
||||
resolveAPN();
|
||||
|
||||
// Sync ESP32 system clock from modem network time
|
||||
bool clockSet = false;
|
||||
for (int attempt = 0; attempt < 5 && !clockSet; attempt++) {
|
||||
@@ -653,7 +810,8 @@ restart:
|
||||
sendAT("AT+CMGD=1,4", "OK", 5000);
|
||||
|
||||
_state = ModemState::READY;
|
||||
MESH_DEBUG_PRINTLN("[Modem] READY (CSQ=%d, operator=%s)", _csq, _operator);
|
||||
MESH_DEBUG_PRINTLN("[Modem] READY (CSQ=%d, operator=%s, APN=%s [%s], IMEI=%s)",
|
||||
_csq, _operator, _apn[0] ? _apn : "(none)", _apnSource, _imei);
|
||||
|
||||
// ---- Phase 4: Main loop ----
|
||||
unsigned long lastCSQPoll = 0;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "variant.h"
|
||||
#include "ApnDatabase.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modem pins (from variant.h, always defined for reference)
|
||||
@@ -158,6 +159,20 @@ public:
|
||||
const char* getCallPhone() const { return _callPhone; }
|
||||
uint32_t getCallStartTime() const { return _callStartTime; }
|
||||
|
||||
// --- Device info (populated during init) ---
|
||||
const char* getIMEI() const { return _imei; }
|
||||
const char* getIMSI() const { return _imsi; }
|
||||
const char* getAPN() const { return _apn; }
|
||||
const char* getAPNSource() const { return _apnSource; } // "auto", "network", "user", "none"
|
||||
|
||||
// --- APN configuration ---
|
||||
// Set APN manually (overrides auto-detection). Persists to SD.
|
||||
void setAPN(const char* apn);
|
||||
// Load user-configured APN from SD card. Returns true if found.
|
||||
static bool loadAPNConfig(char* apnOut, int maxLen);
|
||||
// Save user-configured APN to SD card.
|
||||
static void saveAPNConfig(const char* apn);
|
||||
|
||||
// Pause/resume polling — used by web reader to avoid Core 0 contention
|
||||
// during WiFi TLS handshakes. While paused, the task skips AT commands
|
||||
// (SMS poll, CSQ poll) but still drains URCs and handles call commands
|
||||
@@ -178,6 +193,12 @@ private:
|
||||
volatile bool _paused = false; // Suppresses AT polling when true
|
||||
char _operator[24] = {0};
|
||||
|
||||
// Device identity (populated during Phase 2 init)
|
||||
char _imei[20] = {0}; // IMEI from AT+GSN
|
||||
char _imsi[20] = {0}; // IMSI from AT+CIMI (for APN lookup)
|
||||
char _apn[64] = {0}; // Active APN
|
||||
char _apnSource[8] = {0}; // "auto", "network", "user", "none"
|
||||
|
||||
// Call state (written by modem task, read by main loop)
|
||||
char _callPhone[SMS_PHONE_LEN] = {0}; // Current call number
|
||||
volatile uint32_t _callStartTime = 0; // millis() when call connected
|
||||
@@ -211,6 +232,9 @@ private:
|
||||
void drainURCs(); // Read available UART data, process complete lines
|
||||
void processURCLine(const char* line); // Handle a single URC line
|
||||
|
||||
// APN resolution (called from modem task during init)
|
||||
void resolveAPN(); // Auto-detect APN from network/IMSI/user config
|
||||
|
||||
// Call control (called from modem task)
|
||||
bool doDialCall(const char* phone);
|
||||
bool doAnswerCall();
|
||||
|
||||
@@ -69,6 +69,11 @@ enum SettingsRowType : uint8_t {
|
||||
ROW_INFO_HEADER, // "--- Info ---" separator
|
||||
ROW_PUB_KEY, // Public key display
|
||||
ROW_FIRMWARE, // Firmware version
|
||||
#ifdef HAS_4G_MODEM
|
||||
ROW_IMEI, // IMEI display (read-only)
|
||||
ROW_OPERATOR_INFO, // Carrier/operator display (read-only)
|
||||
ROW_APN, // APN setting (editable)
|
||||
#endif
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -83,7 +88,11 @@ enum EditMode : uint8_t {
|
||||
};
|
||||
|
||||
// Max rows in the settings list
|
||||
#ifdef HAS_4G_MODEM
|
||||
#define SETTINGS_MAX_ROWS 46 // Extra rows for IMEI, Carrier, APN
|
||||
#else
|
||||
#define SETTINGS_MAX_ROWS 40
|
||||
#endif
|
||||
#define SETTINGS_TEXT_BUF 33 // 32 chars + null
|
||||
|
||||
class SettingsScreen : public UIScreen {
|
||||
@@ -160,6 +169,12 @@ private:
|
||||
addRow(ROW_PUB_KEY);
|
||||
addRow(ROW_FIRMWARE);
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
addRow(ROW_IMEI);
|
||||
addRow(ROW_OPERATOR_INFO);
|
||||
addRow(ROW_APN);
|
||||
#endif
|
||||
|
||||
// Clamp cursor
|
||||
if (_cursor >= _numRows) _cursor = _numRows - 1;
|
||||
if (_cursor < 0) _cursor = 0;
|
||||
@@ -177,7 +192,11 @@ private:
|
||||
bool isSelectable(int idx) const {
|
||||
if (idx < 0 || idx >= _numRows) return false;
|
||||
SettingsRowType t = _rows[idx].type;
|
||||
return t != ROW_CH_HEADER && t != ROW_INFO_HEADER;
|
||||
return t != ROW_CH_HEADER && t != ROW_INFO_HEADER
|
||||
#ifdef HAS_4G_MODEM
|
||||
&& t != ROW_IMEI && t != ROW_OPERATOR_INFO
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
void skipNonSelectable(int dir) {
|
||||
@@ -548,7 +567,7 @@ public:
|
||||
// Show first 8 bytes of pub key as hex (16 chars)
|
||||
char hexBuf[17];
|
||||
mesh::Utils::toHex(hexBuf, the_mesh.self_id.pub_key, 8);
|
||||
snprintf(tmp, sizeof(tmp), "ID: %s", hexBuf);
|
||||
snprintf(tmp, sizeof(tmp), "Node ID: %s", hexBuf);
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
@@ -557,6 +576,53 @@ public:
|
||||
snprintf(tmp, sizeof(tmp), "FW: %s", FIRMWARE_VERSION);
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
case ROW_IMEI: {
|
||||
const char* imei = modemManager.getIMEI();
|
||||
snprintf(tmp, sizeof(tmp), "IMEI: %s", imei[0] ? imei : "(unavailable)");
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
|
||||
case ROW_OPERATOR_INFO: {
|
||||
const char* op = modemManager.getOperator();
|
||||
int bars = modemManager.getSignalBars();
|
||||
if (op[0]) {
|
||||
// Show carrier name with signal bar count
|
||||
snprintf(tmp, sizeof(tmp), "Carrier: %s (%d/5)", op, bars);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "Carrier: (searching)");
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
|
||||
case ROW_APN: {
|
||||
if (editing && _editMode == EDIT_TEXT) {
|
||||
snprintf(tmp, sizeof(tmp), "APN: %s_", _editBuf);
|
||||
} else {
|
||||
const char* apn = modemManager.getAPN();
|
||||
const char* src = modemManager.getAPNSource();
|
||||
if (apn[0]) {
|
||||
// Truncate APN to fit: "APN: " (5) + apn (max 28) + " [x]" (4) = ~37 chars
|
||||
char apnShort[29];
|
||||
strncpy(apnShort, apn, 28);
|
||||
apnShort[28] = '\0';
|
||||
// Abbreviate source: auto→A, network→N, user→U, none→?
|
||||
char srcChar = '?';
|
||||
if (strcmp(src, "auto") == 0) srcChar = 'A';
|
||||
else if (strcmp(src, "network") == 0) srcChar = 'N';
|
||||
else if (strcmp(src, "user") == 0) srcChar = 'U';
|
||||
snprintf(tmp, sizeof(tmp), "APN: %s [%c]", apnShort, srcChar);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "APN: (none)");
|
||||
}
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
y += lineHeight;
|
||||
@@ -673,6 +739,20 @@ public:
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
}
|
||||
#ifdef HAS_4G_MODEM
|
||||
else if (type == ROW_APN) {
|
||||
// Save the edited APN (even if empty — clears user override)
|
||||
if (_editPos > 0) {
|
||||
modemManager.setAPN(_editBuf);
|
||||
Serial.printf("Settings: APN set to '%s'\n", _editBuf);
|
||||
} else {
|
||||
// Empty APN: remove user override, revert to auto-detection
|
||||
ModemManager::saveAPNConfig("");
|
||||
Serial.println("Settings: APN cleared (will auto-detect on next boot)");
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
if (c == 'q' || c == 'Q' || c == 27) {
|
||||
@@ -876,6 +956,12 @@ public:
|
||||
Serial.println("Settings: 4G modem DISABLED (shutdown)");
|
||||
}
|
||||
break;
|
||||
case ROW_APN: {
|
||||
// Start text editing with current APN as initial value
|
||||
const char* currentApn = modemManager.getAPN();
|
||||
startEditText(currentApn);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case ROW_ADD_CHANNEL:
|
||||
startEditText("");
|
||||
|
||||
@@ -297,7 +297,7 @@ public:
|
||||
int y = 20;
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "MSG: %d", _task->getMsgCount());
|
||||
sprintf(tmp, "MSG: %d", _task->getUnreadMsgCount());
|
||||
display.drawTextCentered(display.width() / 2, y, tmp);
|
||||
y += 18;
|
||||
|
||||
@@ -985,6 +985,13 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
// Add to channel history screen with channel index and path data
|
||||
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path);
|
||||
|
||||
// If user is currently viewing this channel, mark it as read immediately
|
||||
// (they can see the message arrive in real-time)
|
||||
if (isOnChannelScreen() &&
|
||||
((ChannelScreen *) channel_screen)->getViewChannelIdx() == channel_idx) {
|
||||
((ChannelScreen *) channel_screen)->markChannelRead(channel_idx);
|
||||
}
|
||||
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
// T-Deck Pro: Don't interrupt user with popup - just show brief notification
|
||||
// Messages are stored in channel history, accessible via 'M' key
|
||||
@@ -1365,6 +1372,10 @@ bool UITask::isEditingHomeScreen() const {
|
||||
|
||||
void UITask::gotoChannelScreen() {
|
||||
((ChannelScreen *) channel_screen)->resetScroll();
|
||||
// Mark the currently viewed channel as read
|
||||
((ChannelScreen *) channel_screen)->markChannelRead(
|
||||
((ChannelScreen *) channel_screen)->getViewChannelIdx()
|
||||
);
|
||||
setCurrScreen(channel_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
@@ -1462,6 +1473,10 @@ uint8_t UITask::getChannelScreenViewIdx() const {
|
||||
return ((ChannelScreen *) channel_screen)->getViewChannelIdx();
|
||||
}
|
||||
|
||||
int UITask::getUnreadMsgCount() const {
|
||||
return ((ChannelScreen *) channel_screen)->getTotalUnread();
|
||||
}
|
||||
|
||||
void UITask::addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) {
|
||||
// Format the message as "Sender: message"
|
||||
char formattedMsg[CHANNEL_MSG_TEXT_LEN];
|
||||
|
||||
@@ -115,6 +115,7 @@ public:
|
||||
void showAlert(const char* text, int duration_millis) override;
|
||||
void forceRefresh() override { _next_refresh = 100; }
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
int getUnreadMsgCount() const; // Per-channel unread tracking (standalone)
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool isButtonPressed() const;
|
||||
bool isOnChannelScreen() const { return curr == channel_screen; }
|
||||
|
||||
@@ -80,7 +80,7 @@ build_flags =
|
||||
-D PIN_DISPLAY_BL=45
|
||||
-D PIN_USER_BTN=0
|
||||
-D CST328_PIN_RST=38
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.4A"'
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.3A"'
|
||||
-D ARDUINO_LOOP_STACK_SIZE=32768
|
||||
build_src_filter = ${esp32_base.build_src_filter}
|
||||
+<../variants/LilyGo_TDeck_Pro>
|
||||
@@ -127,8 +127,8 @@ extends = LilyGo_TDeck_Pro
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=400
|
||||
-D MAX_GROUP_CHANNELS=20
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D MECK_AUDIO_VARIANT
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
@@ -166,14 +166,17 @@ lib_deps =
|
||||
${LilyGo_TDeck_Pro.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
; 4G standalone (4G modem hardware, no BLE — maximum battery + modem features)
|
||||
; 4G standalone (4G modem hardware, no BLE — maximum battery + cellular features)
|
||||
; No BLE_PIN_CODE: BLE never initializes, saving ~30KB heap + radio power.
|
||||
; MECK_WEB_READER enabled: works better without BLE (no teardown dance needed,
|
||||
; more free heap from boot). WiFi-first with cellular PPP fallback (future).
|
||||
[env:meck_4g_standalone]
|
||||
extends = LilyGo_TDeck_Pro
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=400
|
||||
-D MAX_GROUP_CHANNELS=20
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D HAS_4G_MODEM=1
|
||||
-D MECK_WEB_READER=1
|
||||
|
||||
Reference in New Issue
Block a user