usb-server/internal/usb/enumerate_linux.go

189 lines
4.0 KiB
Go

//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
}
}