#!/usr/bin/env python3 """ Verify KB2040 composite USB exposes both HID and Vendor/WebUSB interfaces. Usage: python3 tests/test_kb2040_dual.py Dependencies: pip install pyusb Notes: - May require sudo unless udev permissions are configured. - Defaults to VID:PID 239a:8104 per user report. """ import argparse import sys import time try: import usb.core import usb.util except ImportError: print("ERROR: Missing dependency. Install with: pip install pyusb") sys.exit(2) def find_device(vid: int, pid: int): return usb.core.find(idVendor=vid, idProduct=pid) def claim_vendor_interface(dev, vendor_itf): itf_num = vendor_itf.bInterfaceNumber try: if dev.is_kernel_driver_active(itf_num): dev.detach_kernel_driver(itf_num) except (NotImplementedError, usb.core.USBError): pass usb.util.claim_interface(dev, itf_num) def release_vendor_interface(dev, vendor_itf): itf_num = vendor_itf.bInterfaceNumber try: usb.util.release_interface(dev, itf_num) except usb.core.USBError: pass def find_endpoints(vendor_itf): ep_out = None ep_in = None for ep in vendor_itf: direction = usb.util.endpoint_direction(ep.bEndpointAddress) if direction == usb.util.ENDPOINT_OUT: ep_out = ep elif direction == usb.util.ENDPOINT_IN: ep_in = ep return ep_out, ep_in def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--vid", default="239a", help="USB Vendor ID in hex (default: 239a)") parser.add_argument("--pid", default="8104", help="USB Product ID in hex (default: 8104)") parser.add_argument("--payload", default="hello", help="Payload for vendor echo test") parser.add_argument("--timeout-ms", type=int, default=2000, help="Read timeout in ms") args = parser.parse_args() vid = int(args.vid, 16) pid = int(args.pid, 16) passed = 0 failed = 0 dev = find_device(vid, pid) if dev is None: print(f"[FAIL] Device {vid:04x}:{pid:04x} not found") return 1 print(f"[PASS] Device {dev.idVendor:04x}:{dev.idProduct:04x} found") passed += 1 try: dev.set_configuration() except usb.core.USBError: # Usually already configured; continue. pass cfg = dev.get_active_configuration() hid_itf = None vendor_itf = None for itf in cfg: if itf.bInterfaceClass == 0x03: # HID hid_itf = itf elif itf.bInterfaceClass == 0xFF: # Vendor-specific vendor_itf = itf if hid_itf is not None: print(f"[PASS] HID interface found (if={hid_itf.bInterfaceNumber})") passed += 1 else: print("[FAIL] HID interface not found") failed += 1 if vendor_itf is not None: print(f"[PASS] Vendor/WebUSB interface found (if={vendor_itf.bInterfaceNumber})") passed += 1 else: print("[FAIL] Vendor/WebUSB interface not found") failed += 1 if vendor_itf is not None: try: claim_vendor_interface(dev, vendor_itf) ep_out, ep_in = find_endpoints(vendor_itf) if ep_out is None or ep_in is None: print("[FAIL] Could not find bulk IN/OUT endpoints on vendor interface") failed += 1 else: payload = args.payload.encode("utf-8") ep_out.write(payload) time.sleep(0.05) data = ep_in.read(ep_in.wMaxPacketSize, timeout=args.timeout_ms) got = bytes(data) expected = b"KB2040:" + payload if got == expected: print(f"[PASS] Vendor echo OK: sent={payload!r} got={got!r}") passed += 1 else: print(f"[FAIL] Vendor echo mismatch: expected={expected!r} got={got!r}") failed += 1 except usb.core.USBTimeoutError: print("[FAIL] Vendor read timed out") failed += 1 except usb.core.USBError as e: print(f"[FAIL] USB error during vendor test: {e}") failed += 1 finally: release_vendor_interface(dev, vendor_itf) print(f"\nSummary: {passed} passed, {failed} failed") if failed == 0: print("Dual interface support is working (HID + Vendor/WebUSB).") return 0 return 1 if __name__ == "__main__": sys.exit(main())