Reject radio changes when receive mode cannot resume and restore the previous physical profile and power policy. Preserve selected folder OTA captures across disconnects and recoverable host I/O failures, while keeping corruption failures distinct. Validate OTA staging input before arithmetic or buffer writes and revoke published data before edits. Commit companion channels through verified file replacement on ESP32, RP2040, and STM32, and quarantine incomplete startup reads. Recheck the last management connection during delayed wireless changes and prevent conflicting Bluetooth shutdowns. Detect complete OTA implementation sources rather than the shared inflater when configuring release builds. Add fault-injection regressions and CI coverage for these paths, and document folder capture reconnect behavior. Validation: 1,510 native tests; 55 host integration checks; build profile regressions; ESP32, nRF52, STM32, and RP2040 firmware builds.
179 lines
7.6 KiB
C++
179 lines
7.6 KiB
C++
#pragma once
|
|
|
|
#include <atomic>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
namespace mesh {
|
|
namespace wireless {
|
|
|
|
enum Service : uint8_t { WiFi = 1, Bluetooth = 2, EspNow = 4, All = 7, Independent = 8 };
|
|
enum class Result { Done, Pending, Failed };
|
|
enum class Action { None, Get, On, OnAll, Off, ForceOff, Invalid };
|
|
struct Command { uint8_t scope; Action action; };
|
|
|
|
inline const char* skipSpace(const char* s) {
|
|
while (*s == ' ' || *s == '\t') ++s;
|
|
return s;
|
|
}
|
|
inline bool word(const char* s, const char* expected) {
|
|
const size_t n = strlen(expected);
|
|
return strncmp(s, expected, n) == 0 && *skipSpace(s + n) == 0;
|
|
}
|
|
inline Command parse(const char* s) {
|
|
if (!s) return {0, Action::None};
|
|
s = skipSpace(s);
|
|
const bool get = strncmp(s, "get ", 4) == 0;
|
|
if (!get && strncmp(s, "set ", 4) != 0) return {0, Action::None};
|
|
s = skipSpace(s + 4);
|
|
const char* key = strncmp(s, "2.4ghz", 6) == 0 ? "2.4ghz" : strncmp(s, "espnow", 6) == 0 ? "espnow" : "wifi";
|
|
const size_t n = strlen(key);
|
|
if (strncmp(s, key, n) || (s[n] && s[n] != ' ' && s[n] != '\t')) return {0, Action::None};
|
|
const uint8_t scope = key[0] == '2' ? All : key[0] == 'e' ? EspNow : WiFi;
|
|
s = skipSpace(s + n);
|
|
if (get) return {scope, *s ? Action::Invalid : Action::Get};
|
|
if (word(s, "on")) return {scope, Action::On};
|
|
if (word(s, "off")) return {scope, Action::Off};
|
|
if (!strncmp(s, "off", 3) && (s[3] == ' ' || s[3] == '\t')
|
|
&& word(skipSpace(s + 3), "force")) return {scope, Action::ForceOff};
|
|
if (scope == All && !strncmp(s, "on", 2) && (s[2] == ' ' || s[2] == '\t')) {
|
|
s = skipSpace(s + 2);
|
|
if (word(s, "all") || word(s, "force")) return {scope, Action::OnAll};
|
|
}
|
|
return {scope, Action::Invalid};
|
|
}
|
|
|
|
class Backend {
|
|
public:
|
|
virtual uint8_t available() const = 0; // Services usable in this boot.
|
|
virtual uint8_t enabled() const = 0;
|
|
virtual uint8_t clients() const = 0; // Open management sessions, not association/power.
|
|
virtual bool replyBusy() const { return false; }
|
|
virtual Result set(uint8_t service, bool on) = 0;
|
|
};
|
|
|
|
class Control {
|
|
Backend* backend_ = nullptr;
|
|
std::atomic<uint8_t> blocked_{0};
|
|
uint8_t scope_ = 0, target_ = 0, saved_ = 0;
|
|
bool pending_ = false, started_ = false, force_ = false, independent_requester_ = false;
|
|
bool saved_valid_ = false, master_off_ = false;
|
|
uint32_t due_ = 0;
|
|
const char* error_ = nullptr;
|
|
|
|
public:
|
|
void begin(Backend& backend) { backend_ = &backend; }
|
|
bool blocked(uint8_t service) const { return (blocked_.load() & service) != 0; }
|
|
bool pending() const { return pending_; }
|
|
bool masterOff() const { return master_off_; }
|
|
bool allowService(uint8_t service) {
|
|
if (master_off_ || pending_) return false;
|
|
blocked_.store(blocked_.load() & ~service);
|
|
return true;
|
|
}
|
|
uint8_t enabled() const { return backend_ ? backend_->enabled() : 0; }
|
|
|
|
bool handle(const char* text, char* reply, size_t size, uint32_t now, uint8_t requester) {
|
|
const auto command = parse(text);
|
|
if (command.action == Action::None || !reply || !size) return false;
|
|
const uint8_t available = backend_ ? backend_->available() : 0;
|
|
const char* name = command.scope == All ? "2.4ghz" : command.scope == EspNow ? "espnow" : "wifi";
|
|
if (command.action == Action::Invalid) {
|
|
snprintf(reply, size, "Error: use set wifi/espnow on|off [force]; set 2.4ghz on [all|force]|off [force]");
|
|
return true;
|
|
}
|
|
const uint8_t current = enabled();
|
|
if (command.action == Action::Get) {
|
|
const auto state = [=](uint8_t bit) { return !(available & bit) ? "unavailable" : current & bit ? "on" : "off"; };
|
|
if (command.scope != All) snprintf(reply, size, "%s %s%s%s", name, state(command.scope), pending_ ? " (pending)" : "", error_ ? error_ : "");
|
|
else snprintf(reply, size, "wifi=%s, bluetooth=%s, espnow=%s%s%s", state(WiFi), state(Bluetooth), state(EspNow), pending_ ? " (pending)" : "", error_ ? error_ : "");
|
|
return true;
|
|
}
|
|
if (!(available & command.scope)) {
|
|
snprintf(reply, size, "Error: requested wireless service unavailable in this build/boot");
|
|
return true;
|
|
}
|
|
const bool on = command.action == Action::On || command.action == Action::OnAll;
|
|
if (pending_ && command.scope != All && (command.scope & available) != scope_) {
|
|
snprintf(reply, size, "Error: wireless change pending; get 2.4ghz for status");
|
|
return true;
|
|
}
|
|
if (command.scope != All && on && master_off_) {
|
|
snprintf(reply, size, "Error: 2.4ghz is off; use set 2.4ghz on first");
|
|
return true;
|
|
}
|
|
const uint8_t scope = command.scope & available;
|
|
const uint8_t target = !on ? 0 : command.scope == All && command.action != Action::OnAll
|
|
? (saved_valid_ ? saved_ : current) & available : scope;
|
|
// Restoring a saved subset may also switch off services enabled since a
|
|
// failed restore. Protect the actual removals, not just commands named off.
|
|
const uint8_t disabling = scope & ~target;
|
|
const bool retained = (requester & ~disabling) != 0;
|
|
const bool force = command.action == Action::ForceOff;
|
|
if ((current & disabling) && !force && !retained && !(backend_->clients() & ~disabling)) {
|
|
snprintf(reply, size, "Error: no remaining management connection; use %s or USB/Ethernet",
|
|
on ? "on all" : "off force");
|
|
return true;
|
|
}
|
|
if (command.scope == All && !on && !saved_valid_) {
|
|
saved_ = current;
|
|
saved_valid_ = true;
|
|
}
|
|
scope_ = scope;
|
|
target_ = target;
|
|
force_ = force;
|
|
// An actual USB/UART command proves a host session even on adapters with
|
|
// no DTR reporting. Wireless requesters can disconnect during reply drain;
|
|
// only their live management sessions may authorize the delayed shutdown.
|
|
independent_requester_ = (requester & Independent) != 0;
|
|
pending_ = true;
|
|
started_ = false;
|
|
error_ = nullptr;
|
|
due_ = now + 250;
|
|
// Keep the group latch independent of the set of services available on a board.
|
|
if (command.scope == All) master_off_ = !on;
|
|
snprintf(reply, size, "OK - %s %s requested (this boot); get %s for status",
|
|
name, on ? "on" : "off", name);
|
|
return true;
|
|
}
|
|
|
|
void service(uint32_t now) {
|
|
if (!pending_ || !backend_ || int32_t(now - due_) < 0) return;
|
|
if (!started_) {
|
|
const uint8_t disabling = scope_ & ~target_;
|
|
if ((enabled() & disabling) && !force_ && !independent_requester_ && !(backend_->clients() & ~disabling)) {
|
|
pending_ = false;
|
|
if (master_off_) { master_off_ = false; saved_valid_ = false; }
|
|
error_ = " (cancelled: connection lost)";
|
|
return;
|
|
}
|
|
if (backend_->replyBusy() && uint32_t(now - due_) < 1750) return;
|
|
blocked_.store((blocked_.load() & ~scope_) | (scope_ & ~target_));
|
|
started_ = true;
|
|
}
|
|
// Infrastructure services own tasks and sockets: stop them before ESP-NOW's
|
|
// WiFi driver. Restore ESP-NOW first so infrastructure WiFi can coexist.
|
|
const uint8_t order[] = {WiFi, EspNow, Bluetooth, EspNow, WiFi, Bluetooth};
|
|
for (unsigned i = 0; i < sizeof(order); ++i) {
|
|
const uint8_t bit = order[i];
|
|
const bool on = (target_ & bit) != 0;
|
|
if (!(scope_ & bit) || on != (i >= 3)) continue;
|
|
const auto result = backend_->set(bit, on);
|
|
if (result == Result::Pending && uint32_t(now - due_) < 30000) return;
|
|
if (result != Result::Done) {
|
|
pending_ = false;
|
|
error_ = " (failed: retry command; check startup/teardown)";
|
|
return;
|
|
}
|
|
}
|
|
pending_ = false;
|
|
if (!master_off_) saved_valid_ = false;
|
|
}
|
|
};
|
|
|
|
inline Control& control() { static Control instance; return instance; }
|
|
|
|
} // namespace wireless
|
|
} // namespace mesh
|