232 lines
4.9 KiB
Go
232 lines
4.9 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)
|
|
}
|
|
|
|
// 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)
|
|
addr, _ := strconv.ParseUint(readString(epPath, "bEndpointAddress"), 16, 8)
|
|
|
|
var transferType uint8
|
|
switch readString(epPath, "type") {
|
|
case "Control":
|
|
transferType = TransferTypeControl
|
|
case "Isoc":
|
|
transferType = TransferTypeIsochronous
|
|
case "Bulk":
|
|
transferType = TransferTypeBulk
|
|
case "Interrupt":
|
|
transferType = TransferTypeInterrupt
|
|
}
|
|
|
|
maxPkt := readUint32(epPath, "wMaxPacketSize")
|
|
|
|
eps = append(eps, Endpoint{
|
|
Address: uint8(addr),
|
|
TransferType: transferType,
|
|
MaxPacketSize: uint16(maxPkt),
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|