Skip to content

schmidt-x/RawHID

Repository files navigation

Raw HID

Introduction

This is not a complete HID communication library. It's key (and only) functionalities are simply writing to and reading from a HID device.

It's been written to use with the devices that natively support handling raw input/output data. As an example of those, any keyboard that's powered by QMK Firmware can handle (send and receive) raw data using RawHID Feature.

Introduction to Human Interface Devices (HID)

As an example, you can take a look at the communication between the host (my PC) and the device (my keyboard):

Usage

Including library

First of all, clone this repository into your AHK Script Library Folders.

Then, include HidDevices and HidDevice files to your main script:

#Requires AutoHotkey v2.0

#Include <RawHID\HidDevices>
#Include <RawHID\HidDevice>

Finding Device

To find your device, call HidDevices.Find(...).

This function returns a HidDeviceInfo object, that contains the device-specific information:

#Requires AutoHotkey v2.0

#Include <RawHID\HidDevices>
#Include <RawHID\HidDevice>

; both are keyboard specific
VendorID  := 0xFEED
ProductID := 0x0003

; default values for QMK Firmware
UsagePage := 0xFF60 ; The usage page of the Raw HID interface
UsageID   := 0x61   ; The usage ID of the Raw HID interface

DeviceInfo := HidDevices.Find(VendorID, ProductID, UsagePage, UsageID, &err)
if err {
  MsgBox(err is DeviceNotFoundError ? "Device not found" : err.Message)
  ExitApp()
}


^i:: { ; Ctrl + i
  MsgBox("DevicePath: " DeviceInfo.DevicePath)
  MsgBox(Format("DeviceName: {} - {}", DeviceInfo.ManufacturerString, DeviceInfo.ProductString))
}

Note

If your keyboard is powered by QMK, VendorID and ProductID can easily be found in your keyboard's info.json file, under the usb object at: ...\qmk_firmware\keyboards\<keyboard>\info.json.
Alternatively, you can use Device Manager on Windows.

Communication

To communicate with the device, you need to instantiate a HidDevice class, passing HidDeviceInfo object, that is returned by .Find(...):

device := HidDevice(DeviceInfo)

HidDevice class has 6 methods:

Open(err: &Error[, desiredAccess: Integer])

Write(arr: Array, err: &Error)

WriteRaw(buff: Buffer, err: &Error)

Read(timeout: Integer, err: &Error) -> Array

ReadRaw(timeout: Integer, err: &Error) -> Buffer

Close()

and 4 properties:

; Length of an array that is returned by .Read method
InputBufferSize

; Max length of an array that is passed to .Write method
OutputBufferSize

; Size of a buffer that is returned by .ReadRaw method
InputRawBufferSize

; Size of a buffer that is passed to .WriteRaw method
OutputRawBufferSize

Note

Both Write and Read methods open the device and close it after, if the device was not initially opened. Same rule is applied to their Raw versions as well.
However, for repetitive calls, it's recommended to once manually open the device, read/write, and close it after.

Important

When writing the data and reading the immediate response, the device should be manually opened before (and closed after).

Writing Data

To simply send data to a device, call .Write(...) method:

^i:: {
  device := HidDevice(DeviceInfo)
	
  ; output.Length must not exceed device.OutputBufferSize
  output := [1, 2, 3, 4, 5]
	
  device.Write(output, &err)
  if err {
    MsgBox(err is DeviceNotConnectedError ? "Device got disconnected" : "Failed to write: " err.Message)
  }
}

Raw version:

^i:: {
  device := HidDevice(DeviceInfo)

  ; output.Size must always be equal to device.OutputRawBufferSize
  output := Buffer(device.OutputRawBufferSize, 0)

  ; Note that the first byte is Report ID and should be set to 0.
  ; Hence, we specify 1 as an Offset to skip it:
  NumPut(
    "UChar", 1,
    "UChar", 2,
    "UChar", 3,
    "UChar", 4,
    "UChar", 5,
    output, 1)

  device.WriteRaw(output, &err)
  if err {
    ; ...
  }
}

Reading Data

To read data from a device, use .Read(...) method:

Note

By default, .Open(...) opens the device with both reading and writing access rights.
If you need it for only reading or only writing, pass one of the following flags as an optional parameter:

  • HID_READ
  • HID_WRITE
^i:: {
  device := HidDevice(DeviceInfo)
	
  ; Since, in this case, we're going to only read from the device, it's opened with the reading rights.
  device.Open(&err, HID_READ)
  if err {
    MsgBox(err is DeviceNotConnectedError ? "Device is not connected" : "Failed to open: " err.Message)
    return
  }
	
  try {
    timeout := 1000 ; ms

    loop {
      ; if succeeded, input.Length always equals to device.InputBufferSize
      input := device.Read(timeout, &err)
      if err {
        if err is TimeoutError { ; true if it's timed out
          continue
        } else if err is DeviceNotConnectedError {
          ; Try to reconnect to the device and continue reading, or just return
          if TryReconnect(device, 2000, 10, HID_READ) {
            continue
          } else {
            MsgBox("Failed to reconnect")
          }
        } else {
          MsgBox("Failed to read: " err.Message)
        }
        return
      }

      ; Do something with the data

      MsgBox(Format("First 3 bytes: [{}, {}, {}].", input[1], input[2], input[3]))
    }
  } finally device.Close()
}

; your reconnection helper function
TryReconnect(device, timeout, times, access := HID_READ | HID_WRITE) {
  isReconnected := false
  loop times {
    Sleep(timeout)
    device.Open(&err, access)
    if err {
      continue
    } else {
      isReconnected := true
      break
    }
  }
  return isReconnected
}

In its Raw version, the body of a loop would look like the following:

; ...

loop {
  ; if succeeded, input.Size always equals to device.InputRawBufferSize
  input := device.ReadRaw(timeout, &err)
  if err {
    ; ...
  }
	
  ; Do something with the data
	
  ; The first byte is Report ID and should be ignored.
  ; To access the actual data, start from the second byte, specifying an Offset as 1 + i'th index:
  byte1 := NumGet(input, 1, "UChar")
  byte2 := NumGet(input, 2, "UChar")
  byte3 := NumGet(input, 3, "UChar")
  
  MsgBox(Format("First 3 bytes: [{}, {}, {}]", byte1, byte2, byte3))
}

; ...

Measuring Write-Read time in milliseconds

DllCall("kernel32\QueryPerformanceFrequency", "Int64*", &Frequency:=0)

^i:: {
  DllCall("kernel32\QueryPerformanceCounter", "Int64*", &startingTime:=0)
	
  device := HidDevice(DeviceInfo)
	
  device.Open(&err)
  if err {
    MsgBox("Failed to open: " err.Message)
    return
  }
	
  try {
    device.Write([], &err)
    if err {
      MsgBox("Failed to write: " err.Message)
      return
    }
		
    _ := device.Read(1000, &err)
    if err {
      MsgBox("Failed to read: " err.Message)
      return
    }
  } finally device.Close()
	
  DllCall("kernel32\QueryPerformanceCounter", "Int64*", &endingTime:=0)
	
  elapsedMilliseconds := Round((endingTime - startingTime) * 1000 / Frequency)
  MsgBox(elapsedMilliseconds " ms")
}

Note

Usage of MsgBox() is for demonstration purposes only.
Personally, I do not recommend to use it for simply displaying errors, while there is a handle (or anythig that should be closed/released/freed) waiting for it to close, since MsgBox() blocks the executing thread until you close the dialog window.

About

Library for communication with HID devices

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published