diff --git a/CMakeLists.txt b/CMakeLists.txt index e18ee23be..b4c99be51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,9 @@ elseif(NOT WIN32) option(HIDAPI_WITH_HIDRAW "Build HIDRAW-based implementation of HIDAPI" ON) option(HIDAPI_WITH_LIBUSB "Build LIBUSB-based implementation of HIDAPI" ON) endif() + if(CMAKE_SYSTEM_NAME MATCHES "NetBSD") + option(HIDAPI_WITH_NETBSD "Build NetBSD/UHID implementation of HIDAPI" ON) + endif() endif() option(BUILD_SHARED_LIBS "Build shared version of the libraries, otherwise build statically" ON) diff --git a/netbsd/CMakeLists.txt b/netbsd/CMakeLists.txt new file mode 100644 index 000000000..86067f895 --- /dev/null +++ b/netbsd/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.6.3 FATAL_ERROR) + +add_library(hidapi_netbsd + ${HIDAPI_PUBLIC_HEADERS} + hid.c +) +target_link_libraries(hidapi_netbsd PUBLIC hidapi_include) + +find_package(Threads REQUIRED) + +target_link_libraries(hidapi_netbsd PRIVATE Threads::Threads) + +set_target_properties(hidapi_netbsd + PROPERTIES + EXPORT_NAME "netbsd" + OUTPUT_NAME "hidapi-netbsd" + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + PUBLIC_HEADER "${HIDAPI_PUBLIC_HEADERS}" +) + +# compatibility with find_package() +add_library(hidapi::netbsd ALIAS hidapi_netbsd) +# compatibility with raw library link +add_library(hidapi-netbsd ALIAS hidapi_netbsd) + +if(HIDAPI_INSTALL_TARGETS) + install(TARGETS hidapi_netbsd EXPORT hidapi + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/hidapi" + ) +endif() + +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-netbsd.pc.in") diff --git a/netbsd/README.md b/netbsd/README.md new file mode 100644 index 000000000..f1b12a0ae --- /dev/null +++ b/netbsd/README.md @@ -0,0 +1,29 @@ +Implementation Notes +-------------------- +NetBSD maps every `uhidev` device to one or more `uhid` +devices. Each `uhid` device only supports one report ID. +The parent device `uhidev` creates one `uhid` device per +report ID found in the hardware's report descriptor. + +In the event there are no report ID(s) found within the +report descriptor, only one `uhid` device with a report ID +of `0` is created. + +In order to remain compatible with existing `hidapi` APIs, +all the `uhid` devices created by the parent `uhidev` device +must be opened under the same `hid_device` instance to ensure +that we can route reports to their appropriate `uhid` device. + +Internally the `uhid` driver will insert the report ID as +needed so we must also omit the report ID in any situation +where the `hidapi` API expects it to be included in the +report data stream. + +Given the design of `uhid`, it must be augmented with extra +platform specific APIs to ensure that the exact relationship +between `uhidev` devices and `uhid` devices can be determined. + +The NetBSD implementation does this via the `drvctl` kernel +driver. At present there is no known way to do this on OpenBSD +for a `uhid` implementation to be at the same level as the +NetBSD one. diff --git a/netbsd/hid.c b/netbsd/hid.c new file mode 100644 index 000000000..a9b841f81 --- /dev/null +++ b/netbsd/hid.c @@ -0,0 +1,1173 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + James Buren + libusb/hidapi Team + + Copyright 2023, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +/* C */ +#include +#include +#include +#include +#include +#include + +/* Unix */ +#include +#include +#include +#include + +/* NetBSD */ +#include +#include +#include + +#include "hidapi.h" + +#define HIDAPI_MAX_CHILD_DEVICES 256 + +struct hid_device_ { + int device_handle; + int blocking; + wchar_t *last_error_str; + struct hid_device_info *device_info; + size_t poll_handles_length; + struct pollfd poll_handles[256]; + int report_handles[256]; + char path[USB_MAX_DEVNAMELEN]; +}; + +struct hid_enumerate_data { + struct hid_device_info *root; + struct hid_device_info *end; + int drvctl; + uint16_t vendor_id; + uint16_t product_id; +}; + +typedef void (*enumerate_devices_callback) (const struct usb_device_info *, void *); + +static wchar_t *last_global_error_str = NULL; + +/* The caller must free the returned string with free(). */ +static wchar_t *utf8_to_wchar_t(const char *utf8) +{ + wchar_t *ret = NULL; + + if (utf8) { + size_t wlen = mbstowcs(NULL, utf8, 0); + if ((size_t) -1 == wlen) { + return wcsdup(L""); + } + ret = (wchar_t*) calloc(wlen+1, sizeof(wchar_t)); + if (ret == NULL) { + /* as much as we can do at this point */ + return NULL; + } + mbstowcs(ret, utf8, wlen+1); + ret[wlen] = 0x0000; + } + + return ret; +} + +/* Makes a copy of the given error message (and decoded according to the + * currently locale) into the wide string pointer pointed by error_str. + * The last stored error string is freed. + * Use register_error_str(NULL) to free the error message completely. */ +static void register_error_str(wchar_t **error_str, const char *msg) +{ + free(*error_str); + *error_str = utf8_to_wchar_t(msg); +} + +/* Semilar to register_error_str, but allows passing a format string with va_list args into this function. */ +static void register_error_str_vformat(wchar_t **error_str, const char *format, va_list args) +{ + char msg[256]; + vsnprintf(msg, sizeof(msg), format, args); + + register_error_str(error_str, msg); +} + +/* Set the last global error to be reported by hid_error(NULL). + * The given error message will be copied (and decoded according to the + * currently locale, so do not pass in string constants). + * The last stored global error message is freed. + * Use register_global_error(NULL) to indicate "no error". */ +static void register_global_error(const char *msg) +{ + register_error_str(&last_global_error_str, msg); +} + +/* Similar to register_global_error, but allows passing a format string into this function. */ +static void register_global_error_format(const char *format, ...) +{ + va_list args; + va_start(args, format); + register_error_str_vformat(&last_global_error_str, format, args); + va_end(args); +} + +/* Set the last error for a device to be reported by hid_error(dev). + * The given error message will be copied (and decoded according to the + * currently locale, so do not pass in string constants). + * The last stored device error message is freed. + * Use register_device_error(dev, NULL) to indicate "no error". */ +static void register_device_error(hid_device *dev, const char *msg) +{ + register_error_str(&dev->last_error_str, msg); +} + +/* Similar to register_device_error, but you can pass a format string into this function. */ +static void register_device_error_format(hid_device *dev, const char *format, ...) +{ + va_list args; + va_start(args, format); + register_error_str_vformat(&dev->last_error_str, format, args); + va_end(args); +} + + +/* + * 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(const uint8_t *report_descriptor, uint32_t size, unsigned int pos, 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; +} + +/* + * Get bytes from a HID Report Descriptor. + * Only call with a num_bytes of 0, 1, 2, or 4. + */ +static uint32_t get_hid_report_bytes(const uint8_t *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; +} + +/* + * Iterates until the end of a Collection. + * Assumes that *pos is exactly at the beginning of a Collection. + * Skips all nested Collection, i.e. iterates until the end of current level Collection. + * + * The return value is non-0 when an end of current Collection is found, + * 0 when error is occured (broken Descriptor, end of a Collection is found before its begin, + * or no Collection is found at all). + */ +static int hid_iterate_over_collection(const uint8_t *report_descriptor, uint32_t size, unsigned int *pos, int *data_len, int *key_size) +{ + int collection_level = 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, size, *pos, data_len, key_size)) + return 0; /* malformed report */ + + switch (key_cmd) { + case 0xa0: /* Collection 6.2.2.4 (Main) */ + collection_level++; + break; + case 0xc0: /* End Collection 6.2.2.4 (Main) */ + collection_level--; + break; + } + + if (collection_level < 0) { + /* Broken descriptor or someone is using this function wrong, + * i.e. should be called exactly at the collection start */ + return 0; + } + + if (collection_level == 0) { + /* Found it! + * Also possible when called not at the collection start, but should not happen if used correctly */ + return 1; + } + + *pos += *data_len + *key_size; + } + + return 0; /* Did not find the end of a Collection */ +} + +struct hid_usage_iterator { + unsigned int pos; + int usage_page_found; + unsigned short usage_page; +}; + +/* + * 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(const uint8_t *report_descriptor, uint32_t size, struct hid_usage_iterator *ctx, unsigned short *usage_page, unsigned short *usage) +{ + int data_len, key_size; + int initial = ctx->pos == 0; /* Used to handle case where no top-level application collection is defined */ + + int usage_found = 0; + + while (ctx->pos < size) { + int key = report_descriptor[ctx->pos]; + int key_cmd = key & 0xfc; + + /* Determine data_len and key_size */ + if (!get_hid_item_size(report_descriptor, size, ctx->pos, &data_len, &key_size)) + return -1; /* malformed report */ + + switch (key_cmd) { + case 0x4: /* Usage Page 6.2.2.7 (Global) */ + ctx->usage_page = get_hid_report_bytes(report_descriptor, size, data_len, ctx->pos); + ctx->usage_page_found = 1; + break; + + case 0x8: /* Usage 6.2.2.8 (Local) */ + if (data_len == 4) { /* Usages 5.5 / Usage Page 6.2.2.7 */ + ctx->usage_page = get_hid_report_bytes(report_descriptor, size, 2, ctx->pos + 2); + ctx->usage_page_found = 1; + *usage = get_hid_report_bytes(report_descriptor, size, 2, ctx->pos); + usage_found = 1; + } + else { + *usage = get_hid_report_bytes(report_descriptor, size, data_len, ctx->pos); + usage_found = 1; + } + break; + + case 0xa0: /* Collection 6.2.2.4 (Main) */ + if (!hid_iterate_over_collection(report_descriptor, size, &ctx->pos, &data_len, &key_size)) { + return -1; + } + + /* A pair is valid - to be reported when Collection is found */ + if (usage_found && ctx->usage_page_found) { + *usage_page = ctx->usage_page; + return 0; + } + + break; + } + + /* Skip over this key and its associated data */ + ctx->pos += data_len + key_size; + } + + /* 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 && ctx->usage_page_found) { + *usage_page = ctx->usage_page; + return 0; /* success */ + } + + return 1; /* finished processing */ +} + +static struct hid_device_info *create_device_info(const struct usb_device_info *udi, const char *path, const struct usb_ctl_report_desc *ucrd) +{ + struct hid_device_info *root; + struct hid_device_info *end; + + root = (struct hid_device_info *) calloc(1, sizeof(struct hid_device_info)); + if (!root) + return NULL; + + end = root; + + /* Path */ + end->path = (path) ? strdup(path) : NULL; + + /* Vendor Id */ + end->vendor_id = udi->udi_vendorNo; + + /* Product Id */ + end->product_id = udi->udi_productNo; + + /* Serial Number */ + end->serial_number = utf8_to_wchar_t(udi->udi_serial); + + /* Release Number */ + end->release_number = udi->udi_releaseNo; + + /* Manufacturer String */ + end->manufacturer_string = utf8_to_wchar_t(udi->udi_vendor); + + /* Product String */ + end->product_string = utf8_to_wchar_t(udi->udi_product); + + /* Usage Page */ + end->usage_page = 0; + + /* Usage */ + end->usage = 0; + + /* Interface Number */ + end->interface_number = -1; + + /* Next Device Info */ + end->next = NULL; + + /* Bus Type */ + end->bus_type = HID_API_BUS_USB; + + if (ucrd) { + uint16_t page; + uint16_t usage; + struct hid_usage_iterator usage_iterator; + + page = usage = 0; + memset(&usage_iterator, 0, sizeof(usage_iterator)); + + /* + * Parse the first usage and usage page + * out of the report descriptor. + */ + if (get_next_hid_usage(ucrd->ucrd_data, ucrd->ucrd_size, &usage_iterator, &page, &usage) == 0) { + end->usage_page = page; + end->usage = usage; + } + + /* + * Parse any additional usage and usage pages + * out of the report descriptor. + */ + while (get_next_hid_usage(ucrd->ucrd_data, ucrd->ucrd_size, &usage_iterator, &page, &usage) == 0) { + /* Create new record for additional usage pairs */ + struct hid_device_info *node = (struct hid_device_info *) calloc(1, sizeof(struct hid_device_info)); + + if (!node) + continue; + + /* Update fields */ + node->path = (end->path) ? strdup(end->path) : NULL; + node->vendor_id = end->vendor_id; + node->product_id = end->product_id; + node->serial_number = (end->serial_number) ? wcsdup(end->serial_number) : NULL; + node->release_number = end->release_number; + node->manufacturer_string = (end->manufacturer_string) ? wcsdup(end->manufacturer_string) : NULL; + node->product_string = (end->product_string) ? wcsdup(end->product_string) : NULL; + node->usage_page = page; + node->usage = usage; + node->interface_number = end->interface_number; + node->next = NULL; + node->bus_type = end->bus_type; + + /* Insert node */ + end->next = node; + end = node; + } + } + + return root; +} + +static int is_usb_controller(const char *s) +{ + return (!strncmp(s, "usb", 3) && isdigit((int) s[3])); +} + +static int is_uhid_parent_device(const char *s) +{ + return (!strncmp(s, "uhidev", 6) && isdigit((int) s[6])); +} + +static int is_uhid_device(const char *s) +{ + return (!strncmp(s, "uhid", 4) && isdigit((int) s[4])); +} + +static void walk_device_tree(int drvctl, const char *dev, int depth, char arr[static HIDAPI_MAX_CHILD_DEVICES][USB_MAX_DEVNAMELEN], size_t *len, int (*cmp) (const char *)) +{ + int res; + char childname[HIDAPI_MAX_CHILD_DEVICES][USB_MAX_DEVNAMELEN]; + struct devlistargs dla; + + if (depth && (!dev || !*dev)) + return; + + if (cmp(dev) && *len < HIDAPI_MAX_CHILD_DEVICES) + strlcpy(arr[(*len)++], dev, sizeof(*arr)); + + strlcpy(dla.l_devname, dev, sizeof(dla.l_devname)); + dla.l_childname = childname; + dla.l_children = HIDAPI_MAX_CHILD_DEVICES; + + res = ioctl(drvctl, DRVLISTDEV, &dla); + if (res == -1) + return; + + /* + * DO NOT CHANGE THIS. This is a fail-safe check + * for the unlikely event that a parent device has + * more than HIDAPI_MAX_CHILD_DEVICES child devices + * to prevent iterating over uninitialized data. + */ + if (dla.l_children > HIDAPI_MAX_CHILD_DEVICES) + return; + + for (size_t i = 0; i < dla.l_children; i++) + walk_device_tree(drvctl, dla.l_childname[i], depth + 1, arr, len, cmp); +} + +static void enumerate_usb_devices(int bus, uint8_t addr, enumerate_devices_callback func, void *data) +{ + int res; + struct usb_device_info udi; + + udi.udi_addr = addr; + + res = ioctl(bus, USB_DEVICEINFO, &udi); + if (res == -1) + return; + + for (int port = 0; port < udi.udi_nports; port++) { + addr = udi.udi_ports[port]; + if (addr >= USB_MAX_DEVICES) + continue; + + enumerate_usb_devices(bus, addr, func, data); + } + + func(&udi, data); +} + +static void hid_enumerate_callback(const struct usb_device_info *udi, void *data) +{ + struct hid_enumerate_data *hed; + + hed = (struct hid_enumerate_data *) data; + + if (hed->vendor_id != 0 && hed->vendor_id != udi->udi_vendorNo) + return; + + if (hed->product_id != 0 && hed->product_id != udi->udi_productNo) + return; + + for (size_t i = 0; i < USB_MAX_DEVNAMES; i++) { + const char *parent_dev; + char arr[HIDAPI_MAX_CHILD_DEVICES][USB_MAX_DEVNAMELEN]; + size_t len; + const char *child_dev; + char devpath[USB_MAX_DEVNAMELEN]; + int uhid; + struct usb_ctl_report_desc ucrd; + int use_ucrd; + struct hid_device_info *node; + + parent_dev = udi->udi_devnames[i]; + if (!is_uhid_parent_device(parent_dev)) + continue; + + len = 0; + walk_device_tree(hed->drvctl, parent_dev, 0, arr, &len, is_uhid_device); + + if (len == 0) + continue; + + child_dev = arr[0]; + strlcpy(devpath, "/dev/", sizeof(devpath)); + strlcat(devpath, child_dev, sizeof(devpath)); + + uhid = open(devpath, O_RDONLY | O_CLOEXEC); + if (uhid >= 0) { + use_ucrd = (ioctl(uhid, USB_GET_REPORT_DESC, &ucrd) != -1); + close(uhid); + } else { + use_ucrd = 0; + } + + node = create_device_info(udi, parent_dev, (use_ucrd) ? &ucrd : NULL); + if (!node) + continue; + + if (!hed->root) { + hed->root = node; + hed->end = node; + } else { + hed->end->next = node; + hed->end = node; + } + + while (hed->end->next) + hed->end = hed->end->next; + } +} + +static int set_report(hid_device *dev, const uint8_t *data, size_t length, int report) +{ + int res; + int device_handle; + struct usb_ctl_report ucr; + + if (length < 1) { + register_device_error(dev, "report must be greater than 1 byte"); + return -1; + } + + device_handle = dev->report_handles[*data]; + if (device_handle < 0) { + register_device_error_format(dev, "unsupported report id: %hhu", *data); + return -1; + } + + length--; + data++; + + if (length > sizeof(ucr.ucr_data)) { + register_device_error_format(dev, "report must be less than or equal to %zu bytes", sizeof(ucr.ucr_data)); + return -1; + } + + ucr.ucr_report = report; + memcpy(ucr.ucr_data, data, length); + + res = ioctl(device_handle, USB_SET_REPORT, &ucr); + if (res == -1) { + register_device_error_format(dev, "ioctl (USB_SET_REPORT): %s", strerror(errno)); + return -1; + } + + return (int) (length + 1); +} + +static int get_report(hid_device *dev, uint8_t *data, size_t length, int report) +{ + int res; + int device_handle; + struct usb_ctl_report ucr; + + if (length < 1) { + register_device_error(dev, "report must be greater than 1 byte"); + return -1; + } + + device_handle = dev->report_handles[*data]; + if (device_handle < 0) { + register_device_error_format(dev, "unsupported report id: %hhu", *data); + return -1; + } + + length--; + data++; + + if (length > sizeof(ucr.ucr_data)) { + length = sizeof(ucr.ucr_data); + } + + ucr.ucr_report = report; + + res = ioctl(device_handle, USB_GET_REPORT, &ucr); + if (res == -1) { + register_device_error_format(dev, "ioctl (USB_GET_REPORT): %s", strerror(errno)); + return -1; + } + + memcpy(data, ucr.ucr_data, length); + + return (int) (length + 1); +} + +int HID_API_EXPORT HID_API_CALL hid_init(void) +{ + /* indicate no error */ + register_global_error(NULL); + + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_exit(void) +{ + /* Free global error message */ + register_global_error(NULL); + + return 0; +} + +struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + int res; + int drvctl; + char arr[HIDAPI_MAX_CHILD_DEVICES][USB_MAX_DEVNAMELEN]; + size_t len; + struct hid_enumerate_data hed; + + res = hid_init(); + if (res == -1) + return NULL; + + drvctl = open(DRVCTLDEV, O_RDONLY | O_CLOEXEC); + if (drvctl == -1) { + register_global_error_format("failed to open drvctl: %s", strerror(errno)); + return NULL; + } + + len = 0; + walk_device_tree(drvctl, "", 0, arr, &len, is_usb_controller); + + hed.root = NULL; + hed.end = NULL; + hed.drvctl = drvctl; + hed.vendor_id = vendor_id; + hed.product_id = product_id; + + for (size_t i = 0; i < len; i++) { + char devpath[USB_MAX_DEVNAMELEN]; + int bus; + + strlcpy(devpath, "/dev/", sizeof(devpath)); + strlcat(devpath, arr[i], sizeof(devpath)); + + bus = open(devpath, O_RDONLY | O_CLOEXEC); + if (bus == -1) + continue; + + enumerate_usb_devices(bus, 0, hid_enumerate_callback, &hed); + + close(bus); + } + + close(drvctl); + + return hed.root; +} + +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) +{ + while (devs) { + struct hid_device_info *next = devs->next; + free(devs->path); + free(devs->serial_number); + free(devs->manufacturer_string); + free(devs->product_string); + free(devs); + devs = next; + } +} + +HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + struct hid_device_info *devs; + struct hid_device_info *dev; + char path[USB_MAX_DEVNAMELEN]; + + devs = hid_enumerate(vendor_id, product_id); + if (!devs) + return NULL; + + *path = '\0'; + + for (dev = devs; dev; dev = dev->next) { + if (dev->vendor_id != vendor_id) + continue; + + if (dev->product_id != product_id) + continue; + + if (serial_number && wcscmp(dev->serial_number, serial_number)) + continue; + + strlcpy(path, dev->path, sizeof(path)); + + break; + } + + hid_free_enumeration(devs); + + if (*path == '\0') { + register_global_error("Device with requested VID/PID/(SerialNumber) not found"); + return NULL; + } + + return hid_open_path(path); +} + +HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path) +{ + int res; + hid_device *dev; + int drvctl; + char arr[HIDAPI_MAX_CHILD_DEVICES][USB_MAX_DEVNAMELEN]; + size_t len; + + res = hid_init(); + if (res == -1) + goto err_0; + + dev = (hid_device *) calloc(1, sizeof(hid_device)); + if (!dev) { + register_global_error("could not allocate hid_device"); + goto err_0; + } + + drvctl = open(DRVCTLDEV, O_RDONLY | O_CLOEXEC); + if (drvctl == -1) { + register_global_error_format("failed to open drvctl: %s", strerror(errno)); + goto err_1; + } + + if (!is_uhid_parent_device(path)) { + register_global_error("not a uhidev device"); + goto err_2; + } + + len = 0; + walk_device_tree(drvctl, path, 0, arr, &len, is_uhid_device); + + dev->poll_handles_length = 0; + memset(dev->poll_handles, 0x00, sizeof(dev->poll_handles)); + memset(dev->report_handles, 0xff, sizeof(dev->report_handles)); + + for (size_t i = 0; i < len; i++) { + const char *child_dev; + char devpath[USB_MAX_DEVNAMELEN]; + int uhid; + int rep_id; + struct pollfd *ph; + + child_dev = arr[i]; + strlcpy(devpath, "/dev/", sizeof(devpath)); + strlcat(devpath, child_dev, sizeof(devpath)); + + uhid = open(devpath, O_RDWR | O_CLOEXEC); + if (uhid == -1) { + register_global_error_format("failed to open %s: %s", child_dev, strerror(errno)); + goto err_3; + } + + res = ioctl(uhid, USB_GET_REPORT_ID, &rep_id); + if (res == -1) { + close(uhid); + register_global_error_format("failed to get report id %s: %s", child_dev, strerror(errno)); + goto err_3; + } + + ph = &dev->poll_handles[dev->poll_handles_length++]; + ph->fd = uhid; + ph->events = POLLIN; + ph->revents = 0; + dev->report_handles[rep_id] = uhid; + dev->device_handle = uhid; + } + + dev->blocking = 1; + dev->last_error_str = NULL; + dev->device_info = NULL; + strlcpy(dev->path, path, sizeof(dev->path)); + + register_global_error(NULL); + return dev; + +err_3: + for (size_t i = 0; i < dev->poll_handles_length; i++) + close(dev->poll_handles[i].fd); +err_2: + close(drvctl); +err_1: + free(dev); +err_0: + return NULL; +} + +int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, data, length, UHID_OUTPUT_REPORT); +} + +int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + int res; + size_t i; + struct pollfd *ph; + ssize_t n; + + res = poll(dev->poll_handles, dev->poll_handles_length, milliseconds); + if (res == -1) { + register_device_error_format(dev, "error while polling: %s", strerror(errno)); + return -1; + } + + if (res == 0) + return 0; + + for (i = 0; i < dev->poll_handles_length; i++) { + ph = &dev->poll_handles[i]; + + if (ph->revents & (POLLERR | POLLHUP | POLLNVAL)) { + register_device_error(dev, "device IO error while polling"); + return -1; + } + + if (ph->revents & POLLIN) + break; + } + + if (i == dev->poll_handles_length) + return 0; + + n = read(ph->fd, data, length); + if (n == -1) { + if (errno == EAGAIN || errno == EINPROGRESS) + n = 0; + else + register_device_error_format(dev, "error while reading: %s", strerror(errno)); + } + + return n; +} + +int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking) ? -1 : 0); +} + +int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) +{ + dev->blocking = !nonblock; + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, data, length, UHID_FEATURE_REPORT); +} + +int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + return get_report(dev, data, length, UHID_FEATURE_REPORT); +} + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + return get_report(dev, data, length, UHID_INPUT_REPORT); +} + +void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev) +{ + if (!dev) + return; + + /* Free the device error message */ + register_device_error(dev, NULL); + + hid_free_enumeration(dev->device_info); + + for (size_t i = 0; i < dev->poll_handles_length; i++) + close(dev->poll_handles[i].fd); + + free(dev); +} + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + struct hid_device_info *hdi; + + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + hdi = hid_get_device_info(dev); + if (!dev) + return -1; + + if (hdi->manufacturer_string) { + wcsncpy(string, hdi->manufacturer_string, maxlen); + string[maxlen - 1] = L'\0'; + } else { + string[0] = L'\0'; + } + + return 0; +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + struct hid_device_info *hdi; + + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + hdi = hid_get_device_info(dev); + if (!dev) + return -1; + + if (hdi->product_string) { + wcsncpy(string, hdi->product_string, maxlen); + string[maxlen - 1] = L'\0'; + } else { + string[0] = L'\0'; + } + + return 0; +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + struct hid_device_info *hdi; + + if (!string || !maxlen) { + register_device_error(dev, "Zero buffer/length"); + return -1; + } + + hdi = hid_get_device_info(dev); + if (!dev) + return -1; + + if (hdi->serial_number) { + wcsncpy(string, hdi->serial_number, maxlen); + string[maxlen - 1] = L'\0'; + } else { + string[0] = L'\0'; + } + + return 0; +} + +struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_get_device_info(hid_device *dev) +{ + int res; + struct usb_device_info udi; + struct usb_ctl_report_desc ucrd; + int use_ucrd; + struct hid_device_info *hdi; + + if (dev->device_info) + return dev->device_info; + + res = ioctl(dev->device_handle, USB_GET_DEVICEINFO, &udi); + if (res == -1) { + register_device_error_format(dev, "ioctl (USB_GET_DEVICEINFO): %s", strerror(errno)); + return NULL; + } + + use_ucrd = (ioctl(dev->device_handle, USB_GET_REPORT_DESC, &ucrd) != -1); + + hdi = create_device_info(&udi, dev->path, (use_ucrd) ? &ucrd : NULL); + if (!hdi) { + register_device_error(dev, "failed to create device info"); + return NULL; + } + + dev->device_info = hdi; + + return hdi; +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + int res; + struct usb_string_desc usd; + usb_string_descriptor_t *str; + iconv_t ic; + const char *src; + size_t srcleft; + char *dst; + size_t dstleft; + size_t ic_res; + + /* First let us get the supported language IDs. */ + usd.usd_string_index = 0; + usd.usd_language_id = 0; + + res = ioctl(dev->device_handle, USB_GET_STRING_DESC, &usd); + if (res == -1) { + register_device_error_format(dev, "ioctl (USB_GET_STRING_DESC): %s", strerror(errno)); + return -1; + } + + str = &usd.usd_desc; + + if (str->bLength < 4) { + register_device_error(dev, "failed to get supported language IDs"); + return -1; + } + + /* Now we can get the requested string. */ + usd.usd_string_index = string_index; + usd.usd_language_id = UGETW(str->bString[0]); + + res = ioctl(dev->device_handle, USB_GET_STRING_DESC, &usd); + if (res == -1) { + register_device_error_format(dev, "ioctl (USB_GET_STRING_DESC): %s", strerror(errno)); + return -1; + } + + /* Now we need to convert it, using iconv. */ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + ic = iconv_open("utf-32le", "utf-16le"); +#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + ic = iconv_open("utf-32be", "utf-16le"); +#endif + if (ic == (iconv_t) -1) { + register_device_error_format(dev, "iconv_open failed: %s", strerror(errno)); + return -1; + } + + src = (const char *) str->bString; + srcleft = str->bLength - 2; + dst = (char *) string; + dstleft = sizeof(wchar_t[maxlen]); + + ic_res = iconv(ic, &src, &srcleft, &dst, &dstleft); + iconv_close(ic); + if (ic_res == (size_t) -1) { + register_device_error_format(dev, "iconv failed: %s", strerror(errno)); + return -1; + } + + /* Write the terminating NULL. */ + string[maxlen - 1] = L'\0'; + if (dstleft >= sizeof(wchar_t)) + *((wchar_t *) dst) = L'\0'; + + return 0; +} + +int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size) +{ + int res; + struct usb_ctl_report_desc ucrd; + + res = ioctl(dev->device_handle, USB_GET_REPORT_DESC, &ucrd); + if (res == -1) { + register_device_error_format(dev, "ioctl (USB_GET_REPORT_DESC): %s", strerror(errno)); + return -1; + } + + if ((size_t) ucrd.ucrd_size < buf_size) + buf_size = (size_t) ucrd.ucrd_size; + + memcpy(buf, ucrd.ucrd_data, buf_size); + + return (int) buf_size; +} + +HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *dev) +{ + if (dev) { + if (dev->last_error_str == NULL) + return L"Success"; + return dev->last_error_str; + } + + if (last_global_error_str == NULL) + return L"Success"; + return last_global_error_str; +} + +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void) +{ + static const struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH + }; + + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) +{ + return HID_API_VERSION_STR; +} diff --git a/pc/hidapi-netbsd.pc.in b/pc/hidapi-netbsd.pc.in new file mode 100644 index 000000000..fcb5ca215 --- /dev/null +++ b/pc/hidapi-netbsd.pc.in @@ -0,0 +1,11 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: hidapi-netbsd +Description: C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the netbsd implementation. +URL: https://github.com/libusb/hidapi +Version: @VERSION@ +Libs: -L${libdir} -lhidapi-netbsd +Cflags: -I${includedir}/hidapi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d08224be8..8213e55ee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -140,6 +140,18 @@ else() set(HIDAPI_NEED_EXPORT_LIBUDEV TRUE) endif() endif() + elseif(CMAKE_SYSTEM_NAME MATCHES "NetBSD") + if(NOT DEFINED HIDAPI_WITH_NETBSD) + set(HIDAPI_WITH_NETBSD ON) + endif() + if(HIDAPI_WITH_NETBSD) + add_subdirectory("${PROJECT_ROOT}/netbsd" netbsd) + list(APPEND EXPORT_COMPONENTS netbsd) + set(EXPORT_ALIAS netbsd) + if(NOT BUILD_SHARED_LIBS) + set(HIDAPI_NEED_EXPORT_THREADS TRUE) + endif() + endif() else() set(HIDAPI_WITH_LIBUSB ON) endif()