1 Commits

Author SHA1 Message Date
Sashanoraa
c2226071d2 Dynamically determine frame buffer dimensions and format
This commit also refactors a majority of the device specific frame
buffer code into common code into generic_framebuffer.
2025-07-30 22:46:26 -04:00
4 changed files with 336 additions and 112 deletions

View File

@@ -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<Dimensions> {
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<Option<FbFormat>> {
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<u8> {
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<dyn FnMut(&mut FbInner, &[(u8, u8, u8)]) + Send + 'static>;
pub struct FramebufferDevice {
data: FbInner,
pre_write_fn: Option<CallBack>,
post_write_fn: Option<CallBack>,
}
pub struct FbInner {
pub fd: File,
pub dims: Dimensions,
pub format: FbFormat,
}
impl FramebufferDevice {
pub fn new(
path: &str,
pre_write_fn: Option<CallBack>,
post_write_fn: Option<CallBack>,
) -> 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<FixScreeninfo> {
// 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<VarScreeninfo> {
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<FbFormat> {
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,
}
}
}

View File

@@ -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,
)

View File

@@ -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::<fb_fillrect>(),
);
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::<fb_fillrect>(),
);
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,
)

View File

@@ -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,
)