use crate::error::{MmError, MmResult};
use crate::property::PropertyMap;
use crate::traits::{Device, Generic};
use crate::transport::Transport;
use crate::types::{DeviceType, PropertyValue};
fn crc16_ibm(data: &[u8]) -> u16 {
fn bit_reflect(mut data: u16, nbits: u8) -> u16 {
let mut output = 0u16;
for i in 0..nbits {
if data & 1 != 0 {
output |= 1 << (nbits - 1 - i);
}
data >>= 1;
}
output
}
let mut crc = 0u16;
for &byte in data {
let dbyte = bit_reflect(byte as u16, 8);
crc ^= dbyte << 8;
for _ in 0..8 {
let mix = crc & 0x8000;
crc <<= 1;
if mix != 0 {
crc ^= 0x8005;
}
}
}
bit_reflect(crc, 16)
}
fn build_set_current_cmd(current_ma: f64) -> [u8; 6] {
let coded = (current_ma / 293.0 * 4096.0) as i16;
let val_hi = ((coded as u16) >> 8) as u8;
let val_lo = (coded as u16 & 0xFF) as u8;
let payload = [0x41u8, 0x77, val_hi, val_lo];
let crc = crc16_ibm(&payload);
[0x41, 0x77, val_hi, val_lo, (crc & 0xFF) as u8, (crc >> 8) as u8]
}
pub struct EtlDevice {
props: PropertyMap,
transport: Option<Box<dyn Transport>>,
initialized: bool,
current_ma: f64,
min_current_ma: f64,
max_current_ma: f64,
}
impl EtlDevice {
pub fn new() -> Self {
let mut props = PropertyMap::new();
props
.define_property("Port", PropertyValue::String("Undefined".into()), false)
.unwrap();
props
.define_property("Current-mA", PropertyValue::Float(0.0), false)
.unwrap();
props
.define_property("MaxI_mA", PropertyValue::Float(293.0), false)
.unwrap();
props
.define_property("MinI_mA", PropertyValue::Float(-293.0), false)
.unwrap();
Self {
props,
transport: None,
initialized: false,
current_ma: 0.0,
min_current_ma: -293.0,
max_current_ma: 293.0,
}
}
pub fn with_transport(mut self, t: Box<dyn Transport>) -> Self {
self.transport = Some(t);
self
}
fn call_transport<R, F>(&mut self, f: F) -> MmResult<R>
where
F: FnOnce(&mut dyn Transport) -> MmResult<R>,
{
match self.transport.as_mut() {
Some(t) => f(t.as_mut()),
None => Err(MmError::NotConnected),
}
}
pub fn set_current(&mut self, current_ma: f64) -> MmResult<()> {
let clamped = current_ma
.max(self.min_current_ma)
.min(self.max_current_ma);
let cmd = build_set_current_cmd(clamped);
self.call_transport(|t| t.send_bytes(&cmd))?;
self.current_ma = clamped;
Ok(())
}
pub fn get_current(&mut self) -> MmResult<f64> {
let get_cmd: [u8; 6] = [0x41, 0x72, 0x00, 0x00, 0xB4, 0x27];
self.call_transport(|t| t.send_bytes(&get_cmd))?;
let resp = self.call_transport(|t| t.receive_bytes(6))?;
if resp.len() < 3 {
return Ok(self.current_ma);
}
let i1 = resp[1] as i8 as i32;
let i2 = resp[2] as i32;
let current = (i1 * 255 + i2) as f64 * 293.0 / 4096.0;
self.current_ma = current;
Ok(current)
}
pub fn current(&self) -> f64 {
self.current_ma
}
}
impl Default for EtlDevice {
fn default() -> Self {
Self::new()
}
}
impl Device for EtlDevice {
fn name(&self) -> &str {
"ETL"
}
fn description(&self) -> &str {
"Optotune Electrically Tunable Lens"
}
fn initialize(&mut self) -> MmResult<()> {
if self.transport.is_none() {
return Err(MmError::NotConnected);
}
self.call_transport(|t| t.send("Start"))?;
self.set_current(0.0)?;
self.initialized = true;
Ok(())
}
fn shutdown(&mut self) -> MmResult<()> {
if self.initialized {
let _ = self.set_current(0.0);
self.initialized = false;
}
Ok(())
}
fn get_property(&self, name: &str) -> MmResult<PropertyValue> {
self.props.get(name).cloned()
}
fn set_property(&mut self, name: &str, val: PropertyValue) -> MmResult<()> {
self.props.set(name, val)
}
fn property_names(&self) -> Vec<String> {
self.props.property_names().to_vec()
}
fn has_property(&self, name: &str) -> bool {
self.props.has_property(name)
}
fn is_property_read_only(&self, name: &str) -> bool {
self.props.entry(name).map(|e| e.read_only).unwrap_or(false)
}
fn device_type(&self) -> DeviceType {
DeviceType::Generic
}
fn busy(&self) -> bool {
false
}
}
impl Generic for EtlDevice {}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::MockTransport;
fn make_initialized() -> EtlDevice {
let t = MockTransport::new(); let mut d = EtlDevice::new().with_transport(Box::new(t));
d.initialize().unwrap();
d
}
#[test]
fn initialize_succeeds() {
let d = make_initialized();
assert!(d.initialized);
assert_eq!(d.current(), 0.0);
}
#[test]
fn set_current_records_bytes() {
let mut d = make_initialized();
d.transport = Some(Box::new(MockTransport::new()));
d.set_current(146.5).unwrap();
assert!((d.current() - 146.5).abs() < 0.01);
}
#[test]
fn crc16_ibm_known_value() {
let data = [0x41u8, 0x72, 0x00, 0x00];
let crc = crc16_ibm(&data);
assert_eq!(crc, 0x27B4);
}
#[test]
fn build_set_current_zero() {
let cmd = build_set_current_cmd(0.0);
assert_eq!(cmd[0], 0x41);
assert_eq!(cmd[1], 0x77);
assert_eq!(cmd[2], 0x00);
assert_eq!(cmd[3], 0x00);
let expected_crc = crc16_ibm(&[0x41, 0x77, 0x00, 0x00]);
assert_eq!(cmd[4], (expected_crc & 0xFF) as u8);
assert_eq!(cmd[5], (expected_crc >> 8) as u8);
}
#[test]
fn build_set_current_positive() {
let cmd = build_set_current_cmd(293.0);
let coded = 4096i16;
assert_eq!(cmd[2], (coded as u16 >> 8) as u8); assert_eq!(cmd[3], (coded as u16 & 0xFF) as u8); }
#[test]
fn current_clamped_to_range() {
let mut d = make_initialized();
d.transport = Some(Box::new(MockTransport::new()));
d.set_current(999.0).unwrap();
assert_eq!(d.current(), 293.0);
d.transport = Some(Box::new(MockTransport::new()));
d.set_current(-999.0).unwrap();
assert_eq!(d.current(), -293.0);
}
#[test]
fn get_current_parses_response() {
let mut d = make_initialized();
d.transport = Some(Box::new(
MockTransport::new().expect_binary(&[0x41, 0x00, 0x00, 0x00, 0x00, 0x00]),
));
let c = d.get_current().unwrap();
assert_eq!(c, 0.0);
}
#[test]
fn no_transport_error() {
assert!(EtlDevice::new().initialize().is_err());
}
}