1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
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};
/// how long `find` waits for each box it opened to let go of its port
const CLOSE_DEADLINE: Duration = Duration::from_secs(1);
/// connects to the first box found, in port order
pub fn connect() -> Result<Device> {
Connect::auto().open()
}
/// every box on this machine in port order. each one gets opened, described and closed again
pub fn find() -> Result<Vec<Found>> {
Connect::auto().find()
}
/// one box `find` saw
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Found {
pub port: String,
pub info: Info,
/// what it can do, the same as `device.snapshot().capabilities` would say right after opening it
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 {
/// asks every port a box could be behind, which for now means every CH343 (1A86:55D3) port. a Ferrum isn't
/// on one, so it's only asked with `.family(Family::Ferrum)` (every other port then) or `Connect::port`
pub fn auto() -> Self {
Self::default()
}
pub fn port(path: impl Into<PathBuf>) -> Self {
Self {
port: Some(path.into()),
..Self::default()
}
}
/// skips a box that can't do this, checked once when it opens, so with a keyboard box and a mouse box
/// plugged in `.can(Action::Move)` opens the mouse one. call it again for each thing it has to do.
/// `Action::Connect` matches no box
pub fn can(mut self, action: Action) -> Self {
self.can.push(action);
self
}
/// only tries this family, so the port never gets anything meant for another box
pub fn family(self, family: Family) -> Self {
Self {
family: Some(family),
..self
}
}
/// only a Ferrum on this API, so asking for Software never ends up on the box's own legacy port. on Windows a
/// Ferrum search without a port already only takes Software, so ask for Legacy here if that's the one you
/// want. other boxes don't care about it
pub fn ferrum_api(self, api: FerrumApi) -> Self {
Self {
ferrum_api: Some(api),
..self
}
}
/// 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
/// trusted) can't be picked this way, only by its port
pub fn id(self, id: StableId) -> Self {
Self {
id: Some(id),
..self
}
}
/// what `click()` does on a box with no click of its own
pub fn click_timing(self, click_timing: ClickTiming) -> Self {
Self {
click_timing,
..self
}
}
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, true);
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()),
}
}
/// every box that passes the filters in port order. each one gets opened, described and closed again
pub fn find(self) -> Result<Vec<Found>> {
self.find_in(&|| OsSystem.ports(), family::PROBES)
}
pub(crate) fn find_in(
&self,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
) -> Result<Vec<Found>> {
let search = self.search(ports, probes, false);
// a box that's plugged in but busy gives its error, otherwise a picker would show no box at all
if search.seen.is_empty() && !search.skipped.is_empty() {
return Err(search.error());
}
Ok(search.seen)
}
/// the boxes on the candidate ports in port order, and what went wrong on the others. with `first` it stops
/// at the first box that passes the filters
pub(crate) fn search(
&self,
ports: &dyn Fn() -> Vec<Candidate>,
probes: &[(Family, Open)],
first: bool,
) -> Search {
let mut search = Search::default();
let listed = ports();
let mut candidates: Vec<Candidate> = match &self.port {
// a port the caller names gets asked whatever its USB id is, because the caller knows what's on it
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))
.collect(),
};
candidates.sort_by(|a, b| port_order(&a.path, &b.path));
for candidate in &candidates {
if self.port.is_none() && !self.might_be(candidate) {
continue;
}
search.tried.push(candidate.path.display().to_string());
if let Some(opened) = self.probe(candidate, ports, probes, &mut search) {
if first {
search.opened = Some(opened);
break;
}
// close it before asking the next port, so a box we already saw isn't kept busy while the rest
// get asked
opened.backend.shutdown(CLOSE_DEADLINE);
let capabilities = self.capabilities(&opened.snapshot);
search.seen.push(Found {
port: opened.port,
info: opened.snapshot.info,
capabilities,
});
}
}
search
}
/// what the device will say it can do, with the clicks and taps the core builds on the PC where the box has
/// none, so a check here agrees with `device.snapshot()`
fn capabilities(&self, snapshot: &Snapshot) -> Capabilities {
snapshot
.capabilities
.clone()
.with_host_taps(self.click_timing)
}
/// rules out what the port listing alone already can, so a box nobody asked for doesn't get opened
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,
}
}
/// Medius and MAKCU are always behind a CH343. a Ferrum is behind a CP210x or the Ferrum app's virtual port,
/// and a CP210x is on lots of unrelated devices, so those ports only get text sent to them when a Ferrum was
/// asked for
fn lists(&self, candidate: &Candidate) -> bool {
let ferrum = self.family == Some(Family::Ferrum);
match &candidate.usb {
Some(usb) if is_ch343(usb) => !ferrum,
Some(_) => ferrum,
// the virtual port presumably shows up with no USB id, and the app only runs on Windows
None => ferrum && cfg!(windows),
}
}
/// on Windows the app's port is the one with everything, so a search there never picks the box's own legacy
/// port by accident. Linux has no app, so it takes whatever answers
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) -> bool {
// a Ferrum gets no USB id we know of yet, so it's only asked when someone asked for one
if family == Family::Ferrum && self.port.is_none() && self.family != Some(family) {
return false;
}
// every family asks a named port, so another box there comes back as WrongBox and not NotFound
let by_id = self.port.is_some()
|| match &self.id {
// only a Medius gives a MAC (in its VERSION reply), and only a MAKCU has a USB serial we trust
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) {
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 { .. }) => {}
// a busy or forbidden port is the same for every family, and once a box said what it is there's
// no point asking the other families either
Err(error) => {
search.skipped.push(error);
return None;
}
}
}
// a port that isn't there at all isn't a box of the wrong family. the ports get listed again because one
// can go away while it's being asked
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>,
seen: Vec<Found>,
skipped: Vec<Error>,
/// boxes that opened but can't do what `can` asked
incapable: Vec<(Found, Vec<Action>)>,
tried: Vec<String>,
}
impl Search {
/// when no box was found, the error that helps the most. an update to install comes first, then a port
/// someone else has open, then one this user isn't allowed to open, then anything else, and NotFound last
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,
// boxes answered and only fell short of `can`, which says more than the other misses
_ if !self.incapable.is_empty() => Error::NoBoxCan {
boxes: self.incapable,
},
Some(error) => error,
None => Error::NotFound { tried: self.tried },
}
}
}
/// `COM3` before `COM11`, so it compares the name without its trailing number first and then that number
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;