Shell: the console's commands on the device's own screen and keyboard (#67)
CI / build (pull_request) Successful in 1m48s
Site / build (pull_request) Successful in 9s

An App in the Launcher that runs the same commands as USB serial and the
Debug Console, trusted like the first. It shows what the console prints
while it is open, through a second ring of the console's that exists only
meanwhile; Ctrl+b keeps only what follows your own commands. Tab completes
a command's name from the firmware's help text, Fn with up and down recalls
earlier lines, Alt with up and down scrolls back. `rm` asks first in the
Shell, `rm -f` doesn't. Nothing is kept once the App is left.

`screenshot [seconds]` saves the screen as a PNG in /screenshots on the
card, now or after a pause: written a row at a time, indexed colour with
RGB332 as the palette, in one stored deflate block.

487 host tests (11 new: the PNG writer, the Shell's log filter, Tab).
Checked on the device over the Debug Console: commands, Tab, history, a
screenshot fetched and decoded on the PC, rm with and without the question,
a delayed screenshot of another screen. 12 KB of flash; 7 KB of heap while
open. Decisions Q204 to Q212 in docs/milestones/S1.md. Safe Mode: #77.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-07 10:53:31 +02:00
co-authored by Claude Opus 5.5
parent c278a06ca1
commit 3863d28593
29 changed files with 1112 additions and 50 deletions
+1 -1
View File
@@ -8,6 +8,6 @@ sort_by = "weight"
eyebrow = "Developer docs"
+++
The firmware is open source (GPL-3.0) and lives on [Gitea](https://git.twis.la/twisla/roro9stack). It is built for one device, the M5Stack Cardputer ADV with the Cap LoRa-1262, and it is built to be **worked on without touching the device**: install a build over Wi-Fi, read its console, press its keys, take screenshots of it, copy files to and from its SD card, and fetch its crash dumps, all from a PC on the same network. The first section is about exactly that.
The firmware is open source (GPL-3.0) and lives on [Gitea](https://git.twis.la/twisla/roro9stack). It is built for one device, the M5Stack Cardputer ADV with the Cap LoRa-1262, and it is built to be **worked on without touching the device**: install a build over Wi-Fi, read its console, press its keys, take screenshots of it, copy files to and from its SD card, and fetch its crash dumps, all from a PC on the same network. The first section is about exactly that. The same commands also run on the device itself, in the [Shell](/guide/shell/).
Some of these pages are written by hand. The rest are **generated from the repository's own documents** (the decisions, the milestone plans, the README and the firmware's own `help` text), so they are never out of date: each says which file it comes from.
+7 -5
View File
@@ -19,10 +19,11 @@ net bytes each network service has read and written since boot
reboot restart
boot other restart into the other app slot (manual Rollback)
log level <0-5> ESP-IDF log level (0 none ... 5 verbose)
ls [folder] | du <path> | mkdir <path> | rm <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules
ls [folder] | du <path> | mkdir <path> | rm [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules
screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause
lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only
lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)
lora sweep on [from MHz] [to MHz] [step kHz] | off | dump RSSI across a band (863 870 100)
lora sweep on [from MHz] [to MHz] [step kHz] | lora sweep off | lora sweep dump RSSI across a band (863 870 100)
lora custom <MHz> <BW kHz> <SF> <CR 5-8> <sync hex> [preamble] e.g. 868.1 125 7 5 34 8 (LoRaWAN)
gnss quiet on|off pause the GNSS receiver while the LoRa radio listens (it costs the radio 8 dB)
gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>
@@ -35,7 +36,7 @@ wifi dns <a> [b] | wifi dns always on|off | wifi ntp <a> [b] DNS and NTP serve
gemini get <url> fetch a Gemini page and report header, size, certificate, heap
irc start | irc stop | irc dump | irc say <buffer> <text>
install <path.ota> Update from SD
update check | list | status | install <tag> the project's releases on Gitea
update check | update list | update status | update install <tag> the project's releases on Gitea
sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal
debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back
debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token
@@ -47,7 +48,7 @@ lora inject <hex> [rssi] [snr] a packet into the LoRa Scanner as if received (
sd fill <folder> <count> makes that many small files there, to test a crowded folder
coredump get (Debug Console only) send the raw core dump: use scripts/rdbg.py coredump
reset (Debug Console only) restart at once, even if the main loop is stuck
get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary, see rdbg.py
get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary, see rdbg.py: there, a bare `screenshot` sends the screen instead of saving it
quit close the Debug Console connection
```
@@ -84,7 +85,8 @@ In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few r
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
| `log level <0-5>` | ESP-IDF log level |
| `ls [folder]` / `du <path>` | Lists a folder of the SD card with sizes and dates, or counts the files and bytes under a path |
| `cp [-f] <from> <to>` / `mv [-f] <from> <to>` / `rm <path>` / `mkdir <path>` / `cancel` | What the Storage App does, with its rules: copy (folders too), move or rename, delete (a folder with what's in it), new folder. `-f` replaces a file that's in the way; a tab separates paths that hold spaces; `cancel` stops a copy or a delete |
| `screenshot [seconds]` | The screen as a PNG in `/screenshots` on the card, now or after a pause to get to the screen you want (240 x 135, about 33 KB). Over the Debug Console a bare `screenshot` sends the screen to the PC instead |
| `cp [-f] <from> <to>` / `mv [-f] <from> <to>` / `rm [-f] <path>` / `mkdir <path>` / `cancel` | What the Storage App does, with its rules: copy (folders too), move or rename, delete (a folder with what's in it), new folder. `-f` replaces a file that's in the way; a tab separates paths that hold spaces; `cancel` stops a copy or a delete |
| `install <path>` | Update from SD with that `.ota` file, as Settings → Firmware does |
| `update check` / `update list` / `update status` / `update install <tag>` | The project's releases on Gitea: look at the latest, list the last ten, say what's known, or download and install one |
| `update pretend <version>` / `update probe <host>` / `update damage cut\|flip <n>` / `update daily` | Pretend to run another version (so a release counts as an update), see whether a server's certificate is accepted, cut or damage the next download, run the daily check again |
@@ -45,6 +45,7 @@ The device sends `screenshot: rgb332 <width> <height>` and then **one byte per p
- It is read **as it stands**, while the UI may be drawing, so it can **tear**. It is for looking at, not for pixel-exact comparison.
- It is the real thing: the screenshots on this site, in the [user guide](/guide/) and the [devlog](/devlog/), were taken this way.
- **With a number, it saves to the card instead:** `screenshot 5` (or `screenshot 0`) writes a PNG to `/screenshots` on the SD card after that many seconds, as the [Shell](/guide/shell/) does. A bare `screenshot` over the console is the binary one above.
- Its main use is in a loop: send a key, wait a moment, take a screenshot, look. See [Drive the UI](/dev/debug/drive-the-ui/).
## `coredump get`: the crash dump
+41
View File
@@ -144,3 +144,44 @@ M3 found the LoRa radio's noise floor about 15 dB above what the chip hears alon
**Settings > Pause GNSS for LoRa**, off by default: while the LoRa radio listens or sweeps, the GNSS receiver waits in standby, and wakes when the radio goes back to sleep (a Fix again after about 7 s here). Never during a Track. The GNSS App says "GNSS is paused" meanwhile. `gnss quiet on|off` on the console.
It's off by default because GNSS on by default was decided in M2 (Q58), and from M4 the radio listens all the time: then "pause while listening" means GNSS mostly off, which is a decision about position, the clock and Tracks, for M4's design round (issue #23).
## The Shell (issue #67)
The console's commands could only be typed on a PC: over USB, or over Wi-Fi with the Debug Console. A device in a bag, or on a network that is down, could not be asked anything. The Shell is an App that runs the same commands on the device's own screen and keyboard.
### Decisions (design round 2026-10-07)
| # | Decision |
|---|---|
| Q204 | **An App, "Shell", in the Launcher**, in every firmware: its commands already work over USB for anyone holding the device. |
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
| Q206 | **It shows what the console prints while it is open**, replies and background alike, without the free-heap line every ten seconds. **Ctrl+b hides the background:** then only what is printed in the ten seconds after a command is kept. A reply can't be told from other output any better: `ls` and `tasks` answer later, from other tasks. |
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
| Q209 | **`rm` asks first, `rm -f` doesn't** (in the Shell only: over the consoles, scripts delete as before). **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
| Q212 | Its keys are a table in `app_keys.h`, so the help panel and the website have them; a page in the user guide. |
### As built
- **`ShellApp`** (`src/apps/shell_app.cpp`), with its model host-tested in `lib/apps_model/src/shell_log.h`: lines arriving in pieces, the two filters, the 4 KB limit, the command words out of the `help` text, and Tab (7 tests).
- **The console has a second ring** (`Console::openShellRing`), filled like the Debug Console's and independent of it.
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
- **Cost:** 12 KB of flash, 16 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
### Checks on the device (2026-10-07, driven over the Debug Console with `key`)
| Check | Result |
|---|---|
| Open it from the Launcher, type `ls /`, Enter | The folders are listed |
| Tab on `in` | `info install` is shown and the line stays; on `u` it becomes `update `; on `l`, `log ls lora loop` |
| Up | The line before comes back |
| `screenshot` | `/screenshots/20261007-104357.png`, 33,383 bytes. Fetched and decoded on the PC: 240 x 135, indexed, every chunk's CRC right, the pixels the Shell's screen |
| `rm /shelltest` | Asks. Cancel leaves the folder; Delete removes it. `rm -f` removes without asking |
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
| `quit` | Back to the Launcher, and the memory comes back |
| The help panel in the Shell | Its keys, then the ones that work everywhere |
**Not checked:** Ctrl+b and the Alt scroll, which `key` can't press (no modifiers): the filter itself is host-tested, the key that flips it is not; the real keyboard altogether; and the Toast after a screenshot, which was published but not looked at.
+2 -2
View File
@@ -68,9 +68,9 @@ On a new device a short Setup asks four things, then never appears again. It als
## The SD card
Put a microSD card in the Cardputer. Without one the radio, GNSS, Wi-Fi and IRC still work, but nothing can be saved: no notes, IRC logs, Wi-Fi scan logs, GPX tracks, LoRa captures or saved Gemini pages. The [Storage App](/guide/storage/) shows what is on the card.
Put a microSD card in the Cardputer. Without one the radio, GNSS, Wi-Fi and IRC still work, but nothing can be saved: no notes, IRC logs, Wi-Fi scan logs, GPX tracks, LoRa captures, screenshots or saved Gemini pages. The [Storage App](/guide/storage/) shows what is on the card.
The firmware keeps its own folders at the top of the card (`captures`, `gemini`, `gnss`, `irc`, `updates`, `wifi`, plus `notes`). You can use the card in a computer too, but those names are the firmware's.
The firmware keeps its own folders at the top of the card (`captures`, `gemini`, `gnss`, `irc`, `notes`, `screenshots`, `updates`, `wifi`). You can use the card in a computer too, but those names are the firmware's.
## The keys, as the device lists them
+1 -1
View File
@@ -1,7 +1,7 @@
+++
title = "Every key"
description = "The keys of every screen of the firmware, as the help panel lists them on the device: one table for each screen and state."
weight = 12
weight = 13
[extra]
tag = "Reference"
+++
+1 -1
View File
@@ -1,7 +1,7 @@
+++
title = "Settings"
description = "The device's names, region, screen, sound, GNSS and Wi-Fi, and where firmware updates are found."
weight = 10
weight = 11
[extra]
tag = "Settings"
+++
+58
View File
@@ -0,0 +1,58 @@
+++
title = "Shell"
description = "The firmware's own commands, typed on the device: look at its state, the SD card, the radio and the network with no PC and no cable."
weight = 9
[extra]
tag = "Shell"
+++
The firmware has a set of **commands**, made for working on it from a PC. The Shell runs them **on the device itself**: no computer, no cable, no Wi-Fi. It is the tool for the day something is wrong and you are nowhere near a desk.
It can do real damage: `rm` deletes, `reboot` restarts, `debug on` opens the device to the network. It is the same trust as holding the device, and nothing more.
## Using it
Type a command and press <kbd>Enter</kbd>. `help` lists them all; the [command reference](/dev/debug/commands/) says what each does. A few to start with:
| Command | Shows |
|---|---|
| `info` | The firmware's version, uptime, memory, Wi-Fi, the SD card and both firmware slots |
| `wifi status` | The network, the address, and where the DNS and time servers came from |
| `ls /notes` | A folder of the SD card, with sizes and dates |
| `crash` | The last crash, if there was one |
| `update check` | Whether a newer release exists |
| `lora status` | What the radio is set to and what it has heard |
- <kbd>Tab</kbd> **completes** the command's name. If several fit, it lists them.
- <kbd>Fn</kbd> with up and down brings back **lines you typed before**.
- <kbd>Alt</kbd> with up and down **scrolls back** through what was printed.
- `clear` empties the screen, and `quit` (or Back) leaves.
## What you see
The Shell shows **everything the firmware prints** while it is open: the answers to your commands, and whatever the rest of the system says meanwhile (IRC connecting, a packet received). On a ten-line screen that can be a lot.
<kbd>Ctrl</kbd> + <kbd>b</kbd> hides the background: then only what is printed in the **ten seconds after each of your commands** is kept, and `mine` shows in the corner. It is ten seconds and not "the answer" because some commands answer a moment later, from another part of the firmware, and nothing marks their lines as theirs.
## Deleting asks first
`rm <path>` asks before it deletes, here where a slip of the finger is one key away. `rm -f <path>` does not ask.
## Screenshots
```
screenshot the screen, now
screenshot 5 the screen in 5 seconds: time to go to another App
```
The picture is saved as a PNG in `/screenshots` on the SD card, named by date and time, and a Toast says so once it is written (so the Toast is never in the picture). From the Shell, "now" is always a picture of the Shell: use the pause to get to the screen you want. The [Storage App](/guide/storage/) shows the files; to look at them, take the card to a computer.
## What it costs
Nothing while it is closed. Open, about 7 KB of memory, given back when you leave: with IRC connected and a Gemini page open, that can be the difference (see [the memory limit](/howto/not-enough-memory/)).
## The keys, as the device lists them
What <kbd>Fn</kbd> + <kbd>h</kbd> shows on this screen. This table is generated from the firmware's own lists, so it is always the current one.
{{ keys(scopes=["shell"]) }}
+1 -1
View File
@@ -1,7 +1,7 @@
+++
title = "System"
description = "What the device is doing right now: load, tasks, memory, network traffic, battery and temperature. Live, and read-only."
weight = 9
weight = 10
[extra]
tag = "System"
screens = ["system.png"]
+1 -1
View File
@@ -1,7 +1,7 @@
+++
title = "Updates"
description = "How the device updates itself from the project's releases, from the SD card or from a PC, and how it protects itself when an update goes wrong."
weight = 11
weight = 12
[extra]
tag = "Firmware"
screens = ["update.png"]
+1
View File
@@ -19,6 +19,7 @@ Everything the firmware writes goes in a folder at the top of the card. Switch t
| Gemini bookmarks | `/gemini/bookmarks.gmi` | gemtext |
| Files saved from Gemini that are not text | `/gemini/downloads` | whatever they were |
| Update files | `/updates` | `.ota` |
| Screenshots (the Shell's `screenshot`) | `/screenshots` | `.png`, named by date and time |
## Rules worth knowing
+17
View File
@@ -375,6 +375,23 @@ rows = [
["Fn , /", "move the cursor"],
]
[[scope]]
id = "shell"
title = "Shell"
rows = [
["Enter", "run the line"],
["Tab", "complete the command"],
["Fn ; .", "lines you typed before"],
["Alt ; .", "scroll back, forward"],
["Ctrl b", "all output, or only yours"],
["Fn , /", "move the cursor"],
["Del", "delete backwards"],
["help", "every command"],
["clear", "an empty screen"],
["rm -f", "delete without being asked"],
["quit `", "leave the Shell"],
]
[[scope]]
id = "system"
title = "System, any view"
+1 -1
View File
@@ -68,7 +68,7 @@
</article>
{% endfor %}
</div>
<p class="cards-note">Plus Settings, with its Wi-Fi and Firmware pages. Every App has a page in the <a class="accent-link" href="/guide/">user guide</a>.</p>
<p class="cards-note">Plus a Shell that runs the firmware's commands on the device, and Settings, with its Wi-Fi and Firmware pages. Every App has a page in the <a class="accent-link" href="/guide/">user guide</a>.</p>
</section>
<section class="wrap" id="screens" aria-labelledby="screens-title">