|
@@ -0,0 +1,707 @@
|
|
1
|
+#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+#
|
|
4
|
+# bed-leveling.py
|
|
5
|
+#
|
|
6
|
+# Written in June 2016 by:
|
|
7
|
+# Thomas Buck <xythobuz@xythobuz.de>
|
|
8
|
+#
|
|
9
|
+# Manual Mesh Bed Leveling GUI utility for 3D printers with Marlin Firmware.
|
|
10
|
+# At the time of me writing this program, Marlin had a bug that caused the
|
|
11
|
+# manual mesh bed leveling process to not work using the connected LCD display,
|
|
12
|
+# leaving G-Codes sent over the serial port as the only option. Because manually
|
|
13
|
+# inserting these in a terminal program proved to be very tedious, and none of the
|
|
14
|
+# G-Code senders I've tested had the features I wanted (mainly easily configurable
|
|
15
|
+# step sizes), I've wrote this simple little program.
|
|
16
|
+#
|
|
17
|
+# After starting the program, select the serial port connected to your printer,
|
|
18
|
+# the correct baudrate for communicating with it and press Connect.
|
|
19
|
+# The X, Y and Z value displayed are periodically polled from the printer and
|
|
20
|
+# are not very accurate. Zc is what is manipulated using the up- and down-arrow
|
|
21
|
+# buttons or keys. The value shown there will be more accurate.
|
|
22
|
+#
|
|
23
|
+# Before starting with the leveling procedure, the current Mesh info has to be
|
|
24
|
+# polled from the printer. This will happen automatically after connecting.
|
|
25
|
+# When ready, press Start, then adjust the Z-height for each measuring point
|
|
26
|
+# and press Next or the right-arrow key to move on. After you're finished,
|
|
27
|
+# you should store the new mesh data in the EEPROM using the Save button.
|
|
28
|
+#
|
|
29
|
+# Dependencies:
|
|
30
|
+# - pySerial (>= v3.0)
|
|
31
|
+# - wxPython
|
|
32
|
+#
|
|
33
|
+# This has been developed and tested on a Mac OS X 10.10.5 machine
|
|
34
|
+# with Python 2.7 and dependencies installed with MacPorts,
|
|
35
|
+# connected to a modified Fabrikator Mini V1.5 with Marlin 1.1.0-RC6.
|
|
36
|
+#
|
|
37
|
+
|
|
38
|
+import wx
|
|
39
|
+import serial
|
|
40
|
+import serial.threaded
|
|
41
|
+import serial.tools.list_ports
|
|
42
|
+import re
|
|
43
|
+
|
|
44
|
+POLL_SERIAL_PORTS_INTERVALL = 4000
|
|
45
|
+POLL_COORDINATES_INTERVALL = 2500
|
|
46
|
+POLL_TEMPERATURE_INTERVALL = 3000
|
|
47
|
+MESH_DATA_RETRY_TIMEOUT = 2000
|
|
48
|
+BUSY_PROCESSING_STEPS = 6;
|
|
49
|
+AVAILABLE_BAUD_RATES = ["2400", "9600", "19200", "38400", "57600", "115200", "250000"]
|
|
50
|
+DEFAULT_BAUD_RATE = "115200"
|
|
51
|
+DEFAULT_SERIAL_PORTS = ["/dev/cu.usbserial-AI02LQH7", "/dev/cu.SLAB_USBtoUART"]
|
|
52
|
+
|
|
53
|
+GCODE_EEPROM_SAVE = "M500"
|
|
54
|
+GCODE_STOP_IDLE_HOLD = "M84"
|
|
55
|
+GCODE_MOVE_TO_ORIGIN = "G28"
|
|
56
|
+GCODE_GET_CURRENT_POSITION = "M114"
|
|
57
|
+GCODE_MESH_INFO = "G29 S0"
|
|
58
|
+GCODE_MESH_START = "G29 S1"
|
|
59
|
+GCODE_MESH_NEXT = "G29 S2"
|
|
60
|
+GCODE_MOVE_Z = "G1 Z{}"
|
|
61
|
+GCODE_SET_BED_TEMP = "M140 S{}"
|
|
62
|
+GCODE_GET_TEMP = "M105"
|
|
63
|
+
|
|
64
|
+PRINTER_RESPONSE_OK = "ok"
|
|
65
|
+PRINTER_RESPONSE_BUSY = "echo:busy: processing"
|
|
66
|
+PRINTER_REGEX_COORDINATES = "^X:\d+\.\d+ Y:\d+\.\d+ Z:\d+\.\d+ E:\d+\.\d+ Count X:\s*\d+ Y:\s*\d+ Z:\s*\d+$"
|
|
67
|
+PRINTER_REGEX_MESH_POINTS = "^Num X,Y: \d,\d$"
|
|
68
|
+PRINTER_REGEX_MESH_Z = "^Z search height: \d\.*\d*$"
|
|
69
|
+PRINTER_REGEX_TEMP = "^ok T:\d\d*\.*\d* \/\d\d*\.*\d* B:\d\d*\.*\d* \/\d\d*\.*\d* B@:\d\d*\.*\d* @:\d\d*\.*\d*$"
|
|
70
|
+
|
|
71
|
+# Event for sending new XYZ coordinates from the serial thread to the GUI thread
|
|
72
|
+myEVT_COORDINATES = wx.NewEventType()
|
|
73
|
+EVT_COORDINATES = wx.PyEventBinder(myEVT_COORDINATES, 1)
|
|
74
|
+class CoordinatesEvent(wx.PyCommandEvent):
|
|
75
|
+ def __init__(self, etype, eid, value = None):
|
|
76
|
+ wx.PyCommandEvent.__init__(self, etype, eid)
|
|
77
|
+ self.value = value
|
|
78
|
+
|
|
79
|
+ def GetValue(self):
|
|
80
|
+ return self.value
|
|
81
|
+
|
|
82
|
+# Event for sending new mesh point info
|
|
83
|
+myEVT_MESH_POINTS = wx.NewEventType()
|
|
84
|
+EVT_MESH_POINTS = wx.PyEventBinder(myEVT_MESH_POINTS, 1)
|
|
85
|
+class MeshPointEvent(wx.PyCommandEvent):
|
|
86
|
+ def __init__(self, etype, eid, x = None, y = None):
|
|
87
|
+ wx.PyCommandEvent.__init__(self, etype, eid)
|
|
88
|
+ self.x = x
|
|
89
|
+ self.y = y
|
|
90
|
+
|
|
91
|
+ def GetX(self):
|
|
92
|
+ return self.x
|
|
93
|
+
|
|
94
|
+ def GetY(self):
|
|
95
|
+ return self.y
|
|
96
|
+
|
|
97
|
+# Event for sending new mesh point info
|
|
98
|
+myEVT_MESH_Z = wx.NewEventType()
|
|
99
|
+EVT_MESH_Z = wx.PyEventBinder(myEVT_MESH_Z, 1)
|
|
100
|
+class MeshZEvent(wx.PyCommandEvent):
|
|
101
|
+ def __init__(self, etype, eid, z = None):
|
|
102
|
+ wx.PyCommandEvent.__init__(self, etype, eid)
|
|
103
|
+ self.z = z
|
|
104
|
+
|
|
105
|
+ def GetZ(self):
|
|
106
|
+ return self.z
|
|
107
|
+
|
|
108
|
+# Event for sending temperature info
|
|
109
|
+myEVT_TEMPERATURE = wx.NewEventType()
|
|
110
|
+EVT_TEMPERATURE = wx.PyEventBinder(myEVT_TEMPERATURE, 1)
|
|
111
|
+class TemperatureEvent(wx.PyCommandEvent):
|
|
112
|
+ def __init__(self, etype, eid, bed = None):
|
|
113
|
+ wx.PyCommandEvent.__init__(self, etype, eid)
|
|
114
|
+ self.bed = bed
|
|
115
|
+
|
|
116
|
+ def GetBed(self):
|
|
117
|
+ return self.bed
|
|
118
|
+
|
|
119
|
+# Event for sending a new busy indicator status update
|
|
120
|
+myEVT_STATUS = wx.NewEventType()
|
|
121
|
+EVT_STATUS = wx.PyEventBinder(myEVT_STATUS, 1)
|
|
122
|
+class StatusEvent(wx.PyCommandEvent):
|
|
123
|
+ def __init__(self, etype, eid, value = None):
|
|
124
|
+ wx.PyCommandEvent.__init__(self, etype, eid)
|
|
125
|
+ self.value = value
|
|
126
|
+
|
|
127
|
+ def GetValue(self):
|
|
128
|
+ return self.value
|
|
129
|
+
|
|
130
|
+# Serial Thread reading from and writing to the printer, line-based
|
|
131
|
+class GCodeReader(serial.threaded.LineReader):
|
|
132
|
+
|
|
133
|
+ TERMINATOR = b'\n'
|
|
134
|
+
|
|
135
|
+ regexCoordinates = re.compile(PRINTER_REGEX_COORDINATES, re.M)
|
|
136
|
+ regexMeshPoints = re.compile(PRINTER_REGEX_MESH_POINTS, re.M)
|
|
137
|
+ regexMeshZ = re.compile(PRINTER_REGEX_MESH_Z, re.M)
|
|
138
|
+ regexTemp = re.compile(PRINTER_REGEX_TEMP, re.M)
|
|
139
|
+
|
|
140
|
+ def setParent(self, parent):
|
|
141
|
+ self.parent = parent
|
|
142
|
+
|
|
143
|
+ def handle_line(self, data):
|
|
144
|
+ if data == PRINTER_RESPONSE_OK:
|
|
145
|
+ event = StatusEvent(myEVT_STATUS, -1, 0)
|
|
146
|
+ wx.PostEvent(self.parent, event)
|
|
147
|
+ return
|
|
148
|
+
|
|
149
|
+ if data == PRINTER_RESPONSE_BUSY:
|
|
150
|
+ event = StatusEvent(myEVT_STATUS, -1, 1)
|
|
151
|
+ wx.PostEvent(self.parent, event)
|
|
152
|
+ return
|
|
153
|
+
|
|
154
|
+ if self.regexCoordinates.match(data):
|
|
155
|
+ afterX = data.split("X:")[1]
|
|
156
|
+ x = afterX.split(" ")[0]
|
|
157
|
+ afterY = data.split("Y:")[1]
|
|
158
|
+ y = afterY.split(" ")[0]
|
|
159
|
+ afterZ = data.split("Z:")[1]
|
|
160
|
+ z = afterZ.split(" ")[0]
|
|
161
|
+ str = "X: {} Y: {} Z: {}".format(x, y, z)
|
|
162
|
+ event = CoordinatesEvent(myEVT_COORDINATES, -1, str)
|
|
163
|
+ wx.PostEvent(self.parent, event)
|
|
164
|
+ return
|
|
165
|
+
|
|
166
|
+ if self.regexMeshPoints.match(data):
|
|
167
|
+ firstDigit = data.split("X,Y: ")[1]
|
|
168
|
+ x = firstDigit.split(",")[0]
|
|
169
|
+ y = firstDigit.split(",")[1]
|
|
170
|
+ event = MeshPointEvent(myEVT_MESH_POINTS, -1, x, y)
|
|
171
|
+ wx.PostEvent(self.parent, event)
|
|
172
|
+ return
|
|
173
|
+
|
|
174
|
+ if self.regexMeshZ.match(data):
|
|
175
|
+ num = data.split("Z search height: ")[1]
|
|
176
|
+ event = MeshZEvent(myEVT_MESH_Z, -1, float(num))
|
|
177
|
+ wx.PostEvent(self.parent, event)
|
|
178
|
+ return
|
|
179
|
+
|
|
180
|
+ if self.regexTemp.match(data):
|
|
181
|
+ bed = data.split(" B:")[1]
|
|
182
|
+ temp = bed.split(" /")[0]
|
|
183
|
+ event = TemperatureEvent(myEVT_TEMPERATURE, -1, float(temp))
|
|
184
|
+ wx.PostEvent(self.parent, event)
|
|
185
|
+ return
|
|
186
|
+
|
|
187
|
+# Main Window class
|
|
188
|
+class WizardFrame(wx.Frame):
|
|
189
|
+ def __init__(self, *args, **kwargs):
|
|
190
|
+ wx.Frame.__init__(self, *args, **kwargs)
|
|
191
|
+
|
|
192
|
+ self.hasQuit = False
|
|
193
|
+ self.isConnected = False
|
|
194
|
+ self.serial = serial.Serial()
|
|
195
|
+ self.thread = serial.threaded.ReaderThread(self.serial, GCodeReader)
|
|
196
|
+ self.isLeveling = False
|
|
197
|
+ self.currentPoint = 0
|
|
198
|
+ self.meshPoints = -1
|
|
199
|
+ self.step = 0.025
|
|
200
|
+ self.currentZ = 0.0
|
|
201
|
+ self.startZ = 0.0
|
|
202
|
+
|
|
203
|
+ self.Bind(EVT_COORDINATES, self.OnCoordinates)
|
|
204
|
+ self.Bind(EVT_STATUS, self.OnStatus)
|
|
205
|
+ self.Bind(EVT_MESH_POINTS, self.OnMeshPoints)
|
|
206
|
+ self.Bind(EVT_MESH_Z, self.OnMeshZ)
|
|
207
|
+ self.Bind(EVT_TEMPERATURE, self.OnTemperature)
|
|
208
|
+
|
|
209
|
+ # Menubar and Items
|
|
210
|
+ MenuBar = wx.MenuBar()
|
|
211
|
+ FileMenu = wx.Menu()
|
|
212
|
+ MenuBar.Append(FileMenu, "&File")
|
|
213
|
+ self.SetMenuBar(MenuBar)
|
|
214
|
+ item = FileMenu.Append(wx.ID_EXIT, text = "&Exit")
|
|
215
|
+ self.Bind(wx.EVT_MENU, self.OnQuit, item)
|
|
216
|
+ self.Bind(wx.EVT_CLOSE, self.OnQuit)
|
|
217
|
+ self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
218
|
+
|
|
219
|
+ outerPanelSizer = wx.BoxSizer(wx.VERTICAL)
|
|
220
|
+
|
|
221
|
+ # Panel for serial control widgets
|
|
222
|
+ self.panelSerial = wx.Panel(self)
|
|
223
|
+ panelSerialSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
224
|
+ self.panelSerial.SetSizer(panelSerialSizer)
|
|
225
|
+ outerPanelSizer.Add(self.panelSerial, 0, wx.TOP | wx.EXPAND, 4)
|
|
226
|
+ self.panelSerial.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
227
|
+
|
|
228
|
+ # List all available serial ports
|
|
229
|
+ serialPortList = serial.tools.list_ports.comports()
|
|
230
|
+ serialPortNames = []
|
|
231
|
+ for port in serialPortList:
|
|
232
|
+ serialPortNames.append(port[0])
|
|
233
|
+
|
|
234
|
+ # Combobox for serial ports
|
|
235
|
+ self.comboBoxSerial = wx.ComboBox(parent = self.panelSerial, choices = serialPortNames, style = wx.CB_READONLY)
|
|
236
|
+ self.comboBoxSerial.SetMinSize((200, -1))
|
|
237
|
+ panelSerialSizer.Add(self.comboBoxSerial, 1, wx.LEFT, 5)
|
|
238
|
+ self.comboBoxSerial.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
239
|
+
|
|
240
|
+ # Select default serial port(s)
|
|
241
|
+ for port in serialPortList:
|
|
242
|
+ if port[0] in DEFAULT_SERIAL_PORTS:
|
|
243
|
+ self.comboBoxSerial.SetStringSelection(port[0])
|
|
244
|
+
|
|
245
|
+ # Regularly update serial port list
|
|
246
|
+ wx.FutureCall(POLL_SERIAL_PORTS_INTERVALL, self.EnumerateSerialPorts)
|
|
247
|
+
|
|
248
|
+ # Combobox for baudrates
|
|
249
|
+ self.comboBoxBaud = wx.ComboBox(parent = self.panelSerial, choices = AVAILABLE_BAUD_RATES, style = wx.CB_READONLY)
|
|
250
|
+ self.comboBoxBaud.SetMinSize((80, -1))
|
|
251
|
+ panelSerialSizer.Add(self.comboBoxBaud, 0)
|
|
252
|
+ self.comboBoxBaud.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
253
|
+
|
|
254
|
+ for baud in AVAILABLE_BAUD_RATES:
|
|
255
|
+ if baud == DEFAULT_BAUD_RATE:
|
|
256
|
+ self.comboBoxBaud.SetStringSelection(DEFAULT_BAUD_RATE)
|
|
257
|
+
|
|
258
|
+ # Button for connecting and disconnecting
|
|
259
|
+ self.buttonSerial = wx.Button(parent = self.panelSerial, label = "Connect")
|
|
260
|
+ self.buttonSerial.Bind(wx.EVT_BUTTON, self.OnConnectDisconnect)
|
|
261
|
+ panelSerialSizer.Add(self.buttonSerial, 0, wx.TOP, 2)
|
|
262
|
+ panelSerialSizer.AddSpacer(5)
|
|
263
|
+ self.buttonSerial.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
264
|
+
|
|
265
|
+ # Panel for status labels
|
|
266
|
+ self.panelStatus = wx.Panel(self)
|
|
267
|
+ panelStatusSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
268
|
+ self.panelStatus.SetSizer(panelStatusSizer)
|
|
269
|
+ outerPanelSizer.Add(self.panelStatus, 0, wx.EXPAND | wx.TOP, 3)
|
|
270
|
+ self.panelStatus.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
271
|
+
|
|
272
|
+ # Labels for coordinates
|
|
273
|
+ self.labelX = wx.StaticText(parent = self.panelStatus, label = "X: ??.??")
|
|
274
|
+ panelStatusSizer.Add(self.labelX, 0, wx.LEFT, 5)
|
|
275
|
+ panelStatusSizer.AddStretchSpacer(1)
|
|
276
|
+ self.labelX.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
277
|
+
|
|
278
|
+ self.labelY = wx.StaticText(parent = self.panelStatus, label = "Y: ??.??")
|
|
279
|
+ panelStatusSizer.Add(self.labelY, 0)
|
|
280
|
+ panelStatusSizer.AddStretchSpacer(1)
|
|
281
|
+ self.labelY.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
282
|
+
|
|
283
|
+ self.labelZ = wx.StaticText(parent = self.panelStatus, label = "Z: ??.??")
|
|
284
|
+ panelStatusSizer.Add(self.labelZ, 0, wx.RIGHT, 5)
|
|
285
|
+ panelStatusSizer.AddStretchSpacer(1)
|
|
286
|
+ self.labelZ.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
287
|
+
|
|
288
|
+ self.labelZc = wx.StaticText(parent = self.panelStatus, label = "Zc: ??.???")
|
|
289
|
+ panelStatusSizer.Add(self.labelZc, 0, wx.RIGHT, 5)
|
|
290
|
+ self.labelZc.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
291
|
+
|
|
292
|
+ # Panel for general actions
|
|
293
|
+ self.panelAction = wx.Panel(self)
|
|
294
|
+ panelActionSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
295
|
+ self.panelAction.SetSizer(panelActionSizer)
|
|
296
|
+ outerPanelSizer.Add(self.panelAction, 0, wx.EXPAND | wx.TOP, 5)
|
|
297
|
+ self.panelAction.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
298
|
+
|
|
299
|
+ # Button for homing
|
|
300
|
+ self.buttonHome = wx.Button(parent = self.panelAction, label = "Home")
|
|
301
|
+ self.buttonHome.Bind(wx.EVT_BUTTON, self.OnHome)
|
|
302
|
+ self.buttonHome.Enable(False)
|
|
303
|
+ panelActionSizer.AddSpacer(5)
|
|
304
|
+ panelActionSizer.Add(self.buttonHome, 0, wx.BOTTOM, 1)
|
|
305
|
+ self.buttonHome.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
306
|
+
|
|
307
|
+ # Button for steppers off
|
|
308
|
+ self.buttonOff = wx.Button(parent = self.panelAction, label = "Off")
|
|
309
|
+ self.buttonOff.Bind(wx.EVT_BUTTON, self.OnStepperOff)
|
|
310
|
+ self.buttonOff.Enable(False)
|
|
311
|
+ panelActionSizer.AddSpacer(5)
|
|
312
|
+ panelActionSizer.Add(self.buttonOff, 0, wx.BOTTOM, 1)
|
|
313
|
+ self.buttonOff.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
314
|
+
|
|
315
|
+ # Status Gauge
|
|
316
|
+ self.gauge = wx.Gauge(parent = self.panelAction)
|
|
317
|
+ self.gauge.SetRange(BUSY_PROCESSING_STEPS)
|
|
318
|
+ panelActionSizer.Add(self.gauge, 1, wx.LEFT | wx.RIGHT, 5)
|
|
319
|
+ self.gauge.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
320
|
+
|
|
321
|
+ # Bottom panel
|
|
322
|
+ self.panelBottom = wx.Panel(self)
|
|
323
|
+ panelBottomSizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
324
|
+ self.panelBottom.SetSizer(panelBottomSizer)
|
|
325
|
+ outerPanelSizer.Add(self.panelBottom, 0, wx.EXPAND | wx.TOP, 5)
|
|
326
|
+ self.panelBottom.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
327
|
+
|
|
328
|
+ # Config panel
|
|
329
|
+ self.panelConfig = wx.Panel(self.panelBottom)
|
|
330
|
+ panelConfigSizer = wx.BoxSizer(wx.VERTICAL)
|
|
331
|
+ self.panelConfig.SetSizer(panelConfigSizer)
|
|
332
|
+ panelBottomSizer.Add(self.panelConfig, 2, wx.EXPAND | wx.LEFT, 5)
|
|
333
|
+ self.panelConfig.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
334
|
+
|
|
335
|
+ # Step Size Label
|
|
336
|
+ stepSizeLabel = wx.StaticText(self.panelConfig, label="Step:")
|
|
337
|
+ panelConfigSizer.Add(stepSizeLabel, 0, wx.LEFT, 5)
|
|
338
|
+ stepSizeLabel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
339
|
+
|
|
340
|
+ # Step Size Text Input
|
|
341
|
+ self.stepSize = wx.TextCtrl(parent = self.panelConfig, value="0.025", style = wx.TE_DONTWRAP | wx.TE_PROCESS_ENTER)
|
|
342
|
+ self.stepSize.SetMinSize((65, -1))
|
|
343
|
+ self.stepSize.Enable(False)
|
|
344
|
+ self.stepSize.Bind(wx.EVT_TEXT_ENTER, self.OnNewStepSize)
|
|
345
|
+ panelConfigSizer.Add(self.stepSize, 0, wx.LEFT | wx.TOP, 5)
|
|
346
|
+
|
|
347
|
+ # Bed Temperature Label
|
|
348
|
+ self.bedTemperatureLabel = wx.StaticText(self.panelConfig, label="Bed: ??.?")
|
|
349
|
+ panelConfigSizer.Add(self.bedTemperatureLabel, 0, wx.LEFT | wx.TOP, 5)
|
|
350
|
+ self.bedTemperatureLabel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
351
|
+
|
|
352
|
+ # Bed Temperature Text Input
|
|
353
|
+ self.bedTemperature = wx.TextCtrl(parent = self.panelConfig, value="0", style = wx.TE_DONTWRAP | wx.TE_PROCESS_ENTER)
|
|
354
|
+ self.bedTemperature.SetMinSize((65, -1))
|
|
355
|
+ self.bedTemperature.Enable(False)
|
|
356
|
+ self.bedTemperature.Bind(wx.EVT_TEXT_ENTER, self.OnNewBedTemperature)
|
|
357
|
+ panelConfigSizer.Add(self.bedTemperature, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 5)
|
|
358
|
+
|
|
359
|
+ # Control-Buttons Panel
|
|
360
|
+ self.panelControl = wx.Panel(self.panelBottom)
|
|
361
|
+ panelControlSizer = wx.BoxSizer(wx.VERTICAL)
|
|
362
|
+ self.panelControl.SetSizer(panelControlSizer)
|
|
363
|
+ panelBottomSizer.Add(self.panelControl, 2, wx.EXPAND | wx.LEFT, 5)
|
|
364
|
+ self.panelControl.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
365
|
+
|
|
366
|
+ # Up Button
|
|
367
|
+ self.buttonUp = wx.Button(parent = self.panelControl, label = "/\\")
|
|
368
|
+ self.buttonUp.Bind(wx.EVT_BUTTON, self.OnUp)
|
|
369
|
+ self.buttonUp.Enable(False)
|
|
370
|
+ panelControlSizer.AddStretchSpacer(1)
|
|
371
|
+ panelControlSizer.Add(self.buttonUp, 2, wx.ALIGN_CENTER_HORIZONTAL)
|
|
372
|
+ self.buttonUp.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
373
|
+
|
|
374
|
+ # Down Button
|
|
375
|
+ self.buttonDown = wx.Button(parent = self.panelControl, label = "\\/")
|
|
376
|
+ self.buttonDown.Bind(wx.EVT_BUTTON, self.OnDown)
|
|
377
|
+ self.buttonDown.Enable(False)
|
|
378
|
+ panelControlSizer.Add(self.buttonDown, 2, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5)
|
|
379
|
+ panelControlSizer.AddStretchSpacer(1)
|
|
380
|
+ self.buttonDown.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
381
|
+
|
|
382
|
+ # Level Status Panel
|
|
383
|
+ self.panelLevelStatus = wx.Panel(self.panelBottom)
|
|
384
|
+ panelLevelStatusSizer = wx.BoxSizer(wx.VERTICAL)
|
|
385
|
+ self.panelLevelStatus.SetSizer(panelLevelStatusSizer)
|
|
386
|
+ panelBottomSizer.Add(self.panelLevelStatus, 1, wx.EXPAND)
|
|
387
|
+ self.panelLevelStatus.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
388
|
+
|
|
389
|
+ # Level Status Gauge
|
|
390
|
+ self.gaugeLevel = wx.Gauge(parent = self.panelLevelStatus, style = wx.GA_VERTICAL)
|
|
391
|
+ self.gaugeLevel.SetRange(1)
|
|
392
|
+ self.gaugeLevel.SetValue(1)
|
|
393
|
+ panelLevelStatusSizer.Add(self.gaugeLevel, 1, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM | wx.LEFT, 10)
|
|
394
|
+ self.gaugeLevel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
395
|
+
|
|
396
|
+ # Level Panel
|
|
397
|
+ self.panelLevel = wx.Panel(self.panelBottom)
|
|
398
|
+ panelLevelSizer = wx.BoxSizer(wx.VERTICAL)
|
|
399
|
+ self.panelLevel.SetSizer(panelLevelSizer)
|
|
400
|
+ panelBottomSizer.Add(self.panelLevel, 2, wx.EXPAND)
|
|
401
|
+ self.panelLevel.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
402
|
+
|
|
403
|
+ # Start Button
|
|
404
|
+ self.buttonStart = wx.Button(parent = self.panelLevel, label = "Start")
|
|
405
|
+ self.buttonStart.Bind(wx.EVT_BUTTON, self.OnStart)
|
|
406
|
+ self.buttonStart.Enable(False)
|
|
407
|
+ panelLevelSizer.Add(self.buttonStart, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 5)
|
|
408
|
+ self.buttonStart.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
409
|
+
|
|
410
|
+ # Next Button
|
|
411
|
+ self.buttonNext = wx.Button(parent = self.panelLevel, label = "Next")
|
|
412
|
+ self.buttonNext.Bind(wx.EVT_BUTTON, self.OnNext)
|
|
413
|
+ self.buttonNext.Enable(False)
|
|
414
|
+ panelLevelSizer.Add(self.buttonNext, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 5)
|
|
415
|
+ self.buttonNext.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
416
|
+
|
|
417
|
+ self.buttonSave = wx.Button(parent = self.panelLevel, label = "Save")
|
|
418
|
+ self.buttonSave.Bind(wx.EVT_BUTTON, self.OnSave)
|
|
419
|
+ self.buttonSave.Enable(False)
|
|
420
|
+ panelLevelSizer.Add(self.buttonSave, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP | wx.BOTTOM, 5)
|
|
421
|
+ self.buttonSave.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
|
|
422
|
+
|
|
423
|
+ self.SetSizerAndFit(outerPanelSizer)
|
|
424
|
+ #self.SetSizer(outerPanelSizer)
|
|
425
|
+
|
|
426
|
+ def EnumerateSerialPorts(self):
|
|
427
|
+ # List all available serial ports
|
|
428
|
+ serialPortList = serial.tools.list_ports.comports()
|
|
429
|
+
|
|
430
|
+ # Fill Combobox, remember selected value
|
|
431
|
+ selection = self.comboBoxSerial.GetValue()
|
|
432
|
+ self.comboBoxSerial.Clear()
|
|
433
|
+ for port in serialPortList:
|
|
434
|
+ self.comboBoxSerial.Append(port[0])
|
|
435
|
+ if port[0] == selection:
|
|
436
|
+ self.comboBoxSerial.SetStringSelection(selection)
|
|
437
|
+
|
|
438
|
+ # Select first port if nothing is selected
|
|
439
|
+ if not selection:
|
|
440
|
+ self.comboBoxSerial.SetStringSelection(serialPortList[0][0])
|
|
441
|
+
|
|
442
|
+ self.panelSerial.Layout()
|
|
443
|
+
|
|
444
|
+ # Check every 5 seconds for new available serial ports
|
|
445
|
+ wx.FutureCall(POLL_SERIAL_PORTS_INTERVALL, self.EnumerateSerialPorts)
|
|
446
|
+
|
|
447
|
+ def EnableDisableUI(self):
|
|
448
|
+ if not self.isConnected:
|
|
449
|
+ self.buttonSerial.SetLabel("Connect")
|
|
450
|
+ self.labelX.SetLabel("X: ??.??")
|
|
451
|
+ self.labelY.SetLabel("Y: ??.??")
|
|
452
|
+ self.labelZ.SetLabel("Z: ??.??")
|
|
453
|
+ self.labelZc.SetLabel("Zc: ??.???")
|
|
454
|
+ else:
|
|
455
|
+ self.buttonSerial.SetLabel("Disconnect")
|
|
456
|
+
|
|
457
|
+ self.panelStatus.Layout()
|
|
458
|
+ self.panelSerial.Layout()
|
|
459
|
+
|
|
460
|
+ self.comboBoxSerial.Enable(not self.isConnected)
|
|
461
|
+ self.comboBoxBaud.Enable(not self.isConnected)
|
|
462
|
+
|
|
463
|
+ self.buttonHome.Enable(self.isConnected)
|
|
464
|
+ self.buttonOff.Enable(self.isConnected)
|
|
465
|
+ self.buttonUp.Enable(self.isConnected)
|
|
466
|
+ self.buttonDown.Enable(self.isConnected)
|
|
467
|
+ self.stepSize.Enable(self.isConnected)
|
|
468
|
+ self.bedTemperature.Enable(self.isConnected)
|
|
469
|
+
|
|
470
|
+ self.buttonStart.Enable(False)
|
|
471
|
+ self.buttonNext.Enable(False)
|
|
472
|
+ self.buttonSave.Enable(False)
|
|
473
|
+ if self.isConnected:
|
|
474
|
+ if not self.isLeveling:
|
|
475
|
+ self.buttonSave.Enable(True)
|
|
476
|
+ if self.meshPoints > 0:
|
|
477
|
+ self.buttonStart.Enable(True)
|
|
478
|
+
|
|
479
|
+ if self.isLeveling:
|
|
480
|
+ self.buttonNext.Enable(True)
|
|
481
|
+
|
|
482
|
+ def OnSave(self, event):
|
|
483
|
+ if self.isLeveling:
|
|
484
|
+ print "Leveling is already in progress!"
|
|
485
|
+ return
|
|
486
|
+
|
|
487
|
+ self.protocol.write_line(GCODE_EEPROM_SAVE)
|
|
488
|
+
|
|
489
|
+ def OnStart(self, event):
|
|
490
|
+ if self.isLeveling:
|
|
491
|
+ print "Leveling is already in progress!"
|
|
492
|
+ return
|
|
493
|
+
|
|
494
|
+ self.isLeveling = True
|
|
495
|
+ self.currentPoint = 0
|
|
496
|
+ self.gauge.SetValue(0)
|
|
497
|
+ self.protocol.write_line(GCODE_MESH_START)
|
|
498
|
+ self.gaugeLevel.SetValue(0)
|
|
499
|
+ self.EnableDisableUI()
|
|
500
|
+
|
|
501
|
+ def OnNext(self, event):
|
|
502
|
+ if not self.isLeveling:
|
|
503
|
+ print "Start leveling before proceeding!"
|
|
504
|
+ return
|
|
505
|
+
|
|
506
|
+ self.gauge.SetValue(0)
|
|
507
|
+ self.protocol.write_line(GCODE_MESH_NEXT)
|
|
508
|
+ self.currentPoint += 1
|
|
509
|
+ if self.currentPoint >= self.meshPoints:
|
|
510
|
+ self.isLeveling = False
|
|
511
|
+ self.gaugeLevel.SetValue(self.currentPoint)
|
|
512
|
+ self.EnableDisableUI()
|
|
513
|
+ self.currentZ = self.startZ
|
|
514
|
+ self.labelZc.SetLabel("Zc: {}".format(self.currentZ))
|
|
515
|
+ self.panelStatus.Layout()
|
|
516
|
+
|
|
517
|
+ def OnNewStepSize(self, event):
|
|
518
|
+ print "New Step Size: {}".format(self.stepSize.GetValue())
|
|
519
|
+ self.step = float(self.stepSize.GetValue())
|
|
520
|
+
|
|
521
|
+ def OnKeyDown(self, event):
|
|
522
|
+ key = event.GetKeyCode()
|
|
523
|
+
|
|
524
|
+ if self.isConnected:
|
|
525
|
+ if key == wx.WXK_DOWN:
|
|
526
|
+ self.OnDown(event)
|
|
527
|
+ elif key == wx.WXK_UP:
|
|
528
|
+ self.OnUp(event)
|
|
529
|
+ elif key == wx.WXK_RIGHT:
|
|
530
|
+ self.OnNext(event)
|
|
531
|
+
|
|
532
|
+ def OnUp(self, event):
|
|
533
|
+ self.currentZ += self.step
|
|
534
|
+ print "Moving up to {}".format(self.currentZ)
|
|
535
|
+ self.gauge.SetValue(0)
|
|
536
|
+ self.protocol.write_line(GCODE_MOVE_Z.format(self.currentZ))
|
|
537
|
+ self.labelZc.SetLabel("Zc: {}".format(self.currentZ))
|
|
538
|
+ self.panelStatus.Layout()
|
|
539
|
+
|
|
540
|
+ def OnDown(self, event):
|
|
541
|
+ if self.currentZ <= 0.0:
|
|
542
|
+ print "Can't move further down!"
|
|
543
|
+ return
|
|
544
|
+
|
|
545
|
+ self.currentZ -= self.step
|
|
546
|
+ print "Moving down to {}".format(self.currentZ)
|
|
547
|
+ self.gauge.SetValue(0)
|
|
548
|
+ self.protocol.write_line(GCODE_MOVE_Z.format(self.currentZ))
|
|
549
|
+ self.labelZc.SetLabel("Zc: {}".format(self.currentZ))
|
|
550
|
+ self.panelStatus.Layout()
|
|
551
|
+
|
|
552
|
+ def OnStepperOff(self, event):
|
|
553
|
+ self.gauge.SetValue(0)
|
|
554
|
+ self.protocol.write_line(GCODE_STOP_IDLE_HOLD)
|
|
555
|
+
|
|
556
|
+ def OnHome(self, event):
|
|
557
|
+ self.gauge.SetValue(0)
|
|
558
|
+ self.protocol.write_line(GCODE_MOVE_TO_ORIGIN)
|
|
559
|
+
|
|
560
|
+ def OnStatus(self, event):
|
|
561
|
+ if event.GetValue() == 0:
|
|
562
|
+ self.OnDone(event)
|
|
563
|
+ else:
|
|
564
|
+ self.OnBusy(event)
|
|
565
|
+
|
|
566
|
+ def OnBusy(self, event):
|
|
567
|
+ val = self.gauge.GetValue()
|
|
568
|
+ if val < self.gauge.GetRange():
|
|
569
|
+ self.gauge.SetValue(val + 1)
|
|
570
|
+
|
|
571
|
+ def OnDone(self, event):
|
|
572
|
+ self.gauge.SetValue(self.gauge.GetRange())
|
|
573
|
+
|
|
574
|
+ def PollMeshData(self):
|
|
575
|
+ if self.isConnected:
|
|
576
|
+ self.protocol.write_line(GCODE_MESH_INFO)
|
|
577
|
+
|
|
578
|
+ def PollMeshDataRetry(self):
|
|
579
|
+ if self.meshPoints <= 0:
|
|
580
|
+ self.PollMeshData()
|
|
581
|
+ wx.FutureCall(MESH_DATA_RETRY_TIMEOUT, self.PollMeshDataRetry)
|
|
582
|
+
|
|
583
|
+ def OnMeshPoints(self, event):
|
|
584
|
+ x = int(event.GetX())
|
|
585
|
+ y = int(event.GetY())
|
|
586
|
+ self.meshPoints = x * y
|
|
587
|
+ print "Mesh Points: X: {} Y: {} --> {}".format(x, y, self.meshPoints)
|
|
588
|
+ self.gaugeLevel.SetRange(self.meshPoints)
|
|
589
|
+ self.gaugeLevel.SetValue(0)
|
|
590
|
+ self.EnableDisableUI()
|
|
591
|
+
|
|
592
|
+ def OnMeshZ(self, event):
|
|
593
|
+ self.startZ = float(event.GetZ())
|
|
594
|
+ self.currentZ = float(event.GetZ())
|
|
595
|
+ self.labelZc.SetLabel("Zc: {}".format(self.currentZ))
|
|
596
|
+ self.gauge.SetValue(BUSY_PROCESSING_STEPS)
|
|
597
|
+ print "Mesh starting height: {}".format(self.currentZ)
|
|
598
|
+
|
|
599
|
+ def PollPosition(self):
|
|
600
|
+ if self.isConnected:
|
|
601
|
+ #if not self.isLeveling:
|
|
602
|
+ self.protocol.write_line(GCODE_GET_CURRENT_POSITION)
|
|
603
|
+ wx.FutureCall(POLL_COORDINATES_INTERVALL, self.PollPosition)
|
|
604
|
+
|
|
605
|
+ def OnCoordinates(self, event):
|
|
606
|
+ data = event.GetValue()
|
|
607
|
+ print "New coordinates {}".format(data)
|
|
608
|
+ afterX = data.split("X: ")[1]
|
|
609
|
+ x = afterX.split(" ")[0]
|
|
610
|
+ afterY = data.split("Y: ")[1]
|
|
611
|
+ y = afterY.split(" ")[0]
|
|
612
|
+ afterZ = data.split("Z: ")[1]
|
|
613
|
+ z = afterZ.split(" ")[0]
|
|
614
|
+
|
|
615
|
+ self.labelX.SetLabel("X: {}".format(x))
|
|
616
|
+ self.labelY.SetLabel("Y: {}".format(y))
|
|
617
|
+ self.labelZ.SetLabel("Z: {}".format(z))
|
|
618
|
+
|
|
619
|
+ self.panelStatus.Layout()
|
|
620
|
+
|
|
621
|
+ def OnTemperature(self, event):
|
|
622
|
+ print "Current Bed Temperature: {}".format(event.GetBed())
|
|
623
|
+ self.bedTemperatureLabel.SetLabel("Bed: {}".format(event.GetBed()))
|
|
624
|
+
|
|
625
|
+ def OnNewBedTemperature(self, event):
|
|
626
|
+ self.protocol.write_line(GCODE_SET_BED_TEMP.format(float(self.bedTemperature.GetValue())))
|
|
627
|
+
|
|
628
|
+ def PollTemperature(self):
|
|
629
|
+ if self.isConnected:
|
|
630
|
+ self.protocol.write_line(GCODE_GET_TEMP)
|
|
631
|
+ wx.FutureCall(POLL_TEMPERATURE_INTERVALL, self.PollTemperature)
|
|
632
|
+
|
|
633
|
+ def OnConnectDisconnect(self, Event):
|
|
634
|
+ self.isLeveling = False
|
|
635
|
+ self.currentPoint = 0
|
|
636
|
+ if self.isConnected:
|
|
637
|
+ self.thread.close()
|
|
638
|
+ self.serial.close()
|
|
639
|
+ self.isConnected = False
|
|
640
|
+ self.gauge.SetValue(0)
|
|
641
|
+ self.gaugeLevel.SetRange(1)
|
|
642
|
+ self.gaugeLevel.SetValue(1)
|
|
643
|
+ self.EnableDisableUI()
|
|
644
|
+
|
|
645
|
+ # Recreate thread, so we can start it again next time
|
|
646
|
+ self.thread = serial.threaded.ReaderThread(self.serial, GCodeReader)
|
|
647
|
+ else:
|
|
648
|
+ self.serial.baudrate = self.comboBoxBaud.GetValue()
|
|
649
|
+ self.serial.port = self.comboBoxSerial.GetValue()
|
|
650
|
+ try:
|
|
651
|
+ self.serial.open()
|
|
652
|
+ except serial.SerialException as msg:
|
|
653
|
+ wx.MessageBox("SerialException: {}".format(msg), "Error", wx.OK | wx.ICON_ERROR)
|
|
654
|
+ except OSError as msg:
|
|
655
|
+ wx.MessageBox("OSError: {}".format(msg), "Error", wx.OK | wx.ICON_ERROR)
|
|
656
|
+ if self.serial.isOpen():
|
|
657
|
+ self.isConnected = True
|
|
658
|
+ self.thread.start()
|
|
659
|
+ self.transport, self.protocol = self.thread.connect()
|
|
660
|
+ self.protocol.setParent(self)
|
|
661
|
+ self.meshPoints = -1
|
|
662
|
+ self.EnableDisableUI()
|
|
663
|
+ self.PollPosition()
|
|
664
|
+ self.PollMeshData()
|
|
665
|
+ self.PollTemperature()
|
|
666
|
+ wx.FutureCall(MESH_DATA_RETRY_TIMEOUT, self.PollMeshDataRetry)
|
|
667
|
+
|
|
668
|
+ def OnQuit(self, Event):
|
|
669
|
+ if not self.hasQuit:
|
|
670
|
+ self.hasQuit = True
|
|
671
|
+ if self.isConnected:
|
|
672
|
+ print("Closing serial port...")
|
|
673
|
+ self.thread.close()
|
|
674
|
+ self.serial.close()
|
|
675
|
+ self.isConnected = False
|
|
676
|
+ self.Destroy()
|
|
677
|
+
|
|
678
|
+# App wrapper class
|
|
679
|
+class WizardApp(wx.App):
|
|
680
|
+ def __init__(self, *args, **kwargs):
|
|
681
|
+ wx.App.__init__(self, *args, **kwargs)
|
|
682
|
+ self.Bind(wx.EVT_ACTIVATE_APP, self.OnActivate)
|
|
683
|
+
|
|
684
|
+ def OnInit(self):
|
|
685
|
+ frame = WizardFrame(parent = None, title = "Bed Leveling Wizard", style = wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN)
|
|
686
|
+ frame.Centre()
|
|
687
|
+ frame.Show()
|
|
688
|
+ return True
|
|
689
|
+
|
|
690
|
+ def BringWindowToFront(self):
|
|
691
|
+ self.GetTopWindow().Raise()
|
|
692
|
+
|
|
693
|
+ def OnActivate(self, event):
|
|
694
|
+ if event.GetActive():
|
|
695
|
+ self.BringWindowToFront()
|
|
696
|
+ event.Skip()
|
|
697
|
+
|
|
698
|
+ def MacReopenApp(self):
|
|
699
|
+ self.BringWindowToFront()
|
|
700
|
+
|
|
701
|
+def main():
|
|
702
|
+ app = WizardApp(False)
|
|
703
|
+ app.MainLoop()
|
|
704
|
+
|
|
705
|
+if __name__ == '__main__':
|
|
706
|
+ main()
|
|
707
|
+
|