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