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