diff --git a/rust/pm-kit/Cargo.toml b/rust/pm-kit/Cargo.toml index f147079..475a68a 100644 --- a/rust/pm-kit/Cargo.toml +++ b/rust/pm-kit/Cargo.toml @@ -12,6 +12,7 @@ embedded-hal = "1" embedded-hal-bus = "0.2" mipidsi = "0.9" embedded-graphics = "0.8" +pm-ui = { path = "../pm-ui" } [profile.release] opt-level = "s" diff --git a/rust/pm-kit/src/main.rs b/rust/pm-kit/src/main.rs index bda949f..249ba8a 100644 --- a/rust/pm-kit/src/main.rs +++ b/rust/pm-kit/src/main.rs @@ -7,13 +7,6 @@ #![no_std] #![no_main] -use embedded_graphics::{ - mono_font::{ascii::FONT_10X20, MonoTextStyle}, - pixelcolor::Rgb565, - prelude::*, - primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, - text::Text, -}; use embedded_hal::delay::DelayNs; use embedded_hal::digital::OutputPin; use embedded_hal_bus::spi::ExclusiveDevice; @@ -56,6 +49,7 @@ fn main() -> ! { let pins = hal::gpio::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS); let mut led = pins.gpio25.into_push_pull_output(); + led.set_high().unwrap(); // solid ON during init: if it stays solid → hung in init; slow blink → reached the loop // --- ST7796 over SPI0 --- let sclk = pins.gpio2.into_function::(); @@ -92,61 +86,22 @@ fn main() -> ! { let di = unsafe { display.dcs() }; di.send_command(0xF0, &[0xC3]).unwrap(); // unlock extension command set di.send_command(0xF0, &[0x96]).unwrap(); - di.send_command(0xB4, &[0x01]).unwrap(); // 1-dot inversion - di.send_command(0xB6, &[0x80, 0x02, 0x3B]).unwrap(); // display function control: 480 lines - di.send_command(0xE8, &[0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33]).unwrap(); - di.send_command(0xC1, &[0x06]).unwrap(); - di.send_command(0xC2, &[0xA7]).unwrap(); - di.send_command(0xC5, &[0x18]).unwrap(); - di.send_command(0xE0, &[0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B]).unwrap(); - di.send_command(0xE1, &[0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]).unwrap(); - di.send_command(0xF0, &[0x3C]).unwrap(); // lock - di.send_command(0xF0, &[0x69]).unwrap(); + di.send_command(0xB6, &[0x80, 0x02, 0x3B]).unwrap(); // display function control: 480 lines (the essential one) } - timer.delay_ms(120); + timer.delay_ms(20); - let w = WIDTH as i32; - let h = HEIGHT as i32; - let m = 36; // marker size - - // Full-screen fill via the SAME draw path as the shapes (clear() left snow last time). - Rectangle::new(Point::zero(), Size::new(WIDTH as u32, HEIGHT as u32)) - .into_styled(PrimitiveStyle::with_fill(Rgb565::new(0, 0, 12))) - .draw(&mut display) - .unwrap(); - // Red 8px border on all four edges — if any edge is missing, the addressed area != panel. - Rectangle::new(Point::zero(), Size::new(WIDTH as u32, HEIGHT as u32)) - .into_styled( - PrimitiveStyleBuilder::new() - .stroke_color(Rgb565::RED) - .stroke_width(8) - .build(), - ) - .draw(&mut display) - .unwrap(); - // Distinct corner markers so orientation/mirror is unambiguous. - let ms = Size::new(m as u32, m as u32); - for (x, y, c) in [ - (0, 0, Rgb565::GREEN), // top-left - (w - m, 0, Rgb565::YELLOW), // top-right - (0, h - m, Rgb565::CYAN), // bottom-left - (w - m, h - m, Rgb565::MAGENTA), // bottom-right - ] { - Rectangle::new(Point::new(x, y), ms) - .into_styled(PrimitiveStyle::with_fill(c)) - .draw(&mut display) - .unwrap(); - } - // Labels: "TL" near origin, big "PMK" centred. - let label = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); - Text::new("TL", Point::new(44, 28), label).draw(&mut display).unwrap(); - Text::new("PMK", Point::new(w / 2 - 30, h / 2), label).draw(&mut display).unwrap(); + // Same UI code the host simulator renders (rust/uisim → PNG). If this is wrong, the sim + // shows it without the bench; if the sim is right but the panel is wrong, it's a controller + // issue (init/MADCTL/0xB6), not a draw bug. + pm_ui::draw_ui(&mut display).unwrap(); + // 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. loop { led.set_high().unwrap(); - timer.delay_ms(250); + timer.delay_ms(500); led.set_low().unwrap(); - timer.delay_ms(250); + timer.delay_ms(500); } } diff --git a/rust/pm-ui/Cargo.toml b/rust/pm-ui/Cargo.toml new file mode 100644 index 0000000..0cb831c --- /dev/null +++ b/rust/pm-ui/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pm-ui" +version = "0.1.0" +edition = "2021" +description = "Shared PM_K-1 UI rendering (embedded-graphics, no_std). Used by the firmware and the host simulator." + +[dependencies] +embedded-graphics = "0.8" diff --git a/rust/pm-ui/src/lib.rs b/rust/pm-ui/src/lib.rs new file mode 100644 index 0000000..5b3e2cf --- /dev/null +++ b/rust/pm-ui/src/lib.rs @@ -0,0 +1,58 @@ +//! Shared PM_K-1 UI rendering — `no_std`, generic over any `embedded-graphics` `DrawTarget`. +//! +//! The firmware draws this onto the real ST7796; the host simulator (`rust/uisim`) draws the +//! SAME code onto a framebuffer and exports a PNG. So UI/layout can be developed and reviewed +//! without the device — only true panel/controller quirks need the bench. + +#![no_std] + +use embedded_graphics::{ + mono_font::{ascii::FONT_10X20, MonoTextStyle}, + pixelcolor::Rgb565, + prelude::*, + primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, + text::Text, +}; + +/// Current bring-up diagnostic: solid fill + 4-edge border + distinct corner markers + labels. +/// (This is where the real metronome UI will grow.) +pub fn draw_ui(d: &mut D) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let bb = d.bounding_box(); + let w = bb.size.width as i32; + let h = bb.size.height as i32; + let m: i32 = 36; + + // Full-screen background. + Rectangle::new(Point::zero(), bb.size) + .into_styled(PrimitiveStyle::with_fill(Rgb565::new(0, 0, 31))) + .draw(d)?; + // Red 8px border on all four edges. + Rectangle::new(Point::zero(), bb.size) + .into_styled( + PrimitiveStyleBuilder::new() + .stroke_color(Rgb565::RED) + .stroke_width(8) + .build(), + ) + .draw(d)?; + // Distinct corner markers: TL green / TR yellow / BL cyan / BR magenta. + let ms = Size::new(m as u32, m as u32); + for (x, y, c) in [ + (0, 0, Rgb565::GREEN), + (w - m, 0, Rgb565::YELLOW), + (0, h - m, Rgb565::CYAN), + (w - m, h - m, Rgb565::MAGENTA), + ] { + Rectangle::new(Point::new(x, y), ms) + .into_styled(PrimitiveStyle::with_fill(c)) + .draw(d)?; + } + // Labels. + let label = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); + Text::new("TL", Point::new(44, 28), label).draw(d)?; + Text::new("PMK", Point::new(w / 2 - 30, h / 2), label).draw(d)?; + Ok(()) +} diff --git a/rust/uisim/.gitignore b/rust/uisim/.gitignore new file mode 100644 index 0000000..7078560 --- /dev/null +++ b/rust/uisim/.gitignore @@ -0,0 +1,3 @@ +target/ +*.png +Cargo.lock diff --git a/rust/uisim/Cargo.toml b/rust/uisim/Cargo.toml new file mode 100644 index 0000000..c120927 --- /dev/null +++ b/rust/uisim/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "uisim" +version = "0.1.0" +edition = "2021" +description = "Host renderer for pm-ui: draws the firmware UI to a framebuffer and exports a PNG (no hardware)." + +[dependencies] +pm-ui = { path = "../pm-ui" } +embedded-graphics = "0.8" +image = { version = "0.25", default-features = false, features = ["png"] } diff --git a/rust/uisim/src/main.rs b/rust/uisim/src/main.rs new file mode 100644 index 0000000..ddafd6e --- /dev/null +++ b/rust/uisim/src/main.rs @@ -0,0 +1,58 @@ +//! Render the shared pm-ui onto a 320×480 framebuffer and save it as a PNG — no device, no SDL. +//! `cargo run` (host) → pm-kit-ui.png. Lets the UI be developed/reviewed off the bench. + +use embedded_graphics::{pixelcolor::Rgb565, prelude::*}; + +const W: u32 = 320; +const H: u32 = 480; + +/// A trivial framebuffer DrawTarget. +struct Fb { + px: Vec, +} + +impl Fb { + fn new() -> Self { + Fb { px: vec![Rgb565::BLACK; (W * H) as usize] } + } +} + +impl DrawTarget for Fb { + type Color = Rgb565; + type Error = core::convert::Infallible; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + 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::new(); + pm_ui::draw_ui(&mut fb).unwrap(); + + let img = image::RgbImage::from_fn(W, H, |x, y| { + let c = fb.px[(y * W + x) as usize]; + // Rgb565 channels → 8-bit + let r = (c.r() << 3) | (c.r() >> 2); + let g = (c.g() << 2) | (c.g() >> 4); + let b = (c.b() << 3) | (c.b() >> 2); + image::Rgb([r, g, b]) + }); + let out = std::env::args().nth(1).unwrap_or_else(|| "pm-kit-ui.png".into()); + img.save(&out).unwrap(); + println!("wrote {out} ({W}x{H})"); +}