use std::any::Any;
#[cfg(feature = "lasercube-wifi")]
use std::io;
use std::net::IpAddr;
#[cfg(any(feature = "ether-dream", feature = "idn", feature = "lasercube-wifi"))]
use std::time::Duration;
use crate::backend::{BackendKind, Error, Result};
use crate::types::{DacType, EnabledDacTypes};
pub trait ExternalDiscoverer: Send {
fn dac_type(&self) -> DacType;
fn scan(&mut self) -> Vec<ExternalDevice>;
fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<BackendKind>;
}
pub struct ExternalDevice {
pub ip_address: Option<IpAddr>,
pub mac_address: Option<[u8; 6]>,
pub hostname: Option<String>,
pub usb_address: Option<String>,
pub hardware_name: Option<String>,
pub device_index: Option<u16>,
pub opaque_data: Box<dyn Any + Send>,
}
impl ExternalDevice {
pub fn new<T: Any + Send + 'static>(opaque_data: T) -> Self {
Self {
ip_address: None,
mac_address: None,
hostname: None,
usb_address: None,
hardware_name: None,
device_index: None,
opaque_data: Box::new(opaque_data),
}
}
}
#[cfg(feature = "helios")]
use crate::backend::HeliosBackend;
#[cfg(feature = "helios")]
use crate::protocols::helios::{HeliosDac, HeliosDacController};
#[cfg(feature = "ether-dream")]
use crate::backend::EtherDreamBackend;
#[cfg(feature = "ether-dream")]
use crate::protocols::ether_dream::protocol::DacBroadcast as EtherDreamBroadcast;
#[cfg(feature = "ether-dream")]
use crate::protocols::ether_dream::recv_dac_broadcasts;
#[cfg(feature = "idn")]
use crate::backend::IdnBackend;
#[cfg(feature = "idn")]
use crate::protocols::idn::dac::ServerInfo as IdnServerInfo;
#[cfg(feature = "idn")]
use crate::protocols::idn::dac::ServiceInfo as IdnServiceInfo;
#[cfg(feature = "idn")]
use crate::protocols::idn::scan_for_servers;
#[cfg(all(feature = "idn", feature = "testutils"))]
use crate::protocols::idn::ServerScanner;
#[cfg(all(feature = "idn", feature = "testutils"))]
use std::net::SocketAddr;
#[cfg(feature = "lasercube-wifi")]
use crate::backend::LasercubeWifiBackend;
#[cfg(feature = "lasercube-wifi")]
use crate::protocols::lasercube_wifi::dac::Addressed as LasercubeAddressed;
#[cfg(feature = "lasercube-wifi")]
use crate::protocols::lasercube_wifi::discover_dacs as discover_lasercube_wifi;
#[cfg(feature = "lasercube-wifi")]
use crate::protocols::lasercube_wifi::protocol::DeviceInfo as LasercubeDeviceInfo;
#[cfg(feature = "lasercube-usb")]
use crate::backend::LasercubeUsbBackend;
#[cfg(feature = "lasercube-usb")]
use crate::protocols::lasercube_usb::rusb;
#[cfg(feature = "lasercube-usb")]
use crate::protocols::lasercube_usb::DacController as LasercubeUsbController;
#[cfg(feature = "avb")]
use crate::backend::AvbBackend;
#[cfg(feature = "avb")]
use crate::protocols::avb::{discover_device_selectors as discover_avb_selectors, AvbSelector};
pub struct DiscoveredDevice {
dac_type: DacType,
ip_address: Option<IpAddr>,
mac_address: Option<[u8; 6]>,
hostname: Option<String>,
usb_address: Option<String>,
hardware_name: Option<String>,
device_index: Option<u16>,
inner: DiscoveredDeviceInner,
}
impl DiscoveredDevice {
fn new(dac_type: DacType, inner: DiscoveredDeviceInner) -> Self {
Self {
dac_type,
ip_address: None,
mac_address: None,
hostname: None,
usb_address: None,
hardware_name: None,
device_index: None,
inner,
}
}
pub fn name(&self) -> String {
self.info().name()
}
pub fn dac_type(&self) -> DacType {
self.dac_type.clone()
}
pub fn info(&self) -> DiscoveredDeviceInfo {
DiscoveredDeviceInfo {
dac_type: self.dac_type.clone(),
ip_address: self.ip_address,
mac_address: self.mac_address,
hostname: self.hostname.clone(),
usb_address: self.usb_address.clone(),
hardware_name: self.hardware_name.clone(),
device_index: self.device_index,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DiscoveredDeviceInfo {
pub dac_type: DacType,
pub ip_address: Option<IpAddr>,
pub mac_address: Option<[u8; 6]>,
pub hostname: Option<String>,
pub usb_address: Option<String>,
pub hardware_name: Option<String>,
pub device_index: Option<u16>,
}
impl DiscoveredDeviceInfo {
pub fn name(&self) -> String {
self.ip_address
.map(|ip| ip.to_string())
.or_else(|| self.hardware_name.clone())
.or_else(|| self.usb_address.clone())
.unwrap_or_else(|| "Unknown".into())
}
pub fn stable_id(&self) -> String {
match &self.dac_type {
DacType::EtherDream => {
if let Some(mac) = self.mac_address {
return format!(
"etherdream:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
);
}
if let Some(ip) = self.ip_address {
return format!("etherdream:{}", ip);
}
}
DacType::Idn => {
if let Some(ref hostname) = self.hostname {
return format!("idn:{}", hostname);
}
if let Some(ip) = self.ip_address {
return format!("idn:{}", ip);
}
}
DacType::Helios => {
if let Some(ref hw_name) = self.hardware_name {
return format!("helios:{}", hw_name);
}
if let Some(ref usb_addr) = self.usb_address {
return format!("helios:{}", usb_addr);
}
}
DacType::LasercubeUsb => {
if let Some(ref hw_name) = self.hardware_name {
return format!("lasercube-usb:{}", hw_name);
}
if let Some(ref usb_addr) = self.usb_address {
return format!("lasercube-usb:{}", usb_addr);
}
}
DacType::LasercubeWifi => {
if let Some(ip) = self.ip_address {
return format!("lasercube-wifi:{}", ip);
}
}
DacType::Avb => {
if let Some(ref hw_name) = self.hardware_name {
let slug = slugify_device_id(hw_name);
if let Some(index) = self.device_index {
return format!("avb:{}:{}", slug, index);
}
return format!("avb:{}", slug);
}
}
DacType::Custom(name) => {
if let Some(ip) = self.ip_address {
return format!("{}:{}", name.to_lowercase(), ip);
}
if let Some(ref hw_name) = self.hardware_name {
return format!("{}:{}", name.to_lowercase(), hw_name);
}
}
}
format!("unknown:{:?}", self.dac_type)
}
}
fn slugify_device_id(name: &str) -> String {
let normalized = name
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_ascii_lowercase();
let mut slug = String::with_capacity(normalized.len());
let mut prev_dash = false;
for ch in normalized.chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch);
prev_dash = false;
} else if !prev_dash {
slug.push('-');
prev_dash = true;
}
}
slug.trim_matches('-').to_string()
}
enum DiscoveredDeviceInner {
#[cfg(feature = "helios")]
Helios(HeliosDac),
#[cfg(feature = "ether-dream")]
EtherDream {
broadcast: EtherDreamBroadcast,
ip: IpAddr,
},
#[cfg(feature = "idn")]
Idn {
server: IdnServerInfo,
service: IdnServiceInfo,
},
#[cfg(feature = "lasercube-wifi")]
LasercubeWifi {
info: LasercubeDeviceInfo,
source_addr: std::net::SocketAddr,
},
#[cfg(feature = "lasercube-usb")]
LasercubeUsb(rusb::Device<rusb::Context>),
#[cfg(feature = "avb")]
Avb(AvbSelector),
External {
discoverer_index: usize,
opaque_data: Box<dyn Any + Send>,
},
#[cfg(not(any(
feature = "helios",
feature = "ether-dream",
feature = "idn",
feature = "lasercube-wifi",
feature = "lasercube-usb",
feature = "avb"
)))]
_Placeholder,
}
#[cfg(feature = "helios")]
pub struct HeliosDiscovery {
controller: HeliosDacController,
}
#[cfg(feature = "helios")]
impl HeliosDiscovery {
pub fn new() -> Option<Self> {
HeliosDacController::new()
.ok()
.map(|controller| Self { controller })
}
pub fn scan(&self) -> Vec<DiscoveredDevice> {
let Ok(devices) = self.controller.list_devices() else {
return Vec::new();
};
let mut discovered = Vec::new();
for device in devices {
let HeliosDac::Idle(_) = &device else {
continue;
};
let opened = match device.open() {
Ok(o) => o,
Err(_) => continue,
};
let hardware_name = opened.name().unwrap_or_else(|_| "Unknown Helios".into());
let mut device =
DiscoveredDevice::new(DacType::Helios, DiscoveredDeviceInner::Helios(opened));
device.hardware_name = Some(hardware_name);
discovered.push(device);
}
discovered
}
pub fn connect(&self, device: DiscoveredDevice) -> Result<BackendKind> {
let DiscoveredDeviceInner::Helios(dac) = device.inner else {
return Err(Error::invalid_config("Invalid device type for Helios"));
};
Ok(BackendKind::FrameSwap(Box::new(HeliosBackend::from_dac(
dac,
))))
}
}
#[cfg(feature = "ether-dream")]
pub struct EtherDreamDiscovery {
timeout: Duration,
}
#[cfg(feature = "ether-dream")]
impl EtherDreamDiscovery {
pub fn new() -> Self {
Self {
timeout: Duration::from_millis(1500),
}
}
pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
let Ok(mut rx) = recv_dac_broadcasts() else {
return Vec::new();
};
if rx.set_timeout(Some(self.timeout)).is_err() {
return Vec::new();
}
let mut discovered = Vec::new();
let mut seen_macs = std::collections::HashSet::new();
for _ in 0..3 {
let (broadcast, source_addr) = match rx.next_broadcast() {
Ok(b) => b,
Err(_) => break,
};
let ip = source_addr.ip();
let device_mac = broadcast.mac_address;
if seen_macs.contains(&device_mac) {
continue;
}
seen_macs.insert(device_mac);
let mut device = DiscoveredDevice::new(
DacType::EtherDream,
DiscoveredDeviceInner::EtherDream { broadcast, ip },
);
device.ip_address = Some(ip);
device.mac_address = Some(device_mac);
discovered.push(device);
}
discovered
}
pub fn connect(&self, device: DiscoveredDevice) -> Result<BackendKind> {
let DiscoveredDeviceInner::EtherDream { broadcast, ip } = device.inner else {
return Err(Error::invalid_config("Invalid device type for EtherDream"));
};
let backend = EtherDreamBackend::new(broadcast, ip);
Ok(BackendKind::Fifo(Box::new(backend)))
}
}
#[cfg(feature = "ether-dream")]
impl Default for EtherDreamDiscovery {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "idn")]
pub struct IdnDiscovery {
scan_timeout: Duration,
}
#[cfg(feature = "idn")]
impl IdnDiscovery {
pub fn new() -> Self {
Self {
scan_timeout: Duration::from_millis(500),
}
}
pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
let Ok(servers) = scan_for_servers(self.scan_timeout) else {
return Vec::new();
};
Self::servers_to_devices(servers)
}
pub fn connect(&self, device: DiscoveredDevice) -> Result<BackendKind> {
let DiscoveredDeviceInner::Idn { server, service } = device.inner else {
return Err(Error::invalid_config("Invalid device type for IDN"));
};
Ok(BackendKind::Fifo(Box::new(IdnBackend::new(
server, service,
))))
}
#[cfg(feature = "testutils")]
pub fn scan_address(&mut self, addr: SocketAddr) -> Vec<DiscoveredDevice> {
let Ok(mut scanner) = ServerScanner::new(0) else {
return Vec::new();
};
let Ok(servers) = scanner.scan_address(addr, self.scan_timeout) else {
return Vec::new();
};
Self::servers_to_devices(servers)
}
fn servers_to_devices(servers: Vec<IdnServerInfo>) -> Vec<DiscoveredDevice> {
servers
.into_iter()
.filter_map(|server| {
let service = server.find_laser_projector().cloned()?;
let ip_address = server.addresses.first().map(|addr| addr.ip());
let hostname = server.hostname.clone();
let mut device = DiscoveredDevice::new(
DacType::Idn,
DiscoveredDeviceInner::Idn { server, service },
);
device.ip_address = ip_address;
device.hostname = Some(hostname);
Some(device)
})
.collect()
}
}
#[cfg(feature = "idn")]
impl Default for IdnDiscovery {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "lasercube-wifi")]
pub struct LasercubeWifiDiscovery {
timeout: Duration,
}
#[cfg(feature = "lasercube-wifi")]
impl LasercubeWifiDiscovery {
pub fn new() -> Self {
Self {
timeout: Duration::from_millis(100),
}
}
pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
let Ok(mut discovery) = discover_lasercube_wifi() else {
return Vec::new();
};
if discovery.set_timeout(Some(self.timeout)).is_err() {
return Vec::new();
}
let mut discovered = Vec::new();
for _ in 0..10 {
let (device_info, source_addr) = match discovery.next_device() {
Ok(d) => d,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) if e.kind() == io::ErrorKind::TimedOut => break,
Err(_) => continue,
};
let ip_address = source_addr.ip();
let mut device = DiscoveredDevice::new(
DacType::LasercubeWifi,
DiscoveredDeviceInner::LasercubeWifi {
info: device_info,
source_addr,
},
);
device.ip_address = Some(ip_address);
discovered.push(device);
}
discovered
}
pub fn connect(&self, device: DiscoveredDevice) -> Result<BackendKind> {
let DiscoveredDeviceInner::LasercubeWifi { info, source_addr } = device.inner else {
return Err(Error::invalid_config(
"Invalid device type for LaserCube WiFi",
));
};
let addressed = LasercubeAddressed::from_discovery(&info, source_addr);
Ok(BackendKind::Fifo(Box::new(LasercubeWifiBackend::new(
addressed,
))))
}
}
#[cfg(feature = "lasercube-wifi")]
impl Default for LasercubeWifiDiscovery {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "lasercube-usb")]
pub struct LasercubeUsbDiscovery {
controller: LasercubeUsbController,
}
#[cfg(feature = "lasercube-usb")]
impl LasercubeUsbDiscovery {
pub fn new() -> Option<Self> {
LasercubeUsbController::new()
.ok()
.map(|controller| Self { controller })
}
pub fn scan(&self) -> Vec<DiscoveredDevice> {
let Ok(devices) = self.controller.list_devices() else {
return Vec::new();
};
let mut discovered = Vec::new();
for device in devices {
let usb_address = format!("{}:{}", device.bus_number(), device.address());
let serial = crate::protocols::lasercube_usb::get_serial_number(&device);
let mut discovered_device = DiscoveredDevice::new(
DacType::LasercubeUsb,
DiscoveredDeviceInner::LasercubeUsb(device),
);
discovered_device.usb_address = Some(usb_address);
discovered_device.hardware_name = serial;
discovered.push(discovered_device);
}
discovered
}
pub fn connect(&self, device: DiscoveredDevice) -> Result<BackendKind> {
let DiscoveredDeviceInner::LasercubeUsb(usb_device) = device.inner else {
return Err(Error::invalid_config(
"Invalid device type for LaserCube USB",
));
};
let backend = LasercubeUsbBackend::new(usb_device);
Ok(BackendKind::Fifo(Box::new(backend)))
}
}
#[cfg(feature = "avb")]
pub struct AvbDiscovery;
#[cfg(feature = "avb")]
impl AvbDiscovery {
pub fn new() -> Self {
Self
}
pub fn scan(&self) -> Vec<DiscoveredDevice> {
let Ok(selectors) = discover_avb_selectors() else {
return Vec::new();
};
selectors
.into_iter()
.map(|selector| {
let hardware_name = selector.name.clone();
let index = selector.duplicate_index;
let mut device =
DiscoveredDevice::new(DacType::Avb, DiscoveredDeviceInner::Avb(selector));
device.hardware_name = Some(hardware_name);
device.device_index = Some(index);
device
})
.collect()
}
pub fn connect(&self, device: DiscoveredDevice) -> Result<BackendKind> {
let DiscoveredDeviceInner::Avb(selector) = device.inner else {
return Err(Error::invalid_config("Invalid device type for AVB"));
};
Ok(BackendKind::Fifo(Box::new(AvbBackend::from_selector(
selector,
))))
}
}
#[cfg(feature = "avb")]
impl Default for AvbDiscovery {
fn default() -> Self {
Self::new()
}
}
pub struct DacDiscovery {
#[cfg(feature = "helios")]
helios: Option<HeliosDiscovery>,
#[cfg(feature = "ether-dream")]
etherdream: EtherDreamDiscovery,
#[cfg(feature = "idn")]
idn: IdnDiscovery,
#[cfg(all(feature = "idn", feature = "testutils"))]
idn_scan_addresses: Vec<SocketAddr>,
#[cfg(feature = "lasercube-wifi")]
lasercube_wifi: LasercubeWifiDiscovery,
#[cfg(feature = "lasercube-usb")]
lasercube_usb: Option<LasercubeUsbDiscovery>,
#[cfg(feature = "avb")]
avb: AvbDiscovery,
enabled: EnabledDacTypes,
external: Vec<Box<dyn ExternalDiscoverer>>,
}
impl DacDiscovery {
pub fn new(enabled: EnabledDacTypes) -> Self {
Self {
#[cfg(feature = "helios")]
helios: HeliosDiscovery::new(),
#[cfg(feature = "ether-dream")]
etherdream: EtherDreamDiscovery::new(),
#[cfg(feature = "idn")]
idn: IdnDiscovery::new(),
#[cfg(all(feature = "idn", feature = "testutils"))]
idn_scan_addresses: Vec::new(),
#[cfg(feature = "lasercube-wifi")]
lasercube_wifi: LasercubeWifiDiscovery::new(),
#[cfg(feature = "lasercube-usb")]
lasercube_usb: LasercubeUsbDiscovery::new(),
#[cfg(feature = "avb")]
avb: AvbDiscovery::new(),
enabled,
external: Vec::new(),
}
}
#[cfg(all(feature = "idn", feature = "testutils"))]
pub fn set_idn_scan_addresses(&mut self, addresses: Vec<SocketAddr>) {
self.idn_scan_addresses = addresses;
}
pub fn set_enabled(&mut self, enabled: EnabledDacTypes) {
self.enabled = enabled;
}
pub fn enabled(&self) -> &EnabledDacTypes {
&self.enabled
}
pub fn register(&mut self, discoverer: Box<dyn ExternalDiscoverer>) {
self.external.push(discoverer);
}
pub fn scan(&mut self) -> Vec<DiscoveredDevice> {
let mut devices = Vec::new();
#[cfg(feature = "helios")]
if self.enabled.is_enabled(DacType::Helios) {
if let Some(ref discovery) = self.helios {
devices.extend(discovery.scan());
}
}
#[cfg(feature = "ether-dream")]
if self.enabled.is_enabled(DacType::EtherDream) {
devices.extend(self.etherdream.scan());
}
#[cfg(feature = "idn")]
if self.enabled.is_enabled(DacType::Idn) {
#[cfg(feature = "testutils")]
{
if self.idn_scan_addresses.is_empty() {
devices.extend(self.idn.scan());
} else {
for addr in &self.idn_scan_addresses {
devices.extend(self.idn.scan_address(*addr));
}
}
}
#[cfg(not(feature = "testutils"))]
{
devices.extend(self.idn.scan());
}
}
#[cfg(feature = "lasercube-wifi")]
if self.enabled.is_enabled(DacType::LasercubeWifi) {
devices.extend(self.lasercube_wifi.scan());
}
#[cfg(feature = "lasercube-usb")]
if self.enabled.is_enabled(DacType::LasercubeUsb) {
if let Some(ref discovery) = self.lasercube_usb {
devices.extend(discovery.scan());
}
}
#[cfg(feature = "avb")]
if self.enabled.is_enabled(DacType::Avb) {
devices.extend(self.avb.scan());
}
for (index, discoverer) in self.external.iter_mut().enumerate() {
let dac_type = discoverer.dac_type();
for ext_device in discoverer.scan() {
let mut device = DiscoveredDevice::new(
dac_type.clone(),
DiscoveredDeviceInner::External {
discoverer_index: index,
opaque_data: ext_device.opaque_data,
},
);
device.ip_address = ext_device.ip_address;
device.mac_address = ext_device.mac_address;
device.hostname = ext_device.hostname;
device.usb_address = ext_device.usb_address;
device.hardware_name = ext_device.hardware_name;
device.device_index = ext_device.device_index;
devices.push(device);
}
}
devices
}
fn dac_type_from_id_prefix(id: &str) -> Option<DacType> {
let prefix = id.split(':').next()?;
match prefix {
"etherdream" => Some(DacType::EtherDream),
"idn" => Some(DacType::Idn),
"helios" => Some(DacType::Helios),
"lasercube-usb" => Some(DacType::LasercubeUsb),
"lasercube-wifi" => Some(DacType::LasercubeWifi),
"avb" => Some(DacType::Avb),
_ => None,
}
}
pub(crate) fn open_by_id(&mut self, id: &str) -> Result<crate::stream::Dac> {
let discovered = if let Some(dac_type) = Self::dac_type_from_id_prefix(id) {
let saved = self.enabled.clone();
self.enabled = std::iter::once(dac_type).collect();
let result = self.scan();
self.enabled = saved;
result
} else {
self.scan()
};
let device = discovered
.into_iter()
.find(|d| d.info().stable_id() == id)
.ok_or_else(|| Error::disconnected(format!("DAC not found: {}", id)))?;
let name = device.info().name();
let dac_type = device.dac_type();
let stream_backend = self.connect(device)?;
let dac_info = crate::types::DacInfo {
id: id.to_string(),
name,
kind: dac_type,
caps: stream_backend.caps().clone(),
};
Ok(crate::stream::Dac::new(dac_info, stream_backend))
}
#[allow(unreachable_patterns)]
pub fn connect(&mut self, device: DiscoveredDevice) -> Result<BackendKind> {
if let DiscoveredDeviceInner::External {
discoverer_index,
opaque_data,
} = device.inner
{
let backend_kind = self
.external
.get_mut(discoverer_index)
.ok_or_else(|| Error::invalid_config("External discoverer not found"))?
.connect(opaque_data)?;
return Ok(backend_kind);
}
match device.dac_type {
#[cfg(feature = "helios")]
DacType::Helios => self
.helios
.as_ref()
.ok_or_else(|| Error::disconnected("Helios discovery not available"))?
.connect(device),
#[cfg(feature = "ether-dream")]
DacType::EtherDream => self.etherdream.connect(device),
#[cfg(feature = "idn")]
DacType::Idn => self.idn.connect(device),
#[cfg(feature = "lasercube-wifi")]
DacType::LasercubeWifi => self.lasercube_wifi.connect(device),
#[cfg(feature = "lasercube-usb")]
DacType::LasercubeUsb => self
.lasercube_usb
.as_ref()
.ok_or_else(|| Error::disconnected("LaserCube USB discovery not available"))?
.connect(device),
#[cfg(feature = "avb")]
DacType::Avb => self.avb.connect(device),
_ => Err(Error::invalid_config(format!(
"DAC type {:?} not supported in this build",
device.dac_type
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stable_id_etherdream_with_mac() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::EtherDream,
ip_address: Some("192.168.1.100".parse().unwrap()),
mac_address: Some([0x01, 0x23, 0x45, 0x67, 0x89, 0xab]),
hostname: None,
usb_address: None,
hardware_name: None,
device_index: None,
};
assert_eq!(info.stable_id(), "etherdream:01:23:45:67:89:ab");
}
#[test]
fn test_stable_id_idn_with_hostname() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::Idn,
ip_address: Some("192.168.1.100".parse().unwrap()),
mac_address: None,
hostname: Some("laser-projector.local".to_string()),
usb_address: None,
hardware_name: None,
device_index: None,
};
assert_eq!(info.stable_id(), "idn:laser-projector.local");
}
#[test]
fn test_stable_id_helios_with_hardware_name() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::Helios,
ip_address: None,
mac_address: None,
hostname: None,
usb_address: Some("1:5".to_string()),
hardware_name: Some("Helios DAC".to_string()),
device_index: None,
};
assert_eq!(info.stable_id(), "helios:Helios DAC");
}
#[test]
fn test_stable_id_lasercube_usb_with_address() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::LasercubeUsb,
ip_address: None,
mac_address: None,
hostname: None,
usb_address: Some("2:3".to_string()),
hardware_name: None,
device_index: None,
};
assert_eq!(info.stable_id(), "lasercube-usb:2:3");
}
#[test]
fn test_stable_id_lasercube_wifi_with_ip() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::LasercubeWifi,
ip_address: Some("192.168.1.50".parse().unwrap()),
mac_address: None,
hostname: None,
usb_address: None,
hardware_name: None,
device_index: None,
};
assert_eq!(info.stable_id(), "lasercube-wifi:192.168.1.50");
}
#[test]
fn test_stable_id_avb_with_index() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::Avb,
ip_address: None,
mac_address: None,
hostname: None,
usb_address: None,
hardware_name: Some("MOTU AVB Main".to_string()),
device_index: Some(1),
};
assert_eq!(info.stable_id(), "avb:motu-avb-main:1");
}
#[test]
fn test_stable_id_custom_fallback() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::Custom("MyDAC".to_string()),
ip_address: None,
mac_address: None,
hostname: None,
usb_address: None,
hardware_name: None,
device_index: None,
};
assert_eq!(info.stable_id(), "unknown:Custom(\"MyDAC\")");
}
#[test]
fn test_stable_id_custom_with_ip() {
let info = DiscoveredDeviceInfo {
dac_type: DacType::Custom("MyDAC".to_string()),
ip_address: Some("10.0.0.1".parse().unwrap()),
mac_address: None,
hostname: None,
usb_address: None,
hardware_name: None,
device_index: None,
};
assert_eq!(info.stable_id(), "mydac:10.0.0.1");
}
use crate::backend::{BackendKind, DacBackend, FifoBackend};
use crate::types::{DacCapabilities, LaserPoint};
use crate::WriteOutcome;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone)]
struct MockConnectionInfo {
_device_id: u32,
}
struct MockBackend {
connected: bool,
}
impl DacBackend for MockBackend {
fn dac_type(&self) -> DacType {
DacType::Custom("MockDAC".into())
}
fn caps(&self) -> &DacCapabilities {
static CAPS: DacCapabilities = DacCapabilities {
pps_min: 1,
pps_max: 100_000,
max_points_per_chunk: 4096,
output_model: crate::types::OutputModel::NetworkFifo,
};
&CAPS
}
fn connect(&mut self) -> Result<()> {
self.connected = true;
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
fn stop(&mut self) -> Result<()> {
Ok(())
}
fn set_shutter(&mut self, _open: bool) -> Result<()> {
Ok(())
}
}
impl FifoBackend for MockBackend {
fn try_write_points(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
Ok(WriteOutcome::Written)
}
}
struct MockExternalDiscoverer {
scan_count: Arc<AtomicUsize>,
connect_called: Arc<AtomicBool>,
devices_to_return: Vec<(u32, Option<IpAddr>)>,
}
impl MockExternalDiscoverer {
fn new(devices: Vec<(u32, Option<IpAddr>)>) -> Self {
Self {
scan_count: Arc::new(AtomicUsize::new(0)),
connect_called: Arc::new(AtomicBool::new(false)),
devices_to_return: devices,
}
}
}
impl ExternalDiscoverer for MockExternalDiscoverer {
fn dac_type(&self) -> DacType {
DacType::Custom("MockDAC".into())
}
fn scan(&mut self) -> Vec<ExternalDevice> {
self.scan_count.fetch_add(1, Ordering::SeqCst);
self.devices_to_return
.iter()
.map(|(id, ip)| {
let mut device = ExternalDevice::new(MockConnectionInfo { _device_id: *id });
device.ip_address = *ip;
device.hardware_name = Some(format!("Mock Device {}", id));
device
})
.collect()
}
fn connect(&mut self, opaque_data: Box<dyn Any + Send>) -> Result<BackendKind> {
self.connect_called.store(true, Ordering::SeqCst);
let _info = opaque_data
.downcast::<MockConnectionInfo>()
.map_err(|_| Error::invalid_config("wrong device type"))?;
Ok(BackendKind::Fifo(Box::new(MockBackend {
connected: false,
})))
}
}
#[test]
fn test_external_discoverer_scan_is_called() {
let discoverer = MockExternalDiscoverer::new(vec![(1, Some("10.0.0.1".parse().unwrap()))]);
let scan_count = discoverer.scan_count.clone();
let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
discovery.register(Box::new(discoverer));
assert_eq!(scan_count.load(Ordering::SeqCst), 0);
let devices = discovery.scan();
assert_eq!(scan_count.load(Ordering::SeqCst), 1);
assert_eq!(devices.len(), 1);
}
#[test]
fn test_external_discoverer_device_info() {
let discoverer =
MockExternalDiscoverer::new(vec![(42, Some("192.168.1.100".parse().unwrap()))]);
let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
discovery.register(Box::new(discoverer));
let devices = discovery.scan();
assert_eq!(devices.len(), 1);
let device = &devices[0];
assert_eq!(device.dac_type(), DacType::Custom("MockDAC".into()));
assert_eq!(
device.info().ip_address,
Some("192.168.1.100".parse().unwrap())
);
assert_eq!(device.info().hardware_name, Some("Mock Device 42".into()));
}
#[test]
fn test_external_discoverer_connect() {
let discoverer = MockExternalDiscoverer::new(vec![(99, None)]);
let connect_called = discoverer.connect_called.clone();
let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
discovery.register(Box::new(discoverer));
let devices = discovery.scan();
assert_eq!(devices.len(), 1);
assert!(!connect_called.load(Ordering::SeqCst));
let backend = discovery.connect(devices.into_iter().next().unwrap());
assert!(backend.is_ok());
assert!(connect_called.load(Ordering::SeqCst));
let backend = backend.unwrap();
assert_eq!(backend.dac_type(), DacType::Custom("MockDAC".into()));
}
#[test]
fn test_external_discoverer_multiple_devices() {
let discoverer = MockExternalDiscoverer::new(vec![
(1, Some("10.0.0.1".parse().unwrap())),
(2, Some("10.0.0.2".parse().unwrap())),
(3, None),
]);
let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
discovery.register(Box::new(discoverer));
let devices = discovery.scan();
assert_eq!(devices.len(), 3);
for device in devices {
let backend = discovery.connect(device);
assert!(backend.is_ok());
}
}
#[test]
fn test_multiple_external_discoverers() {
let discoverer1 = MockExternalDiscoverer::new(vec![(1, None)]);
let discoverer2 = MockExternalDiscoverer::new(vec![(2, None), (3, None)]);
let mut discovery = DacDiscovery::new(EnabledDacTypes::none());
discovery.register(Box::new(discoverer1));
discovery.register(Box::new(discoverer2));
let devices = discovery.scan();
assert_eq!(devices.len(), 3);
}
}