diff --git a/rust/pm-kit/Cargo.toml b/rust/pm-kit/Cargo.toml index 475a68a..4955673 100644 --- a/rust/pm-kit/Cargo.toml +++ b/rust/pm-kit/Cargo.toml @@ -9,6 +9,7 @@ rp235x-hal = { version = "0.3", features = ["binary-info", "critical-section-imp cortex-m-rt = "0.7" panic-halt = "1" embedded-hal = "1" +embedded-hal-0-2 = { package = "embedded-hal", version = "0.2.7" } # ADC OneShot trait (rp235x-hal ADC) embedded-hal-bus = "0.2" mipidsi = "0.9" embedded-graphics = "0.8" diff --git a/rust/pm-kit/src/main.rs b/rust/pm-kit/src/main.rs index b3a1851..31dccb0 100644 --- a/rust/pm-kit/src/main.rs +++ b/rust/pm-kit/src/main.rs @@ -8,7 +8,9 @@ #![no_main] use embedded_hal::delay::DelayNs; -use embedded_hal::digital::OutputPin; +use embedded_hal::digital::{InputPin, OutputPin}; +use embedded_hal::pwm::SetDutyCycle; +use embedded_hal_0_2::adc::OneShot; use embedded_hal_bus::spi::ExclusiveDevice; use mipidsi::interface::{Interface, SpiInterface}; use mipidsi::models::ST7796; @@ -133,31 +135,36 @@ fn main() -> ! { .init(&mut timer) .unwrap(); - // The real metronome screen the host simulator renders (rust/uisim → PNG). Static sample data - // for now (no allocator needed — draw_metronome takes borrowed slices); the live track + a - // moving playhead come once pm-core is linked in. - let kick = [2u8, 1, 1, 1]; - let snare = [0u8, 1, 0, 1]; - let hat = [2u8, 1, 1, 1, 1, 1, 1, 1]; - let ride = [2u8, 0, 1, 0, 1, 0, 1, 0]; - let cow = [2u8, 1, 1]; - let lanes = [ - pm_ui::LaneView { name: "kick", levels: &kick, beats: 4, poly: false, muted: false }, - pm_ui::LaneView { name: "snare", levels: &snare, beats: 4, poly: false, muted: false }, - pm_ui::LaneView { name: "hatClosed", levels: &hat, beats: 4, poly: false, muted: false }, - pm_ui::LaneView { name: "ride", levels: &ride, beats: 4, poly: false, muted: false }, - pm_ui::LaneView { name: "cowbell", levels: &cow, beats: 3, poly: true, muted: false }, - ]; - let screen = pm_ui::Screen { name: "Four-on-the-floor", bpm: 128, playing: true, phase: 0.30, lanes: &lanes }; - pm_ui::draw_metronome(&mut display, &screen).unwrap(); + // ---- Peripheral self-test: buttons (GP15/GP14), joystick (GP26/GP27 ADC), speaker (GP13 PWM). + // Verifies the inputs + speaker actually work on the device; reads live and draws state. + let mut btn_a = pins.gpio15.into_pull_up_input(); + let mut btn_b = pins.gpio14.into_pull_up_input(); - // Reached the loop → display init + draw completed. Slow 1 Hz blink (vs the solid-ON during - // init above) so "hung in init" / "running" / "reset loop" are distinguishable on the LED. + let mut adc = hal::adc::Adc::new(pac.ADC, &mut pac.RESETS); + let mut joy_x = hal::adc::AdcPin::new(pins.gpio26).unwrap(); + let mut joy_y = hal::adc::AdcPin::new(pins.gpio27).unwrap(); + + let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS); + let mut spk = pwm_slices.pwm6; // GP13 = PWM slice 6, channel B + spk.set_div_int(125); + spk.set_top(600); // ~2 kHz click tone + spk.enable(); + spk.channel_b.output_to(pins.gpio13); + + let mut hb = false; loop { - led.set_high().unwrap(); - timer.delay_ms(500); - led.set_low().unwrap(); - timer.delay_ms(500); + let a = btn_a.is_low().unwrap(); + let b = btn_b.is_low().unwrap(); + let x: u16 = adc.read(&mut joy_x).unwrap(); + let y: u16 = adc.read(&mut joy_y).unwrap(); + let beep = a || b; + let _ = spk.channel_b.set_duty_cycle(if beep { 300 } else { 0 }); + + pm_ui::draw_peripheral_test(&mut display, &pm_ui::PeriphState { a, b, x, y, beep }).ok(); + + hb = !hb; // LED heartbeat + let _ = if hb { led.set_high() } else { led.set_low() }; + timer.delay_ms(40); } } diff --git a/rust/pm-ui/src/lib.rs b/rust/pm-ui/src/lib.rs index 611737a..5b3f345 100644 --- a/rust/pm-ui/src/lib.rs +++ b/rust/pm-ui/src/lib.rs @@ -182,6 +182,68 @@ where Ok(()) } +/// Live state for the peripheral self-test. +pub struct PeriphState { + pub a: bool, + pub b: bool, + pub x: u16, // joystick X, 0..4095 + pub y: u16, // joystick Y, 0..4095 + pub beep: bool, +} + +/// Peripheral self-test screen: button states, joystick position + values, speaker activity. +pub fn draw_peripheral_test(d: &mut D, s: &PeriphState) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let bb = d.bounding_box(); + let w = bb.size.width as i32; + d.clear(BG)?; + + Text::new("PERIPHERAL TEST", Point::new(12, 22), MonoTextStyle::new(&FONT_10X20, TXT)).draw(d)?; + + // buttons + let lab = MonoTextStyle::new(&FONT_10X20, Rgb565::BLACK); + for (i, (name, on)) in [("A", s.a), ("B", s.b)].iter().enumerate() { + let bx = 16 + i as i32 * 96; + Rectangle::new(Point::new(bx, 48), Size::new(84, 84)) + .into_styled(PrimitiveStyle::with_fill(if *on { GREEN } else { PANEL })) + .draw(d)?; + Text::with_alignment(name, Point::new(bx + 42, 98), lab, Alignment::Center).draw(d)?; + } + // speaker / beep + let beep_sty = MonoTextStyle::new(&FONT_9X18_BOLD, if s.beep { AMBER } else { MUTE }); + Text::new("SPK", Point::new(216, 78), beep_sty).draw(d)?; + if s.beep { + Text::new("BEEP", Point::new(216, 100), MonoTextStyle::new(&FONT_6X10, AMBER)).draw(d)?; + } + + // joystick box + dot + let jb_x = 40; + let jb_y = 170; + let jb = 200; + Rectangle::new(Point::new(jb_x, jb_y), Size::new(jb as u32, jb as u32)) + .into_styled(PrimitiveStyleBuilder::new().stroke_color(GRID).stroke_width(2).fill_color(PANEL).build()) + .draw(d)?; + Line::new(Point::new(jb_x + jb / 2, jb_y), Point::new(jb_x + jb / 2, jb_y + jb)).into_styled(PrimitiveStyle::with_stroke(GRID, 1)).draw(d)?; + Line::new(Point::new(jb_x, jb_y + jb / 2), Point::new(jb_x + jb, jb_y + jb / 2)).into_styled(PrimitiveStyle::with_stroke(GRID, 1)).draw(d)?; + let dx = jb_x + (s.x as i32 * (jb - 12) / 4095) + 6; + let dy = jb_y + (s.y as i32 * (jb - 12) / 4095) + 6; + Rectangle::new(Point::new(dx - 6, dy - 6), Size::new(12, 12)).into_styled(PrimitiveStyle::with_fill(CYAN)).draw(d)?; + + // numeric values + let vs = MonoTextStyle::new(&FONT_6X10, TXT); + let mut nb = [0u8; 12]; + Text::new("X", Point::new(jb_x, jb_y + jb + 18), vs).draw(d)?; + Text::new(fmt_u32(s.x as u32, &mut nb), Point::new(jb_x + 16, jb_y + jb + 18), vs).draw(d)?; + let mut nb2 = [0u8; 12]; + Text::new("Y", Point::new(jb_x + 90, jb_y + jb + 18), vs).draw(d)?; + Text::new(fmt_u32(s.y as u32, &mut nb2), Point::new(jb_x + 106, jb_y + jb + 18), vs).draw(d)?; + + Text::with_alignment("press A/B = click, move joystick", Point::new(w / 2, jb_y + jb + 44), vs, Alignment::Center).draw(d)?; + Ok(()) +} + /// Bring-up diagnostic pattern (kept for hardware bring-up / fallback). pub fn draw_ui(d: &mut D) -> Result<(), D::Error> where diff --git a/rust/uisim/src/bin/periphsim.rs b/rust/uisim/src/bin/periphsim.rs new file mode 100644 index 0000000..de3b864 --- /dev/null +++ b/rust/uisim/src/bin/periphsim.rs @@ -0,0 +1,41 @@ +//! Render the peripheral-test screen (pm-ui) with mock input state → PNG, to check the layout +//! before flashing. `cargo run --bin periphsim`. + +use embedded_graphics::{pixelcolor::Rgb565, prelude::*}; +use pm_ui::PeriphState; + +const W: u32 = 320; +const H: u32 = 480; + +struct Fb { + px: Vec, +} +impl DrawTarget for Fb { + type Color = Rgb565; + type Error = core::convert::Infallible; + fn draw_iter>>(&mut self, pixels: I) -> Result<(), Self::Error> { + for Pixel(p, c) in pixels { + if p.x >= 0 && p.y >= 0 && (p.x as u32) < W && (p.y as u32) < H { + self.px[(p.y as u32 * W + p.x as u32) as usize] = c; + } + } + Ok(()) + } +} +impl OriginDimensions for Fb { + fn size(&self) -> Size { + Size::new(W, H) + } +} + +fn main() { + let mut fb = Fb { px: vec![Rgb565::BLACK; (W * H) as usize] }; + let st = PeriphState { a: true, b: false, x: 2900, y: 1100, beep: true }; + pm_ui::draw_peripheral_test(&mut fb, &st).unwrap(); + let img = image::RgbImage::from_fn(W, H, |x, y| { + let c = fb.px[(y * W + x) as usize]; + image::Rgb([(c.r() << 3) | (c.r() >> 2), (c.g() << 2) | (c.g() >> 4), (c.b() << 3) | (c.b() >> 2)]) + }); + img.save("periph.png").unwrap(); + println!("wrote periph.png"); +}