diff options
| author | Ha Thach <[email protected]> | 2026-03-18 18:10:32 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-18 18:10:32 +0700 |
| commit | 766ac4f7a2c223bb85312c8bb99f8c478c211bae (patch) | |
| tree | 795c7b40c1d8567378ae052a783b0d8b28531ec5 /test | |
| parent | 8f7f286ae347ec1adbfd0888d65fb1aeb263b7d2 (diff) | |
| parent | 0521e6698eb0719c27ef7c2473c9152be30ae172 (diff) | |
Merge pull request #3557 from hathach/hil-host-cdc
add more hil tests
Diffstat (limited to 'test')
| -rwxr-xr-x | test/hil/hil_test.py | 333 | ||||
| -rw-r--r-- | test/hil/requirements.txt | 2 | ||||
| -rw-r--r-- | test/hil/tinyusb.json | 40 |
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", |
