use std::time::Duration;
use super::{DeviceStatus, Frame};
use rusb::{Context, Device, UsbContext};
use thiserror::Error;
type Result<T> = std::result::Result<T, HeliosDacError>;
const SDK_VERSION: u8 = 6;
const HELIOS_VID: u16 = 0x1209;
const HELIOS_PID: u16 = 0xE500;
const ENDPOINT_BULK_OUT: u8 = 0x02;
const ENDPOINT_INT_OUT: u8 = 0x06;
const ENDPOINT_INT_IN: u8 = 0x83;
const CONTROL_STOP: u8 = 0x01;
const CONTROL_SET_SHUTTER: u8 = 0x02;
const CONTROL_GET_STATUS: u8 = 0x03;
const CONTROL_GET_FIRMWARE_VERSION: u8 = 0x04;
const CONTROL_GET_NAME: u8 = 0x05;
const CONTROL_SEND_SDK_VERSION: u8 = 0x07;
pub struct HeliosDacController {
context: rusb::Context,
}
impl HeliosDacController {
pub fn new() -> Result<Self> {
Ok(HeliosDacController {
context: rusb::Context::new()?,
})
}
pub fn list_devices(&self) -> Result<Vec<HeliosDac>> {
let dacs = self
.context
.devices()?
.iter()
.filter_map(|device| {
let descriptor = device.device_descriptor().ok()?;
(descriptor.vendor_id() == HELIOS_VID && descriptor.product_id() == HELIOS_PID)
.then(|| device.into())
})
.collect();
Ok(dacs)
}
}
#[cfg(test)]
pub(crate) struct MockUsbState {
pub connected: bool,
pub disconnect_error: fn() -> rusb::Error,
}
#[cfg(test)]
impl MockUsbState {
pub fn new() -> Self {
Self {
connected: true,
disconnect_error: || rusb::Error::NoDevice,
}
}
pub fn with_disconnect_error(mut self, f: fn() -> rusb::Error) -> Self {
self.disconnect_error = f;
self
}
}
pub enum HeliosDac {
Idle(rusb::Device<rusb::Context>),
Open {
device: rusb::Device<rusb::Context>,
handle: rusb::DeviceHandle<rusb::Context>,
},
#[cfg(test)]
#[allow(private_interfaces)]
MockOpen(std::sync::Arc<std::sync::Mutex<MockUsbState>>),
}
impl HeliosDac {
pub fn open(self) -> Result<Self> {
match self {
HeliosDac::Idle(device) => {
let handle = device.open()?;
handle.claim_interface(0)?;
handle.set_alternate_setting(0, 1)?;
let device = HeliosDac::Open { device, handle };
let _ = device.firmware_version()?;
device.send_sdk_version()?;
Ok(device)
}
open => Ok(open),
}
}
pub fn usb_location(&self) -> String {
match self {
HeliosDac::Idle(device) | HeliosDac::Open { device, .. } => format_usb_location(device),
#[cfg(test)]
HeliosDac::MockOpen(_) => "mock".into(),
}
}
fn handle(&self) -> Result<&rusb::DeviceHandle<rusb::Context>> {
match self {
HeliosDac::Open { handle, .. } => Ok(handle),
#[cfg(test)]
HeliosDac::MockOpen(_) => Err(HeliosDacError::DeviceNotOpened), _ => Err(HeliosDacError::DeviceNotOpened),
}
}
#[cfg(test)]
fn mock_check(state: &std::sync::Arc<std::sync::Mutex<MockUsbState>>) -> Result<()> {
let s = state.lock().unwrap();
if s.connected {
Ok(())
} else {
Err(HeliosDacError::UsbError((s.disconnect_error)()))
}
}
pub fn write_frame(&mut self, frame: Frame) -> Result<()> {
#[cfg(test)]
if let HeliosDac::MockOpen(state) = self {
return Self::mock_check(state);
}
let handle = self.handle()?;
let frame_buffer = encode_frame(frame);
let timeout = bulk_transfer_timeout(frame_buffer.len());
handle.write_bulk(ENDPOINT_BULK_OUT, &frame_buffer, timeout)?;
Ok(())
}
pub fn write_frame_buffer(&mut self, buf: &[u8]) -> Result<()> {
#[cfg(test)]
if let HeliosDac::MockOpen(state) = self {
return Self::mock_check(state);
}
let handle = self.handle()?;
let timeout = bulk_transfer_timeout(buf.len());
handle.write_bulk(ENDPOINT_BULK_OUT, buf, timeout)?;
Ok(())
}
pub fn name(&self) -> Result<String> {
let ctrl_buffer = [CONTROL_GET_NAME, 0];
let (buffer, _) = self.call_control(&ctrl_buffer)?;
match buffer {
[0x85, bytes @ ..] => {
let null_byte_position = bytes.iter().position(|b| *b == 0u8).unwrap_or(31); let (bytes_until_null, _) = bytes.split_at(null_byte_position);
let name = String::from_utf8(bytes_until_null.to_vec())?;
Ok(name)
}
_ => Err(HeliosDacError::InvalidDeviceResult),
}
}
pub fn firmware_version(&self) -> Result<u32> {
let ctrl_buffer = [CONTROL_GET_FIRMWARE_VERSION, 0];
let (buffer, size) = self.call_control(&ctrl_buffer)?;
match &buffer[0..size] {
[0x84, b0, b1, b2, b3, ..] => Ok(u32::from_le_bytes([*b0, *b1, *b2, *b3])),
_ => Err(HeliosDacError::InvalidDeviceResult),
}
}
fn send_sdk_version(&self) -> Result<()> {
let ctrl_buffer = [CONTROL_SEND_SDK_VERSION, SDK_VERSION];
self.send_control(&ctrl_buffer)
}
pub fn status(&self) -> Result<DeviceStatus> {
let ctrl_buffer = [CONTROL_GET_STATUS, 0];
let (buffer, size) = self.call_control(&ctrl_buffer)?;
match &buffer[0..size] {
[0x83, 0] => Ok(DeviceStatus::NotReady),
[0x83, 1] => Ok(DeviceStatus::Ready),
_ => Err(HeliosDacError::InvalidDeviceResult),
}
}
pub fn stop(&self) -> Result<()> {
let ctrl_buffer = [CONTROL_STOP, 0];
self.send_control(&ctrl_buffer)
}
pub fn set_shutter(&self, open: bool) -> Result<()> {
let ctrl_buffer = [CONTROL_SET_SHUTTER, open as u8];
self.send_control(&ctrl_buffer)
}
fn call_control(&self, buffer: &[u8]) -> Result<([u8; 32], usize)> {
self.send_control(buffer)?;
self.read_response()
}
fn send_control(&self, buffer: &[u8]) -> Result<()> {
#[cfg(test)]
if let HeliosDac::MockOpen(state) = self {
return Self::mock_check(state);
}
let handle = self.handle()?;
let written_length =
handle.write_interrupt(ENDPOINT_INT_OUT, buffer, Duration::from_millis(16))?;
assert_eq!(written_length, buffer.len());
Ok(())
}
fn read_response(&self) -> Result<([u8; 32], usize)> {
#[cfg(test)]
if let HeliosDac::MockOpen(state) = self {
Self::mock_check(state)?;
let mut buf = [0u8; 32];
buf[0] = 0x83;
buf[1] = 1;
return Ok((buf, 2));
}
let handle = self.handle()?;
let mut buffer: [u8; 32] = [0; 32];
let size =
handle.read_interrupt(ENDPOINT_INT_IN, &mut buffer, Duration::from_millis(32))?;
Ok((buffer, size))
}
}
impl From<rusb::Device<rusb::Context>> for HeliosDac {
fn from(device: Device<Context>) -> Self {
HeliosDac::Idle(device)
}
}
fn format_usb_location<T: UsbContext>(device: &Device<T>) -> String {
let bus = device.bus_number();
match device.port_numbers() {
Ok(ports) if !ports.is_empty() => {
let path = ports
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(".");
format!("{bus}:{path}")
}
_ => format!("{}:{}", bus, device.address()),
}
}
fn encode_frame(frame: Frame) -> Vec<u8> {
let mut buf = Vec::with_capacity(frame.points.len() * 7 + 5);
encode_frame_into(frame.pps, &frame.points, frame.flags, &mut buf);
buf
}
pub fn encode_frame_into(
pps: u32,
points: &[super::Point],
flags: super::WriteFrameFlags,
buf: &mut Vec<u8>,
) {
let requested_points = points.len();
let (pps_actual, num_of_points_actual) = adjusted_frame_params(pps, requested_points);
buf.clear();
buf.reserve(num_of_points_actual * 7 + 5);
for point in points.iter().take(num_of_points_actual) {
buf.extend_from_slice(&[
(point.coordinate.x >> 4) as u8,
((point.coordinate.x & 0x0F) << 4) as u8 | (point.coordinate.y >> 8) as u8,
(point.coordinate.y & 0xFF) as u8,
point.color.r,
point.color.g,
point.color.b,
point.intensity,
]);
}
buf.push((pps_actual & 0xFF) as u8);
buf.push((pps_actual >> 8) as u8);
buf.push((num_of_points_actual & 0xFF) as u8);
buf.push((num_of_points_actual >> 8) as u8);
buf.push(flags.bits());
}
fn adjusted_frame_params(requested_pps: u32, requested_points: usize) -> (u32, usize) {
if requested_points >= 45 && (requested_points - 45).is_multiple_of(64) {
let actual_points = requested_points - 1;
let pps_actual = (((requested_pps as u64) * (actual_points as u64))
+ (requested_points as u64 / 2))
/ (requested_points as u64);
log::debug!(
"helios transfer-size workaround applied: requested_points={}, actual_points={}, requested_pps={}, actual_pps={}",
requested_points,
actual_points,
requested_pps,
pps_actual
);
(pps_actual as u32, actual_points)
} else {
(requested_pps, requested_points)
}
}
fn bulk_transfer_timeout(buffer_len: usize) -> Duration {
Duration::from_millis((8 + (buffer_len >> 5)) as u64)
}
#[derive(Error, Debug)]
pub enum HeliosDacError {
#[error("device is not opened")]
DeviceNotOpened,
#[error("usb connection error: {0}")]
UsbError(#[from] rusb::Error),
#[error("usb device answered with invalid data")]
InvalidDeviceResult,
#[error("could not parse string: {0}")]
Utf8Error(#[from] std::string::FromUtf8Error),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocols::helios::{Color, Coordinate, Point, WriteFrameFlags};
fn test_point() -> Point {
Point {
coordinate: Coordinate { x: 0x123, y: 0x456 },
color: Color::new(0x11, 0x22, 0x33),
intensity: 0x44,
}
}
#[test]
fn test_bulk_transfer_timeout_matches_sdk() {
assert_eq!(bulk_transfer_timeout(12), Duration::from_millis(8));
assert_eq!(bulk_transfer_timeout(75), Duration::from_millis(10));
assert_eq!(bulk_transfer_timeout(710), Duration::from_millis(30));
assert_eq!(bulk_transfer_timeout(28670), Duration::from_millis(903));
assert_eq!(bulk_transfer_timeout(0), Duration::from_millis(8));
}
#[test]
fn test_adjusted_frame_params_leaves_small_frames_unchanged() {
assert_eq!(adjusted_frame_params(30_000, 1), (30_000, 1));
assert_eq!(adjusted_frame_params(30_000, 44), (30_000, 44));
}
#[test]
fn test_adjusted_frame_params_applies_problem_size_workaround() {
assert_eq!(adjusted_frame_params(30_000, 45), (29_333, 44));
assert_eq!(adjusted_frame_params(30_000, 109), (29_725, 108));
}
#[test]
fn test_encode_frame_truncates_problematic_transfer_payload() {
let points = vec![test_point(); 109];
let buffer = encode_frame(Frame::new_with_flags(
30_000,
points,
WriteFrameFlags::SINGLE_MODE,
));
assert_eq!(buffer.len(), 108 * 7 + 5);
let footer = &buffer[buffer.len() - 5..];
assert_eq!(u16::from_le_bytes([footer[0], footer[1]]), 29_725);
assert_eq!(u16::from_le_bytes([footer[2], footer[3]]), 108);
assert_eq!(footer[4], WriteFrameFlags::SINGLE_MODE.bits());
}
}