//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, plus any device that was // registered from outside the process (see RegisterExternalDevice). func Enumerate() ([]Device, error) { entries, err := os.ReadDir(sysfsUSBDevices) if err != nil { // On Android sysfs is not readable by an app, but devices handed in // through the bridge still work. Only report a failure when there is // nothing at all to go on. if external := ExternalDevices(); len(external) > 0 { return external, 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 mergeExternal(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 from sysfs. This gives us the bound kernel driver per // interface, which the raw descriptors don't contain. dev.Interfaces = readInterfaces(sysPath, busID) // Overlay the raw descriptors from the usbdevfs file. Only these expose // interface alternate settings and correct endpoint attributes; sysfs // shows just the active alternate setting. Without the non-zero alternate // settings the endpoint type map is wrong for webcams and audio devices. applyRawDescriptors(dev) return dev, nil } // applyRawDescriptors reads the device's descriptor blob from its usbdevfs // file and fills in Endpoints plus any interface data sysfs did not provide. // Failure is not fatal: reading /dev/bus/usb requires permissions we may not // have when merely listing devices, and the sysfs data alone is enough for // that. Sharing a device opens the same file anyway and would fail earlier. func applyRawDescriptors(dev *Device) { data, err := os.ReadFile(dev.DevPath) if err != nil { return } pd, err := ParseDescriptors(data) if err != nil { return } cfg := pd.FindConfig(dev.ConfigValue) if cfg == nil { // The device is unconfigured, or sysfs and the descriptors disagree. // Fall back to the first configuration. if len(pd.Configs) == 0 { return } cfg = &pd.Configs[0] } dev.Endpoints = cfg.AllEndpoints() // Merge: keep the driver names from sysfs, take everything else from the // descriptors (which are authoritative and include endpoint intervals). drivers := make(map[uint8]string, len(dev.Interfaces)) for _, iface := range dev.Interfaces { drivers[iface.Number] = iface.Driver } ifaces := cfg.ActiveInterfaces() for i := range ifaces { ifaces[i].Driver = drivers[ifaces[i].Number] } if len(ifaces) > 0 { dev.Interfaces = ifaces } } 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) } // Read endpoints iface.Endpoints = readEndpoints(ifacePath) ifaces = append(ifaces, iface) } return ifaces } func readEndpoints(ifacePath string) []Endpoint { entries, err := os.ReadDir(ifacePath) if err != nil { return nil } var eps []Endpoint for _, entry := range entries { name := entry.Name() if !strings.HasPrefix(name, "ep_") || name == "ep_00" { continue } epPath := filepath.Join(ifacePath, name) // Every numeric endpoint attribute in sysfs is hex, without a 0x // prefix — wMaxPacketSize "0040" means 64, not 40. eps = append(eps, Endpoint{ Address: readHex8(epPath, "bEndpointAddress"), TransferType: readHex8(epPath, "bmAttributes") & 0x03, MaxPacketSize: readHex16(epPath, "wMaxPacketSize"), Interval: readHex8(epPath, "bInterval"), }) } return eps } 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 } }