GTK+ GUI for configuring PD Buddy Sink devices
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #!/usr/bin/env python3
  2. import sys
  3. import pdbuddy
  4. import gi
  5. gi.require_version('Gtk', '3.0')
  6. from gi.repository import Gtk, Gio, GObject, GLib
  7. def comms_error_dialog(parent, e):
  8. dialog = Gtk.MessageDialog(window, 0, Gtk.MessageType.ERROR,
  9. Gtk.ButtonsType.CLOSE, "Error communicating with device")
  10. dialog.format_secondary_text(e.strerror)
  11. dialog.run()
  12. dialog.destroy()
  13. class ListRowModel(GObject.GObject):
  14. def __init__(self, serport):
  15. GObject.GObject.__init__(self)
  16. self.serport = serport
  17. class SelectListStore(Gio.ListStore):
  18. def update_items(self):
  19. # Get a list of serial ports
  20. serports = list(pdbuddy.Sink.get_devices())
  21. # Mark ports to remove or add
  22. remove_list = []
  23. list_len = self.get_n_items()
  24. for i in range(list_len):
  25. remove = True
  26. for j in range(len(serports)):
  27. if serports[j] is not None and self.get_item(i).serport == serports[j]:
  28. serports[j] = None
  29. remove = False
  30. if remove:
  31. remove_list.append(i)
  32. # Remove the missing ones
  33. for i in remove_list:
  34. self.remove(i)
  35. # Add any new ports
  36. for port in serports:
  37. if port is not None:
  38. self.append(ListRowModel(port))
  39. class SelectList(Gtk.Box):
  40. __gsignals__ = {
  41. 'row-activated': (GObject.SIGNAL_RUN_FIRST, None,
  42. (object,))
  43. }
  44. def __init__(self):
  45. Gtk.Box.__init__(self)
  46. self._model = None
  47. self._builder = Gtk.Builder()
  48. self._builder.add_from_file("data/select-stack.ui")
  49. self._builder.connect_signals(self)
  50. sl = self._builder.get_object("select-list")
  51. # Add separators to the list
  52. sl.set_header_func(self._update_header_func, None)
  53. self.pack_start(self._builder.get_object("select-stack"), True, True, 0)
  54. self.show_all()
  55. def _update_header_func(self, row, before, data):
  56. """Add a separator header to all rows but the first one"""
  57. if before is None:
  58. row.set_header(None)
  59. return
  60. current = row.get_header()
  61. if current is None:
  62. current = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
  63. row.set_header(current)
  64. def bind_model(self, model, func):
  65. self._builder.get_object("select-list").bind_model(model, func)
  66. self._model = model
  67. self.reload()
  68. GLib.timeout_add(1000, self.reload)
  69. def reload(self):
  70. self._model.update_items()
  71. # Set the visible child
  72. stack = self._builder.get_object("select-stack")
  73. if self._model.get_n_items():
  74. stack.set_visible_child(self._builder.get_object("select-frame"))
  75. else:
  76. stack.set_visible_child(self._builder.get_object("select-none"))
  77. return True
  78. def on_select_list_row_activated(self, box, row):
  79. self.emit("row-activated", row.model.serport)
  80. class SelectListRow(Gtk.ListBoxRow):
  81. def __init__(self, model):
  82. Gtk.EventBox.__init__(self)
  83. self.model = model
  84. self._builder = Gtk.Builder()
  85. self._builder.add_from_file("data/select-list-row.ui")
  86. self._builder.connect_signals(self)
  87. name = self._builder.get_object("name")
  88. name.set_text('{} {}'.format(self.model.serport.manufacturer,
  89. self.model.serport.product))
  90. device = self._builder.get_object("device")
  91. device.set_text(self.model.serport.device)
  92. self.add(self._builder.get_object("grid"))
  93. self.show_all()
  94. def on_identify_clicked(self, button):
  95. window = self.get_toplevel()
  96. try:
  97. with pdbuddy.Sink(self.model.serport) as pdbs:
  98. pdbs.identify()
  99. except OSError as e:
  100. comms_error_dialog(window, e)
  101. return
  102. class Handler:
  103. def __init__(self, builder):
  104. self.builder = builder
  105. self.serial_port = None
  106. self.voltage = None
  107. self.current = None
  108. self.giveback = None
  109. self.selectlist = None
  110. def on_pdb_window_realize(self, *args):
  111. # Get the list
  112. sb = self.builder.get_object("select-box")
  113. self.selectlist = SelectList()
  114. sb.pack_start(self.selectlist, True, True, 0)
  115. liststore = SelectListStore()
  116. self.selectlist.bind_model(liststore, SelectListRow)
  117. self.selectlist.connect("row-activated", self.on_select_list_row_activated)
  118. def on_pdb_window_delete_event(self, *args):
  119. Gtk.main_quit(*args)
  120. def on_select_list_row_activated(self, selectlist, serport):
  121. # Get voltage and current widgets
  122. voltage = self.builder.get_object("voltage-combobox")
  123. current = self.builder.get_object("current-spinbutton")
  124. giveback = self.builder.get_object("giveback-toggle")
  125. self.serial_port = serport
  126. window = self.builder.get_object("pdb-window")
  127. try:
  128. with pdbuddy.Sink(self.serial_port) as pdbs:
  129. pdbs.load()
  130. tmpcfg = pdbs.get_tmpcfg()
  131. except OSError as e:
  132. comms_error_dialog(window, e)
  133. return
  134. # Set giveback button state
  135. giveback.set_active(bool(tmpcfg.flags & pdbuddy.SinkFlags.GIVEBACK))
  136. # Get voltage and current from device and load them into the GUI
  137. if tmpcfg.v == 5000:
  138. voltage.set_active_id('voltage-five')
  139. elif tmpcfg.v == 9000:
  140. voltage.set_active_id('voltage-nine')
  141. elif tmpcfg.v == 15000:
  142. voltage.set_active_id('voltage-fifteen')
  143. if tmpcfg.v == 20000:
  144. voltage.set_active_id('voltage-twenty')
  145. current.set_value(tmpcfg.i/1000)
  146. self._store_device_settings()
  147. self._set_save_button_visibility()
  148. # Show the Sink page
  149. hst = self.builder.get_object("header-stack")
  150. hsink = self.builder.get_object("header-sink")
  151. hsink.set_title('{} {}'.format(serport.manufacturer, serport.product))
  152. hsink.set_subtitle(serport.device)
  153. hst.set_visible_child(hsink)
  154. st = self.builder.get_object("stack")
  155. sink = self.builder.get_object("sink")
  156. st.set_visible_child(sink)
  157. # Ping the Sink repeatedly
  158. GLib.timeout_add(1000, self._ping)
  159. def _ping(self):
  160. """Ping the device we're configuring, showing to the list on failure"""
  161. if self.serial_port is None:
  162. self.selectlist.reload()
  163. self.on_header_sink_back_clicked(None)
  164. return False
  165. try:
  166. with pdbuddy.Sink(self.serial_port) as pdbs:
  167. pdbs.send_command("")
  168. return True
  169. except:
  170. self.selectlist.reload()
  171. self.on_header_sink_back_clicked(None)
  172. return False
  173. def on_header_sink_back_clicked(self, data):
  174. self.serial_port = None
  175. # Show the Select page
  176. hst = self.builder.get_object("header-stack")
  177. hselect = self.builder.get_object("header-select")
  178. hst.set_visible_child(hselect)
  179. st = self.builder.get_object("stack")
  180. select = self.builder.get_object("select")
  181. st.set_visible_child(select)
  182. def on_header_sink_save_clicked(self, button):
  183. window = self.builder.get_object("pdb-window")
  184. try:
  185. with pdbuddy.Sink(self.serial_port) as pdbs:
  186. pdbs.write()
  187. self._store_device_settings()
  188. self._set_save_button_visibility()
  189. except OSError as e:
  190. comms_error_dialog(window, e)
  191. self.on_header_sink_back_clicked(None)
  192. def _store_device_settings(self):
  193. """Store the settings that were loaded from the device"""
  194. # Get voltage and current widgets
  195. voltage = self.builder.get_object("voltage-combobox")
  196. current = self.builder.get_object("current-spinbutton")
  197. giveback = self.builder.get_object("giveback-toggle")
  198. # Remember the loaded settings
  199. self.voltage = voltage.get_active_id()
  200. self.current = current.get_value()
  201. self.giveback = giveback.get_active()
  202. def _set_save_button_visibility(self):
  203. """Show the save button if there are new settings to save"""
  204. # Get relevant widgets
  205. voltage = self.builder.get_object("voltage-combobox")
  206. current = self.builder.get_object("current-spinbutton")
  207. giveback = self.builder.get_object("giveback-toggle")
  208. rev = self.builder.get_object("header-sink-save-revealer")
  209. # Set visibility
  210. rev.set_reveal_child(voltage.get_active_id() != self.voltage
  211. or current.get_value() != self.current
  212. or giveback.get_active() != self.giveback)
  213. def on_voltage_combobox_changed(self, combo):
  214. window = self.builder.get_object("pdb-window")
  215. try:
  216. with pdbuddy.Sink(self.serial_port) as pdbs:
  217. pdbs.set_v(int(combo.get_active_text())*1000)
  218. self._set_save_button_visibility()
  219. except OSError as e:
  220. comms_error_dialog(window, e)
  221. self.on_header_sink_back_clicked(None)
  222. def on_current_spinbutton_changed(self, spin):
  223. window = self.builder.get_object("pdb-window")
  224. try:
  225. with pdbuddy.Sink(self.serial_port) as pdbs:
  226. pdbs.set_i(int(spin.get_value())*1000)
  227. self._set_save_button_visibility()
  228. except OSError as e:
  229. comms_error_dialog(window, e)
  230. self.on_header_sink_back_clicked(None)
  231. def on_giveback_toggle_toggled(self, toggle):
  232. window = self.builder.get_object("pdb-window")
  233. try:
  234. with pdbuddy.Sink(self.serial_port) as pdbs:
  235. pdbs.toggle_giveback()
  236. self._set_save_button_visibility()
  237. except OSError as e:
  238. comms_error_dialog(window, e)
  239. self.on_header_sink_back_clicked(None)
  240. class Application(Gtk.Application):
  241. def __init__(self, *args, **kwargs):
  242. super().__init__(*args, application_id="com.clayhobbs.pd-buddy-gtk",
  243. **kwargs)
  244. self.window = None
  245. def do_startup(self):
  246. Gtk.Application.do_startup(self)
  247. self.builder = Gtk.Builder()
  248. self.builder.add_from_file("data/pd-buddy-gtk.ui")
  249. self.builder.connect_signals(Handler(self.builder))
  250. def do_activate(self):
  251. # We only allow a single window and raise any existing ones
  252. if not self.window:
  253. # Windows are associated with the application
  254. # when the last one is closed the application shuts down
  255. self.window = self.builder.get_object("pdb-window")
  256. self.add_window(self.window)
  257. self.window.set_wmclass("PD Buddy Configuration",
  258. "PD Buddy Configuration")
  259. self.window.present()
  260. def run():
  261. app = Application()
  262. app.run(sys.argv)
  263. if __name__ == "__main__":
  264. run()