Merge pull request #10096 from fkjagodzinski/test-usb-hid

Add USB HID tests
pull/10439/head
Martin Kojtal 2019-04-17 14:06:51 +01:00 committed by GitHub
commit 6b9d1573fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 977 additions and 1 deletions

View File

@ -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

View File

@ -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, ))

View File

@ -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

View File

@ -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 <stdlib.h>
#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<typename T, uint16_t PID>
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<typename T, uint16_t PID>
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<typename T, uint16_t PID>
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<uint8_t REPORT_SIZE> // 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<TestUSBHID, USB_HID_PID_GENERIC>),
Case("Configuration descriptor, keyboard", test_get_configuration_desc<TestUSBKeyboard, USB_HID_PID_KEYBOARD>),
Case("Configuration descriptor, mouse", test_get_configuration_desc<TestUSBMouse, USB_HID_PID_MOUSE>),
Case("HID class descriptors, generic", test_get_hid_class_desc<TestUSBHID, USB_HID_PID_GENERIC>),
Case("HID class descriptors, keyboard", test_get_hid_class_desc<TestUSBKeyboard, USB_HID_PID_KEYBOARD>),
Case("HID class descriptors, mouse", test_get_hid_class_desc<TestUSBMouse, USB_HID_PID_MOUSE>),
// HID class requests not supported by Mbed
// Case("HID class requests, generic", test_class_requests<TestUSBHID, USB_HID_PID_GENERIC>),
// Case("HID class requests, keyboard", test_class_requests<TestUSBKeyboard, USB_HID_PID_KEYBOARD>),
// Case("HID class requests, mouse", test_class_requests<TestUSBMouse, USB_HID_PID_MOUSE>),
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);
}

View File

@ -0,0 +1 @@
hidapi>=0.7.99,<0.8.0

View File

@ -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

View File

@ -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;