#![cfg(feature = "hidapi-devices")]
use crate::api::common_capnp::NodeType;
use crate::api::Endpoint;
use crate::api::HidApiInfo;
use crate::device::*;
use crate::RUNNING;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::sync::atomic::Ordering;
use std::sync::{Arc, RwLock};
pub const USAGE_PAGE: u16 = 0xFF1C;
pub const USAGE: u16 = 0x1100;
const USB_FULLSPEED_PACKET_SIZE: usize = 64;
const ENUMERATE_DELAY_MS: u64 = 1000;
const TIMEOUT_MS: i32 = 500;
pub struct HidApiDevice {
device: ::hidapi::HidDevice,
timeout: i32,
}
impl HidApiDevice {
pub fn new(device: ::hidapi::HidDevice, timeout: i32) -> HidApiDevice {
device.set_blocking_mode(true).unwrap(); HidApiDevice { device, timeout }
}
}
impl std::io::Read for HidApiDevice {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self.device.read_timeout(buf, self.timeout) {
Ok(len) => {
if len > 0 {
trace!("Received {} bytes", len);
trace!("{:x?}", &buf[0..len]);
}
Ok(len)
}
Err(e) => {
warn!("Read - {:?}", e);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("{e:?}"),
))
}
}
}
}
impl std::io::Write for HidApiDevice {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
let buf = {
#[allow(clippy::needless_bool)]
#[allow(clippy::if_same_then_else)]
let prepend = if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
true
} else if cfg!(target_os = "windows") {
true
} else {
false
};
if prepend {
let mut new_buf = vec![0x00];
new_buf.extend(_buf);
new_buf
} else {
_buf.to_vec()
}
};
match self.device.write(&buf) {
Ok(len) => {
trace!("Sent {} bytes", len);
trace!("{:x?}", &buf[0..len]);
Ok(len)
}
Err(e) => {
warn!("Write - {:?} {:x?}", e, buf);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("{e:?}"),
))
}
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl HidIoTransport for HidApiDevice {}
fn device_name(device_info: &::hidapi::DeviceInfo) -> String {
let mut string = format!(
"[{:04x}:{:04x}-{:x}:{:x}] I:{} ",
device_info.vendor_id(),
device_info.product_id(),
device_info.usage_page(),
device_info.usage(),
device_info.interface_number(),
);
if let Some(m) = &device_info.manufacturer_string() {
string += m;
}
if let Some(p) = &device_info.product_string() {
write!(string, " {p}").unwrap();
}
if let Some(s) = &device_info.serial_number() {
write!(string, " ({s})").unwrap();
}
string
}
#[cfg(target_os = "linux")]
fn match_device(device_info: &::hidapi::DeviceInfo) -> bool {
device_info.usage_page() == USAGE_PAGE && device_info.usage() == USAGE
}
#[cfg(target_os = "macos")]
fn match_device(device_info: &::hidapi::DeviceInfo) -> bool {
device_info.usage_page() == USAGE_PAGE && device_info.usage() == USAGE
}
#[cfg(target_os = "windows")]
fn match_device(device_info: &::hidapi::DeviceInfo) -> bool {
device_info.usage_page() == USAGE_PAGE && device_info.usage() == USAGE
}
async fn processing(mailbox: mailbox::Mailbox) {
info!("Spawning hidapi spawning thread...");
let mut api: ::hidapi::HidApi =
::hidapi::HidApi::new().expect("HID API object creation failed");
let uids: Arc<RwLock<HashMap<u64, tokio::task::JoinHandle<()>>>> =
Arc::new(RwLock::new(HashMap::new()));
let rt = mailbox.rt.clone();
loop {
if !RUNNING.load(Ordering::SeqCst) {
#[cfg(not(feature = "api"))]
mailbox.drop_all_subscribers();
return;
}
api.refresh_devices().unwrap();
trace!("Scanning for devices");
for device_info in api.device_list() {
let device_str = format!(
"Device: {:#?}\n {} R:{}",
device_info.path(),
device_name(device_info),
device_info.release_number()
);
trace!("{}", device_str);
if !match_device(device_info) {
continue;
}
let mut info = HidApiInfo::new(device_info);
let key = info.key();
let uid = match mailbox
.clone()
.assign_uid(key.clone(), format!("{:#?}", device_info.path()))
{
Ok(uid) => uid,
Err(_) => {
continue;
}
};
lazy_static! {
static ref RE: Regex =
Regex::new(r"([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])").unwrap();
}
let is_ble = RE.is_match(match device_info.serial_number() {
Some(s) => s,
_ => "",
});
let device_path = std::ffi::CString::new(device_info.path().to_bytes())
.expect("hidapi path generation failed");
if !uids.clone().read().unwrap().contains_key(&uid) {
info!("Connecting to uid:{} {}", uid, device_str);
let hid_device = api.open_path(&device_path);
let uids = uids.clone();
let uids_outer = uids.clone();
let mailbox = mailbox.clone();
let handle = rt.clone().spawn_blocking(move || {
let mut node = Endpoint::new(
if is_ble {
NodeType::BleKeyboard
} else {
NodeType::UsbKeyboard
},
uid,
);
node.set_hidapi_params(info);
debug!("Attempting to setup {:#?}", node);
match hid_device {
Ok(device) => {
println!("Connected to {node}");
let device = HidApiDevice::new(device, TIMEOUT_MS);
let mut device = HidIoEndpoint::new(
Box::new(device),
USB_FULLSPEED_PACKET_SIZE as u32,
);
if let Err(e) = device.send_sync() {
uids.write().unwrap().remove(&uid);
warn!("Failed to sync device - {}", e);
} else {
let mut master = HidIoController::new(mailbox.clone(), uid, device);
mailbox.nodes.write().unwrap().push(node);
loop {
if !RUNNING.load(Ordering::SeqCst) {
break;
}
let ret = master.process();
if ret.is_err() {
info!("{} disconnected. No longer polling it", uid);
uids.write().unwrap().remove(&uid);
{
let mut nodes = mailbox.nodes.write().unwrap();
let index =
nodes.iter().position(|x| x.uid == uid).unwrap();
nodes.remove(index);
}
break;
}
}
}
}
Err(e) => {
warn!("Failed to open device:{:?} - {}", device_path, e);
uids.write().unwrap().remove(&uid);
}
};
});
uids_outer.write().unwrap().insert(uid, handle);
}
}
tokio::time::sleep(std::time::Duration::from_millis(ENUMERATE_DELAY_MS)).await;
}
}
pub async fn initialize(mailbox: mailbox::Mailbox) {
info!("Initializing device/hidapi...");
let rt = mailbox.rt.clone();
rt.clone()
.spawn_blocking(move || {
rt.block_on(async {
let local = tokio::task::LocalSet::new();
local.run_until(processing(mailbox)).await;
});
})
.await
.unwrap();
}