18 Commits

Author SHA1 Message Date
6c3ce607e0 pref: 针对 HiDPI 屏幕捕获的优化。 2023-04-05 12:25:14 +08:00
3ec983cd95 boardcast + interval tick. 2023-04-03 23:19:45 +08:00
a1e6c6e5fb fix: 灯条段长度为0时,崩溃问题。 2023-04-02 18:36:22 +08:00
822d470605 feat: 支持翻转灯条段。 2023-04-02 17:45:18 +08:00
fa5e27f72a feat(GUI): 灯条段拖拽排序功能完善,支持连续拖拽。 2023-04-02 17:21:58 +08:00
86e9b072bc feat: 灯条段排序。 2023-04-02 16:08:33 +08:00
535f731770 feat: 后端发布未映射排序的色彩。 2023-04-02 14:52:08 +08:00
9ec030488a feat: 发布灯条颜色到 MQTT。 2023-04-02 14:05:53 +08:00
0d47911355 feat: 拖拽排序。 2023-04-01 23:32:31 +08:00
5893c4344c feat(gui): 全局的灯条颜色预览。 2023-04-01 18:49:42 +08:00
d053185cc2 chore: 代码清理。 2023-04-01 18:43:49 +08:00
47e30ec94a fix: 全局灯条颜色发布协程未能等待下一个截图完成,导致性能缺陷。 2023-04-01 18:39:51 +08:00
56137b52a5 fix(ambient_light): 全局颜色中屏幕顺序问题。 2023-04-01 15:39:21 +08:00
85a00cf4f2 pref(gui): 渲染性能。 2023-04-01 13:43:02 +08:00
4e75aa4307 feat: 支持预览灯条排序效果。 2023-04-01 10:42:46 +08:00
958a422672 feat(GUI): 指针悬浮时,使用 tooltip 显示灯珠数。 2023-03-26 23:33:38 +08:00
58e8c30fe2 feat(gui, ambient_light): 鼠标滚轮修改 LED 灯条的灯珠数。 2023-03-26 22:39:47 +08:00
3e54d30498 fix: 缩放的屏幕,灯条颜色预览效果错误 2023-03-26 15:54:57 +08:00
32 changed files with 2281 additions and 682 deletions

35
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Tauri Development Debug",
"cargo": {
"args": [
"build",
"--manifest-path=./src-tauri/Cargo.toml",
"--no-default-features"
]
},
// task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json`
"preLaunchTask": "ui:dev"
},
{
"type": "lldb",
"request": "launch",
"name": "Tauri Production Debug",
"cargo": {
"args": [
"build",
"--release",
"--manifest-path=./src-tauri/Cargo.toml"
]
},
// task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json`
"preLaunchTask": "ui:build"
}
]

View File

@@ -1,3 +1,7 @@
{
"files.autoSave": "onWindowChange"
"files.autoSave": "onWindowChange",
"cSpell.words": [
"Itertools",
"Leds"
]
}

29
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "ui:dev",
"type": "shell",
// `dev` keeps running in the background
// ideally you should also configure a `problemMatcher`
// see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson
"isBackground": true,
// change this to your `beforeDevCommand`:
"command": "yarn",
"args": [
"dev"
]
},
{
"label": "ui:build",
"type": "shell",
// change this to your `beforeBuildCommand`:
"command": "yarn",
"args": [
"build"
]
}
]
}

View File

@@ -12,7 +12,9 @@
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^1.2.0",
"solid-js": "^1.4.7"
"solid-js": "^1.4.7",
"solid-tippy": "^0.2.1",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",

25
pnpm-lock.yaml generated
View File

@@ -7,7 +7,9 @@ specifiers:
autoprefixer: ^10.4.14
postcss: ^8.4.21
solid-js: ^1.4.7
solid-tippy: ^0.2.1
tailwindcss: ^3.2.7
tippy.js: ^6.3.7
typescript: ^4.7.4
vite: ^4.0.0
vite-plugin-solid: ^2.3.0
@@ -15,6 +17,8 @@ specifiers:
dependencies:
'@tauri-apps/api': 1.2.0
solid-js: 1.6.14
solid-tippy: 0.2.1_rick4tq22pxy45qudhdfrizose
tippy.js: 6.3.7
devDependencies:
'@tauri-apps/cli': 1.2.3
@@ -603,6 +607,10 @@ packages:
fastq: 1.15.0
dev: true
/@popperjs/core/2.11.7:
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
dev: false
/@tauri-apps/api/1.2.0:
resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
@@ -1329,6 +1337,17 @@ packages:
solid-js: 1.6.14
dev: true
/solid-tippy/0.2.1_rick4tq22pxy45qudhdfrizose:
resolution: {integrity: sha512-8qB6X1iMn7nBd5BX+x7tS+5mDVragw5vCaXLOxEQFWUsyRRGKAY8JmbmmyVFIMIvF+pgkIIVIArhNfAGGtYVLA==}
engines: {node: '>=10'}
peerDependencies:
solid-js: ^1.2
tippy.js: ^6.3
dependencies:
solid-js: 1.6.14
tippy.js: 6.3.7
dev: false
/source-map-js/1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@@ -1380,6 +1399,12 @@ packages:
- ts-node
dev: true
/tippy.js/6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
dependencies:
'@popperjs/core': 2.11.7
dev: false
/to-fast-properties/2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}

417
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,10 @@ 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"] }
itertools = "0.10.5"
core-foundation = "0.9.3"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem

View File

@@ -1,10 +1,15 @@
use std::env::current_dir;
use display_info::DisplayInfo;
use paris::{error, info};
use serde::{Deserialize, Serialize};
use tauri::api::path::config_dir;
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
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 {
Top,
Bottom,
@@ -12,14 +17,6 @@ pub enum Border {
Right,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct LedStripConfigOfBorders {
pub top: Option<LedStripConfig>,
pub bottom: Option<LedStripConfig>,
pub left: Option<LedStripConfig>,
pub right: Option<LedStripConfig>,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct LedStripConfig {
pub index: usize,
@@ -29,12 +26,20 @@ pub struct LedStripConfig {
pub len: usize,
}
impl LedStripConfig {
pub async fn read_config() -> anyhow::Result<Vec<Self>> {
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct LedStripConfigGroup {
pub strips: Vec<LedStripConfig>,
pub mappers: Vec<SamplePointMapper>,
}
impl LedStripConfigGroup {
pub async fn read_config() -> anyhow::Result<Self> {
let displays = DisplayInfo::all()?;
// 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
@@ -43,9 +48,15 @@ impl LedStripConfig {
if exists {
let config = tokio::fs::read_to_string(path).await?;
let config: Vec<Self> = toml::from_str(&config)
let mut config: LedStripConfigGroup = toml::from_str(&config)
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
for strip in config.strips.iter_mut() {
strip.display_id = displays[strip.index / 4].id;
}
// log::info!("config loaded: {:?}", config);
Ok(config)
} else {
info!("config file not exist, fallback to default config");
@@ -53,32 +64,37 @@ impl LedStripConfig {
}
}
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 config = toml::to_string(configs)
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
tokio::fs::create_dir_all(path.parent().unwrap()).await?;
tokio::fs::write(path, config)
.await
.map_err(|e| anyhow::anyhow!("Failed to write config file: {}", 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_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,
@@ -87,53 +103,31 @@ 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,
pos: (j + i * 4) * 30,
})
}
}
Ok(configs)
Ok(Self { strips, mappers })
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct DisplayConfig {
pub id: u32,
pub index_of_display: usize,
pub display_width: usize,
pub display_height: usize,
pub led_strip_of_borders: LedStripConfigOfBorders,
pub scale_factor: f32,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplePointMapper {
pub start: usize,
pub end: usize,
pub pos: usize,
}
impl LedStripConfigOfBorders {
pub fn default() -> Self {
Self {
top: None,
bottom: None,
left: None,
right: None,
}
}
}
impl DisplayConfig {
pub fn default(
id: u32,
index_of_display: usize,
display_width: usize,
display_height: usize,
scale_factor: f32,
) -> Self {
Self {
id,
index_of_display,
display_width,
display_height,
led_strip_of_borders: LedStripConfigOfBorders::default(),
scale_factor,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplePointConfig {
pub display_id: u32,
pub points: Vec<LedSamplePoints>,
}

View File

@@ -0,0 +1,226 @@
use std::{borrow::BorrowMut, sync::Arc};
use tauri::async_runtime::RwLock;
use tokio::sync::OnceCell;
use crate::ambient_light::{config, LedStripConfigGroup};
use super::{Border, SamplePointMapper};
pub struct ConfigManager {
config: Arc<RwLock<LedStripConfigGroup>>,
config_update_receiver: tokio::sync::watch::Receiver<LedStripConfigGroup>,
config_update_sender: tokio::sync::watch::Sender<LedStripConfigGroup>,
}
impl ConfigManager {
pub async fn global() -> &'static Self {
static CONFIG_MANAGER_GLOBAL: OnceCell<ConfigManager> = OnceCell::const_new();
CONFIG_MANAGER_GLOBAL
.get_or_init(|| async {
let configs = LedStripConfigGroup::read_config().await.unwrap();
let (config_update_sender, config_update_receiver) =
tokio::sync::watch::channel(configs.clone());
config_update_sender.send(configs.clone()).unwrap();
ConfigManager {
config: Arc::new(RwLock::new(configs)),
config_update_receiver,
config_update_sender,
}
})
.await
}
pub async fn reload(&self) -> anyhow::Result<()> {
let mut configs = self.config.write().await;
*configs = LedStripConfigGroup::read_config().await?;
Ok(())
}
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))?;
// log::info!("config updated: {:?}", configs);
Ok(())
}
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 config = self.config.write().await;
for strip in config.strips.iter_mut() {
if strip.display_id == display_id && strip.border == border {
let target = strip.len as i64 + delta_len as i64;
if target < 0 || target > 1000 {
return Err(anyhow::anyhow!(
"Overflow. range: 0-1000, current: {}",
target
));
}
strip.len = target as usize;
}
}
Self::rebuild_mappers(&mut config);
let cloned_config = config.clone();
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 move_strip_part(
&self,
display_id: u32,
border: Border,
target_start: usize,
) -> anyhow::Result<()> {
let mut config = self.config.write().await;
for (index, strip) in config.clone().strips.iter().enumerate() {
if strip.display_id == display_id && strip.border == border {
let mut mapper = config.mappers[index].borrow_mut();
if target_start == mapper.start {
return Ok(());
}
let target_end = mapper.end + target_start - mapper.start;
if target_start > 1000 || target_end > 1000 {
return Err(anyhow::anyhow!(
"Overflow. range: 0-1000, current: {}-{}",
target_start,
target_end
));
}
mapper.start = target_start as usize;
mapper.end = target_end as usize;
log::info!("mapper: {:?}", mapper);
}
}
let cloned_config = config.clone();
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 reverse_led_strip_part(
&self,
display_id: u32,
border: Border,
) -> anyhow::Result<()> {
let mut config = self.config.write().await;
for (index, strip) in config.clone().strips.iter().enumerate() {
if strip.display_id == display_id && strip.border == border {
let mut mapper = config.mappers[index].borrow_mut();
let start = mapper.start;
mapper.start = mapper.end;
mapper.end = start;
}
}
let cloned_config = config.clone();
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(())
}
fn rebuild_mappers(config: &mut LedStripConfigGroup) {
let mut prev_pos_end = 0;
let mappers: Vec<SamplePointMapper> = config
.strips
.iter()
.enumerate()
.map(|(index, strip)| {
let mapper = &config.mappers[index];
if mapper.start < mapper.end {
let mapper = SamplePointMapper {
start: mapper.start,
end: mapper.start + strip.len,
pos: prev_pos_end,
};
prev_pos_end = prev_pos_end + strip.len;
mapper
} else {
let mapper = SamplePointMapper {
end: mapper.end,
start: mapper.end + strip.len,
pos: prev_pos_end,
};
prev_pos_end = prev_pos_end + strip.len;
mapper
}
})
.collect();
config.mappers = mappers;
}
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?;
self.config_update_sender
.send(cloned_config)
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
Ok(())
}
pub fn clone_config_update_receiver(
&self,
) -> tokio::sync::watch::Receiver<LedStripConfigGroup> {
self.config_update_receiver.clone()
}
}

View File

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

View File

@@ -0,0 +1,459 @@
use std::{collections::HashMap, sync::Arc, time::Duration};
use paris::warn;
use tauri::async_runtime::RwLock;
use tokio::{
sync::{broadcast, watch},
time::sleep,
};
use crate::{
ambient_light::{config, ConfigManager},
rpc::MqttRpc,
screenshot::LedSamplePoints,
screenshot_manager::{self, ScreenshotManager},
};
use itertools::Itertools;
use super::{LedStripConfigGroup, SamplePointConfig, SamplePointMapper};
pub struct LedColorsPublisher {
sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
sorted_colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
display_colors_rx: Arc<RwLock<broadcast::Receiver<(u32, Vec<u8>)>>>,
display_colors_tx: Arc<RwLock<broadcast::Sender<(u32, Vec<u8>)>>>,
inner_tasks_version: Arc<RwLock<usize>>,
}
impl LedColorsPublisher {
pub async fn global() -> &'static Self {
static LED_COLORS_PUBLISHER_GLOBAL: tokio::sync::OnceCell<LedColorsPublisher> =
tokio::sync::OnceCell::const_new();
let (sorted_tx, sorted_rx) = watch::channel(Vec::new());
let (tx, rx) = watch::channel(Vec::new());
let (display_colors_tx, display_colors_rx) = broadcast::channel(8);
LED_COLORS_PUBLISHER_GLOBAL
.get_or_init(|| async {
LedColorsPublisher {
sorted_colors_rx: Arc::new(RwLock::new(sorted_rx)),
sorted_colors_tx: Arc::new(RwLock::new(sorted_tx)),
colors_rx: Arc::new(RwLock::new(rx)),
colors_tx: Arc::new(RwLock::new(tx)),
display_colors_rx: Arc::new(RwLock::new(display_colors_rx)),
display_colors_tx: Arc::new(RwLock::new(display_colors_tx)),
inner_tasks_version: Arc::new(RwLock::new(0)),
}
})
.await
}
fn start_one_display_colors_fetcher(
&self,
display_id: u32,
sample_points: Vec<Vec<LedSamplePoints>>,
) {
let display_colors_tx = self.display_colors_tx.clone();
let internal_tasks_version = self.inner_tasks_version.clone();
tokio::spawn(async move {
let display_colors_tx = display_colors_tx.read().await.clone();
let colors = screenshot_manager::get_display_colors(display_id, &sample_points);
if let Err(err) = colors {
warn!("Failed to get colors: {}", err);
return;
}
let mut start: tokio::time::Instant = tokio::time::Instant::now();
let mut interval = tokio::time::interval(Duration::from_millis(66));
let init_version = internal_tasks_version.read().await.clone();
loop {
interval.tick().await;
tokio::time::sleep(Duration::from_millis(1)).await;
if internal_tasks_version.read().await.clone() != init_version {
log::info!(
"inner task version changed, stop. {} != {}",
internal_tasks_version.read().await.clone(),
init_version
);
break;
}
// log::info!("tick: {}ms", start.elapsed().as_millis());
start = tokio::time::Instant::now();
let colors = screenshot_manager::get_display_colors(display_id, &sample_points);
if let Err(err) = colors {
warn!("Failed to get colors: {}", err);
sleep(Duration::from_millis(100)).await;
continue;
}
let colors = colors.unwrap();
let color_len = colors.len();
match display_colors_tx.send((
display_id,
colors
.into_iter()
.map(|color| color.get_rgb())
.flatten()
.collect::<Vec<_>>(),
)) {
Ok(_) => {
// log::info!("sent colors: {:?}", color_len);
}
Err(err) => {
warn!("Failed to send display_colors: {}", err);
}
};
}
});
}
fn start_all_colors_worker(&self, display_ids: Vec<u32>, mappers: Vec<SamplePointMapper>) {
let sorted_colors_tx = self.sorted_colors_tx.clone();
let colors_tx = self.colors_tx.clone();
let display_colors_rx = self.display_colors_rx.clone();
tokio::spawn(async move {
for _ in 0..10 {
let mut rx = display_colors_rx.read().await.resubscribe();
let sorted_colors_tx = sorted_colors_tx.write().await;
let colors_tx = colors_tx.write().await;
let mut all_colors: Vec<Option<Vec<u8>>> = vec![None; display_ids.len()];
let mut start: tokio::time::Instant = tokio::time::Instant::now();
log::info!("start all_colors_worker");
loop {
// log::info!("display_colors_rx changed");
let color_info = rx.recv().await;
if let Err(err) = color_info {
match err {
broadcast::error::RecvError::Closed => {
break;
}
broadcast::error::RecvError::Lagged(_) => {
warn!("display_colors_rx lagged");
continue;
}
}
}
let (display_id, colors) = color_info.unwrap();
let index = display_ids.iter().position(|id| *id == display_id);
if index.is_none() {
warn!("display id not found");
continue;
}
all_colors[index.unwrap()] = Some(colors);
if all_colors.iter().all(|color| color.is_some()) {
let flatten_colors = all_colors
.clone()
.into_iter()
.flat_map(|c| c.unwrap())
.collect::<Vec<_>>();
match colors_tx.send(flatten_colors.clone()) {
Ok(_) => {}
Err(err) => {
warn!("Failed to send colors: {}", err);
}
};
let sorted_colors =
ScreenshotManager::get_sorted_colors(&flatten_colors, &mappers);
match sorted_colors_tx.send(sorted_colors) {
Ok(_) => {}
Err(err) => {
warn!("Failed to send sorted colors: {}", err);
}
};
log::info!("tick: {}ms", start.elapsed().as_millis());
start = tokio::time::Instant::now();
}
}
}
});
}
pub fn start(&self) {
let inner_tasks_version = self.inner_tasks_version.clone();
tokio::spawn(async move {
let publisher = Self::global().await;
let mut inner_tasks_version = inner_tasks_version.write().await;
*inner_tasks_version = inner_tasks_version.overflowing_add(1).0;
let config_manager = ConfigManager::global().await;
let mut config_receiver = config_manager.clone_config_update_receiver();
log::info!("waiting for config update...");
while config_receiver.changed().await.is_ok() {
log::info!("config updated, restart inner tasks...");
let configs = config_receiver.borrow().clone();
let configs = Self::get_colors_configs(&configs).await;
if let Err(err) = configs {
warn!("Failed to get configs: {}", err);
sleep(Duration::from_millis(100)).await;
continue;
}
let configs = configs.unwrap();
for sample_point_group in configs.sample_point_groups.clone() {
let display_id = sample_point_group.display_id;
let sample_points = sample_point_group.points;
publisher.start_one_display_colors_fetcher(display_id, sample_points);
}
let display_ids = configs.sample_point_groups;
publisher.start_all_colors_worker(
display_ids.iter().map(|c| c.display_id).collect(),
configs.mappers,
);
break;
}
});
// tokio::spawn(async move {
// loop {
// let sorted_colors_tx = sorted_colors_tx.write().await;
// let colors_tx = colors_tx.write().await;
// let screenshot_manager = ScreenshotManager::global().await;
// let config_manager = ConfigManager::global().await;
// let config_receiver = config_manager.clone_config_update_receiver();
// let configs = config_receiver.borrow().clone();
// let configs = Self::get_colors_configs(&configs).await;
// if let Err(err) = configs {
// warn!("Failed to get configs: {}", err);
// sleep(Duration::from_millis(100)).await;
// continue;
// }
// let configs = configs.unwrap();
// let mut merged_screenshot_receiver =
// screenshot_manager.clone_merged_screenshot_rx().await;
// let mut screenshots = HashMap::new();
// // let mut start = tokio::time::Instant::now();
// loop {
// let screenshot = merged_screenshot_receiver.recv().await;
// if let Err(err) = screenshot {
// match err {
// tokio::sync::broadcast::error::RecvError::Closed => {
// warn!("closed");
// continue;
// }
// tokio::sync::broadcast::error::RecvError::Lagged(_) => {
// warn!("lagged");
// continue;
// }
// }
// }
// let screenshot = screenshot.unwrap();
// // log::info!("got screenshot: {:?}", screenshot.display_id);
// screenshots.insert(screenshot.display_id, screenshot);
// if screenshots.len() == configs.sample_point_groups.len() {
// // log::info!("{}", start.elapsed().as_millis().to_string());
// {
// let screenshots = configs
// .sample_point_groups
// .iter()
// .map(|strip| screenshots.get(&strip.display_id).unwrap())
// .collect::<Vec<_>>();
// let colors = screenshot_manager
// .get_all_colors(&configs.sample_point_groups, &screenshots)
// .await;
// let sorted_colors =
// ScreenshotManager::get_sorted_colors(&colors, &configs.mappers)
// .await;
// match colors_tx.send(colors) {
// Ok(_) => {
// // log::info!("colors updated");
// }
// Err(_) => {
// warn!("colors update failed");
// }
// }
// match sorted_colors_tx.send(sorted_colors) {
// Ok(_) => {
// // log::info!("colors updated");
// }
// Err(_) => {
// warn!("colors update failed");
// }
// }
// }
// // screenshots.clear();
// // start = tokio::time::Instant::now();
// }
// }
// }
// });
let rx = self.sorted_colors_rx.clone();
tokio::spawn(async move {
let mut rx = rx.read().await.clone();
loop {
if let Err(err) = rx.changed().await {
warn!("rx changed error: {}", err);
sleep(Duration::from_millis(1000)).await;
continue;
}
let colors = rx.borrow().clone();
let len = colors.len();
match Self::send_colors(colors).await {
Ok(_) => {
// log::info!("colors sent. len: {}", len);
}
Err(err) => {
warn!("colors send failed: {}", err);
}
}
}
});
}
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_sorted_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
self.sorted_colors_rx.read().await.clone()
}
pub async fn get_colors_configs(
configs: &LedStripConfigGroup,
) -> anyhow::Result<AllColorConfig> {
let screenshot_manager = ScreenshotManager::global().await;
let display_ids = configs
.strips
.iter()
.map(|c| c.display_id)
.unique()
.collect::<Vec<_>>();
let mappers = configs.mappers.clone();
let mut colors_configs = Vec::new();
let mut merged_screenshot_receiver = screenshot_manager.clone_merged_screenshot_rx().await;
let mut screenshots = HashMap::new();
loop {
log::info!("waiting merged screenshot...");
let screenshot = merged_screenshot_receiver.recv().await;
if let Err(err) = screenshot {
match err {
tokio::sync::broadcast::error::RecvError::Closed => {
warn!("closed");
continue;
}
tokio::sync::broadcast::error::RecvError::Lagged(_) => {
warn!("lagged");
continue;
}
}
}
let screenshot = screenshot.unwrap();
// log::info!("got screenshot: {:?}", screenshot.display_id);
screenshots.insert(screenshot.display_id, screenshot);
if screenshots.len() == display_ids.len() {
for display_id in display_ids {
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 screenshot = screenshots.get(&display_id).unwrap();
log::debug!("screenshot updated: {:?}", display_id);
let points: Vec<_> = led_strip_configs
.iter()
.map(|config| screenshot.get_sample_points(&config))
.collect();
let colors_config = DisplaySamplePointGroup { display_id, points };
colors_configs.push(colors_config);
}
return Ok(AllColorConfig {
sample_point_groups: colors_configs,
mappers,
// screenshot_receivers: local_rx_list,
});
}
}
}
pub async fn clone_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
self.colors_rx.read().await.clone()
}
}
#[derive(Debug)]
pub struct AllColorConfig {
pub sample_point_groups: Vec<DisplaySamplePointGroup>,
pub mappers: Vec<config::SamplePointMapper>,
// pub screenshot_receivers: Vec<watch::Receiver<Screenshot>>,
}
#[derive(Debug, Clone)]
pub struct DisplaySamplePointGroup {
pub display_id: u32,
pub points: Vec<Vec<LedSamplePoints>>,
}

View File

@@ -159,7 +159,8 @@ impl Manager {
let rpc = rpc::Manager::global().await;
rpc.publish_desktop_cmd(
format!("display{}/brightness", display_brightness.display_index).as_str(),
format!("display{}/brightness", display_brightness.display_index)
.as_str(),
target.to_be_bytes().to_vec(),
)
.await;

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::LedStripConfig;
use ambient_light::{Border, LedColorsPublisher, LedStripConfig, LedStripConfigGroup};
use core_graphics::display::{
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
};
@@ -17,7 +18,7 @@ use screenshot::Screenshot;
use screenshot_manager::ScreenshotManager;
use serde::{Deserialize, Serialize};
use serde_json::to_string;
use tauri::{http::ResponseBuilder, regex};
use tauri::{http::ResponseBuilder, regex, Manager};
#[derive(Serialize, Deserialize)]
#[serde(remote = "DisplayInfo")]
@@ -57,65 +58,42 @@ fn list_display_info() -> Result<String, String> {
}
#[tauri::command]
async fn subscribe_encoded_screenshot_updated(
window: tauri::Window,
display_id: u32,
) -> Result<(), String> {
let screenshot_manager = ScreenshotManager::global().await;
screenshot_manager
.subscribe_encoded_screenshot_updated(window, display_id)
.await
.map_err(|err| {
error!("subscribe_encoded_screenshot_updated: {}", err);
err.to_string()
})
}
#[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]
async fn get_led_strips_sample_points(
config: LedStripConfig,
) -> Result<Vec<screenshot::LedSamplePoints>, String> {
let displays = DisplayInfo::all().map_err(|e| {
error!("can not read led strip config: {}", e);
e.to_string()
});
let display = displays?.into_iter().find(|d| d.id == config.display_id);
if let None = display {
let screenshot_manager = ScreenshotManager::global().await;
let channels = screenshot_manager.channels.read().await;
if let Some(rx) = channels.get(&config.display_id) {
let rx = rx.clone();
let screenshot = rx.borrow().clone();
let sample_points = screenshot.get_sample_points(&config);
Ok(sample_points)
} else {
return Err(format!("display not found: {}", config.display_id));
}
let display = display.unwrap();
let config = screenshot::Screenshot::get_sample_point(
config,
display.width as usize,
display.height as usize,
);
Ok(config)
}
#[tauri::command]
@@ -128,7 +106,7 @@ async fn get_one_edge_colors(
if let Some(rx) = channels.get(&display_id) {
let rx = rx.clone();
let screenshot = rx.borrow().clone();
let bytes = screenshot.bytes.read().await;
let bytes = screenshot.bytes.read().await.to_owned();
let colors =
Screenshot::get_one_edge_colors(&sample_points, &bytes, screenshot.bytes_per_row);
Ok(colors)
@@ -137,21 +115,85 @@ async fn get_one_edge_colors(
}
}
#[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
);
let config_manager = ambient_light::ConfigManager::global().await;
config_manager
.patch_led_strip_len(display_id, border, delta_len)
.await
.map_err(|e| {
error!("can not patch led strip len: {}", e);
e.to_string()
})?;
info!("patch_led_strip_len: ok");
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()
})
}
#[tauri::command]
async fn move_strip_part(display_id: u32, border: Border, target_start: usize) -> Result<(), String> {
let config_manager = ambient_light::ConfigManager::global().await;
config_manager
.move_strip_part(
display_id,
border,
target_start,
)
.await
.map_err(|e| {
error!("can not move strip part: {}", e);
e.to_string()
})
}
#[tauri::command]
async fn reverse_led_strip_part(display_id: u32, border: Border) -> Result<(), String> {
let config_manager = ambient_light::ConfigManager::global().await;
config_manager
.reverse_led_strip_part(display_id, border)
.await
.map_err(|e| {
error!("can not reverse led strip part: {}", 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();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
greet,
list_display_info,
subscribe_encoded_screenshot_updated,
read_led_strip_configs,
write_led_strip_configs,
get_led_strips_sample_points,
get_one_edge_colors
get_one_edge_colors,
patch_led_strip_len,
send_colors,
move_strip_part,
reverse_led_strip_part,
])
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
@@ -280,6 +322,63 @@ async fn main() {
.status(500)
.body(err.to_string().into_bytes());
})
.setup(move |app| {
let app_handle = app.handle().clone();
tokio::spawn(async move {
let config_manager = ambient_light::ConfigManager::global().await;
let config_update_receiver = config_manager.clone_config_update_receiver();
let mut config_update_receiver = config_update_receiver;
loop {
if let Err(err) = config_update_receiver.changed().await {
error!("config update receiver changed error: {}", err);
return;
}
log::info!("config changed. emit config_changed event.");
let config = config_update_receiver.borrow().clone();
app_handle.emit_all("config_changed", config).unwrap();
}
});
let app_handle = app.handle().clone();
tokio::spawn(async move {
let publisher = ambient_light::LedColorsPublisher::global().await;
let mut publisher_update_receiver = publisher.clone_sorted_colors_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_sorted_colors_changed", publisher)
.unwrap();
}
});
let app_handle = app.handle().clone();
tokio::spawn(async move {
let publisher = ambient_light::LedColorsPublisher::global().await;
let mut publisher_update_receiver = publisher.clone_colors_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!())
.expect("error while running tauri application");
}

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

@@ -1,13 +1,11 @@
use std::iter;
use std::cell::RefCell;
use std::{iter, cell::Ref};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tauri::async_runtime::RwLock;
use tauri::async_runtime::{RwLock, Mutex};
use crate::{
ambient_light::{DisplayConfig, LedStripConfig},
led_color::LedColor,
};
use crate::{ambient_light::LedStripConfig, led_color::LedColor};
#[derive(Debug, Clone)]
pub struct Screenshot {
@@ -17,9 +15,10 @@ pub struct Screenshot {
pub bytes_per_row: usize,
pub bytes: Arc<RwLock<Vec<u8>>>,
pub scale_factor: f32,
pub sample_points: ScreenSamplePoints,
}
static SINGLE_AXIS_POINTS: usize = 5;
impl Screenshot {
pub fn new(
display_id: u32,
@@ -28,7 +27,6 @@ impl Screenshot {
bytes_per_row: usize,
bytes: Vec<u8>,
scale_factor: f32,
sample_points: ScreenSamplePoints,
) -> Self {
Self {
display_id,
@@ -37,21 +35,19 @@ impl Screenshot {
bytes_per_row,
bytes: Arc::new(RwLock::new(bytes)),
scale_factor,
sample_points,
}
}
pub fn get_sample_point(
config: LedStripConfig,
width: usize,
height: usize,
) -> Vec<LedSamplePoints> {
pub fn get_sample_points(&self, config: &LedStripConfig) -> 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)
Self::get_one_edge_sample_points(height / 18, width, config.len, SINGLE_AXIS_POINTS)
}
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 / 18, width, config.len, SINGLE_AXIS_POINTS);
points
.into_iter()
.map(|groups| -> Vec<Point> {
@@ -60,7 +56,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 / 32, height, config.len, SINGLE_AXIS_POINTS);
points
.into_iter()
.map(|groups| -> Vec<Point> {
@@ -69,7 +65,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 / 32, height, config.len, SINGLE_AXIS_POINTS);
points
.into_iter()
.map(|groups| -> Vec<Point> {
@@ -80,99 +76,16 @@ impl Screenshot {
}
}
fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints {
let top = match config.led_strip_of_borders.top {
Some(led_strip_config) => Self::get_one_edge_sample_points(
config.display_height / 8,
config.display_width,
led_strip_config.len,
1,
),
None => {
vec![]
}
};
let bottom: Vec<LedSamplePoints> = match config.led_strip_of_borders.bottom {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_height / 9,
config.display_width,
led_strip_config.len,
5,
);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups
.into_iter()
.map(|(x, y)| (x, config.display_height - y))
.collect()
})
.collect()
}
None => {
vec![]
}
};
let left: Vec<LedSamplePoints> = match config.led_strip_of_borders.left {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_width / 16,
config.display_height,
led_strip_config.len,
5,
);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups.into_iter().map(|(x, y)| (y, x)).collect()
})
.collect()
}
None => {
vec![]
}
};
let right: Vec<LedSamplePoints> = match config.led_strip_of_borders.right {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_width / 16,
config.display_height,
led_strip_config.len,
5,
);
points
.into_iter()
.map(|groups| -> Vec<Point> {
groups
.into_iter()
.map(|(x, y)| (config.display_width - y, x))
.collect()
})
.collect()
}
None => {
vec![]
}
};
ScreenSamplePoints {
top,
bottom,
left,
right,
}
}
fn get_one_edge_sample_points(
width: usize,
length: usize,
leds: usize,
single_axis_points: usize,
) -> Vec<LedSamplePoints> {
if leds == 0 {
return vec![];
}
let cell_size_x = length as f64 / single_axis_points as f64 / leds as f64;
let cell_size_y = width / single_axis_points;
@@ -191,6 +104,7 @@ impl Screenshot {
.map(|&x| point_y_list.iter().map(move |&y| (x, y)))
.flatten()
.collect();
points
.chunks(single_axis_points * single_axis_points)
.into_iter()
@@ -198,46 +112,6 @@ impl Screenshot {
.collect()
}
pub async fn get_colors(&self) -> DisplayColorsOfLedStrips {
let bitmap = self.bytes.read().await;
let top =
Self::get_one_edge_colors(&self.sample_points.top, bitmap.as_ref(), self.bytes_per_row)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
let bottom = Self::get_one_edge_colors(
&self.sample_points.bottom,
bitmap.as_ref(),
self.bytes_per_row,
)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
let left = Self::get_one_edge_colors(
&self.sample_points.left,
bitmap.as_ref(),
self.bytes_per_row,
)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
let right = Self::get_one_edge_colors(
&self.sample_points.right,
bitmap.as_ref(),
self.bytes_per_row,
)
.into_iter()
.flat_map(|color| color.get_rgb())
.collect();
DisplayColorsOfLedStrips {
top,
bottom,
left,
right,
}
}
pub fn get_one_edge_colors(
sample_points_of_leds: &Vec<LedSamplePoints>,
bitmap: &Vec<u8>,
@@ -250,17 +124,50 @@ impl Screenshot {
let mut b = 0.0;
let len = led_points.len() as f64;
for (x, y) in led_points {
// log::info!("x: {}, y: {}, bytes_per_row: {}", x, y, bytes_per_row);
let position = x * 4 + y * bytes_per_row;
b += bitmap[position] as f64;
g += bitmap[position + 1] as f64;
r += bitmap[position + 2] as f64;
}
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
// paris::info!("color: {:?}", color.get_rgb());
colors.push(color);
}
colors
}
pub fn get_one_edge_colors_by_cg_image(
sample_points_of_leds: &Vec<LedSamplePoints>,
bitmap: core_foundation::data::CFData,
bytes_per_row: usize,
) -> Vec<LedColor> {
let mut colors = vec![];
for led_points in sample_points_of_leds {
let mut r = 0.0;
let mut g = 0.0;
let mut b = 0.0;
let len = led_points.len() as f64;
for (x, y) in led_points {
// log::info!("x: {}, y: {}, bytes_per_row: {}", x, y, bytes_per_row);
let position = x * 4 + y * bytes_per_row;
b += bitmap[position] as f64;
g += bitmap[position + 1] as f64;
r += bitmap[position + 2] as f64;
}
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
colors.push(color);
}
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

@@ -1,17 +1,24 @@
use std::cell::{Ref, RefCell};
use std::{collections::HashMap, sync::Arc};
use core_graphics::display::{
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
};
use paris::{error, info, warn};
use tauri::{async_runtime::RwLock, Window};
use tokio::sync::{watch, OnceCell};
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use paris::warn;
use tauri::async_runtime::RwLock;
use tokio::sync::{broadcast, watch, OnceCell};
use tokio::time::{self, Duration};
use crate::screenshot::{Screenshot, ScreenshotPayload, ScreenSamplePoints};
use crate::screenshot::LedSamplePoints;
use crate::{
ambient_light::{SamplePointConfig, SamplePointMapper},
led_color::LedColor,
screenshot::{ScreenSamplePoints, Screenshot},
};
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
log::debug!("take_screenshot");
// let start_at = std::time::Instant::now();
let cg_display = CGDisplay::new(display_id);
let cg_image = CGDisplay::screenshot(
@@ -21,7 +28,6 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
kCGWindowImageDefault,
)
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
// println!("take screenshot took {}ms", start_at.elapsed().as_millis());
let buffer = cg_image.data();
let bytes_per_row = cg_image.bytes_per_row();
@@ -29,8 +35,7 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
let height = cg_image.height();
let width = cg_image.width();
let mut bytes = vec![0u8; buffer.len() as usize];
bytes.copy_from_slice(&buffer);
let bytes = buffer.bytes().to_owned();
Ok(Screenshot::new(
display_id,
@@ -39,13 +44,77 @@ 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![] }
))
}
pub fn get_display_colors(
display_id: u32,
sample_points: &Vec<Vec<LedSamplePoints>>,
) -> anyhow::Result<Vec<LedColor>> {
log::debug!("take_screenshot");
let cg_display = CGDisplay::new(display_id);
let mut colors = vec![];
let start_at = std::time::Instant::now();
for points in sample_points {
if points.len() == 0 {
continue;
}
let start_x = points[0][0].0;
let start_y = points[0][0].1;
let end_x = points.last().unwrap().last().unwrap().0;
let end_y = points.last().unwrap().last().unwrap().1;
let (start_x, end_x) = (usize::min(start_x, end_x), usize::max(start_x, end_x));
let (start_y, end_y) = (usize::min(start_y, end_y), usize::max(start_y, end_y));
let origin = CGPoint {
x: start_x as f64 + cg_display.bounds().origin.x,
y: start_y as f64 + cg_display.bounds().origin.y,
};
let size = CGSize {
width: (end_x - start_x + 1) as f64,
height: (end_y - start_y + 1) as f64,
};
let cg_image = CGDisplay::screenshot(
CGRect::new(&origin, &size),
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
kCGWindowImageDefault,
)
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
let bitmap = cg_image.data();
let points = points
.iter()
.map(|points| {
points
.iter()
.map(|(x, y)| (*x - start_x, *y - start_y))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let mut part_colors =
Screenshot::get_one_edge_colors_by_cg_image(&points, bitmap, cg_image.bytes_per_row());
colors.append(&mut part_colors);
}
// if display_id == 4849664 {
// log::info!(
// "======= get_display_colors {} took {}ms",
// display_id,
// start_at.elapsed().as_millis()
// );
// }
Ok(colors)
}
pub struct ScreenshotManager {
pub channels: Arc<RwLock<HashMap<u32, watch::Receiver<Screenshot>>>>,
encode_listeners: Arc<RwLock<HashMap<u32, Vec<Window>>>>,
merged_screenshot_tx: Arc<RwLock<broadcast::Sender<Screenshot>>>,
}
impl ScreenshotManager {
@@ -55,10 +124,10 @@ impl ScreenshotManager {
SCREENSHOT_MANAGER
.get_or_init(|| async {
let channels = Arc::new(RwLock::new(HashMap::new()));
let encode_listeners = Arc::new(RwLock::new(HashMap::new()));
let (merged_screenshot_tx, _) = broadcast::channel::<Screenshot>(2);
Self {
channels,
encode_listeners,
merged_screenshot_tx: Arc::new(RwLock::new(merged_screenshot_tx)),
}
})
.await
@@ -74,6 +143,7 @@ impl ScreenshotManager {
fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> {
let channels = self.channels.to_owned();
let merged_screenshot_tx = self.merged_screenshot_tx.clone();
tokio::spawn(async move {
let screenshot = take_screenshot(display_id, scale_factor);
@@ -81,128 +151,124 @@ impl ScreenshotManager {
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
return;
}
let mut interval = time::interval(Duration::from_millis(3300));
let mut start = tokio::time::Instant::now();
let screenshot = screenshot.unwrap();
let (tx, rx) = watch::channel(screenshot);
let (screenshot_tx, screenshot_rx) = watch::channel(screenshot);
{
let channels = channels.clone();
let mut channels = channels.write().await;
channels.insert(display_id, rx);
channels.insert(display_id, screenshot_rx.clone());
}
let merged_screenshot_tx = merged_screenshot_tx.read().await.clone();
loop {
Self::take_screenshot_loop(display_id, scale_factor, &tx).await;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
start = tokio::time::Instant::now();
Self::take_screenshot_loop(
display_id,
scale_factor,
&screenshot_tx,
&merged_screenshot_tx,
)
.await;
interval.tick().await;
tokio::time::sleep(Duration::from_millis(1)).await;
}
});
Ok(())
}
pub async fn subscribe_encoded_screenshot_updated(
&self,
window: Window,
display_id: u32,
) -> anyhow::Result<()> {
let channels = self.channels.to_owned();
let encode_listeners = self.encode_listeners.to_owned();
log::info!("subscribe_encoded_screenshot_updated. {}", display_id);
{
let encode_listeners = encode_listeners.read().await;
let listening_windows = encode_listeners.get(&display_id);
if listening_windows.is_some() && listening_windows.unwrap().contains(&window) {
log::debug!("subscribe_encoded_screenshot_updated: already listening. display#{}, window#{}", display_id, window.label());
return Ok(());
}
}
{
encode_listeners
.write()
.await
.entry(display_id)
.or_default()
.push(window);
}
tokio::spawn(async move {
info!("subscribe_encoded_screenshot_updated: start");
let channels = channels.read().await;
let rx = channels.get(&display_id);
if rx.is_none() {
error!(
"subscribe_encoded_screenshot_updated: can not find display_id {}",
display_id
);
return;
}
let mut rx = rx.unwrap().clone();
loop {
if let Err(err) = rx.changed().await {
error!(
"subscribe_encoded_screenshot_updated: can not wait rx {}",
err
);
break;
}
let encode_listeners = encode_listeners.read().await;
let windows = encode_listeners.get(&display_id);
if windows.is_none() || windows.unwrap().is_empty() {
info!("subscribe_encoded_screenshot_updated: no listener, stop");
break;
}
let screenshot = rx.borrow().clone();
// let base64_image = Self::encode_screenshot_to_base64(&screenshot).await;
let height = screenshot.height;
let width = screenshot.width;
// if base64_image.is_err() {
// error!(
// "subscribe_encoded_screenshot_updated: encode_screenshot_to_base64 error {}",
// base64_image.err().unwrap()
// );
// continue;
// }
// let base64_image = base64_image.unwrap();
for window in windows.unwrap().into_iter() {
// let base64_image = base64_image.clone();
let payload = ScreenshotPayload {
display_id,
// base64_image,
height,
width,
};
if let Err(err) = window.emit("encoded-screenshot-updated", payload) {
error!("subscribe_encoded_screenshot_updated: emit error {}", err)
} else {
log::debug!(
"subscribe_encoded_screenshot_updated: emit success. display#{}",
display_id
)
}
}
}
});
Ok(())
}
async fn unsubscribe_encoded_screenshot_updated(&self, display_id: u32) -> anyhow::Result<()> {
let channels = self.channels.to_owned();
let mut channels = channels.write().await;
channels.remove(&display_id);
Ok(())
}
async fn take_screenshot_loop(
display_id: u32,
scale_factor: f32,
tx: &watch::Sender<Screenshot>,
screenshot_tx: &watch::Sender<Screenshot>,
merged_screenshot_tx: &broadcast::Sender<Screenshot>,
) {
let screenshot = take_screenshot(display_id, scale_factor);
if let Ok(screenshot) = screenshot {
tx.send(screenshot).unwrap();
match merged_screenshot_tx.send(screenshot.clone()) {
Ok(_) => {}
Err(err) => {
// warn!("take_screenshot_loop: merged_screenshot_tx.send failed. display#{}. err: {}", display_id, err);
}
}
screenshot_tx.send(screenshot).unwrap();
// log::info!("take_screenshot_loop: send success. display#{}", display_id)
} else {
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
}
}
pub async fn get_all_colors(
&self,
configs: &Vec<SamplePointConfig>,
screenshots: &Vec<&Screenshot>,
) -> Vec<LedColor> {
let mut all_colors = vec![];
for (index, screenshot) in screenshots.iter().enumerate() {
let config = &configs[index];
let mut colors = screenshot.get_colors_by_sample_points(&config.points).await;
all_colors.append(&mut colors);
}
all_colors
}
pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> {
let total_leds = mappers
.iter()
.map(|mapper| usize::max(mapper.start, mapper.end))
.max()
.unwrap_or(0) as usize;
let mut global_colors = vec![0u8; total_leds * 3];
let mut color_index = 0;
mappers.iter().for_each(|group| {
if group.end > global_colors.len() || group.start > global_colors.len() {
warn!(
"get_sorted_colors: group out of range. start: {}, end: {}, global_colors.len(): {}",
group.start,
group.end,
global_colors.len()
);
return;
}
if color_index + group.start.abs_diff(group.end) * 3 > colors.len(){
warn!(
"get_sorted_colors: color_index out of range. color_index: {}, strip len: {}, colors.len(): {}",
color_index / 3,
group.start.abs_diff(group.end),
colors.len() / 3
);
return;
}
if group.end > group.start {
for i in group.start..group.end {
global_colors[i * 3] = colors[color_index +0];
global_colors[i * 3 + 1] = colors[color_index +1];
global_colors[i * 3 + 2] = colors[color_index +2];
color_index += 3;
}
} else {
for i in (group.end..group.start).rev() {
global_colors[i * 3] = colors[color_index +0];
global_colors[i * 3 + 1] = colors[color_index +1];
global_colors[i * 3 + 2] = colors[color_index +2];
color_index += 3;
}
}
});
global_colors
}
pub async fn clone_merged_screenshot_rx(&self) -> broadcast::Receiver<Screenshot> {
self.merged_screenshot_tx.read().await.subscribe()
}
}

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,11 +1,17 @@
import { createEffect } from 'solid-js';
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri';
import { createEffect, onCleanup } from 'solid-js';
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 { path } from '@tauri-apps/api';
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';
import { createStore } from 'solid-js/store';
import {
LedStripConfigurationContext,
LedStripConfigurationContextType,
} from './contexts/led-strip-configuration.context';
function App() {
createEffect(() => {
@@ -14,22 +20,85 @@ function App() {
displays: JSON.parse(displays),
});
});
invoke<LedStripConfig[]>('read_led_strip_configs').then((strips) => {
console.log(strips);
setLedStripStore({
strips,
});
invoke<LedStripConfigContainer>('read_led_strip_configs').then((configs) => {
console.log(configs);
setLedStripStore(configs);
});
});
// listen to config_changed event
createEffect(() => {
const unlisten = listen('config_changed', (event) => {
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,
});
});
onCleanup(() => {
unlisten.then((unlisten) => unlisten());
});
});
// listen to led_sorted_colors_changed event
createEffect(() => {
const unlisten = listen<Uint8ClampedArray>('led_sorted_colors_changed', (event) => {
const sortedColors = event.payload;
setLedStripStore({
sortedColors,
});
});
onCleanup(() => {
unlisten.then((unlisten) => unlisten());
});
});
const [ledStripConfiguration, setLedStripConfiguration] = createStore<
LedStripConfigurationContextType[0]
>({
selectedStripPart: null,
});
const ledStripConfigurationContextValue: LedStripConfigurationContextType = [
ledStripConfiguration,
{
setSelectedStripPart: (v) => {
setLedStripConfiguration({
selectedStripPart: v,
});
},
},
];
return (
<div>
<DisplayListContainer>
{displayStore.displays.map((display) => {
return <DisplayView display={display} />;
})}
</DisplayListContainer>
<LedStripConfigurationContext.Provider value={ledStripConfigurationContextValue}>
<LedStripPartsSorter />
<DisplayListContainer>
{displayStore.displays.map((display) => {
return <DisplayView display={display} />;
})}
</DisplayListContainer>
</LedStripConfigurationContext.Provider>
</div>
);
}

View File

@@ -0,0 +1,16 @@
<!-- http://mike.eire.ca/2010/02/25/easy-svg-grid/ -->
<!-- "I needed a grid in the background while I was debugging an SVG image I was creating, something
like Photoshops transparency grid. Heres what I did." -->
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="200" height="400">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<rect fill="black" x="0" y="0" width="5" height="5" opacity="0.3" />
<rect fill="white" x="5" y="0" width="5" height="5" />
<rect fill="black" x="5" y="5" width="5" height="5" opacity="0.3" />
<rect fill="white" x="0" y="5" width="5" height="5" />
</pattern>
</defs>
<rect fill="url(#grid)" x="0" y="0" width="100%" height="100%" />
</svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -7,7 +7,7 @@ type DisplayInfoItemProps = {
export const DisplayInfoItem: ParentComponent<DisplayInfoItemProps> = (props) => {
return (
<dl class="px-3 py-1 flex hover:bg-gray-100/50 gap-2 text-black rounded">
<dl class="px-3 py-1 flex hover:bg-slate-900/50 gap-2 text-white drop-shadow-[0_2px_2px_rgba(0,0,0,0.8)] rounded">
<dt class="uppercase w-1/2 select-all whitespace-nowrap">{props.label}</dt>
<dd class="select-all w-1/2 whitespace-nowrap">{props.children}</dd>
</dl>

View File

@@ -1,11 +1,13 @@
import {
createEffect,
createSignal,
JSX,
onCleanup,
onMount,
ParentComponent,
} from 'solid-js';
import { displayStore, setDisplayStore } from '../stores/display.store';
import background from '../assets/transparent-grid-background.svg?url';
export const DisplayListContainer: ParentComponent = (props) => {
let root: HTMLElement;
@@ -13,8 +15,7 @@ export const DisplayListContainer: ParentComponent = (props) => {
top: '0px',
left: '0px',
});
const [rootStyle, setRootStyle] = createSignal({
// width: '100%',
const [rootStyle, setRootStyle] = createSignal<JSX.CSSProperties>({
height: '100%',
});
const [bound, setBound] = createSignal({
@@ -38,6 +39,7 @@ export const DisplayListContainer: ParentComponent = (props) => {
setRootStyle({
height: `${(_bound.bottom - _bound.top) * displayStore.viewScale}px`,
background: `url(${background})`,
});
};
@@ -74,7 +76,7 @@ export const DisplayListContainer: ParentComponent = (props) => {
return (
<section ref={root!} class="relative bg-gray-400/30" style={rootStyle()}>
<ol class="absolute bg-gray-700" style={olStyle()}>
<ol class="absolute" style={olStyle()}>
{props.children}
</ol>
</section>

View File

@@ -1,5 +1,4 @@
import { Component, createMemo } from 'solid-js';
import { LedStripConfigOfBorders } from '../models/display-config';
import { DisplayInfo } from '../models/display-info.model';
import { displayStore } from '../stores/display.store';
import { ledStripStore } from '../stores/led-strip.store';
@@ -30,34 +29,35 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
return (
<section
class="absolute bg-gray-300 grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
class="absolute grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
style={style()}
>
<ScreenView
class="row-start-2 col-start-2"
class="row-start-2 col-start-2 group"
displayId={props.display.id}
style={{
'object-fit': 'contain',
}}
/>
<DisplayInfoPanel
display={props.display}
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
/>
>
<DisplayInfoPanel
display={props.display}
class="absolute bg-slate-700/20 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black group-hover:opacity-100 opacity-0 transition-opacity"
/>
</ScreenView>
<LedStripPart
class="row-start-1 col-start-2 flex-row"
class="row-start-1 col-start-2 flex-row overflow-hidden"
config={ledStripConfigs().find((c) => c.border === 'Top')}
/>
<LedStripPart
class="row-start-2 col-start-1 flex-col"
class="row-start-2 col-start-1 flex-col overflow-hidden"
config={ledStripConfigs().find((c) => c.border === 'Left')}
/>
<LedStripPart
class="row-start-2 col-start-3 flex-col"
class="row-start-2 col-start-3 flex-col overflow-hidden"
config={ledStripConfigs().find((c) => c.border === 'Right')}
/>
<LedStripPart
class="row-start-3 col-start-2 flex-row"
class="row-start-3 col-start-2 flex-row overflow-hidden"
config={ledStripConfigs().find((c) => c.border === 'Bottom')}
/>
</section>

View File

@@ -1,18 +1,20 @@
import { invoke } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event';
import {
Component,
createEffect,
createMemo,
createRoot,
createSignal,
For,
JSX,
on,
onCleanup,
splitProps,
useContext,
} from 'solid-js';
import { borders } from '../constants/border';
import { useTippy } from 'solid-tippy';
import { followCursor } from 'tippy.js';
import { LedStripConfig } from '../models/led-strip-config';
import { LedStripConfigurationContext } from '../contexts/led-strip-configuration.context';
import { ledStripStore } from '../stores/led-strip.store';
type LedStripPartProps = {
config?: LedStripConfig | null;
@@ -34,97 +36,120 @@ export const Pixel: Component<PixelProps> = (props) => {
}));
return (
<div
class="inline-block flex-shrink w-2 h-2 aspect-square rounded-full border border-black"
style={style()}
class="flex-auto flex h-full w-full justify-center items-center relative"
title={props.color}
/>
>
<div
class="absolute top-1/2 -translate-y-1/2 h-2.5 w-2.5 rounded-full ring-1 ring-stone-300"
style={style()}
/>
</div>
);
};
export const LedStripPart: Component<LedStripPartProps> = (props) => {
const [localProps, rootProps] = splitProps(props, ['config']);
const [stripConfiguration] = useContext(LedStripConfigurationContext);
const [ledSamplePoints, setLedSamplePoints] = createSignal();
const [colors, setColors] = createSignal<string[]>([]);
// update led strip colors from global store
createEffect(() => {
const samplePoints = ledSamplePoints();
if (!localProps.config || !samplePoints) {
if (!localProps.config) {
return;
}
let pendingCount = 0;
const unlisten = listen<{
base64_image: string;
display_id: number;
height: number;
width: number;
}>('encoded-screenshot-updated', (event) => {
if (event.payload.display_id !== localProps.config!.display_id) {
return;
}
if (pendingCount >= 1) {
return;
}
pendingCount++;
console.log({
samplePoints,
displayId: event.payload.display_id,
border: localProps.config!.border,
});
const index = ledStripStore.strips.findIndex(
(s) =>
s.display_id === localProps.config!.display_id &&
s.border === localProps.config!.border,
);
invoke<string[]>('get_one_edge_colors', {
samplePoints,
displayId: event.payload.display_id,
})
.then((colors) => {
setColors(colors);
})
.finally(() => {
pendingCount--;
});
if (index === -1) {
return;
}
const mapper = ledStripStore.mappers[index];
if (!mapper) {
return;
}
const offset = mapper.pos * 3;
const colors = new Array(localProps.config.len).fill(null).map((_, i) => {
const index = offset + i * 3;
return `rgb(${ledStripStore.colors[index]}, ${ledStripStore.colors[index + 1]}, ${
ledStripStore.colors[index + 2]
})`;
});
subscribeScreenshotUpdate(localProps.config.display_id);
onCleanup(() => {
unlisten.then((unlisten) => unlisten());
});
setColors(colors);
});
// get led strip sample points
createEffect(() => {
if (localProps.config) {
invoke('get_led_strips_sample_points', {
config: localProps.config,
}).then((points) => {
console.log({ points });
setLedSamplePoints(points);
});
}
});
const pixels = createMemo(() => {
const _colors = colors();
if (_colors) {
return <For each={_colors}>{(item) => <Pixel color={item} />}</For>;
} else if (localProps.config) {
return null;
return (
<For each={new Array(localProps.config.len).fill(undefined)}>
{() => <Pixel color="transparent" />}
</For>
);
}
const [anchor, setAnchor] = createSignal<HTMLElement>();
useTippy(anchor, {
hidden: true,
props: {
trigger: 'mouseenter focus',
followCursor: true,
plugins: [followCursor],
content: () =>
createRoot(() => {
return (
<span class="rounded-lg bg-slate-400/50 backdrop-blur text-white p-2 drop-shadow">
Count: {localProps.config?.len ?? '--'}
</span>
);
}) as Element,
},
});
const onWheel = (e: WheelEvent) => {
if (localProps.config) {
invoke('patch_led_strip_len', {
displayId: localProps.config.display_id,
border: localProps.config.border,
deltaLen: e.deltaY > 0 ? 1 : -1,
})
.then(() => {})
.catch((e) => {
console.error(e);
});
}
};
return (
<section
{...rootProps}
ref={setAnchor}
class={
'bg-yellow-50 flex flex-nowrap justify-around items-center overflow-hidden' +
'flex rounded-full flex-nowrap justify-around items-center overflow-hidden ' +
rootProps.class
}
classList={{
'ring ring-inset bg-yellow-400/50 ring-orange-400 animate-pulse':
stripConfiguration.selectedStripPart?.border === localProps.config?.border &&
stripConfiguration.selectedStripPart?.displayId ===
localProps.config?.display_id,
}}
onWheel={onWheel}
>
{pixels()}
<For each={colors()}>{(item) => <Pixel color={item} />}</For>
</section>
);
};

View File

@@ -0,0 +1,228 @@
import {
batch,
Component,
createContext,
createEffect,
createMemo,
createSignal,
For,
Index,
JSX,
on,
untrack,
useContext,
} from 'solid-js';
import { LedStripConfig, LedStripPixelMapper } from '../models/led-strip-config';
import { ledStripStore } from '../stores/led-strip.store';
import { invoke } from '@tauri-apps/api';
import { LedStripConfigurationContext } from '../contexts/led-strip-configuration.context';
import background from '../assets/transparent-grid-background.svg?url';
const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper }> = (
props,
) => {
const [fullLeds, setFullLeds] = createSignal<Array<string | null>>([]);
const [dragging, setDragging] = createSignal<boolean>(false);
const [dragStart, setDragStart] = createSignal<{ x: number; y: number } | null>(null);
const [dragCurr, setDragCurr] = createSignal<{ x: number; y: number } | null>(null);
const [dragStartIndex, setDragStartIndex] = createSignal<number>(0);
const [cellWidth, setCellWidth] = createSignal<number>(0);
const [, { setSelectedStripPart }] = useContext(LedStripConfigurationContext);
const move = (targetStart: number) => {
if (targetStart === props.mapper.start) {
return;
}
invoke('move_strip_part', {
displayId: props.strip.display_id,
border: props.strip.border,
targetStart,
}).catch((err) => console.error(err));
};
// reset translateX on config updated
createEffect(() => {
const indexDiff = props.mapper.start - dragStartIndex();
untrack(() => {
if (!dragStart() || !dragCurr()) {
return;
}
const compensation = indexDiff * cellWidth();
batch(() => {
setDragStartIndex(props.mapper.start);
setDragStart({
x: dragStart()!.x + compensation,
y: dragCurr()!.y,
});
});
});
});
const onPointerDown = (ev: PointerEvent) => {
if (ev.button !== 0) {
return;
}
setDragging(true);
setDragStart({ x: ev.clientX, y: ev.clientY });
setDragCurr({ x: ev.clientX, y: ev.clientY });
setDragStartIndex(props.mapper.start);
};
const onPointerUp = () => (ev: PointerEvent) => {
if (ev.button !== 0) {
return;
}
setDragging(false);
};
const onPointerMove = (ev: PointerEvent) => {
setSelectedStripPart({
displayId: props.strip.display_id,
border: props.strip.border,
});
if (!(ev.buttons & 1)) {
return;
}
const draggingInfo = dragging();
if (!draggingInfo) {
return;
}
setDragCurr({ x: ev.clientX, y: ev.clientY });
const cellWidth =
(ev.currentTarget as HTMLDivElement).clientWidth / ledStripStore.totalLedCount;
const diff = ev.clientX - dragStart()!.x;
const moved = Math.round(diff / cellWidth);
if (moved === 0) {
return;
}
setCellWidth(cellWidth);
move(props.mapper.start + moved);
};
const onPointerLeave = () => {
setSelectedStripPart(null);
};
const reverse = () => {
invoke('reverse_led_strip_part', {
displayId: props.strip.display_id,
border: props.strip.border,
}).catch((err) => console.error(err));
};
// update fullLeds
createEffect(() => {
const fullLeds = new Array(ledStripStore.totalLedCount).fill(null);
const colors = ledStripStore.colors;
const { start, end, pos } = props.mapper;
const isForward = start < end;
const step = isForward ? 1 : -1;
for (let i = start, j = pos; i !== end; i += step, j++) {
let c1 = `rgb(${Math.floor(colors[j * 3] * 0.8)}, ${Math.floor(
colors[j * 3 + 1] * 0.8,
)}, ${Math.floor(colors[j * 3 + 2] * 0.8)})`;
let c2 = `rgb(${Math.min(Math.floor(colors[j * 3] * 1.2), 255)}, ${Math.min(
Math.floor(colors[j * 3 + 1] * 1.2),
255,
)}, ${Math.min(Math.floor(colors[j * 3 + 2] * 1.2), 255)})`;
fullLeds[i] = `linear-gradient(70deg, ${c1} 10%, ${c2})`;
}
setFullLeds(fullLeds);
});
const style = createMemo<JSX.CSSProperties>(() => {
return {
transform: `translateX(${(dragCurr()?.x ?? 0) - (dragStart()?.x ?? 0)}px)`,
};
});
return (
<div
class="flex h-2 m-2 select-none cursor-ew-resize focus:cursor-ew-resize"
style={style()}
onPointerMove={onPointerMove}
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onPointerLeave={onPointerLeave}
ondblclick={reverse}
>
<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-100"
classList={{ 'ring-stone-300/50': !it }}
style={{ background: it ?? 'transparent' }}
/>
</div>
)}
</For>
</div>
);
};
const SorterResult: Component = () => {
const [fullLeds, setFullLeds] = createSignal<string[]>([]);
createEffect(() => {
const colors = ledStripStore.sortedColors;
const fullLeds = new Array(ledStripStore.totalLedCount)
.fill('rgba(255,255,255,0.1)')
.map((_, i) => {
let c1 = `rgb(${Math.floor(colors[i * 3] * 0.8)}, ${Math.floor(
colors[i * 3 + 1] * 0.8,
)}, ${Math.floor(colors[i * 3 + 2] * 0.8)})`;
let c2 = `rgb(${Math.min(Math.floor(colors[i * 3] * 1.2), 255)}, ${Math.min(
Math.floor(colors[i * 3 + 1] * 1.2),
255,
)}, ${Math.min(Math.floor(colors[i * 3 + 2] * 1.2), 255)})`;
return `linear-gradient(70deg, ${c1} 10%, ${c2})`;
});
setFullLeds(fullLeds);
});
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 = () => {
return (
<div
class="select-none overflow-hidden"
style={{
'background-image': `url(${background})`,
}}
>
<SorterResult />
<Index each={ledStripStore.strips}>
{(strip, index) => (
<SorterItem strip={strip()} mapper={ledStripStore.mappers[index]} />
)}
</Index>
</div>
);
};

View File

@@ -1,5 +1,3 @@
import { invoke } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event';
import { convertFileSrc } from '@tauri-apps/api/tauri';
import {
Component,
@@ -15,12 +13,6 @@ type ScreenViewProps = {
displayId: number;
} & JSX.HTMLAttributes<HTMLDivElement>;
async function subscribeScreenshotUpdate(displayId: number) {
await invoke('subscribe_encoded_screenshot_updated', {
displayId,
});
}
export const ScreenView: Component<ScreenViewProps> = (props) => {
const [localProps, rootProps] = splitProps(props, ['displayId']);
let canvas: HTMLCanvasElement;
@@ -37,6 +29,7 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
width: number;
height: number;
} | null>(null);
const [hidden, setHidden] = createSignal(false);
const resetSize = () => {
const aspectRatio = canvas.width / canvas.height;
@@ -81,56 +74,49 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
}
};
// get screenshot
createEffect(() => {
let pendingCount = 0;
const unlisten = listen<{
base64_image: string;
display_id: number;
height: number;
width: number;
}>('encoded-screenshot-updated', (event) => {
let stopped = false;
const frame = async () => {
const { drawWidth, drawHeight } = drawInfo();
if (event.payload.display_id === localProps.displayId) {
const url = convertFileSrc(
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
'ambient-light',
);
if (pendingCount >= 1) {
return;
}
pendingCount++;
fetch(url, {
mode: 'cors',
})
.then((res) => res.body?.getReader().read())
.then((buffer) => {
if (buffer?.value) {
setImageData({
buffer: new Uint8ClampedArray(buffer?.value),
width: drawWidth,
height: drawHeight,
});
} else {
setImageData(null);
}
draw();
})
.finally(() => {
pendingCount--;
});
}
const url = convertFileSrc(
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
'ambient-light',
);
await fetch(url, {
mode: 'cors',
})
.then((res) => res.body?.getReader().read())
.then((buffer) => {
if (buffer?.value) {
setImageData({
buffer: new Uint8ClampedArray(buffer?.value),
width: drawWidth,
height: drawHeight,
});
} else {
setImageData(null);
}
draw();
});
};
// console.log(event.payload.display_id, localProps.displayId);
});
subscribeScreenshotUpdate(localProps.displayId);
(async () => {
while (!stopped) {
if (hidden()) {
await new Promise((resolve) => setTimeout(resolve, 1000));
continue;
}
await frame();
}
})();
onCleanup(() => {
unlisten.then((unlisten) => {
unlisten();
});
stopped = true;
});
});
// resize
createEffect(() => {
let resizeObserver: ResizeObserver;
@@ -146,6 +132,26 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
});
});
// update hidden
createEffect(() => {
const hide = () => {
setHidden(true);
console.log('hide');
};
const show = () => {
setHidden(false);
console.log('show');
};
window.addEventListener('focus', show);
window.addEventListener('blur', hide);
onCleanup(() => {
window.removeEventListener('focus', show);
window.removeEventListener('blur', hide);
});
});
return (
<div
ref={root!}
@@ -153,6 +159,7 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
class={'overflow-hidden h-full w-full ' + rootProps.class}
>
<canvas ref={canvas!} />
{rootProps.children}
</div>
);
};

View File

@@ -0,0 +1,24 @@
import { createContext } from 'solid-js';
import { Borders } from '../constants/border';
export type LedStripConfigurationContextType = [
{
selectedStripPart: {
displayId: number;
border: Borders;
} | null;
},
{
setSelectedStripPart: (v: { displayId: number; border: Borders } | null) => void;
},
];
export const LedStripConfigurationContext =
createContext<LedStripConfigurationContextType>([
{
selectedStripPart: null,
},
{
setSelectedStripPart: () => {},
},
]);

View File

@@ -1,16 +0,0 @@
import { Borders } from '../constants/border';
import { LedStripConfig } from './led-strip-config';
export class LedStripConfigOfBorders implements Record<Borders, LedStripConfig | null> {
constructor(
public top: LedStripConfig | null = null,
public bottom: LedStripConfig | null = null,
public left: LedStripConfig | null = null,
public right: LedStripConfig | null = null,
) {}
}
export class DisplayConfig {
led_strip_of_borders = new LedStripConfigOfBorders();
constructor(public id: number) {}
}

View File

@@ -1,10 +1,20 @@
import { Borders } from '../constants/border';
export type LedStripPixelMapper = {
start: number;
end: number;
pos: 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 +0,0 @@
import { DisplayConfig } from './display-config';
export class PickerConfiguration {
constructor(
public display_configs: DisplayConfig[] = [],
public config_version: number = 1,
) {}
}

View File

@@ -1,12 +0,0 @@
import { DisplayConfig } from './display-config';
export class ScreenshotDto {
encode_image!: string;
config!: DisplayConfig;
colors!: {
top: Uint8Array;
bottom: Uint8Array;
left: Uint8Array;
right: Uint8Array;
};
}

View File

@@ -1,8 +1,12 @@
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(),
sortedColors: new Uint8ClampedArray(),
get totalLedCount() {
return Math.max(0, ...ledStripStore.mappers.map((m) => Math.max(m.start, m.end)));
},
});