This commit is contained in:
2026-03-16 17:31:21 +01:00
parent da1ef93f8d
commit 98d6ad7d18
8 changed files with 89 additions and 62 deletions
+7 -2
View File
@@ -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" }
+6 -3
View File
@@ -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
+26 -38
View File
@@ -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
+1
View File
@@ -57,6 +57,7 @@
rustfmt
clippy
rustPlatform.bindgenHook
pkgs.pkgsCross.mingwW64.windows.mingw_w64_headers
dbus
gtk3
glib
+41 -15
View File
@@ -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<RGB<u8>> = 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();
}
+4 -1
View File
@@ -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();
+2 -1
View File
@@ -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;
+2 -2
View File
@@ -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 {