Skip to main content

midi_controller/
state.rs

1//! Per-preset runtime state: tracks toggle/cycle/encoder state per preset
2//! and generates recall MIDI on preset switch.
3
4use crate::action::{action_to_midi, MidiMessage};
5use crate::config::{EncoderAction, Preset, MAX_BUTTONS, MAX_ENCODERS, MAX_PRESETS};
6
7/// Runtime state for a single preset (not persisted across power cycles).
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct PresetState<const B: usize = MAX_BUTTONS, const E: usize = MAX_ENCODERS> {
10    pub button_active: [bool; B],
11    pub cycle_index: [u8; B],
12    pub encoder_values: [u8; E],
13}
14
15impl<const B: usize, const E: usize> Default for PresetState<B, E> {
16    fn default() -> Self {
17        Self {
18            button_active: [false; B],
19            cycle_index: [0; B],
20            encoder_values: [0; E],
21        }
22    }
23}
24
25impl<const B: usize, const E: usize> PresetState<B, E> {
26    /// Create a PresetState from a preset's declared defaults.
27    pub fn from_defaults<const A: usize>(preset: &Preset<B, E, A>) -> Self {
28        let mut state = Self::default();
29        for (i, &active) in preset.defaults.button_active.iter().enumerate() {
30            if i < B {
31                state.button_active[i] = active;
32            }
33        }
34        for (i, &val) in preset.defaults.encoder_values.iter().enumerate() {
35            if i < E {
36                state.encoder_values[i] = val;
37            }
38        }
39        state
40    }
41}
42
43/// Manages per-preset state and generates recall MIDI on switch.
44#[derive(Clone)]
45pub struct PresetStateStore<const B: usize = MAX_BUTTONS, const E: usize = MAX_ENCODERS> {
46    states: [PresetState<B, E>; MAX_PRESETS],
47    active: u8,
48}
49
50impl<const B: usize, const E: usize> Default for PresetStateStore<B, E> {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl<const B: usize, const E: usize> PresetStateStore<B, E> {
57    pub fn new() -> Self {
58        Self {
59            states: core::array::from_fn(|_| PresetState::default()),
60            active: 0,
61        }
62    }
63
64    /// Get a reference to the current active preset's state.
65    pub fn current(&self) -> &PresetState<B, E> {
66        &self.states[self.active as usize]
67    }
68
69    /// Get a mutable reference to the current active preset's state.
70    pub fn current_mut(&mut self) -> &mut PresetState<B, E> {
71        &mut self.states[self.active as usize]
72    }
73
74    /// Current active preset index.
75    pub fn active_index(&self) -> u8 {
76        self.active
77    }
78
79    /// Save working state into the active preset slot (for serialization without switching).
80    pub fn save_working(&mut self, working: &PresetState<B, E>) {
81        self.states[self.active as usize] = working.clone();
82    }
83
84    /// Reset all state (presets changed, state is stale).
85    pub fn clear(&mut self) {
86        *self = Self::new();
87    }
88
89    /// Set state for a specific preset slot.
90    pub fn set_state(&mut self, index: usize, state: PresetState<B, E>) {
91        if index < MAX_PRESETS {
92            self.states[index] = state;
93        }
94    }
95
96    /// Reset state using preset defaults (after upload / first boot).
97    /// Each preset's declared initial state is applied.
98    pub fn apply_defaults<const A: usize>(&mut self, presets: &[&Preset<B, E, A>]) {
99        for (i, preset) in presets.iter().enumerate() {
100            if i < MAX_PRESETS {
101                self.states[i] = PresetState::from_defaults(preset);
102            }
103        }
104    }
105
106    /// Switch to a new preset. Saves current working state, loads new state,
107    /// and returns MIDI messages to recall the new preset's state to external gear.
108    pub fn switch<const A: usize>(
109        &mut self,
110        new_preset: u8,
111        working: &mut PresetState<B, E>,
112        preset: &Preset<B, E, A>,
113    ) -> heapless::Vec<MidiMessage, 16> {
114        let mut recall = heapless::Vec::new();
115
116        // Save current working state
117        self.states[self.active as usize] = working.clone();
118
119        // Load new state into working
120        self.active = new_preset;
121        *working = self.states[new_preset as usize].clone();
122
123        // Recall: send MIDI state to external gear
124        for (i, btn) in preset.buttons.iter().enumerate() {
125            if working.button_active[i] {
126                for action in &btn.on_press {
127                    if let Some(msg) = action_to_midi(action) {
128                        recall.push(msg).ok();
129                    }
130                }
131            } else if !btn.on_release.is_empty() {
132                for action in &btn.on_release {
133                    if let Some(msg) = action_to_midi(action) {
134                        recall.push(msg).ok();
135                    }
136                }
137            }
138        }
139
140        // Recall encoder values
141        for (i, enc) in preset.encoders.iter().enumerate() {
142            if let EncoderAction::Cc { cc, channel, .. } = &enc.action {
143                recall
144                    .push(MidiMessage {
145                        data: [0xB0 | (channel - 1), *cc as u8, working.encoder_values[i]],
146                        len: 3,
147                    })
148                    .ok();
149            }
150        }
151
152        recall
153    }
154}
155
156// --- EEPROM persistence (AT24CS01: 128 bytes) ---
157
158/// EEPROM magic encodes format version in the low nibble: 0xE0 | version.
159/// Bump EEPROM_VERSION when PresetState layout changes (field count, order, size).
160const EEPROM_VERSION: u8 = 2;
161const EEPROM_MAGIC: u8 = 0xE0 | EEPROM_VERSION;
162const EEPROM_HEADER_SIZE: usize = 2; // magic + active_preset
163
164/// Compute the serialized size of a single PresetState<B, E>.
165/// Layout: B bools + B cycle_indices + E encoder_values = B*2 + E bytes.
166const fn preset_state_size(b: usize, e: usize) -> usize {
167    b * 2 + e
168}
169
170/// Maximum presets that fit in 128 bytes for the default configuration.
171pub const EEPROM_MAX_PRESETS: usize =
172    (128 - EEPROM_HEADER_SIZE) / preset_state_size(MAX_BUTTONS, MAX_ENCODERS); // = 9
173
174impl<const B: usize, const E: usize> PresetState<B, E> {
175    /// Serialized byte size for this configuration.
176    pub const SIZE: usize = preset_state_size(B, E);
177
178    /// Serialize to a fixed-size buffer.
179    pub fn to_bytes(&self, buf: &mut [u8]) {
180        debug_assert!(buf.len() >= Self::SIZE);
181        for (i, &active) in self.button_active.iter().enumerate() {
182            buf[i] = active as u8;
183        }
184        for (i, &idx) in self.cycle_index.iter().enumerate() {
185            buf[B + i] = idx;
186        }
187        for (i, &val) in self.encoder_values.iter().enumerate() {
188            buf[B * 2 + i] = val;
189        }
190    }
191
192    /// Deserialize from a byte buffer.
193    pub fn from_bytes(buf: &[u8]) -> Self {
194        debug_assert!(buf.len() >= Self::SIZE);
195        let mut state = Self::default();
196        for (i, &b) in buf[..B].iter().enumerate() {
197            state.button_active[i] = b != 0;
198        }
199        state.cycle_index[..B].copy_from_slice(&buf[B..B * 2]);
200        for (i, &b) in buf[B * 2..B * 2 + E].iter().enumerate() {
201            state.encoder_values[i] = b;
202        }
203        state
204    }
205}
206
207impl<const B: usize, const E: usize> PresetStateStore<B, E> {
208    /// Maximum presets that fit in 128 bytes for this configuration.
209    pub const EEPROM_MAX_PRESETS: usize = (128 - EEPROM_HEADER_SIZE) / preset_state_size(B, E);
210
211    /// Return a cleared EEPROM buffer (for writing after preset upload).
212    pub fn cleared_eeprom() -> [u8; 128] {
213        let mut buf = [0u8; 128];
214        Self::new().to_eeprom(&mut buf);
215        buf
216    }
217
218    /// Serialize entire store to EEPROM buffer (128 bytes).
219    /// Layout: [magic][active_preset][state0..stateN]
220    pub fn to_eeprom(&self, buf: &mut [u8; 128]) {
221        buf.fill(0xFF);
222        buf[0] = EEPROM_MAGIC;
223        buf[1] = self.active;
224        let state_size = PresetState::<B, E>::SIZE;
225        let max_presets = Self::EEPROM_MAX_PRESETS;
226        for i in 0..max_presets {
227            let offset = EEPROM_HEADER_SIZE + i * state_size;
228            if offset + state_size > 128 {
229                break;
230            }
231            self.states[i].to_bytes(&mut buf[offset..offset + state_size]);
232        }
233    }
234
235    /// Deserialize from EEPROM buffer. Returns None if magic/version doesn't match.
236    pub fn from_eeprom(buf: &[u8; 128]) -> Option<Self> {
237        if buf[0] != EEPROM_MAGIC {
238            return None;
239        }
240        let mut store = Self::new();
241        store.active = buf[1];
242        let state_size = PresetState::<B, E>::SIZE;
243        let max_presets = Self::EEPROM_MAX_PRESETS;
244        for i in 0..max_presets {
245            let offset = EEPROM_HEADER_SIZE + i * state_size;
246            if offset + state_size > 128 {
247                break;
248            }
249            store.states[i] = PresetState::from_bytes(&buf[offset..offset + state_size]);
250        }
251        Some(store)
252    }
253}
254
255/// Type alias for the default preset state (6 buttons, 2 encoders).
256pub type DefaultPresetState = PresetState<MAX_BUTTONS, MAX_ENCODERS>;
257/// Type alias for the default preset state store.
258pub type DefaultPresetStateStore = PresetStateStore<MAX_BUTTONS, MAX_ENCODERS>;
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::config::*;
264    use heapless::Vec;
265
266    fn make_preset() -> Preset {
267        let mut buttons: Vec<ButtonConfig, MAX_BUTTONS> = Vec::new();
268        let mut on_press: Vec<Action, MAX_ACTIONS> = Vec::new();
269        on_press.push(Action::cc(10, 127, 1).unwrap()).ok();
270        let mut on_release: Vec<Action, MAX_ACTIONS> = Vec::new();
271        on_release.push(Action::cc(10, 0, 1).unwrap()).ok();
272        buttons
273            .push(ButtonConfig {
274                label: Label::new(),
275                color: LedConfig::default(),
276                mode: ButtonMode::Toggle,
277                on_press,
278                on_release,
279                on_long_press: Vec::new(),
280                cycle_values: Vec::new(),
281                listen_cc: None,
282            })
283            .ok();
284
285        let mut encoders: Vec<EncoderConfig, MAX_ENCODERS> = Vec::new();
286        encoders
287            .push(EncoderConfig {
288                label: Label::try_from("Vol").unwrap(),
289                action: EncoderAction::Cc {
290                    cc: 7,
291                    channel: 1,
292                    min: 0,
293                    max: 127,
294                },
295                ..Default::default()
296            })
297            .ok();
298
299        Preset {
300            name: Label::try_from("Test").unwrap(),
301            buttons,
302            encoders,
303            analog: Vec::new(),
304            defaults: Default::default(),
305            on_enter: heapless::Vec::new(),
306            on_exit: heapless::Vec::new(),
307            triggers: heapless::Vec::new(),
308        }
309    }
310
311    #[test]
312    fn switch_saves_and_restores_state() {
313        let preset = make_preset();
314        let mut store: PresetStateStore = PresetStateStore::new();
315        let mut working = PresetState::default();
316
317        // Activate toggle in preset 0
318        working.button_active[0] = true;
319        working.encoder_values[0] = 80;
320
321        // Switch to preset 1
322        store.switch(1, &mut working, &preset);
323        assert!(!working.button_active[0]);
324        assert_eq!(working.encoder_values[0], 0);
325
326        // Switch back to preset 0
327        store.switch(0, &mut working, &preset);
328        assert!(working.button_active[0]);
329        assert_eq!(working.encoder_values[0], 80);
330    }
331
332    #[test]
333    fn recall_sends_active_button_on_press() {
334        let preset = make_preset();
335        let mut store: PresetStateStore = PresetStateStore::new();
336        let mut working = PresetState::default();
337
338        // Set button active in preset 0, switch away, switch back
339        working.button_active[0] = true;
340        store.switch(1, &mut working, &preset);
341        let recall = store.switch(0, &mut working, &preset);
342
343        // Should contain CC 10 = 127 (on_press of active button)
344        assert!(recall.iter().any(|m| m.data == [0xB0, 10, 127]));
345    }
346
347    #[test]
348    fn recall_sends_inactive_button_on_release() {
349        let preset = make_preset();
350        let mut store: PresetStateStore = PresetStateStore::new();
351        let mut working = PresetState::default();
352
353        // Button inactive in preset 0 (default), switch away, switch back
354        store.switch(1, &mut working, &preset);
355        let recall = store.switch(0, &mut working, &preset);
356
357        // Should contain CC 10 = 0 (on_release of inactive button)
358        assert!(recall.iter().any(|m| m.data == [0xB0, 10, 0]));
359    }
360
361    #[test]
362    fn recall_sends_encoder_cc() {
363        let preset = make_preset();
364        let mut store: PresetStateStore = PresetStateStore::new();
365        let mut working = PresetState::default();
366
367        working.encoder_values[0] = 64;
368        store.switch(1, &mut working, &preset);
369        let recall = store.switch(0, &mut working, &preset);
370
371        // Should contain CC 7 = 64
372        assert!(recall.iter().any(|m| m.data == [0xB0, 7, 64]));
373    }
374
375    #[test]
376    fn eeprom_roundtrip() {
377        let mut store: PresetStateStore = PresetStateStore::new();
378        store.states[0].button_active[0] = true;
379        store.states[0].button_active[3] = true;
380        store.states[0].cycle_index[2] = 5;
381        store.states[0].encoder_values[0] = 100;
382        store.states[0].encoder_values[1] = 42;
383        store.states[1].button_active[5] = true;
384        store.active = 1;
385
386        let mut buf = [0u8; 128];
387        store.to_eeprom(&mut buf);
388
389        let restored: PresetStateStore = PresetStateStore::from_eeprom(&buf).unwrap();
390        assert_eq!(restored.active_index(), 1);
391        assert!(restored.states[0].button_active[0]);
392        assert!(restored.states[0].button_active[3]);
393        assert_eq!(restored.states[0].cycle_index[2], 5);
394        assert_eq!(restored.states[0].encoder_values[0], 100);
395        assert_eq!(restored.states[0].encoder_values[1], 42);
396        assert!(restored.states[1].button_active[5]);
397    }
398
399    #[test]
400    fn eeprom_invalid_magic_returns_none() {
401        let buf = [0xFFu8; 128];
402        assert!(PresetStateStore::<MAX_BUTTONS, MAX_ENCODERS>::from_eeprom(&buf).is_none());
403    }
404
405    #[test]
406    fn from_defaults_applies_initial_state() {
407        use crate::config::InitialState;
408
409        let mut preset = make_preset();
410        let mut btn_active = Vec::new();
411        btn_active.push(true).ok(); // button A = on
412        let mut enc_vals = Vec::new();
413        enc_vals.push(64).ok(); // encoder 0 = 64
414        preset.defaults = InitialState {
415            button_active: btn_active,
416            encoder_values: enc_vals,
417        };
418
419        let state = PresetState::from_defaults(&preset);
420        assert!(state.button_active[0]);
421        assert!(!state.button_active[1]);
422        assert_eq!(state.encoder_values[0], 64);
423        assert_eq!(state.encoder_values[1], 0);
424    }
425}