Skip to main content

midi_controller/
controller.rs

1//! Unified controller: single entry point for all input processing.
2//!
3//! Owns all timing-sensitive state (long-press detection, encoder acceleration,
4//! tap tempo, preset state) and delegates pure logic to the engine.
5//!
6//! The Controller handles system actions (preset switching) internally,
7//! including on_enter/on_exit actions and recall MIDI. Callers only need to
8//! send the resulting MIDI output — no orchestration required.
9//!
10//! Both firmware and simulator use the same `Controller` — the only difference
11//! is where `now_ms` comes from (hardware monotonic vs std::Instant).
12
13use crate::action::{action_to_midi, EncoderDirection};
14use crate::config::{Action, ButtonMode, Config, Preset, MAX_BUTTONS, MAX_ENCODERS};
15use crate::encoder_accel::EncoderAccel;
16use crate::engine::{
17    self, process_triggers, ActionStep, ButtonEvent, DisplayEvent, EngineResult, SystemAction,
18};
19use crate::long_press::{Edge, Gesture, LongPressDetector};
20use crate::routing::MidiPort;
21use crate::state::{PresetState, PresetStateStore};
22use crate::tap_tempo::TapTempo;
23
24/// Abstract input event. Hardware-agnostic — firmware maps GPIO edges to these,
25/// the simulator maps keyboard/WebSocket events to these.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Event {
28    /// Button edge (index 0..5).
29    ButtonEdge { index: u8, edge: Edge },
30    /// Encoder detent (index 0..1).
31    EncoderTurn { index: u8, clockwise: bool },
32    /// Analog input (expression pedal). Raw ADC value.
33    /// Calibration (min/max) is read from Config.global.
34    Analog { index: u8, raw: u16 },
35    /// Incoming MIDI message from any port.
36    Midi {
37        data: [u8; 8],
38        len: u8,
39        source: MidiPort,
40    },
41    /// Periodic tick — drives long-press detection. Send every 1-10ms.
42    /// No-op if no buttons are held.
43    Tick,
44}
45
46/// Result of processing an input event through the Controller.
47/// Contains everything the caller needs to emit — no further logic required.
48pub struct Output {
49    /// MIDI actions to emit (includes on_enter/on_exit/recall on preset switch).
50    pub midi: heapless::Vec<ActionStep, 32>,
51    /// Routed MIDI messages with destination port(s).
52    /// Includes thru-forwarded messages and future controller-generated output.
53    pub midi_out: heapless::Vec<crate::routing::MidiOut, 16>,
54    /// Display events (overlays, hints).
55    pub display: heapless::Vec<DisplayEvent, 2>,
56    /// Whether LED state changed and needs re-rendering.
57    pub leds_changed: bool,
58    /// Whether a preset switch occurred (caller may need to update display).
59    pub preset_changed: bool,
60    /// BPM computed from tap tempo (if any).
61    pub bpm: Option<u16>,
62    /// Mon indicator LED: flashes on MIDI activity.
63    /// Green = MIDI output generated, Blue = incoming MIDI processed.
64    pub mon_led: Option<crate::led::Rgb>,
65    /// Mode indicator LED: color represents the active preset/bank.
66    /// Set on preset change; None means no change.
67    pub mode_led: Option<crate::led::Rgb>,
68    /// Reactive LED updates from incoming MIDI (CC → ring heatmap/trigger).
69    pub reactive_led: Option<engine::ReactiveResult>,
70    /// State changed — caller should persist (EEPROM/flash).
71    /// Set on preset switch or button/encoder state change.
72    pub state_dirty: bool,
73    /// Internal: pending system actions to be handled by the controller.
74    pending_system: heapless::Vec<SystemAction, 2>,
75}
76
77impl Output {
78    fn new() -> Self {
79        Self {
80            midi: heapless::Vec::new(),
81            midi_out: heapless::Vec::new(),
82            display: heapless::Vec::new(),
83            leds_changed: false,
84            preset_changed: false,
85            bpm: None,
86            mon_led: None,
87            mode_led: None,
88            reactive_led: None,
89            state_dirty: false,
90            pending_system: heapless::Vec::new(),
91        }
92    }
93}
94
95/// Unified timing controller. Owns all stateful input processing.
96///
97/// # Usage
98///
99/// ```ignore
100/// let mut ctrl = Controller::new();
101/// // On each input poll:
102/// let result = ctrl.process(event, now_ms, &config);
103/// // Send result.midi via MIDI output
104/// // Update display from result.display
105/// // Re-render LEDs if result.leds_changed
106/// ```
107pub struct Controller<const B: usize = MAX_BUTTONS, const E: usize = MAX_ENCODERS> {
108    state_store: PresetStateStore<B, E>,
109    long_press: [LongPressDetector; B],
110    encoder_accel: [EncoderAccel; E],
111    tap_tempo: TapTempo,
112    button_active: [bool; B],
113    active_preset: u8,
114}
115
116/// Type alias for the default controller configuration (6 buttons, 2 encoders).
117pub type DefaultController = Controller<MAX_BUTTONS, MAX_ENCODERS>;
118
119impl<const B: usize, const E: usize> Default for Controller<B, E> {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl<const B: usize, const E: usize> Controller<B, E> {
126    pub fn new() -> Self {
127        Self {
128            state_store: PresetStateStore::new(),
129            long_press: core::array::from_fn(|_| LongPressDetector::new_fired()),
130            encoder_accel: core::array::from_fn(|_| EncoderAccel::new()),
131            tap_tempo: TapTempo::new(),
132            button_active: [false; B],
133            active_preset: 0,
134        }
135    }
136
137    /// Create with a restored state store (from EEPROM/persistence).
138    pub fn with_state(store: PresetStateStore<B, E>) -> Self {
139        let state = store.current().clone();
140        let active = store.active_index();
141        Self {
142            state_store: store,
143            long_press: core::array::from_fn(|_| LongPressDetector::new_fired()),
144            encoder_accel: core::array::from_fn(|_| EncoderAccel::new()),
145            tap_tempo: TapTempo::new(),
146            button_active: state.button_active,
147            active_preset: active,
148        }
149    }
150
151    /// Process a single input event. Returns all MIDI output, display events,
152    /// and flags. System actions (preset switch, tap tempo) are handled internally.
153    pub fn process<const A: usize>(
154        &mut self,
155        event: Event,
156        now_ms: u32,
157        config: &Config<B, E, A>,
158    ) -> Output {
159        let mut result = match event {
160            Event::ButtonEdge { index, edge } => {
161                self.process_button(index as usize, edge, now_ms, config)
162            }
163            Event::EncoderTurn { index, clockwise } => {
164                self.process_encoder(index as usize, clockwise, now_ms, config)
165            }
166            Event::Analog { index, raw } => self.process_analog(index as usize, raw, config),
167            Event::Midi { data, len, source } => {
168                self.do_process_incoming_midi(&data[..len as usize], source, config)
169            }
170            Event::Tick => self.do_tick(now_ms, config),
171        };
172
173        // Handle system actions produced by event processing
174        self.handle_system_actions(&mut result, now_ms, config);
175
176        // Indicator LEDs
177        if !result.midi.is_empty() {
178            // Green flash: MIDI output generated
179            result.mon_led = Some(crate::led::Rgb::new(0, 255, 0));
180        } else if matches!(event, Event::Midi { .. }) {
181            // Blue flash: incoming MIDI processed (even if no output)
182            result.mon_led = Some(crate::led::Rgb::new(0, 0, 255));
183        }
184
185        result
186    }
187
188    fn do_tick<const A: usize>(&mut self, now_ms: u32, config: &Config<B, E, A>) -> Output {
189        let mut result = Output::new();
190
191        if !self.button_held() {
192            return result;
193        }
194
195        let preset = match config.presets.get(self.active_preset as usize) {
196            Some(p) => p,
197            None => return result,
198        };
199
200        for i in 0..B {
201            if self.long_press[i].is_active() && !self.long_press[i].has_fired() {
202                let has_long_press = preset
203                    .buttons
204                    .get(i)
205                    .map(|b| !b.on_long_press.is_empty())
206                    .unwrap_or(false);
207                if has_long_press {
208                    if let Some(gesture) = self.long_press[i].update(None, now_ms) {
209                        self.handle_gesture(i, gesture, preset, &mut result);
210                    }
211                }
212            }
213        }
214
215        result
216    }
217
218    fn do_process_incoming_midi<const A: usize>(
219        &mut self,
220        raw: &[u8],
221        source: MidiPort,
222        config: &Config<B, E, A>,
223    ) -> Output {
224        let mut result = Output::new();
225
226        // Thru routing: forward incoming MIDI to configured destination ports
227        let thru_dest = self.thru_destination(source, config);
228        if !thru_dest.is_empty() && !raw.is_empty() {
229            result
230                .midi_out
231                .push(crate::routing::MidiOut::new(raw, thru_dest))
232                .ok();
233        }
234
235        let preset = match config.presets.get(self.active_preset as usize) {
236            Some(p) => p,
237            None => return result,
238        };
239
240        // Reactive LEDs: check incoming CC against preset's listen_cc bindings
241        if raw.len() >= 3 && (raw[0] & 0xF0) == 0xB0 {
242            let channel = (raw[0] & 0x0F) + 1;
243            result.reactive_led = engine::process_incoming_cc(preset, channel, raw[1], raw[2]);
244        }
245
246        if raw.len() >= 2 && !preset.triggers.is_empty() {
247            let mut state = self.working_state();
248            let data2 = if raw.len() >= 3 { raw[2] } else { 0 };
249            let trigger_result = process_triggers(&mut state, preset, raw[0], raw[1], data2);
250            self.apply_state(&state);
251
252            for step in &trigger_result.midi {
253                result.midi.push(step.clone()).ok();
254            }
255            if trigger_result.led_dirty {
256                result.leds_changed = true;
257            }
258
259            // Handle system actions from triggers (preset switch)
260            for s in &trigger_result.system {
261                self.execute_system_action(*s, &mut result, 0, config);
262            }
263        }
264
265        result
266    }
267
268    /// Compute thru destination ports for a given source port based on global config.
269    fn thru_destination<const A: usize>(
270        &self,
271        source: MidiPort,
272        config: &Config<B, E, A>,
273    ) -> MidiPort {
274        let mut dest = MidiPort::empty();
275        if source.contains(MidiPort::DIN) && config.global.din_to_usb_thru {
276            dest |= MidiPort::USB;
277        }
278        if source.contains(MidiPort::USB) {
279            if config.global.usb_to_din_thru {
280                dest |= MidiPort::DIN;
281            }
282            if config.global.usb_to_usb_thru {
283                dest |= MidiPort::USB;
284            }
285        }
286        dest
287    }
288
289    /// Returns true if any button is currently held.
290    pub fn button_held(&self) -> bool {
291        self.long_press.iter().any(|lp| lp.is_active())
292    }
293
294    /// Returns the current button active state (toggle ON / momentary held).
295    pub fn button_states(&self) -> &[bool; B] {
296        &self.button_active
297    }
298
299    /// Get the active preset index.
300    pub fn active_preset(&self) -> u8 {
301        self.active_preset
302    }
303
304    /// Get the encoder values.
305    pub fn encoder_values(&self) -> [u8; E] {
306        self.state_store.current().encoder_values
307    }
308
309    /// Get a snapshot of the state store (with current working state saved).
310    /// Use this for persistence — the caller decides the serialization format.
311    pub fn snapshot_store(&self) -> PresetStateStore<B, E> {
312        let mut store = self.state_store.clone();
313        let working = self.working_state();
314        store.save_working(&working);
315        store
316    }
317
318    /// Manually switch to a preset (e.g., on boot or from external command).
319    /// Returns on_enter + recall MIDI in the result.
320    pub fn select_preset<const A: usize>(
321        &mut self,
322        preset_idx: u8,
323        config: &Config<B, E, A>,
324    ) -> Output {
325        let mut result = Output::new();
326        self.do_switch_preset(preset_idx, &mut result, config);
327        result
328    }
329
330    /// Set encoder value (for initial state setup from config defaults).
331    pub fn set_encoder_value(&mut self, index: usize, value: u8) {
332        let mut state = self.state_store.current().clone();
333        if index < E {
334            state.encoder_values[index] = value;
335        }
336        self.state_store.save_working(&state);
337    }
338
339    // --- Private ---
340
341    /// Map preset index to a distinct indicator color.
342    /// Cycles through 8 hues so each bank/preset is visually distinct.
343    fn preset_color(index: u8) -> crate::led::Rgb {
344        match index % 8 {
345            0 => crate::led::Rgb::new(255, 0, 0),     // red
346            1 => crate::led::Rgb::new(0, 255, 0),     // green
347            2 => crate::led::Rgb::new(0, 0, 255),     // blue
348            3 => crate::led::Rgb::new(255, 255, 0),   // yellow
349            4 => crate::led::Rgb::new(255, 0, 255),   // magenta
350            5 => crate::led::Rgb::new(0, 255, 255),   // cyan
351            6 => crate::led::Rgb::new(255, 128, 0),   // orange
352            7 => crate::led::Rgb::new(128, 0, 255),   // purple
353            _ => crate::led::Rgb::new(255, 255, 255), // unreachable
354        }
355    }
356
357    fn current_preset<'a, const A: usize>(
358        &self,
359        config: &'a Config<B, E, A>,
360    ) -> Option<&'a Preset<B, E, A>> {
361        config.presets.get(self.active_preset as usize)
362    }
363
364    fn handle_system_actions<const A: usize>(
365        &mut self,
366        result: &mut Output,
367        now_ms: u32,
368        config: &Config<B, E, A>,
369    ) {
370        let actions: heapless::Vec<SystemAction, 2> = result.pending_system.clone();
371        result.pending_system.clear();
372        for action in &actions {
373            self.execute_system_action(*action, result, now_ms, config);
374        }
375    }
376
377    fn process_button<const A: usize>(
378        &mut self,
379        index: usize,
380        edge: Edge,
381        now_ms: u32,
382        config: &Config<B, E, A>,
383    ) -> Output {
384        let mut result = Output::new();
385
386        let preset = match self.current_preset(config) {
387            Some(p) => p,
388            None => return result,
389        };
390
391        if index >= B {
392            return result;
393        }
394
395        let has_long_press = preset
396            .buttons
397            .get(index)
398            .map(|b| !b.on_long_press.is_empty())
399            .unwrap_or(false);
400
401        let mode = preset
402            .buttons
403            .get(index)
404            .map(|b| &b.mode)
405            .unwrap_or(&ButtonMode::Momentary);
406
407        if has_long_press {
408            if matches!(mode, ButtonMode::Momentary) {
409                match edge {
410                    Edge::Activate => {
411                        self.button_active[index] = true;
412                        result.leds_changed = true;
413                        result.state_dirty = true;
414                    }
415                    Edge::Deactivate => {
416                        self.button_active[index] = false;
417                        result.leds_changed = true;
418                        result.state_dirty = true;
419                    }
420                }
421            }
422
423            if edge == Edge::Activate {
424                if let Some(btn) = preset.buttons.get(index) {
425                    let hint_action = btn.on_long_press.iter().find_map(|a| match a {
426                        Action::PresetNext => Some(SystemAction::PresetNext),
427                        Action::PresetPrev => Some(SystemAction::PresetPrev),
428                        _ => None,
429                    });
430                    if let Some(action) = hint_action {
431                        result
432                            .display
433                            .push(DisplayEvent::LongPressHint { action })
434                            .ok();
435                    }
436                }
437            }
438
439            if let Some(gesture) = self.long_press[index].update(Some(edge), now_ms) {
440                self.handle_gesture(index, gesture, preset, &mut result);
441            }
442        } else {
443            match edge {
444                Edge::Activate => {
445                    let mut state = self.working_state();
446                    let r = engine::process_button(&mut state, preset, index, ButtonEvent::Press);
447                    self.apply_state(&state);
448                    self.merge_engine_result(&r, &mut result);
449                }
450                Edge::Deactivate => {
451                    let mut state = self.working_state();
452                    let r = engine::process_button(&mut state, preset, index, ButtonEvent::Release);
453                    self.apply_state(&state);
454                    self.merge_engine_result(&r, &mut result);
455                }
456            }
457        }
458
459        result
460    }
461
462    fn handle_gesture<const A: usize>(
463        &mut self,
464        index: usize,
465        gesture: Gesture,
466        preset: &Preset<B, E, A>,
467        result: &mut Output,
468    ) {
469        let mode = preset
470            .buttons
471            .get(index)
472            .map(|b| &b.mode)
473            .unwrap_or(&ButtonMode::Momentary);
474
475        match gesture {
476            Gesture::ShortPress => {
477                result.display.push(DisplayEvent::LongPressCancel).ok();
478                let mut state = self.working_state();
479                let r = engine::process_button(&mut state, preset, index, ButtonEvent::Press);
480                self.apply_state(&state);
481                self.merge_engine_result(&r, result);
482
483                if let Some(btn) = preset.buttons.get(index) {
484                    if mode == &ButtonMode::Momentary || !btn.on_release.is_empty() {
485                        let mut state2 = self.working_state();
486                        let r2 = engine::process_button(
487                            &mut state2,
488                            preset,
489                            index,
490                            ButtonEvent::Release,
491                        );
492                        self.apply_state(&state2);
493                        self.merge_engine_result(&r2, result);
494                    }
495                }
496            }
497            Gesture::LongPress => {
498                let mut state = self.working_state();
499                let r = engine::process_button(&mut state, preset, index, ButtonEvent::LongPress);
500                self.apply_state(&state);
501                self.merge_engine_result(&r, result);
502            }
503        }
504    }
505
506    fn process_encoder<const A: usize>(
507        &mut self,
508        index: usize,
509        clockwise: bool,
510        now_ms: u32,
511        config: &Config<B, E, A>,
512    ) -> Output {
513        let mut result = Output::new();
514        let preset = match self.current_preset(config) {
515            Some(p) => p,
516            None => return result,
517        };
518
519        if index >= E {
520            return result;
521        }
522
523        let steps = self.encoder_accel[index].steps(now_ms);
524        let direction = if clockwise {
525            EncoderDirection::Clockwise
526        } else {
527            EncoderDirection::CounterClockwise
528        };
529
530        let mut state = self.working_state();
531        let r = engine::process_encoder(&mut state, preset, index, direction, steps);
532        self.apply_state(&state);
533        self.merge_engine_result(&r, &mut result);
534        result
535    }
536
537    fn process_analog<const A: usize>(
538        &mut self,
539        index: usize,
540        raw: u16,
541        config: &Config<B, E, A>,
542    ) -> Output {
543        let mut result = Output::new();
544        let preset = match self.current_preset(config) {
545            Some(p) => p,
546            None => return result,
547        };
548        let (min, max) = match index {
549            0 => (config.global.exp1_min, config.global.exp1_max),
550            1 => (config.global.exp2_min, config.global.exp2_max),
551            _ => return result,
552        };
553        let r = engine::process_analog(preset, index, raw, min, max);
554        self.merge_engine_result(&r, &mut result);
555        result
556    }
557
558    /// Merge engine result into controller result, intercepting system actions.
559    fn merge_engine_result(&mut self, r: &EngineResult, result: &mut Output) {
560        for step in &r.midi {
561            result.midi.push(step.clone()).ok();
562        }
563        for d in &r.display {
564            result.display.push(d.clone()).ok();
565        }
566        if r.led_dirty {
567            result.leds_changed = true;
568            result.state_dirty = true;
569        }
570        for s in &r.system {
571            result.pending_system.push(*s).ok();
572        }
573    }
574
575    fn execute_system_action<const A: usize>(
576        &mut self,
577        action: SystemAction,
578        result: &mut Output,
579        now_ms: u32,
580        config: &Config<B, E, A>,
581    ) {
582        let num_presets = config.presets.iter().filter(|p| !p.name.is_empty()).count() as u8;
583
584        match action {
585            SystemAction::PresetNext => {
586                if num_presets > 0 {
587                    let next = (self.active_preset + 1) % num_presets;
588                    self.do_switch_preset(next, result, config);
589                }
590            }
591            SystemAction::PresetPrev => {
592                if num_presets > 0 {
593                    let prev = if self.active_preset == 0 {
594                        num_presets - 1
595                    } else {
596                        self.active_preset - 1
597                    };
598                    self.do_switch_preset(prev, result, config);
599                }
600            }
601            SystemAction::PresetSelect(idx) => {
602                if (idx as usize) < config.presets.len() {
603                    self.do_switch_preset(idx, result, config);
604                }
605            }
606            SystemAction::SetBpm(bpm) => {
607                result.bpm = Some(bpm);
608                result.display.push(DisplayEvent::BpmOverlay { bpm }).ok();
609            }
610            SystemAction::TapTempo => {
611                result.bpm = self.tap_tempo.tap(now_ms);
612                if let Some(bpm) = result.bpm {
613                    result.display.push(DisplayEvent::BpmOverlay { bpm }).ok();
614                }
615            }
616        }
617    }
618
619    fn do_switch_preset<const A: usize>(
620        &mut self,
621        new_idx: u8,
622        result: &mut Output,
623        config: &Config<B, E, A>,
624    ) {
625        let old_preset = config.presets.get(self.active_preset as usize);
626        let new_preset = config.presets.get(new_idx as usize);
627
628        // Fire on_exit for old preset
629        if let Some(old) = old_preset {
630            for action in &old.on_exit {
631                match action {
632                    Action::Delay(ms) => {
633                        result.midi.push(ActionStep::Delay(*ms)).ok();
634                    }
635                    _ => {
636                        if let Some(msg) = action_to_midi(action) {
637                            result.midi.push(ActionStep::Send(msg)).ok();
638                        }
639                    }
640                }
641            }
642        }
643
644        // Switch state
645        if let Some(new_p) = new_preset {
646            let mut working = self.working_state();
647            let recall = self.state_store.switch(new_idx, &mut working, new_p);
648            self.apply_state(&working);
649            self.long_press = core::array::from_fn(|_| LongPressDetector::new_fired());
650            self.encoder_accel = core::array::from_fn(|_| EncoderAccel::new());
651            self.active_preset = new_idx;
652
653            // Fire on_enter for new preset
654            for action in &new_p.on_enter {
655                match action {
656                    Action::Delay(ms) => {
657                        result.midi.push(ActionStep::Delay(*ms)).ok();
658                    }
659                    _ => {
660                        if let Some(msg) = action_to_midi(action) {
661                            result.midi.push(ActionStep::Send(msg)).ok();
662                        }
663                    }
664                }
665            }
666
667            // Recall MIDI (state sync)
668            for msg in &recall {
669                result.midi.push(ActionStep::Send(msg.clone())).ok();
670            }
671        }
672
673        result.preset_changed = true;
674        result.leds_changed = true;
675        result.state_dirty = true;
676        result.mode_led = Some(Self::preset_color(self.active_preset));
677    }
678
679    fn working_state(&self) -> PresetState<B, E> {
680        let current = self.state_store.current();
681        PresetState {
682            button_active: self.button_active,
683            cycle_index: current.cycle_index,
684            encoder_values: current.encoder_values,
685        }
686    }
687
688    fn apply_state(&mut self, state: &PresetState<B, E>) {
689        self.button_active = state.button_active;
690        self.state_store.save_working(state);
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use crate::config::*;
698    use heapless::Vec as HVec;
699
700    // --- Helpers ---
701
702    /// Build a minimal config with one preset containing the given buttons and encoders.
703    fn make_config(
704        buttons: HVec<ButtonConfig, MAX_BUTTONS>,
705        encoders: HVec<EncoderConfig, MAX_ENCODERS>,
706    ) -> Config {
707        make_config_with_presets(buttons, encoders, HVec::new(), HVec::new(), HVec::new())
708    }
709
710    fn make_config_with_presets(
711        buttons: HVec<ButtonConfig, MAX_BUTTONS>,
712        encoders: HVec<EncoderConfig, MAX_ENCODERS>,
713        on_enter: HVec<Action, MAX_ACTIONS>,
714        on_exit: HVec<Action, MAX_ACTIONS>,
715        triggers: HVec<Trigger, MAX_TRIGGERS>,
716    ) -> Config {
717        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
718        presets
719            .push(Preset {
720                name: Label::try_from("P1").unwrap(),
721                buttons,
722                encoders,
723                analog: HVec::new(),
724                defaults: Default::default(),
725                on_enter,
726                on_exit,
727                triggers,
728            })
729            .ok();
730        Config {
731            global: GlobalConfig::default(),
732            presets,
733        }
734    }
735
736    fn make_two_preset_config() -> Config {
737        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
738
739        // Preset 0: one toggle button with CC#80
740        let mut buttons0: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
741        let mut on_press0: HVec<Action, MAX_ACTIONS> = HVec::new();
742        on_press0.push(Action::cc(80, 127, 1).unwrap()).ok();
743        let mut on_release0: HVec<Action, MAX_ACTIONS> = HVec::new();
744        on_release0.push(Action::cc(80, 0, 1).unwrap()).ok();
745        buttons0
746            .push(ButtonConfig {
747                label: Label::try_from("FX1").unwrap(),
748                color: LedConfig::default(),
749                mode: ButtonMode::Toggle,
750                on_press: on_press0,
751                on_release: on_release0,
752                on_long_press: HVec::new(),
753                cycle_values: HVec::new(),
754                listen_cc: None,
755            })
756            .ok();
757
758        let mut on_enter0: HVec<Action, MAX_ACTIONS> = HVec::new();
759        on_enter0.push(Action::program_change(0, 1).unwrap()).ok();
760
761        presets
762            .push(Preset {
763                name: Label::try_from("Preset0").unwrap(),
764                buttons: buttons0,
765                encoders: HVec::new(),
766                analog: HVec::new(),
767                defaults: Default::default(),
768                on_enter: on_enter0,
769                on_exit: HVec::new(),
770                triggers: HVec::new(),
771            })
772            .ok();
773
774        // Preset 1: one momentary button with CC#81
775        let mut buttons1: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
776        let mut on_press1: HVec<Action, MAX_ACTIONS> = HVec::new();
777        on_press1.push(Action::cc(81, 127, 1).unwrap()).ok();
778        buttons1
779            .push(ButtonConfig {
780                label: Label::try_from("FX2").unwrap(),
781                color: LedConfig::default(),
782                mode: ButtonMode::Momentary,
783                on_press: on_press1,
784                on_release: HVec::new(),
785                on_long_press: HVec::new(),
786                cycle_values: HVec::new(),
787                listen_cc: None,
788            })
789            .ok();
790
791        let mut on_enter1: HVec<Action, MAX_ACTIONS> = HVec::new();
792        on_enter1.push(Action::program_change(1, 1).unwrap()).ok();
793
794        presets
795            .push(Preset {
796                name: Label::try_from("Preset1").unwrap(),
797                buttons: buttons1,
798                encoders: HVec::new(),
799                analog: HVec::new(),
800                defaults: Default::default(),
801                on_enter: on_enter1,
802                on_exit: HVec::new(),
803                triggers: HVec::new(),
804            })
805            .ok();
806
807        Config {
808            global: GlobalConfig::default(),
809            presets,
810        }
811    }
812
813    fn make_three_preset_config() -> Config {
814        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
815        for i in 0..3u8 {
816            let mut on_enter: HVec<Action, MAX_ACTIONS> = HVec::new();
817            on_enter.push(Action::program_change(i, 1).unwrap()).ok();
818            presets
819                .push(Preset {
820                    name: Label::try_from("P").unwrap(),
821                    buttons: HVec::new(),
822                    encoders: HVec::new(),
823                    analog: HVec::new(),
824                    defaults: Default::default(),
825                    on_enter,
826                    on_exit: HVec::new(),
827                    triggers: HVec::new(),
828                })
829                .ok();
830        }
831        Config {
832            global: GlobalConfig::default(),
833            presets,
834        }
835    }
836
837    fn momentary_button(
838        on_press: HVec<Action, MAX_ACTIONS>,
839        on_release: HVec<Action, MAX_ACTIONS>,
840    ) -> ButtonConfig {
841        ButtonConfig {
842            label: Label::new(),
843            color: LedConfig::default(),
844            mode: ButtonMode::Momentary,
845            on_press,
846            on_release,
847            on_long_press: HVec::new(),
848            cycle_values: HVec::new(),
849            listen_cc: None,
850        }
851    }
852
853    fn momentary_button_with_long_press(
854        on_press: HVec<Action, MAX_ACTIONS>,
855        on_long_press: HVec<Action, MAX_ACTIONS>,
856    ) -> ButtonConfig {
857        ButtonConfig {
858            label: Label::new(),
859            color: LedConfig::default(),
860            mode: ButtonMode::Momentary,
861            on_press,
862            on_release: HVec::new(),
863            on_long_press,
864            cycle_values: HVec::new(),
865            listen_cc: None,
866        }
867    }
868
869    fn has_midi_msg(output: &Output, expected: [u8; 3]) -> bool {
870        output
871            .midi
872            .iter()
873            .any(|step| matches!(step, ActionStep::Send(m) if m.data == expected))
874    }
875
876    // --- Test 1: Simple button press generates MIDI (no long-press configured) ---
877
878    #[test]
879    fn simple_button_press_generates_midi() {
880        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
881        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
882
883        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
884        buttons.push(momentary_button(on_press, HVec::new())).ok();
885
886        let config = make_config(buttons, HVec::new());
887        let mut ctrl = Controller::new();
888
889        let result = ctrl.process(
890            Event::ButtonEdge {
891                index: 0,
892                edge: Edge::Activate,
893            },
894            0,
895            &config,
896        );
897
898        assert!(has_midi_msg(&result, [0xB0, 80, 127]));
899        assert!(result.leds_changed);
900    }
901
902    // --- Test 2: Button release generates MIDI ---
903
904    #[test]
905    fn button_release_generates_midi() {
906        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
907        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
908        let mut on_release: HVec<Action, MAX_ACTIONS> = HVec::new();
909        on_release.push(Action::cc(80, 0, 1).unwrap()).ok();
910
911        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
912        buttons.push(momentary_button(on_press, on_release)).ok();
913
914        let config = make_config(buttons, HVec::new());
915        let mut ctrl = Controller::new();
916
917        // Press
918        ctrl.process(
919            Event::ButtonEdge {
920                index: 0,
921                edge: Edge::Activate,
922            },
923            0,
924            &config,
925        );
926
927        // Release
928        let result = ctrl.process(
929            Event::ButtonEdge {
930                index: 0,
931                edge: Edge::Deactivate,
932            },
933            100,
934            &config,
935        );
936
937        assert!(has_midi_msg(&result, [0xB0, 80, 0]));
938    }
939
940    // --- Test 3: Short press with long-press configured ---
941
942    #[test]
943    fn short_press_with_long_press_configured_fires_on_press_on_release() {
944        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
945        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
946        let mut on_long_press: HVec<Action, MAX_ACTIONS> = HVec::new();
947        on_long_press.push(Action::PresetNext).ok();
948
949        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
950        buttons
951            .push(momentary_button_with_long_press(on_press, on_long_press))
952            .ok();
953
954        let config = make_config(buttons, HVec::new());
955        let mut ctrl = Controller::new();
956
957        // Press at t=0
958        let result = ctrl.process(
959            Event::ButtonEdge {
960                index: 0,
961                edge: Edge::Activate,
962            },
963            0,
964            &config,
965        );
966        // With long-press configured, press is deferred — no MIDI yet
967        assert!(!has_midi_msg(&result, [0xB0, 80, 127]));
968
969        // Tick at t=200 (under 500ms threshold)
970        let result = ctrl.process(Event::Tick, 200, &config);
971        assert!(result.midi.is_empty());
972
973        // Release at t=300 (<500ms = short press)
974        let result = ctrl.process(
975            Event::ButtonEdge {
976                index: 0,
977                edge: Edge::Deactivate,
978            },
979            300,
980            &config,
981        );
982
983        // Short press fires on_press actions
984        assert!(has_midi_msg(&result, [0xB0, 80, 127]));
985    }
986
987    // --- Test 4: Long press fires on_long_press actions ---
988
989    #[test]
990    fn long_press_fires_on_long_press_actions() {
991        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
992        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
993        let mut on_long_press: HVec<Action, MAX_ACTIONS> = HVec::new();
994        on_long_press.push(Action::cc(99, 127, 2).unwrap()).ok();
995
996        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
997        buttons
998            .push(momentary_button_with_long_press(on_press, on_long_press))
999            .ok();
1000
1001        let config = make_config(buttons, HVec::new());
1002        let mut ctrl = Controller::new();
1003
1004        // Press at t=0
1005        ctrl.process(
1006            Event::ButtonEdge {
1007                index: 0,
1008                edge: Edge::Activate,
1009            },
1010            0,
1011            &config,
1012        );
1013
1014        // Tick at t=500 (threshold reached)
1015        let result = ctrl.process(Event::Tick, 500, &config);
1016
1017        // Long press fires on_long_press: CC#99 on ch2
1018        assert!(has_midi_msg(&result, [0xB1, 99, 127]));
1019    }
1020
1021    // --- Test 5: Long press suppresses short press ---
1022
1023    #[test]
1024    fn long_press_suppresses_short_press_on_release() {
1025        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1026        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
1027        let mut on_long_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1028        on_long_press.push(Action::cc(99, 127, 2).unwrap()).ok();
1029
1030        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1031        buttons
1032            .push(momentary_button_with_long_press(on_press, on_long_press))
1033            .ok();
1034
1035        let config = make_config(buttons, HVec::new());
1036        let mut ctrl = Controller::new();
1037
1038        // Press at t=0
1039        ctrl.process(
1040            Event::ButtonEdge {
1041                index: 0,
1042                edge: Edge::Activate,
1043            },
1044            0,
1045            &config,
1046        );
1047
1048        // Long press fires at t=500
1049        ctrl.process(Event::Tick, 500, &config);
1050
1051        // Release after long press
1052        let result = ctrl.process(
1053            Event::ButtonEdge {
1054                index: 0,
1055                edge: Edge::Deactivate,
1056            },
1057            600,
1058            &config,
1059        );
1060
1061        // Release should NOT fire on_press (suppressed)
1062        assert!(!has_midi_msg(&result, [0xB0, 80, 127]));
1063        assert!(result.midi.is_empty());
1064    }
1065
1066    // --- Test 6: Long press triggers preset switch ---
1067
1068    #[test]
1069    fn long_press_triggers_preset_switch() {
1070        let mut on_long_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1071        on_long_press.push(Action::PresetNext).ok();
1072
1073        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1074        buttons
1075            .push(momentary_button_with_long_press(HVec::new(), on_long_press))
1076            .ok();
1077
1078        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
1079        presets
1080            .push(Preset {
1081                name: Label::try_from("P1").unwrap(),
1082                buttons,
1083                encoders: HVec::new(),
1084                analog: HVec::new(),
1085                defaults: Default::default(),
1086                on_enter: HVec::new(),
1087                on_exit: HVec::new(),
1088                triggers: HVec::new(),
1089            })
1090            .ok();
1091        presets
1092            .push(Preset {
1093                name: Label::try_from("P2").unwrap(),
1094                buttons: HVec::new(),
1095                encoders: HVec::new(),
1096                analog: HVec::new(),
1097                defaults: Default::default(),
1098                on_enter: HVec::new(),
1099                on_exit: HVec::new(),
1100                triggers: HVec::new(),
1101            })
1102            .ok();
1103        let config: Config = Config {
1104            global: GlobalConfig::default(),
1105            presets,
1106        };
1107        let mut ctrl = Controller::new();
1108
1109        assert_eq!(ctrl.active_preset(), 0);
1110
1111        // Press at t=0
1112        ctrl.process(
1113            Event::ButtonEdge {
1114                index: 0,
1115                edge: Edge::Activate,
1116            },
1117            0,
1118            &config,
1119        );
1120
1121        // Long press fires at t=500 → PresetNext
1122        let result = ctrl.process(Event::Tick, 500, &config);
1123
1124        assert!(result.preset_changed);
1125        assert_eq!(ctrl.active_preset(), 1);
1126    }
1127
1128    // --- Test 7: Toggle state preserved across preset switch round-trip ---
1129
1130    #[test]
1131    fn toggle_state_preserved_across_preset_switch() {
1132        let config = make_two_preset_config();
1133        let mut ctrl = Controller::new();
1134
1135        // Toggle button 0 ON in preset 0 (no long-press → immediate fire)
1136        let result = ctrl.process(
1137            Event::ButtonEdge {
1138                index: 0,
1139                edge: Edge::Activate,
1140            },
1141            0,
1142            &config,
1143        );
1144        assert!(has_midi_msg(&result, [0xB0, 80, 127]));
1145        assert!(ctrl.button_states()[0]);
1146
1147        // Switch to preset 1
1148        ctrl.select_preset(1, &config);
1149        assert_eq!(ctrl.active_preset(), 1);
1150        // Button state is for preset 1 now
1151        assert!(!ctrl.button_states()[0]);
1152
1153        // Switch back to preset 0
1154        ctrl.select_preset(0, &config);
1155        assert_eq!(ctrl.active_preset(), 0);
1156        // Toggle state should be restored
1157        assert!(ctrl.button_states()[0]);
1158    }
1159
1160    // --- Test 8: Encoder turn generates CC ---
1161
1162    #[test]
1163    fn encoder_turn_generates_cc() {
1164        let mut encoders: HVec<EncoderConfig, MAX_ENCODERS> = HVec::new();
1165        encoders
1166            .push(EncoderConfig {
1167                label: Label::try_from("Vol").unwrap(),
1168                action: EncoderAction::Cc {
1169                    cc: 7,
1170                    channel: 1,
1171                    min: 0,
1172                    max: 127,
1173                },
1174                ..Default::default()
1175            })
1176            .ok();
1177
1178        let config = make_config(HVec::new(), encoders);
1179        let mut ctrl = Controller::new();
1180        ctrl.set_encoder_value(0, 64);
1181
1182        let result = ctrl.process(
1183            Event::EncoderTurn {
1184                index: 0,
1185                clockwise: true,
1186            },
1187            1000,
1188            &config,
1189        );
1190
1191        // First turn always gives 1 step (no prior timestamp for acceleration)
1192        assert!(has_midi_msg(&result, [0xB0, 7, 65]));
1193        assert_eq!(ctrl.encoder_values()[0], 65);
1194    }
1195
1196    // --- Test 9: Encoder acceleration (fast turns → bigger steps) ---
1197
1198    #[test]
1199    fn encoder_acceleration_fast_turns_bigger_steps() {
1200        let mut encoders: HVec<EncoderConfig, MAX_ENCODERS> = HVec::new();
1201        encoders
1202            .push(EncoderConfig {
1203                label: Label::try_from("Vol").unwrap(),
1204                action: EncoderAction::Cc {
1205                    cc: 7,
1206                    channel: 1,
1207                    min: 0,
1208                    max: 127,
1209                },
1210                ..Default::default()
1211            })
1212            .ok();
1213
1214        let config = make_config(HVec::new(), encoders);
1215        let mut ctrl = Controller::new();
1216        ctrl.set_encoder_value(0, 50);
1217
1218        // First turn at t=0 (initializes, returns 1 step)
1219        ctrl.process(
1220            Event::EncoderTurn {
1221                index: 0,
1222                clockwise: true,
1223            },
1224            0,
1225            &config,
1226        );
1227        assert_eq!(ctrl.encoder_values()[0], 51);
1228
1229        // Fast turn at t=30 (30ms interval → 4 steps from accel_curve)
1230        let result = ctrl.process(
1231            Event::EncoderTurn {
1232                index: 0,
1233                clockwise: true,
1234            },
1235            30,
1236            &config,
1237        );
1238        // 51 + 4 = 55
1239        assert_eq!(ctrl.encoder_values()[0], 55);
1240        assert!(has_midi_msg(&result, [0xB0, 7, 55]));
1241    }
1242
1243    // --- Test 10: Tap tempo via Action::TapTempo ---
1244
1245    #[test]
1246    fn tap_tempo_fires_bpm() {
1247        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1248        on_press.push(Action::TapTempo).ok();
1249
1250        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1251        buttons.push(momentary_button(on_press, HVec::new())).ok();
1252
1253        let config = make_config(buttons, HVec::new());
1254        let mut ctrl = Controller::new();
1255
1256        // First tap at t=0 — no BPM yet (need 4 taps)
1257        let result = ctrl.process(
1258            Event::ButtonEdge {
1259                index: 0,
1260                edge: Edge::Activate,
1261            },
1262            0,
1263            &config,
1264        );
1265        assert_eq!(result.bpm, None);
1266
1267        // Taps 2 and 3 — still no BPM
1268        for t in [500, 1000] {
1269            ctrl.process(
1270                Event::ButtonEdge {
1271                    index: 0,
1272                    edge: Edge::Deactivate,
1273                },
1274                t - 50,
1275                &config,
1276            );
1277            let result = ctrl.process(
1278                Event::ButtonEdge {
1279                    index: 0,
1280                    edge: Edge::Activate,
1281                },
1282                t,
1283                &config,
1284            );
1285            assert_eq!(result.bpm, None);
1286        }
1287
1288        // Fourth tap at t=1500 → 120 BPM
1289        ctrl.process(
1290            Event::ButtonEdge {
1291                index: 0,
1292                edge: Edge::Deactivate,
1293            },
1294            1450,
1295            &config,
1296        );
1297        let result = ctrl.process(
1298            Event::ButtonEdge {
1299                index: 0,
1300                edge: Edge::Activate,
1301            },
1302            1500,
1303            &config,
1304        );
1305        assert_eq!(result.bpm, Some(120));
1306    }
1307
1308    // --- Test 11: Midi triggers (CC trigger activates button) ---
1309
1310    #[test]
1311    fn incoming_midi_trigger_activates_button() {
1312        let mut triggers: HVec<Trigger, MAX_TRIGGERS> = HVec::new();
1313        triggers
1314            .push(Trigger {
1315                match_msg: TriggerMatch::Cc {
1316                    cc: 100,
1317                    channel: 1,
1318                    value_min: 64,
1319                    value_max: 127,
1320                },
1321                action: TriggerAction::Activate(0),
1322            })
1323            .ok();
1324
1325        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1326        buttons
1327            .push(ButtonConfig {
1328                label: Label::new(),
1329                color: LedConfig::default(),
1330                mode: ButtonMode::Toggle,
1331                on_press: HVec::new(),
1332                on_release: HVec::new(),
1333                on_long_press: HVec::new(),
1334                cycle_values: HVec::new(),
1335                listen_cc: None,
1336            })
1337            .ok();
1338
1339        let config =
1340            make_config_with_presets(buttons, HVec::new(), HVec::new(), HVec::new(), triggers);
1341        let mut ctrl = Controller::new();
1342
1343        assert!(!ctrl.button_states()[0]);
1344
1345        // Send CC#100 = 127 on channel 1
1346        let result = ctrl.process(
1347            Event::Midi {
1348                data: [0xB0, 100, 127, 0, 0, 0, 0, 0],
1349                len: 3,
1350                source: MidiPort::USB,
1351            },
1352            0,
1353            &config,
1354        );
1355
1356        assert!(ctrl.button_states()[0]);
1357        assert!(result.leds_changed);
1358    }
1359
1360    // --- Test 12: Tick is no-op when no button held ---
1361
1362    #[test]
1363    fn tick_is_noop_when_no_button_held() {
1364        let mut on_long_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1365        on_long_press.push(Action::cc(99, 127, 1).unwrap()).ok();
1366
1367        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1368        buttons
1369            .push(momentary_button_with_long_press(HVec::new(), on_long_press))
1370            .ok();
1371
1372        let config = make_config(buttons, HVec::new());
1373        let mut ctrl = Controller::new();
1374
1375        // Tick without any button held
1376        let result = ctrl.process(Event::Tick, 1000, &config);
1377
1378        assert!(result.midi.is_empty());
1379        assert!(!result.leds_changed);
1380        assert!(!result.preset_changed);
1381        assert_eq!(result.bpm, None);
1382    }
1383
1384    // --- Test 13: select_preset fires on_enter MIDI ---
1385
1386    #[test]
1387    fn select_preset_fires_on_enter_midi() {
1388        let config = make_two_preset_config();
1389        let mut ctrl = Controller::new();
1390
1391        // Select preset 1 (has on_enter: PC#1 on ch1)
1392        let result = ctrl.select_preset(1, &config);
1393
1394        assert!(result.preset_changed);
1395        assert_eq!(ctrl.active_preset(), 1);
1396        // on_enter for preset 1: Program Change 1 on channel 1
1397        assert!(has_midi_msg(&result, [0xC0, 1, 0]));
1398    }
1399
1400    // --- Test 14: button_held() tracks press/release ---
1401
1402    #[test]
1403    fn button_held_tracks_press_release() {
1404        // Must use a button with long-press configured (long-press detector tracks active state)
1405        let mut on_long_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1406        on_long_press.push(Action::cc(99, 127, 1).unwrap()).ok();
1407
1408        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1409        buttons
1410            .push(momentary_button_with_long_press(HVec::new(), on_long_press))
1411            .ok();
1412
1413        let config = make_config(buttons, HVec::new());
1414        let mut ctrl = Controller::new();
1415
1416        assert!(!ctrl.button_held());
1417
1418        // Press
1419        ctrl.process(
1420            Event::ButtonEdge {
1421                index: 0,
1422                edge: Edge::Activate,
1423            },
1424            0,
1425            &config,
1426        );
1427        assert!(ctrl.button_held());
1428
1429        // Release
1430        ctrl.process(
1431            Event::ButtonEdge {
1432                index: 0,
1433                edge: Edge::Deactivate,
1434            },
1435            100,
1436            &config,
1437        );
1438        assert!(!ctrl.button_held());
1439    }
1440
1441    // --- Test 15: Preset prev wraps around ---
1442
1443    #[test]
1444    fn preset_prev_wraps_around() {
1445        // Build config with PresetPrev button in all 3 presets
1446        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
1447        for i in 0..3u8 {
1448            let mut btn_on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1449            btn_on_press.push(Action::PresetPrev).ok();
1450            let mut btns: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1451            btns.push(momentary_button(btn_on_press, HVec::new())).ok();
1452
1453            let mut on_enter: HVec<Action, MAX_ACTIONS> = HVec::new();
1454            on_enter.push(Action::program_change(i, 1).unwrap()).ok();
1455
1456            presets
1457                .push(Preset {
1458                    name: Label::try_from("P").unwrap(),
1459                    buttons: btns,
1460                    encoders: HVec::new(),
1461                    analog: HVec::new(),
1462                    defaults: Default::default(),
1463                    on_enter,
1464                    on_exit: HVec::new(),
1465                    triggers: HVec::new(),
1466                })
1467                .ok();
1468        }
1469        let config: Config = Config {
1470            global: GlobalConfig::default(),
1471            presets,
1472        };
1473        let mut ctrl = Controller::new();
1474
1475        assert_eq!(ctrl.active_preset(), 0);
1476
1477        // Press button → PresetPrev wraps from 0 → 2
1478        let result = ctrl.process(
1479            Event::ButtonEdge {
1480                index: 0,
1481                edge: Edge::Activate,
1482            },
1483            0,
1484            &config,
1485        );
1486
1487        assert!(result.preset_changed);
1488        assert_eq!(ctrl.active_preset(), 2);
1489    }
1490
1491    // --- Additional edge-case tests ---
1492
1493    #[test]
1494    fn button_held_false_for_button_without_long_press() {
1495        // A button without on_long_press doesn't use the long-press detector
1496        // in "active" mode, so button_held() should remain false.
1497        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1498        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
1499
1500        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1501        buttons.push(momentary_button(on_press, HVec::new())).ok();
1502
1503        let config = make_config(buttons, HVec::new());
1504        let mut ctrl = Controller::new();
1505
1506        ctrl.process(
1507            Event::ButtonEdge {
1508                index: 0,
1509                edge: Edge::Activate,
1510            },
1511            0,
1512            &config,
1513        );
1514
1515        // Without long-press, the detector doesn't track "active" — button_held returns false
1516        assert!(!ctrl.button_held());
1517    }
1518
1519    #[test]
1520    fn select_preset_fires_on_exit_of_old_preset() {
1521        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
1522
1523        let mut on_exit: HVec<Action, MAX_ACTIONS> = HVec::new();
1524        on_exit.push(Action::cc(123, 0, 1).unwrap()).ok(); // All Notes Off
1525
1526        presets
1527            .push(Preset {
1528                name: Label::try_from("P1").unwrap(),
1529                buttons: HVec::new(),
1530                encoders: HVec::new(),
1531                analog: HVec::new(),
1532                defaults: Default::default(),
1533                on_enter: HVec::new(),
1534                on_exit,
1535                triggers: HVec::new(),
1536            })
1537            .ok();
1538
1539        let mut on_enter: HVec<Action, MAX_ACTIONS> = HVec::new();
1540        on_enter.push(Action::program_change(5, 1).unwrap()).ok();
1541
1542        presets
1543            .push(Preset {
1544                name: Label::try_from("P2").unwrap(),
1545                buttons: HVec::new(),
1546                encoders: HVec::new(),
1547                analog: HVec::new(),
1548                defaults: Default::default(),
1549                on_enter,
1550                on_exit: HVec::new(),
1551                triggers: HVec::new(),
1552            })
1553            .ok();
1554
1555        let config: Config = Config {
1556            global: GlobalConfig::default(),
1557            presets,
1558        };
1559        let mut ctrl = Controller::new();
1560
1561        let result = ctrl.select_preset(1, &config);
1562
1563        // on_exit of preset 0 should fire: CC#123=0 (All Notes Off)
1564        assert!(has_midi_msg(&result, [0xB0, 123, 0]));
1565        // on_enter of preset 1 should fire: PC#5
1566        assert!(has_midi_msg(&result, [0xC0, 5, 0]));
1567    }
1568
1569    #[test]
1570    fn encoder_values_persist_across_preset_switch() {
1571        let mut encoders: HVec<EncoderConfig, MAX_ENCODERS> = HVec::new();
1572        encoders
1573            .push(EncoderConfig {
1574                label: Label::try_from("Vol").unwrap(),
1575                action: EncoderAction::Cc {
1576                    cc: 7,
1577                    channel: 1,
1578                    min: 0,
1579                    max: 127,
1580                },
1581                ..Default::default()
1582            })
1583            .ok();
1584
1585        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
1586        presets
1587            .push(Preset {
1588                name: Label::try_from("P1").unwrap(),
1589                buttons: HVec::new(),
1590                encoders: encoders.clone(),
1591                analog: HVec::new(),
1592                defaults: Default::default(),
1593                on_enter: HVec::new(),
1594                on_exit: HVec::new(),
1595                triggers: HVec::new(),
1596            })
1597            .ok();
1598        presets
1599            .push(Preset {
1600                name: Label::try_from("P2").unwrap(),
1601                buttons: HVec::new(),
1602                encoders,
1603                analog: HVec::new(),
1604                defaults: Default::default(),
1605                on_enter: HVec::new(),
1606                on_exit: HVec::new(),
1607                triggers: HVec::new(),
1608            })
1609            .ok();
1610
1611        let config: Config = Config {
1612            global: GlobalConfig::default(),
1613            presets,
1614        };
1615        let mut ctrl = Controller::new();
1616        ctrl.set_encoder_value(0, 80);
1617
1618        // Switch to preset 1
1619        ctrl.select_preset(1, &config);
1620        assert_eq!(ctrl.encoder_values()[0], 0); // fresh preset 1 state
1621
1622        // Switch back to preset 0
1623        ctrl.select_preset(0, &config);
1624        assert_eq!(ctrl.encoder_values()[0], 80); // restored
1625    }
1626
1627    #[test]
1628    fn snapshot_store_captures_current_state() {
1629        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1630        on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
1631
1632        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1633        buttons
1634            .push(ButtonConfig {
1635                label: Label::new(),
1636                color: LedConfig::default(),
1637                mode: ButtonMode::Toggle,
1638                on_press,
1639                on_release: HVec::new(),
1640                on_long_press: HVec::new(),
1641                cycle_values: HVec::new(),
1642                listen_cc: None,
1643            })
1644            .ok();
1645
1646        let config = make_config(buttons, HVec::new());
1647        let mut ctrl = Controller::new();
1648        ctrl.set_encoder_value(0, 42);
1649
1650        // Toggle button on
1651        ctrl.process(
1652            Event::ButtonEdge {
1653                index: 0,
1654                edge: Edge::Activate,
1655            },
1656            0,
1657            &config,
1658        );
1659
1660        let store = ctrl.snapshot_store();
1661        let state = store.current();
1662        assert!(state.button_active[0]);
1663        assert_eq!(state.encoder_values[0], 42);
1664    }
1665
1666    #[test]
1667    fn incoming_midi_trigger_below_value_min_no_match() {
1668        let mut triggers: HVec<Trigger, MAX_TRIGGERS> = HVec::new();
1669        triggers
1670            .push(Trigger {
1671                match_msg: TriggerMatch::Cc {
1672                    cc: 100,
1673                    channel: 1,
1674                    value_min: 64,
1675                    value_max: 127,
1676                },
1677                action: TriggerAction::Activate(0),
1678            })
1679            .ok();
1680
1681        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1682        buttons
1683            .push(ButtonConfig {
1684                label: Label::new(),
1685                color: LedConfig::default(),
1686                mode: ButtonMode::Toggle,
1687                on_press: HVec::new(),
1688                on_release: HVec::new(),
1689                on_long_press: HVec::new(),
1690                cycle_values: HVec::new(),
1691                listen_cc: None,
1692            })
1693            .ok();
1694
1695        let config =
1696            make_config_with_presets(buttons, HVec::new(), HVec::new(), HVec::new(), triggers);
1697        let mut ctrl = Controller::new();
1698
1699        // Send CC#100 = 63 (below value_min=64)
1700        let result = ctrl.process(
1701            Event::Midi {
1702                data: [0xB0, 100, 63, 0, 0, 0, 0, 0],
1703                len: 3,
1704                source: MidiPort::USB,
1705            },
1706            0,
1707            &config,
1708        );
1709
1710        assert!(!ctrl.button_states()[0]);
1711        assert!(!result.leds_changed);
1712    }
1713
1714    #[test]
1715    fn preset_next_wraps_around() {
1716        let config = make_three_preset_config();
1717        let mut ctrl = Controller::new();
1718
1719        // Go to preset 2 first
1720        ctrl.select_preset(2, &config);
1721        assert_eq!(ctrl.active_preset(), 2);
1722
1723        // Now process PresetNext via a button
1724        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1725        on_press.push(Action::PresetNext).ok();
1726
1727        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1728        buttons.push(momentary_button(on_press, HVec::new())).ok();
1729
1730        let mut presets: HVec<Preset, MAX_PRESETS> = HVec::new();
1731        for _i in 0..3u8 {
1732            let mut btns: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1733            let mut bp: HVec<Action, MAX_ACTIONS> = HVec::new();
1734            bp.push(Action::PresetNext).ok();
1735            btns.push(momentary_button(bp, HVec::new())).ok();
1736            presets
1737                .push(Preset {
1738                    name: Label::try_from("P").unwrap(),
1739                    buttons: btns,
1740                    encoders: HVec::new(),
1741                    analog: HVec::new(),
1742                    defaults: Default::default(),
1743                    on_enter: HVec::new(),
1744                    on_exit: HVec::new(),
1745                    triggers: HVec::new(),
1746                })
1747                .ok();
1748        }
1749        let config2 = Config {
1750            global: GlobalConfig::default(),
1751            presets,
1752        };
1753        let mut ctrl2 = Controller::new();
1754        ctrl2.select_preset(2, &config2);
1755
1756        let result = ctrl2.process(
1757            Event::ButtonEdge {
1758                index: 0,
1759                edge: Edge::Activate,
1760            },
1761            0,
1762            &config2,
1763        );
1764
1765        assert!(result.preset_changed);
1766        assert_eq!(ctrl2.active_preset(), 0); // wraps from 2 → 0
1767    }
1768
1769    #[test]
1770    fn mon_led_green_on_midi_output() {
1771        let mut on_press: HVec<Action, MAX_ACTIONS> = HVec::new();
1772        on_press.push(Action::cc(10, 127, 1).unwrap()).ok();
1773        let mut buttons: HVec<ButtonConfig, MAX_BUTTONS> = HVec::new();
1774        buttons.push(momentary_button(on_press, HVec::new())).ok();
1775        let config = make_config(buttons, HVec::new());
1776        let mut ctrl = Controller::new();
1777
1778        let result = ctrl.process(
1779            Event::ButtonEdge {
1780                index: 0,
1781                edge: Edge::Activate,
1782            },
1783            100,
1784            &config,
1785        );
1786        assert!(!result.midi.is_empty());
1787        assert_eq!(result.mon_led, Some(crate::led::Rgb::new(0, 255, 0)));
1788    }
1789
1790    #[test]
1791    fn mon_led_blue_on_incoming_midi() {
1792        let config = make_config(HVec::new(), HVec::new());
1793        let mut ctrl = Controller::new();
1794
1795        let result = ctrl.process(
1796            Event::Midi {
1797                data: [0xB0, 10, 127, 0, 0, 0, 0, 0],
1798                len: 3,
1799                source: MidiPort::USB,
1800            },
1801            100,
1802            &config,
1803        );
1804        assert_eq!(result.mon_led, Some(crate::led::Rgb::new(0, 0, 255)));
1805    }
1806
1807    #[test]
1808    fn mode_led_set_on_preset_change() {
1809        let config = make_three_preset_config();
1810        let mut ctrl = Controller::new();
1811
1812        let result = ctrl.select_preset(1, &config);
1813        assert!(result.preset_changed);
1814        // Preset 1 = green (index 1 in the color table)
1815        assert_eq!(result.mode_led, Some(crate::led::Rgb::new(0, 255, 0)));
1816    }
1817
1818    #[test]
1819    fn mon_led_none_on_tick() {
1820        let config = make_config(HVec::new(), HVec::new());
1821        let mut ctrl = Controller::new();
1822        let result = ctrl.process(Event::Tick, 100, &config);
1823        assert_eq!(result.mon_led, None);
1824        assert_eq!(result.mode_led, None);
1825    }
1826
1827    // --- Test: Thru routing (DIN → USB when enabled) ---
1828    #[test]
1829    fn thru_din_to_usb_enabled() {
1830        let mut ctrl = Controller::<6, 2>::new();
1831        let mut config: Config = Config::default();
1832        config.global.din_to_usb_thru = true;
1833        config.presets.push(Preset::default()).ok();
1834
1835        let result = ctrl.process(
1836            Event::Midi {
1837                data: [0x90, 60, 100, 0, 0, 0, 0, 0],
1838                len: 3,
1839                source: MidiPort::DIN,
1840            },
1841            0,
1842            &config,
1843        );
1844
1845        assert_eq!(result.midi_out.len(), 1);
1846        assert_eq!(result.midi_out[0].bytes(), &[0x90, 60, 100]);
1847        assert_eq!(result.midi_out[0].dest, MidiPort::USB);
1848    }
1849
1850    #[test]
1851    fn thru_din_to_usb_disabled() {
1852        let mut ctrl = Controller::<6, 2>::new();
1853        let mut config: Config = Config::default();
1854        config.global.din_to_usb_thru = false;
1855        config.presets.push(Preset::default()).ok();
1856
1857        let result = ctrl.process(
1858            Event::Midi {
1859                data: [0x90, 60, 100, 0, 0, 0, 0, 0],
1860                len: 3,
1861                source: MidiPort::DIN,
1862            },
1863            0,
1864            &config,
1865        );
1866
1867        assert!(result.midi_out.is_empty());
1868    }
1869
1870    #[test]
1871    fn thru_usb_to_din_enabled() {
1872        let mut ctrl = Controller::<6, 2>::new();
1873        let mut config: Config = Config::default();
1874        config.global.usb_to_din_thru = true;
1875        config.presets.push(Preset::default()).ok();
1876
1877        let result = ctrl.process(
1878            Event::Midi {
1879                data: [0xB0, 7, 64, 0, 0, 0, 0, 0],
1880                len: 3,
1881                source: MidiPort::USB,
1882            },
1883            0,
1884            &config,
1885        );
1886
1887        assert_eq!(result.midi_out.len(), 1);
1888        assert!(result.midi_out[0].dest.contains(MidiPort::DIN));
1889        assert!(!result.midi_out[0].dest.contains(MidiPort::USB));
1890    }
1891
1892    #[test]
1893    fn thru_usb_to_both() {
1894        let mut ctrl = Controller::<6, 2>::new();
1895        let mut config: Config = Config::default();
1896        config.global.usb_to_din_thru = true;
1897        config.global.usb_to_usb_thru = true;
1898        config.presets.push(Preset::default()).ok();
1899
1900        let result = ctrl.process(
1901            Event::Midi {
1902                data: [0xB0, 1, 127, 0, 0, 0, 0, 0],
1903                len: 3,
1904                source: MidiPort::USB,
1905            },
1906            0,
1907            &config,
1908        );
1909
1910        assert_eq!(result.midi_out.len(), 1);
1911        assert!(result.midi_out[0].dest.contains(MidiPort::DIN));
1912        assert!(result.midi_out[0].dest.contains(MidiPort::USB));
1913    }
1914
1915    #[test]
1916    fn thru_no_routing_when_all_disabled() {
1917        let mut ctrl = Controller::<6, 2>::new();
1918        let mut config: Config = Config::default();
1919        config.global.din_to_usb_thru = false;
1920        config.global.usb_to_din_thru = false;
1921        config.global.usb_to_usb_thru = false;
1922        config.presets.push(Preset::default()).ok();
1923
1924        let result = ctrl.process(
1925            Event::Midi {
1926                data: [0x90, 48, 80, 0, 0, 0, 0, 0],
1927                len: 3,
1928                source: MidiPort::DIN,
1929            },
1930            0,
1931            &config,
1932        );
1933
1934        assert!(result.midi_out.is_empty());
1935    }
1936
1937    #[test]
1938    fn incoming_midi_sets_mon_led_blue() {
1939        let mut ctrl = Controller::<6, 2>::new();
1940        let mut config: Config = Config::default();
1941        config.presets.push(Preset::default()).ok();
1942
1943        let result = ctrl.process(
1944            Event::Midi {
1945                data: [0x90, 60, 100, 0, 0, 0, 0, 0],
1946                len: 3,
1947                source: MidiPort::USB,
1948            },
1949            0,
1950            &config,
1951        );
1952
1953        // No triggers → no MIDI output → Mon LED should be blue (incoming processed)
1954        assert_eq!(result.mon_led, Some(crate::led::Rgb::new(0, 0, 255)));
1955    }
1956
1957    #[test]
1958    fn reactive_led_heatmap_on_matching_cc() {
1959        use crate::config::{ButtonConfig, ButtonMode, Label, LedConfig, ListenCc, ListenMode};
1960        use crate::engine::ReactiveResult;
1961        use heapless::Vec as HVec;
1962
1963        let mut ctrl = Controller::<6, 2>::new();
1964        let mut config: Config = Config::default();
1965        let mut preset = Preset::default();
1966        preset.name = heapless::String::try_from("Test").unwrap();
1967        let btn = ButtonConfig {
1968            label: Label::new(),
1969            color: LedConfig::default(),
1970            mode: ButtonMode::Toggle,
1971            on_press: HVec::new(),
1972            on_release: HVec::new(),
1973            on_long_press: HVec::new(),
1974            cycle_values: HVec::new(),
1975            listen_cc: Some(ListenCc {
1976                cc: 7,
1977                channel: 1,
1978                mode: ListenMode::Heatmap,
1979                threshold: 64,
1980            }),
1981        };
1982        preset.buttons.push(btn).ok();
1983        config.presets.push(preset).ok();
1984
1985        let result = ctrl.process(
1986            Event::Midi {
1987                data: [0xB0, 7, 127, 0, 0, 0, 0, 0],
1988                len: 3,
1989                source: MidiPort::DIN,
1990            },
1991            0,
1992            &config,
1993        );
1994
1995        assert_eq!(result.reactive_led, Some(ReactiveResult::Heatmap(0, 12)));
1996    }
1997
1998    #[test]
1999    fn reactive_led_trigger_on_matching_cc() {
2000        use crate::config::{ButtonConfig, ButtonMode, Label, LedConfig, ListenCc, ListenMode};
2001        use crate::engine::ReactiveResult;
2002        use heapless::Vec as HVec;
2003
2004        let mut ctrl = Controller::<6, 2>::new();
2005        let mut config: Config = Config::default();
2006        let mut preset = Preset::default();
2007        preset.name = heapless::String::try_from("Test").unwrap();
2008        let btn = ButtonConfig {
2009            label: Label::new(),
2010            color: LedConfig::default(),
2011            mode: ButtonMode::Toggle,
2012            on_press: HVec::new(),
2013            on_release: HVec::new(),
2014            on_long_press: HVec::new(),
2015            cycle_values: HVec::new(),
2016            listen_cc: Some(ListenCc {
2017                cc: 50,
2018                channel: 2,
2019                mode: ListenMode::Trigger,
2020                threshold: 64,
2021            }),
2022        };
2023        preset.buttons.push(btn).ok();
2024        config.presets.push(preset).ok();
2025
2026        // Value above threshold → active
2027        let result = ctrl.process(
2028            Event::Midi {
2029                data: [0xB1, 50, 100, 0, 0, 0, 0, 0],
2030                len: 3,
2031                source: MidiPort::USB,
2032            },
2033            0,
2034            &config,
2035        );
2036        assert_eq!(result.reactive_led, Some(ReactiveResult::Trigger(0, true)));
2037
2038        // Value below threshold → inactive
2039        let result = ctrl.process(
2040            Event::Midi {
2041                data: [0xB1, 50, 10, 0, 0, 0, 0, 0],
2042                len: 3,
2043                source: MidiPort::USB,
2044            },
2045            0,
2046            &config,
2047        );
2048        assert_eq!(result.reactive_led, Some(ReactiveResult::Trigger(0, false)));
2049    }
2050
2051    #[test]
2052    fn no_reactive_led_on_non_cc() {
2053        let mut ctrl = Controller::<6, 2>::new();
2054        let mut config: Config = Config::default();
2055        config.presets.push(Preset::default()).ok();
2056
2057        // Note On — not a CC, no reactive LED
2058        let result = ctrl.process(
2059            Event::Midi {
2060                data: [0x90, 60, 127, 0, 0, 0, 0, 0],
2061                len: 3,
2062                source: MidiPort::DIN,
2063            },
2064            0,
2065            &config,
2066        );
2067        assert_eq!(result.reactive_led, None);
2068    }
2069}