Skip to main content

midi_controller/
controller.rs

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