#pragma once #include #include #include #include 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 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