A step sequencer for Adafruit's RP2040-based macropad

refactoring, working step menu

+282 -148
-1
Cargo.lock
··· 1013 1013 "itoa", 1014 1014 "log", 1015 1015 "midi-convert", 1016 - "num_enum", 1017 1016 "panic-probe", 1018 1017 "pio", 1019 1018 "pio-proc",
-1
Cargo.toml
··· 64 64 midi-convert = "0.2.0" 65 65 usbd-midi = "0.5.0" 66 66 itoa = "1.0.17" 67 - num_enum = { version = "0.7.4", default-features = false } 68 67 69 68 [profile.release] 70 69 debug = true
+66 -39
src/menus/mod.rs
··· 4 4 5 5 pub use render::*; 6 6 pub use sequencer::{SEQUENCER_MENU, SequencerMenuItems, SequencerMenuValue}; 7 + pub use step::{StepMenuItems, StepMenuValue}; 7 8 8 9 use crate::display::MonoDisplay; 9 10 10 11 const WIDTH: u32 = 128; 11 - const HEIGHT: u32 = 64; 12 + const _HEIGHT: u32 = 64; 12 13 13 - pub struct Menu<'a, const SIZE: usize> { 14 + pub struct Menu<'a, T, const SIZE: usize> 15 + where 16 + T: Copy, 17 + { 14 18 title: &'a str, 15 19 index: usize, 16 - items: [&'a mut dyn MenuItem; SIZE], 20 + items: [&'a mut dyn MenuItem<T>; SIZE], 17 21 selecting: bool, 22 + pub value: T, 23 + callback: &'a dyn Fn(&T), 18 24 } 19 25 20 - impl<'a, const SIZE: usize> Menu<'a, SIZE> { 21 - pub fn new(title: &'a str, items: [&'a mut dyn MenuItem; SIZE]) -> Self { 26 + impl<'a, T, const SIZE: usize> Menu<'a, T, SIZE> 27 + where 28 + T: Copy, 29 + { 30 + pub fn new( 31 + title: &'a str, 32 + value: T, 33 + items: [&'a mut dyn MenuItem<T>; SIZE], 34 + callback: &'a dyn Fn(&T), 35 + ) -> Self { 22 36 let index = 0; 23 37 let selecting = false; 24 38 ··· 27 41 index, 28 42 items, 29 43 selecting, 44 + value, 45 + callback, 30 46 } 31 47 } 32 48 33 - pub fn on_change(&mut self, step: i32) { 49 + pub async fn on_change(&mut self, step: i32) { 34 50 if self.selecting { 35 51 let next = (self.index as i32 + step).rem_euclid(SIZE as i32); 36 52 self.index = next as usize; 37 53 } else { 38 - self.items[self.index].on_change(step); 54 + self.items[self.index].on_change(&mut self.value, step); 55 + (*self.callback)(&self.value); 39 56 } 40 57 } 41 58 ··· 72 89 } 73 90 } 74 91 75 - pub trait MenuItem { 92 + pub trait MenuItem<T> { 76 93 fn as_str(&mut self) -> (&str, &str); 77 - fn on_change(&mut self, step: i32); 94 + fn on_change(&mut self, value: &mut T, step: i32); 78 95 } 79 96 80 - pub struct NumericMenuItem<'a> { 97 + pub struct NumericMenuItem<'a, T> { 81 98 title: &'a str, 82 - value: u32, 99 + pub value: u32, 83 100 buffer: itoa::Buffer, 84 - callback: &'a dyn Fn(u32), 101 + callback: &'a dyn Fn(&mut T, u32), 85 102 } 86 103 87 - impl<'a> NumericMenuItem<'a> { 88 - pub fn new(title: &'a str, value: u32, on_change: &'a dyn Fn(u32)) -> Self { 104 + impl<'a, T> NumericMenuItem<'a, T> { 105 + pub fn new(title: &'a str, value: u32, on_change: &'a dyn Fn(&mut T, u32)) -> Self { 89 106 let buffer = itoa::Buffer::new(); 90 107 91 108 Self { ··· 97 114 } 98 115 } 99 116 100 - impl<'a> MenuItem for NumericMenuItem<'a> { 117 + impl<'a, T> MenuItem<T> for NumericMenuItem<'a, T> { 101 118 fn as_str(&mut self) -> (&str, &str) { 102 119 (self.title, self.buffer.format(self.value)) 103 120 } 104 121 105 - fn on_change(&mut self, step: i32) { 122 + fn on_change(&mut self, value: &mut T, step: i32) { 106 123 let mut intermediate = self.value as i32; 107 124 intermediate += step; 108 125 if intermediate < 0 { ··· 110 127 } 111 128 112 129 self.value = intermediate as u32; 113 - (*self.callback)(self.value); 130 + (*self.callback)(value, self.value); 114 131 } 115 132 } 116 133 117 - pub struct BooleanMenuItem<'a> { 134 + pub struct BooleanMenuItem<'a, T> { 118 135 title: &'a str, 119 - value: bool, 136 + pub value: bool, 120 137 on_str: &'a str, 121 138 off_str: &'a str, 122 - callback: &'a dyn Fn(bool), 139 + callback: &'a dyn Fn(&mut T, bool), 123 140 } 124 141 125 - impl<'a> BooleanMenuItem<'a> { 142 + impl<'a, T> BooleanMenuItem<'a, T> { 126 143 pub fn new( 127 144 title: &'a str, 128 145 on_str: &'a str, 129 146 off_str: &'a str, 130 147 value: bool, 131 - on_change: &'a dyn Fn(bool), 148 + on_change: &'a dyn Fn(&mut T, bool), 132 149 ) -> Self { 133 150 Self { 134 151 title, ··· 140 157 } 141 158 } 142 159 143 - impl MenuItem for BooleanMenuItem<'static> { 160 + impl<'a, T> MenuItem<T> for BooleanMenuItem<'a, T> { 144 161 fn as_str(&mut self) -> (&str, &str) { 145 162 let value = if self.value { 146 163 self.on_str ··· 150 167 (self.title, value) 151 168 } 152 169 153 - fn on_change(&mut self, _step: i32) { 170 + fn on_change(&mut self, value: &mut T, _step: i32) { 154 171 self.value = !self.value; 155 - (*self.callback)(self.value); 172 + (*self.callback)(value, self.value); 156 173 } 157 174 } 158 175 ··· 160 177 fn as_str(&self) -> &str; 161 178 } 162 179 163 - pub struct EnumMenuItem<'a, const SIZE: usize, T> 180 + pub struct EnumMenuItem<'a, T, const SIZE: usize, E> 164 181 where 165 - T: Stringable, 182 + E: Stringable, 166 183 { 167 184 title: &'a str, 168 - options: [T; SIZE], 185 + options: [E; SIZE], 169 186 index: usize, 170 - value: T, 171 - callback: &'a dyn Fn(T), 187 + pub value: E, 188 + callback: &'a dyn Fn(&mut T, E), 172 189 } 173 190 174 - impl<'a, const SIZE: usize, T> EnumMenuItem<'a, SIZE, T> 191 + impl<'a, T, const SIZE: usize, E> EnumMenuItem<'a, T, SIZE, E> 175 192 where 176 - T: Stringable, 193 + E: Stringable + PartialEq, 177 194 { 178 - pub fn new(title: &'a str, options: [T; SIZE], value: T, on_change: &'a dyn Fn(T)) -> Self { 179 - // TODO: set index to currently selected! 195 + pub fn new( 196 + title: &'a str, 197 + options: [E; SIZE], 198 + value: E, 199 + on_change: &'a dyn Fn(&mut T, E), 200 + ) -> Self { 201 + let index = options.iter().position(|i| *i == value).unwrap_or(0); 180 202 181 203 Self { 182 204 title, 183 205 options, 184 - index: 0, 206 + index, 185 207 value, 186 208 callback: on_change, 187 209 } 188 210 } 211 + 212 + pub fn set(&mut self, value: E) { 213 + self.index = self.options.iter().position(|i| *i == value).unwrap_or(0); 214 + self.value = value; 215 + } 189 216 } 190 217 191 - impl<'a, const SIZE: usize, T> MenuItem for EnumMenuItem<'a, SIZE, T> 218 + impl<'a, T, const SIZE: usize, E> MenuItem<T> for EnumMenuItem<'a, T, SIZE, E> 192 219 where 193 - T: Stringable + Copy + Into<u8>, 220 + E: Stringable + Copy, 194 221 { 195 222 fn as_str(&mut self) -> (&str, &str) { 196 223 (self.title, self.options[self.index].as_str()) 197 224 } 198 225 199 - fn on_change(&mut self, step: i32) { 226 + fn on_change(&mut self, value: &mut T, step: i32) { 200 227 let next = (self.index as i32 + step).rem_euclid(SIZE as i32); 201 228 self.index = next as usize; 202 229 self.value = self.options[self.index]; 203 - (*self.callback)(self.value); 230 + (*self.callback)(value, self.value); 204 231 } 205 232 }
+30 -43
src/menus/sequencer.rs
··· 1 - use core::cell::RefCell; 2 - 3 1 use embassy_sync::blocking_mutex::{Mutex, raw::ThreadModeRawMutex}; 4 2 5 3 use crate::{ ··· 43 41 pub static SEQUENCER_MENU: SequencerMenuMutex = Mutex::new(None); 44 42 45 43 pub struct SequencerMenuItems<'a> { 46 - pub play_menu: BooleanMenuItem<'a>, 47 - pub bpm_menu: NumericMenuItem<'a>, 48 - pub timing_menu: EnumMenuItem<'a, 6, TimingOption>, 49 - pub swing_menu: NumericMenuItem<'a>, 44 + pub play_menu: BooleanMenuItem<'a, SequencerMenuValue>, 45 + pub bpm_menu: NumericMenuItem<'a, SequencerMenuValue>, 46 + pub timing_menu: EnumMenuItem<'a, SequencerMenuValue, 6, TimingOption>, 47 + pub swing_menu: NumericMenuItem<'a, SequencerMenuValue>, 50 48 } 51 49 52 50 impl<'a> SequencerMenuItems<'a> { 53 51 pub fn new() -> Self { 54 52 let defaults = SequencerMenuValue::default(); 55 - let play_menu = 56 - BooleanMenuItem::new("STATUS", "PLAYING", "PAUSED", defaults.play, &|value| { 57 - unsafe { 58 - SEQUENCER_MENU.lock_mut(|inner| { 59 - if let Some(menu_value) = inner { 60 - menu_value.play = value; 61 - } 62 - }) 63 - }; 64 - }); 53 + let play_menu = BooleanMenuItem::<SequencerMenuValue>::new( 54 + "STATUS", 55 + "PLAYING", 56 + "PAUSED", 57 + defaults.play, 58 + &|menu_value, value| { 59 + menu_value.play = value; 60 + }, 61 + ); 65 62 66 - let bpm_menu = NumericMenuItem::new("BPM", defaults.bpm, &|value| { 67 - unsafe { 68 - SEQUENCER_MENU.lock_mut(|inner| { 69 - if let Some(menu_value) = inner { 70 - menu_value.bpm = value; 71 - } 72 - }) 73 - }; 74 - }); 63 + let bpm_menu = NumericMenuItem::<SequencerMenuValue>::new( 64 + "BPM", 65 + defaults.bpm, 66 + &|menu_value, value| { 67 + menu_value.bpm = value; 68 + }, 69 + ); 75 70 76 - let timing_menu = EnumMenuItem::new( 71 + let timing_menu = EnumMenuItem::<'_, SequencerMenuValue, 6, TimingOption>::new( 77 72 "TIMING", 78 73 [ 79 74 TimingOption::Quarter, ··· 84 79 TimingOption::SixteenthTriplet, 85 80 ], 86 81 defaults.timing, 87 - &|value| { 88 - unsafe { 89 - SEQUENCER_MENU.lock_mut(|inner| { 90 - if let Some(menu_value) = inner { 91 - menu_value.timing = value; 92 - } 93 - }) 94 - }; 82 + &|menu_value, value| { 83 + menu_value.timing = value; 95 84 }, 96 85 ); 97 - let swing_menu = NumericMenuItem::new("SWING", defaults.swing, &|value| { 98 - unsafe { 99 - SEQUENCER_MENU.lock_mut(|inner| { 100 - if let Some(menu_value) = inner { 101 - menu_value.swing = value; 102 - } 103 - }) 104 - }; 105 - }); 86 + let swing_menu = NumericMenuItem::<SequencerMenuValue>::new( 87 + "SWING", 88 + defaults.swing, 89 + &|menu_value, value| { 90 + menu_value.swing = value; 91 + }, 92 + ); 106 93 107 94 unsafe { 108 95 SEQUENCER_MENU.lock_mut(|value| {
+75 -35
src/menus/step.rs
··· 1 - use crate::{ 2 - menus::{BooleanMenuItem, EnumMenuItem, NumericMenuItem, Stringable}, 3 - sequencer_timer::TimingOption, 4 - }; 1 + use crate::menus::{EnumMenuItem, NumericMenuItem, Stringable}; 5 2 6 - enum Note { 3 + #[derive(Clone, Copy, PartialEq)] 4 + pub enum Note { 7 5 A, 8 6 BFlat, 9 7 B, ··· 37 35 } 38 36 } 39 37 40 - //pub struct StepMenuItems<'a> { 41 - // pub note_menu: EnumMenuItem<'a, 12>, 42 - // pub octave_menu: NumericMenuItem<'a>, 43 - // pub velocity_menu: NumericMenuItem<'a>, 44 - //} 45 - // 46 - //impl<'a> StepMenuItems<'a> { 47 - // pub fn new() -> Self { 48 - // 49 - // let note_menu = EnumMenuItem::new("NOTE", &[ 50 - // Note::BFlat 51 - // Note::B, 52 - // Note::C, 53 - // Note::CSharp, 54 - // Note::D, 55 - // Note::EFlat, 56 - // Note::E, 57 - // Note::F, 58 - // Note::FSharp, 59 - // Note::G, 60 - // Note::AFlat, 61 - // ]); 62 - // 63 - // let octave_menu = NumericMenuItem::new() 64 - // 65 - // Self { 66 - // note_menu, 67 - // } 68 - // } 69 - //} 38 + #[derive(Clone, Copy)] 39 + pub struct StepMenuValue { 40 + pub note: Note, 41 + pub octave: u32, 42 + pub velocity: u32, 43 + } 44 + 45 + impl Default for StepMenuValue { 46 + fn default() -> Self { 47 + Self { 48 + note: Note::C, 49 + octave: 1, 50 + velocity: 100, 51 + } 52 + } 53 + } 54 + 55 + pub struct StepMenuItems<'a> { 56 + pub note_menu: EnumMenuItem<'a, StepMenuValue, 12, Note>, 57 + pub octave_menu: NumericMenuItem<'a, StepMenuValue>, 58 + pub velocity_menu: NumericMenuItem<'a, StepMenuValue>, 59 + } 60 + 61 + impl<'a> StepMenuItems<'a> { 62 + pub fn new() -> Self { 63 + let defaults = StepMenuValue::default(); 64 + 65 + let note_menu = EnumMenuItem::<'_, StepMenuValue, 12, Note>::new( 66 + "NOTE", 67 + [ 68 + Note::A, 69 + Note::BFlat, 70 + Note::B, 71 + Note::C, 72 + Note::CSharp, 73 + Note::D, 74 + Note::EFlat, 75 + Note::E, 76 + Note::F, 77 + Note::FSharp, 78 + Note::G, 79 + Note::AFlat, 80 + ], 81 + defaults.note, 82 + &|menu_value, value| { 83 + menu_value.note = value; 84 + }, 85 + ); 86 + 87 + let octave_menu = NumericMenuItem::<StepMenuValue>::new( 88 + "OCTAVE", 89 + defaults.octave, 90 + &|menu_value, value| { 91 + menu_value.octave = value; 92 + }, 93 + ); 94 + 95 + let velocity_menu = NumericMenuItem::<StepMenuValue>::new( 96 + "VELOCITY", 97 + defaults.velocity, 98 + &|menu_value, value| { 99 + menu_value.velocity = value; 100 + }, 101 + ); 102 + 103 + Self { 104 + note_menu, 105 + octave_menu, 106 + velocity_menu, 107 + } 108 + } 109 + }
+1 -3
src/sequencer_timer.rs
··· 1 1 use embassy_time::Timer; 2 - use num_enum::{FromPrimitive, IntoPrimitive}; 3 2 4 3 use crate::{COLS, ROWS}; 5 4 6 - #[derive(Clone, Copy, IntoPrimitive, FromPrimitive, Default)] 7 - #[repr(u8)] 5 + #[derive(Clone, Copy, Default, PartialEq)] 8 6 pub enum TimingOption { 9 7 #[default] 10 8 Quarter,
+48 -8
src/tasks/controls.rs
··· 1 - use core::sync::atomic::Ordering; 2 - 3 1 use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel}; 4 2 use smart_leds::RGB; 5 3 6 4 use crate::{ 7 5 COLS, KeyGrid, ROWS, 8 6 key_leds::Coord, 7 + menus::{SEQUENCER_MENU, SequencerMenuValue, StepMenuValue}, 9 8 tasks::{ 10 9 display::{DISPLAY_CHANNEL, DisplayUpdate}, 11 10 lights::{LIGHTS_CHANNEL, LedUpdate}, ··· 19 18 RotaryButton { pressed: bool }, 20 19 SequencerStep { coord: Coord }, 21 20 RotaryEncoder { increment: i32 }, 21 + SequencerMenuChange { value: SequencerMenuValue }, 22 + StepMenuChange { value: StepMenuValue }, 23 + } 24 + 25 + #[derive(Default, Clone, Copy)] 26 + struct StepState { 27 + active: bool, 28 + pressed: bool, 29 + value: StepMenuValue, 22 30 } 23 31 24 32 #[embassy_executor::task] 25 33 pub async fn read_controls() { 26 - let mut key_state: KeyGrid<bool> = [[false; COLS]; ROWS]; 34 + let mut step_state: KeyGrid<StepState> = [[StepState::default(); COLS]; ROWS]; 27 35 28 36 let active = RGB { r: 5, g: 5, b: 5 }; 29 37 let off = RGB { r: 0, g: 0, b: 0 }; 30 38 let current = RGB { r: 32, g: 0, b: 0 }; 31 39 40 + let mut num_keys_pressed = 0; 41 + let mut selected_step: Option<Coord> = None; 32 42 let mut step: Coord = (0, 0); 33 43 34 44 loop { 35 45 match CONTROLS_CHANNEL.receive().await { 36 46 ControlEvent::Key { pressed, coord } => { 47 + let state = &mut step_state[coord.1 as usize][coord.0 as usize]; 48 + 37 49 if !pressed { 50 + state.pressed = false; 51 + num_keys_pressed -= 1; 52 + if num_keys_pressed != 1 { 53 + set_step_menu(None).await; 54 + } 38 55 continue; 39 56 } 40 57 41 - let mut state = key_state[coord.1 as usize][coord.0 as usize]; 42 - state = !state; 43 - key_state[coord.1 as usize][coord.0 as usize] = state; 44 - let color = if state { active } else { off }; 58 + state.pressed = true; 59 + state.active = !state.active; 60 + let color = if state.active { active } else { off }; 45 61 update_key_light(coord, color).await; 62 + 63 + num_keys_pressed += 1; 64 + let value = if num_keys_pressed == 1 { 65 + selected_step = Some(coord); 66 + Some(state.value) 67 + } else { 68 + selected_step = None; 69 + None 70 + }; 71 + set_step_menu(value).await; 46 72 } 47 73 ControlEvent::RotaryButton { pressed } => { 48 74 if !pressed { ··· 53 79 } 54 80 ControlEvent::RotaryEncoder { increment } => rotary_change(increment).await, 55 81 ControlEvent::SequencerStep { coord } => { 56 - let prev_color = if key_state[step.1 as usize][step.0 as usize] { 82 + let prev_color = if step_state[step.1 as usize][step.0 as usize].active { 57 83 active 58 84 } else { 59 85 off ··· 63 89 update_key_light(coord, current).await; 64 90 step = coord; 65 91 } 92 + ControlEvent::SequencerMenuChange { value } => unsafe { 93 + SEQUENCER_MENU.lock_mut(|inner| *inner = Some(value)); 94 + }, 95 + ControlEvent::StepMenuChange { value } => { 96 + if let Some(coord) = selected_step { 97 + step_state[coord.1 as usize][coord.0 as usize].value = value; 98 + } 99 + } 66 100 } 67 101 } 102 + } 103 + 104 + async fn set_step_menu(value: Option<StepMenuValue>) { 105 + DISPLAY_CHANNEL 106 + .send(DisplayUpdate::StepMenu { value }) 107 + .await; 68 108 } 69 109 70 110 async fn rotary_press() {
+61 -17
src/tasks/display.rs
··· 1 - use crate::menus::{Menu, SequencerMenuItems}; 1 + use crate::{ 2 + menus::{Menu, SequencerMenuItems, SequencerMenuValue, StepMenuItems, StepMenuValue}, 3 + tasks::{CONTROLS_CHANNEL, ControlEvent}, 4 + }; 2 5 use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel}; 3 6 4 7 use crate::display::Display; ··· 8 11 pub enum DisplayUpdate { 9 12 RotaryMove { increment: i32 }, 10 13 RotaryPress, 11 - } 12 - 13 - struct Menus<'a> { 14 - pub sequencer: Menu<'a, 4>, 15 - } 16 - 17 - impl<'a> Menus<'a> { 18 - pub fn new(sequencer: Menu<'a, 4>) -> Self { 19 - Self { sequencer } 20 - } 14 + StepMenu { value: Option<StepMenuValue> }, 21 15 } 22 16 23 17 #[embassy_executor::task] ··· 25 19 display.init(); 26 20 27 21 let mut sequencer_items = SequencerMenuItems::new(); 28 - let sequencer = Menu::new( 22 + let mut sequencer_menu = Menu::new( 29 23 "Sequencer", 24 + SequencerMenuValue::default(), 30 25 [ 31 26 &mut sequencer_items.play_menu, 32 27 &mut sequencer_items.bpm_menu, 33 28 &mut sequencer_items.timing_menu, 34 29 &mut sequencer_items.swing_menu, 35 30 ], 31 + &|value| { 32 + let _ = CONTROLS_CHANNEL.try_send(ControlEvent::SequencerMenuChange { value: *value }); 33 + }, 36 34 ); 37 35 38 - let mut menus = Menus::new(sequencer); 39 - menus.sequencer.render(&mut display.display); 36 + let mut step_items = StepMenuItems::new(); 37 + let mut step_menu = Menu::new( 38 + "Step", 39 + StepMenuValue::default(), 40 + [ 41 + &mut step_items.note_menu, 42 + &mut step_items.octave_menu, 43 + &mut step_items.velocity_menu, 44 + ], 45 + &|_| {}, 46 + ); 47 + 48 + sequencer_menu.render(&mut display.display); 40 49 display.flush(); 50 + let mut show_step = false; 41 51 42 52 loop { 43 53 match DISPLAY_CHANNEL.receive().await { 44 54 DisplayUpdate::RotaryMove { increment } => { 45 - menus.sequencer.on_change(increment); 55 + if show_step { 56 + step_menu.on_change(increment).await; 57 + } else { 58 + sequencer_menu.on_change(increment).await; 59 + } 46 60 } 47 61 DisplayUpdate::RotaryPress => { 48 - menus.sequencer.on_select(); 62 + if show_step { 63 + step_menu.on_select(); 64 + } else { 65 + sequencer_menu.on_select(); 66 + } 67 + } 68 + DisplayUpdate::StepMenu { value } => { 69 + show_step = value.is_some(); 70 + if let Some(value) = value { 71 + step_items = StepMenuItems::new(); 72 + step_items.note_menu.set(value.note); 73 + step_items.octave_menu.value = value.octave; 74 + step_items.velocity_menu.value = value.velocity; 75 + step_menu = Menu::new( 76 + "Step", 77 + StepMenuValue::default(), 78 + [ 79 + &mut step_items.note_menu, 80 + &mut step_items.octave_menu, 81 + &mut step_items.velocity_menu, 82 + ], 83 + &|value| { 84 + let _ = CONTROLS_CHANNEL 85 + .try_send(ControlEvent::StepMenuChange { value: *value }); 86 + }, 87 + ); 88 + } 49 89 } 50 90 } 51 91 52 92 display.clear(); 53 - menus.sequencer.render(&mut display.display); 93 + if show_step { 94 + step_menu.render(&mut display.display); 95 + } else { 96 + sequencer_menu.render(&mut display.display); 97 + } 54 98 display.flush(); 55 99 } 56 100 }
+1 -1
src/tasks/mod.rs
··· 6 6 mod sequencer; 7 7 8 8 pub use buttons::{read_button, read_key}; 9 - pub use controls::{CONTROLS_CHANNEL, read_controls}; 9 + pub use controls::{CONTROLS_CHANNEL, ControlEvent, read_controls}; 10 10 pub use display::drive_display; 11 11 pub use lights::update_lights; 12 12 pub use rotary::read_rotary_encoder;