Files
Meck/examples/simple_repeater/wifimqtt.cpp
T
Nic Boet abc2457123 Fix remaining build failures found by a full pio run across all envs
A complete `pio run` (all 16 environments) on Linux turned up 6
failures beyond the case-sensitive-include fix already on this
branch:

1. meck_remote_repeater, meck_wifi_repeater, meck_wifi_repeater_t5s3
   compile GxEPDDisplay.cpp / FastEPDDisplay.cpp, which #include
   MeckFonts.h whenever HAS_MECK_FONTS is defined. HAS_MECK_FONTS is
   set at the shared variant-base level (so all envs in that variant
   inherit it), but the `-I examples/companion_radio/ui-new` include
   path needed to find MeckFonts.h was only added to the
   companion-radio envs, not the repeater envs in the same file.
   Added the missing -I flag to the three repeater env sections.

2. meck_wifi_repeater_heltec_v3/v4/v4_headless failed with "missing
   SConscript file 'merge-bin.py'". esp32_base's extra_scripts still
   pointed at merge-bin.py, which was deleted in 451f4b01 ("remove
   redundant merge script"). Other variants override extra_scripts
   themselves so they never hit the stale base default; the Heltec
   variants don't, so they inherited the broken reference. Pointed
   esp32_base at merge_firmware.py instead, matching every variant
   that already sets this explicitly.

3. After fix #1, meck_wifi_repeater_t5s3 still failed: wifimqtt.cpp
   had a hardcoded `extern AutoDiscoverRTCClock rtc_clock;`, but the
   T5S3 variant declares its global rtc_clock as PCF85063Clock (a
   different concrete RTCClock subclass) in target.h. The local
   extern was redundant anyway, since target.h is already included
   at the top of this file and declares the correctly-typed global
   for whichever variant is being built. Removed the stale extern so
   the call resolves through the existing declaration.

Verified: `pio run` now succeeds for all 16 environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 16:11:36 -05:00

509 lines
16 KiB
C++

#ifdef MECK_WIFI_REMOTE
#include "target.h"
#include "WiFiMQTT.h"
#include <esp_mac.h>
#include <Update.h>
#include <HTTPClient.h>
WiFiMQTT wifiMQTT;
#define WIFI_CONFIG_FILE "/remote/wifi.cfg"
#define MQTT_CONFIG_FILE "/remote/mqtt.cfg"
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void WiFiMQTT::begin() {
Serial.println("[WiFi] begin()");
_state = WiFiMQTTState::OFF;
_cmdHead = _cmdTail = 0;
_rspHead = _rspTail = 0;
_activeNetwork = 0;
if (!loadConfig(_config)) {
Serial.println("[WiFi] ERROR: Missing config files — cannot start");
_state = WiFiMQTTState::ERROR;
return;
}
Serial.printf("[WiFi] Config: %d network(s), broker=%s:%d id=%s\n",
_config.networkCount, _config.broker, _config.port, _config.deviceId);
for (int i = 0; i < _config.networkCount; i++) {
Serial.printf("[WiFi] %d: %s\n", i + 1, _config.networks[i].ssid);
}
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);
// Configure TLS — skip server cert verification (same as cellular)
_wifiClient.setInsecure();
_mqttClient.setClient(_wifiClient);
_mqttClient.setServer(_config.broker, _config.port);
_mqttClient.setCallback(mqttCallback);
_mqttClient.setBufferSize(MQTT_PAYLOAD_MAX + MQTT_TOPIC_MAX);
_state = WiFiMQTTState::WIFI_CONNECTING;
}
void WiFiMQTT::loop() {
if (_state == WiFiMQTTState::OFF || _state == WiFiMQTTState::ERROR) return;
// Check for pending OTA
if (_otaPending && _state == WiFiMQTTState::CONNECTED) {
performOTA();
return;
}
// WiFi connection management
if (WiFi.status() != WL_CONNECTED) {
if (_state == WiFiMQTTState::CONNECTED || _state == WiFiMQTTState::MQTT_CONNECTING) {
Serial.println("[WiFi] Connection lost");
_state = WiFiMQTTState::WIFI_CONNECTING;
}
if (millis() - _lastWifiAttempt > WIFI_RECONNECT_MS) {
connectWiFi();
_lastWifiAttempt = millis();
}
return;
}
// WiFi is up — check MQTT
if (!_mqttClient.connected()) {
if (_state == WiFiMQTTState::CONNECTED) {
Serial.println("[WiFi] MQTT disconnected");
}
_state = WiFiMQTTState::MQTT_CONNECTING;
if (millis() - _lastMqttAttempt > MQTT_RECONNECT_MS) {
connectMQTT();
_lastMqttAttempt = millis();
}
return;
}
// Connected — run MQTT loop
_mqttClient.loop();
// Publish queued responses
publishQueuedResponses();
// Periodic RSSI
if (millis() - _lastRSSI > 30000) {
_rssi = WiFi.RSSI();
_lastRSSI = millis();
}
// Periodic telemetry
if (millis() - _lastTelem > TELEMETRY_INTERVAL) {
publishTelemetry();
_lastTelem = millis();
}
}
bool WiFiMQTT::recvCommand(MQTTCommand& out) {
if (_cmdHead == _cmdTail) return false;
memcpy(&out, &_cmdBuf[_cmdTail], sizeof(MQTTCommand));
_cmdTail = (_cmdTail + 1) % CMD_QUEUE_SIZE;
return true;
}
bool WiFiMQTT::sendResponse(const char* topic, const char* payload) {
int next = (_rspHead + 1) % RSP_QUEUE_SIZE;
if (next == _rspTail) return false; // Full
memset(&_rspBuf[_rspHead], 0, sizeof(MQTTResponse));
strncpy(_rspBuf[_rspHead].topic, topic, MQTT_TOPIC_MAX - 1);
strncpy(_rspBuf[_rspHead].payload, payload, MQTT_PAYLOAD_MAX - 1);
_rspHead = next;
return true;
}
void WiFiMQTT::updateTelemetry(const TelemetryData& data) {
memcpy(&_telemetry, &data, sizeof(data));
}
void WiFiMQTT::requestOTA(const char* url) {
if (_state == WiFiMQTTState::OTA_IN_PROGRESS) return;
strncpy(_otaUrl, url, sizeof(_otaUrl) - 1);
_otaUrl[sizeof(_otaUrl) - 1] = '\0';
_otaPending = true;
Serial.printf("[OTA] Requested: %s\n", url);
}
int WiFiMQTT::getSignalBars() const {
if (_rssi == 0) return 0;
if (_rssi > -50) return 5;
if (_rssi > -60) return 4;
if (_rssi > -70) return 3;
if (_rssi > -80) return 2;
return 1;
}
const char* WiFiMQTT::stateString() const {
switch (_state) {
case WiFiMQTTState::OFF: return "OFF";
case WiFiMQTTState::WIFI_CONNECTING: return "WiFi...";
case WiFiMQTTState::WIFI_CONNECTED: return "WiFi OK";
case WiFiMQTTState::MQTT_CONNECTING: return "MQTT...";
case WiFiMQTTState::CONNECTED: return "CONNECTED";
case WiFiMQTTState::OTA_IN_PROGRESS: return "OTA";
case WiFiMQTTState::ERROR: return "ERROR";
default: return "???";
}
}
// ---------------------------------------------------------------------------
// Config files
//
// /remote/wifi.cfg — SSID/password pairs, two lines each:
// HomeNetwork
// HomePassword
// BackupNetwork
// BackupPassword
//
// /remote/mqtt.cfg — same format as cellular variant
// ---------------------------------------------------------------------------
bool WiFiMQTT::loadConfig(WiFiMQTTConfig& cfg) {
memset(&cfg, 0, sizeof(cfg));
// Determine filesystem: SD if available, otherwise SPIFFS
// Heltec V4 and other headless boards have no SD slot — config lives in SPIFFS.
// Upload config files via: pio run -t uploadfs (with data/ folder)
#if defined(HAS_SDCARD) || defined(SDCARD_CS)
fs::FS& configFS = SD;
Serial.println("[WiFi] Config source: SD card");
#else
fs::FS& configFS = SPIFFS;
Serial.println("[WiFi] Config source: SPIFFS");
#endif
// WiFi config: read SSID/password pairs
File wf = configFS.open(WIFI_CONFIG_FILE, FILE_READ);
if (!wf) {
Serial.printf("[WiFi] No %s\n", WIFI_CONFIG_FILE);
return false;
}
cfg.networkCount = 0;
while (wf.available() && cfg.networkCount < MAX_WIFI_NETWORKS) {
String ssid = wf.readStringUntil('\n'); ssid.trim();
if (ssid.length() == 0) break;
String pass = wf.readStringUntil('\n'); pass.trim();
strncpy(cfg.networks[cfg.networkCount].ssid, ssid.c_str(), sizeof(cfg.networks[0].ssid) - 1);
strncpy(cfg.networks[cfg.networkCount].password, pass.c_str(), sizeof(cfg.networks[0].password) - 1);
cfg.networkCount++;
}
wf.close();
if (cfg.networkCount == 0) {
Serial.println("[WiFi] No networks in wifi.cfg");
return false;
}
// MQTT config: /remote/mqtt.cfg (same format as cellular)
File mf = configFS.open(MQTT_CONFIG_FILE, FILE_READ);
if (!mf) {
Serial.printf("[WiFi] No %s\n", MQTT_CONFIG_FILE);
return false;
}
String line;
line = mf.readStringUntil('\n'); line.trim();
strncpy(cfg.broker, line.c_str(), sizeof(cfg.broker) - 1);
line = mf.readStringUntil('\n'); line.trim();
cfg.port = line.length() > 0 ? line.toInt() : 8883;
line = mf.readStringUntil('\n'); line.trim();
strncpy(cfg.username, line.c_str(), sizeof(cfg.username) - 1);
line = mf.readStringUntil('\n'); line.trim();
strncpy(cfg.password, line.c_str(), sizeof(cfg.password) - 1);
if (mf.available()) {
line = mf.readStringUntil('\n'); line.trim();
if (line.length() > 0) {
strncpy(cfg.deviceId, line.c_str(), sizeof(cfg.deviceId) - 1);
}
}
mf.close();
// Auto-generate device ID 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';
}
// ---------------------------------------------------------------------------
// WiFi connection — tries each configured network in order
// ---------------------------------------------------------------------------
bool WiFiMQTT::connectWiFi() {
WiFi.mode(WIFI_STA);
for (int n = 0; n < _config.networkCount; n++) {
Serial.printf("[WiFi] Trying %s (%d/%d)...\n",
_config.networks[n].ssid, n + 1, _config.networkCount);
WiFi.begin(_config.networks[n].ssid, _config.networks[n].password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
delay(100);
}
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
snprintf(_ipAddr, sizeof(_ipAddr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
_rssi = WiFi.RSSI();
_activeNetwork = n;
Serial.printf("[WiFi] Connected to %s — IP: %s RSSI: %d\n",
_config.networks[n].ssid, _ipAddr, _rssi);
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
snprintf(_ipAddr, sizeof(_ipAddr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
_rssi = WiFi.RSSI();
_activeNetwork = n;
Serial.printf("[WiFi] Connected to %s — IP: %s RSSI: %d\n",
_config.networks[n].ssid, _ipAddr, _rssi);
// Sync clock via NTP
configTime(0, 0, "pool.ntp.org", "time.google.com");
Serial.print("[WiFi] NTP sync...");
int tries = 0;
while (time(nullptr) < 1700000000 && tries < 20) {
delay(500);
tries++;
}
time_t now = time(nullptr);
if (now > 1700000000) {
rtc_clock.setCurrentTime((uint32_t)now);
Serial.printf(" OK (%lu)\n", (unsigned long)now);
} else {
Serial.println(" timeout");
}
_state = WiFiMQTTState::WIFI_CONNECTED;
return true;
}
}
WiFi.disconnect();
delay(500);
}
Serial.println("[WiFi] All networks failed");
return false;
}
// ---------------------------------------------------------------------------
// MQTT connection
// ---------------------------------------------------------------------------
bool WiFiMQTT::connectMQTT() {
Serial.printf("[WiFi] MQTT connecting to %s:%d...\n", _config.broker, _config.port);
char clientId[48];
snprintf(clientId, sizeof(clientId), "%s-%lu", _config.deviceId, millis() & 0xFFFF);
if (_mqttClient.connect(clientId, _config.username, _config.password)) {
Serial.println("[WiFi] MQTT connected!");
_mqttClient.subscribe(_topicCmd, 1);
_mqttClient.subscribe(_topicOta, 1);
_state = WiFiMQTTState::CONNECTED;
// Publish boot event
_mqttClient.publish(_topicTelem, "{\"event\":\"boot\",\"state\":\"connected\"}", true);
return true;
}
Serial.printf("[WiFi] MQTT connect failed, rc=%d\n", _mqttClient.state());
return false;
}
// ---------------------------------------------------------------------------
// MQTT message callback
// ---------------------------------------------------------------------------
void WiFiMQTT::mqttCallback(char* topic, byte* payload, unsigned int length) {
wifiMQTT.onMessage(topic, payload, length);
}
void WiFiMQTT::onMessage(char* topic, byte* payload, unsigned int length) {
char buf[MQTT_PAYLOAD_MAX];
int len = (length < MQTT_PAYLOAD_MAX - 1) ? length : MQTT_PAYLOAD_MAX - 1;
memcpy(buf, payload, len);
buf[len] = '\0';
Serial.printf("[WiFi] RX [%s]: %.80s\n", topic, buf);
if (strstr(topic, "/cmd")) {
int next = (_cmdHead + 1) % CMD_QUEUE_SIZE;
if (next != _cmdTail) {
memset(&_cmdBuf[_cmdHead], 0, sizeof(MQTTCommand));
strncpy(_cmdBuf[_cmdHead].cmd, buf, MQTT_PAYLOAD_MAX - 1);
_cmdHead = next;
Serial.printf("[WiFi] Queued CLI: %s\n", buf);
} else {
Serial.println("[WiFi] Command queue full");
}
} else if (strstr(topic, "/ota")) {
requestOTA(buf);
}
}
// ---------------------------------------------------------------------------
// Publish helpers
// ---------------------------------------------------------------------------
void WiFiMQTT::publishQueuedResponses() {
while (_rspHead != _rspTail) {
_mqttClient.publish(_rspBuf[_rspTail].topic, _rspBuf[_rspTail].payload);
_rspTail = (_rspTail + 1) % RSP_QUEUE_SIZE;
}
}
void WiFiMQTT::publishTelemetry() {
_rssi = WiFi.RSSI();
static const char* loopLabels[] = { "off", "minimal", "moderate", "strict" };
const char* loopStr = _telemetry.loop_detect <= 3 ? loopLabels[_telemetry.loop_detect] : "off";
char json[512];
snprintf(json, sizeof(json),
"{\"uptime\":%lu,\"batt_mv\":%d,\"batt_pct\":%d,\"temp\":%.1f,"
"\"rssi\":%d,\"bars\":%d,\"neighbors\":%d,"
"\"freq\":%.3f,\"bw\":%.1f,\"sf\":%d,\"cr\":%d,\"tx\":%d,"
"\"name\":\"%s\",\"ip\":\"%s\",\"ssid\":\"%s\","
"\"heap\":%d,"
"\"loop_detect\":\"%s\",\"path_hash_mode\":%d,\"flood_max\":%d}",
_telemetry.uptime_secs, _telemetry.battery_mv, _telemetry.battery_pct,
_telemetry.temperature / 10.0f,
_rssi, getSignalBars(), _telemetry.neighbor_count,
_telemetry.freq, _telemetry.bw, _telemetry.sf, _telemetry.cr, _telemetry.tx_power,
_telemetry.node_name, _ipAddr, _config.networks[_activeNetwork].ssid,
ESP.getFreeHeap(),
loopStr, _telemetry.path_hash_mode, _telemetry.flood_max);
_mqttClient.publish(_topicTelem, json);
}
// ---------------------------------------------------------------------------
// OTA — HTTP download over WiFi + ESP32 flash
// ---------------------------------------------------------------------------
void WiFiMQTT::performOTA() {
_otaPending = false;
_state = WiFiMQTTState::OTA_IN_PROGRESS;
Serial.printf("[OTA] URL: %s\n", _otaUrl);
_mqttClient.publish(_topicRsp, "OTA: Starting download...");
_mqttClient.loop();
// Disconnect MQTT cleanly before starting HTTP download —
// we can't reuse _wifiClient (PubSubClient holds the socket)
_mqttClient.disconnect();
delay(100);
// Use a dedicated TLS client for the firmware download
WiFiClientSecure otaClient;
otaClient.setInsecure(); // skip cert verification (same as MQTT client)
otaClient.setTimeout(30); // 30 second socket timeout
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(180000);
if (!http.begin(otaClient, _otaUrl)) {
Serial.println("[OTA] HTTP begin failed");
_state = WiFiMQTTState::MQTT_CONNECTING; // trigger MQTT reconnect
return;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("[OTA] HTTP error: %d\n", httpCode);
http.end();
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
int fileSize = http.getSize();
if (fileSize <= 0) {
Serial.println("[OTA] Unknown content length");
http.end();
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
Serial.printf("[OTA] File size: %d bytes\n", fileSize);
if (!Update.begin(fileSize)) {
Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString());
http.end();
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
WiFiClient* stream = http.getStreamPtr();
uint8_t buf[1024];
int offset = 0;
int lastPct = -1;
while (offset < fileSize) {
int avail = stream->available();
if (avail <= 0) {
if (!stream->connected()) break;
delay(10);
continue;
}
int toRead = (avail < (int)sizeof(buf)) ? avail : sizeof(buf);
int got = stream->readBytes(buf, toRead);
if (got <= 0) break;
size_t written = Update.write(buf, got);
if (written != (size_t)got) {
Serial.printf("[OTA] Write failed: %d of %d\n", written, got);
break;
}
offset += got;
int pct = (offset * 100) / fileSize;
if (pct / 10 != lastPct / 10) {
Serial.printf("[OTA] Progress: %d%% (%d/%d)\n", pct, offset, fileSize);
lastPct = pct;
}
delay(1);
}
http.end();
if (offset < fileSize) {
Serial.printf("[OTA] Incomplete: %d of %d\n", offset, fileSize);
Update.abort();
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
if (!Update.end(true)) {
Serial.printf("[OTA] Update.end failed: %s\n", Update.errorString());
_state = WiFiMQTTState::MQTT_CONNECTING;
return;
}
Serial.println("[OTA] SUCCESS — rebooting in 3 seconds");
delay(3000);
ESP.restart();
}
#endif // MECK_WIFI_REMOTE