Revert "Enhance number of functions for USB Host"
This reverts commit 95e250643c.
This was meant to be a PR but inadvertently got committed directly.
This commit is contained in:
parent
95e250643c
commit
9fe2f6f2fb
3 changed files with 80 additions and 2351 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,892 +0,0 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Anne Barela for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Enhanced USB HID Device Detection Example for CircuitPython
|
||||
===========================================================
|
||||
|
||||
Comprehensive demonstration of the enhanced adafruit_usb_host_descriptors library
|
||||
with advanced HID device detection, composite device support, and real-time monitoring.
|
||||
|
||||
This example demonstrates:
|
||||
- Basic device counting and enumeration
|
||||
- Composite device detection (keyboard+trackpad combos)
|
||||
- Enhanced input parsing with human-readable output
|
||||
- Real-time device change monitoring
|
||||
- Device filtering and configuration
|
||||
- Performance benchmarking
|
||||
|
||||
Hardware Requirements:
|
||||
- Adafruit board with USB host support (Feather RP2040 USB Host, Fruit Jam, etc.)
|
||||
- USB keyboards, mice, gamepads, and composite devices for testing
|
||||
|
||||
Software Requirements:
|
||||
- CircuitPython 10.0.0 or highher
|
||||
- Enhanced adafruit_usb_host_descriptors library
|
||||
|
||||
"""
|
||||
# pylint: disable=imported-but-not-used
|
||||
|
||||
import time
|
||||
import array
|
||||
import board
|
||||
import sys
|
||||
import usb.core
|
||||
|
||||
# Import the enhanced USB host descriptors library
|
||||
try:
|
||||
from adafruit_usb_host_descriptors import (
|
||||
# Basic device counting
|
||||
count_keyboards,
|
||||
count_mice,
|
||||
count_gamepads,
|
||||
# Device information retrieval
|
||||
get_keyboard_info,
|
||||
get_mouse_info,
|
||||
get_gamepad_info,
|
||||
get_keyboard_device,
|
||||
get_mouse_device,
|
||||
get_gamepad_device,
|
||||
# Advanced features
|
||||
list_all_hid_devices,
|
||||
force_refresh,
|
||||
get_composite_device_info,
|
||||
is_composite_device,
|
||||
get_companion_interfaces,
|
||||
# Configuration
|
||||
set_cache_timeout,
|
||||
register_device_change_callback,
|
||||
set_device_filter,
|
||||
# Utility functions
|
||||
is_device_connected,
|
||||
get_info,
|
||||
)
|
||||
|
||||
ENHANCED_API_AVAILABLE = True
|
||||
print("Enhanced HID detection API loaded successfully!")
|
||||
except ImportError as e:
|
||||
print(f"Enhanced HID API not available: {e}")
|
||||
print("Please ensure you have the enhanced adafruit_usb_host_descriptors library")
|
||||
ENHANCED_API_AVAILABLE = False
|
||||
sys.exit(1)
|
||||
# ============================================================================
|
||||
# HID REPORT PARSING UTILITIES
|
||||
# ============================================================================
|
||||
|
||||
# HID Usage IDs for common keys (Boot Keyboard Report)
|
||||
HID_KEY_NAMES = {
|
||||
0x04: "A",
|
||||
0x05: "B",
|
||||
0x06: "C",
|
||||
0x07: "D",
|
||||
0x08: "E",
|
||||
0x09: "F",
|
||||
0x0A: "G",
|
||||
0x0B: "H",
|
||||
0x0C: "I",
|
||||
0x0D: "J",
|
||||
0x0E: "K",
|
||||
0x0F: "L",
|
||||
0x10: "M",
|
||||
0x11: "N",
|
||||
0x12: "O",
|
||||
0x13: "P",
|
||||
0x14: "Q",
|
||||
0x15: "R",
|
||||
0x16: "S",
|
||||
0x17: "T",
|
||||
0x18: "U",
|
||||
0x19: "V",
|
||||
0x1A: "W",
|
||||
0x1B: "X",
|
||||
0x1C: "Y",
|
||||
0x1D: "Z",
|
||||
0x1E: "1",
|
||||
0x1F: "2",
|
||||
0x20: "3",
|
||||
0x21: "4",
|
||||
0x22: "5",
|
||||
0x23: "6",
|
||||
0x24: "7",
|
||||
0x25: "8",
|
||||
0x26: "9",
|
||||
0x27: "0",
|
||||
0x28: "ENTER",
|
||||
0x29: "ESC",
|
||||
0x2A: "BACKSPACE",
|
||||
0x2B: "TAB",
|
||||
0x2C: "SPACE",
|
||||
0x2D: "-",
|
||||
0x2E: "=",
|
||||
0x2F: "[",
|
||||
0x30: "]",
|
||||
0x31: "\\",
|
||||
0x33: ";",
|
||||
0x34: "'",
|
||||
0x35: "`",
|
||||
0x36: ",",
|
||||
0x37: ".",
|
||||
0x38: "/",
|
||||
0x39: "CAPS_LOCK",
|
||||
0x3A: "F1",
|
||||
0x3B: "F2",
|
||||
0x3C: "F3",
|
||||
0x3D: "F4",
|
||||
0x3E: "F5",
|
||||
0x3F: "F6",
|
||||
0x40: "F7",
|
||||
0x41: "F8",
|
||||
0x42: "F9",
|
||||
0x43: "F10",
|
||||
0x44: "F11",
|
||||
0x45: "F12",
|
||||
}
|
||||
|
||||
MODIFIER_NAMES = {
|
||||
0x01: "L_CTRL",
|
||||
0x02: "L_SHIFT",
|
||||
0x04: "L_ALT",
|
||||
0x08: "L_GUI",
|
||||
0x10: "R_CTRL",
|
||||
0x20: "R_SHIFT",
|
||||
0x40: "R_ALT",
|
||||
0x80: "R_GUI",
|
||||
}
|
||||
|
||||
|
||||
def parse_keyboard_report(report_data):
|
||||
"""
|
||||
Parse a keyboard HID report into human-readable format.
|
||||
|
||||
@param report_data: Raw HID report bytes (8 bytes for boot keyboard)
|
||||
@return: Dictionary with parsed key information
|
||||
"""
|
||||
if len(report_data) < 8:
|
||||
return {"error": "Report too short", "raw": list(report_data)}
|
||||
modifier_byte = report_data[0]
|
||||
# Byte 1 is reserved
|
||||
key_codes = report_data[2:8] # Up to 6 simultaneous keys
|
||||
|
||||
# Parse modifiers
|
||||
active_modifiers = []
|
||||
for bit, name in MODIFIER_NAMES.items():
|
||||
if modifier_byte & bit:
|
||||
active_modifiers.append(name)
|
||||
# Parse key codes
|
||||
active_keys = []
|
||||
for key_code in key_codes:
|
||||
if key_code != 0:
|
||||
key_name = HID_KEY_NAMES.get(key_code, f"KEY_{key_code:02X}")
|
||||
active_keys.append(key_name)
|
||||
return {
|
||||
"modifiers": active_modifiers,
|
||||
"keys": active_keys,
|
||||
"raw_modifier": modifier_byte,
|
||||
"raw_keys": [k for k in key_codes if k != 0],
|
||||
"has_input": bool(active_modifiers or active_keys),
|
||||
}
|
||||
|
||||
|
||||
def parse_mouse_report(report_data):
|
||||
"""
|
||||
Parse a mouse HID report into human-readable format.
|
||||
|
||||
@param report_data: Raw HID report bytes (3-4 bytes for boot mouse)
|
||||
@return: Dictionary with parsed mouse information
|
||||
"""
|
||||
if len(report_data) < 3:
|
||||
return {"error": "Report too short", "raw": list(report_data)}
|
||||
buttons = report_data[0]
|
||||
x_movement = (
|
||||
report_data[1] if report_data[1] < 128 else report_data[1] - 256
|
||||
) # Convert to signed
|
||||
y_movement = (
|
||||
report_data[2] if report_data[2] < 128 else report_data[2] - 256
|
||||
) # Convert to signed
|
||||
wheel = 0
|
||||
|
||||
if len(report_data) > 3:
|
||||
wheel = report_data[3] if report_data[3] < 128 else report_data[3] - 256
|
||||
# Parse button names
|
||||
button_names = []
|
||||
if buttons & 0x01:
|
||||
button_names.append("LEFT")
|
||||
if buttons & 0x02:
|
||||
button_names.append("RIGHT")
|
||||
if buttons & 0x04:
|
||||
button_names.append("MIDDLE")
|
||||
if buttons & 0x08:
|
||||
button_names.append("BTN4")
|
||||
if buttons & 0x10:
|
||||
button_names.append("BTN5")
|
||||
return {
|
||||
"buttons": button_names,
|
||||
"x_movement": x_movement,
|
||||
"y_movement": y_movement,
|
||||
"wheel": wheel,
|
||||
"raw_buttons": buttons,
|
||||
"has_input": bool(buttons or x_movement or y_movement or wheel),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DEVICE CHANGE MONITORING
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class DeviceMonitor:
|
||||
"""Device change monitoring with callbacks and statistics"""
|
||||
|
||||
def __init__(self):
|
||||
self.device_history = []
|
||||
self.change_count = 0
|
||||
register_device_change_callback(self._on_device_change)
|
||||
|
||||
def _on_device_change(self, device_type, action, index):
|
||||
"""Internal callback for device changes"""
|
||||
self.change_count += 1
|
||||
timestamp = time.monotonic()
|
||||
|
||||
change_info = {
|
||||
"timestamp": timestamp,
|
||||
"device_type": device_type,
|
||||
"action": action,
|
||||
"index": index,
|
||||
}
|
||||
|
||||
self.device_history.append(change_info)
|
||||
|
||||
# Keep only last 50 changes
|
||||
if len(self.device_history) > 50:
|
||||
self.device_history.pop(0)
|
||||
print(f"[{timestamp:.1f}s] Device {action}: {device_type}[{index}]")
|
||||
|
||||
def get_statistics(self):
|
||||
"""Get monitoring statistics"""
|
||||
return {
|
||||
"total_changes": self.change_count,
|
||||
"recent_changes": len(self.device_history),
|
||||
"history": self.device_history.copy(),
|
||||
}
|
||||
|
||||
|
||||
# Global device monitor instance
|
||||
device_monitor = DeviceMonitor()
|
||||
|
||||
# ============================================================================
|
||||
# ENHANCED INPUT READING FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def safe_device_operation(operation_func, *args, **kwargs):
|
||||
"""
|
||||
Safely perform device operations with automatic retry and error handling.
|
||||
"""
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return operation_func(*args, **kwargs)
|
||||
except usb.core.USBError as e:
|
||||
if attempt < max_retries - 1:
|
||||
print(f"USB error (attempt {attempt + 1}): {e}, retrying...")
|
||||
time.sleep(0.1)
|
||||
force_refresh()
|
||||
else:
|
||||
print(f"USB operation failed after {max_retries} attempts: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Unexpected error in device operation: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def enhanced_read_keyboard_input(keyboard_index, duration=5):
|
||||
"""
|
||||
Enhanced keyboard reading with better error handling and parsing.
|
||||
"""
|
||||
print(f"\n=== Reading from Keyboard[{keyboard_index}] ===")
|
||||
|
||||
try:
|
||||
device = safe_device_operation(get_keyboard_device, keyboard_index)
|
||||
interface_index, endpoint_address = safe_device_operation(
|
||||
get_keyboard_info, keyboard_index
|
||||
)
|
||||
|
||||
if device is None or endpoint_address is None:
|
||||
print(f"Keyboard[{keyboard_index}] not available")
|
||||
return
|
||||
print(f"Device: {device.manufacturer} {device.product}")
|
||||
print(f"VID:PID: {device.idVendor:04x}:{device.idProduct:04x}")
|
||||
print(f"Interface: {interface_index}, Endpoint: 0x{endpoint_address:02x}")
|
||||
|
||||
# Check if this is part of a composite device
|
||||
is_composite, other_types = is_composite_device("keyboard", keyboard_index)
|
||||
if is_composite:
|
||||
print(f"Composite device - also has: {', '.join(other_types)}")
|
||||
# Setup device
|
||||
device.set_configuration()
|
||||
if device.is_kernel_driver_active(interface_index):
|
||||
device.detach_kernel_driver(interface_index)
|
||||
buf = array.array("B", [0] * 8)
|
||||
print(f"\nPress keys for {duration} seconds (parsed output will appear):")
|
||||
|
||||
start_time = time.monotonic()
|
||||
last_report = None
|
||||
key_press_count = 0
|
||||
|
||||
while time.monotonic() - start_time < duration:
|
||||
try:
|
||||
count = device.read(endpoint_address, buf, timeout=100)
|
||||
if count > 0:
|
||||
# Only process if report changed (avoid spam from key repeat)
|
||||
current_report = bytes(buf[:count])
|
||||
if current_report != last_report:
|
||||
parsed = parse_keyboard_report(buf)
|
||||
|
||||
if parsed.get("has_input"):
|
||||
key_press_count += 1
|
||||
output_parts = []
|
||||
|
||||
if parsed["modifiers"]:
|
||||
output_parts.append(
|
||||
f"Modifiers: {'+'.join(parsed['modifiers'])}"
|
||||
)
|
||||
if parsed["keys"]:
|
||||
output_parts.append(f"Keys: {'+'.join(parsed['keys'])}")
|
||||
print(
|
||||
f" [{key_press_count:2d}] {' | '.join(output_parts)}"
|
||||
)
|
||||
last_report = current_report
|
||||
except usb.core.USBTimeoutError:
|
||||
pass # Normal timeout
|
||||
except usb.core.USBError as e:
|
||||
print(f" USB read error: {e}")
|
||||
break
|
||||
print(f"Keyboard reading complete. Detected {key_press_count} key events.")
|
||||
except Exception as e:
|
||||
print(f"Error in enhanced keyboard reading: {e}")
|
||||
|
||||
|
||||
def enhanced_read_mouse_input(mouse_index, duration=5):
|
||||
"""
|
||||
Enhanced mouse reading with better error handling and parsing.
|
||||
"""
|
||||
print(f"\n=== Reading from Mouse[{mouse_index}] ===")
|
||||
|
||||
try:
|
||||
device = safe_device_operation(get_mouse_device, mouse_index)
|
||||
interface_index, endpoint_address = safe_device_operation(
|
||||
get_mouse_info, mouse_index
|
||||
)
|
||||
|
||||
if device is None or endpoint_address is None:
|
||||
print(f"Mouse[{mouse_index}] not available")
|
||||
return
|
||||
print(f"Device: {device.manufacturer} {device.product}")
|
||||
print(f"VID:PID: {device.idVendor:04x}:{device.idProduct:04x}")
|
||||
print(f"Interface: {interface_index}, Endpoint: 0x{endpoint_address:02x}")
|
||||
|
||||
# Check if this is part of a composite device
|
||||
is_composite, other_types = is_composite_device("mouse", mouse_index)
|
||||
if is_composite:
|
||||
print(f"Composite device - also has: {', '.join(other_types)}")
|
||||
print("(This might be a trackpad from a keyboard+trackpad combo)")
|
||||
# Setup device
|
||||
device.set_configuration()
|
||||
if device.is_kernel_driver_active(interface_index):
|
||||
device.detach_kernel_driver(interface_index)
|
||||
buf = array.array("B", [0] * 4)
|
||||
print(f"\nMove mouse/trackpad and click buttons for {duration} seconds:")
|
||||
|
||||
start_time = time.monotonic()
|
||||
last_report = None
|
||||
mouse_event_count = 0
|
||||
total_x_movement = 0
|
||||
total_y_movement = 0
|
||||
|
||||
while time.monotonic() - start_time < duration:
|
||||
try:
|
||||
count = device.read(endpoint_address, buf, timeout=100)
|
||||
if count > 0:
|
||||
current_report = bytes(buf[:count])
|
||||
if current_report != last_report:
|
||||
parsed = parse_mouse_report(buf)
|
||||
|
||||
if parsed.get("has_input"):
|
||||
mouse_event_count += 1
|
||||
total_x_movement += abs(parsed["x_movement"])
|
||||
total_y_movement += abs(parsed["y_movement"])
|
||||
|
||||
output_parts = []
|
||||
|
||||
if parsed["buttons"]:
|
||||
output_parts.append(
|
||||
f"Buttons: {'+'.join(parsed['buttons'])}"
|
||||
)
|
||||
if parsed["x_movement"] or parsed["y_movement"]:
|
||||
output_parts.append(
|
||||
f"Move: X{parsed['x_movement']:+d} \
|
||||
Y{parsed['y_movement']:+d}"
|
||||
)
|
||||
if parsed["wheel"]:
|
||||
output_parts.append(f"Wheel: {parsed['wheel']:+d}")
|
||||
print(
|
||||
f" [{mouse_event_count:2d}] {' | '.join(output_parts)}"
|
||||
)
|
||||
last_report = current_report
|
||||
except usb.core.USBTimeoutError:
|
||||
pass # Normal timeout
|
||||
except usb.core.USBError as e:
|
||||
print(f" USB read error: {e}")
|
||||
break
|
||||
print(f"Mouse reading complete. Detected {mouse_event_count} mouse events.")
|
||||
print(f"Total movement: X={total_x_movement} Y={total_y_movement}")
|
||||
except Exception as e:
|
||||
print(f"Error in enhanced mouse reading: {e}")
|
||||
|
||||
|
||||
def read_composite_device(keyboard_index, duration=5):
|
||||
"""
|
||||
Read from both keyboard and trackpad interfaces of a composite device.
|
||||
"""
|
||||
print(f"\n=== Reading Composite Device (Keyboard[{keyboard_index}]) ===")
|
||||
|
||||
# Verify this is a composite device with mouse interface
|
||||
is_composite, other_types = is_composite_device("keyboard", keyboard_index)
|
||||
if not is_composite or "mouse" not in other_types:
|
||||
print(f"Keyboard[{keyboard_index}] is not a composite device with trackpad")
|
||||
return
|
||||
try:
|
||||
# Get keyboard interfaces
|
||||
kbd_device = get_keyboard_device(keyboard_index)
|
||||
kbd_interface, kbd_endpoint = get_keyboard_info(keyboard_index)
|
||||
|
||||
# Get trackpad interface from companion interfaces
|
||||
companions = get_companion_interfaces("keyboard", keyboard_index)
|
||||
trackpad_info = companions["mouse"]
|
||||
trackpad_interface = trackpad_info["interface_index"]
|
||||
trackpad_endpoint = trackpad_info["endpoint_address"]
|
||||
|
||||
print(f"Composite device: {kbd_device.product}")
|
||||
print(f" Keyboard interface: {kbd_interface}, endpoint: 0x{kbd_endpoint:02x}")
|
||||
print(
|
||||
f" Trackpad interface: {trackpad_interface}, \
|
||||
endpoint: 0x{trackpad_endpoint:02x}"
|
||||
)
|
||||
|
||||
# Setup device
|
||||
kbd_device.set_configuration()
|
||||
|
||||
# Detach kernel drivers for both interfaces
|
||||
if kbd_device.is_kernel_driver_active(kbd_interface):
|
||||
kbd_device.detach_kernel_driver(kbd_interface)
|
||||
if trackpad_interface != kbd_interface and kbd_device.is_kernel_driver_active(
|
||||
trackpad_interface
|
||||
):
|
||||
kbd_device.detach_kernel_driver(trackpad_interface)
|
||||
kbd_buf = array.array("B", [0] * 8)
|
||||
mouse_buf = array.array("B", [0] * 4)
|
||||
|
||||
print(f"\nUse both keyboard and trackpad for {duration} seconds:")
|
||||
print("(Events from both interfaces will be shown)")
|
||||
|
||||
start_time = time.monotonic()
|
||||
last_kbd_report = None
|
||||
last_mouse_report = None
|
||||
kbd_events = 0
|
||||
mouse_events = 0
|
||||
|
||||
while time.monotonic() - start_time < duration:
|
||||
try:
|
||||
# Try to read keyboard
|
||||
try:
|
||||
count = kbd_device.read(kbd_endpoint, kbd_buf, timeout=10)
|
||||
if count > 0:
|
||||
current_kbd_report = bytes(kbd_buf[:count])
|
||||
if current_kbd_report != last_kbd_report:
|
||||
parsed = parse_keyboard_report(kbd_buf)
|
||||
if parsed.get("has_input"):
|
||||
kbd_events += 1
|
||||
output_parts = []
|
||||
if parsed["modifiers"]:
|
||||
output_parts.append(
|
||||
f"Mods: {'+'.join(parsed['modifiers'])}"
|
||||
)
|
||||
if parsed["keys"]:
|
||||
output_parts.append(
|
||||
f"Keys: {'+'.join(parsed['keys'])}"
|
||||
)
|
||||
print(
|
||||
f" 🎹 KEYBOARD[{kbd_events:2d}]: \
|
||||
{' | '.join(output_parts)}"
|
||||
)
|
||||
last_kbd_report = current_kbd_report
|
||||
except usb.core.USBTimeoutError:
|
||||
pass
|
||||
# Try to read trackpad
|
||||
try:
|
||||
count = kbd_device.read(trackpad_endpoint, mouse_buf, timeout=10)
|
||||
if count > 0:
|
||||
current_mouse_report = bytes(mouse_buf[:count])
|
||||
if current_mouse_report != last_mouse_report:
|
||||
parsed = parse_mouse_report(mouse_buf)
|
||||
if parsed.get("has_input"):
|
||||
mouse_events += 1
|
||||
output_parts = []
|
||||
if parsed["buttons"]:
|
||||
output_parts.append(
|
||||
f"Btns: {'+'.join(parsed['buttons'])}"
|
||||
)
|
||||
if parsed["x_movement"] or parsed["y_movement"]:
|
||||
output_parts.append(
|
||||
f"Move: X{parsed['x_movement']:+d} \
|
||||
Y{parsed['y_movement']:+d}"
|
||||
)
|
||||
if parsed["wheel"]:
|
||||
output_parts.append(f"Wheel: {parsed['wheel']:+d}")
|
||||
print(
|
||||
f" 🖱️ TRACKPAD[{mouse_events:2d}]: \
|
||||
{' | '.join(output_parts)}"
|
||||
)
|
||||
last_mouse_report = current_mouse_report
|
||||
except usb.core.USBTimeoutError:
|
||||
pass
|
||||
except usb.core.USBError as e:
|
||||
print(f"USB error: {e}")
|
||||
break
|
||||
print("\nComposite device reading complete:")
|
||||
print(f" Keyboard events: {kbd_events}")
|
||||
print(f" Trackpad events: {mouse_events}")
|
||||
except Exception as e:
|
||||
print(f"Error reading composite device: {e}")
|
||||
|
||||
# ============================================================================
|
||||
# DEVICE DISCOVERY AND ANALYSIS
|
||||
# ============================================================================
|
||||
|
||||
def analyze_all_devices():
|
||||
"""
|
||||
Comprehensive analysis of all connected HID devices.
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("COMPREHENSIVE HID DEVICE ANALYSIS")
|
||||
print("=" * 60)
|
||||
|
||||
# Force fresh scan
|
||||
force_refresh()
|
||||
|
||||
# Get device counts
|
||||
keyboards = count_keyboards()
|
||||
mice = count_mice()
|
||||
gamepads = count_gamepads()
|
||||
|
||||
print("\nDevice Summary:")
|
||||
print(f" Keyboards: {keyboards}")
|
||||
print(f" Mice: {mice}")
|
||||
print(f" Gamepads: {gamepads}")
|
||||
print(f" Total: {keyboards + mice + gamepads}")
|
||||
|
||||
# Detailed device information
|
||||
summary = list_all_hid_devices()
|
||||
|
||||
print("\nDetailed Device Information:")
|
||||
print("-" * 40)
|
||||
|
||||
# Analyze keyboards
|
||||
if summary["keyboards"]["count"] > 0:
|
||||
print(f"\n🎹 KEYBOARDS ({summary['keyboards']['count']}):")
|
||||
for device in summary["keyboards"]["devices"]:
|
||||
print(f" [{device['index']}] {device['manufacturer']} {device['product']}")
|
||||
print(f" VID:PID = {device['vid']:04x}:{device['pid']:04x}")
|
||||
print(
|
||||
f" Interface: {device['interface_index']}, \
|
||||
Endpoint: 0x{device['endpoint_address']:02x}"
|
||||
)
|
||||
|
||||
# Check for composite capabilities
|
||||
is_composite, other_types = is_composite_device("keyboard", device["index"])
|
||||
if is_composite:
|
||||
print(f" 🔗 Composite device with: {', '.join(other_types)}")
|
||||
# Check connectivity
|
||||
connected = is_device_connected("keyboard", device["index"])
|
||||
print(f" Status: {'🟢 Connected' if connected else '🔴 Disconnected'}")
|
||||
# Analyze mice
|
||||
if summary["mice"]["count"] > 0:
|
||||
print(f"\n🖱️ MICE ({summary['mice']['count']}):")
|
||||
for device in summary["mice"]["devices"]:
|
||||
print(f" [{device['index']}] {device['manufacturer']} {device['product']}")
|
||||
print(f" VID:PID = {device['vid']:04x}:{device['pid']:04x}")
|
||||
print(
|
||||
f" Interface: {device['interface_index']}, \
|
||||
Endpoint: 0x{device['endpoint_address']:02x}"
|
||||
)
|
||||
|
||||
# Check for composite capabilities
|
||||
is_composite, other_types = is_composite_device("mouse", device["index"])
|
||||
if is_composite:
|
||||
print(f" 🔗 Composite device with: {', '.join(other_types)}")
|
||||
print(" (This might be a trackpad from a keyboard+trackpad combo)")
|
||||
# Check connectivity
|
||||
connected = is_device_connected("mouse", device["index"])
|
||||
print(f" Status: {'🟢 Connected' if connected else '🔴 Disconnected'}")
|
||||
# Analyze gamepads
|
||||
if summary["gamepads"]["count"] > 0:
|
||||
print(f"\n🎮 GAMEPADS ({summary['gamepads']['count']}):")
|
||||
for device in summary["gamepads"]["devices"]:
|
||||
print(f" [{device['index']}] {device['manufacturer']} {device['product']}")
|
||||
print(f" VID:PID = {device['vid']:04x}:{device['pid']:04x}")
|
||||
print(
|
||||
f" Interface: {device['interface_index']}, \
|
||||
Endpoint: 0x{device['endpoint_address']:02x}"
|
||||
)
|
||||
|
||||
# Check connectivity
|
||||
connected = is_device_connected("gamepad", device["index"])
|
||||
print(f" Status: {'🟢 Connected' if connected else '🔴 Disconnected'}")
|
||||
# Analyze composite devices
|
||||
composite_devices = get_composite_device_info()
|
||||
if composite_devices:
|
||||
print(f"\n🔗 COMPOSITE DEVICES ({len(composite_devices)}):")
|
||||
for device_id, info in composite_devices.items():
|
||||
print(f" {info['product']}")
|
||||
print(f" Device ID: {device_id}")
|
||||
print(f" Interface types: {', '.join(info['types'])}")
|
||||
|
||||
# Show how to access each interface
|
||||
for device_type in info["types"]:
|
||||
if device_type == "keyboard":
|
||||
for i in range(keyboards):
|
||||
kbd_device = get_keyboard_device(i)
|
||||
kbd_id = f"{kbd_device.idVendor:04x}: \
|
||||
{kbd_device.idProduct:04x}: \
|
||||
{getattr(kbd_device, 'serial_number', 'no_serial')}"
|
||||
if kbd_id == device_id:
|
||||
print(f" -> Access keyboard via: get_keyboard_info({i})")
|
||||
break
|
||||
elif device_type == "mouse":
|
||||
for i in range(mice):
|
||||
mouse_device = get_mouse_device(i)
|
||||
# pylint: disable=line-too-long
|
||||
mouse_id = f"{mouse_device.idVendor:04x}: \
|
||||
{mouse_device.idProduct:04x}: \
|
||||
{getattr(mouse_device, 'serial_number', 'no_serial')}"
|
||||
if mouse_id == device_id:
|
||||
print(
|
||||
f" -> Access mouse/trackpad via: get_mouse_info({i})"
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
def benchmark_performance():
|
||||
"""
|
||||
Benchmark the performance of device detection operations.
|
||||
"""
|
||||
print("\n🔍 PERFORMANCE BENCHMARK")
|
||||
print("-" * 30)
|
||||
|
||||
import gc
|
||||
|
||||
# Warm up
|
||||
force_refresh()
|
||||
|
||||
# Benchmark full device scan
|
||||
gc.collect()
|
||||
start_time = time.monotonic()
|
||||
force_refresh()
|
||||
scan_time = time.monotonic() - start_time
|
||||
|
||||
# Benchmark cached access
|
||||
start_time = time.monotonic()
|
||||
keyboards = count_keyboards()
|
||||
mice = count_mice()
|
||||
gamepads = count_gamepads()
|
||||
cache_time = time.monotonic() - start_time
|
||||
|
||||
# Benchmark device info access
|
||||
start_time = time.monotonic()
|
||||
for i in range(keyboards):
|
||||
get_keyboard_info(i)
|
||||
for i in range(mice):
|
||||
get_mouse_info(i)
|
||||
for i in range(gamepads):
|
||||
get_gamepad_info(i)
|
||||
info_time = time.monotonic() - start_time
|
||||
|
||||
# Benchmark composite device operations
|
||||
start_time = time.monotonic()
|
||||
get_composite_device_info()
|
||||
for i in range(keyboards):
|
||||
is_composite_device("keyboard", i)
|
||||
composite_time = time.monotonic() - start_time
|
||||
|
||||
print(f"Full device scan: {scan_time*1000:6.1f} ms")
|
||||
print(f"Cached device counts: {cache_time*1000:6.3f} ms")
|
||||
print(f"Device info access: {info_time*1000:6.1f} ms")
|
||||
print(f"Composite operations: {composite_time*1000:6.1f} ms")
|
||||
print(f"Devices found: {keyboards} kbd, {mice} mouse, {gamepads} gamepad")
|
||||
|
||||
|
||||
def demonstrate_device_filtering():
|
||||
"""
|
||||
Demonstrate device filtering capabilities.
|
||||
"""
|
||||
print("\n🔍 DEVICE FILTERING DEMONSTRATION")
|
||||
print("-" * 40)
|
||||
|
||||
# Show all devices first
|
||||
print("Before filtering:")
|
||||
summary = list_all_hid_devices()
|
||||
total_before = sum(info["count"] for info in summary.values())
|
||||
print(f" Total devices: {total_before}")
|
||||
|
||||
if total_before == 0:
|
||||
print(" No devices to filter. Connect some devices first.")
|
||||
return
|
||||
# Get a VID to filter by (use first keyboard if available)
|
||||
if summary["keyboards"]["count"] > 0:
|
||||
test_vid = summary["keyboards"]["devices"][0]["vid"]
|
||||
print(f"\nFiltering to only allow VID 0x{test_vid:04x}...")
|
||||
|
||||
# Apply filter
|
||||
set_device_filter(allowed_vids={test_vid})
|
||||
|
||||
# Force refresh and check results
|
||||
force_refresh()
|
||||
filtered_summary = list_all_hid_devices()
|
||||
total_after = sum(info["count"] for info in filtered_summary.values())
|
||||
|
||||
print("After filtering:")
|
||||
print(f" Total devices: {total_after}")
|
||||
for device_type, info in filtered_summary.items():
|
||||
if info["count"] > 0:
|
||||
print(f" {device_type}: {info['count']}")
|
||||
# Clear filter
|
||||
print("\nClearing filter...")
|
||||
set_device_filter(allowed_vids=None)
|
||||
force_refresh()
|
||||
|
||||
restored_summary = list_all_hid_devices()
|
||||
total_restored = sum(info["count"] for info in restored_summary.values())
|
||||
print("After clearing filter:")
|
||||
print(f" Total devices: {total_restored}")
|
||||
|
||||
|
||||
def continuous_monitoring(duration=30):
|
||||
"""
|
||||
Demonstrate continuous device monitoring.
|
||||
"""
|
||||
print("\n📡 CONTINUOUS DEVICE MONITORING")
|
||||
print(f"Duration: {duration} seconds")
|
||||
print("-" * 40)
|
||||
print("Connect and disconnect devices to see real-time detection!")
|
||||
print("(Device changes will be logged automatically)")
|
||||
|
||||
start_time = time.monotonic()
|
||||
# last_summary = None
|
||||
check_interval = 1.0 # Check every second
|
||||
|
||||
try:
|
||||
while time.monotonic() - start_time < duration:
|
||||
current_time = time.monotonic() - start_time
|
||||
|
||||
# Check for device changes
|
||||
force_refresh()
|
||||
current_summary = list_all_hid_devices()
|
||||
|
||||
# Show periodic status updates
|
||||
if int(current_time) % 10 == 0 and int(current_time) > 0:
|
||||
total_devices = sum(info["count"] for info in current_summary.values())
|
||||
print(
|
||||
f"[{current_time:5.0f}s] Status: {total_devices} \
|
||||
total devices connected"
|
||||
)
|
||||
# last_summary = current_summary
|
||||
time.sleep(check_interval)
|
||||
except KeyboardInterrupt:
|
||||
print("\nMonitoring stopped by user")
|
||||
# Show monitoring statistics
|
||||
stats = device_monitor.get_statistics()
|
||||
print("\nMonitoring Statistics:")
|
||||
print(f" Total device changes detected: {stats['total_changes']}")
|
||||
print(f" Recent changes in history: {stats['recent_changes']}")
|
||||
|
||||
if stats["recent_changes"] > 0:
|
||||
print(" Recent changes:")
|
||||
for change in stats["history"][-5:]: # Show last 5 changes
|
||||
print(
|
||||
f" {change['timestamp']:.1f}s: {change['device_type']} \
|
||||
{change['action']}"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# MAIN DEMONSTRATION FUNCTION
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main demonstration function showcasing all enhanced features.
|
||||
"""
|
||||
print("Enhanced USB HID Device Detection Example")
|
||||
print("=========================================")
|
||||
print(f"Running on: {board.board_id}")
|
||||
print(f"CircuitPython version: {board.__name__}")
|
||||
|
||||
# Configure library settings
|
||||
print("\nConfiguring library settings...")
|
||||
set_cache_timeout(0.5) # 500ms cache timeout for responsive demo
|
||||
|
||||
# Initial device analysis
|
||||
analyze_all_devices()
|
||||
|
||||
# Performance benchmark
|
||||
benchmark_performance()
|
||||
|
||||
# Quick input demos if devices are available
|
||||
keyboards = count_keyboards()
|
||||
mice = count_mice()
|
||||
|
||||
if keyboards > 0:
|
||||
print("\n⌨️ Quick keyboard demo (first 3 seconds)...")
|
||||
enhanced_read_keyboard_input(0, duration=3)
|
||||
if mice > 0:
|
||||
print("\n🖱️ Quick mouse demo (first 3 seconds)...")
|
||||
enhanced_read_mouse_input(0, duration=3)
|
||||
# Check for composite devices
|
||||
composite_found = False
|
||||
for i in range(keyboards):
|
||||
is_composite, other_types = is_composite_device("keyboard", i)
|
||||
if is_composite and "mouse" in other_types:
|
||||
print("\n🔗 Quick composite device demo (3 seconds)...")
|
||||
read_composite_device(i, duration=3)
|
||||
composite_found = True
|
||||
break
|
||||
if not composite_found and keyboards > 0:
|
||||
print(
|
||||
"\n💡 Tip: Try connecting a keyboard with integrated trackpad \
|
||||
to see composite device features!"
|
||||
)
|
||||
# Device filtering demo
|
||||
demonstrate_device_filtering()
|
||||
|
||||
print("\n🎉 Basic demonstration complete!")
|
||||
print("\nFor extended testing:")
|
||||
print(" - Call analyze_all_devices() for detailed analysis")
|
||||
print(" - Call continuous_monitoring(duration) to monitor device changes")
|
||||
print(" - Call enhanced_read_keyboard_input(index) to test keyboard input")
|
||||
print(" - Call enhanced_read_mouse_input(index) to test mouse input")
|
||||
print(" - Call read_composite_device(index) for composite device testing")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not ENHANCED_API_AVAILABLE:
|
||||
print("Enhanced API not available. Cannot run demonstration.")
|
||||
else:
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\nDemo interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"Demo error: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
# SPDX-FileCopyrightText: Copyright (c) 2025 Anne Barela for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Simple USB Device Monitor
|
||||
=========================
|
||||
|
||||
A simple program that continuously monitors USB host ports for connected devices.
|
||||
Automatically starts monitoring and detects when devices are plugged in or unplugged.
|
||||
|
||||
This program:
|
||||
- Loops continuously checking for USB devices every 5 seconds
|
||||
- Detects device additions and removals automatically
|
||||
- Shows basic device information
|
||||
- Handles composite devices (keyboard+trackpad combos)
|
||||
- Runs until stopped with Ctrl+C
|
||||
|
||||
Hardware Requirements:
|
||||
- Adafruit board with USB host support (Feather RP2040 USB Host, Fruit Jam, etc.)
|
||||
- USB devices can be connected/disconnected during operation
|
||||
|
||||
Software Requirements:
|
||||
- CircuitPython 9.0+ with usb_host support
|
||||
- Enhanced adafruit_usb_host_descriptors library
|
||||
|
||||
Usage:
|
||||
- Simply run this program
|
||||
- Connect and disconnect USB devices to see detection
|
||||
- Press Ctrl+C to stop monitoring
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import board
|
||||
|
||||
# Import the enhanced USB host descriptors library
|
||||
try:
|
||||
from adafruit_usb_host_descriptors import (
|
||||
count_keyboards, count_mice, count_gamepads,
|
||||
list_all_hid_devices, get_composite_device_info,
|
||||
force_refresh, is_device_connected
|
||||
)
|
||||
print("Enhanced USB host descriptors library loaded")
|
||||
except ImportError as e:
|
||||
print("Library import failed: {}".format(e))
|
||||
print("Please ensure you have the enhanced " +
|
||||
"adafruit_usb_host_descriptors library")
|
||||
exit(1)
|
||||
|
||||
class SimpleDeviceMonitor:
|
||||
"""Simple USB device monitor with change detection"""
|
||||
|
||||
def __init__(self):
|
||||
self.last_device_state = None
|
||||
self.loop_count = 0
|
||||
self.total_changes = 0
|
||||
|
||||
def get_current_device_state(self):
|
||||
"""Get current device state as a comparable dictionary"""
|
||||
try:
|
||||
# Force fresh scan to detect changes
|
||||
force_refresh()
|
||||
|
||||
# Get device counts
|
||||
keyboards = count_keyboards()
|
||||
mice = count_mice()
|
||||
gamepads = count_gamepads()
|
||||
|
||||
# Get detailed device info
|
||||
devices = list_all_hid_devices()
|
||||
|
||||
# Create a state snapshot
|
||||
state = {
|
||||
'counts': {
|
||||
'keyboards': keyboards,
|
||||
'mice': mice,
|
||||
'gamepads': gamepads,
|
||||
'total': keyboards + mice + gamepads
|
||||
},
|
||||
'devices': {}
|
||||
}
|
||||
|
||||
# Store device details for comparison
|
||||
for device_type in ['keyboards', 'mice', 'gamepads']:
|
||||
state['devices'][device_type] = []
|
||||
for device in devices[device_type]['devices']:
|
||||
# Create a simple identifier for each device
|
||||
device_id = "{:04x}:{:04x}:{}:{}".format(
|
||||
device['vid'], device['pid'],
|
||||
device['manufacturer'], device['product'])
|
||||
state['devices'][device_type].append({
|
||||
'id': device_id,
|
||||
'index': device['index'],
|
||||
'manufacturer': device['manufacturer'],
|
||||
'product': device['product'],
|
||||
'vid': device['vid'],
|
||||
'pid': device['pid']
|
||||
})
|
||||
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
# Return empty state on error to prevent crash
|
||||
print(" WARNING: Could not get device state: {}".format(e))
|
||||
return {
|
||||
'counts': {'keyboards': 0, 'mice': 0, 'gamepads': 0,
|
||||
'total': 0},
|
||||
'devices': {'keyboards': [], 'mice': [], 'gamepads': []}
|
||||
}
|
||||
|
||||
def detect_changes(self, current_state):
|
||||
"""Detect and report changes between current and last state"""
|
||||
if self.last_device_state is None:
|
||||
# First run - no change detection needed, just note it's first run
|
||||
return
|
||||
|
||||
changes_detected = False
|
||||
|
||||
# Proper singular forms mapping
|
||||
singular_forms = {
|
||||
'keyboards': 'keyboard',
|
||||
'mice': 'mouse',
|
||||
'gamepads': 'gamepad'
|
||||
}
|
||||
|
||||
# Check for count changes
|
||||
for device_type in ['keyboards', 'mice', 'gamepads']:
|
||||
current_count = current_state['counts'][device_type]
|
||||
last_count = self.last_device_state['counts'][device_type]
|
||||
|
||||
if current_count != last_count:
|
||||
changes_detected = True
|
||||
change = current_count - last_count
|
||||
if change > 0:
|
||||
print(" + {}: +{} (now {})".format(
|
||||
device_type, change, current_count))
|
||||
else:
|
||||
print(" - {}: {} (now {})".format(
|
||||
device_type, change, current_count))
|
||||
|
||||
# Detect specific device changes
|
||||
for device_type in ['keyboards', 'mice', 'gamepads']:
|
||||
current_devices = {d['id']: d for d in
|
||||
current_state['devices'][device_type]}
|
||||
last_devices = {d['id']: d for d in
|
||||
self.last_device_state['devices'][device_type]}
|
||||
|
||||
device_type_singular = singular_forms[device_type]
|
||||
|
||||
# Find new devices
|
||||
for device_id, device in current_devices.items():
|
||||
if device_id not in last_devices:
|
||||
changes_detected = True
|
||||
print(" CONNECTED: {} {} ({})".format(
|
||||
device['manufacturer'], device['product'],
|
||||
device_type_singular))
|
||||
|
||||
# Find removed devices
|
||||
for device_id, device in last_devices.items():
|
||||
if device_id not in current_devices:
|
||||
changes_detected = True
|
||||
print(" DISCONNECTED: {} {} ({})".format(
|
||||
device['manufacturer'], device['product'],
|
||||
device_type_singular))
|
||||
|
||||
if changes_detected:
|
||||
self.total_changes += 1
|
||||
|
||||
def report_current_devices(self, state):
|
||||
"""Report current connected devices"""
|
||||
total = state['counts']['total']
|
||||
|
||||
if total == 0:
|
||||
print(" No HID devices connected")
|
||||
else:
|
||||
print(" Current devices ({} total):".format(total))
|
||||
|
||||
for device_type in ['keyboards', 'mice', 'gamepads']:
|
||||
devices = state['devices'][device_type]
|
||||
if devices:
|
||||
# Manual capitalization for CircuitPython compatibility
|
||||
type_name = device_type[0].upper() + device_type[1:]
|
||||
print(" {}: {}".format(type_name, len(devices)))
|
||||
for device in devices:
|
||||
print(" [{}] {} {}".format(
|
||||
device['index'], device['manufacturer'],
|
||||
device['product']))
|
||||
|
||||
# Check for composite devices
|
||||
composite_devices = get_composite_device_info()
|
||||
if composite_devices:
|
||||
print(" Composite devices: {}".format(
|
||||
len(composite_devices)))
|
||||
for device_id, info in composite_devices.items():
|
||||
print(" {} ({})".format(
|
||||
info['product'], ', '.join(info['types'])))
|
||||
|
||||
def check_device_connectivity(self, state):
|
||||
"""Check if previously detected devices are still responding"""
|
||||
connectivity_issues = []
|
||||
|
||||
# Proper singular forms mapping
|
||||
singular_forms = {
|
||||
'keyboards': 'keyboard',
|
||||
'mice': 'mouse', # Fix: was becoming 'mic' incorrectly
|
||||
'gamepads': 'gamepad'
|
||||
}
|
||||
|
||||
for device_type in ['keyboards', 'mice', 'gamepads']:
|
||||
for device in state['devices'][device_type]:
|
||||
device_type_singular = singular_forms[device_type]
|
||||
connected = is_device_connected(device_type_singular,
|
||||
device['index'])
|
||||
if not connected:
|
||||
connectivity_issues.append("{}[{}]".format(
|
||||
device_type_singular, device['index']))
|
||||
|
||||
if connectivity_issues:
|
||||
print(" WARNING: Connectivity issues: {}".format(
|
||||
', '.join(connectivity_issues)))
|
||||
|
||||
def run(self):
|
||||
"""Main monitoring loop"""
|
||||
print("Simple USB Device Monitor")
|
||||
print("=" * 25)
|
||||
print("Running on: {}".format(board.board_id))
|
||||
print("Monitoring USB host ports for device changes...")
|
||||
print("Connect/disconnect devices to see detection in action")
|
||||
print()
|
||||
print("IMPORTANT: Press Ctrl+C to stop monitoring at any time")
|
||||
print(" Monitoring will continue until interrupted")
|
||||
print()
|
||||
|
||||
start_time = time.monotonic()
|
||||
|
||||
try:
|
||||
while True:
|
||||
self.loop_count += 1
|
||||
|
||||
print("Loop #{} - Checking devices...".format(
|
||||
self.loop_count))
|
||||
|
||||
try:
|
||||
# Get current device state
|
||||
current_state = self.get_current_device_state()
|
||||
|
||||
# Detect and report changes
|
||||
self.detect_changes(current_state)
|
||||
|
||||
# Always show current device info each loop
|
||||
self.report_current_devices(current_state)
|
||||
|
||||
# Check device connectivity (optional diagnostic)
|
||||
if current_state['counts']['total'] > 0:
|
||||
self.check_device_connectivity(current_state)
|
||||
|
||||
# Update state for next iteration
|
||||
self.last_device_state = current_state
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Break out of inner loop on Ctrl+C
|
||||
raise
|
||||
except Exception as e:
|
||||
print(" ERROR: Error during device check: {}".format(e))
|
||||
|
||||
# Show statistics periodically
|
||||
if self.loop_count % 10 == 0:
|
||||
print(" Statistics: {} checks, {} changes detected".format(
|
||||
self.loop_count, self.total_changes))
|
||||
|
||||
print() # Empty line for readability
|
||||
|
||||
# Wait 5 seconds before next check
|
||||
time.sleep(5.0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Clean shutdown on Ctrl+C
|
||||
runtime = time.monotonic() - start_time
|
||||
print("\nMonitoring stopped by user (Ctrl+C pressed)")
|
||||
print("Final statistics:")
|
||||
print(" Total checks: {}".format(self.loop_count))
|
||||
print(" Changes detected: {}".format(self.total_changes))
|
||||
print(" Runtime: {:.1f} seconds".format(runtime))
|
||||
print("Thank you for using USB Device Monitor!")
|
||||
|
||||
# Main execution
|
||||
if __name__ == "__main__":
|
||||
print("Starting USB Device Monitor...")
|
||||
print("Press Ctrl+C at any time to stop monitoring")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Create and run the continuous monitor
|
||||
monitor = SimpleDeviceMonitor()
|
||||
monitor.run()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nProgram interrupted by user (Ctrl+C)")
|
||||
print("Goodbye!")
|
||||
except Exception as e:
|
||||
print("\nError starting monitor: {}".format(e))
|
||||
print("Please check your USB host hardware and library installation")
|
||||
print("Program terminated.")
|
||||
Loading…
Reference in a new issue