Skip to main content

midi_controller/
action.rs

1//! Action executor: converts preset button actions into raw MIDI bytes.
2
3use crate::config::{Action, EncoderAction, Preset};
4
5/// A MIDI message ready to send (up to 3 bytes).
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct MidiMessage {
8    pub data: [u8; 3],
9    pub len: usize,
10}
11
12/// Execute on_press actions for a button index. Returns up to 8 MIDI messages.
13pub 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
29/// Convert a single Action to a raw MIDI message.
30pub 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/// Direction for encoder pulses.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum EncoderDirection {
46    Clockwise,
47    CounterClockwise,
48}
49
50/// Generate a CC message for an encoder pulse. `current_value` is updated in place.
51/// Returns None if the encoder has no action configured or uses PresetScroll.
52pub 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
95/// Generate a CC message for an analog input. `raw` is the ADC reading (0-4095).
96pub 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    // Clamp raw to calibrated range and normalize
106    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        // 2048/4095 * 127 ≈ 63
318        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        // Full deflection
344        let msg = analog_cc(&preset, 0, 4095, 0, 4095).unwrap();
345        assert_eq!(msg.data, [0xB1, 4, 100]);
346        // Zero
347        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        // Calibrated range: 200–3700
389        // At heel (200): output = 0
390        let msg = analog_cc(&preset, 0, 200, 200, 3700).unwrap();
391        assert_eq!(msg.data[2], 0);
392        // At toe (3700): output = 127
393        let msg = analog_cc(&preset, 0, 3700, 200, 3700).unwrap();
394        assert_eq!(msg.data[2], 127);
395        // Midpoint: ~63-64
396        let msg = analog_cc(&preset, 0, 1950, 200, 3700).unwrap();
397        assert!(msg.data[2] >= 63 && msg.data[2] <= 64);
398        // Below min: clamped to 0
399        let msg = analog_cc(&preset, 0, 50, 200, 3700).unwrap();
400        assert_eq!(msg.data[2], 0);
401        // Above max: clamped to 127
402        let msg = analog_cc(&preset, 0, 4000, 200, 3700).unwrap();
403        assert_eq!(msg.data[2], 127);
404    }
405}