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:
pelgraine
2026-02-25 19:59:32 +11:00
parent 2a72723eff
commit 049017cd2d
8 changed files with 732 additions and 68 deletions
@@ -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
+61 -56
View File
@@ -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("");
+16 -1
View File
@@ -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];
+1
View File
@@ -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; }
+9 -6
View File
@@ -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