2026-04-15 17:32:32 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
|
|
Stream 6: extract firmware glue under firmware/
Move the Arduino sketch into firmware/MEMLNaut-NISPS/ and bridge the
hardware (memllib) to the platform-agnostic nisps/ library through a
slim glue layer. Delete the legacy root-level *AudioApp.hpp,
modes/MEMLNautMode*.hpp, voicespaces/, IMLInterface.hpp, XiasriAnalysis,
and the src/memlp submodule.
Glue layout (firmware/MEMLNaut-NISPS/glue/):
audio_driver.hpp - bridge memllib block callback to Mode::process
via per-Mode templated trampoline (no virtual dispatch)
peripherals.hpp - joystick/pots/buttons -> Mode::set_input + ML primitives
midi_io.hpp - MIDI in -> mode.note_on/update_bpm/set_playing,
drains mode ControlEvent ring -> MIDI UART
mode_select.hpp - using-aliases mapping MEMLNautMode<Name> to
nisps::modes::*Mode (build script rewrites the
#define MEMLNAUT_MODE_TYPE line)
input_router.hpp / output_router.hpp - top-level wire/drain entry points
The sketch tree uses src/{memllib,daisysp,nisps} symlinks because
Arduino-CLI rejects ".." in include paths from sketch-tree headers.
mode_select.hpp #undefs Arduino's sq/min/max/abs/round macros before
including nisps headers (some nisps engines use those identifiers as
method names). The audio bridge struct is extern in the header and
defined in the .ino because inline + __not_in_flash section attribute
collide at link time.
Verification: arduino-cli compile succeeds for PAFSynth, ChannelStrip,
and BreakOr (rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3,
-std=gnu++20). Host C++ tests under nisps/build still pass (3 binaries,
110+ tests). Build script (scripts/build-firmware.sh) updated to point
at the new sketch path; mode-rewrite logic unchanged.
Closes meml-gkm.
2026-04-29 16:05:38 +02:00
|
|
|
SKETCH_PATH="${MEMLNAUT_FIRMWARE_SKETCH:-$REPO_ROOT/firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino}"
|
2026-04-15 17:32:32 +02:00
|
|
|
SKETCH_NAME="$(basename "$SKETCH_PATH")"
|
|
|
|
|
BUILD_DIR="${MEMLNAUT_FIRMWARE_BUILD_DIR:-/tmp/memlnaut-firmware-build}"
|
|
|
|
|
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}"
|
2026-04-15 17:52:39 +02:00
|
|
|
UF2_LABEL_CANDIDATES=("${MEMLNAUT_FIRMWARE_UF2_LABEL_1:-RP2350}" "${MEMLNAUT_FIRMWARE_UF2_LABEL_2:-RPI-RP2}" "${MEMLNAUT_FIRMWARE_UF2_LABEL_3:-RP2040}")
|
2026-04-15 17:32:32 +02:00
|
|
|
|
2026-04-15 17:37:58 +02:00
|
|
|
FIRMWARE_VARIANTS=()
|
|
|
|
|
ACTIVE_FIRMWARE_VARIANT=""
|
|
|
|
|
|
2026-04-15 17:32:32 +02:00
|
|
|
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
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensure_git() {
|
|
|
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
|
|
|
echo "error: git is not installed or not on PATH" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensure_submodules_ready() {
|
|
|
|
|
ensure_git
|
|
|
|
|
|
|
|
|
|
local status
|
|
|
|
|
status="$(git -C "$REPO_ROOT" submodule status --recursive)"
|
|
|
|
|
if grep -qE '^[+-]' <<<"$status"; then
|
|
|
|
|
echo "error: submodules are not initialized or not at the recorded revision" >&2
|
|
|
|
|
echo "run: git submodule update --init --recursive" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve_path() {
|
|
|
|
|
local path="$1"
|
|
|
|
|
|
|
|
|
|
if [[ "$path" = /* ]]; then
|
|
|
|
|
printf '%s\n' "$path"
|
|
|
|
|
else
|
|
|
|
|
printf '%s\n' "$(pwd)/$path"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
find_boot_mount() {
|
|
|
|
|
local candidates=(
|
|
|
|
|
"/run/media/${USER}/RP2350"
|
|
|
|
|
"/run/media/${USER}/RPI-RP2"
|
|
|
|
|
"/media/${USER}/RP2350"
|
|
|
|
|
"/media/${USER}/RPI-RP2"
|
|
|
|
|
"/mnt/RP2350"
|
|
|
|
|
"/mnt/RPI-RP2"
|
|
|
|
|
)
|
|
|
|
|
local candidate
|
|
|
|
|
|
|
|
|
|
for candidate in "${candidates[@]}"; do
|
|
|
|
|
if [[ -d "$candidate" ]]; then
|
|
|
|
|
printf '%s\n' "$candidate"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
local base
|
|
|
|
|
for base in "/run/media/${USER}" "/media/${USER}" "/mnt"; do
|
|
|
|
|
[[ -d "$base" ]] || continue
|
|
|
|
|
candidate="$(find "$base" -maxdepth 2 -type f -name INFO_UF2.TXT -printf '%h\n' 2>/dev/null | head -n 1 || true)"
|
|
|
|
|
if [[ -n "$candidate" ]]; then
|
|
|
|
|
printf '%s\n' "$candidate"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 17:52:39 +02:00
|
|
|
is_known_uf2_label() {
|
|
|
|
|
local label="$1"
|
|
|
|
|
local candidate
|
|
|
|
|
|
|
|
|
|
for candidate in "${UF2_LABEL_CANDIDATES[@]}"; do
|
|
|
|
|
if [[ -n "$candidate" && "$label" == "$candidate" ]]; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
find_boot_block_device() {
|
|
|
|
|
local line
|
|
|
|
|
local dev_path=""
|
|
|
|
|
local dev_fstype=""
|
|
|
|
|
local dev_label=""
|
|
|
|
|
local dev_mountpoints=""
|
|
|
|
|
|
|
|
|
|
while IFS= read -r line; do
|
|
|
|
|
dev_path=""
|
|
|
|
|
dev_fstype=""
|
|
|
|
|
dev_label=""
|
|
|
|
|
dev_mountpoints=""
|
|
|
|
|
eval "$(
|
|
|
|
|
sed -E \
|
|
|
|
|
-e 's/(^|[[:space:]])PATH=/\1dev_path=/' \
|
|
|
|
|
-e 's/(^|[[:space:]])FSTYPE=/\1dev_fstype=/' \
|
|
|
|
|
-e 's/(^|[[:space:]])LABEL=/\1dev_label=/' \
|
|
|
|
|
-e 's/(^|[[:space:]])MOUNTPOINTS=/\1dev_mountpoints=/' \
|
|
|
|
|
<<<"$line"
|
|
|
|
|
)"
|
|
|
|
|
|
|
|
|
|
[[ -n "$dev_path" ]] || continue
|
|
|
|
|
[[ "$dev_fstype" == "vfat" || "$dev_fstype" == "msdos" || "$dev_fstype" == "fat" ]] || continue
|
|
|
|
|
is_known_uf2_label "$dev_label" || continue
|
|
|
|
|
|
|
|
|
|
printf '%s\n' "$dev_path"
|
|
|
|
|
return 0
|
|
|
|
|
done < <(lsblk -P -o PATH,FSTYPE,LABEL,MOUNTPOINTS 2>/dev/null || true)
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mount_boot_block_device() {
|
|
|
|
|
local device_path="$1"
|
|
|
|
|
local mount_output=""
|
|
|
|
|
local mounted_path=""
|
|
|
|
|
|
|
|
|
|
if command -v udisksctl >/dev/null 2>&1; then
|
|
|
|
|
mount_output="$(udisksctl mount -b "$device_path" 2>/dev/null || true)"
|
|
|
|
|
mounted_path="$(sed -nE "s#.* at (.+)\.?#\1#p" <<<"$mount_output" | tail -n 1)"
|
|
|
|
|
if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then
|
|
|
|
|
printf '%s\n' "$mounted_path"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensure_boot_mount() {
|
|
|
|
|
local mountpoint=""
|
|
|
|
|
local device_path=""
|
|
|
|
|
|
|
|
|
|
mountpoint="$(find_boot_mount || true)"
|
|
|
|
|
if [[ -n "$mountpoint" ]]; then
|
|
|
|
|
printf '%s\n' "$mountpoint"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
device_path="$(find_boot_block_device || true)"
|
|
|
|
|
if [[ -z "$device_path" ]]; then
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
mountpoint="$(mount_boot_block_device "$device_path" || true)"
|
|
|
|
|
if [[ -n "$mountpoint" ]]; then
|
|
|
|
|
printf '%s\n' "$mountpoint"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
mountpoint="$(find_boot_mount || true)"
|
|
|
|
|
if [[ -n "$mountpoint" ]]; then
|
|
|
|
|
printf '%s\n' "$mountpoint"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 17:32:32 +02:00
|
|
|
assert_boot_mount() {
|
|
|
|
|
local mountpoint="$1"
|
|
|
|
|
|
|
|
|
|
if [[ ! -d "$mountpoint" ]]; then
|
|
|
|
|
echo "error: bootloader mount does not exist: $mountpoint" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ ! -f "$mountpoint/INFO_UF2.TXT" && ! -f "$mountpoint/INDEX.HTM" ]]; then
|
|
|
|
|
echo "error: $mountpoint does not look like an RP2040/RP2350 UF2 boot volume" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
2026-04-15 17:37:58 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)"
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 17:46:02 +02:00
|
|
|
variant_display_name() {
|
2026-04-15 17:37:58 +02:00
|
|
|
local variant="$1"
|
2026-04-15 17:46:02 +02:00
|
|
|
printf '%s\n' "${variant#MEMLNautMode}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
variant_alias() {
|
|
|
|
|
variant_display_name "$1" | tr '[:upper:]' '[:lower:]'
|
2026-04-15 17:37:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-15 17:44:52 +02:00
|
|
|
local tty_fd=""
|
2026-04-15 17:37:58 +02:00
|
|
|
|
|
|
|
|
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
|
2026-04-15 17:46:02 +02:00
|
|
|
echo " - $(variant_display_name "$variant") ($variant)" >&2
|
2026-04-15 17:37:58 +02:00
|
|
|
done
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-04-15 17:44:52 +02:00
|
|
|
if exec {tty_fd}<>/dev/tty 2>/dev/null; then
|
|
|
|
|
printf 'Select a firmware variant to build:\n' >&"$tty_fd"
|
2026-04-15 17:37:58 +02:00
|
|
|
local idx=1
|
|
|
|
|
for variant in "${FIRMWARE_VARIANTS[@]}"; do
|
|
|
|
|
if [[ "$variant" == "$ACTIVE_FIRMWARE_VARIANT" ]]; then
|
2026-04-15 17:46:02 +02:00
|
|
|
printf ' %d) %s (%s, current)\n' "$idx" "$(variant_display_name "$variant")" "$variant" >&"$tty_fd"
|
2026-04-15 17:37:58 +02:00
|
|
|
else
|
2026-04-15 17:46:02 +02:00
|
|
|
printf ' %d) %s (%s)\n' "$idx" "$(variant_display_name "$variant")" "$variant" >&"$tty_fd"
|
2026-04-15 17:37:58 +02:00
|
|
|
fi
|
|
|
|
|
idx=$((idx + 1))
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
while true; do
|
2026-04-15 17:44:52 +02:00
|
|
|
printf 'Variant number or name: ' >&"$tty_fd"
|
|
|
|
|
IFS= read -r -u "$tty_fd" choice
|
2026-04-15 17:37:58 +02:00
|
|
|
if [[ "$choice" =~ ^[0-9]+$ ]]; then
|
|
|
|
|
if (( choice >= 1 && choice <= ${#FIRMWARE_VARIANTS[@]} )); then
|
2026-04-15 17:44:52 +02:00
|
|
|
exec {tty_fd}>&-
|
2026-04-15 17:37:58 +02:00
|
|
|
printf '%s\n' "${FIRMWARE_VARIANTS[choice-1]}"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
elif resolved="$(resolve_firmware_variant "$choice" 2>/dev/null)"; then
|
2026-04-15 17:44:52 +02:00
|
|
|
exec {tty_fd}>&-
|
2026-04-15 17:37:58 +02:00
|
|
|
printf '%s\n' "$resolved"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
2026-04-15 17:44:52 +02:00
|
|
|
printf 'Invalid selection. Enter a number from the list or a variant name.\n' >&"$tty_fd"
|
2026-04-15 17:37:58 +02:00
|
|
|
done
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ -n "$ACTIVE_FIRMWARE_VARIANT" ]]; then
|
2026-04-15 17:46:02 +02:00
|
|
|
echo "No variant specified and no interactive terminal available; using current variant: $(variant_display_name "$ACTIVE_FIRMWARE_VARIANT")" >&2
|
2026-04-15 17:37:58 +02:00
|
|
|
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"
|
2026-04-15 17:46:02 +02:00
|
|
|
echo "Selected firmware variant: $(variant_display_name "$selected") ($selected)"
|
2026-04-15 17:37:58 +02:00
|
|
|
}
|