Public Access
Everything touching the network or the card is a job on the Gemini task (a missing card can block 5 s, past the main loop's watchdog): about:start is composed from /gemini/bookmarks.gmi and the Saved Pages (by capsule, newest first); file:// opens a Saved Page; s, S, r, d, b and downloads report one line to the URL bar, S with progress Toasts. A Saved Page is the cached body with a first line "> Saved from <url> on <date>": it shows as a quote and gives relative links their base; inside one, links to other Saved Pages open the saved copy, others go online or say "Not saved, and offline". Input prompts (11 masked) request the same URL with the answer as its query. Fixed on the way: re-wrapping indented lines rebuilt the text from its rows, inserting a space where a long word had been cut; refreshing loaded the whole Saved Page next to a TLS connection (heap down to 436 bytes), now it reads one line and every fetch checks the 55 KB floor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
123 lines
7.7 KiB
Markdown
123 lines
7.7 KiB
Markdown
# roro9stack
|
|
|
|
A multi-app firmware for the **M5Stack Cardputer ADV** with the **Cap LoRa-1262**. It's a Meshtastic-compatible mesh messenger, plus Wi-Fi tools, IRC, GNSS and more. Licensed GPL-3.0.
|
|
|
|
- Domain language: [CONTEXT.md](CONTEXT.md)
|
|
- Decisions: [docs/adr/](docs/adr/)
|
|
- Milestones: [docs/milestones/](docs/milestones/)
|
|
|
|
## Requirements
|
|
|
|
Only **Docker** is needed. PlatformIO and the ESP32 toolchain run inside a container, and are cached in the `roro9stack-pio` Docker volume. The first build downloads about 1 GB and takes a few minutes.
|
|
|
|
## Build and test (local CI)
|
|
|
|
```sh
|
|
scripts/ci.sh
|
|
```
|
|
|
|
This runs the host-side unit tests (`test/`, `native` environment), then builds the firmware. The output is `.pio/build/cardputer-adv/firmware.factory.bin`.
|
|
|
|
The framework is rebuilt with the TLS settings in `platformio.ini` (`custom_sdkconfig`, ADR 0006), so the first build after a fresh checkout takes about 4 minutes; later builds take under a minute.
|
|
|
|
## Flash
|
|
|
|
1. Connect the Cardputer by USB-C.
|
|
2. Run:
|
|
|
|
```sh
|
|
scripts/flash.sh # auto-detects the port; or: scripts/flash.sh /dev/ttyACM1
|
|
```
|
|
|
|
This uploads the firmware, then opens the serial monitor. Quit the monitor with `Ctrl+C`.
|
|
|
|
**If the upload can't connect,** put the device in download mode: hold **G0** (the button next to the screen) while plugging in USB, or while pressing reset. Then retry.
|
|
|
|
**If you get "permission denied" on the port,** your user needs access to the serial device. Run this once, then log out and back in:
|
|
|
|
```sh
|
|
sudo usermod -aG dialout "$USER"
|
|
```
|
|
|
|
## Firmware Updates over Wi-Fi (OTA)
|
|
|
|
Once the Cardputer runs an OTA-capable firmware (flashed once over USB), updates can go over Wi-Fi:
|
|
|
|
```sh
|
|
scripts/ota_keygen.sh # once: creates the signing key (see ADR 0003)
|
|
scripts/flash.sh --ota 10.39.39.12 # build, sign and push; or set RORO_OTA_HOST
|
|
```
|
|
|
|
The device shows the push address in **Settings → Firmware**. It installs a correctly signed update right away, restarts (waiting up to 60 s if you're typing), and runs the new firmware on **Probation**. If the new firmware crashes, or can't reconnect Wi-Fi within 3 minutes, it rolls back to the previous one and says so.
|
|
|
|
To install from the SD card instead, copy the `.ota` file from `.pio/build/cardputer-adv/` into `/updates` on the card, then use **Settings → Firmware**. With the Cardputer on USB, the card can stay in: `scripts/sd_put.sh <file.ota>` sends it over the serial console into `/updates` (about 30 s for 1.6 MB, checked with SHA-256 before it's renamed into place; `SD_PUT_DEBUG=1` shows the console while it runs).
|
|
|
|
**The private key** lives in `~/.config/roro9stack/ota-key.pem` and must never be committed. If it's lost, generate a new pair and flash once over USB.
|
|
|
|
## Gemini
|
|
|
|
The Gemini App browses Geminispace (docs/milestones/G1.md): Tab and Shift+Tab pick a link, Enter follows it, Back returns (to where the page was scrolled), Space pages down, `g` types an address. Certificates are trusted on first use; a changed one stops the page and asks.
|
|
|
|
On a page, `b` bookmarks it, `s` saves it to the SD card to read offline (a non-text file goes to `/gemini/downloads/`), `S` saves it with the pages it links to on the same capsule (up to 30). The start page lists bookmarks and Saved Pages; inside a Saved Page, `r` refreshes it and `d` deletes it. With a card, every page streams through `/gemini/cache/` so a large one arrives whole even with IRC connected; what doesn't fit in memory stays on the card.
|
|
|
|
## Development aids
|
|
|
|
`scripts/serial_log.sh [seconds] [command…]` records the serial output, and can send commands to the firmware first. For example, `scripts/serial_log.sh 30 short sleep:12 burst` sets short screen timeouts, waits 12 s, then sends a burst of Toasts.
|
|
|
|
| Command | Effect |
|
|
|---|---|
|
|
| `burst` | Publishes 5 Notifications at once |
|
|
| `key up\|down\|left\|right\|select\|back\|home`, or `key <char>` | Injects a key press |
|
|
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
|
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
|
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
|
| `log <text>` | Appends a line to a test IRC Log (`/irc/dev/#test/<date>.log`) |
|
|
| `sd list` | Lists the files of each Storage Clean-up category |
|
|
| `cat <path>` | Prints the first ~1.2 KB of a file on the SD card |
|
|
| `irc start` | Starts the IRC Service (normally done by opening the IRC App) |
|
|
| `irc stop` | Stops it, as `/quit` does: QUIT if connected, no more retries, and the App stays disconnected until you type |
|
|
| `gemini get <url>` | Fetches a Gemini page and prints its header, size, certificate fingerprint and heap use |
|
|
| `gemini trust <host> <port> <sha256>` | Pins a certificate by hand (the Gemini App asks when one changes) |
|
|
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
|
|
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer |
|
|
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap |
|
|
| `info` | Firmware, uptime, last start reason, memory, Wi-Fi, and both app slots with their versions and OTA states |
|
|
| `tasks` | FreeRTOS tasks: state, priority, lowest free stack, CPU share |
|
|
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
|
|
| `log level <0-5>` | ESP-IDF log level |
|
|
| `ls [folder]` / `rm <path>` | Lists a folder of the SD card, or deletes a file |
|
|
| `install <path>` | Update from SD with that `.ota` file, as Settings → Firmware does |
|
|
| `crash` | The last crash: which firmware, why, task, PC and backtrace (from the core dump in flash) |
|
|
| `coredump erase` | Forgets the core dump |
|
|
| `crash abort` / `crash wdt` | Debug Builds: crash on purpose, or hang the main loop until the watchdog fires |
|
|
| `help` | Lists the commands |
|
|
|
|
`scripts/flash.sh` stops a running serial log first, since it would hold the port.
|
|
|
|
### Debug Builds and the Debug Console
|
|
|
|
`scripts/flash.sh --debug` (USB) or `scripts/flash.sh --debug --ota <ip>` (Wi-Fi) installs a Debug Build: the same firmware plus the Debug Console on TCP 2323 (ADR 0004). Then, with `RORO_OTA_HOST` set to the device's IP:
|
|
|
|
```sh
|
|
scripts/rdbg.py # interactive: the console backlog, live lines, and commands
|
|
scripts/rdbg.py info # one command and its reply
|
|
scripts/rdbg.py -b tasks # the same, after the backlog (boot messages and so on)
|
|
```
|
|
|
|
Every command above works there too, plus a few handled by the PC side or the console's own task:
|
|
|
|
```sh
|
|
scripts/rdbg.py crash # the last crash, its backtrace decoded against that exact build's ELF
|
|
scripts/rdbg.py coredump # fetch the core dump and decode it all (registers, every task) with esp-coredump
|
|
scripts/rdbg.py reset # restart at once, even if the main loop is stuck
|
|
scripts/rdbg.py screenshot # the screen as a PNG (2x)
|
|
scripts/rdbg.py put <file> [card path] # to the SD card (default /updates/<name>), SHA-256 checked, ~300 KB/s
|
|
scripts/rdbg.py get <card path> [file] # from the SD card
|
|
```
|
|
|
|
So a Firmware Update can also go `rdbg.py put roro9stack-….ota` then `rdbg.py install /updates/roro9stack-….ota`: the Update from SD path, without touching the device.
|
|
|
|
Every build keeps its ELF in `.pio/elves/` (version and digest in the name) for that; `scripts/decode_backtrace.sh <version|digest> <addresses>` decodes any backtrace by hand.
|
|
|
|
After 3 crash restarts in a row the firmware starts in **Safe Mode** (ADR 0005): only Wi-Fi, Firmware Updates and the Debug Console, so a fix can be pushed as usual. `reboot` leaves it. The token is in `~/.config/roro9stack/debug-token`, made by the first build; keep developing on Debug Builds, so the firmware a Rollback returns to always has the console.
|