1use heapless::{String, Vec};
8use serde::{Deserialize, Serialize};
9
10pub const PRESET_SCHEMA_VERSION: u8 = 5;
13
14pub const MAX_PRESETS: usize = 32;
15pub const MAX_BUTTONS: usize = 6;
16pub const MAX_ENCODERS: usize = 2;
17pub const MAX_ANALOG: usize = 2;
18pub const MAX_LABEL_LEN: usize = 16;
19pub const MAX_ACTIONS: usize = 8;
20pub const MAX_CYCLE_VALUES: usize = 12;
21
22pub type Label = String<MAX_LABEL_LEN>;
23
24pub const GLOBAL_CONFIG_RESOURCE: u8 = 0x7F;
26
27pub const SYSTEM_COMMAND_RESOURCE: u8 = 0x7E;
29
30pub const DEVICE_INFO_RESOURCE: u8 = 0x7D;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(u8)]
36pub enum SystemCommand {
37 Reboot = 0x01,
38 Bootloader = 0x02,
39 FactoryReset = 0x03,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct DeviceInfo {
45 pub flash_format: u8,
47 pub presets_loaded: u8,
49 pub presets_skipped: u8,
51 pub version: String<24>,
53}
54
55impl SystemCommand {
56 pub fn from_byte(b: u8) -> Option<Self> {
57 match b {
58 0x01 => Some(Self::Reboot),
59 0x02 => Some(Self::Bootloader),
60 0x03 => Some(Self::FactoryReset),
61 _ => None,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct GlobalConfig {
70 #[serde(default = "default_true")]
72 pub din_enabled: bool,
73 #[serde(default = "default_true")]
75 pub din_to_usb_thru: bool,
76 #[serde(default)]
78 pub usb_to_din_thru: bool,
79 #[serde(default)]
81 pub usb_to_usb_thru: bool,
82 #[serde(default)]
84 pub midi_clock: bool,
85 #[serde(default = "default_bpm")]
87 pub bpm: u16,
88 #[serde(default)]
90 pub exp1_min: u16,
91 #[serde(default = "default_adc_max")]
93 pub exp1_max: u16,
94 #[serde(default)]
96 pub exp2_min: u16,
97 #[serde(default = "default_adc_max")]
99 pub exp2_max: u16,
100}
101
102fn default_true() -> bool {
103 true
104}
105
106fn default_bpm() -> u16 {
107 120
108}
109
110fn default_adc_max() -> u16 {
111 3750
112}
113
114impl Default for GlobalConfig {
115 fn default() -> Self {
116 Self {
117 din_enabled: true,
118 din_to_usb_thru: true,
119 usb_to_din_thru: false,
120 usb_to_usb_thru: false,
121 midi_clock: false,
122 bpm: 120,
123 exp1_min: 0,
124 exp1_max: 3750,
125 exp2_min: 0,
126 exp2_max: 3750,
127 }
128 }
129}
130
131impl GlobalConfig {
132 pub fn tick_interval_us(&self) -> u32 {
134 if self.bpm == 0 {
135 return 20_833; }
137 60_000_000 / (self.bpm as u32 * 24)
139 }
140}
141
142#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
143pub struct Config {
144 #[serde(default)]
145 pub global: GlobalConfig,
146 pub presets: Vec<Preset, MAX_PRESETS>,
147}
148
149#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
150pub struct Preset {
151 pub name: Label,
152 pub buttons: Vec<ButtonConfig, MAX_BUTTONS>,
153 pub encoders: Vec<EncoderConfig, MAX_ENCODERS>,
154 pub analog: Vec<AnalogConfig, MAX_ANALOG>,
155 #[serde(default)]
157 pub defaults: InitialState,
158 #[serde(default)]
160 pub on_enter: Vec<Action, MAX_ACTIONS>,
161 #[serde(default)]
163 pub on_exit: Vec<Action, MAX_ACTIONS>,
164 #[serde(default)]
166 pub triggers: Vec<Trigger, MAX_TRIGGERS>,
167}
168
169pub const MAX_TRIGGERS: usize = 8;
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct Trigger {
174 pub match_msg: TriggerMatch,
176 pub action: TriggerAction,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub enum TriggerMatch {
183 Cc {
185 cc: u8,
186 channel: u8,
187 #[serde(default)]
188 value_min: u8,
189 #[serde(default = "default_value_max")]
190 value_max: u8,
191 },
192 ProgramChange { program: u8, channel: u8 },
194 NoteOn { note: u8, channel: u8 },
196}
197
198fn default_value_max() -> u8 {
199 127
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub enum TriggerAction {
205 Activate(u8),
207 Deactivate(u8),
209 PresetSelect(u8),
211 Execute(u8),
213}
214
215#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
217pub struct InitialState {
218 #[serde(default)]
220 pub button_active: Vec<bool, MAX_BUTTONS>,
221 #[serde(default)]
223 pub encoder_values: Vec<u8, MAX_ENCODERS>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct ButtonConfig {
230 pub label: Label,
231 pub color: LedConfig,
232 pub mode: ButtonMode,
233 pub on_press: Vec<Action, MAX_ACTIONS>,
234 pub on_release: Vec<Action, MAX_ACTIONS>,
235 pub on_long_press: Vec<Action, MAX_ACTIONS>,
236 #[serde(default)]
237 pub cycle_values: Vec<u8, MAX_CYCLE_VALUES>,
238 #[serde(default)]
240 pub listen_cc: Option<ListenCc>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct ListenCc {
246 pub cc: u8,
247 pub channel: u8,
248 #[serde(default)]
250 pub mode: ListenMode,
251 #[serde(default = "default_threshold")]
253 pub threshold: u8,
254}
255
256fn default_threshold() -> u8 {
257 64
258}
259
260#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
262pub enum ListenMode {
263 #[default]
265 Heatmap,
266 Trigger,
268}
269
270#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
271pub enum ButtonMode {
272 #[default]
274 Momentary,
275 Toggle,
277 RadioGroup(u8),
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284pub enum Action {
285 Midi { data: [u8; 3], len: u8 },
288 CcCycle { cc: u8, channel: u8, reverse: bool },
290 SetLed {
292 color: Color,
293 animation: LedAnimation,
294 },
295 Delay(u16),
297 PresetSelect(u8),
299 PresetNext,
301 PresetPrev,
303 BankUp,
305 BankDown,
307 TapTempo,
310}
311
312impl Action {
313 pub fn cc(cc: u8, value: u8, channel: u8) -> Option<Self> {
316 if cc > 127 || value > 127 || channel == 0 || channel > 16 {
317 return None;
318 }
319 Some(Self::Midi {
320 data: [0xB0 | (channel - 1), cc, value],
321 len: 3,
322 })
323 }
324 pub fn program_change(program: u8, channel: u8) -> Option<Self> {
327 if program > 127 || channel == 0 || channel > 16 {
328 return None;
329 }
330 Some(Self::Midi {
331 data: [0xC0 | (channel - 1), program, 0],
332 len: 2,
333 })
334 }
335 pub fn note_on(note: u8, channel: u8) -> Option<Self> {
338 if note > 127 || channel == 0 || channel > 16 {
339 return None;
340 }
341 Some(Self::Midi {
342 data: [0x90 | (channel - 1), note, 127],
343 len: 3,
344 })
345 }
346 pub fn note_off(note: u8, channel: u8) -> Option<Self> {
349 if note > 127 || channel == 0 || channel > 16 {
350 return None;
351 }
352 Some(Self::Midi {
353 data: [0x80 | (channel - 1), note, 0],
354 len: 3,
355 })
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct EncoderConfig {
363 pub label: Label,
364 pub action: EncoderAction,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub enum EncoderAction {
369 Cc {
370 cc: u16,
371 channel: u8,
372 min: u8,
373 max: u8,
374 },
375 CcRelative {
377 cc: u8,
378 channel: u8,
379 increment: u8,
380 decrement: u8,
381 },
382 PresetScroll,
383}
384
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct AnalogConfig {
389 pub label: Label,
390 pub cc: u8,
391 pub channel: u8,
392 pub min: u8,
393 pub max: u8,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
399pub struct LedConfig {
400 pub on: Color,
402 pub off: Color,
404 #[serde(default)]
406 pub animation: LedAnimation,
407 #[serde(default)]
409 pub renderer: LedRenderer,
410 #[serde(default)]
412 pub renderer_param: u8,
413}
414
415#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
416pub enum Color {
417 #[default]
418 Off,
419 Red,
420 Green,
421 Blue,
422 Yellow,
423 Cyan,
424 Magenta,
425 White,
426 Orange,
427 Purple,
428 Custom(u8, u8, u8), }
430
431#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
432pub enum LedAnimation {
433 #[default]
434 Solid,
435 Blink,
436 Pulse,
437 Rotate,
438 ColorCycle,
439}
440
441#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
442pub enum LedRenderer {
443 #[default]
445 Solid,
446 Fill,
448 Single,
450 Dots,
452}
453
454impl Default for LedConfig {
457 fn default() -> Self {
458 LedConfig {
459 on: Color::Off,
460 off: Color::Off,
461 animation: LedAnimation::Solid,
462 renderer: LedRenderer::Solid,
463 renderer_param: 0,
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn serialize_roundtrip_morningstar_style() {
474 let config = Config {
475 global: GlobalConfig::default(),
476 presets: {
477 let mut p = Vec::new();
478 let _ = p.push(Preset {
479 name: Label::try_from("Live FX").unwrap(),
480 buttons: {
481 let mut b = Vec::new();
482 let _ = b.push(ButtonConfig {
483 label: Label::try_from("Board 1").unwrap(),
484 color: LedConfig {
485 on: Color::Blue,
486 off: Color::Off,
487 animation: LedAnimation::Solid,
488 renderer: LedRenderer::Solid,
489 renderer_param: 0,
490 },
491 mode: ButtonMode::RadioGroup(1),
492 on_press: {
493 let mut a = Vec::new();
494 let _ = a.push(Action::program_change(0, 2).unwrap());
495 let _ = a.push(Action::SetLed {
496 color: Color::Blue,
497 animation: LedAnimation::Solid,
498 });
499 a
500 },
501 on_release: Vec::new(),
502 on_long_press: Vec::new(),
503 cycle_values: Vec::new(),
504 listen_cc: None,
505 });
506 b
507 },
508 encoders: {
509 let mut e = Vec::new();
510 let _ = e.push(EncoderConfig {
511 label: Label::try_from("Vol").unwrap(),
512 action: EncoderAction::Cc {
513 cc: 7,
514 channel: 1,
515 min: 0,
516 max: 127,
517 },
518 });
519 e
520 },
521 analog: Vec::new(),
522 defaults: Default::default(),
523 on_enter: Vec::new(),
524 on_exit: Vec::new(),
525 triggers: Vec::new(),
526 });
527 p
528 },
529 };
530
531 let mut buf = [0u8; 512];
532 let bytes = postcard::to_slice(&config, &mut buf).unwrap();
533 let decoded: Config = postcard::from_bytes(bytes).unwrap();
534 assert_eq!(config, decoded);
535 }
536
537 #[test]
538 fn multi_action_button() {
539 let btn = ButtonConfig {
541 label: Label::try_from("Scene 1").unwrap(),
542 color: LedConfig {
543 on: Color::Green,
544 off: Color::Off,
545 animation: LedAnimation::Solid,
546 renderer: LedRenderer::Solid,
547 renderer_param: 0,
548 },
549 mode: ButtonMode::Momentary,
550 on_press: {
551 let mut a = Vec::new();
552 let _ = a.push(Action::program_change(0, 1).unwrap());
553 let _ = a.push(Action::cc(69, 127, 1).unwrap());
554 let _ = a.push(Action::Delay(50));
555 let _ = a.push(Action::cc(70, 0, 1).unwrap());
556 a
557 },
558 on_release: Vec::new(),
559 on_long_press: {
560 let mut a = Vec::new();
561 let _ = a.push(Action::PresetNext);
562 a
563 },
564 cycle_values: Vec::new(),
565 listen_cc: None,
566 };
567
568 let mut buf = [0u8; 256];
569 let bytes = postcard::to_slice(&btn, &mut buf).unwrap();
570 assert!(bytes.len() < 80);
571 let decoded: ButtonConfig = postcard::from_bytes(bytes).unwrap();
572 assert_eq!(btn, decoded);
573 }
574
575 #[test]
576 fn global_config_postcard_roundtrip() {
577 let gc = GlobalConfig {
578 din_enabled: true,
579 din_to_usb_thru: true,
580 usb_to_din_thru: false,
581 usb_to_usb_thru: false,
582 midi_clock: true,
583 bpm: 140,
584 exp1_min: 180,
585 exp1_max: 3700,
586 exp2_min: 200,
587 exp2_max: 3750,
588 };
589 let mut buf = [0u8; 32];
590 let bytes = postcard::to_slice(&gc, &mut buf).unwrap();
591 let decoded: GlobalConfig = postcard::from_bytes(bytes).unwrap();
592 assert_eq!(gc, decoded);
593 }
594
595 #[test]
596 fn global_config_default_values() {
597 let gc = GlobalConfig::default();
598 assert!(gc.din_enabled);
599 assert!(gc.din_to_usb_thru);
600 assert!(!gc.usb_to_din_thru);
601 assert!(!gc.usb_to_usb_thru);
602 assert!(!gc.midi_clock);
603 assert_eq!(gc.bpm, 120);
604 }
605
606 #[test]
607 fn global_config_tick_interval_120bpm() {
608 let gc = GlobalConfig {
609 bpm: 120,
610 ..Default::default()
611 };
612 assert_eq!(gc.tick_interval_us(), 20833);
614 }
615
616 #[test]
617 fn global_config_tick_interval_60bpm() {
618 let gc = GlobalConfig {
619 bpm: 60,
620 ..Default::default()
621 };
622 assert_eq!(gc.tick_interval_us(), 41666);
624 }
625
626 #[test]
627 fn global_config_tick_interval_zero_bpm_fallback() {
628 let gc = GlobalConfig {
629 bpm: 0,
630 ..Default::default()
631 };
632 assert_eq!(gc.tick_interval_us(), 20833);
633 }
634
635 #[test]
636 fn global_config_compact_serialization() {
637 let gc = GlobalConfig::default();
638 let mut buf = [0u8; 32];
639 let bytes = postcard::to_slice(&gc, &mut buf).unwrap();
640 assert_eq!(bytes.len(), 12);
642 }
643
644 #[test]
645 fn action_cc_validates_ranges() {
646 assert!(Action::cc(0, 0, 1).is_some());
647 assert!(Action::cc(127, 127, 16).is_some());
648 assert!(Action::cc(0, 0, 0).is_none()); assert!(Action::cc(0, 0, 17).is_none()); }
652
653 #[test]
654 fn action_note_on_validates_ranges() {
655 assert!(Action::note_on(0, 1).is_some());
656 assert!(Action::note_on(127, 16).is_some());
657 assert!(Action::note_on(60, 0).is_none()); assert!(Action::note_on(60, 17).is_none()); }
660
661 #[test]
662 fn action_program_change_validates_ranges() {
663 assert!(Action::program_change(0, 1).is_some());
664 assert!(Action::program_change(127, 16).is_some());
665 assert!(Action::program_change(5, 0).is_none()); assert!(Action::program_change(5, 17).is_none()); }
668
669 #[test]
679 fn preset_serialization_layout_is_stable() {
680 let preset = Preset {
682 name: Label::try_from("Stable").unwrap(),
683 buttons: {
684 let mut b = Vec::new();
685 let _ = b.push(ButtonConfig {
686 label: Label::try_from("Btn").unwrap(),
687 color: LedConfig {
688 on: Color::Red,
689 off: Color::Off,
690 animation: LedAnimation::Blink,
691 renderer: LedRenderer::Fill,
692 renderer_param: 6,
693 },
694 mode: ButtonMode::Toggle,
695 on_press: {
696 let mut a = Vec::new();
697 let _ = a.push(Action::cc(80, 127, 1).unwrap());
698 a
699 },
700 on_release: {
701 let mut a = Vec::new();
702 let _ = a.push(Action::cc(80, 0, 1).unwrap());
703 a
704 },
705 on_long_press: {
706 let mut a = Vec::new();
707 let _ = a.push(Action::PresetNext);
708 a
709 },
710 cycle_values: {
711 let mut cv = Vec::new();
712 let _ = cv.push(0);
713 let _ = cv.push(64);
714 let _ = cv.push(127);
715 cv
716 },
717 listen_cc: Some(ListenCc {
718 cc: 100,
719 channel: 1,
720 mode: ListenMode::Trigger,
721 threshold: 64,
722 }),
723 });
724 b
725 },
726 encoders: {
727 let mut e = Vec::new();
728 let _ = e.push(EncoderConfig {
729 label: Label::try_from("Vol").unwrap(),
730 action: EncoderAction::Cc {
731 cc: 7,
732 channel: 1,
733 min: 0,
734 max: 127,
735 },
736 });
737 e
738 },
739 analog: {
740 let mut a = Vec::new();
741 let _ = a.push(AnalogConfig {
742 label: Label::try_from("Wah").unwrap(),
743 cc: 11,
744 channel: 1,
745 min: 0,
746 max: 127,
747 });
748 a
749 },
750 defaults: InitialState {
751 button_active: {
752 let mut v = Vec::new();
753 let _ = v.push(true);
754 v
755 },
756 encoder_values: {
757 let mut v = Vec::new();
758 let _ = v.push(100);
759 v
760 },
761 },
762 on_enter: {
763 let mut a = Vec::new();
764 let _ = a.push(Action::program_change(0, 2).unwrap());
765 a
766 },
767 on_exit: {
768 let mut a = Vec::new();
769 let _ = a.push(Action::cc(123, 0, 1).unwrap());
770 a
771 },
772 triggers: {
773 let mut t = Vec::new();
774 let _ = t.push(Trigger {
775 match_msg: TriggerMatch::Cc {
776 cc: 80,
777 channel: 1,
778 value_min: 64,
779 value_max: 127,
780 },
781 action: TriggerAction::Activate(0),
782 });
783 t
784 },
785 };
786
787 let mut buf = [0u8; 512];
788 let bytes = postcard::to_slice(&preset, &mut buf).unwrap();
789
790 fn fnv1a(data: &[u8]) -> u32 {
796 let mut hash: u32 = 0x811c_9dc5;
797 for &b in data {
798 hash ^= b as u32;
799 hash = hash.wrapping_mul(0x0100_0193);
800 }
801 hash
802 }
803
804 let hash = fnv1a(bytes);
805 const EXPECTED_HASH: u32 = 0x087d_6074;
806 const EXPECTED_VERSION: u8 = 5;
807
808 assert_eq!(
809 PRESET_SCHEMA_VERSION, EXPECTED_VERSION,
810 "PRESET_SCHEMA_VERSION changed — update EXPECTED_VERSION and EXPECTED_HASH"
811 );
812 assert_eq!(
813 hash,
814 EXPECTED_HASH,
815 "Serialized Preset layout changed (hash {:#010x} != expected {:#010x}).\n\
816 If layout changed, bump PRESET_SCHEMA_VERSION in config.rs.\n\
817 Then update both EXPECTED_VERSION and EXPECTED_HASH.\n\
818 Serialized bytes ({} bytes): {:02x?}",
819 hash,
820 EXPECTED_HASH,
821 bytes.len(),
822 bytes
823 );
824 }
825}