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