Public Access
The Sniffer lists packets newest first (time, RSSI, SNR, and for Meshtastic the sender, receiver and hops), with the clear header and a hex dump on Enter (Q96); `p` picks an EU868 preset, kept in Settings (Q95). `c` starts a Capture: pcap with LoRaTap in /captures/lora, its own Clean-up category, recorded by a small Service so it carries on with the App closed (Q97, Q100). The Status Bar shows L while listening, bright on each packet, and CAP while capturing (Q101). StorageService gains raw appends for binary files. Debug Builds get `lora inject` to test all of this with no transmitter in range: a Capture made on the device reads back in TShark field for field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
133 lines
9.1 KiB
Markdown
133 lines
9.1 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.
|
|
|
|
## LoRa Scanner
|
|
|
|
The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **never transmits**. The Sniffer lists what it hears, newest first: time, RSSI, SNR, and for Meshtastic packets the sender and receiver (their last 4 hex digits) and hops. Enter shows a packet's details: the Meshtastic header (which is never encrypted) and a hex dump. `p` picks one of the 7 Meshtastic presets allowed in EU868 (LongFast by default), `c` starts or stops a Capture: a pcap file with LoRaTap headers in `/captures/lora/`, for Wireshark. A Capture keeps recording with the App closed; otherwise the radio sleeps when the App isn't open. The Status Bar shows `L` while the radio listens (bright for a moment on each packet) and `CAP` while capturing.
|
|
|
|
## 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 |
|
|
| `lora probe` | Finds the radio: chip, oscillator, antenna switch, DIO1 interrupt, noise floor |
|
|
| `lora status` | Radio settings, who's listening, packet and error counters, noise floor, task stack |
|
|
| `lora rx on` / `lora rx off` | Listens and prints each packet on the console |
|
|
| `lora preset <name>` / `lora custom <MHz> <BW kHz> <SF> <CR> <sync hex> [preamble]` | Receive settings: a Meshtastic preset, or anything else (`lora custom 868.1 125 7 5 34 8` for LoRaWAN) |
|
|
| `lora capture start` / `lora capture stop` | A LoRa Capture, as `c` in the App |
|
|
| `lora inject <hex> [rssi] [snr]` | Debug Builds: a packet into the Scanner as if received (nothing is sent) |
|
|
| `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.
|