mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 20:01:17 +00:00
feat(webconfig): let the portal discover board-specific CLI commands
The FEM commands moved out of CommonCLI into Board::handleCommand(), and the T-Beam 1W fan control was added there too, but the portal never caught up: `radio.fem.txgain` and the fan commands were missing from the terminal table entirely, while `radio.fem.rxgain` was offered on every board even though a board with no hook now answers "??:" rather than "unsupported". Whether a node answers these is a property of the board, not of the build, so the page cannot know from the firmware version. Ask the board instead: probeBoardCommands() runs each candidate getter once on the loop task at startup and keeps the ones that answer, which needs no per-variant list because Board::handleCommand() already reports whether it handled a command. /api/status names the survivors and the page hides everything else, so adding a command to a variant means one entry in WC_BOARD_CMDS rather than an edit per board. The two FEM keys also become Radio-panel toggles, gated the same way; their `set` reaches the board hook through the existing config batch, so only the allowlist and /api/config needed to grow. webconfig_cli_audit.py now scans variants/*/*Board.cpp for the boards that actually build the portal, checks set-only keys in the reverse direction, and verifies every gate is a command some board answers; the mock gained --board-cmds so both shapes of board are testable. `stop ota` joins NOT_OFFERED: `start ota` cannot run from the portal, so it has nothing to stop.
This commit is contained in:
1 parent
4dca3688ea
commit
3d2d0b5da6
8 files changed
+319
-41
No files matched your search
@@ -19,7 +19,8 @@
|
||||
static const char* const WC_ALLOWED_SET_KEYS[] = {
|
||||
// NodePrefs (radio / node)
|
||||
"name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
||||
"cad", "radio.rxgain", "repeat", "advert.interval", "flood.advert.interval",
|
||||
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain",
|
||||
"repeat", "advert.interval", "flood.advert.interval",
|
||||
"flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect",
|
||||
// MQTTPrefs (WiFi / MQTT / misc observer)
|
||||
"wifi.ssid", "wifi.pwd", "wifi.powersave",
|
||||
|
||||
@@ -93,6 +93,21 @@ static inline bool wcIsDeferredReboot(const char* cmd) {
|
||||
return strncmp(cmd, "reboot", 6) == 0;
|
||||
}
|
||||
|
||||
// Commands answered by Board::handleCommand(), which CommonCLI dispatches ahead
|
||||
// of its own table. Whether a node answers them is a property of the BOARD, not
|
||||
// of the build, so the page cannot know from the firmware version alone. Rather
|
||||
// than mirror a per-board list here — which would have to be edited every time a
|
||||
// variant grows a command — ask the board itself: probeBoardCommands() runs each
|
||||
// getter once and keeps the ones that answer.
|
||||
static const char* const WC_BOARD_CMDS[] = {
|
||||
"radio.fem.rxgain", // front-end module LNA (Heltec V4/V4 R8/Tracker V2, Station G3)
|
||||
"radio.fem.txgain", // front-end module PA level (Station G3)
|
||||
"fan", // thermal fan, and with it fan.lo / fan.hi (T-Beam 1W)
|
||||
};
|
||||
static const size_t WC_BOARD_CMD_COUNT = sizeof(WC_BOARD_CMDS) / sizeof(WC_BOARD_CMDS[0]);
|
||||
static_assert(sizeof(WC_BOARD_CMDS) / sizeof(WC_BOARD_CMDS[0]) <= 8,
|
||||
"_board_cmds is a uint8_t bitmask");
|
||||
|
||||
// Commands the CLI reaches but the portal cannot honestly serve. Rejected at
|
||||
// POST so nothing in the sequence runs, rather than failing halfway with a
|
||||
// reply that does not explain itself. Returns the reason, or NULL if fine.
|
||||
@@ -455,6 +470,26 @@ void WebConfigServer::finalizeTeardown() {
|
||||
if (_cb) _cb->onWebConfigStopped();
|
||||
}
|
||||
|
||||
// Ask the board which of WC_BOARD_CMDS it answers, once per start. The getters
|
||||
// are pure reads, and the reply says everything needed: CommonCLI answers a real
|
||||
// getter "> value", while every no-answer path — the "??:" fallthrough a board
|
||||
// with no hook reaches, and the "Error: unsupported" a board with the hook but
|
||||
// not the hardware returns — starts with something else.
|
||||
//
|
||||
// Loop task only: execCommand() reaches the CLI, which the async task must not.
|
||||
void WebConfigServer::probeBoardCommands() {
|
||||
char cmd[48], reply[160];
|
||||
uint8_t mask = 0;
|
||||
for (size_t i = 0; i < WC_BOARD_CMD_COUNT; i++) {
|
||||
snprintf(cmd, sizeof(cmd), "get %s", WC_BOARD_CMDS[i]);
|
||||
reply[0] = 0;
|
||||
_cb->execCommand(cmd, reply);
|
||||
if (reply[0] == '>') mask |= (uint8_t)(1 << i);
|
||||
}
|
||||
_board_cmds = mask;
|
||||
_board_cmds_probed = true;
|
||||
}
|
||||
|
||||
void WebConfigServer::tick(uint32_t now) {
|
||||
if (_stopping) {
|
||||
uint32_t refs = handlerRefCount();
|
||||
@@ -474,6 +509,8 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
}
|
||||
if (_mode == MODE_OFF) return;
|
||||
|
||||
if (!_board_cmds_probed) probeBoardCommands();
|
||||
|
||||
if (_mode == MODE_LAN && _initial_setup && _setup_reminder_at != 0 &&
|
||||
(int32_t)(now - _setup_reminder_at) >= 0) {
|
||||
Serial.printf("WC: Ethernet setup http://%s/ code %s\n",
|
||||
@@ -743,7 +780,7 @@ void WebConfigServer::handleStatus(AsyncWebServerRequest* req) {
|
||||
if (_mode == MODE_OFF) { req->send(503); return; }
|
||||
bool authed = checkAuth(req);
|
||||
|
||||
DynamicJsonDocument doc(512);
|
||||
DynamicJsonDocument doc(640);
|
||||
doc["mode"] = (_mode == MODE_SETUP) ? "setup" : "lan";
|
||||
doc["auth"] = authed;
|
||||
doc["needs_setup"] = !mqttNetworkSetupComplete(_obs);
|
||||
@@ -757,6 +794,19 @@ void WebConfigServer::handleStatus(AsyncWebServerRequest* req) {
|
||||
doc["build_date"] = _build_date;
|
||||
doc["role"] = _role;
|
||||
doc["board"] = _board_name;
|
||||
// Board-specific CLI commands this board answers; the page hides the controls
|
||||
// for everything absent here rather than offering one that cannot work.
|
||||
char board_cmds[80];
|
||||
size_t n = 0;
|
||||
board_cmds[0] = 0; // the nothing-supported case
|
||||
for (size_t i = 0; i < WC_BOARD_CMD_COUNT && n < sizeof(board_cmds) - 1; i++) {
|
||||
if (!(_board_cmds & (1 << i))) continue;
|
||||
int w = snprintf(&board_cmds[n], sizeof(board_cmds) - n, "%s%s",
|
||||
n ? "," : "", WC_BOARD_CMDS[i]);
|
||||
if (w < 0) break;
|
||||
n += (size_t)w; // snprintf NUL-terminates; a truncating w stops the loop
|
||||
}
|
||||
doc["board_cmds"] = board_cmds;
|
||||
doc["uptime_s"] = millis() / 1000;
|
||||
doc["runtime_slots"] = RUNTIME_MQTT_SLOTS;
|
||||
doc["max_slots"] = MAX_MQTT_SLOTS;
|
||||
@@ -867,6 +917,10 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
|
||||
radio["txdelay"] = _prefs->tx_delay_factor;
|
||||
radio["cad"] = (bool)_prefs->cad_enabled;
|
||||
radio["rxgain"] = (bool)_prefs->rx_boosted_gain;
|
||||
// FEM gain is driven by Board::handleCommand(), not CommonCLI, so these are
|
||||
// the stored intent; a board with no front-end module rejects the `set`.
|
||||
radio["fem_rxgain"] = (bool)_prefs->radio_fem_rxgain;
|
||||
radio["fem_txgain"] = (bool)_prefs->radio_fem_txgain;
|
||||
radio["repeat"] = !(bool)_prefs->disable_fwd; // CLI `repeat on` == disable_fwd 0
|
||||
radio["flood_max"] = _prefs->flood_max;
|
||||
radio["flood_max_advert"] = _prefs->flood_max_advert;
|
||||
|
||||
@@ -200,6 +200,12 @@ private:
|
||||
uint32_t _stats_built_at = 0;
|
||||
char _stats_json[1024] = {0};
|
||||
|
||||
// Which Board::handleCommand() commands this board actually answers, as a
|
||||
// bitmask over WC_BOARD_CMDS. Probed once per start on the loop task and then
|
||||
// only read, so the async status handler needs no lock for it.
|
||||
uint8_t _board_cmds = 0;
|
||||
bool _board_cmds_probed = false;
|
||||
|
||||
void createServer();
|
||||
void registerRoutes();
|
||||
typedef void (WebConfigServer::*RequestHandler)(AsyncWebServerRequest*);
|
||||
@@ -208,6 +214,7 @@ private:
|
||||
void detachRoutes();
|
||||
uint32_t handlerRefCount() const;
|
||||
void drainBatch(uint32_t now);
|
||||
void probeBoardCommands();
|
||||
void finalizeTeardown();
|
||||
bool checkAuth(AsyncWebServerRequest* req);
|
||||
static void collectBody(AsyncWebServerRequest* req, uint8_t* data, size_t len,
|
||||
|
||||
Reference in new issue
Block a user