# 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 ` 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. Tab switches to **Sweep**: the signal strength across 863–870 MHz in 100 kHz steps, as bars with peak hold and a waterfall, with the Sniffer's frequency marked; the Sniffer is paused meanwhile and picks up where it was. The Status Bar shows `L` while the radio listens (bright for a moment on each packet), `SW` while sweeping, 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 ` | 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 ` | Adds a Saved Network (so credentials stay out of the repo) | | `log ` | Appends a line to a test IRC Log (`/irc/dev/#test/.log`) | | `sd list` | Lists the files of each Storage Clean-up category | | `cat ` | 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 ` | Fetches a Gemini page and prints its header, size, certificate fingerprint and heap use | | `gemini trust ` | Pins a certificate by hand (the Gemini App asks when one changes) | | `irc say ` | 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 ` | Lists a folder of the SD card, or deletes a file | | `install ` | 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 ` / `lora custom [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 sweep on [from MHz] [to MHz] [step kHz]` / `lora sweep off` / `lora sweep dump` | Sweep a band (863 870 100 by default), with a summary every 2 s (floor, strongest, peaks), or print the latest pass | | `lora inject [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 ` (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 [card path] # to the SD card (default /updates/), SHA-256 checked, ~300 KB/s scripts/rdbg.py get [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 ` 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.