mirror of
https://github.com/pelgraine/Meck.git
synced 2026-07-05 17:21:22 +02:00
tdpro remote 4g repeater admin via web app
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
|
||||
@@ -0,0 +1,235 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// CellularMQTT — A7682E Modem + MQTT via native AT commands
|
||||
//
|
||||
// Stripped-down modem driver for the remote repeater use case. No SMS, no
|
||||
// voice calls. Just: power on → register → activate data → MQTT connect.
|
||||
//
|
||||
// Uses the A7682E's built-in MQTT client with TLS (AT+CMQTT* commands).
|
||||
// The UART stays in AT command mode permanently — no PPP needed.
|
||||
//
|
||||
// Runs on a FreeRTOS task (Core 0) to avoid blocking the mesh radio loop.
|
||||
// Commands from the MQTT dashboard arrive as queued strings for the main
|
||||
// loop to process via CommonCLI; responses are queued back for publishing.
|
||||
//
|
||||
// Guard: HAS_4G_MODEM (variant.h provides pin definitions)
|
||||
// =============================================================================
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
|
||||
#ifndef CELLULAR_MQTT_H
|
||||
#define CELLULAR_MQTT_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "variant.h"
|
||||
#include "ApnDatabase.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
#define MQTT_TOPIC_MAX 80
|
||||
#define MQTT_PAYLOAD_MAX 512
|
||||
#define MQTT_CLIENT_ID_MAX 32
|
||||
|
||||
// Queue sizes
|
||||
#define CMD_QUEUE_SIZE 4 // MQTT → main loop (CLI commands)
|
||||
#define RSP_QUEUE_SIZE 4 // main loop → MQTT (CLI responses)
|
||||
|
||||
// Telemetry interval (ms)
|
||||
#define TELEMETRY_INTERVAL 60000 // 60 seconds
|
||||
|
||||
// Task configuration
|
||||
#define CELL_TASK_PRIORITY 1
|
||||
#define CELL_TASK_STACK_SIZE 8192 // MQTT + TLS AT commands need headroom
|
||||
#define CELL_TASK_CORE 0
|
||||
|
||||
// Reconnect timing
|
||||
#define MQTT_RECONNECT_MIN 5000 // 5 seconds
|
||||
#define MQTT_RECONNECT_MAX 300000 // 5 minutes (exponential backoff cap)
|
||||
|
||||
// Consecutive publish failures before forced reconnect
|
||||
#define MQTT_PUB_FAIL_MAX 5
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State machine
|
||||
// ---------------------------------------------------------------------------
|
||||
enum class CellState : uint8_t {
|
||||
OFF,
|
||||
POWERING_ON,
|
||||
INITIALIZING,
|
||||
REGISTERING,
|
||||
DATA_ACTIVATING, // PDP context
|
||||
MQTT_STARTING, // AT+CMQTTSTART
|
||||
MQTT_CONNECTING, // AT+CMQTTCONNECT
|
||||
CONNECTED, // MQTT up, subscribed, publishing telemetry
|
||||
RECONNECTING, // Link lost, attempting reconnect
|
||||
ERROR
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queue message types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Incoming CLI command (from MQTT subscription → main loop)
|
||||
struct MQTTCommand {
|
||||
char cmd[MQTT_PAYLOAD_MAX];
|
||||
};
|
||||
|
||||
// Outgoing CLI response (from main loop → MQTT publish)
|
||||
struct MQTTResponse {
|
||||
char topic[MQTT_TOPIC_MAX];
|
||||
char payload[MQTT_PAYLOAD_MAX];
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MQTT config (loaded from SD: /remote/mqtt.cfg)
|
||||
// ---------------------------------------------------------------------------
|
||||
struct MQTTConfig {
|
||||
char broker[80]; // e.g. "broker.hivemq.cloud"
|
||||
uint16_t port; // e.g. 8883
|
||||
char username[40];
|
||||
char password[40];
|
||||
char deviceId[MQTT_CLIENT_ID_MAX]; // Auto-generated from MAC if empty
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Telemetry snapshot (filled by main loop, published by modem task)
|
||||
// ---------------------------------------------------------------------------
|
||||
struct TelemetryData {
|
||||
uint32_t uptime_secs;
|
||||
uint16_t battery_mv;
|
||||
uint8_t battery_pct;
|
||||
int16_t temperature; // 0.1°C from BQ27220
|
||||
int csq;
|
||||
uint8_t neighbor_count;
|
||||
float freq;
|
||||
float bw;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t tx_power;
|
||||
char node_name[32];
|
||||
char apn[40];
|
||||
char oper[24];
|
||||
bool mqtt_connected;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CellularMQTT class
|
||||
// ---------------------------------------------------------------------------
|
||||
class CellularMQTT {
|
||||
public:
|
||||
void begin();
|
||||
void stop();
|
||||
|
||||
// --- Queue API (called from main loop) ---
|
||||
bool recvCommand(MQTTCommand& out);
|
||||
bool sendResponse(const char* topic, const char* payload);
|
||||
|
||||
// --- Telemetry (set by main loop, published by modem task) ---
|
||||
void updateTelemetry(const TelemetryData& data);
|
||||
|
||||
// --- State queries (lock-free reads from main loop) ---
|
||||
CellState getState() const { return _state; }
|
||||
bool isConnected() const { return _state == CellState::CONNECTED; }
|
||||
int getCSQ() const { return _csq; }
|
||||
int getSignalBars() const;
|
||||
const char* getOperator() const { return _operator; }
|
||||
const char* getIPAddress() const { return _ipAddr; }
|
||||
const char* getBroker() const { return _config.broker; }
|
||||
const char* getAPN() const { return _apn; }
|
||||
const char* getRspTopic() const { return _topicRsp; }
|
||||
const char* stateString() const;
|
||||
uint32_t getLastCmdTime() const { return _lastCmdTime; }
|
||||
|
||||
// Load config from SD card
|
||||
static bool loadConfig(MQTTConfig& cfg);
|
||||
|
||||
private:
|
||||
volatile CellState _state = CellState::OFF;
|
||||
volatile int _csq = 99;
|
||||
volatile uint32_t _lastCmdTime = 0;
|
||||
|
||||
char _operator[24] = {0};
|
||||
char _ipAddr[20] = {0};
|
||||
char _imei[20] = {0};
|
||||
char _imsi[20] = {0};
|
||||
char _apn[64] = {0};
|
||||
|
||||
MQTTConfig _config = {};
|
||||
TelemetryData _telemetry = {};
|
||||
SemaphoreHandle_t _telemetryMutex = nullptr;
|
||||
|
||||
// Topic strings (built from device ID)
|
||||
char _topicCmd[MQTT_TOPIC_MAX] = {0};
|
||||
char _topicRsp[MQTT_TOPIC_MAX] = {0};
|
||||
char _topicTelem[MQTT_TOPIC_MAX] = {0};
|
||||
char _topicOta[MQTT_TOPIC_MAX] = {0};
|
||||
|
||||
TaskHandle_t _taskHandle = nullptr;
|
||||
QueueHandle_t _cmdQueue = nullptr;
|
||||
QueueHandle_t _rspQueue = nullptr;
|
||||
SemaphoreHandle_t _uartMutex = nullptr;
|
||||
|
||||
// Publish failure counter for health monitoring
|
||||
uint8_t _pubFailCount = 0;
|
||||
|
||||
// AT response buffer
|
||||
static const int AT_BUF_SIZE = 512;
|
||||
char _atBuf[AT_BUF_SIZE];
|
||||
|
||||
// URC accumulation
|
||||
static const int URC_BUF_SIZE = 600;
|
||||
char _urcBuf[URC_BUF_SIZE];
|
||||
int _urcPos = 0;
|
||||
|
||||
// MQTT receive state machine (multi-line URC parsing)
|
||||
enum MqttRxState { RX_IDLE, RX_WAIT_TOPIC, RX_WAIT_PAYLOAD };
|
||||
MqttRxState _rxState = RX_IDLE;
|
||||
int _rxTopicLen = 0;
|
||||
int _rxPayloadLen = 0;
|
||||
char _rxTopic[MQTT_TOPIC_MAX];
|
||||
char _rxPayload[MQTT_PAYLOAD_MAX];
|
||||
|
||||
// Reconnect backoff
|
||||
uint32_t _reconnectDelay = MQTT_RECONNECT_MIN;
|
||||
|
||||
// --- Modem UART helpers (modem task only) ---
|
||||
bool modemPowerOn();
|
||||
bool sendAT(const char* cmd, const char* expect, uint32_t timeout_ms = 2000);
|
||||
bool waitResponse(const char* expect, uint32_t timeout_ms, char* buf = nullptr, size_t bufLen = 0);
|
||||
bool waitPrompt(uint32_t timeout_ms = 5000);
|
||||
void drainURCs();
|
||||
void processURCLine(const char* line);
|
||||
|
||||
// --- Data connection ---
|
||||
void resolveAPN();
|
||||
bool activateData();
|
||||
|
||||
// --- MQTT operations (modem task only) ---
|
||||
bool mqttStart();
|
||||
bool mqttConnect();
|
||||
bool mqttSubscribe(const char* topic);
|
||||
bool mqttPublish(const char* topic, const char* payload);
|
||||
void mqttDisconnect();
|
||||
|
||||
// --- URC handlers ---
|
||||
void handleMqttRxStart(const char* line);
|
||||
void handleMqttRxTopic(const char* data, int len);
|
||||
void handleMqttRxPayload(const char* data, int len);
|
||||
void handleMqttRxEnd();
|
||||
void handleMqttConnLost(const char* line);
|
||||
|
||||
// --- Task ---
|
||||
static void taskEntry(void* param);
|
||||
void taskLoop();
|
||||
};
|
||||
|
||||
extern CellularMQTT cellularMQTT;
|
||||
|
||||
#endif // CELLULAR_MQTT_H
|
||||
#endif // HAS_4G_MODEM
|
||||
+1064
-1927
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,14 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/CommonCLI.h>
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds
|
||||
#ifdef HAS_4G_MODEM
|
||||
#include "CellularMQTT.h"
|
||||
#define AUTO_OFF_DISABLED true
|
||||
#else
|
||||
#define AUTO_OFF_DISABLED false
|
||||
#endif
|
||||
|
||||
#define AUTO_OFF_MILLIS 20000 // 20 seconds (ignored when AUTO_OFF_DISABLED)
|
||||
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
||||
// 'meshcore', 128x13px
|
||||
@@ -28,55 +35,97 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
|
||||
_node_prefs = node_prefs;
|
||||
_display->turnOn();
|
||||
|
||||
// strip off dash and commit hash by changing dash to null terminator
|
||||
// e.g: v1.2.3-abcdef -> v1.2.3
|
||||
char *version = strdup(firmware_version);
|
||||
char *dash = strchr(version, '-');
|
||||
if(dash){
|
||||
*dash = 0;
|
||||
}
|
||||
if (dash) *dash = 0;
|
||||
|
||||
// v1.2.3 (1 Jan 2025)
|
||||
sprintf(_version_info, "%s (%s)", version, build_date);
|
||||
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
|
||||
free(version);
|
||||
}
|
||||
|
||||
void UITask::renderCurrScreen() {
|
||||
char tmp[80];
|
||||
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
|
||||
// meshcore logo
|
||||
if (millis() < BOOT_SCREEN_MILLIS) {
|
||||
// Boot screen — logo + version
|
||||
_display->setColor(DisplayDriver::BLUE);
|
||||
int logoWidth = 128;
|
||||
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
||||
|
||||
// version info
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setTextSize(1);
|
||||
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
||||
_display->setCursor((_display->width() - versionWidth) / 2, 22);
|
||||
_display->print(_version_info);
|
||||
|
||||
// node type
|
||||
#ifdef HAS_4G_MODEM
|
||||
const char* node_type = "< Remote Repeater >";
|
||||
#else
|
||||
const char* node_type = "< Repeater >";
|
||||
#endif
|
||||
uint16_t typeWidth = _display->getTextWidth(node_type);
|
||||
_display->setCursor((_display->width() - typeWidth) / 2, 35);
|
||||
_display->print(node_type);
|
||||
} else { // home screen
|
||||
// node name
|
||||
} else {
|
||||
// Home screen — node info + cellular status
|
||||
_display->setCursor(0, 0);
|
||||
_display->setTextSize(1);
|
||||
_display->setColor(DisplayDriver::GREEN);
|
||||
_display->print(_node_prefs->node_name);
|
||||
|
||||
// freq / sf
|
||||
_display->setCursor(0, 20);
|
||||
_display->setColor(DisplayDriver::YELLOW);
|
||||
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
|
||||
_display->print(tmp);
|
||||
|
||||
// bw / cr
|
||||
_display->setCursor(0, 30);
|
||||
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
|
||||
_display->print(tmp);
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
int y = 44;
|
||||
|
||||
_display->setCursor(0, y);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
sprintf(tmp, "4G: %s", cellularMQTT.stateString());
|
||||
_display->print(tmp);
|
||||
y += 10;
|
||||
|
||||
_display->setCursor(0, y);
|
||||
sprintf(tmp, "CSQ: %d (%d bars)", cellularMQTT.getCSQ(), cellularMQTT.getSignalBars());
|
||||
_display->print(tmp);
|
||||
y += 10;
|
||||
|
||||
const char* oper = cellularMQTT.getOperator();
|
||||
if (oper[0]) {
|
||||
_display->setCursor(0, y);
|
||||
sprintf(tmp, "Op: %.16s", oper);
|
||||
_display->print(tmp);
|
||||
y += 10;
|
||||
}
|
||||
|
||||
_display->setCursor(0, y);
|
||||
_display->setColor(cellularMQTT.isConnected() ? DisplayDriver::GREEN : DisplayDriver::YELLOW);
|
||||
sprintf(tmp, "MQTT: %s", cellularMQTT.isConnected() ? "Connected" : "---");
|
||||
_display->print(tmp);
|
||||
y += 10;
|
||||
|
||||
const char* ip = cellularMQTT.getIPAddress();
|
||||
if (ip[0]) {
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, y);
|
||||
sprintf(tmp, "IP: %s", ip);
|
||||
_display->print(tmp);
|
||||
y += 10;
|
||||
}
|
||||
|
||||
uint32_t upSec = millis() / 1000;
|
||||
uint32_t upH = upSec / 3600;
|
||||
uint32_t upM = (upSec % 3600) / 60;
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->setCursor(0, y);
|
||||
sprintf(tmp, "Up: %luh %lum Heap:%dk", upH, upM, ESP.getFreeHeap() / 1024);
|
||||
_display->print(tmp);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,17 +134,15 @@ void UITask::loop() {
|
||||
if (millis() >= _next_read) {
|
||||
int btnState = digitalRead(PIN_USER_BTN);
|
||||
if (btnState != _prevBtnState) {
|
||||
if (btnState == LOW) { // pressed?
|
||||
if (_display->isOn()) {
|
||||
// TODO: any action ?
|
||||
} else {
|
||||
if (btnState == LOW) {
|
||||
if (!_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
}
|
||||
_prevBtnState = btnState;
|
||||
}
|
||||
_next_read = millis() + 200; // 5 reads per second
|
||||
_next_read = millis() + 200;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -105,10 +152,10 @@ void UITask::loop() {
|
||||
renderCurrScreen();
|
||||
_display->endFrame();
|
||||
|
||||
_next_refresh = millis() + 1000; // refresh every second
|
||||
_next_refresh = millis() + 10000;
|
||||
}
|
||||
if (millis() > _auto_off) {
|
||||
if (!AUTO_OFF_DISABLED && millis() > _auto_off) {
|
||||
_display->turnOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ class UITask {
|
||||
unsigned long _next_read, _next_refresh, _auto_off;
|
||||
int _prevBtnState;
|
||||
NodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
char _version_info[48];
|
||||
|
||||
void renderCurrScreen();
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,860 @@
|
||||
#ifdef HAS_4G_MODEM
|
||||
|
||||
#include "CellularMQTT.h"
|
||||
#include <Mesh.h>
|
||||
#include <SD.h>
|
||||
#include <esp_mac.h>
|
||||
|
||||
CellularMQTT cellularMQTT;
|
||||
|
||||
#define MODEM_SERIAL Serial1
|
||||
#define MODEM_BAUD 115200
|
||||
|
||||
#define CONFIG_FILE "/remote/mqtt.cfg"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::begin() {
|
||||
Serial.println("[Cell] begin()");
|
||||
|
||||
_state = CellState::OFF;
|
||||
_csq = 99;
|
||||
_reconnectDelay = MQTT_RECONNECT_MIN;
|
||||
_pubFailCount = 0;
|
||||
|
||||
if (!loadConfig(_config)) {
|
||||
Serial.println("[Cell] ERROR: No /remote/mqtt.cfg — cannot start MQTT");
|
||||
_state = CellState::ERROR;
|
||||
return;
|
||||
}
|
||||
Serial.printf("[Cell] Config: broker=%s:%d user=%s id=%s\n",
|
||||
_config.broker, _config.port, _config.username, _config.deviceId);
|
||||
|
||||
snprintf(_topicCmd, sizeof(_topicCmd), "meck/%s/cmd", _config.deviceId);
|
||||
snprintf(_topicRsp, sizeof(_topicRsp), "meck/%s/rsp", _config.deviceId);
|
||||
snprintf(_topicTelem, sizeof(_topicTelem), "meck/%s/telemetry", _config.deviceId);
|
||||
snprintf(_topicOta, sizeof(_topicOta), "meck/%s/ota", _config.deviceId);
|
||||
|
||||
_cmdQueue = xQueueCreate(CMD_QUEUE_SIZE, sizeof(MQTTCommand));
|
||||
_rspQueue = xQueueCreate(RSP_QUEUE_SIZE, sizeof(MQTTResponse));
|
||||
_uartMutex = xSemaphoreCreateMutex();
|
||||
_telemetryMutex = xSemaphoreCreateMutex();
|
||||
|
||||
xTaskCreatePinnedToCore(taskEntry, "cell", CELL_TASK_STACK_SIZE,
|
||||
this, CELL_TASK_PRIORITY, &_taskHandle, CELL_TASK_CORE);
|
||||
}
|
||||
|
||||
void CellularMQTT::stop() {
|
||||
if (!_taskHandle) return;
|
||||
mqttDisconnect();
|
||||
vTaskDelete(_taskHandle);
|
||||
_taskHandle = nullptr;
|
||||
_state = CellState::OFF;
|
||||
}
|
||||
|
||||
bool CellularMQTT::recvCommand(MQTTCommand& out) {
|
||||
if (!_cmdQueue) return false;
|
||||
return xQueueReceive(_cmdQueue, &out, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
bool CellularMQTT::sendResponse(const char* topic, const char* payload) {
|
||||
if (!_rspQueue) return false;
|
||||
MQTTResponse rsp;
|
||||
memset(&rsp, 0, sizeof(rsp));
|
||||
strncpy(rsp.topic, topic, MQTT_TOPIC_MAX - 1);
|
||||
strncpy(rsp.payload, payload, MQTT_PAYLOAD_MAX - 1);
|
||||
return xQueueSend(_rspQueue, &rsp, 0) == pdTRUE;
|
||||
}
|
||||
|
||||
void CellularMQTT::updateTelemetry(const TelemetryData& data) {
|
||||
if (xSemaphoreTake(_telemetryMutex, pdMS_TO_TICKS(50))) {
|
||||
memcpy(&_telemetry, &data, sizeof(data));
|
||||
xSemaphoreGive(_telemetryMutex);
|
||||
}
|
||||
}
|
||||
|
||||
int CellularMQTT::getSignalBars() const {
|
||||
if (_csq == 99 || _csq == 0) return 0;
|
||||
if (_csq <= 5) return 1;
|
||||
if (_csq <= 10) return 2;
|
||||
if (_csq <= 15) return 3;
|
||||
if (_csq <= 20) return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
const char* CellularMQTT::stateString() const {
|
||||
switch (_state) {
|
||||
case CellState::OFF: return "OFF";
|
||||
case CellState::POWERING_ON: return "PWR ON";
|
||||
case CellState::INITIALIZING: return "INIT";
|
||||
case CellState::REGISTERING: return "REG";
|
||||
case CellState::DATA_ACTIVATING: return "DATA";
|
||||
case CellState::MQTT_STARTING: return "MQTT INIT";
|
||||
case CellState::MQTT_CONNECTING: return "MQTT CONN";
|
||||
case CellState::CONNECTED: return "CONNECTED";
|
||||
case CellState::RECONNECTING: return "RECONN";
|
||||
case CellState::ERROR: return "ERROR";
|
||||
default: return "???";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config file: /remote/mqtt.cfg
|
||||
// Format (one value per line):
|
||||
// broker.hivemq.cloud
|
||||
// 8883
|
||||
// myusername
|
||||
// mypassword
|
||||
// mydeviceid (optional — auto-generated from MAC if omitted)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::loadConfig(MQTTConfig& cfg) {
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
|
||||
File f = SD.open(CONFIG_FILE, FILE_READ);
|
||||
if (!f) return false;
|
||||
|
||||
String line;
|
||||
|
||||
line = f.readStringUntil('\n'); line.trim();
|
||||
if (line.length() == 0) { f.close(); return false; }
|
||||
strncpy(cfg.broker, line.c_str(), sizeof(cfg.broker) - 1);
|
||||
|
||||
line = f.readStringUntil('\n'); line.trim();
|
||||
cfg.port = line.length() > 0 ? line.toInt() : 8883;
|
||||
|
||||
line = f.readStringUntil('\n'); line.trim();
|
||||
strncpy(cfg.username, line.c_str(), sizeof(cfg.username) - 1);
|
||||
|
||||
line = f.readStringUntil('\n'); line.trim();
|
||||
strncpy(cfg.password, line.c_str(), sizeof(cfg.password) - 1);
|
||||
|
||||
// Optional device ID
|
||||
if (f.available()) {
|
||||
line = f.readStringUntil('\n'); line.trim();
|
||||
if (line.length() > 0) {
|
||||
strncpy(cfg.deviceId, line.c_str(), sizeof(cfg.deviceId) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
f.close();
|
||||
|
||||
// Auto-generate device ID from ESP32 MAC if not provided
|
||||
if (cfg.deviceId[0] == '\0') {
|
||||
uint8_t mac[6];
|
||||
esp_efuse_mac_get_default(mac);
|
||||
snprintf(cfg.deviceId, sizeof(cfg.deviceId), "meck-%02x%02x%02x%02x",
|
||||
mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
return cfg.broker[0] != '\0';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modem power-on (same sequence as ModemManager)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::modemPowerOn() {
|
||||
Serial.println("[Cell] Powering on modem...");
|
||||
|
||||
pinMode(MODEM_POWER_EN, OUTPUT);
|
||||
digitalWrite(MODEM_POWER_EN, HIGH);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
pinMode(MODEM_RST, OUTPUT);
|
||||
digitalWrite(MODEM_RST, LOW);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
digitalWrite(MODEM_RST, HIGH);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
pinMode(MODEM_PWRKEY, OUTPUT);
|
||||
digitalWrite(MODEM_PWRKEY, HIGH);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
digitalWrite(MODEM_PWRKEY, LOW);
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
digitalWrite(MODEM_PWRKEY, HIGH);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
|
||||
pinMode(MODEM_DTR, OUTPUT);
|
||||
digitalWrite(MODEM_DTR, LOW);
|
||||
|
||||
MODEM_SERIAL.begin(MODEM_BAUD, SERIAL_8N1, MODEM_TX, MODEM_RX);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
while (MODEM_SERIAL.available()) MODEM_SERIAL.read();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (sendAT("AT", "OK", 1500)) {
|
||||
Serial.println("[Cell] AT responded OK");
|
||||
return true;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
Serial.println("[Cell] No AT response after power-on");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AT command helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::sendAT(const char* cmd, const char* expect, uint32_t timeout_ms) {
|
||||
drainURCs();
|
||||
Serial.printf("[Cell] TX: %s\n", cmd);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
bool ok = waitResponse(expect, timeout_ms, _atBuf, AT_BUF_SIZE);
|
||||
int len = strlen(_atBuf);
|
||||
while (len > 0 && (_atBuf[len-1] == '\r' || _atBuf[len-1] == '\n')) _atBuf[--len] = '\0';
|
||||
if (_atBuf[0]) {
|
||||
Serial.printf("[Cell] RX: %.120s [%s]\n", _atBuf, ok ? "OK" : "FAIL");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool CellularMQTT::waitResponse(const char* expect, uint32_t timeout_ms,
|
||||
char* buf, size_t bufLen) {
|
||||
unsigned long start = millis();
|
||||
int pos = 0;
|
||||
if (buf && bufLen > 0) buf[0] = '\0';
|
||||
|
||||
while (millis() - start < timeout_ms) {
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
if (buf && pos < (int)bufLen - 1) {
|
||||
buf[pos++] = c;
|
||||
buf[pos] = '\0';
|
||||
}
|
||||
if (buf && expect && strstr(buf, expect)) return true;
|
||||
if (buf && strstr(buf, "ERROR")) return false;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (buf && expect && strstr(buf, expect)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CellularMQTT::waitPrompt(uint32_t timeout_ms) {
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < timeout_ms) {
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
if (c == '>') return true;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// URC handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::drainURCs() {
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
|
||||
if (_rxState == RX_WAIT_TOPIC) {
|
||||
if (_urcPos < _rxTopicLen && _urcPos < MQTT_TOPIC_MAX - 1) {
|
||||
_rxTopic[_urcPos] = c;
|
||||
}
|
||||
_urcPos++;
|
||||
if (_urcPos >= _rxTopicLen) {
|
||||
_rxTopic[min(_rxTopicLen, MQTT_TOPIC_MAX - 1)] = '\0';
|
||||
handleMqttRxTopic(_rxTopic, _rxTopicLen);
|
||||
_urcPos = 0;
|
||||
_rxState = RX_IDLE;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_rxState == RX_WAIT_PAYLOAD) {
|
||||
if (_urcPos < _rxPayloadLen && _urcPos < MQTT_PAYLOAD_MAX - 1) {
|
||||
_rxPayload[_urcPos] = c;
|
||||
}
|
||||
_urcPos++;
|
||||
if (_urcPos >= _rxPayloadLen) {
|
||||
_rxPayload[min(_rxPayloadLen, MQTT_PAYLOAD_MAX - 1)] = '\0';
|
||||
handleMqttRxPayload(_rxPayload, _rxPayloadLen);
|
||||
_urcPos = 0;
|
||||
_rxState = RX_IDLE;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
if (_urcPos > 0) {
|
||||
while (_urcPos > 0 && _urcBuf[_urcPos - 1] == '\r') _urcPos--;
|
||||
_urcBuf[_urcPos] = '\0';
|
||||
if (_urcPos > 0) processURCLine(_urcBuf);
|
||||
}
|
||||
_urcPos = 0;
|
||||
} else if (c != '\r' || _urcPos > 0) {
|
||||
if (_urcPos < URC_BUF_SIZE - 1) _urcBuf[_urcPos++] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CellularMQTT::processURCLine(const char* line) {
|
||||
if (strncmp(line, "+CMQTTRXSTART:", 14) == 0) {
|
||||
handleMqttRxStart(line);
|
||||
return;
|
||||
}
|
||||
if (strncmp(line, "+CMQTTRXTOPIC:", 14) == 0) {
|
||||
int client, tlen;
|
||||
if (sscanf(line, "+CMQTTRXTOPIC: %d,%d", &client, &tlen) == 2) {
|
||||
_rxTopicLen = tlen;
|
||||
_urcPos = 0;
|
||||
_rxState = RX_WAIT_TOPIC;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strncmp(line, "+CMQTTRXPAYLOAD:", 16) == 0) {
|
||||
int client, plen;
|
||||
if (sscanf(line, "+CMQTTRXPAYLOAD: %d,%d", &client, &plen) == 2) {
|
||||
_rxPayloadLen = plen;
|
||||
_urcPos = 0;
|
||||
_rxState = RX_WAIT_PAYLOAD;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strncmp(line, "+CMQTTRXEND:", 12) == 0) {
|
||||
handleMqttRxEnd();
|
||||
return;
|
||||
}
|
||||
if (strncmp(line, "+CMQTTCONNLOST:", 15) == 0) {
|
||||
handleMqttConnLost(line);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MQTT receive handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::handleMqttRxStart(const char* line) {
|
||||
int client, tlen, plen;
|
||||
if (sscanf(line, "+CMQTTRXSTART: %d,%d,%d", &client, &tlen, &plen) == 3) {
|
||||
_rxTopicLen = tlen;
|
||||
_rxPayloadLen = plen;
|
||||
_rxTopic[0] = '\0';
|
||||
_rxPayload[0] = '\0';
|
||||
Serial.printf("[Cell] MQTT RX start: topic_len=%d payload_len=%d\n", tlen, plen);
|
||||
}
|
||||
}
|
||||
|
||||
void CellularMQTT::handleMqttRxTopic(const char* data, int len) {
|
||||
Serial.printf("[Cell] MQTT RX topic: %s\n", data);
|
||||
}
|
||||
|
||||
void CellularMQTT::handleMqttRxPayload(const char* data, int len) {
|
||||
Serial.printf("[Cell] MQTT RX payload: %.80s\n", data);
|
||||
|
||||
if (strstr(_rxTopic, "/cmd")) {
|
||||
MQTTCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
strncpy(cmd.cmd, data, MQTT_PAYLOAD_MAX - 1);
|
||||
if (xQueueSend(_cmdQueue, &cmd, 0) == pdTRUE) {
|
||||
_lastCmdTime = millis();
|
||||
Serial.printf("[Cell] Queued CLI command: %s\n", cmd.cmd);
|
||||
} else {
|
||||
Serial.println("[Cell] Command queue full, dropping");
|
||||
}
|
||||
} else if (strstr(_rxTopic, "/ota")) {
|
||||
MQTTCommand cmd;
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
snprintf(cmd.cmd, sizeof(cmd.cmd), "ota:%s", data);
|
||||
xQueueSend(_cmdQueue, &cmd, 0);
|
||||
Serial.printf("[Cell] Queued OTA URL: %s\n", data);
|
||||
}
|
||||
}
|
||||
|
||||
void CellularMQTT::handleMqttRxEnd() {
|
||||
Serial.println("[Cell] MQTT RX end");
|
||||
}
|
||||
|
||||
void CellularMQTT::handleMqttConnLost(const char* line) {
|
||||
int client, cause;
|
||||
sscanf(line, "+CMQTTCONNLOST: %d,%d", &client, &cause);
|
||||
Serial.printf("[Cell] MQTT connection lost (cause=%d)\n", cause);
|
||||
_state = CellState::RECONNECTING;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// APN resolution (reuses Meck's ApnDatabase)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::resolveAPN() {
|
||||
// 1. Check SD config
|
||||
File f = SD.open("/remote/apn.cfg", FILE_READ);
|
||||
if (f) {
|
||||
String line = f.readStringUntil('\n');
|
||||
f.close();
|
||||
line.trim();
|
||||
if (line.length() > 0) {
|
||||
strncpy(_apn, line.c_str(), sizeof(_apn) - 1);
|
||||
Serial.printf("[Cell] APN from config: %s\n", _apn);
|
||||
char cmd[80];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CGDCONT=1,\"IP\",\"%s\"", _apn);
|
||||
sendAT(cmd, "OK", 3000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check modem's current APN
|
||||
if (sendAT("AT+CGDCONT?", "OK", 3000)) {
|
||||
char* p = strstr(_atBuf, "+CGDCONT:");
|
||||
if (p) {
|
||||
char* q1 = strchr(p, '"');
|
||||
if (q1) q1 = strchr(q1 + 1, '"');
|
||||
if (q1) q1 = strchr(q1 + 1, '"');
|
||||
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';
|
||||
Serial.printf("[Cell] APN from network: %s\n", _apn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Auto-detect from IMSI
|
||||
if (_imsi[0]) {
|
||||
const ApnEntry* entry = apnLookupFromIMSI(_imsi);
|
||||
if (entry) {
|
||||
strncpy(_apn, entry->apn, sizeof(_apn) - 1);
|
||||
Serial.printf("[Cell] APN auto-detected: %s (%s)\n", _apn, entry->carrier);
|
||||
char cmd[80];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CGDCONT=1,\"IP\",\"%s\"", _apn);
|
||||
sendAT(cmd, "OK", 3000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_apn[0] = '\0';
|
||||
Serial.println("[Cell] APN: none detected");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data connection — activate PDP context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::activateData() {
|
||||
Serial.println("[Cell] Activating data connection...");
|
||||
|
||||
if (!sendAT("AT+CGACT=1,1", "OK", 15000)) {
|
||||
Serial.println("[Cell] PDP activation failed, trying CGATT first...");
|
||||
sendAT("AT+CGATT=1", "OK", 30000);
|
||||
if (!sendAT("AT+CGACT=1,1", "OK", 15000)) {
|
||||
Serial.println("[Cell] PDP activation failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Query IP address
|
||||
if (sendAT("AT+CGPADDR=1", "OK", 5000)) {
|
||||
char* p = strstr(_atBuf, "+CGPADDR:");
|
||||
if (p) {
|
||||
char* q = strchr(p, '"');
|
||||
if (q) {
|
||||
q++;
|
||||
char* e = strchr(q, '"');
|
||||
if (e && e > q) {
|
||||
int len = e - q;
|
||||
if (len < (int)sizeof(_ipAddr)) {
|
||||
memcpy(_ipAddr, q, len);
|
||||
_ipAddr[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[Cell] Data active, IP: %s\n", _ipAddr[0] ? _ipAddr : "unknown");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MQTT operations via AT commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::mqttStart() {
|
||||
if (!sendAT("AT+CMQTTSTART", "OK", 5000)) {
|
||||
if (!strstr(_atBuf, "+CMQTTSTART: 0")) {
|
||||
Serial.println("[Cell] MQTT start failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire client with SSL enabled (third param = 1 for SSL)
|
||||
char cmd[120];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CMQTTACCQ=0,\"%s\",1", _config.deviceId);
|
||||
if (!sendAT(cmd, "OK", 5000)) {
|
||||
Serial.println("[Cell] MQTT client acquire failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure TLS 1.2 (sslversion 4 = TLS 1.2)
|
||||
sendAT("AT+CSSLCFG=\"sslversion\",0,4", "OK", 3000);
|
||||
|
||||
// Skip certificate verification (no CA cert loaded on device)
|
||||
sendAT("AT+CSSLCFG=\"authmode\",0,0", "OK", 3000);
|
||||
|
||||
// Enable SNI — required for HiveMQ Cloud (shared IP, multiple clusters)
|
||||
sendAT("AT+CSSLCFG=\"enableSNI\",0,1", "OK", 3000);
|
||||
|
||||
// Bind SSL config to MQTT session
|
||||
sendAT("AT+CMQTTSSLCFG=0,0", "OK", 3000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CellularMQTT::mqttConnect() {
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"AT+CMQTTCONNECT=0,\"tcp://%s:%d\",120,1,\"%s\",\"%s\"",
|
||||
_config.broker, _config.port,
|
||||
_config.username, _config.password);
|
||||
|
||||
Serial.printf("[Cell] TX: AT+CMQTTCONNECT=0,\"ssl://%s:%d\",...\n",
|
||||
_config.broker, _config.port);
|
||||
Serial.printf("[Cell] Full cmd (%d chars): %s\n", strlen(cmd), cmd);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
|
||||
// Wait for +CMQTTCONNECT URC (any result code, not just success)
|
||||
// Don't use waitResponse — it bails on "ERROR" before we see the code
|
||||
unsigned long start = millis();
|
||||
int pos = 0;
|
||||
_atBuf[0] = '\0';
|
||||
|
||||
while (millis() - start < 30000) {
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
if (pos < AT_BUF_SIZE - 1) {
|
||||
_atBuf[pos++] = c;
|
||||
_atBuf[pos] = '\0';
|
||||
}
|
||||
// Check for the URC regardless of what else is in the buffer
|
||||
char* p = strstr(_atBuf, "+CMQTTCONNECT:");
|
||||
if (p) {
|
||||
// Give it a moment to complete the line
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
while (MODEM_SERIAL.available() && pos < AT_BUF_SIZE - 1) {
|
||||
_atBuf[pos++] = MODEM_SERIAL.read();
|
||||
_atBuf[pos] = '\0';
|
||||
}
|
||||
|
||||
int client, result;
|
||||
if (sscanf(p, "+CMQTTCONNECT: %d,%d", &client, &result) == 2) {
|
||||
Serial.printf("[Cell] MQTT connect result: %d\n", result);
|
||||
if (result == 0) {
|
||||
Serial.println("[Cell] MQTT connected!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Serial.printf("[Cell] MQTT connect failed (code from URC): %.80s\n", p);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
|
||||
// Timeout — dump what we got
|
||||
int len = strlen(_atBuf);
|
||||
while (len > 0 && (_atBuf[len-1] == '\r' || _atBuf[len-1] == '\n')) _atBuf[--len] = '\0';
|
||||
Serial.printf("[Cell] MQTT connect timeout. Buffer: %.200s\n", _atBuf);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CellularMQTT::mqttSubscribe(const char* topic) {
|
||||
int tlen = strlen(topic);
|
||||
char cmd[80];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CMQTTSUB=0,%d,1", tlen);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
|
||||
if (!waitPrompt(5000)) {
|
||||
Serial.println("[Cell] No prompt for CMQTTSUB");
|
||||
return false;
|
||||
}
|
||||
|
||||
MODEM_SERIAL.write((const uint8_t*)topic, tlen);
|
||||
return waitResponse("OK", 10000, _atBuf, AT_BUF_SIZE);
|
||||
}
|
||||
|
||||
bool CellularMQTT::mqttPublish(const char* topic, const char* payload) {
|
||||
int tlen = strlen(topic);
|
||||
int plen = strlen(payload);
|
||||
|
||||
// Step 1: Set topic
|
||||
char cmd[80];
|
||||
snprintf(cmd, sizeof(cmd), "AT+CMQTTTOPIC=0,%d", tlen);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
if (!waitPrompt(5000)) {
|
||||
_pubFailCount++;
|
||||
Serial.println("[Cell] No prompt for CMQTTTOPIC");
|
||||
return false;
|
||||
}
|
||||
MODEM_SERIAL.write((const uint8_t*)topic, tlen);
|
||||
if (!waitResponse("OK", 5000, _atBuf, AT_BUF_SIZE)) {
|
||||
_pubFailCount++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Set payload
|
||||
snprintf(cmd, sizeof(cmd), "AT+CMQTTPAYLOAD=0,%d", plen);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
if (!waitPrompt(5000)) {
|
||||
_pubFailCount++;
|
||||
return false;
|
||||
}
|
||||
MODEM_SERIAL.write((const uint8_t*)payload, plen);
|
||||
if (!waitResponse("OK", 5000, _atBuf, AT_BUF_SIZE)) {
|
||||
_pubFailCount++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Publish QoS 1, 60s timeout
|
||||
if (!sendAT("AT+CMQTTPUB=0,1,60", "OK", 15000)) {
|
||||
_pubFailCount++;
|
||||
Serial.printf("[Cell] Publish failed (%d consecutive)\n", _pubFailCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Success — reset failure counter
|
||||
_pubFailCount = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CellularMQTT::mqttDisconnect() {
|
||||
sendAT("AT+CMQTTDISC=0,60", "OK", 5000);
|
||||
sendAT("AT+CMQTTREL=0", "OK", 3000);
|
||||
sendAT("AT+CMQTTSTOP", "OK", 5000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FreeRTOS Task
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::taskEntry(void* param) {
|
||||
static_cast<CellularMQTT*>(param)->taskLoop();
|
||||
}
|
||||
|
||||
void CellularMQTT::taskLoop() {
|
||||
Serial.printf("[Cell] Task started on core %d\n", xPortGetCoreID());
|
||||
|
||||
restart:
|
||||
_pubFailCount = 0;
|
||||
|
||||
// ---- Phase 1: Power on ----
|
||||
_state = CellState::POWERING_ON;
|
||||
if (!modemPowerOn()) {
|
||||
Serial.println("[Cell] Power-on failed, retry in 30s");
|
||||
_state = CellState::ERROR;
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
goto restart;
|
||||
}
|
||||
|
||||
// ---- Phase 2: Initialize ----
|
||||
_state = CellState::INITIALIZING;
|
||||
sendAT("ATE0", "OK");
|
||||
|
||||
if (sendAT("AT+GSN", "OK", 3000)) {
|
||||
char* p = _atBuf;
|
||||
while (*p && !isdigit(*p)) p++;
|
||||
int i = 0;
|
||||
while (isdigit(p[i]) && i < 19) { _imei[i] = p[i]; i++; }
|
||||
_imei[i] = '\0';
|
||||
Serial.printf("[Cell] IMEI: %s\n", _imei);
|
||||
}
|
||||
|
||||
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';
|
||||
Serial.printf("[Cell] IMSI: %s\n", _imsi);
|
||||
}
|
||||
|
||||
sendAT("AT+CTZU=1", "OK");
|
||||
|
||||
// ---- Phase 3: Network registration ----
|
||||
_state = CellState::REGISTERING;
|
||||
Serial.println("[Cell] Waiting for network...");
|
||||
|
||||
{
|
||||
bool registered = false;
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (sendAT("AT+CREG?", "OK", 2000)) {
|
||||
char* p = strstr(_atBuf, "+CREG:");
|
||||
if (p) {
|
||||
int n, stat;
|
||||
if (sscanf(p, "+CREG: %d,%d", &n, &stat) == 2) {
|
||||
if (stat == 1 || stat == 5) { registered = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
if (!registered) Serial.println("[Cell] Registration timeout — continuing");
|
||||
}
|
||||
|
||||
// Operator name
|
||||
sendAT("AT+COPS=3,0", "OK", 2000);
|
||||
if (sendAT("AT+COPS?", "OK", 5000)) {
|
||||
char* p = strchr(_atBuf, '"');
|
||||
if (p) {
|
||||
p++;
|
||||
char* e = strchr(p, '"');
|
||||
if (e) {
|
||||
int len = e - p;
|
||||
if (len >= (int)sizeof(_operator)) len = sizeof(_operator) - 1;
|
||||
memcpy(_operator, p, len);
|
||||
_operator[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolveAPN();
|
||||
|
||||
if (sendAT("AT+CSQ", "OK", 2000)) {
|
||||
char* p = strstr(_atBuf, "+CSQ:");
|
||||
if (p) { int csq, ber; if (sscanf(p, "+CSQ: %d,%d", &csq, &ber) >= 1) _csq = csq; }
|
||||
}
|
||||
|
||||
Serial.printf("[Cell] Registered: oper=%s CSQ=%d APN=%s IMEI=%s\n",
|
||||
_operator, _csq, _apn[0] ? _apn : "(none)", _imei);
|
||||
|
||||
// ---- Phase 4: Activate data ----
|
||||
_state = CellState::DATA_ACTIVATING;
|
||||
if (!activateData()) {
|
||||
Serial.println("[Cell] Data activation failed, retry in 30s");
|
||||
_state = CellState::ERROR;
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
goto restart;
|
||||
}
|
||||
|
||||
// ---- Phase 5: MQTT connect ----
|
||||
_state = CellState::MQTT_STARTING;
|
||||
if (!mqttStart()) {
|
||||
_state = CellState::ERROR;
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
goto restart;
|
||||
}
|
||||
|
||||
_state = CellState::MQTT_CONNECTING;
|
||||
if (!mqttConnect()) {
|
||||
mqttDisconnect();
|
||||
_state = CellState::ERROR;
|
||||
vTaskDelay(pdMS_TO_TICKS(30000));
|
||||
goto restart;
|
||||
}
|
||||
|
||||
mqttSubscribe(_topicCmd);
|
||||
mqttSubscribe(_topicOta);
|
||||
|
||||
_state = CellState::CONNECTED;
|
||||
_reconnectDelay = MQTT_RECONNECT_MIN;
|
||||
Serial.println("[Cell] MQTT connected and subscribed — ready");
|
||||
|
||||
mqttPublish(_topicTelem, "{\"event\":\"boot\",\"state\":\"connected\"}");
|
||||
|
||||
// ---- Phase 6: Main loop ----
|
||||
unsigned long lastCSQ = 0;
|
||||
unsigned long lastTelem = 0;
|
||||
|
||||
while (true) {
|
||||
drainURCs();
|
||||
|
||||
// Health check: too many consecutive publish failures = silent disconnect
|
||||
if (_pubFailCount >= MQTT_PUB_FAIL_MAX && _state == CellState::CONNECTED) {
|
||||
Serial.printf("[Cell] %d consecutive publish failures — forcing reconnect\n", _pubFailCount);
|
||||
_state = CellState::RECONNECTING;
|
||||
}
|
||||
|
||||
// Reconnect logic
|
||||
if (_state == CellState::RECONNECTING) {
|
||||
Serial.printf("[Cell] Reconnecting in %lums...\n", _reconnectDelay);
|
||||
vTaskDelay(pdMS_TO_TICKS(_reconnectDelay));
|
||||
_reconnectDelay = min(_reconnectDelay * 2, (uint32_t)MQTT_RECONNECT_MAX);
|
||||
|
||||
mqttDisconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
|
||||
// Check data is still active
|
||||
if (!sendAT("AT+CGACT?", "OK", 5000) || !strstr(_atBuf, ",1")) {
|
||||
if (!activateData()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
goto restart;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mqttStart() || !mqttConnect()) {
|
||||
continue; // Retry with backoff
|
||||
}
|
||||
|
||||
mqttSubscribe(_topicCmd);
|
||||
mqttSubscribe(_topicOta);
|
||||
_state = CellState::CONNECTED;
|
||||
_reconnectDelay = MQTT_RECONNECT_MIN;
|
||||
_pubFailCount = 0;
|
||||
Serial.println("[Cell] Reconnected");
|
||||
}
|
||||
|
||||
// Publish queued responses
|
||||
if (_state == CellState::CONNECTED) {
|
||||
MQTTResponse rsp;
|
||||
while (xQueueReceive(_rspQueue, &rsp, 0) == pdTRUE) {
|
||||
mqttPublish(rsp.topic, rsp.payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic CSQ poll
|
||||
if (millis() - lastCSQ > 60000) {
|
||||
if (sendAT("AT+CSQ", "OK", 2000)) {
|
||||
char* p = strstr(_atBuf, "+CSQ:");
|
||||
if (p) { int csq, ber; if (sscanf(p, "+CSQ: %d,%d", &csq, &ber) >= 1) _csq = csq; }
|
||||
}
|
||||
lastCSQ = millis();
|
||||
}
|
||||
|
||||
// Periodic telemetry publish
|
||||
if (_state == CellState::CONNECTED && millis() - lastTelem > TELEMETRY_INTERVAL) {
|
||||
TelemetryData td;
|
||||
if (xSemaphoreTake(_telemetryMutex, pdMS_TO_TICKS(50))) {
|
||||
memcpy(&td, &_telemetry, sizeof(td));
|
||||
xSemaphoreGive(_telemetryMutex);
|
||||
}
|
||||
|
||||
char json[400];
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"uptime\":%lu,\"batt_mv\":%d,\"batt_pct\":%d,\"temp\":%.1f,"
|
||||
"\"csq\":%d,\"bars\":%d,\"neighbors\":%d,"
|
||||
"\"freq\":%.3f,\"bw\":%.1f,\"sf\":%d,\"cr\":%d,\"tx\":%d,"
|
||||
"\"name\":\"%s\",\"ip\":\"%s\",\"oper\":\"%s\",\"apn\":\"%s\","
|
||||
"\"heap\":%d}",
|
||||
td.uptime_secs, td.battery_mv, td.battery_pct,
|
||||
td.temperature / 10.0f,
|
||||
_csq, getSignalBars(), td.neighbor_count,
|
||||
td.freq, td.bw, td.sf, td.cr, td.tx_power,
|
||||
td.node_name, _ipAddr, _operator, _apn,
|
||||
ESP.getFreeHeap());
|
||||
|
||||
mqttPublish(_topicTelem, json);
|
||||
lastTelem = millis();
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // HAS_4G_MODEM
|
||||
@@ -3,6 +3,11 @@
|
||||
|
||||
#include "MyMesh.h"
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
#include <SD.h>
|
||||
#include "CellularMQTT.h"
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "UITask.h"
|
||||
static UITask ui_task(display);
|
||||
@@ -23,6 +28,10 @@ static char command[160];
|
||||
unsigned long lastActive = 0; // mark last active time
|
||||
unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot
|
||||
|
||||
#ifdef HAS_4G_MODEM
|
||||
static bool sdCardReady = false;
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
@@ -34,10 +43,12 @@ void setup() {
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (display.begin()) {
|
||||
#ifndef HAS_4G_MODEM
|
||||
display.startFrame();
|
||||
display.setCursor(0, 0);
|
||||
display.print("Please wait...");
|
||||
display.endFrame();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -83,6 +94,52 @@ void setup() {
|
||||
|
||||
the_mesh.begin(fs);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SD card init — needed for CellularMQTT config (/remote/mqtt.cfg)
|
||||
// SD, LoRa, and e-ink share the same SPI bus on T-Deck Pro.
|
||||
// ---------------------------------------------------------------------------
|
||||
#ifdef HAS_4G_MODEM
|
||||
{
|
||||
// Deselect all SPI devices before SD init to prevent bus contention
|
||||
#ifdef SDCARD_CS
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
#endif
|
||||
#ifdef PIN_DISPLAY_CS
|
||||
pinMode(PIN_DISPLAY_CS, OUTPUT);
|
||||
digitalWrite(PIN_DISPLAY_CS, HIGH);
|
||||
#endif
|
||||
#ifdef P_LORA_NSS
|
||||
pinMode(P_LORA_NSS, OUTPUT);
|
||||
digitalWrite(P_LORA_NSS, HIGH);
|
||||
#endif
|
||||
delay(100);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
#ifdef SDCARD_CS
|
||||
if (SD.begin(SDCARD_CS)) { sdCardReady = true; break; }
|
||||
#else
|
||||
if (SD.begin(SPI_CS)) { sdCardReady = true; break; }
|
||||
#endif
|
||||
delay(200);
|
||||
}
|
||||
Serial.printf("SD card: %s\n", sdCardReady ? "ready" : "FAILED");
|
||||
|
||||
// Re-claim SPI bus for display — SD.begin() steals the shared
|
||||
// GPIO pins (36/47/33) from the display's HSPI peripheral
|
||||
extern SPIClass displaySpi;
|
||||
displaySpi.begin(PIN_DISPLAY_SCLK, 47, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
|
||||
}
|
||||
|
||||
// Start cellular MQTT
|
||||
if (sdCardReady) {
|
||||
cellularMQTT.begin();
|
||||
Serial.println("Cellular MQTT starting...");
|
||||
} else {
|
||||
Serial.println("Cellular MQTT skipped — no SD card for config");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
||||
#endif
|
||||
@@ -118,6 +175,64 @@ void loop() {
|
||||
command[0] = 0; // reset command buffer
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MQTT → CLI bridge: process incoming commands from MQTT
|
||||
// ---------------------------------------------------------------------------
|
||||
#ifdef HAS_4G_MODEM
|
||||
{
|
||||
MQTTCommand mqttCmd;
|
||||
while (cellularMQTT.recvCommand(mqttCmd)) {
|
||||
// Check for OTA command
|
||||
if (strncmp(mqttCmd.cmd, "ota:", 4) == 0) {
|
||||
const char* url = &mqttCmd.cmd[4];
|
||||
Serial.printf("[MQTT] OTA request: %s\n", url);
|
||||
// TODO: RemoteOTA — download firmware from URL and flash
|
||||
cellularMQTT.sendResponse(cellularMQTT.getRspTopic(), "{\"ota\":\"not yet implemented\"}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// CLI command — process through the same handler as serial/LoRa admin
|
||||
Serial.printf("[MQTT] CLI: %s\n", mqttCmd.cmd);
|
||||
char reply[512];
|
||||
reply[0] = '\0';
|
||||
the_mesh.handleCommand(0, mqttCmd.cmd, reply);
|
||||
|
||||
if (reply[0] == '\0') strcpy(reply, "OK");
|
||||
|
||||
cellularMQTT.sendResponse(cellularMQTT.getRspTopic(), reply);
|
||||
Serial.printf("[MQTT] Reply: %.80s\n", reply);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic telemetry snapshot for MQTT publishing
|
||||
{
|
||||
static unsigned long lastTelemUpdate = 0;
|
||||
if (millis() - lastTelemUpdate > 10000) {
|
||||
NodePrefs* p = the_mesh.getNodePrefs();
|
||||
TelemetryData td;
|
||||
memset(&td, 0, sizeof(td));
|
||||
td.uptime_secs = millis() / 1000;
|
||||
td.battery_mv = board.getBattMilliVolts();
|
||||
td.battery_pct = board.getBatteryPercent();
|
||||
td.temperature = board.getBattTemperature();
|
||||
td.csq = cellularMQTT.getCSQ();
|
||||
td.freq = p->freq;
|
||||
td.bw = p->bw;
|
||||
td.sf = p->sf;
|
||||
td.cr = p->cr;
|
||||
td.tx_power = p->tx_power_dbm;
|
||||
strncpy(td.node_name, p->node_name, sizeof(td.node_name) - 1);
|
||||
strncpy(td.apn, cellularMQTT.getAPN(), sizeof(td.apn) - 1);
|
||||
strncpy(td.oper, cellularMQTT.getOperator(), sizeof(td.oper) - 1);
|
||||
td.mqtt_connected = cellularMQTT.isConnected();
|
||||
td.neighbor_count = 0; // TODO: expose from MyMesh
|
||||
|
||||
cellularMQTT.updateTelemetry(td);
|
||||
lastTelemUpdate = millis();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
the_mesh.loop();
|
||||
sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
@@ -135,4 +250,4 @@ void loop() {
|
||||
nextSleepinSecs += 5; // When there is pending work, to work another 5s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
// T-Deck Pro battery capacity (all variants use 1400 mAh cell)
|
||||
#ifndef BQ27220_DESIGN_CAPACITY_MAH
|
||||
#define BQ27220_DESIGN_CAPACITY_MAH 1400
|
||||
#define BQ27220_DESIGN_CAPACITY_MAH 2000
|
||||
#endif
|
||||
|
||||
class TDeckBoard : public ESP32Board {
|
||||
|
||||
@@ -274,4 +274,37 @@ build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Remote Repeater (T-Deck Pro 4G, cellular MQTT remote management)
|
||||
;
|
||||
; MeshCore repeater firmware + A7682E cellular MQTT for remote admin.
|
||||
; No BLE, no SMS/calls, no companion protocol. All management via MQTT
|
||||
; or USB serial CLI.
|
||||
;
|
||||
; SD card config required: /remote/mqtt.cfg (broker, port, user, pass)
|
||||
; Optional: /remote/apn.cfg (APN override)
|
||||
;
|
||||
; Add this block to the bottom of platformio.ini
|
||||
; Flash with: pio run -e meck_remote_repeater
|
||||
; ---------------------------------------------------------------------------
|
||||
[env:meck_remote_repeater]
|
||||
extends = LilyGo_TDeck_Pro
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro.build_flags}
|
||||
-I examples/simple_repeater
|
||||
-D ADMIN_PASSWORD='"admin"'
|
||||
-D HAS_4G_MODEM=1
|
||||
-D DISABLE_WIFI_OTA=1
|
||||
-D MECK_REMOTE_REPEATER=1
|
||||
-D MAX_NEIGHBOURS=16
|
||||
-D FIRMWARE_VERSION='"Meck RemRptr v0.1"'
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
+<../examples/simple_repeater/*.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro.lib_deps}
|
||||
|
||||
Reference in New Issue
Block a user