1use std::cmp::Ordering;
2use std::path::{Path, PathBuf};
3use std::sync::mpsc::{self, Receiver, Sender};
4use std::time::Duration;
5
6use crate::backend::{Backend, Signal, Sink};
7use crate::capability::{Action, Capabilities, Family, FerrumApi, Info, Snapshot, StableId};
8use crate::click::{ClickTiming, Holds};
9use crate::device::{Device, Start};
10use crate::error::{Error, Result};
11use crate::family::{self, Open};
12use crate::transport::os::OsSystem;
13use crate::transport::{Candidate, System, is_ch343, is_ysc};
14
15const CLOSE_DEADLINE: Duration = Duration::from_secs(1);
17
18pub fn connect() -> Result<Device> {
20 Connect::auto().open()
21}
22
23pub fn scan() -> Vec<Scanned> {
27 Connect::auto().scan()
28}
29
30#[derive(Debug)]
34#[non_exhaustive]
35pub struct Scanned {
36 pub port: String,
37 pub result: Result<Found>,
38}
39
40pub fn candidates() -> Vec<String> {
45 Connect::auto().candidates()
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct Found {
52 pub port: String,
53 pub info: Info,
54 pub capabilities: Capabilities,
56}
57
58#[derive(Clone, Debug, Default)]
59pub struct Connect {
60 port: Option<PathBuf>,
61 family: Option<Family>,
62 id: Option<StableId>,
63 click_timing: ClickTiming,
64 can: Vec<Action>,
65 ferrum_api: Option<FerrumApi>,
66}
67
68impl Connect {
69 pub fn auto() -> Self {
73 Self::default()
74 }
75
76 pub fn port(path: impl Into<PathBuf>) -> Self {
77 Self {
78 port: Some(path.into()),
79 ..Self::default()
80 }
81 }
82
83 pub fn can(mut self, action: Action) -> Self {
87 self.can.push(action);
88 self
89 }
90
91 pub fn family(self, family: Family) -> Self {
93 Self {
94 family: Some(family),
95 ..self
96 }
97 }
98
99 pub fn ferrum_api(self, api: FerrumApi) -> Self {
103 Self {
104 ferrum_api: Some(api),
105 ..self
106 }
107 }
108
109 pub fn id(self, id: StableId) -> Self {
112 Self {
113 id: Some(id),
114 ..self
115 }
116 }
117
118 pub fn click_timing(self, click_timing: ClickTiming) -> Self {
120 Self {
121 click_timing,
122 ..self
123 }
124 }
125
126 pub fn candidates(&self) -> Vec<String> {
130 self.candidates_in(&|| OsSystem.ports())
131 }
132
133 pub(crate) fn candidates_in(&self, ports: &dyn Fn() -> Vec<Candidate>) -> Vec<String> {
134 self.listed(ports())
135 .into_iter()
136 .map(|candidate| candidate.path.display().to_string())
137 .collect()
138 }
139
140 fn listed(&self, listed: Vec<Candidate>) -> Vec<Candidate> {
142 let mut candidates: Vec<Candidate> = match &self.port {
143 Some(path) => match listed.into_iter().find(|candidate| &candidate.path == path) {
145 Some(candidate) => vec![candidate],
146 None => vec![Candidate {
147 path: path.clone(),
148 usb: None,
149 }],
150 },
151 None => listed
152 .into_iter()
153 .filter(|candidate| self.lists(candidate) && self.might_be(candidate))
154 .collect(),
155 };
156 candidates.sort_by(|a, b| port_order(&a.path, &b.path));
157 if self.family == Some(Family::Ysc) {
158 candidates.sort_by_key(|candidate| !candidate.usb.as_ref().is_some_and(is_ysc));
160 }
161 candidates
162 }
163
164 pub fn open(self) -> Result<Device> {
165 self.open_in(&|| OsSystem.ports(), family::PROBES)
166 }
167
168 pub(crate) fn open_in(
169 self,
170 ports: &dyn Fn() -> Vec<Candidate>,
171 probes: &[(Family, Open)],
172 ) -> Result<Device> {
173 let mut search = self.search(ports, probes);
174 match search.opened.take() {
175 Some(found) => Device::start(Start {
176 backend: found.backend,
177 snapshot: found.snapshot,
178 signals: found.signals,
179 incoming: found.incoming,
180 click_timing: self.click_timing,
181 holds: Holds::from_clock(),
182 port: found.port,
183 }),
184 None => Err(search.error()),
185 }
186 }
187
188 pub fn scan(self) -> Vec<Scanned> {
190 self.scan_in(&|| OsSystem.ports(), family::PROBES)
191 }
192
193 pub(crate) fn scan_in(
194 &self,
195 ports: &dyn Fn() -> Vec<Candidate>,
196 probes: &[(Family, Open)],
197 ) -> Vec<Scanned> {
198 self.listed(ports())
199 .iter()
200 .map(|candidate| {
201 let port = candidate.path.display().to_string();
202 let mut search = Search {
203 tried: vec![port.clone()],
204 ..Search::default()
205 };
206 let result = match self.probe(candidate, ports, probes, &mut search) {
207 Some(opened) => {
208 opened.backend.shutdown(CLOSE_DEADLINE);
209 Ok(Found {
210 capabilities: self.capabilities(&opened.snapshot),
211 port: opened.port,
212 info: opened.snapshot.info,
213 })
214 }
215 None => Err(search.error()),
216 };
217 Scanned { port, result }
218 })
219 .collect()
220 }
221
222 #[cfg(test)]
224 pub(crate) fn found_in(
225 &self,
226 ports: &dyn Fn() -> Vec<Candidate>,
227 probes: &[(Family, Open)],
228 ) -> Vec<Found> {
229 self.scan_in(ports, probes)
230 .into_iter()
231 .filter_map(|scanned| scanned.result.ok())
232 .collect()
233 }
234
235 pub(crate) fn search(
238 &self,
239 ports: &dyn Fn() -> Vec<Candidate>,
240 probes: &[(Family, Open)],
241 ) -> Search {
242 let mut search = Search::default();
243 for candidate in &self.listed(ports()) {
244 search.tried.push(candidate.path.display().to_string());
245 if let Some(opened) = self.probe(candidate, ports, probes, &mut search) {
246 search.opened = Some(opened);
247 break;
248 }
249 }
250 search
251 }
252
253 fn capabilities(&self, snapshot: &Snapshot) -> Capabilities {
256 snapshot
257 .capabilities
258 .clone()
259 .with_host_taps(self.click_timing)
260 }
261
262 fn might_be(&self, candidate: &Candidate) -> bool {
264 match &self.id {
265 Some(StableId::UsbSerial(serial)) => {
266 candidate.usb.as_ref().and_then(|usb| usb.serial.as_ref()) == Some(serial)
267 }
268 _ => true,
269 }
270 }
271
272 fn lists(&self, candidate: &Candidate) -> bool {
276 let ferrum = self.family == Some(Family::Ferrum);
277 match &candidate.usb {
278 Some(usb) if is_ch343(usb) => !ferrum,
280 Some(usb) if is_ysc(usb) => {
281 self.family.is_none_or(|family| family == Family::Ysc)
282 && usb.serial.as_deref() == Some("TOWMCULEFT")
283 }
284 Some(_) => ferrum,
285 None => ferrum && cfg!(windows),
287 }
288 }
289
290 fn wanted_ferrum_api(&self) -> Option<FerrumApi> {
293 self.ferrum_api
294 .or_else(|| (cfg!(windows) && self.port.is_none()).then_some(FerrumApi::Software))
295 }
296
297 fn wants(&self, family: Family, candidate: &Candidate) -> bool {
298 if family == Family::Ferrum && self.port.is_none() && self.family != Some(family) {
300 return false;
301 }
302 if family == Family::Ysc
305 && self.family != Some(family)
306 && !candidate.usb.as_ref().is_some_and(is_ysc)
307 {
308 return false;
309 }
310 let by_id = self.port.is_some()
312 || match &self.id {
313 Some(StableId::Mac(_)) => family == Family::Medius,
315 Some(StableId::UsbSerial(_)) => family == Family::MakcuV4,
316 None => true,
317 };
318 by_id && self.family.is_none_or(|wanted| wanted == family)
319 }
320
321 fn probe(
322 &self,
323 candidate: &Candidate,
324 ports: &dyn Fn() -> Vec<Candidate>,
325 probes: &[(Family, Open)],
326 search: &mut Search,
327 ) -> Option<Opened> {
328 let port = candidate.path.display().to_string();
329 for (family, open) in probes {
330 if !self.wants(*family, candidate) {
331 continue;
332 }
333 let (signals, incoming) = mpsc::channel();
334 match open(candidate, Sink(signals.clone())) {
335 Ok((backend, mut snapshot)) => {
336 snapshot.info.port.clone_from(&port);
337 if let Some(expected) = &self.id
338 && snapshot.info.id.as_ref() != Some(expected)
339 {
340 backend.shutdown(CLOSE_DEADLINE);
341 if self.port.is_some() {
342 search.skipped.push(Error::WrongBox {
343 port,
344 expected: expected.clone(),
345 found: snapshot.info.id,
346 });
347 }
348 return None;
349 }
350 if let (Some(expected), Some(found)) =
351 (self.wanted_ferrum_api(), snapshot.info.ferrum_api)
352 && expected != found
353 {
354 backend.shutdown(CLOSE_DEADLINE);
355 search.skipped.push(Error::WrongFerrumApi {
356 port,
357 expected,
358 found,
359 });
360 return None;
361 }
362 let capabilities = self.capabilities(&snapshot);
363 let missing: Vec<Action> = self
364 .can
365 .iter()
366 .copied()
367 .filter(|action| !capabilities.get(*action).is_supported())
368 .collect();
369 if !missing.is_empty() {
370 backend.shutdown(CLOSE_DEADLINE);
371 search.incapable.push((
372 Found {
373 port,
374 info: snapshot.info,
375 capabilities,
376 },
377 missing,
378 ));
379 return None;
380 }
381 return Some(Opened {
382 port,
383 backend,
384 snapshot,
385 signals,
386 incoming,
387 });
388 }
389 Err(Error::NotFound { .. }) => {}
390 Err(error) => {
393 search.skipped.push(error);
394 return None;
395 }
396 }
397 }
398 if let (Some(expected), Some(_)) = (self.family, &self.port)
401 && ports().iter().any(|listed| listed.path == candidate.path)
402 {
403 search.skipped.push(Error::WrongFamily {
404 port,
405 expected,
406 found: None,
407 });
408 }
409 None
410 }
411}
412
413pub(crate) struct Opened {
414 port: String,
415 backend: Box<dyn Backend>,
416 snapshot: Snapshot,
417 signals: Sender<Signal>,
418 incoming: Receiver<Signal>,
419}
420
421#[derive(Default)]
422pub(crate) struct Search {
423 opened: Option<Opened>,
424 skipped: Vec<Error>,
425 incapable: Vec<(Found, Vec<Action>)>,
427 tried: Vec<String>,
428}
429
430impl Search {
431 fn error(self) -> Error {
434 let rank = |error: &Error| match error {
435 Error::FirmwareTooOld { .. } | Error::LibraryTooOld { .. } => 0,
436 Error::PortBusy { .. } => 1,
437 Error::PermissionDenied { .. } => 2,
438 _ => 3,
439 };
440 let best = self.skipped.into_iter().min_by_key(rank);
441 match best {
442 Some(error) if rank(&error) < 3 => error,
443 _ if !self.incapable.is_empty() => Error::NoBoxCan {
445 boxes: self.incapable,
446 },
447 Some(error) => error,
448 None => Error::NotFound { tried: self.tried },
449 }
450 }
451}
452
453fn port_order(a: &Path, b: &Path) -> Ordering {
455 fn split(path: &Path) -> (String, Option<u64>) {
456 let name = path.display().to_string();
457 let stem = name.trim_end_matches(|c: char| c.is_ascii_digit());
458 (stem.to_string(), name[stem.len()..].parse().ok())
459 }
460 split(a).cmp(&split(b)).then_with(|| a.cmp(b))
461}
462
463#[cfg(test)]
464mod tests;