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 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
91pub 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
112pub 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 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 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 self.handle_system_actions(&mut result, now_ms, config);
171
172 if !result.midi.is_empty() {
174 result.mon_led = Some(crate::led::Rgb::new(0, 255, 0));
176 } else if matches!(event, Event::Midi { .. }) {
177 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 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 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 for s in &trigger_result.system {
257 self.execute_system_action(*s, &mut result, 0, config);
258 }
259 }
260
261 result
262 }
263
264 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 pub fn button_held(&self) -> bool {
287 self.long_press.iter().any(|lp| lp.is_active())
288 }
289
290 pub fn button_states(&self) -> &[bool; B] {
292 &self.button_active
293 }
294
295 pub fn active_preset(&self) -> u8 {
297 self.active_preset
298 }
299
300 pub fn encoder_values(&self) -> [u8; E] {
302 self.state_store.current().encoder_values
303 }
304
305 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 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 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 fn preset_color(index: u8) -> crate::led::Rgb {
340 match index % 8 {
341 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), }
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 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 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 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 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 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 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 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 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]
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]
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 ctrl.process(
911 Event::ButtonEdge {
912 index: 0,
913 edge: Edge::Activate,
914 },
915 0,
916 &config,
917 );
918
919 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]
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 let result = ctrl.process(
951 Event::ButtonEdge {
952 index: 0,
953 edge: Edge::Activate,
954 },
955 0,
956 &config,
957 );
958 assert!(!has_midi_msg(&result, [0xB0, 80, 127]));
960
961 let result = ctrl.process(Event::Tick, 200, &config);
963 assert!(result.midi.is_empty());
964
965 let result = ctrl.process(
967 Event::ButtonEdge {
968 index: 0,
969 edge: Edge::Deactivate,
970 },
971 300,
972 &config,
973 );
974
975 assert!(has_midi_msg(&result, [0xB0, 80, 127]));
977 }
978
979 #[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 ctrl.process(
998 Event::ButtonEdge {
999 index: 0,
1000 edge: Edge::Activate,
1001 },
1002 0,
1003 &config,
1004 );
1005
1006 let result = ctrl.process(Event::Tick, 500, &config);
1008
1009 assert!(has_midi_msg(&result, [0xB1, 99, 127]));
1011 }
1012
1013 #[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 ctrl.process(
1032 Event::ButtonEdge {
1033 index: 0,
1034 edge: Edge::Activate,
1035 },
1036 0,
1037 &config,
1038 );
1039
1040 ctrl.process(Event::Tick, 500, &config);
1042
1043 let result = ctrl.process(
1045 Event::ButtonEdge {
1046 index: 0,
1047 edge: Edge::Deactivate,
1048 },
1049 600,
1050 &config,
1051 );
1052
1053 assert!(!has_midi_msg(&result, [0xB0, 80, 127]));
1055 assert!(result.midi.is_empty());
1056 }
1057
1058 #[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 ctrl.process(
1105 Event::ButtonEdge {
1106 index: 0,
1107 edge: Edge::Activate,
1108 },
1109 0,
1110 &config,
1111 );
1112
1113 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]
1123 fn toggle_state_preserved_across_preset_switch() {
1124 let config = make_two_preset_config();
1125 let mut ctrl = Controller::new();
1126
1127 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 ctrl.select_preset(1, &config);
1141 assert_eq!(ctrl.active_preset(), 1);
1142 assert!(!ctrl.button_states()[0]);
1144
1145 ctrl.select_preset(0, &config);
1147 assert_eq!(ctrl.active_preset(), 0);
1148 assert!(ctrl.button_states()[0]);
1150 }
1151
1152 #[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 assert!(has_midi_msg(&result, [0xB0, 7, 65]));
1185 assert_eq!(ctrl.encoder_values()[0], 65);
1186 }
1187
1188 #[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 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 let result = ctrl.process(
1223 Event::EncoderTurn {
1224 index: 0,
1225 clockwise: true,
1226 },
1227 30,
1228 &config,
1229 );
1230 assert_eq!(ctrl.encoder_values()[0], 55);
1232 assert!(has_midi_msg(&result, [0xB0, 7, 55]));
1233 }
1234
1235 #[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 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 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 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]
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 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]
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 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]
1379 fn select_preset_fires_on_enter_midi() {
1380 let config = make_two_preset_config();
1381 let mut ctrl = Controller::new();
1382
1383 let result = ctrl.select_preset(1, &config);
1385
1386 assert!(result.preset_changed);
1387 assert_eq!(ctrl.active_preset(), 1);
1388 assert!(has_midi_msg(&result, [0xC0, 1, 0]));
1390 }
1391
1392 #[test]
1395 fn button_held_tracks_press_release() {
1396 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 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 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]
1436 fn preset_prev_wraps_around() {
1437 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 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 #[test]
1486 fn button_held_false_for_button_without_long_press() {
1487 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 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(); 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 assert!(has_midi_msg(&result, [0xB0, 123, 0]));
1557 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 ctrl.select_preset(1, &config);
1612 assert_eq!(ctrl.encoder_values()[0], 0); ctrl.select_preset(0, &config);
1616 assert_eq!(ctrl.encoder_values()[0], 80); }
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 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 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 ctrl.select_preset(2, &config);
1713 assert_eq!(ctrl.active_preset(), 2);
1714
1715 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); }
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 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]
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 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 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 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 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}