Skip to main content

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}