use crate::backend::{DacBackend, FifoBackend, WriteOutcome};
use crate::buffer_estimate::{BufferEstimator, SoftwareDecayEstimator};
use crate::device::{DacCapabilities, DacType};
use crate::error::{Error, Result};
use crate::point::LaserPoint;
use crate::protocols::lasercube_usb::dac::Stream;
use crate::protocols::lasercube_usb::error::Error as UsbError;
use crate::protocols::lasercube_usb::protocol::Sample as LaserCubeUsbSample;
use crate::protocols::lasercube_usb::{rusb, DacController};
use std::time::Instant;
pub struct LaserCubeUsbBackend {
device: Option<rusb::Device<rusb::Context>>,
stream: Option<Stream<rusb::DeviceHandle<rusb::Context>>>,
caps: DacCapabilities,
point_buffer: Vec<LaserCubeUsbSample>,
estimator: SoftwareDecayEstimator,
}
impl LaserCubeUsbBackend {
pub fn new(device: rusb::Device<rusb::Context>) -> Self {
Self {
device: Some(device),
stream: None,
caps: super::default_capabilities(),
point_buffer: Vec::new(),
estimator: SoftwareDecayEstimator::new(),
}
}
pub fn from_stream(stream: Stream<rusb::DeviceHandle<rusb::Context>>) -> Self {
Self {
device: None,
stream: Some(stream),
caps: super::default_capabilities(),
point_buffer: Vec::new(),
estimator: SoftwareDecayEstimator::new(),
}
}
pub fn discover_devices() -> Result<Vec<rusb::Device<rusb::Context>>> {
let controller = DacController::new().map_err(Error::backend)?;
controller.list_devices().map_err(Error::backend)
}
fn handle_stream_error<R>(&mut self, err: UsbError) -> Result<R> {
match classify_stream_error(&err) {
StreamErrorClass::Fatal => {
self.stream = None;
Err(Error::disconnected(format!("USB device error: {err}")))
}
StreamErrorClass::Transient => Err(Error::backend(err)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StreamErrorClass {
Fatal,
Transient,
}
fn classify_stream_error(err: &UsbError) -> StreamErrorClass {
match err {
UsbError::Usb(rusb::Error::NoDevice | rusb::Error::Io | rusb::Error::Pipe) => {
StreamErrorClass::Fatal
}
UsbError::DeviceNotOpened => StreamErrorClass::Fatal,
_ => StreamErrorClass::Transient,
}
}
fn map_open_error(err: UsbError) -> Error {
if matches!(err, UsbError::Usb(rusb::Error::Access)) {
Error::usb_permission_denied("laser-cube usb")
} else {
Error::backend(err)
}
}
impl DacBackend for LaserCubeUsbBackend {
fn dac_type(&self) -> DacType {
DacType::LaserCubeUsb
}
fn caps(&self) -> &DacCapabilities {
&self.caps
}
fn connect(&mut self) -> Result<()> {
if self.stream.is_some() {
return Ok(());
}
let device = self
.device
.take()
.ok_or_else(|| Error::disconnected("No device available"))?;
let mut stream = Stream::open(device).map_err(map_open_error)?;
stream.enable_output().map_err(map_open_error)?;
let info = stream.info();
if info.max_dac_rate > 0 {
self.caps.pps_max = info.max_dac_rate;
}
self.stream = Some(stream);
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
if let Some(stream) = &mut self.stream {
let _ = stream.stop();
}
self.stream = None;
Ok(())
}
fn is_connected(&self) -> bool {
self.stream.is_some()
}
fn stop(&mut self) -> Result<()> {
let Some(stream) = &mut self.stream else {
return Ok(());
};
match stream.stop() {
Ok(()) => Ok(()),
Err(e) => self.handle_stream_error(e),
}
}
fn set_shutter(&mut self, open: bool) -> Result<()> {
let Some(stream) = &mut self.stream else {
return Ok(());
};
let result = if open {
stream.enable_output()
} else {
stream.disable_output()
};
match result {
Ok(()) => Ok(()),
Err(e) => self.handle_stream_error(e),
}
}
}
impl FifoBackend for LaserCubeUsbBackend {
fn try_write_points(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome> {
let stream = self
.stream
.as_mut()
.ok_or_else(|| Error::disconnected("Not connected"))?;
self.point_buffer.clear();
self.point_buffer
.extend(points.iter().map(LaserCubeUsbSample::from));
let n = self.point_buffer.len();
match stream.write_frame(&self.point_buffer, pps) {
Ok(effective_pps) => {
self.estimator
.record_send(Instant::now(), n as u64, effective_pps);
Ok(WriteOutcome::Written)
}
Err(e) => self.handle_stream_error(e),
}
}
fn estimator(&self) -> &dyn BufferEstimator {
&self.estimator
}
fn reset_device_buffer(&mut self) -> Result<()> {
let Some(stream) = &mut self.stream else {
return Ok(());
};
match stream.clear_ringbuffer() {
Ok(()) => {
self.estimator.reset(Instant::now());
Ok(())
}
Err(e) => self.handle_stream_error(e),
}
}
}
#[cfg(test)]
mod tests {
use super::{classify_stream_error, map_open_error, StreamErrorClass};
use crate::error::Error;
use crate::protocols::lasercube_usb::error::Error as UsbError;
use crate::protocols::lasercube_usb::rusb;
#[test]
fn classify_stream_error_covers_every_variant() {
use StreamErrorClass::{Fatal, Transient};
let cases = [
(UsbError::Usb(rusb::Error::NoDevice), Fatal),
(UsbError::Usb(rusb::Error::Io), Fatal),
(UsbError::Usb(rusb::Error::Pipe), Fatal),
(UsbError::DeviceNotOpened, Fatal),
(UsbError::Usb(rusb::Error::Timeout), Transient),
(UsbError::Usb(rusb::Error::Busy), Transient),
(UsbError::Usb(rusb::Error::Access), Transient),
(UsbError::Usb(rusb::Error::Other), Transient),
(UsbError::InvalidResponse, Transient),
];
for (err, expected) in cases {
assert_eq!(
classify_stream_error(&err),
expected,
"classification of {err:?}"
);
}
}
#[test]
fn map_open_error_access_is_permission_denied() {
let err = map_open_error(UsbError::Usb(rusb::Error::Access));
assert!(err.is_permission_denied());
assert!(!matches!(err, Error::Backend(_)));
}
#[test]
fn map_open_error_other_stays_backend() {
let err = map_open_error(UsbError::Usb(rusb::Error::Timeout));
assert!(!err.is_permission_denied());
assert!(matches!(err, Error::Backend(_)));
}
}