gui/network/udp_client.rs
1//! # UDP Client / Listener Module
2//!
3//! This module handles high-speed, un-acknowledged UDP network reception.
4//! It specifically listens for incoming Art-Net DMX protocol packets (OpOutput / OpDmx)
5//! on the default port (6454), extracts DMX channel values for target universes,
6//! and dispatches universe data frames to the UI for live rendering.
7
8use common::logging::LogLevel::*;
9use common::r_log;
10use eframe::egui;
11use std::io;
12use std::net::UdpSocket;
13use std::sync::mpsc::Sender;
14use std::thread;
15use std::time::Duration;
16
17/// Default UDP port defined by the Art-Net protocol specification.
18const ARTNET_PORT: u16 = 6454;
19
20/// Art-Net protocol magic header string bytes ("Art-Net\0").
21const ARTNET_HEADER: &[u8; 8] = b"Art-Net\0";
22
23/// Maximum number of DMX channels per universe according to DMX512 standard.
24pub const MAX_CHANNEL: usize = 512;
25
26/// Binds a UDP socket and spawns a background thread to listen for incoming Art-Net DMX packets.
27///
28/// Filters incoming UDP datagrams for valid Art-Net magic headers and OpDmx opcode (0x5000).
29/// Extracted DMX frames are sent to the UI via `dmx_sender`, and an `egui::Context::request_repaint()`
30/// is triggered to update the universe display immediately.
31///
32/// # Arguments
33/// * `port` - Optional UDP port to bind to (defaults to Art-Net port 6454 if `None`).
34/// * `dmx_sender` - Channel sender to transmit `(universe_id, dmx_data_array)` tuples to the controller.
35/// * `ctx` - `egui::Context` reference used to request UI repaints upon packet arrival.
36///
37/// # Returns
38/// `io::Result<()>` indicating whether the UDP socket binding was successful.
39pub fn start_udp_listener(
40 port: Option<u16>,
41 dmx_sender: Sender<(u8, [u8; MAX_CHANNEL])>,
42 ctx: egui::Context,
43) -> io::Result<()> {
44 let listen_port = port.unwrap_or(ARTNET_PORT);
45 let addr = format!("0.0.0.0:{}", listen_port);
46
47 let socket = UdpSocket::bind(&addr)?;
48 r_log!(Info, "UDP-Listener on adress {} startet!", addr);
49
50 socket.set_read_timeout(Some(Duration::from_millis(100)))?;
51
52 thread::spawn(move || {
53 let mut buf = [0; 1024];
54
55 loop {
56 match socket.recv_from(&mut buf) {
57 Ok((bytes_count, src_addr)) => {
58 let data = &buf[..bytes_count];
59
60 if is_artnet_packet(data) {
61 if data.len() >= 18 && u16::from_le_bytes([data[8], data[9]]) == 0x5000 {
62 let universe_id = data[14];
63 let dmx_length = u16::from_be_bytes([data[16], data[17]]) as usize;
64 let dmx_start_index = 18;
65
66 if data.len() >= dmx_start_index + dmx_length {
67 let mut dmx_data_array = [0; MAX_CHANNEL];
68 let actual_dmx_len = dmx_length.min(MAX_CHANNEL);
69 dmx_data_array[..actual_dmx_len].copy_from_slice(
70 &data[dmx_start_index..dmx_start_index + actual_dmx_len],
71 );
72
73 if let Err(e) = dmx_sender.send((universe_id, dmx_data_array)) {
74 r_log!(Info, "DMX channel closed, stopping UDP listener: {}", e);
75 break;
76 } else {
77 ctx.request_repaint();
78 }
79 } else {
80 r_log!(Warning, "Uncompleted ArtNet-Paket from {}", src_addr);
81 }
82 }
83 } else {
84 handle_generic_packet(data, src_addr);
85 }
86 }
87 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {}
88 Err(e) => {
89 r_log!(Error, "Error receiving UDP-Packets: {}", e);
90 break;
91 }
92 }
93 }
94 });
95
96 Ok(())
97}
98
99/// Helper function to check if a received byte slice begins with the standard Art-Net header.
100///
101/// # Arguments
102/// * `data` - Raw incoming packet byte buffer.
103///
104/// # Returns
105/// `true` if the packet is at least 10 bytes long and starts with `"Art-Net\0"`.
106fn is_artnet_packet(data: &[u8]) -> bool {
107 data.len() >= 10 && &data[0..8] == ARTNET_HEADER
108}
109
110/// Fallback handler for non-Art-Net UDP datagrams received on the socket.
111///
112/// # Arguments
113/// * `data` - Raw packet byte payload.
114/// * `src` - Sender socket address.
115fn handle_generic_packet(data: &[u8], src: std::net::SocketAddr) {
116 r_log!(Info, "Received other UDP-Packet {}: {} Bytes", src, data.len());
117}