feat(admin): add confirmed "Start OTA" action for remote nodes

start ota was already sendable via Admin's Custom-command row (and
CLI-reachable directly), but had no dedicated menu entry. Adds a row to
the Actions tab that confirms first (Start/Cancel, defaulting to
Cancel) before sending -- unlike Reboot, OTA parks the remote in BLE
DFU mode for the duration of the update, disruptive enough to warrant
the extra step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-08-29 18:52:32 +02:00
co-authored by Claude Sonnet 5
parent 77f4ac48d6
commit 7d09210b9c
3 changed files with 39 additions and 2 deletions
@@ -568,13 +568,13 @@ Send commands to a **repeater/room server you have admin permission on** — the
| **System** | Name, Owner info, Admin password |
| **Radio** | Frequency, Bandwidth, Spreading factor, Coding rate, TX power |
| **Routing** | Repeat, Advert interval, Flood advert interval, Max hops |
| **Actions** | Send advert, Send zero-hop advert, Sync clock, Reboot, **Custom command...** |
| **Actions** | Send advert, Send zero-hop advert, Sync clock, Reboot, Start OTA, **Custom command...** |
**Enter** on a row does one of four things, depending on the field:
- **Name / Owner info** first **fetch** the node's current value, then open the keyboard **pre-filled** with it to edit — submitting sends the change. If the fetch fails or times out, the keyboard still opens (blank), so the value can be set blind.
- **Radio and Routing rows** are typed, not free text: **Repeat** is an ON/OFF toggle; **Advert interval / Flood advert interval / Max hops / TX power** are number steppers (**LEFT/RIGHT** to adjust, within that field's valid range); **Frequency** uses the same digit-by-digit cursor editor as Settings' own Radio screen (**LEFT/RIGHT** moves between digits, **UP/DOWN** changes the selected one); **Bandwidth / Spreading factor / Coding rate** step through their valid discrete LoRa values with **LEFT/RIGHT**. All four Radio-tuple fields (Frequency/Bandwidth/SF/Coding rate) fetch and re-send the same underlying `radio` value together — editing any one of them still only overwrites that one, the other three round-trip unchanged. **Enter** sends the change; **Cancel** discards it and returns to the row list without sending anything.
- **Admin password** has no fetch (there's no way to read a password back) — it opens straight to a blank keyboard.
- **Actions** (Reboot, Send advert, …) send immediately, no editing step.
- **Actions** (Reboot, Send advert, …) send immediately, no editing step — except **Start OTA**, which asks **Start / Cancel** first (defaulting to Cancel): it puts the remote into BLE DFU mode for the duration of the update, far more disruptive than a quick reboot.
- **Custom command...** (last row of Actions) opens the same free-text entry for anything not covered above — up to 160 characters, see the linked reference for the full grammar. The keyboard's **{}** key doubles as command completion here: it lists commands matching whatever's typed since the last space (narrowing as you type), and picking one completes that word instead of just inserting after it.
4. **Read the reply** — the text reply opens in a scrollable view (**UP/DOWN** to scroll, **Cancel/Enter** to go back to the category tabs).
@@ -25,6 +25,7 @@
#include "FullscreenMsgView.h"
#include "TabBar.h"
#include "PopupMenu.h" // "Start OTA" confirmation
#include "RadioParamsEditor.h" // DigitEditor + stepSF/stepBW/stepCR -- same widgets Settings/Repeater use locally
#include <helpers/ClientACL.h> // PERM_ACL_ADMIN / PERM_ACL_ROLE_MASK
@@ -137,6 +138,15 @@ class AdminScreen : public UIScreen {
FullscreenMsgView _reply_view;
char _reply_text[200] = "";
// "Start OTA" confirmation -- pulls the remote out of the mesh into BLE DFU
// mode for the duration of the update, far more disruptive than the other
// one-shot actions on this tab, so unlike Reboot it doesn't fire on a bare
// Enter. Only this one action needs a confirm today, so it's special-cased
// in activateField() by command string rather than adding a generic
// needs_confirm flag to every AdminField literal below.
PopupMenu _confirm;
const AdminField* _pending_confirm_field = nullptr;
KeyboardWidget& kb() { return _task->keyboard(); }
bool isAdminOk(const uint8_t* pub_key) const {
@@ -226,6 +236,14 @@ class AdminScreen : public UIScreen {
if (f.get_cmd == nullptr && f.set_prefix == nullptr) { // Custom command...
openValueKb(_cmd_text, true);
} else if (f.set_prefix == nullptr) { // Action
if (!strcmp(f.get_cmd, "start ota")) { // see _confirm's comment
_pending_confirm_field = &f;
_confirm.begin("Start OTA update?", 2);
_confirm.addItem("Start");
_confirm.addItem("Cancel");
_confirm.setSelected(1); // default highlight = Cancel
return;
}
strncpy(_cmd_text, f.get_cmd, sizeof(_cmd_text) - 1);
_cmd_text[sizeof(_cmd_text) - 1] = '\0';
sendCommand();
@@ -333,6 +351,8 @@ public:
_value_editing = false;
_login_waiting = false;
_admin_ok = false;
_confirm.active = false;
_pending_confirm_field = nullptr;
}
// Canonical entry for a specific target -- called by UITask::openAdminFor(),
@@ -519,6 +539,7 @@ public:
}
}
});
if (_confirm.active) { _confirm.render(display); return 50; }
return _value_editing ? 50 : 2000;
}
@@ -552,6 +573,20 @@ public:
}
if (_phase == COMMAND) {
if (_confirm.active) {
auto res = _confirm.handleInput(c);
if (res == PopupMenu::SELECTED) {
if (_confirm.selectedIndex() == 0 && _pending_confirm_field) {
strncpy(_cmd_text, _pending_confirm_field->get_cmd, sizeof(_cmd_text) - 1);
_cmd_text[sizeof(_cmd_text) - 1] = '\0';
sendCommand();
}
_pending_confirm_field = nullptr;
} else if (res == PopupMenu::CANCELLED) {
_pending_confirm_field = nullptr;
}
return true;
}
if (_kb_active) {
auto r = kb().handleInput(c);
if (r == KeyboardWidget::DONE) {
@@ -670,6 +705,7 @@ const AdminScreen::AdminField AdminScreen::ACTION_FIELDS[] = {
{ "Send zero-hop advert", "advert.zerohop", nullptr },
{ "Sync clock", "clock sync", nullptr },
{ "Reboot", "reboot", nullptr }, // disruptive + no confirm: keep off the default row
{ "Start OTA", "start ota", nullptr }, // confirmed first -- see _confirm's comment
{ "Custom command...", nullptr, nullptr },
};
+1
View File
@@ -6,6 +6,7 @@
- **Optional magnetic "flip cover" screen lock**, for anyone who wants to wire a Hall-effect or reed sensor to a free GPIO — no board ships one built in. Set `PIN_HALL_SENSOR` as a build flag on your own env and closing the cover locks and blanks the screen instantly, opening it unlocks and wakes it — no combo, independent of Auto-lock. See [Screen Lock](docs/solo_features/screen_lock/screen_lock.md#magnetic-cover-hall-sensor).
- **Experimental: solo build for ProMicro (nRF52840)**, with CardKB support sharing the board's primary I2C bus (no free pins for a second one on this board). Contributed by @tchellow — thanks!
- **New [Build Flags](docs/solo_features/build_flags.md) reference** — every optional `-D` flag a solo build understands (GPIO, CardKB/joystick, Hall-sensor cover lock, buzzer/vibration, GPS switch, display/battery tuning) in one place.
- **Tools Admin gains a "Start OTA" action** for a repeater/room server you're logged into — sends the same `start ota` CLI command the Custom-command row already reached, now with its own menu row and a confirm ("Start" defaulting to "Cancel" first) since it puts the remote into BLE DFU mode for the duration of the update.
### Fixes