mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-10-23 10:42:30 +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
@@ -101,6 +101,9 @@ def default_config(setup_mode):
|
||||
"radio": {
|
||||
"freq": 910.525, "bw": 62.5, "sf": 7, "cr": 5, "tx": 22, "af": 1.0,
|
||||
"rxdelay": 0.0, "txdelay": 0.5, "cad": False, "rxgain": True,
|
||||
# Stored in NodePrefs like any other radio pref; only the COMMAND
|
||||
# is board-specific, so the value exists even where nothing drives it.
|
||||
"fem_rxgain": True, "fem_txgain": False,
|
||||
"repeat": True, "flood_max": 64, "flood_max_advert": 8,
|
||||
"flood_max_unscoped": 8, "loop_detect": "moderate",
|
||||
"name": "MockNode", "lat": 39.7392, "lon": -104.9903,
|
||||
@@ -149,6 +152,9 @@ class State:
|
||||
self.lock = threading.Lock()
|
||||
self.setup_mode = args.setup
|
||||
self.active_slots = args.active_slots
|
||||
# Board::handleCommand() commands this "board" answers. Empty by default:
|
||||
# the mock is a Heltec V3, which implements no such hook at all.
|
||||
self.board_cmds = [c for c in args.board_cmds.split(",") if c]
|
||||
self.cfg = default_config(args.setup)
|
||||
# latched at AP start, like WebConfigServer::_initial_setup
|
||||
self.initial_setup = args.setup and self.cfg["wifi"]["ssid"] == ""
|
||||
@@ -192,6 +198,7 @@ class State:
|
||||
"role": "Repeater", "board": "Heltec V3 (mock)",
|
||||
"uptime_s": int(time.time() - self.start),
|
||||
"runtime_slots": 6, "max_slots": 6, "active_slots": self.active_slots,
|
||||
"board_cmds": ",".join(self.board_cmds),
|
||||
"max_cmds": CLI_MAX_CMDS,
|
||||
}
|
||||
|
||||
@@ -253,9 +260,6 @@ def apply_set(cfg, key, val):
|
||||
ADMIN_PASSWORD = val
|
||||
return True, "OK"
|
||||
|
||||
if key == "radio.fem.rxgain":
|
||||
return False, "Error: unsupported" # no FEM on the mock board, see GETTERS
|
||||
|
||||
if key == "dutycycle":
|
||||
try:
|
||||
dc = float(val)
|
||||
@@ -340,13 +344,74 @@ def apply_set(cfg, key, val):
|
||||
sec, f = STR_KEYS[key]
|
||||
cfg[sec][f] = val
|
||||
return True, "OK"
|
||||
gate = BOARD_CMD_GATE.get(key)
|
||||
if gate:
|
||||
if gate not in ST.board_cmds:
|
||||
return False, "unknown config: %s" % key # no such command on this board
|
||||
return apply_board_set(cfg, key, val)
|
||||
|
||||
# Strict fallthrough: this function is the single authority on what can be
|
||||
# set, for the batch and the CLI alike. Accepting unknown keys here once hid
|
||||
# the fact that the CLI could not reach `dutycycle` or `radio.fem.rxgain`.
|
||||
# Verbatim shape from CommonCLI::handleSetCmd's fallthrough.
|
||||
#
|
||||
# This is also where the board-specific commands land. `radio.fem.*` and
|
||||
# `fan*` come from Board::handleCommand(), which is dispatched ahead of
|
||||
# CommonCLI; the mock board is a Heltec V3 and implements no such hook, so
|
||||
# they reach this fallthrough exactly as they do on the real thing.
|
||||
return False, "unknown config: %s" % key
|
||||
|
||||
|
||||
# Commands that reach Board::handleCommand() rather than CommonCLI. Which ones a
|
||||
# node answers is a property of the board, so the portal probes for them at
|
||||
# startup and reports the answers in /api/status; --board-cmds picks which ones
|
||||
# this mock claims. `fan.lo` / `fan.hi` have no getter and ride on `fan`.
|
||||
BOARD_CMD_GATE = {
|
||||
"radio.fem.rxgain": "radio.fem.rxgain",
|
||||
"radio.fem.txgain": "radio.fem.txgain",
|
||||
"fan": "fan",
|
||||
"fan.lo": "fan",
|
||||
"fan.hi": "fan",
|
||||
}
|
||||
BOARD_STATE = {"fan": "auto", "fan.lo": 45, "fan.hi": 60}
|
||||
FEM_FIELD = {"radio.fem.rxgain": "fem_rxgain", "radio.fem.txgain": "fem_txgain"}
|
||||
|
||||
|
||||
def apply_board_set(cfg, key, val):
|
||||
if key in FEM_FIELD:
|
||||
if val not in ("on", "off"):
|
||||
return False, "Error: state must be on or off"
|
||||
cfg["radio"][FEM_FIELD[key]] = val == "on"
|
||||
return True, "OK - LoRa FEM %s gain %s" % ("RX" if "rx" in key else "TX", val)
|
||||
if key == "fan":
|
||||
if val not in ("on", "off", "auto"):
|
||||
return False, "Error: fan must be on, off, or auto"
|
||||
BOARD_STATE["fan"] = val
|
||||
return True, "OK - fan %s" % val
|
||||
try:
|
||||
n = int(val)
|
||||
except ValueError:
|
||||
return False, "Error: expected a number"
|
||||
if key == "fan.lo" and not (0 <= n <= 100 and n < BOARD_STATE["fan.hi"]):
|
||||
return False, "Error: fan.lo must be 0..100 and < fan.hi"
|
||||
if key == "fan.hi" and not (BOARD_STATE["fan.lo"] < n <= 120):
|
||||
return False, "Error: fan.hi must be > fan.lo and <= 120"
|
||||
BOARD_STATE[key] = n
|
||||
return True, "OK - %s %d" % (key, n)
|
||||
|
||||
|
||||
def board_get(cfg, key):
|
||||
"""Getter reply, or None when this board does not answer the command."""
|
||||
if BOARD_CMD_GATE.get(key) not in ST.board_cmds:
|
||||
return None
|
||||
if key == "fan":
|
||||
return "%s 41.0C fan=%s cd=0s" % (
|
||||
BOARD_STATE["fan"], "on" if BOARD_STATE["fan"] == "on" else "off")
|
||||
if key in FEM_FIELD:
|
||||
return "on" if cfg["radio"][FEM_FIELD[key]] else "off"
|
||||
return None # fan.lo / fan.hi are set-only on the board too
|
||||
|
||||
|
||||
# Payload-type names accepted alongside the decimal form. Mirrors
|
||||
# namedPacketTypes() in src/helpers/MQTTPacketFilter.h; 12-14 are reserved
|
||||
# upstream and stay reachable by number only.
|
||||
@@ -516,10 +581,6 @@ GETTERS = {
|
||||
"mqtt.ntp.diag": lambda c: "last sync: 42s ago via %s (offset +0.011s)" % (c["mqtt"]["ntp"] or "none"),
|
||||
"mqtt.stats": lambda c: ("published: %d\ndropped: 0\nqueue: 0/24\nreconnects: 1"
|
||||
% (100 + int(time.time() - ST.start))),
|
||||
# Runtime-gated on the real device (Board::canControlLoRaFemLna), not
|
||||
# compiled out — the command exists everywhere and the board answers for
|
||||
# itself. The mock board is a Heltec V3, which has no FEM.
|
||||
"radio.fem.rxgain": lambda c: None,
|
||||
}
|
||||
|
||||
|
||||
@@ -547,6 +608,11 @@ def cli_get(cfg, key):
|
||||
|
||||
|
||||
def _cli_get_value(cfg, key):
|
||||
if key in BOARD_CMD_GATE:
|
||||
val = board_get(cfg, key)
|
||||
# Not answered: the board has no hook for it, so CommonCLI's own getter
|
||||
# fallthrough is what replies — exactly as on the real thing.
|
||||
return (True, val) if val is not None else (False, "??: %s" % key)
|
||||
if key in GETTERS:
|
||||
val = GETTERS[key](cfg)
|
||||
return (True, val) if val is not None else (False, "Error: unsupported")
|
||||
@@ -1053,6 +1119,9 @@ def main():
|
||||
ap.add_argument("--port", type=int, default=8080)
|
||||
ap.add_argument("--setup", action="store_true", help="first-boot setup wizard mode")
|
||||
ap.add_argument("--active-slots", type=int, default=5, help="server slots to expose (2 or 5)")
|
||||
ap.add_argument("--board-cmds", default="",
|
||||
help="comma list of Board::handleCommand() commands to answer, e.g. "
|
||||
"radio.fem.rxgain,radio.fem.txgain,fan (default: none, like a Heltec V3)")
|
||||
ap.add_argument("--fw-version", default=FW_VERSION,
|
||||
help="version string to report, shaped like build.sh's embedded one")
|
||||
ap.add_argument("--minify", action="store_true",
|
||||
|
||||
Reference in new issue
Block a user