Compare commits
20 Commits
9ed2fa8b53
...
boardcast-
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c3ce607e0 | |||
| 3ec983cd95 | |||
| a1e6c6e5fb | |||
| 822d470605 | |||
| fa5e27f72a | |||
| 86e9b072bc | |||
| 535f731770 | |||
| 9ec030488a | |||
| 0d47911355 | |||
| 5893c4344c | |||
| d053185cc2 | |||
| 47e30ec94a | |||
| 56137b52a5 | |||
| 85a00cf4f2 | |||
| 4e75aa4307 | |||
| 958a422672 | |||
| 58e8c30fe2 | |||
| 3e54d30498 | |||
| 1a3102257e | |||
| 3ede04c31b |
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"files.autoSave": "onWindowChange"
|
"files.autoSave": "onWindowChange",
|
||||||
|
"cSpell.words": [
|
||||||
|
"Itertools",
|
||||||
|
"Leds"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
29
.vscode/tasks.json
vendored
Normal file
29
.vscode/tasks.json
vendored
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@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": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.2.2",
|
"@tauri-apps/cli": "^1.2.2",
|
||||||
|
|||||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -7,7 +7,9 @@ specifiers:
|
|||||||
autoprefixer: ^10.4.14
|
autoprefixer: ^10.4.14
|
||||||
postcss: ^8.4.21
|
postcss: ^8.4.21
|
||||||
solid-js: ^1.4.7
|
solid-js: ^1.4.7
|
||||||
|
solid-tippy: ^0.2.1
|
||||||
tailwindcss: ^3.2.7
|
tailwindcss: ^3.2.7
|
||||||
|
tippy.js: ^6.3.7
|
||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
vite: ^4.0.0
|
vite: ^4.0.0
|
||||||
vite-plugin-solid: ^2.3.0
|
vite-plugin-solid: ^2.3.0
|
||||||
@@ -15,6 +17,8 @@ specifiers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 1.2.0
|
'@tauri-apps/api': 1.2.0
|
||||||
solid-js: 1.6.14
|
solid-js: 1.6.14
|
||||||
|
solid-tippy: 0.2.1_rick4tq22pxy45qudhdfrizose
|
||||||
|
tippy.js: 6.3.7
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tauri-apps/cli': 1.2.3
|
'@tauri-apps/cli': 1.2.3
|
||||||
@@ -603,6 +607,10 @@ packages:
|
|||||||
fastq: 1.15.0
|
fastq: 1.15.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@popperjs/core/2.11.7:
|
||||||
|
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tauri-apps/api/1.2.0:
|
/@tauri-apps/api/1.2.0:
|
||||||
resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==}
|
resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==}
|
||||||
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||||
@@ -1329,6 +1337,17 @@ packages:
|
|||||||
solid-js: 1.6.14
|
solid-js: 1.6.14
|
||||||
dev: true
|
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:
|
/source-map-js/1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1380,6 +1399,12 @@ packages:
|
|||||||
- ts-node
|
- ts-node
|
||||||
dev: true
|
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:
|
/to-fast-properties/2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|||||||
498
src-tauri/Cargo.lock
generated
498
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -16,11 +16,8 @@ tauri-build = { version = "1.2", features = [] }
|
|||||||
tauri = { version = "1.2", features = ["shell-open"] }
|
tauri = { version = "1.2", features = ["shell-open"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
webp = "0.2.2"
|
|
||||||
base64 = "0.21.0"
|
|
||||||
core-graphics = "0.22.3"
|
core-graphics = "0.22.3"
|
||||||
display-info = "0.4.1"
|
display-info = "0.4.1"
|
||||||
png = "0.17.7"
|
|
||||||
anyhow = "1.0.69"
|
anyhow = "1.0.69"
|
||||||
tokio = {version = "1.26.0", features = ["full"] }
|
tokio = {version = "1.26.0", features = ["full"] }
|
||||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||||
@@ -28,6 +25,13 @@ log = "0.4.17"
|
|||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
percent-encoding = "2.2.0"
|
percent-encoding = "2.2.0"
|
||||||
url-build-parse = "9.0.0"
|
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]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
|||||||
133
src-tauri/src/ambient_light/config.rs
Normal file
133
src-tauri/src/ambient_light/config.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use std::env::current_dir;
|
||||||
|
|
||||||
|
use display_info::DisplayInfo;
|
||||||
|
use paris::{error, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::api::path::config_dir;
|
||||||
|
|
||||||
|
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,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LedStripConfig {
|
||||||
|
pub index: usize,
|
||||||
|
pub border: Border,
|
||||||
|
pub display_id: u32,
|
||||||
|
pub start_pos: usize,
|
||||||
|
pub len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(CONFIG_FILE_NAME);
|
||||||
|
|
||||||
|
let exists = tokio::fs::try_exists(path.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to check config file exists: {}", e))?;
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
let config = tokio::fs::read_to_string(path).await?;
|
||||||
|
|
||||||
|
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");
|
||||||
|
Ok(Self::get_default_config().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_config(configs: &Self) -> anyhow::Result<()> {
|
||||||
|
let path = config_dir()
|
||||||
|
.unwrap_or(current_dir().unwrap())
|
||||||
|
.join(CONFIG_FILE_NAME);
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(path.parent().unwrap()).await?;
|
||||||
|
|
||||||
|
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<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 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 item = LedStripConfig {
|
||||||
|
index: j + i * 4,
|
||||||
|
display_id: display.id,
|
||||||
|
border: match j {
|
||||||
|
0 => Border::Top,
|
||||||
|
1 => Border::Bottom,
|
||||||
|
2 => Border::Left,
|
||||||
|
3 => Border::Right,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
start_pos: j + i * 4 * 30,
|
||||||
|
len: 30,
|
||||||
|
};
|
||||||
|
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(Self { strips, mappers })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SamplePointMapper {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SamplePointConfig {
|
||||||
|
pub display_id: u32,
|
||||||
|
pub points: Vec<LedSamplePoints>,
|
||||||
|
}
|
||||||
226
src-tauri/src/ambient_light/config_manager.rs
Normal file
226
src-tauri/src/ambient_light/config_manager.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src-tauri/src/ambient_light/mod.rs
Normal file
7
src-tauri/src/ambient_light/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
mod config;
|
||||||
|
mod config_manager;
|
||||||
|
mod publisher;
|
||||||
|
|
||||||
|
pub use config::*;
|
||||||
|
pub use config_manager::*;
|
||||||
|
pub use publisher::*;
|
||||||
459
src-tauri/src/ambient_light/publisher.rs
Normal file
459
src-tauri/src/ambient_light/publisher.rs
Normal 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>>,
|
||||||
|
}
|
||||||
13
src-tauri/src/display/brightness.rs
Normal file
13
src-tauri/src/display/brightness.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum Brightness {
|
||||||
|
Relative(i16),
|
||||||
|
Absolute(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayBrightness {
|
||||||
|
pub brightness: Brightness,
|
||||||
|
pub display_index: usize,
|
||||||
|
}
|
||||||
36
src-tauri/src/display/display_config.rs
Normal file
36
src-tauri/src/display/display_config.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayConfig {
|
||||||
|
pub id: usize,
|
||||||
|
pub brightness: u16,
|
||||||
|
pub max_brightness: u16,
|
||||||
|
pub min_brightness: u16,
|
||||||
|
pub contrast: u16,
|
||||||
|
pub max_contrast: u16,
|
||||||
|
pub min_contrast: u16,
|
||||||
|
pub mode: u16,
|
||||||
|
pub max_mode: u16,
|
||||||
|
pub min_mode: u16,
|
||||||
|
pub last_modified_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayConfig {
|
||||||
|
pub fn default(index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
id: index,
|
||||||
|
brightness: 30,
|
||||||
|
contrast: 50,
|
||||||
|
mode: 0,
|
||||||
|
last_modified_at: SystemTime::now(),
|
||||||
|
max_brightness: 100,
|
||||||
|
min_brightness: 0,
|
||||||
|
max_contrast: 100,
|
||||||
|
min_contrast: 0,
|
||||||
|
max_mode: 15,
|
||||||
|
min_mode: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
src-tauri/src/display/manager.rs
Normal file
187
src-tauri/src/display/manager.rs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::HashMap,
|
||||||
|
ops::Sub,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::Config;
|
||||||
|
use ddc_hi::Display;
|
||||||
|
use paris::{error, info, warn};
|
||||||
|
use tauri::async_runtime::Mutex;
|
||||||
|
use tokio::sync::{broadcast, OwnedMutexGuard};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{display::Brightness, models, rpc};
|
||||||
|
|
||||||
|
use super::{display_config::DisplayConfig, DisplayBrightness};
|
||||||
|
use ddc_hi::Ddc;
|
||||||
|
|
||||||
|
pub struct Manager {
|
||||||
|
displays: Arc<Mutex<HashMap<usize, Arc<Mutex<DisplayConfig>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn global() -> &'static Self {
|
||||||
|
static DISPLAY_MANAGER: once_cell::sync::OnceCell<Manager> =
|
||||||
|
once_cell::sync::OnceCell::new();
|
||||||
|
|
||||||
|
DISPLAY_MANAGER.get_or_init(|| Self::create())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create() -> Self {
|
||||||
|
let instance = Self {
|
||||||
|
displays: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn subscribe_display_brightness(&self) {
|
||||||
|
let rpc = rpc::Manager::global().await;
|
||||||
|
|
||||||
|
let mut rx = rpc.client().subscribe_change_display_brightness_rx();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Ok(display_brightness) = rx.recv().await {
|
||||||
|
if let Err(err) = self.set_display_brightness(display_brightness).await {
|
||||||
|
error!("set_display_brightness failed. {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_display_config_by_ddc(index: usize) -> anyhow::Result<DisplayConfig> {
|
||||||
|
let mut displays = Display::enumerate();
|
||||||
|
match displays.get_mut(index) {
|
||||||
|
Some(display) => {
|
||||||
|
let mut config = DisplayConfig::default(index);
|
||||||
|
match display.handle.get_vcp_feature(0x10) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.max_brightness = value.maximum();
|
||||||
|
config.min_brightness = 0;
|
||||||
|
config.brightness = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
match display.handle.get_vcp_feature(0x12) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.max_contrast = value.maximum();
|
||||||
|
config.min_contrast = 0;
|
||||||
|
config.contrast = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
match display.handle.get_vcp_feature(0xdc) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.max_mode = value.maximum();
|
||||||
|
config.min_mode = 0;
|
||||||
|
config.mode = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
None => anyhow::bail!("display#{} is missed.", index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_display(&self, index: usize) -> anyhow::Result<OwnedMutexGuard<DisplayConfig>> {
|
||||||
|
let mut displays = self.displays.lock().await;
|
||||||
|
match displays.get_mut(&index) {
|
||||||
|
Some(config) => {
|
||||||
|
let mut config = config.to_owned().lock_owned().await;
|
||||||
|
if config.last_modified_at > SystemTime::now().sub(Duration::from_secs(10)) {
|
||||||
|
info!("cached");
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
return match Self::read_display_config_by_ddc(index) {
|
||||||
|
Ok(config) => {
|
||||||
|
let id = config.id;
|
||||||
|
let value = Arc::new(Mutex::new(config));
|
||||||
|
let valueGuard = value.clone().lock_owned().await;
|
||||||
|
displays.insert(id, value);
|
||||||
|
info!("read form ddc");
|
||||||
|
Ok(valueGuard)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"can not read config from display by ddc, use CACHED value. {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
config.last_modified_at = SystemTime::now();
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let config = Self::read_display_config_by_ddc(index).map_err(|err| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"can not read config from display by ddc,use DEFAULT value. {:?}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let id = config.id;
|
||||||
|
let value = Arc::new(Mutex::new(config));
|
||||||
|
let valueGuard = value.clone().lock_owned().await;
|
||||||
|
displays.insert(id, value);
|
||||||
|
Ok(valueGuard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_display_brightness(
|
||||||
|
&self,
|
||||||
|
display_brightness: DisplayBrightness,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match Display::enumerate().get_mut(display_brightness.display_index) {
|
||||||
|
Some(display) => {
|
||||||
|
match self.get_display(display_brightness.display_index).await {
|
||||||
|
Ok(mut config) => {
|
||||||
|
let curr = config.brightness;
|
||||||
|
info!("curr_brightness: {:?}", curr);
|
||||||
|
let mut target = match display_brightness.brightness {
|
||||||
|
Brightness::Relative(v) => curr.wrapping_add_signed(v),
|
||||||
|
Brightness::Absolute(v) => v,
|
||||||
|
};
|
||||||
|
if target.gt(&config.max_brightness) {
|
||||||
|
target = config.max_brightness;
|
||||||
|
} else if target.lt(&config.min_brightness) {
|
||||||
|
target = config.min_brightness;
|
||||||
|
}
|
||||||
|
config.brightness = target;
|
||||||
|
display
|
||||||
|
.handle
|
||||||
|
.set_vcp_feature(0x10, target as u16)
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not set brightness. {:?}", err))?;
|
||||||
|
|
||||||
|
let rpc = rpc::Manager::global().await;
|
||||||
|
|
||||||
|
rpc.publish_desktop_cmd(
|
||||||
|
format!("display{}/brightness", display_brightness.display_index)
|
||||||
|
.as_str(),
|
||||||
|
target.to_be_bytes().to_vec(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
info!(
|
||||||
|
"can not get display#{} brightness. {:?}",
|
||||||
|
display_brightness.display_index, err
|
||||||
|
);
|
||||||
|
if let Brightness::Absolute(v) = display_brightness.brightness {
|
||||||
|
display.handle.set_vcp_feature(0x10, v).map_err(|err| {
|
||||||
|
anyhow::anyhow!("can not set brightness. {:?}", err)
|
||||||
|
})?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("display#{} is not found.", display_brightness.display_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src-tauri/src/display/mod.rs
Normal file
11
src-tauri/src/display/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// mod brightness;
|
||||||
|
// mod manager;
|
||||||
|
mod display_config;
|
||||||
|
|
||||||
|
pub use display_config::*;
|
||||||
|
|
||||||
|
// pub use brightness::*;
|
||||||
|
// pub use manager::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
54
src-tauri/src/led_color.rs
Normal file
54
src-tauri/src/led_color.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use color_space::{Hsv, Rgb};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LedColor {
|
||||||
|
bits: [u8; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedColor {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self { bits: [0, 0, 0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self { bits: [r, g, b] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
||||||
|
let rgb = Rgb::from(Hsv::new(h, s, v));
|
||||||
|
Self { bits: [rgb.r as u8, rgb.g as u8, rgb.b as u8] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rgb(&self) -> [u8; 3] {
|
||||||
|
self.bits
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.bits.iter().any(|bit| *bit == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
|
self.bits = [r, g, b];
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
|
self.bits = [
|
||||||
|
(self.bits[0] / 2 + r / 2),
|
||||||
|
(self.bits[1] / 2 + g / 2),
|
||||||
|
(self.bits[2] / 2 + b / 2),
|
||||||
|
];
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for LedColor {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let hex = format!("#{}", hex::encode(self.bits));
|
||||||
|
serializer.serialize_str(hex.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod ambient_light;
|
||||||
|
mod display;
|
||||||
|
mod led_color;
|
||||||
|
mod rpc;
|
||||||
pub mod screenshot;
|
pub mod screenshot;
|
||||||
mod screenshot_manager;
|
mod screenshot_manager;
|
||||||
|
|
||||||
use base64::Engine;
|
use ambient_light::{Border, LedColorsPublisher, LedStripConfig, LedStripConfigGroup};
|
||||||
use core_graphics::display::{
|
use core_graphics::display::{
|
||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||||
};
|
};
|
||||||
use display_info::DisplayInfo;
|
use display_info::DisplayInfo;
|
||||||
use paris::{error, info};
|
use paris::{error, info, warn};
|
||||||
|
use screenshot::Screenshot;
|
||||||
use screenshot_manager::ScreenshotManager;
|
use screenshot_manager::ScreenshotManager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::to_string;
|
use serde_json::to_string;
|
||||||
use tauri::{http::ResponseBuilder, regex};
|
use tauri::{http::ResponseBuilder, regex, Manager};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(remote = "DisplayInfo")]
|
#[serde(remote = "DisplayInfo")]
|
||||||
@@ -53,103 +58,117 @@ fn list_display_info() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn take_screenshot(display_id: u32, scale_factor: f32) -> Result<String, String> {
|
async fn read_led_strip_configs() -> Result<LedStripConfigGroup, String> {
|
||||||
let exec = || {
|
let config = ambient_light::LedStripConfigGroup::read_config()
|
||||||
println!("take_screenshot");
|
.await
|
||||||
let start_at = std::time::Instant::now();
|
.map_err(|e| {
|
||||||
|
error!("can not read led strip configs: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
let cg_display = CGDisplay::new(display_id);
|
#[tauri::command]
|
||||||
let cg_image = CGDisplay::screenshot(
|
async fn write_led_strip_configs(
|
||||||
cg_display.bounds(),
|
configs: Vec<ambient_light::LedStripConfig>,
|
||||||
kCGWindowListOptionOnScreenOnly,
|
) -> Result<(), String> {
|
||||||
kCGNullWindowID,
|
let config_manager = ambient_light::ConfigManager::global().await;
|
||||||
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();
|
config_manager.set_items(configs).await.map_err(|e| {
|
||||||
let bytes_per_row = cg_image.bytes_per_row() as f32;
|
error!("can not write led strip configs: {}", e);
|
||||||
|
|
||||||
let height = cg_image.height();
|
|
||||||
let width = cg_image.width();
|
|
||||||
|
|
||||||
let image_height = (height as f32 / scale_factor) as u32;
|
|
||||||
let image_width = (width as f32 / scale_factor) as u32;
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "raw image: {}x{}, output image: {}x{}",
|
|
||||||
// width, height, image_width, image_height
|
|
||||||
// );
|
|
||||||
// // from bitmap vec
|
|
||||||
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
|
|
||||||
|
|
||||||
for y in 0..image_height {
|
|
||||||
for x in 0..image_width {
|
|
||||||
let offset =
|
|
||||||
(((y as f32) * bytes_per_row + (x as f32) * 4.0) * scale_factor) as usize;
|
|
||||||
let b = buffer[offset];
|
|
||||||
let g = buffer[offset + 1];
|
|
||||||
let r = buffer[offset + 2];
|
|
||||||
let offset = (y * image_width + x) as usize;
|
|
||||||
image_buffer[offset * 3] = r;
|
|
||||||
image_buffer[offset * 3 + 1] = g;
|
|
||||||
image_buffer[offset * 3 + 2] = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"convert to image buffer took {}ms",
|
|
||||||
start_at.elapsed().as_millis()
|
|
||||||
);
|
|
||||||
|
|
||||||
// to png image
|
|
||||||
// let mut image_png = Vec::new();
|
|
||||||
// let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
|
|
||||||
// encoder.set_color(png::ColorType::Rgb);
|
|
||||||
// encoder.set_depth(png::BitDepth::Eight);
|
|
||||||
|
|
||||||
// let mut writer = encoder
|
|
||||||
// .write_header()
|
|
||||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
// writer
|
|
||||||
// .write_image_data(&image_buffer)
|
|
||||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
// writer
|
|
||||||
// .finish()
|
|
||||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
// println!("encode to png took {}ms", start_at.elapsed().as_millis());
|
|
||||||
let image_webp =
|
|
||||||
webp::Encoder::from_rgb(&image_buffer, image_width, image_height).encode(90f32);
|
|
||||||
// // base64 image
|
|
||||||
let mut image_base64 = String::new();
|
|
||||||
image_base64.push_str("data:image/webp;base64,");
|
|
||||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_webp);
|
|
||||||
image_base64.push_str(encoded.as_str());
|
|
||||||
|
|
||||||
println!("took {}ms", start_at.elapsed().as_millis());
|
|
||||||
println!("image_base64: {}", image_base64.len());
|
|
||||||
|
|
||||||
Ok(image_base64)
|
|
||||||
};
|
|
||||||
|
|
||||||
exec().map_err(|e: anyhow::Error| {
|
|
||||||
println!("error: {}", e);
|
|
||||||
e.to_string()
|
e.to_string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn subscribe_encoded_screenshot_updated(
|
async fn get_led_strips_sample_points(
|
||||||
window: tauri::Window,
|
config: LedStripConfig,
|
||||||
display_id: u32,
|
) -> Result<Vec<screenshot::LedSamplePoints>, String> {
|
||||||
) -> Result<(), String> {
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
let screenshot_manager = ScreenshotManager::global().await;
|
||||||
screenshot_manager
|
let channels = screenshot_manager.channels.read().await;
|
||||||
.subscribe_encoded_screenshot_updated(window, display_id)
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_one_edge_colors(
|
||||||
|
display_id: u32,
|
||||||
|
sample_points: Vec<screenshot::LedSamplePoints>,
|
||||||
|
) -> Result<Vec<led_color::LedColor>, String> {
|
||||||
|
let screenshot_manager = ScreenshotManager::global().await;
|
||||||
|
let channels = screenshot_manager.channels.read().await;
|
||||||
|
if let Some(rx) = channels.get(&display_id) {
|
||||||
|
let rx = rx.clone();
|
||||||
|
let screenshot = rx.borrow().clone();
|
||||||
|
let bytes = screenshot.bytes.read().await.to_owned();
|
||||||
|
let colors =
|
||||||
|
Screenshot::get_one_edge_colors(&sample_points, &bytes, screenshot.bytes_per_row);
|
||||||
|
Ok(colors)
|
||||||
|
} else {
|
||||||
|
Err(format!("display not found: {}", display_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|e| {
|
||||||
error!("subscribe_encoded_screenshot_updated: {}", err);
|
error!("can not patch led strip len: {}", e);
|
||||||
err.to_string()
|
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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,12 +178,22 @@ async fn main() {
|
|||||||
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
let screenshot_manager = ScreenshotManager::global().await;
|
||||||
screenshot_manager.start().unwrap();
|
screenshot_manager.start().unwrap();
|
||||||
|
|
||||||
|
let led_color_publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
|
led_color_publisher.start();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
take_screenshot,
|
|
||||||
list_display_info,
|
list_display_info,
|
||||||
subscribe_encoded_screenshot_updated
|
read_led_strip_configs,
|
||||||
|
write_led_strip_configs,
|
||||||
|
get_led_strips_sample_points,
|
||||||
|
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| {
|
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
|
||||||
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
||||||
@@ -212,20 +241,33 @@ async fn main() {
|
|||||||
let screenshot = rx.borrow().clone();
|
let screenshot = rx.borrow().clone();
|
||||||
let bytes = screenshot.bytes.read().await;
|
let bytes = screenshot.bytes.read().await;
|
||||||
|
|
||||||
let (scale_factor, width, height) = if url.query.is_some()
|
let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some()
|
||||||
&& url.query.as_ref().unwrap().contains_key("height")
|
&& url.query.as_ref().unwrap().contains_key("height")
|
||||||
&& url.query.as_ref().unwrap().contains_key("width")
|
&& url.query.as_ref().unwrap().contains_key("width")
|
||||||
{
|
{
|
||||||
let width =
|
let width = url.query.as_ref().unwrap()["width"]
|
||||||
url.query.as_ref().unwrap()["width"].parse::<u32>().unwrap();
|
.parse::<u32>()
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!("width parse error: {}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
let height = url.query.as_ref().unwrap()["height"]
|
let height = url.query.as_ref().unwrap()["height"]
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.unwrap();
|
.map_err(|err| {
|
||||||
(screenshot.width as f32 / width as f32, width, height)
|
warn!("height parse error: {}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
(
|
||||||
|
screenshot.width as f32 / width as f32,
|
||||||
|
screenshot.height as f32 / height as f32,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
log::debug!("scale by scale_factor");
|
log::debug!("scale by scale_factor");
|
||||||
let scale_factor = screenshot.scale_factor;
|
let scale_factor = screenshot.scale_factor;
|
||||||
(
|
(
|
||||||
|
scale_factor,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
(screenshot.width as f32 / scale_factor) as u32,
|
(screenshot.width as f32 / scale_factor) as u32,
|
||||||
(screenshot.height as f32 / scale_factor) as u32,
|
(screenshot.height as f32 / scale_factor) as u32,
|
||||||
@@ -245,8 +287,9 @@ async fn main() {
|
|||||||
|
|
||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
let offset = ((y as f32) * scale_factor) as usize * bytes_per_row as usize
|
let offset = ((y as f32) * scale_factor_y).floor() as usize
|
||||||
+ ((x as f32) * scale_factor) as usize * 4;
|
* bytes_per_row as usize
|
||||||
|
+ ((x as f32) * scale_factor_x).floor() as usize * 4;
|
||||||
let b = bytes[offset];
|
let b = bytes[offset];
|
||||||
let g = bytes[offset + 1];
|
let g = bytes[offset + 1];
|
||||||
let r = bytes[offset + 2];
|
let r = bytes[offset + 2];
|
||||||
@@ -279,6 +322,63 @@ async fn main() {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.body(err.to_string().into_bytes());
|
.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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
3
src-tauri/src/rpc/mod.rs
Normal file
3
src-tauri/src/rpc/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod mqtt;
|
||||||
|
|
||||||
|
pub use mqtt::*;
|
||||||
241
src-tauri/src/rpc/mqtt.rs
Normal file
241
src-tauri/src/rpc/mqtt.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::{iter, cell::Ref};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::async_runtime::RwLock;
|
use tauri::async_runtime::{RwLock, Mutex};
|
||||||
|
|
||||||
|
use crate::{ambient_light::LedStripConfig, led_color::LedColor};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Screenshot {
|
pub struct Screenshot {
|
||||||
@@ -13,6 +17,8 @@ pub struct Screenshot {
|
|||||||
pub scale_factor: f32,
|
pub scale_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SINGLE_AXIS_POINTS: usize = 5;
|
||||||
|
|
||||||
impl Screenshot {
|
impl Screenshot {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
display_id: u32,
|
display_id: u32,
|
||||||
@@ -31,12 +37,158 @@ impl Screenshot {
|
|||||||
scale_factor,
|
scale_factor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 / 18, width, config.len, SINGLE_AXIS_POINTS)
|
||||||
|
}
|
||||||
|
crate::ambient_light::Border::Bottom => {
|
||||||
|
let points = Self::get_one_edge_sample_points(height / 18, width, config.len, SINGLE_AXIS_POINTS);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (x, height - y)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
crate::ambient_light::Border::Left => {
|
||||||
|
let points = Self::get_one_edge_sample_points(width / 32, height, config.len, SINGLE_AXIS_POINTS);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (y, x)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
crate::ambient_light::Border::Right => {
|
||||||
|
let points = Self::get_one_edge_sample_points(width / 32, height, config.len, SINGLE_AXIS_POINTS);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (width - y, x)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
let point_start_y = cell_size_y / 2;
|
||||||
|
let point_start_x = cell_size_x / 2.0;
|
||||||
|
let point_y_list: Vec<usize> = (point_start_y..width).step_by(cell_size_y).collect();
|
||||||
|
let point_x_list: Vec<usize> = iter::successors(Some(point_start_x), |i| {
|
||||||
|
let next = i + cell_size_x;
|
||||||
|
(next < (length as f64)).then_some(next)
|
||||||
|
})
|
||||||
|
.map(|i| i as usize)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let points: Vec<Point> = point_x_list
|
||||||
|
.iter()
|
||||||
|
.map(|&x| point_y_list.iter().map(move |&y| (x, y)))
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
points
|
||||||
|
.chunks(single_axis_points * single_axis_points)
|
||||||
|
.into_iter()
|
||||||
|
.map(|points| Vec::from(points))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_one_edge_colors(
|
||||||
|
sample_points_of_leds: &Vec<LedSamplePoints>,
|
||||||
|
bitmap: &Vec<u8>,
|
||||||
|
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 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>;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ScreenSamplePoints {
|
||||||
|
pub top: Vec<LedSamplePoints>,
|
||||||
|
pub bottom: Vec<LedSamplePoints>,
|
||||||
|
pub left: Vec<LedSamplePoints>,
|
||||||
|
pub right: Vec<LedSamplePoints>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DisplayColorsOfLedStrips {
|
||||||
|
pub top: Vec<u8>,
|
||||||
|
pub bottom: Vec<u8>,
|
||||||
|
pub left: Vec<u8>,
|
||||||
|
pub right: Vec<u8>,
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct ScreenshotPayload {
|
pub struct ScreenshotPayload {
|
||||||
pub display_id: u32,
|
pub display_id: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
// pub base64_image: String,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
|
use std::cell::{Ref, RefCell};
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use core_graphics::display::{
|
use core_graphics::display::{
|
||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||||
};
|
};
|
||||||
use paris::{error, info, warn};
|
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
|
||||||
use tauri::{async_runtime::RwLock, Window};
|
use paris::warn;
|
||||||
use tokio::sync::{watch, OnceCell};
|
use tauri::async_runtime::RwLock;
|
||||||
|
use tokio::sync::{broadcast, watch, OnceCell};
|
||||||
|
use tokio::time::{self, Duration};
|
||||||
|
|
||||||
use crate::screenshot::{Screenshot, ScreenshotPayload};
|
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> {
|
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
|
||||||
log::debug!("take_screenshot");
|
log::debug!("take_screenshot");
|
||||||
// let start_at = std::time::Instant::now();
|
|
||||||
|
|
||||||
let cg_display = CGDisplay::new(display_id);
|
let cg_display = CGDisplay::new(display_id);
|
||||||
let cg_image = CGDisplay::screenshot(
|
let cg_image = CGDisplay::screenshot(
|
||||||
@@ -22,7 +28,6 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
kCGWindowImageDefault,
|
kCGWindowImageDefault,
|
||||||
)
|
)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
|
.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 buffer = cg_image.data();
|
||||||
let bytes_per_row = cg_image.bytes_per_row();
|
let bytes_per_row = cg_image.bytes_per_row();
|
||||||
@@ -30,8 +35,7 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
let height = cg_image.height();
|
let height = cg_image.height();
|
||||||
let width = cg_image.width();
|
let width = cg_image.width();
|
||||||
|
|
||||||
let mut bytes = vec![0u8; buffer.len() as usize];
|
let bytes = buffer.bytes().to_owned();
|
||||||
bytes.copy_from_slice(&buffer);
|
|
||||||
|
|
||||||
Ok(Screenshot::new(
|
Ok(Screenshot::new(
|
||||||
display_id,
|
display_id,
|
||||||
@@ -43,9 +47,74 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 struct ScreenshotManager {
|
||||||
pub channels: Arc<RwLock<HashMap<u32, watch::Receiver<Screenshot>>>>,
|
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 {
|
impl ScreenshotManager {
|
||||||
@@ -55,10 +124,10 @@ impl ScreenshotManager {
|
|||||||
SCREENSHOT_MANAGER
|
SCREENSHOT_MANAGER
|
||||||
.get_or_init(|| async {
|
.get_or_init(|| async {
|
||||||
let channels = Arc::new(RwLock::new(HashMap::new()));
|
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 {
|
Self {
|
||||||
channels,
|
channels,
|
||||||
encode_listeners,
|
merged_screenshot_tx: Arc::new(RwLock::new(merged_screenshot_tx)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -74,6 +143,7 @@ impl ScreenshotManager {
|
|||||||
|
|
||||||
fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> {
|
fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> {
|
||||||
let channels = self.channels.to_owned();
|
let channels = self.channels.to_owned();
|
||||||
|
let merged_screenshot_tx = self.merged_screenshot_tx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let screenshot = take_screenshot(display_id, scale_factor);
|
let screenshot = take_screenshot(display_id, scale_factor);
|
||||||
|
|
||||||
@@ -81,172 +151,124 @@ impl ScreenshotManager {
|
|||||||
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
|
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let mut interval = time::interval(Duration::from_millis(3300));
|
||||||
|
let mut start = tokio::time::Instant::now();
|
||||||
|
|
||||||
let screenshot = screenshot.unwrap();
|
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;
|
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 {
|
loop {
|
||||||
Self::take_screenshot_loop(display_id, scale_factor, &tx).await;
|
start = tokio::time::Instant::now();
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
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(())
|
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(
|
async fn take_screenshot_loop(
|
||||||
display_id: u32,
|
display_id: u32,
|
||||||
scale_factor: f32,
|
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);
|
let screenshot = take_screenshot(display_id, scale_factor);
|
||||||
if let Ok(screenshot) = screenshot {
|
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 {
|
} else {
|
||||||
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
|
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn encode_screenshot_to_base64(screenshot: &Screenshot) -> anyhow::Result<String> {
|
pub async fn get_all_colors(
|
||||||
let bytes = screenshot.bytes.read().await;
|
&self,
|
||||||
|
configs: &Vec<SamplePointConfig>,
|
||||||
|
screenshots: &Vec<&Screenshot>,
|
||||||
|
) -> Vec<LedColor> {
|
||||||
|
let mut all_colors = vec![];
|
||||||
|
|
||||||
let scale_factor = screenshot.scale_factor;
|
for (index, screenshot) in screenshots.iter().enumerate() {
|
||||||
|
let config = &configs[index];
|
||||||
|
let mut colors = screenshot.get_colors_by_sample_points(&config.points).await;
|
||||||
|
|
||||||
let image_height = (screenshot.height as f32 / scale_factor) as u32;
|
all_colors.append(&mut colors);
|
||||||
let image_width = (screenshot.width as f32 / scale_factor) as u32;
|
|
||||||
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
|
|
||||||
|
|
||||||
for y in 0..image_height {
|
|
||||||
for x in 0..image_width {
|
|
||||||
let offset = (((y as f32) * screenshot.bytes_per_row as f32 + (x as f32) * 4.0)
|
|
||||||
* scale_factor) as usize;
|
|
||||||
let b = bytes[offset];
|
|
||||||
let g = bytes[offset + 1];
|
|
||||||
let r = bytes[offset + 2];
|
|
||||||
let offset = (y * image_width + x) as usize;
|
|
||||||
image_buffer[offset * 3] = r;
|
|
||||||
image_buffer[offset * 3 + 1] = g;
|
|
||||||
image_buffer[offset * 3 + 2] = b;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut image_png = Vec::new();
|
all_colors
|
||||||
let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
|
}
|
||||||
encoder.set_color(png::ColorType::Rgb);
|
|
||||||
encoder.set_depth(png::BitDepth::Eight);
|
|
||||||
|
|
||||||
let mut writer = encoder
|
pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> {
|
||||||
.write_header()
|
let total_leds = mappers
|
||||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
.iter()
|
||||||
writer
|
.map(|mapper| usize::max(mapper.start, mapper.end))
|
||||||
.write_image_data(&image_buffer)
|
.max()
|
||||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
.unwrap_or(0) as usize;
|
||||||
writer
|
let mut global_colors = vec![0u8; total_leds * 3];
|
||||||
.finish()
|
|
||||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
|
|
||||||
let mut base64_image = String::new();
|
let mut color_index = 0;
|
||||||
base64_image.push_str("data:image/webp;base64,");
|
mappers.iter().for_each(|group| {
|
||||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_png);
|
if group.end > global_colors.len() || group.start > global_colors.len() {
|
||||||
base64_image.push_str(encoded.as_str());
|
warn!(
|
||||||
Ok(base64_image)
|
"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
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
96
src/App.tsx
96
src/App.tsx
@@ -1,9 +1,17 @@
|
|||||||
import { createEffect } from 'solid-js';
|
import { createEffect, onCleanup } from 'solid-js';
|
||||||
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import { DisplayView } from './components/display-view';
|
import { DisplayView } from './components/display-view';
|
||||||
import { DisplayListContainer } from './components/display-list-container';
|
import { DisplayListContainer } from './components/display-list-container';
|
||||||
import { displayStore, setDisplayStore } from './stores/display.store';
|
import { displayStore, setDisplayStore } from './stores/display.store';
|
||||||
import { path } from '@tauri-apps/api';
|
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() {
|
function App() {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -12,15 +20,85 @@ function App() {
|
|||||||
displays: JSON.parse(displays),
|
displays: JSON.parse(displays),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
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 (
|
return (
|
||||||
<div class="container">
|
<div>
|
||||||
<DisplayListContainer>
|
<LedStripConfigurationContext.Provider value={ledStripConfigurationContextValue}>
|
||||||
{displayStore.displays.map((display) => {
|
<LedStripPartsSorter />
|
||||||
return <DisplayView display={display} />;
|
<DisplayListContainer>
|
||||||
})}
|
{displayStore.displays.map((display) => {
|
||||||
</DisplayListContainer>
|
return <DisplayView display={display} />;
|
||||||
|
})}
|
||||||
|
</DisplayListContainer>
|
||||||
|
</LedStripConfigurationContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/assets/transparent-grid-background.svg
Normal file
16
src/assets/transparent-grid-background.svg
Normal 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 Photoshop’s transparency grid. Here’s 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 |
@@ -7,7 +7,7 @@ type DisplayInfoItemProps = {
|
|||||||
|
|
||||||
export const DisplayInfoItem: ParentComponent<DisplayInfoItemProps> = (props) => {
|
export const DisplayInfoItem: ParentComponent<DisplayInfoItemProps> = (props) => {
|
||||||
return (
|
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>
|
<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>
|
<dd class="select-all w-1/2 whitespace-nowrap">{props.children}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -1,15 +1,47 @@
|
|||||||
import { createEffect, createMemo, createSignal, on, ParentComponent } from 'solid-js';
|
import {
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
JSX,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
ParentComponent,
|
||||||
|
} from 'solid-js';
|
||||||
import { displayStore, setDisplayStore } from '../stores/display.store';
|
import { displayStore, setDisplayStore } from '../stores/display.store';
|
||||||
|
import background from '../assets/transparent-grid-background.svg?url';
|
||||||
|
|
||||||
export const DisplayListContainer: ParentComponent = (props) => {
|
export const DisplayListContainer: ParentComponent = (props) => {
|
||||||
|
let root: HTMLElement;
|
||||||
const [olStyle, setOlStyle] = createSignal({
|
const [olStyle, setOlStyle] = createSignal({
|
||||||
top: '0px',
|
top: '0px',
|
||||||
left: '0px',
|
left: '0px',
|
||||||
});
|
});
|
||||||
const [rootStyle, setRootStyle] = createSignal({
|
const [rootStyle, setRootStyle] = createSignal<JSX.CSSProperties>({
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
height: '100%',
|
||||||
});
|
});
|
||||||
|
const [bound, setBound] = createSignal({
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 100,
|
||||||
|
bottom: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetSize = () => {
|
||||||
|
const _bound = bound();
|
||||||
|
|
||||||
|
setDisplayStore({
|
||||||
|
viewScale: root.clientWidth / (_bound.right - _bound.left),
|
||||||
|
});
|
||||||
|
|
||||||
|
setOlStyle({
|
||||||
|
top: `${-_bound.top * displayStore.viewScale}px`,
|
||||||
|
left: `${-_bound.left * displayStore.viewScale}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRootStyle({
|
||||||
|
height: `${(_bound.bottom - _bound.top) * displayStore.viewScale}px`,
|
||||||
|
background: `url(${background})`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
|
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
|
||||||
@@ -23,23 +55,28 @@ export const DisplayListContainer: ParentComponent = (props) => {
|
|||||||
...displayStore.displays.map((display) => display.y + display.height),
|
...displayStore.displays.map((display) => display.y + display.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
setDisplayStore({
|
setBound({
|
||||||
viewScale: 1200 / (boundRight - boundLeft),
|
left: boundLeft,
|
||||||
|
top: boundTop,
|
||||||
|
right: boundRight,
|
||||||
|
bottom: boundBottom,
|
||||||
|
});
|
||||||
|
let observer: ResizeObserver;
|
||||||
|
onMount(() => {
|
||||||
|
observer = new ResizeObserver(resetSize);
|
||||||
|
observer.observe(root);
|
||||||
});
|
});
|
||||||
|
|
||||||
setOlStyle({
|
onCleanup(() => {
|
||||||
top: `${-boundTop * displayStore.viewScale}px`,
|
observer?.unobserve(root);
|
||||||
left: `${-boundLeft * displayStore.viewScale}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
setRootStyle({
|
|
||||||
width: `${(boundRight - boundLeft) * displayStore.viewScale}px`,
|
|
||||||
height: `${(boundBottom - boundTop) * displayStore.viewScale}px`,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class="relative bg-gray-400/30" style={rootStyle()}>
|
<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}
|
{props.children}
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Component, createMemo } from 'solid-js';
|
import { Component, createMemo } from 'solid-js';
|
||||||
import { DisplayInfo } from '../models/display-info.model';
|
import { DisplayInfo } from '../models/display-info.model';
|
||||||
import { displayStore } from '../stores/display.store';
|
import { displayStore } from '../stores/display.store';
|
||||||
|
import { ledStripStore } from '../stores/led-strip.store';
|
||||||
import { DisplayInfoPanel } from './display-info-panel';
|
import { DisplayInfoPanel } from './display-info-panel';
|
||||||
|
import { LedStripPart } from './led-strip-part';
|
||||||
import { ScreenView } from './screen-view';
|
import { ScreenView } from './screen-view';
|
||||||
|
|
||||||
type DisplayViewProps = {
|
type DisplayViewProps = {
|
||||||
@@ -16,17 +18,47 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
|
|||||||
const style = createMemo(() => ({
|
const style = createMemo(() => ({
|
||||||
top: `${props.display.y * displayStore.viewScale}px`,
|
top: `${props.display.y * displayStore.viewScale}px`,
|
||||||
left: `${props.display.x * displayStore.viewScale}px`,
|
left: `${props.display.x * displayStore.viewScale}px`,
|
||||||
|
height: `${size().height}px`,
|
||||||
|
width: `${size().width}px`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const ledStripConfigs = createMemo(() => {
|
||||||
|
console.log('ledStripConfigs', ledStripStore.strips);
|
||||||
|
return ledStripStore.strips.filter((c) => c.display_id === props.display.id);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class="absolute bg-gray-300" style={style()}>
|
<section
|
||||||
|
class="absolute grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
|
||||||
|
style={style()}
|
||||||
|
>
|
||||||
<ScreenView
|
<ScreenView
|
||||||
|
class="row-start-2 col-start-2 group"
|
||||||
displayId={props.display.id}
|
displayId={props.display.id}
|
||||||
height={size().height}
|
style={{
|
||||||
width={size().width}
|
'object-fit': 'contain',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 overflow-hidden"
|
||||||
|
config={ledStripConfigs().find((c) => c.border === 'Top')}
|
||||||
/>
|
/>
|
||||||
<DisplayInfoPanel
|
<LedStripPart
|
||||||
display={props.display}
|
class="row-start-2 col-start-1 flex-col overflow-hidden"
|
||||||
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"
|
config={ledStripConfigs().find((c) => c.border === 'Left')}
|
||||||
|
/>
|
||||||
|
<LedStripPart
|
||||||
|
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 overflow-hidden"
|
||||||
|
config={ledStripConfigs().find((c) => c.border === 'Bottom')}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
155
src/components/led-strip-part.tsx
Normal file
155
src/components/led-strip-part.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createRoot,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
JSX,
|
||||||
|
splitProps,
|
||||||
|
useContext,
|
||||||
|
} from 'solid-js';
|
||||||
|
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;
|
||||||
|
} & JSX.HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
type PixelProps = {
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function subscribeScreenshotUpdate(displayId: number) {
|
||||||
|
await invoke('subscribe_encoded_screenshot_updated', {
|
||||||
|
displayId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pixel: Component<PixelProps> = (props) => {
|
||||||
|
const style = createMemo(() => ({
|
||||||
|
background: props.color,
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
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(() => {
|
||||||
|
if (!localProps.config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = ledStripStore.strips.findIndex(
|
||||||
|
(s) =>
|
||||||
|
s.display_id === localProps.config!.display_id &&
|
||||||
|
s.border === localProps.config!.border,
|
||||||
|
);
|
||||||
|
|
||||||
|
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]
|
||||||
|
})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
setColors(colors);
|
||||||
|
});
|
||||||
|
|
||||||
|
// get led strip sample points
|
||||||
|
createEffect(() => {
|
||||||
|
if (localProps.config) {
|
||||||
|
invoke('get_led_strips_sample_points', {
|
||||||
|
config: localProps.config,
|
||||||
|
}).then((points) => {
|
||||||
|
setLedSamplePoints(points);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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={
|
||||||
|
'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}
|
||||||
|
>
|
||||||
|
<For each={colors()}>{(item) => <Pixel color={item} />}</For>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
228
src/components/led-strip-parts-sorter.tsx
Normal file
228
src/components/led-strip-parts-sorter.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -13,66 +11,155 @@ import {
|
|||||||
|
|
||||||
type ScreenViewProps = {
|
type ScreenViewProps = {
|
||||||
displayId: number;
|
displayId: number;
|
||||||
height: number;
|
} & JSX.HTMLAttributes<HTMLDivElement>;
|
||||||
width: number;
|
|
||||||
} & Omit<JSX.HTMLAttributes<HTMLCanvasElement>, 'height' | 'width'>;
|
|
||||||
|
|
||||||
async function subscribeScreenshotUpdate(displayId: number) {
|
|
||||||
await invoke('subscribe_encoded_screenshot_updated', {
|
|
||||||
displayId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||||
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
|
let root: HTMLDivElement;
|
||||||
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
||||||
createEffect(() => {
|
const [drawInfo, setDrawInfo] = createSignal({
|
||||||
const unlisten = listen<{
|
drawX: 0,
|
||||||
base64_image: string;
|
drawY: 0,
|
||||||
display_id: number;
|
drawWidth: 0,
|
||||||
height: number;
|
drawHeight: 0,
|
||||||
width: number;
|
});
|
||||||
}>('encoded-screenshot-updated', (event) => {
|
const [imageData, setImageData] = createSignal<{
|
||||||
if (event.payload.display_id === localProps.displayId) {
|
buffer: Uint8ClampedArray;
|
||||||
const url = convertFileSrc(
|
width: number;
|
||||||
`displays/${localProps.displayId}?width=${canvas.width}&height=${canvas.height}`,
|
height: number;
|
||||||
'ambient-light',
|
} | null>(null);
|
||||||
);
|
const [hidden, setHidden] = createSignal(false);
|
||||||
fetch(url, {
|
|
||||||
mode: 'cors',
|
|
||||||
})
|
|
||||||
.then((res) => res.body?.getReader().read())
|
|
||||||
.then((buffer) => {
|
|
||||||
console.log(buffer?.value?.length);
|
|
||||||
|
|
||||||
let _ctx = ctx();
|
const resetSize = () => {
|
||||||
if (_ctx && buffer?.value) {
|
const aspectRatio = canvas.width / canvas.height;
|
||||||
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
const img = new ImageData(
|
const drawWidth = Math.round(
|
||||||
new Uint8ClampedArray(buffer.value),
|
Math.min(root.clientWidth, root.clientHeight * aspectRatio),
|
||||||
canvas.width,
|
);
|
||||||
canvas.height,
|
const drawHeight = Math.round(
|
||||||
);
|
Math.min(root.clientHeight, root.clientWidth / aspectRatio),
|
||||||
_ctx.putImageData(img, 0, 0);
|
);
|
||||||
}
|
|
||||||
});
|
const drawX = Math.round((root.clientWidth - drawWidth) / 2);
|
||||||
|
const drawY = Math.round((root.clientHeight - drawHeight) / 2);
|
||||||
|
|
||||||
|
setDrawInfo({
|
||||||
|
drawX,
|
||||||
|
drawY,
|
||||||
|
drawWidth,
|
||||||
|
drawHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.width = root.clientWidth;
|
||||||
|
canvas.height = root.clientHeight;
|
||||||
|
|
||||||
|
draw(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (cached: boolean = false) => {
|
||||||
|
const { drawX, drawY } = drawInfo();
|
||||||
|
|
||||||
|
let _ctx = ctx();
|
||||||
|
let raw = imageData();
|
||||||
|
if (_ctx && raw) {
|
||||||
|
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (cached) {
|
||||||
|
for (let i = 3; i < raw.buffer.length; i += 8) {
|
||||||
|
raw.buffer[i] = Math.floor(raw.buffer[i] * 0.7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const img = new ImageData(raw.buffer, raw.width, raw.height);
|
||||||
|
_ctx.putImageData(img, drawX, drawY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// console.log(event.payload.display_id, localProps.displayId);
|
// get screenshot
|
||||||
});
|
createEffect(() => {
|
||||||
subscribeScreenshotUpdate(localProps.displayId);
|
let stopped = false;
|
||||||
|
const frame = async () => {
|
||||||
|
const { drawWidth, drawHeight } = drawInfo();
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
(async () => {
|
||||||
setCtx(canvas.getContext('2d'));
|
while (!stopped) {
|
||||||
});
|
if (hidden()) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await frame();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
unlisten.then((unlisten) => {
|
stopped = true;
|
||||||
unlisten();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return <canvas ref={canvas!} class="object-contain" {...rootProps} />;
|
// resize
|
||||||
|
createEffect(() => {
|
||||||
|
let resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setCtx(canvas.getContext('2d'));
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
resetSize();
|
||||||
|
}).observe(root);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
resizeObserver?.unobserve(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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!}
|
||||||
|
{...rootProps}
|
||||||
|
class={'overflow-hidden h-full w-full ' + rootProps.class}
|
||||||
|
>
|
||||||
|
<canvas ref={canvas!} />
|
||||||
|
{rootProps.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/constants/border.ts
Normal file
2
src/constants/border.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const borders = ['Top', 'Right', 'Bottom', 'Left'] as const;
|
||||||
|
export type Borders = typeof borders[number];
|
||||||
24
src/contexts/led-strip-configuration.context.ts
Normal file
24
src/contexts/led-strip-configuration.context.ts
Normal 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: () => {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
20
src/models/led-strip-config.ts
Normal file
20
src/models/led-strip-config.ts
Normal file
@@ -0,0 +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 len: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
1
src/models/pixel-rgb.ts
Normal file
1
src/models/pixel-rgb.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type PixelRgb = [number, number, number];
|
||||||
12
src/stores/led-strip.store.tsx
Normal file
12
src/stores/led-strip.store.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import { LedStripConfig, LedStripPixelMapper } from '../models/led-strip-config';
|
||||||
|
|
||||||
|
export const [ledStripStore, setLedStripStore] = createStore({
|
||||||
|
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)));
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user