Skip to content

Commit e5cd87f

Browse files
Merge pull request #3 from random-builder/develop
error handling, fix #2
2 parents 6a11954 + da1e8f3 commit e5cd87f

File tree

4 files changed

+485
-25
lines changed

4 files changed

+485
-25
lines changed

plugin.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11

22
#
3-
# plugin config settings
3+
# plugin settings
44
#
55

66
#
77
# java invocation
88
[java]
99

10-
# name of jave executable
10+
# name of java executable
1111
path = java
1212

1313
# freerouting invocation
1414
[module]
1515

16-
# time to wait for router to return, seconds
17-
timeout = 600
16+
input_ext = dsn
17+
output_ext = ses
1818

1919
#
2020
# jar artifact descriptor

plugin.py

Lines changed: 221 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import wx.aui
44
import time
55
import pcbnew
6+
import textwrap
67
import threading
78
import subprocess
89
import configparser
910

11+
1012
#
1113
# FreeRouting round trip invocation:
1214
# * export board.dsn file from pcbnew
@@ -36,46 +38,244 @@ def prepare(self):
3638
config.read(config_path)
3739

3840
self.java_path = config['java']['path']
39-
self.module_timeout = float(config['module']['timeout'])
4041

4142
self.module_file = config['artifact']['location']
4243
self.module_path = os.path.join(self.here_path, self.module_file)
4344

44-
self.module_input = self.board_prefix + '.dsn'
45-
self.module_output = self.board_prefix + '.ses'
45+
self.module_input = self.board_prefix + '.' + config['module']['input_ext']
46+
self.module_output = self.board_prefix + '.' + config['module']['output_ext']
47+
48+
self.module_command = [self.java_path, "-jar", self.module_path, "-de", self.module_input, "-s"]
4649

4750
if os.path.isfile(self.module_input):
4851
os.remove(self.module_input)
4952
if os.path.isfile(self.module_output):
5053
os.remove(self.module_output)
54+
55+
# export board.dsn file from pcbnew
56+
def RunExport(self):
57+
ok = pcbnew.ExportSpecctraDSN(self.module_input)
58+
if ok and os.path.isfile(self.module_input):
59+
return True
60+
else:
61+
wx_show_error("""
62+
Failed to invoke:
63+
* pcbnew.ExportSpecctraDSN
64+
""")
65+
return False
66+
67+
# auto route by invoking FreeRouting.jar
68+
def RunRouter(self):
69+
70+
dialog = ProcessDialog(None, """
71+
Complete or Terminate FreeRouting:
72+
* to complete, close Java window
73+
* to terminate, press Terminate here
74+
""")
5175

52-
# run inside gui-thread-safe context
53-
def invoke(self, runner):
54-
wx.CallAfter(runner)
76+
def on_complete():
77+
wx_safe_invoke(dialog.terminate)
5578

56-
def RunExport(self):
57-
pcbnew.ExportSpecctraDSN(self.module_input)
79+
invoker = ProcessThread(self.module_command, on_complete)
5880

59-
def RunRouter(self):
60-
command = [self.java_path, "-jar", self.module_path, "-de", self.module_input, "-s"]
61-
process = subprocess.Popen(command)
62-
process.wait(self.module_timeout)
81+
dialog.Show() # dialog first
82+
invoker.start() # run java process
83+
result = dialog.ShowModal() # block pcbnew here
84+
dialog.Destroy()
85+
86+
try:
87+
if result == dialog.result_button: # return via terminate button
88+
invoker.terminate()
89+
return False
90+
elif result == dialog.result_terminate: # return via dialog.terminate()
91+
if invoker.has_ok():
92+
return True
93+
else:
94+
invoker.show_error()
95+
return False
96+
else:
97+
return False # should not happen
98+
finally:
99+
invoker.join(10) # prevent thread resource leak
63100

101+
# import generated board.ses file into pcbnew
64102
def RunImport(self):
65-
pcbnew.ImportSpecctraSES(self.module_output)
103+
ok = pcbnew.ImportSpecctraSES(self.module_output)
104+
if ok and os.path.isfile(self.module_output):
105+
return True
106+
else:
107+
wx_show_error("""
108+
Failed to invoke:
109+
* pcbnew.ImportSpecctraSES
110+
""")
111+
return False
112+
113+
# invoke chain of dependent methods
114+
def RunSteps(self):
115+
self.prepare()
116+
if not self.RunExport() :
117+
return
118+
if not self.RunRouter() :
119+
return
120+
wx_safe_invoke(self.RunImport)
66121

122+
# kicad plugin action entry
67123
def Run(self):
68-
self.prepare()
69-
self.invoke(self.RunExport)
70-
self.invoke(self.RunRouter)
71-
self.invoke(self.RunImport)
124+
if has_pcbnew_api():
125+
self.RunSteps()
126+
else:
127+
wx_show_error("""
128+
Missing required python API:
129+
* pcbnew.ExportSpecctraDSN
130+
* pcbnew.ImportSpecctraSES
131+
---
132+
Try development nightly build:
133+
* http://kicad-pcb.org/download/
134+
""")
72135

73136

74137
# provision gui-thread-safe execution context
75-
if not wx.GetApp():
76-
theApp = wx.App()
77-
else:
78-
theApp = wx.GetApp()
138+
# https://git.launchpad.net/kicad/tree/pcbnew/python/kicad_pyshell/__init__.py#n89
139+
if 'phoenix' in wx.PlatformInfo:
140+
if not wx.GetApp():
141+
theApp = wx.App()
142+
else:
143+
theApp = wx.GetApp()
144+
145+
146+
# run functon inside gui-thread-safe context, requires wx.App on phoenix
147+
def wx_safe_invoke(function, *args, **kwargs):
148+
wx.CallAfter(function, *args, **kwargs)
149+
150+
151+
# verify required pcbnew api is present
152+
def has_pcbnew_api():
153+
return hasattr(pcbnew, 'ExportSpecctraDSN') and hasattr(pcbnew, 'ImportSpecctraSES')
154+
155+
156+
# message dialog style
157+
wx_caption = "KiCad FreeRouting Plugin"
158+
159+
160+
# display error text to the user
161+
def wx_show_error(text):
162+
message = textwrap.dedent(text)
163+
style = wx.OK | wx.ICON_ERROR
164+
dialog = wx.MessageDialog(None, message=message, caption=wx_caption, style=style)
165+
dialog.ShowModal()
166+
dialog.Destroy()
167+
168+
169+
# prompt user to cancel pending action; allow to cancel programmatically
170+
class ProcessDialog (wx.Dialog):
171+
172+
def __init__(self, parent, text):
173+
174+
message = textwrap.dedent(text)
175+
176+
self.result_button = wx.NewId()
177+
self.result_terminate = wx.NewId()
178+
179+
wx.Dialog.__init__ (self, parent, id=wx.ID_ANY, title=wx_caption, pos=wx.DefaultPosition, size=wx.Size(-1, -1), style=wx.CAPTION)
180+
181+
self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
182+
183+
sizer = wx.BoxSizer(wx.VERTICAL)
184+
185+
self.text = wx.StaticText(self, wx.ID_ANY, message, wx.DefaultPosition, wx.DefaultSize, 0)
186+
self.text.Wrap(-1)
187+
sizer.Add(self.text, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10)
188+
189+
self.line = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
190+
sizer.Add(self.line, 0, wx.EXPAND | wx.ALL, 5)
191+
192+
self.bttn = wx.Button(self, wx.ID_ANY, "Terminate", wx.DefaultPosition, wx.DefaultSize, 0)
193+
self.bttn.SetDefault()
194+
sizer.Add(self.bttn, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)
195+
196+
self.SetSizer(sizer)
197+
self.Layout()
198+
sizer.Fit(self)
199+
200+
self.Centre(wx.BOTH)
201+
202+
self.bttn.Bind(wx.EVT_BUTTON, self.bttn_on_click)
203+
204+
def __del__(self):
205+
pass
206+
207+
def bttn_on_click(self, event):
208+
self.EndModal(self.result_button)
209+
210+
def terminate(self):
211+
self.EndModal(self.result_terminate)
212+
213+
214+
# cancelable external process invoker with completion notification
215+
class ProcessThread(threading.Thread):
216+
217+
def __init__(self, command, on_complete=None):
218+
self.command = command
219+
self.on_complete = on_complete
220+
threading.Thread.__init__(self)
221+
self.setDaemon(True)
222+
223+
# thread runner
224+
def run(self):
225+
try:
226+
self.process = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
227+
self.stdout, self.stderr = self.process.communicate()
228+
except Exception as error:
229+
self.error = error
230+
finally:
231+
if self.on_complete is not None:
232+
self.on_complete()
233+
234+
def has_ok(self):
235+
return self.has_process() and self.process.returncode == 0
236+
237+
def has_code(self):
238+
return self.has_process() and self.process.returncode != 0
239+
240+
def has_error(self):
241+
return hasattr(self, "error")
242+
243+
def has_process(self):
244+
return hasattr(self, "process")
245+
246+
def terminate(self):
247+
if self.has_process():
248+
self.process.kill()
249+
else:
250+
pass
251+
252+
def show_error(self):
253+
command = " ".join(self.command)
254+
if self.has_error() :
255+
wx_show_error("""
256+
Process failure:
257+
---
258+
command:
259+
%s
260+
---
261+
error:
262+
%s""" % (command, str(self.error)))
263+
elif self.has_code():
264+
wx_show_error("""
265+
Program failure:
266+
---
267+
command:
268+
%s
269+
---
270+
exit code: %d
271+
--- stdout ---
272+
%s
273+
--- stderr ---
274+
%s
275+
""" % (command, self.process.returncode, self.stdout, self.stderr))
276+
else:
277+
pass
278+
79279

80280
# register plugin with kicad backend
81281
FreeRoutingPlugin().register()

0 commit comments

Comments
 (0)