Skip to main content

oxihuman_export/
artnet_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Art-Net UDP packet builder (DMX over IP stub).
6
7pub const ARTNET_ID: &[u8; 8] = b"Art-Net\0";
8pub const ARTNET_PORT: u16 = 6454;
9pub const ARTNET_OP_DMX: u16 = 0x5000;
10pub const ARTNET_PROTOCOL_VER: u16 = 14;
11
12/// Art-Net DMX packet.
13#[allow(dead_code)]
14pub struct ArtDmxPacket {
15    pub universe: u16,
16    pub sequence: u8,
17    pub physical: u8,
18    pub data: Vec<u8>,
19}
20
21impl ArtDmxPacket {
22    #[allow(dead_code)]
23    pub fn new(universe: u16) -> Self {
24        Self {
25            universe,
26            sequence: 0,
27            physical: 0,
28            data: vec![0u8; 512],
29        }
30    }
31}
32
33/// Build an Art-Net ArtDMX UDP payload.
34#[allow(dead_code)]
35pub fn build_artnet_dmx_packet(pkt: &ArtDmxPacket) -> Vec<u8> {
36    let mut out = Vec::new();
37    out.extend_from_slice(ARTNET_ID);
38    out.extend_from_slice(&ARTNET_OP_DMX.to_le_bytes());
39    out.extend_from_slice(&ARTNET_PROTOCOL_VER.to_be_bytes());
40    out.push(pkt.sequence);
41    out.push(pkt.physical);
42    out.extend_from_slice(&pkt.universe.to_le_bytes());
43    let len = pkt.data.len().min(512) as u16;
44    out.extend_from_slice(&len.to_be_bytes());
45    out.extend_from_slice(&pkt.data[..len as usize]);
46    out
47}
48
49/// Set a DMX channel in the packet (1-indexed).
50#[allow(dead_code)]
51pub fn artnet_set_channel(pkt: &mut ArtDmxPacket, channel: usize, value: u8) {
52    if channel >= 1 && channel <= pkt.data.len() {
53        pkt.data[channel - 1] = value;
54    }
55}
56
57/// Get a DMX channel value.
58#[allow(dead_code)]
59pub fn artnet_get_channel(pkt: &ArtDmxPacket, channel: usize) -> u8 {
60    if channel >= 1 && channel <= pkt.data.len() {
61        pkt.data[channel - 1]
62    } else {
63        0
64    }
65}
66
67/// Packet byte length.
68#[allow(dead_code)]
69pub fn artnet_packet_size(pkt: &ArtDmxPacket) -> usize {
70    build_artnet_dmx_packet(pkt).len()
71}
72
73/// Number of active (non-zero) channels.
74#[allow(dead_code)]
75pub fn artnet_active_channels(pkt: &ArtDmxPacket) -> usize {
76    pkt.data.iter().filter(|&&v| v != 0).count()
77}
78
79/// Clear all channels.
80#[allow(dead_code)]
81pub fn artnet_clear(pkt: &mut ArtDmxPacket) {
82    pkt.data.fill(0);
83}
84
85/// Fill all channels with a value.
86#[allow(dead_code)]
87pub fn artnet_fill(pkt: &mut ArtDmxPacket, value: u8) {
88    pkt.data.fill(value);
89}
90
91/// Universe as subnet/universe bytes.
92#[allow(dead_code)]
93pub fn universe_to_subnet_address(universe: u16) -> (u8, u8) {
94    let subnet = ((universe >> 4) & 0x0F) as u8;
95    let net = (universe & 0x0F) as u8;
96    (subnet, net)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn packet_starts_with_art_net_id() {
105        let pkt = ArtDmxPacket::new(0);
106        let bytes = build_artnet_dmx_packet(&pkt);
107        assert_eq!(&bytes[0..8], ARTNET_ID);
108    }
109
110    #[test]
111    fn packet_op_code_correct() {
112        let pkt = ArtDmxPacket::new(0);
113        let bytes = build_artnet_dmx_packet(&pkt);
114        let op = u16::from_le_bytes([bytes[8], bytes[9]]);
115        assert_eq!(op, ARTNET_OP_DMX);
116    }
117
118    #[test]
119    fn packet_header_length_minimum() {
120        let pkt = ArtDmxPacket::new(0);
121        let bytes = build_artnet_dmx_packet(&pkt);
122        assert!(bytes.len() >= 18);
123    }
124
125    #[test]
126    fn set_get_channel() {
127        let mut pkt = ArtDmxPacket::new(0);
128        artnet_set_channel(&mut pkt, 1, 200);
129        assert_eq!(artnet_get_channel(&pkt, 1), 200);
130    }
131
132    #[test]
133    fn active_channels_after_set() {
134        let mut pkt = ArtDmxPacket::new(0);
135        artnet_set_channel(&mut pkt, 5, 100);
136        assert_eq!(artnet_active_channels(&pkt), 1);
137    }
138
139    #[test]
140    fn artnet_clear_zeros() {
141        let mut pkt = ArtDmxPacket::new(0);
142        artnet_fill(&mut pkt, 255);
143        artnet_clear(&mut pkt);
144        assert_eq!(artnet_active_channels(&pkt), 0);
145    }
146
147    #[test]
148    fn artnet_fill_all_same() {
149        let mut pkt = ArtDmxPacket::new(0);
150        artnet_fill(&mut pkt, 42);
151        assert!(pkt.data.iter().all(|&v| v == 42));
152    }
153
154    #[test]
155    fn universe_subnet_address() {
156        let (subnet, net) = universe_to_subnet_address(0x23);
157        assert_eq!(subnet, 2);
158        assert_eq!(net, 3);
159    }
160
161    #[test]
162    fn packet_size_fixed() {
163        let pkt = ArtDmxPacket::new(0);
164        let sz = artnet_packet_size(&pkt);
165        assert!((18..=600).contains(&sz));
166    }
167
168    #[test]
169    fn default_port_correct() {
170        assert_eq!(ARTNET_PORT, 6454);
171    }
172}