Public Access
One firmware: the Debug Console in every build, off until switched on, with the device's own token
There is no Debug Build any more (ADR 0010, issue #68, Q188 to Q195). The console and the test commands are compiled into every firmware. It listens only while Settings > Debug Console is on, which isn't the default; off, neither its task nor its 4 KB ring exists. The token is made by the device and shown on that page; a client proves it knows it by answering a challenge with an HMAC, so it never crosses the network, and five wrong answers close the console for a minute. DBG in the Status Bar while it listens. Over USB serial only: debug on, debug token <value>, debug token new. scripts/flash.sh --debug uses them to set a device up with the developer's token. scripts/rdbg.py takes the token from -t, $RORO_DEBUG_TOKEN or the file, answers the challenge, and fetches a release's ELF to decode a crash. Gone: the cardputer-adv-debug environment, RORO_DEBUG, the +debug version, scripts/debug_flags.py, update install ... force, and the rule that a Debug Build doesn't install releases. Old clients and old firmwares don't talk to each other. Against the builds it replaces: 30 KB more flash and 88 bytes more static RAM than the release, 4 KB less RAM than the Debug Build. 468 host tests. Checked on the device: off by default, login, the pause after wrong tokens, Safe Mode with the console, the setting surviving an update, debug off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
+3
-1
@@ -6,7 +6,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
[ -n "${RORO_NO_DOCKER:-}" ] || docker build -q -t "$IMAGE" "$ROOT/docker" >/dev/null
|
||||
|
||||
# The Debug Console token (ADR 0004): made once, kept with the OTA key, never committed.
|
||||
# The Debug Console token of the developer's device (ADR 0010): made once, kept with the OTA key, never
|
||||
# committed and never compiled in. scripts/flash.sh --debug gives it to a device over USB, and
|
||||
# scripts/rdbg.py answers the device's challenge with it.
|
||||
DEBUG_TOKEN_FILE="$HOME/.config/roro9stack/debug-token"
|
||||
if [ ! -s "$DEBUG_TOKEN_FILE" ]; then
|
||||
mkdir -p "$(dirname "$DEBUG_TOKEN_FILE")"
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ DOCKER_EXTRA=()
|
||||
|
||||
case "${1:-all}" in
|
||||
tests) STEPS='pio test -e native' ;;
|
||||
builds) STEPS='pio run -e cardputer-adv -e cardputer-adv-debug' ;;
|
||||
all) STEPS='pio test -e native && pio run -e cardputer-adv -e cardputer-adv-debug' ;;
|
||||
builds) STEPS='pio run -e cardputer-adv' ;;
|
||||
all) STEPS='pio test -e native && pio run -e cardputer-adv' ;;
|
||||
*) echo "Usage: scripts/ci.sh [tests|builds]" >&2; exit 1 ;;
|
||||
esac
|
||||
run_in_container bash -c 'git config --global --add safe.directory "$PWD" && '"$STEPS"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Debug Builds only: compiles in the Debug Console token from $RORO_DEBUG_TOKEN (set by _docker.sh
|
||||
# from ~/.config/roro9stack/debug-token). Refuses to build without one rather than use a default.
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
Import("env") # noqa: F821 (provided by PlatformIO)
|
||||
|
||||
token = os.environ.get("RORO_DEBUG_TOKEN", "").strip()
|
||||
if not re.fullmatch(r"[0-9a-f]{32}", token):
|
||||
sys.exit("debug build: RORO_DEBUG_TOKEN is missing; build through scripts/ci.sh or scripts/flash.sh --debug")
|
||||
env.Append(CPPDEFINES=[("RORO_DEBUG_TOKEN", '\\"%s\\"' % token)]) # noqa: F821
|
||||
+16
-7
@@ -1,17 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Flash the firmware over USB, then open the serial monitor.
|
||||
# Usage: scripts/flash.sh [--debug] [port] USB (default: the first Espressif device found)
|
||||
# scripts/flash.sh [--debug] --ota [host] Wi-Fi: build, sign and push a Firmware Update
|
||||
# (host: the device's IP from Settings > About, or $RORO_OTA_HOST)
|
||||
# --debug builds the Debug Build (cardputer-adv-debug): the Debug Console on TCP 2323, see ADR 0004.
|
||||
# Usage: scripts/flash.sh [--debug] [port] USB (default: the first Espressif device found)
|
||||
# scripts/flash.sh --ota [host] Wi-Fi: build, sign and push a Firmware Update
|
||||
# (host: the device's IP from Settings > Firmware, or $RORO_OTA_HOST)
|
||||
# --debug (USB only): once flashed, switches the Debug Console on and gives it the token in
|
||||
# ~/.config/roro9stack/debug-token, over the serial port (ADR 0010, Q192), so scripts/rdbg.py works
|
||||
# at once. The settings survive later updates: it is needed once for a device, not at each flash.
|
||||
set -euo pipefail
|
||||
ENV=cardputer-adv
|
||||
PROVISION=
|
||||
if [ "${1:-}" = "--debug" ]; then
|
||||
ENV=cardputer-adv-debug
|
||||
PROVISION=1
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--ota" ]; then
|
||||
[ -z "$PROVISION" ] || echo "flash.sh: --debug does nothing over Wi-Fi: the console's setting stays as it is on the device" >&2
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HOST="${2:-${RORO_OTA_HOST:-}}"
|
||||
[ -n "$HOST" ] || { echo "Usage: scripts/flash.sh --ota <device IP> (or set RORO_OTA_HOST)" >&2; exit 1; }
|
||||
@@ -19,7 +23,6 @@ if [ "${1:-}" = "--ota" ]; then
|
||||
DOCKER_EXTRA=()
|
||||
run_in_container bash -c "git config --global --add safe.directory /work && pio run -e $ENV" | tail -3
|
||||
VERSION="$(git -C "$ROOT" describe --tags --always --dirty)"
|
||||
[ "$ENV" = cardputer-adv-debug ] && VERSION="$VERSION+debug" # as scripts/version.py names it
|
||||
OUT="$ROOT/.pio/build/$ENV/roro9stack-$VERSION.ota"
|
||||
"$ROOT/scripts/make_ota.py" "$ROOT/.pio/build/$ENV/firmware.bin" "$VERSION" "$OUT"
|
||||
exec "$ROOT/scripts/ota_push.py" "$OUT" "$HOST"
|
||||
@@ -30,4 +33,10 @@ source "$(dirname "$0")/_docker.sh"
|
||||
docker rm -f roro9stack-serial >/dev/null 2>&1 || true # a serial log would hold the port
|
||||
DOCKER_EXTRA=(--device "$PORT" --group-add "$(stat -c %g "$PORT")" -it)
|
||||
|
||||
run_in_container bash -c "git config --global --add safe.directory /work && pio run -e $ENV -t upload --upload-port $PORT && pio device monitor -p $PORT -b 115200"
|
||||
# The token is read from the environment inside the container (_docker.sh passes it), so it is on no
|
||||
# command line of the host; serial_log.py prints what the device says, not what it sends.
|
||||
SETUP=""
|
||||
# serial_log.py finds the port by its name, and follows it when the device re-enumerates after the upload.
|
||||
[ -z "$PROVISION" ] || DOCKER_EXTRA=(--group-add "$(stat -c %g "$PORT")" --privileged -v /dev:/dev -it)
|
||||
[ -z "$PROVISION" ] || SETUP='&& /pio/penv/bin/python scripts/serial_log.py 8 sleep:4 "debug token $RORO_DEBUG_TOKEN" "debug on"'
|
||||
run_in_container bash -c "git config --global --add safe.directory /work && pio run -e $ENV -t upload --upload-port $PORT $SETUP && pio device monitor -p $PORT -b 115200"
|
||||
|
||||
+85
-12
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The Debug Console of a Debug Build, over Wi-Fi (TCP 2323, see ADR 0004).
|
||||
"""The Debug Console, over Wi-Fi (TCP 2323, see ADR 0010). Switch it on first: Settings > Debug Console.
|
||||
|
||||
Usage: scripts/rdbg.py [-H host] [-b] [command ...]
|
||||
Usage: scripts/rdbg.py [-H host] [-t token] [-b] [command ...]
|
||||
no command interactive: type commands, see every console line live; Ctrl-D or `quit` leaves
|
||||
command runs it and prints what follows, until the console has been quiet for a moment
|
||||
-H host the device's IP (Settings > Firmware), default $RORO_OTA_HOST
|
||||
-H host the device's IP (Settings > Debug Console), default $RORO_OTA_HOST
|
||||
-t token the device's token (also --token), as Settings > Debug Console shows it: dashes and
|
||||
case don't matter. Otherwise $RORO_DEBUG_TOKEN, otherwise ~/.config/roro9stack/debug-token
|
||||
-b also print the backlog the device sends on connecting (boot messages and so on)
|
||||
|
||||
Commands handled here as well as on the device:
|
||||
@@ -14,9 +16,11 @@ Commands handled here as well as on the device:
|
||||
put <file> [card path] copy a file to the SD card (default /updates/<name>), checked with SHA-256
|
||||
screenshot [file.png] what the screen shows (default screen-<date>.png), at 2x
|
||||
reset restart at once, even if the main loop is stuck
|
||||
The token is read from ~/.config/roro9stack/debug-token (made by the first build).
|
||||
The token never crosses the network: the device sends a challenge, and this answers with its HMAC.
|
||||
"""
|
||||
import gzip
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
@@ -26,9 +30,53 @@ import zlib
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
PORT = 2323
|
||||
TOKEN_FILE = os.path.expanduser("~/.config/roro9stack/debug-token")
|
||||
RELEASES = "https://git.twis.la/twisla/roro9stack/releases/download"
|
||||
|
||||
|
||||
def tidy_token(typed):
|
||||
"""As the device stores it (lib/debug/src/debug_auth.cpp): no dashes or spaces, in capitals."""
|
||||
tidy = "".join(c for c in typed if c not in "- \t\r\n").upper()
|
||||
return tidy.replace("O", "0").replace("I", "1").replace("L", "1") # read as the digits they look like
|
||||
|
||||
|
||||
def answer_for(token, nonce):
|
||||
"""What the device expects back for a challenge: HMAC-SHA256 of the nonce, keyed by the token."""
|
||||
return hmac.new(token.encode(), nonce, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def find_token(given):
|
||||
token = given or os.environ.get("RORO_DEBUG_TOKEN")
|
||||
if not token and os.path.exists(TOKEN_FILE):
|
||||
token = open(TOKEN_FILE).read()
|
||||
token = tidy_token(token or "")
|
||||
if not token:
|
||||
sys.exit("No token: pass -t <token>, set RORO_DEBUG_TOKEN, or put it in " + TOKEN_FILE +
|
||||
"\n(the device shows it in Settings > Debug Console)")
|
||||
return token
|
||||
|
||||
|
||||
def log_in(sock, token):
|
||||
"""Answers the device's challenge; returns the banner line, or exits saying what went wrong."""
|
||||
first = read_until(sock, b"\n", 10)
|
||||
if first is None:
|
||||
sys.exit("device: nothing came back. Is the console switched on (Settings > Debug Console)?")
|
||||
line = first.decode(errors="replace").strip()
|
||||
if line.startswith("locked"):
|
||||
sys.exit("device: closed for a minute after too many wrong tokens")
|
||||
challenge = re.search(r"challenge ([0-9a-f]{32})$", line)
|
||||
if not challenge:
|
||||
sys.exit("device: no challenge (an older firmware?): " + line[:60])
|
||||
sock.sendall((answer_for(token, bytes.fromhex(challenge.group(1))) + "\n").encode())
|
||||
banner = read_until(sock, b"\n", 15)
|
||||
if banner is None:
|
||||
sys.exit("device: no answer")
|
||||
if banner.startswith(b"denied"):
|
||||
sys.exit("device: wrong token (Settings > Debug Console shows the right one)")
|
||||
return banner
|
||||
|
||||
|
||||
def read_until(sock, marker, timeout):
|
||||
@@ -102,17 +150,40 @@ def crash_firmware(reply):
|
||||
return version.group(1) if version else None
|
||||
|
||||
|
||||
def have_elf(reply):
|
||||
"""Makes sure .pio/elves/ holds the ELF of the firmware that crashed: a release's is on Gitea."""
|
||||
folder = os.path.join(os.path.dirname(SCRIPTS), ".pio", "elves")
|
||||
key = crash_firmware(reply)
|
||||
version = re.search(r"last one in (\S+)", reply)
|
||||
version = version.group(1) if version else None
|
||||
names = os.listdir(folder) if os.path.isdir(folder) else []
|
||||
if not key or any(key in n for n in names) or not version or not re.fullmatch(r"v\d+\.\d+\.\d+", version):
|
||||
return # there already, or not a release: only tags are published
|
||||
url = f"{RELEASES}/{version}/roro9stack-{version}.elf.gz"
|
||||
try:
|
||||
elf = gzip.decompress(urllib.request.urlopen(url, timeout=60).read())
|
||||
except Exception as e:
|
||||
return print(f"(no ELF for {version} here, and none fetched from {url}: {e})")
|
||||
digest = hashlib.sha256(elf).hexdigest()[:16]
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
path = os.path.join(folder, f"{version}.{digest}.elf") # as scripts/version.py names them
|
||||
open(path, "wb").write(elf)
|
||||
print(f"(fetched the ELF of {version} from its release: {os.path.relpath(path)})")
|
||||
|
||||
|
||||
def crash(sock):
|
||||
reply = run(sock, "crash")
|
||||
trace = re.search(r"backtrace((?: 0x[0-9a-f]+)+)", reply)
|
||||
key = crash_firmware(reply)
|
||||
if trace and key:
|
||||
have_elf(reply)
|
||||
print()
|
||||
subprocess.call([os.path.join(SCRIPTS, "decode_backtrace.sh"), key] + trace.group(1).split())
|
||||
|
||||
|
||||
def coredump(sock, path):
|
||||
info = run(sock, "crash", out=None)
|
||||
have_elf(info)
|
||||
sock.sendall(b"coredump get\n")
|
||||
# One buffer throughout: the header, the size and the first bytes often share a packet.
|
||||
buf = b""
|
||||
@@ -237,25 +308,27 @@ def interactive(sock):
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
host, backlog = os.environ.get("RORO_OTA_HOST"), False
|
||||
host, backlog, token = os.environ.get("RORO_OTA_HOST"), False, None
|
||||
while args and args[0].startswith("-"):
|
||||
flag = args.pop(0)
|
||||
if flag == "-H" and args:
|
||||
host = args.pop(0)
|
||||
elif flag in ("-t", "--token") and args:
|
||||
token = args.pop(0)
|
||||
elif flag == "-b":
|
||||
backlog = True
|
||||
else:
|
||||
sys.exit(__doc__)
|
||||
if not host:
|
||||
sys.exit("No device: pass -H <ip> or set RORO_OTA_HOST\n\n" + __doc__)
|
||||
token = open(TOKEN_FILE).read().strip()
|
||||
token = find_token(token)
|
||||
|
||||
with socket.create_connection((host, PORT), timeout=10) as sock:
|
||||
sock.sendall((token + "\n").encode())
|
||||
# The device may still be finishing a previous client: wait for this connection's banner.
|
||||
banner = read_until(sock, b"Backlog follows.\n", 15)
|
||||
if banner is None:
|
||||
sys.exit("device: no banner (wrong token, or another client is connected)")
|
||||
try:
|
||||
sock = socket.create_connection((host, PORT), timeout=10)
|
||||
except OSError as e:
|
||||
sys.exit(f"device: can't connect to {host}:{PORT} ({e}). Is the console switched on (Settings > Debug Console)?")
|
||||
with sock:
|
||||
banner = log_in(sock, token)
|
||||
show = sys.stdout if backlog or not args else None
|
||||
if show:
|
||||
show.write(banner.decode(errors="replace"))
|
||||
|
||||
@@ -10,9 +10,6 @@ try:
|
||||
except Exception:
|
||||
version = "unknown"
|
||||
|
||||
if env["PIOENV"].endswith("-debug"): # noqa: F821
|
||||
version += "+debug" # a Debug Build says so wherever the version shows
|
||||
|
||||
env.Append(CPPDEFINES=[("RORO_VERSION", '\\"%s\\"' % version)]) # noqa: F821
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user