diff --git a/Cargo.lock b/Cargo.lock index 87a1596..0da7cdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -941,6 +941,7 @@ dependencies = [ "slint", "slint-build", "tokio", + "trekstor", "ureq", ] @@ -4997,13 +4998,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2 1.0.107", "quote 1.0.47", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -5102,6 +5103,14 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trekstor" +version = "0.1.0" +dependencies = [ + "evdev", + "slint", +] + [[package]] name = "ttf-parser" version = "0.25.1" diff --git a/Cargo.toml b/Cargo.toml index e29bdbb..bc9a955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ serde_json = "1.0.151" slint = { version = "1.17.1", default-features = false, features = ["std", "compat-1-2", "libm", "renderer-software", "unsafe-single-threaded"] } tokio = { version = "1.53.1", features = ["full"] } ureq = { version = "3.3.0", default-features = false, features = ["json", "rustls"] } +trekstor = { path = "../trekstor" } [features] dev = ["slint/backend-winit-x11"] diff --git a/build.rs b/build.rs index cbde8c2..7a1f536 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,7 @@ fn main() { "ui/main.slint", slint_build::CompilerConfiguration::new() .with_style("material-dark".into()) - .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer), + .embed_resources(slint_build::EmbedResourcesKind::EmbedFiles), ) .unwrap(); } diff --git a/src/main.rs b/src/main.rs index 19d7792..96c6201 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ slint::include_modules!(); mod ha; mod state; -mod trekstor; mod utils; use std::sync::{Arc, Mutex}; @@ -16,8 +15,7 @@ async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); #[cfg(not(feature = "dev"))] - slint::platform::set_platform(Box::new(crate::trekstor::Trekstor::new())) - .expect("set platform"); + slint::platform::set_platform(Box::new(trekstor::Trekstor::new())).expect("set platform"); let app = AppWindow::new()?; app.on_quit(|| std::process::exit(0)); @@ -55,7 +53,7 @@ async fn main() -> Result<(), Box> { app.set_action_status("THERMOSTAT UNAVAILABLE".into()); return; } - let target = thermostat.target.or(thermostat.current).unwrap_or(68.0) + let target = thermostat.target.or(thermostat.current).unwrap_or(16.0) + f64::from(delta); thermostat.target = Some(target); (thermostat.entity_id.clone(), target, snapshot.clone()) @@ -65,11 +63,13 @@ async fn main() -> Result<(), Box> { let weak = weak.clone(); let client = client.clone(); - std::thread::spawn(move || match client.set_temperature(&entity_id, target) { - Ok(()) => set_action_status(&weak, "SET POINT SAVED".into()), - Err(error) => { - eprintln!("Set temperature failed: {error}"); - set_action_status(&weak, "SET POINT FAILED".into()); + tokio::spawn(async move { + match client.set_temperature(&entity_id, target) { + Ok(()) => set_action_status(&weak, "SET POINT SAVED".into()), + Err(error) => { + eprintln!("Set temperature failed: {error}"); + set_action_status(&weak, "SET POINT FAILED".into()); + } } }); } @@ -108,11 +108,13 @@ async fn main() -> Result<(), Box> { let weak = weak.clone(); let client = client.clone(); - std::thread::spawn(move || match client.set_mode(&entity_id, &mode_text) { - Ok(()) => set_action_status(&weak, "MODE SAVED".into()), - Err(error) => { - eprintln!("Set mode failed: {error}"); - set_action_status(&weak, "MODE FAILED".into()); + tokio::spawn(async move { + match client.set_mode(&entity_id, &mode_text) { + Ok(()) => set_action_status(&weak, "MODE SAVED".into()), + Err(error) => { + eprintln!("Set mode failed: {error}"); + set_action_status(&weak, "MODE FAILED".into()); + } } }); } @@ -151,11 +153,13 @@ async fn main() -> Result<(), Box> { let weak = weak.clone(); let client = client.clone(); - std::thread::spawn(move || match client.set_fan_mode(&entity_id, &fan_mode) { - Ok(()) => set_action_status(&weak, "FAN SPEED SAVED".into()), - Err(error) => { - eprintln!("Set fan mode failed: {error}"); - set_action_status(&weak, "FAN SPEED FAILED".into()); + tokio::spawn(async move { + match client.set_fan_mode(&entity_id, &fan_mode) { + Ok(()) => set_action_status(&weak, "FAN SPEED SAVED".into()), + Err(error) => { + eprintln!("Set fan mode failed: {error}"); + set_action_status(&weak, "FAN SPEED FAILED".into()); + } } }); } @@ -184,11 +188,13 @@ async fn main() -> Result<(), Box> { let weak = weak.clone(); let client = client.clone(); - std::thread::spawn(move || match client.set_light(&entity_id, turn_on) { - Ok(()) => set_action_status(&weak, "LIGHT UPDATED".into()), - Err(error) => { - eprintln!("Set light failed: {error}"); - set_action_status(&weak, "LIGHT UPDATE FAILED".into()); + tokio::spawn(async move { + match client.set_light(&entity_id, turn_on) { + Ok(()) => set_action_status(&weak, "LIGHT UPDATED".into()), + Err(error) => { + eprintln!("Set light failed: {error}"); + set_action_status(&weak, "LIGHT UPDATE FAILED".into()); + } } }); } @@ -204,7 +210,7 @@ async fn main() -> Result<(), Box> { let weak = weak.clone(); let state = state.clone(); let client = client.clone(); - std::thread::spawn(move || fetch_once(&client, &state, &weak)); + tokio::spawn(async move { fetch_once(&client, &state, &weak) }); } }); @@ -226,7 +232,7 @@ fn spawn_poll_loop( state: Arc>, weak: slint::Weak, ) { - std::thread::spawn(move || { + tokio::spawn(async move { loop { fetch_once(&client, &state, &weak); std::thread::sleep(std::time::Duration::from_secs(30)); diff --git a/src/trekstor.rs b/src/trekstor.rs deleted file mode 100644 index ecb453b..0000000 --- a/src/trekstor.rs +++ /dev/null @@ -1,165 +0,0 @@ -mod input; -mod rgba; -mod screen; - -pub use rgba::Rgba; - -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -use evdev::KeyCode; -use linfb::shape::Color; -use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType, Rgb565Pixel}; -use slint::platform::{EventLoopProxy, Platform, WindowEvent}; -use slint::{EventLoopError, LogicalPosition, PhysicalSize, Rgb8Pixel, WindowSize}; - -use self::input::InputState; -use self::screen::Screen; - -pub struct Context { - pub screen: Screen, - pub input: InputState, -} - -pub struct Trekstor { - window: Rc, - queue: Queue, - quit_flag: Arc, -} - -impl Trekstor { - pub fn new() -> Self { - let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer); - - Self { - window, - queue: Arc::new(Mutex::new(Vec::new())), - quit_flag: Arc::new(AtomicBool::new(false)), - } - } -} - -pub(crate) type Closure = Box; -pub(crate) type Queue = Arc>>; - -struct TrekstorEventProxy { - pub(crate) queue: Queue, - pub(crate) quit_flag: Arc, -} - -impl EventLoopProxy for TrekstorEventProxy { - fn quit_event_loop(&self) -> Result<(), EventLoopError> { - self.quit_flag.store(true, Ordering::SeqCst); - Ok(()) - } - - fn invoke_from_event_loop(&self, event: Closure) -> Result<(), EventLoopError> { - // Don't queue if the loop is shutting down, the closure would never - // run, and any caller blocking on a channel send inside it would hang. - if self.quit_flag.load(Ordering::SeqCst) { - return Err(EventLoopError::EventLoopTerminated); - } - self.queue - .lock() - .expect("event loop closure queue poisoned") - .push(event); - Ok(()) - } -} - -impl Platform for Trekstor { - fn create_window_adapter( - &self, - ) -> Result, slint::PlatformError> { - Ok(self.window.clone()) - } - - fn run_event_loop(&self) -> Result<(), slint::PlatformError> { - let screen = Screen::new(); - let input = InputState::new(&screen); - let mut ctx = Context { screen, input }; - - let mut buffer1 = [Rgb565Pixel(0); 614400]; - - let window_width = ctx.screen.width(); - let window_height = ctx.screen.height(); - - self.window.set_size(WindowSize::Physical(PhysicalSize::new( - window_width, - window_height, - ))); - - loop { - ctx.input.handle_event(); - - slint::platform::update_timers_and_animations(); - - if ctx.input.is_just_pressed(KeyCode::BTN_TOUCH) { - self.window - .try_dispatch_event(WindowEvent::PointerPressed { - position: LogicalPosition::new(ctx.input.x as f32, ctx.input.y as f32), - button: slint::platform::PointerEventButton::Left, - }) - .unwrap(); - } - - if ctx.input.is_just_released(KeyCode::BTN_TOUCH) { - self.window - .try_dispatch_event(WindowEvent::PointerReleased { - position: LogicalPosition::new(ctx.input.x as f32, ctx.input.y as f32), - button: slint::platform::PointerEventButton::Left, - }) - .unwrap(); - } - - if ctx.input.is_down(KeyCode::BTN_TOUCH) { - self.window - .try_dispatch_event(WindowEvent::PointerMoved { - position: LogicalPosition::new(ctx.input.x as f32, ctx.input.y as f32), - }) - .unwrap(); - } - - if self.quit_flag.load(Ordering::SeqCst) { - break; - } - - self.window.draw_if_needed(|renderer| { - renderer.render(&mut buffer1, window_width as usize); - - for (i, px) in buffer1.into_iter().enumerate() { - let x = (i % window_width as usize) as u32; - let y = (i / window_width as usize) as u32; - - let pixel = Rgb8Pixel::from(px); - let c = Color::from((pixel.r, pixel.g, pixel.b, 255)); - - ctx.screen.framebuffer.set_pixel(x, y, c); - } - ctx.screen.flush(); - }); - - ctx.input.finish(); - - let pending: Vec<_> = self - .queue - .lock() - .expect("event loop closure queue poisoned") - .drain(..) - .collect(); - for c in pending { - c(); - } - } - - Ok(()) - } - - fn new_event_loop_proxy(&self) -> Option> { - Some(Box::new(TrekstorEventProxy { - queue: self.queue.clone(), - quit_flag: self.quit_flag.clone(), - })) - } -} diff --git a/src/trekstor/input.rs b/src/trekstor/input.rs deleted file mode 100644 index 4d8dd57..0000000 --- a/src/trekstor/input.rs +++ /dev/null @@ -1,100 +0,0 @@ -use super::screen::Screen; -use std::collections::HashSet; - -use evdev::{AbsoluteAxisCode, Device, EventSummary, KeyCode}; - -pub struct InputState { - device: Device, - pub x: i32, - pub y: i32, - width: i32, - height: i32, - keys_down: HashSet, - keys_pressed_this_update: HashSet, - keys_released_this_update: HashSet, -} - -impl InputState { - pub fn new(screen: &Screen) -> Self { - let Some((_, device)) = evdev::enumerate() - .filter(|(_, device)| device.name() == Some("silead_ts")) - .next() - else { - panic!("No TS"); - }; - - device.set_nonblocking(true).unwrap(); - - Self { - device, - x: 0, - y: 0, - width: screen.width() as i32, - height: screen.height() as i32, - keys_down: HashSet::new(), - keys_pressed_this_update: HashSet::new(), - keys_released_this_update: HashSet::new(), - } - } - - pub(super) fn finish(&mut self) { - self.keys_pressed_this_update.clear(); - self.keys_released_this_update.clear(); - } - - pub(super) fn handle_event(&mut self) { - let Ok(events) = self.device.fetch_events() else { - return; - }; - - for event in events { - match event.destructure() { - EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_X, value) => { - self.x = ((((value - 13) as f32 / 882.) * (self.width as f32)) as i32) - .max(0) - .min(self.width); - } - EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_Y, value) => { - self.y = ((((value - 13) as f32 / 624.) * (self.height as f32)) as i32) - .max(0) - .min(self.width); - } - EventSummary::Key(_, key, value) => match value { - 1 => { - if !self.keys_down.contains(&key) { - self.keys_pressed_this_update.insert(key); - } - self.keys_down.insert(key); - } - 0 => { - if self.keys_down.contains(&key) { - self.keys_released_this_update.insert(key); - } - self.keys_down.remove(&key); - } - _ => {} - }, - _ => {} - } - } - } -} - -#[allow(unused)] -impl InputState { - pub fn is_just_pressed(&self, key: KeyCode) -> bool { - self.keys_pressed_this_update.contains(&key) - } - - pub fn is_down(&self, key: KeyCode) -> bool { - self.keys_down.contains(&key) - } - - pub fn is_just_released(&self, key: KeyCode) -> bool { - self.keys_released_this_update.contains(&key) - } - - pub fn is_up(&self, key: KeyCode) -> bool { - !self.is_down(key) - } -} diff --git a/src/trekstor/rgba.rs b/src/trekstor/rgba.rs deleted file mode 100644 index 2ba1669..0000000 --- a/src/trekstor/rgba.rs +++ /dev/null @@ -1,125 +0,0 @@ -use embedded_graphics_core::pixelcolor::*; -use linfb::shape::Color; -use slint::platform::software_renderer::TargetPixel; - -/// Simple RGBA color wrapper. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Rgba(C, u8); - -#[allow(unused)] -#[inline(always)] -fn mul_blend_u8(delta: u32, a: u32) -> u32 { - // Exact (delta * a) / 255 using the div255 trick (no slow integer division). - // Valid for 0..=65535 inputs; see Hacker's Delight 10-16. - let t = delta * a + 128; - (t + (t >> 8)) >> 8 -} - -#[allow(unused)] -impl Rgba { - /// Create a new RGBA color. - pub const fn new(color: C, alpha: u8) -> Self { - Self(color, alpha) - } - - /// Get the color component. - pub const fn rgb(&self) -> C { - self.0 - } - - pub fn r(&self) -> u8 { - self.0.r() - } - - pub fn g(&self) -> u8 { - self.0.g() - } - - pub fn b(&self) -> u8 { - self.0.b() - } - - /// Get the alpha component (0..=255). - pub const fn a(&self) -> u8 { - self.1 - } -} - -impl PixelColor for Rgba { - type Raw = C::Raw; -} - -#[allow(unused)] -pub trait Blend { - fn blend(&self, bg: T) -> T; -} - -impl Blend for Rgba { - #[inline(always)] - fn blend(&self, bg: Rgb888) -> Rgb888 { - let a = self.a() as u32; - if a == 0 { - return bg; - } - if a == 255 { - return self.rgb(); - } - - let fr = self.rgb().r() as u32; - let fg = self.rgb().g() as u32; - let fb = self.rgb().b() as u32; - - let br = bg.r() as u32; - let bgc = bg.g() as u32; - let bb = bg.b() as u32; - - let r = (br + mul_blend_u8(fr.wrapping_sub(br), a)) as u8; - let g = (bgc + mul_blend_u8(fg.wrapping_sub(bgc), a)) as u8; - let b = (bb + mul_blend_u8(fb.wrapping_sub(bb), a)) as u8; - - Rgb888::new(r, g, b) - } -} - -impl TargetPixel for Rgba { - fn blend(&mut self, color: slint::platform::software_renderer::PremultipliedRgbaColor) { - let a = self.a() as u32; - // if a == 0 { - // return bg; - // } - // if a == 255 { - // return self.rgb(); - // } - - let fr = self.rgb().r() as u32; - let fg = self.rgb().g() as u32; - let fb = self.rgb().b() as u32; - - let br = color.red as u32; - let bgc = color.green as u32; - let bb = color.blue as u32; - - let r = (br + mul_blend_u8(fr.wrapping_sub(br), a)) as u8; - let g = (bgc + mul_blend_u8(fg.wrapping_sub(bgc), a)) as u8; - let b = (bb + mul_blend_u8(fb.wrapping_sub(bb), a)) as u8; - - self.0 = Rgb888::new(r, g, b); - self.1 = a as u8; - } - - fn from_rgb(red: u8, green: u8, blue: u8) -> Self { - Self(Rgb888::new(red, green, blue), 255) - } -} - -impl From<(u8, u8, u8, u8)> for Rgba { - fn from(value: (u8, u8, u8, u8)) -> Self { - Self(Rgb888::new(value.0, value.1, value.2), value.3) - } -} - -impl From for Rgba { - fn from(value: Color) -> Self { - Self(Rgb888::new(value.red, value.green, value.blue), value.alpha) - } -} diff --git a/src/trekstor/screen.rs b/src/trekstor/screen.rs deleted file mode 100644 index 9dc1b44..0000000 --- a/src/trekstor/screen.rs +++ /dev/null @@ -1,90 +0,0 @@ -use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle}; -use linfb::{Framebuffer, shape::Color}; - -use crate::trekstor::Rgba; - -pub struct Screen { - pub framebuffer: Framebuffer, - width: u32, - height: u32, -} - -#[allow(unused)] -impl Screen { - pub fn new() -> Self { - let framebuffer = Framebuffer::open().expect("Failed to open framebuffer"); - - let width = framebuffer.screen_info.xres; - let height = framebuffer.screen_info.yres; - - Self { - framebuffer, - width, - height, - } - } - - pub fn flush(&mut self) { - self.framebuffer.flush(); - } - - #[inline] - pub fn width(&self) -> u32 { - self.width - } - - #[inline] - pub fn height(&self) -> u32 { - self.height - } - - #[inline] - pub fn rect(&self) -> Rectangle { - Rectangle { - top_left: Point::new(0, 0), - size: Size { - width: self.width(), - height: self.height(), - }, - } - } -} - -impl Dimensions for Screen { - fn bounding_box(&self) -> Rectangle { - Rectangle::new(Point::new(0, 0), Size::new(self.width, self.height)) - } -} - -impl DrawTarget for Screen { - type Color = Rgba; - type Error = Box; - - fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> - where - I: IntoIterator>, - { - let width = self.bounding_box().size.width; - let height = self.bounding_box().size.height; - - for p in pixels { - let x = p.0.x as u32; - let y = p.0.y as u32; - - let color = Color { - red: p.1.r(), - green: p.1.g(), - blue: p.1.b(), - alpha: p.1.a(), - }; - - if !(0..width).contains(&x) || !(0..height).contains(&y) { - continue; - } - - self.framebuffer.set_pixel(x, y, color); - } - - Ok(()) - } -}