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 ..Default::default()
213 })
214 .ok();
215 encoders
216 .push(EncoderConfig {
217 label: Label::try_from("Pan").unwrap(),
218 action: EncoderAction::CcRelative {
219 cc: 16,
220 channel: 2,
221 increment: 65,
222 decrement: 63,
223 },
224 ..Default::default()
225 })
226 .ok();
227 Preset {
228 name: Label::new(),
229 buttons: Vec::new(),
230 encoders,
231 analog: Vec::new(),
232 defaults: Default::default(),
233 on_enter: heapless::Vec::new(),
234 on_exit: heapless::Vec::new(),
235 triggers: heapless::Vec::new(),
236 }
237 }
238
239 #[test]
240 fn encoder_cc_clockwise() {
241 let preset = make_encoder_preset();
242 let mut val = 64u8;
243 let msg = encoder_cc(&preset, 0, EncoderDirection::Clockwise, &mut val).unwrap();
244 assert_eq!(msg.data, [0xB0, 7, 65]);
245 assert_eq!(val, 65);
246 }
247
248 #[test]
249 fn encoder_cc_counter_clockwise() {
250 let preset = make_encoder_preset();
251 let mut val = 64u8;
252 let msg = encoder_cc(&preset, 0, EncoderDirection::CounterClockwise, &mut val).unwrap();
253 assert_eq!(msg.data, [0xB0, 7, 63]);
254 assert_eq!(val, 63);
255 }
256
257 #[test]
258 fn encoder_cc_clamps_at_max() {
259 let preset = make_encoder_preset();
260 let mut val = 127u8;
261 let msg = encoder_cc(&preset, 0, EncoderDirection::Clockwise, &mut val).unwrap();
262 assert_eq!(msg.data, [0xB0, 7, 127]);
263 assert_eq!(val, 127);
264 }
265
266 #[test]
267 fn encoder_cc_clamps_at_min() {
268 let preset = make_encoder_preset();
269 let mut val = 0u8;
270 let msg = encoder_cc(&preset, 0, EncoderDirection::CounterClockwise, &mut val).unwrap();
271 assert_eq!(msg.data, [0xB0, 7, 0]);
272 assert_eq!(val, 0);
273 }
274
275 #[test]
276 fn encoder_cc_relative() {
277 let preset = make_encoder_preset();
278 let mut val = 0u8;
279 let msg = encoder_cc(&preset, 1, EncoderDirection::Clockwise, &mut val).unwrap();
280 assert_eq!(msg.data, [0xB1, 16, 65]);
281 let msg = encoder_cc(&preset, 1, EncoderDirection::CounterClockwise, &mut val).unwrap();
282 assert_eq!(msg.data, [0xB1, 16, 63]);
283 }
284
285 #[test]
286 fn encoder_invalid_index_returns_none() {
287 let preset = make_encoder_preset();
288 let mut val = 0u8;
289 assert!(encoder_cc(&preset, 5, EncoderDirection::Clockwise, &mut val).is_none());
290 }
291
292 #[test]
293 fn analog_cc_mid_range() {
294 let mut analog: Vec<AnalogConfig, MAX_ANALOG> = Vec::new();
295 analog
296 .push(AnalogConfig {
297 label: Label::try_from("Exp").unwrap(),
298 cc: 11,
299 channel: 1,
300 min: 0,
301 max: 127,
302 })
303 .ok();
304 let preset: Preset = Preset {
305 name: Label::new(),
306 buttons: Vec::new(),
307 encoders: Vec::new(),
308 analog,
309 defaults: Default::default(),
310 on_enter: heapless::Vec::new(),
311 on_exit: heapless::Vec::new(),
312 triggers: heapless::Vec::new(),
313 };
314 let msg = analog_cc(&preset, 0, 2048, 0, 4095).unwrap();
315 assert_eq!(msg.data[0], 0xB0);
316 assert_eq!(msg.data[1], 11);
317 assert!(msg.data[2] >= 63 && msg.data[2] <= 64);
319 }
320
321 #[test]
322 fn analog_cc_with_range() {
323 let mut analog: Vec<AnalogConfig, MAX_ANALOG> = Vec::new();
324 analog
325 .push(AnalogConfig {
326 label: Label::new(),
327 cc: 4,
328 channel: 2,
329 min: 20,
330 max: 100,
331 })
332 .ok();
333 let preset: Preset = Preset {
334 name: Label::new(),
335 buttons: Vec::new(),
336 encoders: Vec::new(),
337 analog,
338 defaults: Default::default(),
339 on_enter: heapless::Vec::new(),
340 on_exit: heapless::Vec::new(),
341 triggers: heapless::Vec::new(),
342 };
343 let msg = analog_cc(&preset, 0, 4095, 0, 4095).unwrap();
345 assert_eq!(msg.data, [0xB1, 4, 100]);
346 let msg = analog_cc(&preset, 0, 0, 0, 4095).unwrap();
348 assert_eq!(msg.data, [0xB1, 4, 20]);
349 }
350
351 #[test]
352 fn analog_invalid_index_returns_none() {
353 let preset: Preset = Preset {
354 name: Label::new(),
355 buttons: Vec::new(),
356 encoders: Vec::new(),
357 analog: Vec::new(),
358 defaults: Default::default(),
359 on_enter: heapless::Vec::new(),
360 on_exit: heapless::Vec::new(),
361 triggers: heapless::Vec::new(),
362 };
363 assert!(analog_cc(&preset, 0, 2048, 0, 4095).is_none());
364 }
365
366 #[test]
367 fn analog_cc_with_calibration() {
368 let mut analog: Vec<AnalogConfig, MAX_ANALOG> = Vec::new();
369 analog
370 .push(AnalogConfig {
371 label: Label::new(),
372 cc: 7,
373 channel: 1,
374 min: 0,
375 max: 127,
376 })
377 .ok();
378 let preset: Preset = Preset {
379 name: Label::new(),
380 buttons: Vec::new(),
381 encoders: Vec::new(),
382 analog,
383 defaults: Default::default(),
384 on_enter: heapless::Vec::new(),
385 on_exit: heapless::Vec::new(),
386 triggers: heapless::Vec::new(),
387 };
388 let msg = analog_cc(&preset, 0, 200, 200, 3700).unwrap();
391 assert_eq!(msg.data[2], 0);
392 let msg = analog_cc(&preset, 0, 3700, 200, 3700).unwrap();
394 assert_eq!(msg.data[2], 127);
395 let msg = analog_cc(&preset, 0, 1950, 200, 3700).unwrap();
397 assert!(msg.data[2] >= 63 && msg.data[2] <= 64);
398 let msg = analog_cc(&preset, 0, 50, 200, 3700).unwrap();
400 assert_eq!(msg.data[2], 0);
401 let msg = analog_cc(&preset, 0, 4000, 200, 3700).unwrap();
403 assert_eq!(msg.data[2], 127);
404 }
405}