diff --git a/animator/src/lib.rs b/animator/src/lib.rs index 96df6c1..2306836 100644 --- a/animator/src/lib.rs +++ b/animator/src/lib.rs @@ -335,6 +335,14 @@ impl ControllerBuilder { Ok(self) } + pub 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::udp::UdpLightClient::new(path))); + Ok(self) + } + #[cfg(feature = "visualiser")] pub fn visualiser_lights(mut self) -> Result> { info!("Using local visualiser"); diff --git a/configurator/src/main.rs b/configurator/src/main.rs index aaed2a5..25538c2 100644 --- a/configurator/src/main.rs +++ b/configurator/src/main.rs @@ -97,6 +97,13 @@ fn capturer_from_options( ); Box::new(light_client::tcp::TcpLightClient::new(&tcp_endpoint)) } + udp_endpoint if endpoint.starts_with("udp://") => { + info!( + "Using remote UDP light client at endpoint: {}", + udp_endpoint + ); + Box::new(light_client::udp::UdpLightClient::new(&udp_endpoint)) + } _ => { info!("Using local TTY light client"); Box::new(light_client::tty::TtyLightClient::new()?) diff --git a/light-client/src/lib.rs b/light-client/src/lib.rs index 98ec92f..40bf442 100755 --- a/light-client/src/lib.rs +++ b/light-client/src/lib.rs @@ -3,6 +3,7 @@ pub mod feedback; pub mod http; pub mod tcp; pub mod tty; +pub mod udp; #[cfg(feature = "websocket")] pub mod websocket; diff --git a/light-client/src/udp.rs b/light-client/src/udp.rs new file mode 100644 index 0000000..9d1d33f --- /dev/null +++ b/light-client/src/udp.rs @@ -0,0 +1,78 @@ +use crate::{LightClient, LightClientError}; +use async_trait::async_trait; +use lightfx::Frame; +use log::{debug, error, info}; +use std::{error::Error, sync::Arc}; +use tokio::{net::UdpSocket, sync::Mutex}; + +#[derive(Clone)] +pub struct UdpLightClient { + url: String, + socket: Arc>>, +} + +impl UdpLightClient { + pub fn new(url: &str) -> Self { + let url = url.strip_prefix("udp://").unwrap_or(url); + Self { + url: url.to_owned(), + socket: Arc::new(Mutex::new(None)), + } + } + + async fn connect(&self) -> Result<(), Box> { + let mut socket = self.socket.lock().await; + if socket.is_none() { + info!("Connecting to remote lights via UDP"); + let s = UdpSocket::bind("0.0.0.0:0").await?; + s.connect(&self.url).await?; + // hopefully this gets us some priority, see: + // https://linuxreviews.org/Type_of_Service_(ToS)_and_DSCP_Values + // this sets `high throughput` and `low delay` along with high precedence + s.set_tos(152)?; + *socket = Some(s); + } + + Ok(()) + } +} + +#[async_trait] +impl LightClient for UdpLightClient { + async fn display_frame(&self, frame: &Frame) -> Result<(), LightClientError> { + let pixels: Vec<_> = frame + .pixels_iter() + .cloned() + .map(crate::gamma_correction) + .flat_map(|pixel| vec![pixel.g, pixel.r, pixel.b]) + .collect(); + + if self.socket.lock().await.is_none() { + if let Err(e) = self.connect().await { + error!("Failed to connect to UDP endpoint: {}", e); + } else { + info!("Successfully connected to UDP endpoint"); + } + } + + let res = { + let mut stream = self.socket.lock().await; + let Some(stream) = stream.as_mut() else { + debug!("UDP endpoint not connected!"); + return Err(LightClientError::ConnectionLost); + }; + stream + .send(&[&(pixels.len() as u16).to_le_bytes(), pixels.as_slice()].concat()) + .await + }; + + match res { + Ok(_) => Ok(()), + Err(e) => { + error!("Failed to send frame to light client: {}", e); + *self.socket.lock().await = None; + Err(LightClientError::ConnectionLost) + } + } + } +} diff --git a/webapi/src/main.rs b/webapi/src/main.rs index f7f3a79..b4d9595 100644 --- a/webapi/src/main.rs +++ b/webapi/src/main.rs @@ -216,6 +216,8 @@ async fn main() -> Result<(), Box> { error!("Web API built without websocket support, ignoring"); } else if url.starts_with("tcp://") { builder = builder.tcp_lights(&url)?; + } else if url.starts_with("udp://") { + builder = builder.udp_lights(&url)?; } else { error!("Unknown remote client protocol, ignoring"); }