feat(firmware): on-device Joystick Dual/Single settings menu

Add glue/settings_view.hpp: wire_settings(mode) registers a "Joystick"
SingleSelectView on the MEMLNaut display carousel (TFT + rotary encoder, same
DisplayDriver as SystemView/SelfTest). For the 4-input two-joystick modes,
"Single" pins ML input channels 2,3 (the second joystick) to neutral via
ModeBase::set_input_pinned — the network is never rebuilt and trained state
survives toggling. Default is Dual. Only registered when input arity == 4.

Called from MEMLNaut-NISPS.ino setup() after addSystemInfoView(). MAP.md +
CLAUDE.md glue listings updated.

NOTE: compile-unverified — no arduino-cli/RP2350 toolchain on this host and no
hardware; needs scripts/build-firmware.sh + a flash test (chokepoint A).
This commit is contained in:
monkey-w1n5t0n 2026-06-28 21:02:23 +02:00
parent 2f9cfd1de2
commit 3952c65d75
4 changed files with 70 additions and 1 deletions

View file

@ -58,7 +58,8 @@ firmware/MEMLNaut-NISPS/
│ ├── midi_io.hpp # MIDI in → mode handlers; drain ControlEvent ring → MIDI UART │ ├── midi_io.hpp # MIDI in → mode handlers; drain ControlEvent ring → MIDI UART
│ ├── mode_select.hpp # type aliases firmware mode name → nisps::modes::*Mode │ ├── mode_select.hpp # type aliases firmware mode name → nisps::modes::*Mode
│ ├── input_router.hpp # wire_inputs() entry point │ ├── input_router.hpp # wire_inputs() entry point
│ └── output_router.hpp # drain_outputs() entry point │ ├── output_router.hpp # drain_outputs() entry point
│ └── settings_view.hpp # wire_settings(): TFT/rotary menu (Joystick Dual/Single for 4-in modes)
└── src/{memllib,daisysp,nisps} # symlinks (Arduino-CLI sketch tree convention) └── src/{memllib,daisysp,nisps} # symlinks (Arduino-CLI sketch tree convention)
``` ```

1
MAP.md
View file

@ -23,6 +23,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
- `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line. Includes the six `MEMLNautModeExtSynth*` external-synth variants (one per device template in `nisps/midi`, e.g. `MEMLNautModeExtSynthSub37`). Also defines the `MEMLNautModeSelfTest` pseudo-variant (tag type) + the `NISPS_ST_*`/`NISPS_ST_CAT` token-paste macros the `.ino` uses to compute `NISPS_SELFTEST`. Note: `src/nisps/` exposes each referenced top-level nisps subdir as a symlink — `midi` was added alongside `core/dsp/engines/ml/modes`. - `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line. Includes the six `MEMLNautModeExtSynth*` external-synth variants (one per device template in `nisps/midi`, e.g. `MEMLNautModeExtSynthSub37`). Also defines the `MEMLNautModeSelfTest` pseudo-variant (tag type) + the `NISPS_ST_*`/`NISPS_ST_CAT` token-paste macros the `.ino` uses to compute `NISPS_SELFTEST`. Note: `src/nisps/` exposes each referenced top-level nisps subdir as a symlink — `midi` was added alongside `core/dsp/engines/ml/modes`.
- `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`. - `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`.
- `input_router.hpp`, `output_router.hpp` — top-level `wire_inputs()` / `drain_outputs()` entry points. - `input_router.hpp`, `output_router.hpp` — top-level `wire_inputs()` / `drain_outputs()` entry points.
- `settings_view.hpp``wire_settings(mode)`: adds on-device settings views to the MEMLNaut display carousel (TFT + rotary encoder). Joystick Dual/Single toggle for the 4-input ("two 2-D joystick") modes — "Single" pins ML input channels 2,3 to neutral via `ModeBase::set_input_pinned` (no net rebuild). Registered in the `.ino` after `addSystemInfoView()`.
- `firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}` — symlinks (Arduino-CLI requires sketch-tree includes; preprocessor refuses `..` in headers). - `firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}` — symlinks (Arduino-CLI requires sketch-tree includes; preprocessor refuses `..` in headers).
- `firmware/README.md` — structure + build instructions. - `firmware/README.md` — structure + build instructions.

View file

@ -31,6 +31,7 @@
#include "glue/midi_io.hpp" #include "glue/midi_io.hpp"
#include "glue/mode_select.hpp" #include "glue/mode_select.hpp"
#include "glue/output_router.hpp" #include "glue/output_router.hpp"
#include "glue/settings_view.hpp"
// ---- Mode selection ---- // ---- Mode selection ----
// Build script rewrites which line is uncommented. // Build script rewrites which line is uncommented.
@ -141,6 +142,8 @@ void setup() {
} }
MEMLNaut::Instance()->addSystemInfoView(); MEMLNaut::Instance()->addSystemInfoView();
// Settings menu (e.g. Joystick: Dual/Single for the 4-input modes).
nisps_firmware::wire_settings(g_mode);
Serial.println("Finished initialising core 0."); Serial.println("Finished initialising core 0.");
} }

View file

@ -0,0 +1,64 @@
// glue/settings_view.hpp — on-device settings menu views (TFT + rotary encoder).
//
// Adds entries to the MEMLNaut display carousel (the same DisplayDriver the
// SystemView and SelfTest use). Navigation: rotate to move between views;
// press to focus a view; rotate-while-focused to change its value; press
// again to unfocus. See src/memllib/.../display/DisplayDriver.hpp.
//
// Currently provides:
// * Joystick: Dual / Single — for the 4-input ("two 2-D joystick") modes,
// toggles whether the SECOND joystick (input channels 2,3) feeds the ML
// engine. "Single" pins those channels to the neutral value (0.5) via the
// platform-agnostic ModeBase pin mechanism, so the network is never
// rebuilt and trained state survives toggling. Default is Dual (the 4-D
// variant naturally uses both joysticks).
//
// Only registered for modes whose input arity is exactly 4 (the two-joystick
// modes); other modes (e.g. SoundAnalysisMIDI, whose joystick channels are not
// 2,3) get no joystick toggle.
#pragma once
#include <array>
#include <cstddef>
#include <memory>
#include <span>
#include "../src/memllib/hardware/memlnaut/MEMLNaut.hpp"
#include "../src/memllib/hardware/memlnaut/display/SingleSelectView.hpp"
namespace nisps_firmware {
// The second 2-D joystick maps to ML input channels 2 and 3 (joy_z / joy_w).
inline constexpr std::size_t kSecondJoystickCh0 = 2u;
inline constexpr std::size_t kSecondJoystickCh1 = 3u;
// Add the settings views for `mode` to the display carousel. Must run AFTER
// MEMLNaut::Initialize() (so the display + TFT exist) and after
// addSystemInfoView(), on core 0.
template <typename ModeT>
inline void wire_settings(ModeT& mode) {
if constexpr (ModeT::input_channel_count() == 4u) {
MEMLNaut* meml = MEMLNaut::Instance();
if (!meml || !meml->disp) return;
// Kept alive for the program lifetime (the carousel holds a shared_ptr
// too, but this guarantees ownership regardless of view churn).
static std::shared_ptr<SingleSelectView> joystick_view;
joystick_view = std::make_shared<SingleSelectView>("Joystick");
meml->disp->AddView(joystick_view); // calls Setup() → creates the selector
// Index 0 = Dual (default, no pinning); index 1 = Single.
std::array<String, 2u> options = { String("Dual"), String("Single") };
joystick_view->setOptions(std::span<String>(options.data(), options.size()));
joystick_view->setNewVoiceCallback([&mode](std::size_t idx) {
const bool single = (idx == 1u);
mode.set_input_pinned(kSecondJoystickCh0, single);
mode.set_input_pinned(kSecondJoystickCh1, single);
});
} else {
(void)mode;
}
}
} // namespace nisps_firmware