feat(bluetooth-hid): 支持经典蓝牙 HID 后端

This commit is contained in:
mofeng-git
2026-09-06 11:15:45 +08:00
parent 3014edffbb
commit 2c19208094
33 changed files with 2792 additions and 60 deletions

View 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"

View 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)
}

View 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(())
}

View 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());
}
}

View 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());
}
}

View 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("; "))
}
}
}

View 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 164 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 10300 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)
}

View 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]);
}
}