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