hid-io-core 0.1.2

HID-IO is a host-side daemon for advanced HID devices.
Documentation
#![cfg(feature = "hidapi-devices")]
/* Copyright (C) 2017-2022 by Jacob Alexander
 *
 * This file is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This file is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this file.  If not, see <http://www.gnu.org/licenses/>.
 */

// ----- Crates -----

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(); // Enable blocking mode, use timeouts to unblock
        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") {
                // If the first byte is a 0 its not tranmitted
                // https://github.com/node-hid/node-hid/issues/187#issuecomment-282863702
                //_buf[0] == 0x00
                // TODO(HaaTa): This behaviour seems to have changed?
                // Or perhaps a hid-io-protocol bug was fixed
                true
            } else if cfg!(target_os = "windows") {
                // The first byte always seems to be stripped and not tranmitted
                // https://github.com/node-hid/node-hid/issues/187#issuecomment-285688178
                true
            } else {
                // TODO: Test other platforms
                false
            };

            // Add a report id (unused) if needed so our actual first byte
            // of the packet is sent correctly
            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 {
    // NOTE: This requires some patches to hidapi (https://github.com/libusb/hidapi/pull/139)
    // interface number and usage are both queryable. Prefer usage
    device_info.usage_page() == USAGE_PAGE && device_info.usage() == USAGE
}

#[cfg(target_os = "macos")]
fn match_device(device_info: &::hidapi::DeviceInfo) -> bool {
    // interface_number is always -1 but usage is fine
    device_info.usage_page() == USAGE_PAGE && device_info.usage() == USAGE
}

#[cfg(target_os = "windows")]
fn match_device(device_info: &::hidapi::DeviceInfo) -> bool {
    // interface and usage are both queryable. Prefer usage
    device_info.usage_page() == USAGE_PAGE && device_info.usage() == USAGE
}

/// hidapi processing
///
/// This thread periodically refreshes the USB device list to see if a new device needs to be attached
/// The thread also handles reading/writing from connected interfaces
///
/// XXX (HaaTa) hidapi is not thread-safe on all platforms, so don't try to create a thread per device
/// TODO ^ Is this still valid?
async fn processing(mailbox: mailbox::Mailbox) {
    info!("Spawning hidapi spawning thread...");

    // Initialize HID interface
    let mut api: ::hidapi::HidApi =
        ::hidapi::HidApi::new().expect("HID API object creation failed");

    // List of allocated device uids
    let uids: Arc<RwLock<HashMap<u64, tokio::task::JoinHandle<()>>>> =
        Arc::new(RwLock::new(HashMap::new()));

    // Prepare the runtime
    let rt = mailbox.rt.clone();

    // Loop infinitely, the watcher only exits if the daemon is quit
    // TODO (HaaTa) - There should be a better way using hotplug events (e.g. udev) in a cross
    // platform way
    loop {
        if !RUNNING.load(Ordering::SeqCst) {
            // When the capnproto api isn't enabled use this loop to cancel
            // some of the hidio message filters that may be waiting
            #[cfg(not(feature = "api"))]
            mailbox.drop_all_subscribers();
            return;
        }

        // Refresh devices list
        api.refresh_devices().unwrap();

        // Iterate over found USB interfaces and select usable ones
        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);

            // Use usage page and usage for matching HID-IO compatible device
            if !match_device(device_info) {
                continue;
            }

            // Build set of HID info to make unique comparisons
            let mut info = HidApiInfo::new(device_info);

            // Determine if id can be reused
            // Criteria
            // 1. Must match (even if field isn't valid)
            //    vid, pid, usage page, usage, manufacturer, product, serial, interface
            // 2. Must not currently be in use (generally, use path to differentiate)
            let key = info.key();
            let uid = match mailbox
                .clone()
                .assign_uid(key.clone(), format!("{:#?}", device_info.path()))
            {
                Ok(uid) => uid,
                Err(_) => {
                    // Device has already been registered, or is invalid
                    continue;
                }
            };

            // If serial number is a MAC address, this is a bluetooth device
            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,
                _ => "",
            });

            // Basically, we need to copy the path string to deal with lifetime issues
            let device_path = std::ffi::CString::new(device_info.path().to_bytes())
                .expect("hidapi path generation failed");

            // Start thread if uid not it map (i.e. not already processing)
            if !uids.clone().read().unwrap().contains_key(&uid) {
                // Add device
                info!("Connecting to uid:{} {}", uid, device_str);

                // Connect to device
                let hid_device = api.open_path(&device_path);

                // Start thread
                let uids = uids.clone();
                let uids_outer = uids.clone();
                let mailbox = mailbox.clone();
                let handle = rt.clone().spawn_blocking(move || {
                    // Create node
                    let mut node = Endpoint::new(
                        if is_ble {
                            NodeType::BleKeyboard
                        } else {
                            NodeType::UsbKeyboard
                        },
                        uid,
                    );
                    node.set_hidapi_params(info);

                    // Setup device
                    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,
                            );

                            // Attempt to synchronize device (sync packet)
                            if let Err(e) = device.send_sync() {
                                // Could not open device (likely removed, or in use)
                                uids.write().unwrap().remove(&uid);
                                warn!("Failed to sync device - {}", e);
                            } else {
                                // Setup device controller (handles communication and protocol conversion
                                // for the HidIo device)
                                let mut master = HidIoController::new(mailbox.clone(), uid, device);

                                // Add device to node list
                                mailbox.nodes.write().unwrap().push(node);

                                loop {
                                    // Stop processing, daemon trying to quit
                                    if !RUNNING.load(Ordering::SeqCst) {
                                        break;
                                    }

                                    // Process loop for device
                                    let ret = master.process();
                                    if ret.is_err() {
                                        info!("{} disconnected. No longer polling it", uid);
                                        // Remove handle from map
                                        uids.write().unwrap().remove(&uid);

                                        // Remove node from index
                                        {
                                            let mut nodes = mailbox.nodes.write().unwrap();
                                            let index =
                                                nodes.iter().position(|x| x.uid == uid).unwrap();
                                            nodes.remove(index);
                                        }
                                        break;
                                    }
                                }
                            }
                        }
                        Err(e) => {
                            // Could not open device (likely removed, or in use)
                            warn!("Failed to open device:{:?} - {}", device_path, e);
                            // Remove handle from map
                            uids.write().unwrap().remove(&uid);
                        }
                    };
                });

                // Add uid to hashmap
                uids_outer.write().unwrap().insert(uid, handle);
            }
        }

        // Sleep so we don't starve the CPU
        // XXX - Rewrite hidapi with rust and include async
        tokio::time::sleep(std::time::Duration::from_millis(ENUMERATE_DELAY_MS)).await;
    }
}

/// hidapi initialization
///
/// Sets up a processing thread for hidapi.
pub async fn initialize(mailbox: mailbox::Mailbox) {
    info!("Initializing device/hidapi...");

    // Spawn watcher thread (tokio)
    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();
}