fix wifi repeater and remote repeater ota process, update firmware version platiformio

This commit is contained in:
pelgraine
2026-04-04 11:40:25 +11:00
parent 424e152d4b
commit 9d45ac52eb
4 changed files with 307 additions and 79 deletions
+38 -46
View File
@@ -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,13 @@ 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();
int readRawBytes(uint8_t* dest, int count, uint32_t timeout_ms);
// --- Task ---
static void taskEntry(void* param);
void taskLoop();
+250 -6
View File
@@ -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;
@@ -97,11 +98,23 @@ 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 "???";
}
}
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);
}
// ---------------------------------------------------------------------------
// Config file: /remote/mqtt.cfg
// Format (one value per line):
@@ -366,11 +379,7 @@ 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);
requestOTA(data);
}
}
@@ -642,6 +651,235 @@ void CellularMQTT::mqttDisconnect() {
sendAT("AT+CMQTTSTOP", "OK", 5000);
}
// ---------------------------------------------------------------------------
// OTA — HTTP download via A7682E + ESP32 flash
// ---------------------------------------------------------------------------
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) {
sendAT("AT+HTTPTERM", "OK", 2000);
vTaskDelay(pdMS_TO_TICKS(500));
if (!sendAT("AT+HTTPINIT", "OK", 5000)) {
Serial.println("[OTA] HTTPINIT failed");
return -1;
}
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;
}
if (strncmp(url, "https://", 8) == 0) {
sendAT("AT+HTTPPARA=\"SSLCFG\",0", "OK", 3000);
}
sendAT("AT+HTTPPARA=\"REDIR\",1", "OK", 2000);
MODEM_SERIAL.println("AT+HTTPACTION=0");
unsigned long start = millis();
int pos = 0;
_atBuf[0] = '\0';
while (millis() - start < 180000) {
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;
char cmd[40];
snprintf(cmd, sizeof(cmd), "AT+HTTPREAD=%d,%d", offset, len);
MODEM_SERIAL.println(cmd);
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;
}
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;
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;
Serial.printf("[OTA] URL: %s\n", _otaUrl);
// Disconnect MQTT — modem can only do one thing at a time
mqttDisconnect();
vTaskDelay(pdMS_TO_TICKS(1000));
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);
if (!Update.begin(fileSize)) {
Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString());
httpTerm();
_state = CellState::RECONNECTING;
return;
}
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));
}
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
// ---------------------------------------------------------------------------
@@ -818,6 +1056,12 @@ restart:
unsigned long lastTelem = 0;
while (true) {
// Check for pending OTA request
if (_otaPending && _state == CellState::CONNECTED) {
performOTA();
continue;
}
drainURCs();
// Health check: too many consecutive publish failures = silent disconnect
+13 -23
View File
@@ -389,37 +389,36 @@ void WiFiMQTT::performOTA() {
Serial.printf("[OTA] URL: %s\n", _otaUrl);
_mqttClient.publish(_topicRsp, "OTA: Starting download...");
_mqttClient.loop();
// Disconnect MQTT cleanly — we need TLS resources for HTTP
_mqttClient.disconnect();
// Use a separate TLS client — don't reuse the MQTT one
WiFiClientSecure otaClient;
otaClient.setInsecure();
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(180000);
if (!http.begin(_wifiClient, _otaUrl)) {
if (!http.begin(otaClient, _otaUrl)) {
Serial.println("[OTA] HTTP begin failed");
_mqttClient.publish(_topicRsp, "OTA: HTTP begin failed");
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("[OTA] HTTP error: %d\n", httpCode);
char msg[60];
snprintf(msg, sizeof(msg), "OTA: HTTP error %d", httpCode);
_mqttClient.publish(_topicRsp, msg);
http.end();
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
int fileSize = http.getSize();
if (fileSize <= 0) {
Serial.println("[OTA] Unknown content length");
_mqttClient.publish(_topicRsp, "OTA: Unknown file size");
http.end();
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
@@ -427,9 +426,8 @@ void WiFiMQTT::performOTA() {
if (!Update.begin(fileSize)) {
Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString());
_mqttClient.publish(_topicRsp, "OTA: Flash init failed");
http.end();
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
@@ -461,10 +459,6 @@ void WiFiMQTT::performOTA() {
int pct = (offset * 100) / fileSize;
if (pct / 10 != lastPct / 10) {
Serial.printf("[OTA] Progress: %d%% (%d/%d)\n", pct, offset, fileSize);
char msg[60];
snprintf(msg, sizeof(msg), "OTA: Flashing %d%%", pct);
_mqttClient.publish(_topicRsp, msg);
_mqttClient.loop();
lastPct = pct;
}
@@ -476,21 +470,17 @@ void WiFiMQTT::performOTA() {
if (offset < fileSize) {
Serial.printf("[OTA] Incomplete: %d of %d\n", offset, fileSize);
Update.abort();
_mqttClient.publish(_topicRsp, "OTA: Download incomplete");
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
if (!Update.end(true)) {
Serial.printf("[OTA] Update.end failed: %s\n", Update.errorString());
_mqttClient.publish(_topicRsp, "OTA: Verification failed");
_state = WiFiMQTTState::CONNECTED;
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
Serial.println("[OTA] SUCCESS — rebooting in 3 seconds");
_mqttClient.publish(_topicRsp, "OTA: Success! Rebooting...");
_mqttClient.loop();
delay(3000);
ESP.restart();
}
+6 -4
View File
@@ -299,8 +299,8 @@ build_flags =
-D DISABLE_WIFI_OTA=1
-D MECK_REMOTE_REPEATER=1
-D MAX_NEIGHBOURS=50
-D FIRMWARE_VERSION='"Meck RemRptr v0.2"'
-D FIRMWARE_BUILD_DATE='"3 April 2026"'
-D FIRMWARE_VERSION='"Meck RemRptr v0.3"'
-D FIRMWARE_BUILD_DATE='"4 April 2026"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -318,10 +318,12 @@ build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<../examples/simple_repeater/*.cpp>
build_flags =
${LilyGo_TDeck_Pro.build_flags}
-D FIRMWARE_VERSION='"Meck WiFi Rptr v0.2"'
-D FIRMWARE_BUILD_DATE='"3 April 2026"'
-D FIRMWARE_VERSION='"Meck WiFi Rptr v0.3"'
-D FIRMWARE_BUILD_DATE='"4 April 2026"'
-D MAX_NEIGHBOURS=50
-D MECK_WIFI_REMOTE
-D MECK_REMOTE_REPEATER=1
-D DISABLE_WIFI_OTA=1
lib_deps =
${LilyGo_TDeck_Pro.lib_deps}
knolleary/PubSubClient@^2.8