Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage Page and Usage on Linux with hidraw #139

Merged
merged 11 commits into from
Nov 23, 2020
6 changes: 3 additions & 3 deletions hidapi/hidapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ extern "C" {
/** Product string */
wchar_t *product_string;
/** Usage Page for this Device/Interface
(Windows/Mac only). */
(Windows/Mac/hidraw only) */
unsigned short usage_page;
/** Usage for this Device/Interface
(Windows/Mac only).*/
(Windows/Mac/hidraw only) */
unsigned short usage;
/** The USB interface which this logical device
represents.
Expand All @@ -124,7 +124,7 @@ extern "C" {
needed. This function should be called at the beginning of
execution however, if there is a chance of HIDAPI handles
being opened by different threads simultaneously.

@ingroup API

@returns
Expand Down
301 changes: 264 additions & 37 deletions linux/hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,19 @@ static void register_global_error(const char *msg)
last_global_error_str = utf8_to_wchar_t(msg);
}

/* See register_global_error, but you can pass a format string into this function. */
static void register_global_error_format(const char *format, ...)
{
va_list args;
va_start(args, format);

char msg[100];
vsnprintf(msg, sizeof(msg), format, args);

va_end(args);

register_global_error(msg);
}

/* Set the last error for a device to be reported by hid_error(device).
* The given error message will be copied (and decoded according to the
Expand Down Expand Up @@ -158,11 +171,67 @@ static wchar_t *copy_udev_string(struct udev_device *dev, const char *udev_name)
return utf8_to_wchar_t(udev_device_get_sysattr_value(dev, udev_name));
}

/*
* Gets the size of the HID item at the given position
* Returns 1 if successful, 0 if an invalid key
* Sets data_len and key_size when successful
*/
static int get_hid_item_size(__u8 *report_descriptor, unsigned int pos, __u32 size, int *data_len, int *key_size)
{
int key = report_descriptor[pos];
int size_code;

/*
* This is a Long Item. The next byte contains the
* length of the data section (value) for this key.
* See the HID specification, version 1.11, section
* 6.2.2.3, titled "Long Items."
*/
if ((key & 0xf0) == 0xf0) {
if (pos + 1 < size)
{
*data_len = report_descriptor[pos + 1];
*key_size = 3;
return 1;
}
*data_len = 0; /* malformed report */
*key_size = 0;
}

/*
* This is a Short Item. The bottom two bits of the
* key contain the size code for the data section
* (value) for this key. Refer to the HID
* specification, version 1.11, section 6.2.2.2,
* titled "Short Items."
*/
size_code = key & 0x3;
switch (size_code) {
case 0:
case 1:
case 2:
*data_len = size_code;
*key_size = 1;
return 1;
case 3:
*data_len = 4;
*key_size = 1;
return 1;
default:
/* Can't ever happen since size_code is & 0x3 */
*data_len = 0;
*key_size = 0;
break;
};

/* malformed report */
return 0;
}

/* uses_numbered_reports() returns 1 if report_descriptor describes a device
which contains numbered reports. */
static int uses_numbered_reports(__u8 *report_descriptor, __u32 size) {
unsigned int i = 0;
int size_code;
int data_len, key_size;

while (i < size) {
Expand All @@ -175,42 +244,9 @@ static int uses_numbered_reports(__u8 *report_descriptor, __u32 size) {
return 1;
}

//printf("key: %02hhx\n", key);

if ((key & 0xf0) == 0xf0) {
/* This is a Long Item. The next byte contains the
length of the data section (value) for this key.
See the HID specification, version 1.11, section
6.2.2.3, titled "Long Items." */
if (i+1 < size)
data_len = report_descriptor[i+1];
else
data_len = 0; /* malformed report */
key_size = 3;
}
else {
/* This is a Short Item. The bottom two bits of the
key contain the size code for the data section
(value) for this key. Refer to the HID
specification, version 1.11, section 6.2.2.2,
titled "Short Items." */
size_code = key & 0x3;
switch (size_code) {
case 0:
case 1:
case 2:
data_len = size_code;
break;
case 3:
data_len = 4;
break;
default:
/* Can't ever happen since size_code is & 0x3 */
data_len = 0;
break;
};
key_size = 1;
}
/* Determine data_len and key_size */
if (!get_hid_item_size(report_descriptor, i, size, &data_len, &key_size))
return 0; /* malformed report */

/* Skip over this key and it's associated data */
i += data_len + key_size;
Expand All @@ -220,6 +256,157 @@ static int uses_numbered_reports(__u8 *report_descriptor, __u32 size) {
return 0;
}

/*
* Get bytes from a HID Report Descriptor.
* Only call with a num_bytes of 0, 1, 2, or 4.
*/
static __u32 get_hid_report_bytes(__u8 *rpt, size_t len, size_t num_bytes, size_t cur)
{
/* Return if there aren't enough bytes. */
if (cur + num_bytes >= len)
return 0;

if (num_bytes == 0)
return 0;
else if (num_bytes == 1)
return rpt[cur + 1];
else if (num_bytes == 2)
return (rpt[cur + 2] * 256 + rpt[cur + 1]);
else if (num_bytes == 4)
return (
rpt[cur + 4] * 0x01000000 +
rpt[cur + 3] * 0x00010000 +
rpt[cur + 2] * 0x00000100 +
rpt[cur + 1] * 0x00000001
);
else
return 0;
}

/*
* Retrieves the device's Usage Page and Usage from the report descriptor.
* The algorithm returns the current Usage Page/Usage pair whenever a new
* Collection is found and a Usage Local Item is currently in scope.
* Usage Local Items are consumed by each Main Item (See. 6.2.2.8).
* The algorithm should give similar results as Apple's:
* https://developer.apple.com/documentation/iokit/kiohiddeviceusagepairskey?language=objc
* Physical Collections are also matched (macOS does the same).
*
* This function can be called repeatedly until it returns non-0
* Usage is found. pos is the starting point (initially 0) and will be updated
* to the next search position.
*
* The return value is 0 when a pair is found.
* 1 when finished processing descriptor.
* -1 on a malformed report.
*/
static int get_next_hid_usage(__u8 *report_descriptor, __u32 size, unsigned int *pos, unsigned short *usage_page, unsigned short *usage)
{
int data_len, key_size;
int initial = *pos == 0; /* Used to handle case where no top-level application collection is defined */
int usage_pair_ready = 0;

/* Usage is a Local Item, it must be set before each Main Item (Collection) before a pair is returned */
int usage_found = 0;

while (*pos < size) {
int key = report_descriptor[*pos];
int key_cmd = key & 0xfc;

/* Determine data_len and key_size */
if (!get_hid_item_size(report_descriptor, *pos, size, &data_len, &key_size))
return -1; /* malformed report */

switch (key_cmd) {
case 0x4: /* Usage Page 6.2.2.7 (Global) */
*usage_page = get_hid_report_bytes(report_descriptor, size, data_len, *pos);
break;

case 0x8: /* Usage 6.2.2.8 (Local) */
*usage = get_hid_report_bytes(report_descriptor, size, data_len, *pos);
usage_found = 1;
break;

case 0xa0: /* Collection 6.2.2.4 (Main) */
/* A Usage Item (Local) must be found for the pair to be valid */
if (usage_found)
usage_pair_ready = 1;

/* Usage is a Local Item, unset it */
usage_found = 0;
break;

case 0x80: /* Input 6.2.2.4 (Main) */
case 0x90: /* Output 6.2.2.4 (Main) */
case 0xb0: /* Feature 6.2.2.4 (Main) */
case 0xc0: /* End Collection 6.2.2.4 (Main) */
/* Usage is a Local Item, unset it */
usage_found = 0;
break;
}

/* Skip over this key and it's associated data */
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's "its"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*pos += data_len + key_size;

/* Return usage pair */
if (usage_pair_ready)
return 0;
}

/* If no top-level application collection is found and usage page/usage pair is found, pair is valid
https://docs.microsoft.com/en-us/windows-hardware/drivers/hid/top-level-collections */
if (initial && usage_found)
return 0; /* success */

return 1; /* finished processing */
}

/*
* Retrieves the hidraw report descriptor from a file.
* When using this form, <sysfs_path>/device/report_descriptor, elevated priviledges are not required.
*/
static int get_hid_report_descriptor(const char *rpt_path, struct hidraw_report_descriptor *rpt_desc)
{
int rpt_handle;
ssize_t res;

rpt_handle = open(rpt_path, O_RDONLY);
if (rpt_handle < 0) {
register_global_error_format("open failed (%s): %s", rpt_path, strerror(errno));
return -1;
}

/*
* Read in the Report Descriptor
* The sysfs file has a maximum size of 4096 (which is the same as HID_MAX_DESCRIPTOR_SIZE) so we should always
* be ok when reading the descriptor.
* In practice if the HID descriptor is any larger I suspect many other things will break.
*/
memset(rpt_desc, 0x0, sizeof(*rpt_desc));
res = read(rpt_handle, rpt_desc->value, HID_MAX_DESCRIPTOR_SIZE);
if (res < 0) {
register_global_error_format("read failed (%s): %s", rpt_path, strerror(errno));
}
rpt_desc->size = (__u32) res;

close(rpt_handle);
return (int) res;
}

static int get_hid_report_descriptor_from_sysfs(const char *sysfs_path, struct hidraw_report_descriptor *rpt_desc)
{
int res = -1;
/* Construct <sysfs_path>/device/report_descriptor */
size_t rpt_path_len = strlen(sysfs_path) + 25 + 1;
char* rpt_path = (char*) calloc(1, rpt_path_len);
snprintf(rpt_path, rpt_path_len, "%s/device/report_descriptor", sysfs_path);

res = get_hid_report_descriptor(rpt_path, rpt_desc);
free(rpt_path);

return res;
}

/*
* The caller is responsible for free()ing the (newly-allocated) character
* strings pointed to by serial_number_utf8 and product_name_utf8 after use.
Expand Down Expand Up @@ -451,6 +638,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
char *product_name_utf8 = NULL;
int bus_type;
int result;
struct hidraw_report_descriptor report_desc;

/* Get the filename of the /sys entry for the device
and create a udev_device object (dev) representing it */
Expand Down Expand Up @@ -582,6 +770,45 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
* check for USB and Bluetooth devices above */
break;
}

/* Usage Page and Usage */
result = get_hid_report_descriptor_from_sysfs(sysfs_path, &report_desc);
if (result >= 0) {
unsigned short page = 0, usage = 0;
unsigned int pos = 0;
/*
* Parse the first usage and usage page
* out of the report descriptor.
*/
if (!get_next_hid_usage(report_desc.value, report_desc.size, &pos, &page, &usage)) {
cur_dev->usage_page = page;
cur_dev->usage = usage;
}

/*
* Parse any additional usage and usage pages
* out of the report descriptor.
*/
while (!get_next_hid_usage(report_desc.value, report_desc.size, &pos, &page, &usage)) {
/* Create new record for additional usage pairs */
tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info));
cur_dev->next = tmp;
prev_dev = cur_dev;
cur_dev = tmp;

/* Update fields */
cur_dev->path = strdup(dev_path);
cur_dev->vendor_id = dev_vid;
cur_dev->product_id = dev_pid;
cur_dev->serial_number = prev_dev->serial_number? wcsdup(prev_dev->serial_number): NULL;
cur_dev->release_number = prev_dev->release_number;
cur_dev->interface_number = prev_dev->interface_number;
cur_dev->manufacturer_string = prev_dev->manufacturer_string? wcsdup(prev_dev->manufacturer_string): NULL;
cur_dev->product_string = prev_dev->product_string? wcsdup(prev_dev->product_string): NULL;
cur_dev->usage_page = page;
cur_dev->usage = usage;
}
}
}

next:
Expand Down