use crate::buffer_estimate::BufferEstimator;
use crate::device::{DacCapabilities, DacType};
use crate::point::LaserPoint;
pub use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteOutcome {
Written,
WouldBlock,
}
pub trait DacBackend: Send + 'static {
fn dac_type(&self) -> DacType;
fn caps(&self) -> &DacCapabilities;
fn connect(&mut self) -> Result<()>;
fn disconnect(&mut self) -> Result<()>;
fn is_connected(&self) -> bool;
fn stop(&mut self) -> Result<()>;
fn set_shutter(&mut self, open: bool) -> Result<()>;
}
pub trait FifoBackend: DacBackend {
fn try_write_points(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome>;
fn estimator(&self) -> &dyn BufferEstimator;
fn target_buffer_ceiling(&self) -> Option<usize> {
None
}
fn reset_device_buffer(&mut self) -> Result<()> {
Ok(())
}
}
pub trait FrameSwapBackend: DacBackend {
fn frame_capacity(&self) -> usize;
fn is_ready_for_frame(&mut self) -> bool;
fn write_frame(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome>;
fn write_frame_after_ready(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome> {
self.write_frame(pps, points)
}
}
pub struct BackendKind {
inner: BackendVariant,
}
enum BackendVariant {
Fifo(Box<dyn FifoBackend>),
FrameSwap(Box<dyn FrameSwapBackend>),
}
impl BackendKind {
pub fn fifo(backend: Box<dyn FifoBackend>) -> Result<Self> {
let backend = Self {
inner: BackendVariant::Fifo(backend),
};
backend.validate_output_model()?;
Ok(backend)
}
pub fn frame_swap(backend: Box<dyn FrameSwapBackend>) -> Result<Self> {
let backend = Self {
inner: BackendVariant::FrameSwap(backend),
};
backend.validate_output_model()?;
Ok(backend)
}
#[cfg(test)]
#[allow(non_snake_case)]
pub(crate) fn Fifo(backend: Box<dyn FifoBackend>) -> Self {
Self::fifo(backend).expect("test FIFO backend must advertise a FIFO output model")
}
#[cfg(test)]
#[allow(non_snake_case)]
pub(crate) fn FrameSwap(backend: Box<dyn FrameSwapBackend>) -> Self {
Self::frame_swap(backend).expect("test frame-swap backend must advertise UsbFrameSwap")
}
pub fn validate_output_model(&self) -> Result<()> {
use crate::device::OutputModel;
let caps_frame_swap = matches!(self.caps().output_model, OutputModel::UsbFrameSwap);
if caps_frame_swap == self.is_frame_swap() {
return Ok(());
}
Err(Error::invalid_config(format!(
"backend kind {} disagrees with output model {:?}",
if self.is_frame_swap() {
"FrameSwap"
} else {
"Fifo"
},
self.caps().output_model
)))
}
pub fn dac_type(&self) -> DacType {
match &self.inner {
BackendVariant::Fifo(b) => b.dac_type(),
BackendVariant::FrameSwap(b) => b.dac_type(),
}
}
pub fn caps(&self) -> &DacCapabilities {
match &self.inner {
BackendVariant::Fifo(b) => b.caps(),
BackendVariant::FrameSwap(b) => b.caps(),
}
}
pub fn connect(&mut self) -> Result<()> {
self.validate_output_model()?;
match &mut self.inner {
BackendVariant::Fifo(b) => b.connect()?,
BackendVariant::FrameSwap(b) => b.connect()?,
}
if let Err(invariant_err) = self.validate_output_model() {
let cleanup = match &mut self.inner {
BackendVariant::Fifo(b) => b.disconnect(),
BackendVariant::FrameSwap(b) => b.disconnect(),
};
return match cleanup {
Ok(()) => Err(invariant_err),
Err(cleanup_err) => Err(Error::invalid_config(format!(
"{invariant_err}; disconnect after invariant violation also failed: {cleanup_err}"
))),
};
}
Ok(())
}
pub fn disconnect(&mut self) -> Result<()> {
match &mut self.inner {
BackendVariant::Fifo(b) => b.disconnect(),
BackendVariant::FrameSwap(b) => b.disconnect(),
}
}
pub fn is_connected(&self) -> bool {
match &self.inner {
BackendVariant::Fifo(b) => b.is_connected(),
BackendVariant::FrameSwap(b) => b.is_connected(),
}
}
pub fn stop(&mut self) -> Result<()> {
match &mut self.inner {
BackendVariant::Fifo(b) => b.stop(),
BackendVariant::FrameSwap(b) => b.stop(),
}
}
pub fn set_shutter(&mut self, open: bool) -> Result<()> {
match &mut self.inner {
BackendVariant::Fifo(b) => b.set_shutter(open),
BackendVariant::FrameSwap(b) => b.set_shutter(open),
}
}
pub(crate) fn close_and_disconnect(&mut self) {
let _ = self.set_shutter(false);
let _ = self.disconnect();
}
pub fn try_write(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome> {
match &mut self.inner {
BackendVariant::Fifo(b) => b.try_write_points(pps, points),
BackendVariant::FrameSwap(b) => b.write_frame(pps, points),
}
}
pub(crate) fn write_frame_after_ready(
&mut self,
pps: u32,
points: &[LaserPoint],
) -> Result<WriteOutcome> {
match &mut self.inner {
BackendVariant::FrameSwap(b) => b.write_frame_after_ready(pps, points),
BackendVariant::Fifo(_) => Err(Error::invalid_config(
"write_frame_after_ready called on a FIFO backend",
)),
}
}
pub fn estimator(&self) -> Option<&dyn BufferEstimator> {
match &self.inner {
BackendVariant::Fifo(b) => Some(b.estimator()),
BackendVariant::FrameSwap(_) => None,
}
}
pub fn target_buffer_ceiling(&self) -> Option<usize> {
match &self.inner {
BackendVariant::Fifo(b) => b.target_buffer_ceiling(),
BackendVariant::FrameSwap(_) => None,
}
}
pub fn reset_device_buffer(&mut self) -> Result<()> {
match &mut self.inner {
BackendVariant::Fifo(b) => b.reset_device_buffer(),
BackendVariant::FrameSwap(_) => Ok(()),
}
}
pub fn is_frame_swap(&self) -> bool {
matches!(self.inner, BackendVariant::FrameSwap(_))
}
pub(crate) fn is_ready_for_frame(&mut self) -> Result<bool> {
match &mut self.inner {
BackendVariant::FrameSwap(b) => Ok(b.is_ready_for_frame()),
BackendVariant::Fifo(_) => Err(Error::invalid_config(
"frame readiness queried on a FIFO backend",
)),
}
}
pub fn frame_capacity(&self) -> Option<usize> {
match &self.inner {
BackendVariant::Fifo(_) => None,
BackendVariant::FrameSwap(b) => Some(b.frame_capacity()),
}
}
}
#[cfg(feature = "helios")]
pub use crate::protocols::helios::HeliosBackend;
#[cfg(feature = "ether-dream")]
pub use crate::protocols::ether_dream::EtherDreamBackend;
#[cfg(feature = "idn")]
pub use crate::protocols::idn::IdnBackend;
#[cfg(feature = "lasercube-network")]
pub use crate::protocols::lasercube_network::LaserCubeNetworkBackend;
#[cfg(feature = "lasercube-usb")]
pub use crate::protocols::lasercube_usb::LaserCubeUsbBackend;
#[cfg(feature = "oscilloscope")]
pub use crate::protocols::oscilloscope::OscilloscopeBackend;
#[cfg(feature = "avb")]
pub use crate::protocols::avb::AvbBackend;
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{DacCapabilities, DacType, OutputModel};
struct StubFifo {
caps: DacCapabilities,
estimator: crate::buffer_estimate::SoftwareDecayEstimator,
}
impl DacBackend for StubFifo {
fn dac_type(&self) -> DacType {
DacType::Custom("stub".into())
}
fn caps(&self) -> &DacCapabilities {
&self.caps
}
fn connect(&mut self) -> Result<()> {
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
Ok(())
}
fn is_connected(&self) -> bool {
true
}
fn stop(&mut self) -> Result<()> {
Ok(())
}
fn set_shutter(&mut self, _open: bool) -> Result<()> {
Ok(())
}
}
impl FifoBackend for StubFifo {
fn try_write_points(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
Ok(WriteOutcome::Written)
}
fn estimator(&self) -> &dyn BufferEstimator {
&self.estimator
}
}
struct StubFrameSwap {
caps: DacCapabilities,
}
impl DacBackend for StubFrameSwap {
fn dac_type(&self) -> DacType {
DacType::Custom("stub-fs".into())
}
fn caps(&self) -> &DacCapabilities {
&self.caps
}
fn connect(&mut self) -> Result<()> {
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
Ok(())
}
fn is_connected(&self) -> bool {
true
}
fn stop(&mut self) -> Result<()> {
Ok(())
}
fn set_shutter(&mut self, _open: bool) -> Result<()> {
Ok(())
}
}
impl FrameSwapBackend for StubFrameSwap {
fn frame_capacity(&self) -> usize {
4096
}
fn is_ready_for_frame(&mut self) -> bool {
true
}
fn write_frame(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
Ok(WriteOutcome::Written)
}
}
fn stub_fifo(model: OutputModel) -> Box<dyn FifoBackend> {
Box::new(StubFifo {
caps: DacCapabilities {
output_model: model,
..DacCapabilities::default()
},
estimator: crate::buffer_estimate::SoftwareDecayEstimator::new(),
})
}
fn stub_frame_swap(model: OutputModel) -> Box<dyn FrameSwapBackend> {
Box::new(StubFrameSwap {
caps: DacCapabilities {
output_model: model,
..DacCapabilities::default()
},
})
}
#[test]
fn runtime_kind_is_derived_from_private_variant() {
for model in [
OutputModel::NetworkFifo,
OutputModel::UdpTimed,
OutputModel::BlockingFifo,
] {
let backend = BackendKind::fifo(stub_fifo(model)).unwrap();
assert!(!backend.is_frame_swap());
}
let backend = BackendKind::frame_swap(stub_frame_swap(OutputModel::UsbFrameSwap)).unwrap();
assert!(backend.is_frame_swap());
}
struct MutatingFifo {
caps: DacCapabilities,
connected: bool,
disconnected: std::sync::Arc<std::sync::atomic::AtomicBool>,
fail_disconnect: bool,
estimator: crate::buffer_estimate::SoftwareDecayEstimator,
}
impl DacBackend for MutatingFifo {
fn dac_type(&self) -> DacType {
DacType::Custom("mutating-fifo".into())
}
fn caps(&self) -> &DacCapabilities {
&self.caps
}
fn connect(&mut self) -> Result<()> {
self.connected = true;
self.caps.output_model = OutputModel::UsbFrameSwap;
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
self.connected = false;
self.disconnected
.store(true, std::sync::atomic::Ordering::SeqCst);
if self.fail_disconnect {
Err(Error::backend(std::io::Error::other("cleanup failed")))
} else {
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 MutatingFifo {
fn try_write_points(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
Ok(WriteOutcome::Written)
}
fn estimator(&self) -> &dyn BufferEstimator {
&self.estimator
}
}
#[test]
fn constructors_reject_kind_output_model_disagreement() {
let err = match BackendKind::fifo(stub_fifo(OutputModel::UsbFrameSwap)) {
Ok(_) => panic!("FIFO wrapper must reject UsbFrameSwap metadata"),
Err(err) => err,
};
assert!(err.to_string().contains("Fifo"));
assert!(err.to_string().contains("UsbFrameSwap"));
let err = match BackendKind::frame_swap(stub_frame_swap(OutputModel::NetworkFifo)) {
Ok(_) => panic!("frame-swap wrapper must reject FIFO metadata"),
Err(err) => err,
};
assert!(err.to_string().contains("FrameSwap"));
assert!(err.to_string().contains("NetworkFifo"));
}
#[test]
fn connect_revalidates_output_model_and_disconnects_on_disagreement() {
use std::sync::atomic::Ordering;
let disconnected = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut backend = BackendKind::fifo(Box::new(MutatingFifo {
caps: DacCapabilities {
output_model: OutputModel::NetworkFifo,
..DacCapabilities::default()
},
connected: false,
disconnected: disconnected.clone(),
fail_disconnect: false,
estimator: crate::buffer_estimate::SoftwareDecayEstimator::new(),
}))
.unwrap();
let err = backend.connect().unwrap_err();
assert!(err.to_string().contains("disagrees"));
assert!(!backend.is_connected());
assert!(disconnected.load(Ordering::SeqCst));
}
#[test]
fn connect_reports_cleanup_failure_after_invariant_violation() {
let mut backend = BackendKind::fifo(Box::new(MutatingFifo {
caps: DacCapabilities {
output_model: OutputModel::NetworkFifo,
..DacCapabilities::default()
},
connected: false,
disconnected: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
fail_disconnect: true,
estimator: crate::buffer_estimate::SoftwareDecayEstimator::new(),
}))
.unwrap();
let err = backend.connect().unwrap_err();
assert!(err.to_string().contains("invariant violation"));
assert!(err.to_string().contains("cleanup failed"));
}
}