Share WiFi OTA seeding across full ESP32 builds
This commit is contained in:
1 parent
3a5f330727
commit
e6abf3ea7d
23 files changed
+497
-95
No files matched your search
@@ -4,6 +4,9 @@
|
||||
#include "helpers/ota/OtaContext.h" // OTA mesh-integration is centralized here so every role gets it
|
||||
#include "helpers/ota/OtaProtocol.h" // decode_adv -> the `ota neighbors` discovery table
|
||||
#include "helpers/ota/OtaSelf.h" // ota_self_firmware -> auto-advertise our own image
|
||||
#if defined(ESP32_PLATFORM) && (defined(WIFI_OTA_SEEDER) || defined(WIFI_SSID))
|
||||
#include "helpers/esp32/WiFiOtaSeeder.h"
|
||||
#endif
|
||||
#ifndef OTA_ANNOUNCE_BOOT_MS
|
||||
#define OTA_ANNOUNCE_BOOT_MS 8000UL // first self-advert ~8 s after boot (settled, but quick to discover)
|
||||
#endif
|
||||
@@ -219,6 +222,10 @@ void Mesh::begin() {
|
||||
void Mesh::loop() {
|
||||
Dispatcher::loop();
|
||||
serviceLoopMaintenance();
|
||||
#if defined(ENABLE_OTA) && defined(ESP32_PLATFORM) && \
|
||||
(defined(WIFI_OTA_SEEDER) || defined(WIFI_SSID))
|
||||
ota::WiFiOtaSeeder::loop();
|
||||
#endif
|
||||
}
|
||||
|
||||
void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
|
||||
|
||||
@@ -4066,6 +4066,23 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#endif
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverGetCmd(sender_timestamp, config, reply)) return;
|
||||
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
||||
// Non-MQTT FULL repeater/room-server builds keep WiFi only for WebConfig.
|
||||
// Their credentials live in the standalone WebConfig NVS namespace rather
|
||||
// than MQTTPrefs, so expose the useful read-only status through the role.
|
||||
if (strcmp(config, "wifi.ssid") == 0) {
|
||||
if (!_callbacks->getWiFiSSID(reply)) {
|
||||
strcpy(reply, "Error: WiFi SSID unavailable on this build");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strcmp(config, "wifi.status") == 0) {
|
||||
if (!_callbacks->getWiFiStatus(reply)) {
|
||||
strcpy(reply, "Error: WiFi status unavailable on this build");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if defined(PORTABLE_ESP32_RADIO_CLI)
|
||||
if (strcmp(config, "int.thresh") == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->interference_threshold);
|
||||
|
||||
@@ -394,6 +394,14 @@ public:
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
virtual bool getWiFiSSID(char* reply) const {
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
virtual bool getWiFiStatus(char* reply) const {
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
|
||||
virtual int getQueueSize() {
|
||||
return 0; // no op by default
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
namespace WiFiOtaSeederPolicy {
|
||||
|
||||
enum class ListenerAction {
|
||||
Keep,
|
||||
Start,
|
||||
Stop,
|
||||
};
|
||||
|
||||
inline ListenerAction listenerAction(bool network_ready, bool listening) {
|
||||
if (network_ready) return listening ? ListenerAction::Keep : ListenerAction::Start;
|
||||
return listening ? ListenerAction::Stop : ListenerAction::Keep;
|
||||
}
|
||||
|
||||
inline bool canAttachTcpFolder(bool folder_active, bool tcp_folder_attached) {
|
||||
return !folder_active || tcp_folder_attached;
|
||||
}
|
||||
|
||||
inline bool tcpFolderWasDetached(bool tcp_folder_attached, bool folder_active) {
|
||||
return tcp_folder_attached && !folder_active;
|
||||
}
|
||||
|
||||
} // namespace WiFiOtaSeederPolicy
|
||||
@@ -151,6 +151,86 @@ bool WebConfigServer::saveStandaloneWiFi(const char* ssid, const char* password,
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool WebConfigServer::formatWiFiSSID(char* reply, size_t reply_len) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
|
||||
char ssid[32] = "";
|
||||
char password[64] = "";
|
||||
uint8_t power_save = 1;
|
||||
bool configured = loadStandaloneWiFi(
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save);
|
||||
if (_active && _active->_wifi_ssid[0]) {
|
||||
strncpy(ssid, _active->_wifi_ssid, sizeof(ssid) - 1);
|
||||
ssid[sizeof(ssid) - 1] = 0;
|
||||
configured = true;
|
||||
}
|
||||
|
||||
snprintf(reply, reply_len, configured ? "> %s" : "> (not configured)", ssid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebConfigServer::formatWiFiStatus(char* reply, size_t reply_len) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
|
||||
char ssid[32] = "";
|
||||
char password[64] = "";
|
||||
uint8_t power_save = 1;
|
||||
bool configured = loadStandaloneWiFi(
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save);
|
||||
WebConfigServer* active = _active;
|
||||
if (active && active->_wifi_ssid[0]) {
|
||||
strncpy(ssid, active->_wifi_ssid, sizeof(ssid) - 1);
|
||||
ssid[sizeof(ssid) - 1] = 0;
|
||||
configured = true;
|
||||
}
|
||||
|
||||
if (active && active->_mode == MODE_SETUP) {
|
||||
char ap_ssid[33] = "";
|
||||
char ip[16] = "";
|
||||
getSetupInfo(ap_ssid, sizeof(ap_ssid), ip, sizeof(ip));
|
||||
snprintf(reply, reply_len, "> setup AP, SSID: %s, IP: %s", ap_ssid, ip);
|
||||
return true;
|
||||
}
|
||||
if (active && active->_mode == MODE_CONNECTING) {
|
||||
snprintf(reply, reply_len, "> connecting, SSID: %s",
|
||||
configured ? ssid : "(not configured)");
|
||||
return true;
|
||||
}
|
||||
|
||||
const wl_status_t status = WiFi.status();
|
||||
if (status == WL_CONNECTED) {
|
||||
snprintf(reply, reply_len, "> connected, SSID: %s, IP: %s, RSSI: %d dBm",
|
||||
configured ? ssid : WiFi.SSID().c_str(),
|
||||
WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||
return true;
|
||||
}
|
||||
if (!configured) {
|
||||
strcpy(reply, "> not configured; run 'start webconfig'");
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case WL_NO_SSID_AVAIL:
|
||||
snprintf(reply, reply_len, "> failed, SSID not found: %s", ssid);
|
||||
break;
|
||||
case WL_CONNECT_FAILED:
|
||||
snprintf(reply, reply_len, "> failed to connect, SSID: %s", ssid);
|
||||
break;
|
||||
case WL_CONNECTION_LOST:
|
||||
snprintf(reply, reply_len, "> connection lost, SSID: %s", ssid);
|
||||
break;
|
||||
default:
|
||||
if (active && active->_mode == MODE_LAN) {
|
||||
snprintf(reply, reply_len, "> disconnected, SSID: %s", ssid);
|
||||
} else {
|
||||
snprintf(reply, reply_len,
|
||||
"> off, configured SSID: %s; run 'start webconfig'", ssid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebConfigServer::requestToggleFromButton() {
|
||||
webconfig_button_toggle_requested = true;
|
||||
}
|
||||
|
||||
@@ -131,6 +131,8 @@ public:
|
||||
uint8_t* power_save = NULL);
|
||||
static bool saveStandaloneWiFi(const char* ssid, const char* password,
|
||||
uint8_t power_save);
|
||||
static bool formatWiFiSSID(char* reply, size_t reply_len);
|
||||
static bool formatWiFiStatus(char* reply, size_t reply_len);
|
||||
|
||||
// UI tasks use an otherwise-unused multi-click gesture without reaching into
|
||||
// MyMesh directly. The mesh loop consumes the request and performs the NVS /
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "WiFiOtaSeeder.h"
|
||||
|
||||
#if defined(ESP32_PLATFORM) && defined(ENABLE_OTA) && \
|
||||
(defined(WIFI_OTA_SEEDER) || defined(WIFI_SSID))
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <helpers/WiFiOtaSeederPolicy.h>
|
||||
#include <helpers/ota/FolderMotaStore.h>
|
||||
#include <helpers/ota/MotaSourceSerial.h>
|
||||
#include <helpers/ota/OtaContext.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
namespace {
|
||||
|
||||
WiFiServer seeder_server(OTA_SEEDER_TCP_PORT);
|
||||
WiFiClient seeder_client;
|
||||
SerialMotaSource seeder_source(seeder_client, 3000);
|
||||
FolderMotaStore folder_store(seeder_client, 3000);
|
||||
bool listener_active = false;
|
||||
bool tcp_folder_attached = false;
|
||||
|
||||
bool networkReady() {
|
||||
if (WiFi.status() == WL_CONNECTED) return true;
|
||||
const wifi_mode_t mode = WiFi.getMode();
|
||||
if (mode != WIFI_AP && mode != WIFI_AP_STA) return false;
|
||||
return static_cast<uint32_t>(WiFi.softAPIP()) != 0;
|
||||
}
|
||||
|
||||
void detachTcpFolder() {
|
||||
if (!tcp_folder_attached) return;
|
||||
OtaContext& context = ota_ctx();
|
||||
if (context.folderLink() == OtaContext::FOLDER_LINK_TCP) {
|
||||
context.detach_folder();
|
||||
context.clear_folder_dest();
|
||||
context.manager.announce();
|
||||
}
|
||||
tcp_folder_attached = false;
|
||||
}
|
||||
|
||||
void stopClient() {
|
||||
detachTcpFolder();
|
||||
if (seeder_client) seeder_client.stop();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WiFiOtaSeeder::loop() {
|
||||
using WiFiOtaSeederPolicy::ListenerAction;
|
||||
switch (WiFiOtaSeederPolicy::listenerAction(networkReady(), listener_active)) {
|
||||
case ListenerAction::Start:
|
||||
seeder_server.begin();
|
||||
listener_active = true;
|
||||
Serial.printf("OTA seeder listening on :%u (motatool serve --tcp)\n",
|
||||
static_cast<unsigned>(OTA_SEEDER_TCP_PORT));
|
||||
break;
|
||||
case ListenerAction::Stop:
|
||||
stop();
|
||||
return;
|
||||
case ListenerAction::Keep:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!listener_active) return;
|
||||
|
||||
OtaContext& context = ota_ctx();
|
||||
if (WiFiOtaSeederPolicy::tcpFolderWasDetached(tcp_folder_attached,
|
||||
context.folder_active)) {
|
||||
if (seeder_client) seeder_client.stop();
|
||||
context.clear_folder_dest();
|
||||
tcp_folder_attached = false;
|
||||
}
|
||||
if (seeder_client && seeder_client.connected()) return;
|
||||
|
||||
stopClient();
|
||||
WiFiClient incoming = seeder_server.available();
|
||||
if (!incoming) return;
|
||||
|
||||
if (!WiFiOtaSeederPolicy::canAttachTcpFolder(context.folder_active,
|
||||
tcp_folder_attached)) {
|
||||
incoming.stop();
|
||||
Serial.println("OTA seeder rejected TCP client: another folder link is active");
|
||||
return;
|
||||
}
|
||||
|
||||
seeder_client = incoming;
|
||||
char attach_reply[120];
|
||||
if (!context.attach_folder_source(&seeder_source, OtaContext::FOLDER_LINK_TCP,
|
||||
"tcp", attach_reply, sizeof(attach_reply))) {
|
||||
seeder_client.stop();
|
||||
Serial.printf("OTA seeder rejected TCP client: %s\n", attach_reply);
|
||||
return;
|
||||
}
|
||||
|
||||
tcp_folder_attached = true;
|
||||
char link_info[24];
|
||||
snprintf(link_info, sizeof(link_info), "tcp %s",
|
||||
seeder_client.remoteIP().toString().c_str());
|
||||
context.set_folder_dest(&folder_store, link_info);
|
||||
if (context.manager.fetchState() == OtaManager::PAUSED) {
|
||||
context.manager.resumeStaged(nullptr);
|
||||
}
|
||||
context.manager.announce();
|
||||
Serial.printf("OTA seeder client connected (%s)\n", link_info);
|
||||
}
|
||||
|
||||
void WiFiOtaSeeder::stop() {
|
||||
stopClient();
|
||||
if (listener_active) seeder_server.end();
|
||||
listener_active = false;
|
||||
}
|
||||
|
||||
bool WiFiOtaSeeder::isListening() {
|
||||
return listener_active;
|
||||
}
|
||||
|
||||
bool WiFiOtaSeeder::isAttached() {
|
||||
return tcp_folder_attached;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(ESP32_PLATFORM) && defined(ENABLE_OTA) && \
|
||||
(defined(WIFI_OTA_SEEDER) || defined(WIFI_SSID))
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef OTA_SEEDER_TCP_PORT
|
||||
#define OTA_SEEDER_TCP_PORT 5001
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
class WiFiOtaSeeder {
|
||||
public:
|
||||
static void loop();
|
||||
static void stop();
|
||||
static bool isListening();
|
||||
static bool isAttached();
|
||||
static uint16_t port() { return OTA_SEEDER_TCP_PORT; }
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -158,6 +158,38 @@ struct OtaContext {
|
||||
uint32_t session_started_ms = 0; // when the fetch session last left IDLE (for the age display)
|
||||
uint8_t prev_fstate = OtaManager::IDLE;
|
||||
bool folder_active = false; // an external `.mota` folder is attached + being relayed
|
||||
enum FolderLink : uint8_t {
|
||||
FOLDER_LINK_NONE = 0,
|
||||
FOLDER_LINK_SERIAL,
|
||||
FOLDER_LINK_TCP,
|
||||
};
|
||||
FolderLink folderLink() const { return _folder_link; }
|
||||
|
||||
bool attach_folder_source(MotaSource* source, FolderLink link, const char* label,
|
||||
char* msg, size_t cap) {
|
||||
if (!msg || cap == 0) return false;
|
||||
if (!source || link == FOLDER_LINK_NONE) {
|
||||
strncpy(msg, "ERR invalid folder source", cap);
|
||||
msg[cap - 1] = 0;
|
||||
return false;
|
||||
}
|
||||
if (folder_active && _folder_link != link) {
|
||||
snprintf(msg, cap, "ERR folder already attached via %s",
|
||||
_folder_link == FOLDER_LINK_TCP ? "tcp" : "serial");
|
||||
return false;
|
||||
}
|
||||
manager.clear_sources();
|
||||
if (!manager.add_source(source)) {
|
||||
strncpy(msg, "ERR no free source slot", cap);
|
||||
msg[cap - 1] = 0;
|
||||
return false;
|
||||
}
|
||||
folder_active = true;
|
||||
_folder_link = link;
|
||||
snprintf(msg, cap, "OK folder attached (%s) - serving %u mOTA total (own fw + folder)",
|
||||
label ? label : "external", (unsigned)manager.servedCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attach/detach an external folder of `.mota` served by a host daemon over the seeder UART (the node
|
||||
// then advertises + relays them alongside its own fw). Only built when OTA_FOLDER_SERIAL is configured.
|
||||
@@ -167,15 +199,14 @@ struct OtaContext {
|
||||
#ifdef OTA_FOLDER_SERIAL_BEGIN
|
||||
OTA_FOLDER_SERIAL_STREAM.begin(OTA_FOLDER_SERIAL_BAUD); // dedicated UART; console is already up
|
||||
#endif
|
||||
manager.clear_sources(); // idempotent re-attach
|
||||
if (!manager.add_source(&src)) { strncpy(msg, "ERR no free source slot", cap); return false; }
|
||||
folder_active = true;
|
||||
snprintf(msg, cap, "OK folder attached (serial) - serving %u mOTA total (own fw + folder)",
|
||||
(unsigned)manager.servedCount());
|
||||
return true;
|
||||
return attach_folder_source(&src, FOLDER_LINK_SERIAL, "serial", msg, cap);
|
||||
}
|
||||
#endif
|
||||
void detach_folder() { manager.clear_sources(); folder_active = false; }
|
||||
void detach_folder() {
|
||||
manager.clear_sources();
|
||||
folder_active = false;
|
||||
_folder_link = FOLDER_LINK_NONE;
|
||||
}
|
||||
|
||||
void track_session(uint8_t fstate, uint32_t now) { // stamp the session start (age display)
|
||||
if (fstate != prev_fstate) {
|
||||
@@ -206,6 +237,9 @@ struct OtaContext {
|
||||
#endif
|
||||
manager.set_fetch_store(&fetch_store);
|
||||
}
|
||||
|
||||
private:
|
||||
FolderLink _folder_link = FOLDER_LINK_NONE;
|
||||
};
|
||||
|
||||
OtaContext& ota_ctx(); // process-wide singleton
|
||||
|
||||
Reference in new issue
Block a user