Rust と Win32 API RawInput で HID の TouchPad を深堀り

Pocket

本記事は、 Rust Advent Calendar 2024 シリーズ1 の 17日目 の記事だ。
ついでに、 Windows Advent Calendar 2024 17日目 の記事としても登録させてもらっている。

Win32 API の RawInput を、先日紹介した Rust + windows-rs を使って呼び出し、 USB/Bluetooth HID の生入力情報を読み取っていきたい。

PC にはキーボードやマウス以外にも、スタイラスやタッチパッド等々の多くの入力デバイスが繋がるが、通常 Windows アプリにはひとつの「マウス」または「キーボード」と抽象化された状態で情報(ウィンドウメッセージ)が届く。

生入力… もとい RawInput を使うと、複数のマウス・キーボードや、任意のヒューマンインターフェイスデバイス (HID) などの、より低レベルなメッセージを受け取れるようになる。

受け取れる情報は以下の3種類。

  • マウスからの RawInput
  • キーボードからの RawInput
  • マウスやキーボード以外の、任意の HID からの RawInput

マウスやキーボードの RawInput は Windows 側で使いやすく整形された情報を取得できる。
ゲームなどでよく使われる事もあり、サンプルも数多く見つかるだろう。

本記事では、このうち最後の「任意の HID からの RawInput 情報の取得」について、高精度タッチパッドを例に簡単に説明していく。

Rust 書き始めてまだ数週間なので、あまり Rust らしい書き方になってないかもだが、その点はご容赦を。

HID プロトコルとは

HID プロトコルは、 元々 USB の通信プロファイルのひとつとして作成され、その後 Bluetooth などでも採用された、様々な入力デバイスで使われる通信規格だ。

この規格に則れば、 OS 側に別途ドライバを必須とせずとも、デバイスを使うことができる。

2024年12月現在の USB-IF による最新の資料は、 HID の通信の手続きについては Device Class Definition for HID 1.11 、その情報の種類を識別するタグにあたる「Usage」の一覧は HID Usage Tables 1.5 にある。

この規格に記載されている "Usage Page" と "Usage ID" の組み合わせで、デバイスの種類やそのデバイスが通信する情報を識別している。

デバイスの種類は、 Collection Application (CA) という単位で区別され、 OS はこの CA を見て届いた情報をどう制御するのか決めている。
例えば、 タッチパッドの CA であれば、以下のように定義されている。

  • Usage Page: Digitizers Page (0x0D)
  • Usage ID: Touch Pad (0x05)

デバイスの種類が決まれば HID のデータ通信(レポートと言う)でやりとりされるデータのフォーマットが決まる…… わけではない。
何の情報がどのように並んでいるかは、デバイス側が「レポートディスクリプタ」で宣言する。

このレポートディスクリプタを元に、各レポートを解析して初めて欲しい情報が得られるという流れとなる。

Rust + Win32 API RawInput での HID の読み取り

前置きが長くなったが、 RawInput を叩くコードを実装していく。

Cargo.toml:

[package]
name = "rawinput-touchpad"
version = "0.1.0"
edition = "2021"

[dependencies.windows]
version = "0.*"
features = [
    "Win32_Devices_HumanInterfaceDevice",
    "Win32_Graphics_Gdi",
    "Win32_System_LibraryLoader",
    "Win32_UI_Input",
    "Win32_UI_WindowsAndMessaging",
]

src/main.rs (1/4):

use std::ffi::c_void;
use std::{
    collections::HashMap,
    sync::{LazyLock, RwLock},
};
use windows::Win32::Foundation::NTSTATUS;
use windows::{
    core::*,
    Win32::{
        Devices::HumanInterfaceDevice::*,
        Foundation::{BOOL, HANDLE, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
        Graphics::Gdi::UpdateWindow, System::LibraryLoader::GetModuleHandleW,
        UI::{Input, Input::*, WindowsAndMessaging, WindowsAndMessaging::*}
    }
};

#[derive(Debug)]
struct NtstatusError(NTSTATUS);
impl ::std::error::Error for NtstatusError {}
impl ::core::fmt::Display for NtstatusError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "HIDP returns {:#08x}", self.0.0)
    }
}

type DynResult<T> = ::std::result::Result<T, Box<dyn ::std::error::Error>>;

// RawInput デバイスハンドルをキーにした、 Preparsed Data (レポートディスクリプタの情報) のハッシュマップ
static HID_CAPS_MAP: LazyLock<RwLock<HashMap<usize, CapsResult>>> = LazyLock::new(|| RwLock::new(HashMap::new()));

fn main() -> Result<()> {
    let hinstance: HINSTANCE = unsafe { GetModuleHandleW(PCWSTR::null())?.into() };

    // メインウィンドウクラスの作成
    // ウィンドウの×を押したらアプリケーションを閉じれるようにするためだけで、特にほかに役割はない
    let wc_main = WNDCLASSW::from({
        let mut wc = WNDCLASSW::default();
        wc.lpfnWndProc = Some(window_proc);
        wc.hInstance = hinstance;
        wc.lpszMenuName = w!("MainMenu");
        wc.lpszClassName = w!("MainMenuClass");
        wc
    });
    if unsafe { RegisterClassW(&wc_main) } == 0 { return Err(Error::from_win32()); }
    let hwnd_main = unsafe { CreateWindowExW(
        WINDOW_EX_STYLE(0),
        wc_main.lpszClassName,
        w!("Main"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        HWND(core::ptr::null_mut()),
        HMENU(core::ptr::null_mut()),
        hinstance,
        Option::None
    )? };
    unsafe {
        let _ = ShowWindow(hwnd_main, SHOW_WINDOW_CMD(5));
        let _ = UpdateWindow(hwnd_main);
    };

    // RawInput を受け取るメッセージ専用ウィンドウ (非表示) の作成
    let wc_input = WNDCLASSW::from({
        let mut wc = WNDCLASSW::default();
        wc.lpfnWndProc = Some(input_proc);
        wc.hInstance = hinstance;
        wc.lpszClassName = w!("InputMessageClass");
        wc
    });
    if unsafe { RegisterClassW(&wc_input) } == 0 { return Err(Error::from_win32()); }
    let lett = unsafe { CreateWindowExW(WINDOW_EX_STYLE(0), wc_input.lpszClassName, PCWSTR::null(), WS_OVERLAPPED, 0, 0, 0, 0, HWND_MESSAGE, HMENU(core::ptr::null_mut()), hinstance, None) };
    let hwnd_input = lett?;
    // USB HID (https://usb.org/sites/default/files/hut1_5.pdf) より、
    // Digitizers Page (0x0D) の Touch Pad (0x05) の RawInput を受け取るウィンドウの登録
    let rids: [_; 1] = [
        RAWINPUTDEVICE {
            usUsagePage: HID_USAGE_PAGE_DIGITIZER,
            usUsage: HID_USAGE_DIGITIZER_TOUCH_PAD,
            dwFlags: RIDEV_INPUTSINK,
            hwndTarget: hwnd_input,
        }
    ];
    unsafe { RegisterRawInputDevices(&rids, size_of::<RAWINPUTDEVICE>() as u32)? };

    // メッセージループ
    let mut msg = MSG::default();
    let mut b_ret;
    while {
        b_ret = unsafe { GetMessageW(&mut msg, HWND(core::ptr::null_mut()), 0, 0) };
        BOOL(0) != b_ret
    } {
        if b_ret == BOOL(-1) {
            continue;
        } else {
            unsafe {
                let _ = TranslateMessage(&msg);
                let _ = DispatchMessageW(&msg);
            }
        }
    }

    Ok(())
}

unsafe extern "system" fn window_proc(
    hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM
) -> LRESULT {
    // ウィンドウの×を押したら閉じるだけ
    match msg {
        WindowsAndMessaging::WM_DESTROY => PostQuitMessage(0),
        _ => return DefWindowProcW(hwnd, msg, wparam, lparam),
    };
    return LRESULT(0);
}

実装を楽にするために出力はコンソールウィンドウに出すので、 #![windows_subsystem = "windows"] 書かずにおく。

main 関数では、クローズボタンでアプリを終了させるためだけにメインウィンドウを表示しつつ、 RawInput のウィンドウメッセージを受け取るだけの専用非表示ウィンドウに、専用のウィンドウプロシージャ input_proc を設定して作成する。
その後、 Usage Page: Digitizers Page (0x0D), Usage Id: Touch Pad (0x05) の CA と、フォアグラウンドでない状態でも受け取る設定 (RIDEV_INPUTSINK) を指定して、 RegisterRawInputDevices 関数 を呼び出す。

RAWINPUT 構造体の情報の取得と、 Preparsed Data のキャッシュ

src/main.rs の続き (2/4):

unsafe extern "system" fn input_proc(
    hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM
) -> LRESULT {
    if msg == WM_INPUT {
        if let Err(err) = (|| -> DynResult<()> {
            let mut buf = Vec::<u8>::new();
            let (ri_result, raw_data_list) = get_raw_input_data(lparam, &mut buf)?;
            match RID_DEVICE_INFO_TYPE(ri_result.header.dwType) {
                Input::RIM_TYPEMOUSE => {},
                Input::RIM_TYPEKEYBOARD => {},
                Input::RIM_TYPEHID => {
                    let hdevice_key: usize = ri_result.header.hDevice.0 as usize;
                    {
                        // search from cache
                        let caps_map_read = HID_CAPS_MAP.read()?;
                        if let Some(recent_caps) = caps_map_read.get(&hdevice_key) {
                            for raw_data in raw_data_list {
                                print_raw_input_data(raw_data, recent_caps)?;
                            }
                            return Ok(());
                        }
                    } // free caps_map_read
                    let mut caps_map_write = HID_CAPS_MAP.write()?;
                    let caps_result = get_new_raw_input_preparsed_capabilities(ri_result.header.hDevice)?;
                    caps_map_write.insert(hdevice_key, caps_result.clone());
                    for raw_data in raw_data_list {
                        print_raw_input_data(raw_data, &caps_result)?;
                    }
                    return Ok(());
                },
                _ => {},
            }
            Ok(())
        })() {
            eprintln!("{}", err);
        };
    };
    return DefWindowProcW(hwnd, msg, wparam, lparam);
}

unsafe fn get_raw_input_data<'a>(lparam: LPARAM, buf: &'a mut Vec<u8>) -> Result<(&'a RAWINPUT, Vec<&'a [u8]>)> {
    let mut dw_size: u32 = 0;
    GetRawInputData(HRAWINPUT(lparam.0 as *mut c_void), RID_INPUT, None, &mut dw_size, size_of::<RAWINPUTHEADER>() as u32);
    buf.resize(dw_size as usize, 0);
    if GetRawInputData(HRAWINPUT(lparam.0 as *mut c_void), RID_INPUT, Some(buf.as_mut_ptr() as *mut c_void), &mut dw_size, size_of::<RAWINPUTHEADER>() as u32) != dw_size {
        eprintln!("GetRawInputData does not return correct size!");
        return Err(Error::from_win32());
    }

    let ri = &*(buf.as_ptr() as *const c_void as *const RAWINPUT);
    let mut raw_data_list = Vec::<&'a [u8]>::with_capacity(ri.data.hid.dwCount as usize);
    if RID_DEVICE_INFO_TYPE(ri.header.dwType) == Input::RIM_TYPEHID {
        let offset = size_of::<RAWINPUTHEADER>() + size_of_val(&ri.data.hid.dwSizeHid) + size_of_val(&ri.data.hid.dwCount);
        for i in 0..ri.data.hid.dwCount {
            raw_data_list.push(&buf[(offset + (ri.data.hid.dwSizeHid * i) as usize)..(offset + (ri.data.hid.dwSizeHid * (i + 1)) as usize)]);
        }
    }

    Ok((ri, raw_data_list))
}

input_proc では WM_INPUT メッセージ だけ受け取り、get_raw_input_data 関数を介して lparam に入っている HRAWINPUT ハンドルにて GetRawInputData 関数 を呼び出して、 RAWINPUT 構造体 を取得する。

ただ、 この RAWINPUT 構造体がややこしい。
HID レポートの情報が入っている .data.hid.bRawData (即ち、 RAWHID 構造体bRawData) には可変長のデータが入っているものの、 windows-rs で定義されている構造体にはその表現がされていない(Rust で言うDST のような表現がされていない)。
つまり、 .data.hid.bRawData フィールドを参照しても、欲しい HID レポートの情報の 1バイト 目しかとれないのだ。

このため、 get_raw_input_data 関数の呼び出し Vec<u8> のバッファ変数を作成して参照渡しをして、 関数内でバッファの長さの調整と GetRawInputData 関数によるバッファへの書き込みを行ったのち、 RAWINPUT 構造体の参照に加え、 HID レポートデータのリストが入っている bRawData 相当のスライスの配列 (raw_data_list) を戻り値に返している。

さて、 受け取った RAWINPUT 構造体の .header.dwType (即ち、 RAWINPUTHEADER 構造体dwType) は RIM_TYPEHID のはずなので、その場合のみ実装する。

.header.hDevice (即ち、 RAWINPUTHEADER 構造体hDevice) から Preparsed Data を取得し、レポートの解析に利用する。(後述)
この Preparsed Data は 前述のレポートディスクリプタを読み取りやすく整形された情報のため、 hDevice が同じはずなら常に同じ値となるはずだ。
このため、ハッシュマップを用いて、 hDevice をキーとして Preparsed Data に関する情報をキャッシュして、 2回目以降のレポートで使い回す。

Preparsed Data のパース

src/main.rs の続き (3/4):

#[derive(Clone)]
struct CapsResult {
    preparseddata_buf: Vec<u8>,
    buttoncaps_list: Vec<HIDP_BUTTON_CAPS>,
    valuecaps_list: Vec<HIDP_VALUE_CAPS>,
}
impl CapsResult {
    fn get_preparseddata(&self) -> PHIDP_PREPARSED_DATA {
        PHIDP_PREPARSED_DATA(self.preparseddata_buf.as_ptr() as *const c_void as isize)
    }
}

unsafe fn get_new_raw_input_preparsed_capabilities(hdevice: HANDLE) -> DynResult<CapsResult> {
    // Get PreparsedData
    let mut dw_size: u32 = 0;
    GetRawInputDeviceInfoW(hdevice, RIDI_PREPARSEDDATA, None, &mut dw_size);
    let mut buf: Vec<u8> = vec![0; dw_size as usize];
    if GetRawInputDeviceInfoW(hdevice, RIDI_PREPARSEDDATA, Some(buf.as_mut_ptr() as *mut c_void), &mut dw_size) != dw_size {
        eprintln!("GetRawInputDeviceInfoW does not return correct size!");
        return Err(Error::from_win32().into());
    }
    let preparseddata = PHIDP_PREPARSED_DATA(buf.as_mut_ptr() as *mut c_void as isize);

    // Parse Capabilities
    let mut capabilities = HIDP_CAPS::default();
    let hidp_result = HidP_GetCaps(preparseddata, &mut capabilities);
    if hidp_result != HIDP_STATUS_SUCCESS { return Err(NtstatusError(hidp_result).into()); }

    let mut buttoncapslength = capabilities.NumberInputButtonCaps;
    let mut buttoncaps_list = vec![HIDP_BUTTON_CAPS::default(); buttoncapslength as usize];
    let hidp_result = HidP_GetButtonCaps(HidP_Input, buttoncaps_list.as_mut_ptr(), &mut buttoncapslength, preparseddata);
    if hidp_result != HIDP_STATUS_SUCCESS { return Err(NtstatusError(hidp_result).into()); }

    let mut valuecapslength = capabilities.NumberInputValueCaps;
    let mut valuecaps_list = vec![HIDP_VALUE_CAPS::default(); valuecapslength as usize];
    let hidp_result = HidP_GetValueCaps(HidP_Input, valuecaps_list.as_mut_ptr(), &mut valuecapslength, preparseddata);
    if hidp_result != HIDP_STATUS_SUCCESS { return Err(NtstatusError(hidp_result).into()); }

    Ok(CapsResult { preparseddata_buf: buf, buttoncaps_list, valuecaps_list })
}

先に述べたとおり、 hDevice から Preparsed Data をパースしていく。

まず、 GetRawInputDeviceInfoW 関数Preparsed Data を取得する。
この情報がレポートディスクリプタの情報に相当するのだが、これではまだコードでは扱いにくいので、 HidP_GetCaps関数, HidP_GetButtonCaps関数, HidP_GetValueCaps関数 を呼び出していき、入力レポートの HIDP_BUTTON_CAPS 構造体HIDP_VALUE_CAPS 構造体 それぞれの配列に変換する。

これらを、 CapsResult ユーザー定義構造体に固めて、前述のようにグローバルのハッシュマップにキャッシュしておく。

レポート情報のダンプ

unsafe fn print_raw_input_data(raw_data: &[u8], caps_result: &CapsResult) -> DynResult<()> {
    let preparseddata = caps_result.get_preparseddata();

    //  Momentary Control (MC)
    // ON になっている HID ボタンを一覧
    print!("btn: [ ");

    // buttoncaps を LinkCollection でグループ化
    let linkcl_usage_map = caps_result.buttoncaps_list.iter().fold(
        HashMap::<u16, Vec::<USAGE_AND_PAGE>>::new(),
        |mut acc, btncaps| {
            let array = match acc.get_mut(&btncaps.LinkCollection) {
                Some(array) => array,
                None => {
                    acc.insert(btncaps.LinkCollection, Vec::new());
                    acc.get_mut(&btncaps.LinkCollection).unwrap()
                },
            };
            array.push(USAGE_AND_PAGE { Usage: btncaps.Anonymous.NotRange.Usage, UsagePage: btncaps.UsagePage });
            acc
        }
    );
    for mapitem in linkcl_usage_map {
        let mut dw_length = caps_result.buttoncaps_list.len() as u32;
        let mut buttonlist = vec![USAGE_AND_PAGE::default(); dw_length as usize];
        let hidp_result = HidP_GetUsagesEx(HidP_Input, mapitem.0, buttonlist.as_mut_ptr(), &mut dw_length, preparseddata, raw_data);
        if hidp_result != HIDP_STATUS_SUCCESS { return Err(NtstatusError(hidp_result).into()); }
        if mapitem.0 == 0 {
            // LinkCollection == 0 のとき、 HidP_GetUsagesEx が全ての UsagePage/Usage のペアを返すので、最上位の LinkCollection のものに絞り込む
            for i in 0..(dw_length as usize) {
                if mapitem.1.contains(&buttonlist[i]) {
                    print!("({:#04x}) {:#04x}-{}, ", buttonlist[i].UsagePage, buttonlist[i].Usage, mapitem.0);
                }
            }
        } else {
            for i in 0..(dw_length as usize) {
                print!("({:#04x}) {:#04x}-{}, ", buttonlist[i].UsagePage, buttonlist[i].Usage, mapitem.0);
            }
        }
    }

    // Usage Type が Dynamic Value (DV) などのデータの取得
    print!("] values: [ ");
    for value in caps_result.valuecaps_list.iter() {
        // レポートの数に応じて HidP_GetUsageValue と HidP_GetUsageValueArray を呼び分ける
        // https://learn.microsoft.com/ja-jp/windows-hardware/drivers/hid/value-capability-arrays
        if value.ReportCount == 1 {
            let mut usage_value = 0u32;
            let hidp_result = HidP_GetUsageValue(HidP_Input, value.UsagePage, value.LinkCollection, value.Anonymous.NotRange.Usage, &mut usage_value, preparseddata, raw_data);
            if hidp_result != HIDP_STATUS_SUCCESS { return Err(NtstatusError(hidp_result).into()); }
            print!("({:#04x}) {:#04x}-{}: {}, ", value.UsagePage, value.Anonymous.NotRange.Usage, value.LinkCollection, usage_value);
        } else if value.ReportCount > 1 {
            if value.BitSize > 64 { panic!("unsupported bit size: {}", value.BitSize); }
            print!("({:#04x}) {:#04x}-{}: [", value.UsagePage, value.Anonymous.NotRange.Usage, value.LinkCollection);
            let usage_bytes_length = (value.BitSize * value.ReportCount).div_ceil(8) as usize;
            let mut usagevalue = vec![0u8; usage_bytes_length];
            let hidp_result = HidP_GetUsageValueArray(HidP_Input, value.UsagePage, value.LinkCollection, value.Anonymous.NotRange.Usage, &mut usagevalue, preparseddata, raw_data);
            if hidp_result != HIDP_STATUS_SUCCESS { return Err(NtstatusError(hidp_result).into()); }
            // usagevalue には、 value.BitSize ビットのデータがビット単位に詰められている。 これを value.BitSize 事に切り分ける。
            // https://learn.microsoft.com/ja-jp/windows-hardware/drivers/hid/value-capability-arrays#usage-value-array
            let mut result = Vec::<u64>::new();
            let mut buffer = 0u64;
            let mut bits_in_buffer = 0;
            for &byte in &usagevalue {
                buffer = (buffer << 8) | byte as u64;
                bits_in_buffer += 8;
                while bits_in_buffer >= value.BitSize {
                    bits_in_buffer -= value.BitSize;
                    let chunk = (buffer >> bits_in_buffer) & ((1 << value.BitSize) - 1);
                    result.push(chunk);
                }
            }
            for &value in &result {
                print!("{}, ", value);
            }
            print!("]");
        }
    }
    println!("]");
    Ok(())
}

最後に、取得したレポート情報と、レポートディスクリプタを使って、ボタンの情報を標準出力にダンプしていく。

レポートに含まれる情報は

の2種類ある。

トップレベルコレクションは、ひとつのレポートに各情報ひとつずつ入っているような情報だ。
Windows 高精度タッチパッド コレクション (touchpad-windows-precision-touchpad-collection)#Windows 精度タッチパッド入力レポート を例にすると、以下がそれにあたる。

Top-level Usages:

Member 説明 Usage Page Usage ID
Scan Time 相対スキャン時間。 0x0D 0x56 Mandatory
Contact Count 特定のレポートで報告される接触の総数。 0x0D 0x54 Mandatory
Button 1 デジタイザーに統合されたタッチパッド ボタンのボタンの状態を示します。 0x09 0x01 Option

一方、リンクコレクションは、複数の情報がグループ化されて入っている。
ひとつのレポートに、 LinkCollection のインデックス違いで複数情報が入りうる。
(ひとつのレポートにいくつ情報が入るかは、レポートディスクリプタで決められている。)
Windows 高精度タッチパッド コレクション (touchpad-windows-precision-touchpad-collection)#Windows 精度タッチパッド入力レポート を例にすると、以下の値が「指の接触」毎に入っていて、例えば5本タッチ対応であれば5本分レポートされる。

コレクション:

Member Description Usage Page Usage ID
Contact ID 特定のフレーム内の接触を一意に識別します。 0x0D 0x51
X 接触位置の X 座標。 0x01 0x30
Y 接触位置の Y 座標。 0x01 0x31
Tip 接触がデジタイザーの表面である場合に設定します。 0x0D 0x42
Confidence 1 1 本の指に対して接触が大きすぎる場合に設定します。 0x0D 0x47

HIDP_BUTTON_CAPS 構造体HIDP_VALUE_CAPS 構造体 の各配列では、 LinkCollection が異なる情報は別アイテムとなっている。
例えば5本分のコレクションがレポートディスクリプタに含まれていれば、 X 座標の情報 (Usage Page 0x01, Usage Id: 0x30) についての HIDP_VALUE_CAPS 構造体 が、 LinkCollection 違いで5個入っている。

つまり、 LinkCollection 毎に上手くグループ化して情報を取得するのがコツとなる。

レポートの値が On/Off 二値のもの(例えばタップされた状態を取得するもの)の、 On 状態のボタン一覧は HidP_GetUsagesEx 関数 で、 UsagePage & UsageId の配列として取得できる。
…のだが、関数に LinkCollection=0 を指定してすべてのボタンの情報を取得すると、 リンクコレクションのボタンについては、 どの LinkCollection で ON 状態になっているか判別ができない。
このため、トップレベルコレクションボタンの状況だけ LinkCollection=0 で取得し、その後 LinkCollection 毎に HidP_GetUsagesEx 関数 を呼び出しを繰り返す必要がある。

一方、レポートにデータが関連付けられている場合は HidP_GetUsageValue関数 を使う。 こちらは関数の呼び出しに UsagePage, UsageId, LinkCollection を指定してひとつずつ呼び出せばよいので、実装は素直だ。

ただ、一部ディスクリプタでは、データが配列となっているため、その場合は HidP_GetUsageValueArray関数 を使う。
この関数がバッファに書き込むデータは、 値機能配列 のページに書かれているようにビット毎に情報が詰められた状態で格納されている。このため、 nビット 毎にビットシフトしながら配列データを読み出す必要がある。

マルチタッチの高精度タッチパッドのレポート

マルチタッチパッドのレポートディスクリプタの例は以下に示されている。

マルチタッチ デバイスでのパケット報告モードの選択 のページでは、マルチタッチデバイスのレポート送信の方法が「並列モード」か「ハイブリッド モード」であることが解説されている。
「並列モード」では、複数の指が接触しているときに、すべての接触情報が 1 つのパケットでレポートされる。
一方、「ハイブリッド モード」では、1 つのレポートでレポートできる接触の数以上の接触を、複数のレポートに分割されて配信する仕組みになっている。

手元のノート PC をいくつか確認したところ、 DELL や HP のノートPCは並列モードで、 Surface Laptop は「ハイブリッドモード」だった。

例えば、並列モードな DELL で上記コードを実行し、 1~3本の指を順次接触させた後一気に離すと以下のようなログがとれた。

btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 28789, (0x0d) 0x54-0: 1, ]
...
btn: [ (0x09) 0x01-0, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29073, (0x0d) 0x54-0: 1, ]

一本目の指をタッチパッドに振れると、 LinkCollection=1 について、 Tip (Usage Page: 0x0D / Usage Id: 0x42) と Confidence (〃 0x0D / 0x47) のボタンが On になる。
また、 Contact ID (0x0D / 0x51) 0 が割り当てられ、X 座標 (0x01 / 0x30) ・ Y座標 (0x01 / 0x31) のレポートが始まる。

Scan Time (0x0D / 0x56) や Contact Count (0x0D / 0x54) はトップレベルコレクションなので LinkCollection=0 として常にレポートされる。
さらに、ここでタッチパッドの押し込み機能でクリックすると、ボタン1 (0x09 / 0x01) のクリックもレポートされる。これもトップレベルコレクションだ。

0x31-5: 0, (0x0d) 0x56-0: 29570, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x09) 0x01-0, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 568, (0x01) 0x31-1: 629, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 857, (0x01) 0x31-2: 172, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29641, (0x0d) 0x54-0: 3, ]
...
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 852, (0x01) 0x31-2: 187, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30280, (0x0d) 0x54-0: 3, ]

タッチパネルに触れる指を 2本, 3本 に増やしていくと、 LinkCollection=2 , 3 について、 Tip, Confidence, Contact ID, X座標, Y座標 のレポートが増えていく。
Contact ID はユニークな値が割り当てられる。

...
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 848, (0x01) 0x31-2: 197, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30422, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x47-1, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 847, (0x01) 0x31-2: 203, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30493, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 1, (0x01) 0x30-1: 846, (0x01) 0x31-1: 206, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30564, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 1, (0x01) 0x30-1: 846, (0x01) 0x31-1: 206, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30635, (0x0d) 0x54-0: 1, ]

指を離していくと LinkCollection が大きいものからレポートされなくなっていくが、ここで注目するのは Contact ID (0x0D / 0x51) だ。
1本目の指が先に離されると、 LinkCollection=1 には2本目以降の指のレポートが繰り上がるが、 どの指の情報なのかは Contact ID を見れば追いかけられる。
上記の例だと、先に 1本目 と 3本目 の指が離され、あとから2本目が離されたようだ。

Windows 高精度タッチパッド コレクション (touchpad-windows-precision-touchpad-collection)#Windows 精度タッチパッド入力レポート の説明によると、指が離された場合に Tip (0x0D / 0x42) が Off となったレポートを必ず送るとなっている。
すなわち、すべてのコンタクトについて Tip が Off となったレポートが届けば、全ての指が離されたと認識できるわけだ。

抜粋していない全てのレポートのログ...

btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 28789, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 28860, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 28931, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29002, (0x0d) 0x54-0: 1, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 617, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29073, (0x0d) 0x54-0: 1, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 618, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29144, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, (0x09) 0x01-0, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 619, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29215, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, (0x09) 0x01-0, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 620, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29286, (0x0d) 0x54-0: 1, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 622, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29357, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x09) 0x01-0, (0x0d) 0x42-2, (0x0d) 0x47-2, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 624, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 862, (0x01) 0x31-2: 149, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29428, (0x0d) 0x54-0: 3, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 567, (0x01) 0x31-1: 626, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 860, (0x01) 0x31-2: 158, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29499, (0x0d) 0x54-0: 3, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 568, (0x01) 0x31-1: 628, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 859, (0x01) 0x31-2: 163, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29570, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x09) 0x01-0, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 568, (0x01) 0x31-1: 629, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 857, (0x01) 0x31-2: 172, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29641, (0x0d) 0x54-0: 3, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 568, (0x01) 0x31-1: 630, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 856, (0x01) 0x31-2: 177, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29712, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x09) 0x01-0, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 568, (0x01) 0x31-1: 631, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 855, (0x01) 0x31-2: 180, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29783, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x09) 0x01-0, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 569, (0x01) 0x31-1: 632, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 854, (0x01) 0x31-2: 181, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29854, (0x0d) 0x54-0: 3, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-2, (0x0d) 0x47-2, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 570, (0x01) 0x31-1: 633, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 854, (0x01) 0x31-2: 182, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29925, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x09) 0x01-0, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 572, (0x01) 0x31-1: 634, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 854, (0x01) 0x31-2: 183, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 29996, (0x0d) 0x54-0: 3, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 575, (0x01) 0x31-1: 634, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 854, (0x01) 0x31-2: 184, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30067, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x09) 0x01-0, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 579, (0x01) 0x31-1: 635, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 853, (0x01) 0x31-2: 185, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30138, (0x0d) 0x54-0: 3, ]
btn: [ (0x09) 0x01-0, (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 853, (0x01) 0x31-2: 186, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30209, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 852, (0x01) 0x31-2: 187, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30280, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-3, (0x0d) 0x47-3, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-2, (0x0d) 0x47-2, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 850, (0x01) 0x31-2: 191, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30351, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x42-1, (0x0d) 0x47-1, (0x0d) 0x42-3, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 848, (0x01) 0x31-2: 197, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30422, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-2, (0x0d) 0x47-2, (0x0d) 0x47-1, (0x0d) 0x47-3, ] values: [ (0x0d) 0x51-1: 0, (0x01) 0x30-1: 582, (0x01) 0x31-1: 636, (0x0d) 0x51-2: 1, (0x01) 0x30-2: 847, (0x01) 0x31-2: 203, (0x0d) 0x51-3: 2, (0x01) 0x30-3: 589, (0x01) 0x31-3: 354, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30493, (0x0d) 0x54-0: 3, ]
btn: [ (0x0d) 0x42-1, (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 1, (0x01) 0x30-1: 846, (0x01) 0x31-1: 206, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30564, (0x0d) 0x54-0: 1, ]
btn: [ (0x0d) 0x47-1, ] values: [ (0x0d) 0x51-1: 1, (0x01) 0x30-1: 846, (0x01) 0x31-1: 206, (0x0d) 0x51-2: 0, (0x01) 0x30-2: 0, (0x01) 0x31-2: 0, (0x0d) 0x51-3: 0, (0x01) 0x30-3: 0, (0x01) 0x31-3: 0, (0x0d) 0x51-4: 0, (0x01) 0x30-4: 0, (0x01) 0x31-4: 0, (0x0d) 0x51-5: 0, (0x01) 0x30-5: 0, (0x01) 0x31-5: 0, (0x0d) 0x56-0: 30635, (0x0d) 0x54-0: 1, ]

 

1 本指操作のハイブリッド モードのレポート記述子 のページには、複数の指がタッチされた場合に同じ Scan Time でレポートが複数に分かれて報告される仕組みについても、例示されている。
具体的な例はこの記事では述べないが、こういった報告方法してくるタッチパッドがあることは、アプリケーションの実装時に留意が必要だ。

まとめ

いかがだったろうか?
高精度タッチパッドを例に、 RawInput API で HID の生データを読み取る方法を深堀してみた。

データの読み取り方法は、 タッチパッドに限らずどんな HID デバイスでも同じように読み取れる。
HID Usage Tables の資料を片手に様々な HID デバイスのレポートを読んでみてはいかがだろう?

明日は、この解析で得た知見を使って、 AutoHotKey にて ThumbSense モドキを作ってみようと思う。

最後に、上記ソースをそのままビルドできる Gist を貼っておく。


  1. USB IF の資料では, 0x0D/0x47 は "Touch Valid: タッチコンタクトが意図された有効な接触であるというデバイスの確信度を示す" となっていて、元々デジタイザ向けの定義と言うこともありニュアンスが異なる。 タッチパッドの場合、パームリジェクションと認識するための数値のようだ。 

Rust と Win32 API RawInput で HID の TouchPad を深堀り」への1件のフィードバック

  1. ピンバック: AutoHotKey v2 で 令和の ThumbSense | Aqua Ware つぶやきブログ

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください