kvmscan.go 5.1 KB

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