//go:build linux package usb import ( "fmt" "os" "path/filepath" "strconv" "strings" ) const sysfsUSBDevices = "/sys/bus/usb/devices" // Enumerate lists all USB devices by reading sysfs func Enumerate() ([]Device, error) { entries, err := os.ReadDir(sysfsUSBDevices) if err != nil { return nil, fmt.Errorf("reading sysfs: %w", err) } var devices []Device for _, entry := range entries { name := entry.Name() // Skip interfaces (contain ":") and "usb*" root hubs if strings.Contains(name, ":") || strings.HasPrefix(name, "usb") { continue } // Must be a device path like "1-1", "1-1.4", "2-3", etc. if !isDevicePath(name) { continue } dev, err := readDevice(name) if err != nil { continue // skip devices we can't read } // Skip hubs if dev.IsHub() { continue } devices = append(devices, *dev) } return devices, nil } func isDevicePath(name string) bool { // Device paths look like "1-1", "1-1.4", "2-3.1.2" // First char is a digit (bus number) if len(name) < 3 { return false } if name[0] < '1' || name[0] > '9' { return false } if name[1] != '-' { return false } return true } func readDevice(busID string) (*Device, error) { sysPath := filepath.Join(sysfsUSBDevices, busID) dev := &Device{ BusID: busID, SysPath: sysPath, } // Read basic attributes dev.BusNum = readUint32(sysPath, "busnum") dev.DevNum = readUint32(sysPath, "devnum") dev.Speed = parseSpeed(readString(sysPath, "speed")) dev.VendorID = readHex16(sysPath, "idVendor") dev.ProductID = readHex16(sysPath, "idProduct") dev.BcdDevice = readHex16(sysPath, "bcdDevice") dev.DeviceClass = readHex8(sysPath, "bDeviceClass") dev.DeviceSubClass = readHex8(sysPath, "bDeviceSubClass") dev.DeviceProtocol = readHex8(sysPath, "bDeviceProtocol") dev.ConfigValue = uint8(readUint32(sysPath, "bConfigurationValue")) dev.NumConfigs = uint8(readUint32(sysPath, "bNumConfigurations")) // Read string descriptors dev.Manufacturer = readString(sysPath, "manufacturer") dev.Product = readString(sysPath, "product") dev.Serial = readString(sysPath, "serial") // Compute dev path dev.DevPath = fmt.Sprintf("/dev/bus/usb/%03d/%03d", dev.BusNum, dev.DevNum) // Read interfaces dev.Interfaces = readInterfaces(sysPath, busID) return dev, nil } func readInterfaces(sysPath, busID string) []Interface { entries, err := os.ReadDir(sysPath) if err != nil { return nil } var ifaces []Interface for _, entry := range entries { name := entry.Name() // Interface directories look like "1-1.4:1.0" if !strings.HasPrefix(name, busID+":") { continue } ifacePath := filepath.Join(sysPath, name) iface := Interface{ Number: readHex8(ifacePath, "bInterfaceNumber"), Class: readHex8(ifacePath, "bInterfaceClass"), SubClass: readHex8(ifacePath, "bInterfaceSubClass"), Protocol: readHex8(ifacePath, "bInterfaceProtocol"), } // Read driver driverLink, err := os.Readlink(filepath.Join(ifacePath, "driver")) if err == nil { iface.Driver = filepath.Base(driverLink) } ifaces = append(ifaces, iface) } return ifaces } func readString(dir, attr string) string { data, err := os.ReadFile(filepath.Join(dir, attr)) if err != nil { return "" } return strings.TrimSpace(string(data)) } func readUint32(dir, attr string) uint32 { s := readString(dir, attr) if s == "" { return 0 } v, _ := strconv.ParseUint(s, 10, 32) return uint32(v) } func readHex16(dir, attr string) uint16 { s := readString(dir, attr) if s == "" { return 0 } v, _ := strconv.ParseUint(s, 16, 16) return uint16(v) } func readHex8(dir, attr string) uint8 { s := readString(dir, attr) if s == "" { return 0 } v, _ := strconv.ParseUint(s, 16, 8) return uint8(v) } func parseSpeed(s string) uint32 { switch s { case "1.5": return 1 // Low case "12": return 2 // Full case "480": return 3 // High case "5000": return 5 // Super case "10000": return 6 // Super+ case "20000": return 6 // Super+ (USB 3.2 2x1) default: return 0 // Unknown } }