From 4e75aa4307f588a331773f3dcba8f7abdf382c21 Mon Sep 17 00:00:00 2001 From: Ivan Li Date: Sat, 1 Apr 2023 10:42:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E7=81=AF=E6=9D=A1=E6=8E=92=E5=BA=8F=E6=95=88=E6=9E=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 114 +++++++++ src-tauri/Cargo.toml | 2 + src-tauri/src/ambient_light/config.rs | 70 +++-- src-tauri/src/ambient_light/config_manager.rs | 72 ++++-- src-tauri/src/ambient_light/mod.rs | 2 + src-tauri/src/ambient_light/publisher.rs | 105 ++++++++ src-tauri/src/main.rs | 79 ++++-- src-tauri/src/rpc/mod.rs | 3 + src-tauri/src/rpc/mqtt.rs | 241 ++++++++++++++++++ src-tauri/src/screenshot.rs | 22 +- src-tauri/src/screenshot_manager.rs | 68 ++++- src/App.tsx | 32 ++- src/components/led-strip-parts-sorter.tsx | 79 ++++++ src/models/led-strip-config.ts | 11 +- src/stores/led-strip.store.tsx | 4 +- 15 files changed, 820 insertions(+), 84 deletions(-) create mode 100644 src-tauri/src/ambient_light/publisher.rs create mode 100644 src-tauri/src/rpc/mod.rs create mode 100644 src-tauri/src/rpc/mqtt.rs create mode 100644 src/components/led-strip-parts-sorter.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4b497c7..bac3e48 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -38,6 +38,17 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "atk" version = "0.15.1" @@ -230,6 +241,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cmake" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +dependencies = [ + "cc", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -283,6 +303,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -583,6 +612,12 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "1.9.0" @@ -664,6 +699,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.27" @@ -671,6 +721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -707,21 +758,37 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" + [[package]] name = "futures-task" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1572,12 +1639,51 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-sys" +version = "0.9.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "paho-mqtt" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6a19171f5b405f350373e32b6c2c4b47c225afccc837c11d2e7e22ba1749c62" +dependencies = [ + "async-channel", + "crossbeam-channel", + "futures", + "futures-timer", + "libc", + "log", + "paho-mqtt-sys", + "thiserror", +] + +[[package]] +name = "paho-mqtt-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1782b5e75d712f951a2a4c7d3175a2ef37d93ddb3ad8656b37092f3f05464bc9" +dependencies = [ + "cmake", + "openssl-sys", +] + [[package]] name = "pango" version = "0.15.10" @@ -2653,12 +2759,14 @@ dependencies = [ "env_logger", "hex", "log", + "paho-mqtt", "paris", "percent-encoding", "serde", "serde_json", "tauri", "tauri-build", + "time", "tokio", "toml 0.7.3", "url-build-parse", @@ -2974,6 +3082,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.0.11" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 486088d..29f282c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,6 +28,8 @@ url-build-parse = "9.0.0" color_space = "0.5.3" hex = "0.4.3" toml = "0.7.3" +paho-mqtt = "0.12.1" +time = {version="0.3.20", features= ["formatting"] } [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/ambient_light/config.rs b/src-tauri/src/ambient_light/config.rs index 82d9b92..5d25d88 100644 --- a/src-tauri/src/ambient_light/config.rs +++ b/src-tauri/src/ambient_light/config.rs @@ -3,7 +3,10 @@ use std::env::current_dir; use paris::{error, info}; use serde::{Deserialize, Serialize}; use tauri::api::path::config_dir; -use tokio::sync::OnceCell; + +use crate::screenshot::{self, LedSamplePoints}; + +const CONFIG_FILE_NAME: &str = "cc.ivanli.ambient_light/led_strip_config.toml"; #[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] pub enum Border { @@ -32,23 +35,16 @@ pub struct LedStripConfig { #[derive(Clone, Serialize, Deserialize, Debug)] pub struct LedStripConfigGroup { - pub items: Vec, + pub strips: Vec, + pub mappers: Vec, } - -impl LedStripConfig { - pub async fn global() -> &'static Vec { - static LED_STRIP_CONFIGS_GLOBAL: OnceCell> = OnceCell::const_new(); - LED_STRIP_CONFIGS_GLOBAL - .get_or_init(|| async { Self::read_config().await.unwrap() }) - .await - } - - pub async fn read_config() -> anyhow::Result> { +impl LedStripConfigGroup { + pub async fn read_config() -> anyhow::Result { // config path let path = config_dir() .unwrap_or(current_dir().unwrap()) - .join("led_strip_config.toml"); + .join(CONFIG_FILE_NAME); let exists = tokio::fs::try_exists(path.clone()) .await @@ -60,42 +56,44 @@ impl LedStripConfig { let config: LedStripConfigGroup = toml::from_str(&config) .map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?; - Ok(config.items) + Ok(config) } else { info!("config file not exist, fallback to default config"); Ok(Self::get_default_config().await?) } } - pub async fn write_config(configs: &Vec) -> anyhow::Result<()> { + pub async fn write_config(configs: &Self) -> anyhow::Result<()> { let path = config_dir() .unwrap_or(current_dir().unwrap()) - .join("led_strip_config.toml"); + .join(CONFIG_FILE_NAME); - let configs = LedStripConfigGroup { items: configs.clone() }; + tokio::fs::create_dir_all(path.parent().unwrap()).await?; - let config = toml::to_string(&configs).map_err(|e| { + let config_text = toml::to_string(&configs).map_err(|e| { anyhow::anyhow!("Failed to parse config file: {}. configs: {:?}", e, configs) })?; - tokio::fs::write(&path, config).await.map_err(|e| { + tokio::fs::write (&path, config_text).await.map_err(|e| { anyhow::anyhow!("Failed to write config file: {}. path: {:?}", e, &path) })?; Ok(()) } - pub async fn get_default_config() -> anyhow::Result> { + pub async fn get_default_config() -> anyhow::Result { let displays = display_info::DisplayInfo::all().map_err(|e| { error!("can not list display info: {}", e); anyhow::anyhow!("can not list display info: {}", e) })?; - let mut configs = Vec::new(); + let mut strips = Vec::new(); + let mut mappers = Vec::new(); for (i, display) in displays.iter().enumerate() { + let mut configs = Vec::new(); for j in 0..4 { - let config = Self { - index: j + i * 4 * 30, + let item = LedStripConfig { + index: j + i * 4, display_id: display.id, border: match j { 0 => Border::Top, @@ -104,14 +102,18 @@ impl LedStripConfig { 3 => Border::Right, _ => unreachable!(), }, - start_pos: 0, + start_pos: j + i * 4 * 30, len: 30, }; - configs.push(config); + configs.push(item); + strips.push(item); + mappers.push(SamplePointMapper { + start: (j + i * 4) * 30, + end: (j + i * 4 + 1) * 30, + }) } } - - Ok(configs) + Ok(Self { strips, mappers }) } } @@ -220,7 +222,7 @@ impl LedStripConfigOfDisplays { start_pos: i * 4 * 30 + 90, len: 30, }), - } + }, }; configs.push(config); } @@ -228,3 +230,15 @@ impl LedStripConfigOfDisplays { Ok(configs[0]) } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SamplePointMapper { + pub start: usize, + pub end: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SamplePointConfig { + pub display_id: u32, + pub points: Vec, +} diff --git a/src-tauri/src/ambient_light/config_manager.rs b/src-tauri/src/ambient_light/config_manager.rs index 3b92283..bcde3ec 100644 --- a/src-tauri/src/ambient_light/config_manager.rs +++ b/src-tauri/src/ambient_light/config_manager.rs @@ -1,16 +1,16 @@ -use std::{sync::Arc, borrow::Borrow}; +use std::sync::Arc; use tauri::async_runtime::RwLock; use tokio::sync::OnceCell; -use crate::ambient_light::{config, LedStripConfig}; +use crate::ambient_light::{config, LedStripConfigGroup}; use super::Border; pub struct ConfigManager { - configs: Arc>>, - config_update_receiver: tokio::sync::watch::Receiver>, - config_update_sender: tokio::sync::watch::Sender>, + config: Arc>, + config_update_receiver: tokio::sync::watch::Receiver, + config_update_sender: tokio::sync::watch::Sender, } impl ConfigManager { @@ -18,11 +18,11 @@ impl ConfigManager { static CONFIG_MANAGER_GLOBAL: OnceCell = OnceCell::const_new(); CONFIG_MANAGER_GLOBAL .get_or_init(|| async { - let configs = LedStripConfig::read_config().await.unwrap(); + let configs = LedStripConfigGroup::read_config().await.unwrap(); let (config_update_sender, config_update_receiver) = tokio::sync::watch::channel(configs.clone()); ConfigManager { - configs: Arc::new(RwLock::new(configs)), + config: Arc::new(RwLock::new(configs)), config_update_receiver, config_update_sender, } @@ -31,45 +31,71 @@ impl ConfigManager { } pub async fn reload(&self) -> anyhow::Result<()> { - let mut configs = self.configs.write().await; - *configs = LedStripConfig::read_config().await?; + let mut configs = self.config.write().await; + *configs = LedStripConfigGroup::read_config().await?; Ok(()) } - pub async fn update(&self, configs: &Vec) -> anyhow::Result<()> { - LedStripConfig::write_config(configs).await?; + pub async fn update(&self, configs: &LedStripConfigGroup) -> anyhow::Result<()> { + LedStripConfigGroup::write_config(configs).await?; self.reload().await?; - self.config_update_sender.send(configs.clone()).map_err(|e| { - anyhow::anyhow!("Failed to send config update: {}", e) - })?; + self.config_update_sender + .send(configs.clone()) + .map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?; log::info!("config updated: {:?}", configs); Ok(()) } - pub async fn configs(&self) -> Vec { - self.configs.read().await.clone() + pub async fn configs(&self) -> LedStripConfigGroup { + self.config.read().await.clone() } - pub async fn patch_led_strip_len(&self, display_id: u32, border: Border, delta_len: i8) -> anyhow::Result<()> { - let mut configs = self.configs.write().await; + pub async fn patch_led_strip_len( + &self, + display_id: u32, + border: Border, + delta_len: i8, + ) -> anyhow::Result<()> { + let mut config = self.config.write().await; - for config in configs.iter_mut() { + for config in config.strips.iter_mut() { if config.display_id == display_id && config.border == border { let target = config.len as i64 + delta_len as i64; if target < 0 || target > 1000 { - return Err(anyhow::anyhow!("Overflow. range: 0-1000, current: {}", target)); + return Err(anyhow::anyhow!( + "Overflow. range: 0-1000, current: {}", + target + )); } config.len = target as usize; } } - let cloned_config = configs.clone(); + let cloned_config = config.clone(); - drop(configs); + drop(config); + + self.update(&cloned_config).await?; + + self.config_update_sender + .send(cloned_config) + .map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?; + + Ok(()) + } + + pub async fn set_items(&self, items: Vec) -> anyhow::Result<()> { + let mut config = self.config.write().await; + + config.strips = items; + + let cloned_config = config.clone(); + + drop(config); self.update(&cloned_config).await?; @@ -82,7 +108,7 @@ impl ConfigManager { pub fn clone_config_update_receiver( &self, - ) -> tokio::sync::watch::Receiver> { + ) -> tokio::sync::watch::Receiver { self.config_update_receiver.clone() } } diff --git a/src-tauri/src/ambient_light/mod.rs b/src-tauri/src/ambient_light/mod.rs index 62abcb2..c9e0e88 100644 --- a/src-tauri/src/ambient_light/mod.rs +++ b/src-tauri/src/ambient_light/mod.rs @@ -1,5 +1,7 @@ mod config; mod config_manager; +mod publisher; pub use config::*; pub use config_manager::*; +pub use publisher::*; diff --git a/src-tauri/src/ambient_light/publisher.rs b/src-tauri/src/ambient_light/publisher.rs new file mode 100644 index 0000000..0597208 --- /dev/null +++ b/src-tauri/src/ambient_light/publisher.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use paris::warn; +use tauri::async_runtime::RwLock; +use tokio::sync::watch; + +use crate::{ + ambient_light::{config, ConfigManager}, + rpc::MqttRpc, + screenshot_manager::ScreenshotManager, +}; + +pub struct LedColorsPublisher { + rx: Arc>>>, + tx: Arc>>>, +} + +impl LedColorsPublisher { + pub async fn global() -> &'static Self { + static LED_COLORS_PUBLISHER_GLOBAL: tokio::sync::OnceCell = + tokio::sync::OnceCell::const_new(); + + let (tx, rx) = watch::channel(Vec::new()); + + LED_COLORS_PUBLISHER_GLOBAL + .get_or_init(|| async { + LedColorsPublisher { + rx: Arc::new(RwLock::new(rx)), + tx: Arc::new(RwLock::new(tx)), + } + }) + .await + } + + pub fn start(&self) -> anyhow::Result<()> { + let tx = self.tx.clone(); + + tokio::spawn(async move { + let tx = tx.write().await; + + let screenshot_manager = ScreenshotManager::global().await; + let config_manager = ConfigManager::global().await; + + loop { + let configs = config_manager.configs().await; + let channels = screenshot_manager.channels.read().await; + + let mut colors_configs = Vec::new(); + + for (display_id, rx) in channels.iter() { + let led_strip_configs: Vec<_> = configs + .strips + .iter() + .filter(|c| c.display_id == *display_id) + .collect(); + + if led_strip_configs.len() == 0 { + warn!("no led strip config for display_id: {}", display_id); + continue; + } + + let mut rx = rx.clone(); + + if rx.changed().await.is_ok() { + let screenshot = rx.borrow().clone(); + // log::info!("screenshot updated: {:?}", display_id); + + let points: Vec<_> = led_strip_configs + .iter() + .map(|config| screenshot.get_sample_points(&config)) + .flatten() + .collect(); + + let colors_config = config::SamplePointConfig { + display_id: *display_id, + points, + }; + + colors_configs.push(colors_config); + } + } + let colors = screenshot_manager.get_all_colors(&colors_configs, &configs.mappers, &channels).await; + match tx.send(colors) { + Ok(_) => { + // log::info!("colors updated"); + } + Err(_) => { + warn!("colors update failed"); + } + } + } + }); + Ok(()) + } + + pub async fn send_colors(payload: Vec) -> anyhow::Result<()> { + let mqtt = MqttRpc::global().await; + + mqtt.publish_led_sub_pixels(payload).await + } + + pub async fn clone_receiver(&self) -> watch::Receiver> { + self.rx.read().await.clone() + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 83220d1..06f2ecc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,10 +4,11 @@ mod ambient_light; mod display; mod led_color; +mod rpc; pub mod screenshot; mod screenshot_manager; -use ambient_light::{Border, LedStripConfig}; +use ambient_light::{Border, LedColorsPublisher, LedStripConfig, LedStripConfigGroup}; use core_graphics::display::{ kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay, }; @@ -72,26 +73,26 @@ async fn subscribe_encoded_screenshot_updated( } #[tauri::command] -async fn read_led_strip_configs() -> Result, String> { - let configs = ambient_light::LedStripConfig::read_config() +async fn read_led_strip_configs() -> Result { + let config = ambient_light::LedStripConfigGroup::read_config() .await .map_err(|e| { error!("can not read led strip configs: {}", e); e.to_string() })?; - Ok(configs) + Ok(config) } #[tauri::command] async fn write_led_strip_configs( configs: Vec, ) -> Result<(), String> { - ambient_light::LedStripConfig::write_config(&configs) - .await - .map_err(|e| { - error!("can not write led strip configs: {}", e); - e.to_string() - }) + let config_manager = ambient_light::ConfigManager::global().await; + + config_manager.set_items(configs).await.map_err(|e| { + error!("can not write led strip configs: {}", e); + e.to_string() + }) } #[tauri::command] @@ -103,9 +104,7 @@ async fn get_led_strips_sample_points( if let Some(rx) = channels.get(&config.display_id) { let rx = rx.clone(); let screenshot = rx.borrow().clone(); - let width = screenshot.width; - let height = screenshot.height; - let sample_points = Screenshot::get_sample_point(&config, width as usize, height as usize); + let sample_points = screenshot.get_sample_points(&config); Ok(sample_points) } else { return Err(format!("display not found: {}", config.display_id)); @@ -131,9 +130,27 @@ async fn get_one_edge_colors( } } +#[tauri::command] +async fn get_all_colors( + configs: Vec, + mappers: Vec, +) -> Result, String> { + let screenshot_manager = ScreenshotManager::global().await; + + let channels = screenshot_manager.channels.to_owned(); + let channels = channels.read().await; + + Ok(screenshot_manager + .get_all_colors(&configs, &mappers, &channels) + .await) +} + #[tauri::command] async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) -> Result<(), String> { - info!("patch_led_strip_len: {} {:?} {}", display_id, border, delta_len); + info!( + "patch_led_strip_len: {} {:?} {}", + display_id, border, delta_len + ); let config_manager = ambient_light::ConfigManager::global().await; config_manager .patch_led_strip_len(display_id, border, delta_len) @@ -147,12 +164,26 @@ async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) -> Ok(()) } +#[tauri::command] +async fn send_colors(buffer: Vec) -> Result<(), String> { + ambient_light::LedColorsPublisher::send_colors(buffer) + .await + .map_err(|e| { + error!("can not send colors: {}", e); + e.to_string() + }) +} + #[tokio::main] async fn main() { env_logger::init(); let screenshot_manager = ScreenshotManager::global().await; screenshot_manager.start().unwrap(); + + let led_color_publisher = ambient_light::LedColorsPublisher::global().await; + led_color_publisher.start().unwrap(); + tauri::Builder::default() .invoke_handler(tauri::generate_handler![ greet, @@ -162,7 +193,9 @@ async fn main() { write_led_strip_configs, get_led_strips_sample_points, get_one_edge_colors, - patch_led_strip_len + patch_led_strip_len, + send_colors, + get_all_colors ]) .register_uri_scheme_protocol("ambient-light", move |_app, request| { let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*"); @@ -311,6 +344,22 @@ async fn main() { } }); + let app_handle = app.handle().clone(); + tokio::spawn(async move { + let publisher = ambient_light::LedColorsPublisher::global().await; + let mut publisher_update_receiver = publisher.clone_receiver().await; + loop { + if let Err(err) = publisher_update_receiver.changed().await { + error!("publisher update receiver changed error: {}", err); + return; + } + + let publisher = publisher_update_receiver.borrow().clone(); + + app_handle.emit_all("led_colors_changed", publisher).unwrap(); + } + }); + Ok(()) }) .run(tauri::generate_context!()) diff --git a/src-tauri/src/rpc/mod.rs b/src-tauri/src/rpc/mod.rs new file mode 100644 index 0000000..c9feb8d --- /dev/null +++ b/src-tauri/src/rpc/mod.rs @@ -0,0 +1,3 @@ +mod mqtt; + +pub use mqtt::*; \ No newline at end of file diff --git a/src-tauri/src/rpc/mqtt.rs b/src-tauri/src/rpc/mqtt.rs new file mode 100644 index 0000000..819e270 --- /dev/null +++ b/src-tauri/src/rpc/mqtt.rs @@ -0,0 +1,241 @@ +use paho_mqtt as mqtt; +use paris::{error, info, warn}; +use serde_json::json; +use std::time::Duration; +use time::{format_description, OffsetDateTime}; +use tokio::{sync::OnceCell, task}; + +const DISPLAY_TOPIC: &'static str = "display-ambient-light/display"; +const DESKTOP_TOPIC: &'static str = "display-ambient-light/desktop"; +const DISPLAY_BRIGHTNESS_TOPIC: &'static str = "display-ambient-light/board/brightness"; +const BOARD_SEND_CMD: &'static str = "display-ambient-light/board/cmd"; + +pub struct MqttRpc { + client: mqtt::AsyncClient, + // change_display_brightness_tx: broadcast::Sender, + // message_tx: broadcast::Sender, +} + +impl MqttRpc { + pub async fn global() -> &'static Self { + static MQTT_RPC: OnceCell = OnceCell::const_new(); + + MQTT_RPC + .get_or_init(|| async { + let mqtt_rpc = MqttRpc::new().await.unwrap(); + mqtt_rpc.initialize().await.unwrap(); + mqtt_rpc + }) + .await + } + + pub async fn new() -> anyhow::Result { + let client = mqtt::AsyncClient::new("tcp://192.168.31.11:1883") + .map_err(|err| anyhow::anyhow!("can not create MQTT client. {:?}", err))?; + + client.set_connected_callback(|client| { + info!("MQTT server connected."); + + client.subscribe("display-ambient-light/board/#", mqtt::QOS_1); + + client.subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1); + }); + client.set_connection_lost_callback(|client| { + info!("MQTT server connection lost."); + }); + client.set_disconnected_callback(|_, a1, a2| { + info!("MQTT server disconnected. {:?} {:?}", a1, a2); + }); + + let mut last_will_payload = serde_json::Map::new(); + last_will_payload.insert("message".to_string(), json!("offline")); + last_will_payload.insert( + "time".to_string(), + serde_json::Value::String( + OffsetDateTime::now_utc() + .format(&time::format_description::well_known::iso8601::Iso8601::DEFAULT) + .unwrap() + .to_string(), + ), + ); + + let last_will = mqtt::Message::new( + format!("{}/status", DESKTOP_TOPIC), + serde_json::to_string(&last_will_payload) + .unwrap() + .as_bytes(), + mqtt::QOS_1, + ); + + let connect_options = mqtt::ConnectOptionsBuilder::new() + .keep_alive_interval(Duration::from_secs(5)) + .will_message(last_will) + .automatic_reconnect(Duration::from_secs(1), Duration::from_secs(5)) + .finalize(); + + let token = client.connect(connect_options); + + token.await.map_err(|err| { + anyhow::anyhow!( + "can not connect MQTT server. wait for connect token failed. {:?}", + err + ) + })?; + + // let (change_display_brightness_tx, _) = + // broadcast::channel::(16); + // let (message_tx, _) = broadcast::channel::(32); + Ok(Self { client }) + } + + pub async fn listen(&self) { + // let change_display_brightness_tx2 = self.change_display_brightness_tx.clone(); + // let message_tx_cloned = self.message_tx.clone(); + + // let mut stream = self.client.to_owned().get_stream(100); + + // while let Some(notification) = stream.next().await { + // match notification { + // Some(notification) => match notification.topic() { + // DISPLAY_BRIGHTNESS_TOPIC => { + // let payload_text = String::from_utf8(notification.payload().to_vec()); + // match payload_text { + // Ok(payload_text) => { + // let display_brightness: Result = + // serde_json::from_str(payload_text.as_str()); + // match display_brightness { + // Ok(display_brightness) => { + // match change_display_brightness_tx2.send(display_brightness) + // { + // Ok(_) => {} + // Err(err) => { + // warn!( + // "can not send display brightness to channel. {:?}", + // err + // ); + // } + // } + // } + // Err(err) => { + // warn!( + // "can not parse display brightness from payload. {:?}", + // err + // ); + // } + // } + // } + // Err(err) => { + // warn!("can not parse display brightness from payload. {:?}", err); + // } + // } + // } + // BOARD_SEND_CMD => { + // let payload_text = String::from_utf8(notification.payload().to_vec()); + // match payload_text { + // Ok(payload_text) => { + // let message: Result = + // serde_json::from_str(payload_text.as_str()); + // match message { + // Ok(message) => match message_tx_cloned.send(message) { + // Ok(_) => {} + // Err(err) => { + // warn!("can not send message to channel. {:?}", err); + // } + // }, + // Err(err) => { + // warn!("can not parse message from payload. {:?}", err); + // } + // } + // } + // Err(err) => { + // warn!("can not parse message from payload. {:?}", err); + // } + // } + // } + // _ => {} + // }, + // _ => { + // warn!("can not get notification from MQTT server."); + // } + // } + // } + } + + pub async fn initialize(&self) -> anyhow::Result<()> { + // self.subscribe_board()?; + // self.subscribe_display()?; + self.broadcast_desktop_online(); + anyhow::Ok(()) + } + + fn subscribe_board(&self) -> anyhow::Result<()> { + self.client + .subscribe("display-ambient-light/board/#", mqtt::QOS_1) + .wait() + .map_err(|err| anyhow::anyhow!("subscribe board failed. {:?}", err)) + .map(|_| ()) + } + fn subscribe_display(&self) -> anyhow::Result<()> { + self.client + .subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1) + .wait() + .map_err(|err| anyhow::anyhow!("subscribe board failed. {:?}", err)) + .map(|_| ()) + } + + fn broadcast_desktop_online(&self) { + let client = self.client.to_owned(); + task::spawn(async move { + loop { + match OffsetDateTime::now_utc() + .format(&format_description::well_known::Iso8601::DEFAULT) + { + Ok(now_str) => { + let msg = mqtt::Message::new( + "display-ambient-light/desktop/online", + now_str.as_bytes(), + mqtt::QOS_0, + ); + match client.publish(msg).await { + Ok(_) => {} + Err(error) => { + warn!("can not publish last online time. {}", error) + } + } + } + Err(error) => { + warn!("can not get time for now. {}", error); + } + } + tokio::time::sleep(Duration::from_millis(1000)).await; + } + }); + } + + pub async fn publish_led_sub_pixels(&self, payload: Vec) -> anyhow::Result<()> { + self.client + .publish(mqtt::Message::new( + "display-ambient-light/desktop/colors", + payload, + mqtt::QOS_1, + )) + .await + .map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error)) + } + + // pub fn subscribe_change_display_brightness_rx( + // &self, + // ) -> broadcast::Receiver { + // self.change_display_brightness_tx.subscribe() + // } + pub async fn publish_desktop_cmd(&self, field: &str, payload: Vec) -> anyhow::Result<()> { + self.client + .publish(mqtt::Message::new( + format!("{}/{}", DESKTOP_TOPIC, field), + payload, + mqtt::QOS_1, + )) + .await + .map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error)) + } +} diff --git a/src-tauri/src/screenshot.rs b/src-tauri/src/screenshot.rs index 6b9ee5b..d1798eb 100644 --- a/src-tauri/src/screenshot.rs +++ b/src-tauri/src/screenshot.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use tauri::async_runtime::RwLock; use crate::{ - ambient_light::{LedStripConfigOfDisplays, LedStripConfig}, + ambient_light::{LedStripConfig, LedStripConfigOfDisplays}, led_color::LedColor, }; @@ -41,17 +41,19 @@ impl Screenshot { } } - pub fn get_sample_point( + pub fn get_sample_points( + &self, config: &LedStripConfig, - width: usize, - height: usize, ) -> Vec { + let height = self.height as usize; + let width = self.width as usize; + match config.border { crate::ambient_light::Border::Top => { Self::get_one_edge_sample_points(height / 8, width, config.len, 5) } crate::ambient_light::Border::Bottom => { - let points = Self::get_one_edge_sample_points(height / 9, width, config.len, 1); + let points = Self::get_one_edge_sample_points(height / 9, width, config.len, 5); points .into_iter() .map(|groups| -> Vec { @@ -60,7 +62,7 @@ impl Screenshot { .collect() } crate::ambient_light::Border::Left => { - let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 1); + let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 5); points .into_iter() .map(|groups| -> Vec { @@ -69,7 +71,7 @@ impl Screenshot { .collect() } crate::ambient_light::Border::Right => { - let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 1); + let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 5); points .into_iter() .map(|groups| -> Vec { @@ -261,6 +263,12 @@ impl Screenshot { } colors } + + pub async fn get_colors_by_sample_points(&self, points: &Vec) -> Vec { + let bytes = self.bytes.read().await; + + Self::get_one_edge_colors(points, &bytes, self.bytes_per_row) + } } type Point = (usize, usize); pub type LedSamplePoints = Vec; diff --git a/src-tauri/src/screenshot_manager.rs b/src-tauri/src/screenshot_manager.rs index 6e7a810..d21e054 100644 --- a/src-tauri/src/screenshot_manager.rs +++ b/src-tauri/src/screenshot_manager.rs @@ -4,10 +4,14 @@ use core_graphics::display::{ kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay, }; use paris::{error, info, warn}; +use serde::{Deserialize, Serialize}; use tauri::{async_runtime::RwLock, Window}; use tokio::sync::{watch, OnceCell}; -use crate::screenshot::{Screenshot, ScreenshotPayload, ScreenSamplePoints}; +use crate::{ + ambient_light::{SamplePointConfig, SamplePointMapper}, + screenshot::{LedSamplePoints, ScreenSamplePoints, Screenshot, ScreenshotPayload}, +}; pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result { log::debug!("take_screenshot"); @@ -39,7 +43,12 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result, + mappers: &Vec, + channels: &HashMap>, + ) -> Vec { + let total_leds = configs + .iter() + .fold(0, |acc, config| acc + config.points.len()); + + let mut global_colors = vec![0u8; total_leds * 3]; + let mut all_colors = vec![]; + for config in configs { + let rx = channels.get(&config.display_id); + if rx.is_none() { + error!( + "get_all_colors: can not find display_id {}", + config.display_id + ); + continue; + } + let rx = rx.unwrap(); + let screenshot = rx.borrow().clone(); + let mut colors = screenshot.get_colors_by_sample_points(&config.points).await; + + all_colors.append(&mut colors); + } + + let mut color_index = 0; + mappers.iter().for_each(|group| { + if group.end >= all_colors.len() || group.start >= all_colors.len() { + return; + } + if group.end > group.start { + for i in group.start..group.end - 1 { + let rgb = all_colors[color_index].get_rgb(); + color_index += 1; + + global_colors[i * 3] = rgb[0]; + global_colors[i * 3 + 1] = rgb[1]; + global_colors[i * 3 + 2] = rgb[2]; + } + } else { + for i in (group.end..group.start - 1).rev() { + let rgb = all_colors[color_index].get_rgb(); + color_index += 1; + + global_colors[i * 3] = rgb[0]; + global_colors[i * 3 + 1] = rgb[1]; + global_colors[i * 3 + 2] = rgb[2]; + } + } + }); + global_colors + } } diff --git a/src/App.tsx b/src/App.tsx index 3df9a6d..299120f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,10 @@ import { invoke } from '@tauri-apps/api/tauri'; import { DisplayView } from './components/display-view'; import { DisplayListContainer } from './components/display-list-container'; import { displayStore, setDisplayStore } from './stores/display.store'; -import { LedStripConfig } from './models/led-strip-config'; +import { LedStripConfigContainer } from './models/led-strip-config'; import { setLedStripStore } from './stores/led-strip.store'; import { listen } from '@tauri-apps/api/event'; +import { LedStripPartsSorter } from './components/led-strip-parts-sorter'; function App() { createEffect(() => { @@ -14,19 +15,35 @@ function App() { displays: JSON.parse(displays), }); }); - invoke('read_led_strip_configs').then((strips) => { - setLedStripStore({ - strips, - }); + invoke('read_led_strip_configs').then((configs) => { + console.log(configs); + setLedStripStore(configs); }); }); - // register tauri event listeners + // listen to config_changed event createEffect(() => { const unlisten = listen('config_changed', (event) => { - const strips = event.payload as LedStripConfig[]; + const { strips, mappers } = event.payload as LedStripConfigContainer; + console.log(event.payload); setLedStripStore({ strips, + mappers, + }); + }); + + onCleanup(() => { + unlisten.then((unlisten) => unlisten()); + }); + }); + + // listen to led_colors_changed event + createEffect(() => { + const unlisten = listen('led_colors_changed', (event) => { + const colors = event.payload; + + setLedStripStore({ + colors, }); }); @@ -37,6 +54,7 @@ function App() { return (
+ {displayStore.displays.map((display) => { return ; diff --git a/src/components/led-strip-parts-sorter.tsx b/src/components/led-strip-parts-sorter.tsx new file mode 100644 index 0000000..1ea2b0b --- /dev/null +++ b/src/components/led-strip-parts-sorter.tsx @@ -0,0 +1,79 @@ +import { + Component, + createContext, + createEffect, + createMemo, + createSignal, + For, + JSX, + onCleanup, +} from 'solid-js'; +import { LedStripConfig, LedStripPixelMapper } from '../models/led-strip-config'; +import { ledStripStore } from '../stores/led-strip.store'; + +const SorterItem: Component<{ mapper: LedStripPixelMapper; strip: LedStripConfig }> = ( + props, +) => { + const [fullLeds, setFullLeds] = createSignal([]); + + createEffect(() => { + let stopped = false; + const frame = () => { + const strips = ledStripStore.strips; + const totalLedCount = strips.reduce((acc, strip) => acc + strip.len, 0); + + const fullLeds = new Array(totalLedCount).fill('rgba(255,255,255,0.5)'); + + for (let i = props.mapper.start, j = 0; i < props.mapper.end; i++, j++) { + fullLeds[i] = `rgb(${ledStripStore.colors[i * 3]}, ${ + ledStripStore.colors[i * 3 + 1] + }, ${ledStripStore.colors[i * 3 + 2]})`; + } + + setFullLeds(fullLeds); + + if (!stopped) { + requestAnimationFrame(frame); + } + }; + + frame(); + + onCleanup(() => { + stopped = true; + console.timeEnd('frame'); + }); + }); + + return ( +
+ + {(it) => ( +
+
+
+ )} + +
+ ); +}; + +export const LedStripPartsSorter: Component = () => { + const context = createContext(); + + return ( +
+ + {(strip, index) => ( + + )} + +
+ ); +}; diff --git a/src/models/led-strip-config.ts b/src/models/led-strip-config.ts index 6a8447f..1bdd25a 100644 --- a/src/models/led-strip-config.ts +++ b/src/models/led-strip-config.ts @@ -1,10 +1,19 @@ import { Borders } from '../constants/border'; +export type LedStripPixelMapper = { + start: number; + end: number; +}; + +export type LedStripConfigContainer = { + strips: LedStripConfig[]; + mappers: LedStripPixelMapper[]; +}; + export class LedStripConfig { constructor( public readonly display_id: number, public readonly border: Borders, - public start_pos: number, public len: number, ) {} } diff --git a/src/stores/led-strip.store.tsx b/src/stores/led-strip.store.tsx index fb886d3..b5c69b1 100644 --- a/src/stores/led-strip.store.tsx +++ b/src/stores/led-strip.store.tsx @@ -1,8 +1,10 @@ import { createStore } from 'solid-js/store'; import { DisplayConfig } from '../models/display-config'; -import { LedStripConfig } from '../models/led-strip-config'; +import { LedStripConfig, LedStripPixelMapper } from '../models/led-strip-config'; export const [ledStripStore, setLedStripStore] = createStore({ displays: new Array(), strips: new Array(), + mappers: new Array(), + colors: new Uint8ClampedArray(), });