GTK+ GUI for configuring PD Buddy Sink devices
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

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)