1use crate::config::{Action, EncoderAction, Preset};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct MidiMessage {
8 pub data: [u8; 3],
9 pub len: usize,
10}
11
12pub fn execute_button_press<const B: usize, const E: usize, const A: usize>(
14 preset: &Preset<B, E, A>,
15 btn_idx: usize,
16) -> heapless::Vec<MidiMessage, 8> {
17 let mut messages = heapless::Vec::new();
18 let Some(btn) = preset.buttons.get(btn_idx) else {
19 return messages;
20 };
21 for action in &btn.on_press {
22 if let Some(msg) = action_to_midi(action) {
23 messages.push(msg).ok();
24 }
25 }
26 messages
27}
28
29pub fn action_to_midi(action: &Action) -> Option<MidiMessage> {
31 match action {
32 Action::Midi { data, len } => {
33 let len = (*len as usize).min(3);
34 if len == 0 {
35 return None;
36 }
37 Some(MidiMessage { data: *data, len })
38 }
39 _ => None,
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum EncoderDirection {
46 Clockwise,
47 CounterClockwise,
48}
49
50pub fn encoder_cc<const B: usize, const E: usize, const A: usize>(
53 preset: &Preset<B, E, A>,
54 encoder_idx: usize,
55 direction: EncoderDirection,
56 current_value: &mut u8,
57) -> Option<MidiMessage> {
58 let enc = preset.encoders.get(encoder_idx)?;
59 match &enc.action {
60 EncoderAction::Cc {
61 cc,
62 channel,
63 min,
64 max,
65 } => {
66 let val = match direction {
67 EncoderDirection::Clockwise => (*current_value).saturating_add(1).min(*max),
68 EncoderDirection::CounterClockwise => (*current_value).saturating_sub(1).max(*min),
69 };
70 *current_value = val;
71 Some(MidiMessage {
72 data: [0xB0 | (channel - 1), *cc as u8, val],
73 len: 3,
74 })
75 }
76 EncoderAction::CcRelative {
77 cc,
78 channel,
79 increment,
80 decrement,
81 } => {
82 let val = match direction {
83 EncoderDirection::Clockwise => *increment,
84 EncoderDirection::CounterClockwise => *decrement,
85 };
86 Some(MidiMessage {
87 data: [0xB0 | (channel - 1), *cc, val],
88 len: 3,
89 })
90 }
91 EncoderAction::PresetScroll => None,
92 }
93}
94
95pub fn analog_cc<const B: usize, const E: usize, const A: usize>(
97 preset: &Preset<B, E, A>,
98 analog_idx: usize,
99 raw: u16,
100 adc_min: u16,
101 adc_max: u16,
102) -> Option<MidiMessage> {
103 let cfg = preset.analog.get(analog_idx)?;
104 let range = cfg.max - cfg.min;
105 let clamped = raw.clamp(adc_min, adc_max);
107 let span = adc_max.saturating_sub(adc_min).max(1) as u32;
108 let value =
109 cfg.min + (((clamped - adc_min) as u32 * range as u32) / span).min(range as u32) as u8;
110 Some(MidiMessage {
111 data: [0xB0 | (cfg.channel - 1), cfg.cc, value],
112 len: 3,
113 })
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::config::*;
120 use heapless::Vec;
121
122 fn make_preset(buttons: Vec<ButtonConfig, MAX_BUTTONS>) -> Preset {
123 Preset {
124 name: Label::new(),
125 buttons,
126 encoders: Vec::new(),
127 analog: Vec::new(),
128 defaults: Default::default(),
129 on_enter: heapless::Vec::new(),
130 on_exit: heapless::Vec::new(),
131 triggers: heapless::Vec::new(),
132 }
133 }
134
135 #[test]
136 fn note_on_action() {
137 let msg = action_to_midi(&Action::note_on(60, 1).unwrap()).unwrap();
138 assert_eq!(msg.data, [0x90, 60, 127]);
139 assert_eq!(msg.len, 3);
140 }
141
142 #[test]
143 fn note_on_channel_2() {
144 let msg = action_to_midi(&Action::note_on(64, 2).unwrap()).unwrap();
145 assert_eq!(msg.data, [0x91, 64, 127]);
146 }
147
148 #[test]
149 fn cc_action() {
150 let msg = action_to_midi(&Action::cc(10, 127, 1).unwrap()).unwrap();
151 assert_eq!(msg.data, [0xB0, 10, 127]);
152 assert_eq!(msg.len, 3);
153 }
154
155 #[test]
156 fn program_change_action() {
157 let msg = action_to_midi(&Action::program_change(5, 3).unwrap()).unwrap();
158 assert_eq!(msg.data, [0xC2, 5, 0]);
159 assert_eq!(msg.len, 2);
160 }
161
162 #[test]
163 fn unsupported_action_returns_none() {
164 assert!(action_to_midi(&Action::Delay(100)).is_none());
165 assert!(action_to_midi(&Action::PresetNext).is_none());
166 }
167
168 #[test]
169 fn execute_button_press_multi_action() {
170 let mut buttons: Vec<ButtonConfig, MAX_BUTTONS> = Vec::new();
171 let mut on_press: Vec<Action, MAX_ACTIONS> = Vec::new();
172 on_press.push(Action::program_change(0, 1).unwrap()).ok();
173 on_press.push(Action::cc(69, 127, 1).unwrap()).ok();
174 buttons
175 .push(ButtonConfig {
176 label: Label::new(),
177 color: LedConfig::default(),
178 mode: ButtonMode::default(),
179 on_press,
180 cycle_values: Vec::new(),
181 listen_cc: None,
182 on_release: Vec::new(),
183 on_long_press: Vec::new(),
184 })
185 .ok();
186
187 let preset = make_preset(buttons);
188 let msgs = execute_button_press(&preset, 0);
189 assert_eq!(msgs.len(), 2);
190 assert_eq!(msgs[0].data, [0xC0, 0, 0]);
191 assert_eq!(msgs[1].data, [0xB0, 69, 127]);
192 }
193
194 #[test]
195 fn execute_button_press_invalid_index() {
196 let preset = make_preset(Vec::new());
197 let msgs = execute_button_press(&preset, 5);
198 assert!(msgs.is_empty());
199 }
200
201 fn make_encoder_preset() -> Preset {
202 let mut encoders: Vec<EncoderConfig, MAX_ENCODERS> = Vec::new();
203 encoders
204 .push(EncoderConfig {
205 label: Label::try_from("Vol").unwrap(),
206 action: EncoderAction::Cc {
207 cc: 7,
208 channel: 1,
209 min: 0,
210 max: 127,
211 },
212 })
213 .ok();
214 encoders
215 .push(EncoderConfig {
216 label: Label::try_from("Pan").unwrap(),
217 action: EncoderAction::CcRelative {
218 cc: 16,
219 channel: 2,
220 increment: 65,
221 decrement: 63,
222 },
223 })
224 .ok();
225 Preset {
226 name: Label::new(),
227 buttons: Vec::new(),
228 encoders,
229 analog: Vec::new(),
230 defaults: Default::default(),
231 on_enter: heapless::Vec::new(),
232 on_exit: heapless::Vec::new(),
233 triggers: heapless::Vec::new(),
234 }
235 }
236
237 #[test]
238 fn encoder_cc_clockwise() {
239 let preset = make_encoder_preset();
240 let mut val = 64u8;
241 let msg = encoder_cc(&preset, 0, EncoderDirection::Clockwise, &mut val).unwrap();
242 assert_eq!(msg.data, [0xB0, 7, 65]);
243 assert_eq!(val, 65);
244 }
245
246 #[test]
247 fn encoder_cc_counter_clockwise() {
248 let preset = make_encoder_preset();
249 let mut val = 64u8;
250 let msg = encoder_cc(&preset, 0, EncoderDirection::CounterClockwise, &mut val).unwrap();
251 assert_eq!(msg.data, [0xB0, 7, 63]);
252 assert_eq!(val, 63);
253 }
254
255 #[test]
256 fn encoder_cc_clamps_at_max() {
257 let preset = make_encoder_preset();
258 let mut val = 127u8;
259 let msg = encoder_cc(&preset, 0, EncoderDirection::Clockwise, &mut val).unwrap();
260 assert_eq!(msg.data, [0xB0, 7, 127]);
261 assert_eq!(val, 127);
262 }
263
264 #[test]
265 fn encoder_cc_clamps_at_min() {
266 let preset = make_encoder_preset();
267 let mut val = 0u8;
268 let msg = encoder_cc(&preset, 0, EncoderDirection::CounterClockwise, &mut val).unwrap();
269 assert_eq!(msg.data, [0xB0, 7, 0]);
270 assert_eq!(val, 0);
271 }
272
273 #[test]
274 fn encoder_cc_relative() {
275 let preset = make_encoder_preset();
276 let mut val = 0u8;
277 let msg = encoder_cc(&preset, 1, EncoderDirection::Clockwise, &mut val).unwrap();
278 assert_eq!(msg.data, [0xB1, 16, 65]);
279 let msg = encoder_cc(&preset, 1, EncoderDirection::CounterClockwise, &mut val).unwrap();
280 assert_eq!(msg.data, [0xB1, 16, 63]);
281 }
282
283 #[test]
284 fn encoder_invalid_index_returns_none() {
285 let preset = make_encoder_preset();
286 let mut val = 0u8;
287 assert!(encoder_cc(&preset, 5, EncoderDirection::Clockwise, &mut val).is_none());
288 }
289
290 #[test]
291 fn analog_cc_mid_range() {
292 let mut analog: Vec<AnalogConfig, MAX_ANALOG> = Vec::new();
293 analog
294 .push(AnalogConfig {
295 label: Label::try_from("Exp").unwrap(),
296 cc: 11,
297 channel: 1,
298 min: 0,
299 max: 127,
300 })
301 .ok();
302 let preset: Preset = Preset {
303 name: Label::new(),
304 buttons: Vec::new(),
305 encoders: Vec::new(),
306 analog,
307 defaults: Default::default(),
308 on_enter: heapless::Vec::new(),
309 on_exit: heapless::Vec::new(),
310 triggers: heapless::Vec::new(),
311 };
312 let msg = analog_cc(&preset, 0, 2048, 0, 4095).unwrap();
313 assert_eq!(msg.data[0], 0xB0);
314 assert_eq!(msg.data[1], 11);
315 assert!(msg.data[2] >= 63 && msg.data[2] <= 64);
317 }
318
319 #[test]
320 fn analog_cc_with_range() {
321 let mut analog: Vec<AnalogConfig, MAX_ANALOG> = Vec::new();
322 analog
323 .push(AnalogConfig {
324 label: Label::new(),
325 cc: 4,
326 channel: 2,
327 min: 20,
328 max: 100,
329 })
330 .ok();
331 let preset: Preset = Preset {
332 name: Label::new(),
333 buttons: Vec::new(),
334 encoders: Vec::new(),
335 analog,
336 defaults: Default::default(),
337 on_enter: heapless::Vec::new(),
338 on_exit: heapless::Vec::new(),
339 triggers: heapless::Vec::new(),
340 };
341 let msg = analog_cc(&preset, 0, 4095, 0, 4095).unwrap();
343 assert_eq!(msg.data, [0xB1, 4, 100]);
344 let msg = analog_cc(&preset, 0, 0, 0, 4095).unwrap();
346 assert_eq!(msg.data, [0xB1, 4, 20]);
347 }
348
349 #[test]
350 fn analog_invalid_index_returns_none() {
351 let preset: Preset = Preset {
352 name: Label::new(),
353 buttons: Vec::new(),
354 encoders: Vec::new(),
355 analog: Vec::new(),
356 defaults: Default::default(),
357 on_enter: heapless::Vec::new(),
358 on_exit: heapless::Vec::new(),
359 triggers: heapless::Vec::new(),
360 };
361 assert!(analog_cc(&preset, 0, 2048, 0, 4095).is_none());
362 }
363
364 #[test]
365 fn analog_cc_with_calibration() {
366 let mut analog: Vec<AnalogConfig, MAX_ANALOG> = Vec::new();
367 analog
368 .push(AnalogConfig {
369 label: Label::new(),
370 cc: 7,
371 channel: 1,
372 min: 0,
373 max: 127,
374 })
375 .ok();
376 let preset: Preset = Preset {
377 name: Label::new(),
378 buttons: Vec::new(),
379 encoders: Vec::new(),
380 analog,
381 defaults: Default::default(),
382 on_enter: heapless::Vec::new(),
383 on_exit: heapless::Vec::new(),
384 triggers: heapless::Vec::new(),
385 };
386 let msg = analog_cc(&preset, 0, 200, 200, 3700).unwrap();
389 assert_eq!(msg.data[2], 0);
390 let msg = analog_cc(&preset, 0, 3700, 200, 3700).unwrap();
392 assert_eq!(msg.data[2], 127);
393 let msg = analog_cc(&preset, 0, 1950, 200, 3700).unwrap();
395 assert!(msg.data[2] >= 63 && msg.data[2] <= 64);
396 let msg = analog_cc(&preset, 0, 50, 200, 3700).unwrap();
398 assert_eq!(msg.data[2], 0);
399 let msg = analog_cc(&preset, 0, 4000, 200, 3700).unwrap();
401 assert_eq!(msg.data[2], 127);
402 }
403}