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