feat: 支持预览灯条排序效果。

This commit is contained in:
Ivan Li 2023-04-01 10:42:46 +08:00
parent 958a422672
commit 4e75aa4307
15 changed files with 820 additions and 84 deletions

114
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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<LedStripConfig>,
pub strips: Vec<LedStripConfig>,
pub mappers: Vec<SamplePointMapper>,
}
impl LedStripConfig {
pub async fn global() -> &'static Vec<LedStripConfig> {
static LED_STRIP_CONFIGS_GLOBAL: OnceCell<Vec<LedStripConfig>> = OnceCell::const_new();
LED_STRIP_CONFIGS_GLOBAL
.get_or_init(|| async { Self::read_config().await.unwrap() })
.await
}
pub async fn read_config() -> anyhow::Result<Vec<Self>> {
impl LedStripConfigGroup {
pub async fn read_config() -> anyhow::Result<Self> {
// 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<Self>) -> 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<Vec<Self>> {
pub async fn get_default_config() -> anyhow::Result<Self> {
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<LedSamplePoints>,
}

View File

@ -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<RwLock<Vec<LedStripConfig>>>,
config_update_receiver: tokio::sync::watch::Receiver<Vec<LedStripConfig>>,
config_update_sender: tokio::sync::watch::Sender<Vec<LedStripConfig>>,
config: Arc<RwLock<LedStripConfigGroup>>,
config_update_receiver: tokio::sync::watch::Receiver<LedStripConfigGroup>,
config_update_sender: tokio::sync::watch::Sender<LedStripConfigGroup>,
}
impl ConfigManager {
@ -18,11 +18,11 @@ impl ConfigManager {
static CONFIG_MANAGER_GLOBAL: OnceCell<ConfigManager> = 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<LedStripConfig>) -> 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<LedStripConfig> {
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<config::LedStripConfig>) -> 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<Vec<LedStripConfig>> {
) -> tokio::sync::watch::Receiver<LedStripConfigGroup> {
self.config_update_receiver.clone()
}
}

View File

@ -1,5 +1,7 @@
mod config;
mod config_manager;
mod publisher;
pub use config::*;
pub use config_manager::*;
pub use publisher::*;

View File

@ -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<RwLock<watch::Receiver<Vec<u8>>>>,
tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
}
impl LedColorsPublisher {
pub async fn global() -> &'static Self {
static LED_COLORS_PUBLISHER_GLOBAL: tokio::sync::OnceCell<LedColorsPublisher> =
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<u8>) -> anyhow::Result<()> {
let mqtt = MqttRpc::global().await;
mqtt.publish_led_sub_pixels(payload).await
}
pub async fn clone_receiver(&self) -> watch::Receiver<Vec<u8>> {
self.rx.read().await.clone()
}
}

View File

@ -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<Vec<ambient_light::LedStripConfig>, String> {
let configs = ambient_light::LedStripConfig::read_config()
async fn read_led_strip_configs() -> Result<LedStripConfigGroup, String> {
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<ambient_light::LedStripConfig>,
) -> 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<ambient_light::SamplePointConfig>,
mappers: Vec<ambient_light::SamplePointMapper>,
) -> Result<Vec<u8>, 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<u8>) -> 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!())

3
src-tauri/src/rpc/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod mqtt;
pub use mqtt::*;

241
src-tauri/src/rpc/mqtt.rs Normal file
View File

@ -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<display::DisplayBrightness>,
// message_tx: broadcast::Sender<models::CmdMqMessage>,
}
impl MqttRpc {
pub async fn global() -> &'static Self {
static MQTT_RPC: OnceCell<MqttRpc> = 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<Self> {
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::<display::DisplayBrightness>(16);
// let (message_tx, _) = broadcast::channel::<models::CmdMqMessage>(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<display::DisplayBrightness, _> =
// 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<models::CmdMqMessage, _> =
// 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<u8>) -> 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<display::DisplayBrightness> {
// self.change_display_brightness_tx.subscribe()
// }
pub async fn publish_desktop_cmd(&self, field: &str, payload: Vec<u8>) -> 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))
}
}

View File

@ -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<LedSamplePoints> {
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<Point> {
@ -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<Point> {
@ -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<Point> {
@ -261,6 +263,12 @@ impl Screenshot {
}
colors
}
pub async fn get_colors_by_sample_points(&self, points: &Vec<LedSamplePoints>) -> Vec<LedColor> {
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<Point>;

View File

@ -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<Screenshot> {
log::debug!("take_screenshot");
@ -39,7 +43,12 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
bytes_per_row,
bytes,
scale_factor,
ScreenSamplePoints { top: vec![], bottom: vec![], left: vec![], right: vec![] }
ScreenSamplePoints {
top: vec![],
bottom: vec![],
left: vec![],
right: vec![],
},
))
}
@ -205,4 +214,59 @@ impl ScreenshotManager {
}
}
pub async fn get_all_colors(
&self,
configs: &Vec<SamplePointConfig>,
mappers: &Vec<SamplePointMapper>,
channels: &HashMap<u32, watch::Receiver<Screenshot>>,
) -> Vec<u8> {
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
}
}

View File

@ -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<LedStripConfig[]>('read_led_strip_configs').then((strips) => {
setLedStripStore({
strips,
});
invoke<LedStripConfigContainer>('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<Uint8ClampedArray>('led_colors_changed', (event) => {
const colors = event.payload;
setLedStripStore({
colors,
});
});
@ -37,6 +54,7 @@ function App() {
return (
<div>
<LedStripPartsSorter />
<DisplayListContainer>
{displayStore.displays.map((display) => {
return <DisplayView display={display} />;

View File

@ -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<string[]>([]);
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 (
<div class="flex h-2 m-2">
<For each={fullLeds()}>
{(it) => (
<div
class="flex-auto flex h-full w-full justify-center items-center relative"
title={it}
>
<div
class="absolute top-1/2 -translate-y-1/2 h-2.5 w-2.5 rounded-full ring-1 ring-stone-300"
style={{ background: it }}
/>
</div>
)}
</For>
</div>
);
};
export const LedStripPartsSorter: Component = () => {
const context = createContext();
return (
<div>
<For each={ledStripStore.strips}>
{(strip, index) => (
<SorterItem strip={strip} mapper={ledStripStore.mappers[index()]} />
)}
</For>
</div>
);
};

View File

@ -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,
) {}
}

View File

@ -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<DisplayConfig>(),
strips: new Array<LedStripConfig>(),
mappers: new Array<LedStripPixelMapper>(),
colors: new Uint8ClampedArray(),
});