device_tester.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. #!/usr/bin/env python
  2. import sys
  3. from direct.showbase.ShowBase import ShowBase
  4. from direct.showbase.DirectObject import DirectObject
  5. from panda3d.core import InputDeviceManager, InputDevice
  6. from panda3d.core import VBase4, Vec2
  7. from panda3d.core import TextNode
  8. from direct.gui.DirectGui import (
  9. DGG,
  10. DirectFrame,
  11. DirectButton,
  12. DirectLabel,
  13. DirectScrolledFrame,
  14. DirectSlider,
  15. )
  16. class Main(ShowBase):
  17. def __init__(self):
  18. super().__init__()
  19. base.disableMouse()
  20. self.accept("escape", sys.exit)
  21. self.device_connectivity_monitor = DeviceConnectivityMonitor()
  22. class DeviceConnectivityMonitor(DirectObject):
  23. def __init__(self):
  24. super().__init__()
  25. self.mgr = InputDeviceManager.get_global_ptr()
  26. self.create_device_menu()
  27. self.devices = {}
  28. for device in self.mgr.get_devices():
  29. self.connect_device(device)
  30. self.accept("connect-device", self.connect_device)
  31. self.accept("disconnect-device", self.disconnect_device)
  32. def create_device_menu(self):
  33. self.current_panel = None
  34. self.buttons = {}
  35. self.devices_frame = DirectScrolledFrame(
  36. frameSize=VBase4(
  37. 0,
  38. base.a2dLeft*-0.75,
  39. base.a2dBottom - base.a2dTop,
  40. 0,
  41. ),
  42. frameColor=VBase4(0, 0, 0.25, 1.0),
  43. canvasSize=VBase4(
  44. 0,
  45. base.a2dLeft*-0.75,
  46. 0,
  47. 0,
  48. ),
  49. scrollBarWidth=0.08,
  50. manageScrollBars=True,
  51. autoHideScrollBars=True,
  52. pos=(base.a2dLeft, 0, base.a2dTop),
  53. parent=base.aspect2d,
  54. )
  55. self.devices_frame.setCanvasSize()
  56. def create_menu_button(self, device):
  57. button = DirectButton(
  58. command=self.switch_to_panel,
  59. extraArgs=[device],
  60. text=device.name,
  61. text_scale=0.05,
  62. text_align=TextNode.ALeft,
  63. text_fg=VBase4(0.0, 0.0, 0.0, 1.0),
  64. text_pos=Vec2(0.01, base.a2dBottom / 10.0),
  65. relief=1,
  66. pad=Vec2(0.01, 0.01),
  67. frameColor=VBase4(0.8, 0.8, 0.8, 1.0),
  68. frameSize=VBase4(
  69. 0.0,
  70. base.a2dLeft*-0.75 - 0.081, # 0.08=Scrollbar, 0.001=inaccuracy
  71. base.a2dBottom / 5.0,
  72. 0.0,
  73. ),
  74. parent=self.devices_frame.getCanvas(),
  75. )
  76. self.buttons[device] = button
  77. def destroy_menu_button(self, device):
  78. self.buttons[device].detach_node()
  79. del self.buttons[device]
  80. def refresh_device_menu(self):
  81. self.devices_frame['canvasSize'] = VBase4(
  82. 0,
  83. base.a2dLeft*-0.75,
  84. base.a2dBottom / 5.0 * len(self.buttons),
  85. 0,
  86. )
  87. self.devices_frame.setCanvasSize()
  88. sorted_buttons = sorted(self.buttons.items(), key=lambda i: i[0].name)
  89. for idx, (dev, button) in enumerate(sorted_buttons):
  90. button.set_pos(
  91. 0,
  92. 0,
  93. (base.a2dBottom / 5.0) * idx,
  94. )
  95. def switch_to_panel(self, device):
  96. if self.current_panel is not None:
  97. self.devices[self.current_panel].hide()
  98. self.current_panel = device
  99. self.devices[self.current_panel].show()
  100. def connect_device(self, device):
  101. if device in self.devices:
  102. return
  103. self.devices[device] = DeviceMonitor(device)
  104. self.switch_to_panel(device)
  105. self.create_menu_button(device)
  106. self.refresh_device_menu()
  107. def disconnect_device(self, device):
  108. self.devices[device].deactivate()
  109. del self.devices[device]
  110. if self.current_panel == device:
  111. self.current_panel = None
  112. if len(self.devices) > 0:
  113. active_device = sorted(
  114. self.devices.keys(),
  115. key=lambda d: d.name,
  116. )[0]
  117. self.switch_to_panel(active_device)
  118. self.destroy_menu_button(device)
  119. self.refresh_device_menu()
  120. class DeviceMonitor(DirectObject):
  121. def __init__(self, device):
  122. super().__init__()
  123. self.device = device
  124. self.create_panel()
  125. self.activate()
  126. self.hide()
  127. def activate(self):
  128. print("Device connected")
  129. print(" Name : {}".format(self.device.name))
  130. print(" Type : {}".format(self.device.device_class.name))
  131. print(" Manufacturer: {}".format(self.device.manufacturer))
  132. print(" ID : {:04x}:{:04x}".format(self.device.vendor_id,
  133. self.device.product_id))
  134. axis_names = [axis.axis.name for axis in self.device.axes]
  135. print(" Axes : {} ({})".format(len(self.device.axes),
  136. ', '.join(axis_names)))
  137. button_names = [button.handle.name for button in self.device.buttons]
  138. print(" Buttons : {} ({})".format(len(self.device.buttons),
  139. ', '.join(button_names)))
  140. base.attachInputDevice(self.device)
  141. self.task = base.taskMgr.add(
  142. self.update,
  143. "Monitor for {}".format(self.device.name),
  144. sort=10,
  145. )
  146. def deactivate(self):
  147. print("\"{}\" disconnected".format(self.device.name))
  148. base.taskMgr.remove(self.task)
  149. self.panel.detach_node()
  150. def create_panel(self):
  151. panel_width = base.a2dLeft * -0.25 + base.a2dRight
  152. scroll_bar_width = 0.08
  153. # NOTE: -0.001 because thanks to inaccuracy the vertical bar appears...
  154. canvas_width = panel_width - scroll_bar_width - 0.001
  155. canvas_height = base.a2dBottom - base.a2dTop
  156. self.panel = DirectScrolledFrame(
  157. frameSize=VBase4(
  158. 0,
  159. panel_width,
  160. canvas_height,
  161. 0,
  162. ),
  163. frameColor=VBase4(0.8, 0.8, 0.8, 1),
  164. canvasSize=VBase4(
  165. 0,
  166. canvas_width,
  167. canvas_height,
  168. 0,
  169. ),
  170. scrollBarWidth=scroll_bar_width,
  171. manageScrollBars=True,
  172. autoHideScrollBars=True,
  173. pos=(base.a2dLeft * 0.25, 0, base.a2dTop),
  174. parent=base.aspect2d,
  175. )
  176. panel_canvas = self.panel.getCanvas()
  177. offset = -0.0
  178. # Style sheets
  179. half_width_entry = dict(
  180. frameSize=VBase4(
  181. 0,
  182. canvas_width / 2,
  183. -0.1,
  184. 0,
  185. ),
  186. parent=panel_canvas,
  187. frameColor=VBase4(0.8, 0.8, 0.8, 1),
  188. )
  189. left_aligned_small_text = dict(
  190. text_align=TextNode.ALeft,
  191. text_scale=0.05,
  192. text_fg=VBase4(0,0,0,1),
  193. text_pos=(0.05, -0.06),
  194. )
  195. half_width_text_frame = dict(
  196. **half_width_entry,
  197. **left_aligned_small_text,
  198. )
  199. header = dict(
  200. frameSize=VBase4(
  201. 0,
  202. canvas_width,
  203. -0.1,
  204. 0,
  205. ),
  206. parent=panel_canvas,
  207. frameColor=VBase4(0.6, 0.6, 0.6, 1),
  208. text_align=TextNode.ALeft,
  209. text_scale=0.1,
  210. text_fg=VBase4(0,0,0,1),
  211. text_pos=(0.05, -0.075),
  212. )
  213. # Basic device data (name, device class, manufacturer, USB ID)
  214. self.device_header = DirectLabel(
  215. text="Device data",
  216. pos=(0, 0, offset),
  217. **header,
  218. )
  219. offset -= 0.1
  220. def add_data_entry(offset, label, text):
  221. self.name = DirectLabel(
  222. text=label,
  223. pos=(0, 0, offset),
  224. **half_width_text_frame,
  225. )
  226. self.name = DirectLabel(
  227. text=text,
  228. pos=(canvas_width / 2, 0, offset),
  229. **half_width_text_frame,
  230. )
  231. metadata = [
  232. ('Name', self.device.name),
  233. ('Device class', self.device.device_class.name),
  234. ('Manufacturer', self.device.manufacturer),
  235. ('USB ID',
  236. "{:04x}:{:04x}".format(
  237. self.device.vendor_id,
  238. self.device.product_id,
  239. ),
  240. ),
  241. ]
  242. for label, text in metadata:
  243. add_data_entry(offset, label, text)
  244. offset -= 0.1
  245. # Axes
  246. self.axis_sliders = []
  247. if len(self.device.axes) > 0:
  248. offset -= 0.1
  249. self.axes_header = DirectLabel(
  250. text="Axes",
  251. pos=(0, 0, offset),
  252. **header,
  253. )
  254. offset -= 0.1
  255. def add_axis(offset, axis_name):
  256. slider_width = canvas_width / 2
  257. label = DirectLabel(
  258. text=axis_name,
  259. **left_aligned_small_text,
  260. pos=(0.05, 0, offset),
  261. parent=panel_canvas,
  262. )
  263. slider = DirectSlider(
  264. value=0.0,
  265. range=(-1.0, 1.0),
  266. state=DGG.DISABLED,
  267. frameSize=VBase4(
  268. 0,
  269. slider_width,
  270. -0.1,
  271. 0,
  272. ),
  273. thumb_frameSize=VBase4(
  274. 0.0,
  275. 0.04,
  276. -0.04,
  277. 0.04),
  278. frameColor=VBase4(0.3, 0.3, 0.3, 1),
  279. pos=(canvas_width - slider_width, 0, offset),
  280. parent=panel_canvas,
  281. )
  282. return slider
  283. for axis in self.device.axes:
  284. axis_slider = add_axis(offset, axis.axis.name)
  285. self.axis_sliders.append(axis_slider)
  286. offset -= 0.1
  287. # Buttons
  288. self.button_buttons = []
  289. if len(self.device.buttons) > 0:
  290. offset -= 0.1
  291. self.buttons_header = DirectLabel(
  292. text="Buttons",
  293. pos=(0, 0, offset),
  294. **header,
  295. )
  296. offset -= 0.1
  297. def add_button(offset, button_name):
  298. button_width = canvas_width / 2
  299. label = DirectLabel(
  300. text=button_name,
  301. **left_aligned_small_text,
  302. pos=(0.05, 0, offset),
  303. parent=panel_canvas,
  304. )
  305. button = DirectFrame(
  306. frameSize=VBase4(
  307. 0,
  308. button_width,
  309. -0.1,
  310. 0,
  311. ),
  312. text="",
  313. text_align=TextNode.ACenter,
  314. text_scale=0.05,
  315. text_fg=VBase4(0,0,0,1),
  316. text_pos=(button_width / 2, -0.06),
  317. frameColor=VBase4(0.3, 0.3, 0.3, 1),
  318. pos=(canvas_width - button_width, 0, offset),
  319. parent=panel_canvas,
  320. )
  321. return button
  322. for i in range(len(self.device.buttons)):
  323. button_name = self.device.buttons[i].handle.name
  324. button_button = add_button(offset, button_name)
  325. self.button_buttons.append(button_button)
  326. offset -= 0.1
  327. # Vibration
  328. self.vibration = []
  329. if self.device.has_feature(InputDevice.Feature.vibration):
  330. offset -= 0.1
  331. self.vibration_header = DirectLabel(
  332. text="Vibration",
  333. pos=(0, 0, offset),
  334. **header,
  335. )
  336. offset -= 0.1
  337. def add_vibration(offset, axis_name, index):
  338. slider_width = canvas_width / 2
  339. label = DirectLabel(
  340. text=axis_name,
  341. **left_aligned_small_text,
  342. pos=(0.05, 0, offset),
  343. parent=panel_canvas,
  344. )
  345. slider = DirectSlider(
  346. value=0.0,
  347. range=(0.0, 1.0),
  348. command=self.update_vibration,
  349. frameSize=VBase4(
  350. 0,
  351. slider_width,
  352. -0.1,
  353. 0,
  354. ),
  355. thumb_frameSize=VBase4(
  356. 0.0,
  357. 0.04,
  358. -0.04,
  359. 0.04),
  360. frameColor=VBase4(0.3, 0.3, 0.3, 1),
  361. pos=(canvas_width - slider_width, 0, offset),
  362. parent=panel_canvas,
  363. )
  364. return slider
  365. for index, name in enumerate(["low frequency", "high frequency"]):
  366. self.vibration.append(add_vibration(offset, name, index))
  367. offset -= 0.1
  368. # Resize the panel's canvas to the widgets actually in it.
  369. if -offset > -canvas_height:
  370. self.panel['canvasSize'] = VBase4(
  371. 0,
  372. canvas_width,
  373. offset,
  374. 0,
  375. )
  376. self.panel.setCanvasSize()
  377. def show(self):
  378. # FIXME: Activate update task here, and deactivate it in hide()?
  379. self.panel.show()
  380. def hide(self):
  381. self.panel.hide()
  382. def update_vibration(self):
  383. low = self.vibration[0]['value']
  384. high = self.vibration[1]['value']
  385. self.device.set_vibration(low, high)
  386. def update(self, task):
  387. # FIXME: There needs to be a demo of events here, too.
  388. for idx, slider in enumerate(self.axis_sliders):
  389. slider["value"] = self.device.axes[idx].value
  390. for idx, button in enumerate(self.button_buttons):
  391. if self.device.buttons[idx].known:
  392. if self.device.buttons[idx].pressed:
  393. button['frameColor'] = VBase4(0.0, 0.8, 0.0, 1)
  394. button['text'] = "down"
  395. else:
  396. button['frameColor'] = VBase4(0.3, 0.3, 0.3, 1)
  397. button['text'] = "up"
  398. else:
  399. # State is InputDevice.S_unknown. This happens if the device
  400. # manager hasn't polled yet, and in some cases before a button
  401. # has been pressed after the program's start.
  402. button['frameColor'] = VBase4(0.8, 0.8, 0.0, 1)
  403. button['text'] = "unknown"
  404. return task.cont
  405. if __name__ == '__main__':
  406. main = Main()
  407. main.run()