🕌

hidapi クレートを使ってみる

2023/01/15に公開

はじめに

hidapi クレートの存在は知っていたけど、そういえばちゃんと使ったことがなかった。
ということで hidapi クレートを使ってネイティブ環境の HID からレポートを読んでみる。

デバイス列挙

最後に rust を触ってから日が経ってしまったので、 cargo の使い方も覚束ない。
以下のように始めた。

> rustup update
> cargo new try-hidapi
> cd try-hidapi
> cargo add hidapi
> code .

すると、 "Cargo.toml" は以下のようになっている。

[dependencies]
hidapi = "2.1.0"

"main.rs" を以下のようにしてみる。

main.rs
extern crate hidapi;

use hidapi::HidApi;

fn main() {
    match HidApi::new() {
        Ok(api) => {
            let mut devs: Vec<_> = api.device_list().collect();
            devs.sort_by_key(|d| d.product_id());
            devs.sort_by_key(|d| d.vendor_id());
            for device in devs {
                println!("PID:{:04X}_VID:{:04X}&UP:{:04X}_U:{:04X}", 
                        device.vendor_id(), device.product_id(),
                        device.usage_page(), device.usage());
                if let Ok(hid) = device.open_device(&api) {
                    if let Ok(man) = hid.get_manufacturer_string() {
                        println!("  manufacturer: {}", man.unwrap());
                    } else {
                        println!("  failed to get manufacturer");
                    }
                    if let Ok(prd) = hid.get_product_string() {
                        println!("  product name: {}", prd.unwrap());
                    } else {
                        println!("  failed to get product name");
                    }
                    // try `let...else...` statement
                    let Ok(sn) = hid.get_serial_number_string() else {
                        println!("  failed to get serial number");
                        continue;
                    };
                    println!("  serial number: {}", sn.unwrap());
                } else {
                    println!("  it cannot be opened");
                    continue;
                }
            }
        },
        Err(e) => {
            eprintln!("Error: {}", e);
        },
    }
}

cargo run すると

PID:0000_VID:0000&UP:000D_U:0001
  manufacturer: Microsoft
  product name: HID VHF Driver
  serial number: 1.0
... (中略) ...
PID:046D_VID:0A4C&UP:000B_U:0001
  manufacturer: Logitech
  product name: Logi Z407
  serial number: 00000000
PID:046D_VID:C52B&UP:FF00_U:0004
  manufacturer: Logitech
  product name: USB Receiver
  serial number:
... (中略) ...
PID:054C_VID:09CC&UP:0001_U:0005
  manufacturer: Sony Interactive Entertainment
  product name: Wireless Controller
  serial number: 401b5fc1d427
... (以下略) ...

VID と PID でソートされた状態で HID デバイスが列挙された。
PID:054C_VID:09CC&UP:0001_U:0005 は DualShock4 だ。

Input Report を読む

ということで毎度ながら DualShock4 の HID Input Report を読んでみる。
"main.rs" は以下のようにした。

main.rs
extern crate hidapi;

use hidapi::{HidApi, HidDevice};

/// from "https://bitbucket.org/unessa/dualshock4-rust/src/master/src/dualshock4/mod.rs"
const DUALSHOCK4_VENDOR_ID:u16 = 0x54c;
// Dualshock4 product ID changed after playstation update 5.50
const DUALSHOCK4_PRODUCT_ID_NEW:u16 = 0x9cc;
const DUALSHOCK4_PRODUCT_ID_OLD:u16 = 0x5c4;

fn start(hid: HidDevice) {
    let mut buf = [0u8; 78];
    let timeout =  1000;
    loop {
        match hid.read_timeout(&mut buf, timeout) {
            Ok(sz) => {
                println!("Read ({} bytes): {:?}", sz, &buf[..sz]);
            },
            Err(_) => ()
        }
    }
}

fn main() {
    match HidApi::new() {
        Ok(api) => {
            for info in api.device_list() {
                if info.vendor_id() == DUALSHOCK4_VENDOR_ID && (
                    info.product_id() == DUALSHOCK4_PRODUCT_ID_NEW ||
                    info.product_id() == DUALSHOCK4_PRODUCT_ID_OLD ) {
                    println!("DualShock4 was found");
                    if let Ok(hid) = info.open_device(&api) {
                        println!("Succeeded to open DualShock4");
                        start(hid);
                        break;
                    } else {
                        println!("Failed to open DualShock4");
                        continue;
                    }
                }
            }
        },
        Err(e) => {
            eprintln!("Error: {}", e);
        },
    }
    println!("DualShock4 was not found");
}

出力は以下のようになった。

DualShock4 was found
Succeeded to open DualShock4
Read (78 bytes): [17, 192, 0, 132, 124, 127, 124, 8, 0, 164, 0, 0, 149, 134, 10, 9, 0, 0, 0, 247, 255, 244, 255, 164, 30, 0, 2, 0, 0, 0, 0, 0, 27, 0, 0, 1, 204, 132, 163, 32, 13, 131, 172, 21, 40, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 141, 101, 244, 80]
Read (78 bytes): [17, 192, 0, 132, 124, 127, 124, 8, 0, 168, 0, 0, 132, 137, 10, 9, 0, 255, 255, 245, 255, 245, 255, 132, 30, 12, 2, 0, 0, 0, 0, 0, 27, 0, 0, 1, 204, 132, 163, 32, 13, 131, 172, 21, 40, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 76, 51, 207, 92]
Read (78 bytes): [17, 192, 0, 132, 124, 127, 124, 8, 0, 172, 0, 0, 114, 140, 10, 11, 0, 0, 0, 246, 255, 5, 0, 134, 30, 249, 1, 0, 0, 0, 0, 0, 27, 0, 0, 1, 204, 132, 163, 32, 13, 131, 172, 21, 40, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 228, 96, 6, 13]
...

(すごい勢いでコマンドラインが流れるので、Ctrl-C ですぐに止める)

せっかくなのでデコードする

このバイト列をデコードするために以下のようなモジュールを追加する。

ds4.rs
ds4.rs
/// refer to https://www.psdevwiki.com/ps4/DS4-BT#0x11

#[derive(Debug, Default, Clone)]
pub struct Ds4Buttons<T: Clone> {
  pub north: T,
  pub east: T,
  pub south: T,
  pub west: T,
  pub square: T,
  pub cross: T,
  pub circle: T,
  pub triangle: T,
  pub l1: T,
  pub r1: T,
  pub l2: T,
  pub r2: T,
  pub pause: T,
  pub option: T,
  pub l3: T,
  pub r3: T,
  pub ps: T,
  pub tpad: T,
}

fn gen_buttons(raw: [u8; 3]) -> Ds4Buttons<bool> {
  let mut n = false;
  let mut e = false;
  let mut s = false;
  let mut w = false;
  match raw[0] & 0xF {
    0 => { n = true; },
    1 => { n = true; e = true; },
    2 => { e = true; },
    3 => { e = true; s = true; },
    4 => { s = true; },
    5 => { s = true; w = true; },
    6 => { w = true; },
    7 => { w = true; n = true; },
    _ => (),
  }
  Ds4Buttons {
    north: n,
    east: e,
    south: s,
    west: w,
    square: (raw[0] & 0x10) != 0,
    cross: (raw[0] & 0x20) != 0,
    circle: (raw[0] & 0x40) != 0,
    triangle: (raw[0] & 0x80) != 0,
    l1: (raw[1] & 0x01) != 0,
    r1: (raw[1] & 0x02) != 0,
    l2: (raw[1] & 0x04) != 0,
    r2: (raw[1] & 0x08) != 0,
    pause: (raw[1] & 0x10) != 0,
    option: (raw[1] & 0x20) != 0,
    l3: (raw[1] & 0x40) != 0,
    r3: (raw[1] & 0x80) != 0,
    ps: (raw[2] & 0x01) != 0,
    tpad: (raw[2] & 0x02) != 0,
  }
}

#[derive(Debug, Default, Clone)]
pub struct Ds4Finger {
  pub down: bool,
  pub track_num: u8,
  pub coord: (u16, u16), // (x, y) in 12bit
}

fn gen_finger(raw: [u8;4]) -> Ds4Finger {
  Ds4Finger {
    down: (raw[0] & 0x80) != 0,
    track_num: (raw[0] & 0x7f),
    coord: (
      (raw[1] as u16) | (((raw[2] & 0xF) as u16) << 8),
      ((raw[2] >> 4) as u16) | ((raw[3] as u16) << 4)
    ),
  }
}

#[derive(Debug, Default, Clone)]
pub struct Ds4Input {
  pub report_id: u8,
  pub stick_left: (u8, u8), // 0(left, top)..+256(right, bottom)
  pub stick_right: (u8, u8), // 0(left, top)..+256(right, bottom)
  pub buttons: Ds4Buttons<bool>,
  pub counter: u8,
  pub trigger_left: u8, // 0..+256
  pub trigger_right: u8, // 0..+256
  pub gyro: (i16, i16, i16), // (x, y, z) 
  pub accel: (i16, i16, i16), // (x, y, z) positive: right, up, backward
  pub finger1: Ds4Finger,
  pub finger2: Ds4Finger,
}

pub fn input(rep: &Vec<u8>, ofs: usize) -> Ds4Input {
  Ds4Input {
    report_id: rep[ofs + 0],
    stick_left: (rep[ofs + 2], rep[ofs + 3]),
    stick_right: (rep[ofs + 4], rep[ofs + 5]),
    buttons: gen_buttons([rep[ofs + 6], rep[ofs + 7], rep[ofs + 8]]),
    counter: rep[ofs + 8] >> 2,
    trigger_left: rep[ofs + 9],
    trigger_right: rep[ofs + 10],
    gyro: (
      (rep[ofs + 14] as i16)|((rep[ofs + 15] as i16) << 8),
      (rep[ofs + 16] as i16)|((rep[ofs + 17] as i16) << 8),
      (rep[ofs + 18] as i16)|((rep[ofs + 19] as i16) << 8)),
    accel: (
      (rep[ofs + 20] as i16)|((rep[ofs + 21] as i16) << 8),
      (rep[ofs + 22] as i16)|((rep[ofs + 23] as i16) << 8),
      (rep[ofs + 24] as i16)|((rep[ofs + 25] as i16) << 8)),
    finger1: gen_finger([rep[ofs + 36], rep[ofs + 37], rep[ofs + 39], rep[ofs + 40]]),
    finger2: gen_finger([rep[ofs + 41], rep[ofs + 42], rep[ofs + 43], rep[ofs + 44]]),
  }
}

main.rs の start() は以下のように変更。

main.rs
mod ds4;
// ...
fn start(hid: HidDevice) {
    let mut buf = [0u8; 78];
    let timeout =  1000;
    
    loop {
        match hid.read_timeout(&mut buf, timeout) {
            Ok(_) => {
                let rep: Vec<u8> = buf.to_vec();
                // offset 1 byte for HID report ID
                let input = ds4::input(&rep, 1);
                println!("{:?}", &input);
            },
            Err(_) => ()
        }
    }
}

これで、デコードされた DualShock4 の入力情報が Debug 用フォーマットで表示されるようになった。

DualShock4 was found
Succeeded to open DualShock4
Ds4Input { report_id: 192, stick_left: (133, 124), stick_right: (125, 123), buttons: Ds4Buttons { north: false, east: false, south: false, west: false, square: false, cross: false, circle: false, triangle: false, l1: false, r1: false, l2: false, r2: false, pause: false, option: false, l3: false, r3: false, ps: false, tpad: false }, counter: 56, trigger_left: 0, trigger_right: 0, gyro: (9, -1, -10), accel: (-8, 7824, 548), finger1: Ds4Finger { down: true, track_num: 16, coord: (3696, 2290) }, finger2: Ds4Finger { down: false, track_num: 40, coord: (3671, 1) } }
...

おわりに

今回の github リポジトリ

Discussion