1use crate::action::{action_to_midi, analog_cc, encoder_cc, EncoderDirection, MidiMessage};
5use crate::config::{
6 Action, ButtonMode, EncoderAction, Label, Preset, MAX_ACTIONS, MAX_CYCLE_VALUES,
7};
8use crate::state::PresetState;
9
10const NUM_BUTTONS: usize = 6;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ButtonEvent {
15 Press,
17 Release,
19 LongPress,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SystemAction {
26 PresetNext,
27 PresetPrev,
28 PresetSelect(u8),
29 SetBpm(u16),
30 TapTempo,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum DisplaySide {
36 L,
37 R,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum DisplayEvent {
43 EncoderOverlay {
44 side: DisplaySide,
45 label: Label,
46 value: u8,
47 },
48 AnalogOverlay {
49 side: DisplaySide,
50 label: Label,
51 value: u8,
52 },
53 LongPressHint { action: SystemAction },
55 LongPressCancel,
57 BpmOverlay { bpm: u16 },
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum ActionStep {
64 Send(MidiMessage),
65 Delay(u16),
66 SetLed {
67 color: crate::config::Color,
68 animation: crate::config::LedAnimation,
69 },
70}
71
72pub struct EngineResult {
74 pub midi: heapless::Vec<ActionStep, 8>,
75 pub system: heapless::Vec<SystemAction, 2>,
76 pub display: heapless::Vec<DisplayEvent, 2>,
77 pub led_dirty: bool,
78}
79
80impl EngineResult {
81 fn new() -> Self {
82 Self {
83 midi: heapless::Vec::new(),
84 system: heapless::Vec::new(),
85 display: heapless::Vec::new(),
86 led_dirty: false,
87 }
88 }
89}
90
91pub fn process_button(
93 state: &mut PresetState,
94 preset: &Preset,
95 btn_idx: usize,
96 event: ButtonEvent,
97) -> EngineResult {
98 let mut result = EngineResult::new();
99 let Some(btn) = preset.buttons.get(btn_idx) else {
100 return result;
101 };
102 let mode = &btn.mode;
103
104 match event {
105 ButtonEvent::Press => match mode {
106 ButtonMode::Toggle => {
107 state.button_active[btn_idx] = !state.button_active[btn_idx];
108 result.led_dirty = true;
109 if state.button_active[btn_idx] {
110 execute_actions(
111 &btn.on_press,
112 &btn.cycle_values,
113 &mut result.midi,
114 &mut result.system,
115 &mut state.cycle_index[btn_idx],
116 );
117 } else {
118 execute_actions(
119 &btn.on_release,
120 &btn.cycle_values,
121 &mut result.midi,
122 &mut result.system,
123 &mut state.cycle_index[btn_idx],
124 );
125 }
126 }
127 ButtonMode::Momentary => {
128 state.button_active[btn_idx] = true;
129 result.led_dirty = true;
130 execute_actions(
131 &btn.on_press,
132 &btn.cycle_values,
133 &mut result.midi,
134 &mut result.system,
135 &mut state.cycle_index[btn_idx],
136 );
137 }
138 ButtonMode::RadioGroup(group) => {
139 for j in 0..NUM_BUTTONS {
140 if j != btn_idx {
141 if let Some(other) = preset.buttons.get(j) {
142 if other.mode == ButtonMode::RadioGroup(*group) {
143 state.button_active[j] = false;
144 }
145 }
146 }
147 }
148 state.button_active[btn_idx] = true;
149 result.led_dirty = true;
150 execute_actions(
151 &btn.on_press,
152 &btn.cycle_values,
153 &mut result.midi,
154 &mut result.system,
155 &mut state.cycle_index[btn_idx],
156 );
157 }
158 },
159 ButtonEvent::Release => {
160 if matches!(mode, ButtonMode::Momentary) {
161 state.button_active[btn_idx] = false;
162 result.led_dirty = true;
163 execute_actions(
164 &btn.on_release,
165 &btn.cycle_values,
166 &mut result.midi,
167 &mut result.system,
168 &mut state.cycle_index[btn_idx],
169 );
170 }
171 }
172 ButtonEvent::LongPress => {
173 execute_actions(
174 &btn.on_long_press,
175 &btn.cycle_values,
176 &mut result.midi,
177 &mut result.system,
178 &mut state.cycle_index[btn_idx],
179 );
180 }
181 }
182
183 result
184}
185
186pub fn process_encoder(
188 state: &mut PresetState,
189 preset: &Preset,
190 enc_idx: usize,
191 direction: EncoderDirection,
192 steps: u8,
193) -> EngineResult {
194 let mut result = EngineResult::new();
195 let Some(enc) = preset.encoders.get(enc_idx) else {
196 return result;
197 };
198
199 for _ in 0..steps {
200 encoder_cc(
201 preset,
202 enc_idx,
203 direction,
204 &mut state.encoder_values[enc_idx],
205 );
206 }
207
208 match &enc.action {
209 EncoderAction::Cc { cc, channel, .. } => {
210 result
211 .midi
212 .push(ActionStep::Send(MidiMessage {
213 data: [
214 0xB0 | (channel - 1),
215 *cc as u8,
216 state.encoder_values[enc_idx],
217 ],
218 len: 3,
219 }))
220 .ok();
221 }
222 EncoderAction::CcRelative {
223 cc,
224 channel,
225 increment,
226 decrement,
227 } => {
228 let val = match direction {
229 EncoderDirection::Clockwise => *increment,
230 EncoderDirection::CounterClockwise => *decrement,
231 };
232 result
233 .midi
234 .push(ActionStep::Send(MidiMessage {
235 data: [0xB0 | (channel - 1), *cc, val],
236 len: 3,
237 }))
238 .ok();
239 }
240 EncoderAction::PresetScroll => match direction {
241 EncoderDirection::Clockwise => {
242 result.system.push(SystemAction::PresetNext).ok();
243 }
244 EncoderDirection::CounterClockwise => {
245 result.system.push(SystemAction::PresetPrev).ok();
246 }
247 },
248 }
249
250 let side = if enc_idx == 0 {
251 DisplaySide::L
252 } else {
253 DisplaySide::R
254 };
255 result
256 .display
257 .push(DisplayEvent::EncoderOverlay {
258 side,
259 label: enc.label.clone(),
260 value: state.encoder_values[enc_idx],
261 })
262 .ok();
263 result.led_dirty = true;
264
265 result
266}
267
268pub fn process_analog(
270 preset: &Preset,
271 analog_idx: usize,
272 raw: u16,
273 adc_min: u16,
274 adc_max: u16,
275) -> EngineResult {
276 let mut result = EngineResult::new();
277 if let Some(msg) = analog_cc(preset, analog_idx, raw, adc_min, adc_max) {
278 let side = if analog_idx == 0 {
279 DisplaySide::L
280 } else {
281 DisplaySide::R
282 };
283 let label = preset
284 .analog
285 .get(analog_idx)
286 .map(|a| a.label.clone())
287 .unwrap_or_default();
288 result
289 .display
290 .push(DisplayEvent::AnalogOverlay {
291 side,
292 label,
293 value: msg.data[2],
294 })
295 .ok();
296 result.midi.push(ActionStep::Send(msg)).ok();
297 }
298 result
299}
300
301fn execute_actions(
302 actions: &heapless::Vec<Action, MAX_ACTIONS>,
303 cycle_values: &heapless::Vec<u8, MAX_CYCLE_VALUES>,
304 midi: &mut heapless::Vec<ActionStep, 8>,
305 system: &mut heapless::Vec<SystemAction, 2>,
306 cycle_index: &mut u8,
307) {
308 for action in actions {
309 match action {
310 Action::PresetNext => {
311 system.push(SystemAction::PresetNext).ok();
312 }
313 Action::PresetPrev => {
314 system.push(SystemAction::PresetPrev).ok();
315 }
316 Action::PresetSelect(idx) => {
317 system.push(SystemAction::PresetSelect(*idx)).ok();
318 }
319 Action::BankUp | Action::BankDown => {
320 let sa = if matches!(action, Action::BankUp) {
324 SystemAction::PresetNext
325 } else {
326 SystemAction::PresetPrev
327 };
328 system.push(sa).ok();
329 }
330 Action::TapTempo => {
331 system.push(SystemAction::TapTempo).ok();
334 }
335 Action::Delay(ms) => {
336 midi.push(ActionStep::Delay(*ms)).ok();
337 }
338 Action::CcCycle {
339 cc,
340 channel,
341 reverse,
342 } => {
343 if !cycle_values.is_empty() {
344 let idx = (*cycle_index as usize) % cycle_values.len();
345 let value = cycle_values[idx];
346 midi.push(ActionStep::Send(MidiMessage {
347 data: [0xB0 | (channel - 1), *cc, value],
348 len: 3,
349 }))
350 .ok();
351 if *reverse {
352 *cycle_index = if *cycle_index == 0 {
353 (cycle_values.len() - 1) as u8
354 } else {
355 *cycle_index - 1
356 };
357 } else {
358 *cycle_index = ((*cycle_index as usize + 1) % cycle_values.len()) as u8;
359 }
360 }
361 }
362 Action::SetLed { color, animation } => {
363 midi.push(ActionStep::SetLed {
364 color: *color,
365 animation: *animation,
366 })
367 .ok();
368 }
369 _ => {
370 if let Some(msg) = action_to_midi(action) {
371 midi.push(ActionStep::Send(msg)).ok();
372 }
373 }
374 }
375 }
376}
377
378pub struct TriggerResult {
380 pub midi: heapless::Vec<ActionStep, 8>,
382 pub system: heapless::Vec<SystemAction, 2>,
384 pub led_dirty: bool,
386}
387
388pub fn process_triggers(
390 state: &mut PresetState,
391 preset: &Preset,
392 status: u8,
393 data1: u8,
394 data2: u8,
395) -> TriggerResult {
396 use crate::config::{TriggerAction, TriggerMatch};
397
398 let mut result = TriggerResult {
399 midi: heapless::Vec::new(),
400 system: heapless::Vec::new(),
401 led_dirty: false,
402 };
403
404 let msg_type = status & 0xF0;
405 let channel = (status & 0x0F) + 1;
406
407 for trigger in &preset.triggers {
408 let matched = match &trigger.match_msg {
409 TriggerMatch::Cc {
410 cc,
411 channel: ch,
412 value_min,
413 value_max,
414 } => {
415 msg_type == 0xB0
416 && channel == *ch
417 && data1 == *cc
418 && data2 >= *value_min
419 && data2 <= *value_max
420 }
421 TriggerMatch::ProgramChange {
422 program,
423 channel: ch,
424 } => msg_type == 0xC0 && channel == *ch && data1 == *program,
425 TriggerMatch::NoteOn { note, channel: ch } => {
426 msg_type == 0x90 && channel == *ch && data1 == *note && data2 > 0
427 }
428 };
429
430 if !matched {
431 continue;
432 }
433
434 match &trigger.action {
435 TriggerAction::Activate(btn_idx) => {
436 let idx = *btn_idx as usize;
437 if idx < state.button_active.len() {
438 state.button_active[idx] = true;
439 result.led_dirty = true;
440 }
441 }
442 TriggerAction::Deactivate(btn_idx) => {
443 let idx = *btn_idx as usize;
444 if idx < state.button_active.len() {
445 state.button_active[idx] = false;
446 result.led_dirty = true;
447 }
448 }
449 TriggerAction::PresetSelect(preset_idx) => {
450 result
451 .system
452 .push(SystemAction::PresetSelect(*preset_idx))
453 .ok();
454 }
455 TriggerAction::Execute(btn_idx) => {
456 let idx = *btn_idx as usize;
457 if let Some(btn) = preset.buttons.get(idx) {
458 execute_actions(
459 &btn.on_press,
460 &btn.cycle_values,
461 &mut result.midi,
462 &mut result.system,
463 &mut state.cycle_index[idx],
464 );
465 }
466 }
467 }
468 }
469
470 result
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum ReactiveResult {
476 Heatmap(usize, u8),
478 Trigger(usize, bool),
480}
481
482pub fn process_incoming_cc(
484 preset: &Preset,
485 channel: u8,
486 cc: u8,
487 value: u8,
488) -> Option<ReactiveResult> {
489 use crate::config::ListenMode;
490 for (i, btn) in preset.buttons.iter().enumerate() {
491 if let Some(listen) = &btn.listen_cc {
492 if listen.cc == cc && listen.channel == channel {
493 return Some(match listen.mode {
494 ListenMode::Heatmap => {
495 let fill = ((value as u16 * 12) / 127).min(12) as u8;
496 ReactiveResult::Heatmap(i, fill)
497 }
498 ListenMode::Trigger => ReactiveResult::Trigger(i, value >= listen.threshold),
499 });
500 }
501 }
502 }
503 None
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::config::*;
510
511 fn make_toggle_preset() -> Preset {
512 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
513 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
514 on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
515 let mut on_release: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
516 on_release.push(Action::cc(80, 0, 1).unwrap()).ok();
517 buttons
518 .push(ButtonConfig {
519 label: Label::new(),
520 color: LedConfig::default(),
521 mode: ButtonMode::Toggle,
522 on_press,
523 on_release,
524 on_long_press: heapless::Vec::new(),
525 cycle_values: heapless::Vec::new(),
526 listen_cc: None,
527 })
528 .ok();
529 Preset {
530 name: Label::try_from("Test").unwrap(),
531 buttons,
532 encoders: heapless::Vec::new(),
533 analog: heapless::Vec::new(),
534 defaults: Default::default(),
535 on_enter: heapless::Vec::new(),
536 on_exit: heapless::Vec::new(),
537 triggers: heapless::Vec::new(),
538 }
539 }
540
541 #[test]
542 fn toggle_press_flips_state_and_fires_on_press() {
543 let preset = make_toggle_preset();
544 let mut state = PresetState::default();
545
546 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
547 assert!(state.button_active[0]);
548 assert!(r.led_dirty);
549 assert_eq!(r.midi.len(), 1);
550 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 80, 127]));
551 }
552
553 #[test]
554 fn toggle_second_press_deactivates_and_fires_on_release() {
555 let preset = make_toggle_preset();
556 let mut state = PresetState::default();
557
558 process_button(&mut state, &preset, 0, ButtonEvent::Press);
559 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
560 assert!(!state.button_active[0]);
561 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 80, 0]));
563 }
564
565 #[test]
566 fn momentary_press_and_release() {
567 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
568 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
569 on_press.push(Action::note_on(60, 1).unwrap()).ok();
570 let mut on_release: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
571 on_release.push(Action::note_off(60, 1).unwrap()).ok();
572 buttons
573 .push(ButtonConfig {
574 label: Label::new(),
575 color: LedConfig::default(),
576 mode: ButtonMode::Momentary,
577 on_press,
578 on_release,
579 on_long_press: heapless::Vec::new(),
580 cycle_values: heapless::Vec::new(),
581 listen_cc: None,
582 })
583 .ok();
584 let preset = Preset {
585 name: Label::new(),
586 buttons,
587 encoders: heapless::Vec::new(),
588 analog: heapless::Vec::new(),
589 defaults: Default::default(),
590 on_enter: heapless::Vec::new(),
591 on_exit: heapless::Vec::new(),
592 triggers: heapless::Vec::new(),
593 };
594 let mut state = PresetState::default();
595
596 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
597 assert!(state.button_active[0]);
598 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0x90, 60, 127]));
599
600 let r = process_button(&mut state, &preset, 0, ButtonEvent::Release);
601 assert!(!state.button_active[0]);
602 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0x80, 60, 0]));
603 }
604
605 #[test]
606 fn long_press_fires_on_long_press_actions() {
607 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
608 let mut on_long_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
609 on_long_press.push(Action::PresetNext).ok();
610 buttons
611 .push(ButtonConfig {
612 label: Label::new(),
613 color: LedConfig::default(),
614 mode: ButtonMode::Momentary,
615 on_press: heapless::Vec::new(),
616 on_release: heapless::Vec::new(),
617 on_long_press,
618 cycle_values: heapless::Vec::new(),
619 listen_cc: None,
620 })
621 .ok();
622 let preset = Preset {
623 name: Label::new(),
624 buttons,
625 encoders: heapless::Vec::new(),
626 analog: heapless::Vec::new(),
627 defaults: Default::default(),
628 on_enter: heapless::Vec::new(),
629 on_exit: heapless::Vec::new(),
630 triggers: heapless::Vec::new(),
631 };
632 let mut state = PresetState::default();
633
634 let r = process_button(&mut state, &preset, 0, ButtonEvent::LongPress);
635 assert_eq!(r.system.len(), 1);
636 assert_eq!(r.system[0], SystemAction::PresetNext);
637 }
638
639 #[test]
640 fn radio_group_deactivates_others() {
641 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
642 for _ in 0..3 {
643 buttons
644 .push(ButtonConfig {
645 label: Label::new(),
646 color: LedConfig::default(),
647 mode: ButtonMode::RadioGroup(1),
648 on_press: heapless::Vec::new(),
649 on_release: heapless::Vec::new(),
650 on_long_press: heapless::Vec::new(),
651 cycle_values: heapless::Vec::new(),
652 listen_cc: None,
653 })
654 .ok();
655 }
656 let preset = Preset {
657 name: Label::new(),
658 buttons,
659 encoders: heapless::Vec::new(),
660 analog: heapless::Vec::new(),
661 defaults: Default::default(),
662 on_enter: heapless::Vec::new(),
663 on_exit: heapless::Vec::new(),
664 triggers: heapless::Vec::new(),
665 };
666 let mut state = PresetState::default();
667
668 process_button(&mut state, &preset, 0, ButtonEvent::Press);
669 assert!(state.button_active[0]);
670
671 process_button(&mut state, &preset, 1, ButtonEvent::Press);
672 assert!(!state.button_active[0]);
673 assert!(state.button_active[1]);
674 }
675
676 #[test]
677 fn encoder_cc_increments_and_emits_display() {
678 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
679 encoders
680 .push(EncoderConfig {
681 label: Label::try_from("Vol").unwrap(),
682 action: EncoderAction::Cc {
683 cc: 7,
684 channel: 1,
685 min: 0,
686 max: 127,
687 },
688 })
689 .ok();
690 let preset = Preset {
691 name: Label::new(),
692 buttons: heapless::Vec::new(),
693 encoders,
694 analog: heapless::Vec::new(),
695 defaults: Default::default(),
696 on_enter: heapless::Vec::new(),
697 on_exit: heapless::Vec::new(),
698 triggers: heapless::Vec::new(),
699 };
700 let mut state = PresetState::default();
701 state.encoder_values[0] = 64;
702
703 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
704 assert_eq!(state.encoder_values[0], 65);
705 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 7, 65]));
706 assert_eq!(r.display.len(), 1);
707 match &r.display[0] {
708 DisplayEvent::EncoderOverlay { side, label, value } => {
709 assert_eq!(*side, DisplaySide::L);
710 assert_eq!(label.as_str(), "Vol");
711 assert_eq!(*value, 65);
712 }
713 _ => panic!("expected EncoderOverlay"),
714 }
715 }
716
717 #[test]
718 fn encoder_cc_counter_clockwise_decrements() {
719 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
720 encoders
721 .push(EncoderConfig {
722 label: Label::try_from("Vol").unwrap(),
723 action: EncoderAction::Cc {
724 cc: 7,
725 channel: 1,
726 min: 0,
727 max: 127,
728 },
729 })
730 .ok();
731 let preset = Preset {
732 name: Label::new(),
733 buttons: heapless::Vec::new(),
734 encoders,
735 analog: heapless::Vec::new(),
736 defaults: Default::default(),
737 on_enter: heapless::Vec::new(),
738 on_exit: heapless::Vec::new(),
739 triggers: heapless::Vec::new(),
740 };
741 let mut state = PresetState::default();
742 state.encoder_values[0] = 64;
743
744 let r = process_encoder(
745 &mut state,
746 &preset,
747 0,
748 EncoderDirection::CounterClockwise,
749 1,
750 );
751 assert_eq!(state.encoder_values[0], 63);
752 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 7, 63]));
753 }
754
755 #[test]
756 fn encoder_cc_multi_step_acceleration() {
757 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
758 encoders
759 .push(EncoderConfig {
760 label: Label::try_from("Gain").unwrap(),
761 action: EncoderAction::Cc {
762 cc: 7,
763 channel: 1,
764 min: 0,
765 max: 127,
766 },
767 })
768 .ok();
769 let preset = Preset {
770 name: Label::new(),
771 buttons: heapless::Vec::new(),
772 encoders,
773 analog: heapless::Vec::new(),
774 defaults: Default::default(),
775 on_enter: heapless::Vec::new(),
776 on_exit: heapless::Vec::new(),
777 triggers: heapless::Vec::new(),
778 };
779 let mut state = PresetState::default();
780 state.encoder_values[0] = 50;
781
782 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 4);
784 assert_eq!(state.encoder_values[0], 54);
785 assert_eq!(r.midi.len(), 1);
786 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 7, 54]));
787 }
788
789 #[test]
790 fn encoder_cc_clamps_at_max() {
791 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
792 encoders
793 .push(EncoderConfig {
794 label: Label::new(),
795 action: EncoderAction::Cc {
796 cc: 7,
797 channel: 1,
798 min: 0,
799 max: 100,
800 },
801 })
802 .ok();
803 let preset = Preset {
804 name: Label::new(),
805 buttons: heapless::Vec::new(),
806 encoders,
807 analog: heapless::Vec::new(),
808 defaults: Default::default(),
809 on_enter: heapless::Vec::new(),
810 on_exit: heapless::Vec::new(),
811 triggers: heapless::Vec::new(),
812 };
813 let mut state = PresetState::default();
814 state.encoder_values[0] = 99;
815
816 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 5);
818 assert_eq!(state.encoder_values[0], 100);
819 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 100));
820 }
821
822 #[test]
823 fn encoder_cc_clamps_at_min() {
824 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
825 encoders
826 .push(EncoderConfig {
827 label: Label::new(),
828 action: EncoderAction::Cc {
829 cc: 7,
830 channel: 1,
831 min: 20,
832 max: 127,
833 },
834 })
835 .ok();
836 let preset = Preset {
837 name: Label::new(),
838 buttons: heapless::Vec::new(),
839 encoders,
840 analog: heapless::Vec::new(),
841 defaults: Default::default(),
842 on_enter: heapless::Vec::new(),
843 on_exit: heapless::Vec::new(),
844 triggers: heapless::Vec::new(),
845 };
846 let mut state = PresetState::default();
847 state.encoder_values[0] = 21;
848
849 let r = process_encoder(
851 &mut state,
852 &preset,
853 0,
854 EncoderDirection::CounterClockwise,
855 5,
856 );
857 assert_eq!(state.encoder_values[0], 20);
858 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 20));
859 }
860
861 #[test]
862 fn encoder_relative_sends_increment_decrement() {
863 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
864 encoders
865 .push(EncoderConfig {
866 label: Label::try_from("FX").unwrap(),
867 action: EncoderAction::CcRelative {
868 cc: 16,
869 channel: 1,
870 increment: 65,
871 decrement: 63,
872 },
873 })
874 .ok();
875 let preset = Preset {
876 name: Label::new(),
877 buttons: heapless::Vec::new(),
878 encoders,
879 analog: heapless::Vec::new(),
880 defaults: Default::default(),
881 on_enter: heapless::Vec::new(),
882 on_exit: heapless::Vec::new(),
883 triggers: heapless::Vec::new(),
884 };
885 let mut state = PresetState::default();
886
887 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
888 assert_eq!(r.midi.len(), 1);
889 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 16, 65]));
890
891 let r = process_encoder(
892 &mut state,
893 &preset,
894 0,
895 EncoderDirection::CounterClockwise,
896 1,
897 );
898 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 16, 63]));
899 }
900
901 #[test]
902 fn encoder_preset_scroll_emits_system_action() {
903 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
904 encoders
905 .push(EncoderConfig {
906 label: Label::try_from("Bank").unwrap(),
907 action: EncoderAction::PresetScroll,
908 })
909 .ok();
910 let preset = Preset {
911 name: Label::new(),
912 buttons: heapless::Vec::new(),
913 encoders,
914 analog: heapless::Vec::new(),
915 defaults: Default::default(),
916 on_enter: heapless::Vec::new(),
917 on_exit: heapless::Vec::new(),
918 triggers: heapless::Vec::new(),
919 };
920 let mut state = PresetState::default();
921
922 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
923 assert!(r.midi.is_empty());
924 assert_eq!(r.system.len(), 1);
925 assert_eq!(r.system[0], SystemAction::PresetNext);
926
927 let r = process_encoder(
928 &mut state,
929 &preset,
930 0,
931 EncoderDirection::CounterClockwise,
932 1,
933 );
934 assert_eq!(r.system[0], SystemAction::PresetPrev);
935 }
936
937 #[test]
938 fn encoder_invalid_index_returns_empty() {
939 let preset = Preset {
940 name: Label::new(),
941 buttons: heapless::Vec::new(),
942 encoders: heapless::Vec::new(),
943 analog: heapless::Vec::new(),
944 defaults: Default::default(),
945 on_enter: heapless::Vec::new(),
946 on_exit: heapless::Vec::new(),
947 triggers: heapless::Vec::new(),
948 };
949 let mut state = PresetState::default();
950
951 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
952 assert!(r.midi.is_empty());
953 assert!(r.system.is_empty());
954 assert!(r.display.is_empty());
955 }
956
957 #[test]
958 fn encoder_second_index_uses_right_display() {
959 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
960 encoders
961 .push(EncoderConfig {
962 label: Label::try_from("Vol").unwrap(),
963 action: EncoderAction::Cc {
964 cc: 7,
965 channel: 1,
966 min: 0,
967 max: 127,
968 },
969 })
970 .ok();
971 encoders
972 .push(EncoderConfig {
973 label: Label::try_from("Gain").unwrap(),
974 action: EncoderAction::Cc {
975 cc: 8,
976 channel: 1,
977 min: 0,
978 max: 127,
979 },
980 })
981 .ok();
982 let preset = Preset {
983 name: Label::new(),
984 buttons: heapless::Vec::new(),
985 encoders,
986 analog: heapless::Vec::new(),
987 defaults: Default::default(),
988 on_enter: heapless::Vec::new(),
989 on_exit: heapless::Vec::new(),
990 triggers: heapless::Vec::new(),
991 };
992 let mut state = PresetState::default();
993 state.encoder_values[1] = 50;
994
995 let r = process_encoder(&mut state, &preset, 1, EncoderDirection::Clockwise, 1);
996 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 8, 51]));
997 match &r.display[0] {
998 DisplayEvent::EncoderOverlay { side, label, value } => {
999 assert_eq!(*side, DisplaySide::R);
1000 assert_eq!(label.as_str(), "Gain");
1001 assert_eq!(*value, 51);
1002 }
1003 _ => panic!("expected EncoderOverlay"),
1004 }
1005 }
1006
1007 #[test]
1008 fn delay_in_action_sequence() {
1009 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1010 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1011 on_press.push(Action::cc(1, 127, 1).unwrap()).ok();
1012 on_press.push(Action::Delay(50)).ok();
1013 on_press.push(Action::cc(2, 0, 1).unwrap()).ok();
1014 buttons
1015 .push(ButtonConfig {
1016 label: Label::new(),
1017 color: LedConfig::default(),
1018 mode: ButtonMode::Momentary,
1019 on_press,
1020 on_release: heapless::Vec::new(),
1021 on_long_press: heapless::Vec::new(),
1022 cycle_values: heapless::Vec::new(),
1023 listen_cc: None,
1024 })
1025 .ok();
1026 let preset = Preset {
1027 name: Label::new(),
1028 buttons,
1029 encoders: heapless::Vec::new(),
1030 analog: heapless::Vec::new(),
1031 defaults: Default::default(),
1032 on_enter: heapless::Vec::new(),
1033 on_exit: heapless::Vec::new(),
1034 triggers: heapless::Vec::new(),
1035 };
1036 let mut state = PresetState::default();
1037
1038 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1039 assert_eq!(r.midi.len(), 3);
1040 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 1, 127]));
1041 assert_eq!(r.midi[1], ActionStep::Delay(50));
1042 assert!(matches!(&r.midi[2], ActionStep::Send(m) if m.data == [0xB0, 2, 0]));
1043 }
1044
1045 #[test]
1046 fn set_led_in_action_sequence() {
1047 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1048 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1049 on_press.push(Action::cc(3, 127, 1).unwrap()).ok();
1050 on_press
1051 .push(Action::SetLed {
1052 color: Color::Red,
1053 animation: LedAnimation::Blink,
1054 })
1055 .ok();
1056 on_press.push(Action::Delay(100)).ok();
1057 on_press
1058 .push(Action::SetLed {
1059 color: Color::Green,
1060 animation: LedAnimation::Solid,
1061 })
1062 .ok();
1063 buttons
1064 .push(ButtonConfig {
1065 label: Label::new(),
1066 color: LedConfig::default(),
1067 mode: ButtonMode::Momentary,
1068 on_press,
1069 on_release: heapless::Vec::new(),
1070 on_long_press: heapless::Vec::new(),
1071 cycle_values: heapless::Vec::new(),
1072 listen_cc: None,
1073 })
1074 .ok();
1075 let preset = Preset {
1076 name: Label::new(),
1077 buttons,
1078 encoders: heapless::Vec::new(),
1079 analog: heapless::Vec::new(),
1080 defaults: Default::default(),
1081 on_enter: heapless::Vec::new(),
1082 on_exit: heapless::Vec::new(),
1083 triggers: heapless::Vec::new(),
1084 };
1085 let mut state = PresetState::default();
1086
1087 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1088 assert_eq!(r.midi.len(), 4);
1089 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 3, 127]));
1090 assert_eq!(
1091 r.midi[1],
1092 ActionStep::SetLed {
1093 color: Color::Red,
1094 animation: LedAnimation::Blink
1095 }
1096 );
1097 assert_eq!(r.midi[2], ActionStep::Delay(100));
1098 assert_eq!(
1099 r.midi[3],
1100 ActionStep::SetLed {
1101 color: Color::Green,
1102 animation: LedAnimation::Solid
1103 }
1104 );
1105 }
1106
1107 #[test]
1108 fn incoming_cc_matches_listen_binding() {
1109 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1110 buttons
1111 .push(ButtonConfig {
1112 label: Label::new(),
1113 color: LedConfig::default(),
1114 mode: ButtonMode::default(),
1115 on_press: heapless::Vec::new(),
1116 on_release: heapless::Vec::new(),
1117 on_long_press: heapless::Vec::new(),
1118 cycle_values: heapless::Vec::new(),
1119 listen_cc: Some(ListenCc {
1120 cc: 100,
1121 channel: 3,
1122 mode: ListenMode::default(),
1123 threshold: 64,
1124 }),
1125 })
1126 .ok();
1127 let preset = Preset {
1128 name: Label::new(),
1129 buttons,
1130 encoders: heapless::Vec::new(),
1131 analog: heapless::Vec::new(),
1132 defaults: Default::default(),
1133 on_enter: heapless::Vec::new(),
1134 on_exit: heapless::Vec::new(),
1135 triggers: heapless::Vec::new(),
1136 };
1137
1138 let result = process_incoming_cc(&preset, 3, 100, 127);
1140 assert_eq!(result, Some(ReactiveResult::Heatmap(0, 12)));
1141
1142 let result = process_incoming_cc(&preset, 3, 100, 64);
1144 assert_eq!(result, Some(ReactiveResult::Heatmap(0, 6)));
1145
1146 let result = process_incoming_cc(&preset, 3, 100, 0);
1148 assert_eq!(result, Some(ReactiveResult::Heatmap(0, 0)));
1149
1150 assert_eq!(process_incoming_cc(&preset, 3, 99, 127), None);
1152
1153 assert_eq!(process_incoming_cc(&preset, 1, 100, 127), None);
1155 }
1156
1157 #[test]
1158 fn incoming_cc_trigger_mode() {
1159 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1160 buttons
1161 .push(ButtonConfig {
1162 label: Label::new(),
1163 color: LedConfig::default(),
1164 mode: ButtonMode::default(),
1165 on_press: heapless::Vec::new(),
1166 on_release: heapless::Vec::new(),
1167 on_long_press: heapless::Vec::new(),
1168 cycle_values: heapless::Vec::new(),
1169 listen_cc: Some(ListenCc {
1170 cc: 80,
1171 channel: 2,
1172 mode: ListenMode::Trigger,
1173 threshold: 64,
1174 }),
1175 })
1176 .ok();
1177 let preset = Preset {
1178 name: Label::new(),
1179 buttons,
1180 encoders: heapless::Vec::new(),
1181 analog: heapless::Vec::new(),
1182 defaults: Default::default(),
1183 on_enter: heapless::Vec::new(),
1184 on_exit: heapless::Vec::new(),
1185 triggers: heapless::Vec::new(),
1186 };
1187
1188 assert_eq!(
1189 process_incoming_cc(&preset, 2, 80, 127),
1190 Some(ReactiveResult::Trigger(0, true))
1191 );
1192 assert_eq!(
1193 process_incoming_cc(&preset, 2, 80, 64),
1194 Some(ReactiveResult::Trigger(0, true))
1195 );
1196 assert_eq!(
1197 process_incoming_cc(&preset, 2, 80, 63),
1198 Some(ReactiveResult::Trigger(0, false))
1199 );
1200 assert_eq!(
1201 process_incoming_cc(&preset, 2, 80, 0),
1202 Some(ReactiveResult::Trigger(0, false))
1203 );
1204 }
1205
1206 #[test]
1207 fn preset_select_emits_system_action() {
1208 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1209 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1210 on_press.push(Action::PresetSelect(5)).ok();
1211 buttons
1212 .push(ButtonConfig {
1213 label: Label::new(),
1214 color: LedConfig::default(),
1215 mode: ButtonMode::Momentary,
1216 on_press,
1217 on_release: heapless::Vec::new(),
1218 on_long_press: heapless::Vec::new(),
1219 cycle_values: heapless::Vec::new(),
1220 listen_cc: None,
1221 })
1222 .ok();
1223 let preset = Preset {
1224 name: Label::new(),
1225 buttons,
1226 encoders: heapless::Vec::new(),
1227 analog: heapless::Vec::new(),
1228 defaults: Default::default(),
1229 on_enter: heapless::Vec::new(),
1230 on_exit: heapless::Vec::new(),
1231 triggers: heapless::Vec::new(),
1232 };
1233 let mut state = PresetState::default();
1234
1235 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1236 assert_eq!(r.system.len(), 1);
1237 assert_eq!(r.system[0], SystemAction::PresetSelect(5));
1238 assert!(r.midi.is_empty());
1240 }
1241
1242 #[test]
1243 fn bank_up_down_emit_system_actions() {
1244 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1245 let mut on_press_up: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1246 on_press_up.push(Action::BankUp).ok();
1247 let mut on_press_down: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1248 on_press_down.push(Action::BankDown).ok();
1249 buttons
1250 .push(ButtonConfig {
1251 label: Label::new(),
1252 color: LedConfig::default(),
1253 mode: ButtonMode::Momentary,
1254 on_press: on_press_up,
1255 on_release: heapless::Vec::new(),
1256 on_long_press: heapless::Vec::new(),
1257 cycle_values: heapless::Vec::new(),
1258 listen_cc: None,
1259 })
1260 .ok();
1261 buttons
1262 .push(ButtonConfig {
1263 label: Label::new(),
1264 color: LedConfig::default(),
1265 mode: ButtonMode::Momentary,
1266 on_press: on_press_down,
1267 on_release: heapless::Vec::new(),
1268 on_long_press: heapless::Vec::new(),
1269 cycle_values: heapless::Vec::new(),
1270 listen_cc: None,
1271 })
1272 .ok();
1273 let preset = Preset {
1274 name: Label::new(),
1275 buttons,
1276 encoders: heapless::Vec::new(),
1277 analog: heapless::Vec::new(),
1278 defaults: Default::default(),
1279 on_enter: heapless::Vec::new(),
1280 on_exit: heapless::Vec::new(),
1281 triggers: heapless::Vec::new(),
1282 };
1283 let mut state = PresetState::default();
1284
1285 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1286 assert_eq!(r.system.len(), 1);
1287 assert_eq!(r.system[0], SystemAction::PresetNext);
1288
1289 let r = process_button(&mut state, &preset, 1, ButtonEvent::Press);
1290 assert_eq!(r.system.len(), 1);
1291 assert_eq!(r.system[0], SystemAction::PresetPrev);
1292 }
1293
1294 fn make_cc_cycle_preset(reverse: bool) -> Preset {
1297 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1298 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1299 on_press
1300 .push(Action::CcCycle {
1301 cc: 50,
1302 channel: 1,
1303 reverse,
1304 })
1305 .ok();
1306 let mut cycle_values: heapless::Vec<u8, MAX_CYCLE_VALUES> = heapless::Vec::new();
1307 cycle_values.push(0).ok();
1308 cycle_values.push(32).ok();
1309 cycle_values.push(64).ok();
1310 cycle_values.push(127).ok();
1311 buttons
1312 .push(ButtonConfig {
1313 label: Label::new(),
1314 color: LedConfig::default(),
1315 mode: ButtonMode::Momentary,
1316 on_press,
1317 on_release: heapless::Vec::new(),
1318 on_long_press: heapless::Vec::new(),
1319 cycle_values,
1320 listen_cc: None,
1321 })
1322 .ok();
1323 Preset {
1324 name: Label::new(),
1325 buttons,
1326 encoders: heapless::Vec::new(),
1327 analog: heapless::Vec::new(),
1328 defaults: Default::default(),
1329 on_enter: heapless::Vec::new(),
1330 on_exit: heapless::Vec::new(),
1331 triggers: heapless::Vec::new(),
1332 }
1333 }
1334
1335 #[test]
1336 fn cc_cycle_forward_iterates_values() {
1337 let preset = make_cc_cycle_preset(false);
1338 let mut state = PresetState::default();
1339
1340 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1341 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 0]));
1342
1343 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1344 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 32]));
1345
1346 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1347 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 64]));
1348
1349 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1350 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 127]));
1351 }
1352
1353 #[test]
1354 fn cc_cycle_forward_wraps_around() {
1355 let preset = make_cc_cycle_preset(false);
1356 let mut state = PresetState::default();
1357
1358 for _ in 0..4 {
1360 process_button(&mut state, &preset, 0, ButtonEvent::Press);
1361 }
1362 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1364 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 0]));
1365 }
1366
1367 #[test]
1368 fn cc_cycle_reverse_iterates_backward() {
1369 let preset = make_cc_cycle_preset(true);
1370 let mut state = PresetState::default();
1371
1372 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1374 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 0]));
1375
1376 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1378 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 127]));
1379
1380 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1382 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 64]));
1383 }
1384
1385 #[test]
1386 fn cc_cycle_empty_values_no_output() {
1387 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1388 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1389 on_press
1390 .push(Action::CcCycle {
1391 cc: 50,
1392 channel: 1,
1393 reverse: false,
1394 })
1395 .ok();
1396 buttons
1397 .push(ButtonConfig {
1398 label: Label::new(),
1399 color: LedConfig::default(),
1400 mode: ButtonMode::Momentary,
1401 on_press,
1402 on_release: heapless::Vec::new(),
1403 on_long_press: heapless::Vec::new(),
1404 cycle_values: heapless::Vec::new(), listen_cc: None,
1406 })
1407 .ok();
1408 let preset = Preset {
1409 name: Label::new(),
1410 buttons,
1411 encoders: heapless::Vec::new(),
1412 analog: heapless::Vec::new(),
1413 defaults: Default::default(),
1414 on_enter: heapless::Vec::new(),
1415 on_exit: heapless::Vec::new(),
1416 triggers: heapless::Vec::new(),
1417 };
1418 let mut state = PresetState::default();
1419
1420 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1421 assert!(r.midi.is_empty());
1422 }
1423
1424 fn make_analog_preset(cc: u8, channel: u8, min: u8, max: u8) -> Preset {
1427 let mut analog: heapless::Vec<AnalogConfig, MAX_ANALOG> = heapless::Vec::new();
1428 analog
1429 .push(AnalogConfig {
1430 label: Label::try_from("Wah").unwrap(),
1431 cc,
1432 channel,
1433 min,
1434 max,
1435 })
1436 .ok();
1437 Preset {
1438 name: Label::new(),
1439 buttons: heapless::Vec::new(),
1440 encoders: heapless::Vec::new(),
1441 analog,
1442 defaults: Default::default(),
1443 on_enter: heapless::Vec::new(),
1444 on_exit: heapless::Vec::new(),
1445 triggers: heapless::Vec::new(),
1446 }
1447 }
1448
1449 #[test]
1450 fn analog_full_range_maps_to_max() {
1451 let preset = make_analog_preset(11, 1, 0, 127);
1452 let r = process_analog(&preset, 0, 3750, 0, 3750);
1453 assert_eq!(r.midi.len(), 1);
1454 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 127]));
1455 }
1456
1457 #[test]
1458 fn analog_min_maps_to_zero() {
1459 let preset = make_analog_preset(11, 1, 0, 127);
1460 let r = process_analog(&preset, 0, 0, 0, 3750);
1461 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 0]));
1462 }
1463
1464 #[test]
1465 fn analog_midpoint() {
1466 let preset = make_analog_preset(11, 1, 0, 127);
1467 let r = process_analog(&preset, 0, 1875, 0, 3750);
1468 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 63));
1470 }
1471
1472 #[test]
1473 fn analog_custom_range() {
1474 let preset = make_analog_preset(11, 1, 20, 100);
1475 let r = process_analog(&preset, 0, 3750, 0, 3750);
1476 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 100]));
1477
1478 let r = process_analog(&preset, 0, 0, 0, 3750);
1479 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 20]));
1480 }
1481
1482 #[test]
1483 fn analog_clamps_below_adc_min() {
1484 let preset = make_analog_preset(11, 1, 0, 127);
1485 let r = process_analog(&preset, 0, 100, 200, 3750);
1487 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 0));
1488 }
1489
1490 #[test]
1491 fn analog_clamps_above_adc_max() {
1492 let preset = make_analog_preset(11, 1, 0, 127);
1493 let r = process_analog(&preset, 0, 4000, 0, 3750);
1495 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 127));
1496 }
1497
1498 #[test]
1499 fn analog_emits_display_event() {
1500 let preset = make_analog_preset(11, 1, 0, 127);
1501 let r = process_analog(&preset, 0, 1875, 0, 3750);
1502 assert_eq!(r.display.len(), 1);
1503 match &r.display[0] {
1504 DisplayEvent::AnalogOverlay { side, label, value } => {
1505 assert_eq!(*side, DisplaySide::L);
1506 assert_eq!(label.as_str(), "Wah");
1507 assert_eq!(*value, 63);
1508 }
1509 _ => panic!("expected AnalogOverlay"),
1510 }
1511 }
1512
1513 #[test]
1514 fn analog_invalid_index_returns_empty() {
1515 let preset = make_analog_preset(11, 1, 0, 127);
1516 let r = process_analog(&preset, 5, 1875, 0, 3750);
1517 assert!(r.midi.is_empty());
1518 assert!(r.display.is_empty());
1519 }
1520
1521 fn make_trigger_preset(triggers: heapless::Vec<Trigger, MAX_TRIGGERS>) -> Preset {
1524 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1525 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1526 on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
1527 buttons
1528 .push(ButtonConfig {
1529 label: Label::new(),
1530 color: LedConfig::default(),
1531 mode: ButtonMode::Toggle,
1532 on_press,
1533 on_release: heapless::Vec::new(),
1534 on_long_press: heapless::Vec::new(),
1535 cycle_values: heapless::Vec::new(),
1536 listen_cc: None,
1537 })
1538 .ok();
1539 Preset {
1540 name: Label::new(),
1541 buttons,
1542 encoders: heapless::Vec::new(),
1543 analog: heapless::Vec::new(),
1544 defaults: Default::default(),
1545 on_enter: heapless::Vec::new(),
1546 on_exit: heapless::Vec::new(),
1547 triggers,
1548 }
1549 }
1550
1551 #[test]
1552 fn trigger_cc_activates_button() {
1553 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1554 triggers
1555 .push(Trigger {
1556 match_msg: TriggerMatch::Cc {
1557 cc: 100,
1558 channel: 1,
1559 value_min: 64,
1560 value_max: 127,
1561 },
1562 action: TriggerAction::Activate(0),
1563 })
1564 .ok();
1565 let preset = make_trigger_preset(triggers);
1566 let mut state = PresetState::default();
1567
1568 let r = process_triggers(&mut state, &preset, 0xB0, 100, 127);
1569 assert!(state.button_active[0]);
1570 assert!(r.led_dirty);
1571 }
1572
1573 #[test]
1574 fn trigger_cc_below_value_min_no_match() {
1575 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1576 triggers
1577 .push(Trigger {
1578 match_msg: TriggerMatch::Cc {
1579 cc: 100,
1580 channel: 1,
1581 value_min: 64,
1582 value_max: 127,
1583 },
1584 action: TriggerAction::Activate(0),
1585 })
1586 .ok();
1587 let preset = make_trigger_preset(triggers);
1588 let mut state = PresetState::default();
1589
1590 let r = process_triggers(&mut state, &preset, 0xB0, 100, 63);
1591 assert!(!state.button_active[0]);
1592 assert!(!r.led_dirty);
1593 }
1594
1595 #[test]
1596 fn trigger_deactivate_button() {
1597 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1598 triggers
1599 .push(Trigger {
1600 match_msg: TriggerMatch::Cc {
1601 cc: 100,
1602 channel: 1,
1603 value_min: 0,
1604 value_max: 63,
1605 },
1606 action: TriggerAction::Deactivate(0),
1607 })
1608 .ok();
1609 let preset = make_trigger_preset(triggers);
1610 let mut state = PresetState::default();
1611 state.button_active[0] = true;
1612
1613 let r = process_triggers(&mut state, &preset, 0xB0, 100, 0);
1614 assert!(!state.button_active[0]);
1615 assert!(r.led_dirty);
1616 }
1617
1618 #[test]
1619 fn trigger_program_change_selects_preset() {
1620 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1621 triggers
1622 .push(Trigger {
1623 match_msg: TriggerMatch::ProgramChange {
1624 program: 5,
1625 channel: 1,
1626 },
1627 action: TriggerAction::PresetSelect(3),
1628 })
1629 .ok();
1630 let preset = make_trigger_preset(triggers);
1631 let mut state = PresetState::default();
1632
1633 let r = process_triggers(&mut state, &preset, 0xC0, 5, 0);
1634 assert_eq!(r.system.len(), 1);
1635 assert_eq!(r.system[0], SystemAction::PresetSelect(3));
1636 }
1637
1638 #[test]
1639 fn trigger_note_on_executes_button() {
1640 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1641 triggers
1642 .push(Trigger {
1643 match_msg: TriggerMatch::NoteOn {
1644 note: 60,
1645 channel: 1,
1646 },
1647 action: TriggerAction::Execute(0),
1648 })
1649 .ok();
1650 let preset = make_trigger_preset(triggers);
1651 let mut state = PresetState::default();
1652
1653 let r = process_triggers(&mut state, &preset, 0x90, 60, 127);
1654 assert_eq!(r.midi.len(), 1);
1656 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 80, 127]));
1657 }
1658
1659 #[test]
1660 fn trigger_note_on_velocity_zero_no_match() {
1661 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1662 triggers
1663 .push(Trigger {
1664 match_msg: TriggerMatch::NoteOn {
1665 note: 60,
1666 channel: 1,
1667 },
1668 action: TriggerAction::Activate(0),
1669 })
1670 .ok();
1671 let preset = make_trigger_preset(triggers);
1672 let mut state = PresetState::default();
1673
1674 let r = process_triggers(&mut state, &preset, 0x90, 60, 0);
1676 assert!(!state.button_active[0]);
1677 assert!(!r.led_dirty);
1678 }
1679
1680 #[test]
1681 fn trigger_wrong_channel_no_match() {
1682 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1683 triggers
1684 .push(Trigger {
1685 match_msg: TriggerMatch::Cc {
1686 cc: 100,
1687 channel: 2,
1688 value_min: 0,
1689 value_max: 127,
1690 },
1691 action: TriggerAction::Activate(0),
1692 })
1693 .ok();
1694 let preset = make_trigger_preset(triggers);
1695 let mut state = PresetState::default();
1696
1697 let r = process_triggers(&mut state, &preset, 0xB0, 100, 127);
1699 assert!(!state.button_active[0]);
1700 assert!(!r.led_dirty);
1701 }
1702
1703 #[test]
1704 fn trigger_multiple_triggers_all_fire() {
1705 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1706 triggers
1707 .push(Trigger {
1708 match_msg: TriggerMatch::Cc {
1709 cc: 100,
1710 channel: 1,
1711 value_min: 0,
1712 value_max: 127,
1713 },
1714 action: TriggerAction::Activate(0),
1715 })
1716 .ok();
1717 triggers
1718 .push(Trigger {
1719 match_msg: TriggerMatch::Cc {
1720 cc: 100,
1721 channel: 1,
1722 value_min: 64,
1723 value_max: 127,
1724 },
1725 action: TriggerAction::PresetSelect(2),
1726 })
1727 .ok();
1728 let preset = make_trigger_preset(triggers);
1729 let mut state = PresetState::default();
1730
1731 let r = process_triggers(&mut state, &preset, 0xB0, 100, 100);
1733 assert!(state.button_active[0]);
1734 assert!(r.led_dirty);
1735 assert_eq!(r.system.len(), 1);
1736 assert_eq!(r.system[0], SystemAction::PresetSelect(2));
1737 }
1738}