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.

pd-buddy-gtk.py 11KB

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