use crate::serial_terminal::{FleaTerminalError, IdleFleaTerminal, StatelessFleaTerminal};
use std::thread;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct FleaDevice {
pub name: String,
pub port: String,
}
#[derive(Debug, thiserror::Error)]
pub enum FleaConnectorError {
#[error("Serial terminal error: {0}")]
SerialTerminal(#[from] FleaTerminalError),
#[error("Serial port error: {0}")]
SerialPort(#[from] serialport::Error),
#[error("Port {port} is not the FleaScope device you're looking for")]
InvalidPort { port: String },
#[error(
"No FleaScope device {name} found. Please connect a FleaScope or specify the port manually"
)]
DeviceNotFound { name: String },
#[error("Device validation failed")]
DeviceValidationFailed,
}
pub struct FleaConnector;
impl FleaConnector {
pub fn connect(
name: Option<&str>,
port: Option<&str>,
_read_calibrations: bool,
) -> Result<IdleFleaTerminal, FleaConnectorError> {
let terminal = if let Some(port) = port {
log::debug!("Connecting to FleaScope on port {}", port);
Self::validate_port(name, port)?;
StatelessFleaTerminal::new(port)?.try_into().unwrap()
} else {
let device_name = name.unwrap_or("FleaScope");
Self::get_working_serial(device_name)?
};
Ok(terminal)
}
fn validate_port(name: Option<&str>, port: &str) -> Result<(), FleaConnectorError> {
let mut devices = Self::get_available_devices(name)?;
if !devices.any(|d| d.port == port) {
return Err(FleaConnectorError::InvalidPort {
port: port.to_string(),
});
}
Ok(())
}
fn validate_device(name: Option<&str>, port_info: &serialport::SerialPortInfo) -> bool {
let valid_vendor_product_variants = [
(0x0403, 0xa660), (0x1b4f, 0xa660), (0x1b4f, 0xe66e), (0x04d8, 0xe66e), ];
let usb_info = match &port_info.port_type {
serialport::SerialPortType::UsbPort(usb_info) => usb_info,
_ => return false,
};
let is_valid_variant = valid_vendor_product_variants
.iter()
.any(|(vid, pid)| usb_info.vid == *vid && usb_info.pid == *pid);
if !is_valid_variant {
return false;
}
if let Some(expected_name) = name {
if let Some(product_name) = &usb_info.product {
if product_name != expected_name {
return false;
}
} else {
return false;
}
}
true
}
pub fn get_available_devices(
name: Option<&str>,
) -> Result<impl Iterator<Item = FleaDevice>, FleaConnectorError> {
let ports = serialport::available_ports()?;
let name_owned = name.map(|s| s.to_string());
Ok(ports.into_iter().filter_map(move |port_info| {
if let serialport::SerialPortType::UsbPort(usb_info) = &port_info.port_type {
if let Some(device_name) = usb_info.product.clone() {
if Self::validate_device(name_owned.as_deref(), &port_info) {
return Some(FleaDevice {
name: device_name,
port: port_info.port_name,
});
}
}
}
None
}))
}
pub fn get_available_devices_vec(
name: Option<&str>,
) -> Result<Vec<FleaDevice>, FleaConnectorError> {
Ok(Self::get_available_devices(name)?.collect())
}
fn get_device_port(name: &str) -> Result<String, FleaConnectorError> {
log::debug!("Searching for FleaScope device with name {}", name);
let mut devices = Self::get_available_devices(Some(name))?;
devices
.next()
.map(|device| device.port)
.ok_or_else(|| FleaConnectorError::DeviceNotFound {
name: name.to_string(),
})
}
fn get_working_serial(name: &str) -> Result<IdleFleaTerminal, FleaConnectorError> {
loop {
let port_candidate = Self::get_device_port(name)?;
let serial = StatelessFleaTerminal::new(&port_candidate)?;
match serial.try_into() {
Ok(s) => break Ok(s),
Err((mut serial, FleaTerminalError::Timeout { .. })) => {
log::debug!("Timeout during initialization, sending reset and retrying");
let _ = serial.send_reset(); thread::sleep(Duration::from_secs(2));
continue;
}
Err((_serial, e)) => return Err(e.into()),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_available_devices() {
let result = FleaConnector::get_available_devices_vec(None);
match result {
Ok(devices) => {
for device in devices {
assert!(!device.name.is_empty());
assert!(!device.port.is_empty());
assert!(device.port.starts_with('/') || device.port.starts_with("COM"));
}
}
Err(FleaConnectorError::SerialPort(_)) => {
}
Err(e) => {
panic!("Unexpected error: {:?}", e);
}
}
}
#[test]
fn test_device_validation_logic() {
let valid_usb_info = serialport::UsbPortInfo {
vid: 0x0403,
pid: 0xa660,
serial_number: Some("12345".to_string()),
manufacturer: Some("FTDI".to_string()),
product: Some("FleaScope".to_string()),
};
let valid_port_info = serialport::SerialPortInfo {
port_name: "/dev/ttyUSB0".to_string(),
port_type: serialport::SerialPortType::UsbPort(valid_usb_info),
};
assert!(FleaConnector::validate_device(None, &valid_port_info));
assert!(FleaConnector::validate_device(
Some("FleaScope"),
&valid_port_info
));
assert!(!FleaConnector::validate_device(
Some("OtherDevice"),
&valid_port_info
));
let invalid_usb_info = serialport::UsbPortInfo {
vid: 0x1234,
pid: 0x5678,
serial_number: None,
manufacturer: None,
product: None,
};
let invalid_port_info = serialport::SerialPortInfo {
port_name: "/dev/ttyUSB1".to_string(),
port_type: serialport::SerialPortType::UsbPort(invalid_usb_info),
};
assert!(!FleaConnector::validate_device(None, &invalid_port_info));
}
#[test]
fn test_iterator_benefits() {
let result = FleaConnector::get_available_devices(None);
match result {
Ok(mut devices) => {
let _first_device = devices.next();
let device_count = FleaConnector::get_available_devices(None)
.map(|iter| iter.count())
.unwrap_or(0);
println!("Found {} FleaScope devices", device_count);
}
Err(_) => {
}
}
}
}