Files
roro9stack/README.md
T
twislaandClaude Opus 5.5 acf7697bdc S1 #7 step 4: fixed IPv4, DNS and NTP in Settings
Enter on a Saved Network opens its page instead of asking to forget it:
"IP address" switches between Automatic and Fixed, with an address, a
prefix and an optional gateway. Fixed starts from what the network is
giving the device; the draft is checked and applied on leaving the page,
so a half-typed address is never used. "DNS and NTP" holds the two DNS
servers, "Always use my DNS" and the two NTP servers. Enter on Status
shows the connection's details and where each value came from. Address
fields take digits and dots only; refusals show as Toasts.

Checked on the device through the screens: Fixed 10.39.39.13 applied and
reverted to Automatic, a prefix of 99 refused. Measurements in
docs/milestones/S1.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 00:04:53 +02:00

11 KiB
Raw Blame History

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.

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)

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:

    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:

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:

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.

Networks without DHCP

Each Saved Network gets its address automatically (DHCP) or has a Fixed one (docs/milestones/S1.md): in Settings > Wi-Fi, Enter on a network opens its page, where "IP address" switches between Automatic and Fixed, with an address, a prefix length (24 is 255.255.255.0) and an optional gateway. Switching to Fixed starts from what the network is giving the device at that moment. The setting is checked and applied when you leave the page. IPv4 only.

"DNS and NTP" on the same screen holds two DNS servers (9.9.9.9 and 1.1.1.1 by default), used on Fixed networks, or on every network with "Always use my DNS"; and two NTP servers (pool.ntp.org and time.cloudflare.com), used after any the network's DHCP offers. Enter on "Status" shows what's in use and where each value came from.

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 <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)
wifi ip <ssid> dhcp / wifi ip <ssid> <address>/<prefix> [gateway] A Saved Network's IP setting: Automatic, or Fixed. Debug Builds: add try <seconds> to go back to the previous setting unless wifi ip keep follows
wifi dns <a> [b] / wifi dns always on|off / wifi ntp <a> [b] DNS servers (used on Fixed networks, or always), and NTP servers
log <text> Appends a line to a test IRC Log (/irc/dev/#test/<date>.log)
sd card What the SD card says it is: type, size, and its identity register (maker, name, revision, serial, date)
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, then the address, gateway, DNS and NTP servers in use and where each came from
info Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, 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 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 <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:

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:

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.