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 ..Default::default()
687 })
688 .ok();
689 let preset: Preset = Preset {
690 name: Label::new(),
691 buttons: heapless::Vec::new(),
692 encoders,
693 analog: heapless::Vec::new(),
694 defaults: Default::default(),
695 on_enter: heapless::Vec::new(),
696 on_exit: heapless::Vec::new(),
697 triggers: heapless::Vec::new(),
698 };
699 let mut state = PresetState::default();
700 state.encoder_values[0] = 64;
701
702 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
703 assert_eq!(state.encoder_values[0], 65);
704 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 7, 65]));
705 assert_eq!(r.display.len(), 1);
706 match &r.display[0] {
707 DisplayEvent::EncoderOverlay { side, label, value } => {
708 assert_eq!(*side, DisplaySide::L);
709 assert_eq!(label.as_str(), "Vol");
710 assert_eq!(*value, 65);
711 }
712 _ => panic!("expected EncoderOverlay"),
713 }
714 }
715
716 #[test]
717 fn encoder_cc_counter_clockwise_decrements() {
718 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
719 encoders
720 .push(EncoderConfig {
721 label: Label::try_from("Vol").unwrap(),
722 action: EncoderAction::Cc {
723 cc: 7,
724 channel: 1,
725 min: 0,
726 max: 127,
727 },
728 ..Default::default()
729 })
730 .ok();
731 let preset: 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 ..Default::default()
768 })
769 .ok();
770 let preset: Preset = Preset {
771 name: Label::new(),
772 buttons: heapless::Vec::new(),
773 encoders,
774 analog: heapless::Vec::new(),
775 defaults: Default::default(),
776 on_enter: heapless::Vec::new(),
777 on_exit: heapless::Vec::new(),
778 triggers: heapless::Vec::new(),
779 };
780 let mut state = PresetState::default();
781 state.encoder_values[0] = 50;
782
783 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 4);
785 assert_eq!(state.encoder_values[0], 54);
786 assert_eq!(r.midi.len(), 1);
787 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 7, 54]));
788 }
789
790 #[test]
791 fn encoder_cc_clamps_at_max() {
792 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
793 encoders
794 .push(EncoderConfig {
795 label: Label::new(),
796 action: EncoderAction::Cc {
797 cc: 7,
798 channel: 1,
799 min: 0,
800 max: 100,
801 },
802 ..Default::default()
803 })
804 .ok();
805 let preset: Preset = Preset {
806 name: Label::new(),
807 buttons: heapless::Vec::new(),
808 encoders,
809 analog: heapless::Vec::new(),
810 defaults: Default::default(),
811 on_enter: heapless::Vec::new(),
812 on_exit: heapless::Vec::new(),
813 triggers: heapless::Vec::new(),
814 };
815 let mut state = PresetState::default();
816 state.encoder_values[0] = 99;
817
818 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 5);
820 assert_eq!(state.encoder_values[0], 100);
821 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 100));
822 }
823
824 #[test]
825 fn encoder_cc_clamps_at_min() {
826 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
827 encoders
828 .push(EncoderConfig {
829 label: Label::new(),
830 action: EncoderAction::Cc {
831 cc: 7,
832 channel: 1,
833 min: 20,
834 max: 127,
835 },
836 ..Default::default()
837 })
838 .ok();
839 let preset: Preset = Preset {
840 name: Label::new(),
841 buttons: heapless::Vec::new(),
842 encoders,
843 analog: heapless::Vec::new(),
844 defaults: Default::default(),
845 on_enter: heapless::Vec::new(),
846 on_exit: heapless::Vec::new(),
847 triggers: heapless::Vec::new(),
848 };
849 let mut state = PresetState::default();
850 state.encoder_values[0] = 21;
851
852 let r = process_encoder(
854 &mut state,
855 &preset,
856 0,
857 EncoderDirection::CounterClockwise,
858 5,
859 );
860 assert_eq!(state.encoder_values[0], 20);
861 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 20));
862 }
863
864 #[test]
865 fn encoder_relative_sends_increment_decrement() {
866 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
867 encoders
868 .push(EncoderConfig {
869 label: Label::try_from("FX").unwrap(),
870 action: EncoderAction::CcRelative {
871 cc: 16,
872 channel: 1,
873 increment: 65,
874 decrement: 63,
875 },
876 ..Default::default()
877 })
878 .ok();
879 let preset: Preset = Preset {
880 name: Label::new(),
881 buttons: heapless::Vec::new(),
882 encoders,
883 analog: heapless::Vec::new(),
884 defaults: Default::default(),
885 on_enter: heapless::Vec::new(),
886 on_exit: heapless::Vec::new(),
887 triggers: heapless::Vec::new(),
888 };
889 let mut state = PresetState::default();
890
891 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
892 assert_eq!(r.midi.len(), 1);
893 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 16, 65]));
894
895 let r = process_encoder(
896 &mut state,
897 &preset,
898 0,
899 EncoderDirection::CounterClockwise,
900 1,
901 );
902 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 16, 63]));
903 }
904
905 #[test]
906 fn encoder_preset_scroll_emits_system_action() {
907 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
908 encoders
909 .push(EncoderConfig {
910 label: Label::try_from("Bank").unwrap(),
911 action: EncoderAction::PresetScroll,
912 ..Default::default()
913 })
914 .ok();
915 let preset: Preset = Preset {
916 name: Label::new(),
917 buttons: heapless::Vec::new(),
918 encoders,
919 analog: heapless::Vec::new(),
920 defaults: Default::default(),
921 on_enter: heapless::Vec::new(),
922 on_exit: heapless::Vec::new(),
923 triggers: heapless::Vec::new(),
924 };
925 let mut state = PresetState::default();
926
927 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
928 assert!(r.midi.is_empty());
929 assert_eq!(r.system.len(), 1);
930 assert_eq!(r.system[0], SystemAction::PresetNext);
931
932 let r = process_encoder(
933 &mut state,
934 &preset,
935 0,
936 EncoderDirection::CounterClockwise,
937 1,
938 );
939 assert_eq!(r.system[0], SystemAction::PresetPrev);
940 }
941
942 #[test]
943 fn encoder_invalid_index_returns_empty() {
944 let preset: Preset = Preset {
945 name: Label::new(),
946 buttons: heapless::Vec::new(),
947 encoders: heapless::Vec::new(),
948 analog: heapless::Vec::new(),
949 defaults: Default::default(),
950 on_enter: heapless::Vec::new(),
951 on_exit: heapless::Vec::new(),
952 triggers: heapless::Vec::new(),
953 };
954 let mut state = PresetState::default();
955
956 let r = process_encoder(&mut state, &preset, 0, EncoderDirection::Clockwise, 1);
957 assert!(r.midi.is_empty());
958 assert!(r.system.is_empty());
959 assert!(r.display.is_empty());
960 }
961
962 #[test]
963 fn encoder_second_index_uses_right_display() {
964 let mut encoders: heapless::Vec<EncoderConfig, MAX_ENCODERS> = heapless::Vec::new();
965 encoders
966 .push(EncoderConfig {
967 label: Label::try_from("Vol").unwrap(),
968 action: EncoderAction::Cc {
969 cc: 7,
970 channel: 1,
971 min: 0,
972 max: 127,
973 },
974 ..Default::default()
975 })
976 .ok();
977 encoders
978 .push(EncoderConfig {
979 label: Label::try_from("Gain").unwrap(),
980 action: EncoderAction::Cc {
981 cc: 8,
982 channel: 1,
983 min: 0,
984 max: 127,
985 },
986 ..Default::default()
987 })
988 .ok();
989 let preset: Preset = Preset {
990 name: Label::new(),
991 buttons: heapless::Vec::new(),
992 encoders,
993 analog: heapless::Vec::new(),
994 defaults: Default::default(),
995 on_enter: heapless::Vec::new(),
996 on_exit: heapless::Vec::new(),
997 triggers: heapless::Vec::new(),
998 };
999 let mut state = PresetState::default();
1000 state.encoder_values[1] = 50;
1001
1002 let r = process_encoder(&mut state, &preset, 1, EncoderDirection::Clockwise, 1);
1003 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 8, 51]));
1004 match &r.display[0] {
1005 DisplayEvent::EncoderOverlay { side, label, value } => {
1006 assert_eq!(*side, DisplaySide::R);
1007 assert_eq!(label.as_str(), "Gain");
1008 assert_eq!(*value, 51);
1009 }
1010 _ => panic!("expected EncoderOverlay"),
1011 }
1012 }
1013
1014 #[test]
1015 fn delay_in_action_sequence() {
1016 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1017 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1018 on_press.push(Action::cc(1, 127, 1).unwrap()).ok();
1019 on_press.push(Action::Delay(50)).ok();
1020 on_press.push(Action::cc(2, 0, 1).unwrap()).ok();
1021 buttons
1022 .push(ButtonConfig {
1023 label: Label::new(),
1024 color: LedConfig::default(),
1025 mode: ButtonMode::Momentary,
1026 on_press,
1027 on_release: heapless::Vec::new(),
1028 on_long_press: heapless::Vec::new(),
1029 cycle_values: heapless::Vec::new(),
1030 listen_cc: None,
1031 })
1032 .ok();
1033 let preset: Preset = Preset {
1034 name: Label::new(),
1035 buttons,
1036 encoders: heapless::Vec::new(),
1037 analog: heapless::Vec::new(),
1038 defaults: Default::default(),
1039 on_enter: heapless::Vec::new(),
1040 on_exit: heapless::Vec::new(),
1041 triggers: heapless::Vec::new(),
1042 };
1043 let mut state = PresetState::default();
1044
1045 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1046 assert_eq!(r.midi.len(), 3);
1047 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 1, 127]));
1048 assert_eq!(r.midi[1], ActionStep::Delay(50));
1049 assert!(matches!(&r.midi[2], ActionStep::Send(m) if m.data == [0xB0, 2, 0]));
1050 }
1051
1052 #[test]
1053 fn set_led_in_action_sequence() {
1054 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1055 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1056 on_press.push(Action::cc(3, 127, 1).unwrap()).ok();
1057 on_press
1058 .push(Action::SetLed {
1059 color: Color::Red,
1060 animation: LedAnimation::Blink,
1061 })
1062 .ok();
1063 on_press.push(Action::Delay(100)).ok();
1064 on_press
1065 .push(Action::SetLed {
1066 color: Color::Green,
1067 animation: LedAnimation::Solid,
1068 })
1069 .ok();
1070 buttons
1071 .push(ButtonConfig {
1072 label: Label::new(),
1073 color: LedConfig::default(),
1074 mode: ButtonMode::Momentary,
1075 on_press,
1076 on_release: heapless::Vec::new(),
1077 on_long_press: heapless::Vec::new(),
1078 cycle_values: heapless::Vec::new(),
1079 listen_cc: None,
1080 })
1081 .ok();
1082 let preset: Preset = Preset {
1083 name: Label::new(),
1084 buttons,
1085 encoders: heapless::Vec::new(),
1086 analog: heapless::Vec::new(),
1087 defaults: Default::default(),
1088 on_enter: heapless::Vec::new(),
1089 on_exit: heapless::Vec::new(),
1090 triggers: heapless::Vec::new(),
1091 };
1092 let mut state = PresetState::default();
1093
1094 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1095 assert_eq!(r.midi.len(), 4);
1096 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 3, 127]));
1097 assert_eq!(
1098 r.midi[1],
1099 ActionStep::SetLed {
1100 color: Color::Red,
1101 animation: LedAnimation::Blink
1102 }
1103 );
1104 assert_eq!(r.midi[2], ActionStep::Delay(100));
1105 assert_eq!(
1106 r.midi[3],
1107 ActionStep::SetLed {
1108 color: Color::Green,
1109 animation: LedAnimation::Solid
1110 }
1111 );
1112 }
1113
1114 #[test]
1115 fn incoming_cc_matches_listen_binding() {
1116 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1117 buttons
1118 .push(ButtonConfig {
1119 label: Label::new(),
1120 color: LedConfig::default(),
1121 mode: ButtonMode::default(),
1122 on_press: heapless::Vec::new(),
1123 on_release: heapless::Vec::new(),
1124 on_long_press: heapless::Vec::new(),
1125 cycle_values: heapless::Vec::new(),
1126 listen_cc: Some(ListenCc {
1127 cc: 100,
1128 channel: 3,
1129 mode: ListenMode::default(),
1130 threshold: 64,
1131 }),
1132 })
1133 .ok();
1134 let preset: Preset = Preset {
1135 name: Label::new(),
1136 buttons,
1137 encoders: heapless::Vec::new(),
1138 analog: heapless::Vec::new(),
1139 defaults: Default::default(),
1140 on_enter: heapless::Vec::new(),
1141 on_exit: heapless::Vec::new(),
1142 triggers: heapless::Vec::new(),
1143 };
1144
1145 let result = process_incoming_cc(&preset, 3, 100, 127);
1147 assert_eq!(result, Some(ReactiveResult::Heatmap(0, 12)));
1148
1149 let result = process_incoming_cc(&preset, 3, 100, 64);
1151 assert_eq!(result, Some(ReactiveResult::Heatmap(0, 6)));
1152
1153 let result = process_incoming_cc(&preset, 3, 100, 0);
1155 assert_eq!(result, Some(ReactiveResult::Heatmap(0, 0)));
1156
1157 assert_eq!(process_incoming_cc(&preset, 3, 99, 127), None);
1159
1160 assert_eq!(process_incoming_cc(&preset, 1, 100, 127), None);
1162 }
1163
1164 #[test]
1165 fn incoming_cc_trigger_mode() {
1166 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1167 buttons
1168 .push(ButtonConfig {
1169 label: Label::new(),
1170 color: LedConfig::default(),
1171 mode: ButtonMode::default(),
1172 on_press: heapless::Vec::new(),
1173 on_release: heapless::Vec::new(),
1174 on_long_press: heapless::Vec::new(),
1175 cycle_values: heapless::Vec::new(),
1176 listen_cc: Some(ListenCc {
1177 cc: 80,
1178 channel: 2,
1179 mode: ListenMode::Trigger,
1180 threshold: 64,
1181 }),
1182 })
1183 .ok();
1184 let preset: Preset = Preset {
1185 name: Label::new(),
1186 buttons,
1187 encoders: heapless::Vec::new(),
1188 analog: heapless::Vec::new(),
1189 defaults: Default::default(),
1190 on_enter: heapless::Vec::new(),
1191 on_exit: heapless::Vec::new(),
1192 triggers: heapless::Vec::new(),
1193 };
1194
1195 assert_eq!(
1196 process_incoming_cc(&preset, 2, 80, 127),
1197 Some(ReactiveResult::Trigger(0, true))
1198 );
1199 assert_eq!(
1200 process_incoming_cc(&preset, 2, 80, 64),
1201 Some(ReactiveResult::Trigger(0, true))
1202 );
1203 assert_eq!(
1204 process_incoming_cc(&preset, 2, 80, 63),
1205 Some(ReactiveResult::Trigger(0, false))
1206 );
1207 assert_eq!(
1208 process_incoming_cc(&preset, 2, 80, 0),
1209 Some(ReactiveResult::Trigger(0, false))
1210 );
1211 }
1212
1213 #[test]
1214 fn preset_select_emits_system_action() {
1215 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1216 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1217 on_press.push(Action::PresetSelect(5)).ok();
1218 buttons
1219 .push(ButtonConfig {
1220 label: Label::new(),
1221 color: LedConfig::default(),
1222 mode: ButtonMode::Momentary,
1223 on_press,
1224 on_release: heapless::Vec::new(),
1225 on_long_press: heapless::Vec::new(),
1226 cycle_values: heapless::Vec::new(),
1227 listen_cc: None,
1228 })
1229 .ok();
1230 let preset: Preset = Preset {
1231 name: Label::new(),
1232 buttons,
1233 encoders: heapless::Vec::new(),
1234 analog: heapless::Vec::new(),
1235 defaults: Default::default(),
1236 on_enter: heapless::Vec::new(),
1237 on_exit: heapless::Vec::new(),
1238 triggers: heapless::Vec::new(),
1239 };
1240 let mut state = PresetState::default();
1241
1242 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1243 assert_eq!(r.system.len(), 1);
1244 assert_eq!(r.system[0], SystemAction::PresetSelect(5));
1245 assert!(r.midi.is_empty());
1247 }
1248
1249 #[test]
1250 fn bank_up_down_emit_system_actions() {
1251 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1252 let mut on_press_up: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1253 on_press_up.push(Action::BankUp).ok();
1254 let mut on_press_down: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1255 on_press_down.push(Action::BankDown).ok();
1256 buttons
1257 .push(ButtonConfig {
1258 label: Label::new(),
1259 color: LedConfig::default(),
1260 mode: ButtonMode::Momentary,
1261 on_press: on_press_up,
1262 on_release: heapless::Vec::new(),
1263 on_long_press: heapless::Vec::new(),
1264 cycle_values: heapless::Vec::new(),
1265 listen_cc: None,
1266 })
1267 .ok();
1268 buttons
1269 .push(ButtonConfig {
1270 label: Label::new(),
1271 color: LedConfig::default(),
1272 mode: ButtonMode::Momentary,
1273 on_press: on_press_down,
1274 on_release: heapless::Vec::new(),
1275 on_long_press: heapless::Vec::new(),
1276 cycle_values: heapless::Vec::new(),
1277 listen_cc: None,
1278 })
1279 .ok();
1280 let preset: Preset = Preset {
1281 name: Label::new(),
1282 buttons,
1283 encoders: heapless::Vec::new(),
1284 analog: heapless::Vec::new(),
1285 defaults: Default::default(),
1286 on_enter: heapless::Vec::new(),
1287 on_exit: heapless::Vec::new(),
1288 triggers: heapless::Vec::new(),
1289 };
1290 let mut state = PresetState::default();
1291
1292 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1293 assert_eq!(r.system.len(), 1);
1294 assert_eq!(r.system[0], SystemAction::PresetNext);
1295
1296 let r = process_button(&mut state, &preset, 1, ButtonEvent::Press);
1297 assert_eq!(r.system.len(), 1);
1298 assert_eq!(r.system[0], SystemAction::PresetPrev);
1299 }
1300
1301 fn make_cc_cycle_preset(reverse: bool) -> Preset {
1304 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1305 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1306 on_press
1307 .push(Action::CcCycle {
1308 cc: 50,
1309 channel: 1,
1310 reverse,
1311 })
1312 .ok();
1313 let mut cycle_values: heapless::Vec<u8, MAX_CYCLE_VALUES> = heapless::Vec::new();
1314 cycle_values.push(0).ok();
1315 cycle_values.push(32).ok();
1316 cycle_values.push(64).ok();
1317 cycle_values.push(127).ok();
1318 buttons
1319 .push(ButtonConfig {
1320 label: Label::new(),
1321 color: LedConfig::default(),
1322 mode: ButtonMode::Momentary,
1323 on_press,
1324 on_release: heapless::Vec::new(),
1325 on_long_press: heapless::Vec::new(),
1326 cycle_values,
1327 listen_cc: None,
1328 })
1329 .ok();
1330 Preset {
1331 name: Label::new(),
1332 buttons,
1333 encoders: heapless::Vec::new(),
1334 analog: heapless::Vec::new(),
1335 defaults: Default::default(),
1336 on_enter: heapless::Vec::new(),
1337 on_exit: heapless::Vec::new(),
1338 triggers: heapless::Vec::new(),
1339 }
1340 }
1341
1342 #[test]
1343 fn cc_cycle_forward_iterates_values() {
1344 let preset = make_cc_cycle_preset(false);
1345 let mut state = PresetState::default();
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, 0]));
1349
1350 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1351 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 32]));
1352
1353 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1354 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 64]));
1355
1356 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1357 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 127]));
1358 }
1359
1360 #[test]
1361 fn cc_cycle_forward_wraps_around() {
1362 let preset = make_cc_cycle_preset(false);
1363 let mut state = PresetState::default();
1364
1365 for _ in 0..4 {
1367 process_button(&mut state, &preset, 0, ButtonEvent::Press);
1368 }
1369 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1371 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 0]));
1372 }
1373
1374 #[test]
1375 fn cc_cycle_reverse_iterates_backward() {
1376 let preset = make_cc_cycle_preset(true);
1377 let mut state = PresetState::default();
1378
1379 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1381 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 0]));
1382
1383 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1385 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 127]));
1386
1387 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1389 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 50, 64]));
1390 }
1391
1392 #[test]
1393 fn cc_cycle_empty_values_no_output() {
1394 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1395 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1396 on_press
1397 .push(Action::CcCycle {
1398 cc: 50,
1399 channel: 1,
1400 reverse: false,
1401 })
1402 .ok();
1403 buttons
1404 .push(ButtonConfig {
1405 label: Label::new(),
1406 color: LedConfig::default(),
1407 mode: ButtonMode::Momentary,
1408 on_press,
1409 on_release: heapless::Vec::new(),
1410 on_long_press: heapless::Vec::new(),
1411 cycle_values: heapless::Vec::new(), listen_cc: None,
1413 })
1414 .ok();
1415 let preset: Preset = Preset {
1416 name: Label::new(),
1417 buttons,
1418 encoders: heapless::Vec::new(),
1419 analog: heapless::Vec::new(),
1420 defaults: Default::default(),
1421 on_enter: heapless::Vec::new(),
1422 on_exit: heapless::Vec::new(),
1423 triggers: heapless::Vec::new(),
1424 };
1425 let mut state = PresetState::default();
1426
1427 let r = process_button(&mut state, &preset, 0, ButtonEvent::Press);
1428 assert!(r.midi.is_empty());
1429 }
1430
1431 fn make_analog_preset(cc: u8, channel: u8, min: u8, max: u8) -> Preset {
1434 let mut analog: heapless::Vec<AnalogConfig, MAX_ANALOG> = heapless::Vec::new();
1435 analog
1436 .push(AnalogConfig {
1437 label: Label::try_from("Wah").unwrap(),
1438 cc,
1439 channel,
1440 min,
1441 max,
1442 })
1443 .ok();
1444 Preset {
1445 name: Label::new(),
1446 buttons: heapless::Vec::new(),
1447 encoders: heapless::Vec::new(),
1448 analog,
1449 defaults: Default::default(),
1450 on_enter: heapless::Vec::new(),
1451 on_exit: heapless::Vec::new(),
1452 triggers: heapless::Vec::new(),
1453 }
1454 }
1455
1456 #[test]
1457 fn analog_full_range_maps_to_max() {
1458 let preset = make_analog_preset(11, 1, 0, 127);
1459 let r = process_analog(&preset, 0, 3750, 0, 3750);
1460 assert_eq!(r.midi.len(), 1);
1461 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 127]));
1462 }
1463
1464 #[test]
1465 fn analog_min_maps_to_zero() {
1466 let preset = make_analog_preset(11, 1, 0, 127);
1467 let r = process_analog(&preset, 0, 0, 0, 3750);
1468 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 0]));
1469 }
1470
1471 #[test]
1472 fn analog_midpoint() {
1473 let preset = make_analog_preset(11, 1, 0, 127);
1474 let r = process_analog(&preset, 0, 1875, 0, 3750);
1475 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 63));
1477 }
1478
1479 #[test]
1480 fn analog_custom_range() {
1481 let preset = make_analog_preset(11, 1, 20, 100);
1482 let r = process_analog(&preset, 0, 3750, 0, 3750);
1483 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 100]));
1484
1485 let r = process_analog(&preset, 0, 0, 0, 3750);
1486 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 11, 20]));
1487 }
1488
1489 #[test]
1490 fn analog_clamps_below_adc_min() {
1491 let preset = make_analog_preset(11, 1, 0, 127);
1492 let r = process_analog(&preset, 0, 100, 200, 3750);
1494 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 0));
1495 }
1496
1497 #[test]
1498 fn analog_clamps_above_adc_max() {
1499 let preset = make_analog_preset(11, 1, 0, 127);
1500 let r = process_analog(&preset, 0, 4000, 0, 3750);
1502 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data[2] == 127));
1503 }
1504
1505 #[test]
1506 fn analog_emits_display_event() {
1507 let preset = make_analog_preset(11, 1, 0, 127);
1508 let r = process_analog(&preset, 0, 1875, 0, 3750);
1509 assert_eq!(r.display.len(), 1);
1510 match &r.display[0] {
1511 DisplayEvent::AnalogOverlay { side, label, value } => {
1512 assert_eq!(*side, DisplaySide::L);
1513 assert_eq!(label.as_str(), "Wah");
1514 assert_eq!(*value, 63);
1515 }
1516 _ => panic!("expected AnalogOverlay"),
1517 }
1518 }
1519
1520 #[test]
1521 fn analog_invalid_index_returns_empty() {
1522 let preset = make_analog_preset(11, 1, 0, 127);
1523 let r = process_analog(&preset, 5, 1875, 0, 3750);
1524 assert!(r.midi.is_empty());
1525 assert!(r.display.is_empty());
1526 }
1527
1528 fn make_trigger_preset(triggers: heapless::Vec<Trigger, MAX_TRIGGERS>) -> Preset {
1531 let mut buttons: heapless::Vec<ButtonConfig, MAX_BUTTONS> = heapless::Vec::new();
1532 let mut on_press: heapless::Vec<Action, MAX_ACTIONS> = heapless::Vec::new();
1533 on_press.push(Action::cc(80, 127, 1).unwrap()).ok();
1534 buttons
1535 .push(ButtonConfig {
1536 label: Label::new(),
1537 color: LedConfig::default(),
1538 mode: ButtonMode::Toggle,
1539 on_press,
1540 on_release: heapless::Vec::new(),
1541 on_long_press: heapless::Vec::new(),
1542 cycle_values: heapless::Vec::new(),
1543 listen_cc: None,
1544 })
1545 .ok();
1546 Preset {
1547 name: Label::new(),
1548 buttons,
1549 encoders: heapless::Vec::new(),
1550 analog: heapless::Vec::new(),
1551 defaults: Default::default(),
1552 on_enter: heapless::Vec::new(),
1553 on_exit: heapless::Vec::new(),
1554 triggers,
1555 }
1556 }
1557
1558 #[test]
1559 fn trigger_cc_activates_button() {
1560 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1561 triggers
1562 .push(Trigger {
1563 match_msg: TriggerMatch::Cc {
1564 cc: 100,
1565 channel: 1,
1566 value_min: 64,
1567 value_max: 127,
1568 },
1569 action: TriggerAction::Activate(0),
1570 })
1571 .ok();
1572 let preset = make_trigger_preset(triggers);
1573 let mut state = PresetState::default();
1574
1575 let r = process_triggers(&mut state, &preset, 0xB0, 100, 127);
1576 assert!(state.button_active[0]);
1577 assert!(r.led_dirty);
1578 }
1579
1580 #[test]
1581 fn trigger_cc_below_value_min_no_match() {
1582 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1583 triggers
1584 .push(Trigger {
1585 match_msg: TriggerMatch::Cc {
1586 cc: 100,
1587 channel: 1,
1588 value_min: 64,
1589 value_max: 127,
1590 },
1591 action: TriggerAction::Activate(0),
1592 })
1593 .ok();
1594 let preset = make_trigger_preset(triggers);
1595 let mut state = PresetState::default();
1596
1597 let r = process_triggers(&mut state, &preset, 0xB0, 100, 63);
1598 assert!(!state.button_active[0]);
1599 assert!(!r.led_dirty);
1600 }
1601
1602 #[test]
1603 fn trigger_deactivate_button() {
1604 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1605 triggers
1606 .push(Trigger {
1607 match_msg: TriggerMatch::Cc {
1608 cc: 100,
1609 channel: 1,
1610 value_min: 0,
1611 value_max: 63,
1612 },
1613 action: TriggerAction::Deactivate(0),
1614 })
1615 .ok();
1616 let preset = make_trigger_preset(triggers);
1617 let mut state = PresetState::default();
1618 state.button_active[0] = true;
1619
1620 let r = process_triggers(&mut state, &preset, 0xB0, 100, 0);
1621 assert!(!state.button_active[0]);
1622 assert!(r.led_dirty);
1623 }
1624
1625 #[test]
1626 fn trigger_program_change_selects_preset() {
1627 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1628 triggers
1629 .push(Trigger {
1630 match_msg: TriggerMatch::ProgramChange {
1631 program: 5,
1632 channel: 1,
1633 },
1634 action: TriggerAction::PresetSelect(3),
1635 })
1636 .ok();
1637 let preset = make_trigger_preset(triggers);
1638 let mut state = PresetState::default();
1639
1640 let r = process_triggers(&mut state, &preset, 0xC0, 5, 0);
1641 assert_eq!(r.system.len(), 1);
1642 assert_eq!(r.system[0], SystemAction::PresetSelect(3));
1643 }
1644
1645 #[test]
1646 fn trigger_note_on_executes_button() {
1647 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1648 triggers
1649 .push(Trigger {
1650 match_msg: TriggerMatch::NoteOn {
1651 note: 60,
1652 channel: 1,
1653 },
1654 action: TriggerAction::Execute(0),
1655 })
1656 .ok();
1657 let preset = make_trigger_preset(triggers);
1658 let mut state = PresetState::default();
1659
1660 let r = process_triggers(&mut state, &preset, 0x90, 60, 127);
1661 assert_eq!(r.midi.len(), 1);
1663 assert!(matches!(&r.midi[0], ActionStep::Send(m) if m.data == [0xB0, 80, 127]));
1664 }
1665
1666 #[test]
1667 fn trigger_note_on_velocity_zero_no_match() {
1668 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1669 triggers
1670 .push(Trigger {
1671 match_msg: TriggerMatch::NoteOn {
1672 note: 60,
1673 channel: 1,
1674 },
1675 action: TriggerAction::Activate(0),
1676 })
1677 .ok();
1678 let preset = make_trigger_preset(triggers);
1679 let mut state = PresetState::default();
1680
1681 let r = process_triggers(&mut state, &preset, 0x90, 60, 0);
1683 assert!(!state.button_active[0]);
1684 assert!(!r.led_dirty);
1685 }
1686
1687 #[test]
1688 fn trigger_wrong_channel_no_match() {
1689 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1690 triggers
1691 .push(Trigger {
1692 match_msg: TriggerMatch::Cc {
1693 cc: 100,
1694 channel: 2,
1695 value_min: 0,
1696 value_max: 127,
1697 },
1698 action: TriggerAction::Activate(0),
1699 })
1700 .ok();
1701 let preset = make_trigger_preset(triggers);
1702 let mut state = PresetState::default();
1703
1704 let r = process_triggers(&mut state, &preset, 0xB0, 100, 127);
1706 assert!(!state.button_active[0]);
1707 assert!(!r.led_dirty);
1708 }
1709
1710 #[test]
1711 fn trigger_multiple_triggers_all_fire() {
1712 let mut triggers: heapless::Vec<Trigger, MAX_TRIGGERS> = heapless::Vec::new();
1713 triggers
1714 .push(Trigger {
1715 match_msg: TriggerMatch::Cc {
1716 cc: 100,
1717 channel: 1,
1718 value_min: 0,
1719 value_max: 127,
1720 },
1721 action: TriggerAction::Activate(0),
1722 })
1723 .ok();
1724 triggers
1725 .push(Trigger {
1726 match_msg: TriggerMatch::Cc {
1727 cc: 100,
1728 channel: 1,
1729 value_min: 64,
1730 value_max: 127,
1731 },
1732 action: TriggerAction::PresetSelect(2),
1733 })
1734 .ok();
1735 let preset = make_trigger_preset(triggers);
1736 let mut state = PresetState::default();
1737
1738 let r = process_triggers(&mut state, &preset, 0xB0, 100, 100);
1740 assert!(state.button_active[0]);
1741 assert!(r.led_dirty);
1742 assert_eq!(r.system.len(), 1);
1743 assert_eq!(r.system[0], SystemAction::PresetSelect(2));
1744 }
1745}