mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-09 14:55:02 +02:00
tdpro remote repeater ota firmware update update
This commit is contained in:
@@ -2,18 +2,6 @@
|
||||
|
||||
// =============================================================================
|
||||
// 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
|
||||
@@ -36,25 +24,22 @@
|
||||
#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)
|
||||
#define CMD_QUEUE_SIZE 4
|
||||
#define RSP_QUEUE_SIZE 4
|
||||
|
||||
// Telemetry interval (ms)
|
||||
#define TELEMETRY_INTERVAL 60000 // 60 seconds
|
||||
#define TELEMETRY_INTERVAL 60000
|
||||
|
||||
// Task configuration
|
||||
#define CELL_TASK_PRIORITY 1
|
||||
#define CELL_TASK_STACK_SIZE 8192 // MQTT + TLS AT commands need headroom
|
||||
#define CELL_TASK_STACK_SIZE 8192
|
||||
#define CELL_TASK_CORE 0
|
||||
|
||||
// Reconnect timing
|
||||
#define MQTT_RECONNECT_MIN 5000 // 5 seconds
|
||||
#define MQTT_RECONNECT_MAX 300000 // 5 minutes (exponential backoff cap)
|
||||
#define MQTT_RECONNECT_MIN 5000
|
||||
#define MQTT_RECONNECT_MAX 300000
|
||||
|
||||
// Consecutive publish failures before forced reconnect
|
||||
#define MQTT_PUB_FAIL_MAX 5
|
||||
|
||||
#define OTA_CHUNK_SIZE 1024
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State machine
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -63,11 +48,12 @@ enum class CellState : uint8_t {
|
||||
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
|
||||
DATA_ACTIVATING,
|
||||
MQTT_STARTING,
|
||||
MQTT_CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING,
|
||||
OTA_IN_PROGRESS,
|
||||
ERROR
|
||||
};
|
||||
|
||||
@@ -75,12 +61,10 @@ enum class CellState : uint8_t {
|
||||
// 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];
|
||||
@@ -90,21 +74,21 @@ struct MQTTResponse {
|
||||
// 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 broker[80];
|
||||
uint16_t port;
|
||||
char username[40];
|
||||
char password[40];
|
||||
char deviceId[MQTT_CLIENT_ID_MAX]; // Auto-generated from MAC if empty
|
||||
char deviceId[MQTT_CLIENT_ID_MAX];
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Telemetry snapshot (filled by main loop, published by modem task)
|
||||
// Telemetry snapshot
|
||||
// ---------------------------------------------------------------------------
|
||||
struct TelemetryData {
|
||||
uint32_t uptime_secs;
|
||||
uint16_t battery_mv;
|
||||
uint8_t battery_pct;
|
||||
int16_t temperature; // 0.1°C from BQ27220
|
||||
int16_t temperature;
|
||||
int csq;
|
||||
uint8_t neighbor_count;
|
||||
float freq;
|
||||
@@ -130,10 +114,14 @@ public:
|
||||
bool recvCommand(MQTTCommand& out);
|
||||
bool sendResponse(const char* topic, const char* payload);
|
||||
|
||||
// --- Telemetry (set by main loop, published by modem task) ---
|
||||
// --- Telemetry ---
|
||||
void updateTelemetry(const TelemetryData& data);
|
||||
|
||||
// --- State queries (lock-free reads from main loop) ---
|
||||
// --- OTA ---
|
||||
void requestOTA(const char* url);
|
||||
bool isOTAInProgress() const { return _state == CellState::OTA_IN_PROGRESS; }
|
||||
|
||||
// --- State queries ---
|
||||
CellState getState() const { return _state; }
|
||||
bool isConnected() const { return _state == CellState::CONNECTED; }
|
||||
int getCSQ() const { return _csq; }
|
||||
@@ -146,7 +134,6 @@ public:
|
||||
const char* stateString() const;
|
||||
uint32_t getLastCmdTime() const { return _lastCmdTime; }
|
||||
|
||||
// Load config from SD card
|
||||
static bool loadConfig(MQTTConfig& cfg);
|
||||
|
||||
private:
|
||||
@@ -164,7 +151,6 @@ private:
|
||||
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};
|
||||
@@ -175,19 +161,15 @@ private:
|
||||
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;
|
||||
@@ -195,10 +177,13 @@ private:
|
||||
char _rxTopic[MQTT_TOPIC_MAX];
|
||||
char _rxPayload[MQTT_PAYLOAD_MAX];
|
||||
|
||||
// Reconnect backoff
|
||||
uint32_t _reconnectDelay = MQTT_RECONNECT_MIN;
|
||||
|
||||
// --- Modem UART helpers (modem task only) ---
|
||||
// OTA state
|
||||
volatile bool _otaPending = false;
|
||||
char _otaUrl[256] = {0};
|
||||
|
||||
// --- Modem UART helpers ---
|
||||
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);
|
||||
@@ -210,7 +195,7 @@ private:
|
||||
void resolveAPN();
|
||||
bool activateData();
|
||||
|
||||
// --- MQTT operations (modem task only) ---
|
||||
// --- MQTT operations ---
|
||||
bool mqttStart();
|
||||
bool mqttConnect();
|
||||
bool mqttSubscribe(const char* topic);
|
||||
@@ -224,6 +209,14 @@ private:
|
||||
void handleMqttRxEnd();
|
||||
void handleMqttConnLost(const char* line);
|
||||
|
||||
// --- OTA operations (modem task only) ---
|
||||
void performOTA();
|
||||
int httpGet(const char* url);
|
||||
bool httpReadChunk(int offset, int len, uint8_t* dest, int* bytesRead);
|
||||
void httpTerm();
|
||||
void otaPublish(const char* msg);
|
||||
int readRawBytes(uint8_t* dest, int count, uint32_t timeout_ms);
|
||||
|
||||
// --- Task ---
|
||||
static void taskEntry(void* param);
|
||||
void taskLoop();
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#include <SD.h>
|
||||
#include <esp_mac.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/time.h>
|
||||
#include <Update.h>
|
||||
|
||||
CellularMQTT cellularMQTT;
|
||||
|
||||
@@ -77,6 +78,17 @@ void CellularMQTT::updateTelemetry(const TelemetryData& data) {
|
||||
}
|
||||
}
|
||||
|
||||
void CellularMQTT::requestOTA(const char* url) {
|
||||
if (_state == CellState::OTA_IN_PROGRESS) {
|
||||
Serial.println("[OTA] Already in progress");
|
||||
return;
|
||||
}
|
||||
strncpy(_otaUrl, url, sizeof(_otaUrl) - 1);
|
||||
_otaUrl[sizeof(_otaUrl) - 1] = '\0';
|
||||
_otaPending = true;
|
||||
Serial.printf("[OTA] Requested: %s\n", url);
|
||||
}
|
||||
|
||||
int CellularMQTT::getSignalBars() const {
|
||||
if (_csq == 99 || _csq == 0) return 0;
|
||||
if (_csq <= 5) return 1;
|
||||
@@ -97,6 +109,7 @@ const char* CellularMQTT::stateString() const {
|
||||
case CellState::MQTT_CONNECTING: return "MQTT CONN";
|
||||
case CellState::CONNECTED: return "CONNECTED";
|
||||
case CellState::RECONNECTING: return "RECONN";
|
||||
case CellState::OTA_IN_PROGRESS: return "OTA";
|
||||
case CellState::ERROR: return "ERROR";
|
||||
default: return "???";
|
||||
}
|
||||
@@ -104,12 +117,6 @@ const char* CellularMQTT::stateString() const {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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) {
|
||||
@@ -133,7 +140,6 @@ bool CellularMQTT::loadConfig(MQTTConfig& cfg) {
|
||||
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) {
|
||||
@@ -143,7 +149,6 @@ bool CellularMQTT::loadConfig(MQTTConfig& cfg) {
|
||||
|
||||
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);
|
||||
@@ -155,7 +160,7 @@ bool CellularMQTT::loadConfig(MQTTConfig& cfg) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modem power-on (same sequence as ModemManager)
|
||||
// Modem power-on
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::modemPowerOn() {
|
||||
@@ -366,11 +371,8 @@ void CellularMQTT::handleMqttRxPayload(const char* data, int len) {
|
||||
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);
|
||||
// Handle OTA directly in the modem task (not queued through main loop)
|
||||
requestOTA(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,11 +388,10 @@ void CellularMQTT::handleMqttConnLost(const char* line) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// APN resolution (reuses Meck's ApnDatabase)
|
||||
// APN resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::resolveAPN() {
|
||||
// 1. Check SD config
|
||||
File f = SD.open("/remote/apn.cfg", FILE_READ);
|
||||
if (f) {
|
||||
String line = f.readStringUntil('\n');
|
||||
@@ -406,7 +407,6 @@ void CellularMQTT::resolveAPN() {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check modem's current APN
|
||||
if (sendAT("AT+CGDCONT?", "OK", 3000)) {
|
||||
char* p = strstr(_atBuf, "+CGDCONT:");
|
||||
if (p) {
|
||||
@@ -429,7 +429,6 @@ void CellularMQTT::resolveAPN() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Auto-detect from IMSI
|
||||
if (_imsi[0]) {
|
||||
const ApnEntry* entry = apnLookupFromIMSI(_imsi);
|
||||
if (entry) {
|
||||
@@ -447,7 +446,7 @@ void CellularMQTT::resolveAPN() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data connection — activate PDP context
|
||||
// Data connection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CellularMQTT::activateData() {
|
||||
@@ -462,7 +461,6 @@ bool CellularMQTT::activateData() {
|
||||
}
|
||||
}
|
||||
|
||||
// Query IP address
|
||||
if (sendAT("AT+CGPADDR=1", "OK", 5000)) {
|
||||
char* p = strstr(_atBuf, "+CGPADDR:");
|
||||
if (p) {
|
||||
@@ -497,7 +495,6 @@ bool CellularMQTT::mqttStart() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
@@ -505,16 +502,9 @@ bool CellularMQTT::mqttStart() {
|
||||
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;
|
||||
@@ -529,11 +519,9 @@ bool CellularMQTT::mqttConnect() {
|
||||
|
||||
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);
|
||||
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';
|
||||
@@ -545,10 +533,8 @@ bool CellularMQTT::mqttConnect() {
|
||||
_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();
|
||||
@@ -570,7 +556,6 @@ bool CellularMQTT::mqttConnect() {
|
||||
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);
|
||||
@@ -596,7 +581,6 @@ 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);
|
||||
@@ -611,7 +595,6 @@ bool CellularMQTT::mqttPublish(const char* topic, const char* payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Set payload
|
||||
snprintf(cmd, sizeof(cmd), "AT+CMQTTPAYLOAD=0,%d", plen);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
if (!waitPrompt(5000)) {
|
||||
@@ -624,14 +607,12 @@ bool CellularMQTT::mqttPublish(const char* topic, const char* payload) {
|
||||
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;
|
||||
}
|
||||
@@ -642,6 +623,256 @@ void CellularMQTT::mqttDisconnect() {
|
||||
sendAT("AT+CMQTTSTOP", "OK", 5000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OTA — HTTP download + ESP32 flash
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CellularMQTT::otaPublish(const char* msg) {
|
||||
Serial.printf("[OTA] %s\n", msg);
|
||||
mqttPublish(_topicRsp, msg);
|
||||
}
|
||||
|
||||
int CellularMQTT::readRawBytes(uint8_t* dest, int count, uint32_t timeout_ms) {
|
||||
unsigned long start = millis();
|
||||
int received = 0;
|
||||
while (received < count && millis() - start < timeout_ms) {
|
||||
while (MODEM_SERIAL.available() && received < count) {
|
||||
dest[received++] = MODEM_SERIAL.read();
|
||||
}
|
||||
if (received < count) vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
||||
int CellularMQTT::httpGet(const char* url) {
|
||||
// Terminate any previous HTTP session
|
||||
sendAT("AT+HTTPTERM", "OK", 2000);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
if (!sendAT("AT+HTTPINIT", "OK", 5000)) {
|
||||
Serial.println("[OTA] HTTPINIT failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set URL via prompt pattern
|
||||
int urlLen = strlen(url);
|
||||
char cmd[40];
|
||||
snprintf(cmd, sizeof(cmd), "AT+HTTPPARA=\"URL\",%d", urlLen);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
if (!waitPrompt(5000)) {
|
||||
Serial.println("[OTA] No prompt for HTTPPARA URL");
|
||||
httpTerm();
|
||||
return -1;
|
||||
}
|
||||
MODEM_SERIAL.write((const uint8_t*)url, urlLen);
|
||||
if (!waitResponse("OK", 10000, _atBuf, AT_BUF_SIZE)) {
|
||||
Serial.println("[OTA] HTTPPARA URL failed");
|
||||
httpTerm();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// SSL config for HTTPS
|
||||
if (strncmp(url, "https://", 8) == 0) {
|
||||
sendAT("AT+HTTPPARA=\"SSLCFG\",0", "OK", 3000);
|
||||
}
|
||||
|
||||
// Follow redirects (GitHub releases use 302)
|
||||
sendAT("AT+HTTPPARA=\"REDIR\",1", "OK", 2000);
|
||||
|
||||
// Execute GET — response: +HTTPACTION: 0,<status>,<content_length>
|
||||
MODEM_SERIAL.println("AT+HTTPACTION=0");
|
||||
|
||||
// Wait for +HTTPACTION URC — download can take minutes over Cat-1
|
||||
unsigned long start = millis();
|
||||
int pos = 0;
|
||||
_atBuf[0] = '\0';
|
||||
|
||||
while (millis() - start < 180000) { // 3 minute timeout
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
if (pos < AT_BUF_SIZE - 1) {
|
||||
_atBuf[pos++] = c;
|
||||
_atBuf[pos] = '\0';
|
||||
}
|
||||
char* p = strstr(_atBuf, "+HTTPACTION:");
|
||||
if (p) {
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
while (MODEM_SERIAL.available() && pos < AT_BUF_SIZE - 1) {
|
||||
_atBuf[pos++] = MODEM_SERIAL.read();
|
||||
_atBuf[pos] = '\0';
|
||||
}
|
||||
|
||||
int method, status, contentLen;
|
||||
if (sscanf(p, "+HTTPACTION: %d,%d,%d", &method, &status, &contentLen) == 3) {
|
||||
Serial.printf("[OTA] HTTP status=%d content_length=%d\n", status, contentLen);
|
||||
if (status == 200 && contentLen > 0) {
|
||||
return contentLen;
|
||||
}
|
||||
Serial.printf("[OTA] HTTP download failed (status %d)\n", status);
|
||||
httpTerm();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
Serial.println("[OTA] HTTP download timeout");
|
||||
httpTerm();
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool CellularMQTT::httpReadChunk(int offset, int len, uint8_t* dest, int* bytesRead) {
|
||||
*bytesRead = 0;
|
||||
|
||||
// AT+HTTPREAD=<offset>,<length>
|
||||
// Response: +HTTPREAD: <actual_len>\r\n<binary data>\r\nOK
|
||||
char cmd[40];
|
||||
snprintf(cmd, sizeof(cmd), "AT+HTTPREAD=%d,%d", offset, len);
|
||||
MODEM_SERIAL.println(cmd);
|
||||
|
||||
// Wait for +HTTPREAD: <len> header
|
||||
unsigned long start = millis();
|
||||
int pos = 0;
|
||||
_atBuf[0] = '\0';
|
||||
|
||||
while (millis() - start < 10000) {
|
||||
while (MODEM_SERIAL.available()) {
|
||||
char c = MODEM_SERIAL.read();
|
||||
if (pos < AT_BUF_SIZE - 1) {
|
||||
_atBuf[pos++] = c;
|
||||
_atBuf[pos] = '\0';
|
||||
}
|
||||
|
||||
char* p = strstr(_atBuf, "+HTTPREAD:");
|
||||
if (p) {
|
||||
char* nl = strchr(p, '\n');
|
||||
if (nl) {
|
||||
int actualLen = 0;
|
||||
sscanf(p, "+HTTPREAD: %d", &actualLen);
|
||||
if (actualLen <= 0 || actualLen > len) {
|
||||
Serial.printf("[OTA] Bad HTTPREAD len: %d\n", actualLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read exactly actualLen binary bytes
|
||||
int got = readRawBytes(dest, actualLen, 15000);
|
||||
if (got != actualLen) {
|
||||
Serial.printf("[OTA] Short read: got %d expected %d\n", got, actualLen);
|
||||
return false;
|
||||
}
|
||||
|
||||
*bytesRead = actualLen;
|
||||
|
||||
// Drain trailing \r\nOK\r\n
|
||||
waitResponse("OK", 3000, _atBuf, AT_BUF_SIZE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
Serial.println("[OTA] HTTPREAD timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
void CellularMQTT::httpTerm() {
|
||||
sendAT("AT+HTTPTERM", "OK", 3000);
|
||||
}
|
||||
|
||||
void CellularMQTT::performOTA() {
|
||||
_otaPending = false;
|
||||
_state = CellState::OTA_IN_PROGRESS;
|
||||
|
||||
otaPublish("OTA: Starting download...");
|
||||
Serial.printf("[OTA] URL: %s\n", _otaUrl);
|
||||
|
||||
// Disconnect MQTT — modem can only do one thing at a time
|
||||
mqttDisconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
|
||||
// Download firmware via HTTP
|
||||
int fileSize = httpGet(_otaUrl);
|
||||
if (fileSize <= 0) {
|
||||
Serial.println("[OTA] Download failed");
|
||||
httpTerm();
|
||||
_state = CellState::RECONNECTING;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[OTA] Downloaded %d bytes, flashing...\n", fileSize);
|
||||
|
||||
// Begin ESP32 OTA
|
||||
if (!Update.begin(fileSize)) {
|
||||
Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString());
|
||||
httpTerm();
|
||||
_state = CellState::RECONNECTING;
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate chunk buffer
|
||||
uint8_t* chunk = (uint8_t*)malloc(OTA_CHUNK_SIZE);
|
||||
if (!chunk) {
|
||||
Serial.println("[OTA] malloc failed");
|
||||
Update.abort();
|
||||
httpTerm();
|
||||
_state = CellState::RECONNECTING;
|
||||
return;
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
int lastPct = -1;
|
||||
|
||||
while (offset < fileSize) {
|
||||
int remaining = fileSize - offset;
|
||||
int toRead = (remaining < OTA_CHUNK_SIZE) ? remaining : OTA_CHUNK_SIZE;
|
||||
|
||||
int bytesRead = 0;
|
||||
if (!httpReadChunk(offset, toRead, chunk, &bytesRead) || bytesRead == 0) {
|
||||
Serial.printf("[OTA] Read failed at offset %d\n", offset);
|
||||
free(chunk);
|
||||
Update.abort();
|
||||
httpTerm();
|
||||
_state = CellState::RECONNECTING;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t written = Update.write(chunk, bytesRead);
|
||||
if (written != (size_t)bytesRead) {
|
||||
Serial.printf("[OTA] Write failed: wrote %d of %d\n", written, bytesRead);
|
||||
free(chunk);
|
||||
Update.abort();
|
||||
httpTerm();
|
||||
_state = CellState::RECONNECTING;
|
||||
return;
|
||||
}
|
||||
|
||||
offset += bytesRead;
|
||||
|
||||
int pct = (offset * 100) / fileSize;
|
||||
if (pct / 10 != lastPct / 10) {
|
||||
Serial.printf("[OTA] Flash progress: %d%% (%d/%d)\n", pct, offset, fileSize);
|
||||
lastPct = pct;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); // Yield to watchdog
|
||||
}
|
||||
|
||||
free(chunk);
|
||||
httpTerm();
|
||||
|
||||
if (!Update.end(true)) {
|
||||
Serial.printf("[OTA] Update.end failed: %s\n", Update.errorString());
|
||||
_state = CellState::RECONNECTING;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("[OTA] SUCCESS — rebooting in 3 seconds");
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FreeRTOS Task
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -710,7 +941,6 @@ restart:
|
||||
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, '"');
|
||||
@@ -736,7 +966,7 @@ restart:
|
||||
Serial.printf("[Cell] Registered: oper=%s CSQ=%d APN=%s IMEI=%s\n",
|
||||
_operator, _csq, _apn[0] ? _apn : "(none)", _imei);
|
||||
|
||||
// Sync ESP32 system clock from modem network time
|
||||
// Sync ESP32 system clock from modem network time
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
if (attempt > 0) vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
if (sendAT("AT+CCLK?", "OK", 3000)) {
|
||||
@@ -744,7 +974,7 @@ restart:
|
||||
if (p) {
|
||||
int yy=0, mo=0, dd=0, hh=0, mm=0, ss=0, tz=0;
|
||||
if (sscanf(p, "+CCLK: \"%d/%d/%d,%d:%d:%d", &yy, &mo, &dd, &hh, &mm, &ss) >= 6) {
|
||||
if (yy < 24 || yy > 50) continue; // Not synced yet
|
||||
if (yy < 24 || yy > 50) continue;
|
||||
char* tzp = p + 7;
|
||||
while (*tzp && *tzp != '+' && *tzp != '-') tzp++;
|
||||
if (*tzp) tz = atoi(tzp);
|
||||
@@ -792,10 +1022,9 @@ restart:
|
||||
goto restart;
|
||||
}
|
||||
|
||||
// Allow MQTT session to stabilise before subscribing
|
||||
// Allow MQTT session to stabilise before subscribing
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
|
||||
// Subscribe with retry — the modem sometimes misses the first prompt
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (mqttSubscribe(_topicCmd)) break;
|
||||
Serial.printf("[Cell] Subscribe retry %d for cmd topic\n", i + 1);
|
||||
@@ -818,9 +1047,15 @@ restart:
|
||||
unsigned long lastTelem = 0;
|
||||
|
||||
while (true) {
|
||||
// Check for pending OTA request
|
||||
if (_otaPending && _state == CellState::CONNECTED) {
|
||||
performOTA();
|
||||
continue; // After OTA failure, reconnect loop handles recovery
|
||||
}
|
||||
|
||||
drainURCs();
|
||||
|
||||
// Health check: too many consecutive publish failures = silent disconnect
|
||||
// Health check
|
||||
if (_pubFailCount >= MQTT_PUB_FAIL_MAX && _state == CellState::CONNECTED) {
|
||||
Serial.printf("[Cell] %d consecutive publish failures — forcing reconnect\n", _pubFailCount);
|
||||
_state = CellState::RECONNECTING;
|
||||
@@ -835,7 +1070,6 @@ restart:
|
||||
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));
|
||||
@@ -844,7 +1078,7 @@ restart:
|
||||
}
|
||||
|
||||
if (!mqttStart() || !mqttConnect()) {
|
||||
continue; // Retry with backoff
|
||||
continue;
|
||||
}
|
||||
|
||||
mqttSubscribe(_topicCmd);
|
||||
|
||||
@@ -43,12 +43,10 @@ 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
|
||||
|
||||
@@ -124,16 +122,7 @@ void setup() {
|
||||
#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);
|
||||
|
||||
// Re-claim shared HSPI bus — SD.begin() steals GPIO 36/47/33
|
||||
extern SPIClass displaySpi;
|
||||
displaySpi.begin(PIN_DISPLAY_SCLK, 47, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
|
||||
Serial.printf("SD card: %s\n", sdCardReady ? "ready" : "FAILED");
|
||||
}
|
||||
|
||||
// Start cellular MQTT
|
||||
@@ -187,15 +176,6 @@ void loop() {
|
||||
{
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user