diff --git a/Cargo.lock b/Cargo.lock index 14334c6..2d11aba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2422,6 +2422,7 @@ name = "rayhunter-daemon" version = "0.5.0" dependencies = [ "anyhow", + "async-trait", "async_zip", "axum", "chrono", diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 92f3395..223b675 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" rayhunter = { path = "../lib" } toml = "0.8.8" serde = { version = "1.0.193", features = ["derive"] } -tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] } +tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] } axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] } thiserror = "1.0.52" libc = "0.2.150" @@ -24,3 +24,4 @@ image = { version = "0.25.1", default-features = false, features = ["png", "gif tempfile = "3.10.1" async_zip = { version = "0.0.17", features = ["tokio"] } anyhow = "1.0.98" +async-trait = "0.1.88" diff --git a/daemon/src/display/generic_framebuffer.rs b/daemon/src/display/generic_framebuffer.rs index e5e6a96..f75b59d 100644 --- a/daemon/src/display/generic_framebuffer.rs +++ b/daemon/src/display/generic_framebuffer.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType}; use std::io::Cursor; use std::time::Duration; @@ -11,8 +12,6 @@ use tokio::sync::oneshot; use tokio::sync::oneshot::error::TryRecvError; use tokio_util::task::TaskTracker; -use std::thread::sleep; - use include_dir::{Dir, include_dir}; #[derive(Copy, Clone)] @@ -65,15 +64,13 @@ impl Color { } } +#[async_trait] pub trait GenericFramebuffer: Send + 'static { fn dimensions(&self) -> Dimensions; - fn write_buffer( - &mut self, - buffer: &[(u8, u8, u8)], // rgb, row-wise, left-to-right, top-to-bottom - ); + async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>); // rgb, row-wise, left-to-right, top-to-bottom - fn write_dynamic_image(&mut self, img: DynamicImage) { + async fn write_dynamic_image(&mut self, img: DynamicImage) { let dimensions = self.dimensions(); let mut width = img.width(); let mut height = img.height(); @@ -94,28 +91,35 @@ pub trait GenericFramebuffer: Send + 'static { } } - self.write_buffer(&buf); + self.write_buffer(buf).await } - fn draw_gif(&mut self, img_buffer: &[u8]) { - // this is dumb and i'm sure there's a better way to loop this + async fn draw_gif(&mut self, img_buffer: &[u8]) { let cursor = Cursor::new(img_buffer); - let decoder = GifDecoder::new(cursor).unwrap(); - for maybe_frame in decoder.into_frames() { - let frame = maybe_frame.unwrap(); - let (numerator, _) = frame.delay().numer_denom_ms(); - let img = DynamicImage::from(frame.into_buffer()); - self.write_dynamic_image(img); - std::thread::sleep(Duration::from_millis(numerator as u64)); + if let Ok(decoder) = GifDecoder::new(cursor) { + let frames: Vec<_> = decoder + .into_frames() + .filter_map(|f| f.ok()) + .map(|frame| { + let (numerator, _) = frame.delay().numer_denom_ms(); + let img = DynamicImage::from(frame.into_buffer()); + (img, numerator as u64) + }) + .collect(); + + for (img, delay_ms) in frames { + self.write_dynamic_image(img).await; + tokio::time::sleep(Duration::from_millis(delay_ms)).await; + } } } - fn draw_img(&mut self, img_buffer: &[u8]) { + async fn draw_img(&mut self, img_buffer: &[u8]) { let img = image::load_from_memory(img_buffer).unwrap(); - self.write_dynamic_image(img); + self.write_dynamic_image(img).await } - fn draw_line(&mut self, color: Color, height: u32) { + async fn draw_line(&mut self, color: Color, height: u32) { let width = self.dimensions().width; let px_num = height * width; let mut buffer = Vec::new(); @@ -123,7 +127,7 @@ pub trait GenericFramebuffer: Send + 'static { buffer.push(color.rgb()); } - self.write_buffer(&buffer); + self.write_buffer(buffer).await } } @@ -143,7 +147,7 @@ pub fn update_ui( let colorblind_mode = config.colorblind_mode; let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode); - task_tracker.spawn_blocking(move || { + task_tracker.spawn(async move { // this feels wrong, is there a more rusty way to do this? let mut img: Option<&[u8]> = None; if display_level == 2 { @@ -179,21 +183,21 @@ pub fn update_ui( } match display_level { - 2 => fb.draw_gif(img.unwrap()), - 3 => fb.draw_img(img.unwrap()), + 2 => fb.draw_gif(img.unwrap()).await, + 3 => fb.draw_img(img.unwrap()).await, 128 => { - fb.draw_line(Color::Cyan, 128); - fb.draw_line(Color::Pink, 102); - fb.draw_line(Color::White, 76); - fb.draw_line(Color::Pink, 50); - fb.draw_line(Color::Cyan, 25); + fb.draw_line(Color::Cyan, 128).await; + fb.draw_line(Color::Pink, 102).await; + fb.draw_line(Color::White, 76).await; + fb.draw_line(Color::Pink, 50).await; + fb.draw_line(Color::Cyan, 25).await; } // this branch id for ui_level 1, which is also the default if an // unknown value is used _ => {} }; - fb.draw_line(display_color, 2); - sleep(Duration::from_millis(1000)); + fb.draw_line(display_color, 2).await; + tokio::time::sleep(Duration::from_millis(1000)).await; } }); } diff --git a/daemon/src/display/orbic.rs b/daemon/src/display/orbic.rs index 787bb93..af92637 100644 --- a/daemon/src/display/orbic.rs +++ b/daemon/src/display/orbic.rs @@ -1,6 +1,7 @@ use crate::config; use crate::display::DisplayState; use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; +use async_trait::async_trait; use tokio::sync::mpsc::Receiver; use tokio::sync::oneshot; @@ -11,6 +12,7 @@ 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? @@ -20,16 +22,16 @@ impl GenericFramebuffer for Framebuffer { } } - fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) { + 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; + 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()); } - std::fs::write(FB_PATH, &raw_buffer).unwrap(); + tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap(); } } diff --git a/daemon/src/display/tmobile.rs b/daemon/src/display/tmobile.rs index 23ef351..8cbcc8d 100644 --- a/daemon/src/display/tmobile.rs +++ b/daemon/src/display/tmobile.rs @@ -7,8 +7,6 @@ use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio_util::task::TaskTracker; -use std::fs::write; -use std::thread::sleep; use std::time::Duration; use crate::config; @@ -18,12 +16,12 @@ macro_rules! led { ($l:expr) => {{ format!("/sys/class/leds/led:{}/blink", $l) }}; } -fn start_blinking(path: String) { - write(&path, "1").ok(); +async fn start_blinking(path: String) { + tokio::fs::write(&path, "1").await.ok(); } -fn stop_blinking(path: String) { - write(&path, "0").ok(); +async fn stop_blinking(path: String) { + tokio::fs::write(&path, "0").await.ok(); } pub fn update_ui( @@ -37,7 +35,7 @@ pub fn update_ui( info!("Invisible mode, not spawning UI."); invisible = true; } - task_tracker.spawn_blocking(move || { + task_tracker.spawn(async move { let mut state = DisplayState::Recording; let mut last_state = DisplayState::Paused; @@ -56,28 +54,28 @@ pub fn update_ui( Err(e) => error!("error receiving ui update message: {e}"), }; if invisible || state == last_state { - sleep(Duration::from_secs(1)); + tokio::time::sleep(Duration::from_secs(1)).await; continue; } match state { DisplayState::Paused => { - stop_blinking(led!("signal_blue")); - stop_blinking(led!("signal_red")); - start_blinking(led!("wlan_white")); + stop_blinking(led!("signal_blue")).await; + stop_blinking(led!("signal_red")).await; + start_blinking(led!("wlan_white")).await; } DisplayState::Recording => { - stop_blinking(led!("wlan_white")); - stop_blinking(led!("signal_red")); - start_blinking(led!("signal_blue")); + stop_blinking(led!("wlan_white")).await; + stop_blinking(led!("signal_red")).await; + start_blinking(led!("signal_blue")).await; } DisplayState::WarningDetected => { - stop_blinking(led!("wlan_white")); - stop_blinking(led!("signal_blue")); - start_blinking(led!("signal_red")); + stop_blinking(led!("wlan_white")).await; + stop_blinking(led!("signal_blue")).await; + start_blinking(led!("signal_red")).await; } } last_state = state; - sleep(Duration::from_secs(1)); + tokio::time::sleep(Duration::from_secs(1)).await; } }); } diff --git a/daemon/src/display/tplink.rs b/daemon/src/display/tplink.rs index 4c330ef..522e839 100644 --- a/daemon/src/display/tplink.rs +++ b/daemon/src/display/tplink.rs @@ -19,6 +19,8 @@ pub fn update_ui( info!("Invisible mode, not spawning UI."); } + // Since this is a one-time check at startup, using sync is acceptable + // The alternative would be to make the entire initialization async if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() { info!("detected one-bit display"); tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx) diff --git a/daemon/src/display/tplink_framebuffer.rs b/daemon/src/display/tplink_framebuffer.rs index 95255eb..5963538 100644 --- a/daemon/src/display/tplink_framebuffer.rs +++ b/daemon/src/display/tplink_framebuffer.rs @@ -1,6 +1,7 @@ -use std::fs::File; -use std::io::Write; +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; @@ -24,6 +25,7 @@ struct fb_fillrect { rop: u32, } +#[async_trait] impl GenericFramebuffer for Framebuffer { fn dimensions(&self) -> Dimensions { // TODO actually poll for this, maybe w/ fbset? @@ -33,12 +35,12 @@ impl GenericFramebuffer for Framebuffer { } } - fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) { + 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 = File::options().write(true).open(FB_PATH).unwrap(); + let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap(); let mut arg = fb_fillrect { dx: 0, dy: 0, @@ -50,15 +52,16 @@ impl GenericFramebuffer for Framebuffer { 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; + 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).unwrap(); + 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(), diff --git a/daemon/src/display/tplink_onebit.rs b/daemon/src/display/tplink_onebit.rs index 6b94b1d..3b55721 100644 --- a/daemon/src/display/tplink_onebit.rs +++ b/daemon/src/display/tplink_onebit.rs @@ -10,8 +10,6 @@ use tokio::sync::oneshot; use tokio::sync::oneshot::error::TryRecvError; use tokio_util::task::TaskTracker; -use std::fs; -use std::thread::sleep; use std::time::Duration; pub const OLED_PATH: &str = "/sys/class/display/oled/oled_buffer"; @@ -122,7 +120,7 @@ pub fn update_ui( info!("Invisible mode, not spawning UI."); } - task_tracker.spawn_blocking(move || { + task_tracker.spawn(async move { let mut pixels = STATUS_SMILING; loop { @@ -148,12 +146,12 @@ pub fn update_ui( // we write the status every second because it may have been overwritten through menu // navigation. if display_level != 0 { - if let Err(e) = fs::write(OLED_PATH, pixels) { + if let Err(e) = tokio::fs::write(OLED_PATH, pixels).await { error!("failed to write to display: {e}"); } } - sleep(Duration::from_millis(1000)); + tokio::time::sleep(Duration::from_millis(1000)).await; } }); } diff --git a/daemon/src/display/wingtech.rs b/daemon/src/display/wingtech.rs index 10fb54c..d88951c 100644 --- a/daemon/src/display/wingtech.rs +++ b/daemon/src/display/wingtech.rs @@ -1,12 +1,13 @@ +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 crate::config; -use crate::display::DisplayState; -use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; +use async_trait::async_trait; use tokio::sync::mpsc::Receiver; use tokio::sync::oneshot; @@ -17,6 +18,7 @@ const FB_PATH: &str = "/dev/fb0"; #[derive(Copy, Clone, Default)] struct Framebuffer; +#[async_trait] impl GenericFramebuffer for Framebuffer { fn dimensions(&self) -> Dimensions { Dimensions { @@ -25,16 +27,16 @@ impl GenericFramebuffer for Framebuffer { } } - fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) { + 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; + 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()); } - std::fs::write(FB_PATH, &raw_buffer).unwrap(); + tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap(); } } diff --git a/daemon/src/main.rs b/daemon/src/main.rs index e3449a2..9da6b81 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -182,7 +182,7 @@ fn run_shutdown_thread( }) } -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), RayhunterError> { env_logger::init();