mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-02 03:22:38 +02:00
487 lines
15 KiB
C++
487 lines
15 KiB
C++
#ifdef MECK_WIFI_REMOTE
|
|
|
|
#include "WiFiMQTT.h"
|
|
#include <esp_mac.h>
|
|
#include <Update.h>
|
|
#include <HTTPClient.h>
|
|
#include "target.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));
|
|
|
|
// WiFi config: read SSID/password pairs
|
|
File wf = SD.open(WIFI_CONFIG_FILE, FILE_READ);
|
|
if (!wf) {
|
|
Serial.println("[WiFi] No /remote/wifi.cfg");
|
|
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 = SD.open(MQTT_CONFIG_FILE, FILE_READ);
|
|
if (!mf) {
|
|
Serial.println("[WiFi] No /remote/mqtt.cfg");
|
|
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();
|
|
|
|
char json[400];
|
|
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}",
|
|
_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());
|
|
|
|
_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);
|
|
|
|
// 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(otaClient, _otaUrl)) {
|
|
Serial.println("[OTA] HTTP begin failed");
|
|
_state = WiFiMQTTState::MQTT_CONNECTING;
|
|
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
|