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            })
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        // 2048/4095 * 127 ≈ 63
316        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        // Full deflection
342        let msg = analog_cc(&preset, 0, 4095, 0, 4095).unwrap();
343        assert_eq!(msg.data, [0xB1, 4, 100]);
344        // Zero
345        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        // Calibrated range: 200–3700
387        // At heel (200): output = 0
388        let msg = analog_cc(&preset, 0, 200, 200, 3700).unwrap();
389        assert_eq!(msg.data[2], 0);
390        // At toe (3700): output = 127
391        let msg = analog_cc(&preset, 0, 3700, 200, 3700).unwrap();
392        assert_eq!(msg.data[2], 127);
393        // Midpoint: ~63-64
394        let msg = analog_cc(&preset, 0, 1950, 200, 3700).unwrap();
395        assert!(msg.data[2] >= 63 && msg.data[2] <= 64);
396        // Below min: clamped to 0
397        let msg = analog_cc(&preset, 0, 50, 200, 3700).unwrap();
398        assert_eq!(msg.data[2], 0);
399        // Above max: clamped to 127
400        let msg = analog_cc(&preset, 0, 4000, 200, 3700).unwrap();
401        assert_eq!(msg.data[2], 127);
402    }
403}