diff --git a/daemon/src/display/generic_framebuffer.rs b/daemon/src/display/generic_framebuffer.rs index f75b59d..0f52708 100644 --- a/daemon/src/display/generic_framebuffer.rs +++ b/daemon/src/display/generic_framebuffer.rs @@ -1,7 +1,11 @@ use async_trait::async_trait; +use fb_utils::{determine_format, get_var_screeninfo}; use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType}; use std::io::Cursor; +use std::os::fd::{AsFd, BorrowedFd}; use std::time::Duration; +use tokio::fs::File; +use tokio::io::{AsyncSeekExt, AsyncWriteExt}; use crate::config; use crate::display::DisplayState; @@ -20,6 +24,20 @@ pub struct Dimensions { pub width: u32, } +#[derive(Copy, Clone, Debug)] +pub enum FbFormat { + ARGB888, + ABGR888, + RGB888, + BGR888, + RGB666, + RGB565, + BGR565, + RGB555, + BGR555, + RGB444, +} + #[allow(dead_code)] #[derive(Copy, Clone)] pub enum Color { @@ -131,6 +149,109 @@ pub trait GenericFramebuffer: Send + 'static { } } +/// Attempt to determine the FB dimensions from FB vinfo. +pub fn read_fb_dimentions(fb: BorrowedFd<'_>) -> std::io::Result { + let vinfo = get_var_screeninfo(fb)?; + Ok(Dimensions { + height: vinfo.yres, + width: vinfo.xres, + }) +} + +/// Attempt to determine the FBs format +/// +/// Returns `Ok(None)` if the format cannot be determined +pub fn read_fb_format(fb: BorrowedFd<'_>) -> std::io::Result> { + let vinfo = get_var_screeninfo(fb)?; + Ok(determine_format(vinfo)) +} + +pub fn buffer_to_fb_format( + buffer: &Vec<(u8, u8, u8)>, + format: &FbFormat, + big_endian: bool, +) -> Vec { + let mut raw_buffer = Vec::new(); + for (r, g, b) in buffer { + match format { + FbFormat::RGB565 => { + let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8; + rgb565 |= (*g as u16 & 0b11111100) << 3; + rgb565 |= (*b as u16) >> 3; + if big_endian { + raw_buffer.extend(rgb565.to_be_bytes()); + } else { + raw_buffer.extend(rgb565.to_le_bytes()); + } + } + other => panic!("This display uses a format we haven't implemneted yet {other:?}"), + } + } + raw_buffer +} + +pub type CallBack = Box; + +pub struct FramebufferDevice { + data: FbInner, + pre_write_fn: Option, + post_write_fn: Option, +} + +pub struct FbInner { + pub fd: File, + pub dims: Dimensions, + pub format: FbFormat, +} + +impl FramebufferDevice { + pub fn new( + path: &str, + pre_write_fn: Option, + post_write_fn: Option, + ) -> Self { + // This is done as a blocking call to prevent all of the UI init code from having to + // be made async, making it more verbose. This is a single syscall that would have been + // done via spawn_blocking anyway, and it's done once on startup. + let fb = std::fs::File::create(path).expect("Failed to open /dev/fb0"); + let dims = read_fb_dimentions(fb.as_fd()).expect("Failed to read FB dimensions"); + let format = read_fb_format(fb.as_fd()) + .expect("Failed to read FB format") + .expect("FB retruned unexpected format"); + Self { + data: FbInner { + fd: File::from_std(fb), + dims, + format, + }, + pre_write_fn, + post_write_fn, + } + } +} + +#[async_trait] +impl GenericFramebuffer for FramebufferDevice { + fn dimensions(&self) -> Dimensions { + self.data.dims + } + + async fn write_buffer( + &mut self, + buffer: Vec<(u8, u8, u8)>, // rgb, row-wise, left-to-right, top-to-bottom + ) { + if let Some(func) = self.pre_write_fn.as_mut() { + func(&mut self.data, &buffer); + } + let raw_buffer = buffer_to_fb_format(&buffer, &self.data.format, false); + self.data.fd.write_all(&raw_buffer).await.unwrap(); + self.data.fd.rewind().await.unwrap(); + if let Some(func) = self.post_write_fn.as_mut() { + func(&mut self.data, &buffer); + } + } +} + pub fn update_ui( task_tracker: &TaskTracker, config: &config::Config, @@ -201,3 +322,184 @@ pub fn update_ui( } }); } + +mod fb_utils { + use std::io::{Error, Result}; + use std::os::fd::{AsRawFd, BorrowedFd}; + + use libc::ioctl; + + use super::FbFormat; + + const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600; + // const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602; + + /// Bitfield which is a part of VarScreeninfo. + #[repr(C)] + #[derive(Clone, Debug, Default, PartialEq, Eq)] + pub struct Bitfield { + pub offset: u32, + pub length: u32, + pub msb_right: u32, + } + + /// Struct as defined in /usr/include/linux/fb.h + #[repr(C)] + #[derive(Clone, Debug, Default)] + pub struct VarScreeninfo { + pub xres: u32, + pub yres: u32, + pub xres_virtual: u32, + pub yres_virtual: u32, + pub xoffset: u32, + pub yoffset: u32, + pub bits_per_pixel: u32, + pub grayscale: u32, + pub red: Bitfield, + pub green: Bitfield, + pub blue: Bitfield, + pub transp: Bitfield, + pub nonstd: u32, + pub activate: u32, + pub height: u32, + pub width: u32, + pub accel_flags: u32, + pub pixclock: u32, + pub left_margin: u32, + pub right_margin: u32, + pub upper_margin: u32, + pub lower_margin: u32, + pub hsync_len: u32, + pub vsync_len: u32, + pub sync: u32, + pub vmode: u32, + pub rotate: u32, + pub colorspace: u32, + pub reserved: [u32; 4], + } + + // /// Struct as defined in /usr/include/linux/fb.h + // /// Note: type is a keyword in Rust and therefore has been changed to fb_type. + // #[repr(C)] + // #[derive(Clone, Debug, Default)] + // pub struct FixScreeninfo { + // pub id: [u8; 16], + // pub smem_start: usize, + // pub smem_len: u32, + // pub fb_type: u32, + // pub type_aux: u32, + // pub visual: u32, + // pub xpanstep: u16, + // pub ypanstep: u16, + // pub ywrapstep: u16, + // pub line_length: u32, + // pub mmio_start: usize, + // pub mmio_len: u32, + // pub accel: u32, + // pub capabilities: u16, + // pub reserved: [u16; 2], + // } + + // pub fn get_fix_screeninfo(fb: BorrowedFd<'_>) -> Result { + // let mut info: FixScreeninfo = Default::default(); + // let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_FSCREENINFO as _, &mut info) }; + // match result { + // -1 => Err(Error::last_os_error()), + // _ => Ok(info), + // } + // } + + pub fn get_var_screeninfo(fb: BorrowedFd<'_>) -> Result { + let mut info: VarScreeninfo = Default::default(); + let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_VSCREENINFO as _, &mut info) }; + match result { + -1 => Err(Error::last_os_error()), + _ => Ok(info), + } + } + + #[derive(Clone, Debug, Default, PartialEq, Eq)] + struct RgbaBitfield { + red: Bitfield, + green: Bitfield, + blue: Bitfield, + transp: Bitfield, + } + + impl From<&VarScreeninfo> for RgbaBitfield { + fn from(value: &VarScreeninfo) -> Self { + Self { + red: value.red.clone(), + green: value.green.clone(), + blue: value.blue.clone(), + transp: value.transp.clone(), + } + } + } + + type BitfieldShort = (u32, u32); + type FbInfoShort = (BitfieldShort, BitfieldShort, BitfieldShort, BitfieldShort); + + const fn tuple_to_bitfield(v: BitfieldShort) -> Bitfield { + let (offset, length) = v; + // None of formats we support have msb_right set. + Bitfield { + offset, + length, + msb_right: 0, + } + } + + /// Takes a tuple of 4 tuples `(r, g, b, a)`. Each color tuple is a tuple of `(offset, length)`. + const fn rgba_bitfield(v: FbInfoShort) -> RgbaBitfield { + let (r, g, b, a) = v; + RgbaBitfield { + red: tuple_to_bitfield(r), + green: tuple_to_bitfield(g), + blue: tuple_to_bitfield(b), + transp: tuple_to_bitfield(a), + } + } + + // Logic borrowed from QT https://github.com/qt/qtbase/blob/498ae026e98ed181d1480fe5f6f2f1453a725e78/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp + + const ARGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (24, 8))); + const ABGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (24, 8))); + const RGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (0, 0))); + const BGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (0, 0))); + const RGB666: RgbaBitfield = rgba_bitfield(((12, 6), (6, 6), (0, 6), (0, 0))); + const RGB565: RgbaBitfield = rgba_bitfield(((11, 5), (5, 6), (0, 5), (0, 0))); + const BGR565: RgbaBitfield = rgba_bitfield(((0, 5), (5, 6), (11, 5), (0, 0))); + const RGB555: RgbaBitfield = rgba_bitfield(((10, 5), (5, 5), (0, 5), (0, 0))); + const BGR555: RgbaBitfield = rgba_bitfield(((0, 5), (5, 5), (10, 5), (0, 0))); + const RGB444: RgbaBitfield = rgba_bitfield(((8, 4), (4, 4), (0, 4), (0, 0))); + + fn determine_depth(vinfo: &VarScreeninfo) -> u32 { + let depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length; + match vinfo.bits_per_pixel { + 24 if depth == 0 => 24, + 16 if depth == 0 => 16, + 24 | 16 => depth, + v => v, + } + } + + pub fn determine_format(vinfo: VarScreeninfo) -> Option { + let rgba = RgbaBitfield::from(&vinfo); + let depth = determine_depth(&vinfo); + + match (depth, rgba) { + (32, ARGB888) => Some(FbFormat::ARGB888), + (32, ABGR888) => Some(FbFormat::ABGR888), + (24, RGB888) => Some(FbFormat::RGB888), + (24, BGR888) => Some(FbFormat::BGR888), + (18, RGB666) => Some(FbFormat::RGB666), + (16, RGB565) => Some(FbFormat::RGB565), + (16, BGR565) => Some(FbFormat::BGR565), + (15, RGB555) => Some(FbFormat::RGB555), + (15, BGR555) => Some(FbFormat::BGR555), + (12, RGB444) => Some(FbFormat::RGB444), + _ => None, + } + } +} diff --git a/daemon/src/display/orbic.rs b/daemon/src/display/orbic.rs index af92637..317a0e1 100644 --- a/daemon/src/display/orbic.rs +++ b/daemon/src/display/orbic.rs @@ -1,40 +1,15 @@ use crate::config; use crate::display::DisplayState; -use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; -use async_trait::async_trait; +use crate::display::generic_framebuffer; use tokio::sync::mpsc::Receiver; use tokio::sync::oneshot; use tokio_util::task::TaskTracker; +use super::generic_framebuffer::FramebufferDevice; + const FB_PATH: &str = "/dev/fb0"; -#[derive(Copy, Clone, Default)] -struct Framebuffer; - -#[async_trait] -impl GenericFramebuffer for Framebuffer { - fn dimensions(&self) -> Dimensions { - // TODO actually poll for this, maybe w/ fbset? - Dimensions { - height: 128, - width: 128, - } - } - - async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) { - let mut raw_buffer = Vec::new(); - for (r, g, b) in buffer { - let mut rgb565: u16 = (r as u16 & 0b11111000) << 8; - rgb565 |= (g as u16 & 0b11111100) << 3; - rgb565 |= (b as u16) >> 3; - raw_buffer.extend(rgb565.to_le_bytes()); - } - - tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap(); - } -} - pub fn update_ui( task_tracker: &TaskTracker, config: &config::Config, @@ -44,7 +19,7 @@ pub fn update_ui( generic_framebuffer::update_ui( task_tracker, config, - Framebuffer, + FramebufferDevice::new(FB_PATH, None, None), ui_shutdown_rx, ui_update_rx, ) diff --git a/daemon/src/display/tplink_framebuffer.rs b/daemon/src/display/tplink_framebuffer.rs index 5963538..0e180b4 100644 --- a/daemon/src/display/tplink_framebuffer.rs +++ b/daemon/src/display/tplink_framebuffer.rs @@ -1,19 +1,16 @@ -use async_trait::async_trait; use std::os::fd::AsRawFd; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; use crate::config; use crate::display::DisplayState; -use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; +use crate::display::generic_framebuffer; use tokio::sync::mpsc::Receiver; use tokio::sync::oneshot; use tokio_util::task::TaskTracker; -const FB_PATH: &str = "/dev/fb0"; +use super::generic_framebuffer::{FbInner, FramebufferDevice}; -struct Framebuffer; +const FB_PATH: &str = "/dev/fb0"; #[repr(C)] struct fb_fillrect { @@ -25,54 +22,28 @@ struct fb_fillrect { rop: u32, } -#[async_trait] -impl GenericFramebuffer for Framebuffer { - fn dimensions(&self) -> Dimensions { - // TODO actually poll for this, maybe w/ fbset? - Dimensions { - height: 128, - width: 128, - } - } +fn update_display(fb: &mut FbInner, buffer: &[(u8, u8, u8)]) { + let width = fb.dims.width; + let height = buffer.len() as u32 / width; + let mut arg = fb_fillrect { + dx: 0, + dy: 0, + width, + height, + color: 0xffff, // not sure what this is + rop: 0, + }; - async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) { - // for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c - let dimensions = self.dimensions(); - let width = dimensions.width; - let height = buffer.len() as u32 / width; - let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap(); - let mut arg = fb_fillrect { - dx: 0, - dy: 0, - width, - height, - color: 0xffff, // not sure what this is - rop: 0, - }; + unsafe { + let res = libc::ioctl( + fb.fd.as_raw_fd(), + 0x4619, // FBIORECT_DISPLAY + &mut arg as *mut _, + std::mem::size_of::(), + ); - let mut raw_buffer = Vec::new(); - for (r, g, b) in buffer { - let mut rgb565: u16 = (r as u16 & 0b11111000) << 8; - rgb565 |= (g as u16 & 0b11111100) << 3; - rgb565 |= (b as u16) >> 3; - // note: big-endian! - raw_buffer.extend(rgb565.to_be_bytes()); - } - - f.write_all(&raw_buffer).await.unwrap(); - - // ioctl is a synchronous operation, but it's fast enough that it shouldn't block - unsafe { - let res = libc::ioctl( - f.as_raw_fd(), - 0x4619, // FBIORECT_DISPLAY - &mut arg as *mut _, - std::mem::size_of::(), - ); - - if res < 0 { - panic!("failed to send FBIORECT_DISPLAY ioctl, {res}"); - } + if res < 0 { + panic!("failed to send FBIORECT_DISPLAY ioctl, {res}"); } } } @@ -86,7 +57,7 @@ pub fn update_ui( generic_framebuffer::update_ui( task_tracker, config, - Framebuffer, + FramebufferDevice::new(FB_PATH, None, Some(Box::new(update_display))), ui_shutdown_rx, ui_update_rx, ) diff --git a/daemon/src/display/wingtech.rs b/daemon/src/display/wingtech.rs index d88951c..43cbeb7 100644 --- a/daemon/src/display/wingtech.rs +++ b/daemon/src/display/wingtech.rs @@ -1,45 +1,21 @@ -use crate::config; -use crate::display::DisplayState; -use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; /// Display support for the Wingtech CT2MHS01 hotspot. /// /// Tested on (from `/etc/wt_version`): /// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP /// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55 /// WT_HARDWARE_VERSION=89323_1_20 -use async_trait::async_trait; +use crate::config; +use crate::display::DisplayState; +use crate::display::generic_framebuffer; use tokio::sync::mpsc::Receiver; use tokio::sync::oneshot; use tokio_util::task::TaskTracker; +use super::generic_framebuffer::FramebufferDevice; + const FB_PATH: &str = "/dev/fb0"; -#[derive(Copy, Clone, Default)] -struct Framebuffer; - -#[async_trait] -impl GenericFramebuffer for Framebuffer { - fn dimensions(&self) -> Dimensions { - Dimensions { - height: 128, - width: 160, - } - } - - async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) { - let mut raw_buffer = Vec::new(); - for (r, g, b) in buffer { - let mut rgb565: u16 = (r as u16 & 0b11111000) << 8; - rgb565 |= (g as u16 & 0b11111100) << 3; - rgb565 |= (b as u16) >> 3; - raw_buffer.extend(rgb565.to_le_bytes()); - } - - tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap(); - } -} - pub fn update_ui( task_tracker: &TaskTracker, config: &config::Config, @@ -49,7 +25,7 @@ pub fn update_ui( generic_framebuffer::update_ui( task_tracker, config, - Framebuffer, + FramebufferDevice::new(FB_PATH, None, None), ui_shutdown_rx, ui_update_rx, )