From 572beb6731f725089d5068852e3bdfa09e3e18ef Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Wed, 13 Mar 2019 16:01:16 +0100 Subject: [PATCH 1/7] Tests: USBHID: Add tests --- TESTS/host_tests/usb_device_hid.py | 552 +++++++++++++++++++++++++++++ TESTS/usb_device/hid/main.cpp | 373 +++++++++++++++++++ requirements.txt | 1 + 3 files changed, 926 insertions(+) create mode 100644 TESTS/host_tests/usb_device_hid.py create mode 100644 TESTS/usb_device/hid/main.cpp diff --git a/TESTS/host_tests/usb_device_hid.py b/TESTS/host_tests/usb_device_hid.py new file mode 100644 index 00000000000..04b21c725a9 --- /dev/null +++ b/TESTS/host_tests/usb_device_hid.py @@ -0,0 +1,552 @@ +""" +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 hid +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) + + +# 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 = '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' + +# 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.""" + 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 + 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.""" + 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/main.cpp b/TESTS/usb_device/hid/main.cpp new file mode 100644 index 00000000000..2ea81beb651 --- /dev/null +++ b/TESTS/usb_device/hid/main.cpp @@ -0,0 +1,373 @@ +/* + * 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" + +#define USB_HID_VID 0x1234 +#define USB_HID_PID_GENERIC 0x0007 +#define USB_HID_PID_KEYBOARD 0x0008 +#define USB_HID_PID_MOUSE 0x0009 + +#define MSG_VALUE_LEN 24 +#define MSG_KEY_LEN 24 +#define MSG_KEY_DEVICE_READY "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 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); + usb_hid.wait_ready(); + + 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); + usb_hid.wait_ready(); + + 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); + usb_hid.wait_ready(); + + 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_GENERIC, usb_dev_sn, REPORT_SIZE, REPORT_SIZE); + usb_hid.connect(); + greentea_send_kv(MSG_KEY_TEST_RAW_IO, REPORT_SIZE); + usb_hid.wait_ready(); + + // 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/requirements.txt b/requirements.txt index ee65e0c4a7d..98286465481 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 cmsis-pack-manager>=0.2.3,<0.3.0 From 565ceb004848a77e24534e6b992cbba993334bd3 Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Wed, 13 Mar 2019 14:04:47 +0100 Subject: [PATCH 2/7] USBHID: Fix the initial HID report read operation The first 4 bytes received were lost due to a wrong address. Read the output report into HID_REPORT.data. --- usb/device/USBHID/USBHID.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 4f880691fbbede9cfecab96f2a317620e17f72c7 Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Thu, 14 Mar 2019 17:21:33 +0100 Subject: [PATCH 3/7] Tests: USBHID: Reuse VID & PID from basic test To successfully use pyusb on Windows hosts, a Zadig configuration has to be performed. Since config for basic tests has already been provided, use it again. --- TESTS/usb_device/hid/main.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/TESTS/usb_device/hid/main.cpp b/TESTS/usb_device/hid/main.cpp index 2ea81beb651..ced18d9e499 100644 --- a/TESTS/usb_device/hid/main.cpp +++ b/TESTS/usb_device/hid/main.cpp @@ -28,10 +28,12 @@ #include "USBMouse.h" #include "USBKeyboard.h" -#define USB_HID_VID 0x1234 -#define USB_HID_PID_GENERIC 0x0007 -#define USB_HID_PID_KEYBOARD 0x0008 -#define USB_HID_PID_MOUSE 0x0009 +// 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 @@ -300,7 +302,7 @@ void test_class_requests() template // Range [1, MAX_HID_REPORT_SIZE]. void test_generic_raw_io() { - TestUSBHID usb_hid(USB_HID_VID, USB_HID_PID_GENERIC, usb_dev_sn, REPORT_SIZE, REPORT_SIZE); + 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); usb_hid.wait_ready(); From 36eb700ede7deb844ecbc21145015fab3b557405 Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Fri, 22 Mar 2019 16:27:49 +0100 Subject: [PATCH 4/7] Tests: USBHID: Handle variable driver setup time Wait for the host driver to finish setup before sending any HID reports from the device. USBHID::wait_ready() blocks until the device reaches 'configured' state, but the state of the host HID driver remains unknown to the device. --- TESTS/host_tests/usb_device_hid.py | 5 ++++- TESTS/usb_device/hid/main.cpp | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/TESTS/host_tests/usb_device_hid.py b/TESTS/host_tests/usb_device_hid.py index 04b21c725a9..7c3d1f12c72 100644 --- a/TESTS/host_tests/usb_device_hid.py +++ b/TESTS/host_tests/usb_device_hid.py @@ -69,7 +69,8 @@ HID_PROTOCOL_MOUSE = 2 # Greentea message keys used for callbacks -MSG_KEY_DEVICE_READY = 'ready' +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' @@ -495,6 +496,8 @@ def raw_loopback(self, report_size): 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. diff --git a/TESTS/usb_device/hid/main.cpp b/TESTS/usb_device/hid/main.cpp index ced18d9e499..da29852bf30 100644 --- a/TESTS/usb_device/hid/main.cpp +++ b/TESTS/usb_device/hid/main.cpp @@ -38,6 +38,8 @@ #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" @@ -307,6 +309,12 @@ void test_generic_raw_io() greentea_send_kv(MSG_KEY_TEST_RAW_IO, REPORT_SIZE); usb_hid.wait_ready(); + // 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); + // Report ID omitted here. There are no Report ID tags in the Report descriptor. HID_REPORT input_report = {}; HID_REPORT output_report = {}; From 111f485cbe3011672b8dc921298860c9bc109dc1 Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Mon, 25 Mar 2019 15:56:36 +0100 Subject: [PATCH 5/7] Tests: USBHID: Remove unnecessary wait_ready call Every test case waits at greentea_parse_kv() anyway. --- TESTS/usb_device/hid/main.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TESTS/usb_device/hid/main.cpp b/TESTS/usb_device/hid/main.cpp index da29852bf30..19a50cdb16c 100644 --- a/TESTS/usb_device/hid/main.cpp +++ b/TESTS/usb_device/hid/main.cpp @@ -236,7 +236,6 @@ 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); - usb_hid.wait_ready(); char key[MSG_KEY_LEN + 1] = { }; char value[MSG_VALUE_LEN + 1] = { }; @@ -263,7 +262,6 @@ 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); - usb_hid.wait_ready(); char key[MSG_KEY_LEN + 1] = { }; char value[MSG_VALUE_LEN + 1] = { }; @@ -286,7 +284,6 @@ 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); - usb_hid.wait_ready(); char key[MSG_KEY_LEN + 1] = { }; char value[MSG_VALUE_LEN + 1] = { }; @@ -307,7 +304,6 @@ 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); - usb_hid.wait_ready(); // Wait for the host HID driver to complete setup. char key[MSG_KEY_LEN + 1] = { }; From 852390fcdcfd030325301fc4e74e18fa5e71ec6e Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Mon, 25 Mar 2019 18:02:51 +0100 Subject: [PATCH 6/7] Tests: USBHID: Make report test optional on Linux This test case uses `hidapi` -- a cross-platform Python module. To keep the initial Mbed setup as simple as possible, the `hidapi` module is skipped on Linux hosts because of its external dependancies for this platform. The module can be easily installed following instructions from the README file. The test case is skipped if the host machine lacks `hidapi` module. --- TESTS/host_tests/usb_device_hid.py | 13 ++++++++++++- TESTS/usb_device/hid/README.md | 23 +++++++++++++++++++++++ TESTS/usb_device/hid/main.cpp | 5 +++++ TESTS/usb_device/hid/requirements.txt | 1 + requirements.txt | 2 +- 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 TESTS/usb_device/hid/README.md create mode 100644 TESTS/usb_device/hid/requirements.txt diff --git a/TESTS/host_tests/usb_device_hid.py b/TESTS/host_tests/usb_device_hid.py index 7c3d1f12c72..9a1c0f7142b 100644 --- a/TESTS/host_tests/usb_device_hid.py +++ b/TESTS/host_tests/usb_device_hid.py @@ -20,7 +20,6 @@ import threading import uuid import mbed_host_tests -import hid import usb.core from usb.util import ( CTRL_IN, @@ -32,6 +31,12 @@ 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 @@ -81,6 +86,7 @@ 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) @@ -95,6 +101,8 @@ def build_get_desc_value(desc_type, 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'] @@ -544,6 +552,9 @@ def start_bg_task(self, **thread_kwargs): 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) 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 index 19a50cdb16c..44aa1ea83c7 100644 --- a/TESTS/usb_device/hid/main.cpp +++ b/TESTS/usb_device/hid/main.cpp @@ -49,6 +49,7 @@ #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 @@ -310,6 +311,10 @@ void test_generic_raw_io() 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 = {}; 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 98286465481..68f387e1246 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +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 +hidapi>=0.7.99,<0.8.0;platform_system!="Linux" cmsis-pack-manager>=0.2.3,<0.3.0 From 491ea5530ae8f46582ebed5a0bedf729750a3f27 Mon Sep 17 00:00:00 2001 From: Filip Jagodzinski Date: Tue, 26 Mar 2019 16:09:10 +0100 Subject: [PATCH 7/7] Tests: USBHID: Specify `hidapi` license --- LICENSE.md | 1 + 1 file changed, 1 insertion(+) 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