From 98d6ad7d187d514f1f2d7ba41158a103c8e8298a Mon Sep 17 00:00:00 2001 From: Adrian G L Date: Mon, 16 Mar 2026 17:31:21 +0100 Subject: [PATCH] paralell --- Cargo.toml | 9 +++++-- README.md | 9 ++++--- config.toml | 64 +++++++++++++++++++---------------------------- flake.nix | 1 + src/eventloop.rs | 56 ++++++++++++++++++++++++++++++----------- src/main.rs | 5 +++- src/screenshot.rs | 3 ++- src/settings.rs | 4 +-- 8 files changed, 89 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 437252c..929730e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,19 @@ anyhow = "1.0" config = "0.15.21" ctrlc = "3.5.2" futures = "0.3" -grim-rs = "0.1.6" image = "0.25.10" image-compare = "0.5.0" -libc = "0.2.183" okmain = "0.2.0" reqwest = { version = "0.13.2", features = ["json"] } rgb = "0.8" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tokio = { version = "1.50.0", features = ["full"] } + +[target.'cfg(unix)'.dependencies] +grim-rs = "0.1.6" +libc = "0.2.183" tray-item = { version = "0.10.0", features = ["ksni"] } + +[target.'cfg(windows)'.dependencies] +tray-item = { version = "0.10.0" } diff --git a/README.md b/README.md index fd688e6..0cde7e6 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ ha_url = "http://192.168.1.100:8123" # Your Long-Lived Access Token (Required) ha_token = "ey..." -# Target Frames Per Second to process (default: 1) -target_fps = 1 +# Target Frames Per Second for light updates (default: 15) +# This is the MAXIMUM rate - lights only update when color actually changes +target_fps = 15 # Color smoothing factor from 0.0 to 1.0 (default: 0.0) # Higher values mean slower, smoother color transitions. @@ -81,7 +82,9 @@ nix develop -c cargo run --release -# TODO: Parallel Event Loop Restructuring +# TODO: Parallel Event Loop Restructuring (COMPLETED) + +This architecture has been implemented. See `src/eventloop.rs` for details. ## Architecture Overview diff --git a/config.toml b/config.toml index e1351f5..6e7dece 100644 --- a/config.toml +++ b/config.toml @@ -1,43 +1,31 @@ -# # Configuration for Ambilight Home Assistant Integration -# -# # The base URL of your Home Assistant instance. -# # Example: "http://192.168.1.100:8123" or "https://ha.yourdomain.com" -# ha_url = "http://192.168.1.238:8123" -# -# # A Long-Lived Access Token from Home Assistant. -# # You can generate this in Home Assistant by going to your user profile. -# ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0YjNjMmYxNGE3NWM0YmJjYmNhYzA0YWFmYTlhOTVkNCIsImlhdCI6MTc3MzUyOTcwNCwiZXhwIjoyMDg4ODg5NzA0fQ.I923kml1zWqYYZk-0JSunexbo5NKcgehhwmG-T8jBcg" -# -# # Target updates per second (FPS) -# target_fps = 3 -# -# # Color smoothing factor (0.0 to 1.0) -# # 0.0 = no smoothing (instant updates) -# # 1.0 = completely static (no updates) -# smoothing = 0.1 -# -# # Specific lights to control. If empty, all supported color lights are used. -# # Example: ["light.living_room", "light.bedroom"] -# lights = [] -# -# # Restore lights to their original state/color when the program exits. -# restore_on_exit = true -# -# # Time (in seconds) it takes to fade to the new color. 0.0 means instant. -# transition = 0.1 -# -# # Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%) -# # Helps with flickering on static images, from the non deterministic color algorithm. -# min_diff_percent = 5.0 - - - +# Configuration for Ambilight Home Assistant Integration +# The base URL of your Home Assistant instance. +# Example: "http://192.168.1.100:8123" or "https://ha.yourdomain.com" ha_url = "https://homeassistant.pvv.ntnu.no:8123" -ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3ZmIxMzQ4NzE5ZmU0MmMyYTA5M2JiNWU2NjQ2OWJmYSIsImlhdCI6MTc3MzY2MzQwNCwiZXhwIjoyMDg5MDIzNDA0fQ.JKn8hPvK7aPS-SuXKKEREk7vva-PGXehLUH7ijoSbjQ" -target_fps = 15.0 + +# A Long-Lived Access Token from Home Assistant. +# You can generate this in Home Assistant by going to your user profile. +ha_token = "" + +# Screenshot capture rate (FPS). How often to capture and analyze the screen. +screenshot_fps = 15 + +# Maximum light update rate (FPS). Lights only update when color actually changes. +# This acts as a rate limiter to prevent spamming Home Assistant. +target_fps = 3 + +# Color smoothing factor (0.0 to 1.0) +# Higher values = slower, smoother transitions (0.0 = instant, 1.0 = no movement) smoothing = 0.6 -lights = [ "light.fargelys" ] + +# Specific lights to control. If empty, all supported color lights are used. +# Example: ["light.living_room", "light.bedroom"] +lights = ["light.fargelys"] + +# Restore lights to their original state/color when the program exits. restore_on_exit = true -transition = 0.0 + +# Minimum percentage of difference required between frames to trigger an update. +# Helps prevent flickering on static screens due to non-deterministic color algorithm. min_diff_percent = 10.0 diff --git a/flake.nix b/flake.nix index 96e4e48..6c6a903 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,7 @@ rustfmt clippy rustPlatform.bindgenHook + pkgs.pkgsCross.mingwW64.windows.mingw_w64_headers dbus gtk3 glib diff --git a/src/eventloop.rs b/src/eventloop.rs index a4c4df3..34b7a39 100644 --- a/src/eventloop.rs +++ b/src/eventloop.rs @@ -20,23 +20,25 @@ pub fn run_loop( let rt = tokio::runtime::Runtime::new().unwrap(); let screenshot_fps = settings.screenshot_fps.max(1); - let target_fps = settings.target_fps.max(1); let screenshot_interval = Duration::from_millis((1000 / screenshot_fps) as u64); - let target_interval = Duration::from_millis((1000 / target_fps) as u64); let smoothing = settings.smoothing.clamp(0.0, 1.0); - let min_diff_percent = settings.min_diff_percent; + let max_fps = settings.target_fps.max(1); + let min_update_interval = Duration::from_millis((1000 / max_fps) as u64); println!( - "Starting Ambilight loop (screenshot: {} FPS, light update: {} FPS)...", - screenshot_fps, target_fps + "Starting Ambilight loop (screenshot: {} FPS, max light updates: {} FPS)...", + screenshot_fps, max_fps ); + let exit_flag = Arc::new(AtomicBool::new(false)); + let is_running_screenshot = is_running.clone(); let state_screenshot = state.clone(); let settings_screenshot = settings.clone(); + let exit_flag_screenshot = exit_flag.clone(); let screenshot_handle = thread::spawn(move || { - loop { + while !exit_flag_screenshot.load(Ordering::Relaxed) { let start = Instant::now(); if is_running_screenshot.load(Ordering::Relaxed) { @@ -77,14 +79,23 @@ pub fn run_loop( let ha_client_smoothing = ha_client.clone(); let target_lights_smoothing = target_lights.clone(); let is_running_smoothing = is_running.clone(); + let exit_flag_smoothing = exit_flag.clone(); + let min_update_interval_clone = min_update_interval; rt.block_on(async { + let mut last_sent_color: Option> = None; + let color_threshold: f32 = 5.0; let mut last_calc_time = Instant::now(); let calc_interval = Duration::from_millis(50); + let mut last_update_time = Instant::now() - min_update_interval_clone; loop { let start = Instant::now(); + if exit_flag_smoothing.load(Ordering::Relaxed) { + break; + } + if is_running_smoothing.load(Ordering::Relaxed) { if start.duration_since(last_calc_time) >= calc_interval { let active_screenshot = { @@ -123,26 +134,41 @@ pub fn run_loop( (guard.r.round() as u8, guard.g.round() as u8, guard.b.round() as u8) }; - if let Err(e) = ha_client_smoothing - .set_lights_color_parallel(&target_lights_smoothing, r, g, b) - .await - { - eprintln!("Failed to update lights: {}", e); + let should_update = match last_sent_color { + None => true, + Some(last) => { + let dr = (r as f32 - last.r as f32).abs(); + let dg = (g as f32 - last.g as f32).abs(); + let db = (b as f32 - last.b as f32).abs(); + dr > color_threshold || dg > color_threshold || db > color_threshold + } + }; + + if should_update && start.duration_since(last_update_time) >= min_update_interval_clone { + if let Err(e) = ha_client_smoothing + .set_lights_color_parallel(&target_lights_smoothing, r, g, b) + .await + { + eprintln!("Failed to update lights: {}", e); + } else { + last_sent_color = Some(RGB::new(r, g, b)); + last_update_time = start; + } } } - let elapsed = start.elapsed(); - let sleep_duration = target_interval.saturating_sub(elapsed); - + let poll_interval = Duration::from_millis(10); tokio::select! { - _ = tokio::time::sleep(sleep_duration) => {} + _ = tokio::time::sleep(poll_interval) => {} _ = exit_rx.recv() => { println!("\nExit signal received, stopping loop..."); + exit_flag_smoothing.store(true, Ordering::Relaxed); break; } } } }); + exit_flag.store(true, Ordering::Relaxed); let _ = screenshot_handle.join(); } diff --git a/src/main.rs b/src/main.rs index edf8689..4466ca3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,10 @@ mod state; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use tray_item::{IconSource, TrayItem}; + +#[cfg(unix)] +use tray_item::IconSource; +use tray_item::TrayItem; fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); diff --git a/src/screenshot.rs b/src/screenshot.rs index 6b9013d..4018940 100644 --- a/src/screenshot.rs +++ b/src/screenshot.rs @@ -211,7 +211,8 @@ mod ffi { mod ffi { #![allow(non_snake_case, dead_code)] use super::{Pixel, ScreenResult, Screenshot}; - use libc::{c_int, c_long, c_uint, c_void}; + use std::ffi::c_void; + use std::os::raw::{c_int, c_long, c_uint}; use std::mem::size_of; type PVOID = *mut c_void; diff --git a/src/settings.rs b/src/settings.rs index cc8a522..9a1a4a3 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -37,11 +37,11 @@ fn default_restore_on_exit() -> bool { } fn default_target_fps() -> u32 { - 1 + 3 } fn default_screenshot_fps() -> u32 { - 3 + 15 } fn default_smoothing() -> f32 {