diff --git a/LICENSE.md b/LICENSE.md index 5468640da28..0f4a3097a1e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -50,3 +50,4 @@ The Python modules used by Mbed tools are used under the following licenses: - [pycryptodome](https://pypi.org/project/pycryptodome) - BSD-2-Clause - [pyusb](https://pypi.org/project/pyusb/) - Apache-2.0 - [cmsis-pack-manager](https://pypi.org/project/cmsis-pack-manager) - Apache-2.0 +- [hidapi](https://pypi.org/project/hidapi/) - BSD-style diff --git a/TESTS/host_tests/usb_device_hid.py b/TESTS/host_tests/usb_device_hid.py new file mode 100644 index 00000000000..9a1c0f7142b --- /dev/null +++ b/TESTS/host_tests/usb_device_hid.py @@ -0,0 +1,566 @@ +""" +mbed SDK +Copyright (c) 2019 ARM Limited + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from __future__ import print_function +import functools +import time +import threading +import uuid +import mbed_host_tests +import usb.core +from usb.util import ( + CTRL_IN, + CTRL_OUT, + CTRL_TYPE_STANDARD, + CTRL_TYPE_CLASS, + CTRL_RECIPIENT_DEVICE, + CTRL_RECIPIENT_INTERFACE, + DESC_TYPE_CONFIG, + build_request_type) + +try: + import hid +except ImportError: + CYTHON_HIDAPI_PRESENT = False +else: + CYTHON_HIDAPI_PRESENT = True + +# USB device -- device classes +USB_CLASS_HID = 0x03 + +# USB device -- standard requests +USB_REQUEST_GET_DESCRIPTOR = 0x06 + +# USB device -- HID class requests +HID_REQUEST_GET_REPORT = 0x01 +HID_REQUEST_SET_REPORT = 0x09 +HID_REQUEST_GET_IDLE = 0x02 +HID_REQUEST_SET_IDLE = 0x0A +HID_REQUEST_GET_PROTOCOL = 0x03 +HID_REQUEST_SET_PROTOCOL = 0x0B + +# USB device -- HID class descriptors +DESC_TYPE_HID_HID = 0x21 +DESC_TYPE_HID_REPORT = 0x22 +DESC_TYPE_HID_PHYSICAL = 0x23 + +# USB device -- HID class descriptor lengths +DESC_LEN_HID_HID = 0x09 + +# USB device -- descriptor fields offsets +DESC_OFFSET_BLENGTH = 0 +DESC_OFFSET_BDESCRIPTORTYPE = 1 + +# USB device -- HID subclasses +HID_SUBCLASS_NONE = 0 +HID_SUBCLASS_BOOT = 1 + +# USB device -- HID protocols +HID_PROTOCOL_NONE = 0 +HID_PROTOCOL_KEYBOARD = 1 +HID_PROTOCOL_MOUSE = 2 + +# Greentea message keys used for callbacks +MSG_KEY_DEVICE_READY = 'dev_ready' +MSG_KEY_HOST_READY = 'host_ready' +MSG_KEY_SERIAL_NUMBER = 'usb_dev_sn' +MSG_KEY_TEST_GET_DESCRIPTOR_HID = 'test_get_desc_hid' +MSG_KEY_TEST_GET_DESCRIPTOR_CFG = 'test_get_desc_cfg' +MSG_KEY_TEST_REQUESTS = 'test_requests' +MSG_KEY_TEST_RAW_IO = 'test_raw_io' + +# Greentea message keys used to notify DUT of test status +MSG_KEY_TEST_CASE_FAILED = 'fail' +MSG_KEY_TEST_CASE_PASSED = 'pass' +MSG_VALUE_DUMMY = '0' +MSG_VALUE_NOT_SUPPORTED = 'not_supported' + +# Constants for the tests. +KEYBOARD_IDLE_RATE_TO_SET = 0x00 # Duration = 0 (indefinite) +HID_PROTOCOL_TO_SET = 0x01 # Protocol = 1 (Report Protocol) +RAW_IO_REPS = 16 # Number of loopback test reps. + + +def build_get_desc_value(desc_type, desc_index): + """Build and return a wValue field for control requests.""" + return (desc_type << 8) | desc_index + + +def usb_hid_path(serial_number): + """Get a USB HID device system path based on the serial number.""" + if not CYTHON_HIDAPI_PRESENT: + return None + for device_info in hid.enumerate(): # pylint: disable=no-member + if device_info.get('serial_number') == serial_number: # pylint: disable=not-callable + return device_info['path'] + return None + + +def get_descriptor_types(desc): + """Return a list of all bDescriptorType values found in desc. + + desc is expected to be a sequence of bytes, i.e. array.array('B') + returned from usb.core. + + From the USB 2.0 spec, paragraph 9.5: + Each descriptor begins with a byte-wide field that contains the total + number of bytes in the descriptor followed by a byte-wide field that + identifies the descriptor type. + """ + tmp_desc = desc[DESC_OFFSET_BLENGTH:] + desc_types = [] + while True: + try: + bLength = tmp_desc[DESC_OFFSET_BLENGTH] # pylint: disable=invalid-name + bDescriptorType = tmp_desc[DESC_OFFSET_BDESCRIPTORTYPE] # pylint: disable=invalid-name + desc_types.append(int(bDescriptorType)) + tmp_desc = tmp_desc[int(bLength):] + except IndexError: + break + return desc_types + + +def get_hid_descriptor_parts(hid_descriptor): + """Return bNumDescriptors, bDescriptorType, wDescriptorLength from hid_descriptor.""" + err_msg = 'Invalid HID class descriptor' + try: + if hid_descriptor[1] != DESC_TYPE_HID_HID: + raise TypeError(err_msg) + bNumDescriptors = int(hid_descriptor[5]) # pylint: disable=invalid-name + bDescriptorType = int(hid_descriptor[6]) # pylint: disable=invalid-name + wDescriptorLength = int((hid_descriptor[8] << 8) | hid_descriptor[7]) # pylint: disable=invalid-name + except (IndexError, ValueError): + raise TypeError(err_msg) + return bNumDescriptors, bDescriptorType, wDescriptorLength + + +def get_usbhid_dev_type(intf): + """Return a name of the HID device class type for intf.""" + if not isinstance(intf, usb.core.Interface): + return None + if intf.bInterfaceClass != USB_CLASS_HID: + # USB Device Class Definition for HID, v1.11, paragraphs 4.1, 4.2 & 4.3: + # the class is specified in the Interface descriptor + # and not the Device descriptor. + return None + if (intf.bInterfaceSubClass == HID_SUBCLASS_BOOT + and intf.bInterfaceProtocol == HID_PROTOCOL_KEYBOARD): + return 'boot_keyboard' + if (intf.bInterfaceSubClass == HID_SUBCLASS_BOOT + and intf.bInterfaceProtocol == HID_PROTOCOL_MOUSE): + return 'boot_mouse' + # Determining any other HID dev type, like a non-boot_keyboard or + # a non-boot_mouse requires getting and parsing a HID Report descriptor + # for intf. + # Only the boot_keyboard, boot_mouse and other_device are used for this + # greentea test suite. + return 'other_device' + + +class RetryError(Exception): + """Exception raised by retry_fun_call().""" + + +def retry_fun_call(fun, num_retries=3, retry_delay=0.0): + """Call fun and retry if any exception was raised. + + fun is called at most num_retries with a retry_dalay in between calls. + Raises RetryError if the retry limit is exhausted. + """ + verbose = False + final_err = None + for retry in range(1, num_retries + 1): + try: + return fun() # pylint: disable=not-callable + except Exception as exc: # pylint: disable=broad-except + final_err = exc + if verbose: + print('Retry {}/{} failed ({})' + .format(retry, num_retries, str(fun))) + time.sleep(retry_delay) + err_msg = 'Failed with "{}". Tried {} times.' + raise RetryError(err_msg.format(final_err, num_retries)) + + +def raise_if_different(expected, actual, text=''): + """Raise a RuntimeError if actual is different than expected.""" + if expected != actual: + raise RuntimeError('{}Got {!r}, expected {!r}.'.format(text, actual, expected)) + + +def raise_if_false(expression, text): + """Raise a RuntimeError if expression is False.""" + if not expression: + raise RuntimeError(text) + + +class USBHIDTest(mbed_host_tests.BaseHostTest): + """Host side test for USB device HID class.""" + + @staticmethod + def get_usb_hid_path(usb_id_str): + """Get a USB HID device path as registered in the system. + + Search is based on the unique USB SN generated by the host + during test suite setup. + Raises RuntimeError if the device is not found. + """ + hid_path = usb_hid_path(usb_id_str) + if hid_path is None: + err_msg = 'USB HID device (SN={}) not found.' + raise RuntimeError(err_msg.format(usb_id_str)) + return hid_path + + @staticmethod + def get_usb_dev(usb_id_str): + """Get a usb.core.Device instance. + + Search is based on the unique USB SN generated by the host + during test suite setup. + Raises RuntimeError if the device is not found. + """ + usb_dev = usb.core.find(custom_match=lambda d: d.serial_number == usb_id_str) + if usb_dev is None: + err_msg = 'USB device (SN={}) not found.' + raise RuntimeError(err_msg.format(usb_id_str)) + return usb_dev + + def __init__(self): + super(USBHIDTest, self).__init__() + self.__bg_task = None + self.dut_usb_dev_sn = uuid.uuid4().hex # 32 hex digit string + + def notify_error(self, msg): + """Terminate the test with an error msg.""" + self.log('TEST ERROR: {}'.format(msg)) + self.notify_complete(None) + + def notify_failure(self, msg): + """Report a host side test failure to the DUT.""" + self.log('TEST FAILED: {}'.format(msg)) + self.send_kv(MSG_KEY_TEST_CASE_FAILED, MSG_VALUE_DUMMY) + + def notify_success(self, value=None, msg=''): + """Report a host side test success to the DUT.""" + if msg: + self.log('TEST PASSED: {}'.format(msg)) + if value is None: + value = MSG_VALUE_DUMMY + self.send_kv(MSG_KEY_TEST_CASE_PASSED, value) + + def cb_test_get_hid_desc(self, key, value, timestamp): + """Verify the device handles Get_Descriptor request correctly. + + Two requests are tested for every HID interface: + 1. Get_Descriptor(HID), + 2. Get_Descriptor(Report). + Details in USB Device Class Definition for HID, v1.11, paragraph 7.1. + """ + kwargs_hid_desc_req = { + 'bmRequestType': build_request_type( + CTRL_IN, CTRL_TYPE_STANDARD, CTRL_RECIPIENT_INTERFACE), + 'bRequest': USB_REQUEST_GET_DESCRIPTOR, + # Descriptor Index (part of wValue) is reset to zero for + # HID class descriptors other than Physical ones. + 'wValue': build_get_desc_value(DESC_TYPE_HID_HID, 0x00), + # wIndex is replaced with the Interface Number in the loop. + 'wIndex': None, + 'data_or_wLength': DESC_LEN_HID_HID} + kwargs_report_desc_req = { + 'bmRequestType': build_request_type( + CTRL_IN, CTRL_TYPE_STANDARD, CTRL_RECIPIENT_INTERFACE), + 'bRequest': USB_REQUEST_GET_DESCRIPTOR, + # Descriptor Index (part of wValue) is reset to zero for + # HID class descriptors other than Physical ones. + 'wValue': build_get_desc_value(DESC_TYPE_HID_REPORT, 0x00), + # wIndex is replaced with the Interface Number in the loop. + 'wIndex': None, + # wLength is replaced with the Report Descriptor Length in the loop. + 'data_or_wLength': None} + mbed_hid_dev = None + report_desc_lengths = [] + try: + mbed_hid_dev = retry_fun_call( + fun=functools.partial(self.get_usb_dev, self.dut_usb_dev_sn), # pylint: disable=not-callable + num_retries=20, + retry_delay=0.05) + except RetryError as exc: + self.notify_error(exc) + return + try: + for intf in mbed_hid_dev.get_active_configuration(): # pylint: disable=not-callable + if intf.bInterfaceClass != USB_CLASS_HID: + continue + try: + if mbed_hid_dev.is_kernel_driver_active(intf.bInterfaceNumber): + mbed_hid_dev.detach_kernel_driver(intf.bInterfaceNumber) # pylint: disable=not-callable + except (NotImplementedError, AttributeError): + pass + + # Request the HID descriptor. + kwargs_hid_desc_req['wIndex'] = intf.bInterfaceNumber + hid_desc = mbed_hid_dev.ctrl_transfer(**kwargs_hid_desc_req) # pylint: disable=not-callable + try: + bNumDescriptors, bDescriptorType, wDescriptorLength = get_hid_descriptor_parts(hid_desc) # pylint: disable=invalid-name + except TypeError as exc: + self.notify_error(exc) + return + raise_if_different(1, bNumDescriptors, 'Exactly one HID Report descriptor expected. ') + raise_if_different(DESC_TYPE_HID_REPORT, bDescriptorType, 'Invalid HID class descriptor type. ') + raise_if_false(wDescriptorLength > 0, 'Invalid HID Report descriptor length. ') + + # Request the Report descriptor. + kwargs_report_desc_req['wIndex'] = intf.bInterfaceNumber + kwargs_report_desc_req['data_or_wLength'] = wDescriptorLength + report_desc = mbed_hid_dev.ctrl_transfer(**kwargs_report_desc_req) # pylint: disable=not-callable + raise_if_different(wDescriptorLength, len(report_desc), + 'The size of data received does not match the HID Report descriptor length. ') + report_desc_lengths.append(len(report_desc)) + except usb.core.USBError as exc: + self.notify_failure('Get_Descriptor request failed. {}'.format(exc)) + except RuntimeError as exc: + self.notify_failure(exc) + else: + # Send the report desc len to the device. + # USBHID::report_desc_length() returns uint16_t + msg_value = '{0:04x}'.format(max(report_desc_lengths)) + self.notify_success(msg_value) + + def cb_test_get_cfg_desc(self, key, value, timestamp): + """Verify the device provides required HID descriptors. + + USB Device Class Definition for HID, v1.11, paragraph 7.1: + When a Get_Descriptor(Configuration) request is issued, it + returns (...), and the HID descriptor for each interface. + """ + kwargs_cfg_desc_req = { + 'bmRequestType': build_request_type( + CTRL_IN, CTRL_TYPE_STANDARD, CTRL_RECIPIENT_DEVICE), + 'bRequest': USB_REQUEST_GET_DESCRIPTOR, + # Descriptor Index (part of wValue) is reset to zero. + 'wValue': build_get_desc_value(DESC_TYPE_CONFIG, 0x00), + # wIndex is reset to zero. + 'wIndex': 0x00, + # wLength unknown, set to 1024. + 'data_or_wLength': 1024} + mbed_hid_dev = None + try: + mbed_hid_dev = retry_fun_call( + fun=functools.partial(self.get_usb_dev, self.dut_usb_dev_sn), # pylint: disable=not-callable + num_retries=20, + retry_delay=0.05) + except RetryError as exc: + self.notify_error(exc) + return + try: + # Request the Configuration descriptor. + cfg_desc = mbed_hid_dev.ctrl_transfer(**kwargs_cfg_desc_req) # pylint: disable=not-callable + raise_if_false(DESC_TYPE_HID_HID in get_descriptor_types(cfg_desc), + 'No HID class descriptor in the Configuration descriptor.') + except usb.core.USBError as exc: + self.notify_failure('Get_Descriptor request failed. {}'.format(exc)) + except RuntimeError as exc: + self.notify_failure(exc) + else: + self.notify_success() + + def cb_test_class_requests(self, key, value, timestamp): + """Verify all required HID requests are supported. + + USB Device Class Definition for HID, v1.11, Appendix G: + 1. Get_Report -- required for all types, + 2. Set_Report -- not required if dev doesn't declare an Output Report, + 3. Get_Idle -- required for keyboards, + 4. Set_Idle -- required for keyboards, + 5. Get_Protocol -- required for boot_keyboard and boot_mouse, + 6. Set_Protocol -- required for boot_keyboard and boot_mouse. + + Details in USB Device Class Definition for HID, v1.11, paragraph 7.2. + """ + kwargs_get_report_request = { + 'bmRequestType': build_request_type( + CTRL_IN, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE), + 'bRequest': HID_REQUEST_GET_REPORT, + # wValue: ReportType = Input, ReportID = 0 (not used) + 'wValue': (0x01 << 8) | 0x00, + # wIndex: InterfaceNumber (defined later) + 'wIndex': None, + # wLength: unknown, set to 1024 + 'data_or_wLength': 1024} + kwargs_get_idle_request = { + 'bmRequestType': build_request_type( + CTRL_IN, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE), + 'bRequest': HID_REQUEST_GET_IDLE, + # wValue: 0, ReportID = 0 (not used) + 'wValue': (0x00 << 8) | 0x00, + # wIndex: InterfaceNumber (defined later) + 'wIndex': None, + 'data_or_wLength': 1} + kwargs_set_idle_request = { + 'bmRequestType': build_request_type( + CTRL_OUT, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE), + 'bRequest': HID_REQUEST_SET_IDLE, + # wValue: Duration, ReportID = 0 (all input reports) + 'wValue': (KEYBOARD_IDLE_RATE_TO_SET << 8) | 0x00, + # wIndex: InterfaceNumber (defined later) + 'wIndex': None, + 'data_or_wLength': 0} + kwargs_get_protocol_request = { + 'bmRequestType': build_request_type( + CTRL_IN, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE), + 'bRequest': HID_REQUEST_GET_PROTOCOL, + 'wValue': 0x00, + # wIndex: InterfaceNumber (defined later) + 'wIndex': None, + 'data_or_wLength': 1} + kwargs_set_protocol_request = { + 'bmRequestType': build_request_type( + CTRL_OUT, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE), + 'bRequest': HID_REQUEST_SET_PROTOCOL, + 'wValue': HID_PROTOCOL_TO_SET, + # wIndex: InterfaceNumber (defined later) + 'wIndex': None, + 'data_or_wLength': 0} + mbed_hid_dev = None + try: + mbed_hid_dev = retry_fun_call( + fun=functools.partial(self.get_usb_dev, self.dut_usb_dev_sn), # pylint: disable=not-callable + num_retries=20, + retry_delay=0.05) + except RetryError as exc: + self.notify_error(exc) + return + hid_dev_type = None + tested_request_name = None + try: + for intf in mbed_hid_dev.get_active_configuration(): # pylint: disable=not-callable + hid_dev_type = get_usbhid_dev_type(intf) + if hid_dev_type is None: + continue + try: + if mbed_hid_dev.is_kernel_driver_active(intf.bInterfaceNumber): + mbed_hid_dev.detach_kernel_driver(intf.bInterfaceNumber) # pylint: disable=not-callable + except (NotImplementedError, AttributeError): + pass + if hid_dev_type == 'boot_keyboard': + # 4. Set_Idle + tested_request_name = 'Set_Idle' + kwargs_set_idle_request['wIndex'] = intf.bInterfaceNumber + mbed_hid_dev.ctrl_transfer(**kwargs_set_idle_request) # pylint: disable=not-callable + # 3. Get_Idle + tested_request_name = 'Get_Idle' + kwargs_get_idle_request['wIndex'] = intf.bInterfaceNumber + idle_rate = mbed_hid_dev.ctrl_transfer(**kwargs_get_idle_request) # pylint: disable=not-callable + raise_if_different(KEYBOARD_IDLE_RATE_TO_SET, idle_rate, 'Invalid idle rate received. ') + if hid_dev_type in ('boot_keyboard', 'boot_mouse'): + # 6. Set_Protocol + tested_request_name = 'Set_Protocol' + kwargs_set_protocol_request['wIndex'] = intf.bInterfaceNumber + mbed_hid_dev.ctrl_transfer(**kwargs_set_protocol_request) # pylint: disable=not-callable + # 5. Get_Protocol + tested_request_name = 'Get_Protocol' + kwargs_get_protocol_request['wIndex'] = intf.bInterfaceNumber + protocol = mbed_hid_dev.ctrl_transfer(**kwargs_get_protocol_request) # pylint: disable=not-callable + raise_if_different(HID_PROTOCOL_TO_SET, protocol, 'Invalid protocol received. ') + # 1. Get_Report + tested_request_name = 'Get_Report' + kwargs_get_report_request['wIndex'] = intf.bInterfaceNumber + mbed_hid_dev.ctrl_transfer(**kwargs_get_report_request) # pylint: disable=not-callable + except usb.core.USBError as exc: + self.notify_failure('The {!r} does not support the {!r} HID class request ({}).' + .format(hid_dev_type, tested_request_name, exc)) + except RuntimeError as exc: + self.notify_failure('Set/Get data mismatch for {!r} for the {!r} HID class request ({}).' + .format(hid_dev_type, tested_request_name, exc)) + else: + self.notify_success() + + def raw_loopback(self, report_size): + """Send every input report back to the device.""" + mbed_hid_path = None + mbed_hid = hid.device() + try: + mbed_hid_path = retry_fun_call( + fun=functools.partial(self.get_usb_hid_path, self.dut_usb_dev_sn), # pylint: disable=not-callable + num_retries=20, + retry_delay=0.05) + retry_fun_call( + fun=functools.partial(mbed_hid.open_path, mbed_hid_path), # pylint: disable=not-callable + num_retries=10, + retry_delay=0.05) + except RetryError as exc: + self.notify_error(exc) + return + # Notify the device it can send reports now. + self.send_kv(MSG_KEY_HOST_READY, MSG_VALUE_DUMMY) + try: + for _ in range(RAW_IO_REPS): + # There are no Report ID tags in the Report descriptor. + # Receiving only the Report Data, Report ID is omitted. + report_in = mbed_hid.read(report_size) + report_out = report_in[:] + # Set the Report ID to 0x00 (not used). + report_out.insert(0, 0x00) + mbed_hid.write(report_out) + except (ValueError, IOError) as exc: + self.notify_failure('HID Report transfer failed. {}'.format(exc)) + finally: + mbed_hid.close() + + def setup(self): + self.register_callback(MSG_KEY_DEVICE_READY, self.cb_device_ready) + self.register_callback(MSG_KEY_TEST_GET_DESCRIPTOR_HID, self.cb_test_get_hid_desc) + self.register_callback(MSG_KEY_TEST_GET_DESCRIPTOR_CFG, self.cb_test_get_cfg_desc) + self.register_callback(MSG_KEY_TEST_REQUESTS, self.cb_test_class_requests) + self.register_callback(MSG_KEY_TEST_RAW_IO, self.cb_test_raw_io) + + def cb_device_ready(self, key, value, timestamp): + """Send a unique USB SN to the device. + + DUT uses this SN every time it connects to host as a USB device. + """ + self.send_kv(MSG_KEY_SERIAL_NUMBER, self.dut_usb_dev_sn) + + def start_bg_task(self, **thread_kwargs): + """Start a new daemon thread. + + Some callbacks delegate HID dev handling to a background task to + prevent any delays in the device side assert handling. Only one + background task is kept running to prevent multiple access + to the HID device. + """ + try: + self.__bg_task.join() + except (AttributeError, RuntimeError): + pass + self.__bg_task = threading.Thread(**thread_kwargs) + self.__bg_task.daemon = True + self.__bg_task.start() + + def cb_test_raw_io(self, key, value, timestamp): + """Receive HID reports and send them back to the device.""" + if not CYTHON_HIDAPI_PRESENT: + self.send_kv(MSG_KEY_HOST_READY, MSG_VALUE_NOT_SUPPORTED) + return + try: + # The size of input and output reports used in test. + report_size = int(value) + except ValueError as exc: + self.notify_error(exc) + return + self.start_bg_task( + target=self.raw_loopback, + args=(report_size, )) diff --git a/TESTS/usb_device/hid/README.md b/TESTS/usb_device/hid/README.md new file mode 100644 index 00000000000..0ea4d4c8eb1 --- /dev/null +++ b/TESTS/usb_device/hid/README.md @@ -0,0 +1,23 @@ +# Testing the USB HID device with a Linux host + +Before running `tests-usb_device-hid` test suite on a Linux machine, please +make sure to install the `hidapi` Python module first, otherwise some test +cases will be skipped. Due to external dependencies for Linux, this module +is not installed during the initial setup, to keep the process as simple +as possible. + +For Debian-based Linux distros, the dependencies can be installed as follows +(based on module's [README][1]): + +```bash +apt-get install python-dev libusb-1.0-0-dev libudev-dev +pip install --upgrade setuptools +``` +To install the `hidapi` module itself, please use the attached +`TESTS/usb_device/hid/requirements.txt` file: +```bash +pip install -r requirements.txt +``` + +[1]: https://github.com/trezor/cython-hidapi/blob/master/README.rst#install + diff --git a/TESTS/usb_device/hid/main.cpp b/TESTS/usb_device/hid/main.cpp new file mode 100644 index 00000000000..44aa1ea83c7 --- /dev/null +++ b/TESTS/usb_device/hid/main.cpp @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2018, ARM Limited, All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#if !defined(DEVICE_USBDEVICE) || !DEVICE_USBDEVICE +#error [NOT_SUPPORTED] USB Device not supported for this target +#endif + +#include "greentea-client/test_env.h" +#include "utest/utest.h" +#include "unity/unity.h" +#include "mbed.h" +#include +#include "usb_phy_api.h" +#include "USBHID.h" +#include "USBMouse.h" +#include "USBKeyboard.h" + +// Reuse the VID & PID from basic USB test. +#define USB_HID_VID 0x0d28 +#define USB_HID_PID_GENERIC 0x0206 +#define USB_HID_PID_KEYBOARD 0x0206 +#define USB_HID_PID_MOUSE 0x0206 +#define USB_HID_PID_GENERIC2 0x0007 + +#define MSG_VALUE_LEN 24 +#define MSG_KEY_LEN 24 +#define MSG_KEY_DEVICE_READY "ready" +#define MSG_KEY_DEVICE_READY "dev_ready" +#define MSG_KEY_HOST_READY "host_ready" +#define MSG_KEY_SERIAL_NUMBER "usb_dev_sn" +#define MSG_KEY_TEST_GET_DESCRIPTOR_HID "test_get_desc_hid" +#define MSG_KEY_TEST_GET_DESCRIPTOR_CFG "test_get_desc_cfg" +#define MSG_KEY_TEST_REQUESTS "test_requests" +#define MSG_KEY_TEST_RAW_IO "test_raw_io" + +#define MSG_KEY_TEST_CASE_FAILED "fail" +#define MSG_KEY_TEST_CASE_PASSED "pass" +#define MSG_VALUE_DUMMY "0" +#define MSG_VALUE_NOT_SUPPORTED "not_supported" + +#define RAW_IO_REPS 16 + +#define USB_DEV_SN_LEN (32) // 32 hex digit UUID +#define NONASCII_CHAR ('?') +#define USB_DEV_SN_DESC_SIZE (USB_DEV_SN_LEN * 2 + 2) + +const char *default_serial_num = "0123456789"; +char usb_dev_sn[USB_DEV_SN_LEN + 1]; + +using utest::v1::Case; +using utest::v1::Specification; +using utest::v1::Harness; + +/** + * Convert a USB string descriptor to C style ASCII + * + * The string placed in str is always null-terminated which may cause the + * loss of data if n is to small. If the length of descriptor string is less + * than n, additional null bytes are written to str. + * + * @param str output buffer for the ASCII string + * @param usb_desc USB string descriptor + * @param n size of str buffer + * @returns number of non-null bytes returned in str or -1 on failure + */ +int usb_string_desc2ascii(char *str, const uint8_t *usb_desc, size_t n) +{ + if (str == NULL || usb_desc == NULL || n < 1) { + return -1; + } + // bDescriptorType @ offset 1 + if (usb_desc[1] != STRING_DESCRIPTOR) { + return -1; + } + // bLength @ offset 0 + const size_t bLength = usb_desc[0]; + if (bLength % 2 != 0) { + return -1; + } + size_t s, d; + for (s = 0, d = 2; s < n - 1 && d < bLength; s++, d += 2) { + // handle non-ASCII characters + if (usb_desc[d] > 0x7f || usb_desc[d + 1] != 0) { + str[s] = NONASCII_CHAR; + } else { + str[s] = usb_desc[d]; + } + } + int str_len = s; + for (; s < n; s++) { + str[s] = '\0'; + } + return str_len; +} + +/** + * Convert a C style ASCII to a USB string descriptor + * + * @param usb_desc output buffer for the USB string descriptor + * @param str ASCII string + * @param n size of usb_desc buffer, even number + * @returns number of bytes returned in usb_desc or -1 on failure + */ +int ascii2usb_string_desc(uint8_t *usb_desc, const char *str, size_t n) +{ + if (str == NULL || usb_desc == NULL || n < 4) { + return -1; + } + if (n % 2 != 0) { + return -1; + } + size_t s, d; + // set bString (@ offset 2 onwards) as a UNICODE UTF-16LE string + memset(usb_desc, 0, n); + for (s = 0, d = 2; str[s] != '\0' && d < n; s++, d += 2) { + usb_desc[d] = str[s]; + } + // set bLength @ offset 0 + usb_desc[0] = d; + // set bDescriptorType @ offset 1 + usb_desc[1] = STRING_DESCRIPTOR; + return d; +} + +class TestUSBHID: public USBHID { +private: + uint8_t _serial_num_descriptor[USB_DEV_SN_DESC_SIZE]; +public: + TestUSBHID(uint16_t vendor_id, uint16_t product_id, const char *serial_number = default_serial_num, uint8_t output_report_length = 64, uint8_t input_report_length = 64) : + USBHID(get_usb_phy(), output_report_length, input_report_length, vendor_id, product_id, 0x01) + { + init(); + int rc = ascii2usb_string_desc(_serial_num_descriptor, serial_number, USB_DEV_SN_DESC_SIZE); + if (rc < 0) { + ascii2usb_string_desc(_serial_num_descriptor, default_serial_num, USB_DEV_SN_DESC_SIZE); + } + } + + virtual ~TestUSBHID() + { + deinit(); + } + + virtual const uint8_t *string_iserial_desc() + { + return (const uint8_t *) _serial_num_descriptor; + } + + // Make this accessible for tests (public). + using USBHID::report_desc_length; +}; + +class TestUSBMouse: public USBMouse { +private: + uint8_t _serial_num_descriptor[USB_DEV_SN_DESC_SIZE]; +public: + TestUSBMouse(uint16_t vendor_id, uint16_t product_id, const char *serial_number = default_serial_num) : + USBMouse(get_usb_phy(), REL_MOUSE, vendor_id, product_id, 0x01) + { + init(); + int rc = ascii2usb_string_desc(_serial_num_descriptor, serial_number, USB_DEV_SN_DESC_SIZE); + if (rc < 0) { + ascii2usb_string_desc(_serial_num_descriptor, default_serial_num, USB_DEV_SN_DESC_SIZE); + } + } + + virtual ~TestUSBMouse() + { + deinit(); + } + + virtual const uint8_t *string_iserial_desc() + { + return (const uint8_t *) _serial_num_descriptor; + } + + // Make this accessible for tests (public). + using USBHID::report_desc_length; +}; + +class TestUSBKeyboard: public USBKeyboard { +private: + uint8_t _serial_num_descriptor[USB_DEV_SN_DESC_SIZE]; +public: + TestUSBKeyboard(uint16_t vendor_id, uint16_t product_id, const char *serial_number = default_serial_num) : + USBKeyboard(get_usb_phy(), vendor_id, product_id, 0x01) + { + init(); + int rc = ascii2usb_string_desc(_serial_num_descriptor, serial_number, USB_DEV_SN_DESC_SIZE); + if (rc < 0) { + ascii2usb_string_desc(_serial_num_descriptor, default_serial_num, USB_DEV_SN_DESC_SIZE); + } + } + + virtual ~TestUSBKeyboard() + { + deinit(); + } + + virtual const uint8_t *string_iserial_desc() + { + return (const uint8_t *) _serial_num_descriptor; + } + + // Make this accessible for tests (public). + using USBHID::report_desc_length; +}; + +/** Test Get_Descriptor request with the HID class descriptors + * + * Given a USB HID class device connected to a host, + * when the host issues the Get_Descriptor(HID) request, + * then the device returns the HID descriptor. + * + * When the host issues the Get_Descriptor(Report) request, + * then the device returns the Report descriptor + * and the size of the descriptor is equal to USBHID::report_desc_length(). + * + * Details in USB Device Class Definition for HID, v1.11, paragraph 7.1. + */ +template +void test_get_hid_class_desc() +{ + T usb_hid(USB_HID_VID, PID, usb_dev_sn); + usb_hid.connect(); + greentea_send_kv(MSG_KEY_TEST_GET_DESCRIPTOR_HID, MSG_VALUE_DUMMY); + + char key[MSG_KEY_LEN + 1] = { }; + char value[MSG_VALUE_LEN + 1] = { }; + greentea_parse_kv(key, value, MSG_KEY_LEN, MSG_VALUE_LEN); + TEST_ASSERT_EQUAL_STRING(MSG_KEY_TEST_CASE_PASSED, key); + uint16_t host_report_desc_len; + int num_args = sscanf(value, "%04hx", &host_report_desc_len); + TEST_ASSERT_MESSAGE(num_args != 0 && num_args != EOF, "Invalid data received from host."); + TEST_ASSERT_EQUAL_UINT16(usb_hid.report_desc_length(), host_report_desc_len); +} + +/** Test Get_Descriptor request with the Configuration descriptor + * + * Given a USB HID class device connected to a host, + * when the host issues the Get_Descriptor(Configuration) request, + * then the device returns the Configuration descriptor and a HID + * descriptor for each HID interface. + * + * Details in USB Device Class Definition for HID, v1.11, paragraph 7.1. + */ +template +void test_get_configuration_desc() +{ + T usb_hid(USB_HID_VID, PID, usb_dev_sn); + usb_hid.connect(); + greentea_send_kv(MSG_KEY_TEST_GET_DESCRIPTOR_CFG, MSG_VALUE_DUMMY); + + char key[MSG_KEY_LEN + 1] = { }; + char value[MSG_VALUE_LEN + 1] = { }; + greentea_parse_kv(key, value, MSG_KEY_LEN, MSG_VALUE_LEN); + TEST_ASSERT_EQUAL_STRING(MSG_KEY_TEST_CASE_PASSED, key); +} + +/** Test HID class requests + * + * Given a USB HID class device connected to a host, + * when the host issues a request specific to the HID class device type, + * then the device returns valid data. + * + * Details in USB Device Class Definition for HID, v1.11, + * paragraph 7.2 and Appendix G. + */ +template +void test_class_requests() +{ + T usb_hid(USB_HID_VID, PID, usb_dev_sn); + usb_hid.connect(); + greentea_send_kv(MSG_KEY_TEST_REQUESTS, MSG_VALUE_DUMMY); + + char key[MSG_KEY_LEN + 1] = { }; + char value[MSG_VALUE_LEN + 1] = { }; + greentea_parse_kv(key, value, MSG_KEY_LEN, MSG_VALUE_LEN); + TEST_ASSERT_EQUAL_STRING(MSG_KEY_TEST_CASE_PASSED, key); +} + +/** Test send & read + * + * Given a USB HID class device connected to a host, + * when the device sends input reports with a random data to the host + * and the host sends them back to the device, + * then received output report data is equal to the input report data. + */ +template // Range [1, MAX_HID_REPORT_SIZE]. +void test_generic_raw_io() +{ + TestUSBHID usb_hid(USB_HID_VID, USB_HID_PID_GENERIC2, usb_dev_sn, REPORT_SIZE, REPORT_SIZE); + usb_hid.connect(); + greentea_send_kv(MSG_KEY_TEST_RAW_IO, REPORT_SIZE); + + // Wait for the host HID driver to complete setup. + char key[MSG_KEY_LEN + 1] = { }; + char value[MSG_VALUE_LEN + 1] = { }; + greentea_parse_kv(key, value, MSG_KEY_LEN, MSG_VALUE_LEN); + TEST_ASSERT_EQUAL_STRING(MSG_KEY_HOST_READY, key); + if (strcmp(value, MSG_VALUE_NOT_SUPPORTED) == 0) { + TEST_IGNORE_MESSAGE("Test case not supported by host plarform."); + return; + } + + // Report ID omitted here. There are no Report ID tags in the Report descriptor. + HID_REPORT input_report = {}; + HID_REPORT output_report = {}; + for (size_t r = 0; r < RAW_IO_REPS; r++) { + for (size_t i = 0; i < REPORT_SIZE; i++) { + input_report.data[i] = (uint8_t)(rand() % 0x100); + } + input_report.length = REPORT_SIZE; + output_report.length = 0; + TEST_ASSERT(usb_hid.send(&input_report)); + TEST_ASSERT(usb_hid.read(&output_report)); + TEST_ASSERT_EQUAL_UINT32(input_report.length, output_report.length); + TEST_ASSERT_EQUAL_UINT8_ARRAY(input_report.data, output_report.data, REPORT_SIZE); + } +} + +utest::v1::status_t testsuite_setup(const size_t number_of_cases) +{ + GREENTEA_SETUP(45, "usb_device_hid"); + srand((unsigned) ticker_read_us(get_us_ticker_data())); + + utest::v1::status_t status = utest::v1::greentea_test_setup_handler(number_of_cases); + if (status != utest::v1::STATUS_CONTINUE) { + return status; + } + + char key[MSG_KEY_LEN + 1] = { }; + char usb_dev_uuid[USB_DEV_SN_LEN + 1] = { }; + + greentea_send_kv(MSG_KEY_DEVICE_READY, MSG_VALUE_DUMMY); + greentea_parse_kv(key, usb_dev_uuid, MSG_KEY_LEN, USB_DEV_SN_LEN + 1); + + if (strcmp(key, MSG_KEY_SERIAL_NUMBER) != 0) { + utest_printf("Invalid message key.\n"); + return utest::v1::STATUS_ABORT; + } + + strncpy(usb_dev_sn, usb_dev_uuid, USB_DEV_SN_LEN + 1); + return status; +} + +Case cases[] = { + Case("Configuration descriptor, generic", test_get_configuration_desc), + Case("Configuration descriptor, keyboard", test_get_configuration_desc), + Case("Configuration descriptor, mouse", test_get_configuration_desc), + + Case("HID class descriptors, generic", test_get_hid_class_desc), + Case("HID class descriptors, keyboard", test_get_hid_class_desc), + Case("HID class descriptors, mouse", test_get_hid_class_desc), + + // HID class requests not supported by Mbed + // Case("HID class requests, generic", test_class_requests), + // Case("HID class requests, keyboard", test_class_requests), + // Case("HID class requests, mouse", test_class_requests), + + Case("Raw input/output, 1-byte reports", test_generic_raw_io<1>), + Case("Raw input/output, 20-byte reports", test_generic_raw_io<20>), + Case("Raw input/output, 64-byte reports", test_generic_raw_io<64>), +}; + +Specification specification((utest::v1::test_setup_handler_t) testsuite_setup, cases); + +int main() +{ + return !Harness::run(specification); +} diff --git a/TESTS/usb_device/hid/requirements.txt b/TESTS/usb_device/hid/requirements.txt new file mode 100644 index 00000000000..f9453c942cf --- /dev/null +++ b/TESTS/usb_device/hid/requirements.txt @@ -0,0 +1 @@ +hidapi>=0.7.99,<0.8.0 diff --git a/requirements.txt b/requirements.txt index ee65e0c4a7d..68f387e1246 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,5 @@ manifest-tool==1.4.8 icetea>=1.2.1,<1.3 pycryptodome>=3.7.2,<=3.7.3 pyusb>=1.0.0,<2.0.0 +hidapi>=0.7.99,<0.8.0;platform_system!="Linux" cmsis-pack-manager>=0.2.3,<0.3.0 diff --git a/usb/device/USBHID/USBHID.cpp b/usb/device/USBHID/USBHID.cpp index 846c857a37e..1518e2a578a 100644 --- a/usb/device/USBHID/USBHID.cpp +++ b/usb/device/USBHID/USBHID.cpp @@ -380,7 +380,7 @@ void USBHID::callback_set_configuration(uint8_t configuration) endpoint_add(_int_out, MAX_HID_REPORT_SIZE, USB_EP_TYPE_INT, &USBHID::_read_isr); // We activate the endpoint to be able to recceive data - read_start(_int_out, (uint8_t *)&_output_report, MAX_HID_REPORT_SIZE); + read_start(_int_out, _output_report.data, MAX_HID_REPORT_SIZE); _read_idle = false;