use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, RwLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crate::backend::{
Backend, Command, InputKind, InputKinds, LOCKED, LockSlot, Sink, StreamFailure, UNLOCKED,
Usage, WRITE_DEADLINE,
};
use crate::capability::{
Action, Axis, Button, Capabilities, Capability, Family, Firmware, Info, Quirk, Snapshot,
StableId,
};
use crate::error::{Error, Result};
use crate::transport::link::{Link, Target};
use crate::transport::probe::Probe;
use crate::transport::{Candidate, System, is_ch343};
mod frames;
mod poller;
use frames::{MakcuFrames, frame, parts, refused};
pub(crate) use poller::Poller;
pub(crate) const BAUDS: [u32; 2] = [4_000_000, 115_200];
pub(crate) const MIN_BUILD: u32 = 4085;
const NEXT_MAJOR: u32 = 5000;
const MOVE_NOW_BUILD: u32 = 4086;
const TEXT_WAIT: Duration = Duration::from_millis(500);
const BINARY_WAIT: Duration = Duration::from_millis(250);
const TRIES: usize = 2;
const MAX_HOLD: Duration = Duration::from_millis(5000);
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()],
}
}
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)?;
*lock(&backend.poller) = Some(poller.spawn()?);
Ok((Box::new(backend), snapshot))
}
pub(crate) fn attach(
system: Arc<dyn System>,
candidate: &Candidate,
sink: Sink,
) -> Result<(MakcuBackend, Snapshot, Poller)> {
let path = candidate.path.as_path();
let usb = candidate.usb.clone();
if usb.as_ref().is_some_and(|usb| !is_ch343(usb)) {
return Err(not_found(path));
}
let id = usb
.as_ref()
.and_then(|usb| usb.serial.clone())
.map(StableId::UsbSerial);
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);
if !identify(&probe) {
continue;
}
let snapshot = describe(&probe, id.clone())?;
for kind in [frames::MOUSE, frames::KEYBOARD] {
port.write_all(&frame(frames::INPUT_STREAM, &[kind, 0]))
.map_err(|_| Error::Disconnected)?;
}
let mut bauds = BAUDS.to_vec();
bauds.rotate_left(at);
let target = Target {
family: Family::MakcuV4,
path: path.to_path_buf(),
usb,
id: id.clone(),
bauds,
};
return start(system, port, target, snapshot, sink);
}
Err(not_found(path))
}
fn start(
system: Arc<dyn System>,
port: Box<dyn crate::transport::Port>,
target: Target,
snapshot: Snapshot,
sink: Sink,
) -> Result<(MakcuBackend, Snapshot, Poller)> {
let shared = Arc::new(Shared {
snapshot: RwLock::new(snapshot.clone()),
expect: Mutex::new(Expect::default()),
streams: Mutex::new(()),
buttons: Mutex::new(()),
key_masks: Mutex::new(HashSet::new()),
last: Arc::default(),
stop: AtomicBool::new(false),
});
let id = target.id.clone();
let reprobe = {
let shared = shared.clone();
Box::new(move |probe: &Probe<'_>| {
if !identify(probe) {
return Err(Error::Disconnected);
}
let snapshot = describe(probe, id.clone())?;
shared.last.forget_locks();
lock(&shared.key_masks).clear();
let mut expect = lock(&shared.expect);
*expect = Expect {
generation: expect.generation + 1,
..Expect::default()
};
*shared
.snapshot
.write()
.unwrap_or_else(PoisonError::into_inner) = snapshot.clone();
Ok(snapshot)
})
};
let link = Arc::new(Link::start(
system,
port,
Box::new(MakcuFrames::binary(shared.last.clone())),
target,
reprobe,
sink.clone(),
)?);
let poller = Poller::new(shared.clone(), link.clone(), sink);
let backend = MakcuBackend {
shared,
link,
poller: Mutex::new(None),
};
Ok((backend, snapshot, poller))
}
fn identify(probe: &Probe<'_>) -> bool {
(0..TRIES).any(|_| {
matches!(
probe.ask(
frames::VERSION_QUERY,
&mut MakcuFrames::probe(),
|frame| frames::says_makcu(frame),
TEXT_WAIT,
),
Ok(Some(_))
)
})
}
fn get(probe: &Probe<'_>, command: u8, len: usize) -> Option<Vec<u8>> {
(0..TRIES).find_map(|_| {
let reply = probe.ask(
&frame(command, &[]),
&mut MakcuFrames::probe(),
|frame| {
parts(frame).is_some_and(|(cmd, payload)| {
cmd == command && (payload.len() == len || refused(payload))
})
},
BINARY_WAIT,
);
let frame = reply.ok().flatten()?;
let (_, payload) = parts(&frame)?;
(payload.len() == len).then(|| payload.to_vec())
})
}
fn describe(probe: &Probe<'_>, id: Option<StableId>) -> Result<Snapshot> {
let Some(build) = get(probe, frames::FIRMWARE_VERSION, 4) else {
return Err(Error::FirmwareTooOld {
family: Family::MakcuV4,
found: Firmware::Text("V3 or older".into()),
needed: Firmware::Build(MIN_BUILD),
});
};
let build = u32::from_le_bytes([build[0], build[1], build[2], build[3]]);
if build < MIN_BUILD {
return Err(Error::FirmwareTooOld {
family: Family::MakcuV4,
found: Firmware::Build(build),
needed: Firmware::Build(MIN_BUILD),
});
}
if build >= NEXT_MAJOR {
return Err(Error::LibraryTooOld {
family: Family::MakcuV4,
found: Firmware::Build(build),
});
}
let kinds = get(probe, frames::DEVICE, 1).ok_or(Error::Timeout {
action: Action::Connect,
})?[0];
Ok(snapshot(build, kinds, id))
}
pub(crate) fn snapshot(build: u32, kinds: u8, id: Option<StableId>) -> Snapshot {
let mut info = Info::new(Family::MakcuV4);
info.model = Some("MAKCU".into());
info.firmware = Some(Firmware::Build(build));
info.id = id;
let mouse = kinds & frames::MOUSE_ROUTED != 0;
let keyboard = kinds & frames::KEYBOARD_ROUTED != 0;
let capabilities = if mouse {
let move_now = if build >= MOVE_NOW_BUILD {
Capability::native()
} else {
Capability::unsupported(
"older builds can still smooth move_now, so update the box to V4.086 or later",
)
};
Capabilities::new(vec![
Button::Left,
Button::Right,
Button::Middle,
Button::Side1,
Button::Side2,
])
.set(Action::Move, Capability::native())
.set(Action::MoveNow, move_now)
.set(Action::Press, Capability::native())
.set(Action::Release, Capability::native())
.set(Action::Wheel, Capability::native())
.set(
Action::Click,
Capability::native()
.with(Quirk::BoxClickTiming {
min: Duration::from_millis(35),
max: Duration::from_millis(75),
})
.with(Quirk::MaxHold(MAX_HOLD)),
)
.set(Action::ButtonEvents, Capability::native())
} else {
let none = Capability::unsupported(
"the box doesn't route a mouse right now, plug one in and it shows up by itself",
);
[
Action::Move,
Action::MoveNow,
Action::Press,
Action::Release,
Action::Wheel,
Action::Click,
Action::ButtonEvents,
]
.into_iter()
.fold(Capabilities::new(Vec::new()), |capabilities, action| {
capabilities.set(action, none.clone())
})
};
let capabilities = if keyboard {
capabilities
.set(Action::KeyPress, Capability::native())
.set(Action::KeyRelease, Capability::native())
.set(
Action::KeyTap,
Capability::native().with(Quirk::MaxHold(MAX_HOLD)),
)
.set(Action::KeyEvents, Capability::native())
} else {
let none = Capability::unsupported(
"the device behind the box has no keyboard interface right now",
);
capabilities
.set(Action::KeyPress, none.clone())
.set(Action::KeyRelease, none.clone())
.set(Action::KeyTap, none.clone())
.set(Action::KeyEvents, none)
};
let capabilities = if mouse {
capabilities
.with_axes(vec![Axis::X, Axis::Y, Axis::Wheel])
.set(Action::LockButton, Capability::native())
.set(Action::LockAxis, Capability::native())
} else {
let none = Capability::unsupported(
"the box doesn't route a mouse right now, plug one in and it shows up by itself",
);
capabilities
.set(Action::LockButton, none.clone())
.set(Action::LockAxis, none)
};
let capabilities = capabilities
.set(
Action::LockKey,
if keyboard {
Capability::native()
} else {
Capability::unsupported(
"the device behind the box has no keyboard interface right now",
)
},
)
.set(
Action::ScaleAxis,
Capability::unsupported("MAKCU can only block an axis, not scale it"),
);
let no_media = Capability::unsupported("MAKCU V4 has no media keys");
let capabilities = capabilities
.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::DiscardPendingMoves,
Capability::unsupported("MAKCU can't drop moves it already took"),
)
.set(
Action::Pan,
Capability::unsupported("MAKCU V4 has no horizontal scroll, km.pan only exists on V3"),
)
.set(
Action::MotionEvents,
Capability::unsupported("MAKCU doesn't report mouse motion"),
);
Snapshot::new(info, capabilities)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Stream {
Mouse,
Keyboard,
}
impl Stream {
pub(crate) fn kind(self) -> InputKind {
match self {
Self::Mouse => InputKind::Buttons,
Self::Keyboard => InputKind::Keys,
}
}
fn of(kind: InputKind) -> Option<Self> {
match kind {
InputKind::Buttons => Some(Self::Mouse),
InputKind::Keys => Some(Self::Keyboard),
InputKind::Motion | InputKind::Media => None,
}
}
}
#[derive(Clone, Copy, Default)]
pub(crate) struct StreamState {
pub(crate) wanted: bool,
pub(crate) confirmed: bool,
}
impl Expect {
pub(crate) fn stream(&mut self, kind: Stream) -> &mut StreamState {
match kind {
Stream::Mouse => &mut self.mouse,
Stream::Keyboard => &mut self.keys,
}
}
}
pub(crate) fn kind_byte(kind: Stream) -> u8 {
match kind {
Stream::Mouse => frames::MOUSE,
Stream::Keyboard => frames::KEYBOARD,
}
}
#[derive(Default)]
pub(crate) struct Expect {
held: HashMap<Button, bool>,
mouse: StreamState,
keys: StreamState,
generation: u64,
}
pub(crate) struct Shared {
snapshot: RwLock<Snapshot>,
expect: Mutex<Expect>,
streams: Mutex<()>,
buttons: Mutex<()>,
key_masks: Mutex<HashSet<u8>>,
last: Arc<frames::LastSent>,
stop: AtomicBool,
}
impl Shared {
fn snapshot(&self) -> Snapshot {
self.snapshot
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}
pub(crate) struct MakcuBackend {
shared: Arc<Shared>,
link: Arc<Link>,
poller: Mutex<Option<JoinHandle<()>>>,
}
fn hold_ms(hold: Duration) -> u32 {
let ms = hold.as_micros().div_ceil(1000).max(1);
u32::try_from(ms).unwrap_or(u32::MAX)
}
impl MakcuBackend {
fn check(&self, command: Command) -> Result<()> {
let button = match command {
Command::Release(_) | Command::KeyRelease(_) | Command::MediaRelease(_) => {
return Ok(());
}
Command::Lock { keep: UNLOCKED, .. } => return Ok(()),
Command::Lock {
slot: LockSlot::Button(button),
..
} => Some(button),
Command::Lock { .. } => None,
Command::Press(button) | Command::Click { button, .. } => Some(button),
Command::Move(..)
| Command::MoveNow(..)
| Command::Wheel(_)
| Command::Pan(_)
| Command::DiscardPendingMoves
| Command::KeyPress(_)
| Command::KeyTap { .. }
| Command::MediaPress(_)
| Command::MediaTap { .. } => None,
};
let action = command.action();
let snapshot = self.shared.snapshot();
let capability = snapshot.capabilities.get(action);
let supported = capability.is_supported()
&& button.is_none_or(|button| snapshot.capabilities.buttons().contains(&button));
if supported {
Ok(())
} else {
Err(Error::Unsupported {
action,
family: Family::MakcuV4,
reason: "the box changed during this call, check its snapshot again".into(),
})
}
}
fn encode(command: Command) -> Option<Vec<u8>> {
let button = |button| frames::button_id(button).map(|id| frames::LEFT + id);
Some(match command {
Command::Move(dx, dy) => frame(frames::MOVE, &xy(dx, dy)),
Command::MoveNow(dx, dy) => frame(frames::MOVE_NOW, &xy(dx, dy)),
Command::Press(b) => frame(button(b)?, &[1]),
Command::Release(b) => frame(button(b)?, &[0]),
Command::Wheel(notches) => frame(frames::WHEEL, ¬ches.to_le_bytes()),
Command::Click { button, hold } => {
let hold = hold.map_or(0, hold_ms);
let mut payload = vec![frames::button_id(button)? + 1, 1];
payload.extend_from_slice(&u16::try_from(hold).unwrap_or(u16::MAX).to_le_bytes());
frame(frames::CLICK, &payload)
}
Command::KeyPress(key) => frame(frames::KEY_DOWN, &[key.0]),
Command::KeyRelease(key) => frame(frames::KEY_UP, &[key.0]),
Command::KeyTap { key, hold: None } => frame(frames::KEY_PRESS, &[key.0]),
Command::KeyTap {
key,
hold: Some(hold),
} => {
let mut payload = vec![key.0];
payload.extend_from_slice(&hold_ms(hold).to_le_bytes());
frame(frames::KEY_PRESS, &payload)
}
Command::Lock {
slot: LockSlot::Key(key),
keep,
} if keep == LOCKED || keep == UNLOCKED => {
frame(frames::KEY_MASK, &[key.0, u8::from(keep == LOCKED)])
}
Command::Lock { slot, keep } if keep == LOCKED || keep == UNLOCKED => frame(
frames::LOCK,
&[frames::lock_target(slot)?, u8::from(keep == LOCKED)],
),
Command::Lock { .. } => return None,
Command::DiscardPendingMoves
| Command::Pan(_)
| Command::MediaPress(_)
| Command::MediaRelease(_)
| Command::MediaTap { .. } => return None,
})
}
fn stream(&self, kind: Stream, on: bool) -> Result<()> {
let _switching = lock(&self.shared.streams);
*lock(&self.shared.expect).stream(kind) = StreamState {
wanted: on,
confirmed: false,
};
let byte = kind_byte(kind);
let action = match kind {
Stream::Mouse => Action::ButtonEvents,
Stream::Keyboard => Action::KeyEvents,
};
let name = match kind {
Stream::Mouse => "button",
Stream::Keyboard => "key",
};
self.shared.last.set_stream(byte);
let state = u8::from(on);
self.link
.request(frame(frames::INPUT_STREAM, &[byte, state]), None)?;
let reply = self.link.request(
frame(frames::INPUT_STREAM, &[byte]),
Some(u32::from(frames::INPUT_STREAM)),
)?;
let payload = reply.as_deref().and_then(parts).map(|(_, payload)| payload);
match payload {
Some([got]) if *got == state => {
lock(&self.shared.expect).stream(kind).confirmed = on;
Ok(())
}
Some(payload) if refused(payload) => Err(Error::Rejected {
action,
detail: format!("it won't switch the {name} stream right now"),
}),
_ => Err(Error::Rejected {
action,
detail: format!(
"the {name} stream still isn't {} after asking",
if on { "on" } else { "off" }
),
}),
}
}
}
fn xy(dx: i16, dy: i16) -> [u8; 4] {
let [x0, x1] = dx.to_le_bytes();
let [y0, y1] = dy.to_le_bytes();
[x0, x1, y0, y1]
}
impl Backend for MakcuBackend {
fn send(&self, command: Command) -> Result<()> {
self.check(command)?;
let Some(bytes) = Self::encode(command) else {
let action = command.action();
return Err(Error::Unsupported {
action,
family: Family::MakcuV4,
reason: self.shared.snapshot().capabilities.get(action).reason(),
});
};
let _buttons = lock(&self.shared.buttons);
if let Command::Press(button) | Command::Release(button) | Command::Click { button, .. } =
command
&& let Some(id) = frames::button_id(button)
{
let pressed = !matches!(command, Command::Release(_));
self.shared.last.set(id, u8::from(pressed));
}
match command {
Command::KeyPress(key) | Command::KeyTap { key, .. } => {
self.shared.last.set_key(key.0, true)
}
Command::KeyRelease(key) => self.shared.last.set_key(key.0, false),
Command::Lock { slot, keep } => {
let state = if keep == UNLOCKED {
frames::LastSent::UNLOCKED
} else {
frames::LastSent::LOCKED
};
if let Some(target) = frames::lock_target(slot) {
self.shared.last.set_lock(target, state);
}
if let LockSlot::Key(key) = slot {
let mut masks = lock(&self.shared.key_masks);
if keep == UNLOCKED {
masks.remove(&key.0);
} else {
masks.insert(key.0);
}
}
}
_ => {}
}
let released = match command {
Command::Release(button) => {
Some((button, lock(&self.shared.expect).held.remove(&button)))
}
_ => None,
};
let sent = self.link.request(bytes, None);
match (command, &sent, released) {
(Command::Press(button), Ok(_), _) => {
lock(&self.shared.expect)
.held
.entry(button)
.or_insert(false);
}
(_, Err(_), Some((button, Some(seen)))) => {
lock(&self.shared.expect).held.insert(button, seen);
}
_ => {}
}
sent.map(|_| ())
}
fn set_streams(&self, kinds: InputKinds) -> std::result::Result<(), StreamFailure> {
let until = Instant::now() + WRITE_DEADLINE;
let mut failed = InputKinds::default();
let mut first = None;
for (kind, on) in [
(Stream::Mouse, kinds.buttons),
(Stream::Keyboard, kinds.keys),
] {
let current = *lock(&self.shared.expect).stream(kind);
if current.wanted == on && (current.confirmed || !on) {
continue;
}
if Instant::now() >= until {
if on {
*lock(&self.shared.expect).stream(kind) = StreamState {
wanted: true,
confirmed: false,
};
}
continue;
}
match self.stream(kind, on) {
Ok(()) => {}
Err(Error::Disconnected) => {
return Err(StreamFailure {
failed: kinds,
error: Error::Disconnected,
});
}
Err(_) if kind == Stream::Keyboard && !on => {}
Err(error) => {
let one = kind.kind().kinds();
failed.buttons |= one.buttons;
failed.keys |= one.keys;
first.get_or_insert(error);
}
}
}
match first {
None => Ok(()),
Some(error) => Err(StreamFailure { failed, error }),
}
}
fn restart_stream(&self, kind: InputKind) -> Result<()> {
let Some(kind) = Stream::of(kind) else {
return Ok(());
};
if lock(&self.shared.expect).stream(kind).wanted {
self.stream(kind, true)
} else {
Ok(())
}
}
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 still_locked = self
.link
.request_until(
frame(frames::INJECT_SNAPSHOT, &[]),
Some(u32::from(frames::INJECT_SNAPSHOT)),
until,
)
.ok()
.flatten()
.and_then(|reply| parts(&reply).map(|(_, payload)| frames::locked_targets(payload)))
.unwrap_or(0);
for target in 0..14u8 {
if still_locked & (1 << target) != 0
&& self.shared.last.lock(target) != frames::LastSent::NEVER
{
let _ = self
.link
.request_until(frame(frames::LOCK, &[target, 0]), None, until);
}
}
let masks: Vec<u8> = lock(&self.shared.key_masks).drain().collect();
for key in masks {
let _ = self
.link
.request_until(frame(frames::KEY_MASK, &[key, 0]), None, until);
}
let wanted = {
let mut expect = lock(&self.shared.expect);
[Stream::Mouse, Stream::Keyboard].map(|kind| (kind, expect.stream(kind).wanted))
};
for (kind, wanted) in wanted {
if wanted {
let _ = self.link.request_until(
frame(frames::INPUT_STREAM, &[kind_byte(kind), 0]),
None,
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 as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests;