testing paralell
This commit is contained in:
187
src/eventloop.rs
187
src/eventloop.rs
@@ -1,93 +1,148 @@
|
||||
use crate::homeassistant::HaClient;
|
||||
use crate::screenshot;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::AppState;
|
||||
use rgb::RGB;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub async fn run_loop(
|
||||
settings: &Settings,
|
||||
ha_client: &HaClient,
|
||||
target_lights: &[String],
|
||||
pub fn run_loop(
|
||||
settings: Arc<Settings>,
|
||||
ha_client: Arc<HaClient>,
|
||||
target_lights: Arc<Vec<String>>,
|
||||
state: Arc<AppState>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
mut exit_rx: mpsc::Receiver<()>,
|
||||
) {
|
||||
let fps = settings.target_fps.max(1);
|
||||
let target_duration = Duration::from_millis((1000 / fps) as u64);
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let mut current_color: Option<(f32, f32, f32)> = None;
|
||||
let mut last_screenshot: Option<screenshot::Screenshot> = None;
|
||||
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;
|
||||
|
||||
println!("Starting Ambilight loop at {} FPS...", fps);
|
||||
println!(
|
||||
"Starting Ambilight loop (screenshot: {} FPS, light update: {} FPS)...",
|
||||
screenshot_fps, target_fps
|
||||
);
|
||||
|
||||
loop {
|
||||
let start_time = Instant::now();
|
||||
let is_running_screenshot = is_running.clone();
|
||||
let state_screenshot = state.clone();
|
||||
let settings_screenshot = settings.clone();
|
||||
|
||||
if is_running.load(Ordering::Relaxed) {
|
||||
match screenshot::get_screenshot(0) {
|
||||
Ok(current_screenshot) => {
|
||||
let mut should_update = true;
|
||||
let screenshot_handle = thread::spawn(move || {
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
|
||||
if let Some(prev) = &last_screenshot {
|
||||
let diff = current_screenshot.diff_percent(prev);
|
||||
if diff < settings.min_diff_percent {
|
||||
should_update = false;
|
||||
if is_running_screenshot.load(Ordering::Relaxed) {
|
||||
if let Ok(current) = screenshot::get_screenshot(0) {
|
||||
let current_arc = Arc::new(current);
|
||||
|
||||
let should_update = {
|
||||
let active = state_screenshot.active_screenshot.read().unwrap();
|
||||
match active.as_ref() {
|
||||
None => true,
|
||||
Some(active_screenshot) => {
|
||||
let diff = current_arc.diff_percent(active_screenshot);
|
||||
diff >= settings_screenshot.min_diff_percent
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if should_update {
|
||||
let color = current_screenshot.dominant_color();
|
||||
let (new_r, new_g, new_b) =
|
||||
(color.r as f32, color.g as f32, color.b as f32);
|
||||
|
||||
let (r, g, b) = match current_color {
|
||||
None => (new_r, new_g, new_b),
|
||||
Some((cr, cg, cb)) => {
|
||||
let s_factor = settings.smoothing.clamp(0.0, 1.0);
|
||||
(
|
||||
cr * s_factor + new_r * (1.0 - s_factor),
|
||||
cg * s_factor + new_g * (1.0 - s_factor),
|
||||
cb * s_factor + new_b * (1.0 - s_factor),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
current_color = Some((r, g, b));
|
||||
let (final_r, final_g, final_b) =
|
||||
(r.round() as u8, g.round() as u8, b.round() as u8);
|
||||
|
||||
if let Err(e) = ha_client
|
||||
.set_lights_color(
|
||||
target_lights,
|
||||
final_r,
|
||||
final_g,
|
||||
final_b,
|
||||
settings.transition,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to update lights: {}", e);
|
||||
let mut current_guard = state_screenshot.current_screenshot.write().unwrap();
|
||||
*current_guard = Some(current_arc.clone());
|
||||
}
|
||||
{
|
||||
let mut active_guard = state_screenshot.active_screenshot.write().unwrap();
|
||||
*active_guard = Some(current_arc);
|
||||
}
|
||||
|
||||
last_screenshot = Some(current_screenshot);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Screenshot error: {}", e),
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let sleep_duration = screenshot_interval.saturating_sub(elapsed);
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
});
|
||||
|
||||
let state_smoothing = state.clone();
|
||||
let ha_client_smoothing = ha_client.clone();
|
||||
let target_lights_smoothing = target_lights.clone();
|
||||
let is_running_smoothing = is_running.clone();
|
||||
|
||||
rt.block_on(async {
|
||||
let mut last_calc_time = Instant::now();
|
||||
let calc_interval = Duration::from_millis(50);
|
||||
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
|
||||
if is_running_smoothing.load(Ordering::Relaxed) {
|
||||
if start.duration_since(last_calc_time) >= calc_interval {
|
||||
let active_screenshot = {
|
||||
let guard = state_smoothing.active_screenshot.read().unwrap();
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
if let Some(screenshot) = active_screenshot {
|
||||
let color = screenshot.dominant_color();
|
||||
let new_target = RGB::new(color.r as f32, color.g as f32, color.b as f32);
|
||||
|
||||
{
|
||||
let mut target_guard = state_smoothing.target_color.write().unwrap();
|
||||
*target_guard = Some(new_target);
|
||||
}
|
||||
}
|
||||
last_calc_time = start;
|
||||
}
|
||||
|
||||
{
|
||||
let target_color = {
|
||||
let guard = state_smoothing.target_color.read().unwrap();
|
||||
*guard
|
||||
};
|
||||
|
||||
if let Some(target) = target_color {
|
||||
let mut active_guard = state_smoothing.active_color.write().unwrap();
|
||||
active_guard.r = active_guard.r * smoothing + target.r * (1.0 - smoothing);
|
||||
active_guard.g = active_guard.g * smoothing + target.g * (1.0 - smoothing);
|
||||
active_guard.b = active_guard.b * smoothing + target.b * (1.0 - smoothing);
|
||||
}
|
||||
}
|
||||
|
||||
let (r, g, b) = {
|
||||
let guard = state_smoothing.active_color.read().unwrap();
|
||||
(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 elapsed = start.elapsed();
|
||||
let sleep_duration = target_interval.saturating_sub(elapsed);
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(sleep_duration) => {}
|
||||
_ = exit_rx.recv() => {
|
||||
println!("\nExit signal received, stopping loop...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sleep to maintain target FPS, saturating_sub prevents negative duration panics if we run slow
|
||||
let elapsed = start_time.elapsed();
|
||||
let sleep_duration = target_duration.saturating_sub(elapsed);
|
||||
|
||||
tokio::select! {
|
||||
_ = sleep(sleep_duration) => {}
|
||||
_ = exit_rx.recv() => {
|
||||
println!("\nExit signal received, stopping loop...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = screenshot_handle.join();
|
||||
}
|
||||
|
||||
@@ -90,77 +90,98 @@ impl HaClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_lights_color(
|
||||
pub async fn set_light_color(
|
||||
&self,
|
||||
entity_id: &str,
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
states: &[HaState],
|
||||
) -> Result<(), Error> {
|
||||
let url = format!("{}/api/services/light/turn_on", self.base_url);
|
||||
|
||||
let mut payload = json!({
|
||||
"entity_id": entity_id,
|
||||
});
|
||||
|
||||
if let Some(state) = states.iter().find(|s| s.entity_id == entity_id) {
|
||||
if let Some(brightness) = state.attributes.get("brightness") {
|
||||
payload["brightness"] = brightness.clone();
|
||||
}
|
||||
|
||||
let color_mode = state
|
||||
.attributes
|
||||
.get("color_mode")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match color_mode {
|
||||
"rgbw" => {
|
||||
let w = state
|
||||
.attributes
|
||||
.get("rgbw_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbw_color"] = json!([r, g, b, w]);
|
||||
}
|
||||
"rgbww" => {
|
||||
let cw = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
let ww = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(4))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
|
||||
}
|
||||
_ => {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
}
|
||||
|
||||
self.client
|
||||
.post(&url)
|
||||
.bearer_auth(&self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_lights_color_parallel(
|
||||
&self,
|
||||
entity_ids: &[String],
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
transition: f32,
|
||||
) -> Result<(), Error> {
|
||||
let states = self.get_lights().await.unwrap_or_default();
|
||||
let url = format!("{}/api/services/light/turn_on", self.base_url);
|
||||
|
||||
for entity_id in entity_ids {
|
||||
let mut payload = json!({
|
||||
"entity_id": entity_id,
|
||||
"transition": transition,
|
||||
});
|
||||
let futures: Vec<_> = entity_ids
|
||||
.iter()
|
||||
.map(|entity_id| self.set_light_color(entity_id, r, g, b, &states))
|
||||
.collect();
|
||||
|
||||
if let Some(state) = states.iter().find(|s| s.entity_id == *entity_id) {
|
||||
if let Some(brightness) = state.attributes.get("brightness") {
|
||||
payload["brightness"] = brightness.clone();
|
||||
}
|
||||
let results = futures::future::join_all(futures).await;
|
||||
|
||||
let color_mode = state
|
||||
.attributes
|
||||
.get("color_mode")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
match color_mode {
|
||||
"rgbw" => {
|
||||
let w = state
|
||||
.attributes
|
||||
.get("rgbw_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbw_color"] = json!([r, g, b, w]);
|
||||
}
|
||||
"rgbww" => {
|
||||
let cw = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(3))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
let ww = state
|
||||
.attributes
|
||||
.get("rgbww_color")
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|a| a.get(4))
|
||||
.cloned()
|
||||
.unwrap_or(json!(0));
|
||||
payload["rgbww_color"] = json!([r, g, b, cw, ww]);
|
||||
}
|
||||
_ => {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload["rgb_color"] = json!([r, g, b]);
|
||||
for result in results {
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed to update light: {}", e);
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.post(&url)
|
||||
.bearer_auth(&self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
39
src/main.rs
39
src/main.rs
@@ -2,6 +2,7 @@ mod eventloop;
|
||||
mod homeassistant;
|
||||
mod screenshot;
|
||||
mod settings;
|
||||
mod state;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -48,32 +49,34 @@ fn main() {
|
||||
let is_running = Arc::new(AtomicBool::new(true));
|
||||
let (exit_tx, exit_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
// Leak variables to give them a 'static lifetime for the thread
|
||||
let settings_ref = &*Box::leak(Box::new(settings));
|
||||
let ha_client_ref = &*Box::leak(Box::new(ha_client));
|
||||
let target_lights_ref = &*Box::leak(Box::new(target_lights));
|
||||
let settings_arc = Arc::new(settings);
|
||||
let ha_client_arc = Arc::new(ha_client);
|
||||
let target_lights_arc = Arc::new(target_lights);
|
||||
let state_arc = Arc::new(state::AppState::new());
|
||||
|
||||
let is_running_clone = is_running.clone();
|
||||
let settings_arc_clone = settings_arc.clone();
|
||||
let ha_client_arc_clone = ha_client_arc.clone();
|
||||
let target_lights_arc_clone = target_lights_arc.clone();
|
||||
|
||||
// Spawn the main application event loop on a separate thread
|
||||
let app_thread = std::thread::spawn(move || {
|
||||
let rt2 = tokio::runtime::Runtime::new().unwrap();
|
||||
rt2.block_on(eventloop::run_loop(
|
||||
settings_ref,
|
||||
ha_client_ref,
|
||||
target_lights_ref,
|
||||
eventloop::run_loop(
|
||||
settings_arc_clone,
|
||||
ha_client_arc_clone,
|
||||
target_lights_arc_clone,
|
||||
state_arc,
|
||||
is_running_clone,
|
||||
exit_rx,
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
// Create the tray application
|
||||
let mut tray = TrayItem::new("Ambiligth", IconSource::Resource("")).unwrap();
|
||||
|
||||
let is_running_toggle = is_running.clone();
|
||||
let restore_on_toggle = settings_ref.restore_on_exit;
|
||||
let ha_client_toggle = ha_client_ref;
|
||||
let target_lights_toggle = target_lights_ref;
|
||||
let restore_on_toggle = settings_arc.restore_on_exit;
|
||||
let ha_client_toggle = ha_client_arc.clone();
|
||||
let target_lights_toggle = target_lights_arc.clone();
|
||||
let rt_handle_toggle = rt_handle.clone();
|
||||
|
||||
tray.add_menu_item("Toggle Ambilight", move || {
|
||||
@@ -98,7 +101,7 @@ fn main() {
|
||||
println!("Creating scene snapshot for restore on exit...");
|
||||
if let Err(e) = rt_handle_toggle.block_on(
|
||||
ha_client_toggle
|
||||
.create_scene_snapshot("ambilight_restore", target_lights_toggle),
|
||||
.create_scene_snapshot("ambilight_restore", &*target_lights_toggle),
|
||||
) {
|
||||
eprintln!("Failed to create scene snapshot: {}", e);
|
||||
}
|
||||
@@ -121,16 +124,14 @@ fn main() {
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
// Block the main thread until a quit signal is received
|
||||
let _ = sync_exit_rx.recv();
|
||||
println!("Shutting down...");
|
||||
|
||||
// Stop the event loop
|
||||
let _ = exit_tx.try_send(());
|
||||
|
||||
if settings_ref.restore_on_exit {
|
||||
if settings_arc.restore_on_exit {
|
||||
println!("Restoring lights to original state...");
|
||||
if let Err(e) = rt.block_on(ha_client_ref.turn_on_scene("scene.ambilight_restore")) {
|
||||
if let Err(e) = rt.block_on(ha_client_arc.turn_on_scene("scene.ambilight_restore")) {
|
||||
eprintln!("Failed to restore scene: {}", e);
|
||||
} else {
|
||||
println!("Lights restored successfully.");
|
||||
|
||||
@@ -12,6 +12,9 @@ pub struct Settings {
|
||||
#[serde(default = "default_target_fps")]
|
||||
pub target_fps: u32,
|
||||
|
||||
#[serde(default = "default_screenshot_fps")]
|
||||
pub screenshot_fps: u32,
|
||||
|
||||
#[serde(default = "default_smoothing")]
|
||||
pub smoothing: f32,
|
||||
|
||||
@@ -21,9 +24,6 @@ pub struct Settings {
|
||||
#[serde(default = "default_restore_on_exit")]
|
||||
pub restore_on_exit: bool,
|
||||
|
||||
#[serde(default = "default_transition")]
|
||||
pub transition: f32,
|
||||
|
||||
#[serde(default = "default_min_diff_percent")]
|
||||
pub min_diff_percent: f32,
|
||||
}
|
||||
@@ -32,10 +32,6 @@ fn default_min_diff_percent() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn default_transition() -> f32 {
|
||||
0.5
|
||||
}
|
||||
|
||||
fn default_restore_on_exit() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -44,6 +40,10 @@ fn default_target_fps() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_screenshot_fps() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_smoothing() -> f32 {
|
||||
0.0
|
||||
}
|
||||
|
||||
27
src/state.rs
Normal file
27
src/state.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::screenshot::Screenshot;
|
||||
use rgb::RGB;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub struct AppState {
|
||||
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
||||
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
|
||||
pub target_color: RwLock<Option<RGB<f32>>>,
|
||||
pub active_color: RwLock<RGB<f32>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_screenshot: RwLock::new(None),
|
||||
active_screenshot: RwLock::new(None),
|
||||
target_color: RwLock::new(None),
|
||||
active_color: RwLock::new(RGB::new(0.0, 0.0, 0.0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user