- Add LED test effects page with multiple test patterns (solid colors, rainbow, breathing, flowing) - Implement Rust backend for LED test effects with proper task management - Add automatic cleanup when navigating away from test page using onCleanup hook - Ensure test mode is properly disabled to resume normal ambient lighting - Clean up debug logging for production readiness - Fix menu navigation issues by using SolidJS router components Features: - Multiple test patterns: solid colors, rainbow cycle, breathing effect, flowing lights - Configurable animation speed - Automatic cleanup prevents LED conflicts with ambient lighting - Responsive UI with proper error handling
730 lines
25 KiB
Rust
730 lines
25 KiB
Rust
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
mod ambient_light;
|
|
mod display;
|
|
mod led_color;
|
|
mod led_test_effects;
|
|
mod rpc;
|
|
mod screenshot;
|
|
mod screenshot_manager;
|
|
mod screen_stream;
|
|
mod volume;
|
|
|
|
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup, LedType};
|
|
use display::{DisplayManager, DisplayState};
|
|
use display_info::DisplayInfo;
|
|
use led_test_effects::{LedTestEffects, TestEffectConfig, TestEffectType};
|
|
use paris::{error, info, warn};
|
|
use rpc::{BoardInfo, UdpRpc};
|
|
use screenshot::Screenshot;
|
|
use screenshot_manager::ScreenshotManager;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::to_string;
|
|
use tauri::{Manager, Emitter, Runtime};
|
|
use regex;
|
|
use tauri::http::{Request, Response};
|
|
use volume::VolumeManager;
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
|
|
// Global static variables for LED test effect management
|
|
static EFFECT_HANDLE: tokio::sync::OnceCell<Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>> =
|
|
tokio::sync::OnceCell::const_new();
|
|
static CANCEL_TOKEN: tokio::sync::OnceCell<Arc<RwLock<Option<tokio_util::sync::CancellationToken>>>> =
|
|
tokio::sync::OnceCell::const_new();
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(remote = "DisplayInfo")]
|
|
struct DisplayInfoDef {
|
|
pub id: u32,
|
|
pub x: i32,
|
|
pub y: i32,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub rotation: f32,
|
|
pub scale_factor: f32,
|
|
pub is_primary: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct DisplayInfoWrapper<'a>(#[serde(with = "DisplayInfoDef")] &'a DisplayInfo);
|
|
|
|
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
|
#[tauri::command]
|
|
fn greet(name: &str) -> String {
|
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn list_display_info() -> Result<String, String> {
|
|
let displays = display_info::DisplayInfo::all().map_err(|e| {
|
|
error!("can not list display info: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
let displays: Vec<DisplayInfoWrapper> =
|
|
displays.iter().map(|v| DisplayInfoWrapper(v)).collect();
|
|
let json_str = to_string(&displays).map_err(|e| {
|
|
error!("can not list display info: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
Ok(json_str)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn read_led_strip_configs() -> Result<LedStripConfigGroup, String> {
|
|
let config = ambient_light::LedStripConfigGroup::read_config()
|
|
.await
|
|
.map_err(|e| {
|
|
error!("can not read led strip configs: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
Ok(config)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn write_led_strip_configs(
|
|
configs: Vec<ambient_light::LedStripConfig>,
|
|
) -> Result<(), String> {
|
|
let config_manager = ambient_light::ConfigManager::global().await;
|
|
|
|
config_manager.set_items(configs).await.map_err(|e| {
|
|
error!("can not write led strip configs: {}", e);
|
|
e.to_string()
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_led_strips_sample_points(
|
|
config: LedStripConfig,
|
|
) -> Result<Vec<screenshot::LedSamplePoints>, String> {
|
|
let screenshot_manager = ScreenshotManager::global().await;
|
|
let channels = screenshot_manager.channels.read().await;
|
|
if let Some(rx) = channels.get(&config.display_id) {
|
|
let rx = rx.read().await;
|
|
let screenshot = rx.borrow().clone();
|
|
let sample_points = screenshot.get_sample_points(&config);
|
|
Ok(sample_points)
|
|
} else {
|
|
return Err(format!("display not found: {}", config.display_id));
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_one_edge_colors(
|
|
display_id: u32,
|
|
sample_points: Vec<screenshot::LedSamplePoints>,
|
|
) -> Result<Vec<led_color::LedColor>, String> {
|
|
let screenshot_manager = ScreenshotManager::global().await;
|
|
let channels = screenshot_manager.channels.read().await;
|
|
if let Some(rx) = channels.get(&display_id) {
|
|
let rx = rx.read().await;
|
|
let screenshot = rx.borrow().clone();
|
|
let bytes = screenshot.bytes.read().await.to_owned();
|
|
let colors =
|
|
Screenshot::get_one_edge_colors(&sample_points, &bytes, screenshot.bytes_per_row);
|
|
Ok(colors)
|
|
} else {
|
|
Err(format!("display not found: {}", display_id))
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) -> Result<(), String> {
|
|
info!(
|
|
"patch_led_strip_len: {} {:?} {}",
|
|
display_id, border, delta_len
|
|
);
|
|
let config_manager = ambient_light::ConfigManager::global().await;
|
|
config_manager
|
|
.patch_led_strip_len(display_id, border, delta_len)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("can not patch led strip len: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
|
|
info!("patch_led_strip_len: ok");
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn patch_led_strip_type(display_id: u32, border: Border, led_type: LedType) -> Result<(), String> {
|
|
let config_manager = ambient_light::ConfigManager::global().await;
|
|
config_manager
|
|
.patch_led_strip_type(display_id, border, led_type)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("can not patch led strip type: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> {
|
|
ambient_light::LedColorsPublisher::send_colors(offset, buffer)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("can not send colors: {}", e);
|
|
e.to_string()
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn send_test_colors_to_board(board_address: String, offset: u16, buffer: Vec<u8>) -> Result<(), String> {
|
|
use tokio::net::UdpSocket;
|
|
|
|
let socket = UdpSocket::bind("0.0.0.0:0").await.map_err(|e| {
|
|
error!("Failed to bind UDP socket: {}", e);
|
|
e.to_string()
|
|
})?;
|
|
|
|
let mut packet = vec![0x02]; // Header
|
|
packet.push((offset >> 8) as u8); // Offset high
|
|
packet.push((offset & 0xff) as u8); // Offset low
|
|
packet.extend_from_slice(&buffer); // Color data
|
|
|
|
socket.send_to(&packet, &board_address).await.map_err(|e| {
|
|
error!("Failed to send test colors to board {}: {}", board_address, e);
|
|
e.to_string()
|
|
})?;
|
|
|
|
info!("Sent test colors to board {} with offset {} and {} bytes", board_address, offset, buffer.len());
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn enable_test_mode() -> Result<(), String> {
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
publisher.enable_test_mode().await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn disable_test_mode() -> Result<(), String> {
|
|
info!("🔄 disable_test_mode command called from frontend");
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
publisher.disable_test_mode().await;
|
|
info!("✅ disable_test_mode command completed");
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn is_test_mode_active() -> Result<bool, String> {
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
Ok(publisher.is_test_mode_active().await)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn start_led_test_effect(
|
|
board_address: String,
|
|
effect_config: TestEffectConfig,
|
|
update_interval_ms: u64,
|
|
) -> Result<(), String> {
|
|
use tokio::time::{interval, Duration};
|
|
|
|
// Enable test mode first
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
publisher.enable_test_mode().await;
|
|
|
|
let handle_storage = EFFECT_HANDLE.get_or_init(|| async {
|
|
Arc::new(RwLock::new(None))
|
|
}).await;
|
|
|
|
let cancel_storage = CANCEL_TOKEN.get_or_init(|| async {
|
|
Arc::new(RwLock::new(None))
|
|
}).await;
|
|
|
|
// Stop any existing effect
|
|
{
|
|
let mut cancel_guard = cancel_storage.write().await;
|
|
if let Some(token) = cancel_guard.take() {
|
|
token.cancel();
|
|
}
|
|
|
|
let mut handle_guard = handle_storage.write().await;
|
|
if let Some(handle) = handle_guard.take() {
|
|
let _ = handle.await; // Wait for graceful shutdown
|
|
}
|
|
}
|
|
|
|
// Start new effect
|
|
let effect_config = Arc::new(effect_config);
|
|
let board_address = Arc::new(board_address);
|
|
let start_time = std::time::Instant::now();
|
|
|
|
// Create new cancellation token
|
|
let cancel_token = tokio_util::sync::CancellationToken::new();
|
|
let cancel_token_clone = cancel_token.clone();
|
|
|
|
let handle = tokio::spawn(async move {
|
|
let mut interval = interval(Duration::from_millis(update_interval_ms));
|
|
|
|
loop {
|
|
tokio::select! {
|
|
_ = interval.tick() => {
|
|
let elapsed_ms = start_time.elapsed().as_millis() as u64;
|
|
let colors = LedTestEffects::generate_colors(&effect_config, elapsed_ms);
|
|
|
|
// Send to board
|
|
if let Err(e) = send_test_colors_to_board_internal(&board_address, 0, colors).await {
|
|
error!("Failed to send test effect colors: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
_ = cancel_token_clone.cancelled() => {
|
|
info!("LED test effect cancelled gracefully");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
info!("LED test effect task ended");
|
|
});
|
|
|
|
// Store the handle and cancel token
|
|
{
|
|
let mut handle_guard = handle_storage.write().await;
|
|
*handle_guard = Some(handle);
|
|
|
|
let mut cancel_guard = cancel_storage.write().await;
|
|
*cancel_guard = Some(cancel_token);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn stop_led_test_effect(board_address: String, led_count: u32, led_type: led_test_effects::LedType) -> Result<(), String> {
|
|
// Stop the effect task first
|
|
|
|
info!("🛑 Stopping LED test effect - board: {}", board_address);
|
|
|
|
// Cancel the task gracefully first
|
|
if let Some(cancel_storage) = CANCEL_TOKEN.get() {
|
|
let mut cancel_guard = cancel_storage.write().await;
|
|
if let Some(token) = cancel_guard.take() {
|
|
info!("🔄 Cancelling test effect task gracefully");
|
|
token.cancel();
|
|
}
|
|
}
|
|
|
|
// Wait for the task to finish
|
|
if let Some(handle_storage) = EFFECT_HANDLE.get() {
|
|
let mut handle_guard = handle_storage.write().await;
|
|
if let Some(handle) = handle_guard.take() {
|
|
info!("⏳ Waiting for test effect task to finish");
|
|
match handle.await {
|
|
Ok(_) => info!("✅ Test effect task finished successfully"),
|
|
Err(e) => warn!("⚠️ Test effect task finished with error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Turn off all LEDs
|
|
let bytes_per_led = match led_type {
|
|
led_test_effects::LedType::RGB => 3,
|
|
led_test_effects::LedType::RGBW => 4,
|
|
};
|
|
let buffer = vec![0u8; (led_count * bytes_per_led) as usize];
|
|
|
|
send_test_colors_to_board_internal(&board_address, 0, buffer).await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
info!("💡 Sent LED off command");
|
|
|
|
// Disable test mode to resume normal publishing
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
publisher.disable_test_mode().await;
|
|
|
|
info!("🔄 Test mode disabled, normal publishing resumed");
|
|
info!("✅ LED test effect stopped completely");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Internal helper function
|
|
async fn send_test_colors_to_board_internal(board_address: &str, offset: u16, buffer: Vec<u8>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
use tokio::net::UdpSocket;
|
|
|
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
|
|
|
let mut packet = vec![0x02]; // Header
|
|
packet.push((offset >> 8) as u8); // Offset high
|
|
packet.push((offset & 0xff) as u8); // Offset low
|
|
packet.extend_from_slice(&buffer); // Color data
|
|
|
|
socket.send_to(&packet, board_address).await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn move_strip_part(
|
|
display_id: u32,
|
|
border: Border,
|
|
target_start: usize,
|
|
) -> Result<(), String> {
|
|
let config_manager = ambient_light::ConfigManager::global().await;
|
|
config_manager
|
|
.move_strip_part(display_id, border, target_start)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("can not move strip part: {}", e);
|
|
e.to_string()
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn reverse_led_strip_part(display_id: u32, border: Border) -> Result<(), String> {
|
|
let config_manager = ambient_light::ConfigManager::global().await;
|
|
config_manager
|
|
.reverse_led_strip_part(display_id, border)
|
|
.await
|
|
.map_err(|e| {
|
|
error!("can not reverse led strip part: {}", e);
|
|
e.to_string()
|
|
})
|
|
}
|
|
|
|
#[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
|
|
}
|
|
|
|
// Protocol handler for ambient-light://
|
|
fn handle_ambient_light_protocol<R: Runtime>(
|
|
_ctx: tauri::UriSchemeContext<R>,
|
|
request: Request<Vec<u8>>
|
|
) -> Response<Vec<u8>> {
|
|
let url = request.uri();
|
|
// info!("Handling ambient-light protocol request: {}", url);
|
|
|
|
// Parse the URL to extract parameters
|
|
let url_str = url.to_string();
|
|
let re = regex::Regex::new(r"ambient-light://displays/(\d+)\?width=(\d+)&height=(\d+)").unwrap();
|
|
|
|
if let Some(captures) = re.captures(&url_str) {
|
|
let display_id: u32 = captures[1].parse().unwrap_or(0);
|
|
let width: u32 = captures[2].parse().unwrap_or(400);
|
|
let height: u32 = captures[3].parse().unwrap_or(300);
|
|
|
|
// info!("Efficient screenshot request for display {}, {}x{}", display_id, width, height);
|
|
|
|
// Optimized screenshot processing with much smaller intermediate size
|
|
// info!("Screenshot request received: display_id={}, width={}, height={}", display_id, width, height);
|
|
|
|
let screenshot_data = tokio::task::block_in_place(|| {
|
|
tokio::runtime::Handle::current().block_on(async {
|
|
let screenshot_manager = ScreenshotManager::global().await;
|
|
let channels = screenshot_manager.channels.read().await;
|
|
|
|
if let Some(rx) = channels.get(&display_id) {
|
|
let rx = rx.read().await;
|
|
let screenshot = rx.borrow().clone();
|
|
let bytes = screenshot.bytes.read().await.to_owned();
|
|
|
|
// Use much smaller intermediate resolution for performance
|
|
let intermediate_width = 800; // Much smaller than original 5120
|
|
let intermediate_height = 450; // Much smaller than original 2880
|
|
|
|
// Convert BGRA to RGBA format
|
|
let mut rgba_bytes = bytes.as_ref().clone();
|
|
for chunk in rgba_bytes.chunks_exact_mut(4) {
|
|
chunk.swap(0, 2); // Swap B and R channels
|
|
}
|
|
|
|
let image_result = image::RgbaImage::from_raw(
|
|
screenshot.width as u32,
|
|
screenshot.height as u32,
|
|
rgba_bytes,
|
|
);
|
|
|
|
if let Some(img) = image_result {
|
|
// Step 1: Fast downscale to intermediate size
|
|
let intermediate_image = image::imageops::resize(
|
|
&img,
|
|
intermediate_width,
|
|
intermediate_height,
|
|
image::imageops::FilterType::Nearest, // Fastest possible
|
|
);
|
|
|
|
// Step 2: Scale to final target size
|
|
let final_image = if width == intermediate_width && height == intermediate_height {
|
|
intermediate_image
|
|
} else {
|
|
image::imageops::resize(
|
|
&intermediate_image,
|
|
width,
|
|
height,
|
|
image::imageops::FilterType::Triangle,
|
|
)
|
|
};
|
|
|
|
let raw_data = final_image.into_raw();
|
|
// info!("Efficient resize completed: {}x{}, {} bytes", width, height, raw_data.len());
|
|
Ok(raw_data)
|
|
} else {
|
|
error!("Failed to create image from raw bytes");
|
|
Err("Failed to create image from raw bytes".to_string())
|
|
}
|
|
} else {
|
|
error!("Display {} not found", display_id);
|
|
Err(format!("Display {} not found", display_id))
|
|
}
|
|
})
|
|
});
|
|
|
|
match screenshot_data {
|
|
Ok(data) => {
|
|
Response::builder()
|
|
.header("Content-Type", "application/octet-stream")
|
|
.header("Access-Control-Allow-Origin", "*")
|
|
.header("X-Image-Width", width.to_string())
|
|
.header("X-Image-Height", height.to_string())
|
|
.body(data)
|
|
.unwrap_or_else(|_| {
|
|
Response::builder()
|
|
.status(500)
|
|
.body("Failed to build response".as_bytes().to_vec())
|
|
.unwrap()
|
|
})
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to get screenshot: {}", e);
|
|
Response::builder()
|
|
.status(500)
|
|
.body(format!("Error: {}", e).into_bytes())
|
|
.unwrap()
|
|
}
|
|
}
|
|
} else {
|
|
warn!("Invalid ambient-light URL format: {}", url_str);
|
|
Response::builder()
|
|
.status(400)
|
|
.body("Invalid URL format".as_bytes().to_vec())
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
env_logger::init();
|
|
|
|
// Initialize display info (removed debug output)
|
|
|
|
tokio::spawn(async move {
|
|
let screenshot_manager = ScreenshotManager::global().await;
|
|
screenshot_manager.start().await.unwrap_or_else(|e| {
|
|
error!("can not start screenshot manager: {}", e);
|
|
})
|
|
});
|
|
|
|
tokio::spawn(async move {
|
|
let led_color_publisher = ambient_light::LedColorsPublisher::global().await;
|
|
led_color_publisher.start().await;
|
|
});
|
|
|
|
// Start WebSocket server for screen streaming
|
|
tokio::spawn(async move {
|
|
if let Err(e) = start_websocket_server().await {
|
|
error!("Failed to start WebSocket server: {}", e);
|
|
}
|
|
});
|
|
|
|
let _volume = VolumeManager::global().await;
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_shell::init())
|
|
.invoke_handler(tauri::generate_handler![
|
|
greet,
|
|
list_display_info,
|
|
read_led_strip_configs,
|
|
write_led_strip_configs,
|
|
get_led_strips_sample_points,
|
|
get_one_edge_colors,
|
|
patch_led_strip_len,
|
|
patch_led_strip_type,
|
|
send_colors,
|
|
send_test_colors_to_board,
|
|
enable_test_mode,
|
|
disable_test_mode,
|
|
is_test_mode_active,
|
|
start_led_test_effect,
|
|
stop_led_test_effect,
|
|
move_strip_part,
|
|
reverse_led_strip_part,
|
|
set_color_calibration,
|
|
read_config,
|
|
get_boards,
|
|
get_displays
|
|
])
|
|
.register_uri_scheme_protocol("ambient-light", handle_ambient_light_protocol)
|
|
|
|
|
|
.setup(move |app| {
|
|
let app_handle = app.handle().clone();
|
|
tokio::spawn(async move {
|
|
let config_manager = ambient_light::ConfigManager::global().await;
|
|
let mut config_update_receiver = config_manager.clone_config_update_receiver();
|
|
loop {
|
|
if let Err(err) = config_update_receiver.changed().await {
|
|
error!("config update receiver changed error: {}", err);
|
|
return;
|
|
}
|
|
|
|
log::info!("config changed. emit config_changed event.");
|
|
|
|
let config = config_update_receiver.borrow().clone();
|
|
|
|
app_handle.emit("config_changed", config).unwrap();
|
|
}
|
|
});
|
|
|
|
let app_handle = app.handle().clone();
|
|
tokio::spawn(async move {
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
let mut publisher_update_receiver = publisher.clone_sorted_colors_receiver().await;
|
|
loop {
|
|
if let Err(err) = publisher_update_receiver.changed().await {
|
|
error!("publisher update receiver changed error: {}", err);
|
|
return;
|
|
}
|
|
|
|
let publisher = publisher_update_receiver.borrow().clone();
|
|
|
|
app_handle
|
|
.emit("led_sorted_colors_changed", publisher)
|
|
.unwrap();
|
|
}
|
|
});
|
|
|
|
let app_handle = app.handle().clone();
|
|
tokio::spawn(async move {
|
|
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
let mut publisher_update_receiver = publisher.clone_colors_receiver().await;
|
|
loop {
|
|
if let Err(err) = publisher_update_receiver.changed().await {
|
|
error!("publisher update receiver changed error: {}", err);
|
|
return;
|
|
}
|
|
|
|
let publisher = publisher_update_receiver.borrow().clone();
|
|
|
|
app_handle
|
|
.emit("led_colors_changed", publisher)
|
|
.unwrap();
|
|
}
|
|
});
|
|
|
|
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("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("displays_changed", displays).unwrap();
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
})
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|
|
|
|
// WebSocket server for screen streaming
|
|
async fn start_websocket_server() -> anyhow::Result<()> {
|
|
use tokio::net::TcpListener;
|
|
|
|
let listener = TcpListener::bind("127.0.0.1:8765").await?;
|
|
info!("WebSocket server listening on ws://127.0.0.1:8765");
|
|
|
|
while let Ok((stream, addr)) = listener.accept().await {
|
|
info!("New WebSocket connection from: {}", addr);
|
|
|
|
tokio::spawn(async move {
|
|
info!("Starting WebSocket handler for connection from: {}", addr);
|
|
match screen_stream::handle_websocket_connection(stream).await {
|
|
Ok(_) => {
|
|
info!("WebSocket connection from {} completed successfully", addr);
|
|
}
|
|
Err(e) => {
|
|
warn!("WebSocket connection error from {}: {}", addr, e);
|
|
}
|
|
}
|
|
info!("WebSocket handler task completed for: {}", addr);
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|