From 599338bbf9cb3227510883727cc31a4b534262bb Mon Sep 17 00:00:00 2001 From: Krzysztof Mazur Date: Sun, 5 Nov 2023 11:42:51 +0100 Subject: [PATCH] Add initial UDP client implementation --- animator/src/cli.rs | 2 +- animator/src/lib.rs | 14 ++++++++--- configurator/src/main.rs | 33 ++++++++++++++++++------- light-client/src/lib.rs | 52 ++++++++++++++++++++++++++++++++++++---- webapi/src/main.rs | 4 +++- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/animator/src/cli.rs b/animator/src/cli.rs index 683b6cc..0db357f 100644 --- a/animator/src/cli.rs +++ b/animator/src/cli.rs @@ -23,7 +23,7 @@ async fn main() -> Result<(), Box> { rustmas_animator::Controller::builder().points_from_file(&cli.positions_file)?; builder = match cli.lights_endpoint { - Some(path) => builder.remote_lights(&path)?, + Some(path) => builder.http_lights(&path)?, None => { #[cfg(not(feature = "visualiser"))] panic!("Visualiser feature is disabled, please provide a light client endpoint"); diff --git a/animator/src/lib.rs b/animator/src/lib.rs index 1fb481a..b0032dd 100644 --- a/animator/src/lib.rs +++ b/animator/src/lib.rs @@ -307,11 +307,19 @@ impl ControllerBuilder { Ok(self) } - pub fn remote_lights(mut self, path: &str) -> Result> { - info!("Using remote light client with endpoint {}", path); + pub fn http_lights(mut self, path: &str) -> Result> { + info!("Using http light client with endpoint {}", path); self.client_builder = self .client_builder - .with(Box::new(client::RemoteLightClient::new(path))); + .with(Box::new(client::HttpLightClient::new(path))); + Ok(self) + } + + pub async fn udp_lights(mut self, path: &str) -> Result> { + info!("Using udp light client with endpoint {}", path); + self.client_builder = self + .client_builder + .with(Box::new(client::UdpLightClient::new(path).await)); Ok(self) } diff --git a/configurator/src/main.rs b/configurator/src/main.rs index 3e422a5..ebd26aa 100644 --- a/configurator/src/main.rs +++ b/configurator/src/main.rs @@ -66,7 +66,7 @@ enum Commands { }, } -fn capturer_from_options( +async fn capturer_from_options( lights_endpoint: Option, ip_camera: Option, number_of_lights: usize, @@ -80,12 +80,25 @@ fn capturer_from_options( }; let light_client: Box = if let Some(endpoint) = lights_endpoint { - if endpoint.starts_with("http://") { - info!("Using remote HTTP light client at endpoint: {}", endpoint); - Box::new(light_client::RemoteLightClient::new(&endpoint)) - } else { - info!("Using local TTY light client"); - Box::new(light_client::tty::TtyLightClient::new()?) + match endpoint { + http_endpoint if endpoint.starts_with("http://") => { + info!( + "Using remote HTTP light client at endpoint: {}", + http_endpoint + ); + Box::new(light_client::HttpLightClient::new(&http_endpoint)) + } + udp_endpoint if endpoint.starts_with("udp://") => { + info!( + "Using remote HTTP light client at endpoint: {}", + udp_endpoint + ); + Box::new(light_client::UdpLightClient::new(&udp_endpoint).await) + } + _ => { + info!("Using local TTY light client"); + Box::new(light_client::tty::TtyLightClient::new()?) + } } } else { info!("Using mock light client"); @@ -134,7 +147,8 @@ async fn main() -> Result<(), Box> { front_coordinates, back_coordinates, } => { - let mut capturer = capturer_from_options(lights_endpoint, ip_camera, number_of_lights)?; + let mut capturer = + capturer_from_options(lights_endpoint, ip_camera, number_of_lights).await?; let front = capture_or_read_coordinates( &mut capturer, "front", @@ -175,7 +189,8 @@ async fn main() -> Result<(), Box> { number_of_lights, save_pictures, } => { - let mut capturer = capturer_from_options(lights_endpoint, ip_camera, number_of_lights)?; + let mut capturer = + capturer_from_options(lights_endpoint, ip_camera, number_of_lights).await?; let coords = capturer.capture_perspective("2d", save_pictures).await?; let light_positions = coords .iter() diff --git a/light-client/src/lib.rs b/light-client/src/lib.rs index 5d3b641..abd3513 100755 --- a/light-client/src/lib.rs +++ b/light-client/src/lib.rs @@ -8,16 +8,17 @@ pub mod websocket; use std::{error::Error, sync::mpsc}; use std::{ fmt, - sync::{Mutex, MutexGuard}, + sync::{Arc, Mutex, MutexGuard}, time::Duration, }; use async_trait::async_trait; use lightfx::{Color, Frame}; -use log::debug; +use log::{debug, info}; #[cfg(feature = "visualiser")] use log::{error, info}; use reqwest::header::CONNECTION; +use tokio::net::{self, UdpSocket}; #[derive(Debug, Copy, Clone, PartialEq)] pub enum LightClientError { @@ -63,12 +64,12 @@ impl LightClient for MockLightClient { } #[derive(Clone)] -pub struct RemoteLightClient { +pub struct HttpLightClient { url: String, http_client: reqwest::Client, } -impl RemoteLightClient { +impl HttpLightClient { pub fn new(url: &str) -> Self { Self { url: url.to_owned(), @@ -95,7 +96,7 @@ fn gamma_correction(color: Color) -> Color { } #[async_trait] -impl LightClient for RemoteLightClient { +impl LightClient for HttpLightClient { async fn display_frame(&self, frame: &Frame) -> Result<(), LightClientError> { let pixels: Vec<_> = frame .pixels_iter() @@ -121,6 +122,47 @@ impl LightClient for RemoteLightClient { } } +#[derive(Clone)] +pub struct UdpLightClient { + // url: String, + socket: Arc, +} + +impl UdpLightClient { + pub async fn new(url: &str) -> Self { + let url = url.replace("udp://", ""); + let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); + info!("Bound socket: {}", socket.local_addr().unwrap()); + info!("Trying to connect to: {}", url); + let _ = socket.connect(url).await; + // we don't want old news + socket.set_ttl(1).unwrap(); + // hopefully this gets us some priority + socket.set_tos(152).unwrap(); + Self { socket } + } +} + +#[async_trait] +impl LightClient for UdpLightClient { + async fn display_frame(&self, frame: &Frame) -> Result<(), LightClientError> { + let pixels: Vec<_> = frame + .pixels_iter() + .cloned() + .map(gamma_correction) + .flat_map(|pixel| vec![pixel.g, pixel.r, pixel.b]) + .collect(); + + match self.socket.send(&pixels).await { + Ok(_) => Ok(()), + Err(err) => { + debug!("Failed to send frame to light client: {}", err); + Err(LightClientError::ConnectionLost) + } + } + } +} + #[cfg(feature = "visualiser")] pub struct VisualiserLightClient { _join_handle: std::thread::JoinHandle<()>, diff --git a/webapi/src/main.rs b/webapi/src/main.rs index 4c04b0b..ac1a0e2 100644 --- a/webapi/src/main.rs +++ b/webapi/src/main.rs @@ -205,7 +205,7 @@ async fn main() -> Result<(), Box> { if let Ok(url) = env::var("RUSTMAS_LIGHTS_URL") { if url.starts_with("http://") { - builder = builder.remote_lights(&url)?; + builder = builder.http_lights(&url)?; } else if url.starts_with("ws://") { #[cfg(feature = "websocket")] { @@ -214,6 +214,8 @@ async fn main() -> Result<(), Box> { #[cfg(not(feature = "websocket"))] error!("Web API built without websocket support, ignoring"); + } else if url.starts_with("udp://") { + builder = builder.udp_lights(&url).await?; } else { error!("Unknown remote client protocol, ignoring"); }