kvmscan.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package dezukvm
  2. import (
  3. "errors"
  4. "log"
  5. "os/exec"
  6. "path/filepath"
  7. "regexp"
  8. "strings"
  9. "imuslab.com/dezukvm/dezukvmd/mod/kvmaux"
  10. "imuslab.com/dezukvm/dezukvmd/mod/usbcapture"
  11. )
  12. /*
  13. Each of the USB-KVM device has the same set of USB devices
  14. connected under a single USB hub chip. This function
  15. will scan the USB device tree to find the connected
  16. USB devices and match them to the configured device paths.
  17. Commonly found devices are:
  18. - USB hub (the main hub chip)
  19. -- USB UART device (HID KVM)
  20. -- USB CDC ACM device (auxiliary MCU)
  21. -- USB Video Class device (webcam capture)
  22. -- USB Audio Class device (audio capture)
  23. The AuxMCU will provide a UUID to uniquely identify
  24. the USB KVM device subtree.
  25. */
  26. type UsbKvmDevice struct {
  27. UUID string // 16 bytes UUID obtained from AuxMCU, might change after power cycle
  28. USBKVMDevicePath string // e.g. /dev/ttyUSB0
  29. AuxMCUDevicePath string // e.g. /dev/ttyACM0
  30. CaptureDevicePaths []string // e.g. /dev/video0, /dev/video1, etc.
  31. AlsaDevicePaths []string // e.g. /dev/snd/pcmC1D0c, etc.
  32. }
  33. // ScanConnectedUsbKvmDevices scans and lists all connected USB KVM devices in the system.
  34. func ScanConnectedUsbKvmDevices() ([]*UsbKvmDeviceOption, error) {
  35. possibleKvmDeviceGroup, err := DiscoverUsbKvmSubtree()
  36. if err != nil {
  37. return nil, err
  38. }
  39. if len(possibleKvmDeviceGroup) == 0 {
  40. return nil, errors.New("no USB KVM devices found")
  41. }
  42. result := []*UsbKvmDeviceOption{}
  43. for _, dev := range possibleKvmDeviceGroup {
  44. option := &UsbKvmDeviceOption{
  45. USBKVMDevicePath: dev.USBKVMDevicePath,
  46. AuxMCUDevicePath: dev.AuxMCUDevicePath,
  47. VideoCaptureDevicePath: "",
  48. AudioCaptureDevicePath: "",
  49. }
  50. for _, videoPath := range dev.CaptureDevicePaths {
  51. isCaptureCard := usbcapture.IsCaptureCardVideoInterface(videoPath)
  52. if isCaptureCard {
  53. option.VideoCaptureDevicePath = videoPath
  54. }
  55. }
  56. // In theory one capture card shd only got 1 alsa audio device file
  57. if len(dev.AlsaDevicePaths) > 0 {
  58. option.AudioCaptureDevicePath = dev.AlsaDevicePaths[0] // Use the first audio device by default
  59. }
  60. result = append(result, option)
  61. }
  62. return result, nil
  63. }
  64. // populateUsbKvmUUID tries to get the UUID from the AuxMCU device
  65. func populateUsbKvmUUID(dev *UsbKvmDevice) error {
  66. if dev.AuxMCUDevicePath == "" {
  67. return nil
  68. }
  69. // The standard baudrate for AuxMCU is 115200
  70. aux, err := kvmaux.NewAuxOutbandController(dev.AuxMCUDevicePath, 115200)
  71. if err != nil {
  72. return err
  73. }
  74. defer aux.Close()
  75. uuid, err := aux.GetUUID()
  76. if err != nil {
  77. return err
  78. }
  79. dev.UUID = uuid
  80. return nil
  81. }
  82. func DiscoverUsbKvmSubtree() ([]*UsbKvmDevice, error) {
  83. // Scan all /dev/tty*, /dev/video*, /dev/snd/pcmC* devices
  84. getMatchingDevs := func(pattern string) ([]string, error) {
  85. files, err := filepath.Glob(pattern)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return files, nil
  90. }
  91. // Get all ttyUSB*, ttyACM*
  92. ttyDevs1, _ := getMatchingDevs("/dev/ttyUSB*")
  93. ttyDevs2, _ := getMatchingDevs("/dev/ttyACM*")
  94. ttyDevs := append(ttyDevs1, ttyDevs2...)
  95. // Get all video*
  96. videoDevs, _ := getMatchingDevs("/dev/video*")
  97. // Get all ALSA PCM devices (USB audio is usually card > 0)
  98. alsaDevs, _ := getMatchingDevs("/dev/snd/pcmC*")
  99. type devInfo struct {
  100. path string
  101. sysPath string
  102. }
  103. getSys := func(devs []string) []devInfo {
  104. var out []devInfo
  105. for _, d := range devs {
  106. sys, err := getDeviceFullPath(d)
  107. if err == nil {
  108. out = append(out, devInfo{d, sys})
  109. }
  110. }
  111. return out
  112. }
  113. ttys := getSys(ttyDevs)
  114. videos := getSys(videoDevs)
  115. alsas := getSys(alsaDevs)
  116. // Find common USB root hub prefix
  117. hubPattern := regexp.MustCompile(`^\d+-\d+(\.\d+)*$`)
  118. getHub := func(sys string) string {
  119. parts := strings.Split(sys, "/")
  120. for i := range parts {
  121. // Look for USB hub pattern (e.g. 1-2, 2-1, etc.)
  122. if hubPattern.MatchString(parts[i]) {
  123. return strings.Join(parts[:i+1], "/")
  124. }
  125. }
  126. return ""
  127. }
  128. // Map hub -> device info
  129. type hubGroup struct {
  130. ttys []string
  131. acms []string
  132. videos []string
  133. alsas []string
  134. }
  135. hubs := make(map[string]*hubGroup)
  136. for _, t := range ttys {
  137. hub := getHub(t.sysPath)
  138. if hub != "" {
  139. if hubs[hub] == nil {
  140. hubs[hub] = &hubGroup{}
  141. }
  142. if strings.Contains(t.path, "ACM") {
  143. hubs[hub].acms = append(hubs[hub].acms, t.path)
  144. } else {
  145. hubs[hub].ttys = append(hubs[hub].ttys, t.path)
  146. }
  147. }
  148. }
  149. for _, v := range videos {
  150. hub := getHub(v.sysPath)
  151. if hub != "" {
  152. if hubs[hub] == nil {
  153. hubs[hub] = &hubGroup{}
  154. }
  155. hubs[hub].videos = append(hubs[hub].videos, v.path)
  156. }
  157. }
  158. for _, alsa := range alsas {
  159. hub := getHub(alsa.sysPath)
  160. if hub != "" {
  161. if hubs[hub] == nil {
  162. hubs[hub] = &hubGroup{}
  163. }
  164. hubs[hub].alsas = append(hubs[hub].alsas, alsa.path)
  165. }
  166. }
  167. var result []*UsbKvmDevice
  168. for _, g := range hubs {
  169. // At least one tty or acm, one video, optionally alsa
  170. if (len(g.ttys) > 0 || len(g.acms) > 0) && len(g.videos) > 0 {
  171. // Pick the first tty as USBKVMDevicePath, first acm as AuxMCUDevicePath
  172. usbKvm := ""
  173. auxMcu := ""
  174. if len(g.ttys) > 0 {
  175. usbKvm = g.ttys[0]
  176. }
  177. if len(g.acms) > 0 {
  178. auxMcu = g.acms[0]
  179. }
  180. result = append(result, &UsbKvmDevice{
  181. USBKVMDevicePath: usbKvm,
  182. AuxMCUDevicePath: auxMcu,
  183. CaptureDevicePaths: g.videos,
  184. AlsaDevicePaths: g.alsas,
  185. })
  186. }
  187. }
  188. // Populate UUIDs
  189. for _, dev := range result {
  190. err := populateUsbKvmUUID(dev)
  191. if err != nil {
  192. log.Printf("Warning: could not get UUID for AuxMCU %s: %v, is this a third party device?", dev.AuxMCUDevicePath, err)
  193. }
  194. }
  195. if len(result) == 0 {
  196. return nil, errors.New("no USB KVM device found")
  197. }
  198. return result, nil
  199. }
  200. func resolveSymlink(path string) (string, error) {
  201. resolved, err := filepath.EvalSymlinks(path)
  202. if err != nil {
  203. return "", err
  204. }
  205. return resolved, nil
  206. }
  207. func getDeviceFullPath(devicePath string) (string, error) {
  208. resolvedPath, err := resolveSymlink(devicePath)
  209. if err != nil {
  210. return "", err
  211. }
  212. // Use udevadm to get the device chain
  213. out, err := exec.Command("udevadm", "info", "-q", "path", "-n", resolvedPath).Output()
  214. if err != nil {
  215. return "", err
  216. }
  217. sysPath := strings.TrimSpace(string(out))
  218. if sysPath == "" {
  219. return "", errors.New("could not resolve sysfs path")
  220. }
  221. fullPath := "/sys" + sysPath
  222. return fullPath, nil
  223. }