diff --git a/images/Keyboard-Keyboard-Spritesheet.raw b/images/Keyboard-Keyboard-Spritesheet.raw new file mode 100644 index 0000000..431def0 Binary files /dev/null and b/images/Keyboard-Keyboard-Spritesheet.raw differ diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index ceda34e..1ec6a9e 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -17,6 +17,8 @@ pub const FILTER_SHIFT: u32 = 2; pub const VELOCITY_WINDOW_MS: u32 = 80; pub const CALIBRATION_SAMPLES: usize = 64; +pub const SPLASH_DURATION_MS: u32 = 3000; + pub const DIAG_LOGGING: bool = false; // set true to see raw ADC / calibration logs pub const LOG_INTERVAL_MS: u32 = 500; pub const LOG_SWITCH: usize = 0; // HE1 — first switch @@ -53,22 +55,23 @@ pub const VIBRATO_INTERVAL_MS: u32 = 10; pub const NUM_POTS: usize = 12; pub const POT_SCAN_MS: u32 = 10; pub const POT_CC_HYSTERESIS: u8 = 2; -// (decoder_idx, mux_channel, CC_number) -// decoder_idx 4 = AM14 (Y4), 5 = AM15 (Y5) — both read via Daisy28 / A11 +pub const POT_ADC_MAX: u32 = 3776; // physical ceiling — pots don't reach full ADC range + // (decoder_idx, mux_channel, CC_number) + // decoder_idx 4 = AM14 (Y4), 5 = AM15 (Y5) — both read via Daisy28 / A11 #[rustfmt::skip] pub const POT_MAP: [(u8, u8, u8); NUM_POTS] = [ - (4, 4, 20), // RV1 AM14 X4 → CC20 - (4, 6, 21), // RV2 AM14 X6 → CC21 - (4, 7, 22), // RV3 AM14 X7 → CC22 - (4, 5, 23), // RV4 AM14 X5 → CC23 - (4, 2, 24), // RV5 AM14 X2 → CC24 - (4, 1, 25), // RV6 AM14 X1 → CC25 - (4, 0, 26), // RV7 AM14 X0 → CC26 - (4, 3, 27), // RV8 AM14 X3 → CC27 - (5, 4, 28), // RV9 AM15 X4 → CC28 - (5, 6, 29), // RV10 AM15 X6 → CC29 - (5, 7, 30), // RV11 AM15 X7 → CC30 - (5, 5, 31), // RV12 AM15 X5 → CC31 + (4, 4, 7), // RV1 AM14 X4 → CC7 (volume) + (4, 6, 10), // RV2 AM14 X6 → CC10 (pan) + (4, 7, 11), // RV3 AM14 X7 → CC11 (expression) + (4, 5, 74), // RV4 AM14 X5 → CC74 (brightness) + (4, 2, 36), // RV5 AM14 X2 → CC36 + (4, 1, 37), // RV6 AM14 X1 → CC37 + (4, 0, 38), // RV7 AM14 X0 → CC38 + (4, 3, 39), // RV8 AM14 X3 → CC39 + (5, 4, 40), // RV9 AM15 X4 → CC40 + (5, 6, 41), // RV10 AM15 X6 → CC41 + (5, 7, 42), // RV11 AM15 X7 → CC42 + (5, 5, 43), // RV12 AM15 X5 → CC43 ]; // ── Switch map: (mux_index, channel) per switch index ──────────────────────── diff --git a/keyboard_keyboard/code/src/display.rs b/keyboard_keyboard/code/src/display.rs deleted file mode 100644 index 8b88147..0000000 --- a/keyboard_keyboard/code/src/display.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::types::LcdDisplay; -use embedded_graphics::{ - mono_font::{ascii::FONT_10X20, MonoTextStyleBuilder}, - pixelcolor::BinaryColor, - prelude::*, - text::{Baseline, Text}, -}; - -/// Draw the boot splash screen ("key*key" centred on 128×32). -pub fn draw_startup(disp: &mut LcdDisplay) { - disp.clear(); - let style = MonoTextStyleBuilder::new() - .font(&FONT_10X20) - .text_color(BinaryColor::On) - .build(); - Text::with_baseline("key*key", Point::new(29, 6), style, Baseline::Top) - .draw(disp) - .ok(); - disp.flush().ok(); -} diff --git a/keyboard_keyboard/code/src/display/mod.rs b/keyboard_keyboard/code/src/display/mod.rs new file mode 100644 index 0000000..d22f171 --- /dev/null +++ b/keyboard_keyboard/code/src/display/mod.rs @@ -0,0 +1,10 @@ +pub mod screens; +pub use screens::{draw_main, draw_splash}; + +const NOTE_NAMES: [&str; 12] = [ + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", +]; + +pub fn note_name(midi: u8) -> (&'static str, i8) { + (NOTE_NAMES[(midi % 12) as usize], (midi / 12) as i8 - 1) +} diff --git a/keyboard_keyboard/code/src/display/screens.rs b/keyboard_keyboard/code/src/display/screens.rs new file mode 100644 index 0000000..b4e32cb --- /dev/null +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -0,0 +1,103 @@ +use crate::display::note_name; +use crate::types::{DisplayState, LastEvent, LcdDisplay}; +use core::fmt::Write; +use embedded_graphics::{ + image::{Image, ImageRawBE}, + mono_font::{ + ascii::{FONT_10X20, FONT_6X9}, + MonoTextStyle, + }, + pixelcolor::BinaryColor, + prelude::*, + primitives::Rectangle, + text::{Alignment, Text}, +}; +use heapless::String; + +const SPRITE_ATLAS: &[u8] = include_bytes!("../../../../images/Keyboard-Keyboard-Spritesheet.raw"); + +pub fn draw_splash(disp: &mut LcdDisplay) { + disp.clear(); + let atlas = ImageRawBE::::new(SPRITE_ATLAS, 128); + let splash = atlas.sub_image(&Rectangle::new(Point::new(0, 0), Size::new(128, 32))); + Image::new(&splash, Point::new(0, 0)).draw(disp).ok(); + disp.flush().ok(); +} + +pub fn draw_main(disp: &mut LcdDisplay, state: &DisplayState) { + disp.clear(); + + let atlas = ImageRawBE::::new(SPRITE_ATLAS, 128); + + // Background from atlas rows 32–63 + let bg = atlas.sub_image(&Rectangle::new(Point::new(0, 32), Size::new(128, 32))); + Image::new(&bg, Point::new(0, 0)).draw(disp).ok(); + + // All text is Off (dark on the light background) + let center = Point::new(64, 16); + let off = BinaryColor::Off; + + // Small label above center: note name+octave ("C4") or CC number ("CC 12") + let mut label: String<16> = String::new(); + // Large value in center: MIDI note number ("60") or CC value ("127") + let mut number: String<8> = String::new(); + + match state.last_event { + Some(LastEvent::Note { note }) => { + let (name, oct) = note_name(note); + write!(label, "{}{}", name, oct).ok(); + write!(number, "{}", note).ok(); + } + Some(LastEvent::Cc { num, value }) => { + write!(label, "CC {}", num).ok(); + write!(number, "{}", value).ok(); + } + Some(LastEvent::Clear) | None => {} + } + + if !label.is_empty() { + Text::with_alignment( + label.as_str(), + center + Point::new(0, -8), + MonoTextStyle::new(&FONT_6X9, off), + Alignment::Center, + ) + .draw(disp) + .ok(); + } + + if !number.is_empty() { + Text::with_alignment( + number.as_str(), + center + Point::new(0, 12), + MonoTextStyle::new(&FONT_10X20, off), + Alignment::Center, + ) + .draw(disp) + .ok(); + } + + // Keys channel — top-left + let mut keys_ch: String<4> = String::new(); + write!(keys_ch, "{}", state.melody_channel + 1).ok(); + Text::with_alignment( + keys_ch.as_str(), + center + Point::new(-42, -9), + MonoTextStyle::new(&FONT_6X9, off), + Alignment::Center, + ) + .draw(disp) + .ok(); + + // Drums channel — top-right, always 10 + Text::with_alignment( + "10", + center + Point::new(54, -9), + MonoTextStyle::new(&FONT_6X9, off), + Alignment::Center, + ) + .draw(disp) + .ok(); + + disp.flush().ok(); +} diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 3fdba60..5a30cfa 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -25,7 +25,7 @@ mod app { }; use crate::midi::MidiSender; use crate::switch::{ChannelFilter, SwitchEvent, SwitchState}; - use crate::types::LcdDisplay; + use crate::types::{DisplayState, LastEvent, LcdDisplay}; use libdaisy::gpio::*; use libdaisy::logger; @@ -60,6 +60,8 @@ mod app { baselines: [u16; NUM_SWITCHES], event_queue: heapless::spsc::Queue<(usize, SwitchEvent), 64>, midi_tx_flag: bool, + display_state: DisplayState, + splash_done: bool, } // ── Local resources ─────────────────────────────────────────────────────── @@ -310,7 +312,7 @@ mod app { ) .into_buffered_graphics_mode(), ); - display_init::spawn().ok(); + display_update::spawn().ok(); set_mux_channel(0, &mut s0, &mut s1, &mut s2); info!( @@ -325,6 +327,8 @@ mod app { baselines, event_queue: heapless::spsc::Queue::new(), midi_tx_flag: false, + display_state: DisplayState::new(), + splash_done: false, }, Local { audio: system.audio, @@ -367,18 +371,25 @@ mod app { } // Priority 1 — below process_events so a slow display never blocks key events. - #[task(local = [display], priority = 1)] - fn display_init(ctx: display_init::Context) { + // First spawn: init hardware + show splash. Subsequent spawns: redraw main screen. + #[task(local = [display, initialized: bool = false], shared = [display_state], priority = 1, capacity = 2)] + fn display_update(mut ctx: display_update::Context) { let Some(disp) = ctx.local.display.as_mut() else { return; }; - match disp.init() { - Ok(()) => { - crate::display::draw_startup(disp); - info!("display ok"); + if !*ctx.local.initialized { + match disp.init() { + Ok(()) => { + *ctx.local.initialized = true; + crate::display::draw_splash(disp); + info!("display ok"); + } + Err(_) => warn!("display not found"), } - Err(_) => warn!("display not found"), + return; } + let state = ctx.shared.display_state.lock(|s| *s); + crate::display::draw_main(disp, &state); } #[task(binds = DMA1_STR1, priority = 8, local = [audio])] @@ -392,7 +403,7 @@ mod app { local = [timer2, adc, adc_pins, enb_a, enb_b, enb_c, adc_pin_a9, adc_pin_a10, adc_pin_a11, pot_last_cc, s0, s1, s2, mux_raw, filters, last_pitch_bend, last_vibrato_cc, led1, led2, led3, midi_rx, led1_off_at, led2_off_at], - shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag], + shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag, splash_done], priority = 15 )] fn timer_handler(mut ctx: timer_handler::Context) { @@ -407,6 +418,11 @@ mod app { info!("tick={}", now); } + if now == SPLASH_DURATION_MS { + ctx.shared.splash_done.lock(|d| *d = true); + display_update::spawn().ok(); + } + let baselines = ctx.shared.baselines.lock(|b| *b); let mut pending: heapless::Vec<(usize, SwitchEvent), 32> = heapless::Vec::new(); @@ -537,7 +553,8 @@ mod app { set_mux_channel(mux_ch as usize, ctx.local.s0, ctx.local.s1, ctx.local.s2); set_decoder(dec_idx, ctx.local.enb_a, ctx.local.enb_b, ctx.local.enb_c); cortex_m::asm::delay(480 * 5); - let cc_val = (ctx.local.adc.read(ctx.local.adc_pin_a11).unwrap_or(0u32) >> 5) as u8; + let raw = ctx.local.adc.read(ctx.local.adc_pin_a11).unwrap_or(0u32); + let cc_val = (POT_ADC_MAX.saturating_sub(raw) * 127 / POT_ADC_MAX).min(127) as u8; if cc_val.abs_diff(ctx.local.pot_last_cc[pot_idx]) >= POT_CC_HYSTERESIS { ctx.local.pot_last_cc[pot_idx] = cc_val; pending @@ -589,13 +606,16 @@ mod app { // ── MIDI output ─────────────────────────────────────────────────────────── #[task( - shared = [event_queue, midi_tx_flag], + shared = [event_queue, midi_tx_flag, display_state, splash_done], local = [midi_sender, melody_channel], priority = 2, capacity = 32 )] fn process_events(mut ctx: process_events::Context) { let mut did_send = false; + let mut new_display_event: Option = None; + let mut melody_changed = false; + ctx.shared.event_queue.lock(|queue| { while let Some((switch_idx, event)) = queue.dequeue() { if switch_idx == SETTINGS_CHAN1 || switch_idx == SETTINGS_CHAN2 { @@ -603,6 +623,7 @@ mod app { *ctx.local.melody_channel = if switch_idx == SETTINGS_CHAN1 { 0 } else { 1 }; info!("melody ch → {}", *ctx.local.melody_channel + 1); + melody_changed = true; } continue; } @@ -630,14 +651,17 @@ mod app { velocity ); ctx.local.midi_sender.note_on(note, velocity); + new_display_event = Some(LastEvent::Note { note }); } SwitchEvent::NoteOff => { info!("NoteOff HE{} ch={} note={}", he, channel + 1, note); ctx.local.midi_sender.note_off(note, 0); + new_display_event = Some(LastEvent::Clear); } SwitchEvent::PotChange { cc, value } => { info!("CC{} = {}", cc, value); ctx.local.midi_sender.control_change(cc, value); + new_display_event = Some(LastEvent::Cc { num: cc, value }); } SwitchEvent::PitchBend { value } => { info!("PitchBend value={}", value); @@ -647,6 +671,22 @@ mod app { did_send = true; } }); + + let done = ctx.shared.splash_done.lock(|d| *d); + if (new_display_event.is_some() || melody_changed) && done { + ctx.shared.display_state.lock(|s| { + match new_display_event { + Some(LastEvent::Clear) => s.last_event = None, + Some(ev) => s.last_event = Some(ev), + None => {} + } + if melody_changed { + s.melody_channel = *ctx.local.melody_channel; + } + }); + display_update::spawn().ok(); + } + if did_send { ctx.shared.midi_tx_flag.lock(|f| *f = true); } diff --git a/keyboard_keyboard/code/src/types.rs b/keyboard_keyboard/code/src/types.rs index 5600f7e..25784cd 100644 --- a/keyboard_keyboard/code/src/types.rs +++ b/keyboard_keyboard/code/src/types.rs @@ -10,3 +10,25 @@ pub type LcdDisplay = Ssd1306< DisplaySize128x32, BufferedGraphicsMode, >; + +#[derive(Clone, Copy)] +pub enum LastEvent { + Note { note: u8 }, + Cc { num: u8, value: u8 }, + Clear, +} + +#[derive(Clone, Copy)] +pub struct DisplayState { + pub last_event: Option, + pub melody_channel: u8, +} + +impl DisplayState { + pub const fn new() -> Self { + Self { + last_event: None, + melody_channel: 0, + } + } +}