use std::any::Any;
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, RwLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::backend::{
Backend, ChordKind, Command, InputKind, InputKinds, LOCKED, LockSlot, Sign, Sink,
StreamFailure, Typing, UNLOCKED, Usage,
};
use crate::capability::{
Action, Axis, Button, Capabilities, Capability, Family, FerrumAction, FerrumApi, Firmware,
Info, Quirk, Snapshot, UsbIds,
};
use crate::error::{Error, Result};
use crate::extension::{Extension, ExtensionReply};
use crate::transport::link::{Link, Target};
use crate::transport::probe::Probe;
use crate::transport::{Candidate, Port, System, is_ch343, is_ysc};
mod frames;
mod poller;
use frames::{END, FerrumFrames, PROMPT};
pub(crate) use poller::Poller;
pub(crate) const BAUDS: [u32; 11] = [
115_200, 3_000_000, 2_000_000, 1_000_000, 921_600, 460_800, 230_400, 57_600, 38_400, 19_200,
9_600,
];
const WAIT: Duration = Duration::from_millis(300);
const NOTCHES: u16 = 127;
const NO_TYPING: &str = "Ferrum has no command to type text, tap the keys yourself instead";
const CLICK_HOLD: (Duration, Duration) = (Duration::from_millis(75), Duration::from_millis(125));
const FORCED_UP: (Duration, Duration) = (Duration::from_millis(125), Duration::from_millis(175));
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn not_found(path: &Path) -> Error {
Error::NotFound {
tried: vec![path.display().to_string()],
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Roles {
pub(crate) mouse: bool,
pub(crate) keyboard: bool,
pub(crate) directional: bool,
pub(crate) peripheral: Option<UsbIds>,
pub(crate) firmware: Option<Firmware>,
}
pub(crate) fn open(candidate: &Candidate, sink: Sink) -> Result<(Box<dyn Backend>, Snapshot)> {
let (backend, snapshot, poller) =
attach(Arc::new(crate::transport::os::OsSystem), candidate, sink)?;
if let Some(poller) = poller {
*lock(&backend.poller) = Some(poller.spawn()?);
}
Ok((Box::new(backend), snapshot))
}
pub(crate) fn attach(
system: Arc<dyn System>,
candidate: &Candidate,
sink: Sink,
) -> Result<(FerrumBackend, Snapshot, Option<Poller>)> {
let path = candidate.path.as_path();
if candidate
.usb
.as_ref()
.is_some_and(|usb| is_ch343(usb) || is_ysc(usb))
{
return Err(not_found(path));
}
let stop = AtomicBool::new(false);
for (at, &baud) in BAUDS.iter().enumerate() {
let port = match system.open(path, baud) {
Ok(port) => port,
Err(Error::NotFound { .. }) => return Err(not_found(path)),
Err(error) => return Err(error),
};
let probe = Probe::new(&*port, &stop);
let Some(api) = identify(&probe) else {
continue;
};
let roles = describe(&probe, api)?;
let mut bauds = BAUDS.to_vec();
bauds.rotate_left(at);
let target = Target {
family: Family::Ferrum,
path: path.to_path_buf(),
usb: candidate.usb.clone(),
id: None,
bauds,
};
return start(system, port, api, roles, target, sink);
}
Err(not_found(path))
}
fn start(
system: Arc<dyn System>,
port: Box<dyn Port>,
api: FerrumApi,
roles: Roles,
target: Target,
sink: Sink,
) -> Result<(FerrumBackend, Snapshot, Option<Poller>)> {
let snapshot = snapshot(api, &roles);
let shared = Arc::new(Shared {
api,
snapshot: RwLock::new(snapshot.clone()),
directional: AtomicBool::new(roles.directional),
streams: Mutex::new(InputKinds::default()),
seen: Arc::default(),
generation: AtomicU64::new(0),
stop: AtomicBool::new(false),
});
let reprobe = {
let shared = shared.clone();
Box::new(move |probe: &Probe<'_>| {
if identify(probe) != Some(shared.api) {
return Err(Error::Disconnected);
}
let roles = describe(probe, shared.api)?;
let snapshot = self::snapshot(shared.api, &roles);
*lock(&shared.streams) = InputKinds::default();
shared.generation.fetch_add(1, Ordering::AcqRel);
shared
.directional
.store(roles.directional, Ordering::Release);
*shared
.snapshot
.write()
.unwrap_or_else(PoisonError::into_inner) = snapshot.clone();
Ok(snapshot)
})
};
let link = Arc::new(Link::start(
system,
port,
Box::new(FerrumFrames::new(
api == FerrumApi::Software,
shared.seen.clone(),
)),
target,
reprobe,
sink.clone(),
)?);
let poller = (api == FerrumApi::Software)
.then(|| Poller::new(shared.clone(), link.clone(), sink.clone()));
let backend = FerrumBackend {
shared,
link,
poller: Mutex::new(None),
};
Ok((backend, snapshot, poller))
}
fn identify(probe: &Probe<'_>) -> Option<FerrumApi> {
let reply = probe.ask(
frames::VERSION_QUERY,
&mut FerrumFrames::probe(false),
|frame| frames::version(frame).is_some(),
WAIT,
);
frames::version(&reply.ok()??)
}
fn line(command: &str) -> Vec<u8> {
let mut bytes = command.as_bytes().to_vec();
bytes.extend_from_slice(END);
bytes
}
fn query(probe: &Probe<'_>, command: &str) -> Result<Option<Vec<u8>>> {
let reply = probe
.ask(
&line(command),
&mut FerrumFrames::probe(true),
|frame| frame.starts_with(command.as_bytes()) && frame.ends_with(PROMPT),
WAIT,
)?
.ok_or(Error::Timeout {
action: Action::Connect,
})?;
Ok(frames::result(&reply, true).map(<[u8]>::to_vec))
}
fn describe(probe: &Probe<'_>, api: FerrumApi) -> Result<Roles> {
match api {
FerrumApi::Legacy => {
probe.say(&line("km.buttons(0)"))?;
Ok(Roles {
mouse: true,
keyboard: false,
directional: false,
peripheral: None,
firmware: None,
})
}
FerrumApi::Software => {
for stream in ["buttons", "axes", "keys"] {
query(probe, &format!("km.{stream}(0)"))?;
}
match query(probe, "mouse_proxy.stop()") {
Ok(_) | Err(Error::Timeout { .. }) => {}
Err(error) => return Err(error),
}
let has = |command| {
bit(query(probe, command)?.as_deref()).ok_or_else(|| Error::Unsupported {
action: Action::Connect,
family: Family::Ferrum,
reason: format!(
"the Ferrum app doesn't answer {command}, so anybox can't tell what's plugged into the box. update the app"
)
.into(),
})
};
let mut roles = Roles {
mouse: has("km.has_mouse()")?,
keyboard: has("km.has_keyboard()")?,
directional: bit(query(probe, "km.lock_mx+()")?.as_deref()).is_some(),
peripheral: None,
firmware: match query(probe, "ferrum.version()") {
Ok(result) => app_version(result.as_deref()),
Err(Error::Timeout { .. }) => None,
Err(error) => return Err(error),
},
};
roles.peripheral = peripheral(&roles, |command| query(probe, command))?;
Ok(roles)
}
}
}
pub(crate) fn peripheral(
roles: &Roles,
mut ask: impl FnMut(&str) -> Result<Option<Vec<u8>>>,
) -> Result<Option<UsbIds>> {
if !roles.mouse && !roles.keyboard {
return Ok(None);
}
let mut id = |name: &str| match ask(&format!("device.{name}()")) {
Ok(result) => Ok(usb_id(result.as_deref(), name)),
Err(Error::Timeout { .. }) => Ok(None),
Err(error) => Err(error),
};
Ok(id("VID")?
.zip(id("PID")?)
.map(|(vid, pid)| UsbIds::new(vid, pid)))
}
pub(crate) fn usb_id(result: Option<&[u8]>, name: &str) -> Option<u16> {
let hex = std::str::from_utf8(result?)
.ok()?
.strip_prefix(name)?
.strip_prefix('=')?;
(hex.len() == 4 && hex.bytes().all(|b| b.is_ascii_hexdigit()))
.then(|| u16::from_str_radix(hex, 16).ok())
.flatten()
}
pub(crate) fn app_version(result: Option<&[u8]>) -> Option<Firmware> {
let text = std::str::from_utf8(result?).ok()?.trim();
let numbers: Option<Vec<u16>> = text
.strip_prefix('v')
.and_then(|rest| rest.split('.').map(|n| n.parse().ok()).collect());
match numbers.as_deref() {
Some(&[major, minor, patch]) => Some(Firmware::Version {
major,
minor,
patch,
}),
_ if text.starts_with('v') => Some(Firmware::Text(text.to_owned())),
_ => None,
}
}
pub(crate) fn bit(result: Option<&[u8]>) -> Option<bool> {
match result? {
b"1" => Some(true),
b"0" => Some(false),
_ => None,
}
}
pub(crate) fn snapshot(api: FerrumApi, roles: &Roles) -> Snapshot {
let mut info = Info::new(Family::Ferrum);
info.ferrum_api = Some(api);
info.peripheral = roles.peripheral;
info.firmware = roles.firmware.clone();
info.model = Some(
match api {
FerrumApi::Software => "Ferrum",
FerrumApi::Legacy => "Ferrum legacy",
}
.into(),
);
let released = || {
Capability::native().with(Quirk::ForcedUpOnRelease {
min: FORCED_UP.0,
max: FORCED_UP.1,
})
};
let tap = || {
released()
.with(Quirk::BoxClickTiming {
min: CLICK_HOLD.0,
max: CLICK_HOLD.1,
})
.with(Quirk::FixedClickTiming)
};
let mouse = [
Action::Move,
Action::Press,
Action::Release,
Action::Click,
Action::Wheel,
Action::ButtonEvents,
Action::MotionEvents,
Action::LockButton,
Action::LockAxis,
];
let keyboard = [
Action::KeyPress,
Action::KeyRelease,
Action::KeyTap,
Action::KeyEvents,
Action::LockKey,
Action::KeyChord,
];
let mut capabilities = if roles.mouse {
Capabilities::new(BUTTONS.to_vec())
.set(Action::Move, Capability::native())
.set(
Action::Press,
Capability::native().with(Quirk::PhysicalOverride),
)
.set(Action::Release, released())
.set(Action::Click, tap())
.set(
Action::Wheel,
Capability::native().with(Quirk::NotchesPerCommand(NOTCHES)),
)
.set(Action::ButtonEvents, Capability::native())
.set(Action::LockButton, Capability::native())
.set(
Action::MotionEvents,
match api {
FerrumApi::Software => Capability::native(),
FerrumApi::Legacy => Capability::unsupported(
"the legacy port only reports buttons, motion needs the Ferrum app's port",
),
},
)
.set(
Action::LockAxis,
if roles.directional {
Capability::native()
} else {
Capability::native().with(Quirk::WholeAxisOnly)
},
)
} else {
let none = Capability::unsupported(
"the Ferrum app has no device set as a mouse right now, or the box isn't plugged into this PC, set one or plug the box back in and it shows up by itself",
);
mouse
.into_iter()
.fold(Capabilities::new(Vec::new()), |capabilities, action| {
capabilities.set(action, none.clone())
})
};
if roles.mouse {
capabilities = capabilities.with_axes(vec![Axis::X, Axis::Y, Axis::Wheel]);
}
capabilities = capabilities.set(
Action::Ferrum(FerrumAction::CatchXy),
match (api, roles.mouse) {
(FerrumApi::Legacy, _) => Capability::unsupported(
"the legacy port has no catch_xy, it needs the Ferrum app's port",
),
(FerrumApi::Software, true) => Capability::native(),
(FerrumApi::Software, false) => Capability::unsupported(
"the Ferrum app has no device set as a mouse right now, set one and it shows up by itself",
),
},
);
capabilities = match api {
FerrumApi::Software if roles.keyboard => capabilities
.set(
Action::KeyPress,
Capability::native().with(Quirk::PhysicalOverride),
)
.set(Action::KeyRelease, released())
.set(Action::KeyTap, tap())
.set(Action::KeyEvents, Capability::native())
.set(Action::LockKey, Capability::native())
.set(
Action::KeyChord,
released().with(Quirk::BoxClickTiming {
min: CLICK_HOLD.0,
max: CLICK_HOLD.1,
}),
),
FerrumApi::Software => {
let none = Capability::unsupported(
"the Ferrum app has no device set as a keyboard right now, or the box isn't plugged into this PC, set one or plug the box back in and it shows up by itself",
);
keyboard
.into_iter()
.fold(capabilities, |capabilities, action| {
capabilities.set(action, none.clone())
})
}
FerrumApi::Legacy => {
let none = Capability::unsupported(
"the legacy port has no keyboard commands, only the Ferrum app's port has them",
);
keyboard
.into_iter()
.fold(capabilities, |capabilities, action| {
capabilities.set(action, none.clone())
})
}
};
let no_media = Capability::unsupported("Ferrum has no media keys, only keyboard keys");
let capabilities = capabilities
.set(Action::TypeText, Capability::unsupported(NO_TYPING))
.set(
Action::ForceReleaseKey,
Capability::unsupported(
"it isn't known yet whether Ferrum's key mask lets go of a key that's already held",
),
)
.set(Action::MediaPress, no_media.clone())
.set(Action::MediaRelease, no_media.clone())
.set(Action::MediaTap, no_media.clone())
.set(Action::MediaEvents, no_media)
.set(
Action::MoveNow,
Capability::unsupported("Ferrum only has km.move, there's no move that skips ahead"),
)
.set(
Action::DiscardPendingMoves,
Capability::unsupported("Ferrum can't drop moves it already took"),
)
.set(
Action::Pan,
Capability::unsupported("Ferrum has no horizontal scroll"),
)
.set(
Action::ScaleAxis,
Capability::unsupported("Ferrum can only block an axis, not scale it"),
);
Snapshot::new(info, capabilities)
}
const BUTTONS: [Button; 5] = [
Button::Left,
Button::Right,
Button::Middle,
Button::Side1,
Button::Side2,
];
pub(crate) struct Shared {
api: FerrumApi,
snapshot: RwLock<Snapshot>,
directional: AtomicBool,
streams: Mutex<InputKinds>,
seen: Arc<Mutex<frames::Seen>>,
generation: AtomicU64,
stop: AtomicBool,
}
impl Shared {
fn snapshot(&self) -> Snapshot {
self.snapshot
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}
pub(crate) struct FerrumBackend {
shared: Arc<Shared>,
link: Arc<Link>,
poller: Mutex<Option<JoinHandle<()>>>,
}
fn button_name(button: Button) -> Option<&'static str> {
Some(match button {
Button::Left => "left",
Button::Right => "right",
Button::Middle => "middle",
Button::Side1 => "side1",
Button::Side2 => "side2",
_ => return None,
})
}
fn button_number(button: Button) -> Option<u8> {
BUTTONS
.iter()
.position(|&known| known == button)
.and_then(|at| u8::try_from(at).ok())
}
fn lock_name(slot: LockSlot, whole: bool) -> Option<String> {
Some(match slot {
LockSlot::Button(button) => match button {
Button::Left => "ml".into(),
Button::Right => "mr".into(),
Button::Middle => "mm".into(),
Button::Side1 => "ms1".into(),
Button::Side2 => "ms2".into(),
_ => return None,
},
LockSlot::Axis(axis, sign) => {
let axis = match axis {
Axis::X => "mx",
Axis::Y => "my",
Axis::Wheel => "mw",
_ => return None,
};
let sign = match sign {
_ if whole => "",
Sign::Positive => "+",
Sign::Negative => "-",
};
format!("{axis}{sign}")
}
LockSlot::Key(_) => return None,
})
}
fn stream_name(kind: InputKind) -> Option<&'static str> {
match kind {
InputKind::Buttons => Some("buttons"),
InputKind::Motion => Some("axes"),
InputKind::Keys => Some("keys"),
InputKind::Media => None,
}
}
fn kind_on(kinds: InputKinds, kind: InputKind) -> bool {
match kind {
InputKind::Buttons => kinds.buttons,
InputKind::Motion => kinds.motion,
InputKind::Keys => kinds.keys,
InputKind::Media => kinds.media,
}
}
fn set_kind(kinds: &mut InputKinds, kind: InputKind, on: bool) {
match kind {
InputKind::Buttons => kinds.buttons = on,
InputKind::Motion => kinds.motion = on,
InputKind::Keys => kinds.keys = on,
InputKind::Media => kinds.media = on,
}
}
impl FerrumBackend {
fn check(&self, command: Command) -> Result<()> {
if matches!(
command,
Command::Release(_)
| Command::KeyRelease(_)
| Command::Lock { keep: UNLOCKED, .. }
| Command::KeyChord {
kind: ChordKind::Release,
..
}
) {
return Ok(());
}
let action = command.action();
if self
.shared
.snapshot()
.capabilities
.get(action)
.is_supported()
{
Ok(())
} else {
Err(Error::Unsupported {
action,
family: Family::Ferrum,
reason: "the box changed during this call, check its snapshot again".into(),
})
}
}
fn encode(command: Command, whole: bool) -> Option<Vec<u8>> {
let text = match command {
Command::Move(dx, dy) => format!("km.move({dx},{dy})"),
Command::Press(button) => format!("km.{}(1)", button_name(button)?),
Command::Release(button) => format!("km.{}(0)", button_name(button)?),
Command::Click { button, hold: None } => {
format!("km.click({})", button_number(button)?)
}
Command::Wheel(notches) => format!("km.wheel({notches})"),
Command::KeyPress(key) => format!("km.down({})", key.0),
Command::KeyRelease(key) => format!("km.up({})", key.0),
Command::KeyTap { key, hold: None } => format!("km.press({})", key.0),
Command::KeyChord { kind, keys } => format!(
"km.{}({})",
match kind {
ChordKind::Press => "multidown",
ChordKind::Release => "multiup",
ChordKind::Tap => "multipress",
},
keys.as_slice()
.iter()
.map(|key| key.0.to_string())
.collect::<Vec<_>>()
.join(",")
),
Command::Lock {
slot: LockSlot::Key(key),
keep,
} if keep == LOCKED || keep == UNLOCKED => {
format!("km.mask({},{})", key.0, u8::from(keep == LOCKED))
}
Command::Lock { slot, keep } if keep == LOCKED || keep == UNLOCKED => {
format!(
"km.lock_{}({})",
lock_name(slot, whole)?,
u8::from(keep == LOCKED)
)
}
_ => return None,
};
Some(line(&text))
}
fn request(&self, bytes: Vec<u8>, until: Instant) -> Result<()> {
let echo = bytes.strip_suffix(END).unwrap_or(&bytes);
let expect = (self.shared.api == FerrumApi::Software).then(|| frames::key(echo));
self.link.request_until(bytes, expect, until).map(|_| ())
}
fn switch(&self, kind: InputKind, on: bool, until: Instant) -> Result<()> {
let Some(name) = stream_name(kind) else {
return Ok(());
};
if on {
let mut seen = frames::lock(&self.shared.seen);
match kind {
InputKind::Buttons => seen.forget_buttons(),
InputKind::Keys => seen.forget_keys(),
InputKind::Motion | InputKind::Media => {}
}
}
self.request(line(&format!("km.{name}({})", u8::from(on))), until)
}
}
const KINDS: [InputKind; 3] = [InputKind::Buttons, InputKind::Motion, InputKind::Keys];
impl Backend for FerrumBackend {
fn send(&self, command: Command) -> Result<()> {
self.check(command)?;
let whole = !self.shared.directional.load(Ordering::Acquire);
if whole
&& let Command::Lock {
slot: LockSlot::Axis(_, Sign::Negative),
keep,
} = command
&& keep != UNLOCKED
{
return Ok(());
}
let Some(bytes) = Self::encode(command, whole) else {
let action = command.action();
return Err(Error::Unsupported {
action,
family: Family::Ferrum,
reason: self.shared.snapshot().capabilities.get(action).reason(),
});
};
self.request(bytes, Instant::now() + crate::backend::WRITE_DEADLINE)
}
fn set_streams(&self, kinds: InputKinds) -> std::result::Result<(), StreamFailure> {
let until = Instant::now() + crate::backend::WRITE_DEADLINE;
let mut streams = lock(&self.shared.streams);
for kind in KINDS {
let on = kind_on(kinds, kind);
if kind_on(*streams, kind) == on {
continue;
}
if let Err(error) = self.switch(kind, on, until) {
return Err(StreamFailure {
failed: kinds,
error,
});
}
set_kind(&mut streams, kind, on);
}
Ok(())
}
fn restart_stream(&self, kind: InputKind) -> Result<()> {
let streams = lock(&self.shared.streams);
if kind_on(*streams, kind) {
self.switch(kind, true, Instant::now() + crate::backend::WRITE_DEADLINE)
} else {
Ok(())
}
}
fn type_piece(&self, _piece: &[u8], _typing: &mut Typing) -> Result<()> {
Err(Error::Unsupported {
action: Action::TypeText,
family: Family::Ferrum,
reason: NO_TYPING.into(),
})
}
fn release_all(&self, held: &[Usage]) -> Result<()> {
let mut first = Ok(());
for &usage in held {
let released = self.send(usage.release());
if first.is_ok() {
first = released;
}
}
first
}
fn unlock_all(&self, slots: &[LockSlot]) -> Result<()> {
let mut first = Ok(());
for &slot in slots {
let unlocked = self.send(Command::Lock {
slot,
keep: UNLOCKED,
});
if matches!(unlocked, Err(Error::Disconnected)) {
return unlocked;
}
if first.is_ok() {
first = unlocked;
}
}
first
}
fn shutdown(&self, deadline: Duration) {
let until = Instant::now() + deadline;
let streams = *lock(&self.shared.streams);
for kind in KINDS {
if kind_on(streams, kind) {
let _ = self.switch(kind, false, until);
}
}
self.shared.stop.store(true, Ordering::Release);
if let Some(poller) = lock(&self.poller).take() {
while !poller.is_finished() && Instant::now() < until {
thread::sleep(Duration::from_millis(5));
}
if poller.is_finished() {
let _ = poller.join();
}
}
self.link
.shutdown(until.saturating_duration_since(Instant::now()));
}
fn extension(&self, request: Extension) -> Result<ExtensionReply> {
let Extension::FerrumCatchXy {
window,
with_software,
} = request
else {
return Err(Error::Unsupported {
action: request.action(),
family: Family::Ferrum,
reason: "a Ferrum doesn't have this".into(),
});
};
let command = format!("km.catch_xy({}, {with_software})", window.as_millis());
let reply = self
.link
.request(line(&command), Some(frames::key(command.as_bytes())))?
.ok_or(Error::Disconnected)?;
frames::result(&reply, true)
.and_then(xy)
.map(|(x, y)| ExtensionReply::Xy(x, y))
.ok_or_else(|| Error::Rejected {
action: request.action(),
detail: "it answered something that isn't an (x, y)".into(),
})
}
fn as_any(&self) -> &dyn Any {
self
}
}
pub(crate) fn xy(result: &[u8]) -> Option<(i32, i32)> {
let text = std::str::from_utf8(result).ok()?.trim();
let (x, y) = text.strip_prefix('(')?.strip_suffix(')')?.split_once(',')?;
Some((x.trim().parse().ok()?, y.trim().parse().ok()?))
}
#[cfg(any(test, feature = "mock"))]
pub(crate) mod mock;
#[cfg(test)]
mod tests;