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, 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};
14
15/// how long `find` 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 box on this machine in port order. each one gets opened, described and closed again
24pub fn find() -> Result<Vec<Found>> {
25    Connect::auto().find()
26}
27
28/// one box `find` saw
29#[derive(Clone, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Found {
32    pub port: String,
33    pub info: Info,
34    /// what it can do, the same as `device.snapshot().capabilities` would say right after opening it
35    pub capabilities: Capabilities,
36}
37
38#[derive(Clone, Debug, Default)]
39pub struct Connect {
40    port: Option<PathBuf>,
41    family: Option<Family>,
42    id: Option<StableId>,
43    click_timing: ClickTiming,
44    can: Vec<Action>,
45}
46
47impl Connect {
48    /// asks every port a box could be behind, which for now means every CH343 (1A86:55D3) port
49    pub fn auto() -> Self {
50        Self::default()
51    }
52
53    pub fn port(path: impl Into<PathBuf>) -> Self {
54        Self {
55            port: Some(path.into()),
56            ..Self::default()
57        }
58    }
59
60    /// skips a box that can't do this, checked once when it opens, so with a keyboard box and a mouse box
61    /// plugged in `.can(Action::Move)` opens the mouse one. call it again for each thing it has to do.
62    /// `Action::Connect` matches no box
63    pub fn can(mut self, action: Action) -> Self {
64        self.can.push(action);
65        self
66    }
67
68    /// only tries this family, so the port never gets anything meant for another box
69    pub fn family(self, family: Family) -> Self {
70        Self {
71            family: Some(family),
72            ..self
73        }
74    }
75
76    /// 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
77    /// trusted) can't be picked this way, only by its port
78    pub fn id(self, id: StableId) -> Self {
79        Self {
80            id: Some(id),
81            ..self
82        }
83    }
84
85    /// what `click()` does on a box with no click of its own
86    pub fn click_timing(self, click_timing: ClickTiming) -> Self {
87        Self {
88            click_timing,
89            ..self
90        }
91    }
92
93    pub fn open(self) -> Result<Device> {
94        self.open_in(&|| OsSystem.ports(), family::PROBES)
95    }
96
97    pub(crate) fn open_in(
98        self,
99        ports: &dyn Fn() -> Vec<Candidate>,
100        probes: &[(Family, Open)],
101    ) -> Result<Device> {
102        let mut search = self.search(ports, probes, true);
103        match search.opened.take() {
104            Some(found) => Device::start(Start {
105                backend: found.backend,
106                snapshot: found.snapshot,
107                signals: found.signals,
108                incoming: found.incoming,
109                click_timing: self.click_timing,
110                holds: Holds::from_clock(),
111                port: found.port,
112            }),
113            None => Err(search.error()),
114        }
115    }
116
117    /// every box that passes the filters in port order. each one gets opened, described and closed again
118    pub fn find(self) -> Result<Vec<Found>> {
119        self.find_in(&|| OsSystem.ports(), family::PROBES)
120    }
121
122    pub(crate) fn find_in(
123        &self,
124        ports: &dyn Fn() -> Vec<Candidate>,
125        probes: &[(Family, Open)],
126    ) -> Result<Vec<Found>> {
127        let search = self.search(ports, probes, false);
128        // a box that's plugged in but busy gives its error, otherwise a picker would show no box at all
129        if search.seen.is_empty() && !search.skipped.is_empty() {
130            return Err(search.error());
131        }
132        Ok(search.seen)
133    }
134
135    /// the boxes on the candidate ports in port order, and what went wrong on the others. with `first` it stops
136    /// at the first box that passes the filters
137    pub(crate) fn search(
138        &self,
139        ports: &dyn Fn() -> Vec<Candidate>,
140        probes: &[(Family, Open)],
141        first: bool,
142    ) -> Search {
143        let mut search = Search::default();
144        let listed = ports();
145        let mut candidates: Vec<Candidate> = match &self.port {
146            // a port the caller names gets asked whatever its USB id is, because the caller knows what's on it
147            Some(path) => match listed.into_iter().find(|candidate| &candidate.path == path) {
148                Some(candidate) => vec![candidate],
149                None => vec![Candidate {
150                    path: path.clone(),
151                    usb: None,
152                }],
153            },
154            None => listed
155                .into_iter()
156                .filter(|candidate| candidate.usb.as_ref().is_some_and(is_ch343))
157                .collect(),
158        };
159        candidates.sort_by(|a, b| port_order(&a.path, &b.path));
160        for candidate in &candidates {
161            if self.port.is_none() && !self.might_be(candidate) {
162                continue;
163            }
164            search.tried.push(candidate.path.display().to_string());
165            if let Some(opened) = self.probe(candidate, ports, probes, &mut search) {
166                if first {
167                    search.opened = Some(opened);
168                    break;
169                }
170                // close it before asking the next port, so a box we already saw isn't kept busy while the rest
171                // get asked
172                opened.backend.shutdown(CLOSE_DEADLINE);
173                let capabilities = self.capabilities(&opened.snapshot);
174                search.seen.push(Found {
175                    port: opened.port,
176                    info: opened.snapshot.info,
177                    capabilities,
178                });
179            }
180        }
181        search
182    }
183
184    /// what the device will say it can do, with the clicks and taps the core builds on the PC where the box has
185    /// none, so a check here agrees with `device.snapshot()`
186    fn capabilities(&self, snapshot: &Snapshot) -> Capabilities {
187        snapshot
188            .capabilities
189            .clone()
190            .with_host_taps(self.click_timing)
191    }
192
193    /// rules out what the port listing alone already can, so a box nobody asked for doesn't get opened
194    fn might_be(&self, candidate: &Candidate) -> bool {
195        match &self.id {
196            Some(StableId::UsbSerial(serial)) => {
197                candidate.usb.as_ref().and_then(|usb| usb.serial.as_ref()) == Some(serial)
198            }
199            _ => true,
200        }
201    }
202
203    fn wants(&self, family: Family) -> bool {
204        // every family asks a named port, so another box there comes back as WrongBox and not NotFound
205        let by_id = self.port.is_some()
206            || match &self.id {
207                // only a Medius gives a MAC (in its VERSION reply), and only a MAKCU has a USB serial we trust
208                Some(StableId::Mac(_)) => family == Family::Medius,
209                Some(StableId::UsbSerial(_)) => family == Family::MakcuV4,
210                None => true,
211            };
212        by_id && self.family.is_none_or(|wanted| wanted == family)
213    }
214
215    fn probe(
216        &self,
217        candidate: &Candidate,
218        ports: &dyn Fn() -> Vec<Candidate>,
219        probes: &[(Family, Open)],
220        search: &mut Search,
221    ) -> Option<Opened> {
222        let port = candidate.path.display().to_string();
223        for (family, open) in probes {
224            if !self.wants(*family) {
225                continue;
226            }
227            let (signals, incoming) = mpsc::channel();
228            match open(candidate, Sink(signals.clone())) {
229                Ok((backend, mut snapshot)) => {
230                    snapshot.info.port.clone_from(&port);
231                    if let Some(expected) = &self.id
232                        && snapshot.info.id.as_ref() != Some(expected)
233                    {
234                        backend.shutdown(CLOSE_DEADLINE);
235                        if self.port.is_some() {
236                            search.skipped.push(Error::WrongBox {
237                                port,
238                                expected: expected.clone(),
239                                found: snapshot.info.id,
240                            });
241                        }
242                        return None;
243                    }
244                    let capabilities = self.capabilities(&snapshot);
245                    let missing: Vec<Action> = self
246                        .can
247                        .iter()
248                        .copied()
249                        .filter(|action| !capabilities.get(*action).is_supported())
250                        .collect();
251                    if !missing.is_empty() {
252                        backend.shutdown(CLOSE_DEADLINE);
253                        search.incapable.push((
254                            Found {
255                                port,
256                                info: snapshot.info,
257                                capabilities,
258                            },
259                            missing,
260                        ));
261                        return None;
262                    }
263                    return Some(Opened {
264                        port,
265                        backend,
266                        snapshot,
267                        signals,
268                        incoming,
269                    });
270                }
271                Err(Error::NotFound { .. }) => {}
272                // a busy or forbidden port is the same for every family, and once a box said what it is there's
273                // no point asking the other families either
274                Err(error) => {
275                    search.skipped.push(error);
276                    return None;
277                }
278            }
279        }
280        // a port that isn't there at all isn't a box of the wrong family. the ports get listed again because one
281        // can go away while it's being asked
282        if let (Some(expected), Some(_)) = (self.family, &self.port)
283            && ports().iter().any(|listed| listed.path == candidate.path)
284        {
285            search.skipped.push(Error::WrongFamily {
286                port,
287                expected,
288                found: None,
289            });
290        }
291        None
292    }
293}
294
295pub(crate) struct Opened {
296    port: String,
297    backend: Box<dyn Backend>,
298    snapshot: Snapshot,
299    signals: Sender<Signal>,
300    incoming: Receiver<Signal>,
301}
302
303#[derive(Default)]
304pub(crate) struct Search {
305    opened: Option<Opened>,
306    seen: Vec<Found>,
307    skipped: Vec<Error>,
308    /// boxes that opened but can't do what `can` asked
309    incapable: Vec<(Found, Vec<Action>)>,
310    tried: Vec<String>,
311}
312
313impl Search {
314    /// when no box was found, the error that helps the most. an update to install comes first, then a port
315    /// someone else has open, then one this user isn't allowed to open, then anything else, and NotFound last
316    fn error(self) -> Error {
317        let rank = |error: &Error| match error {
318            Error::FirmwareTooOld { .. } | Error::LibraryTooOld { .. } => 0,
319            Error::PortBusy { .. } => 1,
320            Error::PermissionDenied { .. } => 2,
321            _ => 3,
322        };
323        let best = self.skipped.into_iter().min_by_key(rank);
324        match best {
325            Some(error) if rank(&error) < 3 => error,
326            // boxes answered and only fell short of `can`, which says more than the other misses
327            _ if !self.incapable.is_empty() => Error::NoBoxCan {
328                boxes: self.incapable,
329            },
330            Some(error) => error,
331            None => Error::NotFound { tried: self.tried },
332        }
333    }
334}
335
336/// `COM3` before `COM11`, so it compares the name without its trailing number first and then that number
337fn port_order(a: &Path, b: &Path) -> Ordering {
338    fn split(path: &Path) -> (String, Option<u64>) {
339        let name = path.display().to_string();
340        let stem = name.trim_end_matches(|c: char| c.is_ascii_digit());
341        (stem.to_string(), name[stem.len()..].parse().ok())
342    }
343    split(a).cmp(&split(b)).then_with(|| a.cmp(b))
344}
345
346#[cfg(test)]
347mod tests;