170 lines
7.0 KiB
C++
170 lines
7.0 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, retained_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 bool retained = (requester & ~scope) != 0;
|
|
const bool force = command.action == Action::ForceOff;
|
|
if (!on && (current & scope) && !force && !retained && !(backend_->clients() & ~scope)) {
|
|
snprintf(reply, size, "Error: no remaining management connection; use off force or USB/Ethernet");
|
|
return true;
|
|
}
|
|
if (command.scope == All && !on && !saved_valid_) {
|
|
saved_ = current;
|
|
saved_valid_ = true;
|
|
}
|
|
scope_ = scope;
|
|
target_ = !on ? 0 : command.scope == All && command.action != Action::OnAll
|
|
? (saved_valid_ ? saved_ : current) & available : scope;
|
|
force_ = force;
|
|
retained_requester_ = retained;
|
|
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_) {
|
|
if (!target_ && (enabled() & scope_) && !force_ && !retained_requester_ && !(backend_->clients() & ~scope_)) {
|
|
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
|