Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added images/Keyboard-Keyboard-Spritesheet.raw
Binary file not shown.
31 changes: 17 additions & 14 deletions keyboard_keyboard/code/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ────────────────────────
Expand Down
20 changes: 0 additions & 20 deletions keyboard_keyboard/code/src/display.rs

This file was deleted.

10 changes: 10 additions & 0 deletions keyboard_keyboard/code/src/display/mod.rs
Original file line number Diff line number Diff line change
@@ -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)
}
103 changes: 103 additions & 0 deletions keyboard_keyboard/code/src/display/screens.rs
Original file line number Diff line number Diff line change
@@ -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::<BinaryColor>::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::<BinaryColor>::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();
}
64 changes: 52 additions & 12 deletions keyboard_keyboard/code/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -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!(
Expand All @@ -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,
Expand Down Expand Up @@ -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])]
Expand All @@ -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) {
Expand All @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -589,20 +606,24 @@ 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<LastEvent> = 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 {
if let SwitchEvent::NoteOn { .. } = event {
*ctx.local.melody_channel =
if switch_idx == SETTINGS_CHAN1 { 0 } else { 1 };
info!("melody ch → {}", *ctx.local.melody_channel + 1);
melody_changed = true;
}
continue;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
22 changes: 22 additions & 0 deletions keyboard_keyboard/code/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,25 @@ pub type LcdDisplay = Ssd1306<
DisplaySize128x32,
BufferedGraphicsMode<DisplaySize128x32>,
>;

#[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<LastEvent>,
pub melody_channel: u8,
}

impl DisplayState {
pub const fn new() -> Self {
Self {
last_event: None,
melody_channel: 0,
}
}
}
Loading