Compare commits
30 Commits
6c3ce607e0
...
2c5ac11579
Author | SHA1 | Date | |
---|---|---|---|
2c5ac11579 | |||
9109518822 | |||
d9d73f01d7 | |||
239144a446 | |||
3a430716d6 | |||
800c0d3fc4 | |||
091bcf33da | |||
9b863508e4 | |||
174840403f | |||
ca9a2ba34d | |||
82d4adfe0f | |||
6c90a5e655 | |||
11045f27d8 | |||
f6e3257670 | |||
e5527ce3c3 | |||
3deb14823d | |||
7a87748cf1 | |||
9d11abfa6e | |||
d97eb0115f | |||
effcb1e192 | |||
1c08c17fd4 | |||
81d666557b | |||
6e6160fc0a | |||
fc8b3164d8 | |||
932cc78bcf | |||
782f3bf029 | |||
09799cb2d5 | |||
a905c98823 | |||
9cbccedc72 | |||
aa7430c54e |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
.DS_Store
|
||||||
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -2,6 +2,10 @@
|
|||||||
"files.autoSave": "onWindowChange",
|
"files.autoSave": "onWindowChange",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Itertools",
|
"Itertools",
|
||||||
"Leds"
|
"Leds",
|
||||||
]
|
"unlisten"
|
||||||
|
],
|
||||||
|
"idf.customExtraVars": {
|
||||||
|
"OPENOCD_SCRIPTS": "/Users/ivan/.espressif/tools/openocd-esp32/v0.11.0-esp32-20211220/openocd-esp32/share/openocd/scripts"
|
||||||
|
}
|
||||||
}
|
}
|
27
.vscode/tasks.json
vendored
27
.vscode/tasks.json
vendored
@ -3,15 +3,29 @@
|
|||||||
// for the documentation about the tasks.json format
|
// for the documentation about the tasks.json format
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "dev",
|
||||||
|
"type": "shell",
|
||||||
|
"isBackground": true,
|
||||||
|
"command": "pnpm",
|
||||||
|
"args": [
|
||||||
|
"tauri",
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"problemMatcher": [
|
||||||
|
"$eslint-stylish"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"RUST_LOG": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "ui:dev",
|
"label": "ui:dev",
|
||||||
"type": "shell",
|
"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,
|
"isBackground": true,
|
||||||
// change this to your `beforeDevCommand`:
|
"command": "pnpm",
|
||||||
"command": "yarn",
|
|
||||||
"args": [
|
"args": [
|
||||||
"dev"
|
"dev"
|
||||||
]
|
]
|
||||||
@ -19,8 +33,7 @@
|
|||||||
{
|
{
|
||||||
"label": "ui:build",
|
"label": "ui:build",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
// change this to your `beforeBuildCommand`:
|
"command": "pnpm",
|
||||||
"command": "yarn",
|
|
||||||
"args": [
|
"args": [
|
||||||
"build"
|
"build"
|
||||||
]
|
]
|
||||||
|
22
package.json
22
package.json
@ -11,19 +11,23 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@solidjs/router": "^0.8.2",
|
||||||
"solid-js": "^1.4.7",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"solid-icons": "^1.0.4",
|
||||||
|
"solid-js": "^1.7.4",
|
||||||
"solid-tippy": "^0.2.1",
|
"solid-tippy": "^0.2.1",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.2.2",
|
"@tauri-apps/cli": "^1.3.0",
|
||||||
"@types/node": "^18.7.10",
|
"@types/debug": "^4.1.7",
|
||||||
|
"@types/node": "^18.16.3",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.23",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.0.0",
|
"vite": "^4.3.4",
|
||||||
"vite-plugin-solid": "^2.3.0"
|
"vite-plugin-solid": "^2.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1168
pnpm-lock.yaml
1168
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
1232
src-tauri/Cargo.lock
generated
1232
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,11 @@ paho-mqtt = "0.12.1"
|
|||||||
time = {version="0.3.20", features= ["formatting"] }
|
time = {version="0.3.20", features= ["formatting"] }
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
core-foundation = "0.9.3"
|
core-foundation = "0.9.3"
|
||||||
|
tokio-stream = "0.1.14"
|
||||||
|
mdns-sd = "0.7.2"
|
||||||
|
futures = "0.3.28"
|
||||||
|
ddc-hi = "0.4.1"
|
||||||
|
coreaudio-rs = "0.11.2"
|
||||||
|
|
||||||
[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
|
||||||
|
@ -5,7 +5,7 @@ use paris::{error, info};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::api::path::config_dir;
|
use tauri::api::path::config_dir;
|
||||||
|
|
||||||
use crate::screenshot::{self, LedSamplePoints};
|
use crate::screenshot::LedSamplePoints;
|
||||||
|
|
||||||
const CONFIG_FILE_NAME: &str = "cc.ivanli.ambient_light/led_strip_config.toml";
|
const CONFIG_FILE_NAME: &str = "cc.ivanli.ambient_light/led_strip_config.toml";
|
||||||
|
|
||||||
@ -26,10 +26,28 @@ pub struct LedStripConfig {
|
|||||||
pub len: usize,
|
pub len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ColorCalibration {
|
||||||
|
r: f32,
|
||||||
|
g: f32,
|
||||||
|
b: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorCalibration {
|
||||||
|
pub fn to_bytes(&self) -> [u8; 3] {
|
||||||
|
[
|
||||||
|
(self.r * 255.0) as u8,
|
||||||
|
(self.g * 255.0) as u8,
|
||||||
|
(self.b * 255.0) as u8,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct LedStripConfigGroup {
|
pub struct LedStripConfigGroup {
|
||||||
pub strips: Vec<LedStripConfig>,
|
pub strips: Vec<LedStripConfig>,
|
||||||
pub mappers: Vec<SamplePointMapper>,
|
pub mappers: Vec<SamplePointMapper>,
|
||||||
|
pub color_calibration: ColorCalibration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LedStripConfigGroup {
|
impl LedStripConfigGroup {
|
||||||
@ -115,7 +133,17 @@ impl LedStripConfigGroup {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Self { strips, mappers })
|
let color_calibration = ColorCalibration {
|
||||||
|
r: 1.0,
|
||||||
|
g: 1.0,
|
||||||
|
b: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
strips,
|
||||||
|
mappers,
|
||||||
|
color_calibration,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use tokio::sync::OnceCell;
|
|||||||
|
|
||||||
use crate::ambient_light::{config, LedStripConfigGroup};
|
use crate::ambient_light::{config, LedStripConfigGroup};
|
||||||
|
|
||||||
use super::{Border, SamplePointMapper};
|
use super::{Border, SamplePointMapper, ColorCalibration};
|
||||||
|
|
||||||
pub struct ConfigManager {
|
pub struct ConfigManager {
|
||||||
config: Arc<RwLock<LedStripConfigGroup>>,
|
config: Arc<RwLock<LedStripConfigGroup>>,
|
||||||
@ -223,4 +223,15 @@ impl ConfigManager {
|
|||||||
) -> tokio::sync::watch::Receiver<LedStripConfigGroup> {
|
) -> tokio::sync::watch::Receiver<LedStripConfigGroup> {
|
||||||
self.config_update_receiver.clone()
|
self.config_update_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_color_calibration(&self, color_calibration: ColorCalibration) -> anyhow::Result<()> {
|
||||||
|
let config = self.config.write().await;
|
||||||
|
|
||||||
|
let mut cloned_config = config.clone();
|
||||||
|
cloned_config.color_calibration = color_calibration;
|
||||||
|
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
self.update(&cloned_config).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,27 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
|
|||||||
use paris::warn;
|
use paris::warn;
|
||||||
use tauri::async_runtime::RwLock;
|
use tauri::async_runtime::RwLock;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
net::UdpSocket,
|
||||||
sync::{broadcast, watch},
|
sync::{broadcast, watch},
|
||||||
time::sleep,
|
time::sleep,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ambient_light::{config, ConfigManager},
|
ambient_light::{config, ConfigManager},
|
||||||
rpc::MqttRpc,
|
led_color::LedColor,
|
||||||
screenshot::LedSamplePoints,
|
screenshot::LedSamplePoints,
|
||||||
screenshot_manager::{self, ScreenshotManager},
|
screenshot_manager::{self, ScreenshotManager}, rpc::UdpRpc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::{LedStripConfigGroup, SamplePointConfig, SamplePointMapper};
|
use super::{LedStripConfigGroup, SamplePointMapper};
|
||||||
|
|
||||||
pub struct LedColorsPublisher {
|
pub struct LedColorsPublisher {
|
||||||
sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
||||||
sorted_colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
|
sorted_colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
|
||||||
colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
||||||
colors_tx: Arc<RwLock<watch::Sender<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>>,
|
inner_tasks_version: Arc<RwLock<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +34,6 @@ impl LedColorsPublisher {
|
|||||||
|
|
||||||
let (sorted_tx, sorted_rx) = watch::channel(Vec::new());
|
let (sorted_tx, sorted_rx) = watch::channel(Vec::new());
|
||||||
let (tx, 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
|
LED_COLORS_PUBLISHER_GLOBAL
|
||||||
.get_or_init(|| async {
|
.get_or_init(|| async {
|
||||||
@ -44,8 +42,6 @@ impl LedColorsPublisher {
|
|||||||
sorted_colors_tx: Arc::new(RwLock::new(sorted_tx)),
|
sorted_colors_tx: Arc::new(RwLock::new(sorted_tx)),
|
||||||
colors_rx: Arc::new(RwLock::new(rx)),
|
colors_rx: Arc::new(RwLock::new(rx)),
|
||||||
colors_tx: Arc::new(RwLock::new(tx)),
|
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)),
|
inner_tasks_version: Arc::new(RwLock::new(0)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -56,29 +52,34 @@ impl LedColorsPublisher {
|
|||||||
&self,
|
&self,
|
||||||
display_id: u32,
|
display_id: u32,
|
||||||
sample_points: Vec<Vec<LedSamplePoints>>,
|
sample_points: Vec<Vec<LedSamplePoints>>,
|
||||||
|
bound_scale_factor: f32,
|
||||||
|
mappers: Vec<SamplePointMapper>,
|
||||||
|
display_colors_tx: broadcast::Sender<(u32, Vec<u8>)>,
|
||||||
) {
|
) {
|
||||||
let display_colors_tx = self.display_colors_tx.clone();
|
|
||||||
let internal_tasks_version = self.inner_tasks_version.clone();
|
let internal_tasks_version = self.inner_tasks_version.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let display_colors_tx = display_colors_tx.read().await.clone();
|
let colors = screenshot_manager::get_display_colors(
|
||||||
|
display_id,
|
||||||
let colors = screenshot_manager::get_display_colors(display_id, &sample_points);
|
&sample_points,
|
||||||
|
bound_scale_factor,
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(err) = colors {
|
if let Err(err) = colors {
|
||||||
warn!("Failed to get colors: {}", err);
|
warn!("Failed to get colors: {}", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut start: tokio::time::Instant = tokio::time::Instant::now();
|
let mut interval = tokio::time::interval(Duration::from_millis(33));
|
||||||
let mut interval = tokio::time::interval(Duration::from_millis(66));
|
|
||||||
let init_version = internal_tasks_version.read().await.clone();
|
let init_version = internal_tasks_version.read().await.clone();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||||
|
|
||||||
if internal_tasks_version.read().await.clone() != init_version {
|
let version = internal_tasks_version.read().await.clone();
|
||||||
|
|
||||||
|
if version != init_version {
|
||||||
log::info!(
|
log::info!(
|
||||||
"inner task version changed, stop. {} != {}",
|
"inner task version changed, stop. {} != {}",
|
||||||
internal_tasks_version.read().await.clone(),
|
internal_tasks_version.read().await.clone(),
|
||||||
@ -88,9 +89,11 @@ impl LedColorsPublisher {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log::info!("tick: {}ms", start.elapsed().as_millis());
|
let colors = screenshot_manager::get_display_colors(
|
||||||
start = tokio::time::Instant::now();
|
display_id,
|
||||||
let colors = screenshot_manager::get_display_colors(display_id, &sample_points);
|
&sample_points,
|
||||||
|
bound_scale_factor,
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(err) = colors {
|
if let Err(err) = colors {
|
||||||
warn!("Failed to get colors: {}", err);
|
warn!("Failed to get colors: {}", err);
|
||||||
@ -98,13 +101,26 @@ impl LedColorsPublisher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let colors = colors.unwrap();
|
let colors: Vec<crate::led_color::LedColor> = colors.unwrap();
|
||||||
|
|
||||||
let color_len = colors.len();
|
let colors_copy = colors.clone();
|
||||||
|
|
||||||
|
let mappers = mappers.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match Self::send_colors_by_display(colors, mappers).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// log::info!("sent colors: #{: >15}", display_id);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to send colors: #{: >15}\t{}", display_id, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
match display_colors_tx.send((
|
match display_colors_tx.send((
|
||||||
display_id,
|
display_id,
|
||||||
colors
|
colors_copy
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|color| color.get_rgb())
|
.map(|color| color.get_rgb())
|
||||||
.flatten()
|
.flatten()
|
||||||
@ -121,30 +137,32 @@ impl LedColorsPublisher {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_all_colors_worker(&self, display_ids: Vec<u32>, mappers: Vec<SamplePointMapper>) {
|
fn start_all_colors_worker(
|
||||||
|
&self,
|
||||||
|
display_ids: Vec<u32>,
|
||||||
|
mappers: Vec<SamplePointMapper>,
|
||||||
|
mut display_colors_rx: broadcast::Receiver<(u32, Vec<u8>)>,
|
||||||
|
) {
|
||||||
let sorted_colors_tx = self.sorted_colors_tx.clone();
|
let sorted_colors_tx = self.sorted_colors_tx.clone();
|
||||||
let colors_tx = self.colors_tx.clone();
|
let colors_tx = self.colors_tx.clone();
|
||||||
let display_colors_rx = self.display_colors_rx.clone();
|
log::debug!("start all_colors_worker");
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let mut rx = display_colors_rx.read().await.resubscribe();
|
|
||||||
|
|
||||||
let sorted_colors_tx = sorted_colors_tx.write().await;
|
let sorted_colors_tx = sorted_colors_tx.write().await;
|
||||||
let colors_tx = 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 all_colors: Vec<Option<Vec<u8>>> = vec![None; display_ids.len()];
|
||||||
let mut start: tokio::time::Instant = tokio::time::Instant::now();
|
let mut start: tokio::time::Instant = tokio::time::Instant::now();
|
||||||
|
|
||||||
log::info!("start all_colors_worker");
|
log::debug!("start all_colors_worker task");
|
||||||
loop {
|
loop {
|
||||||
// log::info!("display_colors_rx changed");
|
let color_info = display_colors_rx.recv().await;
|
||||||
let color_info = rx.recv().await;
|
|
||||||
|
|
||||||
if let Err(err) = color_info {
|
if let Err(err) = color_info {
|
||||||
match err {
|
match err {
|
||||||
broadcast::error::RecvError::Closed => {
|
broadcast::error::RecvError::Closed => {
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
broadcast::error::RecvError::Lagged(_) => {
|
broadcast::error::RecvError::Lagged(_) => {
|
||||||
warn!("display_colors_rx lagged");
|
warn!("display_colors_rx lagged");
|
||||||
@ -186,7 +204,7 @@ impl LedColorsPublisher {
|
|||||||
warn!("Failed to send sorted colors: {}", err);
|
warn!("Failed to send sorted colors: {}", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log::info!("tick: {}ms", start.elapsed().as_millis());
|
log::debug!("tick: {}ms", start.elapsed().as_millis());
|
||||||
start = tokio::time::Instant::now();
|
start = tokio::time::Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,9 +218,6 @@ impl LedColorsPublisher {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let publisher = Self::global().await;
|
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 config_manager = ConfigManager::global().await;
|
||||||
let mut config_receiver = config_manager.clone_config_update_receiver();
|
let mut config_receiver = config_manager.clone_config_update_receiver();
|
||||||
|
|
||||||
@ -221,143 +236,108 @@ impl LedColorsPublisher {
|
|||||||
|
|
||||||
let configs = configs.unwrap();
|
let configs = configs.unwrap();
|
||||||
|
|
||||||
|
let mut inner_tasks_version = inner_tasks_version.write().await;
|
||||||
|
*inner_tasks_version = inner_tasks_version.overflowing_add(1).0;
|
||||||
|
drop(inner_tasks_version);
|
||||||
|
|
||||||
|
let (display_colors_tx, display_colors_rx) =
|
||||||
|
broadcast::channel::<(u32, Vec<u8>)>(8);
|
||||||
|
|
||||||
for sample_point_group in configs.sample_point_groups.clone() {
|
for sample_point_group in configs.sample_point_groups.clone() {
|
||||||
let display_id = sample_point_group.display_id;
|
let display_id = sample_point_group.display_id;
|
||||||
let sample_points = sample_point_group.points;
|
let sample_points = sample_point_group.points;
|
||||||
|
let bound_scale_factor = sample_point_group.bound_scale_factor;
|
||||||
publisher.start_one_display_colors_fetcher(display_id, sample_points);
|
publisher.start_one_display_colors_fetcher(
|
||||||
|
display_id,
|
||||||
|
sample_points,
|
||||||
|
bound_scale_factor,
|
||||||
|
sample_point_group.mappers,
|
||||||
|
display_colors_tx.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_ids = configs.sample_point_groups;
|
let display_ids = configs.sample_point_groups;
|
||||||
publisher.start_all_colors_worker(
|
publisher.start_all_colors_worker(
|
||||||
display_ids.iter().map(|c| c.display_id).collect(),
|
display_ids.iter().map(|c| c.display_id).collect(),
|
||||||
configs.mappers,
|
configs.mappers,
|
||||||
|
display_colors_rx,
|
||||||
);
|
);
|
||||||
|
|
||||||
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<()> {
|
pub async fn send_colors(offset: u16, mut payload: Vec<u8>) -> anyhow::Result<()> {
|
||||||
let mqtt = MqttRpc::global().await;
|
// let mqtt = MqttRpc::global().await;
|
||||||
|
|
||||||
mqtt.publish_led_sub_pixels(payload).await
|
// mqtt.publish_led_sub_pixels(payload).await;
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:8000").await?;
|
||||||
|
let mut buffer = vec![2];
|
||||||
|
buffer.push((offset >> 8) as u8);
|
||||||
|
buffer.push((offset & 0xff) as u8);
|
||||||
|
buffer.append(&mut payload);
|
||||||
|
socket.send_to(&buffer, "192.168.31.206:23042").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_colors_by_display(
|
||||||
|
colors: Vec<LedColor>,
|
||||||
|
mappers: Vec<SamplePointMapper>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// let color_len = colors.len();
|
||||||
|
let display_led_offset = mappers
|
||||||
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|mapper| [mapper.start, mapper.end])
|
||||||
|
.min()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let udp_rpc = UdpRpc::global().await;
|
||||||
|
if let Err(err) = udp_rpc {
|
||||||
|
warn!("udp_rpc can not be initialized: {}", err);
|
||||||
|
}
|
||||||
|
let udp_rpc = udp_rpc.as_ref().unwrap();
|
||||||
|
|
||||||
|
// let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
for group in mappers.clone() {
|
||||||
|
if (group.start.abs_diff(group.end)) > colors.len() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"get_sorted_colors: color_index out of range. color_index: {}, strip len: {}, colors.len(): {}",
|
||||||
|
group.pos,
|
||||||
|
group.start.abs_diff(group.end),
|
||||||
|
colors.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let group_size = group.start.abs_diff(group.end);
|
||||||
|
let mut buffer = Vec::<u8>::with_capacity(group_size * 3);
|
||||||
|
|
||||||
|
if group.end > group.start {
|
||||||
|
for i in group.pos - display_led_offset..group_size + group.pos - display_led_offset
|
||||||
|
{
|
||||||
|
let bytes = colors[i].as_bytes();
|
||||||
|
buffer.append(&mut bytes.to_vec());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i in (group.pos - display_led_offset
|
||||||
|
..group_size + group.pos - display_led_offset)
|
||||||
|
.rev()
|
||||||
|
{
|
||||||
|
let bytes = colors[i].as_bytes();
|
||||||
|
buffer.append(&mut bytes.to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = group.start.min(group.end);
|
||||||
|
let mut tx_buffer = vec![2];
|
||||||
|
tx_buffer.push((offset >> 8) as u8);
|
||||||
|
tx_buffer.push((offset & 0xff) as u8);
|
||||||
|
tx_buffer.append(&mut buffer);
|
||||||
|
|
||||||
|
udp_rpc.send_to_all(&tx_buffer).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clone_sorted_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
|
pub async fn clone_sorted_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
|
||||||
@ -380,6 +360,7 @@ impl LedColorsPublisher {
|
|||||||
let mut colors_configs = Vec::new();
|
let mut colors_configs = Vec::new();
|
||||||
|
|
||||||
let mut merged_screenshot_receiver = screenshot_manager.clone_merged_screenshot_rx().await;
|
let mut merged_screenshot_receiver = screenshot_manager.clone_merged_screenshot_rx().await;
|
||||||
|
merged_screenshot_receiver.resubscribe();
|
||||||
|
|
||||||
let mut screenshots = HashMap::new();
|
let mut screenshots = HashMap::new();
|
||||||
|
|
||||||
@ -406,35 +387,50 @@ impl LedColorsPublisher {
|
|||||||
screenshots.insert(screenshot.display_id, screenshot);
|
screenshots.insert(screenshot.display_id, screenshot);
|
||||||
|
|
||||||
if screenshots.len() == display_ids.len() {
|
if screenshots.len() == display_ids.len() {
|
||||||
|
let mut led_start = 0;
|
||||||
|
|
||||||
for display_id in display_ids {
|
for display_id in display_ids {
|
||||||
let led_strip_configs: Vec<_> = configs
|
let led_strip_configs = configs
|
||||||
.strips
|
.strips
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.display_id == display_id)
|
.enumerate()
|
||||||
.collect();
|
.filter(|(_, c)| c.display_id == display_id);
|
||||||
|
|
||||||
if led_strip_configs.len() == 0 {
|
|
||||||
warn!("no led strip config for display_id: {}", display_id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let screenshot = screenshots.get(&display_id).unwrap();
|
let screenshot = screenshots.get(&display_id).unwrap();
|
||||||
log::debug!("screenshot updated: {:?}", display_id);
|
log::debug!("screenshot updated: {:?}", display_id);
|
||||||
|
|
||||||
let points: Vec<_> = led_strip_configs
|
let points: Vec<_> = led_strip_configs
|
||||||
.iter()
|
.clone()
|
||||||
.map(|config| screenshot.get_sample_points(&config))
|
.map(|(_, config)| screenshot.get_sample_points(&config))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let colors_config = DisplaySamplePointGroup { display_id, points };
|
if points.len() == 0 {
|
||||||
|
warn!("no led strip config for display_id: {}", display_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bound_scale_factor = screenshot.bound_scale_factor;
|
||||||
|
|
||||||
|
let led_end = led_start + points.iter().map(|p| p.len()).sum::<usize>();
|
||||||
|
|
||||||
|
let mappers = led_strip_configs.map(|(i, _)| mappers[i].clone()).collect();
|
||||||
|
|
||||||
|
let colors_config = DisplaySamplePointGroup {
|
||||||
|
display_id,
|
||||||
|
points,
|
||||||
|
bound_scale_factor,
|
||||||
|
mappers,
|
||||||
|
};
|
||||||
|
|
||||||
colors_configs.push(colors_config);
|
colors_configs.push(colors_config);
|
||||||
|
led_start = led_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("got all colors configs: {:?}", colors_configs.len());
|
||||||
|
|
||||||
return Ok(AllColorConfig {
|
return Ok(AllColorConfig {
|
||||||
sample_point_groups: colors_configs,
|
sample_point_groups: colors_configs,
|
||||||
mappers,
|
mappers,
|
||||||
// screenshot_receivers: local_rx_list,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -456,4 +452,6 @@ pub struct AllColorConfig {
|
|||||||
pub struct DisplaySamplePointGroup {
|
pub struct DisplaySamplePointGroup {
|
||||||
pub display_id: u32,
|
pub display_id: u32,
|
||||||
pub points: Vec<Vec<LedSamplePoints>>,
|
pub points: Vec<Vec<LedSamplePoints>>,
|
||||||
|
pub bound_scale_factor: f32,
|
||||||
|
pub mappers: Vec<config::SamplePointMapper>,
|
||||||
}
|
}
|
||||||
|
96
src-tauri/src/display/display_handler.rs
Normal file
96
src-tauri/src/display/display_handler.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use std::{sync::Arc, time::SystemTime};
|
||||||
|
|
||||||
|
use ddc_hi::{Ddc, Display};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use super::DisplayState;
|
||||||
|
|
||||||
|
pub struct DisplayHandler {
|
||||||
|
pub state: Arc<RwLock<DisplayState>>,
|
||||||
|
pub controller: Arc<RwLock<Display>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayHandler {
|
||||||
|
pub async fn fetch_state(&self) {
|
||||||
|
let mut controller = self.controller.write().await;
|
||||||
|
|
||||||
|
let mut temp_state = DisplayState::default();
|
||||||
|
|
||||||
|
match controller.handle.get_vcp_feature(0x10) {
|
||||||
|
Ok(value) => {
|
||||||
|
temp_state.max_brightness = value.maximum();
|
||||||
|
temp_state.min_brightness = 0;
|
||||||
|
temp_state.brightness = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
match controller.handle.get_vcp_feature(0x12) {
|
||||||
|
Ok(value) => {
|
||||||
|
temp_state.max_contrast = value.maximum();
|
||||||
|
temp_state.min_contrast = 0;
|
||||||
|
temp_state.contrast = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
match controller.handle.get_vcp_feature(0xdc) {
|
||||||
|
Ok(value) => {
|
||||||
|
temp_state.max_mode = value.maximum();
|
||||||
|
temp_state.min_mode = 0;
|
||||||
|
temp_state.mode = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
temp_state.last_fetched_at = SystemTime::now();
|
||||||
|
|
||||||
|
let mut state = self.state.write().await;
|
||||||
|
*state = temp_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_brightness(&self, brightness: u16) -> anyhow::Result<()> {
|
||||||
|
let mut controller = self.controller.write().await;
|
||||||
|
let mut state = self.state.write().await;
|
||||||
|
|
||||||
|
controller
|
||||||
|
.handle
|
||||||
|
.set_vcp_feature(0x10, brightness)
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not set brightness. {:?}", err))?;
|
||||||
|
|
||||||
|
|
||||||
|
state.brightness = brightness;
|
||||||
|
|
||||||
|
state.last_modified_at = SystemTime::now();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_contrast(&self, contrast: u16) -> anyhow::Result<()> {
|
||||||
|
let mut controller = self.controller.write().await;
|
||||||
|
let mut state = self.state.write().await;
|
||||||
|
|
||||||
|
controller
|
||||||
|
.handle
|
||||||
|
.set_vcp_feature(0x12, contrast)
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not set contrast. {:?}", err))?;
|
||||||
|
|
||||||
|
state.contrast = contrast;
|
||||||
|
state.last_modified_at = SystemTime::now();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_mode(&self, mode: u16) -> anyhow::Result<()> {
|
||||||
|
let mut controller = self.controller.write().await;
|
||||||
|
let mut state = self.state.write().await;
|
||||||
|
|
||||||
|
controller
|
||||||
|
.handle
|
||||||
|
.set_vcp_feature(0xdc, mode)
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not set mode. {:?}", err))?;
|
||||||
|
|
||||||
|
state.mode = mode;
|
||||||
|
state.last_modified_at = SystemTime::now();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,7 @@ use std::time::SystemTime;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
pub struct DisplayConfig {
|
pub struct DisplayState {
|
||||||
pub id: usize,
|
|
||||||
pub brightness: u16,
|
pub brightness: u16,
|
||||||
pub max_brightness: u16,
|
pub max_brightness: u16,
|
||||||
pub min_brightness: u16,
|
pub min_brightness: u16,
|
||||||
@ -15,22 +14,23 @@ pub struct DisplayConfig {
|
|||||||
pub max_mode: u16,
|
pub max_mode: u16,
|
||||||
pub min_mode: u16,
|
pub min_mode: u16,
|
||||||
pub last_modified_at: SystemTime,
|
pub last_modified_at: SystemTime,
|
||||||
|
pub last_fetched_at: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayConfig {
|
impl DisplayState {
|
||||||
pub fn default(index: usize) -> Self {
|
pub fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: index,
|
|
||||||
brightness: 30,
|
brightness: 30,
|
||||||
contrast: 50,
|
contrast: 50,
|
||||||
mode: 0,
|
mode: 0,
|
||||||
last_modified_at: SystemTime::now(),
|
last_modified_at: SystemTime::UNIX_EPOCH,
|
||||||
max_brightness: 100,
|
max_brightness: 100,
|
||||||
min_brightness: 0,
|
min_brightness: 0,
|
||||||
max_contrast: 100,
|
max_contrast: 100,
|
||||||
min_contrast: 0,
|
min_contrast: 0,
|
||||||
max_mode: 15,
|
max_mode: 15,
|
||||||
min_mode: 0,
|
min_mode: 0,
|
||||||
|
last_fetched_at: SystemTime::UNIX_EPOCH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,187 +1,129 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Borrow,
|
|
||||||
collections::HashMap,
|
|
||||||
ops::Sub,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use base64::Config;
|
|
||||||
use ddc_hi::Display;
|
use ddc_hi::Display;
|
||||||
use paris::{error, info, warn};
|
use paris::{error, info, warn};
|
||||||
use tauri::async_runtime::Mutex;
|
use tokio::{sync::{watch, OnceCell, RwLock}, task::yield_now};
|
||||||
use tokio::sync::{broadcast, OwnedMutexGuard};
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::{display::Brightness, models, rpc};
|
use crate::rpc::{BoardMessageChannels, DisplaySetting};
|
||||||
|
|
||||||
use super::{display_config::DisplayConfig, DisplayBrightness};
|
use super::{display_handler::DisplayHandler, display_state::DisplayState};
|
||||||
use ddc_hi::Ddc;
|
|
||||||
|
|
||||||
pub struct Manager {
|
pub struct DisplayManager {
|
||||||
displays: Arc<Mutex<HashMap<usize, Arc<Mutex<DisplayConfig>>>>>,
|
displays: Arc<RwLock<Vec<Arc<RwLock<DisplayHandler>>>>>,
|
||||||
|
setting_request_handler: Option<tokio::task::JoinHandle<()>>,
|
||||||
|
displays_changed_sender: Arc<watch::Sender<Vec<DisplayState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manager {
|
impl DisplayManager {
|
||||||
pub fn global() -> &'static Self {
|
pub async fn global() -> &'static Self {
|
||||||
static DISPLAY_MANAGER: once_cell::sync::OnceCell<Manager> =
|
static DISPLAY_MANAGER: OnceCell<DisplayManager> = OnceCell::const_new();
|
||||||
once_cell::sync::OnceCell::new();
|
|
||||||
|
|
||||||
DISPLAY_MANAGER.get_or_init(|| Self::create())
|
DISPLAY_MANAGER.get_or_init(|| Self::create()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create() -> Self {
|
pub async fn create() -> Self {
|
||||||
let instance = Self {
|
let (displays_changed_sender, _) = watch::channel(Vec::new());
|
||||||
displays: Arc::new(Mutex::new(HashMap::new())),
|
let displays_changed_sender = Arc::new(displays_changed_sender);
|
||||||
|
|
||||||
|
let mut instance = Self {
|
||||||
|
displays: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
setting_request_handler: None,
|
||||||
|
displays_changed_sender,
|
||||||
};
|
};
|
||||||
|
instance.fetch_displays().await;
|
||||||
|
instance.subscribe_setting_request();
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn subscribe_display_brightness(&self) {
|
async fn fetch_displays(&self) {
|
||||||
let rpc = rpc::Manager::global().await;
|
let mut displays = self.displays.write().await;
|
||||||
|
displays.clear();
|
||||||
|
|
||||||
let mut rx = rpc.client().subscribe_change_display_brightness_rx();
|
let controllers = Display::enumerate();
|
||||||
|
|
||||||
loop {
|
for display in controllers {
|
||||||
if let Ok(display_brightness) = rx.recv().await {
|
let controller = Arc::new(RwLock::new(display));
|
||||||
if let Err(err) = self.set_display_brightness(display_brightness).await {
|
let state = Arc::new(RwLock::new(DisplayState::default()));
|
||||||
error!("set_display_brightness failed. {:?}", err);
|
let handler = DisplayHandler {
|
||||||
}
|
state: state.clone(),
|
||||||
}
|
controller: controller.clone(),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
handler.fetch_state().await;
|
||||||
}
|
|
||||||
None => anyhow::bail!("display#{} is missed.", index),
|
displays.push(Arc::new(RwLock::new(handler)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_display(&self, index: usize) -> anyhow::Result<OwnedMutexGuard<DisplayConfig>> {
|
pub async fn get_displays(&self) -> Vec<DisplayState> {
|
||||||
let mut displays = self.displays.lock().await;
|
let displays = self.displays.read().await;
|
||||||
match displays.get_mut(&index) {
|
let mut states = Vec::new();
|
||||||
Some(config) => {
|
for display in displays.iter() {
|
||||||
let mut config = config.to_owned().lock_owned().await;
|
let state = display.read().await.state.read().await.clone();
|
||||||
if config.last_modified_at > SystemTime::now().sub(Duration::from_secs(10)) {
|
states.push(state);
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
states
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_display_brightness(
|
fn subscribe_setting_request(&mut self) {
|
||||||
&self,
|
let displays = self.displays.clone();
|
||||||
display_brightness: DisplayBrightness,
|
let displays_changed_sender = self.displays_changed_sender.clone();
|
||||||
) -> anyhow::Result<()> {
|
let handler = tokio::spawn(async move {
|
||||||
match Display::enumerate().get_mut(display_brightness.display_index) {
|
let channels = BoardMessageChannels::global().await;
|
||||||
Some(display) => {
|
let mut request_rx = channels.display_setting_request_sender.subscribe();
|
||||||
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;
|
while let Ok(message) = request_rx.recv().await {
|
||||||
|
let displays = displays.write().await;
|
||||||
|
|
||||||
rpc.publish_desktop_cmd(
|
let display = displays.get(message.display_index);
|
||||||
format!("display{}/brightness", display_brightness.display_index)
|
if display.is_none() {
|
||||||
.as_str(),
|
warn!("display#{} not found", message.display_index);
|
||||||
target.to_be_bytes().to_vec(),
|
continue;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
|
||||||
info!(
|
|
||||||
"can not get display#{} brightness. {:?}",
|
let display = display.unwrap().write().await;
|
||||||
display_brightness.display_index, err
|
let result = match message.setting {
|
||||||
);
|
DisplaySetting::Brightness(value) => display.set_brightness(value as u16).await,
|
||||||
if let Brightness::Absolute(v) = display_brightness.brightness {
|
DisplaySetting::Contrast(value) => display.set_contrast(value as u16).await,
|
||||||
display.handle.set_vcp_feature(0x10, v).map_err(|err| {
|
DisplaySetting::Mode(value) => display.set_mode(value as u16).await,
|
||||||
anyhow::anyhow!("can not set brightness. {:?}", err)
|
|
||||||
})?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("failed to set display setting: {}", err);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
drop(display);
|
||||||
|
|
||||||
|
let mut states = Vec::new();
|
||||||
|
for display in displays.iter() {
|
||||||
|
let state = display.read().await.state.read().await.clone();
|
||||||
|
states.push(state);
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
warn!("display#{} is not found.", display_brightness.display_index);
|
if let Err(err) = displays_changed_sender.send(states) {
|
||||||
|
error!("failed to send displays changed event: {}", err);
|
||||||
}
|
}
|
||||||
|
yield_now().await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.setting_request_handler = Some(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_displays_changed(&self) -> watch::Receiver<Vec<DisplayState>> {
|
||||||
|
self.displays_changed_sender.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DisplayManager {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(handler) = self.setting_request_handler.take() {
|
||||||
|
handler.abort();
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
// mod brightness;
|
// mod brightness;
|
||||||
// mod manager;
|
// mod manager;
|
||||||
mod display_config;
|
mod display_state;
|
||||||
|
mod manager;
|
||||||
|
mod display_handler;
|
||||||
|
|
||||||
pub use display_config::*;
|
pub use display_state::*;
|
||||||
|
|
||||||
// pub use brightness::*;
|
// pub use brightness::*;
|
||||||
// pub use manager::*;
|
pub use manager::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,45 +2,47 @@ use color_space::{Hsv, Rgb};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct LedColor {
|
pub struct LedColor([u8; 3]);
|
||||||
bits: [u8; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LedColor {
|
impl LedColor {
|
||||||
pub fn default() -> Self {
|
pub fn default() -> Self {
|
||||||
Self { bits: [0, 0, 0] }
|
Self ([0, 0, 0] )
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self { bits: [r, g, b] }
|
Self ([r, g, b])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
||||||
let rgb = Rgb::from(Hsv::new(h, s, v));
|
let rgb = Rgb::from(Hsv::new(h, s, v));
|
||||||
Self { bits: [rgb.r as u8, rgb.g as u8, rgb.b as u8] }
|
Self ([rgb.r as u8, rgb.g as u8, rgb.b as u8])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rgb(&self) -> [u8; 3] {
|
pub fn get_rgb(&self) -> [u8; 3] {
|
||||||
self.bits
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.bits.iter().any(|bit| *bit == 0)
|
self.0.iter().any(|bit| *bit == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
self.bits = [r, g, b];
|
self.0 = [r, g, b];
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
self.bits = [
|
self.0 = [
|
||||||
(self.bits[0] / 2 + r / 2),
|
(self.0[0] / 2 + r / 2),
|
||||||
(self.bits[1] / 2 + g / 2),
|
(self.0[1] / 2 + g / 2),
|
||||||
(self.bits[2] / 2 + b / 2),
|
(self.0[2] / 2 + b / 2),
|
||||||
];
|
];
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes (&self) -> [u8; 3] {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for LedColor {
|
impl Serialize for LedColor {
|
||||||
@ -48,7 +50,7 @@ impl Serialize for LedColor {
|
|||||||
where
|
where
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
{
|
{
|
||||||
let hex = format!("#{}", hex::encode(self.bits));
|
let hex = format!("#{}", hex::encode(self.0));
|
||||||
serializer.serialize_str(hex.as_str())
|
serializer.serialize_str(hex.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,21 @@ mod ambient_light;
|
|||||||
mod display;
|
mod display;
|
||||||
mod led_color;
|
mod led_color;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
pub mod screenshot;
|
mod screenshot;
|
||||||
mod screenshot_manager;
|
mod screenshot_manager;
|
||||||
|
mod volume;
|
||||||
|
|
||||||
use ambient_light::{Border, LedColorsPublisher, LedStripConfig, LedStripConfigGroup};
|
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup};
|
||||||
use core_graphics::display::{
|
use display::{DisplayManager, DisplayState};
|
||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
|
||||||
};
|
|
||||||
use display_info::DisplayInfo;
|
use display_info::DisplayInfo;
|
||||||
use paris::{error, info, warn};
|
use paris::{error, info, warn};
|
||||||
|
use rpc::{BoardInfo, MqttRpc, UdpRpc};
|
||||||
use screenshot::Screenshot;
|
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, Manager};
|
use tauri::{http::ResponseBuilder, regex, Manager};
|
||||||
|
use volume::VolumeManager;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(remote = "DisplayInfo")]
|
#[serde(remote = "DisplayInfo")]
|
||||||
@ -135,8 +136,8 @@ async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn send_colors(buffer: Vec<u8>) -> Result<(), String> {
|
async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> {
|
||||||
ambient_light::LedColorsPublisher::send_colors(buffer)
|
ambient_light::LedColorsPublisher::send_colors(offset, buffer)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("can not send colors: {}", e);
|
error!("can not send colors: {}", e);
|
||||||
@ -145,14 +146,14 @@ async fn send_colors(buffer: Vec<u8>) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn move_strip_part(display_id: u32, border: Border, target_start: usize) -> Result<(), String> {
|
async fn move_strip_part(
|
||||||
|
display_id: u32,
|
||||||
|
border: Border,
|
||||||
|
target_start: usize,
|
||||||
|
) -> Result<(), String> {
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
let config_manager = ambient_light::ConfigManager::global().await;
|
||||||
config_manager
|
config_manager
|
||||||
.move_strip_part(
|
.move_strip_part(display_id, border, target_start)
|
||||||
display_id,
|
|
||||||
border,
|
|
||||||
target_start,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("can not move strip part: {}", e);
|
error!("can not move strip part: {}", e);
|
||||||
@ -172,6 +173,46 @@ async fn reverse_led_strip_part(display_id: u32, border: Border) -> Result<(), S
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn set_color_calibration(calibration: ColorCalibration) -> Result<(), String> {
|
||||||
|
let config_manager = ambient_light::ConfigManager::global().await;
|
||||||
|
config_manager
|
||||||
|
.set_color_calibration(calibration)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("can not set color calibration: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn read_config() -> ambient_light::LedStripConfigGroup {
|
||||||
|
let config_manager = ambient_light::ConfigManager::global().await;
|
||||||
|
config_manager.configs().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_boards() -> Result<Vec<BoardInfo>, String> {
|
||||||
|
let udp_rpc = UdpRpc::global().await;
|
||||||
|
|
||||||
|
if let Err(e) = udp_rpc {
|
||||||
|
return Err(format!("can not ping: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
let udp_rpc = udp_rpc.as_ref().unwrap();
|
||||||
|
|
||||||
|
let boards = udp_rpc.get_boards().await;
|
||||||
|
let boards = boards.into_iter().collect::<Vec<_>>();
|
||||||
|
Ok(boards)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_displays() -> Vec<DisplayState> {
|
||||||
|
let display_manager = DisplayManager::global().await;
|
||||||
|
|
||||||
|
display_manager.get_displays().await
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@ -182,6 +223,10 @@ async fn main() {
|
|||||||
let led_color_publisher = ambient_light::LedColorsPublisher::global().await;
|
let led_color_publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
led_color_publisher.start();
|
led_color_publisher.start();
|
||||||
|
|
||||||
|
let _mqtt = MqttRpc::global().await;
|
||||||
|
|
||||||
|
let _volume = VolumeManager::global().await;
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
@ -194,6 +239,10 @@ async fn main() {
|
|||||||
send_colors,
|
send_colors,
|
||||||
move_strip_part,
|
move_strip_part,
|
||||||
reverse_led_strip_part,
|
reverse_led_strip_part,
|
||||||
|
set_color_calibration,
|
||||||
|
read_config,
|
||||||
|
get_boards,
|
||||||
|
get_displays
|
||||||
])
|
])
|
||||||
.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", "*");
|
||||||
@ -359,6 +408,7 @@ async fn main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let publisher = ambient_light::LedColorsPublisher::global().await;
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
||||||
@ -377,6 +427,47 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match UdpRpc::global().await {
|
||||||
|
Ok(udp_rpc) => {
|
||||||
|
let mut receiver = udp_rpc.subscribe_boards_change();
|
||||||
|
loop {
|
||||||
|
if let Err(err) = receiver.changed().await {
|
||||||
|
error!("boards change receiver changed error: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let boards = receiver.borrow().clone();
|
||||||
|
|
||||||
|
let boards = boards.into_iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
app_handle.emit_all("boards_changed", boards).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("udp rpc error: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let display_manager = DisplayManager::global().await;
|
||||||
|
let mut rx =display_manager.subscribe_displays_changed();
|
||||||
|
|
||||||
|
while rx.changed().await.is_ok() {
|
||||||
|
let displays = rx.borrow().clone();
|
||||||
|
|
||||||
|
log::info!("displays changed. emit displays_changed event.");
|
||||||
|
|
||||||
|
app_handle.emit_all("displays_changed", displays).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
163
src-tauri/src/rpc/board.rs
Normal file
163
src-tauri/src/rpc/board.rs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use paris::{error, info, warn};
|
||||||
|
use tokio::{io, net::UdpSocket, sync::RwLock, task::yield_now, time::timeout};
|
||||||
|
|
||||||
|
use crate::rpc::DisplaySettingRequest;
|
||||||
|
|
||||||
|
use super::{BoardConnectStatus, BoardInfo};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Board {
|
||||||
|
pub info: Arc<RwLock<BoardInfo>>,
|
||||||
|
socket: Option<Arc<UdpSocket>>,
|
||||||
|
listen_handler: Option<tokio::task::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
pub fn new(info: BoardInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
info: Arc::new(RwLock::new(info)),
|
||||||
|
socket: None,
|
||||||
|
listen_handler: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_socket(&mut self) -> anyhow::Result<()> {
|
||||||
|
let info = self.info.read().await;
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
|
||||||
|
socket.connect((info.address, info.port)).await?;
|
||||||
|
let socket = Arc::new(socket);
|
||||||
|
self.socket = Some(socket.clone());
|
||||||
|
|
||||||
|
let info = self.info.clone();
|
||||||
|
|
||||||
|
let handler = tokio::spawn(async move {
|
||||||
|
let mut buf = [0u8; 128];
|
||||||
|
if let Err(err) = socket.readable().await {
|
||||||
|
error!("socket read error: {:?}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let board_message_channels = crate::rpc::channels::BoardMessageChannels::global().await;
|
||||||
|
|
||||||
|
let display_setting_request_sender = board_message_channels
|
||||||
|
.display_setting_request_sender
|
||||||
|
.clone();
|
||||||
|
let volume_setting_request_sender =
|
||||||
|
board_message_channels.volume_setting_request_sender.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match socket.try_recv(&mut buf) {
|
||||||
|
Ok(len) => {
|
||||||
|
log::info!("recv: {:?}", &buf[..len]);
|
||||||
|
if buf[0] == 3 {
|
||||||
|
let result =
|
||||||
|
display_setting_request_sender.send(DisplaySettingRequest {
|
||||||
|
display_index: buf[1] as usize,
|
||||||
|
setting: crate::rpc::DisplaySetting::Brightness(buf[2]),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("send display setting request to channel failed: {:?}", err);
|
||||||
|
}
|
||||||
|
} else if buf[0] == 4 {
|
||||||
|
let result = volume_setting_request_sender.send(buf[1] as f32 / 100.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
yield_now().await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("socket recv error: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.listen_handler = Some(handler);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_colors(&self, buf: &[u8]) {
|
||||||
|
let info = self.info.read().await;
|
||||||
|
if self.socket.is_none() || info.connect_status != BoardConnectStatus::Connected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let socket = self.socket.as_ref().unwrap();
|
||||||
|
|
||||||
|
socket.send(buf).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check(&self) -> anyhow::Result<()> {
|
||||||
|
let info = self.info.read().await;
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
socket.connect((info.address, info.port)).await?;
|
||||||
|
drop(info);
|
||||||
|
|
||||||
|
let instant = std::time::Instant::now();
|
||||||
|
|
||||||
|
socket.send(&[1]).await?;
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
let recv_future = socket.recv(&mut buf);
|
||||||
|
|
||||||
|
let check_result = timeout(Duration::from_secs(1), recv_future).await;
|
||||||
|
let mut info = self.info.write().await;
|
||||||
|
match check_result {
|
||||||
|
Ok(_) => {
|
||||||
|
let ttl = instant.elapsed();
|
||||||
|
if buf == [1] {
|
||||||
|
info.connect_status = BoardConnectStatus::Connected;
|
||||||
|
} else {
|
||||||
|
if let BoardConnectStatus::Connecting(retry) = info.connect_status {
|
||||||
|
if retry < 10 {
|
||||||
|
info.connect_status = BoardConnectStatus::Connecting(retry + 1);
|
||||||
|
info!("reconnect: {}", retry + 1);
|
||||||
|
} else {
|
||||||
|
info.connect_status = BoardConnectStatus::Disconnected;
|
||||||
|
warn!("board Disconnected: bad pong.");
|
||||||
|
}
|
||||||
|
} else if info.connect_status != BoardConnectStatus::Disconnected {
|
||||||
|
info.connect_status = BoardConnectStatus::Connecting(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.ttl = Some(ttl.as_millis());
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if let BoardConnectStatus::Connecting(retry) = info.connect_status {
|
||||||
|
if retry < 10 {
|
||||||
|
info.connect_status = BoardConnectStatus::Connecting(retry + 1);
|
||||||
|
info!("reconnect: {}", retry + 1);
|
||||||
|
} else {
|
||||||
|
info.connect_status = BoardConnectStatus::Disconnected;
|
||||||
|
warn!("board Disconnected: timeout");
|
||||||
|
}
|
||||||
|
} else if info.connect_status != BoardConnectStatus::Disconnected {
|
||||||
|
info.connect_status = BoardConnectStatus::Connecting(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
info.ttl = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.checked_at = Some(std::time::SystemTime::now());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Board {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(handler) = self.listen_handler.take() {
|
||||||
|
info!("aborting listen handler");
|
||||||
|
tokio::task::block_in_place(move || {
|
||||||
|
handler.abort();
|
||||||
|
});
|
||||||
|
info!("listen handler aborted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src-tauri/src/rpc/board_info.rs
Normal file
36
src-tauri/src/rpc/board_info.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use std::{net::Ipv4Addr, time::Duration};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum BoardConnectStatus {
|
||||||
|
Connected,
|
||||||
|
Connecting(u8),
|
||||||
|
Disconnected,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct BoardInfo {
|
||||||
|
pub fullname: String,
|
||||||
|
pub host: String,
|
||||||
|
pub address: Ipv4Addr,
|
||||||
|
pub port: u16,
|
||||||
|
pub connect_status: BoardConnectStatus,
|
||||||
|
pub checked_at: Option<std::time::SystemTime>,
|
||||||
|
pub ttl: Option<u128>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardInfo {
|
||||||
|
pub fn new(fullname: String, host: String, address: Ipv4Addr, port: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
fullname,
|
||||||
|
host,
|
||||||
|
address,
|
||||||
|
port,
|
||||||
|
connect_status: BoardConnectStatus::Unknown,
|
||||||
|
checked_at: None,
|
||||||
|
ttl: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src-tauri/src/rpc/channels.rs
Normal file
31
src-tauri/src/rpc/channels.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::{broadcast, OnceCell};
|
||||||
|
|
||||||
|
use super::DisplaySettingRequest;
|
||||||
|
|
||||||
|
pub struct BoardMessageChannels {
|
||||||
|
pub display_setting_request_sender: Arc<broadcast::Sender<DisplaySettingRequest>>,
|
||||||
|
pub volume_setting_request_sender: Arc<broadcast::Sender<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardMessageChannels {
|
||||||
|
pub async fn global() -> &'static Self {
|
||||||
|
static BOARD_MESSAGE_CHANNELS: OnceCell<BoardMessageChannels> = OnceCell::const_new();
|
||||||
|
|
||||||
|
BOARD_MESSAGE_CHANNELS.get_or_init(|| async {Self::new()}).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (display_setting_request_sender, _) = broadcast::channel(16);
|
||||||
|
let display_setting_request_sender = Arc::new(display_setting_request_sender);
|
||||||
|
|
||||||
|
let (volume_setting_request_sender, _) = broadcast::channel(16);
|
||||||
|
let volume_setting_request_sender = Arc::new(volume_setting_request_sender);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
display_setting_request_sender,
|
||||||
|
volume_setting_request_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src-tauri/src/rpc/display_setting_request.rs
Normal file
13
src-tauri/src/rpc/display_setting_request.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum DisplaySetting {
|
||||||
|
Brightness(u8),
|
||||||
|
Contrast(u8),
|
||||||
|
Mode(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DisplaySettingRequest {
|
||||||
|
pub display_index: usize,
|
||||||
|
pub setting: DisplaySetting,
|
||||||
|
}
|
@ -1,3 +1,13 @@
|
|||||||
|
mod board_info;
|
||||||
mod mqtt;
|
mod mqtt;
|
||||||
|
mod udp;
|
||||||
|
mod board;
|
||||||
|
mod display_setting_request;
|
||||||
|
mod channels;
|
||||||
|
|
||||||
|
pub use board_info::*;
|
||||||
pub use mqtt::*;
|
pub use mqtt::*;
|
||||||
|
pub use udp::*;
|
||||||
|
pub use board::*;
|
||||||
|
pub use display_setting_request::*;
|
||||||
|
pub use channels::*;
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
use paho_mqtt as mqtt;
|
use paho_mqtt as mqtt;
|
||||||
use paris::{error, info, warn};
|
use paris::{info, warn};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::{format_description, OffsetDateTime};
|
use time::{format_description, OffsetDateTime};
|
||||||
use tokio::{sync::OnceCell, task};
|
use tokio::{sync::OnceCell, task};
|
||||||
|
|
||||||
|
use crate::ambient_light::{ColorCalibration, ConfigManager};
|
||||||
|
|
||||||
const DISPLAY_TOPIC: &'static str = "display-ambient-light/display";
|
const DISPLAY_TOPIC: &'static str = "display-ambient-light/display";
|
||||||
const DESKTOP_TOPIC: &'static str = "display-ambient-light/desktop";
|
const DESKTOP_TOPIC: &'static str = "display-ambient-light/desktop";
|
||||||
const DISPLAY_BRIGHTNESS_TOPIC: &'static str = "display-ambient-light/board/brightness";
|
const COLOR_CALIBRATION: &'static str = "display-ambient-light/desktop/color-calibration";
|
||||||
const BOARD_SEND_CMD: &'static str = "display-ambient-light/board/cmd";
|
|
||||||
|
|
||||||
pub struct MqttRpc {
|
pub struct MqttRpc {
|
||||||
client: mqtt::AsyncClient,
|
client: mqtt::AsyncClient,
|
||||||
@ -40,7 +41,7 @@ impl MqttRpc {
|
|||||||
|
|
||||||
client.subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1);
|
client.subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1);
|
||||||
});
|
});
|
||||||
client.set_connection_lost_callback(|client| {
|
client.set_connection_lost_callback(|_| {
|
||||||
info!("MQTT server connection lost.");
|
info!("MQTT server connection lost.");
|
||||||
});
|
});
|
||||||
client.set_disconnected_callback(|_, a1, a2| {
|
client.set_disconnected_callback(|_, a1, a2| {
|
||||||
@ -88,99 +89,36 @@ impl MqttRpc {
|
|||||||
Ok(Self { client })
|
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<()> {
|
pub async fn initialize(&self) -> anyhow::Result<()> {
|
||||||
// self.subscribe_board()?;
|
|
||||||
// self.subscribe_display()?;
|
|
||||||
self.broadcast_desktop_online();
|
self.broadcast_desktop_online();
|
||||||
|
Self::publish_color_calibration_worker();
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_board(&self) -> anyhow::Result<()> {
|
fn publish_color_calibration_worker() {
|
||||||
self.client
|
tokio::spawn(async move {
|
||||||
.subscribe("display-ambient-light/board/#", mqtt::QOS_1)
|
let mqtt = Self::global().await;
|
||||||
.wait()
|
let config_manager = ConfigManager::global().await;
|
||||||
.map_err(|err| anyhow::anyhow!("subscribe board failed. {:?}", err))
|
let mut config_receiver = config_manager.clone_config_update_receiver();
|
||||||
.map(|_| ())
|
|
||||||
|
let config = config_manager.configs().await;
|
||||||
|
if let Err(err) = mqtt
|
||||||
|
.publish_color_calibration(config.color_calibration)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("can not publish color calibration. {}", err);
|
||||||
}
|
}
|
||||||
fn subscribe_display(&self) -> anyhow::Result<()> {
|
|
||||||
self.client
|
while config_receiver.changed().await.is_ok() {
|
||||||
.subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1)
|
let config = config_receiver.borrow().clone();
|
||||||
.wait()
|
if let Err(err) = mqtt
|
||||||
.map_err(|err| anyhow::anyhow!("subscribe board failed. {:?}", err))
|
.publish_color_calibration(config.color_calibration)
|
||||||
.map(|_| ())
|
.await
|
||||||
|
{
|
||||||
|
warn!("can not publish color calibration. {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast_desktop_online(&self) {
|
fn broadcast_desktop_online(&self) {
|
||||||
@ -212,30 +150,14 @@ impl MqttRpc {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn publish_led_sub_pixels(&self, payload: Vec<u8>) -> anyhow::Result<()> {
|
pub async fn publish_color_calibration(&self, payload: ColorCalibration) -> anyhow::Result<()> {
|
||||||
self.client
|
self.client
|
||||||
.publish(mqtt::Message::new(
|
.publish(mqtt::Message::new(
|
||||||
"display-ambient-light/desktop/colors",
|
COLOR_CALIBRATION,
|
||||||
payload,
|
payload.to_bytes(),
|
||||||
mqtt::QOS_1,
|
mqtt::QOS_1,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error))
|
.map_err(|error| anyhow::anyhow!("mqtt publish color calibration 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
216
src-tauri/src/rpc/udp.rs
Normal file
216
src-tauri/src/rpc/udp.rs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use futures::future::join_all;
|
||||||
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
|
use paris::{error, info, warn};
|
||||||
|
use tokio::sync::{watch, OnceCell, RwLock};
|
||||||
|
|
||||||
|
use super::{Board, BoardInfo};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UdpRpc {
|
||||||
|
boards: Arc<RwLock<HashMap<String, Board>>>,
|
||||||
|
boards_change_sender: Arc<watch::Sender<Vec<BoardInfo>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UdpRpc {
|
||||||
|
pub async fn global() -> &'static anyhow::Result<Self> {
|
||||||
|
static UDP_RPC: OnceCell<anyhow::Result<UdpRpc>> = OnceCell::const_new();
|
||||||
|
|
||||||
|
UDP_RPC
|
||||||
|
.get_or_init(|| async {
|
||||||
|
let udp_rpc = UdpRpc::new().await?;
|
||||||
|
udp_rpc.initialize().await;
|
||||||
|
Ok(udp_rpc)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn new() -> anyhow::Result<Self> {
|
||||||
|
let boards = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
let (boards_change_sender, _) = watch::channel(Vec::new());
|
||||||
|
let boards_change_sender = Arc::new(boards_change_sender);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
boards,
|
||||||
|
boards_change_sender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize(&self) {
|
||||||
|
let shared_self = Arc::new(self.clone());
|
||||||
|
|
||||||
|
let shared_self_for_search = shared_self.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match shared_self_for_search.search_boards().await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("search_boards finished");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("search_boards failed: {:?}", err);
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let shared_self_for_check = shared_self.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
shared_self_for_check.check_boards().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search_boards(&self) -> anyhow::Result<()> {
|
||||||
|
let service_type = "_ambient_light._udp.local.";
|
||||||
|
let mdns = ServiceDaemon::new()?;
|
||||||
|
let receiver = mdns.browse(&service_type).map_err(|e| {
|
||||||
|
warn!("Failed to browse for {:?}: {:?}", service_type, e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
let sender = self.boards_change_sender.clone();
|
||||||
|
|
||||||
|
while let Ok(event) = receiver.recv() {
|
||||||
|
match event {
|
||||||
|
ServiceEvent::ServiceResolved(info) => {
|
||||||
|
info!(
|
||||||
|
"Resolved a new service: {} host: {} port: {} IP: {:?} TXT properties: {:?}",
|
||||||
|
info.get_fullname(),
|
||||||
|
info.get_hostname(),
|
||||||
|
info.get_port(),
|
||||||
|
info.get_addresses(),
|
||||||
|
info.get_properties(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut boards = self.boards.write().await;
|
||||||
|
|
||||||
|
let board_info = BoardInfo::new(
|
||||||
|
info.get_fullname().to_string(),
|
||||||
|
info.get_hostname().to_string(),
|
||||||
|
info.get_addresses().iter().next().unwrap().clone(),
|
||||||
|
info.get_port(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut board = Board::new(board_info.clone());
|
||||||
|
|
||||||
|
if let Err(err) = board.init_socket().await {
|
||||||
|
error!("failed to init socket: {:?}", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if boards.insert(board_info.fullname.clone(), board).is_some() {
|
||||||
|
info!("added board {:?}", board_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx_boards = boards
|
||||||
|
.values()
|
||||||
|
.map(|it| async move { it.info.read().await.clone() });
|
||||||
|
let tx_boards = join_all(tx_boards).await;
|
||||||
|
|
||||||
|
drop(boards);
|
||||||
|
|
||||||
|
sender.send(tx_boards)?;
|
||||||
|
}
|
||||||
|
ServiceEvent::ServiceRemoved(_, fullname) => {
|
||||||
|
info!("removed board {:?}", fullname);
|
||||||
|
let mut boards = self.boards.write().await;
|
||||||
|
if boards.remove(&fullname).is_some() {
|
||||||
|
info!("removed board {:?} successful", fullname);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx_boards = boards
|
||||||
|
.values()
|
||||||
|
.map(|it| async move { it.info.read().await.clone() });
|
||||||
|
let tx_boards = join_all(tx_boards).await;
|
||||||
|
|
||||||
|
drop(boards);
|
||||||
|
|
||||||
|
sender.send(tx_boards)?;
|
||||||
|
}
|
||||||
|
other_event => {
|
||||||
|
// log::info!("{:?}", &other_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_boards_change(&self) -> watch::Receiver<Vec<BoardInfo>> {
|
||||||
|
self.boards_change_sender.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_boards(&self) -> Vec<BoardInfo> {
|
||||||
|
self.boards_change_sender.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_to_all(&self, buff: &Vec<u8>) -> anyhow::Result<()> {
|
||||||
|
let boards = self.boards.read().await;
|
||||||
|
|
||||||
|
for board in boards.values() {
|
||||||
|
board.send_colors(buff).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let socket = self.socket.clone();
|
||||||
|
|
||||||
|
// let handlers = boards.into_iter().map(|board| {
|
||||||
|
// if board.connect_status == BoardConnectStatus::Disconnected {
|
||||||
|
// return tokio::spawn(async move {
|
||||||
|
// log::debug!("board {} is disconnected, skip.", board.host);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let socket = socket.clone();
|
||||||
|
// let buff = buff.clone();
|
||||||
|
// tokio::spawn(async move {
|
||||||
|
// match socket.send_to(&buff, (board.address, board.port)).await {
|
||||||
|
// Ok(_) => {}
|
||||||
|
// Err(err) => {
|
||||||
|
// error!("failed to send to {}: {:?}", board.host, err);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
|
||||||
|
// join_all(handlers).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_boards(&self) {
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||||
|
loop {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
let boards = self.boards.read().await;
|
||||||
|
|
||||||
|
if boards.is_empty() {
|
||||||
|
info!("no boards found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for board in boards.values() {
|
||||||
|
if let Err(err) = board.check().await {
|
||||||
|
error!("failed to check board: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx_boards = boards
|
||||||
|
.values()
|
||||||
|
.map(|it| async move { it.info.read().await.clone() });
|
||||||
|
let tx_boards = join_all(tx_boards).await;
|
||||||
|
|
||||||
|
drop(boards);
|
||||||
|
|
||||||
|
let board_change_sender = self.boards_change_sender.clone();
|
||||||
|
if let Err(err) = board_change_sender.send(tx_boards) {
|
||||||
|
error!("failed to send board change: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(board_change_sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::{iter, cell::Ref};
|
use std::iter;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::async_runtime::{RwLock, Mutex};
|
use tauri::async_runtime::RwLock;
|
||||||
|
|
||||||
use crate::{ambient_light::LedStripConfig, led_color::LedColor};
|
use crate::{ambient_light::LedStripConfig, led_color::LedColor};
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ pub struct Screenshot {
|
|||||||
pub bytes_per_row: usize,
|
pub bytes_per_row: usize,
|
||||||
pub bytes: Arc<RwLock<Vec<u8>>>,
|
pub bytes: Arc<RwLock<Vec<u8>>>,
|
||||||
pub scale_factor: f32,
|
pub scale_factor: f32,
|
||||||
|
pub bound_scale_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
static SINGLE_AXIS_POINTS: usize = 5;
|
static SINGLE_AXIS_POINTS: usize = 5;
|
||||||
@ -27,6 +28,7 @@ impl Screenshot {
|
|||||||
bytes_per_row: usize,
|
bytes_per_row: usize,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
|
bound_scale_factor: f32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
display_id,
|
display_id,
|
||||||
@ -35,12 +37,15 @@ impl Screenshot {
|
|||||||
bytes_per_row,
|
bytes_per_row,
|
||||||
bytes: Arc::new(RwLock::new(bytes)),
|
bytes: Arc::new(RwLock::new(bytes)),
|
||||||
scale_factor,
|
scale_factor,
|
||||||
|
bound_scale_factor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sample_points(&self, config: &LedStripConfig) -> Vec<LedSamplePoints> {
|
pub fn get_sample_points(&self, config: &LedStripConfig) -> Vec<LedSamplePoints> {
|
||||||
let height = self.height as usize;
|
let height = self.height as usize;
|
||||||
let width = self.width as usize;
|
let width = self.width as usize;
|
||||||
|
// let height = CGDisplay::new(self.display_id).bounds().size.height as usize;
|
||||||
|
// let width = CGDisplay::new(self.display_id).bounds().size.width as usize;
|
||||||
|
|
||||||
match config.border {
|
match config.border {
|
||||||
crate::ambient_light::Border::Top => {
|
crate::ambient_light::Border::Top => {
|
||||||
@ -153,6 +158,7 @@ impl Screenshot {
|
|||||||
b += bitmap[position] as f64;
|
b += bitmap[position] as f64;
|
||||||
g += bitmap[position + 1] as f64;
|
g += bitmap[position + 1] as f64;
|
||||||
r += bitmap[position + 2] as f64;
|
r += bitmap[position + 2] as f64;
|
||||||
|
// log::info!("position: {}, total: {}", position, bitmap.len());
|
||||||
}
|
}
|
||||||
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
||||||
colors.push(color);
|
colors.push(color);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use std::cell::{Ref, RefCell};
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use core_graphics::display::{
|
use core_graphics::display::{
|
||||||
@ -11,11 +10,7 @@ use tokio::sync::{broadcast, watch, OnceCell};
|
|||||||
use tokio::time::{self, Duration};
|
use tokio::time::{self, Duration};
|
||||||
|
|
||||||
use crate::screenshot::LedSamplePoints;
|
use crate::screenshot::LedSamplePoints;
|
||||||
use crate::{
|
use crate::{ambient_light::SamplePointMapper, led_color::LedColor, screenshot::Screenshot};
|
||||||
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");
|
||||||
@ -37,6 +32,9 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
|
|
||||||
let bytes = buffer.bytes().to_owned();
|
let bytes = buffer.bytes().to_owned();
|
||||||
|
|
||||||
|
let cg_display = CGDisplay::new(display_id);
|
||||||
|
let bound_scale_factor = (cg_display.bounds().size.width / width as f64) as f32;
|
||||||
|
|
||||||
Ok(Screenshot::new(
|
Ok(Screenshot::new(
|
||||||
display_id,
|
display_id,
|
||||||
height as u32,
|
height as u32,
|
||||||
@ -44,18 +42,19 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
bytes_per_row,
|
bytes_per_row,
|
||||||
bytes,
|
bytes,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
|
bound_scale_factor,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_display_colors(
|
pub fn get_display_colors(
|
||||||
display_id: u32,
|
display_id: u32,
|
||||||
sample_points: &Vec<Vec<LedSamplePoints>>,
|
sample_points: &Vec<Vec<LedSamplePoints>>,
|
||||||
|
bound_scale_factor: f32,
|
||||||
) -> anyhow::Result<Vec<LedColor>> {
|
) -> anyhow::Result<Vec<LedColor>> {
|
||||||
log::debug!("take_screenshot");
|
log::debug!("take_screenshot");
|
||||||
let cg_display = CGDisplay::new(display_id);
|
let cg_display = CGDisplay::new(display_id);
|
||||||
|
|
||||||
let mut colors = vec![];
|
let mut colors = vec![];
|
||||||
let start_at = std::time::Instant::now();
|
|
||||||
for points in sample_points {
|
for points in sample_points {
|
||||||
if points.len() == 0 {
|
if points.len() == 0 {
|
||||||
continue;
|
continue;
|
||||||
@ -69,14 +68,23 @@ pub fn get_display_colors(
|
|||||||
let (start_y, end_y) = (usize::min(start_y, end_y), usize::max(start_y, end_y));
|
let (start_y, end_y) = (usize::min(start_y, end_y), usize::max(start_y, end_y));
|
||||||
|
|
||||||
let origin = CGPoint {
|
let origin = CGPoint {
|
||||||
x: start_x as f64 + cg_display.bounds().origin.x,
|
x: start_x as f64 * bound_scale_factor as f64 + cg_display.bounds().origin.x,
|
||||||
y: start_y as f64 + cg_display.bounds().origin.y,
|
y: start_y as f64 * bound_scale_factor as f64 + cg_display.bounds().origin.y,
|
||||||
};
|
};
|
||||||
let size = CGSize {
|
let size = CGSize {
|
||||||
width: (end_x - start_x + 1) as f64,
|
width: (end_x - start_x + 1) as f64,
|
||||||
height: (end_y - start_y + 1) as f64,
|
height: (end_y - start_y + 1) as f64,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// log::info!(
|
||||||
|
// "origin: {:?}, size: {:?}, start_x: {}, start_y: {}, bounds: {:?}",
|
||||||
|
// origin,
|
||||||
|
// size,
|
||||||
|
// start_x,
|
||||||
|
// start_y,
|
||||||
|
// cg_display.bounds().size
|
||||||
|
// );
|
||||||
|
|
||||||
let cg_image = CGDisplay::screenshot(
|
let cg_image = CGDisplay::screenshot(
|
||||||
CGRect::new(&origin, &size),
|
CGRect::new(&origin, &size),
|
||||||
kCGWindowListOptionOnScreenOnly,
|
kCGWindowListOptionOnScreenOnly,
|
||||||
@ -102,13 +110,6 @@ pub fn get_display_colors(
|
|||||||
colors.append(&mut part_colors);
|
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)
|
Ok(colors)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,8 +152,7 @@ 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 interval = time::interval(Duration::from_millis(1000));
|
||||||
let mut start = tokio::time::Instant::now();
|
|
||||||
|
|
||||||
let screenshot = screenshot.unwrap();
|
let screenshot = screenshot.unwrap();
|
||||||
let (screenshot_tx, screenshot_rx) = watch::channel(screenshot);
|
let (screenshot_tx, screenshot_rx) = watch::channel(screenshot);
|
||||||
@ -165,7 +165,6 @@ impl ScreenshotManager {
|
|||||||
let merged_screenshot_tx = merged_screenshot_tx.read().await.clone();
|
let merged_screenshot_tx = merged_screenshot_tx.read().await.clone();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
start = tokio::time::Instant::now();
|
|
||||||
Self::take_screenshot_loop(
|
Self::take_screenshot_loop(
|
||||||
display_id,
|
display_id,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
@ -190,9 +189,13 @@ impl ScreenshotManager {
|
|||||||
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 {
|
||||||
match merged_screenshot_tx.send(screenshot.clone()) {
|
match merged_screenshot_tx.send(screenshot.clone()) {
|
||||||
Ok(_) => {}
|
Ok(_) => {
|
||||||
Err(err) => {
|
log::info!(
|
||||||
// warn!("take_screenshot_loop: merged_screenshot_tx.send failed. display#{}. err: {}", display_id, err);
|
"take_screenshot_loop: merged_screenshot_tx.send success. display#{}",
|
||||||
|
display_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
screenshot_tx.send(screenshot).unwrap();
|
screenshot_tx.send(screenshot).unwrap();
|
||||||
@ -202,23 +205,6 @@ impl ScreenshotManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_colors(
|
|
||||||
&self,
|
|
||||||
configs: &Vec<SamplePointConfig>,
|
|
||||||
screenshots: &Vec<&Screenshot>,
|
|
||||||
) -> Vec<LedColor> {
|
|
||||||
let mut all_colors = vec![];
|
|
||||||
|
|
||||||
for (index, screenshot) in screenshots.iter().enumerate() {
|
|
||||||
let config = &configs[index];
|
|
||||||
let mut colors = screenshot.get_colors_by_sample_points(&config.points).await;
|
|
||||||
|
|
||||||
all_colors.append(&mut colors);
|
|
||||||
}
|
|
||||||
|
|
||||||
all_colors
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> {
|
pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> {
|
||||||
let total_leds = mappers
|
let total_leds = mappers
|
||||||
.iter()
|
.iter()
|
||||||
|
101
src-tauri/src/volume/manager.rs
Normal file
101
src-tauri/src/volume/manager.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
use std::{
|
||||||
|
mem,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use coreaudio::{
|
||||||
|
audio_unit::macos_helpers::get_default_device_id,
|
||||||
|
sys::{
|
||||||
|
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, kAudioObjectPropertyScopeOutput,
|
||||||
|
AudioObjectHasProperty, AudioObjectPropertyAddress, AudioObjectSetPropertyData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use paris::error;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
use crate::rpc::BoardMessageChannels;
|
||||||
|
|
||||||
|
pub struct VolumeManager {
|
||||||
|
current_volume: Arc<RwLock<f32>>,
|
||||||
|
handler: Option<tokio::task::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VolumeManager {
|
||||||
|
pub async fn global() -> &'static Self {
|
||||||
|
static VOLUME_MANAGER: OnceCell<VolumeManager> = OnceCell::const_new();
|
||||||
|
|
||||||
|
VOLUME_MANAGER
|
||||||
|
.get_or_init(|| async { Self::create() })
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create() -> Self {
|
||||||
|
let mut instance = Self {
|
||||||
|
current_volume: Arc::new(RwLock::new(0.0)),
|
||||||
|
handler: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.subscribe_volume_setting_request();
|
||||||
|
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscribe_volume_setting_request(&mut self) {
|
||||||
|
let handler = tokio::spawn(async {
|
||||||
|
let channels = BoardMessageChannels::global().await;
|
||||||
|
let mut request_rx = channels.volume_setting_request_sender.subscribe();
|
||||||
|
|
||||||
|
while let Ok(volume) = request_rx.recv().await {
|
||||||
|
if let Err(err) = Self::set_volume(volume) {
|
||||||
|
error!("failed to set volume: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.handler = Some(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_volume(volume: f32) -> anyhow::Result<()> {
|
||||||
|
log::debug!("set volume: {}", volume);
|
||||||
|
|
||||||
|
let device_id = get_default_device_id(false);
|
||||||
|
|
||||||
|
if device_id.is_none() {
|
||||||
|
anyhow::bail!("default audio output device is not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_id = device_id.unwrap();
|
||||||
|
|
||||||
|
let address = AudioObjectPropertyAddress {
|
||||||
|
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
||||||
|
mScope: kAudioObjectPropertyScopeOutput,
|
||||||
|
mElement: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("device id: {}", device_id);
|
||||||
|
log::debug!("address: {:?}", address);
|
||||||
|
|
||||||
|
if 0 == unsafe { AudioObjectHasProperty(device_id, &address) } {
|
||||||
|
anyhow::bail!("Can not get audio property");
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = mem::size_of::<f32>() as u32;
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
AudioObjectSetPropertyData(
|
||||||
|
device_id,
|
||||||
|
&address,
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
size,
|
||||||
|
&volume as *const f32 as *const std::ffi::c_void,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
anyhow::bail!("Can not set audio property");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
3
src-tauri/src/volume/mod.rs
Normal file
3
src-tauri/src/volume/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod manager;
|
||||||
|
|
||||||
|
pub use manager::*;
|
115
src/App.tsx
115
src/App.tsx
@ -1,104 +1,39 @@
|
|||||||
import { createEffect, onCleanup } from 'solid-js';
|
import { Routes, Route } from '@solidjs/router';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { LedStripConfiguration } from './components/led-strip-configuration/led-strip-configuration';
|
||||||
import { DisplayView } from './components/display-view';
|
import { WhiteBalance } from './components/white-balance/white-balance';
|
||||||
import { DisplayListContainer } from './components/display-list-container';
|
import { createEffect } from 'solid-js';
|
||||||
import { displayStore, setDisplayStore } from './stores/display.store';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { LedStripConfigContainer } from './models/led-strip-config';
|
|
||||||
import { setLedStripStore } from './stores/led-strip.store';
|
import { setLedStripStore } from './stores/led-strip.store';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { LedStripConfigContainer } from './models/led-strip-config';
|
||||||
import { LedStripPartsSorter } from './components/led-strip-parts-sorter';
|
import { InfoIndex } from './components/info/info-index';
|
||||||
import { createStore } from 'solid-js/store';
|
import { DisplayStateIndex } from './components/displays/display-state-index';
|
||||||
import {
|
|
||||||
LedStripConfigurationContext,
|
|
||||||
LedStripConfigurationContextType,
|
|
||||||
} from './contexts/led-strip-configuration.context';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
invoke<string>('list_display_info').then((displays) => {
|
invoke<LedStripConfigContainer>('read_config').then((config) => {
|
||||||
setDisplayStore({
|
console.log('read config', config);
|
||||||
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({
|
setLedStripStore({
|
||||||
strips,
|
strips: config.strips,
|
||||||
mappers,
|
mappers: config.mappers,
|
||||||
|
colorCalibration: config.color_calibration,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
<div>
|
||||||
<LedStripConfigurationContext.Provider value={ledStripConfigurationContextValue}>
|
<div>
|
||||||
<LedStripPartsSorter />
|
<a href="/info">基本信息</a>
|
||||||
<DisplayListContainer>
|
<a href="/displays">显示器信息</a>
|
||||||
{displayStore.displays.map((display) => {
|
<a href="/led-strips-configuration">灯条配置</a>
|
||||||
return <DisplayView display={display} />;
|
<a href="/white-balance">白平衡</a>
|
||||||
})}
|
</div>
|
||||||
</DisplayListContainer>
|
<Routes>
|
||||||
</LedStripConfigurationContext.Provider>
|
<Route path="/info" component={InfoIndex} />
|
||||||
|
<Route path="/displays" component={DisplayStateIndex} />
|
||||||
|
<Route path="/led-strips-configuration" component={LedStripConfiguration} />
|
||||||
|
<Route path="/white-balance" component={WhiteBalance} />
|
||||||
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
36
src/components/displays/display-state-card.tsx
Normal file
36
src/components/displays/display-state-card.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Component, ParentComponent } from 'solid-js';
|
||||||
|
import { DisplayState } from '../../models/display-state.model';
|
||||||
|
|
||||||
|
type DisplayStateCardProps = {
|
||||||
|
state: DisplayState;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item: ParentComponent<ItemProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<dl class="flex">
|
||||||
|
<dt class="w-20">{props.label}</dt>
|
||||||
|
<dd class="flex-auto">{props.children}</dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisplayStateCard: Component<DisplayStateCardProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<section class="p-2 rounded shadow">
|
||||||
|
<Item label="Brightness">{props.state.brightness}</Item>
|
||||||
|
<Item label="Max Brightness">{props.state.max_brightness}</Item>
|
||||||
|
<Item label="Min Brightness">{props.state.min_brightness}</Item>
|
||||||
|
<Item label="Contrast">{props.state.contrast}</Item>
|
||||||
|
<Item label="Max Contrast">{props.state.max_contrast}</Item>
|
||||||
|
<Item label="Min Contrast">{props.state.min_contrast}</Item>
|
||||||
|
<Item label="Max Mode">{props.state.max_mode}</Item>
|
||||||
|
<Item label="Min Mode">{props.state.min_mode}</Item>
|
||||||
|
<Item label="Mode">{props.state.mode}</Item>
|
||||||
|
<Item label="Last Modified At">{props.state.last_modified_at.toISOString()}</Item>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
52
src/components/displays/display-state-index.tsx
Normal file
52
src/components/displays/display-state-index.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Component, For, createEffect, createSignal } from 'solid-js';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { DisplayState, RawDisplayState } from '../../models/display-state.model';
|
||||||
|
import { DisplayStateCard } from './display-state-card';
|
||||||
|
|
||||||
|
const logger = debug('app:components:displays:display-state-index');
|
||||||
|
|
||||||
|
export const DisplayStateIndex: Component = () => {
|
||||||
|
const [states, setStates] = createSignal<DisplayState[]>([]);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const unlisten = listen<RawDisplayState[]>('displays_changed', (ev) => {
|
||||||
|
logger('displays_changed', ev);
|
||||||
|
setStates(
|
||||||
|
ev.payload.map((it) => ({
|
||||||
|
...it,
|
||||||
|
last_modified_at: new Date(it.last_modified_at.secs_since_epoch * 1000),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
invoke<RawDisplayState[]>('get_displays').then((states) => {
|
||||||
|
logger('get_displays', states);
|
||||||
|
setStates(
|
||||||
|
states.map((it) => ({
|
||||||
|
...it,
|
||||||
|
last_modified_at: new Date(it.last_modified_at.secs_since_epoch * 1000),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<ol class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 p-2 gap-2">
|
||||||
|
<For each={states()}>
|
||||||
|
{(state, index) => (
|
||||||
|
<li class="bg-slate-50 text-gray-800 relative border-2 border-slate-50 hover:border-sky-300 focus:border-sky-300 transition">
|
||||||
|
<DisplayStateCard state={state} />
|
||||||
|
<span class="absolute left-2 -top-3 bg-sky-300 text-white px-1 py-0.5 text-xs rounded-sm font-mono">
|
||||||
|
#{index() + 1}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
42
src/components/info/board-index.tsx
Normal file
42
src/components/info/board-index.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Component, For, createEffect, createSignal } from 'solid-js';
|
||||||
|
import { BoardInfo } from '../../models/board-info.model';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { BoardInfoPanel } from './board-info-panel';
|
||||||
|
|
||||||
|
const logger = debug('app:components:info:board-index');
|
||||||
|
|
||||||
|
export const BoardIndex: Component = () => {
|
||||||
|
const [boards, setBoards] = createSignal<BoardInfo[]>([]);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const unlisten = listen<BoardInfo[]>('boards_changed', (ev) => {
|
||||||
|
logger('boards_changed', ev);
|
||||||
|
setBoards(ev.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
invoke<BoardInfo[]>('get_boards').then((boards) => {
|
||||||
|
logger('get_boards', boards);
|
||||||
|
setBoards(boards);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<ol class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 p-2 gap-2">
|
||||||
|
<For each={boards()}>
|
||||||
|
{(board, index) => (
|
||||||
|
<li class="bg-slate-50 text-gray-800 relative border-2 border-slate-50 hover:border-sky-300 focus:border-sky-300 transition">
|
||||||
|
<BoardInfoPanel board={board} />
|
||||||
|
<span class="absolute left-2 -top-3 bg-sky-300 text-white px-1 py-0.5 text-xs rounded-sm font-mono">
|
||||||
|
#{index() + 1}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
};
|
60
src/components/info/board-info-panel.tsx
Normal file
60
src/components/info/board-info-panel.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Component, ParentComponent, createMemo } from 'solid-js';
|
||||||
|
import { BoardInfo } from '../../models/board-info.model';
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item: ParentComponent<ItemProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<dl class="flex">
|
||||||
|
<dt class="w-20">{props.label}</dt>
|
||||||
|
<dd class="flex-auto">{props.children}</dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardInfoPanel: Component<{ board: BoardInfo }> = (props) => {
|
||||||
|
const ttl = createMemo(() => {
|
||||||
|
if (props.board.connect_status !== 'Connected') {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.board.ttl == null) {
|
||||||
|
return 'timeout';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span class="font-mono">{props.board.ttl.toFixed(0)}</span> ms
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectStatus = createMemo(() => {
|
||||||
|
if (typeof props.board.connect_status === 'string') {
|
||||||
|
return props.board.connect_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('Connecting' in props.board.connect_status) {
|
||||||
|
return `Connecting (${props.board.connect_status.Connecting.toFixed(0)})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class="p-2 rounded shadow">
|
||||||
|
<Item label="Host">{props.board.fullname}</Item>
|
||||||
|
<Item label="Host">{props.board.host}</Item>
|
||||||
|
<Item label="Ip Addr">
|
||||||
|
<span class="font-mono">{props.board.address}</span>
|
||||||
|
</Item>
|
||||||
|
<Item label="Port">
|
||||||
|
<span class="font-mono">{props.board.port}</span>
|
||||||
|
</Item>
|
||||||
|
<Item label="Status">
|
||||||
|
<span class="font-mono">{connectStatus()}</span>
|
||||||
|
</Item>
|
||||||
|
<Item label="TTL">{ttl()}</Item>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
10
src/components/info/info-index.tsx
Normal file
10
src/components/info/info-index.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from 'solid-js';
|
||||||
|
import { BoardIndex } from './board-index';
|
||||||
|
|
||||||
|
export const InfoIndex: Component = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BoardIndex />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, JSX, ParentComponent, splitProps } from 'solid-js';
|
import { Component, JSX, ParentComponent, splitProps } from 'solid-js';
|
||||||
import { DisplayInfo } from '../models/display-info.model';
|
import { DisplayInfo } from '../../models/display-info.model';
|
||||||
|
|
||||||
type DisplayInfoItemProps = {
|
type DisplayInfoItemProps = {
|
||||||
label: string;
|
label: string;
|
@ -6,8 +6,8 @@ import {
|
|||||||
onMount,
|
onMount,
|
||||||
ParentComponent,
|
ParentComponent,
|
||||||
} from 'solid-js';
|
} 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';
|
import background from '../../assets/transparent-grid-background.svg?url';
|
||||||
|
|
||||||
export const DisplayListContainer: ParentComponent = (props) => {
|
export const DisplayListContainer: ParentComponent = (props) => {
|
||||||
let root: HTMLElement;
|
let root: HTMLElement;
|
@ -1,7 +1,7 @@
|
|||||||
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 { 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 { LedStripPart } from './led-strip-part';
|
||||||
import { ScreenView } from './screen-view';
|
import { ScreenView } from './screen-view';
|
@ -0,0 +1,106 @@
|
|||||||
|
import { createEffect, onCleanup } from 'solid-js';
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { DisplayView } from './display-view';
|
||||||
|
import { DisplayListContainer } from './display-list-container';
|
||||||
|
import { displayStore, setDisplayStore } from '../../stores/display.store';
|
||||||
|
import { LedStripConfigContainer } from '../../models/led-strip-config';
|
||||||
|
import { setLedStripStore } from '../../stores/led-strip.store';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { LedStripPartsSorter } from './led-strip-parts-sorter';
|
||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import {
|
||||||
|
LedStripConfigurationContext,
|
||||||
|
LedStripConfigurationContextType,
|
||||||
|
} from '../../contexts/led-strip-configuration.context';
|
||||||
|
|
||||||
|
export const LedStripConfiguration = () => {
|
||||||
|
createEffect(() => {
|
||||||
|
invoke<string>('list_display_info').then((displays) => {
|
||||||
|
setDisplayStore({
|
||||||
|
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) => {
|
||||||
|
if (!window.document.hidden) {
|
||||||
|
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) => {
|
||||||
|
if (!window.document.hidden) {
|
||||||
|
const sortedColors = event.payload;
|
||||||
|
setLedStripStore({
|
||||||
|
sortedColors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ledStripConfiguration, setLedStripConfiguration] = createStore<
|
||||||
|
LedStripConfigurationContextType[0]
|
||||||
|
>({
|
||||||
|
selectedStripPart: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ledStripConfigurationContextValue: LedStripConfigurationContextType = [
|
||||||
|
ledStripConfiguration,
|
||||||
|
{
|
||||||
|
setSelectedStripPart: (v) => {
|
||||||
|
setLedStripConfiguration({
|
||||||
|
selectedStripPart: v,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LedStripConfigurationContext.Provider value={ledStripConfigurationContextValue}>
|
||||||
|
<LedStripPartsSorter />
|
||||||
|
<DisplayListContainer>
|
||||||
|
{displayStore.displays.map((display) => {
|
||||||
|
return <DisplayView display={display} />;
|
||||||
|
})}
|
||||||
|
</DisplayListContainer>
|
||||||
|
</LedStripConfigurationContext.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -12,9 +12,9 @@ import {
|
|||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
import { useTippy } from 'solid-tippy';
|
import { useTippy } from 'solid-tippy';
|
||||||
import { followCursor } from 'tippy.js';
|
import { followCursor } from 'tippy.js';
|
||||||
import { LedStripConfig } from '../models/led-strip-config';
|
import { LedStripConfig } from '../../models/led-strip-config';
|
||||||
import { LedStripConfigurationContext } from '../contexts/led-strip-configuration.context';
|
import { LedStripConfigurationContext } from '../../contexts/led-strip-configuration.context';
|
||||||
import { ledStripStore } from '../stores/led-strip.store';
|
import { ledStripStore } from '../../stores/led-strip.store';
|
||||||
|
|
||||||
type LedStripPartProps = {
|
type LedStripPartProps = {
|
||||||
config?: LedStripConfig | null;
|
config?: LedStripConfig | null;
|
||||||
@ -24,12 +24,6 @@ type PixelProps = {
|
|||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function subscribeScreenshotUpdate(displayId: number) {
|
|
||||||
await invoke('subscribe_encoded_screenshot_updated', {
|
|
||||||
displayId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Pixel: Component<PixelProps> = (props) => {
|
export const Pixel: Component<PixelProps> = (props) => {
|
||||||
const style = createMemo(() => ({
|
const style = createMemo(() => ({
|
||||||
background: props.color,
|
background: props.color,
|
||||||
@ -51,7 +45,6 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
|||||||
const [localProps, rootProps] = splitProps(props, ['config']);
|
const [localProps, rootProps] = splitProps(props, ['config']);
|
||||||
const [stripConfiguration] = useContext(LedStripConfigurationContext);
|
const [stripConfiguration] = useContext(LedStripConfigurationContext);
|
||||||
|
|
||||||
const [ledSamplePoints, setLedSamplePoints] = createSignal();
|
|
||||||
const [colors, setColors] = createSignal<string[]>([]);
|
const [colors, setColors] = createSignal<string[]>([]);
|
||||||
|
|
||||||
// update led strip colors from global store
|
// update led strip colors from global store
|
||||||
@ -87,17 +80,6 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
|||||||
setColors(colors);
|
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>();
|
const [anchor, setAnchor] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
useTippy(anchor, {
|
useTippy(anchor, {
|
@ -0,0 +1,316 @@
|
|||||||
|
import {
|
||||||
|
batch,
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
Index,
|
||||||
|
JSX,
|
||||||
|
Match,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
Switch,
|
||||||
|
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 [leds, setLeds] = 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 [rootWidth, setRootWidth] = createSignal<number>(0);
|
||||||
|
|
||||||
|
let root: HTMLDivElement;
|
||||||
|
|
||||||
|
const move = (targetStart: number) => {
|
||||||
|
if (targetStart === props.mapper.start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`moving strip part ${props.strip.display_id} ${props.strip.border} from ${props.mapper.start} to ${targetStart}`,
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
const start = untrack(dragStart);
|
||||||
|
const curr = untrack(dragCurr);
|
||||||
|
const _dragging = untrack(dragging);
|
||||||
|
|
||||||
|
if (start === null || curr === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_dragging && indexDiff !== 0) {
|
||||||
|
const compensation = indexDiff * cellWidth();
|
||||||
|
batch(() => {
|
||||||
|
setDragStartIndex(props.mapper.start);
|
||||||
|
setDragStart({
|
||||||
|
x: start.x + compensation,
|
||||||
|
y: curr.y,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
batch(() => {
|
||||||
|
setDragStartIndex(props.mapper.start);
|
||||||
|
setDragStart(null);
|
||||||
|
setDragCurr(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPointerDown = (ev: PointerEvent) => {
|
||||||
|
if (ev.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batch(() => {
|
||||||
|
setDragging(true);
|
||||||
|
if (dragStart() === null) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragging() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDragging(false);
|
||||||
|
const diff = ev.clientX - dragStart()!.x;
|
||||||
|
const moved = Math.round(diff / cellWidth());
|
||||||
|
if (moved === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
move(props.mapper.start + moved);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = (ev: PointerEvent) => {
|
||||||
|
if (dragging() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 onPointerLeave = () => {
|
||||||
|
setSelectedStripPart(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('pointermove', onPointerMove);
|
||||||
|
window.addEventListener('pointerleave', onPointerLeave);
|
||||||
|
window.addEventListener('pointerup', onPointerUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener('pointermove', onPointerMove);
|
||||||
|
window.removeEventListener('pointerleave', onPointerLeave);
|
||||||
|
window.removeEventListener('pointerup', onPointerUp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const reverse = () => {
|
||||||
|
invoke('reverse_led_strip_part', {
|
||||||
|
displayId: props.strip.display_id,
|
||||||
|
border: props.strip.border,
|
||||||
|
}).catch((err) => console.error(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setColor = (fullIndex: number, colorsIndex: number, fullLeds: string[]) => {
|
||||||
|
const colors = ledStripStore.colors;
|
||||||
|
let c1 = `rgb(${Math.floor(colors[colorsIndex * 3] * 0.8)}, ${Math.floor(
|
||||||
|
colors[colorsIndex * 3 + 1] * 0.8,
|
||||||
|
)}, ${Math.floor(colors[colorsIndex * 3 + 2] * 0.8)})`;
|
||||||
|
let c2 = `rgb(${Math.min(Math.floor(colors[colorsIndex * 3] * 1.2), 255)}, ${Math.min(
|
||||||
|
Math.floor(colors[colorsIndex * 3 + 1] * 1.2),
|
||||||
|
255,
|
||||||
|
)}, ${Math.min(Math.floor(colors[colorsIndex * 3 + 2] * 1.2), 255)})`;
|
||||||
|
|
||||||
|
if (fullLeds.length <= fullIndex) {
|
||||||
|
console.error('out of range', fullIndex, fullLeds.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullLeds[fullIndex] = `linear-gradient(70deg, ${c1} 10%, ${c2})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// update fullLeds
|
||||||
|
createEffect(() => {
|
||||||
|
const { start, end, pos } = props.mapper;
|
||||||
|
|
||||||
|
const leds = new Array(Math.abs(start - end)).fill(null);
|
||||||
|
|
||||||
|
if (start < end) {
|
||||||
|
for (let i = 0, j = pos; i < leds.length; i++, j++) {
|
||||||
|
setColor(i, j, leds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = leds.length - 1, j = pos; i >= 0; i--, j++) {
|
||||||
|
setColor(i, j, leds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLeds(leds);
|
||||||
|
});
|
||||||
|
|
||||||
|
// update rootWidth
|
||||||
|
createEffect(() => {
|
||||||
|
let observer: ResizeObserver;
|
||||||
|
onMount(() => {
|
||||||
|
observer = new ResizeObserver(() => {
|
||||||
|
setRootWidth(root.clientWidth);
|
||||||
|
});
|
||||||
|
observer.observe(root);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
observer?.unobserve(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// update cellWidth
|
||||||
|
createEffect(() => {
|
||||||
|
const cellWidth = rootWidth() / ledStripStore.totalLedCount;
|
||||||
|
setCellWidth(cellWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = createMemo<JSX.CSSProperties>(() => {
|
||||||
|
return {
|
||||||
|
transform: `translateX(${
|
||||||
|
(dragCurr()?.x ?? 0) -
|
||||||
|
(dragStart()?.x ?? 0) +
|
||||||
|
cellWidth() * Math.min(props.mapper.start, props.mapper.end)
|
||||||
|
}px)`,
|
||||||
|
width: `${cellWidth() * leds().length}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="flex mx-2 select-none cursor-ew-resize focus:cursor-ew-resize"
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
ondblclick={reverse}
|
||||||
|
ref={root!}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={style()}
|
||||||
|
class="rounded-full border border-white flex h-3"
|
||||||
|
classList={{
|
||||||
|
'bg-gradient-to-b from-yellow-500/60 to-orange-300/60': dragging(),
|
||||||
|
'bg-gradient-to-b from-white/50 to-stone-500/40': !dragging(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={leds()}>
|
||||||
|
{(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>
|
||||||
|
</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) => (
|
||||||
|
<Switch>
|
||||||
|
<Match when={strip().len > 0}>
|
||||||
|
<SorterItem strip={strip()} mapper={ledStripStore.mappers[index]} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,228 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
22
src/components/white-balance/color-slider.tsx
Normal file
22
src/components/white-balance/color-slider.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Component, JSX } from 'solid-js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value?: number;
|
||||||
|
} & JSX.HTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
|
export const ColorSlider: Component<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
{...props}
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
value={props.value}
|
||||||
|
class={
|
||||||
|
'w-full h-2 bg-gradient-to-r rounded-lg appearance-none cursor-pointer dark:bg-gray-700 drop-shadow ' +
|
||||||
|
props.class
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
100
src/components/white-balance/test-colors-bg.tsx
Normal file
100
src/components/white-balance/test-colors-bg.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Component, createSignal } from 'solid-js';
|
||||||
|
|
||||||
|
const ColorItem: Component<{
|
||||||
|
color: string;
|
||||||
|
position: [number, number];
|
||||||
|
size?: [number, number];
|
||||||
|
onClick?: (color: string) => void;
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: props.color,
|
||||||
|
'grid-row-start': props.position[0],
|
||||||
|
'grid-column-start': props.position[1],
|
||||||
|
'grid-row-end': props.position[0] + (props.size ? props.size[0] : 1),
|
||||||
|
'grid-column-end': props.position[1] + (props.size ? props.size[1] : 1),
|
||||||
|
cursor: props.onClick ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
props.onClick?.(props.color);
|
||||||
|
}}
|
||||||
|
title={props.color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TestColorsBg: Component = () => {
|
||||||
|
const [singleColor, setSingleColor] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section
|
||||||
|
class="grid grid-cols-[8] grid-rows-[8] h-full w-full"
|
||||||
|
classList={{
|
||||||
|
hidden: singleColor() !== null,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorItem color="#ff0000" position={[1, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffff00" position={[1, 2]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ff00" position={[1, 3]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ffff" position={[1, 4]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#0000ff" position={[1, 5]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ff00ff" position={[1, 6]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffffff" position={[1, 7]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#000000" position={[1, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffff00" position={[2, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ff00" position={[3, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ffff" position={[4, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#0000ff" position={[5, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ff00ff" position={[6, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffffff" position={[7, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#000000" position={[8, 1]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffffff" position={[2, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ff00ff" position={[3, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#0000ff" position={[4, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ffff" position={[5, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ff00" position={[6, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffff00" position={[7, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ff0000" position={[8, 8]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffffff" position={[8, 2]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ff00ff" position={[8, 3]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#0000ff" position={[8, 4]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ffff" position={[8, 5]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#00ff00" position={[8, 6]} onClick={setSingleColor} />
|
||||||
|
<ColorItem color="#ffff00" position={[8, 7]} onClick={setSingleColor} />
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
class="grid grid-cols-[8] grid-rows-[8] h-full w-full"
|
||||||
|
classList={{
|
||||||
|
hidden: singleColor() === null,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorItem
|
||||||
|
color={singleColor()!}
|
||||||
|
position={[1, 1]}
|
||||||
|
size={[1, 7]}
|
||||||
|
onClick={() => setSingleColor(null)}
|
||||||
|
/>
|
||||||
|
<ColorItem
|
||||||
|
color={singleColor()!}
|
||||||
|
position={[8, 2]}
|
||||||
|
size={[1, 7]}
|
||||||
|
onClick={() => setSingleColor(null)}
|
||||||
|
/>
|
||||||
|
<ColorItem
|
||||||
|
color={singleColor()!}
|
||||||
|
position={[2, 1]}
|
||||||
|
size={[7, 1]}
|
||||||
|
onClick={() => setSingleColor(null)}
|
||||||
|
/>
|
||||||
|
<ColorItem
|
||||||
|
color={singleColor()!}
|
||||||
|
position={[1, 8]}
|
||||||
|
size={[7, 1]}
|
||||||
|
onClick={() => setSingleColor(null)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
131
src/components/white-balance/white-balance.tsx
Normal file
131
src/components/white-balance/white-balance.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { Component, createEffect, onCleanup } from 'solid-js';
|
||||||
|
import { ColorCalibration, LedStripConfigContainer } from '../../models/led-strip-config';
|
||||||
|
import { ledStripStore, setLedStripStore } from '../../stores/led-strip.store';
|
||||||
|
import { ColorSlider } from './color-slider';
|
||||||
|
import { TestColorsBg } from './test-colors-bg';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { VsClose } from 'solid-icons/vs';
|
||||||
|
import { BiRegularReset } from 'solid-icons/bi';
|
||||||
|
import transparentBg from '../../assets/transparent-grid-background.svg?url';
|
||||||
|
|
||||||
|
const Value: Component<{ value: number }> = (props) => {
|
||||||
|
return (
|
||||||
|
<span class="w-10 text-sm block font-mono text-right ">
|
||||||
|
{(props.value * 100).toFixed(0)}
|
||||||
|
<span class="text-xs text-stone-600">%</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WhiteBalance = () => {
|
||||||
|
// listen to config_changed event
|
||||||
|
createEffect(() => {
|
||||||
|
const unlisten = listen('config_changed', (event) => {
|
||||||
|
const { strips, mappers, color_calibration } =
|
||||||
|
event.payload as LedStripConfigContainer;
|
||||||
|
console.log(event.payload);
|
||||||
|
setLedStripStore({
|
||||||
|
strips,
|
||||||
|
mappers,
|
||||||
|
colorCalibration: color_calibration,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateColorCalibration = (field: keyof ColorCalibration, value: number) => {
|
||||||
|
const calibration = { ...ledStripStore.colorCalibration, [field]: value };
|
||||||
|
invoke('set_color_calibration', {
|
||||||
|
calibration,
|
||||||
|
}).catch((error) => console.log(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
const exit = () => {
|
||||||
|
window.history.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
invoke('set_color_calibration', {
|
||||||
|
calibration: new ColorCalibration(),
|
||||||
|
}).catch((error) => console.log(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class="select-none text-stone-800">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 left-0 right-0 bottom-0"
|
||||||
|
style={{
|
||||||
|
'background-image': `url(${transparentBg})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TestColorsBg />
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 max-w-lg bg-stone-100/20 backdrop-blur p-5 rounded-xl shadow-lg">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="w-3 block">R:</span>
|
||||||
|
<ColorSlider
|
||||||
|
class="from-cyan-500 to-red-500"
|
||||||
|
value={ledStripStore.colorCalibration.r}
|
||||||
|
onInput={(ev) =>
|
||||||
|
updateColorCalibration(
|
||||||
|
'r',
|
||||||
|
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Value value={ledStripStore.colorCalibration.r} />
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="w-3 block">G:</span>
|
||||||
|
<ColorSlider
|
||||||
|
class="from-pink-500 to-green-500"
|
||||||
|
value={ledStripStore.colorCalibration.g}
|
||||||
|
onInput={(ev) =>
|
||||||
|
updateColorCalibration(
|
||||||
|
'g',
|
||||||
|
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Value value={ledStripStore.colorCalibration.g} />
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="w-3 block">B:</span>
|
||||||
|
<ColorSlider
|
||||||
|
class="from-yellow-500 to-blue-500"
|
||||||
|
value={ledStripStore.colorCalibration.b}
|
||||||
|
onInput={(ev) =>
|
||||||
|
updateColorCalibration(
|
||||||
|
'b',
|
||||||
|
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Value value={ledStripStore.colorCalibration.b} />
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<span class="w-3 block">W:</span>
|
||||||
|
<ColorSlider class="from-yellow-50 to-cyan-50" />
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="absolute -right-4 -top-4 rounded-full aspect-square bg-stone-100/20 backdrop-blur p-1 shadow hover:bg-stone-200/20 active:bg-stone-300"
|
||||||
|
onClick={exit}
|
||||||
|
title="Go Back"
|
||||||
|
>
|
||||||
|
<VsClose size={24} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="absolute -right-4 -bottom-4 rounded-full aspect-square bg-stone-100/20 backdrop-blur p-1 shadow hover:bg-stone-200/20 active:bg-stone-300"
|
||||||
|
onClick={reset}
|
||||||
|
title="Reset to 100%"
|
||||||
|
>
|
||||||
|
<BiRegularReset size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -3,5 +3,13 @@ import { render } from "solid-js/web";
|
|||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import { Router } from '@solidjs/router';
|
||||||
|
|
||||||
render(() => <App />, document.getElementById("root") as HTMLElement);
|
render(
|
||||||
|
() => (
|
||||||
|
<Router>
|
||||||
|
<App />
|
||||||
|
</Router>
|
||||||
|
),
|
||||||
|
document.getElementById('root') as HTMLElement,
|
||||||
|
);
|
||||||
|
9
src/models/board-info.model.ts
Normal file
9
src/models/board-info.model.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type BoardInfo = {
|
||||||
|
fullname: string;
|
||||||
|
host: string;
|
||||||
|
address: string;
|
||||||
|
port: number;
|
||||||
|
ttl: number;
|
||||||
|
connect_status: 'Connected' | 'Disconnected' | { Connecting: number };
|
||||||
|
checked_at: Date;
|
||||||
|
};
|
16
src/models/display-state.model.ts
Normal file
16
src/models/display-state.model.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export type DisplayState = {
|
||||||
|
brightness: number;
|
||||||
|
max_brightness: number;
|
||||||
|
min_brightness: number;
|
||||||
|
contrast: number;
|
||||||
|
max_contrast: number;
|
||||||
|
min_contrast: number;
|
||||||
|
mode: number;
|
||||||
|
max_mode: number;
|
||||||
|
min_mode: number;
|
||||||
|
last_modified_at: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RawDisplayState = DisplayState & {
|
||||||
|
last_modified_at: { secs_since_epoch: number };
|
||||||
|
};
|
@ -6,9 +6,16 @@ export type LedStripPixelMapper = {
|
|||||||
pos: number;
|
pos: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class ColorCalibration {
|
||||||
|
r: number = 1;
|
||||||
|
g: number = 1;
|
||||||
|
b: number = 1;
|
||||||
|
}
|
||||||
|
|
||||||
export type LedStripConfigContainer = {
|
export type LedStripConfigContainer = {
|
||||||
strips: LedStripConfig[];
|
strips: LedStripConfig[];
|
||||||
mappers: LedStripPixelMapper[];
|
mappers: LedStripPixelMapper[];
|
||||||
|
color_calibration: ColorCalibration;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LedStripConfig {
|
export class LedStripConfig {
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
import { createStore } from 'solid-js/store';
|
import { createStore } from 'solid-js/store';
|
||||||
import { LedStripConfig, LedStripPixelMapper } from '../models/led-strip-config';
|
import {
|
||||||
|
ColorCalibration,
|
||||||
|
LedStripConfig,
|
||||||
|
LedStripPixelMapper,
|
||||||
|
} from '../models/led-strip-config';
|
||||||
|
|
||||||
export const [ledStripStore, setLedStripStore] = createStore({
|
export const [ledStripStore, setLedStripStore] = createStore({
|
||||||
strips: new Array<LedStripConfig>(),
|
strips: new Array<LedStripConfig>(),
|
||||||
mappers: new Array<LedStripPixelMapper>(),
|
mappers: new Array<LedStripPixelMapper>(),
|
||||||
|
colorCalibration: new ColorCalibration(),
|
||||||
colors: new Uint8ClampedArray(),
|
colors: new Uint8ClampedArray(),
|
||||||
sortedColors: new Uint8ClampedArray(),
|
sortedColors: new Uint8ClampedArray(),
|
||||||
get totalLedCount() {
|
get totalLedCount() {
|
||||||
return Math.max(0, ...ledStripStore.mappers.map((m) => Math.max(m.start, m.end)));
|
return Math.max(
|
||||||
|
0,
|
||||||
|
...ledStripStore.mappers.map((m) => {
|
||||||
|
if (m.start === m.end) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return Math.max(m.start, m.end);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user