mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 18:44:25 +08:00
feat(bluetooth-hid): 支持经典蓝牙 HID 后端
This commit is contained in:
16
libs/bluetooth-hid/Cargo.toml
Normal file
16
libs/bluetooth-hid/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "one-kvm-bluetooth-hid"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
|
||||
dbus = { version = "0.9", features = ["vendored"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "process"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dbus-crossroads = "0.5"
|
||||
dbus-tokio = "0.7"
|
||||
libc = "0.2"
|
||||
tracing = "0.1"
|
||||
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Explicit hardware regression: opens a 10-second pairing window without
|
||||
//! removing existing bonds or sending input. Stop the production HID first.
|
||||
use one_kvm_bluetooth_hid::{Action, Config, Peripheral};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), String> {
|
||||
let peripheral = Peripheral::start(Config {
|
||||
adapter: "hci0".into(),
|
||||
name: "One-KVM HID".into(),
|
||||
peer: None,
|
||||
})?;
|
||||
let result = async {
|
||||
for _ in 0..50 {
|
||||
if peripheral.status().initialized {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
let before = peripheral.status();
|
||||
if !before.initialized {
|
||||
return Err(format!("Initialization failed: {:?}", before.error));
|
||||
}
|
||||
if !before
|
||||
.devices
|
||||
.iter()
|
||||
.any(|d| Some(&d.address) == before.peer.as_ref() && d.paired)
|
||||
{
|
||||
return Err("This regression needs an existing bonded computer".into());
|
||||
}
|
||||
peripheral.action(Action::Pair(10)).await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let open = peripheral.status();
|
||||
println!("open: {}", serde_json::to_string(&open).unwrap());
|
||||
if open.pairing_seconds == 0 {
|
||||
return Err("Existing bond prematurely closed pairing".into());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
let expired = peripheral.status();
|
||||
println!("expired: {}", serde_json::to_string(&expired).unwrap());
|
||||
if expired.pairing_seconds != 0 || expired.peer != before.peer {
|
||||
return Err("Window did not expire while preserving the bonded peer".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
let cleanup = peripheral.shutdown().await;
|
||||
result.and(cleanup)
|
||||
}
|
||||
24
libs/bluetooth-hid/examples/probe.rs
Normal file
24
libs/bluetooth-hid/examples/probe.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Native hardware smoke check. Registers for 12 seconds, sends no input,
|
||||
//! opens no pairing window, then restores the adapter configuration.
|
||||
use one_kvm_bluetooth_hid::{Config, Peripheral};
|
||||
use std::time::Duration;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), String> {
|
||||
let peripheral = Peripheral::start(Config {
|
||||
adapter: "hci0".into(),
|
||||
name: "One-KVM HID".into(),
|
||||
peer: None,
|
||||
})?;
|
||||
let mut initialized = false;
|
||||
for _ in 0..12 {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let status = peripheral.status();
|
||||
initialized |= status.initialized;
|
||||
println!("{}", serde_json::to_string(&status).unwrap());
|
||||
}
|
||||
peripheral.shutdown().await?;
|
||||
if !initialized {
|
||||
return Err("Native BlueZ peripheral never initialized".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
236
libs/bluetooth-hid/src/agent.rs
Normal file
236
libs/bluetooth-hid/src/agent.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use crate::Shared;
|
||||
use dbus::{channel::MatchingReceiver, message::MatchRule, nonblock::Proxy, Path};
|
||||
use dbus_crossroads::Crossroads;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const PATH: &str = "/org/onekvm/bluetooth/agent";
|
||||
pub struct Agent {
|
||||
connection: Arc<dbus::nonblock::SyncConnection>,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
impl Agent {
|
||||
pub async fn register(shared: Arc<Shared>, adapter: String) -> Result<Self, String> {
|
||||
let (resource, connection) =
|
||||
dbus_tokio::connection::new_system_sync().map_err(|e| e.to_string())?;
|
||||
let task = tokio::spawn(async move {
|
||||
let _ = resource.await;
|
||||
});
|
||||
let mut cr = Crossroads::new();
|
||||
let iface = cr.register("org.bluez.Agent1", |b| {
|
||||
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||
b.method("Cancel", (), (), |_, _: &mut (), ()| Ok(()));
|
||||
let state = shared.clone();
|
||||
let name = adapter.clone();
|
||||
b.method(
|
||||
"RequestAuthorization",
|
||||
("device",),
|
||||
(),
|
||||
move |_, _: &mut (), (path,): (Path<'static>,)| {
|
||||
authorize(&state, &name, &path, true)
|
||||
},
|
||||
);
|
||||
let state = shared.clone();
|
||||
let name = adapter.clone();
|
||||
b.method(
|
||||
"RequestConfirmation",
|
||||
("device", "passkey"),
|
||||
(),
|
||||
move |_, _: &mut (), (path, _): (Path<'static>, u32)| {
|
||||
authorize(&state, &name, &path, true)
|
||||
},
|
||||
);
|
||||
b.method(
|
||||
"AuthorizeService",
|
||||
("device", "uuid"),
|
||||
(),
|
||||
move |_, _: &mut (), (path, uuid): (Path<'static>, String)| {
|
||||
if !matches!(uuid.as_str(), "00001124-0000-1000-8000-00805f9b34fb") {
|
||||
return Err(dbus::MethodErr::from((
|
||||
"org.bluez.Error.Rejected",
|
||||
"Not an HID service",
|
||||
)));
|
||||
}
|
||||
authorize(&shared, &adapter, &path, false)
|
||||
},
|
||||
);
|
||||
});
|
||||
cr.insert(PATH, &[iface], ());
|
||||
let profile = cr.register("org.bluez.Profile1", |b| {
|
||||
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||
b.method(
|
||||
"RequestDisconnection",
|
||||
("device",),
|
||||
(),
|
||||
|_, _: &mut (), (_device,): (Path<'static>,)| Ok(()),
|
||||
);
|
||||
b.method(
|
||||
"NewConnection",
|
||||
("device", "fd", "properties"),
|
||||
(),
|
||||
|_,
|
||||
_: &mut (),
|
||||
(_device, _fd, _props): (
|
||||
Path<'static>,
|
||||
dbus::arg::OwnedFd,
|
||||
dbus::arg::PropMap,
|
||||
)| {
|
||||
// This profile only publishes SDP. Our L2CAP sockets own both HID channels.
|
||||
Err::<(), _>(dbus::MethodErr::from((
|
||||
"org.bluez.Error.Rejected",
|
||||
"Unexpected profile connection",
|
||||
)))
|
||||
},
|
||||
);
|
||||
});
|
||||
cr.insert("/org/onekvm/bluetooth/profile", &[profile], ());
|
||||
connection.start_receive(
|
||||
MatchRule::new_method_call(),
|
||||
Box::new(move |msg, conn| {
|
||||
let _ = cr.handle_message(msg, conn);
|
||||
true
|
||||
}),
|
||||
);
|
||||
let agent = Self { connection, task };
|
||||
let proxy = Proxy::new(
|
||||
"org.bluez",
|
||||
"/org/bluez",
|
||||
Duration::from_secs(5),
|
||||
agent.connection.clone(),
|
||||
);
|
||||
let result: Result<(), dbus::Error> = proxy
|
||||
.method_call(
|
||||
"org.bluez.AgentManager1",
|
||||
"RegisterAgent",
|
||||
(Path::from(PATH), "NoInputNoOutput"),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
return Err(error.to_string());
|
||||
}
|
||||
let result: Result<(), dbus::Error> = proxy
|
||||
.method_call(
|
||||
"org.bluez.AgentManager1",
|
||||
"RequestDefaultAgent",
|
||||
(Path::from(PATH),),
|
||||
)
|
||||
.await;
|
||||
result.map_err(|e| e.to_string())?;
|
||||
let mut options: dbus::arg::PropMap = std::collections::HashMap::new();
|
||||
options.insert(
|
||||
"ServiceRecord".into(),
|
||||
dbus::arg::Variant(Box::new(crate::protocol::sdp())),
|
||||
);
|
||||
options.insert(
|
||||
"Role".into(),
|
||||
dbus::arg::Variant(Box::new("server".to_string())),
|
||||
);
|
||||
options.insert(
|
||||
"RequireAuthentication".into(),
|
||||
dbus::arg::Variant(Box::new(true)),
|
||||
);
|
||||
options.insert(
|
||||
"RequireAuthorization".into(),
|
||||
dbus::arg::Variant(Box::new(false)),
|
||||
);
|
||||
let result: Result<(), dbus::Error> = proxy
|
||||
.method_call(
|
||||
"org.bluez.ProfileManager1",
|
||||
"RegisterProfile",
|
||||
(
|
||||
Path::from("/org/onekvm/bluetooth/profile"),
|
||||
crate::protocol::HID_UUID,
|
||||
options,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
result.map_err(|e| format!("Register HID SDP: {e}"))?;
|
||||
Ok(agent)
|
||||
}
|
||||
}
|
||||
impl Drop for Agent {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
fn authorize(
|
||||
shared: &Shared,
|
||||
adapter: &str,
|
||||
path: &str,
|
||||
pairing: bool,
|
||||
) -> Result<(), dbus::MethodErr> {
|
||||
let rejected = || {
|
||||
dbus::MethodErr::from((
|
||||
"org.bluez.Error.Rejected",
|
||||
"Open pairing in One-KVM or select the paired computer",
|
||||
))
|
||||
};
|
||||
let prefix = format!("/org/bluez/{adapter}/dev_");
|
||||
let address = path
|
||||
.strip_prefix(&prefix)
|
||||
.ok_or_else(rejected)?
|
||||
.replace('_', ":");
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
if pairing
|
||||
&& !state
|
||||
.pairing_until
|
||||
.is_some_and(|until| until > Instant::now())
|
||||
{
|
||||
return Err(rejected());
|
||||
}
|
||||
if state.peer.as_ref().is_some_and(|peer| peer != &address) {
|
||||
return Err(rejected());
|
||||
}
|
||||
if state.peer.is_none() {
|
||||
if !pairing || state.bonded_before_pairing.contains(&address) {
|
||||
return Err(rejected());
|
||||
}
|
||||
state.peer = Some(address);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn existing_unrelated_bond_cannot_be_adopted_by_agent_callback() {
|
||||
let shared = Shared::new(None);
|
||||
{
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state
|
||||
.bonded_before_pairing
|
||||
.insert("10:6F:D9:66:97:88".into());
|
||||
}
|
||||
assert!(authorize(
|
||||
&shared,
|
||||
"hci0",
|
||||
"/org/bluez/hci0/dev_10_6F_D9_66_97_88",
|
||||
true
|
||||
)
|
||||
.is_err());
|
||||
assert!(shared.state.lock().unwrap().peer.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn only_pair_during_window_and_pin_first_peer() {
|
||||
let state = Shared::new(None);
|
||||
let first = "/org/bluez/hci0/dev_10_6F_D9_66_97_88";
|
||||
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||
state.state.lock().unwrap().pairing_until = Some(Instant::now() + Duration::from_secs(60));
|
||||
assert!(authorize(&state, "hci0", first, true).is_ok());
|
||||
assert!(authorize(
|
||||
&state,
|
||||
"hci0",
|
||||
"/org/bluez/hci0/dev_10_6F_D9_66_97_89",
|
||||
true
|
||||
)
|
||||
.is_err());
|
||||
assert!(authorize(&state, "hci1", first, true).is_err());
|
||||
state.state.lock().unwrap().pairing_until = None;
|
||||
assert!(authorize(&state, "hci0", first, false).is_ok());
|
||||
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||
}
|
||||
}
|
||||
145
libs/bluetooth-hid/src/bonds.rs
Normal file
145
libs/bluetooth-hid/src/bonds.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
//! Only bonds explicitly owned by this HID peripheral may be removed.
|
||||
use bluer::{Adapter, Session};
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Bond {
|
||||
pub adapter: String,
|
||||
pub peer: String,
|
||||
pub pending: bool,
|
||||
}
|
||||
pub type Operation<'a, T> = Pin<Box<dyn Future<Output = Result<T, String>> + Send + 'a>>;
|
||||
pub trait BondStore: Send + Sync {
|
||||
fn list(&self) -> Operation<'_, Vec<Bond>>;
|
||||
fn save(&self, bond: Bond) -> Operation<'_, ()>;
|
||||
fn remove(&self, bond: Bond) -> Operation<'_, ()>;
|
||||
}
|
||||
|
||||
pub async fn clean_adapter(store: &dyn BondStore, adapter: &Adapter) -> Result<(), String> {
|
||||
let address = adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string();
|
||||
clean_owned(store, &address, |peer| async move {
|
||||
let peer = peer.parse().map_err(|_| "Invalid stored HID peer")?;
|
||||
if adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.contains(&peer)
|
||||
{
|
||||
adapter
|
||||
.remove_device(peer)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn clean_owned<F, Fut>(
|
||||
store: &dyn BondStore,
|
||||
address: &str,
|
||||
mut remove: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnMut(String) -> Fut,
|
||||
Fut: Future<Output = Result<(), String>>,
|
||||
{
|
||||
for bond in store
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|b| b.pending && b.adapter == address)
|
||||
{
|
||||
remove(bond.peer.clone()).await?;
|
||||
// Keep the tombstone until BlueZ confirms removal; retry safely after interruption.
|
||||
store.remove(bond).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset(store: &dyn BondStore, explicit: Vec<Bond>) -> Result<(), String> {
|
||||
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||
let mut owned = store.list().await?;
|
||||
owned.extend(explicit);
|
||||
for mut bond in owned {
|
||||
bond.pending = true;
|
||||
store.save(bond).await?;
|
||||
}
|
||||
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||
clean_adapter(store, &session.adapter(&name).map_err(|e| e.to_string())?).await?;
|
||||
}
|
||||
// Records for absent adapters remain pending and are cleaned before their next use.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
#[derive(Default)]
|
||||
struct Memory(Mutex<Vec<Bond>>);
|
||||
impl BondStore for Memory {
|
||||
fn list(&self) -> Operation<'_, Vec<Bond>> {
|
||||
Box::pin(async { Ok(self.0.lock().unwrap().clone()) })
|
||||
}
|
||||
fn save(&self, bond: Bond) -> Operation<'_, ()> {
|
||||
Box::pin(async move {
|
||||
let mut rows = self.0.lock().unwrap();
|
||||
rows.retain(|b| b.adapter != bond.adapter || b.peer != bond.peer);
|
||||
rows.push(bond);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
fn remove(&self, bond: Bond) -> Operation<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.0.lock().unwrap().retain(|b| b != &bond);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
fn bond(adapter: &str, peer: &str, pending: bool) -> Bond {
|
||||
Bond {
|
||||
adapter: adapter.into(),
|
||||
peer: peer.into(),
|
||||
pending,
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn cleanup_only_removes_owned_pending_bonds_on_the_selected_hardware() {
|
||||
let current = bond("adapter-A", "host-1", true);
|
||||
let absent = bond("adapter-B", "host-2", true);
|
||||
let retained = bond("adapter-A", "host-3", false);
|
||||
let store = Memory(Mutex::new(vec![current, absent.clone(), retained.clone()]));
|
||||
let removed = Mutex::new(Vec::new());
|
||||
clean_owned(&store, "adapter-A", |peer| {
|
||||
removed.lock().unwrap().push(peer);
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(*removed.lock().unwrap(), vec!["host-1"]);
|
||||
assert_eq!(store.list().await.unwrap(), vec![absent, retained]);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn cleanup_failure_keeps_tombstone_and_is_reported() {
|
||||
let record = bond("adapter-A", "host-1", true);
|
||||
let store = Memory(Mutex::new(vec![record.clone()]));
|
||||
assert_eq!(
|
||||
clean_owned(&store, "adapter-A", |_| async {
|
||||
Err("BlueZ denied removal".into())
|
||||
})
|
||||
.await
|
||||
.unwrap_err(),
|
||||
"BlueZ denied removal"
|
||||
);
|
||||
assert_eq!(store.list().await.unwrap(), vec![record]);
|
||||
clean_owned(&store, "adapter-A", |_| async { Ok(()) })
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.list().await.unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
135
libs/bluetooth-hid/src/controller.rs
Normal file
135
libs/bluetooth-hid/src/controller.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! Exclusive controller ownership. No BLE switching or power cycling.
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
|
||||
process::Stdio,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
pub struct Controller {
|
||||
_lock: File,
|
||||
adapter: String,
|
||||
old_class: (u8, u8),
|
||||
old_connectable: bool,
|
||||
}
|
||||
async fn run_once(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||
let mut cmd = Command::new("btmgmt");
|
||||
cmd.arg("--index")
|
||||
.arg(adapter.trim_start_matches("hci"))
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||
// BlueZ 5.55's btmgmt exits on stdin EOF, even with a command argument.
|
||||
// Keep a pipe open until it finishes; systemd normally supplies /dev/null.
|
||||
let stdin = child.stdin.take();
|
||||
let result = tokio::time::timeout(Duration::from_secs(5), child.wait_with_output())
|
||||
.await
|
||||
.map_err(|_| "btmgmt timeout")?
|
||||
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||
drop(stdin);
|
||||
let text = String::from_utf8_lossy(&result.stdout).into_owned();
|
||||
if !result.status.success()
|
||||
|| !result.stderr.is_empty()
|
||||
|| text.to_lowercase().contains("failed")
|
||||
{
|
||||
return Err(format!(
|
||||
"btmgmt {}: {text} {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
));
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
async fn run(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||
// BlueZ updates the EIR/class asynchronously after registering SDP.
|
||||
// Kernel management rejects a simultaneous class change with Busy.
|
||||
for attempt in 0..5 {
|
||||
match run_once(adapter, args).await {
|
||||
Err(error) if error.contains("(Busy)") && attempt < 4 => {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
impl Controller {
|
||||
pub async fn acquire(adapter: &str) -> Result<Self, String> {
|
||||
let lock = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.mode(0o600)
|
||||
.custom_flags(libc::O_NOFOLLOW)
|
||||
.open(format!("/run/one-kvm-bluetooth-{adapter}.lock"))
|
||||
.map_err(|e| e.to_string())?;
|
||||
if unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
|
||||
return Err("Bluetooth adapter already owned by One-KVM".into());
|
||||
}
|
||||
let info = run(adapter, &["info"]).await?;
|
||||
let settings = info
|
||||
.lines()
|
||||
.find_map(|l| l.trim().strip_prefix("current settings:"))
|
||||
.ok_or("Cannot read Bluetooth settings")?;
|
||||
if !settings.split_whitespace().any(|s| s == "br/edr") {
|
||||
return Err(
|
||||
"Classic Bluetooth disabled; enable BR/EDR with btmgmt before using HID".into(),
|
||||
);
|
||||
}
|
||||
let class = info
|
||||
.split_whitespace()
|
||||
.skip_while(|s| *s != "class")
|
||||
.nth(1)
|
||||
.ok_or("Missing Bluetooth class")?;
|
||||
let class =
|
||||
u32::from_str_radix(class.trim_start_matches("0x"), 16).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
_lock: lock,
|
||||
adapter: adapter.into(),
|
||||
old_class: (((class >> 8) & 0x1f) as u8, (class & 0xfc) as u8),
|
||||
old_connectable: settings.split_whitespace().any(|s| s == "connectable"),
|
||||
})
|
||||
}
|
||||
pub async fn configure(&self) -> Result<(), String> {
|
||||
run(&self.adapter, &["class", "5", "192"]).await?;
|
||||
run(&self.adapter, &["connectable", "on"]).await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn restore(&self) -> Result<(), String> {
|
||||
let mut errors = vec![];
|
||||
if let Err(e) = run(
|
||||
&self.adapter,
|
||||
&[
|
||||
"class",
|
||||
&self.old_class.0.to_string(),
|
||||
&self.old_class.1.to_string(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
if let Err(e) = run(
|
||||
&self.adapter,
|
||||
&[
|
||||
"connectable",
|
||||
if self.old_connectable { "on" } else { "off" },
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
}
|
||||
964
libs/bluetooth-hid/src/lib.rs
Normal file
964
libs/bluetooth-hid/src/lib.rs
Normal file
@@ -0,0 +1,964 @@
|
||||
//! Classic Bluetooth HID peripheral: SDP + L2CAP. Linux/BlueZ only.
|
||||
mod agent;
|
||||
pub mod bonds;
|
||||
mod controller;
|
||||
mod protocol;
|
||||
|
||||
use bluer::{
|
||||
l2cap::{Security, SecurityLevel, SeqPacket, SeqPacketListener, Socket, SocketAddr},
|
||||
Adapter, Address, AddressType, Session,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot, watch, Mutex as AsyncMutex};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub adapter: String,
|
||||
pub name: String,
|
||||
pub peer: Option<String>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if !self
|
||||
.adapter
|
||||
.strip_prefix("hci")
|
||||
.is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()))
|
||||
{
|
||||
return Err("Bluetooth adapter must be hci followed by an index".into());
|
||||
}
|
||||
if self.name.is_empty() || self.name.len() > 64 || self.name.chars().any(char::is_control) {
|
||||
return Err(
|
||||
"Bluetooth name must contain 1–64 UTF-8 bytes without control characters".into(),
|
||||
);
|
||||
}
|
||||
if let Some(peer) = &self.peer {
|
||||
peer.parse::<Address>()
|
||||
.map_err(|_| "Invalid Bluetooth peer address")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Status {
|
||||
pub initialized: bool,
|
||||
pub connected: bool,
|
||||
pub ready: bool,
|
||||
pub adapter: String,
|
||||
pub adapter_address: String,
|
||||
pub peer: Option<String>,
|
||||
pub pairing_seconds: u32,
|
||||
pub control_connected: bool,
|
||||
pub interrupt_connected: bool,
|
||||
pub leds: u8,
|
||||
pub generation: u64,
|
||||
pub error: Option<String>,
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Device {
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub paired: bool,
|
||||
pub connected: bool,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Report {
|
||||
Keyboard,
|
||||
Mouse,
|
||||
Consumer,
|
||||
}
|
||||
impl Report {
|
||||
fn len(self) -> usize {
|
||||
match self {
|
||||
Self::Keyboard => 8,
|
||||
Self::Mouse => 4,
|
||||
Self::Consumer => 2,
|
||||
}
|
||||
}
|
||||
fn id(self) -> u8 {
|
||||
match self {
|
||||
Self::Keyboard => 1,
|
||||
Self::Mouse => 2,
|
||||
Self::Consumer => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct State {
|
||||
peer: Option<String>,
|
||||
pairing_until: Option<Instant>,
|
||||
bonded_before_pairing: HashSet<String>,
|
||||
}
|
||||
impl State {
|
||||
fn observe_bond(&mut self, address: &str, paired: bool) {
|
||||
// Just Works may complete without an Agent1 authorization callback.
|
||||
// Adopt only a new bond from our explicit window, respecting a pinned peer.
|
||||
if paired
|
||||
&& self.peer.is_none()
|
||||
&& self
|
||||
.pairing_until
|
||||
.is_some_and(|until| until > Instant::now())
|
||||
&& !self.bonded_before_pairing.contains(address)
|
||||
{
|
||||
self.peer = Some(address.to_owned());
|
||||
tracing::info!(
|
||||
peer = address,
|
||||
"Bluetooth HID selected newly bonded computer"
|
||||
);
|
||||
}
|
||||
}
|
||||
fn close_pairing(&mut self, paired: bool, fallback: Option<String>) {
|
||||
self.pairing_until = None;
|
||||
if !paired {
|
||||
self.peer = fallback;
|
||||
}
|
||||
}
|
||||
fn pairing_completed(&self, selected_paired: bool) -> bool {
|
||||
selected_paired
|
||||
&& self
|
||||
.peer
|
||||
.as_ref()
|
||||
.is_some_and(|peer| !self.bonded_before_pairing.contains(peer))
|
||||
}
|
||||
}
|
||||
struct Shared {
|
||||
state: Mutex<State>,
|
||||
}
|
||||
impl Shared {
|
||||
fn new(peer: Option<String>) -> Self {
|
||||
Self {
|
||||
state: Mutex::new(State {
|
||||
peer,
|
||||
pairing_until: None,
|
||||
bonded_before_pairing: HashSet::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
fn allowed(&self, address: Address) -> bool {
|
||||
self.state.lock().unwrap().peer.as_deref() == Some(address.to_string().as_str())
|
||||
}
|
||||
}
|
||||
pub enum Action {
|
||||
Pair(u32),
|
||||
ClosePairing,
|
||||
Forget,
|
||||
Disconnect,
|
||||
Reset,
|
||||
}
|
||||
enum Command {
|
||||
Report(Report, Vec<u8>),
|
||||
Action(Action),
|
||||
Stop,
|
||||
}
|
||||
struct Request {
|
||||
command: Command,
|
||||
result: oneshot::Sender<Result<(), String>>,
|
||||
expires: Instant,
|
||||
}
|
||||
pub struct Peripheral {
|
||||
tx: mpsc::Sender<Request>,
|
||||
status: watch::Receiver<Status>,
|
||||
task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
}
|
||||
impl Peripheral {
|
||||
pub fn start(config: Config) -> Result<Self, String> {
|
||||
Self::start_with_store(config, None)
|
||||
}
|
||||
pub fn start_with_store(
|
||||
config: Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
) -> Result<Self, String> {
|
||||
config.validate()?;
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
let (state_tx, status) = watch::channel(Status {
|
||||
adapter: config.adapter.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
let task = tokio::spawn(supervise(config, bonds, rx, state_tx));
|
||||
Ok(Self {
|
||||
tx,
|
||||
status,
|
||||
task: AsyncMutex::new(Some(task)),
|
||||
})
|
||||
}
|
||||
pub fn status(&self) -> Status {
|
||||
self.status.borrow().clone()
|
||||
}
|
||||
pub fn subscribe(&self) -> watch::Receiver<Status> {
|
||||
self.status.clone()
|
||||
}
|
||||
async fn request(&self, command: Command, duration: Duration) -> Result<(), String> {
|
||||
let (result, rx) = oneshot::channel();
|
||||
let request = Request {
|
||||
command,
|
||||
result,
|
||||
expires: Instant::now() + duration,
|
||||
};
|
||||
self.tx
|
||||
.try_send(request)
|
||||
.map_err(|_| "Bluetooth input queue unavailable or full".to_string())?;
|
||||
tokio::time::timeout(duration, rx)
|
||||
.await
|
||||
.map_err(|_| "Bluetooth operation timed out".to_string())?
|
||||
.map_err(|_| "Bluetooth worker stopped".to_string())?
|
||||
}
|
||||
pub async fn send(&self, report: Report, value: Vec<u8>) -> Result<(), String> {
|
||||
if value.len() != report.len() {
|
||||
return Err("Invalid HID report size".into());
|
||||
}
|
||||
self.request(Command::Report(report, value), Duration::from_millis(500))
|
||||
.await
|
||||
}
|
||||
pub async fn action(&self, action: Action) -> Result<(), String> {
|
||||
self.request(Command::Action(action), Duration::from_secs(15))
|
||||
.await
|
||||
}
|
||||
pub async fn shutdown(&self) -> Result<(), String> {
|
||||
if let Some(task) = self.task.lock().await.take() {
|
||||
let result = self.request(Command::Stop, Duration::from_secs(45)).await;
|
||||
// Do not abort: the worker must restore the radio before a backend switch.
|
||||
task.await.map_err(|e| e.to_string())?;
|
||||
result
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn listen(address: Address, psm: u16) -> Result<SeqPacketListener, String> {
|
||||
let socket = Socket::<SeqPacket>::new_seq_packet().map_err(|e| e.to_string())?;
|
||||
socket
|
||||
.set_security(Security {
|
||||
level: SecurityLevel::Medium,
|
||||
key_size: 0,
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
socket.bind(SocketAddr::new(address,AddressType::BrEdr,psm))
|
||||
.map_err(|e|format!("Cannot bind classic HID PSM 0x{psm:02x}: {e}. Disable BlueZ input plugin; see Bluetooth HID setup."))?;
|
||||
socket.listen(2).map_err(|e| e.to_string())
|
||||
}
|
||||
struct Runtime {
|
||||
adapter: Adapter,
|
||||
_session: Session,
|
||||
shared: Arc<Shared>,
|
||||
agent: Option<agent::Agent>,
|
||||
controller: controller::Controller,
|
||||
listeners: [SeqPacketListener; 2],
|
||||
channels: [Option<Arc<SeqPacket>>; 2],
|
||||
channel_peer: Option<Address>,
|
||||
partial_since: Option<Instant>,
|
||||
hid: protocol::HidProtocol,
|
||||
generation: u64,
|
||||
old_pairable: bool,
|
||||
old_powered: bool,
|
||||
old_alias: String,
|
||||
old_discoverable: bool,
|
||||
old_discoverable_timeout: u32,
|
||||
old_pairable_timeout: u32,
|
||||
config: Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
recorded_peer: Option<String>,
|
||||
}
|
||||
impl Runtime {
|
||||
async fn open(
|
||||
config: &Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
) -> Result<Self, String> {
|
||||
let controller = controller::Controller::acquire(&config.adapter).await?;
|
||||
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||
let adapter = session
|
||||
.adapter(&config.adapter)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let address = adapter.address().await.map_err(|e| e.to_string())?;
|
||||
let mut config = config.clone();
|
||||
if let Some(store) = &bonds {
|
||||
bonds::clean_adapter(store.as_ref(), &adapter).await?;
|
||||
if config.peer.is_none() {
|
||||
config.peer = store
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|bond| bond.adapter == address.to_string() && !bond.pending)
|
||||
.map(|bond| bond.peer);
|
||||
}
|
||||
}
|
||||
// Acquire BOTH channels before changing adapter state. Conflict is a startup error.
|
||||
let listeners = [listen(address, 0x11)?, listen(address, 0x13)?];
|
||||
let old_powered = adapter.is_powered().await.map_err(|e| e.to_string())?;
|
||||
let old_pairable = adapter.is_pairable().await.map_err(|e| e.to_string())?;
|
||||
let old_alias = adapter.alias().await.map_err(|e| e.to_string())?;
|
||||
let old_discoverable = adapter.is_discoverable().await.map_err(|e| e.to_string())?;
|
||||
let old_discoverable_timeout = adapter
|
||||
.discoverable_timeout()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let old_pairable_timeout = adapter
|
||||
.pairable_timeout()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let shared = Arc::new(Shared::new(
|
||||
config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||
));
|
||||
let mut runtime = Self {
|
||||
adapter,
|
||||
_session: session,
|
||||
shared,
|
||||
agent: None,
|
||||
controller,
|
||||
listeners,
|
||||
channels: [None, None],
|
||||
channel_peer: None,
|
||||
partial_since: None,
|
||||
hid: Default::default(),
|
||||
generation: 0,
|
||||
old_pairable,
|
||||
old_powered,
|
||||
old_alias,
|
||||
old_discoverable,
|
||||
old_discoverable_timeout,
|
||||
old_pairable_timeout,
|
||||
config: config.clone(),
|
||||
bonds,
|
||||
recorded_peer: None,
|
||||
};
|
||||
if let Err(e) = runtime.setup().await {
|
||||
let restore = runtime.close().await;
|
||||
return Err(format!("{e}; cleanup: {restore:?}"));
|
||||
}
|
||||
Ok(runtime)
|
||||
}
|
||||
async fn setup(&mut self) -> Result<(), String> {
|
||||
self.adapter
|
||||
.set_powered(true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_discoverable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_pairable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_alias(self.config.name.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.agent =
|
||||
Some(agent::Agent::register(self.shared.clone(), self.config.adapter.clone()).await?);
|
||||
self.controller.configure().await?;
|
||||
self.public(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn public(&self, seconds: Option<u32>) -> Result<(), String> {
|
||||
if let Some(seconds) = seconds {
|
||||
self.adapter
|
||||
.set_pairable_timeout(seconds)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_discoverable_timeout(seconds)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_pairable(true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if let Err(e) = self.adapter.set_discoverable(true).await {
|
||||
let _ = self.adapter.set_pairable(false).await;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
} else {
|
||||
self.adapter
|
||||
.set_discoverable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_pairable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn drop_channels(&mut self) {
|
||||
for socket in self.channels.iter_mut().filter_map(Option::take) {
|
||||
let _ = socket.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
self.channel_peer = None;
|
||||
self.partial_since = None;
|
||||
self.hid = Default::default();
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
async fn packet(&self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||
let socket = self.channels[index]
|
||||
.as_ref()
|
||||
.ok_or("Classic HID channel not connected")?;
|
||||
let sent = tokio::time::timeout(Duration::from_millis(250), socket.send(data))
|
||||
.await
|
||||
.map_err(|_| "HID send timeout")?
|
||||
.map_err(|e| e.to_string())?;
|
||||
if sent != data.len() {
|
||||
return Err("Short HID packet write".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn release(&mut self) {
|
||||
for packet in self.hid.release() {
|
||||
if self.channels[1].is_some() && self.packet(1, &packet).await.is_err() {
|
||||
self.drop_channels();
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
async fn accept(
|
||||
&mut self,
|
||||
index: usize,
|
||||
socket: SeqPacket,
|
||||
peer: SocketAddr,
|
||||
) -> Result<(), String> {
|
||||
if peer.addr_type != AddressType::BrEdr {
|
||||
return Ok(());
|
||||
}
|
||||
// A host can open HID channels before the next status poll.
|
||||
let paired = self
|
||||
.adapter
|
||||
.device(peer.addr)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_paired()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.observe_bond(&peer.addr.to_string(), paired);
|
||||
if !self.shared.allowed(peer.addr) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.channel_peer.is_some_and(|p| p != peer.addr) || self.channels[index].is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
// Kernel BT_SECURITY_MEDIUM negotiates encryption before accept completes.
|
||||
if socket
|
||||
.as_ref()
|
||||
.security()
|
||||
.map_err(|e| e.to_string())?
|
||||
.key_size
|
||||
< 7
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.channel_peer = Some(peer.addr);
|
||||
self.channels[index] = Some(Arc::new(socket));
|
||||
if self.channels.iter().all(Option::is_some) {
|
||||
self.partial_since = None;
|
||||
self.release().await;
|
||||
} else {
|
||||
self.partial_since = Some(Instant::now());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn status(&mut self) -> Result<Status, String> {
|
||||
if !self.adapter.is_powered().await.map_err(|e| e.to_string())? {
|
||||
return Err("Bluetooth adapter powered off".into());
|
||||
}
|
||||
// A daemon restart loses the SDP registration even if the adapter comes back powered.
|
||||
if !self
|
||||
.adapter
|
||||
.uuids()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.any(|id| id.to_string() == protocol::HID_UUID)
|
||||
{
|
||||
return Err("Classic HID SDP registration lost; restarting Bluetooth backend".into());
|
||||
}
|
||||
let mut status = Status {
|
||||
initialized: true,
|
||||
adapter: self.config.adapter.clone(),
|
||||
adapter_address: self
|
||||
.adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
for address in self
|
||||
.adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
let device = self.adapter.device(address).map_err(|e| e.to_string())?;
|
||||
let connected = device.is_connected().await.unwrap_or(false);
|
||||
let paired = device.is_paired().await.unwrap_or(false);
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.observe_bond(&address.to_string(), paired);
|
||||
if paired || connected {
|
||||
status.devices.push(Device {
|
||||
address: address.to_string(),
|
||||
name: device.alias().await.unwrap_or_default(),
|
||||
paired,
|
||||
connected,
|
||||
});
|
||||
}
|
||||
}
|
||||
status.devices.sort_by(|a, b| a.address.cmp(&b.address));
|
||||
let (peer, until) = {
|
||||
let state = self.shared.state.lock().unwrap();
|
||||
(state.peer.clone(), state.pairing_until)
|
||||
};
|
||||
status.peer = peer.clone();
|
||||
let selected = status
|
||||
.devices
|
||||
.iter()
|
||||
.find(|device| Some(&device.address) == peer.as_ref());
|
||||
let selected_paired = selected.is_some_and(|device| device.paired);
|
||||
if selected_paired && self.recorded_peer != peer {
|
||||
if let (Some(store), Some(peer)) = (&self.bonds, &peer) {
|
||||
store
|
||||
.save(bonds::Bond {
|
||||
adapter: status.adapter_address.clone(),
|
||||
peer: peer.clone(),
|
||||
pending: false,
|
||||
})
|
||||
.await?;
|
||||
self.recorded_peer = Some(peer.clone());
|
||||
}
|
||||
}
|
||||
status.connected = selected.is_some_and(|device| device.paired && device.connected);
|
||||
if self.channels.iter().any(Option::is_some)
|
||||
&& (!status.connected
|
||||
|| self
|
||||
.partial_since
|
||||
.is_some_and(|time| time.elapsed() > Duration::from_secs(10)))
|
||||
{
|
||||
self.drop_channels();
|
||||
}
|
||||
if let Some(until) = until {
|
||||
let completed = self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.pairing_completed(selected_paired);
|
||||
if until <= Instant::now() || completed {
|
||||
{
|
||||
let mut state = self.shared.state.lock().unwrap();
|
||||
state.close_pairing(
|
||||
selected_paired,
|
||||
self.config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||
);
|
||||
status.peer = state.peer.clone();
|
||||
}
|
||||
self.public(None).await?;
|
||||
tracing::info!(reason = if completed { "bonded" } else { "expired" }, peer = ?status.peer, "Bluetooth HID pairing window closed");
|
||||
} else {
|
||||
status.pairing_seconds =
|
||||
until.saturating_duration_since(Instant::now()).as_secs() as u32 + 1;
|
||||
}
|
||||
}
|
||||
status.control_connected = self.channels[0].is_some();
|
||||
status.interrupt_connected = self.channels[1].is_some();
|
||||
status.ready = status.connected
|
||||
&& status.control_connected
|
||||
&& status.interrupt_connected
|
||||
&& !self.hid.suspended;
|
||||
status.leds = self.hid.leds;
|
||||
status.generation = self.generation;
|
||||
Ok(status)
|
||||
}
|
||||
async fn forget(&mut self) -> Result<(), String> {
|
||||
self.release().await;
|
||||
self.drop_channels();
|
||||
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||
if let Some(peer) = &peer {
|
||||
let address = peer.parse().map_err(|_| "Invalid peer")?;
|
||||
if self
|
||||
.adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.contains(&address)
|
||||
{
|
||||
self.adapter
|
||||
.remove_device(address)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.bonded_before_pairing
|
||||
.remove(peer);
|
||||
tracing::info!(%peer, "Bluetooth HID forgot computer");
|
||||
}
|
||||
if let Some(store) = &self.bonds {
|
||||
let address = self
|
||||
.adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string();
|
||||
for bond in store
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|b| b.adapter == address && Some(&b.peer) == peer.as_ref())
|
||||
{
|
||||
store.remove(bond).await?;
|
||||
}
|
||||
}
|
||||
self.recorded_peer = None;
|
||||
self.config.peer = None;
|
||||
self.shared.state.lock().unwrap().peer = None;
|
||||
Ok(())
|
||||
}
|
||||
async fn incoming(&mut self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||
if index == 1 {
|
||||
self.hid.output(data);
|
||||
return Ok(());
|
||||
}
|
||||
let result = self.hid.control(data);
|
||||
if let Some(reply) = result.reply {
|
||||
self.packet(0, &reply).await?;
|
||||
}
|
||||
if result.reset {
|
||||
self.release().await;
|
||||
}
|
||||
if result.unplug {
|
||||
self.forget().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn execute(&mut self, command: Command) -> Result<(), String> {
|
||||
match command {
|
||||
Command::Report(kind, value) => {
|
||||
let peer = self.channel_peer.ok_or("Classic HID is not connected")?;
|
||||
if !self.shared.allowed(peer) || self.channels.iter().any(Option::is_none) {
|
||||
return Err("Classic HID channels are not ready".into());
|
||||
}
|
||||
let device = self.adapter.device(peer).map_err(|e| e.to_string())?;
|
||||
if !device.is_paired().await.unwrap_or(false)
|
||||
|| !device.is_connected().await.unwrap_or(false)
|
||||
{
|
||||
self.drop_channels();
|
||||
return Err("Selected Bluetooth computer disconnected".into());
|
||||
}
|
||||
let packet = self.hid.input(kind, &value)?;
|
||||
if let Err(e) = self.packet(1, &packet).await {
|
||||
self.drop_channels();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Command::Action(Action::Pair(seconds)) => {
|
||||
if !(10..=300).contains(&seconds) {
|
||||
return Err("Pairing window must be 10–300 seconds".into());
|
||||
}
|
||||
let mut bonded = HashSet::new();
|
||||
for address in self
|
||||
.adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
if self
|
||||
.adapter
|
||||
.device(address)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_paired()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
bonded.insert(address.to_string());
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut state = self.shared.state.lock().unwrap();
|
||||
// Preserve the original baseline when extending an open window.
|
||||
if !state
|
||||
.pairing_until
|
||||
.is_some_and(|until| until > Instant::now())
|
||||
{
|
||||
state.bonded_before_pairing = bonded;
|
||||
}
|
||||
state.pairing_until =
|
||||
Some(Instant::now() + Duration::from_secs(seconds.into()));
|
||||
}
|
||||
if let Err(e) = self.public(Some(seconds)).await {
|
||||
self.shared.state.lock().unwrap().pairing_until = None;
|
||||
return Err(e);
|
||||
}
|
||||
tracing::info!(seconds, "Bluetooth HID pairing window opened");
|
||||
}
|
||||
Command::Action(Action::ClosePairing) => {
|
||||
self.public(None).await?;
|
||||
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||
let paired = if let Some(peer) = peer {
|
||||
self.adapter
|
||||
.device(peer.parse().map_err(|_| "Invalid HID peer")?)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_paired()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.close_pairing(paired, self.config.peer.clone());
|
||||
tracing::info!(reason = "manual", "Bluetooth HID pairing window closed");
|
||||
}
|
||||
Command::Action(Action::Forget) => self.forget().await?,
|
||||
Command::Action(Action::Disconnect) => {
|
||||
self.release().await;
|
||||
self.drop_channels();
|
||||
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||
if let Some(peer) = peer {
|
||||
self.adapter
|
||||
.device(peer.parse().map_err(|_| "Invalid peer")?)
|
||||
.map_err(|e| e.to_string())?
|
||||
.disconnect()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Command::Action(Action::Reset) => self.release().await,
|
||||
Command::Stop => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn close(&mut self) -> Result<(), String> {
|
||||
self.release().await;
|
||||
self.drop_channels();
|
||||
self.agent.take();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
let mut errors = vec![];
|
||||
if let Err(e) = self.adapter.set_discoverable(false).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.controller.restore().await {
|
||||
errors.push(e);
|
||||
}
|
||||
if let Err(e) = self.adapter.set_alias(self.old_alias.clone()).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self
|
||||
.adapter
|
||||
.set_discoverable_timeout(self.old_discoverable_timeout)
|
||||
.await
|
||||
{
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self
|
||||
.adapter
|
||||
.set_pairable_timeout(self.old_pairable_timeout)
|
||||
.await
|
||||
{
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.adapter.set_pairable(self.old_pairable).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.adapter.set_discoverable(self.old_discoverable).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.adapter.set_powered(self.old_powered).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn receive(socket: Option<Arc<SeqPacket>>, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||
match socket {
|
||||
Some(socket) => socket.recv(buffer).await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
}
|
||||
async fn supervise(
|
||||
config: Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
mut rx: mpsc::Receiver<Request>,
|
||||
tx: watch::Sender<Status>,
|
||||
) {
|
||||
let mut generation = 0;
|
||||
loop {
|
||||
let mut runtime = match Runtime::open(&config, bonds.clone()).await {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
tx.send_replace(Status {
|
||||
adapter: config.adapter.clone(),
|
||||
error: Some(error.clone()),
|
||||
generation,
|
||||
..Default::default()
|
||||
});
|
||||
let delay = tokio::time::sleep(Duration::from_secs(5));
|
||||
tokio::pin!(delay);
|
||||
loop {
|
||||
tokio::select! {_=&mut delay=>break,request=rx.recv()=>{
|
||||
let Some(request)=request else{return;};
|
||||
if matches!(request.command,Command::Stop){let _=request.result.send(Ok(()));return;}
|
||||
let _=request.result.send(Err(error.clone()));
|
||||
}}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
runtime.generation = generation;
|
||||
let mut timer = tokio::time::interval(Duration::from_millis(500));
|
||||
let failure = loop {
|
||||
let ctl = runtime.channels[0].clone();
|
||||
let intr = runtime.channels[1].clone();
|
||||
let mut ctl_buffer = [0; 1024];
|
||||
let mut intr_buffer = [0; 1024];
|
||||
tokio::select! {
|
||||
accepted=runtime.listeners[0].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(0,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||
accepted=runtime.listeners[1].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(1,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||
read=receive(ctl,&mut ctl_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(0,&ctl_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||
read=receive(intr,&mut intr_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(1,&intr_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||
_=timer.tick()=>{match runtime.status().await{Ok(status)=>{tx.send_if_modified(|old|{if *old!=status{*old=status;true}else{false}});},Err(e)=>break e}}
|
||||
request=rx.recv()=>{
|
||||
let Some(request)=request else{let _=runtime.close().await;return;};
|
||||
if matches!(request.command,Command::Stop){let result=runtime.close().await;tx.send_replace(Status{adapter:config.adapter.clone(),..Default::default()});let _=request.result.send(result);return;}
|
||||
if request.expires<=Instant::now()||request.result.is_closed(){runtime.release().await;continue;}
|
||||
let action = matches!(&request.command, Command::Action(_));
|
||||
let result=runtime.execute(request.command).await;
|
||||
if result.is_err(){runtime.release().await;}
|
||||
if action {
|
||||
if let Ok(status) = runtime.status().await { tx.send_replace(status); }
|
||||
}
|
||||
let _=request.result.send(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
let restore = runtime.close().await;
|
||||
generation = runtime.generation.wrapping_add(1);
|
||||
tx.send_replace(Status {
|
||||
adapter: config.adapter.clone(),
|
||||
error: Some(format!("{failure}; cleanup: {restore:?}")),
|
||||
generation,
|
||||
..Default::default()
|
||||
});
|
||||
drop(runtime);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn closing_pairing_preserves_completed_bonds_but_releases_unpaired_targets() {
|
||||
let shared = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state.close_pairing(true, None);
|
||||
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:88"));
|
||||
assert!(state.pairing_until.is_none());
|
||||
state.close_pairing(false, None);
|
||||
assert!(state.peer.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn configuration_validation() {
|
||||
let mut c = Config {
|
||||
adapter: "hci0".into(),
|
||||
name: "One-KVM".into(),
|
||||
peer: None,
|
||||
};
|
||||
assert!(c.validate().is_ok());
|
||||
c.adapter = "hci0;reboot".into();
|
||||
assert!(c.validate().is_err());
|
||||
c.adapter = "hci1".into();
|
||||
c.peer = Some("bogus".into());
|
||||
assert!(c.validate().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn peer_isolation() {
|
||||
let state = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||
assert!(state.allowed("10:6F:D9:66:97:88".parse().unwrap()));
|
||||
assert!(!state.allowed("10:6F:D9:66:97:89".parse().unwrap()));
|
||||
}
|
||||
#[test]
|
||||
fn new_bond_without_agent_callback_completes_pairing() {
|
||||
let shared = Shared::new(None);
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
let peer = "10:6F:D9:66:97:88";
|
||||
state.observe_bond(peer, false);
|
||||
assert_eq!(state.peer, None);
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||
assert!(state.pairing_completed(true));
|
||||
// A second computer cannot replace the one selected in this window.
|
||||
state.observe_bond("10:6F:D9:66:97:89", true);
|
||||
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||
}
|
||||
#[test]
|
||||
fn old_bond_does_not_finish_a_new_window() {
|
||||
let peer = "10:6F:D9:66:97:88";
|
||||
let shared = Shared::new(Some(peer.into()));
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state.bonded_before_pairing.insert(peer.into());
|
||||
state.observe_bond(peer, true);
|
||||
assert!(!state.pairing_completed(true));
|
||||
state.peer = None;
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer, None);
|
||||
}
|
||||
#[test]
|
||||
fn bond_observation_respects_closed_window_and_pinned_peer() {
|
||||
let peer = "10:6F:D9:66:97:88";
|
||||
let shared = Shared::new(None);
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer, None);
|
||||
state.pairing_until = Some(Instant::now() - Duration::from_secs(1));
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer, None);
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state.peer = Some("10:6F:D9:66:97:89".into());
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:89"));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AdapterInfo {
|
||||
pub name: String,
|
||||
pub address: String,
|
||||
pub powered: bool,
|
||||
}
|
||||
pub async fn adapters() -> Result<Vec<AdapterInfo>, String> {
|
||||
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||
let mut output = vec![];
|
||||
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||
let adapter = session.adapter(&name).map_err(|e| e.to_string())?;
|
||||
output.push(AdapterInfo {
|
||||
name,
|
||||
address: adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string(),
|
||||
powered: adapter.is_powered().await.map_err(|e| e.to_string())?,
|
||||
});
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
267
libs/bluetooth-hid/src/protocol.rs
Normal file
267
libs/bluetooth-hid/src/protocol.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! Classic HIDP framing and report descriptor.
|
||||
use crate::Report;
|
||||
pub const MAP: &[u8] = &[
|
||||
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
|
||||
0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
|
||||
0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
||||
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
||||
0xc0, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x85, 0x02, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19,
|
||||
0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75,
|
||||
0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75,
|
||||
0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x03, 0x15,
|
||||
0x00, 0x26, 0xff, 0x03, 0x19, 0x00, 0x2a, 0xff, 0x03, 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, 0xc0,
|
||||
];
|
||||
|
||||
pub const HID_UUID: &str = "00001124-0000-1000-8000-00805f9b34fb";
|
||||
pub fn sdp() -> String {
|
||||
let hex: String = MAP.iter().map(|byte| format!("{byte:02x}")).collect();
|
||||
format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<record>
|
||||
<attribute id="0x0001"><sequence><uuid value="0x1124"/></sequence></attribute>
|
||||
<attribute id="0x0004"><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0011"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></attribute>
|
||||
<attribute id="0x0005"><sequence><uuid value="0x1002"/></sequence></attribute>
|
||||
<attribute id="0x0006"><sequence><uint16 value="0x656e"/><uint16 value="0x006a"/><uint16 value="0x0100"/></sequence></attribute>
|
||||
<attribute id="0x0009"><sequence><sequence><uuid value="0x1124"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||
<attribute id="0x000d"><sequence><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0013"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></sequence></attribute>
|
||||
<attribute id="0x0100"><text value="One-KVM Keyboard and Mouse"/></attribute>
|
||||
<attribute id="0x0101"><text value="Classic Bluetooth HID"/></attribute>
|
||||
<attribute id="0x0102"><text value="One-KVM"/></attribute>
|
||||
<attribute id="0x0200"><uint16 value="0x0100"/></attribute>
|
||||
<attribute id="0x0201"><uint16 value="0x0111"/></attribute>
|
||||
<attribute id="0x0202"><uint8 value="0xc0"/></attribute>
|
||||
<attribute id="0x0203"><uint8 value="0x00"/></attribute>
|
||||
<attribute id="0x0204"><boolean value="false"/></attribute>
|
||||
<attribute id="0x0205"><boolean value="false"/></attribute>
|
||||
<attribute id="0x0206"><sequence><sequence><uint8 value="0x22"/><text encoding="hex" value="{hex}"/></sequence></sequence></attribute>
|
||||
<attribute id="0x0207"><sequence><sequence><uint16 value="0x0409"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||
<attribute id="0x0209"><boolean value="false"/></attribute>
|
||||
<attribute id="0x020a"><boolean value="false"/></attribute>
|
||||
<attribute id="0x020b"><uint16 value="0x0100"/></attribute>
|
||||
<attribute id="0x020c"><uint16 value="0x0c80"/></attribute>
|
||||
<attribute id="0x020d"><boolean value="true"/></attribute>
|
||||
<attribute id="0x020e"><boolean value="true"/></attribute>
|
||||
</record>"#
|
||||
)
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HidProtocol {
|
||||
pub boot: bool,
|
||||
pub suspended: bool,
|
||||
pub leds: u8,
|
||||
pub keyboard: [u8; 8],
|
||||
pub buttons: u8,
|
||||
pub consumer: [u8; 2],
|
||||
}
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct ControlResult {
|
||||
pub reply: Option<Vec<u8>>,
|
||||
pub unplug: bool,
|
||||
pub reset: bool,
|
||||
}
|
||||
impl HidProtocol {
|
||||
pub fn input(&mut self, kind: Report, value: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if value.len() != kind.len() {
|
||||
return Err("Invalid HID report length".into());
|
||||
}
|
||||
if self.suspended {
|
||||
return Err("Bluetooth HID is suspended".into());
|
||||
}
|
||||
let mut packet = vec![0xa1, kind.id()];
|
||||
match kind {
|
||||
Report::Keyboard => {
|
||||
self.keyboard.copy_from_slice(value);
|
||||
packet.extend_from_slice(value);
|
||||
}
|
||||
Report::Mouse => {
|
||||
self.buttons = value[0];
|
||||
packet.extend_from_slice(&value[..if self.boot { 3 } else { 4 }]);
|
||||
if self.boot {
|
||||
packet[2] &= 7;
|
||||
}
|
||||
}
|
||||
Report::Consumer if !self.boot => {
|
||||
self.consumer.copy_from_slice(value);
|
||||
packet.extend_from_slice(value);
|
||||
}
|
||||
Report::Consumer => return Err("Consumer keys unavailable in boot protocol".into()),
|
||||
}
|
||||
Ok(packet)
|
||||
}
|
||||
pub fn release(&mut self) -> Vec<Vec<u8>> {
|
||||
self.keyboard = [0; 8];
|
||||
self.buttons = 0;
|
||||
self.consumer = [0; 2];
|
||||
let mut packets = vec![
|
||||
vec![0xa1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
if self.boot {
|
||||
vec![0xa1, 2, 0, 0, 0]
|
||||
} else {
|
||||
vec![0xa1, 2, 0, 0, 0, 0]
|
||||
},
|
||||
];
|
||||
if !self.boot {
|
||||
packets.push(vec![0xa1, 3, 0, 0]);
|
||||
}
|
||||
packets
|
||||
}
|
||||
pub fn output(&mut self, data: &[u8]) -> bool {
|
||||
if data.len() == 3 && data[0] == 0xa2 && data[1] == 1 {
|
||||
self.leds = data[2] & 0x1f;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn control(&mut self, data: &[u8]) -> ControlResult {
|
||||
let mut result = ControlResult::default();
|
||||
let handshake = |code| ControlResult {
|
||||
reply: Some(vec![code]),
|
||||
..Default::default()
|
||||
};
|
||||
let Some(&header) = data.first() else {
|
||||
return handshake(4);
|
||||
};
|
||||
match header {
|
||||
0x13 if data.len() == 1 => {
|
||||
self.suspended = true;
|
||||
result.reset = true;
|
||||
}
|
||||
0x14 if data.len() == 1 => {
|
||||
self.suspended = false;
|
||||
result.reset = true;
|
||||
}
|
||||
0x15 if data.len() == 1 => {
|
||||
result.unplug = true;
|
||||
}
|
||||
0x11 | 0x12 if data.len() == 1 => {
|
||||
self.suspended = false;
|
||||
result.reset = true;
|
||||
}
|
||||
0x60 if data.len() == 1 => {
|
||||
result.reply = Some(vec![0xa0, if self.boot { 0 } else { 1 }]);
|
||||
}
|
||||
0x70 | 0x71 if data.len() == 1 => {
|
||||
self.boot = header == 0x70;
|
||||
result.reply = Some(vec![0]);
|
||||
result.reset = true;
|
||||
}
|
||||
0x52 if data.len() == 3 && data[1] == 1 => {
|
||||
self.leds = data[2] & 0x1f;
|
||||
return handshake(0);
|
||||
}
|
||||
0x41 | 0x49 | 0x42 | 0x4a => {
|
||||
let sized = header & 8 != 0;
|
||||
if data.len() != if sized { 4 } else { 2 } {
|
||||
return handshake(4);
|
||||
}
|
||||
let mut report = vec![0xa0 | (header & 3), data[1]];
|
||||
match (header & 3, data[1]) {
|
||||
(1, 1) => report.extend(self.keyboard),
|
||||
(1, 2) => report.extend(if self.boot {
|
||||
vec![self.buttons & 7, 0, 0]
|
||||
} else {
|
||||
vec![self.buttons, 0, 0, 0]
|
||||
}),
|
||||
(1, 3) if !self.boot => report.extend(self.consumer),
|
||||
(2, 1) => report.push(self.leds),
|
||||
_ => return handshake(2),
|
||||
}
|
||||
if sized {
|
||||
report.truncate(1 + u16::from_le_bytes([data[2], data[3]]) as usize);
|
||||
}
|
||||
result.reply = Some(report);
|
||||
}
|
||||
_ => return handshake(3), // ERR_UNSUPPORTED_REQUEST
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn classic_frames_and_boot_protocol() {
|
||||
let mut hid = HidProtocol::default();
|
||||
assert_eq!(
|
||||
hid.input(Report::Keyboard, &[2, 0, 4, 0, 0, 0, 0, 0])
|
||||
.unwrap(),
|
||||
[0xa1, 1, 2, 0, 4, 0, 0, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(hid.control(&[0x60]).reply.unwrap(), [0xa0, 1]);
|
||||
assert_eq!(hid.control(&[0x70]).reply.unwrap(), [0]);
|
||||
assert_eq!(
|
||||
hid.input(Report::Mouse, &[0x1f, 127, 128, 1]).unwrap(),
|
||||
[0xa1, 2, 7, 127, 128]
|
||||
);
|
||||
assert!(hid.input(Report::Consumer, &[0, 0]).is_err());
|
||||
assert!(hid.input(Report::Keyboard, &[0]).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn led_read_write_and_errors() {
|
||||
let mut hid = HidProtocol::default();
|
||||
assert!(hid.output(&[0xa2, 1, 3]));
|
||||
assert_eq!(hid.leds, 3);
|
||||
assert_eq!(hid.control(&[0x42, 1]).reply.unwrap(), [0xa2, 1, 3]);
|
||||
assert_eq!(hid.control(&[0x52, 1, 2]).reply.unwrap(), [0]);
|
||||
assert_eq!(hid.leds, 2);
|
||||
assert_eq!(hid.control(&[0x41, 99]).reply.unwrap(), [2]);
|
||||
assert_eq!(hid.control(&[0x41]).reply.unwrap(), [4]);
|
||||
assert_eq!(hid.control(&[0x90, 0]).reply.unwrap(), [3]);
|
||||
assert_eq!(
|
||||
hid.control(&[0x49, 1, 3, 0]).reply.unwrap(),
|
||||
[0xa1, 1, 0, 0]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn suspend_reset_and_virtual_unplug() {
|
||||
let mut hid = HidProtocol::default();
|
||||
assert!(hid.control(&[0x13]).reset);
|
||||
assert!(hid.input(Report::Mouse, &[0; 4]).is_err());
|
||||
assert!(hid.control(&[0x14]).reset);
|
||||
assert!(hid.input(Report::Mouse, &[0; 4]).is_ok());
|
||||
assert!(hid.control(&[0x15]).unplug);
|
||||
hid.keyboard[2] = 4;
|
||||
hid.buttons = 1;
|
||||
assert_eq!(hid.release().len(), 3);
|
||||
assert_eq!(hid.keyboard, [0; 8]);
|
||||
assert_eq!(hid.buttons, 0);
|
||||
}
|
||||
#[test]
|
||||
fn sdp_describes_both_classic_channels() {
|
||||
let record = sdp();
|
||||
assert!(record.contains("uuid value=\"0x1124\""));
|
||||
assert!(record.contains("uint16 value=\"0x0011\""));
|
||||
assert!(record.contains("uint16 value=\"0x0013\""));
|
||||
assert!(!record.contains("0x1812"));
|
||||
}
|
||||
#[test]
|
||||
fn descriptor_report_sizes() {
|
||||
let (mut pos, mut size, mut count, mut id, mut depth) = (0, 0, 0, 0, 0);
|
||||
let mut input = [0; 4];
|
||||
let mut output = [0; 4];
|
||||
while pos < MAP.len() {
|
||||
let prefix = MAP[pos];
|
||||
pos += 1;
|
||||
let length = [0, 1, 2, 4][(prefix & 3) as usize];
|
||||
let mut value = 0;
|
||||
for i in 0..length {
|
||||
value |= (MAP[pos + i] as usize) << (8 * i);
|
||||
}
|
||||
pos += length;
|
||||
match ((prefix >> 2) & 3, prefix >> 4) {
|
||||
(1, 7) => size = value,
|
||||
(1, 8) => id = value,
|
||||
(1, 9) => count = value,
|
||||
(0, 8) => input[id] += size * count,
|
||||
(0, 9) => output[id] += size * count,
|
||||
(0, 10) => depth += 1,
|
||||
(0, 12) => depth -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(depth, 0);
|
||||
assert_eq!(input, [0, 64, 32, 16]);
|
||||
assert_eq!(output, [0, 8, 0, 0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user