diff --git a/docs/solo_features/settings_screen/settings_screen.md b/docs/solo_features/settings_screen/settings_screen.md index 83b786fd..cd8b1db8 100644 --- a/docs/solo_features/settings_screen/settings_screen.md +++ b/docs/solo_features/settings_screen/settings_screen.md @@ -88,9 +88,11 @@ The **repeater** mode and its flood filters live on their own screen — see **T | Setting | Options | Notes | | ----------- | --------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Name | keyboard entry (up to 31 chars) | This device's node name, shown to others and in every advert. **Enter** opens the keyboard pre-filled with the current name; applied and saved on submit | | Timezone | −12 h … +14 h | UTC offset in whole hours | | Low battery | off / 3.0 V / 3.1 V / 3.2 V / 3.3 V / 3.4 V / 3.5 V | Auto-shutdown threshold; also sets the 0 % anchor for the battery percentage indicator | | Units | Metric / Imperial | Global unit system for every distance/speed shown in Tools (Nearby Nodes, Trail, navigate-to-point). Metric: m / km, km/h, min/km. Imperial: ft / mi, mph, min/mi | +| Reboot | action (**Enter**) | Restarts this device. Pending setting changes are saved first. Last row, so it isn't the default-selected one | --- diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index e1b91571..f7fead1c 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -517,13 +517,9 @@ Live forwarding stats — **Forwarded**, **Pool free**, **Queue** — are shown -Send commands to **this device**, or to a **repeater/room server you have admin permission on** — the on-device equivalent of the companion app's repeater-admin feature for remote nodes, plus a local shim over settings this device already has. See [CLI Commands](../../cli_commands.md) for the full remote command grammar. +Send commands to a **repeater/room server you have admin permission on** — the on-device equivalent of the companion app's repeater-admin feature. See [CLI Commands](../../cli_commands.md) for the full command grammar. (Admin only manages *remote* nodes; this device's own name, radio, TX power and reboot live in **Settings** — see below.) -Opening **Tools › Admin** shows a two-row chooser: **This device** or **Remote node...**. - -### Remote node - -1. **Select a node** — "Remote node..." opens **Tools › Nodes** (the same screen, filters, sort and live scan as browsing it normally) so picking a node for Admin looks exactly like using Nodes for anything else; **Enter** on a repeater/room row hands it to Admin. Admin is also reachable directly from a node's own **Hold Enter** menu in Nodes, without going through the chooser at all. +1. **Select a node** — opening **Tools › Admin** goes straight to **Tools › Nodes** (the same screen, filters, sort and live scan as browsing it normally) so picking a node for Admin looks exactly like using Nodes for anything else; **Enter** on a repeater/room row hands it to Admin, **Cancel** returns to Tools. Admin is also reachable directly from a node's own **Hold Enter** menu in Nodes. 2. **Log in** — type the node's **admin password** (the same login handshake Messages uses for room servers; a repeater's admin password is set with the `password` CLI command). If a password was already saved for this node from an earlier successful login, it retries silently instead of prompting. Only a login that comes back with **admin**-level permission unlocks the next step — anything less shows "Not admin on this node". 3. **Pick a category and a field** — a tab carousel (**LEFT/RIGHT** to switch category, **UP/DOWN** to move within it, same as Auto-Reply Bot's tabs), so common settings don't need the CLI grammar memorised: @@ -548,11 +544,4 @@ Opening **Tools › Admin** shows a two-row chooser: **This device** or **Remote ### This device -A lighter, login-free version of the same tab carousel, for settings this device already has: - -| Tab | Rows | -| --- | ---- | -| **System** | Name, Radio (freq, bandwidth, spreading factor, coding rate), TX power, Lat, Lon | -| **Actions** | Send advert, Reboot | - -There's no fetch step here — the current value is already known, so **Enter** opens the keyboard pre-filled with it immediately, and submitting applies the change at once (the same apply logic Settings' own Radio/TX power rows use) and saves it. Reboot and Send advert fire immediately, same as their remote counterparts. This is a fast, lightly-validated shortcut alongside Settings' own fuller editors (radio presets, chip-bounds frequency entry, etc.) — for careful bounds-checked editing, prefer Settings. +Admin doesn't manage the companion itself — its own settings live in **Settings**: **Radio** (preset / freq / SF / BW / CR) and **TX power** in the Radio section, and **Name** and **Reboot** in the System section. **Send advert** is the home **ADVERT** page. diff --git a/examples/companion_radio/ui-new/AdminScreen.h b/examples/companion_radio/ui-new/AdminScreen.h index eda5c6fe..ead5e23c 100644 --- a/examples/companion_radio/ui-new/AdminScreen.h +++ b/examples/companion_radio/ui-new/AdminScreen.h @@ -1,28 +1,24 @@ #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). +// see docs/cli_commands.md). This device's own settings live in Settings / +// Tools, not here -- Admin is purely for remote nodes. // -// 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(). +// Entry is always a target-then-act flow: Tools > Admin (or a repeater/room's +// Hold-Enter "Admin" action in Nodes) opens Tools > Nodes in a pick-mode +// (NearbyScreen::startPickAdminTarget(), the same borrow-another-screen's-list +// idiom the channel/bot pickers use), and picking a node calls the single +// canonical UITask::openAdminFor(ContactInfo) -> startFor(). Backing out of a +// command screen returns to that picker; backing out of the picker returns to +// Tools. // -// REMOTE mode: login (session password, self-heals like room logins) unlocks a -// category tab carousel of AdminField{label, get_cmd, set_prefix} rows: +// After login (session password, self-heals like room logins) a category tab +// carousel of AdminField{label, get_cmd, set_prefix} rows unlocks: // set_prefix==null -> action: send get_cmd literally (e.g. "reboot") // get_cmd==null -> set-only: blank keyboard, send " " // 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" @@ -31,31 +27,26 @@ class AdminScreen : public UIScreen { UITask* _task; - enum Phase : uint8_t { CHOOSER, LOGIN, COMMAND, REPLY }; - Phase _phase = CHOOSER; + enum Phase : uint8_t { LOGIN, COMMAND, REPLY }; + Phase _phase = LOGIN; - // CHOOSER -- "This device" (row 0) / "Remote node..." (row 1). Reuses _sel. - int _sel = 0; + ContactInfo _target; - bool _local_mode = false; // COMMAND is administering this device, not _target - ContactInfo _target; // valid only when !_local_mode - - // LOGIN (remote only) + // LOGIN 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). + // True while waiting for a login result with no keyboard on screen -- either a + // silent retry with a remembered password, or right after the typed password + // was submitted. Distinct from the keyboard 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. + // One-slot memo: the last contact 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(). uint8_t _admin_ok_prefix[4] = { 0, 0, 0, 0 }; bool _admin_ok = false; - // ── COMMAND: remote (CLI-grammar) tabs/rows ───────────────────────────────── + // ── COMMAND: category tabs / rows ─────────────────────────────────────────── enum AdminTab : uint8_t { ATAB_SYSTEM, ATAB_RADIO, ATAB_ROUTING, ATAB_ACTIONS, ATAB_COUNT }; static const char* TAB_LABELS[ATAB_COUNT]; @@ -75,38 +66,17 @@ class AdminScreen : public UIScreen { } } - // ── 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 + char _cmd_text[161] = ""; // the command being built/sent + bool _waiting = false; // 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 + bool _fetch_for_edit = false; // true while waiting on a "get" whose reply opens an editor + const char* _pending_set_prefix = nullptr; // non-null => edited keyboard text gets this prefix on submit - // REPLY (remote only) + // REPLY FullscreenMsgView _reply_view; char _reply_text[200] = ""; @@ -152,9 +122,8 @@ class AdminScreen : public UIScreen { } // 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. + // Custom-command row and remote set-only/get-then-edit fields. `cli_autocomplete` + // wires the {} picker to CLI command-name completion (Custom-command row only). 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 @@ -162,17 +131,16 @@ class AdminScreen : public UIScreen { _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. + // Candidate completions for the 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). 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. + // KeyboardWidget::PlaceholderRefreshFn -- repopulates the {} list with only the + // CLI_COMMANDS entries matching the word currently being typed (text since the + // last space), so it narrows as you type. An empty word matches everything. static void refreshCmdPlaceholders(KeyboardWidget& kbw, void* /*ctx*/) { kbw.clearPlaceholders(); int word_start = kbw.len; @@ -184,9 +152,8 @@ class AdminScreen : public UIScreen { 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. + // Dispatch Enter on a 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; @@ -209,148 +176,36 @@ class AdminScreen : public UIScreen { } // 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. + // 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. + // Per-visit reset (see UIScreen::onShow). startFor() runs immediately after + // and sets the real phase/target; these are just safe defaults. void onShow() override { - _phase = CHOOSER; - _sel = 0; - _local_mode = false; + _phase = LOGIN; _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. + // Canonical entry for a specific 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; @@ -379,14 +234,14 @@ public: } 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); + _task->pickAdminTarget(); } 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); + _task->pickAdminTarget(); } } @@ -422,17 +277,6 @@ public: 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"); @@ -453,11 +297,11 @@ public: 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); + tabbar::draw(display, TAB_LABELS, ATAB_COUNT, _tab); + int n = ROWS_PER_TAB[_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)); + display.drawTextEllipsized(2, y, display.width() - 4 - reserve, fieldAt(_tab, i).label); }); return 2000; } @@ -467,37 +311,20 @@ public: } 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; } + if (c == KEY_CANCEL) { _login_waiting = false; _task->pickAdminTarget(); } return true; } auto r = kb().handleInput(c); if (r == KeyboardWidget::CANCELLED) { - _phase = CHOOSER; _sel = 0; + _task->pickAdminTarget(); } 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; } + if (sent) _login_waiting = true; else _task->pickAdminTarget(); // else: stay in LOGIN until onRoomLoginResult() fires above. } return true; @@ -511,11 +338,7 @@ public: 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) { + if (_pending_set_prefix) { snprintf(_cmd_text, sizeof(_cmd_text), "%s %s", _pending_set_prefix, edited); _pending_set_prefix = nullptr; sendCommand(); @@ -527,7 +350,6 @@ public: } else if (r == KeyboardWidget::CANCELLED) { _kb_active = false; _pending_set_prefix = nullptr; - _pending_local_kind = LK_NONE; } return true; } @@ -538,18 +360,13 @@ public: } 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_CANCEL) { _task->pickAdminTarget(); return true; } + if (keyIsPrev(c)) { _tab = (_tab + ATAB_COUNT - 1) % ATAB_COUNT; _row_sel = _row_scroll = 0; return true; } + if (keyIsNext(c)) { _tab = (_tab + 1) % ATAB_COUNT; _row_sel = _row_scroll = 0; return true; } + int n = ROWS_PER_TAB[_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; - } + if (c == KEY_ENTER) { activateField(fieldAt(_tab, _row_sel)); return true; } return true; } @@ -561,7 +378,6 @@ public: }; 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" }, @@ -593,22 +409,6 @@ const int AdminScreen::ROWS_PER_TAB[AdminScreen::ATAB_COUNT] = { 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", diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 216d8dfd..e8e4dd46 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -27,7 +27,7 @@ class NearbyScreen : public UIScreen { enum Action : uint8_t { ACT_NAV, ACT_PING, ACT_WAYPOINT, ACT_LOCATOR, ACT_ADD, ACT_DELETE, ACT_FAV, ACT_ADMIN, ACT_SORT, ACT_SCAN }; - // Set by UITask::pickAdminTarget() (Tools > Admin's "Remote node..." choice): + // Set by UITask::pickAdminTarget() (Tools > Admin, which is remote-only): // while true, ENTER on an eligible row (a stored repeater/room contact) hands // the node straight to Admin instead of opening the detail view -- everything // else (filters, scan, ping, sort, the Hold-Enter menu) behaves identically to @@ -934,7 +934,7 @@ public: // ── list view ─────────────────────────────────────────────────────────── if (c == KEY_CANCEL) { - if (_pick_admin_target) { _pick_admin_target = false; _task->gotoAdminScreen(); return true; } + if (_pick_admin_target) { _pick_admin_target = false; _task->gotoToolsScreen(); return true; } if (_source == SRC_SCAN) leaveScan(); else _task->gotoToolsScreen(); return true; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 7b3fe2e8..8c99ecfe 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -64,9 +64,11 @@ class SettingsScreen : public UIScreen { TX_APC, // System section SECTION_SYSTEM, + DEVICE_NAME, TIMEZONE, LOW_BAT, UNITS, + REBOOT, // Keyboard section SECTION_KEYBOARD, KEYBOARD_TYPE, @@ -554,6 +556,12 @@ class SettingsScreen : public UIScreen { display.print("Units"); display.setCursor(valCol(display), y); display.print((p && p->units_imperial) ? "Imperial" : "Metric"); + } else if (item == DEVICE_NAME) { + display.print("Name"); + int vx = valCol(display); + display.drawTextEllipsized(vx, y, display.width() - vx - _reserve, the_mesh.getNodeName()); + } else if (item == REBOOT) { + display.print("Reboot"); // action row: Enter reboots this device } else if (item == KEYBOARD_TYPE) { display.print("Type"); display.setCursor(valCol(display), y); @@ -632,6 +640,7 @@ class SettingsScreen : public UIScreen { // Keyboard state for editing message slots int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited + bool _edit_name = false; // editing DEVICE_NAME via the keyboard KeyboardWidget* _kb; // Radio preset picker — names are too long for the value column, so Enter on @@ -654,6 +663,7 @@ public: void onShow() override { _dirty = false; + _edit_name = false; resetList(); _editor.freq.active = false; } @@ -661,7 +671,7 @@ public: int render(DisplayDriver& display) override { display.setTextSize(1); - if (_edit_slot >= 0 || _picker.saving) { + if (_edit_slot >= 0 || _edit_name || _picker.saving) { return _kb->render(display); } @@ -708,6 +718,22 @@ public: return true; } + // Keyboard editing mode for the device name + if (_edit_name) { + auto res = _kb->handleInput(c); + if (res == KeyboardWidget::DONE) { + if (p) { + strncpy(p->node_name, _kb->buf, sizeof(p->node_name) - 1); + p->node_name[sizeof(p->node_name) - 1] = '\0'; + _dirty = true; // savePrefsIfDirty on exit; getNodeName()/self-advert read node_name live + } + _edit_name = false; + } else if (res == KeyboardWidget::CANCELLED) { + _edit_name = false; + } + return true; + } + // Digit-by-digit Freq editor if (_editor.active()) { if (_editor.handleFreqInput(c) && p) { _task->applyRadioParams(); _dirty = true; } @@ -886,6 +912,18 @@ public: _dirty = true; return true; } + if (_selected == DEVICE_NAME && p && enter) { + _edit_name = true; + _kb->begin(the_mesh.getNodeName(), (int)sizeof(p->node_name) - 1); + _kb->clearPlaceholders(); // a device name is literal, not a message + return true; + } + if (_selected == REBOOT && enter) { + _task->savePrefsIfDirty(_dirty); // don't lose pending edits across the restart + _task->showAlert("Rebooting...", 800); + board.reboot(); + return true; + } if (_selected == KEYBOARD_TYPE && p && (left || right || enter)) { p->keyboard_type ^= 1; _dirty = true; diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 7efb2166..f0595a38 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -49,7 +49,7 @@ class ToolsScreen : public UIScreen { case ACT_BOT: _task->gotoBotScreen(); break; case ACT_AUTOADVERT: _task->gotoAutoAdvertScreen(); break; case ACT_REPEATER: _task->gotoRepeaterScreen(); break; - case ACT_ADMIN: _task->gotoAdminScreen(); break; + case ACT_ADMIN: _task->pickAdminTarget(); break; // Admin is remote-only: pick a node first case ACT_CLOCK: _task->gotoClockTools(); break; case ACT_RINGTONE: _task->gotoRingtoneEditor(); break; case ACT_DIAGNOSTICS: _task->gotoDiagnosticsScreen(); break; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8c87a6b1..879ea477 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1469,7 +1469,6 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no void UITask::gotoSettingsScreen() { setCurrScreen(settings); } void UITask::gotoToolsScreen() { setCurrScreen(tools_screen); } void UITask::gotoBotScreen() { setCurrScreen(bot_screen); } -void UITask::gotoAdminScreen() { setCurrScreen(admin_screen); } // AdminScreen::onShow() resets it void UITask::gotoNearbyScreen() { setCurrScreen(nearby_screen); } void UITask::pickAdminTarget() { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 7aae03e5..2f4f882a 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -230,8 +230,7 @@ public: void gotoToolsScreen(); void gotoRingtoneEditor(int slot = 0); void gotoBotScreen(); - void gotoAdminScreen(); - void pickAdminTarget(); // open Nodes to choose a remote repeater/room for Admin + void pickAdminTarget(); // Admin is remote-only: open Nodes to pick a repeater/room void openAdminFor(const ContactInfo& ci); // canonical Admin entry for a specific target (Nodes' Hold-Enter menu or the picker above) void gotoNearbyScreen(); void gotoDashboardConfig();