application.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. from io import StringIO
  2. from gi.repository import GObject
  3. from gi.repository import Gdk
  4. from gi.repository import Gtk
  5. from gi.repository import GtkFlow
  6. import pinmagik.nodes
  7. from pinmagik.nodes.source import Source
  8. from pinmagik.raspi import RaspiContext, RaspiInNode, RaspiOutNode
  9. import json
  10. # Placeholder function for gettext
  11. def _(string):
  12. return string
  13. try:
  14. import RPi.GPIO as GPIO
  15. IS_REAL_RASPI = True
  16. except ImportError:
  17. IS_REAL_RASPI = False
  18. PD_FIELD_ID = 0
  19. PD_FIELD_NAME = 1
  20. PD_FIELD_HUMAN_NAME = 2
  21. PROJECT_TYPES = {
  22. "raspi" : (0x01, "raspi", _("Raspberry Pi Model A/B")),
  23. "raspi_plus" : (0x02, "raspi_plus", _("Raspberry Pi Model A+/B+")),
  24. }
  25. class Compiler(object):
  26. FRAME = """#!/usr/bin/python3
  27. #
  28. # *** Generated by Pinmagic ***
  29. #
  30. from sys import exit
  31. from time import sleep
  32. import RPi.GPIO as GPIO
  33. def init():
  34. GPIO.setmode(GPIO.BCM)
  35. %(INIT)s
  36. def loopstep():
  37. %(LOOP)s
  38. init()
  39. if not __name__ == "__main__":
  40. exit(0)
  41. try:
  42. while True:
  43. sleep(0.01)
  44. loopstep()
  45. except KeyboardInterrupt:
  46. GPIO.cleanup()
  47. """
  48. def __init__(self, project):
  49. self._project = project
  50. self._visited_nodes_init = []
  51. self._visited_nodes_loop = []
  52. self._init_buffer = StringIO()
  53. self._loop_buffer = StringIO()
  54. def compile(self):
  55. self._visited_nodes_init = []
  56. self._visited_nodes_loop = []
  57. self._init_buffer = StringIO()
  58. self._loop_buffer = StringIO()
  59. start_node = self._project.get_output_node()
  60. start_node.generate_raspi_init(self)
  61. start_node.generate_raspi_loop(self)
  62. code = Compiler.FRAME%{"INIT": self._init_buffer.getvalue(),
  63. "LOOP": self._loop_buffer.getvalue()}
  64. return code
  65. def get_init_buffer(self):
  66. return self._init_buffer
  67. def get_loop_buffer(self):
  68. return self._loop_buffer
  69. def rendered_as_init(self, node):
  70. return node in self._visited_nodes_init
  71. def rendered_as_loop(self, node):
  72. return node in self._visited_nodes_loop
  73. def set_rendered_init(self, node):
  74. self._visited_nodes_init.append(node)
  75. def set_rendered_loop(self, node):
  76. self._visited_nodes_loop.append(node)
  77. class Serializer(object):
  78. def __init__(self, project):
  79. self._project = project
  80. self._visited_nodes = []
  81. self._remaining_nodes = project.get_nodes()
  82. self._serialized_nodes = []
  83. def serialize_node(self,node):
  84. cons = []
  85. sinks = node.get_sinks()
  86. for sink in sinks:
  87. try:
  88. source = sink.get_sources()[0]
  89. except IndexError:
  90. continue
  91. targetnode = source.get_node()
  92. sources = targetnode.get_sources()
  93. cons.append((sinks.index(sink), id(targetnode), sources.index(source)))
  94. alloc = PinMagic.INSTANCE.nodeview.get_node_allocation(node)
  95. return {
  96. "clsid": node.__class__.ID,
  97. "x": alloc.x,
  98. "y": alloc.y,
  99. "node_info": {},
  100. "id": id(node),
  101. "connections": cons
  102. }
  103. def serialize(self):
  104. startnode = self._project.get_output_node()
  105. startnode.serialize(self)
  106. while len(self._remaining_nodes) > 0:
  107. startnode = self._remaining_nodes[0]
  108. startnode.serialize(self)
  109. return json.dumps({
  110. "type":self._project.get_type(),
  111. "nodes":self._serialized_nodes
  112. })
  113. def is_serialized(self, node):
  114. return node in self._visited_nodes
  115. def set_serialized(self, node, serialized):
  116. self._serialized_nodes.append(serialized)
  117. self._visited_nodes.append(node)
  118. self._remaining_nodes.remove(node)
  119. class Deserializer(object):
  120. def __init__(self, project, json_data):
  121. self._data = json.loads(json_data)
  122. self._project = project
  123. def deserialize(self):
  124. rc = RaspiContext(RaspiContext.REV_1)
  125. node_id_map = {}
  126. in_node = out_node = None
  127. for nd in self._data["nodes"]:
  128. if not nd["clsid"] in PinMagic.NODE_INDEX:
  129. return
  130. node_cls = PinMagic.NODE_INDEX[nd["clsid"]]
  131. if node_cls is None:
  132. return
  133. if nd["clsid"] in (0x8001, 0x8002):
  134. new_node = node_cls(rc)
  135. else:
  136. new_node = node_cls()
  137. node_id_map[nd["id"]] = new_node
  138. new_node.deserialize(nd["node_info"])
  139. if new_node.__class__.ID in (0x8001, 0x8002):
  140. new_node.add_to_nodeview(PinMagic.S().nodeview)
  141. elif hasattr(new_node, "childwidget") and new_node.childwidget:
  142. PinMagic.S().nodeview.add_with_child(new_node, new_node.childwidget)
  143. else:
  144. PinMagic.S().nodeview.add_node(new_node)
  145. if new_node.__class__.ID == 0x8001:
  146. in_node = new_node
  147. elif new_node.__class__.ID == 0x8002:
  148. out_node = new_node
  149. else:
  150. self._project.get_nodes().append(new_node)
  151. PinMagic.S().nodeview.set_node_position(new_node, nd["x"], nd["y"])
  152. PinMagic.S().nodeview.set_show_types(False)
  153. self._project.get_nodes().insert(0, in_node)
  154. self._project.get_nodes().insert(1, out_node)
  155. for nd in self._data["nodes"]:
  156. node = node_id_map[nd["id"]]
  157. for con in nd["connections"]:
  158. target = node_id_map[con[1]]
  159. node.get_sinks()[con[0]].link(target.get_sources()[con[2]])
  160. class Project(object):
  161. def __init__(self, typ):
  162. self._type = typ
  163. self._nodes = []
  164. self._filename = None
  165. def get_node_by_id(id_):
  166. for node in self._nodes:
  167. if id(node) == id_:
  168. return node
  169. return None
  170. def compile(self):
  171. return Compiler(self).compile()
  172. def set_filename(self, fn):
  173. self._filename = fn
  174. def get_filename(self):
  175. return self._filename
  176. def get_nodes(self):
  177. return self._nodes
  178. def set_nodes(self, nodes):
  179. self._nodes = nodes
  180. def get_type(self):
  181. return self._type
  182. def get_output_node(self):
  183. return self._nodes[1]
  184. def serialize(self):
  185. return Serializer(self).serialize()
  186. def deserialize(self, json_data):
  187. Deserializer(self, json_data).deserialize()
  188. class PinMagic(object):
  189. NODE_INDEX = {}
  190. INSTANCE = None
  191. @staticmethod
  192. def get_node_classes():
  193. ret = []
  194. for x in dir(pinmagik.nodes):
  195. if not x.startswith("_") and x not in pinmagik.nodes.EXCLUDES:
  196. exec("ret.append(pinmagik.nodes.%s)"%(x,))
  197. return ret
  198. @classmethod
  199. def build_node_index(cls):
  200. ret = []
  201. for x in dir(pinmagik.nodes):
  202. if not x.startswith("_") and x not in pinmagik.nodes.EXCLUDES:
  203. exec("cls.NODE_INDEX[pinmagik.nodes.%s.ID] = pinmagik.nodes.%s"%(x,x))
  204. cls.NODE_INDEX[RaspiOutNode.ID] = RaspiOutNode
  205. cls.NODE_INDEX[RaspiInNode.ID] = RaspiInNode
  206. @classmethod
  207. def S(cls):
  208. if cls.INSTANCE is None:
  209. cls.INSTANCE = PinMagic()
  210. return cls.INSTANCE
  211. def __init__(self):
  212. Gtk.init([])
  213. PinMagic.build_node_index()
  214. self._current_project = None
  215. self.headerbar = Gtk.HeaderBar.new()
  216. self.headerbar.set_title("PinMagic")
  217. self.headerbar.set_subtitle("untitled")
  218. self.nodeview = GtkFlow.NodeView.new()
  219. self.nodeview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
  220. self.nodeview.connect("drag-data-received", self.on_new_node)
  221. self.nodeview.drag_dest_add_text_targets()
  222. self.builder = Gtk.Builder.new()
  223. self.builder.add_from_file("main.ui")
  224. self.window = self.builder.get_object("window")
  225. self.scrollarea = self.builder.get_object("scrolledwindow")
  226. self.nodestree = self.builder.get_object("nodestreeview")
  227. box = self.builder.get_object("box")
  228. revealer = self.builder.get_object("info_revealer")
  229. revealer.set_reveal_child(False)
  230. box.pack_start(self.headerbar, False, True, 0)
  231. box.reorder_child(self.headerbar,0)
  232. self.scrollarea.add_with_viewport(self.nodeview)
  233. crt = Gtk.CellRendererText()
  234. col = Gtk.TreeViewColumn(_("Toolbox"), crt, text=0)
  235. self.nodestree.append_column(col)
  236. self.nodestree.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [],
  237. Gdk.DragAction.COPY)
  238. self.nodestree.connect("drag-data-get", self.on_drag_toolbox)
  239. self.nodestree.drag_source_add_text_targets()
  240. self.export = Gtk.Button.new_from_icon_name("document-send", Gtk.IconSize.BUTTON)
  241. self.export.connect("clicked", self.on_export)
  242. self.headerbar.pack_end(self.export)
  243. self.new = Gtk.MenuButton.new()
  244. self.new.set_popup(self._build_new_menu())
  245. self.new.set_image(Gtk.Image.new_from_icon_name("document-new", Gtk.IconSize.BUTTON))
  246. self.headerbar.pack_start(self.new)
  247. self.save = Gtk.Button.new_from_icon_name("document-save", Gtk.IconSize.BUTTON)
  248. self.save.connect("clicked", self.on_save)
  249. self.headerbar.pack_start(self.save)
  250. self.open = Gtk.Button.new_from_icon_name("document-open", Gtk.IconSize.BUTTON)
  251. self.open.connect("clicked", self.on_load)
  252. self.headerbar.pack_start(self.open)
  253. self.live = None
  254. if IS_REAL_RASPI:
  255. self.live = Gtk.Button.new_from_icon_name("media-playback-start", Gtk.IconSize.BUTTON)
  256. self.headerbar.pack_end(self.live)
  257. self.window.show_all()
  258. self.window.connect("destroy", self.quit)
  259. self.update_ui()
  260. PinMagic.get_node_classes()
  261. def on_drag_toolbox(self, widget, darg_context, data, info, time):
  262. selected_path = self.nodestree.get_selection().get_selected_rows()[1][0]
  263. if len(selected_path) < 2:
  264. return
  265. m = self.nodestree.get_model()
  266. treeiter = m.get_iter(selected_path)
  267. data.set_text("node_"+str(m.get_value(treeiter,1)),-1)
  268. def on_new_node(self, widget, drag_context, x, y, data, info, time):
  269. txt = data.get_text()
  270. if txt is None or not txt.startswith("node_"):
  271. return
  272. node_cls_id = int(txt.replace("node_","",1))
  273. if not node_cls_id in PinMagic.NODE_INDEX:
  274. return
  275. node_cls = PinMagic.NODE_INDEX[node_cls_id]
  276. if node_cls is None:
  277. return
  278. new_node = node_cls()
  279. if hasattr(new_node, "childwidget") and new_node.childwidget:
  280. self.nodeview.add_with_child(new_node, new_node.childwidget)
  281. else:
  282. self.nodeview.add_node(new_node)
  283. self._current_project.get_nodes().append(new_node)
  284. self.nodeview.set_node_position(new_node, x, y)
  285. self.nodeview.set_show_types(False)
  286. def _build_new_model(self):
  287. if self._current_project:
  288. store = Gtk.TreeStore.new([GObject.TYPE_STRING,GObject.TYPE_INT])
  289. categories = {}
  290. for node in PinMagic.get_node_classes():
  291. if not pinmagik.nodes.supports(
  292. node, self._current_project.get_type()[PD_FIELD_NAME]):
  293. continue
  294. if not node.CATEGORY in categories:
  295. categories[node.CATEGORY] = store.append(None, (node.CATEGORY,-1))
  296. store.append(categories[node.CATEGORY],(node.HR_NAME,node.ID))
  297. else:
  298. store = None
  299. self.nodestree.set_model(store)
  300. def _build_new_menu(self):
  301. menu = Gtk.Menu.new()
  302. for descriptor in PROJECT_TYPES.values():
  303. i = Gtk.MenuItem.new_with_label(descriptor[PD_FIELD_HUMAN_NAME])
  304. i.connect("activate", self.new_project, descriptor[PD_FIELD_NAME])
  305. menu.add(i)
  306. menu.show_all()
  307. return menu
  308. def update_ui(self):
  309. has_project = self._current_project is not None
  310. self.export.set_sensitive(has_project)
  311. self.save.set_sensitive(has_project)
  312. self.scrollarea.set_sensitive(has_project)
  313. self.nodeview.set_sensitive(has_project)
  314. if has_project:
  315. if self._current_project.get_filename() is not None:
  316. self.headerbar.set_subtitle(self._current_project.get_filename())
  317. else:
  318. self.headerbar.set_subtitle(_("untitled"))
  319. else:
  320. self.headerbar.set_subtitle("")
  321. if self.live:
  322. self.live.set_sensitive(has_project)
  323. self.live.set_visible(self._current_project.get_type()[PD_FIELDNMAE] == "raspi")
  324. self._build_new_model()
  325. def on_export(self, widget=None, data=None):
  326. if self._current_project:
  327. dialog = Gtk.FileChooserDialog(_("Choose a filename"), self.window,
  328. Gtk.FileChooserAction.SAVE,
  329. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  330. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  331. #self.window.add_filters(dialog)
  332. response = dialog.run()
  333. if response == Gtk.ResponseType.OK:
  334. code = self._current_project.compile()
  335. f = open(dialog.get_filename(),"w")
  336. f.write(code)
  337. f.close()
  338. dialog.destroy()
  339. def on_save(self, widget=None, data=None):
  340. if self._current_project:
  341. dialog = Gtk.FileChooserDialog(_("Choose a filename"), self.window,
  342. Gtk.FileChooserAction.SAVE,
  343. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  344. Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
  345. filt = Gtk.FileFilter()
  346. filt.set_name(_("PinMagic Projects"))
  347. filt.add_pattern("*.pimp")
  348. dialog.add_filter(filt)
  349. response = dialog.run()
  350. if response == Gtk.ResponseType.OK:
  351. json_data = self._current_project.serialize()
  352. f = open(dialog.get_filename(),"w")
  353. f.write(json_data)
  354. f.close()
  355. self._current_project.set_filename(dialog.get_filename())
  356. self.update_ui()
  357. dialog.destroy()
  358. def on_load(self, widget=None, data=None):
  359. dialog = Gtk.FileChooserDialog(_("Choose a filename"), self.window,
  360. Gtk.FileChooserAction.SAVE,
  361. (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
  362. Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
  363. filt = Gtk.FileFilter()
  364. filt.set_name(_("PinMagic Projects"))
  365. filt.add_pattern("*.pimp")
  366. dialog.add_filter(filt)
  367. response = dialog.run()
  368. if response == Gtk.ResponseType.OK:
  369. f = open(dialog.get_filename(),"r")
  370. json_data = f.read()
  371. f.close()
  372. self.nodeview.set_sensitive(True)
  373. self._clear_current_project()
  374. self._current_project = Project(json.loads(json_data)["type"])
  375. self._current_project.set_filename(dialog.get_filename())
  376. self.update_ui()
  377. self._current_project.deserialize(json_data)
  378. dialog.destroy()
  379. def _clear_current_project(self):
  380. if self._current_project:
  381. for node in self._current_project.get_nodes():
  382. self.nodeview.remove_node(node)
  383. def new_project(self, widget=None, data=None):
  384. self._clear_current_project()
  385. self._current_project = Project(PROJECT_TYPES[data])
  386. self.update_ui()
  387. rc = RaspiContext(RaspiContext.REV_1)
  388. rin = RaspiInNode(rc)
  389. rin.add_to_nodeview(self.nodeview)
  390. ron = RaspiOutNode(rc)
  391. ron.add_to_nodeview(self.nodeview)
  392. self.nodeview.set_node_position(rin, 1, 1)
  393. self.nodeview.set_node_position(ron, 600, 1)
  394. self._current_project.get_nodes().append(rin)
  395. self._current_project.get_nodes().append(ron)
  396. def load_project(self, project):
  397. self._clear_current_project()
  398. self._current_project = project
  399. self.update_ui()
  400. def quit(self, widget=None, data=None):
  401. Gtk.main_quit()
  402. @staticmethod
  403. def run():
  404. pm = PinMagic.S()
  405. Gtk.main()