The node gates the shell on its security.admin_key list - the same list the
remote-admin passkey exchange goes through - so opening a shell on a node
that has not authorized this phone got no reply at all.
Remote Shell now has its own section rather than sitting under
Administration, and runs that check itself: the row stays enabled, tapping
it ensures the session and then opens the terminal. Previously the session
was only ever established as a side effect of visiting the remote-admin
screen, so the user had to know to go there first.
Also: openRemoteAdmin and openRemoteShell share one ensureSessionThenNavigate
helper, and the ViewModel now gives up on OPEN after 20 s instead of sitting
in OPENING forever, naming the admin-key requirement when it does.
Verified against meshtasticd: from a node with no session, one tap
establishes the session and lands on a live prompt; with the node stopped,
the watchdog reports 'Session failed' with a Reconnect action.
OPEN and RESIZE carried a hardcoded 80x24, so the remote shell wrapped to
a width the phone does not have. The viewport is now measured in monospace
cells and the session waits for that measurement, so OPEN carries the real
size - 'stty size' on a medium_phone emulator reports 29 54.
Output sits in a SelectionContainer, so a command's output can be copied
out instead of retyped.
Three gaps the rig made obvious:
- The ViewModel tracked a full session lifecycle that nothing rendered, so
a refused or stalled session was indistinguishable from a working one -
both are a blank screen. A status bar now reports anything that is not
OPEN and offers Reconnect.
- Pending input was its own list row, putting the caret under the prompt
rather than after it, which reads as a hung terminal.
- No way to send Ctrl-C, Tab, Esc or arrows. A key bar sends them, and
typeControlSequence() bypasses the keystroke debounce so an interrupt is
not queued behind a 500 ms batch window.
The input sink moved into its own composable with hoisted callbacks.
Verified against meshtasticd: 'uname -s' echoes exactly and returns Linux;
Ctrl-C interrupts and returns a fresh prompt.
Three things stood between the April branch and a working session:
- The CRT theatrics are gone - scanlines, phosphor presets, flicker, and
the AGSL curvature shader, which rendered the block cursor as a skewed
amber quad in the corner. The terminal is now monospace text on the app
theme.
- Turn-taking ('talking stick', RemoteShell.flags) landed on the firmware
side after this branch was written. DMShellModule marks every session
turn-managed and withholds output until the peer grants a turn, so a
client that never grants sees an open session and no prompt, and the
device re-sends RTS every 250 ms until its TX queue fills. We now grant
on every frame we originate and answer any request immediately.
- The invisible input sink passed a constant "" as its value, so Compose
re-delivered characters we had already consumed: 'whoami' reached the
PTY as 'wwhwhowhoa'. It now tracks what it consumed and forwards the
delta, mapping a shrinking field to backspaces.
Verified against meshtasticd: session opens, 'whoami' echoes exactly and
returns 'root'.
- license header year: the tree standardised on 2026, not 2025-2026
- spotless: ktlint's blank-line-between-when-conditions is new since
- MeshDataHandlerImpl: drop shouldBroadcast, removed with the AIDL
service rework (#5586); nothing broadcasts on this path any more
- RemoteShellViewModel: DataPacket.nodeNumToDefaultId and
DataPacket.PKC_CHANNEL_INDEX both moved to NodeAddress
Conflicts, all re-integration of the April branch into the current tree:
- MeshDataHandlerImpl/Test: keep main's constructor (ServiceScope, session
context, geofence/beacon collaborators) and re-add remoteShellHandler.
- Capabilities.kt: re-add supportsRemoteShell, still gated to UNRELEASED.
- strings.xml: main re-sorted the file; re-insert remote_shell and
phosphor_colour in their sorted positions.
- NodesNavigation/AdministrationSection: union both sides.