package main import ( "errors" "log" "os/exec" "path/filepath" "regexp" "strings" "imuslab.com/dezukvm/dezukvmd/mod/remdesaux" ) /* Each of the USB-KVM device has the same set of USB devices connected under a single USB hub chip. This function will scan the USB device tree to find the connected USB devices and match them to the configured device paths. Commonly found devices are: - USB hub (the main hub chip) -- USB UART device (HID KVM) -- USB CDC ACM device (auxiliary MCU) -- USB Video Class device (webcam capture) -- USB Audio Class device (audio capture) The AuxMCU will provide a UUID to uniquely identify the USB KVM device subtree. */ type UsbKvmDevice struct { UUID string // 16 bytes UUID obtained from AuxMCU, might change after power cycle USBKVMDevicePath string // e.g. /dev/ttyUSB0 AuxMCUDevicePath string // e.g. /dev/ttyACM0 CaptureDevicePaths []string // e.g. /dev/video0, /dev/video1, etc. AlsaDevicePaths []string // e.g. /dev/snd/pcmC1D0c, etc. } // populateUsbKvmUUID tries to get the UUID from the AuxMCU device func populateUsbKvmUUID(dev *UsbKvmDevice) error { if dev.AuxMCUDevicePath == "" { return nil } // The standard baudrate for AuxMCU is 115200 aux, err := remdesaux.NewAuxOutbandController(dev.AuxMCUDevicePath, 115200) if err != nil { return err } defer aux.Close() uuid, err := aux.GetUUID() if err != nil { return err } dev.UUID = uuid return nil } func discoverUsbKvmSubtree() ([]*UsbKvmDevice, error) { // Scan all /dev/tty*, /dev/video*, /dev/snd/pcmC* devices getMatchingDevs := func(pattern string) ([]string, error) { files, err := filepath.Glob(pattern) if err != nil { return nil, err } return files, nil } // Get all ttyUSB*, ttyACM* ttyDevs1, _ := getMatchingDevs("/dev/ttyUSB*") ttyDevs2, _ := getMatchingDevs("/dev/ttyACM*") ttyDevs := append(ttyDevs1, ttyDevs2...) // Get all video* videoDevs, _ := getMatchingDevs("/dev/video*") // Get all ALSA PCM devices (USB audio is usually card > 0) alsaDevs, _ := getMatchingDevs("/dev/snd/pcmC*") type devInfo struct { path string sysPath string } getSys := func(devs []string) []devInfo { var out []devInfo for _, d := range devs { sys, err := getDeviceFullPath(d) if err == nil { out = append(out, devInfo{d, sys}) } } return out } ttys := getSys(ttyDevs) videos := getSys(videoDevs) alsas := getSys(alsaDevs) // Find common USB root hub prefix hubPattern := regexp.MustCompile(`^\d+-\d+(\.\d+)*$`) getHub := func(sys string) string { parts := strings.Split(sys, "/") for i := range parts { // Look for USB hub pattern (e.g. 1-2, 2-1, etc.) if hubPattern.MatchString(parts[i]) { return strings.Join(parts[:i+1], "/") } } return "" } // Map hub -> device info type hubGroup struct { ttys []string acms []string videos []string alsas []string } hubs := make(map[string]*hubGroup) for _, t := range ttys { hub := getHub(t.sysPath) if hub != "" { if hubs[hub] == nil { hubs[hub] = &hubGroup{} } if strings.Contains(t.path, "ACM") { hubs[hub].acms = append(hubs[hub].acms, t.path) } else { hubs[hub].ttys = append(hubs[hub].ttys, t.path) } } } for _, v := range videos { hub := getHub(v.sysPath) if hub != "" { if hubs[hub] == nil { hubs[hub] = &hubGroup{} } hubs[hub].videos = append(hubs[hub].videos, v.path) } } for _, alsa := range alsas { hub := getHub(alsa.sysPath) if hub != "" { if hubs[hub] == nil { hubs[hub] = &hubGroup{} } hubs[hub].alsas = append(hubs[hub].alsas, alsa.path) } } var result []*UsbKvmDevice for _, g := range hubs { // At least one tty or acm, one video, optionally alsa if (len(g.ttys) > 0 || len(g.acms) > 0) && len(g.videos) > 0 { // Pick the first tty as USBKVMDevicePath, first acm as AuxMCUDevicePath usbKvm := "" auxMcu := "" if len(g.ttys) > 0 { usbKvm = g.ttys[0] } if len(g.acms) > 0 { auxMcu = g.acms[0] } result = append(result, &UsbKvmDevice{ USBKVMDevicePath: usbKvm, AuxMCUDevicePath: auxMcu, CaptureDevicePaths: g.videos, AlsaDevicePaths: g.alsas, }) } } // Populate UUIDs for _, dev := range result { err := populateUsbKvmUUID(dev) if err != nil { log.Printf("Warning: could not get UUID for AuxMCU %s: %v, is this a third party device?", dev.AuxMCUDevicePath, err) } } if len(result) == 0 { return nil, errors.New("no USB KVM device found") } return result, nil } func resolveSymlink(path string) (string, error) { resolved, err := filepath.EvalSymlinks(path) if err != nil { return "", err } return resolved, nil } func getDeviceFullPath(devicePath string) (string, error) { resolvedPath, err := resolveSymlink(devicePath) if err != nil { return "", err } // Use udevadm to get the device chain out, err := exec.Command("udevadm", "info", "-q", "path", "-n", resolvedPath).Output() if err != nil { return "", err } sysPath := strings.TrimSpace(string(out)) if sysPath == "" { return "", errors.New("could not resolve sysfs path") } fullPath := "/sys" + sysPath return fullPath, nil }