Files
MeshCore-Solo/examples/companion_radio/ui-new/AdminScreen.h
Jakub 9bfccf4cf4 feat(admin): local-device mode + reuse Nodes screen for target picking
Tools > Admin now opens on a This-device / Remote-node chooser. "This
device" is a login-free 2-tab carousel (System: Name/Radio/TX power/
Lat/Lon, Actions: Send advert/Reboot) mapped onto NodePrefs/sensors and
reusing Settings' own apply chains -- no CommonCLI port. "Remote node..."
opens Tools > Nodes in a pick-mode (borrow-another-screen's-list idiom),
and Nodes gains an "Admin" Hold-Enter action; both converge on the single
canonical UITask::openAdminFor() -> AdminScreen::startFor().

Also:
- KeyboardWidget: opt-in PlaceholderRefreshFn hook for contextual {}-key
  CLI command-name completion (word-replacing); default behaviour and all
  other keyboard uses unchanged.
- TabBar: ellipsize() re-measures the real "text..." candidate per step
  (+ width clamp) so dots can't spill into a neighbour/reserved icon.
- ChannelsView: form rows truncate instead of wrapping.
- Reboot moved off the default-selected Actions row (fires with no confirm).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:32:04 +02:00

648 lines
27 KiB
C++

#pragma once
// Tools > Admin: administer a remote repeater/room you have admin rights on
// (over the mesh -- the on-device equivalent of the app's "repeater admin",
// see docs/cli_commands.md) or this device itself. Also reachable from Tools >
// Nodes' Hold-Enter menu (NearbyScreen::ACT_ADMIN).
//
// CHOOSER -> "This device" (LOCAL mode) or "Remote node..." (opens Tools > Nodes
// in pick-mode -- NearbyScreen::startPickAdminTarget(), the same borrow-another-
// screen's-list idiom the channel/bot pickers use, so it looks exactly like
// normal Nodes browsing). Both the picker and the Nodes context-menu converge
// on the single canonical UITask::openAdminFor(ContactInfo) -> startFor().
//
// REMOTE mode: login (session password, self-heals like room logins) unlocks a
// category tab carousel of AdminField{label, get_cmd, set_prefix} rows:
// set_prefix==null -> action: send get_cmd literally (e.g. "reboot")
// get_cmd==null -> set-only: blank keyboard, send "<set_prefix> <value>"
// both set -> get, pre-fill keyboard with the reply, then set
// The trailing "Custom command..." row (both null) is a free-text escape hatch
// with {}-key CLI command-name completion (CLI_COMMANDS).
//
// LOCAL mode: no login, no network -- a 2-tab carousel of LocalField{label, kind}
// rows mapped straight onto NodePrefs/sensors, reusing Settings' own apply chains
// (UITask::applyRadioParams()/applyTxPower(), MyMesh::savePrefs()) rather than a
// CLI-grammar port (this firmware has no CommonCLI -- see CODE_REVIEW.md). Fields
// pre-fill with the current value read synchronously and write+apply on submit.
#include "FullscreenMsgView.h"
#include "TabBar.h"
#include <helpers/ClientACL.h> // PERM_ACL_ADMIN / PERM_ACL_ROLE_MASK
class AdminScreen : public UIScreen {
UITask* _task;
enum Phase : uint8_t { CHOOSER, LOGIN, COMMAND, REPLY };
Phase _phase = CHOOSER;
// CHOOSER -- "This device" (row 0) / "Remote node..." (row 1). Reuses _sel.
int _sel = 0;
bool _local_mode = false; // COMMAND is administering this device, not _target
ContactInfo _target; // valid only when !_local_mode
// LOGIN (remote only)
char _login_pw[16] = "";
// True while waiting for the result of a login attempt with no keyboard on
// screen -- either a silent retry with a remembered password, or right after
// the keyboard-typed password was submitted. Distinct from the keyboard
// itself being open (kb().render()/handleInput() only run while this is false).
bool _login_waiting = false;
// One-slot memo: the last contact this screen successfully admin-logged
// into this visit, so re-entering COMMAND for the same target right after
// doesn't require retyping the password. Cleared in onShow() -- see the
// session-only design note in the file header.
uint8_t _admin_ok_prefix[4] = { 0, 0, 0, 0 };
bool _admin_ok = false;
// ── COMMAND: remote (CLI-grammar) tabs/rows ─────────────────────────────────
enum AdminTab : uint8_t { ATAB_SYSTEM, ATAB_RADIO, ATAB_ROUTING, ATAB_ACTIONS, ATAB_COUNT };
static const char* TAB_LABELS[ATAB_COUNT];
struct AdminField { const char* label; const char* get_cmd; const char* set_prefix; };
static const AdminField SYSTEM_FIELDS[];
static const AdminField RADIO_FIELDS[];
static const AdminField ROUTING_FIELDS[];
static const AdminField ACTION_FIELDS[];
static const int ROWS_PER_TAB[ATAB_COUNT];
static const AdminField& fieldAt(int tab, int row) {
switch (tab) {
case ATAB_SYSTEM: return SYSTEM_FIELDS[row];
case ATAB_RADIO: return RADIO_FIELDS[row];
case ATAB_ROUTING: return ROUTING_FIELDS[row];
default: return ACTION_FIELDS[row];
}
}
// ── COMMAND: local (this-device) tabs/rows ──────────────────────────────────
static const int LOCAL_TAB_COUNT = 2;
static const char* LOCAL_TAB_LABELS[LOCAL_TAB_COUNT];
enum LocalKind : uint8_t { LK_NONE, LK_NAME, LK_RADIO, LK_TXPOWER, LK_LAT, LK_LON, LK_REBOOT, LK_ADVERT };
struct LocalField { const char* label; LocalKind kind; };
static const LocalField LOCAL_SYSTEM_FIELDS[];
static const LocalField LOCAL_ACTION_FIELDS[];
static const int LOCAL_ROWS_PER_TAB[LOCAL_TAB_COUNT];
static const LocalField& localFieldAt(int tab, int row) {
return (tab == 0) ? LOCAL_SYSTEM_FIELDS[row] : LOCAL_ACTION_FIELDS[row];
}
int tabCount() const { return _local_mode ? LOCAL_TAB_COUNT : (int)ATAB_COUNT; }
int rowsInTab(int tab) const { return _local_mode ? LOCAL_ROWS_PER_TAB[tab] : ROWS_PER_TAB[tab]; }
const char* rowLabel(int tab, int row) const {
return _local_mode ? localFieldAt(tab, row).label : fieldAt(tab, row).label;
}
uint8_t _tab = 0;
int _row_sel = 0, _row_scroll = 0;
bool _kb_active = false;
char _cmd_text[161] = ""; // remote: the command being built/sent
bool _waiting = false; // remote: awaiting sendAdminCommand()'s reply
uint32_t _cmd_deadline_ms = 0;
bool _fetch_for_edit = false; // remote: true while waiting on a "get" whose reply opens an editor
const char* _pending_set_prefix = nullptr; // remote: non-null => edited keyboard text gets this prefix on submit
LocalKind _pending_local_kind = LK_NONE; // local: which field the open keyboard is editing
// REPLY (remote only)
FullscreenMsgView _reply_view;
char _reply_text[200] = "";
KeyboardWidget& kb() { return _task->keyboard(); }
bool isAdminOk(const uint8_t* pub_key) const {
return _admin_ok && memcmp(_admin_ok_prefix, pub_key, 4) == 0;
}
void startLogin() {
_login_pw[0] = '\0';
_login_waiting = false;
kb().begin("", 15); // admin password: same max length as room/repeater login
kb().clearPlaceholders(); // {loc}/{time} are for messages, not a password
_phase = LOGIN;
}
// Silent retry with a remembered password (see startFor()) -- same idea as
// MessagesScreen's room-login flow: no keyboard shown, just a "Logging
// in..." wait for the async result.
void startLoginWithSaved(const char* password) {
strncpy(_login_pw, password, sizeof(_login_pw) - 1);
_login_pw[sizeof(_login_pw) - 1] = '\0';
bool sent = the_mesh.sendRoomLogin(_target, _login_pw);
_task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500);
if (sent) { _login_waiting = true; _phase = LOGIN; }
}
void sendCommand() {
uint32_t est_timeout = 0;
if (!the_mesh.sendAdminCommand(_target, _cmd_text, est_timeout)) {
_task->showAlert("Send failed", 1200);
_fetch_for_edit = false;
_pending_set_prefix = nullptr;
return;
}
_waiting = true;
// Generous margin over the base estimate, same reasoning as the DM ACK
// deadline in MessagesScreen::sendText() -- a slow multi-hop reply isn't
// prematurely shown as a timeout.
_cmd_deadline_ms = millis() + est_timeout + 4000;
_task->showAlert(_fetch_for_edit ? "Fetching..." : "Sent, waiting...", 800);
}
// Open the free-text keyboard, pre-filled with `initial`. Shared by the
// Custom-command row, remote set-only/get-then-edit fields, and every local
// field. `cli_autocomplete` wires up the {} picker to complete CLI command
// names contextually -- only meaningful for the remote Custom-command row.
void openValueKb(const char* initial, bool cli_autocomplete = false) {
kb().begin(initial, (int)sizeof(_cmd_text) - 1); // begin() resets any previous placeholder hook
kb().clearPlaceholders(); // a CLI command/value is literal, not a message
if (cli_autocomplete) kb().setPlaceholderRefresh(refreshCmdPlaceholders, nullptr, "Commands:");
_kb_active = true;
}
// Candidate completions for the remote Custom-command row's {} picker: every
// remotely-usable command from docs/cli_commands.md (Serial-Only ones and
// the region sub-grammar's many variants excluded -- typing those out stays
// manual). Entries needing a value end in a trailing space, ready to type it.
static const char* const CLI_COMMANDS[];
static const int CLI_COMMAND_COUNT;
// KeyboardWidget::PlaceholderRefreshFn -- repopulates the {} list with only
// the CLI_COMMANDS entries matching the word currently being typed (the
// text since the last space), so the list narrows as you type. An empty
// word (nothing typed yet, or just after a space) matches everything.
static void refreshCmdPlaceholders(KeyboardWidget& kbw, void* /*ctx*/) {
kbw.clearPlaceholders();
int word_start = kbw.len;
while (word_start > 0 && kbw.buf[word_start - 1] != ' ') word_start--;
const char* word = kbw.buf + word_start;
int word_len = kbw.len - word_start;
for (int i = 0; i < CLI_COMMAND_COUNT; i++)
if (word_len == 0 || strncmp(CLI_COMMANDS[i], word, word_len) == 0)
kbw.addPlaceholder(CLI_COMMANDS[i]);
}
// Dispatch Enter on a remote COMMAND row per the three field shapes (see
// file header). The trailing "Custom command..." row (both null) is
// handled first.
void activateField(const AdminField& f) {
_fetch_for_edit = false;
_pending_set_prefix = nullptr;
if (f.get_cmd == nullptr && f.set_prefix == nullptr) { // Custom command...
openValueKb(_cmd_text, true);
} else if (f.set_prefix == nullptr) { // Action
strncpy(_cmd_text, f.get_cmd, sizeof(_cmd_text) - 1);
_cmd_text[sizeof(_cmd_text) - 1] = '\0';
sendCommand();
} else if (f.get_cmd == nullptr) { // Set-only
_pending_set_prefix = f.set_prefix;
openValueKb("");
} else { // Get-then-edit-then-set
_pending_set_prefix = f.set_prefix;
_fetch_for_edit = true;
strncpy(_cmd_text, f.get_cmd, sizeof(_cmd_text) - 1);
_cmd_text[sizeof(_cmd_text) - 1] = '\0';
sendCommand();
}
}
// A fetch-for-edit didn't get a reply (timeout or the user cancelled the
// wait) -- fall back to a blank editor rather than dead-ending, so the
// field can still be set blind.
void fallBackToBlankEdit() {
_fetch_for_edit = false;
openValueKb("");
}
// Dispatch Enter on a local COMMAND row: actions fire immediately; value
// fields open the keyboard pre-filled with the current reading.
void activateLocalField(const LocalField& f) {
NodePrefs* p = _task->getNodePrefs();
char buf[32];
switch (f.kind) {
case LK_NAME:
_pending_local_kind = LK_NAME;
openValueKb(the_mesh.getNodeName());
break;
case LK_RADIO:
if (p) snprintf(buf, sizeof(buf), "%.3f,%.0f,%d,%d", p->freq, p->bw, (int)p->sf, (int)p->cr);
else buf[0] = '\0';
_pending_local_kind = LK_RADIO;
openValueKb(buf);
break;
case LK_TXPOWER:
snprintf(buf, sizeof(buf), "%d", p ? (int)p->tx_power_dbm : 0);
_pending_local_kind = LK_TXPOWER;
openValueKb(buf);
break;
case LK_LAT:
snprintf(buf, sizeof(buf), "%.6f", sensors.node_lat);
_pending_local_kind = LK_LAT;
openValueKb(buf);
break;
case LK_LON:
snprintf(buf, sizeof(buf), "%.6f", sensors.node_lon);
_pending_local_kind = LK_LON;
openValueKb(buf);
break;
case LK_REBOOT:
_task->showAlert("Rebooting...", 800);
board.reboot();
break;
case LK_ADVERT:
_task->showAlert(the_mesh.advert() ? "Advert sent!" : "Advert failed", 1200);
break;
default: break;
}
}
// Parse+apply a submitted local field value. Deliberately light validation
// (parse-success only, no chip/physical-range checks like Settings' radio
// editor does) -- this panel is a fast power-user shim over the same
// settings, not a replacement for Settings' fuller, bounds-checked editors.
void commitLocalField(LocalKind kind, const char* text) {
NodePrefs* p = _task->getNodePrefs();
if (!p) return;
switch (kind) {
case LK_NAME: {
strncpy(p->node_name, text, sizeof(p->node_name) - 1);
p->node_name[sizeof(p->node_name) - 1] = '\0';
the_mesh.savePrefs();
_task->showAlert("Saved", 800);
break;
}
case LK_RADIO: {
float freq, bw; int sf, cr;
if (sscanf(text, "%f,%f,%d,%d", &freq, &bw, &sf, &cr) == 4) {
p->freq = freq; p->bw = bw; p->sf = (uint8_t)sf; p->cr = (uint8_t)cr;
_task->applyRadioParams();
the_mesh.savePrefs();
_task->showAlert("Saved", 800);
} else {
_task->showAlert("Expected freq,bw,sf,cr", 1600);
}
break;
}
case LK_TXPOWER: {
int dbm;
if (sscanf(text, "%d", &dbm) == 1) {
p->tx_power_dbm = (int8_t)dbm;
_task->applyTxPower();
the_mesh.savePrefs();
_task->showAlert("Saved", 800);
} else {
_task->showAlert("Invalid number", 1400);
}
break;
}
case LK_LAT: {
double v;
if (sscanf(text, "%lf", &v) == 1 && v >= -90.0 && v <= 90.0) {
sensors.node_lat = v;
the_mesh.savePrefs();
_task->showAlert("Saved", 800);
} else {
_task->showAlert("Lat must be -90..90", 1600);
}
break;
}
case LK_LON: {
double v;
if (sscanf(text, "%lf", &v) == 1 && v >= -180.0 && v <= 180.0) {
sensors.node_lon = v;
the_mesh.savePrefs();
_task->showAlert("Saved", 800);
} else {
_task->showAlert("Lon must be -180..180", 1600);
}
break;
}
default: break;
}
}
public:
explicit AdminScreen(UITask* task) : _task(task) {}
// Central per-visit reset (see UIScreen::onShow) -- always starts back at
// the This-device/Remote-node chooser and forgets the admin-login memo, so
// remote credentials really are re-typed each time the tool is (re-)entered.
void onShow() override {
_phase = CHOOSER;
_sel = 0;
_local_mode = false;
_tab = 0;
_row_sel = _row_scroll = 0;
_kb_active = false;
_waiting = false;
_fetch_for_edit = false;
_pending_set_prefix = nullptr;
_pending_local_kind = LK_NONE;
_login_waiting = false;
_admin_ok = false;
}
// Canonical entry for a specific remote target -- called by
// UITask::openAdminFor(), itself reached from NearbyScreen's "Admin"
// context-menu action or its Admin-target pick-mode. Skips straight past
// login if this contact is already admin-ok this visit.
void startFor(const ContactInfo& ci) {
_target = ci;
_local_mode = false;
if (isAdminOk(ci.id.pub_key)) {
_tab = ATAB_SYSTEM; _row_sel = _row_scroll = 0;
_phase = COMMAND;
} else {
char saved_pw[sizeof(_login_pw)];
if (the_mesh.getRoomPassword(ci.id.pub_key, saved_pw, sizeof(saved_pw)))
startLoginWithSaved(saved_pw);
else
startLogin();
}
}
// Async result of the on-device login, routed here from UITask::onRoomLoginResult()
// only while this screen is current (see that routing for why).
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
if (_phase != LOGIN) return; // stale/unrelated result
_login_waiting = false;
if (success && (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
memcpy(_admin_ok_prefix, pub_key, 4);
_admin_ok = true;
the_mesh.saveRoomPassword(pub_key, _login_pw); // remember it -- same logic as room login
_tab = ATAB_SYSTEM;
_row_sel = _row_scroll = 0;
_phase = COMMAND;
_task->showAlert("Logged in (admin)", 1000);
} else if (success) {
// Correct password, just insufficient permission -- leave any saved
// password alone, retyping the same one won't change the outcome.
_phase = CHOOSER; _sel = 0;
_task->showAlert("Not admin on this node", 1600);
} else {
// Wrong/stale password -- forget it so the next attempt prompts fresh,
// same self-healing behaviour as a room login.
the_mesh.forgetRoomPassword(pub_key);
_phase = CHOOSER; _sel = 0;
_task->showAlert("Login failed", 1400);
}
}
// Async CLI reply, routed here from UITask::onAdminReply().
void onAdminReply(const uint8_t* pub_key, const char* text) {
if (!_waiting || memcmp(_target.id.pub_key, pub_key, 4) != 0) return;
_waiting = false;
if (_fetch_for_edit) {
_fetch_for_edit = false;
char trimmed[161];
strncpy(trimmed, text, sizeof(trimmed) - 1);
trimmed[sizeof(trimmed) - 1] = '\0';
size_t n = strlen(trimmed);
while (n > 0 && (trimmed[n-1] == '\n' || trimmed[n-1] == '\r' || trimmed[n-1] == ' ')) trimmed[--n] = '\0';
openValueKb(trimmed); // _pending_set_prefix is already set from activateField()
return;
}
strncpy(_reply_text, text, sizeof(_reply_text) - 1);
_reply_text[sizeof(_reply_text) - 1] = '\0';
_reply_view.begin();
_phase = REPLY;
}
void poll() override {
if (_phase == COMMAND && _waiting && (int32_t)(millis() - _cmd_deadline_ms) >= 0) {
_waiting = false;
if (_fetch_for_edit) { fallBackToBlankEdit(); _task->showAlert("Fetch failed - enter value", 1400); }
else { _task->showAlert("No response (timeout)", 1600); }
}
}
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
if (_phase == CHOOSER) {
display.drawCenteredHeader("ADMIN");
static const char* const ROWS[2] = { "This device", "Remote node..." };
int scroll = 0;
drawList(display, 2, _sel, scroll, [&](int i, int y, bool sel, int reserve) {
drawRowSelection(display, y, sel, reserve);
display.drawTextEllipsized(2, y, display.width() - 4 - reserve, ROWS[i]);
});
return 2000;
}
if (_phase == LOGIN) {
if (_login_waiting) {
display.drawCenteredHeader("ADMIN LOGIN");
display.setCursor(2, display.listStart());
display.print("Logging in...");
return 500;
}
return kb().render(display);
}
if (_phase == COMMAND) {
if (_kb_active) return kb().render(display);
if (_waiting) {
char title[24];
snprintf(title, sizeof(title), "%.23s", _target.name);
display.drawCenteredHeader(title);
display.setCursor(2, display.listStart());
display.print(_fetch_for_edit ? "Fetching..." : "Waiting for reply...");
return 500;
}
tabbar::draw(display, _local_mode ? LOCAL_TAB_LABELS : TAB_LABELS, tabCount(), _tab);
int n = rowsInTab(_tab);
drawList(display, n, _row_sel, _row_scroll, [&](int i, int y, bool sel, int reserve) {
drawRowSelection(display, y, sel, reserve);
display.drawTextEllipsized(2, y, display.width() - 4 - reserve, rowLabel(_tab, i));
});
return 2000;
}
// REPLY
return _reply_view.render(display, _target.name, _reply_text, false, false);
}
bool handleInput(char c) override {
if (_phase == CHOOSER) {
if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; }
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 1; return true; }
if (c == KEY_DOWN) { _sel = (_sel < 1) ? _sel + 1 : 0; return true; }
if (c == KEY_ENTER) {
if (_sel == 0) {
_local_mode = true;
_tab = 0; _row_sel = _row_scroll = 0;
_phase = COMMAND;
} else {
_task->pickAdminTarget(); // -> Tools > Nodes pick-mode -> startFor()
}
return true;
}
return true;
}
if (_phase == LOGIN) {
if (_login_waiting) {
if (c == KEY_CANCEL) { _login_waiting = false; _phase = CHOOSER; _sel = 0; }
return true;
}
auto r = kb().handleInput(c);
if (r == KeyboardWidget::CANCELLED) {
_phase = CHOOSER; _sel = 0;
} else if (r == KeyboardWidget::DONE) {
strncpy(_login_pw, kb().buf, sizeof(_login_pw) - 1);
_login_pw[sizeof(_login_pw) - 1] = '\0';
bool sent = the_mesh.sendRoomLogin(_target, _login_pw);
_task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500);
if (sent) _login_waiting = true; else { _phase = CHOOSER; _sel = 0; }
// else: stay in LOGIN until onRoomLoginResult() fires above.
}
return true;
}
if (_phase == COMMAND) {
if (_kb_active) {
auto r = kb().handleInput(c);
if (r == KeyboardWidget::DONE) {
_kb_active = false;
char edited[sizeof(_cmd_text)];
strncpy(edited, kb().buf, sizeof(edited) - 1);
edited[sizeof(edited) - 1] = '\0';
if (_local_mode) {
LocalKind k = _pending_local_kind;
_pending_local_kind = LK_NONE;
commitLocalField(k, edited);
} else if (_pending_set_prefix) {
snprintf(_cmd_text, sizeof(_cmd_text), "%s %s", _pending_set_prefix, edited);
_pending_set_prefix = nullptr;
sendCommand();
} else {
strncpy(_cmd_text, edited, sizeof(_cmd_text) - 1);
_cmd_text[sizeof(_cmd_text) - 1] = '\0';
if (_cmd_text[0]) sendCommand();
}
} else if (r == KeyboardWidget::CANCELLED) {
_kb_active = false;
_pending_set_prefix = nullptr;
_pending_local_kind = LK_NONE;
}
return true;
}
if (_waiting) {
if (c == KEY_CANCEL) {
_waiting = false;
if (_fetch_for_edit) fallBackToBlankEdit();
}
return true;
}
if (c == KEY_CANCEL) { _phase = CHOOSER; _sel = 0; return true; }
int count = tabCount();
if (keyIsPrev(c)) { _tab = (_tab + count - 1) % count; _row_sel = _row_scroll = 0; return true; }
if (keyIsNext(c)) { _tab = (_tab + 1) % count; _row_sel = _row_scroll = 0; return true; }
int n = rowsInTab(_tab);
if (c == KEY_UP) { _row_sel = (_row_sel > 0) ? _row_sel - 1 : n - 1; return true; }
if (c == KEY_DOWN) { _row_sel = (_row_sel < n - 1) ? _row_sel + 1 : 0; return true; }
if (c == KEY_ENTER) {
if (_local_mode) activateLocalField(localFieldAt(_tab, _row_sel));
else activateField(fieldAt(_tab, _row_sel));
return true;
}
return true;
}
// REPLY
auto res = _reply_view.handleInput(c);
if (res != FullscreenMsgView::NONE) _phase = COMMAND; // CLOSE / PREV / NEXT / REPLY all just return here
return true;
}
};
const char* AdminScreen::TAB_LABELS[AdminScreen::ATAB_COUNT] = { "System", "Radio", "Routing", "Actions" };
const char* AdminScreen::LOCAL_TAB_LABELS[AdminScreen::LOCAL_TAB_COUNT] = { "System", "Actions" };
const AdminScreen::AdminField AdminScreen::SYSTEM_FIELDS[] = {
{ "Name", "get name", "set name" },
{ "Owner info", "get owner.info", "set owner.info" },
{ "Admin password", nullptr, "password" },
};
const AdminScreen::AdminField AdminScreen::RADIO_FIELDS[] = {
{ "Radio (f,bw,sf,cr)", "get radio", "set radio" },
{ "TX power", "get tx", "set tx" },
};
const AdminScreen::AdminField AdminScreen::ROUTING_FIELDS[] = {
{ "Repeat", "get repeat", "set repeat" },
{ "Advert interval (min)", "get advert.interval", "set advert.interval" },
{ "Flood advert interval (h)", "get flood.advert.interval", "set flood.advert.interval" },
{ "Max hops", "get flood.max", "set flood.max" },
};
const AdminScreen::AdminField AdminScreen::ACTION_FIELDS[] = {
{ "Send advert", "advert", nullptr },
{ "Send zero-hop advert", "advert.zerohop", nullptr },
{ "Sync clock", "clock sync", nullptr },
{ "Reboot", "reboot", nullptr }, // disruptive + no confirm: keep off the default row
{ "Custom command...", nullptr, nullptr },
};
const int AdminScreen::ROWS_PER_TAB[AdminScreen::ATAB_COUNT] = {
sizeof(AdminScreen::SYSTEM_FIELDS) / sizeof(AdminScreen::AdminField),
sizeof(AdminScreen::RADIO_FIELDS) / sizeof(AdminScreen::AdminField),
sizeof(AdminScreen::ROUTING_FIELDS) / sizeof(AdminScreen::AdminField),
sizeof(AdminScreen::ACTION_FIELDS) / sizeof(AdminScreen::AdminField),
};
const AdminScreen::LocalField AdminScreen::LOCAL_SYSTEM_FIELDS[] = {
{ "Name", AdminScreen::LK_NAME },
{ "Radio (f,bw,sf,cr)", AdminScreen::LK_RADIO },
{ "TX power", AdminScreen::LK_TXPOWER },
{ "Lat", AdminScreen::LK_LAT },
{ "Lon", AdminScreen::LK_LON },
};
const AdminScreen::LocalField AdminScreen::LOCAL_ACTION_FIELDS[] = {
{ "Send advert", AdminScreen::LK_ADVERT },
{ "Reboot", AdminScreen::LK_REBOOT }, // disruptive + no confirm: keep off the default row
};
const int AdminScreen::LOCAL_ROWS_PER_TAB[AdminScreen::LOCAL_TAB_COUNT] = {
sizeof(AdminScreen::LOCAL_SYSTEM_FIELDS) / sizeof(AdminScreen::LocalField),
sizeof(AdminScreen::LOCAL_ACTION_FIELDS) / sizeof(AdminScreen::LocalField),
};
const char* const AdminScreen::CLI_COMMANDS[] = {
"reboot", "poweroff", "shutdown", "clkreboot", "clock", "clock sync",
"advert", "advert.zerohop", "start ota", "erase",
"neighbors", "neighbor.remove ", "discover.neighbors",
"ver", "board",
"get radio", "set radio ", "get tx", "set tx ", "tempradio ",
"get freq", "set freq ", "get radio.rxgain", "set radio.rxgain ",
"get name", "set name ", "get lat", "set lat ", "get lon", "set lon ",
"get owner.info", "set owner.info ", "get adc.multiplier", "set adc.multiplier ",
"get public.key", "get role",
"powersaving", "powersaving on", "powersaving off",
"get repeat", "set repeat ",
"get path.hash.mode", "set path.hash.mode ",
"get loop.detect", "set loop.detect ",
"get txdelay", "set txdelay ", "get direct.txdelay", "set direct.txdelay ",
"get rxdelay", "set rxdelay ", "get dutycycle", "set dutycycle ",
"get af", "set af ", "get int.thresh", "set int.thresh ",
"get agc.reset.interval", "set agc.reset.interval ",
"get multi.acks", "set multi.acks ",
"get flood.advert.interval", "set flood.advert.interval ",
"get advert.interval", "set advert.interval ",
"get flood.max", "set flood.max ",
"get flood.max.unscoped", "set flood.max.unscoped ",
"get flood.max.advert", "set flood.max.advert ",
"setperm ", "get acl", "get allow.read.only", "set allow.read.only ",
"region", "region load", "region save", "region list ",
"gps", "gps sync", "gps setloc", "gps advert ",
"sensor list", "sensor get ", "sensor set ",
"get bridge.type", "get bridge.enabled", "set bridge.enabled ",
"get bridge.delay", "set bridge.delay ", "get bridge.source", "set bridge.source ",
"get bridge.baud", "set bridge.baud ", "get bridge.channel", "set bridge.channel ",
"get bridge.secret", "set bridge.secret ",
"get pwrmgt.support", "get pwrmgt.source", "get pwrmgt.bootreason", "get pwrmgt.bootmv",
"password ", "get guest.password", "set guest.password ",
};
const int AdminScreen::CLI_COMMAND_COUNT = sizeof(AdminScreen::CLI_COMMANDS) / sizeof(AdminScreen::CLI_COMMANDS[0]);