/** * USB device enumeration and matching utilities */ import type { DeviceConfig } from '../config/schema.js'; import type { DeviceId, DeviceInfo } from '../types/common.js'; import { DeviceState } from '../types/common.js'; import { logger } from '../utils/logger.js'; import { type LocalDevice, listLocalDevices, matchesPathPattern } from './commands.js'; /** * Find local device by USB path */ export async function findLocalDevice(deviceId: DeviceId): Promise { const devices = await listLocalDevices(); const match = devices.find((d) => d.path === deviceId.path); if (!match) { return null; } return { ...deviceId, busId: match.busId, vendorId: match.vendorId, productId: match.productId, serial: match.serial, state: DeviceState.CONNECTED, }; } /** * Find multiple local devices */ export async function findLocalDevices(deviceIds: DeviceId[]): Promise { const devices = await listLocalDevices(); const found: DeviceInfo[] = []; for (const deviceId of deviceIds) { const match = devices.find((d) => d.path === deviceId.path); if (match) { found.push({ ...deviceId, busId: match.busId, vendorId: match.vendorId, productId: match.productId, serial: match.serial, state: DeviceState.CONNECTED, }); } } return found; } /** * Find local devices matching a device configuration * Supports both exact path matching and tree-based wildcard matching */ export async function findMatchingDevices(deviceConfig: DeviceConfig): Promise { const devices = await listLocalDevices(); const found: DeviceInfo[] = []; logger.info( `[DEBUG] listLocalDevices() returned ${devices.length} devices for pattern ${deviceConfig.path}` ); for (const device of devices) { logger.info(`[DEBUG] - ${device.path} (${device.busId}) class=${device.deviceClass}`); } for (const device of devices) { if (matchesDeviceConfig(device, deviceConfig)) { found.push({ path: device.path, busId: device.busId, vendorId: device.vendorId, productId: device.productId, serial: device.serial, description: deviceConfig.description || device.description, state: DeviceState.CONNECTED, }); } else { logger.info(`[DEBUG] Device ${device.path} did not match config ${deviceConfig.path}`); } } logger.info(`[DEBUG] findMatchingDevices() found ${found.length} matching devices`); return found; } /** * Check if device matches configuration (exact or tree mode) * Supports wildcard patterns and optional vendor/product ID filtering */ export function matchesDeviceConfig(device: LocalDevice, config: DeviceConfig): boolean { // Check path pattern (exact or tree with wildcard) const pathMatches = matchesPathPattern(device.path, config.path); if (!pathMatches) { logger.info(`[DEBUG] Path '${device.path}' does not match pattern '${config.path}'`); return false; } // For wildcard patterns, skip USB hubs (device class 09) // Hubs themselves cannot be bound, only devices connected to them const isWildcard = config.path.endsWith(':**') || config.path.endsWith('.**'); if (isWildcard && device.deviceClass === '09') { logger.info(`[DEBUG] Device '${device.path}' is a hub (class 09), skipping`); return false; } // Optional vendor ID filter if (config.vendorId && device.vendorId !== config.vendorId) { return false; } // Optional product ID filter if (config.productId && device.productId !== config.productId) { return false; } // Optional serial filter if (config.serial && device.serial !== config.serial) { return false; } logger.info(`[DEBUG] Device '${device.path}' MATCHES!`); return true; } /** * Check if device matches the given ID (by path) */ export function matchesDeviceId(device: LocalDevice, deviceId: DeviceId): boolean { return device.path === deviceId.path; } /** * Format device for display */ export function formatDevice(device: DeviceInfo): string { const vendorProduct = device.vendorId && device.productId ? `${device.vendorId}:${device.productId}` : 'Unknown'; const serial = device.serial ? ` SN:${device.serial}` : ''; return `${device.path} (${vendorProduct}${serial}) [${device.busId}] - ${device.description || 'No description'}`; }