use anyhow::{Context, Result, anyhow};
use artnet_protocol::{ArtCommand, Poll, PollReply};
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use std::{
net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs, UdpSocket},
sync::Mutex,
time::{Duration, Instant},
};
use crate::{DmxPort, PortListing};
const PORT: u16 = 6454;
#[derive(Serialize, Deserialize)]
#[serde(try_from = "ArtnetDmxPortParams")]
pub struct ArtnetDmxPort {
#[serde(skip_serializing)]
socket: UdpSocket,
#[serde(flatten)]
params: ArtnetDmxPortParams,
#[serde(skip_serializing)]
send_buf: Vec<u8>,
}
impl TryFrom<ArtnetDmxPortParams> for ArtnetDmxPort {
type Error = anyhow::Error;
fn try_from(params: ArtnetDmxPortParams) -> Result<Self, Self::Error> {
Ok(Self {
socket: get_socket()?,
params,
send_buf: vec![],
})
}
}
#[derive(Serialize, Deserialize)]
struct ArtnetDmxPortParams {
addr: Ipv4Addr,
port_address: u16,
short_name: String,
long_name: String,
}
impl std::fmt::Display for ArtnetDmxPort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ArtNet {} at {} universe {} ({})",
self.params.short_name,
self.params.addr,
self.params.port_address,
self.params.long_name
)
}
}
static ARTNET_SOCKET: Mutex<Option<UdpSocket>> = Mutex::new(None);
fn get_socket() -> anyhow::Result<UdpSocket> {
let mut socket_guard = ARTNET_SOCKET
.lock()
.map_err(|_| anyhow!("failed to acquire global artnet socket lock"))?;
if let Some(s) = socket_guard.as_ref() {
return s.try_clone().context("cloning artnet socket");
}
let s = UdpSocket::bind(("0.0.0.0", PORT)).context("failed to bind UDP socket for artnet")?;
let cloned = s.try_clone().context("cloning artnet socket")?;
*socket_guard = Some(s);
Ok(cloned)
}
impl ArtnetDmxPort {
pub fn available_ports(wait: Duration) -> Result<PortListing> {
let socket = get_socket()?;
let broadcast_addr = ("255.255.255.255", PORT)
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
socket
.set_broadcast(true)
.context("setting ArtNet socket to allow broadcast")?;
let buff = ArtCommand::Poll(Poll::default())
.write_to_buffer()
.context("writing ArtNet poll command")?;
socket
.send_to(&buff, broadcast_addr)
.context("sending ArtNet poll message")?;
let start = Instant::now();
let mut ports: Vec<Self> = vec![];
let mut receive_poll = |timeout| -> anyhow::Result<()> {
socket.set_read_timeout(Some(timeout))?;
let mut buffer = [0u8; 1024];
let (length, _addr) = socket.recv_from(&mut buffer)?;
let command = ArtCommand::from_buffer(&buffer[..length])?;
if let ArtCommand::PollReply(reply) = command {
ports.extend(Self::ports_from_poll(&reply)?);
}
Ok(())
};
loop {
let waited_so_far = start.elapsed();
if waited_so_far > wait {
break;
}
if let Err(err) = receive_poll(wait - waited_so_far) {
debug!("Error receiving artnet poll response: {err}.");
}
}
if let Err(err) = socket.set_read_timeout(None) {
warn!("Error disabling ArtNet socket timeout: {err}");
}
let listing = Self::sorted_unique(ports)
.into_iter()
.map(|p| Box::new(p) as Box<dyn DmxPort>)
.collect();
Ok(listing)
}
fn sorted_unique(mut ports: Vec<Self>) -> Vec<Self> {
ports.sort_by_key(|p| (p.params.addr, p.params.port_address));
ports.dedup_by_key(|p| (p.params.addr, p.params.port_address));
ports
}
fn ports_from_poll(reply: &PollReply) -> Result<Vec<Self>> {
let mut ports = Vec::new();
for i in 0..4 {
if reply.port_types[i] & 0x80 == 0 {
continue;
}
ports.push(Self {
socket: get_socket()?,
params: ArtnetDmxPortParams {
addr: reply.address,
port_address: output_port_address(reply, i),
short_name: null_terminated_string_lossy(&reply.short_name).to_string(),
long_name: null_terminated_string_lossy(&reply.long_name).to_string(),
},
send_buf: vec![],
});
}
Ok(ports)
}
fn write(&mut self, frame: &[u8]) -> Result<()> {
self.send_buf.clear();
send::write(&mut self.send_buf, self.params.port_address, frame)
.context("constructing artnet buffer")?;
let dest = SocketAddrV4::new(self.params.addr, PORT);
self.socket.send_to(&self.send_buf, dest)?;
Ok(())
}
}
#[typetag::serde]
impl DmxPort for ArtnetDmxPort {
fn open(&mut self) -> Result<(), crate::OpenError> {
Ok(())
}
fn close(&mut self) {}
fn write(&mut self, frame: &[u8]) -> Result<(), crate::WriteError> {
self.write(frame)?;
Ok(())
}
}
fn output_port_address(reply: &PollReply, output_index: usize) -> u16 {
let net = (reply.port_address[0] & 0x7F) as u16;
let sub_net = (reply.port_address[1] & 0x0F) as u16;
let universe = (reply.swout[output_index] & 0x0F) as u16;
(net << 8) | (sub_net << 4) | universe
}
fn null_terminated_string_lossy(bytes: &[u8]) -> String {
let null_pos = bytes
.iter()
.position(|c| *c == b'\0')
.unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[0..null_pos]).to_string()
}
mod send {
use anyhow::{Result, ensure};
use std::io::Write;
const ARTNET_HEADER: &[u8; 8] = b"Art-Net\0";
const ARTNET_PROTOCOL_VERSION: [u8; 2] = [0, 14];
pub fn write(mut w: impl Write, arnet_port_address: u16, buf: &[u8]) -> Result<()> {
ensure!(!buf.is_empty(), "cannot send zero-length artnet frame");
ensure!(
buf.len() <= 512,
"artnet frame payload too long: {}",
buf.len()
);
let opcode: u16 = 0x5000;
w.write_all(ARTNET_HEADER)?;
w.write_all(&opcode.to_le_bytes())?;
w.write_all(&ARTNET_PROTOCOL_VERSION)?;
write_u8(&mut w, 0)?;
write_u8(&mut w, 0)?;
w.write_all(&arnet_port_address.to_le_bytes())?;
let add_pad_byte = !buf.len().is_multiple_of(2);
let padded_len = buf.len() as u16 + add_pad_byte as u16;
w.write_all(&padded_len.to_be_bytes())?;
w.write_all(buf)?;
if add_pad_byte {
write_u8(&mut w, 0)?;
}
Ok(())
}
fn write_u8(mut w: impl Write, v: u8) -> std::io::Result<()> {
let buf: [u8; 1] = [v];
w.write_all(&buf)
}
#[cfg(test)]
mod test {
use artnet_protocol::{ArtCommand, Output};
use super::write;
#[test]
fn test_match() {
for len in 1..512 {
let buf = vec![0u8; len];
assert_match(&buf);
}
}
fn write_vec(buf: &[u8]) -> Vec<u8> {
let mut w = vec![];
write(&mut w, 1, buf).unwrap();
w
}
fn assert_match(buf: &[u8]) {
let custom = write_vec(buf);
let library = ArtCommand::Output(Output {
data: buf.to_vec().into(),
..Default::default()
})
.write_to_buffer()
.unwrap();
assert_eq!(library, custom);
}
}
}
#[cfg(test)]
mod test {
use super::*;
use artnet_protocol::PollReply;
fn universes(reply: &PollReply) -> Vec<u16> {
ArtnetDmxPort::ports_from_poll(reply)
.unwrap()
.iter()
.map(|p| p.params.port_address)
.collect()
}
#[test]
fn enumerates_advertised_output_universes() {
let reply = PollReply {
port_types: [0x80, 0x80, 0, 0],
swout: [1, 2, 0, 0],
..Default::default()
};
assert_eq!(universes(&reply), vec![1, 2]);
let reply = PollReply {
port_address: [0x01, 0x02], port_types: [0x80, 0, 0, 0],
swout: [3, 0, 0, 0],
..Default::default()
};
assert_eq!(universes(&reply), vec![(1 << 8) | (2 << 4) | 3]);
}
#[test]
fn skips_non_output_ports() {
let reply = PollReply {
port_types: [0x40, 0xC0, 0x80, 0x00],
swout: [9, 5, 6, 7],
..Default::default()
};
assert_eq!(universes(&reply), vec![5, 6]);
}
#[test]
fn sorts_and_dedupes_destinations_across_replies() {
let page = |addr: [u8; 4], sub_net: u8, swout: [u8; 4]| PollReply {
address: addr.into(),
port_address: [0, sub_net],
port_types: [0x80, 0x80, 0, 0],
swout,
..Default::default()
};
let lo = [10, 0, 0, 5];
let hi = [10, 0, 0, 7];
let ports = [
page(hi, 0, [4, 3, 0, 0]),
page(lo, 1, [2, 1, 0, 0]),
page(lo, 0, [2, 1, 0, 0]),
page(lo, 1, [2, 1, 0, 0]),
]
.iter()
.flat_map(|r| ArtnetDmxPort::ports_from_poll(r).unwrap())
.collect();
let destinations: Vec<(Ipv4Addr, u16)> = ArtnetDmxPort::sorted_unique(ports)
.iter()
.map(|p| (p.params.addr, p.params.port_address))
.collect();
assert_eq!(
destinations,
vec![
(lo.into(), 1),
(lo.into(), 2),
(lo.into(), (1 << 4) | 1),
(lo.into(), (1 << 4) | 2),
(hi.into(), 3),
(hi.into(), 4),
]
);
}
}