use std::time::Duration;
use super::{DeviceStatus, Frame};
use crate::protocols::usb_transfer::UsbEndpoints;
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;
pub const HELIOS_MAX_POINTS: usize = 4095;
const ENDPOINT_BULK_OUT: u8 = 0x02;
const ENDPOINT_INT_OUT: u8 = 0x06;
const ENDPOINT_INT_IN: u8 = 0x83;
const INIT_SETTLE: Duration = Duration::from_millis(100);
const INIT_DRAIN_TIMEOUT: Duration = Duration::from_millis(5);
const INIT_INTERRUPT_TIMEOUT: Duration = Duration::from_millis(32);
const INIT_FW_OUT_ATTEMPTS: usize = 2;
const INIT_FW_IN_ATTEMPTS: usize = 3;
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)
}
}
struct HeliosComm<H: UsbEndpoints> {
handle: H,
firmware_version: Option<u32>,
}
impl<H: UsbEndpoints> HeliosComm<H> {
fn probe_firmware_version(&self) -> Result<u32> {
let ctrl_buffer = [CONTROL_GET_FIRMWARE_VERSION, 0];
let mut last_err: Option<rusb::Error> = None;
for out_attempt in 1..=INIT_FW_OUT_ATTEMPTS {
match self.handle.write_interrupt(
ENDPOINT_INT_OUT,
&ctrl_buffer,
INIT_INTERRUPT_TIMEOUT,
) {
Ok(2) => {}
Ok(_) => {
last_err = Some(rusb::Error::Io);
log::debug!(
"helios: firmware probe OUT short-write (attempt {out_attempt}/{INIT_FW_OUT_ATTEMPTS})"
);
continue;
}
Err(e) => {
last_err = Some(e);
log::debug!(
"helios: firmware probe OUT failed (attempt {out_attempt}/{INIT_FW_OUT_ATTEMPTS}): {e:?}"
);
continue;
}
}
for in_attempt in 1..=INIT_FW_IN_ATTEMPTS {
let mut buf = [0u8; 32];
match self
.handle
.read_interrupt(ENDPOINT_INT_IN, &mut buf, INIT_INTERRUPT_TIMEOUT)
{
Ok(size) => match &buf[0..size] {
[0x84, b0, b1, b2, b3, ..] => {
return Ok(u32::from_le_bytes([*b0, *b1, *b2, *b3]));
}
_ => {
log::debug!(
"helios: firmware probe IN unexpected reply (out {out_attempt}/{INIT_FW_OUT_ATTEMPTS}, in {in_attempt}/{INIT_FW_IN_ATTEMPTS}): {:?}",
&buf[0..size]
);
continue;
}
},
Err(e) => {
last_err = Some(e);
log::debug!(
"helios: firmware probe IN failed (out {out_attempt}/{INIT_FW_OUT_ATTEMPTS}, in {in_attempt}/{INIT_FW_IN_ATTEMPTS}): {e:?}"
);
}
}
}
}
Err(HeliosDacError::UsbError(
last_err.unwrap_or(rusb::Error::Timeout),
))
}
fn drain_stale_responses(&self) {
let mut drain_buf = [0u8; 32];
while self
.handle
.read_interrupt(ENDPOINT_INT_IN, &mut drain_buf, INIT_DRAIN_TIMEOUT)
.is_ok()
{}
}
fn write_frame(&self, frame: Frame) -> Result<()> {
let frame_buffer = encode_frame(frame);
self.write_bulk_all(&frame_buffer)
}
fn write_frame_buffer(&self, buf: &[u8]) -> Result<()> {
self.write_bulk_all(buf)
}
fn write_bulk_all(&self, buf: &[u8]) -> Result<()> {
let timeout = bulk_transfer_timeout(buf.len());
match self.handle.write_bulk(ENDPOINT_BULK_OUT, buf, timeout) {
Ok(n) if n == buf.len() => Ok(()),
Ok(n) => {
log::debug!("helios: bulk-OUT short-write ({n}/{} bytes)", buf.len());
Err(HeliosDacError::UsbError(rusb::Error::Io))
}
Err(rusb::Error::Pipe) => {
log::debug!("helios: bulk-OUT stalled (Pipe); clearing halt and retrying once");
self.handle.clear_halt(ENDPOINT_BULK_OUT)?;
match self.handle.write_bulk(ENDPOINT_BULK_OUT, buf, timeout)? {
n if n == buf.len() => Ok(()),
n => {
log::debug!(
"helios: bulk-OUT short-write after clear_halt ({n}/{} bytes)",
buf.len()
);
Err(HeliosDacError::UsbError(rusb::Error::Io))
}
}
}
Err(e) => Err(HeliosDacError::UsbError(e)),
}
}
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)
}
_ => {
self.drain_stale_responses();
Err(HeliosDacError::InvalidDeviceResult)
}
}
}
fn read_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])),
_ => {
self.drain_stale_responses();
Err(HeliosDacError::InvalidDeviceResult)
}
}
}
fn send_sdk_version(&self) -> Result<()> {
let ctrl_buffer = [CONTROL_SEND_SDK_VERSION, SDK_VERSION];
self.send_control(&ctrl_buffer)
}
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),
_ => {
self.drain_stale_responses();
Err(HeliosDacError::InvalidDeviceResult)
}
}
}
fn stop(&self) -> Result<()> {
let ctrl_buffer = [CONTROL_STOP, 0];
self.send_control(&ctrl_buffer)
}
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<()> {
let written_length =
self.handle
.write_interrupt(ENDPOINT_INT_OUT, buffer, Duration::from_millis(16))?;
if written_length != buffer.len() {
log::debug!(
"helios: control OUT short-write ({written_length}/{} bytes)",
buffer.len()
);
return Err(HeliosDacError::UsbError(rusb::Error::Io));
}
Ok(())
}
fn read_response(&self) -> Result<([u8; 32], usize)> {
let mut buffer: [u8; 32] = [0; 32];
let size =
self.handle
.read_interrupt(ENDPOINT_INT_IN, &mut buffer, Duration::from_millis(32))?;
Ok((buffer, size))
}
fn leak_handle(self) {
let HeliosComm { handle, .. } = self;
std::mem::forget(handle);
}
}
pub enum HeliosDac {
Idle(rusb::Device<rusb::Context>),
#[allow(private_interfaces)]
Open {
device: Option<rusb::Device<rusb::Context>>,
comm: HeliosComm<Box<dyn UsbEndpoints + Send>>,
},
}
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)?;
std::thread::sleep(INIT_SETTLE);
let mut comm: HeliosComm<Box<dyn UsbEndpoints + Send>> = HeliosComm {
handle: Box::new(handle),
firmware_version: None,
};
comm.drain_stale_responses();
let fw = comm.probe_firmware_version()?;
comm.firmware_version = Some(fw);
log::debug!("helios: connected, firmware version {fw}");
comm.send_sdk_version()?;
Ok(HeliosDac::Open {
device: Some(device),
comm,
})
}
open => Ok(open),
}
}
fn comm(&self) -> Result<&HeliosComm<Box<dyn UsbEndpoints + Send>>> {
match self {
HeliosDac::Open { comm, .. } => Ok(comm),
_ => Err(HeliosDacError::DeviceNotOpened),
}
}
pub fn usb_location(&self) -> String {
match self {
HeliosDac::Idle(device) => format_usb_location(device),
HeliosDac::Open {
device: Some(device),
..
} => format_usb_location(device),
HeliosDac::Open { device: None, .. } => "test:fake".to_string(),
}
}
pub(crate) fn read_name(&self) -> Result<String> {
let device = match self {
HeliosDac::Idle(device) => device.clone(),
HeliosDac::Open {
device: Some(device),
..
} => device.clone(),
HeliosDac::Open { device: None, .. } => return Err(HeliosDacError::DeviceNotOpened),
};
let handle = device.open()?;
handle.claim_interface(0)?;
handle.set_alternate_setting(0, 1)?;
std::thread::sleep(INIT_SETTLE);
let comm = HeliosComm {
handle,
firmware_version: None,
};
comm.drain_stale_responses();
comm.name()
}
pub fn probed_firmware_version(&self) -> Option<u32> {
match self {
HeliosDac::Open { comm, .. } => comm.firmware_version,
_ => None,
}
}
pub(crate) fn leak_handle(self) {
if let HeliosDac::Open { comm, .. } = self {
comm.leak_handle();
}
}
pub fn write_frame(&mut self, frame: Frame) -> Result<()> {
self.comm()?.write_frame(frame)
}
pub fn write_frame_buffer(&mut self, buf: &[u8]) -> Result<()> {
self.comm()?.write_frame_buffer(buf)
}
pub fn name(&self) -> Result<String> {
self.comm()?.name()
}
pub fn firmware_version(&self) -> Result<u32> {
self.comm()?.read_firmware_version()
}
pub fn status(&self) -> Result<DeviceStatus> {
self.comm()?.status()
}
pub fn stop(&self) -> Result<()> {
self.comm()?.stop()
}
pub fn set_shutter(&self, open: bool) -> Result<()> {
self.comm()?.set_shutter(open)
}
}
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().min(HELIOS_MAX_POINTS);
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)
}
}
pub(crate) 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)]
impl<H: UsbEndpoints> HeliosComm<H> {
fn from_parts(handle: H, firmware_version: Option<u32>) -> Self {
HeliosComm {
handle,
firmware_version,
}
}
}
#[cfg(test)]
impl HeliosDac {
pub(crate) fn from_endpoints(
handle: impl UsbEndpoints + Send + 'static,
firmware_version: Option<u32>,
) -> Self {
HeliosDac::Open {
device: None,
comm: HeliosComm {
handle: Box::new(handle),
firmware_version,
},
}
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::ENDPOINT_BULK_OUT;
use crate::protocols::usb_transfer::UsbEndpoints;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Clone)]
pub(crate) enum WriteScript {
Full,
Short(usize),
Err(rusb::Error),
}
impl WriteScript {
fn apply(&self, len: usize) -> rusb::Result<usize> {
match self {
WriteScript::Full => Ok(len),
WriteScript::Short(n) => Ok(*n),
WriteScript::Err(e) => Err(*e),
}
}
}
#[derive(Clone, Default)]
pub(crate) struct FakeUsb {
pub(crate) inner: Arc<Mutex<FakeInner>>,
}
#[derive(Default)]
pub(crate) struct FakeInner {
writes: Vec<(u8, Vec<u8>)>,
bulk_out: VecDeque<WriteScript>,
int_out: VecDeque<WriteScript>,
pub(crate) int_in: VecDeque<rusb::Result<Vec<u8>>>,
clear_halt_calls: Vec<u8>,
}
impl FakeUsb {
pub(crate) fn writes_to(&self, endpoint: u8) -> Vec<Vec<u8>> {
self.inner
.lock()
.unwrap()
.writes
.iter()
.filter(|(ep, _)| *ep == endpoint)
.map(|(_, b)| b.clone())
.collect()
}
pub(crate) fn bulk_writes(&self) -> Vec<Vec<u8>> {
self.writes_to(ENDPOINT_BULK_OUT)
}
pub(crate) fn clear_halt_calls(&self) -> Vec<u8> {
self.inner.lock().unwrap().clear_halt_calls.clone()
}
pub(crate) fn script_bulk_out(&self, scripts: impl IntoIterator<Item = WriteScript>) {
self.inner.lock().unwrap().bulk_out.extend(scripts);
}
pub(crate) fn script_int_out(&self, scripts: impl IntoIterator<Item = WriteScript>) {
self.inner.lock().unwrap().int_out.extend(scripts);
}
pub(crate) fn script_int_in(&self, reads: impl IntoIterator<Item = rusb::Result<Vec<u8>>>) {
self.inner.lock().unwrap().int_in.extend(reads);
}
}
impl UsbEndpoints for FakeUsb {
fn write_bulk(&self, endpoint: u8, buf: &[u8], _t: Duration) -> rusb::Result<usize> {
let mut inner = self.inner.lock().unwrap();
inner.writes.push((endpoint, buf.to_vec()));
let script = inner.bulk_out.pop_front().unwrap_or(WriteScript::Full);
script.apply(buf.len())
}
fn read_bulk(&self, _e: u8, _buf: &mut [u8], _t: Duration) -> rusb::Result<usize> {
Ok(0)
}
fn write_interrupt(&self, endpoint: u8, buf: &[u8], _t: Duration) -> rusb::Result<usize> {
let mut inner = self.inner.lock().unwrap();
inner.writes.push((endpoint, buf.to_vec()));
let script = inner.int_out.pop_front().unwrap_or(WriteScript::Full);
script.apply(buf.len())
}
fn read_interrupt(&self, _e: u8, buf: &mut [u8], _t: Duration) -> rusb::Result<usize> {
let reply = self
.inner
.lock()
.unwrap()
.int_in
.pop_front()
.unwrap_or(Err(rusb::Error::Timeout))?;
let n = reply.len().min(buf.len());
buf[..n].copy_from_slice(&reply[..n]);
Ok(n)
}
fn clear_halt(&self, endpoint: u8) -> rusb::Result<()> {
self.inner.lock().unwrap().clear_halt_calls.push(endpoint);
Ok(())
}
fn release_interface(&self, _i: u8) -> rusb::Result<()> {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::test_support::{FakeUsb, WriteScript};
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());
}
fn comm() -> (HeliosComm<FakeUsb>, FakeUsb) {
let fake = FakeUsb::default();
(HeliosComm::from_parts(fake.clone(), None), fake)
}
#[test]
fn write_frame_bulk_writes_exactly_encode_frame_output() {
let (comm, fake) = comm();
let frame = Frame::new(30_000, vec![test_point(); 3]);
let expected = encode_frame(frame.clone());
comm.write_frame(frame).unwrap();
let bulk = fake.writes_to(ENDPOINT_BULK_OUT);
assert_eq!(bulk.len(), 1, "one bulk-OUT transfer");
assert_eq!(
bulk[0], expected,
"bytes on the wire == encode_frame(frame)"
);
}
#[test]
fn write_bulk_all_recovers_from_single_stall() {
let (comm, fake) = comm();
fake.script_bulk_out([WriteScript::Err(rusb::Error::Pipe), WriteScript::Full]);
comm.write_frame_buffer(&[0u8; 12]).unwrap();
assert_eq!(
fake.clear_halt_calls(),
vec![ENDPOINT_BULK_OUT],
"halt cleared exactly once on the stalled endpoint"
);
assert_eq!(
fake.writes_to(ENDPOINT_BULK_OUT).len(),
2,
"wrote, then retried"
);
}
#[test]
fn write_bulk_all_stall_then_short_retry_is_io_error() {
let (comm, fake) = comm();
fake.script_bulk_out([WriteScript::Err(rusb::Error::Pipe), WriteScript::Short(3)]);
let err = comm.write_frame_buffer(&[0u8; 12]).unwrap_err();
assert!(matches!(err, HeliosDacError::UsbError(rusb::Error::Io)));
assert_eq!(fake.clear_halt_calls(), vec![ENDPOINT_BULK_OUT]);
}
#[test]
fn write_bulk_all_stall_then_stall_propagates_pipe() {
let (comm, fake) = comm();
fake.script_bulk_out([
WriteScript::Err(rusb::Error::Pipe),
WriteScript::Err(rusb::Error::Pipe),
]);
let err = comm.write_frame_buffer(&[0u8; 12]).unwrap_err();
assert!(matches!(err, HeliosDacError::UsbError(rusb::Error::Pipe)));
}
#[test]
fn write_bulk_all_short_write_is_io_error() {
let (comm, fake) = comm();
fake.script_bulk_out([WriteScript::Short(3)]);
let err = comm.write_frame_buffer(&[0u8; 12]).unwrap_err();
assert!(matches!(err, HeliosDacError::UsbError(rusb::Error::Io)));
assert!(
fake.clear_halt_calls().is_empty(),
"no clear_halt without a stall"
);
}
#[test]
fn send_control_short_interrupt_write_is_io_error() {
let (comm, fake) = comm();
fake.script_int_out([WriteScript::Short(1)]);
let err = comm.stop().unwrap_err();
assert!(matches!(err, HeliosDacError::UsbError(rusb::Error::Io)));
}
#[test]
fn probe_firmware_version_retries_out_and_in_then_parses() {
let (comm, fake) = comm();
fake.script_int_out([WriteScript::Short(1)]);
fake.script_int_in([
Ok(vec![0x99, 0, 0, 0, 0]),
Ok(vec![0x84, 0x2A, 0x00, 0x00, 0x00]),
]);
assert_eq!(comm.probe_firmware_version().unwrap(), 42);
}
#[test]
fn probe_firmware_version_exhausts_to_timeout() {
let (comm, _fake) = comm();
let err = comm.probe_firmware_version().unwrap_err();
assert!(matches!(
err,
HeliosDacError::UsbError(rusb::Error::Timeout)
));
}
#[test]
fn name_parses_scripted_reply() {
let (comm, fake) = comm();
fake.script_int_in([Ok(vec![0x85, b'H', b'i', 0, 0, 0])]);
assert_eq!(comm.name().unwrap(), "Hi");
}
#[test]
fn read_firmware_version_parses_scripted_reply() {
let (comm, fake) = comm();
fake.script_int_in([Ok(vec![0x84, 0x07, 0x00, 0x00, 0x00])]);
assert_eq!(comm.read_firmware_version().unwrap(), 7);
}
#[test]
fn status_parses_ready_and_not_ready() {
let (ready, fake_ready) = comm();
fake_ready.script_int_in([Ok(vec![0x83, 1])]);
assert!(matches!(ready.status().unwrap(), DeviceStatus::Ready));
let (not_ready, fake_nr) = comm();
fake_nr.script_int_in([Ok(vec![0x83, 0])]);
assert!(matches!(
not_ready.status().unwrap(),
DeviceStatus::NotReady
));
}
#[test]
fn status_malformed_reply_errors_and_drains() {
let (comm, fake) = comm();
fake.script_int_in([Ok(vec![0x83, 9]), Ok(vec![0xAB, 0xCD])]);
let err = comm.status().unwrap_err();
assert!(matches!(err, HeliosDacError::InvalidDeviceResult));
assert!(
fake.inner.lock().unwrap().int_in.is_empty(),
"drain_stale_responses drained the lingering IN reply"
);
}
#[test]
fn leak_handle_does_not_panic() {
let (comm, _fake) = comm();
comm.leak_handle();
}
}