diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 50d1f2f..d3ad111 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -1,2 +1,3 @@ {"id":"int-691bec44","kind":"field_change","created_at":"2026-04-15T15:26:14.033393505Z","actor":"w1n5t0n","issue_id":"meml-kx6a","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} {"id":"int-55a0a37d","kind":"field_change","created_at":"2026-04-15T15:32:17.624534929Z","actor":"w1n5t0n","issue_id":"meml-kx6a","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added firmware helper scripts and updated docs to the RP2350/C++20/O3 build flow"}} +{"id":"int-c4271206","kind":"field_change","created_at":"2026-04-15T15:35:23.153751063Z","actor":"w1n5t0n","issue_id":"meml-efb2","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} diff --git a/CLAUDE.md b/CLAUDE.md index 48220aa..cfb2d04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,6 +244,9 @@ git submodule update --init --recursive # Build only scripts/build-firmware.sh +# Build a specific variant +scripts/build-firmware.sh memlcelium + # Flash a previously-built UF2 scripts/flash-firmware.sh @@ -252,6 +255,7 @@ scripts/build-and-flash-firmware.sh ``` The scripts build for `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3` and force C++20 via `compiler.cpp.extra_flags=-std=gnu++20`. +If no variant is passed to `build-firmware.sh` in an interactive shell, it parses the available `MEMLNautMode*` options from `MEMLNaut-NISPS.ino`, prompts for one, and rewrites the active `MEMLNAUT_MODE_TYPE` before compiling. ## Architecture diff --git a/MAP.md b/MAP.md index 5e69e7f..1c5c8bb 100644 --- a/MAP.md +++ b/MAP.md @@ -53,7 +53,7 @@ MEMLNaut-NISPS: Neural Interactive Shaping of Parameter Spaces. Two living artef - `README.md` — short quickstart. ## Entry points -- **Firmware**: `scripts/build-firmware.sh`, `scripts/flash-firmware.sh`, or `scripts/build-and-flash-firmware.sh` (requires submodules initialised). The scripts target `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3` and force C++20. Execution = `setup()`/`loop()` on Core 0, `setup1()`/`loop1()` on Core 1, audio ISR on Core 1. +- **Firmware**: `scripts/build-firmware.sh`, `scripts/flash-firmware.sh`, or `scripts/build-and-flash-firmware.sh` (requires submodules initialised). `build-firmware.sh` can take an explicit variant name like `memlcelium` or prompt interactively from the parsed `MEMLNautMode*` list and rewrite the active mode in `MEMLNaut-NISPS.ino`. The scripts target `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3` and force C++20. Execution = `setup()`/`loop()` on Core 0, `setup1()`/`loop1()` on Core 1, audio ISR on Core 1. - **Playground**: `cd playground && python3 -m http.server` (or `serve.sh` / `serve-coop.py`), open `a-immersive.html`. Append `?debug=1` to expose `window.__nisps`. - **WASM rebuild**: `cd playground/wasm && ./build.sh` (needs `emcc`). - **Tests**: `npx playwright test` (auto-spawns server on 7331). diff --git a/README.md b/README.md index 50a26f9..88e1913 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ scripts/build-and-flash-firmware.sh Notes: - The scripts build for `rp2040:rp2040:solderparty_rp2350_stamp_xl` with `Optimize3`. - The build forces C++20 because the firmware uses `std::span` and concepts. +- `build-firmware.sh` accepts an optional variant name such as `memlcelium` or `breakor`. If you omit it in an interactive shell, the script parses `MEMLNaut-NISPS.ino`, prompts for a variant, and rewrites the active `MEMLNAUT_MODE_TYPE` before building. - `flash-firmware.sh` accepts an optional mountpoint argument, or auto-detects common UF2 bootloader mounts such as `/run/media/$USER/RP2350` and `/run/media/$USER/RPI-RP2`. ## Web Playground diff --git a/scripts/build-and-flash-firmware.sh b/scripts/build-and-flash-firmware.sh index 443e8da..831569c 100755 --- a/scripts/build-and-flash-firmware.sh +++ b/scripts/build-and-flash-firmware.sh @@ -7,7 +7,8 @@ usage() { cat <<'EOF' Usage: scripts/build-and-flash-firmware.sh - scripts/build-and-flash-firmware.sh [mountpoint] + scripts/build-and-flash-firmware.sh [variant] + scripts/build-and-flash-firmware.sh --variant VARIANT [mountpoint] If no mountpoint is provided, the flash step auto-detects a standard UF2 bootloader location such as /run/media/$USER/RP2350 or /run/media/$USER/RPI-RP2. @@ -19,21 +20,48 @@ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then exit 0 fi -case $# in - 0) - ;; - 1) - ;; - *) - usage >&2 - exit 1 - ;; -esac +source "$SCRIPT_DIR/firmware-common.sh" +load_firmware_variants -"$SCRIPT_DIR/build-firmware.sh" +variant_arg="${MEMLNAUT_FIRMWARE_VARIANT:-}" +mountpoint="" -if [[ $# -eq 1 ]]; then - "$SCRIPT_DIR/flash-firmware.sh" "$1" +while [[ $# -gt 0 ]]; do + case "$1" in + --variant) + if [[ $# -lt 2 ]]; then + echo "error: --variant requires a value" >&2 + exit 1 + fi + variant_arg="$2" + shift 2 + ;; + --variant=*) + variant_arg="${1#*=}" + shift + ;; + *) + if [[ -z "$variant_arg" ]] && resolve_firmware_variant "$1" >/dev/null 2>&1; then + variant_arg="$1" + elif [[ -z "$mountpoint" ]]; then + mountpoint="$1" + else + usage >&2 + exit 1 + fi + shift + ;; + esac +done + +if [[ -n "$variant_arg" ]]; then + "$SCRIPT_DIR/build-firmware.sh" --variant "$variant_arg" +else + "$SCRIPT_DIR/build-firmware.sh" +fi + +if [[ -n "$mountpoint" ]]; then + "$SCRIPT_DIR/flash-firmware.sh" "$mountpoint" else "$SCRIPT_DIR/flash-firmware.sh" fi diff --git a/scripts/build-firmware.sh b/scripts/build-firmware.sh index 4200090..af7e35d 100755 --- a/scripts/build-firmware.sh +++ b/scripts/build-firmware.sh @@ -8,9 +8,13 @@ source "$SCRIPT_DIR/firmware-common.sh" if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then cat <<'EOF' Usage: - scripts/build-firmware.sh [extra arduino-cli compile args...] + scripts/build-firmware.sh [variant] + scripts/build-firmware.sh --variant VARIANT [extra arduino-cli compile args...] + scripts/build-firmware.sh [variant] -- [extra arduino-cli compile args...] Builds the MEMLNaut firmware with the repo's known-good RP2350 target and C++20 flag. +If no variant is provided and the script is run in a TTY, it prompts from the parsed +MEMLNaut mode list in MEMLNaut-NISPS.ino and rewrites the active mode before building. EOF exit 0 fi @@ -18,12 +22,49 @@ fi ensure_arduino_cli ensure_submodules_ready +variant_arg="${MEMLNAUT_FIRMWARE_VARIANT:-}" +compile_extra=() + +load_firmware_variants + +while [[ $# -gt 0 ]]; do + case "$1" in + --variant) + if [[ $# -lt 2 ]]; then + echo "error: --variant requires a value" >&2 + exit 1 + fi + variant_arg="$2" + shift 2 + ;; + --variant=*) + variant_arg="${1#*=}" + shift + ;; + --) + shift + compile_extra+=("$@") + break + ;; + *) + if [[ -z "$variant_arg" ]] && resolve_firmware_variant "$1" >/dev/null 2>&1; then + variant_arg="$1" + else + compile_extra+=("$1") + fi + shift + ;; + esac +done + +set_firmware_variant "$variant_arg" mkdir -p "$BUILD_DIR" echo "Building firmware:" echo " sketch: $SKETCH_PATH" echo " fqbn: $FQBN" echo " out: $BUILD_DIR" +echo " variant: $(variant_alias "$ACTIVE_FIRMWARE_VARIANT")" compile_args=( compile @@ -32,7 +73,7 @@ compile_args=( --output-dir "$BUILD_DIR" "$SKETCH_PATH" ) -compile_args+=("$@") +compile_args+=("${compile_extra[@]}") arduino-cli "${compile_args[@]}" diff --git a/scripts/firmware-common.sh b/scripts/firmware-common.sh index a6cdace..d5737de 100755 --- a/scripts/firmware-common.sh +++ b/scripts/firmware-common.sh @@ -12,6 +12,9 @@ UF2_PATH_DEFAULT="${MEMLNAUT_FIRMWARE_UF2:-$BUILD_DIR/${SKETCH_NAME}.uf2}" FQBN="${MEMLNAUT_FIRMWARE_FQBN:-rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3}" CXX20_BUILD_PROPERTY="${MEMLNAUT_FIRMWARE_CXX20_PROPERTY:-compiler.cpp.extra_flags=-std=gnu++20}" +FIRMWARE_VARIANTS=() +ACTIVE_FIRMWARE_VARIANT="" + ensure_arduino_cli() { if ! command -v arduino-cli >/dev/null 2>&1; then echo "error: arduino-cli is not installed or not on PATH" >&2 @@ -92,3 +95,143 @@ assert_boot_mount() { exit 1 fi } + +load_firmware_variants() { + mapfile -t FIRMWARE_VARIANTS < <( + grep -E '^[[:space:]]*(//[[:space:]]*)?#define[[:space:]]+MEMLNAUT_MODE_TYPE[[:space:]]+MEMLNautMode[A-Za-z0-9_]+' "$SKETCH_PATH" \ + | sed -E 's/^[[:space:]]*(\/\/[[:space:]]*)?#define[[:space:]]+MEMLNAUT_MODE_TYPE[[:space:]]+//' + ) + + if [[ ${#FIRMWARE_VARIANTS[@]} -eq 0 ]]; then + echo "error: could not find any MEMLNaut mode variants in $SKETCH_PATH" >&2 + exit 1 + fi + + ACTIVE_FIRMWARE_VARIANT="$( + grep -E '^[[:space:]]*#define[[:space:]]+MEMLNAUT_MODE_TYPE[[:space:]]+MEMLNautMode[A-Za-z0-9_]+' "$SKETCH_PATH" \ + | sed -E 's/^[[:space:]]*#define[[:space:]]+MEMLNAUT_MODE_TYPE[[:space:]]+//' \ + | head -n 1 + )" +} + +variant_alias() { + local variant="$1" + variant="${variant#MEMLNautMode}" + printf '%s\n' "$variant" | tr '[:upper:]' '[:lower:]' +} + +resolve_firmware_variant() { + local requested="$1" + local requested_lower + local variant + + requested_lower="$(printf '%s' "$requested" | tr '[:upper:]' '[:lower:]')" + for variant in "${FIRMWARE_VARIANTS[@]}"; do + if [[ "$requested" == "$variant" || "$requested_lower" == "$(printf '%s' "$variant" | tr '[:upper:]' '[:lower:]')" || "$requested_lower" == "$(variant_alias "$variant")" ]]; then + printf '%s\n' "$variant" + return 0 + fi + done + + return 1 +} + +choose_firmware_variant() { + local requested="${1:-}" + local resolved="" + local variant + local choice + + load_firmware_variants + + if [[ -n "$requested" ]]; then + if resolved="$(resolve_firmware_variant "$requested")"; then + printf '%s\n' "$resolved" + return 0 + fi + + echo "error: unknown firmware variant: $requested" >&2 + echo "available variants:" >&2 + for variant in "${FIRMWARE_VARIANTS[@]}"; do + echo " - $(variant_alias "$variant") ($variant)" >&2 + done + exit 1 + fi + + if [[ -t 0 && -t 1 ]]; then + echo "Select a firmware variant to build:" + local idx=1 + for variant in "${FIRMWARE_VARIANTS[@]}"; do + if [[ "$variant" == "$ACTIVE_FIRMWARE_VARIANT" ]]; then + echo " $idx) $(variant_alias "$variant") ($variant, current)" + else + echo " $idx) $(variant_alias "$variant") ($variant)" + fi + idx=$((idx + 1)) + done + + while true; do + read -r -p "Variant number or name: " choice + if [[ "$choice" =~ ^[0-9]+$ ]]; then + if (( choice >= 1 && choice <= ${#FIRMWARE_VARIANTS[@]} )); then + printf '%s\n' "${FIRMWARE_VARIANTS[choice-1]}" + return 0 + fi + elif resolved="$(resolve_firmware_variant "$choice" 2>/dev/null)"; then + printf '%s\n' "$resolved" + return 0 + fi + echo "Invalid selection. Enter a number from the list or a variant name." + done + fi + + if [[ -n "$ACTIVE_FIRMWARE_VARIANT" ]]; then + echo "No variant specified and no interactive terminal available; using current variant: $(variant_alias "$ACTIVE_FIRMWARE_VARIANT")" >&2 + printf '%s\n' "$ACTIVE_FIRMWARE_VARIANT" + return 0 + fi + + echo "error: no variant specified and no active variant found in $SKETCH_PATH" >&2 + exit 1 +} + +set_firmware_variant() { + local requested="$1" + local selected + + selected="$(choose_firmware_variant "$requested")" + python - "$SKETCH_PATH" "$selected" <<'PY' +from pathlib import Path +import re +import sys + +sketch_path = Path(sys.argv[1]) +selected = sys.argv[2] + +pattern = re.compile(r'^(\s*)(//\s*)?#define(\s+MEMLNAUT_MODE_TYPE\s+)(MEMLNautMode[A-Za-z0-9_]+)(\s*)$') +lines = sketch_path.read_text().splitlines() +updated = [] +found_selected = False + +for line in lines: + match = pattern.match(line) + if not match: + updated.append(line) + continue + + indent, _, middle, variant, trailing = match.groups() + if variant == selected: + updated.append(f"{indent}#define{middle}{variant}{trailing}") + found_selected = True + else: + updated.append(f"{indent}// #define{middle}{variant}{trailing}") + +if not found_selected: + raise SystemExit(f"selected variant not found in sketch: {selected}") + +sketch_path.write_text("\n".join(updated) + "\n") +PY + + ACTIVE_FIRMWARE_VARIANT="$selected" + echo "Selected firmware variant: $(variant_alias "$selected") ($selected)" +}