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