diff --git a/Cargo.lock b/Cargo.lock index 9bfe8eb..2147a08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,65 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "embedded-graphics" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" +dependencies = [ + "az", + "byteorder", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "graphics" +version = "0.1.0" +dependencies = [ + "embedded-graphics", + "embedded-graphics-core", + "memmap2", +] + [[package]] name = "itoa" version = "1.0.15" @@ -14,6 +73,21 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "minapk" version = "0.1.0" @@ -51,6 +125,15 @@ dependencies = [ "syn", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "oc2r-core" version = "0.1.0" diff --git a/crates/graphics/Cargo.toml b/crates/graphics/Cargo.toml new file mode 100644 index 0000000..cb4e0a3 --- /dev/null +++ b/crates/graphics/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "graphics" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dependencies] +memmap2 = "0.9" +embedded-graphics = "0.8" +embedded-graphics-core = "0.4" + diff --git a/crates/graphics/src/main.rs b/crates/graphics/src/main.rs new file mode 100644 index 0000000..46f3e64 --- /dev/null +++ b/crates/graphics/src/main.rs @@ -0,0 +1,119 @@ +#![allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_possible_wrap +)] +use std::{convert::Infallible, fs::OpenOptions, io, thread, time::Duration}; + +use embedded_graphics::{ + geometry::Size, + mono_font::{MonoTextStyle, ascii::FONT_6X10}, + pixelcolor::Rgb565, + prelude::*, + text::{Baseline, Text}, +}; +use memmap2::{MmapMut, MmapOptions}; + +const FB_PATH: &str = "/dev/fb0"; +const WIDTH: u32 = 640; +const HEIGHT: u32 = 480; +const BYTES_PER_PIXEL: usize = 2; + +struct FbDisplay { + fb: MmapMut, +} + +impl FbDisplay { + fn new() -> io::Result { + println!("Opening framebuffer device at {FB_PATH}"); + let file = OpenOptions::new().read(true).write(true).open(FB_PATH)?; + let fb_size = (WIDTH as usize) * (HEIGHT as usize) * BYTES_PER_PIXEL; + // Safety: map /dev/fb0 as writable memory + let fb = unsafe { MmapOptions::new().len(fb_size).map_mut(&file)? }; + println!( + "Mapped framebuffer: {}x{} at {} bytes ({} bytes per pixel)", + WIDTH, + HEIGHT, + fb.len(), + BYTES_PER_PIXEL + ); + Ok(Self { fb }) + } + + fn clear(&mut self) { + println!("Clearing framebuffer to black"); + for chunk in self.fb.chunks_exact_mut(BYTES_PER_PIXEL) { + chunk.copy_from_slice(&[0x00, 0x00]); // RGB565 black + } + } + + fn put_pixel(&mut self, x: u32, y: u32, color: Rgb565) { + if x >= WIDTH || y >= HEIGHT { + return; + } + + let idx = ((y * WIDTH + x) as usize) * BYTES_PER_PIXEL; + if idx + 1 >= self.fb.len() { + return; + } + + // Rgb565 stores 16-bit value; little endian in memory. + let raw: u16 = color.into_storage(); + let bytes = raw.to_le_bytes(); + self.fb[idx] = bytes[0]; + self.fb[idx + 1] = bytes[1]; + } +} + +impl DrawTarget for FbDisplay { + type Color = Rgb565; + type Error = Infallible; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for Pixel(coord, color) in pixels { + let x = coord.x; + let y = coord.y; + if x >= 0 && y >= 0 { + self.put_pixel(x as u32, y as u32, color); + } + } + Ok(()) + } +} + +impl OriginDimensions for FbDisplay { + fn size(&self) -> Size { + Size::new(WIDTH, HEIGHT) + } +} + +pub fn main() -> Result<(), Box> { + let mut fb = FbDisplay::new()?; + fb.clear(); + + let style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); + + let text = "Hello embedded-graphics on RGB565"; + + let text_width = (text.len() as u32) * 6; + let text_height = 10; + + let x = (WIDTH - text_width) / 2; + let y = (HEIGHT / 2) + (text_height / 2); + + Text::with_baseline( + text, + Point::new(x as i32, y as i32), + style, + Baseline::Bottom, + ) + .draw(&mut fb)?; + + // Keep the framebuffer content visible + loop { + thread::sleep(Duration::from_secs(1)); + } +}