summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorHa Thach <[email protected]>2026-03-18 18:10:32 +0700
committerGitHub <[email protected]>2026-03-18 18:10:32 +0700
commit766ac4f7a2c223bb85312c8bb99f8c478c211bae (patch)
tree795c7b40c1d8567378ae052a783b0d8b28531ec5 /test
parent8f7f286ae347ec1adbfd0888d65fb1aeb263b7d2 (diff)
parent0521e6698eb0719c27ef7c2473c9152be30ae172 (diff)
Merge pull request #3557 from hathach/hil-host-cdc
add more hil tests
Diffstat (limited to 'test')
-rwxr-xr-xtest/hil/hil_test.py333
-rw-r--r--test/hil/requirements.txt2
-rw-r--r--test/hil/tinyusb.json40
3 files changed, 353 insertions, 22 deletions
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index 7cffd2da8..41e9fad88 100755
--- a/test/hil/hil_test.py
+++ b/test/hil/hil_test.py
@@ -80,6 +80,11 @@ flash bank $_FLASHNAME wch_riscv 0x00000000 0 0 0 $_TARGETNAME.0
echo "Ready for Remote Connections"
"""
+MSC_README_TXT = \
+b"This is tinyusb's MassStorage Class demo.\r\n\r\n\
+If you find any bugs or get any questions, feel free to file an\r\n\
+issue at github.com/hathach/tinyusb"
+
# -------------------------------------------------------------
# Path
# -------------------------------------------------------------
@@ -360,10 +365,27 @@ def test_dual_host_info_to_device_cdc(board):
declared_devs = [f'{d["vid_pid"]}_{d["serial"]}' for d in board['tests']['dev_attached']]
port = get_serial_dev(uid, 'TinyUSB', "TinyUSB_Device", 0)
ser = open_serial_dev(port)
+ ser.timeout = 0.1
- # read from cdc, first line should contain vid/pid and serial
- data = ser.read(10000)
+ # read until all expected devices are enumerated
+ data = b''
+ timeout = ENUM_TIMEOUT
+ while timeout > 0:
+ new_data = ser.read(ser.in_waiting or 1)
+ if new_data:
+ data += new_data
+ # check if all devices found
+ enum_dev_sn = []
+ for l in data.decode('utf-8', errors='ignore').splitlines():
+ vid_pid_sn = re.search(r'ID ([0-9a-fA-F]+):([0-9a-fA-F]+) SN (\w+)', l)
+ if vid_pid_sn:
+ enum_dev_sn.append(f'{vid_pid_sn.group(1)}_{vid_pid_sn.group(2)}_{vid_pid_sn.group(3)}')
+ if set(declared_devs).issubset(set(enum_dev_sn)):
+ break
+ time.sleep(0.1)
+ timeout -= 0.1
ser.close()
+
if len(data) == 0:
assert False, 'No data from device'
lines = data.decode('utf-8', errors='ignore').splitlines()
@@ -391,13 +413,31 @@ def test_host_device_info(board):
port = get_serial_dev(flasher["uid"], None, None, 0)
ser = open_serial_dev(port)
+ ser.timeout = 0.1
# reset device since we can miss the first line
ret = globals()[f'reset_{flasher["name"].lower()}'](board)
- assert ret.returncode == 0, 'Failed to reset device'
+ assert ret.returncode == 0, 'Failed to reset device'
- data = ser.read(10000)
+ # read until all expected devices are enumerated
+ data = b''
+ timeout = ENUM_TIMEOUT
+ while timeout > 0:
+ new_data = ser.read(ser.in_waiting or 1)
+ if new_data:
+ data += new_data
+ # check if all devices found
+ enum_dev_sn = []
+ for l in data.decode('utf-8', errors='ignore').splitlines():
+ vid_pid_sn = re.search(r'ID ([0-9a-fA-F]+):([0-9a-fA-F]+) SN (\w+)', l)
+ if vid_pid_sn:
+ enum_dev_sn.append(f'{vid_pid_sn.group(1)}_{vid_pid_sn.group(2)}_{vid_pid_sn.group(3)}')
+ if set(declared_devs).issubset(set(enum_dev_sn)):
+ break
+ time.sleep(0.1)
+ timeout -= 0.1
ser.close()
+
if len(data) == 0:
assert False, 'No data from device'
lines = data.decode('utf-8', errors='ignore').splitlines()
@@ -416,6 +456,173 @@ def test_host_device_info(board):
return 0
+def print_msc_info(lines):
+ """Print MSC inquiry and disk size on a single line"""
+ inquiry = ''
+ disk_size = ''
+ for l in lines:
+ if re.match(r'^[A-Za-z].*\s+rev\s+', l):
+ inquiry = l.strip()
+ if 'Disk Size' in l:
+ disk_size = l.strip()
+ if inquiry or disk_size:
+ print(f'\r\n {inquiry} {disk_size} ', end='')
+
+
+def test_host_cdc_msc_hid(board):
+ flasher = board['flasher']
+ dev_attached = board['tests'].get('dev_attached', [])
+ cdc_devs = [d for d in dev_attached if d.get('is_cdc')]
+ msc_devs = [d for d in dev_attached if d.get('is_msc')]
+ if not cdc_devs and not msc_devs:
+ return
+
+ port = get_serial_dev(flasher["uid"], None, None, 0)
+ ser = open_serial_dev(port)
+ ser.timeout = 0.1
+
+ # reset device to catch mount messages
+ ret = globals()[f'reset_{flasher["name"].lower()}'](board)
+ assert ret.returncode == 0, 'Failed to reset device'
+
+ # Wait for all expected mount messages
+ data = b''
+ timeout = ENUM_TIMEOUT
+ wait_cdc = len(cdc_devs) > 0
+ wait_msc = len(msc_devs) > 0
+ while timeout > 0:
+ new_data = ser.read(ser.in_waiting or 1)
+ if new_data:
+ data += new_data
+ cdc_ok = (not wait_cdc) or (b'CDC Interface is mounted' in data)
+ msc_ok = (not wait_msc) or (b'Disk Size' in data)
+ if cdc_ok and msc_ok:
+ break
+ time.sleep(0.1)
+ timeout -= 0.1
+
+ # Lookup serial chip name from vid_pid
+ vid_pid_name = {
+ '0403_6001': 'FTDI', '0403_6010': 'FTDI', '0403_6011': 'FTDI', '0403_6014': 'FTDI',
+ '10c4_ea60': 'CP210x', '10c4_ea70': 'CP210x',
+ '067b_2303': 'PL2303', '067b_23a3': 'PL2303',
+ '1a86_7523': 'CH340', '1a86_7522': 'CH340',
+ '1a86_55d3': 'CH9102', '1a86_55d4': 'CH9102',
+ }
+
+ lines = data.decode('utf-8', errors='ignore').splitlines()
+
+ # Verify and print CDC mount
+ if cdc_devs:
+ assert b'CDC Interface is mounted' in data, 'CDC device not mounted on host'
+ dev = cdc_devs[0]
+ chip_name = vid_pid_name.get(dev['vid_pid'], dev['vid_pid'])
+ for l in lines:
+ if 'CDC Interface is mounted' in l:
+ print(f'\r\n {chip_name}: {l} ', end='')
+
+ # Verify and print MSC mount (inquiry + disk size)
+ if msc_devs:
+ assert b'MassStorage device is mounted' in data, 'MSC device not mounted on host'
+ assert b'Disk Size' in data, 'MSC Disk Size not reported'
+ print_msc_info(lines)
+
+ # CDC echo test via flasher serial
+ if not cdc_devs:
+ ser.close()
+ return
+
+ time.sleep(2)
+ ser.reset_input_buffer()
+
+ def rand_ascii(length):
+ return "".join(random.choices(string.ascii_letters + string.digits, k=length)).encode("ascii")
+
+ sizes = [8, 32, 64, 128]
+ for size in sizes:
+ test_data = rand_ascii(size)
+ ser.reset_input_buffer()
+
+ # Write byte-by-byte with delay to avoid UART overrun
+ for b in test_data:
+ ser.write(bytes([b]))
+ ser.flush()
+ time.sleep(0.001)
+
+ # Read echo back with timeout
+ echo = b''
+ t = 5.0
+ while t > 0 and len(echo) < size:
+ rd = ser.read(max(1, ser.in_waiting))
+ if rd:
+ echo += rd
+ time.sleep(0.05)
+ t -= 0.05
+ assert echo == test_data, (f'CDC echo wrong data ({size} bytes):\n'
+ f' expected: {test_data}\n received: {echo}')
+
+ ser.close()
+
+
+def test_host_msc_file_explorer(board):
+ flasher = board['flasher']
+ msc_devs = [d for d in board['tests'].get('dev_attached', []) if d.get('is_msc')]
+ if not msc_devs:
+ return
+
+ port = get_serial_dev(flasher["uid"], None, None, 0)
+ ser = open_serial_dev(port)
+ ser.timeout = 0.1
+
+ # reset device to catch mount messages
+ ret = globals()[f'reset_{flasher["name"].lower()}'](board)
+ assert ret.returncode == 0, 'Failed to reset device'
+
+ # Wait for MSC mount (Disk Size message)
+ data = b''
+ timeout = ENUM_TIMEOUT
+ while timeout > 0:
+ new_data = ser.read(ser.in_waiting or 1)
+ if new_data:
+ data += new_data
+ if b'Disk Size' in data:
+ break
+ time.sleep(0.1)
+ timeout -= 0.1
+ assert b'Disk Size' in data, 'MSC device not mounted'
+ lines = data.decode('utf-8', errors='ignore').splitlines()
+ print_msc_info(lines)
+
+ # Send "cat README.TXT" and read response
+ time.sleep(1)
+ ser.reset_input_buffer()
+ for ch in 'cat README.TXT\r':
+ ser.write(ch.encode())
+ ser.flush()
+ time.sleep(0.002)
+
+ # Read response
+ resp = b''
+ t = 10.0
+ while t > 0:
+ rd = ser.read(max(1, ser.in_waiting))
+ if rd:
+ resp += rd
+ # wait for prompt after command output
+ if b'>' in resp and resp.rstrip().endswith(b'>'):
+ break
+ time.sleep(0.05)
+ t -= 0.05
+
+ # Verify response contains README content
+ resp_text = resp.decode('utf-8', errors='ignore')
+ assert MSC_README_TXT.decode() in resp_text, (f'MSC README.TXT not found in response:\n'
+ f' received: {resp_text}')
+ print('README.TXT matched ', end='')
+
+ ser.close()
+
+
# -------------------------------------------------------------
# Tests: device
# -------------------------------------------------------------
@@ -491,12 +698,7 @@ def test_device_cdc_msc(board):
# MSC Block test
data = read_disk_file(uid, 0, 'README.TXT')
- readme = \
- b"This is tinyusb's MassStorage Class demo.\r\n\r\n\
-If you find any bugs or get any questions, feel free to file an\r\n\
-issue at github.com/hathach/tinyusb"
-
- assert data == readme, f'MSC wrong data in README.TXT\n expected: {readme.decode()}\n received: {data.decode()}'
+ assert data == MSC_README_TXT, f'MSC wrong data in README.TXT\n expected: {MSC_README_TXT.decode()}\n received: {data.decode()}'
def test_device_cdc_msc_freertos(board):
@@ -742,6 +944,112 @@ def test_device_mtp(board):
mtp.disconnect()
+def test_device_msc_dual_lun(board):
+ uid = board['uid']
+
+ # Read README from LUN 0
+ data0 = read_disk_file(uid, 0, 'README0.TXT')
+ readme0 = b"LUN0: " + MSC_README_TXT
+ assert data0 == readme0, f'MSC LUN0 wrong data in README0.TXT\n expected: {readme0}\n received: {data0}'
+
+ # Read README from LUN 1
+ data1 = read_disk_file(uid, 1, 'README1.TXT')
+ readme1 = b"LUN1: " + MSC_README_TXT
+ assert data1 == readme1, f'MSC LUN1 wrong data in README1.TXT\n expected: {readme1}\n received: {data1}'
+
+
+def test_device_midi_test(board):
+ uid = board['uid']
+
+ # Find MIDI device via /dev/snd/by-id using board UID
+ timeout = ENUM_TIMEOUT
+ midi_port = None
+ while timeout > 0:
+ pattern = f'/dev/snd/by-id/usb-*_{uid}-*'
+ devs = glob.glob(pattern)
+ if devs:
+ # by-id entry points to controlCX, derive card number for midiCXD0
+ link = os.path.basename(os.readlink(devs[0])) # e.g. "controlC2"
+ card_num = link.replace('controlC', '')
+ midi_path = f'/dev/snd/midiC{card_num}D0'
+ if os.path.exists(midi_path):
+ midi_port = midi_path
+ break
+ time.sleep(1)
+ timeout -= 1
+ assert midi_port is not None, f'MIDI device not found for {uid}'
+
+ # Read MIDI messages and verify note on/off
+ import select
+ with open(midi_port, 'rb') as f:
+ notes = []
+ # Read for up to 3 seconds to capture a few notes (286ms interval)
+ end_time = time.time() + 3
+ while time.time() < end_time:
+ ready, _, _ = select.select([f], [], [], 0.5)
+ if ready:
+ data = f.read(64)
+ if data:
+ # Parse MIDI bytes: note_on = 0x90, note_off = 0x80
+ i = 0
+ while i + 2 < len(data):
+ status = data[i]
+ if (status & 0xF0) == 0x90: # Note On
+ notes.append(data[i + 1])
+ i += 3
+ elif (status & 0xF0) == 0x80: # Note Off
+ i += 3
+ else:
+ i += 1
+
+ assert len(notes) >= 2, f'Expected at least 2 MIDI notes, got {len(notes)}'
+ # Verify notes are from the expected sequence
+ note_sequence = [
+ 74, 78, 81, 86, 90, 93, 98, 102, 57, 61, 66, 69, 73, 78, 81, 85,
+ 88, 92, 97, 100, 97, 92, 88, 85, 81, 78, 74, 69, 66, 62, 57, 62,
+ 66, 69, 74, 78, 81, 86, 90, 93, 97, 102, 97, 93, 90, 85, 81, 78,
+ 73, 68, 64, 61, 56, 61, 64, 68, 74, 78, 81, 86, 90, 93, 98, 102
+ ]
+ for n in notes:
+ assert n in note_sequence, f'Unexpected MIDI note {n}'
+
+
+def test_device_hid_generic_inout(board):
+ uid = board['uid']
+ import hid
+
+ # Find HID device by UID (VID=0xCafe)
+ timeout = ENUM_TIMEOUT
+ dev = None
+ while timeout > 0:
+ for d in hid.enumerate(0xCafe):
+ if d['serial_number'] == uid:
+ dev = d
+ break
+ if dev:
+ break
+ time.sleep(1)
+ timeout -= 1
+ assert dev is not None, f'HID device not found for {uid}'
+
+ h = hid.Device(vid=dev['vendor_id'], pid=dev['product_id'], serial=uid)
+
+ # Echo test: send random data and verify echo
+ for size in [8, 32, 63]:
+ # Report ID (0) + payload, padded to 64 bytes
+ payload = bytes([random.randint(1, 255) for _ in range(size)])
+ report = bytes([0]) + payload + bytes(64 - size)
+ h.write(report)
+ echo = h.read(64, timeout=2000)
+ assert echo is not None and len(echo) >= size, (
+ f'HID echo timeout or short read ({size} bytes)')
+ assert bytes(echo[:size]) == payload, (
+ f'HID echo wrong data ({size} bytes):\n'
+ f' expected: {payload.hex()}\n received: {bytes(echo[:size]).hex()}')
+
+ h.close()
+
+
# -------------------------------------------------------------
# Main
# -------------------------------------------------------------
@@ -754,7 +1062,10 @@ device_tests = [
'device/dfu_runtime',
'device/cdc_msc_freertos',
'device/hid_boot_interface',
+ 'device/msc_dual_lun',
+ 'device/hid_generic_inout',
'device/printer_to_cdc',
+ 'device/midi_test',
'device/mtp'
]
@@ -763,6 +1074,8 @@ dual_tests = [
]
host_test = [
+ 'host/cdc_msc_hid',
+ 'host/msc_file_explorer',
'host/device_info',
]
diff --git a/test/hil/requirements.txt b/test/hil/requirements.txt
index c33980c9d..ef2fecebe 100644
--- a/test/hil/requirements.txt
+++ b/test/hil/requirements.txt
@@ -1,2 +1,4 @@
fs
+hid
pyfatfs
+pyserial
diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json
index e8449ba74..b4c6aaefe 100644
--- a/test/hil/tinyusb.json
+++ b/test/hil/tinyusb.json
@@ -8,7 +8,7 @@
},
"tests": {
"only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"],
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427"}]
+ "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427", "is_cdc": true}]
},
"flasher": {
"name": "esptool",
@@ -26,7 +26,7 @@
},
"tests": {
"only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"],
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402"}]
+ "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402", "is_cdc": true}]
},
"flasher": {
"name": "esptool",
@@ -67,7 +67,7 @@
},
"tests": {
"device": true, "host": false, "dual": true,
- "dev_attached": [{"vid_pid": "067b_2303", "serial": "0"}],
+ "dev_attached": [{"vid_pid": "067b_2303", "serial": "0", "is_cdc": true}],
"comment": "pl23x"
},
"flasher": {
@@ -93,7 +93,7 @@
"uid": "BAE96FB95AFA6DBB8F00005002001200",
"tests": {
"device": true, "host": true, "dual": true,
- "dev_attached": [{"vid_pid": "10c4_ea60", "serial": "0001"}],
+ "dev_attached": [{"vid_pid": "10c4_ea60", "serial": "0001", "is_cdc": true}],
"comment": "cp2102"
},
"flasher": {
@@ -136,7 +136,7 @@
},
"tests": {
"device": true, "host": true, "dual": true,
- "dev_attached": [{"vid_pid": "1a86_7523", "serial": "0"}],
+ "dev_attached": [{"vid_pid": "1a86_7523", "serial": "0", "is_cdc": true}],
"comment": "ch34x"
},
"flasher": {
@@ -150,7 +150,7 @@
"uid": "E6614C311B764A37",
"tests": {
"device": false, "host": true, "dual": false,
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2023934"}]
+ "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2023934", "is_cdc": true}]
},
"flasher": {
"name": "openocd",
@@ -162,12 +162,9 @@
{
"name": "raspberry_pi_pico2",
"uid": "560AE75E1C7152C9",
- "build" : {
- "flags_on": ["CFG_TUH_RPI_PIO_USB"]
- },
"tests": {
- "device": true, "host": true, "dual": true,
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "533D004242"}]
+ "device": false, "host": true, "dual": false,
+ "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002694", "is_cdc": true}]
},
"flasher": {
"name": "openocd",
@@ -176,6 +173,25 @@
}
},
{
+ "name": "adafruit_fruit_jam",
+ "uid": "2B0DC7A45781189E",
+ "tests": {
+ "device": true,
+ "host": true,
+ "dual": true,
+ "dev_attached": [
+ {"vid_pid": "0403_6001", "serial": "0", "is_cdc": true},
+ {"vid_pid": "058f_6387", "serial": "A8BEE062633D", "is_msc": true,
+ "msc_disk_size": 3730, "msc_inquiry": "Generic Flash Disk rev 8.07"}
+ ]
+ },
+ "flasher": {
+ "name": "openocd",
+ "uid": "E6614103E78E8324",
+ "args": "-f interface/cmsis-dap.cfg -f target/rp2350.cfg -c \"adapter speed 5000\""
+ }
+ },
+ {
"name": "stm32f072disco",
"uid": "3A001A001357364230353532",
"flasher": {
@@ -193,7 +209,7 @@
},
"tests": {
"device": true, "host": true, "dual": false,
- "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2003414"}]
+ "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2003414", "is_cdc": true}]
},
"flasher": {
"name": "jlink",