mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-25 04:11:13 +02:00
abc2457123
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>
509 lines
16 KiB
C++
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
|