use crate::error::{AsynError, AsynResult, AsynStatus};
use crate::port::{PortDriver, PortDriverBase, PortFlags};
use crate::user::AsynUser;
pub const UART_SPI_BIT: i32 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FtdiBitMode {
#[default]
Uart,
Spi,
}
impl FtdiBitMode {
pub fn from_c_mode(mode: i32) -> Self {
if (mode & UART_SPI_BIT) == UART_SPI_BIT {
FtdiBitMode::Spi
} else {
FtdiBitMode::Uart
}
}
}
#[derive(Debug, Clone)]
pub struct FtdiConfig {
pub vendor: i32,
pub product: i32,
pub baudrate: i32,
pub latency: i32,
pub bitmode: FtdiBitMode,
pub mode_raw: i32,
}
impl FtdiConfig {
pub fn from_positional(
vendor: i32,
product: i32,
baudrate: i32,
latency: i32,
mode: i32,
) -> Self {
let latency = latency.clamp(1, 255);
Self {
vendor,
product,
baudrate,
latency,
bitmode: FtdiBitMode::from_c_mode(mode),
mode_raw: mode,
}
}
}
pub struct DrvAsynFtdiPort {
base: PortDriverBase,
config: FtdiConfig,
priority: u32,
no_process_eos: bool,
}
impl DrvAsynFtdiPort {
#[allow(clippy::too_many_arguments)] pub fn configure(
port_name: &str,
vendor: i32,
product: i32,
baudrate: i32,
latency: i32,
priority: u32,
no_auto_connect: bool,
no_process_eos: bool,
mode: i32,
) -> AsynResult<Self> {
let config = FtdiConfig::from_positional(vendor, product, baudrate, latency, mode);
let mut base = PortDriverBase::new(
port_name,
1,
PortFlags {
multi_device: false,
can_block: true,
destructible: true,
},
);
base.connected = false;
base.auto_connect = !no_auto_connect;
Ok(Self {
base,
config,
priority,
no_process_eos,
})
}
pub fn config(&self) -> &FtdiConfig {
&self.config
}
pub fn priority(&self) -> u32 {
self.priority
}
pub fn no_process_eos(&self) -> bool {
self.no_process_eos
}
pub fn has_hw_support() -> bool {
cfg!(feature = "ftdi-mpsse")
}
}
impl PortDriver for DrvAsynFtdiPort {
fn base(&self) -> &PortDriverBase {
&self.base
}
fn base_mut(&mut self) -> &mut PortDriverBase {
&mut self.base
}
fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
if !Self::has_hw_support() {
return Err(AsynError::Status {
status: AsynStatus::Error,
message: format!(
"FTDI driver scaffold: hardware feature 'ftdi-mpsse' not enabled \
in this build. Config parsed (vid=0x{:04X}, pid=0x{:04X}, \
baudrate={}, latency={}, mode={:?}) — rebuild with \
`--features asyn-rs/ftdi-mpsse` to enable.",
self.config.vendor as u16,
self.config.product as u16,
self.config.baudrate,
self.config.latency,
self.config.bitmode,
),
});
}
Err(AsynError::Status {
status: AsynStatus::Error,
message: "FTDI hardware path not yet implemented".into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mode_bit0_clear_selects_uart() {
assert_eq!(FtdiBitMode::from_c_mode(0), FtdiBitMode::Uart);
assert_eq!(FtdiBitMode::from_c_mode(0x10), FtdiBitMode::Uart);
}
#[test]
fn mode_bit0_set_selects_spi() {
assert_eq!(FtdiBitMode::from_c_mode(1), FtdiBitMode::Spi);
assert_eq!(FtdiBitMode::from_c_mode(0x11), FtdiBitMode::Spi);
}
#[test]
fn latency_clamped_low() {
let cfg = FtdiConfig::from_positional(0x0403, 0x6014, 115_200, 0, 0);
assert_eq!(cfg.latency, 1);
}
#[test]
fn latency_clamped_high() {
let cfg = FtdiConfig::from_positional(0x0403, 0x6014, 115_200, 999, 0);
assert_eq!(cfg.latency, 255);
}
#[test]
fn latency_passthrough_in_range() {
let cfg = FtdiConfig::from_positional(0x0403, 0x6014, 115_200, 16, 0);
assert_eq!(cfg.latency, 16);
}
#[test]
fn configure_records_all_positional_fields() {
let drv =
DrvAsynFtdiPort::configure("ftdi0", 0x0403, 0x6014, 921_600, 4, 50, true, false, 1)
.unwrap();
assert_eq!(drv.config().vendor, 0x0403);
assert_eq!(drv.config().product, 0x6014);
assert_eq!(drv.config().baudrate, 921_600);
assert_eq!(drv.config().latency, 4);
assert_eq!(drv.config().bitmode, FtdiBitMode::Spi);
assert_eq!(drv.config().mode_raw, 1);
assert_eq!(drv.priority(), 50);
assert!(!drv.no_process_eos());
assert!(!drv.base().auto_connect);
}
#[test]
fn configure_no_auto_connect_false_enables_auto() {
let drv =
DrvAsynFtdiPort::configure("ftdi0", 0x0403, 0x6014, 115_200, 16, 0, false, false, 0)
.unwrap();
assert!(drv.base().auto_connect);
}
#[cfg(not(feature = "ftdi-mpsse"))]
#[test]
fn connect_without_hw_feature_reports_error() {
let mut drv =
DrvAsynFtdiPort::configure("ftdi0", 0x0403, 0x6014, 115_200, 16, 0, false, false, 0)
.unwrap();
let err = drv.connect(&AsynUser::default()).unwrap_err();
match err {
AsynError::Status { message, .. } => {
assert!(
message.contains("ftdi-mpsse"),
"error must mention the feature: {message}"
);
}
_ => panic!("expected Status error"),
}
}
#[test]
fn has_hw_support_matches_feature_flag() {
assert_eq!(
DrvAsynFtdiPort::has_hw_support(),
cfg!(feature = "ftdi-mpsse")
);
}
}