use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
use crate::backend::{Backend, Signal, Sink};
use crate::capability::{Action, Capabilities, Family, FerrumApi, Info, Snapshot, StableId};
use crate::click::{ClickTiming, Holds};
use crate::device::{Device, Start};
use crate::error::{Error, Result};
use crate::family::{self, Open};
use crate::transport::os::OsSystem;
use crate::transport::{Candidate, System, is_ch343, is_ysc};
const CLOSE_DEADLINE: Duration = Duration::from_secs(1);
pub fn connect() -> Result<Device> {
Connect::auto().open()
}
pub fn scan() -> Vec<Scanned> {
Connect::auto().scan()
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Scanned {
pub port: String,
pub result: Result<Found>,
}
pub fn candidates() -> Vec<String> {
Connect::auto().candidates()
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Found {
pub port: String,
pub info: Info,
pub capabilities: Capabilities,
}
#[derive(Clone, Debug, Default)]
pub struct Connect {
port: Option<PathBuf>,
family: Option<Family>,
id: Option<StableId>,
click_timing: ClickTiming,
can: Vec<Action>,
ferrum_api: Option<FerrumApi>,
}
impl Connect {
pub fn auto() -> Self {
Self::default()
}
pub fn port(path: impl Into<PathBuf>) -> Self {
Self {
port: Some(path.into()),
..Self::default()
}
}
pub fn can(mut self, action: Action) -> Self {
self.can.push(action);
self
}
pub fn family(self, family: Family) -> Self {
Self {
family: Some(family),
..self
}
}
pub fn ferrum_api(self, api: FerrumApi) -> Self {
Self {
ferrum_api: Some(api),
..self
}
}
pub fn id(self, id: StableId) -> Self {
Self {
id: Some(id),
..self
}
}
pub fn click_timing(self, click_timing: ClickTiming) -> Self {
Self {
click_timing,
..self
}
}
pub fn candidates(&self) -> Vec<String> {
self.candidates_in(&|| OsSystem.ports())
}
pub(crate) fn candidates_in(&self, ports: &dyn Fn() -> Vec<Candidate>) -> Vec<String> {
self.listed(ports())
.into_iter()
.map(|candidate| candidate.path.display().to_string())
.collect()
}
fn listed(&self, listed: Vec<Candidate>) -> Vec<Candidate> {
let mut candidates: Vec<Candidate> = match &self.port {
Some(path) => match listed.into_iter().find(|candidate| &candidate.path == path) {
Some(candidate) => vec![candidate],
None => vec![Candidate {
path: path.clone(),
usb: None,
}],
},
None => listed
.into_iter()
.filter(|candidate| self.lists(candidate) && self.might_be(candidate))
.collect(),
};
candidates.sort_by(|a, b| port_order(&a.path, &b.path));
if self.family == Some(Family::Ysc) {
candidates.sort_by_key(|candidate| !candidate.usb.as_ref().is_some_and(is_ysc));
}
candidates
}
pub fn open(self) -> Result<Device> {
self.open_in(&|| OsSystem.ports(), family::PROBES)
}
pub(crate) fn open_in(
self,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
) -> Result<Device> {
let mut search = self.search(ports, probes);
match search.opened.take() {
Some(found) => Device::start(Start {
backend: found.backend,
snapshot: found.snapshot,
signals: found.signals,
incoming: found.incoming,
click_timing: self.click_timing,
holds: Holds::from_clock(),
port: found.port,
}),
None => Err(search.error()),
}
}
pub fn scan(self) -> Vec<Scanned> {
self.scan_in(&|| OsSystem.ports(), family::PROBES)
}
pub(crate) fn scan_in(
&self,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
) -> Vec<Scanned> {
self.listed(ports())
.iter()
.map(|candidate| {
let port = candidate.path.display().to_string();
let mut search = Search {
tried: vec![port.clone()],
..Search::default()
};
let result = match self.probe(candidate, ports, probes, &mut search) {
Some(opened) => {
opened.backend.shutdown(CLOSE_DEADLINE);
Ok(Found {
capabilities: self.capabilities(&opened.snapshot),
port: opened.port,
info: opened.snapshot.info,
})
}
None => Err(search.error()),
};
Scanned { port, result }
})
.collect()
}
#[cfg(test)]
pub(crate) fn found_in(
&self,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
) -> Vec<Found> {
self.scan_in(ports, probes)
.into_iter()
.filter_map(|scanned| scanned.result.ok())
.collect()
}
pub(crate) fn search(
&self,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
) -> Search {
let mut search = Search::default();
for candidate in &self.listed(ports()) {
search.tried.push(candidate.path.display().to_string());
if let Some(opened) = self.probe(candidate, ports, probes, &mut search) {
search.opened = Some(opened);
break;
}
}
search
}
fn capabilities(&self, snapshot: &Snapshot) -> Capabilities {
snapshot
.capabilities
.clone()
.with_host_taps(self.click_timing)
}
fn might_be(&self, candidate: &Candidate) -> bool {
match &self.id {
Some(StableId::UsbSerial(serial)) => {
candidate.usb.as_ref().and_then(|usb| usb.serial.as_ref()) == Some(serial)
}
_ => true,
}
}
fn lists(&self, candidate: &Candidate) -> bool {
let ferrum = self.family == Some(Family::Ferrum);
match &candidate.usb {
Some(usb) if is_ch343(usb) => !ferrum,
Some(usb) if is_ysc(usb) => {
self.family.is_none_or(|family| family == Family::Ysc)
&& usb.serial.as_deref() == Some("TOWMCULEFT")
}
Some(_) => ferrum,
None => ferrum && cfg!(windows),
}
}
fn wanted_ferrum_api(&self) -> Option<FerrumApi> {
self.ferrum_api
.or_else(|| (cfg!(windows) && self.port.is_none()).then_some(FerrumApi::Software))
}
fn wants(&self, family: Family, candidate: &Candidate) -> bool {
if family == Family::Ferrum && self.port.is_none() && self.family != Some(family) {
return false;
}
if family == Family::Ysc
&& self.family != Some(family)
&& !candidate.usb.as_ref().is_some_and(is_ysc)
{
return false;
}
let by_id = self.port.is_some()
|| match &self.id {
Some(StableId::Mac(_)) => family == Family::Medius,
Some(StableId::UsbSerial(_)) => family == Family::MakcuV4,
None => true,
};
by_id && self.family.is_none_or(|wanted| wanted == family)
}
fn probe(
&self,
candidate: &Candidate,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
search: &mut Search,
) -> Option<Opened> {
let port = candidate.path.display().to_string();
for (family, open) in probes {
if !self.wants(*family, candidate) {
continue;
}
let (signals, incoming) = mpsc::channel();
match open(candidate, Sink(signals.clone())) {
Ok((backend, mut snapshot)) => {
snapshot.info.port.clone_from(&port);
if let Some(expected) = &self.id
&& snapshot.info.id.as_ref() != Some(expected)
{
backend.shutdown(CLOSE_DEADLINE);
if self.port.is_some() {
search.skipped.push(Error::WrongBox {
port,
expected: expected.clone(),
found: snapshot.info.id,
});
}
return None;
}
if let (Some(expected), Some(found)) =
(self.wanted_ferrum_api(), snapshot.info.ferrum_api)
&& expected != found
{
backend.shutdown(CLOSE_DEADLINE);
search.skipped.push(Error::WrongFerrumApi {
port,
expected,
found,
});
return None;
}
let capabilities = self.capabilities(&snapshot);
let missing: Vec<Action> = self
.can
.iter()
.copied()
.filter(|action| !capabilities.get(*action).is_supported())
.collect();
if !missing.is_empty() {
backend.shutdown(CLOSE_DEADLINE);
search.incapable.push((
Found {
port,
info: snapshot.info,
capabilities,
},
missing,
));
return None;
}
return Some(Opened {
port,
backend,
snapshot,
signals,
incoming,
});
}
Err(Error::NotFound { .. }) => {}
Err(error) => {
search.skipped.push(error);
return None;
}
}
}
if let (Some(expected), Some(_)) = (self.family, &self.port)
&& ports().iter().any(|listed| listed.path == candidate.path)
{
search.skipped.push(Error::WrongFamily {
port,
expected,
found: None,
});
}
None
}
}
pub(crate) struct Opened {
port: String,
backend: Box<dyn Backend>,
snapshot: Snapshot,
signals: Sender<Signal>,
incoming: Receiver<Signal>,
}
#[derive(Default)]
pub(crate) struct Search {
opened: Option<Opened>,
skipped: Vec<Error>,
incapable: Vec<(Found, Vec<Action>)>,
tried: Vec<String>,
}
impl Search {
fn error(self) -> Error {
let rank = |error: &Error| match error {
Error::FirmwareTooOld { .. } | Error::LibraryTooOld { .. } => 0,
Error::PortBusy { .. } => 1,
Error::PermissionDenied { .. } => 2,
_ => 3,
};
let best = self.skipped.into_iter().min_by_key(rank);
match best {
Some(error) if rank(&error) < 3 => error,
_ if !self.incapable.is_empty() => Error::NoBoxCan {
boxes: self.incapable,
},
Some(error) => error,
None => Error::NotFound { tried: self.tried },
}
}
}
fn port_order(a: &Path, b: &Path) -> Ordering {
fn split(path: &Path) -> (String, Option<u64>) {
let name = path.display().to_string();
let stem = name.trim_end_matches(|c: char| c.is_ascii_digit());
(stem.to_string(), name[stem.len()..].parse().ok())
}
split(a).cmp(&split(b)).then_with(|| a.cmp(b))
}
#[cfg(test)]
mod tests;