#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LaserPoint {
pub x: f32,
pub y: f32,
pub r: u16,
pub g: u16,
pub b: u16,
pub intensity: u16,
}
impl LaserPoint {
pub fn new(x: f32, y: f32, r: u16, g: u16, b: u16, intensity: u16) -> Self {
Self {
x,
y,
r,
g,
b,
intensity,
}
}
pub fn blanked(x: f32, y: f32) -> Self {
Self {
x,
y,
..Default::default()
}
}
#[inline]
pub(crate) fn coord_to_u12_inverted(v: f32) -> u16 {
((1.0 - (v + 1.0) / 2.0).clamp(0.0, 1.0) * 4095.0).round() as u16
}
#[inline]
pub(crate) fn coord_to_u12(v: f32) -> u16 {
(((v.clamp(-1.0, 1.0) + 1.0) / 2.0) * 4095.0).round() as u16
}
#[inline]
pub(crate) fn coord_to_i16_inverted(v: f32) -> i16 {
(v.clamp(-1.0, 1.0) * -32767.0).round() as i16
}
#[inline]
pub(crate) fn color_to_u8(v: u16) -> u8 {
(v >> 8) as u8
}
#[inline]
pub(crate) fn color_to_u12(v: u16) -> u16 {
v >> 4
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DacType {
Helios,
EtherDream,
Idn,
LasercubeWifi,
LasercubeUsb,
#[cfg(feature = "oscilloscope")]
Oscilloscope,
Avb,
Custom(String),
}
impl DacType {
#[cfg(not(feature = "oscilloscope"))]
pub fn all() -> &'static [DacType] {
&[
DacType::Helios,
DacType::EtherDream,
DacType::Idn,
DacType::LasercubeWifi,
DacType::LasercubeUsb,
DacType::Avb,
]
}
#[cfg(feature = "oscilloscope")]
pub fn all() -> &'static [DacType] {
&[
DacType::Helios,
DacType::EtherDream,
DacType::Idn,
DacType::LasercubeWifi,
DacType::LasercubeUsb,
DacType::Avb,
DacType::Oscilloscope,
]
}
pub fn display_name(&self) -> &str {
match self {
DacType::Helios => "Helios",
DacType::EtherDream => "Ether Dream",
DacType::Idn => "IDN",
DacType::LasercubeWifi => "LaserCube WiFi",
DacType::LasercubeUsb => "LaserCube USB (Laserdock)",
#[cfg(feature = "oscilloscope")]
DacType::Oscilloscope => "Oscilloscope",
DacType::Avb => "AVB Audio Device",
DacType::Custom(name) => name,
}
}
pub fn description(&self) -> &'static str {
match self {
DacType::Helios => "USB laser DAC",
DacType::EtherDream => "Network laser DAC",
DacType::Idn => "ILDA Digital Network laser DAC",
DacType::LasercubeWifi => "WiFi laser DAC",
DacType::LasercubeUsb => "USB laser DAC",
#[cfg(feature = "oscilloscope")]
DacType::Oscilloscope => "Oscilloscope XY output via stereo audio",
DacType::Avb => "AVB audio network output",
DacType::Custom(_) => "Custom DAC",
}
}
}
impl fmt::Display for DacType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EnabledDacTypes {
types: HashSet<DacType>,
}
impl EnabledDacTypes {
pub fn all() -> Self {
Self {
types: DacType::all().iter().cloned().collect(),
}
}
pub fn none() -> Self {
Self {
types: HashSet::new(),
}
}
pub fn is_enabled(&self, dac_type: DacType) -> bool {
self.types.contains(&dac_type)
}
pub fn enable(&mut self, dac_type: DacType) -> &mut Self {
self.types.insert(dac_type);
self
}
pub fn disable(&mut self, dac_type: DacType) -> &mut Self {
self.types.remove(&dac_type);
self
}
pub fn iter(&self) -> impl Iterator<Item = DacType> + '_ {
self.types.iter().cloned()
}
pub fn is_empty(&self) -> bool {
self.types.is_empty()
}
}
impl Default for EnabledDacTypes {
fn default() -> Self {
Self::all()
}
}
impl std::iter::FromIterator<DacType> for EnabledDacTypes {
fn from_iter<I: IntoIterator<Item = DacType>>(iter: I) -> Self {
Self {
types: iter.into_iter().collect(),
}
}
}
impl Extend<DacType> for EnabledDacTypes {
fn extend<I: IntoIterator<Item = DacType>>(&mut self, iter: I) {
self.types.extend(iter);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DacDevice {
pub name: String,
pub dac_type: DacType,
}
impl DacDevice {
pub fn new(name: String, dac_type: DacType) -> Self {
Self { name, dac_type }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DacConnectionState {
Connected { name: String },
Stopped { name: String },
Lost { name: String, error: Option<String> },
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DacCapabilities {
pub pps_min: u32,
pub pps_max: u32,
pub max_points_per_chunk: usize,
pub output_model: OutputModel,
}
impl Default for DacCapabilities {
fn default() -> Self {
Self {
pps_min: 1,
pps_max: 100_000,
max_points_per_chunk: 4096,
output_model: OutputModel::NetworkFifo,
}
}
}
pub fn caps_for_dac_type(dac_type: &DacType) -> DacCapabilities {
match dac_type {
#[cfg(feature = "helios")]
DacType::Helios => crate::protocols::helios::default_capabilities(),
#[cfg(not(feature = "helios"))]
DacType::Helios => DacCapabilities::default(),
#[cfg(feature = "ether-dream")]
DacType::EtherDream => crate::protocols::ether_dream::default_capabilities(),
#[cfg(not(feature = "ether-dream"))]
DacType::EtherDream => DacCapabilities::default(),
#[cfg(feature = "idn")]
DacType::Idn => crate::protocols::idn::default_capabilities(),
#[cfg(not(feature = "idn"))]
DacType::Idn => DacCapabilities::default(),
#[cfg(feature = "lasercube-wifi")]
DacType::LasercubeWifi => crate::protocols::lasercube_wifi::default_capabilities(),
#[cfg(not(feature = "lasercube-wifi"))]
DacType::LasercubeWifi => DacCapabilities::default(),
#[cfg(feature = "lasercube-usb")]
DacType::LasercubeUsb => crate::protocols::lasercube_usb::default_capabilities(),
#[cfg(not(feature = "lasercube-usb"))]
DacType::LasercubeUsb => DacCapabilities::default(),
#[cfg(feature = "oscilloscope")]
DacType::Oscilloscope => DacCapabilities::default(),
#[cfg(feature = "avb")]
DacType::Avb => crate::protocols::avb::default_capabilities(),
#[cfg(not(feature = "avb"))]
DacType::Avb => DacCapabilities::default(),
DacType::Custom(_) => DacCapabilities::default(),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum OutputModel {
UsbFrameSwap,
NetworkFifo,
UdpTimed,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StreamInstant(pub u64);
impl StreamInstant {
pub fn new(points: u64) -> Self {
Self(points)
}
pub fn points(&self) -> u64 {
self.0
}
pub fn as_seconds(&self, pps: u32) -> f64 {
self.0 as f64 / pps as f64
}
#[inline]
pub fn as_secs_f64(&self, pps: u32) -> f64 {
self.as_seconds(pps)
}
pub fn from_seconds(seconds: f64, pps: u32) -> Self {
Self((seconds * pps as f64) as u64)
}
pub fn add_points(&self, points: u64) -> Self {
Self(self.0.saturating_add(points))
}
pub fn sub_points(&self, points: u64) -> Self {
Self(self.0.saturating_sub(points))
}
}
impl std::ops::Add<u64> for StreamInstant {
type Output = Self;
fn add(self, rhs: u64) -> Self::Output {
self.add_points(rhs)
}
}
impl std::ops::Sub<u64> for StreamInstant {
type Output = Self;
fn sub(self, rhs: u64) -> Self::Output {
self.sub_points(rhs)
}
}
impl std::ops::AddAssign<u64> for StreamInstant {
fn add_assign(&mut self, rhs: u64) {
self.0 = self.0.saturating_add(rhs);
}
}
impl std::ops::SubAssign<u64> for StreamInstant {
fn sub_assign(&mut self, rhs: u64) {
self.0 = self.0.saturating_sub(rhs);
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StreamConfig {
pub pps: u32,
#[cfg_attr(feature = "serde", serde(with = "duration_millis"))]
pub target_buffer: std::time::Duration,
#[cfg_attr(feature = "serde", serde(with = "duration_millis"))]
pub min_buffer: std::time::Duration,
pub idle_policy: IdlePolicy,
#[cfg_attr(feature = "serde", serde(with = "duration_millis"))]
pub drain_timeout: std::time::Duration,
#[cfg_attr(feature = "serde", serde(with = "duration_micros"))]
pub color_delay: std::time::Duration,
#[cfg_attr(feature = "serde", serde(with = "duration_micros"))]
pub startup_blank: std::time::Duration,
#[cfg_attr(feature = "serde", serde(skip))]
pub reconnect: Option<ReconnectConfig>,
}
#[cfg(feature = "serde")]
macro_rules! duration_serde_module {
($mod_name:ident, $as_unit:ident, $from_unit:ident) => {
mod $mod_name {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let value = duration.$as_unit().min(u64::MAX as u128) as u64;
value.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let value = u64::deserialize(deserializer)?;
Ok(Duration::$from_unit(value))
}
}
};
}
#[cfg(feature = "serde")]
duration_serde_module!(duration_millis, as_millis, from_millis);
#[cfg(feature = "serde")]
duration_serde_module!(duration_micros, as_micros, from_micros);
impl Default for StreamConfig {
fn default() -> Self {
Self {
pps: 30_000,
target_buffer: Self::DEFAULT_TARGET_BUFFER,
min_buffer: Self::DEFAULT_MIN_BUFFER,
idle_policy: IdlePolicy::default(),
drain_timeout: std::time::Duration::from_secs(1),
color_delay: std::time::Duration::ZERO,
startup_blank: std::time::Duration::from_millis(1),
reconnect: None,
}
}
}
impl StreamConfig {
pub const DEFAULT_TARGET_BUFFER: std::time::Duration = std::time::Duration::from_millis(20);
pub const DEFAULT_MIN_BUFFER: std::time::Duration = std::time::Duration::from_millis(8);
pub const NETWORK_DEFAULT_TARGET_BUFFER: std::time::Duration =
std::time::Duration::from_millis(50);
pub const NETWORK_DEFAULT_MIN_BUFFER: std::time::Duration =
std::time::Duration::from_millis(20);
pub fn new(pps: u32) -> Self {
Self {
pps,
..Default::default()
}
}
pub fn with_target_buffer(mut self, duration: std::time::Duration) -> Self {
self.target_buffer = duration;
self
}
pub fn with_min_buffer(mut self, duration: std::time::Duration) -> Self {
self.min_buffer = duration;
self
}
pub fn with_idle_policy(mut self, policy: IdlePolicy) -> Self {
self.idle_policy = policy;
self
}
#[deprecated(since = "0.8.0", note = "renamed to with_idle_policy")]
pub fn with_underrun(self, policy: IdlePolicy) -> Self {
self.with_idle_policy(policy)
}
pub fn with_drain_timeout(mut self, timeout: std::time::Duration) -> Self {
self.drain_timeout = timeout;
self
}
pub fn with_color_delay(mut self, delay: std::time::Duration) -> Self {
self.color_delay = delay;
self
}
pub fn with_startup_blank(mut self, duration: std::time::Duration) -> Self {
self.startup_blank = duration;
self
}
pub fn with_reconnect(mut self, config: ReconnectConfig) -> Self {
self.reconnect = Some(config);
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Default)]
pub enum IdlePolicy {
RepeatLast,
#[default]
Blank,
Park { x: f32, y: f32 },
Stop,
}
#[deprecated(since = "0.8.0", note = "renamed to IdlePolicy")]
pub type UnderrunPolicy = IdlePolicy;
#[derive(Clone, Debug)]
pub struct ChunkRequest {
pub start: StreamInstant,
pub pps: u32,
pub min_points: usize,
pub target_points: usize,
pub buffered_points: u64,
pub buffered: std::time::Duration,
pub device_queued_points: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChunkResult {
Filled(usize),
Starved,
End,
}
#[derive(Clone, Debug)]
pub struct StreamStatus {
pub connected: bool,
pub scheduled_ahead_points: u64,
pub device_queued_points: Option<u64>,
pub stats: Option<StreamStats>,
}
#[derive(Clone, Debug, Default)]
pub struct StreamStats {
pub underrun_count: u64,
pub late_chunk_count: u64,
pub reconnect_count: u64,
pub chunks_written: u64,
pub points_written: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RunExit {
Stopped,
ProducerEnded,
Disconnected,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DacInfo {
pub id: String,
pub name: String,
pub kind: DacType,
pub caps: DacCapabilities,
}
impl DacInfo {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
kind: DacType,
caps: DacCapabilities,
) -> Self {
Self {
id: id.into(),
name: name.into(),
kind,
caps,
}
}
}
type DisconnectCb = Box<dyn FnMut(&crate::Error) + Send + 'static>;
type ReconnectCb = Box<dyn FnMut(&DacInfo) + Send + 'static>;
pub struct ReconnectConfig {
pub(crate) max_retries: Option<u32>,
pub(crate) backoff: std::time::Duration,
pub(crate) on_disconnect: Option<DisconnectCb>,
pub(crate) on_reconnect: Option<ReconnectCb>,
}
impl ReconnectConfig {
pub fn new() -> Self {
Self {
max_retries: None,
backoff: std::time::Duration::from_secs(1),
on_disconnect: None,
on_reconnect: None,
}
}
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = Some(max_retries);
self
}
pub fn backoff(mut self, backoff: std::time::Duration) -> Self {
self.backoff = backoff;
self
}
pub fn on_disconnect<F>(mut self, f: F) -> Self
where
F: FnMut(&crate::Error) + Send + 'static,
{
self.on_disconnect = Some(Box::new(f));
self
}
pub fn on_reconnect<F>(mut self, f: F) -> Self
where
F: FnMut(&DacInfo) + Send + 'static,
{
self.on_reconnect = Some(Box::new(f));
self
}
}
impl Default for ReconnectConfig {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for ReconnectConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReconnectConfig")
.field("max_retries", &self.max_retries)
.field("backoff", &self.backoff)
.field("on_disconnect", &self.on_disconnect.as_ref().map(|_| ".."))
.field("on_reconnect", &self.on_reconnect.as_ref().map(|_| ".."))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_laser_point_blanked_sets_all_colors_to_zero() {
let point = LaserPoint::blanked(0.25, 0.75);
assert_eq!(point.x, 0.25);
assert_eq!(point.y, 0.75);
assert_eq!(point.r, 0);
assert_eq!(point.g, 0);
assert_eq!(point.b, 0);
assert_eq!(point.intensity, 0);
}
#[test]
fn test_dac_type_all_returns_all_builtin_types() {
let all_types = DacType::all();
#[cfg(not(feature = "oscilloscope"))]
assert_eq!(all_types.len(), 6);
#[cfg(feature = "oscilloscope")]
assert_eq!(all_types.len(), 7);
assert!(all_types.contains(&DacType::Helios));
assert!(all_types.contains(&DacType::EtherDream));
assert!(all_types.contains(&DacType::Idn));
assert!(all_types.contains(&DacType::LasercubeWifi));
assert!(all_types.contains(&DacType::LasercubeUsb));
assert!(all_types.contains(&DacType::Avb));
#[cfg(feature = "oscilloscope")]
assert!(all_types.contains(&DacType::Oscilloscope));
}
#[test]
fn test_dac_type_display_uses_display_name() {
assert_eq!(
format!("{}", DacType::Helios),
DacType::Helios.display_name()
);
assert_eq!(
format!("{}", DacType::EtherDream),
DacType::EtherDream.display_name()
);
}
#[test]
fn test_dac_type_can_be_used_in_hashset() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(DacType::Helios);
set.insert(DacType::Helios);
assert_eq!(set.len(), 1);
}
#[test]
fn test_enabled_dac_types_all_enables_everything() {
let enabled = EnabledDacTypes::all();
for dac_type in DacType::all() {
assert!(
enabled.is_enabled(dac_type.clone()),
"{:?} should be enabled",
dac_type
);
}
assert!(!enabled.is_empty());
}
#[test]
fn test_enabled_dac_types_none_disables_everything() {
let enabled = EnabledDacTypes::none();
for dac_type in DacType::all() {
assert!(
!enabled.is_enabled(dac_type.clone()),
"{:?} should be disabled",
dac_type
);
}
assert!(enabled.is_empty());
}
#[test]
fn test_enabled_dac_types_enable_disable_toggles_correctly() {
let mut enabled = EnabledDacTypes::none();
enabled.enable(DacType::Helios);
assert!(enabled.is_enabled(DacType::Helios));
assert!(!enabled.is_enabled(DacType::EtherDream));
enabled.enable(DacType::EtherDream);
assert!(enabled.is_enabled(DacType::Helios));
assert!(enabled.is_enabled(DacType::EtherDream));
enabled.disable(DacType::Helios);
assert!(!enabled.is_enabled(DacType::Helios));
assert!(enabled.is_enabled(DacType::EtherDream));
}
#[test]
fn test_enabled_dac_types_iter_only_returns_enabled() {
let mut enabled = EnabledDacTypes::none();
enabled.enable(DacType::Helios);
enabled.enable(DacType::Idn);
let types: Vec<DacType> = enabled.iter().collect();
assert_eq!(types.len(), 2);
assert!(types.contains(&DacType::Helios));
assert!(types.contains(&DacType::Idn));
assert!(!types.contains(&DacType::EtherDream));
}
#[test]
fn test_enabled_dac_types_default_enables_all() {
let enabled = EnabledDacTypes::default();
for dac_type in DacType::all() {
assert!(enabled.is_enabled(dac_type.clone()));
}
}
#[test]
fn test_enabled_dac_types_idempotent_operations() {
let mut enabled = EnabledDacTypes::none();
enabled.enable(DacType::Helios);
enabled.enable(DacType::Helios);
assert!(enabled.is_enabled(DacType::Helios));
enabled.disable(DacType::Helios);
enabled.disable(DacType::Helios);
assert!(!enabled.is_enabled(DacType::Helios));
}
#[test]
fn test_enabled_dac_types_chaining() {
let mut enabled = EnabledDacTypes::none();
enabled
.enable(DacType::Helios)
.enable(DacType::EtherDream)
.disable(DacType::Helios);
assert!(!enabled.is_enabled(DacType::Helios));
assert!(enabled.is_enabled(DacType::EtherDream));
}
#[test]
fn test_dac_connection_state_equality() {
let s1 = DacConnectionState::Connected {
name: "DAC1".to_string(),
};
let s2 = DacConnectionState::Connected {
name: "DAC1".to_string(),
};
let s3 = DacConnectionState::Connected {
name: "DAC2".to_string(),
};
let s4 = DacConnectionState::Lost {
name: "DAC1".to_string(),
error: None,
};
assert_eq!(s1, s2);
assert_ne!(s1, s3); assert_ne!(s1, s4); }
#[cfg(feature = "serde")]
#[test]
fn test_stream_config_serde_roundtrip() {
use std::time::Duration;
let config = StreamConfig {
pps: 45000,
target_buffer: Duration::from_millis(50),
min_buffer: Duration::from_millis(12),
idle_policy: IdlePolicy::Park { x: 0.5, y: -0.3 },
drain_timeout: Duration::from_secs(2),
color_delay: Duration::from_micros(150),
startup_blank: Duration::from_micros(800),
reconnect: None,
};
let json = serde_json::to_string(&config).expect("serialize to JSON");
let restored: StreamConfig = serde_json::from_str(&json).expect("deserialize from JSON");
assert_eq!(restored.pps, config.pps);
assert_eq!(restored.target_buffer, config.target_buffer);
assert_eq!(restored.min_buffer, config.min_buffer);
assert_eq!(restored.drain_timeout, config.drain_timeout);
assert_eq!(restored.color_delay, config.color_delay);
assert_eq!(restored.startup_blank, config.startup_blank);
match restored.idle_policy {
IdlePolicy::Park { x, y } => {
assert!((x - 0.5).abs() < f32::EPSILON);
assert!((y - (-0.3)).abs() < f32::EPSILON);
}
_ => panic!("Expected Park policy"),
}
}
#[cfg(feature = "serde")]
#[test]
fn test_duration_millis_roundtrip_consistency() {
use std::time::Duration;
let test_durations = [
Duration::from_millis(0),
Duration::from_millis(1),
Duration::from_millis(10),
Duration::from_millis(100),
Duration::from_millis(1000),
Duration::from_millis(u64::MAX / 1000), ];
for &duration in &test_durations {
let config = StreamConfig {
target_buffer: duration,
..StreamConfig::default()
};
let json = serde_json::to_string(&config).expect("serialize");
let restored: StreamConfig = serde_json::from_str(&json).expect("deserialize");
assert_eq!(
restored.target_buffer, duration,
"Duration {:?} did not round-trip correctly",
duration
);
}
}
}