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