diff --git a/plugin.ini b/plugin.ini index 8da30a5..2875f5a 100644 --- a/plugin.ini +++ b/plugin.ini @@ -1,20 +1,20 @@ # -# plugin config settings +# plugin settings # # # java invocation [java] -# name of jave executable +# name of java executable path = java # freerouting invocation [module] -# time to wait for router to return, seconds -timeout = 600 +input_ext = dsn +output_ext = ses # # jar artifact descriptor diff --git a/plugin.py b/plugin.py index 5f5d401..ce5d4a0 100644 --- a/plugin.py +++ b/plugin.py @@ -3,10 +3,12 @@ import wx.aui import time import pcbnew +import textwrap import threading import subprocess import configparser + # # FreeRouting round trip invocation: # * export board.dsn file from pcbnew @@ -36,46 +38,244 @@ def prepare(self): config.read(config_path) self.java_path = config['java']['path'] - self.module_timeout = float(config['module']['timeout']) self.module_file = config['artifact']['location'] self.module_path = os.path.join(self.here_path, self.module_file) - self.module_input = self.board_prefix + '.dsn' - self.module_output = self.board_prefix + '.ses' + self.module_input = self.board_prefix + '.' + config['module']['input_ext'] + self.module_output = self.board_prefix + '.' + config['module']['output_ext'] + + self.module_command = [self.java_path, "-jar", self.module_path, "-de", self.module_input, "-s"] if os.path.isfile(self.module_input): os.remove(self.module_input) if os.path.isfile(self.module_output): os.remove(self.module_output) + + # export board.dsn file from pcbnew + def RunExport(self): + ok = pcbnew.ExportSpecctraDSN(self.module_input) + if ok and os.path.isfile(self.module_input): + return True + else: + wx_show_error(""" + Failed to invoke: + * pcbnew.ExportSpecctraDSN + """) + return False + + # auto route by invoking FreeRouting.jar + def RunRouter(self): + + dialog = ProcessDialog(None, """ + Complete or Terminate FreeRouting: + * to complete, close Java window + * to terminate, press Terminate here + """) - # run inside gui-thread-safe context - def invoke(self, runner): - wx.CallAfter(runner) + def on_complete(): + wx_safe_invoke(dialog.terminate) - def RunExport(self): - pcbnew.ExportSpecctraDSN(self.module_input) + invoker = ProcessThread(self.module_command, on_complete) - def RunRouter(self): - command = [self.java_path, "-jar", self.module_path, "-de", self.module_input, "-s"] - process = subprocess.Popen(command) - process.wait(self.module_timeout) + dialog.Show() # dialog first + invoker.start() # run java process + result = dialog.ShowModal() # block pcbnew here + dialog.Destroy() + + try: + if result == dialog.result_button: # return via terminate button + invoker.terminate() + return False + elif result == dialog.result_terminate: # return via dialog.terminate() + if invoker.has_ok(): + return True + else: + invoker.show_error() + return False + else: + return False # should not happen + finally: + invoker.join(10) # prevent thread resource leak + # import generated board.ses file into pcbnew def RunImport(self): - pcbnew.ImportSpecctraSES(self.module_output) + ok = pcbnew.ImportSpecctraSES(self.module_output) + if ok and os.path.isfile(self.module_output): + return True + else: + wx_show_error(""" + Failed to invoke: + * pcbnew.ImportSpecctraSES + """) + return False + + # invoke chain of dependent methods + def RunSteps(self): + self.prepare() + if not self.RunExport() : + return + if not self.RunRouter() : + return + wx_safe_invoke(self.RunImport) + # kicad plugin action entry def Run(self): - self.prepare() - self.invoke(self.RunExport) - self.invoke(self.RunRouter) - self.invoke(self.RunImport) + if has_pcbnew_api(): + self.RunSteps() + else: + wx_show_error(""" + Missing required python API: + * pcbnew.ExportSpecctraDSN + * pcbnew.ImportSpecctraSES + --- + Try development nightly build: + * http://kicad-pcb.org/download/ + """) # provision gui-thread-safe execution context -if not wx.GetApp(): - theApp = wx.App() -else: - theApp = wx.GetApp() +# https://git.launchpad.net/kicad/tree/pcbnew/python/kicad_pyshell/__init__.py#n89 +if 'phoenix' in wx.PlatformInfo: + if not wx.GetApp(): + theApp = wx.App() + else: + theApp = wx.GetApp() + + +# run functon inside gui-thread-safe context, requires wx.App on phoenix +def wx_safe_invoke(function, *args, **kwargs): + wx.CallAfter(function, *args, **kwargs) + + +# verify required pcbnew api is present +def has_pcbnew_api(): + return hasattr(pcbnew, 'ExportSpecctraDSN') and hasattr(pcbnew, 'ImportSpecctraSES') + + +# message dialog style +wx_caption = "KiCad FreeRouting Plugin" + + +# display error text to the user +def wx_show_error(text): + message = textwrap.dedent(text) + style = wx.OK | wx.ICON_ERROR + dialog = wx.MessageDialog(None, message=message, caption=wx_caption, style=style) + dialog.ShowModal() + dialog.Destroy() + + +# prompt user to cancel pending action; allow to cancel programmatically +class ProcessDialog (wx.Dialog): + + def __init__(self, parent, text): + + message = textwrap.dedent(text) + + self.result_button = wx.NewId() + self.result_terminate = wx.NewId() + + wx.Dialog.__init__ (self, parent, id=wx.ID_ANY, title=wx_caption, pos=wx.DefaultPosition, size=wx.Size(-1, -1), style=wx.CAPTION) + + self.SetSizeHints(wx.DefaultSize, wx.DefaultSize) + + sizer = wx.BoxSizer(wx.VERTICAL) + + self.text = wx.StaticText(self, wx.ID_ANY, message, wx.DefaultPosition, wx.DefaultSize, 0) + self.text.Wrap(-1) + sizer.Add(self.text, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10) + + self.line = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) + sizer.Add(self.line, 0, wx.EXPAND | wx.ALL, 5) + + self.bttn = wx.Button(self, wx.ID_ANY, "Terminate", wx.DefaultPosition, wx.DefaultSize, 0) + self.bttn.SetDefault() + sizer.Add(self.bttn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5) + + self.SetSizer(sizer) + self.Layout() + sizer.Fit(self) + + self.Centre(wx.BOTH) + + self.bttn.Bind(wx.EVT_BUTTON, self.bttn_on_click) + + def __del__(self): + pass + + def bttn_on_click(self, event): + self.EndModal(self.result_button) + + def terminate(self): + self.EndModal(self.result_terminate) + + +# cancelable external process invoker with completion notification +class ProcessThread(threading.Thread): + + def __init__(self, command, on_complete=None): + self.command = command + self.on_complete = on_complete + threading.Thread.__init__(self) + self.setDaemon(True) + + # thread runner + def run(self): + try: + self.process = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.stdout, self.stderr = self.process.communicate() + except Exception as error: + self.error = error + finally: + if self.on_complete is not None: + self.on_complete() + + def has_ok(self): + return self.has_process() and self.process.returncode == 0 + + def has_code(self): + return self.has_process() and self.process.returncode != 0 + + def has_error(self): + return hasattr(self, "error") + + def has_process(self): + return hasattr(self, "process") + + def terminate(self): + if self.has_process(): + self.process.kill() + else: + pass + + def show_error(self): + command = " ".join(self.command) + if self.has_error() : + wx_show_error(""" + Process failure: + --- + command: + %s + --- + error: + %s""" % (command, str(self.error))) + elif self.has_code(): + wx_show_error(""" + Program failure: + --- + command: + %s + --- + exit code: %d + --- stdout --- + %s + --- stderr --- + %s + """ % (command, self.process.returncode, self.stdout, self.stderr)) + else: + pass + # register plugin with kicad backend FreeRoutingPlugin().register() diff --git a/res/plugin.fbp b/res/plugin.fbp new file mode 100644 index 0000000..ca6a4f4 --- /dev/null +++ b/res/plugin.fbp @@ -0,0 +1,258 @@ + + + + + ; + + 1 + source_name + 0 + 0 + res + UTF-8 + connect + + 1000 + none + + 1 + 0 + Plugin + + . + + 1 + 1 + 1 + 1 + UI + 0 + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + ProcessDialog + + -1,-1 + wxCAPTION + ; ; forward_declare + wx_caption + + + + + + + sizer + wxVERTICAL + none + + 10 + wxALIGN_CENTER_HORIZONTAL|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + message + 0 + + 0 + + + 0 + + 1 + text + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND | wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + line + 1 + + + protected + 1 + + Resizable + 1 + + wxLI_HORIZONTAL + ; ; forward_declare + 0 + + + + + + + + 5 + wxALIGN_CENTER_HORIZONTAL|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + + 1 + 0 + 1 + + 1 + + 1 + 0 + + Dock + 0 + Left + 1 + + 1 + + + 0 + 0 + wxID_ANY + Terminate + + 0 + + 0 + + + 0 + + 1 + bttn + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + wxDefaultValidator + + + + + bttn_on_click + + + + + + diff --git a/res/readme.md b/res/readme.md new file mode 100644 index 0000000..3d87aee --- /dev/null +++ b/res/readme.md @@ -0,0 +1,2 @@ + +### developer resources