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")"
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
"${PYTHON:-python3}" - "$SKETCH_PATH" "$selected" <<'PY'
|
2026-04-15 17:37:58 +02:00
|
|
|
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
|
|
|
}
|