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