Skip to main content

anybox/
connect.rs

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
15/// how long `scan` waits for each box it opened to let go of its port
16const CLOSE_DEADLINE: Duration = Duration::from_secs(1);
17
18/// connects to the first box found, in port order
19pub fn connect() -> Result<Device> {
20    Connect::auto().open()
21}
22
23/// every port a search looks at and what's on it, in port order, so a picker can show the boxes you can use and
24/// why you can't use the others. it opens, describes and closes every box, so it's slow (a YSC search takes about
25/// 2 s a port), `candidates()` is the cheap one to call often
26pub fn scan() -> Vec<Scanned> {
27    Connect::auto().scan()
28}
29
30/// one port a scan looked at. `result` is the box on it, or the error a search of just that port gives. mostly
31/// that's busy (the box you already have open shows up as busy too, so leave its port out), not allowed, too old,
32/// `NoBoxCan` for a box that can't do what `can` asked, or `NotFound` when nothing answered
33#[derive(Debug)]
34#[non_exhaustive]
35pub struct Scanned {
36    pub port: String,
37    pub result: Result<Found>,
38}
39
40/// the ports a search would look at, in port order, without opening any of them or sending them a byte. it can't
41/// tell whether a box is on one, only where one could be, so it's cheap enough to call every second and run `scan`
42/// when the list changes. a Ferrum has no USB id anybox knows, so its port is never listed here. a YSC V1 sits behind the same
43/// chip as a Medius or a MAKCU, so its port shows up here, but only `.family(Family::Ysc)` ever looks for a V1 on it
44pub fn candidates() -> Vec<String> {
45    Connect::auto().candidates()
46}
47
48/// one box a scan saw
49#[derive(Clone, Debug, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct Found {
52    pub port: String,
53    pub info: Info,
54    /// what it can do, the same as `device.snapshot().capabilities` would say right after opening it
55    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    /// asks CH343 (1A86:55D3) ports and YSC V2 left ports (1A86:FE0C with serial TOWMCULEFT). a Ferrum is only
70    /// asked with `.family(Family::Ferrum)` (the other ports then) or `Connect::port`, and a YSC V1 only with
71    /// `.family(Family::Ysc)`, which then asks every CH343 port at up to 9 rates
72    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    /// skips a box that can't do this, checked once when it opens, so with a keyboard box and a mouse box
84    /// plugged in `.can(Action::Move)` opens the mouse one. call it again for each thing it has to do.
85    /// `Action::Connect` matches no box
86    pub fn can(mut self, action: Action) -> Self {
87        self.can.push(action);
88        self
89    }
90
91    /// only tries this family, so the port never gets anything meant for another box
92    pub fn family(self, family: Family) -> Self {
93        Self {
94            family: Some(family),
95            ..self
96        }
97    }
98
99    /// only a Ferrum on this API, so asking for Software never ends up on the box's own legacy port. on Windows a
100    /// Ferrum search without a port already only takes Software, so ask for Legacy here if that's the one you
101    /// want. other boxes don't care about it
102    pub fn ferrum_api(self, api: FerrumApi) -> Self {
103        Self {
104            ferrum_api: Some(api),
105            ..self
106        }
107    }
108
109    /// only the box with this id, whatever port it's on. a box with no id (like a MAKCU whose USB serial can't be
110    /// trusted) can't be picked this way, only by its port
111    pub fn id(self, id: StableId) -> Self {
112        Self {
113            id: Some(id),
114            ..self
115        }
116    }
117
118    /// what `click()` does on a box with no click of its own
119    pub fn click_timing(self, click_timing: ClickTiming) -> Self {
120        Self {
121            click_timing,
122            ..self
123        }
124    }
125
126    /// the ports `scan` and `open` would look at with these filters, see `candidates()`. with
127    /// `.family(Family::Ferrum)` that's every port that isn't a CH343 or a YSC port, plus on Windows the ones with no
128    /// USB id
129    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    /// the candidates in port order, before anything gets opened
141    fn listed(&self, listed: Vec<Candidate>) -> Vec<Candidate> {
142        let mut candidates: Vec<Candidate> = match &self.port {
143            // a port the caller names gets asked whatever its USB id is, because the caller knows what's on it
144            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            // a V2 port answers at once, so it goes before the CH343 ports that each take a whole rate sweep
159            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    /// what happened on every port this search looks at, in port order, see `anybox::scan`
189    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    /// the boxes a scan found, for the tests that only care about those
223    #[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    /// the first box on the candidate ports in port order that passes the filters, and what went wrong on the
236    /// ports before it
237    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    /// what the device will say it can do, with the clicks and taps the core builds on the PC where the box has
254    /// none, so a check here agrees with `device.snapshot()`
255    fn capabilities(&self, snapshot: &Snapshot) -> Capabilities {
256        snapshot
257            .capabilities
258            .clone()
259            .with_host_taps(self.click_timing)
260    }
261
262    /// rules out what the port listing alone already can, so a box nobody asked for doesn't get opened
263    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    /// Medius and MAKCU are always behind a CH343. a Ferrum is behind a CP210x or the Ferrum app's virtual port,
273    /// and a CP210x is on lots of unrelated devices, so those ports only get text sent to them when a Ferrum was
274    /// asked for
275    fn lists(&self, candidate: &Candidate) -> bool {
276        let ferrum = self.family == Some(Family::Ferrum);
277        match &candidate.usb {
278            // a YSC V1 sits behind a CH343 too
279            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            // the virtual port presumably shows up with no USB id, and the app only runs on Windows
286            None => ferrum && cfg!(windows),
287        }
288    }
289
290    /// on Windows the app's port is the one with everything, so a search there never picks the box's own legacy
291    /// port by accident. Linux has no app, so it takes whatever answers
292    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        // a Ferrum gets no USB id we know of yet, so it's only asked when someone asked for one
299        if family == Family::Ferrum && self.port.is_none() && self.family != Some(family) {
300            return false;
301        }
302        // a YSC V1 has no USB id of its own (the one we've seen sits behind a CH343 like a Medius or a MAKCU), so a port
303        // that isn't a V2 one only gets the YSC rate sweep when someone asked for a YSC
304        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        // every family asks a named port, so another box there comes back as WrongBox and not NotFound
311        let by_id = self.port.is_some()
312            || match &self.id {
313                // only a Medius gives a MAC (in its VERSION reply), and only a MAKCU has a USB serial we trust
314                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                // a busy or forbidden port is the same for every family, and once a box said what it is there's
391                // no point asking the other families either
392                Err(error) => {
393                    search.skipped.push(error);
394                    return None;
395                }
396            }
397        }
398        // a port that isn't there at all isn't a box of the wrong family. the ports get listed again because one
399        // can go away while it's being asked
400        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    /// boxes that opened but can't do what `can` asked
426    incapable: Vec<(Found, Vec<Action>)>,
427    tried: Vec<String>,
428}
429
430impl Search {
431    /// when no box was found, the error that helps the most. an update to install comes first, then a port
432    /// someone else has open, then one this user isn't allowed to open, then anything else, and NotFound last
433    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            // boxes answered and only fell short of `can`, which says more than the other misses
444            _ 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
453/// `COM3` before `COM11`, so it compares the name without its trailing number first and then that number
454fn 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;