Wed, 20 Jan 2021 11:37:03 +0100
reimplemented lasercutter changes
48 | 1 | # FILE MODIFIED BY NEOSOFT - MALTE DI DONATO |
2 | # Embed Lasercut functions from laser.py | |
3 | from . import laser | |
4 | try: | |
5 | from . import module_watcher | |
6 | mw = module_watcher.ModuleWatcher() | |
7 | mw.watch_module('laser') | |
8 | mw.start_watching() | |
9 | except Exception, e: | |
10 | print e | |
11 | print "ModuleWatcher not loaded, skipping autoreloading of changed modules" | |
12 | ||
15 | 13 | # This file is part of the Printrun suite. |
14 | # | |
15 | # Printrun is free software: you can redistribute it and/or modify | |
16 | # it under the terms of the GNU General Public License as published by | |
17 | # the Free Software Foundation, either version 3 of the License, or | |
18 | # (at your option) any later version. | |
19 | # | |
20 | # Printrun is distributed in the hope that it will be useful, | |
21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
23 | # GNU General Public License for more details. | |
24 | # | |
25 | # You should have received a copy of the GNU General Public License | |
26 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
27 | ||
28 | import os | |
47 | 29 | import platform |
30 | import queue | |
15 | 31 | import sys |
32 | import time | |
33 | import threading | |
34 | import traceback | |
47 | 35 | import io as StringIO |
15 | 36 | import subprocess |
37 | import glob | |
38 | import logging | |
47 | 39 | import re |
15 | 40 | |
41 | try: import simplejson as json | |
42 | except ImportError: import json | |
43 | ||
44 | from . import pronsole | |
45 | from . import printcore | |
47 | 46 | from printrun.spoolmanager import spoolmanager_gui |
15 | 47 | |
48 | from .utils import install_locale, setup_logging, dosify, \ | |
49 | iconfile, configfile, format_time, format_duration, \ | |
50 | hexcolor_to_float, parse_temperature_report, \ | |
47 | 51 | prepare_command, check_rgb_color, check_rgba_color, compile_file, \ |
52 | write_history_to, read_history_from | |
15 | 53 | install_locale('pronterface') |
54 | ||
55 | try: | |
56 | import wx | |
47 | 57 | import wx.adv |
58 | if wx.VERSION < (4,): | |
59 | raise ImportError() | |
15 | 60 | except: |
47 | 61 | logging.error(_("WX >= 4 is not installed. This program requires WX >= 4 to run.")) |
15 | 62 | raise |
63 | ||
64 | from .gui.widgets import SpecialButton, MacroEditor, PronterOptions, ButtonEdit | |
65 | ||
66 | winsize = (800, 500) | |
67 | layerindex = 0 | |
68 | if os.name == "nt": | |
69 | winsize = (800, 530) | |
70 | ||
71 | pronterface_quitting = False | |
72 | ||
73 | class PronterfaceQuitException(Exception): | |
74 | pass | |
75 | ||
76 | from .gui import MainWindow | |
77 | from .settings import wxSetting, HiddenSetting, StringSetting, SpinSetting, \ | |
47 | 78 | FloatSpinSetting, BooleanSetting, StaticTextSetting, ColorSetting, ComboSetting |
15 | 79 | from printrun import gcoder |
80 | from .pronsole import REPORT_NONE, REPORT_POS, REPORT_TEMP, REPORT_MANUAL | |
81 | ||
47 | 82 | def format_length(mm, fractional=2): |
83 | if mm <= 10: | |
84 | units = mm | |
85 | suffix = 'mm' | |
86 | elif mm < 1000: | |
87 | units = mm / 10 | |
88 | suffix = 'cm' | |
89 | else: | |
90 | units = mm / 1000 | |
91 | suffix = 'm' | |
92 | return '%%.%df' % fractional % units + suffix | |
93 | ||
94 | class ConsoleOutputHandler: | |
15 | 95 | """Handle console output. All messages go through the logging submodule. We setup a logging handler to get logged messages and write them to both stdout (unless a log file path is specified, in which case we add another logging handler to write to this file) and the log panel. |
96 | We also redirect stdout and stderr to ourself to catch print messages and al.""" | |
97 | ||
98 | def __init__(self, target, log_path): | |
99 | self.stdout = sys.stdout | |
100 | self.stderr = sys.stderr | |
101 | sys.stdout = self | |
102 | sys.stderr = self | |
47 | 103 | self.print_on_stdout = not log_path |
15 | 104 | if log_path: |
105 | setup_logging(self, log_path, reset_handlers = True) | |
106 | else: | |
47 | 107 | setup_logging(sys.stdout, reset_handlers = True) |
108 | self.target = target | |
15 | 109 | |
110 | def __del__(self): | |
111 | sys.stdout = self.stdout | |
112 | sys.stderr = self.stderr | |
113 | ||
114 | def write(self, data): | |
115 | try: | |
116 | self.target(data) | |
117 | except: | |
118 | pass | |
119 | if self.print_on_stdout: | |
120 | self.stdout.write(data) | |
121 | ||
122 | def flush(self): | |
123 | if self.stdout: | |
124 | self.stdout.flush() | |
125 | ||
126 | class PronterWindow(MainWindow, pronsole.pronsole): | |
127 | ||
128 | _fgcode = None | |
47 | 129 | printer_progress_time = time.time() |
15 | 130 | |
131 | def _get_fgcode(self): | |
132 | return self._fgcode | |
133 | ||
134 | def _set_fgcode(self, value): | |
135 | self._fgcode = value | |
136 | self.excluder = None | |
137 | self.excluder_e = None | |
138 | self.excluder_z_abs = None | |
139 | self.excluder_z_rel = None | |
140 | fgcode = property(_get_fgcode, _set_fgcode) | |
141 | ||
142 | def _get_display_graph(self): | |
143 | return self.settings.tempgraph | |
144 | display_graph = property(_get_display_graph) | |
145 | ||
146 | def _get_display_gauges(self): | |
147 | return self.settings.tempgauges | |
148 | display_gauges = property(_get_display_gauges) | |
149 | ||
150 | def __init__(self, app, filename = None, size = winsize): | |
151 | pronsole.pronsole.__init__(self) | |
152 | self.app = app | |
153 | self.window_ready = False | |
154 | self.ui_ready = False | |
155 | self._add_settings(size) | |
156 | ||
157 | self.pauseScript = None #"pause.gcode" | |
158 | self.endScript = None #"end.gcode" | |
159 | ||
160 | self.filename = filename | |
161 | ||
162 | self.capture_skip = {} | |
163 | self.capture_skip_newline = False | |
164 | self.fgcode = None | |
165 | self.excluder = None | |
166 | self.slicep = None | |
167 | self.current_pos = [0, 0, 0] | |
168 | self.paused = False | |
169 | self.uploading = False | |
47 | 170 | self.sentglines = queue.Queue(0) |
15 | 171 | self.cpbuttons = { |
172 | "motorsoff": SpecialButton(_("Motors off"), ("M84"), (250, 250, 250), _("Switch all motors off")), | |
173 | "extrude": SpecialButton(_("Extrude"), ("pront_extrude"), (225, 200, 200), _("Advance extruder by set length")), | |
174 | "reverse": SpecialButton(_("Reverse"), ("pront_reverse"), (225, 200, 200), _("Reverse extruder by set length")), | |
175 | } | |
176 | self.custombuttons = [] | |
177 | self.btndict = {} | |
178 | self.filehistory = None | |
179 | self.autoconnect = False | |
47 | 180 | self.autoscrolldisable = False |
181 | ||
15 | 182 | self.parse_cmdline(sys.argv[1:]) |
47 | 183 | for field in dir(self.settings): |
184 | if field.startswith("_gcview_color_"): | |
185 | cleanname = field[1:] | |
186 | color = hexcolor_to_float(getattr(self.settings, cleanname), 4) | |
187 | setattr(self, cleanname, list(color)) | |
15 | 188 | |
189 | # FIXME: We need to initialize the main window after loading the | |
190 | # configs to restore the size, but this might have some unforeseen | |
191 | # consequences. | |
192 | # -- Okai, it seems it breaks things like update_gviz_params >< | |
193 | os.putenv("UBUNTU_MENUPROXY", "0") | |
194 | size = (self.settings.last_window_width, self.settings.last_window_height) | |
195 | MainWindow.__init__(self, None, title = _("Pronterface"), size = size) | |
196 | if self.settings.last_window_maximized: | |
197 | self.Maximize() | |
198 | self.SetIcon(wx.Icon(iconfile("pronterface.png"), wx.BITMAP_TYPE_PNG)) | |
199 | self.Bind(wx.EVT_SIZE, self.on_resize) | |
200 | self.Bind(wx.EVT_MAXIMIZE, self.on_maximize) | |
201 | self.window_ready = True | |
47 | 202 | self.Bind(wx.EVT_CLOSE, self.closewin) |
203 | self.Bind(wx.EVT_CHAR_HOOK, self.on_key) | |
15 | 204 | # set feedrates in printcore for pause/resume |
205 | self.p.xy_feedrate = self.settings.xy_feedrate | |
206 | self.p.z_feedrate = self.settings.z_feedrate | |
207 | ||
208 | self.panel.SetBackgroundColour(self.bgcolor) | |
209 | customdict = {} | |
210 | try: | |
47 | 211 | exec(compile_file(configfile("custombtn.txt")), customdict) |
15 | 212 | if len(customdict["btns"]): |
213 | if not len(self.custombuttons): | |
214 | try: | |
215 | self.custombuttons = customdict["btns"] | |
47 | 216 | for n in range(len(self.custombuttons)): |
15 | 217 | self.cbutton_save(n, self.custombuttons[n]) |
218 | os.rename("custombtn.txt", "custombtn.old") | |
219 | rco = open("custombtn.txt", "w") | |
220 | rco.write(_("# I moved all your custom buttons into .pronsolerc.\n# Please don't add them here any more.\n# Backup of your old buttons is in custombtn.old\n")) | |
221 | rco.close() | |
47 | 222 | except IOError as x: |
15 | 223 | logging.error(str(x)) |
224 | else: | |
225 | logging.warning(_("Note!!! You have specified custom buttons in both custombtn.txt and .pronsolerc")) | |
226 | logging.warning(_("Ignoring custombtn.txt. Remove all current buttons to revert to custombtn.txt")) | |
227 | ||
228 | except: | |
229 | pass | |
47 | 230 | self.menustrip = wx.MenuBar() |
15 | 231 | self.reload_ui() |
232 | # disable all printer controls until we connect to a printer | |
233 | self.gui_set_disconnected() | |
234 | self.statusbar = self.CreateStatusBar() | |
235 | self.statusbar.SetStatusText(_("Not connected to printer.")) | |
236 | ||
237 | self.t = ConsoleOutputHandler(self.catchprint, self.settings.log_path) | |
238 | self.stdout = sys.stdout | |
239 | self.slicing = False | |
240 | self.loading_gcode = False | |
241 | self.loading_gcode_message = "" | |
242 | self.mini = False | |
243 | self.p.sendcb = self.sentcb | |
244 | self.p.preprintsendcb = self.preprintsendcb | |
245 | self.p.printsendcb = self.printsentcb | |
246 | self.p.startcb = self.startcb | |
247 | self.p.endcb = self.endcb | |
248 | self.cur_button = None | |
249 | self.predisconnect_mainqueue = None | |
250 | self.predisconnect_queueindex = None | |
251 | self.predisconnect_layer = None | |
252 | self.hsetpoint = 0.0 | |
253 | self.bsetpoint = 0.0 | |
254 | if self.autoconnect: | |
255 | self.connect() | |
256 | if self.filename is not None: | |
257 | self.do_load(self.filename) | |
258 | if self.settings.monitor: | |
259 | self.update_monitor() | |
260 | ||
261 | # -------------------------------------------------------------- | |
48 | 262 | # Lasercutter methods |
263 | # -------------------------------------------------------------- | |
264 | ||
265 | def on_lc_printfile(self, event): | |
266 | # lc print button | |
267 | self.log("Priming Z axis to initial focus") | |
268 | line = self.precmd("G1 Z%.2f" % (self.settings.lc_z_focus + self.lc_material_thickness.GetValue())) | |
269 | wx.CallAfter(self.onecmd, line) | |
270 | self.lc_printing = True | |
271 | wx.CallAfter(self.printfile, None) | |
272 | ||
273 | def endcb_lasercut(self): | |
274 | # LASERCUT: Now check if we should do another print pass? | |
275 | self.log("event: endcb_lasercut") | |
276 | if self.lc_printing: | |
277 | self.log(" -> checking if something to do...") | |
278 | pass_count = self.lc_pass_count.GetValue() | |
279 | if pass_count > 1: | |
280 | time.sleep(0.5) | |
281 | if self.pass_current < pass_count: | |
282 | self.pass_current += 1 | |
283 | self.log("Starting lasercut pass # %i of %i" % (self.pass_current, pass_count)) | |
284 | if self.lc_pass_zdiff.GetValue() != 0: | |
285 | # move Z focus | |
286 | new_z = self.settings.lc_z_focus + self.lc_material_thickness.GetValue() + ( | |
287 | self.lc_pass_zdiff.GetValue() * (self.pass_current - 1)) | |
288 | self.log("Re-Positioning laser focus by %.1f mm to %.1f" % (self.lc_pass_zdiff.GetValue(), new_z)) | |
289 | line = self.precmd("G1 Z%.2f" % (new_z)) | |
290 | self.onecmd(line) | |
291 | time.sleep(0.5) | |
292 | ||
293 | # "click" print button again | |
294 | tmp = self.pass_current | |
295 | self.printfile(None) | |
296 | self.pass_current = tmp | |
297 | else: | |
298 | self.lc_printing = False | |
299 | wx.CallAfter(self.lc_printbtn.Enable) | |
300 | wx.CallAfter(self.lc_printbtn.SetLabel, _("Start cutting")) | |
301 | ||
302 | self.log("Resetting Z axis to initial focus") | |
303 | line = self.precmd("G1 Z%.2f" % (self.settings.lc_z_focus + self.lc_material_thickness.GetValue())) | |
304 | self.onecmd(line) | |
305 | else: | |
306 | self.lc_printing = False | |
307 | wx.CallAfter(self.lc_printbtn.Enable) | |
308 | wx.CallAfter(self.lc_printbtn.SetLabel, _("Start cutting")) | |
309 | ||
310 | ||
311 | def update_lc_settings(self, key, value): | |
312 | return True | |
313 | ||
314 | def _lc_add_settings(self, size): | |
315 | # first add the lasercutter options | |
316 | self.settings._add(StaticTextSetting("separator_lc_general", "General laser settings", "", group = "Laser")) | |
317 | self.settings._add(BooleanSetting("lc_melzi_hack", False, "Use Melzi M571 Hack instead M3/M5", "no description :)", "Laser"), self.update_lc_settings) | |
318 | self.settings._add(SpinSetting("lc_travel_speed", 120, 1, 300, "Travel speed in mm/s", "", "Laser"), self.update_lc_settings) | |
319 | self.settings._add(SpinSetting("lc_engrave_speed", 10, 1, 300, "Engrave speed in mm/s", "", "Laser"), self.update_lc_settings) | |
320 | self.settings._add(SpinSetting("lc_z_focus", 16, -80, 80, "Laser Z focus position", "", "Laser"), self.update_lc_settings) | |
321 | self.settings._add(SpinSetting("lc_pass_count", 1, 0, 20, "Default Number of cutting passes", "", "Laser"), self.reload_ui) | |
322 | self.settings._add(FloatSpinSetting("lc_pass_zdiff", -0.25, -2.0, 2.0, "Default Z movement after each cut", "", "Laser"), self.reload_ui) | |
323 | self.settings._add(FloatSpinSetting("lc_material_thickness", 4.0, 0.0, 80.0, "Default Material Thickness", "", "Laser"), self.reload_ui) | |
324 | ||
325 | self.settings._add(StaticTextSetting("separator_lc_bitmap", "PNG Bitmap processing", "", group = "Laser")) | |
326 | self.settings._add(FloatSpinSetting("lc_bitmap_speed_factor", 1.0, 0.1, 2.0, "Engrave speed factor", "", "Laser"), self.update_lc_settings) | |
327 | self.settings._add(SpinSetting("lc_dpi", 300, 25, 600, "Image DPI", "Image resolution for scaling", "Laser"), self.update_lc_settings) | |
328 | self.settings._add(SpinSetting("lc_grey_threshold", 0, 0, 255, "Grey threshold value for RGB", "", "Laser"), self.update_lc_settings) | |
329 | self.settings._add(BooleanSetting("lc_invert_cut", True, "PNG: Invert grey threshold", "Invert laser on/off logic", "Laser"), self.update_lc_settings) | |
330 | self.settings._add(BooleanSetting("lc_change_dir", True, "PNG: Change direction", "Engrave in both directions on Y Axis", "Laser"), self.update_lc_settings) | |
331 | ||
332 | self.settings._add(StaticTextSetting("separator_lc_hpgl", "HPGL processing", "", group = "Laser")) | |
333 | self.settings._add(FloatSpinSetting("lc_hpgl_speed_factor", 1.0, 0.1, 2.0, "Engrave speed factor", "", "Laser"), self.update_lc_settings) | |
334 | ||
335 | self.settings._add(StaticTextSetting("separator_lc_svg", "SVG processing", "", group = "Laser")) | |
336 | self.settings._add(FloatSpinSetting("lc_svg_speed_factor", 1.0, 0.1, 2.0, "Engrave speed factor", "", "Laser"), self.update_lc_settings) | |
337 | self.settings._add(FloatSpinSetting("lc_svg_smoothness", 0.2, 0.1, 10.0, "Smoothness", "Smoothness of curves (smaller value = smoother curve)", "Laser"), self.update_lc_settings) | |
338 | self.settings._add(SpinSetting("lc_svg_width", 50, 1, 9999, "Width (mm)", "Image width", "Laser"), self.update_lc_settings) | |
339 | self.settings._add(SpinSetting("lc_svg_height", 50, 1, 9999, "Height (mm)", "Image height", "Laser"), self.update_lc_settings) | |
340 | self.settings._add(ComboSetting("lc_svg_scalemode", "original", ["original", "scale", "stretch"], "Scaling mode", "scale/stretch to above dimensions", "Laser"), self.update_lc_settings) | |
341 | self.settings._add(BooleanSetting("lc_svg_offset", True, "Calculate offset to X=0, Y=0", "If enabled, move image to origin position", "Laser"), self.update_lc_settings) | |
342 | ||
343 | ||
344 | # -------------------------------------------------------------- | |
15 | 345 | # Main interface handling |
346 | # -------------------------------------------------------------- | |
347 | ||
348 | def reset_ui(self): | |
349 | MainWindow.reset_ui(self) | |
350 | self.custombuttons_widgets = [] | |
351 | ||
352 | def reload_ui(self, *args): | |
353 | if not self.window_ready: return | |
47 | 354 | temp_monitor = self.settings.monitor |
355 | self.settings.monitor = False | |
356 | self.update_monitor() | |
15 | 357 | self.Freeze() |
358 | ||
359 | # If UI is being recreated, delete current one | |
360 | if self.ui_ready: | |
361 | # Store log console content | |
362 | logcontent = self.logbox.GetValue() | |
47 | 363 | self.menustrip.SetMenus([]) |
364 | if len(self.commandbox.history): | |
365 | #save current command box history | |
366 | if not os.path.exists(self.history_file): | |
367 | if not os.path.exists(self.cache_dir): | |
368 | os.makedirs(self.cache_dir) | |
369 | write_history_to(self.history_file, self.commandbox.history) | |
15 | 370 | # Create a temporary panel to reparent widgets with state we want |
371 | # to retain across UI changes | |
372 | temppanel = wx.Panel(self) | |
373 | # TODO: add viz widgets to statefulControls | |
374 | for control in self.statefulControls: | |
375 | control.GetContainingSizer().Detach(control) | |
376 | control.Reparent(temppanel) | |
377 | self.panel.DestroyChildren() | |
378 | self.gwindow.Destroy() | |
379 | self.reset_ui() | |
380 | ||
381 | # Create UI | |
47 | 382 | self.create_menu() |
383 | self.update_recent_files("recentfiles", self.settings.recentfiles) | |
384 | self.splitterwindow = None | |
15 | 385 | if self.settings.uimode in (_("Tabbed"), _("Tabbed with platers")): |
386 | self.createTabbedGui() | |
387 | else: | |
388 | self.createGui(self.settings.uimode == _("Compact"), | |
389 | self.settings.controlsmode == "Mini") | |
390 | ||
47 | 391 | if self.splitterwindow: |
15 | 392 | self.splitterwindow.SetSashPosition(self.settings.last_sash_position) |
393 | ||
394 | def splitter_resize(event): | |
395 | self.splitterwindow.UpdateSize() | |
396 | self.splitterwindow.Bind(wx.EVT_SIZE, splitter_resize) | |
397 | ||
398 | def sash_position_changed(event): | |
399 | self.set("last_sash_position", self.splitterwindow.GetSashPosition()) | |
400 | self.splitterwindow.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGED, sash_position_changed) | |
401 | ||
402 | # Set gcview parameters here as they don't get set when viewers are | |
403 | # created | |
404 | self.update_gcview_params() | |
405 | ||
406 | # Finalize | |
47 | 407 | if self.p.online: |
15 | 408 | self.gui_set_connected() |
409 | if self.ui_ready: | |
410 | self.logbox.SetValue(logcontent) | |
411 | temppanel.Destroy() | |
412 | self.panel.Layout() | |
413 | if self.fgcode: | |
414 | self.start_viz_thread() | |
415 | self.ui_ready = True | |
47 | 416 | self.settings.monitor = temp_monitor |
417 | self.commandbox.history = read_history_from(self.history_file) | |
418 | self.commandbox.histindex = len(self.commandbox.history) | |
15 | 419 | self.Thaw() |
47 | 420 | if self.settings.monitor: |
421 | self.update_monitor() | |
15 | 422 | |
423 | def on_resize(self, event): | |
424 | wx.CallAfter(self.on_resize_real) | |
425 | event.Skip() | |
426 | ||
427 | def on_resize_real(self): | |
428 | maximized = self.IsMaximized() | |
429 | self.set("last_window_maximized", maximized) | |
430 | if not maximized and not self.IsIconized(): | |
431 | size = self.GetSize() | |
432 | self.set("last_window_width", size[0]) | |
433 | self.set("last_window_height", size[1]) | |
434 | ||
435 | def on_maximize(self, event): | |
436 | self.set("last_window_maximized", self.IsMaximized()) | |
437 | event.Skip() | |
438 | ||
439 | def on_exit(self, event): | |
440 | self.Close() | |
441 | ||
47 | 442 | def on_settings_change(self, changed_settings): |
443 | if self.gviz: | |
444 | self.gviz.on_settings_change(changed_settings) | |
445 | ||
446 | def on_key(self, event): | |
447 | if not isinstance(event.EventObject, (wx.TextCtrl, wx.ComboBox)) \ | |
448 | or event.HasModifiers(): | |
449 | ch = chr(event.KeyCode) | |
450 | keys = {'B': self.btemp, 'H': self.htemp, 'J': self.xyb, 'S': self.commandbox, | |
451 | 'V': self.gviz} | |
452 | widget = keys.get(ch) | |
453 | #ignore Alt+(S, H), so it can open Settings, Help menu | |
454 | if widget and (ch not in 'SH' or not event.AltDown()) \ | |
455 | and not (event.ControlDown() and ch == 'V' | |
456 | and event.EventObject is self.commandbox): | |
457 | widget.SetFocus() | |
458 | return | |
459 | # On MSWindows button mnemonics are processed only if the | |
460 | # focus is in the parent panel | |
461 | if event.AltDown() and ch < 'Z': | |
462 | in_toolbar = self.toolbarsizer.GetItem(event.EventObject) | |
463 | candidates = (self.connectbtn, self.connectbtn_cb_var), \ | |
464 | (self.pausebtn, self.pause), \ | |
465 | (self.printbtn, self.printfile) | |
466 | for ctl, cb in candidates: | |
467 | match = ('&' + ch) in ctl.Label.upper() | |
468 | handled = in_toolbar and match | |
469 | if handled: | |
470 | break | |
471 | # react to 'P' even for 'Restart', 'Resume' | |
472 | # print('match', match, 'handled', handled, ctl.Label, ctl.Enabled) | |
473 | if (match or ch == 'P' and ctl != self.connectbtn) and ctl.Enabled: | |
474 | # print('call', ch, cb) | |
475 | cb() | |
476 | # react to only 1 of 'P' buttons, prefer Resume | |
477 | return | |
478 | ||
479 | event.Skip() | |
480 | ||
481 | def closewin(self, e): | |
482 | e.StopPropagation() | |
483 | self.do_exit("force") | |
484 | ||
485 | def kill(self, e=None): | |
486 | if len(self.commandbox.history): | |
487 | #save current command box history | |
488 | history = (self.history_file) | |
489 | if not os.path.exists(history): | |
490 | if not os.path.exists(self.cache_dir): | |
491 | os.makedirs(self.cache_dir) | |
492 | write_history_to(history,self.commandbox.history) | |
15 | 493 | if self.p.printing or self.p.paused: |
494 | dlg = wx.MessageDialog(self, _("Print in progress ! Are you really sure you want to quit ?"), _("Exit"), wx.YES_NO | wx.ICON_WARNING) | |
495 | if dlg.ShowModal() == wx.ID_NO: | |
496 | return | |
497 | pronsole.pronsole.kill(self) | |
498 | global pronterface_quitting | |
499 | pronterface_quitting = True | |
500 | self.p.recvcb = None | |
501 | self.p.disconnect() | |
502 | if hasattr(self, "feedrates_changed"): | |
503 | self.save_in_rc("set xy_feedrate", "set xy_feedrate %d" % self.settings.xy_feedrate) | |
504 | self.save_in_rc("set z_feedrate", "set z_feedrate %d" % self.settings.z_feedrate) | |
505 | self.save_in_rc("set e_feedrate", "set e_feedrate %d" % self.settings.e_feedrate) | |
506 | if self.settings.last_extrusion != self.settings.default_extrusion: | |
507 | self.save_in_rc("set last_extrusion", "set last_extrusion %d" % self.settings.last_extrusion) | |
508 | if self.excluder: | |
509 | self.excluder.close_window() | |
510 | wx.CallAfter(self.gwindow.Destroy) | |
511 | wx.CallAfter(self.Destroy) | |
512 | ||
47 | 513 | @property |
514 | def bgcolor(self): | |
515 | return (wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWFRAME) | |
516 | if self.settings.bgcolor == 'auto' | |
517 | else self.settings.bgcolor) | |
15 | 518 | |
519 | # -------------------------------------------------------------- | |
520 | # Main interface actions | |
521 | # -------------------------------------------------------------- | |
522 | ||
523 | def do_monitor(self, l = ""): | |
524 | if l.strip() == "": | |
525 | self.set("monitor", not self.settings.monitor) | |
526 | elif l.strip() == "off": | |
527 | self.set("monitor", False) | |
528 | else: | |
529 | try: | |
530 | self.monitor_interval = float(l) | |
531 | self.set("monitor", self.monitor_interval > 0) | |
532 | except: | |
533 | self.log(_("Invalid period given.")) | |
534 | if self.settings.monitor: | |
535 | self.log(_("Monitoring printer.")) | |
536 | else: | |
537 | self.log(_("Done monitoring.")) | |
538 | ||
539 | def do_pront_extrude(self, l = ""): | |
47 | 540 | if self.p.printing and not self.paused: |
541 | self.log(_("Please pause or stop print before extruding.")) | |
542 | return | |
15 | 543 | feed = self.settings.e_feedrate |
544 | self.do_extrude_final(self.edist.GetValue(), feed) | |
545 | ||
546 | def do_pront_reverse(self, l = ""): | |
47 | 547 | if self.p.printing and not self.paused: |
548 | self.log(_("Please pause or stop print before reversing.")) | |
549 | return | |
15 | 550 | feed = self.settings.e_feedrate |
551 | self.do_extrude_final(- self.edist.GetValue(), feed) | |
552 | ||
553 | def do_settemp(self, l = ""): | |
554 | try: | |
47 | 555 | if not isinstance(l, str) or not len(l): |
15 | 556 | l = str(self.htemp.GetValue().split()[0]) |
557 | l = l.lower().replace(", ", ".") | |
558 | for i in self.temps.keys(): | |
559 | l = l.replace(i, self.temps[i]) | |
560 | f = float(l) | |
561 | if f >= 0: | |
562 | if self.p.online: | |
563 | self.p.send_now("M104 S" + l) | |
47 | 564 | self.log(_("Setting hotend temperature to %g degrees Celsius.") % f) |
15 | 565 | self.sethotendgui(f) |
566 | else: | |
567 | self.logError(_("Printer is not online.")) | |
568 | else: | |
569 | self.logError(_("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.")) | |
47 | 570 | except Exception as x: |
15 | 571 | self.logError(_("You must enter a temperature. (%s)") % (repr(x),)) |
572 | ||
573 | def do_bedtemp(self, l = ""): | |
574 | try: | |
47 | 575 | if not isinstance(l, str) or not len(l): |
15 | 576 | l = str(self.btemp.GetValue().split()[0]) |
577 | l = l.lower().replace(", ", ".") | |
578 | for i in self.bedtemps.keys(): | |
579 | l = l.replace(i, self.bedtemps[i]) | |
580 | f = float(l) | |
581 | if f >= 0: | |
582 | if self.p.online: | |
583 | self.p.send_now("M140 S" + l) | |
47 | 584 | self.log(_("Setting bed temperature to %g degrees Celsius.") % f) |
15 | 585 | self.setbedgui(f) |
586 | else: | |
587 | self.logError(_("Printer is not online.")) | |
588 | else: | |
589 | self.logError(_("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.")) | |
47 | 590 | except Exception as x: |
15 | 591 | self.logError(_("You must enter a temperature. (%s)") % (repr(x),)) |
592 | ||
593 | def do_setspeed(self, l = ""): | |
594 | try: | |
47 | 595 | if not isinstance(l, str) or not len(l): |
15 | 596 | l = str(self.speed_slider.GetValue()) |
597 | else: | |
598 | l = l.lower() | |
599 | speed = int(l) | |
600 | if self.p.online: | |
601 | self.p.send_now("M220 S" + l) | |
602 | self.log(_("Setting print speed factor to %d%%.") % speed) | |
603 | else: | |
604 | self.logError(_("Printer is not online.")) | |
47 | 605 | except Exception as x: |
15 | 606 | self.logError(_("You must enter a speed. (%s)") % (repr(x),)) |
607 | ||
608 | def do_setflow(self, l = ""): | |
609 | try: | |
47 | 610 | if not isinstance(l, str) or not len(l): |
15 | 611 | l = str(self.flow_slider.GetValue()) |
612 | else: | |
613 | l = l.lower() | |
614 | flow = int(l) | |
615 | if self.p.online: | |
616 | self.p.send_now("M221 S" + l) | |
617 | self.log(_("Setting print flow factor to %d%%.") % flow) | |
618 | else: | |
619 | self.logError(_("Printer is not online.")) | |
47 | 620 | except Exception as x: |
15 | 621 | self.logError(_("You must enter a flow. (%s)") % (repr(x),)) |
622 | ||
623 | def setbedgui(self, f): | |
624 | self.bsetpoint = f | |
625 | if self.display_gauges: self.bedtgauge.SetTarget(int(f)) | |
626 | if self.display_graph: wx.CallAfter(self.graph.SetBedTargetTemperature, int(f)) | |
627 | if f > 0: | |
628 | wx.CallAfter(self.btemp.SetValue, str(f)) | |
629 | self.set("last_bed_temperature", str(f)) | |
630 | wx.CallAfter(self.setboff.SetBackgroundColour, None) | |
631 | wx.CallAfter(self.setboff.SetForegroundColour, None) | |
632 | wx.CallAfter(self.setbbtn.SetBackgroundColour, "#FFAA66") | |
633 | wx.CallAfter(self.setbbtn.SetForegroundColour, "#660000") | |
634 | wx.CallAfter(self.btemp.SetBackgroundColour, "#FFDABB") | |
635 | else: | |
636 | wx.CallAfter(self.setboff.SetBackgroundColour, "#0044CC") | |
637 | wx.CallAfter(self.setboff.SetForegroundColour, "white") | |
638 | wx.CallAfter(self.setbbtn.SetBackgroundColour, None) | |
639 | wx.CallAfter(self.setbbtn.SetForegroundColour, None) | |
640 | wx.CallAfter(self.btemp.SetBackgroundColour, "white") | |
641 | wx.CallAfter(self.btemp.Refresh) | |
642 | ||
643 | def sethotendgui(self, f): | |
644 | self.hsetpoint = f | |
645 | if self.display_gauges: self.hottgauge.SetTarget(int(f)) | |
646 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0TargetTemperature, int(f)) | |
647 | if f > 0: | |
648 | wx.CallAfter(self.htemp.SetValue, str(f)) | |
649 | self.set("last_temperature", str(f)) | |
650 | wx.CallAfter(self.settoff.SetBackgroundColour, None) | |
651 | wx.CallAfter(self.settoff.SetForegroundColour, None) | |
652 | wx.CallAfter(self.settbtn.SetBackgroundColour, "#FFAA66") | |
653 | wx.CallAfter(self.settbtn.SetForegroundColour, "#660000") | |
654 | wx.CallAfter(self.htemp.SetBackgroundColour, "#FFDABB") | |
655 | else: | |
656 | wx.CallAfter(self.settoff.SetBackgroundColour, "#0044CC") | |
657 | wx.CallAfter(self.settoff.SetForegroundColour, "white") | |
658 | wx.CallAfter(self.settbtn.SetBackgroundColour, None) | |
659 | wx.CallAfter(self.settbtn.SetForegroundColour, None) | |
660 | wx.CallAfter(self.htemp.SetBackgroundColour, "white") | |
661 | wx.CallAfter(self.htemp.Refresh) | |
662 | ||
663 | def rescanports(self, event = None): | |
664 | scanned = self.scanserial() | |
665 | portslist = list(scanned) | |
666 | if self.settings.port != "" and self.settings.port not in portslist: | |
667 | portslist.append(self.settings.port) | |
668 | self.serialport.Clear() | |
669 | self.serialport.AppendItems(portslist) | |
670 | if os.path.exists(self.settings.port) or self.settings.port in scanned: | |
671 | self.serialport.SetValue(self.settings.port) | |
672 | elif portslist: | |
673 | self.serialport.SetValue(portslist[0]) | |
674 | ||
47 | 675 | def appendCommandHistory(self): |
676 | cmd = self.commandbox.Value | |
677 | hist = self.commandbox.history | |
678 | append = cmd and (not hist or hist[-1] != cmd) | |
679 | if append: | |
680 | self.commandbox.history.append(cmd) | |
681 | return append | |
682 | ||
15 | 683 | def cbkey(self, e): |
47 | 684 | dir = {wx.WXK_UP: -1, wx.WXK_DOWN: 1}.get(e.KeyCode) |
685 | if dir: | |
15 | 686 | if self.commandbox.histindex == len(self.commandbox.history): |
47 | 687 | if dir == 1: |
688 | # do not cycle top => bottom | |
689 | return | |
690 | #save unsent command before going back | |
691 | self.appendCommandHistory() | |
692 | self.commandbox.histindex = max(0, min(self.commandbox.histindex + dir, len(self.commandbox.history))) | |
693 | self.commandbox.Value = (self.commandbox.history[self.commandbox.histindex] | |
694 | if self.commandbox.histindex < len(self.commandbox.history) | |
695 | else '') | |
696 | self.commandbox.SetInsertionPointEnd() | |
15 | 697 | else: |
698 | e.Skip() | |
699 | ||
700 | def plate(self, e): | |
701 | from . import stlplater as plater | |
702 | self.log(_("Plate function activated")) | |
703 | plater.StlPlater(size = (800, 580), callback = self.platecb, | |
704 | parent = self, | |
705 | build_dimensions = self.build_dimensions_list, | |
706 | circular_platform = self.settings.circular_bed, | |
707 | simarrange_path = self.settings.simarrange_path, | |
708 | antialias_samples = int(self.settings.antialias3dsamples)).Show() | |
709 | ||
710 | def plate_gcode(self, e): | |
711 | from . import gcodeplater as plater | |
712 | self.log(_("G-Code plate function activated")) | |
713 | plater.GcodePlater(size = (800, 580), callback = self.platecb, | |
714 | parent = self, | |
715 | build_dimensions = self.build_dimensions_list, | |
716 | circular_platform = self.settings.circular_bed, | |
717 | antialias_samples = int(self.settings.antialias3dsamples)).Show() | |
718 | ||
719 | def platecb(self, name): | |
720 | self.log(_("Plated %s") % name) | |
721 | self.loadfile(None, name) | |
722 | if self.settings.uimode in (_("Tabbed"), _("Tabbed with platers")): | |
723 | # Switch to page 1 (Status tab) | |
724 | self.notebook.SetSelection(1) | |
725 | ||
726 | def do_editgcode(self, e = None): | |
727 | if self.filename is not None: | |
728 | MacroEditor(self.filename, [line.raw for line in self.fgcode], self.doneediting, True) | |
729 | ||
730 | def doneediting(self, gcode): | |
731 | open(self.filename, "w").write("\n".join(gcode)) | |
732 | wx.CallAfter(self.loadfile, None, self.filename) | |
733 | ||
734 | def sdmenu(self, e): | |
735 | obj = e.GetEventObject() | |
736 | popupmenu = wx.Menu() | |
737 | item = popupmenu.Append(-1, _("SD Upload")) | |
738 | if not self.fgcode: | |
739 | item.Enable(False) | |
740 | self.Bind(wx.EVT_MENU, self.upload, id = item.GetId()) | |
741 | item = popupmenu.Append(-1, _("SD Print")) | |
742 | self.Bind(wx.EVT_MENU, self.sdprintfile, id = item.GetId()) | |
743 | self.panel.PopupMenu(popupmenu, obj.GetPosition()) | |
744 | ||
745 | def htemp_change(self, event): | |
746 | if self.hsetpoint > 0: | |
747 | self.do_settemp("") | |
748 | wx.CallAfter(self.htemp.SetInsertionPoint, 0) | |
749 | ||
750 | def btemp_change(self, event): | |
751 | if self.bsetpoint > 0: | |
752 | self.do_bedtemp("") | |
753 | wx.CallAfter(self.btemp.SetInsertionPoint, 0) | |
754 | ||
755 | def tool_change(self, event): | |
756 | self.do_tool(self.extrudersel.GetValue()) | |
757 | ||
758 | def show_viz_window(self, event): | |
759 | if self.fgcode: | |
760 | self.gwindow.Show(True) | |
761 | self.gwindow.SetToolTip(wx.ToolTip("Mousewheel zooms the display\nShift / Mousewheel scrolls layers")) | |
762 | self.gwindow.Raise() | |
763 | ||
764 | def setfeeds(self, e): | |
765 | self.feedrates_changed = True | |
766 | try: | |
767 | if self.efeedc is not None: | |
768 | self.settings._set("e_feedrate", self.efeedc.GetValue()) | |
769 | except: | |
770 | pass | |
771 | try: | |
772 | self.settings._set("z_feedrate", self.zfeedc.GetValue()) | |
773 | except: | |
774 | pass | |
775 | try: | |
776 | self.settings._set("xy_feedrate", self.xyfeedc.GetValue()) | |
777 | except: | |
778 | pass | |
779 | try: | |
780 | self.settings._set("last_extrusion", self.edist.GetValue()) | |
781 | except: | |
782 | pass | |
783 | ||
784 | def homeButtonClicked(self, axis): | |
785 | # When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals | |
786 | self.zb.clearRepeat() | |
787 | if axis == "x": | |
788 | self.onecmd('home X') | |
789 | elif axis == "y": # upper-right | |
790 | self.onecmd('home Y') | |
791 | elif axis == "z": | |
792 | self.onecmd('home Z') | |
793 | elif axis == "all": | |
794 | self.onecmd('home') | |
795 | elif axis == "center": | |
796 | center_x = self.build_dimensions_list[0] / 2 + self.build_dimensions_list[3] | |
797 | center_y = self.build_dimensions_list[1] / 2 + self.build_dimensions_list[4] | |
798 | feed = self.settings.xy_feedrate | |
799 | self.onecmd('G0 X%s Y%s F%s' % (center_x, center_y, feed)) | |
800 | else: | |
801 | return | |
802 | self.p.send_now('M114') | |
803 | ||
804 | def clamped_move_message(self): | |
805 | self.log(_("Manual move outside of the build volume prevented (see the \"Clamp manual moves\" option).")) | |
806 | ||
807 | def moveXY(self, x, y): | |
808 | # When user clicks on the XY control, the Z control no longer gets spacebar/repeat signals | |
809 | self.zb.clearRepeat() | |
810 | if x != 0: | |
811 | if self.settings.clamp_jogging: | |
812 | new_x = self.current_pos[0] + x | |
813 | if new_x < self.build_dimensions_list[3] or new_x > self.build_dimensions_list[0] + self.build_dimensions_list[3]: | |
814 | self.clamped_move_message() | |
815 | return | |
816 | self.onecmd('move X %s' % x) | |
817 | elif y != 0: | |
818 | if self.settings.clamp_jogging: | |
819 | new_y = self.current_pos[1] + y | |
820 | if new_y < self.build_dimensions_list[4] or new_y > self.build_dimensions_list[1] + self.build_dimensions_list[4]: | |
821 | self.clamped_move_message() | |
822 | return | |
823 | self.onecmd('move Y %s' % y) | |
824 | else: | |
825 | return | |
826 | self.p.send_now('M114') | |
827 | ||
828 | def moveZ(self, z): | |
829 | if z != 0: | |
830 | if self.settings.clamp_jogging: | |
831 | new_z = self.current_pos[2] + z | |
832 | if new_z < self.build_dimensions_list[5] or new_z > self.build_dimensions_list[2] + self.build_dimensions_list[5]: | |
833 | self.clamped_move_message() | |
834 | return | |
835 | self.onecmd('move Z %s' % z) | |
836 | self.p.send_now('M114') | |
837 | # When user clicks on the Z control, the XY control no longer gets spacebar/repeat signals | |
838 | self.xyb.clearRepeat() | |
839 | ||
840 | def spacebarAction(self): | |
841 | self.zb.repeatLast() | |
842 | self.xyb.repeatLast() | |
843 | ||
844 | # -------------------------------------------------------------- | |
845 | # Console handling | |
846 | # -------------------------------------------------------------- | |
847 | ||
848 | def catchprint(self, l): | |
849 | """Called by the Tee operator to write to the log box""" | |
850 | if not self.IsFrozen(): | |
851 | wx.CallAfter(self.addtexttolog, l) | |
852 | ||
853 | def addtexttolog(self, text): | |
854 | try: | |
855 | max_length = 20000 | |
856 | current_length = self.logbox.GetLastPosition() | |
857 | if current_length > max_length: | |
858 | self.logbox.Remove(0, current_length / 10) | |
47 | 859 | currentCaretPosition = self.logbox.GetInsertionPoint() |
860 | currentLengthOfText = self.logbox.GetLastPosition() | |
861 | if self.autoscrolldisable: | |
862 | self.logbox.Freeze() | |
863 | currentSelectionStart, currentSelectionEnd = self.logbox.GetSelection() | |
864 | self.logbox.SetInsertionPointEnd() | |
865 | self.logbox.AppendText(text) | |
866 | self.logbox.SetInsertionPoint(currentCaretPosition) | |
867 | self.logbox.SetSelection(currentSelectionStart, currentSelectionEnd) | |
868 | self.logbox.Thaw() | |
869 | else: | |
870 | self.logbox.SetInsertionPointEnd() | |
871 | self.logbox.AppendText(text) | |
872 | ||
15 | 873 | except: |
874 | self.log(_("Attempted to write invalid text to console, which could be due to an invalid baudrate")) | |
875 | ||
876 | def clear_log(self, e): | |
877 | self.logbox.Clear() | |
878 | ||
879 | def set_verbose_communications(self, e): | |
880 | self.p.loud = e.IsChecked() | |
881 | ||
47 | 882 | def set_autoscrolldisable(self,e): |
883 | self.autoscrolldisable = e.IsChecked() | |
884 | ||
15 | 885 | def sendline(self, e): |
47 | 886 | command = self.commandbox.Value |
15 | 887 | if not len(command): |
888 | return | |
47 | 889 | logging.info(">>> " + command) |
15 | 890 | line = self.precmd(str(command)) |
891 | self.onecmd(line) | |
47 | 892 | self.appendCommandHistory() |
15 | 893 | self.commandbox.histindex = len(self.commandbox.history) |
47 | 894 | self.commandbox.Value = '' |
15 | 895 | |
896 | # -------------------------------------------------------------- | |
897 | # Main menu handling & actions | |
898 | # -------------------------------------------------------------- | |
899 | ||
900 | def create_menu(self): | |
901 | """Create main menu""" | |
47 | 902 | |
15 | 903 | # File menu |
904 | m = wx.Menu() | |
47 | 905 | self.Bind(wx.EVT_MENU, self.loadfile, m.Append(-1, _("&Open...\tCtrl+O"), _(" Open file"))) |
15 | 906 | self.savebtn = m.Append(-1, _("&Save..."), _(" Save file")) |
907 | self.savebtn.Enable(False) | |
908 | self.Bind(wx.EVT_MENU, self.savefile, self.savebtn) | |
909 | ||
910 | self.filehistory = wx.FileHistory(maxFiles = 8, idBase = wx.ID_FILE1) | |
911 | recent = wx.Menu() | |
912 | self.filehistory.UseMenu(recent) | |
913 | self.Bind(wx.EVT_MENU_RANGE, self.load_recent_file, | |
914 | id = wx.ID_FILE1, id2 = wx.ID_FILE9) | |
47 | 915 | m.Append(wx.ID_ANY, _("&Recent Files"), recent) |
916 | self.Bind(wx.EVT_MENU, self.clear_log, m.Append(-1, _("Clear console\tCtrl+L"), _(" Clear output console"))) | |
15 | 917 | self.Bind(wx.EVT_MENU, self.on_exit, m.Append(wx.ID_EXIT, _("E&xit"), _(" Closes the Window"))) |
918 | self.menustrip.Append(m, _("&File")) | |
919 | ||
47 | 920 | # Tools Menu |
15 | 921 | m = wx.Menu() |
922 | self.Bind(wx.EVT_MENU, self.do_editgcode, m.Append(-1, _("&Edit..."), _(" Edit open file"))) | |
923 | self.Bind(wx.EVT_MENU, self.plate, m.Append(-1, _("Plater"), _(" Compose 3D models into a single plate"))) | |
924 | self.Bind(wx.EVT_MENU, self.plate_gcode, m.Append(-1, _("G-Code Plater"), _(" Compose G-Codes into a single plate"))) | |
925 | self.Bind(wx.EVT_MENU, self.exclude, m.Append(-1, _("Excluder"), _(" Exclude parts of the bed from being printed"))) | |
926 | self.Bind(wx.EVT_MENU, self.project, m.Append(-1, _("Projector"), _(" Project slices"))) | |
47 | 927 | self.Bind(wx.EVT_MENU, |
928 | self.show_spool_manager, | |
929 | m.Append(-1, _("Spool Manager"), | |
930 | _(" Manage different spools of filament"))) | |
15 | 931 | self.menustrip.Append(m, _("&Tools")) |
932 | ||
47 | 933 | # Advanced Menu |
15 | 934 | m = wx.Menu() |
935 | self.recoverbtn = m.Append(-1, _("Recover"), _(" Recover previous print after a disconnect (homes X, Y, restores Z and E status)")) | |
936 | self.recoverbtn.Disable = lambda *a: self.recoverbtn.Enable(False) | |
937 | self.Bind(wx.EVT_MENU, self.recover, self.recoverbtn) | |
938 | self.menustrip.Append(m, _("&Advanced")) | |
939 | ||
940 | if self.settings.slic3rintegration: | |
941 | m = wx.Menu() | |
942 | print_menu = wx.Menu() | |
943 | filament_menu = wx.Menu() | |
944 | printer_menu = wx.Menu() | |
945 | m.AppendSubMenu(print_menu, _("Print &settings")) | |
946 | m.AppendSubMenu(filament_menu, _("&Filament")) | |
947 | m.AppendSubMenu(printer_menu, _("&Printer")) | |
948 | menus = {"print": print_menu, | |
949 | "filament": filament_menu, | |
950 | "printer": printer_menu} | |
951 | try: | |
952 | self.load_slic3r_configs(menus) | |
953 | self.menustrip.Append(m, _("&Slic3r")) | |
954 | except IOError: | |
955 | self.logError(_("Failed to load Slic3r configuration:") + | |
956 | "\n" + traceback.format_exc()) | |
957 | ||
958 | # Settings menu | |
959 | m = wx.Menu() | |
960 | self.macros_menu = wx.Menu() | |
961 | m.AppendSubMenu(self.macros_menu, _("&Macros")) | |
962 | self.Bind(wx.EVT_MENU, self.new_macro, self.macros_menu.Append(-1, _("<&New...>"))) | |
963 | self.Bind(wx.EVT_MENU, lambda *e: PronterOptions(self), m.Append(-1, _("&Options"), _(" Options dialog"))) | |
964 | ||
965 | self.Bind(wx.EVT_MENU, lambda x: threading.Thread(target = lambda: self.do_slice("set")).start(), m.Append(-1, _("Slicing settings"), _(" Adjust slicing settings"))) | |
966 | ||
967 | mItem = m.AppendCheckItem(-1, _("Debug communications"), | |
968 | _("Print all G-code sent to and received from the printer.")) | |
969 | m.Check(mItem.GetId(), self.p.loud) | |
970 | self.Bind(wx.EVT_MENU, self.set_verbose_communications, mItem) | |
971 | ||
47 | 972 | mItem = m.AppendCheckItem(-1, _("Don't autoscroll"), |
973 | _("Disables automatic scrolling of the console when new text is added")) | |
974 | m.Check(mItem.GetId(), self.autoscrolldisable) | |
975 | self.Bind(wx.EVT_MENU, self.set_autoscrolldisable, mItem) | |
976 | ||
15 | 977 | self.menustrip.Append(m, _("&Settings")) |
978 | self.update_macros_menu() | |
979 | self.SetMenuBar(self.menustrip) | |
980 | ||
981 | m = wx.Menu() | |
982 | self.Bind(wx.EVT_MENU, self.about, | |
983 | m.Append(-1, _("&About Printrun"), _("Show about dialog"))) | |
984 | self.menustrip.Append(m, _("&Help")) | |
985 | ||
986 | def project(self, event): | |
987 | """Start Projector tool""" | |
988 | from printrun import projectlayer | |
989 | projectlayer.SettingsFrame(self, self.p).Show() | |
990 | ||
991 | def exclude(self, event): | |
992 | """Start part excluder tool""" | |
993 | if not self.fgcode: | |
994 | wx.CallAfter(self.statusbar.SetStatusText, _("No file loaded. Please use load first.")) | |
995 | return | |
996 | if not self.excluder: | |
997 | from .excluder import Excluder | |
998 | self.excluder = Excluder() | |
999 | self.excluder.pop_window(self.fgcode, bgcolor = self.bgcolor, | |
1000 | build_dimensions = self.build_dimensions_list) | |
1001 | ||
47 | 1002 | def show_spool_manager(self, event): |
1003 | """Show Spool Manager Window""" | |
1004 | spoolmanager_gui.SpoolManagerMainWindow(self, self.spool_manager).Show() | |
1005 | ||
15 | 1006 | def about(self, event): |
1007 | """Show about dialog""" | |
1008 | ||
47 | 1009 | info = wx.adv.AboutDialogInfo() |
15 | 1010 | |
1011 | info.SetIcon(wx.Icon(iconfile("pronterface.png"), wx.BITMAP_TYPE_PNG)) | |
1012 | info.SetName('Printrun') | |
1013 | info.SetVersion(printcore.__version__) | |
1014 | ||
1015 | description = _("Printrun is a pure Python 3D printing" | |
1016 | " (and other types of CNC) host software.") | |
1017 | ||
1018 | description += "\n\n" + \ | |
1019 | _("%.02fmm of filament have been extruded during prints") \ | |
1020 | % self.settings.total_filament_used | |
1021 | ||
1022 | info.SetDescription(description) | |
47 | 1023 | info.SetCopyright('(C) 2011 - 2020') |
15 | 1024 | info.SetWebSite('https://github.com/kliment/Printrun') |
1025 | ||
1026 | licence = """\ | |
1027 | Printrun is free software: you can redistribute it and/or modify it under the | |
1028 | terms of the GNU General Public License as published by the Free Software | |
1029 | Foundation, either version 3 of the License, or (at your option) any later | |
1030 | version. | |
1031 | ||
1032 | Printrun is distributed in the hope that it will be useful, but WITHOUT ANY | |
1033 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A | |
1034 | PARTICULAR PURPOSE. See the GNU General Public License for more details. | |
1035 | ||
1036 | You should have received a copy of the GNU General Public License along with | |
1037 | Printrun. If not, see <http://www.gnu.org/licenses/>.""" | |
1038 | ||
1039 | info.SetLicence(licence) | |
1040 | info.AddDeveloper('Kliment Yanev') | |
1041 | info.AddDeveloper('Guillaume Seguin') | |
48 | 1042 | info.AddDeveloper('Malte Di Donato') |
15 | 1043 | |
47 | 1044 | wx.adv.AboutBox(info) |
15 | 1045 | |
1046 | # -------------------------------------------------------------- | |
1047 | # Settings & command line handling (including update callbacks) | |
1048 | # -------------------------------------------------------------- | |
47 | 1049 | |
15 | 1050 | def _add_settings(self, size): |
48 | 1051 | self._lc_add_settings(size) |
1052 | ||
15 | 1053 | self.settings._add(BooleanSetting("monitor", True, _("Monitor printer status"), _("Regularly monitor printer temperatures (required to have functional temperature graph or gauges)"), "Printer"), self.update_monitor) |
1054 | self.settings._add(StringSetting("simarrange_path", "", _("Simarrange command"), _("Path to the simarrange binary to use in the STL plater"), "External")) | |
1055 | self.settings._add(BooleanSetting("circular_bed", False, _("Circular build platform"), _("Draw a circular (or oval) build platform instead of a rectangular one"), "Printer"), self.update_bed_viz) | |
1056 | self.settings._add(SpinSetting("extruders", 0, 1, 5, _("Extruders count"), _("Number of extruders"), "Printer")) | |
1057 | self.settings._add(BooleanSetting("clamp_jogging", False, _("Clamp manual moves"), _("Prevent manual moves from leaving the specified build dimensions"), "Printer")) | |
47 | 1058 | self.settings._add(BooleanSetting("display_progress_on_printer", False, _("Display progress on printer"), _("Show progress on printers display (sent via M117, might not be supported by all printers)"), "Printer")) |
1059 | self.settings._add(SpinSetting("printer_progress_update_interval", 10., 0, 120, _("Printer progress update interval"), _("Interval in which pronterface sends the progress to the printer if enabled, in seconds"), "Printer")) | |
1060 | self.settings._add(BooleanSetting("cutting_as_extrusion", True, _("Display cutting moves"), _("Show moves where spindle is active as printing moves"), "Printer")) | |
1061 | self.settings._add(ComboSetting("uimode", _("Standard"), [_("Standard"), _("Compact"), ], _("Interface mode"), _("Standard interface is a one-page, three columns layout with controls/visualization/log\nCompact mode is a one-page, two columns layout with controls + log/visualization"), "UI"), self.reload_ui) | |
1062 | #self.settings._add(ComboSetting("uimode", _("Standard"), [_("Standard"), _("Compact"), _("Tabbed"), _("Tabbed with platers")], _("Interface mode"), _("Standard interface is a one-page, three columns layout with controls/visualization/log\nCompact mode is a one-page, two columns layout with controls + log/visualization"), "UI"), self.reload_ui) | |
1063 | self.settings._add(ComboSetting("controlsmode", "Standard", ("Standard", "Mini"), _("Controls mode"), _("Standard controls include all controls needed for printer setup and calibration, while Mini controls are limited to the ones needed for daily printing"), "UI"), self.reload_ui) | |
15 | 1064 | self.settings._add(BooleanSetting("slic3rintegration", False, _("Enable Slic3r integration"), _("Add a menu to select Slic3r profiles directly from Pronterface"), "UI"), self.reload_ui) |
1065 | self.settings._add(BooleanSetting("slic3rupdate", False, _("Update Slic3r default presets"), _("When selecting a profile in Slic3r integration menu, also save it as the default Slic3r preset"), "UI")) | |
47 | 1066 | self.settings._add(ComboSetting("mainviz", "3D", ("2D", "3D", "None"), _("Main visualization"), _("Select visualization for main window."), "Viewer"), self.reload_ui) |
15 | 1067 | self.settings._add(BooleanSetting("viz3d", False, _("Use 3D in GCode viewer window"), _("Use 3D mode instead of 2D layered mode in the visualization window"), "Viewer"), self.reload_ui) |
1068 | self.settings._add(StaticTextSetting("separator_3d_viewer", _("3D viewer options"), "", group = "Viewer")) | |
1069 | self.settings._add(BooleanSetting("light3d", False, _("Use a lighter 3D visualization"), _("Use a lighter visualization with simple lines instead of extruded paths for 3D viewer"), "Viewer"), self.reload_ui) | |
1070 | self.settings._add(ComboSetting("antialias3dsamples", "0", ["0", "2", "4", "8"], _("Number of anti-aliasing samples"), _("Amount of anti-aliasing samples used in the 3D viewer"), "Viewer"), self.reload_ui) | |
1071 | self.settings._add(BooleanSetting("trackcurrentlayer3d", False, _("Track current layer in main 3D view"), _("Track the currently printing layer in the main 3D visualization"), "Viewer")) | |
1072 | self.settings._add(FloatSpinSetting("gcview_path_width", 0.4, 0.01, 2, _("Extrusion width for 3D viewer"), _("Width of printed path in 3D viewer"), "Viewer", increment = 0.05), self.update_gcview_params) | |
1073 | self.settings._add(FloatSpinSetting("gcview_path_height", 0.3, 0.01, 2, _("Layer height for 3D viewer"), _("Height of printed path in 3D viewer"), "Viewer", increment = 0.05), self.update_gcview_params) | |
1074 | self.settings._add(BooleanSetting("tempgraph", True, _("Display temperature graph"), _("Display time-lapse temperature graph"), "UI"), self.reload_ui) | |
1075 | self.settings._add(BooleanSetting("tempgauges", False, _("Display temperature gauges"), _("Display graphical gauges for temperatures visualization"), "UI"), self.reload_ui) | |
1076 | self.settings._add(BooleanSetting("lockbox", False, _("Display interface lock checkbox"), _("Display a checkbox that, when check, locks most of Pronterface"), "UI"), self.reload_ui) | |
1077 | self.settings._add(BooleanSetting("lockonstart", False, _("Lock interface upon print start"), _("If lock checkbox is enabled, lock the interface when starting a print"), "UI")) | |
1078 | self.settings._add(BooleanSetting("refreshwhenloading", True, _("Update UI during G-Code load"), _("Regularly update visualization during the load of a G-Code file"), "UI")) | |
1079 | self.settings._add(HiddenSetting("last_window_width", size[0])) | |
1080 | self.settings._add(HiddenSetting("last_window_height", size[1])) | |
1081 | self.settings._add(HiddenSetting("last_window_maximized", False)) | |
1082 | self.settings._add(HiddenSetting("last_sash_position", -1)) | |
1083 | self.settings._add(HiddenSetting("last_bed_temperature", 0.0)) | |
47 | 1084 | self.settings._add(HiddenSetting("last_file_path", "")) |
15 | 1085 | self.settings._add(HiddenSetting("last_file_filter", 0)) |
1086 | self.settings._add(HiddenSetting("last_temperature", 0.0)) | |
1087 | self.settings._add(StaticTextSetting("separator_2d_viewer", _("2D viewer options"), "", group = "Viewer")) | |
1088 | self.settings._add(FloatSpinSetting("preview_extrusion_width", 0.5, 0, 10, _("Preview extrusion width"), _("Width of Extrusion in Preview"), "Viewer", increment = 0.1), self.update_gviz_params) | |
1089 | self.settings._add(SpinSetting("preview_grid_step1", 10., 0, 200, _("Fine grid spacing"), _("Fine Grid Spacing"), "Viewer"), self.update_gviz_params) | |
1090 | self.settings._add(SpinSetting("preview_grid_step2", 50., 0, 200, _("Coarse grid spacing"), _("Coarse Grid Spacing"), "Viewer"), self.update_gviz_params) | |
47 | 1091 | self.settings._add(ColorSetting("bgcolor", self._preferred_bgcolour_hex(), _("Background color"), _("Pronterface background color"), "Colors", isRGBA=False), self.reload_ui) |
1092 | self.settings._add(ColorSetting("graph_color_background", "#FAFAC7", _("Graph background color"), _("Color of the temperature graph background"), "Colors", isRGBA=False), self.reload_ui) | |
1093 | self.settings._add(ColorSetting("gcview_color_background", "#FAFAC7FF", _("3D view background color"), _("Color of the 3D view background"), "Colors"), self.update_gcview_colors) | |
1094 | self.settings._add(ColorSetting("gcview_color_travel", "#99999999", _("3D view travel moves color"), _("Color of travel moves in 3D view"), "Colors"), self.update_gcview_colors) | |
1095 | self.settings._add(ColorSetting("gcview_color_tool0", "#FF000099", _("3D view print moves color"), _("Color of print moves with tool 0 in 3D view"), "Colors"), self.update_gcview_colors) | |
1096 | self.settings._add(ColorSetting("gcview_color_tool1", "#AC0DFF99", _("3D view tool 1 moves color"), _("Color of print moves with tool 1 in 3D view"), "Colors"), self.update_gcview_colors) | |
1097 | self.settings._add(ColorSetting("gcview_color_tool2", "#FFCE0099", _("3D view tool 2 moves color"), _("Color of print moves with tool 2 in 3D view"), "Colors"), self.update_gcview_colors) | |
1098 | self.settings._add(ColorSetting("gcview_color_tool3", "#FF009F99", _("3D view tool 3 moves color"), _("Color of print moves with tool 3 in 3D view"), "Colors"), self.update_gcview_colors) | |
1099 | self.settings._add(ColorSetting("gcview_color_tool4", "#00FF8F99", _("3D view tool 4 moves color"), _("Color of print moves with tool 4 in 3D view"), "Colors"), self.update_gcview_colors) | |
1100 | self.settings._add(ColorSetting("gcview_color_printed", "#33BF0099", _("3D view printed moves color"), _("Color of printed moves in 3D view"), "Colors"), self.update_gcview_colors) | |
1101 | self.settings._add(ColorSetting("gcview_color_current", "#00E5FFCC", _("3D view current layer moves color"), _("Color of moves in current layer in 3D view"), "Colors"), self.update_gcview_colors) | |
1102 | self.settings._add(ColorSetting("gcview_color_current_printed", "#196600CC", _("3D view printed current layer moves color"), _("Color of already printed moves from current layer in 3D view"), "Colors"), self.update_gcview_colors) | |
15 | 1103 | self.settings._add(StaticTextSetting("note1", _("Note:"), _("Changing some of these settings might require a restart to get effect"), group = "UI")) |
1104 | recentfilessetting = StringSetting("recentfiles", "[]") | |
1105 | recentfilessetting.hidden = True | |
1106 | self.settings._add(recentfilessetting, self.update_recent_files) | |
1107 | ||
47 | 1108 | def _preferred_bgcolour_hex(self): |
1109 | id = wx.SYS_COLOUR_WINDOW \ | |
1110 | if platform.system() == 'Windows' \ | |
1111 | else wx.SYS_COLOUR_BACKGROUND | |
1112 | sys_bgcolour = wx.SystemSettings.GetColour(id) | |
1113 | return sys_bgcolour.GetAsString(flags=wx.C2S_HTML_SYNTAX) | |
1114 | ||
15 | 1115 | def add_cmdline_arguments(self, parser): |
1116 | pronsole.pronsole.add_cmdline_arguments(self, parser) | |
1117 | parser.add_argument('-a', '--autoconnect', help = _("automatically try to connect to printer on startup"), action = "store_true") | |
1118 | ||
1119 | def process_cmdline_arguments(self, args): | |
1120 | pronsole.pronsole.process_cmdline_arguments(self, args) | |
1121 | self.autoconnect = args.autoconnect | |
1122 | ||
1123 | def update_recent_files(self, param, value): | |
1124 | if self.filehistory is None: | |
1125 | return | |
1126 | recent_files = [] | |
1127 | try: | |
1128 | recent_files = json.loads(value) | |
1129 | except: | |
1130 | self.logError(_("Failed to load recent files list:") + | |
1131 | "\n" + traceback.format_exc()) | |
1132 | # Clear history | |
1133 | while self.filehistory.GetCount(): | |
1134 | self.filehistory.RemoveFileFromHistory(0) | |
1135 | recent_files.reverse() | |
1136 | for f in recent_files: | |
1137 | self.filehistory.AddFileToHistory(f) | |
1138 | ||
1139 | def update_gviz_params(self, param, value): | |
1140 | params_map = {"preview_extrusion_width": "extrusion_width", | |
1141 | "preview_grid_step1": "grid", | |
1142 | "preview_grid_step2": "grid"} | |
1143 | if param not in params_map: | |
1144 | return | |
1145 | if not hasattr(self, "gviz"): | |
1146 | # GUI hasn't been loaded yet, ignore this setting | |
1147 | return | |
1148 | trueparam = params_map[param] | |
1149 | if hasattr(self.gviz, trueparam): | |
1150 | gviz = self.gviz | |
1151 | elif hasattr(self.gwindow, "p") and hasattr(self.gwindow.p, trueparam): | |
1152 | gviz = self.gwindow.p | |
1153 | else: | |
1154 | return | |
1155 | if trueparam == "grid": | |
1156 | try: | |
1157 | item = int(param[-1]) # extract list item position | |
1158 | grid = list(gviz.grid) | |
1159 | grid[item - 1] = value | |
1160 | value = tuple(grid) | |
1161 | except: | |
1162 | self.logError(traceback.format_exc()) | |
1163 | if hasattr(self.gviz, trueparam): | |
1164 | self.apply_gviz_params(self.gviz, trueparam, value) | |
1165 | if hasattr(self.gwindow, "p") and hasattr(self.gwindow.p, trueparam): | |
1166 | self.apply_gviz_params(self.gwindow.p, trueparam, value) | |
1167 | ||
1168 | def apply_gviz_params(self, widget, param, value): | |
1169 | setattr(widget, param, value) | |
1170 | widget.dirty = 1 | |
1171 | wx.CallAfter(widget.Refresh) | |
1172 | ||
1173 | def update_gcview_colors(self, param, value): | |
47 | 1174 | if not self.window_ready: |
1175 | return | |
15 | 1176 | color = hexcolor_to_float(value, 4) |
1177 | # This is sort of a hack: we copy the color values into the preexisting | |
1178 | # color tuple so that we don't need to update the tuple used by gcview | |
1179 | target_color = getattr(self, param) | |
1180 | for i, v in enumerate(color): | |
1181 | target_color[i] = v | |
1182 | wx.CallAfter(self.Refresh) | |
1183 | ||
1184 | def update_build_dimensions(self, param, value): | |
1185 | pronsole.pronsole.update_build_dimensions(self, param, value) | |
1186 | self.update_bed_viz() | |
1187 | ||
1188 | def update_bed_viz(self, *args): | |
1189 | """Update bed visualization when size/type changed""" | |
1190 | if hasattr(self, "gviz") and hasattr(self.gviz, "recreate_platform"): | |
47 | 1191 | self.gviz.recreate_platform(self.build_dimensions_list, self.settings.circular_bed, |
1192 | grid = (self.settings.preview_grid_step1, self.settings.preview_grid_step2)) | |
15 | 1193 | if hasattr(self, "gwindow") and hasattr(self.gwindow, "recreate_platform"): |
47 | 1194 | self.gwindow.recreate_platform(self.build_dimensions_list, self.settings.circular_bed, |
1195 | grid = (self.settings.preview_grid_step1, self.settings.preview_grid_step2)) | |
15 | 1196 | |
1197 | def update_gcview_params(self, *args): | |
1198 | need_reload = False | |
1199 | if hasattr(self, "gviz") and hasattr(self.gviz, "set_gcview_params"): | |
1200 | need_reload |= self.gviz.set_gcview_params(self.settings.gcview_path_width, self.settings.gcview_path_height) | |
1201 | if hasattr(self, "gwindow") and hasattr(self.gwindow, "set_gcview_params"): | |
1202 | need_reload |= self.gwindow.set_gcview_params(self.settings.gcview_path_width, self.settings.gcview_path_height) | |
1203 | if need_reload: | |
1204 | self.start_viz_thread() | |
1205 | ||
1206 | def update_monitor(self, *args): | |
1207 | if hasattr(self, "graph") and self.display_graph: | |
1208 | if self.settings.monitor: | |
1209 | wx.CallAfter(self.graph.StartPlotting, 1000) | |
1210 | else: | |
1211 | wx.CallAfter(self.graph.StopPlotting) | |
1212 | ||
1213 | # -------------------------------------------------------------- | |
1214 | # Statusbar handling | |
1215 | # -------------------------------------------------------------- | |
1216 | ||
1217 | def statuschecker_inner(self): | |
1218 | status_string = "" | |
1219 | if self.sdprinting or self.uploading or self.p.printing: | |
1220 | secondsremain, secondsestimate, progress = self.get_eta() | |
1221 | if self.sdprinting or self.uploading: | |
1222 | if self.uploading: | |
1223 | status_string += _("SD upload: %04.2f%% |") % (100 * progress,) | |
1224 | status_string += _(" Line# %d of %d lines |") % (self.p.queueindex, len(self.p.mainqueue)) | |
1225 | else: | |
1226 | status_string += _("SD printing: %04.2f%% |") % (self.percentdone,) | |
1227 | elif self.p.printing: | |
1228 | status_string += _("Printing: %04.2f%% |") % (100 * float(self.p.queueindex) / len(self.p.mainqueue),) | |
1229 | status_string += _(" Line# %d of %d lines |") % (self.p.queueindex, len(self.p.mainqueue)) | |
1230 | if progress > 0: | |
1231 | status_string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain), | |
1232 | format_duration(secondsestimate)) | |
1233 | status_string += _(" Z: %.3f mm") % self.curlayer | |
47 | 1234 | if self.settings.display_progress_on_printer and time.time() - self.printer_progress_time >= self.settings.printer_progress_update_interval: |
1235 | self.printer_progress_time = time.time() | |
1236 | printer_progress_string = "M117 " + str(round(100 * float(self.p.queueindex) / len(self.p.mainqueue), 2)) + "% Est " + format_duration(secondsremain) | |
1237 | #":" seems to be some kind of seperator for G-CODE" | |
1238 | self.p.send_now(printer_progress_string.replace(":", ".")) | |
1239 | if len(printer_progress_string) > 25: | |
1240 | logging.info("Warning: The print progress message might be too long to be displayed properly") | |
1241 | #13 chars for up to 99h est. | |
15 | 1242 | elif self.loading_gcode: |
1243 | status_string = self.loading_gcode_message | |
1244 | wx.CallAfter(self.statusbar.SetStatusText, status_string) | |
1245 | wx.CallAfter(self.gviz.Refresh) | |
1246 | # Call pronsole's statuschecker inner loop function to handle | |
1247 | # temperature monitoring and status loop sleep | |
1248 | pronsole.pronsole.statuschecker_inner(self, self.settings.monitor) | |
1249 | try: | |
1250 | while not self.sentglines.empty(): | |
1251 | gc = self.sentglines.get_nowait() | |
1252 | wx.CallAfter(self.gviz.addgcodehighlight, gc) | |
1253 | self.sentglines.task_done() | |
47 | 1254 | except queue.Empty: |
15 | 1255 | pass |
1256 | ||
1257 | def statuschecker(self): | |
1258 | pronsole.pronsole.statuschecker(self) | |
1259 | wx.CallAfter(self.statusbar.SetStatusText, _("Not connected to printer.")) | |
1260 | ||
1261 | # -------------------------------------------------------------- | |
1262 | # Interface lock handling | |
1263 | # -------------------------------------------------------------- | |
1264 | ||
1265 | def lock(self, event = None, force = None): | |
1266 | if force is not None: | |
1267 | self.locker.SetValue(force) | |
1268 | if self.locker.GetValue(): | |
1269 | self.log(_("Locking interface.")) | |
1270 | for panel in self.panels: | |
1271 | panel.Disable() | |
1272 | else: | |
1273 | self.log(_("Unlocking interface.")) | |
1274 | for panel in self.panels: | |
1275 | panel.Enable() | |
1276 | ||
1277 | # -------------------------------------------------------------- | |
1278 | # Printer connection handling | |
1279 | # -------------------------------------------------------------- | |
1280 | ||
47 | 1281 | def connectbtn_cb(self, event): |
1282 | # Implement toggle behavior with a single Bind | |
1283 | # and switched variable, so we have reference to | |
1284 | # the actual callback to use in on_key | |
1285 | self.connectbtn_cb_var() | |
1286 | ||
15 | 1287 | def connect(self, event = None): |
1288 | self.log(_("Connecting...")) | |
1289 | port = None | |
1290 | if self.serialport.GetValue(): | |
1291 | port = str(self.serialport.GetValue()) | |
1292 | else: | |
1293 | scanned = self.scanserial() | |
1294 | if scanned: | |
1295 | port = scanned[0] | |
1296 | baud = 115200 | |
1297 | try: | |
1298 | baud = int(self.baud.GetValue()) | |
1299 | except: | |
1300 | self.logError(_("Could not parse baud rate: ") | |
1301 | + "\n" + traceback.format_exc()) | |
1302 | if self.paused: | |
1303 | self.p.paused = 0 | |
1304 | self.p.printing = 0 | |
47 | 1305 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
1306 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) | |
15 | 1307 | wx.CallAfter(self.toolbarsizer.Layout) |
1308 | self.paused = 0 | |
1309 | if self.sdprinting: | |
1310 | self.p.send_now("M26 S0") | |
1311 | if not self.connect_to_printer(port, baud, self.settings.dtr): | |
1312 | return | |
1313 | if port != self.settings.port: | |
1314 | self.set("port", port) | |
1315 | if baud != self.settings.baudrate: | |
1316 | self.set("baudrate", str(baud)) | |
1317 | if self.predisconnect_mainqueue: | |
1318 | self.recoverbtn.Enable() | |
1319 | ||
1320 | def store_predisconnect_state(self): | |
1321 | self.predisconnect_mainqueue = self.p.mainqueue | |
1322 | self.predisconnect_queueindex = self.p.queueindex | |
1323 | self.predisconnect_layer = self.curlayer | |
1324 | ||
1325 | def disconnect(self, event = None): | |
1326 | self.log(_("Disconnected.")) | |
1327 | if self.p.printing or self.p.paused or self.paused: | |
1328 | self.store_predisconnect_state() | |
1329 | self.p.disconnect() | |
1330 | self.statuscheck = False | |
1331 | if self.status_thread: | |
1332 | self.status_thread.join() | |
1333 | self.status_thread = None | |
1334 | ||
47 | 1335 | def toggle(): |
1336 | self.connectbtn.SetLabel(_("&Connect")) | |
1337 | self.connectbtn.SetToolTip(wx.ToolTip(_("Connect to the printer"))) | |
1338 | self.connectbtn_cb_var = self.connect | |
1339 | self.gui_set_disconnected() | |
1340 | wx.CallAfter(toggle) | |
15 | 1341 | |
1342 | if self.paused: | |
1343 | self.p.paused = 0 | |
1344 | self.p.printing = 0 | |
47 | 1345 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
1346 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) | |
15 | 1347 | self.paused = 0 |
1348 | if self.sdprinting: | |
1349 | self.p.send_now("M26 S0") | |
1350 | ||
1351 | # Relayout the toolbar to handle new buttons size | |
1352 | wx.CallAfter(self.toolbarsizer.Layout) | |
1353 | ||
1354 | def reset(self, event): | |
1355 | self.log(_("Reset.")) | |
1356 | dlg = wx.MessageDialog(self, _("Are you sure you want to reset the printer?"), _("Reset?"), wx.YES | wx.NO) | |
1357 | if dlg.ShowModal() == wx.ID_YES: | |
1358 | self.p.reset() | |
1359 | self.sethotendgui(0) | |
1360 | self.setbedgui(0) | |
1361 | self.p.printing = 0 | |
47 | 1362 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) |
15 | 1363 | if self.paused: |
1364 | self.p.paused = 0 | |
47 | 1365 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
15 | 1366 | self.paused = 0 |
1367 | wx.CallAfter(self.toolbarsizer.Layout) | |
1368 | dlg.Destroy() | |
1369 | ||
1370 | # -------------------------------------------------------------- | |
1371 | # Print/upload handling | |
1372 | # -------------------------------------------------------------- | |
1373 | ||
1374 | def on_startprint(self): | |
47 | 1375 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
15 | 1376 | wx.CallAfter(self.pausebtn.Enable) |
1377 | wx.CallAfter(self.printbtn.SetLabel, _("Restart")) | |
1378 | wx.CallAfter(self.toolbarsizer.Layout) | |
1379 | ||
47 | 1380 | def printfile(self, event=None): |
15 | 1381 | self.extra_print_time = 0 |
1382 | if self.paused: | |
1383 | self.p.paused = 0 | |
1384 | self.paused = 0 | |
1385 | if self.sdprinting: | |
1386 | self.on_startprint() | |
1387 | self.p.send_now("M26 S0") | |
1388 | self.p.send_now("M24") | |
1389 | return | |
1390 | ||
1391 | if not self.fgcode: | |
1392 | wx.CallAfter(self.statusbar.SetStatusText, _("No file loaded. Please use load first.")) | |
1393 | return | |
1394 | if not self.p.online: | |
1395 | wx.CallAfter(self.statusbar.SetStatusText, _("Not connected to printer.")) | |
1396 | return | |
1397 | self.sdprinting = False | |
1398 | self.on_startprint() | |
1399 | self.p.startprint(self.fgcode) | |
1400 | ||
1401 | def sdprintfile(self, event): | |
1402 | self.extra_print_time = 0 | |
1403 | self.on_startprint() | |
1404 | threading.Thread(target = self.getfiles).start() | |
1405 | ||
1406 | def upload(self, event): | |
1407 | if not self.fgcode: | |
1408 | return | |
1409 | if not self.p.online: | |
1410 | return | |
1411 | dlg = wx.TextEntryDialog(self, ("Enter a target filename in 8.3 format:"), _("Pick SD filename"), dosify(self.filename)) | |
1412 | if dlg.ShowModal() == wx.ID_OK: | |
1413 | self.p.send_now("M21") | |
1414 | self.p.send_now("M28 " + str(dlg.GetValue())) | |
1415 | self.recvlisteners.append(self.uploadtrigger) | |
1416 | dlg.Destroy() | |
1417 | ||
1418 | def uploadtrigger(self, l): | |
1419 | if "Writing to file" in l: | |
1420 | self.uploading = True | |
1421 | self.p.startprint(self.fgcode) | |
1422 | self.p.endcb = self.endupload | |
1423 | self.recvlisteners.remove(self.uploadtrigger) | |
1424 | elif "open failed, File" in l: | |
1425 | self.recvlisteners.remove(self.uploadtrigger) | |
1426 | ||
1427 | def endupload(self): | |
1428 | self.p.send_now("M29 ") | |
1429 | wx.CallAfter(self.statusbar.SetStatusText, _("File upload complete")) | |
1430 | time.sleep(0.5) | |
1431 | self.p.clear = True | |
1432 | self.uploading = False | |
1433 | ||
1434 | def pause(self, event = None): | |
1435 | if not self.paused: | |
1436 | self.log(_("Print paused at: %s") % format_time(time.time())) | |
47 | 1437 | if self.settings.display_progress_on_printer: |
1438 | printer_progress_string = "M117 PausedInPronterface" | |
1439 | self.p.send_now(printer_progress_string) | |
15 | 1440 | if self.sdprinting: |
1441 | self.p.send_now("M25") | |
1442 | else: | |
1443 | if not self.p.printing: | |
1444 | return | |
1445 | self.p.pause() | |
1446 | self.p.runSmallScript(self.pauseScript) | |
1447 | self.paused = True | |
1448 | # self.p.runSmallScript(self.pauseScript) | |
1449 | self.extra_print_time += int(time.time() - self.starttime) | |
1450 | wx.CallAfter(self.pausebtn.SetLabel, _("Resume")) | |
1451 | wx.CallAfter(self.toolbarsizer.Layout) | |
1452 | else: | |
1453 | self.log(_("Resuming.")) | |
47 | 1454 | if self.settings.display_progress_on_printer: |
1455 | printer_progress_string = "M117 Resuming" | |
1456 | self.p.send_now(printer_progress_string) | |
15 | 1457 | self.paused = False |
1458 | if self.sdprinting: | |
1459 | self.p.send_now("M24") | |
1460 | else: | |
1461 | self.p.resume() | |
47 | 1462 | wx.CallAfter(self.pausebtn.SetLabel, _("&Pause")) |
15 | 1463 | wx.CallAfter(self.toolbarsizer.Layout) |
1464 | ||
1465 | def recover(self, event): | |
1466 | self.extra_print_time = 0 | |
1467 | if not self.p.online: | |
1468 | wx.CallAfter(self.statusbar.SetStatusText, _("Not connected to printer.")) | |
1469 | return | |
1470 | # Reset Z | |
1471 | self.p.send_now("G92 Z%f" % self.predisconnect_layer) | |
1472 | # Home X and Y | |
1473 | self.p.send_now("G28 X Y") | |
1474 | self.on_startprint() | |
1475 | self.p.startprint(self.predisconnect_mainqueue, self.p.queueindex) | |
1476 | ||
1477 | # -------------------------------------------------------------- | |
1478 | # File loading handling | |
1479 | # -------------------------------------------------------------- | |
1480 | ||
1481 | def filesloaded(self): | |
1482 | dlg = wx.SingleChoiceDialog(self, _("Select the file to print"), _("Pick SD file"), self.sdfiles) | |
1483 | if dlg.ShowModal() == wx.ID_OK: | |
1484 | target = dlg.GetStringSelection() | |
1485 | if len(target): | |
1486 | self.recvlisteners.append(self.waitforsdresponse) | |
1487 | self.p.send_now("M23 " + target.lower()) | |
1488 | dlg.Destroy() | |
1489 | ||
1490 | def getfiles(self): | |
1491 | if not self.p.online: | |
1492 | self.sdfiles = [] | |
1493 | return | |
1494 | self.sdlisting = 0 | |
1495 | self.sdfiles = [] | |
1496 | self.recvlisteners.append(self.listfiles) | |
1497 | self.p.send_now("M21") | |
1498 | self.p.send_now("M20") | |
1499 | ||
1500 | def model_to_gcode_filename(self, filename): | |
1501 | suffix = "_export.gcode" | |
1502 | for ext in [".stl", ".obj"]: | |
1503 | filename = filename.replace(ext, suffix) | |
1504 | filename = filename.replace(ext.upper(), suffix) | |
1505 | return filename | |
1506 | ||
1507 | def slice_func(self): | |
1508 | try: | |
1509 | output_filename = self.model_to_gcode_filename(self.filename) | |
47 | 1510 | pararray = prepare_command(self.settings.slicecommandpath+self.settings.slicecommand, |
15 | 1511 | {"$s": self.filename, "$o": output_filename}) |
1512 | if self.settings.slic3rintegration: | |
1513 | for cat, config in self.slic3r_configs.items(): | |
1514 | if config: | |
1515 | fpath = os.path.join(self.slic3r_configpath, cat, config) | |
1516 | pararray += ["--load", fpath] | |
1517 | self.log(_("Running ") + " ".join(pararray)) | |
47 | 1518 | self.slicep = subprocess.Popen(pararray, stdin=subprocess.DEVNULL, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, universal_newlines = True) |
15 | 1519 | while True: |
1520 | o = self.slicep.stdout.read(1) | |
1521 | if o == '' and self.slicep.poll() is not None: break | |
1522 | sys.stdout.write(o) | |
1523 | self.slicep.wait() | |
1524 | self.stopsf = 1 | |
1525 | except: | |
1526 | self.logError(_("Failed to execute slicing software: ") | |
1527 | + "\n" + traceback.format_exc()) | |
1528 | self.stopsf = 1 | |
1529 | ||
1530 | def slice_monitor(self): | |
1531 | while not self.stopsf: | |
1532 | try: | |
1533 | wx.CallAfter(self.statusbar.SetStatusText, _("Slicing...")) # +self.cout.getvalue().split("\n")[-1]) | |
1534 | except: | |
1535 | pass | |
1536 | time.sleep(0.1) | |
1537 | fn = self.filename | |
1538 | try: | |
1539 | self.load_gcode_async(self.model_to_gcode_filename(self.filename)) | |
1540 | except: | |
1541 | self.filename = fn | |
1542 | self.slicing = False | |
1543 | self.slicep = None | |
47 | 1544 | self.loadbtn.SetLabel, _("Load file") |
15 | 1545 | |
1546 | def slice(self, filename): | |
1547 | wx.CallAfter(self.loadbtn.SetLabel, _("Cancel")) | |
1548 | wx.CallAfter(self.toolbarsizer.Layout) | |
1549 | self.log(_("Slicing ") + filename) | |
1550 | self.cout = StringIO.StringIO() | |
1551 | self.filename = filename | |
1552 | self.stopsf = 0 | |
1553 | self.slicing = True | |
1554 | threading.Thread(target = self.slice_func).start() | |
1555 | threading.Thread(target = self.slice_monitor).start() | |
1556 | ||
1557 | def cmdline_filename_callback(self, filename): | |
1558 | # Do nothing when processing a filename from command line, as we'll | |
1559 | # handle it when everything has been prepared | |
1560 | self.filename = filename | |
1561 | ||
1562 | def do_load(self, l): | |
1563 | if hasattr(self, 'slicing'): | |
1564 | self.loadfile(None, l) | |
1565 | else: | |
1566 | self._do_load(l) | |
1567 | ||
1568 | def load_recent_file(self, event): | |
1569 | fileid = event.GetId() - wx.ID_FILE1 | |
1570 | path = self.filehistory.GetHistoryFile(fileid) | |
1571 | self.loadfile(None, filename = path) | |
1572 | ||
1573 | def loadfile(self, event, filename = None): | |
1574 | if self.slicing and self.slicep is not None: | |
1575 | self.slicep.terminate() | |
1576 | return | |
1577 | basedir = self.settings.last_file_path | |
1578 | if not os.path.exists(basedir): | |
1579 | basedir = "." | |
1580 | try: | |
1581 | basedir = os.path.split(self.filename)[0] | |
1582 | except: | |
1583 | pass | |
1584 | dlg = None | |
1585 | if filename is None: | |
1586 | dlg = wx.FileDialog(self, _("Open file to print"), basedir, style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) | |
48 | 1587 | # add image files to GCODE file list |
1588 | dlg.SetWildcard(_("GCODE and Image files|*.gcode;*.gco;*.g;*.png;*.svg;*.hpgl;*.plt|OBJ, STL, and GCODE files (*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ)|*.gcode;*.gco;*.g;*.stl;*.STL;*.obj;*.OBJ|GCODE files (*.gcode;*.gco;*.g)|*.gcode;*.gco;*.g|OBJ, STL files (*.stl;*.STL;*.obj;*.OBJ)|*.stl;*.STL;*.obj;*.OBJ|All Files (*.*)|*.*")) | |
15 | 1589 | try: |
1590 | dlg.SetFilterIndex(self.settings.last_file_filter) | |
1591 | except: | |
1592 | pass | |
1593 | if filename or dlg.ShowModal() == wx.ID_OK: | |
1594 | if filename: | |
1595 | name = filename | |
1596 | else: | |
1597 | name = dlg.GetPath() | |
1598 | self.set("last_file_filter", dlg.GetFilterIndex()) | |
1599 | dlg.Destroy() | |
1600 | if not os.path.exists(name): | |
1601 | self.statusbar.SetStatusText(_("File not found!")) | |
1602 | return | |
1603 | path = os.path.split(name)[0] | |
1604 | if path != self.settings.last_file_path: | |
1605 | self.set("last_file_path", path) | |
1606 | try: | |
1607 | abspath = os.path.abspath(name) | |
1608 | recent_files = [] | |
1609 | try: | |
1610 | recent_files = json.loads(self.settings.recentfiles) | |
1611 | except: | |
1612 | self.logError(_("Failed to load recent files list:") + | |
1613 | "\n" + traceback.format_exc()) | |
1614 | if abspath in recent_files: | |
1615 | recent_files.remove(abspath) | |
1616 | recent_files.insert(0, abspath) | |
1617 | if len(recent_files) > 5: | |
1618 | recent_files = recent_files[:5] | |
1619 | self.set("recentfiles", json.dumps(recent_files)) | |
1620 | except: | |
1621 | self.logError(_("Could not update recent files list:") + | |
1622 | "\n" + traceback.format_exc()) | |
48 | 1623 | |
1624 | # reload the library local so we dont have to restart the whole app when making code changes | |
1625 | reload(laser) | |
1626 | ||
15 | 1627 | if name.lower().endswith(".stl") or name.lower().endswith(".obj"): |
1628 | self.slice(name) | |
48 | 1629 | elif name.lower().endswith(".png") or name.lower().endswith(".jpg") or name.lower().endswith(".gif"): |
1630 | # Generate GCODE from IMAGE | |
1631 | lc = laser.Lasercutter(pronterwindow = self) | |
1632 | lc.image2gcode(name) | |
1633 | wx.CallAfter(self.endcb_lasercut) | |
1634 | elif name.lower().endswith(".svg"): | |
1635 | # Generate GCODE from SVG | |
1636 | lc = laser.Lasercutter(pronterwindow = self) | |
1637 | lc.svg2gcode(name) | |
1638 | wx.CallAfter(self.endcb_lasercut) | |
1639 | elif name.lower().endswith(".hpgl") or name.lower().endswith(".plt"): | |
1640 | # Generate GCODE from HPGL | |
1641 | lc = laser.Lasercutter(pronterwindow = self) | |
1642 | lc.hpgl2gcode(name) | |
1643 | wx.CallAfter(self.endcb_lasercut) | |
15 | 1644 | else: |
1645 | self.load_gcode_async(name) | |
1646 | else: | |
1647 | dlg.Destroy() | |
1648 | ||
1649 | def load_gcode_async(self, filename): | |
1650 | self.filename = filename | |
1651 | gcode = self.pre_gcode_load() | |
1652 | self.log(_("Loading file: %s") % filename) | |
1653 | threading.Thread(target = self.load_gcode_async_thread, args = (gcode,)).start() | |
1654 | ||
1655 | def load_gcode_async_thread(self, gcode): | |
1656 | try: | |
1657 | self.load_gcode(self.filename, | |
1658 | layer_callback = self.layer_ready_cb, | |
1659 | gcode = gcode) | |
1660 | except PronterfaceQuitException: | |
1661 | return | |
47 | 1662 | except Exception as e: |
1663 | self.log(str(e)) | |
1664 | wx.CallAfter(self.post_gcode_load,False,True) | |
1665 | return | |
15 | 1666 | wx.CallAfter(self.post_gcode_load) |
1667 | ||
1668 | def layer_ready_cb(self, gcode, layer): | |
1669 | global pronterface_quitting | |
1670 | if pronterface_quitting: | |
1671 | raise PronterfaceQuitException | |
1672 | if not self.settings.refreshwhenloading: | |
1673 | return | |
1674 | self.viz_last_layer = layer | |
1675 | if time.time() - self.viz_last_yield > 1.0: | |
1676 | time.sleep(0.2) | |
1677 | self.loading_gcode_message = _("Loading %s: %d layers loaded (%d lines)") % (self.filename, layer + 1, len(gcode)) | |
1678 | self.viz_last_yield = time.time() | |
1679 | wx.CallAfter(self.statusbar.SetStatusText, self.loading_gcode_message) | |
1680 | ||
1681 | def start_viz_thread(self, gcode = None): | |
1682 | threading.Thread(target = self.loadviz, args = (gcode,)).start() | |
1683 | ||
1684 | def pre_gcode_load(self): | |
1685 | self.loading_gcode = True | |
1686 | self.loading_gcode_message = _("Loading %s...") % self.filename | |
1687 | if self.settings.mainviz == "None": | |
1688 | gcode = gcoder.LightGCode(deferred = True) | |
1689 | else: | |
47 | 1690 | gcode = gcoder.GCode(deferred = True, cutting_as_extrusion = self.settings.cutting_as_extrusion) |
15 | 1691 | self.viz_last_yield = 0 |
1692 | self.viz_last_layer = -1 | |
1693 | self.start_viz_thread(gcode) | |
1694 | return gcode | |
1695 | ||
47 | 1696 | def post_gcode_load(self, print_stats = True, failed=False): |
15 | 1697 | # Must be called in wx.CallAfter for safety |
1698 | self.loading_gcode = False | |
47 | 1699 | if not failed: |
1700 | self.SetTitle(_("Pronterface - %s") % self.filename) | |
1701 | message = _("Loaded %s, %d lines") % (self.filename, len(self.fgcode),) | |
1702 | self.log(message) | |
1703 | self.statusbar.SetStatusText(message) | |
1704 | self.savebtn.Enable(True) | |
15 | 1705 | self.loadbtn.SetLabel(_("Load File")) |
47 | 1706 | self.printbtn.SetLabel(_("&Print")) |
1707 | self.pausebtn.SetLabel(_("&Pause")) | |
15 | 1708 | self.pausebtn.Disable() |
1709 | self.recoverbtn.Disable() | |
47 | 1710 | if not failed and self.p.online: |
15 | 1711 | self.printbtn.Enable() |
1712 | self.toolbarsizer.Layout() | |
1713 | self.viz_last_layer = None | |
1714 | if print_stats: | |
1715 | self.output_gcode_stats() | |
1716 | ||
47 | 1717 | def calculate_remaining_filament(self, length, extruder = 0): |
1718 | """ | |
1719 | float calculate_remaining_filament( float length, int extruder ) | |
1720 | ||
1721 | Calculate the remaining length of filament for the given extruder if | |
1722 | the given length were to be extruded. | |
1723 | """ | |
1724 | ||
1725 | remainder = self.spool_manager.getRemainingFilament(extruder) - length | |
1726 | minimum_warning_length = 1000.0 | |
1727 | if remainder < minimum_warning_length: | |
1728 | self.log(_("\nWARNING: Currently loaded spool for extruder " + | |
1729 | "%d will likely run out of filament during the print.\n" % | |
1730 | extruder)) | |
1731 | return remainder | |
1732 | ||
15 | 1733 | def output_gcode_stats(self): |
1734 | gcode = self.fgcode | |
47 | 1735 | self.spool_manager.refresh() |
1736 | ||
1737 | self.log(_("%s of filament used in this print") % format_length(gcode.filament_length)) | |
1738 | ||
1739 | if len(gcode.filament_length_multi) > 1: | |
15 | 1740 | for i in enumerate(gcode.filament_length_multi): |
47 | 1741 | if self.spool_manager.getSpoolName(i[0]) == None: |
1742 | logging.info("- Extruder %d: %0.02fmm" % (i[0], i[1])) | |
1743 | else: | |
1744 | logging.info(("- Extruder %d: %0.02fmm" % (i[0], i[1]) + | |
1745 | " from spool '%s' (%.2fmm will remain)" % | |
1746 | (self.spool_manager.getSpoolName(i[0]), | |
1747 | self.calculate_remaining_filament(i[1], i[0])))) | |
1748 | elif self.spool_manager.getSpoolName(0) != None: | |
1749 | self.log( | |
1750 | _("Using spool '%s' (%s of filament will remain)") % | |
1751 | (self.spool_manager.getSpoolName(0), | |
1752 | format_length(self.calculate_remaining_filament( | |
1753 | gcode.filament_length, 0)))) | |
1754 | ||
15 | 1755 | self.log(_("The print goes:")) |
1756 | self.log(_("- from %.2f mm to %.2f mm in X and is %.2f mm wide") % (gcode.xmin, gcode.xmax, gcode.width)) | |
1757 | self.log(_("- from %.2f mm to %.2f mm in Y and is %.2f mm deep") % (gcode.ymin, gcode.ymax, gcode.depth)) | |
1758 | self.log(_("- from %.2f mm to %.2f mm in Z and is %.2f mm high") % (gcode.zmin, gcode.zmax, gcode.height)) | |
1759 | self.log(_("Estimated duration: %d layers, %s") % gcode.estimate_duration()) | |
1760 | ||
1761 | def loadviz(self, gcode = None): | |
47 | 1762 | try: |
1763 | self.gviz.clear() | |
1764 | self.gwindow.p.clear() | |
1765 | if gcode is not None: | |
1766 | generator = self.gviz.addfile_perlayer(gcode, True) | |
1767 | next_layer = 0 | |
1768 | # Progressive loading of visualization | |
1769 | # We load layers up to the last one which has been processed in GCoder | |
1770 | # (self.viz_last_layer) | |
1771 | # Once the GCode has been entirely loaded, this variable becomes None, | |
1772 | # indicating that we can do the last generator call to finish the | |
1773 | # loading of the visualization, which will itself return None. | |
1774 | # During preloading we verify that the layer we added is the one we | |
1775 | # expected through the assert call. | |
1776 | while True: | |
1777 | global pronterface_quitting | |
1778 | if pronterface_quitting: | |
1779 | return | |
1780 | max_layer = self.viz_last_layer | |
1781 | if max_layer is None: | |
1782 | break | |
1783 | start_layer = next_layer | |
1784 | while next_layer <= max_layer: | |
1785 | assert next(generator) == next_layer | |
1786 | next_layer += 1 | |
1787 | if next_layer != start_layer: | |
1788 | wx.CallAfter(self.gviz.Refresh) | |
1789 | time.sleep(0.1) | |
1790 | generator_output = next(generator) | |
1791 | while generator_output is not None: | |
1792 | assert generator_output == next_layer | |
15 | 1793 | next_layer += 1 |
47 | 1794 | generator_output = next(generator) |
1795 | else: | |
1796 | # If GCode is not being loaded asynchroneously, it is already | |
1797 | # loaded, so let's make visualization sequentially | |
1798 | gcode = self.fgcode | |
1799 | self.gviz.addfile(gcode) | |
1800 | wx.CallAfter(self.gviz.Refresh) | |
1801 | # Load external window sequentially now that everything is ready. | |
1802 | # We can't really do any better as the 3D viewer might clone the | |
1803 | # finalized model from the main visualization | |
1804 | self.gwindow.p.addfile(gcode) | |
1805 | except: | |
1806 | logging.error(traceback.format_exc()) | |
1807 | wx.CallAfter(self.gviz.Refresh) | |
15 | 1808 | |
1809 | # -------------------------------------------------------------- | |
1810 | # File saving handling | |
1811 | # -------------------------------------------------------------- | |
1812 | ||
1813 | def savefile(self, event): | |
1814 | basedir = self.settings.last_file_path | |
1815 | if not os.path.exists(basedir): | |
1816 | basedir = "." | |
1817 | try: | |
1818 | basedir = os.path.split(self.filename)[0] | |
1819 | except: | |
1820 | pass | |
1821 | dlg = wx.FileDialog(self, _("Save as"), basedir, style = wx.FD_SAVE) | |
1822 | dlg.SetWildcard(_("GCODE files (*.gcode;*.gco;*.g)|*.gcode;*.gco;*.g|All Files (*.*)|*.*")) | |
1823 | if dlg.ShowModal() == wx.ID_OK: | |
1824 | name = dlg.GetPath() | |
1825 | open(name, "w").write("\n".join((line.raw for line in self.fgcode))) | |
1826 | self.log(_("G-Code succesfully saved to %s") % name) | |
1827 | dlg.Destroy() | |
1828 | ||
1829 | # -------------------------------------------------------------- | |
1830 | # Printcore callbacks | |
1831 | # -------------------------------------------------------------- | |
1832 | ||
1833 | def process_host_command(self, command): | |
1834 | """Override host command handling""" | |
1835 | command = command.lstrip() | |
1836 | if command.startswith(";@pause"): | |
1837 | self.pause(None) | |
1838 | else: | |
1839 | pronsole.pronsole.process_host_command(self, command) | |
1840 | ||
1841 | def startcb(self, resuming = False): | |
1842 | """Callback on print start""" | |
1843 | pronsole.pronsole.startcb(self, resuming) | |
1844 | if self.settings.lockbox and self.settings.lockonstart: | |
1845 | wx.CallAfter(self.lock, force = True) | |
1846 | ||
1847 | def endcb(self): | |
1848 | """Callback on print end/pause""" | |
1849 | pronsole.pronsole.endcb(self) | |
1850 | if self.p.queueindex == 0: | |
1851 | self.p.runSmallScript(self.endScript) | |
47 | 1852 | if self.settings.display_progress_on_printer: |
1853 | printer_progress_string = "M117 Finished Print" | |
1854 | self.p.send_now(printer_progress_string) | |
15 | 1855 | wx.CallAfter(self.pausebtn.Disable) |
47 | 1856 | wx.CallAfter(self.printbtn.SetLabel, _("&Print")) |
15 | 1857 | wx.CallAfter(self.toolbarsizer.Layout) |
48 | 1858 | wx.CallAfter(self.endcb_lasercut) |
33
eee51ca7cbe7
Added support for multiple cutting passes with automatic Z refocusing
mdd
parents:
31
diff
changeset
|
1859 | |
15 | 1860 | def online(self): |
1861 | """Callback when printer goes online""" | |
1862 | self.log(_("Printer is now online.")) | |
1863 | wx.CallAfter(self.online_gui) | |
1864 | ||
1865 | def online_gui(self): | |
1866 | """Callback when printer goes online (graphical bits)""" | |
47 | 1867 | self.connectbtn.SetLabel(_("Dis&connect")) |
15 | 1868 | self.connectbtn.SetToolTip(wx.ToolTip("Disconnect from the printer")) |
47 | 1869 | self.connectbtn_cb_var = self.disconnect |
15 | 1870 | |
1871 | if hasattr(self, "extrudersel"): | |
1872 | self.do_tool(self.extrudersel.GetValue()) | |
1873 | ||
1874 | self.gui_set_connected() | |
1875 | ||
1876 | if self.filename: | |
1877 | self.printbtn.Enable() | |
1878 | ||
1879 | wx.CallAfter(self.toolbarsizer.Layout) | |
1880 | ||
1881 | def sentcb(self, line, gline): | |
1882 | """Callback when a printer gcode has been sent""" | |
1883 | if not gline: | |
1884 | pass | |
1885 | elif gline.command in ["M104", "M109"]: | |
1886 | gline_s = gcoder.S(gline) | |
1887 | if gline_s is not None: | |
1888 | temp = gline_s | |
1889 | if self.display_gauges: wx.CallAfter(self.hottgauge.SetTarget, temp) | |
1890 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0TargetTemperature, temp) | |
1891 | elif gline.command in ["M140", "M190"]: | |
1892 | gline_s = gcoder.S(gline) | |
1893 | if gline_s is not None: | |
1894 | temp = gline_s | |
1895 | if self.display_gauges: wx.CallAfter(self.bedtgauge.SetTarget, temp) | |
1896 | if self.display_graph: wx.CallAfter(self.graph.SetBedTargetTemperature, temp) | |
1897 | elif gline.command in ["M106"]: | |
1898 | gline_s=gcoder.S(gline) | |
1899 | fanpow=255 | |
1900 | if gline_s is not None: | |
1901 | fanpow=gline_s | |
1902 | if self.display_graph: wx.CallAfter(self.graph.SetFanPower, fanpow) | |
1903 | elif gline.command in ["M107"]: | |
1904 | if self.display_graph: wx.CallAfter(self.graph.SetFanPower, 0) | |
1905 | elif gline.command.startswith("T"): | |
1906 | tool = gline.command[1:] | |
1907 | if hasattr(self, "extrudersel"): wx.CallAfter(self.extrudersel.SetValue, tool) | |
1908 | if gline.is_move: | |
1909 | self.sentglines.put_nowait(gline) | |
1910 | ||
1911 | def is_excluded_move(self, gline): | |
1912 | """Check whether the given moves ends at a position specified as | |
1913 | excluded in the part excluder""" | |
1914 | if not gline.is_move or not self.excluder or not self.excluder.rectangles: | |
1915 | return False | |
1916 | for (x0, y0, x1, y1) in self.excluder.rectangles: | |
1917 | if x0 <= gline.current_x <= x1 and y0 <= gline.current_y <= y1: | |
1918 | return True | |
1919 | return False | |
1920 | ||
1921 | def preprintsendcb(self, gline, next_gline): | |
1922 | """Callback when a printer gcode is about to be sent. We use it to | |
1923 | exclude moves defined by the part excluder tool""" | |
1924 | if not self.is_excluded_move(gline): | |
1925 | return gline | |
1926 | else: | |
1927 | if gline.z is not None: | |
1928 | if gline.relative: | |
1929 | if self.excluder_z_abs is not None: | |
1930 | self.excluder_z_abs += gline.z | |
1931 | elif self.excluder_z_rel is not None: | |
1932 | self.excluder_z_rel += gline.z | |
1933 | else: | |
1934 | self.excluder_z_rel = gline.z | |
1935 | else: | |
1936 | self.excluder_z_rel = None | |
1937 | self.excluder_z_abs = gline.z | |
1938 | if gline.e is not None and not gline.relative_e: | |
1939 | self.excluder_e = gline.e | |
1940 | # If next move won't be excluded, push the changes we have to do | |
1941 | if next_gline is not None and not self.is_excluded_move(next_gline): | |
1942 | if self.excluder_e is not None: | |
1943 | self.p.send_now("G92 E%.5f" % self.excluder_e) | |
1944 | self.excluder_e = None | |
1945 | if self.excluder_z_abs is not None: | |
1946 | if gline.relative: | |
1947 | self.p.send_now("G90") | |
1948 | self.p.send_now("G1 Z%.5f" % self.excluder_z_abs) | |
1949 | self.excluder_z_abs = None | |
1950 | if gline.relative: | |
1951 | self.p.send_now("G91") | |
1952 | if self.excluder_z_rel is not None: | |
1953 | if not gline.relative: | |
1954 | self.p.send_now("G91") | |
1955 | self.p.send_now("G1 Z%.5f" % self.excluder_z_rel) | |
1956 | self.excluder_z_rel = None | |
1957 | if not gline.relative: | |
1958 | self.p.send_now("G90") | |
1959 | return None | |
1960 | ||
1961 | def printsentcb(self, gline): | |
1962 | """Callback when a print gcode has been sent""" | |
1963 | if gline.is_move: | |
1964 | if hasattr(self.gwindow, "set_current_gline"): | |
1965 | wx.CallAfter(self.gwindow.set_current_gline, gline) | |
1966 | if hasattr(self.gviz, "set_current_gline"): | |
1967 | wx.CallAfter(self.gviz.set_current_gline, gline) | |
1968 | ||
1969 | def layer_change_cb(self, newlayer): | |
1970 | """Callback when the printed layer changed""" | |
1971 | pronsole.pronsole.layer_change_cb(self, newlayer) | |
1972 | if self.settings.mainviz != "3D" or self.settings.trackcurrentlayer3d: | |
1973 | wx.CallAfter(self.gviz.setlayer, newlayer) | |
1974 | ||
1975 | def update_tempdisplay(self): | |
1976 | try: | |
1977 | temps = parse_temperature_report(self.tempreadings) | |
1978 | if "T0" in temps and temps["T0"][0]: | |
1979 | hotend_temp = float(temps["T0"][0]) | |
1980 | elif "T" in temps and temps["T"][0]: | |
1981 | hotend_temp = float(temps["T"][0]) | |
1982 | else: | |
1983 | hotend_temp = None | |
1984 | if hotend_temp is not None: | |
1985 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0Temperature, hotend_temp) | |
1986 | if self.display_gauges: wx.CallAfter(self.hottgauge.SetValue, hotend_temp) | |
1987 | setpoint = None | |
1988 | if "T0" in temps and temps["T0"][1]: setpoint = float(temps["T0"][1]) | |
1989 | elif temps["T"][1]: setpoint = float(temps["T"][1]) | |
1990 | if setpoint is not None: | |
1991 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder0TargetTemperature, setpoint) | |
1992 | if self.display_gauges: wx.CallAfter(self.hottgauge.SetTarget, setpoint) | |
1993 | if "T1" in temps: | |
1994 | hotend_temp = float(temps["T1"][0]) | |
1995 | if self.display_graph: wx.CallAfter(self.graph.SetExtruder1Temperature, hotend_temp) | |
1996 | setpoint = temps["T1"][1] | |
1997 | if setpoint and self.display_graph: | |
1998 | wx.CallAfter(self.graph.SetExtruder1TargetTemperature, float(setpoint)) | |
1999 | bed_temp = float(temps["B"][0]) if "B" in temps and temps["B"][0] else None | |
2000 | if bed_temp is not None: | |
2001 | if self.display_graph: wx.CallAfter(self.graph.SetBedTemperature, bed_temp) | |
2002 | if self.display_gauges: wx.CallAfter(self.bedtgauge.SetValue, bed_temp) | |
2003 | setpoint = temps["B"][1] | |
2004 | if setpoint: | |
2005 | setpoint = float(setpoint) | |
2006 | if self.display_graph: wx.CallAfter(self.graph.SetBedTargetTemperature, setpoint) | |
2007 | if self.display_gauges: wx.CallAfter(self.bedtgauge.SetTarget, setpoint) | |
2008 | except: | |
2009 | self.logError(traceback.format_exc()) | |
2010 | ||
2011 | def update_pos(self): | |
2012 | bits = gcoder.m114_exp.findall(self.posreport) | |
2013 | x = None | |
2014 | y = None | |
2015 | z = None | |
2016 | for bit in bits: | |
2017 | if not bit[0]: continue | |
2018 | if x is None and bit[0] == "X": | |
2019 | x = float(bit[1]) | |
2020 | elif y is None and bit[0] == "Y": | |
2021 | y = float(bit[1]) | |
2022 | elif z is None and bit[0] == "Z": | |
2023 | z = float(bit[1]) | |
2024 | if x is not None: self.current_pos[0] = x | |
2025 | if y is not None: self.current_pos[1] = y | |
2026 | if z is not None: self.current_pos[2] = z | |
2027 | ||
2028 | def recvcb_actions(self, l): | |
2029 | if l.startswith("!!"): | |
2030 | if not self.paused: | |
2031 | wx.CallAfter(self.pause) | |
2032 | msg = l.split(" ", 1) | |
2033 | if len(msg) > 1 and not self.p.loud: | |
47 | 2034 | self.log(msg[1] + "\n") |
15 | 2035 | return True |
2036 | elif l.startswith("//"): | |
2037 | command = l.split(" ", 1) | |
2038 | if len(command) > 1: | |
2039 | command = command[1] | |
2040 | command = command.split(":") | |
2041 | if len(command) == 2 and command[0] == "action": | |
2042 | command = command[1] | |
2043 | self.log(_("Received command %s") % command) | |
2044 | if command == "pause": | |
2045 | if not self.paused: | |
2046 | wx.CallAfter(self.pause) | |
2047 | return True | |
2048 | elif command == "resume": | |
2049 | if self.paused: | |
2050 | wx.CallAfter(self.pause) | |
2051 | return True | |
2052 | elif command == "disconnect": | |
2053 | wx.CallAfter(self.disconnect) | |
2054 | return True | |
2055 | return False | |
2056 | ||
2057 | def recvcb(self, l): | |
2058 | l = l.rstrip() | |
2059 | if not self.recvcb_actions(l): | |
2060 | report_type = self.recvcb_report(l) | |
2061 | isreport = report_type != REPORT_NONE | |
2062 | if report_type & REPORT_POS: | |
2063 | self.update_pos() | |
2064 | elif report_type & REPORT_TEMP: | |
2065 | wx.CallAfter(self.tempdisp.SetLabel, self.tempreadings.strip().replace("ok ", "")) | |
2066 | self.update_tempdisplay() | |
47 | 2067 | if not self.lineignorepattern.match(l) and not self.p.loud and (l not in ["ok", "wait"] and (not isreport or report_type & REPORT_MANUAL)): |
2068 | self.log(l) | |
15 | 2069 | for listener in self.recvlisteners: |
2070 | listener(l) | |
2071 | ||
2072 | def listfiles(self, line, ignored = False): | |
2073 | if "Begin file list" in line: | |
2074 | self.sdlisting = True | |
2075 | elif "End file list" in line: | |
2076 | self.sdlisting = False | |
2077 | self.recvlisteners.remove(self.listfiles) | |
2078 | wx.CallAfter(self.filesloaded) | |
2079 | elif self.sdlisting: | |
47 | 2080 | self.sdfiles.append(re.sub(" \d+$","",line.strip().lower())) |
15 | 2081 | |
2082 | def waitforsdresponse(self, l): | |
2083 | if "file.open failed" in l: | |
2084 | wx.CallAfter(self.statusbar.SetStatusText, _("Opening file failed.")) | |
2085 | self.recvlisteners.remove(self.waitforsdresponse) | |
2086 | return | |
2087 | if "File opened" in l: | |
2088 | wx.CallAfter(self.statusbar.SetStatusText, l) | |
2089 | if "File selected" in l: | |
2090 | wx.CallAfter(self.statusbar.SetStatusText, _("Starting print")) | |
2091 | self.sdprinting = True | |
2092 | self.p.send_now("M24") | |
2093 | self.startcb() | |
2094 | return | |
2095 | if "Done printing file" in l: | |
2096 | wx.CallAfter(self.statusbar.SetStatusText, l) | |
2097 | self.sdprinting = False | |
2098 | self.recvlisteners.remove(self.waitforsdresponse) | |
2099 | self.endcb() | |
2100 | return | |
2101 | if "SD printing byte" in l: | |
2102 | # M27 handler | |
2103 | try: | |
2104 | resp = l.split() | |
2105 | vals = resp[-1].split("/") | |
2106 | self.percentdone = 100.0 * int(vals[0]) / int(vals[1]) | |
2107 | except: | |
2108 | pass | |
2109 | ||
2110 | # -------------------------------------------------------------- | |
2111 | # Custom buttons handling | |
2112 | # -------------------------------------------------------------- | |
2113 | ||
2114 | def cbuttons_reload(self): | |
2115 | allcbs = getattr(self, "custombuttons_widgets", []) | |
2116 | for button in allcbs: | |
2117 | self.cbuttonssizer.Detach(button) | |
2118 | button.Destroy() | |
2119 | self.custombuttons_widgets = [] | |
2120 | custombuttons = self.custombuttons[:] + [None] | |
2121 | for i, btndef in enumerate(custombuttons): | |
2122 | if btndef is None: | |
2123 | if i == len(custombuttons) - 1: | |
47 | 2124 | self.newbuttonbutton = b = wx.Button(self.centerpanel, -1, "+", size = (35, 18), style = wx.BU_EXACTFIT) |
15 | 2125 | b.SetForegroundColour("#4444ff") |
2126 | b.SetToolTip(wx.ToolTip(_("click to add new custom button"))) | |
2127 | b.Bind(wx.EVT_BUTTON, self.cbutton_edit) | |
2128 | else: | |
2129 | b = wx.StaticText(self.panel, -1, "") | |
2130 | else: | |
2131 | b = wx.Button(self.centerpanel, -1, btndef.label, style = wx.BU_EXACTFIT) | |
2132 | b.SetToolTip(wx.ToolTip(_("Execute command: ") + btndef.command)) | |
2133 | if btndef.background: | |
2134 | b.SetBackgroundColour(btndef.background) | |
47 | 2135 | rr, gg, bb, aa = b.GetBackgroundColour().Get() #last item is alpha |
15 | 2136 | if 0.3 * rr + 0.59 * gg + 0.11 * bb < 60: |
2137 | b.SetForegroundColour("#ffffff") | |
2138 | b.custombutton = i | |
2139 | b.properties = btndef | |
2140 | if btndef is not None: | |
2141 | b.Bind(wx.EVT_BUTTON, self.process_button) | |
2142 | b.Bind(wx.EVT_MOUSE_EVENTS, self.editbutton) | |
2143 | self.custombuttons_widgets.append(b) | |
47 | 2144 | if isinstance(self.cbuttonssizer, wx.GridBagSizer): |
15 | 2145 | self.cbuttonssizer.Add(b, pos = (i // 4, i % 4), flag = wx.EXPAND) |
2146 | else: | |
2147 | self.cbuttonssizer.Add(b, flag = wx.EXPAND) | |
2148 | self.centerpanel.Layout() | |
2149 | self.centerpanel.GetContainingSizer().Layout() | |
2150 | ||
2151 | def help_button(self): | |
2152 | self.log(_('Defines custom button. Usage: button <num> "title" [/c "colour"] command')) | |
2153 | ||
2154 | def do_button(self, argstr): | |
2155 | def nextarg(rest): | |
2156 | rest = rest.lstrip() | |
2157 | if rest.startswith('"'): | |
2158 | return rest[1:].split('"', 1) | |
2159 | else: | |
2160 | return rest.split(None, 1) | |
2161 | # try: | |
2162 | num, argstr = nextarg(argstr) | |
2163 | num = int(num) | |
2164 | title, argstr = nextarg(argstr) | |
2165 | colour = None | |
2166 | try: | |
2167 | c1, c2 = nextarg(argstr) | |
2168 | if c1 == "/c": | |
2169 | colour, argstr = nextarg(c2) | |
2170 | except: | |
2171 | pass | |
2172 | command = argstr.strip() | |
2173 | if num < 0 or num >= 64: | |
2174 | self.log(_("Custom button number should be between 0 and 63")) | |
2175 | return | |
2176 | while num >= len(self.custombuttons): | |
2177 | self.custombuttons.append(None) | |
2178 | self.custombuttons[num] = SpecialButton(title, command) | |
2179 | if colour is not None: | |
2180 | self.custombuttons[num].background = colour | |
2181 | if not self.processing_rc: | |
2182 | self.cbuttons_reload() | |
2183 | ||
2184 | def cbutton_save(self, n, bdef, new_n = None): | |
2185 | if new_n is None: new_n = n | |
2186 | if bdef is None or bdef == "": | |
2187 | self.save_in_rc(("button %d" % n), '') | |
2188 | elif bdef.background: | |
2189 | colour = bdef.background | |
47 | 2190 | if not isinstance(colour, str): |
2191 | if isinstance(colour, tuple) and tuple(map(type, colour)) == (int, int, int): | |
2192 | colour = (x % 256 for x in colour) | |
15 | 2193 | colour = wx.Colour(*colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) |
2194 | else: | |
2195 | colour = wx.Colour(colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) | |
2196 | self.save_in_rc(("button %d" % n), 'button %d "%s" /c "%s" %s' % (new_n, bdef.label, colour, bdef.command)) | |
2197 | else: | |
2198 | self.save_in_rc(("button %d" % n), 'button %d "%s" %s' % (new_n, bdef.label, bdef.command)) | |
2199 | ||
2200 | def cbutton_edit(self, e, button = None): | |
2201 | bedit = ButtonEdit(self) | |
2202 | if button is not None: | |
2203 | n = button.custombutton | |
2204 | bedit.name.SetValue(button.properties.label) | |
2205 | bedit.command.SetValue(button.properties.command) | |
2206 | if button.properties.background: | |
2207 | colour = button.properties.background | |
47 | 2208 | if not isinstance(colour, str): |
2209 | if isinstance(colour, tuple) and tuple(map(type, colour)) == (int, int, int): | |
2210 | colour = (x % 256 for x in colour) | |
15 | 2211 | colour = wx.Colour(*colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) |
2212 | else: | |
2213 | colour = wx.Colour(colour).GetAsString(wx.C2S_NAME | wx.C2S_HTML_SYNTAX) | |
2214 | bedit.color.SetValue(colour) | |
2215 | else: | |
2216 | n = len(self.custombuttons) | |
2217 | while n > 0 and self.custombuttons[n - 1] is None: | |
2218 | n -= 1 | |
2219 | if bedit.ShowModal() == wx.ID_OK: | |
2220 | if n == len(self.custombuttons): | |
2221 | self.custombuttons.append(None) | |
2222 | self.custombuttons[n] = SpecialButton(bedit.name.GetValue().strip(), bedit.command.GetValue().strip(), custom = True) | |
2223 | if bedit.color.GetValue().strip() != "": | |
2224 | self.custombuttons[n].background = bedit.color.GetValue() | |
2225 | self.cbutton_save(n, self.custombuttons[n]) | |
2226 | wx.CallAfter(bedit.Destroy) | |
2227 | wx.CallAfter(self.cbuttons_reload) | |
2228 | ||
2229 | def cbutton_remove(self, e, button): | |
2230 | n = button.custombutton | |
2231 | self.cbutton_save(n, None) | |
2232 | del self.custombuttons[n] | |
2233 | for i in range(n, len(self.custombuttons)): | |
2234 | self.cbutton_save(i, self.custombuttons[i]) | |
2235 | wx.CallAfter(self.cbuttons_reload) | |
2236 | ||
2237 | def cbutton_order(self, e, button, dir): | |
2238 | n = button.custombutton | |
2239 | if dir < 0: | |
2240 | n = n - 1 | |
2241 | if n + 1 >= len(self.custombuttons): | |
2242 | self.custombuttons.append(None) # pad | |
2243 | # swap | |
2244 | self.custombuttons[n], self.custombuttons[n + 1] = self.custombuttons[n + 1], self.custombuttons[n] | |
2245 | self.cbutton_save(n, self.custombuttons[n]) | |
2246 | self.cbutton_save(n + 1, self.custombuttons[n + 1]) | |
2247 | wx.CallAfter(self.cbuttons_reload) | |
2248 | ||
2249 | def editbutton(self, e): | |
2250 | if e.IsCommandEvent() or e.ButtonUp(wx.MOUSE_BTN_RIGHT): | |
2251 | if e.IsCommandEvent(): | |
2252 | pos = (0, 0) | |
2253 | else: | |
2254 | pos = e.GetPosition() | |
2255 | popupmenu = wx.Menu() | |
2256 | obj = e.GetEventObject() | |
2257 | if hasattr(obj, "custombutton"): | |
2258 | item = popupmenu.Append(-1, _("Edit custom button '%s'") % e.GetEventObject().GetLabelText()) | |
2259 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_edit(e, button), item) | |
2260 | item = popupmenu.Append(-1, _("Move left <<")) | |
2261 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_order(e, button, -1), item) | |
2262 | if obj.custombutton == 0: item.Enable(False) | |
2263 | item = popupmenu.Append(-1, _("Move right >>")) | |
2264 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_order(e, button, 1), item) | |
2265 | if obj.custombutton == 63: item.Enable(False) | |
2266 | pos = self.panel.ScreenToClient(e.GetEventObject().ClientToScreen(pos)) | |
2267 | item = popupmenu.Append(-1, _("Remove custom button '%s'") % e.GetEventObject().GetLabelText()) | |
2268 | self.Bind(wx.EVT_MENU, lambda e, button = e.GetEventObject(): self.cbutton_remove(e, button), item) | |
2269 | else: | |
2270 | item = popupmenu.Append(-1, _("Add custom button")) | |
2271 | self.Bind(wx.EVT_MENU, self.cbutton_edit, item) | |
2272 | self.panel.PopupMenu(popupmenu, pos) | |
47 | 2273 | elif e.Dragging() and e.LeftIsDown(): |
15 | 2274 | obj = e.GetEventObject() |
2275 | scrpos = obj.ClientToScreen(e.GetPosition()) | |
2276 | if not hasattr(self, "dragpos"): | |
2277 | self.dragpos = scrpos | |
2278 | e.Skip() | |
2279 | return | |
2280 | else: | |
2281 | dx, dy = self.dragpos[0] - scrpos[0], self.dragpos[1] - scrpos[1] | |
47 | 2282 | if dx * dx + dy * dy < 30 * 30: # threshold to detect dragging for jittery mice |
15 | 2283 | e.Skip() |
2284 | return | |
2285 | if not hasattr(self, "dragging"): | |
2286 | # init dragging of the custom button | |
47 | 2287 | if hasattr(obj, "custombutton") and (not hasattr(obj,"properties") or obj.properties is not None): |
15 | 2288 | for b in self.custombuttons_widgets: |
47 | 2289 | if not hasattr(b,"properties") or b.properties is None: |
15 | 2290 | b.Enable() |
2291 | b.SetLabel("") | |
2292 | b.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)) | |
2293 | b.SetForegroundColour("black") | |
2294 | b.SetSize(obj.GetSize()) | |
2295 | if self.toolbarsizer.GetItem(b) is not None: | |
2296 | self.toolbarsizer.SetItemMinSize(b, obj.GetSize()) | |
2297 | self.mainsizer.Layout() | |
2298 | self.dragging = wx.Button(self.panel, -1, obj.GetLabel(), style = wx.BU_EXACTFIT) | |
2299 | self.dragging.SetBackgroundColour(obj.GetBackgroundColour()) | |
2300 | self.dragging.SetForegroundColour(obj.GetForegroundColour()) | |
2301 | self.dragging.sourcebutton = obj | |
2302 | self.dragging.Raise() | |
2303 | self.dragging.Disable() | |
2304 | self.dragging.SetPosition(self.panel.ScreenToClient(scrpos)) | |
2305 | self.last_drag_dest = obj | |
2306 | self.dragging.label = obj.s_label = obj.GetLabel() | |
2307 | self.dragging.bgc = obj.s_bgc = obj.GetBackgroundColour() | |
2308 | self.dragging.fgc = obj.s_fgc = obj.GetForegroundColour() | |
2309 | else: | |
2310 | # dragging in progress | |
2311 | self.dragging.SetPosition(self.panel.ScreenToClient(scrpos)) | |
2312 | wx.CallAfter(self.dragging.Refresh) | |
2313 | dst = None | |
2314 | src = self.dragging.sourcebutton | |
2315 | drg = self.dragging | |
2316 | for b in self.custombuttons_widgets: | |
2317 | if b.GetScreenRect().Contains(scrpos): | |
2318 | dst = b | |
2319 | break | |
2320 | if dst is not self.last_drag_dest: | |
2321 | if self.last_drag_dest is not None: | |
2322 | self.last_drag_dest.SetBackgroundColour(self.last_drag_dest.s_bgc) | |
2323 | self.last_drag_dest.SetForegroundColour(self.last_drag_dest.s_fgc) | |
2324 | self.last_drag_dest.SetLabel(self.last_drag_dest.s_label) | |
2325 | if dst is not None and dst is not src: | |
2326 | dst.s_bgc = dst.GetBackgroundColour() | |
2327 | dst.s_fgc = dst.GetForegroundColour() | |
2328 | dst.s_label = dst.GetLabel() | |
2329 | src.SetBackgroundColour(dst.GetBackgroundColour()) | |
2330 | src.SetForegroundColour(dst.GetForegroundColour()) | |
2331 | src.SetLabel(dst.GetLabel()) | |
2332 | dst.SetBackgroundColour(drg.bgc) | |
2333 | dst.SetForegroundColour(drg.fgc) | |
2334 | dst.SetLabel(drg.label) | |
2335 | else: | |
2336 | src.SetBackgroundColour(drg.bgc) | |
2337 | src.SetForegroundColour(drg.fgc) | |
2338 | src.SetLabel(drg.label) | |
2339 | self.last_drag_dest = dst | |
47 | 2340 | elif hasattr(self, "dragging") and not e.LeftIsDown(): |
15 | 2341 | # dragging finished |
2342 | obj = e.GetEventObject() | |
2343 | scrpos = obj.ClientToScreen(e.GetPosition()) | |
2344 | dst = None | |
2345 | src = self.dragging.sourcebutton | |
2346 | drg = self.dragging | |
2347 | for b in self.custombuttons_widgets: | |
2348 | if b.GetScreenRect().Contains(scrpos): | |
2349 | dst = b | |
2350 | break | |
47 | 2351 | if dst is not None and hasattr(dst,"custombutton"): |
15 | 2352 | src_i = src.custombutton |
2353 | dst_i = dst.custombutton | |
2354 | self.custombuttons[src_i], self.custombuttons[dst_i] = self.custombuttons[dst_i], self.custombuttons[src_i] | |
2355 | self.cbutton_save(src_i, self.custombuttons[src_i]) | |
2356 | self.cbutton_save(dst_i, self.custombuttons[dst_i]) | |
2357 | while self.custombuttons[-1] is None: | |
2358 | del self.custombuttons[-1] | |
2359 | wx.CallAfter(self.dragging.Destroy) | |
2360 | del self.dragging | |
2361 | wx.CallAfter(self.cbuttons_reload) | |
2362 | del self.last_drag_dest | |
2363 | del self.dragpos | |
2364 | else: | |
2365 | e.Skip() | |
2366 | ||
2367 | def process_button(self, e): | |
2368 | try: | |
2369 | if hasattr(e.GetEventObject(), "custombutton"): | |
2370 | if wx.GetKeyState(wx.WXK_CONTROL) or wx.GetKeyState(wx.WXK_ALT): | |
2371 | return self.editbutton(e) | |
2372 | self.cur_button = e.GetEventObject().custombutton | |
2373 | command = e.GetEventObject().properties.command | |
2374 | command = self.precmd(command) | |
2375 | self.onecmd(command) | |
2376 | self.cur_button = None | |
2377 | except: | |
2378 | self.log(_("Failed to handle button")) | |
2379 | self.cur_button = None | |
2380 | raise | |
2381 | ||
2382 | # -------------------------------------------------------------- | |
2383 | # Macros handling | |
2384 | # -------------------------------------------------------------- | |
2385 | ||
2386 | def start_macro(self, macro_name, old_macro_definition = ""): | |
2387 | if not self.processing_rc: | |
2388 | def cb(definition): | |
2389 | if len(definition.strip()) == 0: | |
2390 | if old_macro_definition != "": | |
2391 | dialog = wx.MessageDialog(self, _("Do you want to erase the macro?"), style = wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION) | |
2392 | if dialog.ShowModal() == wx.ID_YES: | |
2393 | self.delete_macro(macro_name) | |
2394 | return | |
2395 | self.log(_("Cancelled.")) | |
2396 | return | |
2397 | self.cur_macro_name = macro_name | |
2398 | self.cur_macro_def = definition | |
2399 | self.end_macro() | |
2400 | MacroEditor(macro_name, old_macro_definition, cb) | |
2401 | else: | |
2402 | pronsole.pronsole.start_macro(self, macro_name, old_macro_definition) | |
2403 | ||
2404 | def end_macro(self): | |
2405 | pronsole.pronsole.end_macro(self) | |
2406 | self.update_macros_menu() | |
2407 | ||
2408 | def delete_macro(self, macro_name): | |
2409 | pronsole.pronsole.delete_macro(self, macro_name) | |
2410 | self.update_macros_menu() | |
2411 | ||
2412 | def new_macro(self, e = None): | |
47 | 2413 | dialog = wx.Dialog(self, -1, _("Enter macro name")) |
2414 | text = wx.StaticText(dialog, -1, _("Macro name:")) | |
2415 | namectrl = wx.TextCtrl(dialog, -1, style = wx.TE_PROCESS_ENTER) | |
2416 | okb = wx.Button(dialog, wx.ID_OK, _("Ok")) | |
2417 | dialog.Bind(wx.EVT_TEXT_ENTER, | |
2418 | lambda e: dialog.EndModal(wx.ID_OK), namectrl) | |
2419 | cancel_button = wx.Button(dialog, wx.ID_CANCEL, _("Cancel")) | |
2420 | ||
2421 | # Layout | |
2422 | ## Group the buttons horizontally | |
2423 | buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) | |
2424 | buttons_sizer.Add(okb, 0) | |
2425 | buttons_sizer.Add(cancel_button, 0) | |
2426 | ## Set a minimum size for the name control box | |
2427 | min_size = namectrl.GetTextExtent('Default Long Macro Name') | |
2428 | namectrl.SetMinSize(wx.Size(min_size.width, -1)) | |
2429 | ## Group the text and the name control box horizontally | |
2430 | name_sizer = wx.BoxSizer(wx.HORIZONTAL) | |
2431 | name_sizer.Add(text, 0, flag = wx.ALIGN_CENTER) | |
2432 | name_sizer.AddSpacer(10) | |
2433 | name_sizer.Add(namectrl, 1, wx.EXPAND) | |
2434 | ## Group everything vertically | |
2435 | dialog_sizer = wx.BoxSizer(wx.VERTICAL) | |
2436 | dialog_sizer.Add(name_sizer, 0, border = 10, | |
2437 | flag = wx.LEFT | wx.TOP | wx.RIGHT) | |
2438 | dialog_sizer.Add(buttons_sizer, 0, border = 10, | |
2439 | flag = wx.ALIGN_CENTER | wx.ALL) | |
2440 | dialog.SetSizerAndFit(dialog_sizer) | |
15 | 2441 | dialog.Centre() |
47 | 2442 | |
15 | 2443 | macro = "" |
2444 | if dialog.ShowModal() == wx.ID_OK: | |
47 | 2445 | macro = namectrl.GetValue() |
15 | 2446 | if macro != "": |
2447 | wx.CallAfter(self.edit_macro, macro) | |
2448 | dialog.Destroy() | |
2449 | return macro | |
2450 | ||
2451 | def edit_macro(self, macro): | |
2452 | if macro == "": return self.new_macro() | |
2453 | if macro in self.macros: | |
2454 | old_def = self.macros[macro] | |
47 | 2455 | elif len([chr(c) for c in macro.encode("ascii", "replace") if not chr(c).isalnum() and chr(c) != "_"]): |
15 | 2456 | self.log(_("Macro name may contain only ASCII alphanumeric symbols and underscores")) |
2457 | return | |
2458 | elif hasattr(self.__class__, "do_" + macro): | |
2459 | self.log(_("Name '%s' is being used by built-in command") % macro) | |
2460 | return | |
2461 | else: | |
2462 | old_def = "" | |
2463 | self.start_macro(macro, old_def) | |
2464 | return macro | |
2465 | ||
2466 | def update_macros_menu(self): | |
2467 | if not hasattr(self, "macros_menu"): | |
2468 | return # too early, menu not yet built | |
2469 | try: | |
2470 | while True: | |
2471 | item = self.macros_menu.FindItemByPosition(1) | |
2472 | if item is None: break | |
47 | 2473 | self.macros_menu.DestroyItem(item) |
15 | 2474 | except: |
2475 | pass | |
2476 | for macro in self.macros.keys(): | |
2477 | self.Bind(wx.EVT_MENU, lambda x, m = macro: self.start_macro(m, self.macros[m]), self.macros_menu.Append(-1, macro)) | |
2478 | ||
2479 | # -------------------------------------------------------------- | |
2480 | # Slic3r integration | |
2481 | # -------------------------------------------------------------- | |
2482 | ||
2483 | def load_slic3r_configs(self, menus): | |
2484 | """List Slic3r configurations and create menu""" | |
2485 | # Hack to get correct path for Slic3r config | |
2486 | orig_appname = self.app.GetAppName() | |
2487 | self.app.SetAppName("Slic3r") | |
2488 | configpath = wx.StandardPaths.Get().GetUserDataDir() | |
2489 | self.slic3r_configpath = configpath | |
2490 | configfile = os.path.join(configpath, "slic3r.ini") | |
47 | 2491 | if not os.path.exists(configfile): |
2492 | self.app.SetAppName("Slic3rPE") | |
2493 | configpath = wx.StandardPaths.Get().GetUserDataDir() | |
2494 | self.slic3r_configpath = configpath | |
2495 | configfile = os.path.join(configpath, "slic3r.ini") | |
2496 | if not os.path.exists(configfile): | |
2497 | self.settings.slic3rintegration=False; | |
2498 | return | |
2499 | self.app.SetAppName(orig_appname) | |
15 | 2500 | config = self.read_slic3r_config(configfile) |
47 | 2501 | version = config.get("dummy", "version") # Slic3r version |
15 | 2502 | self.slic3r_configs = {} |
2503 | for cat in menus: | |
2504 | menu = menus[cat] | |
2505 | pattern = os.path.join(configpath, cat, "*.ini") | |
2506 | files = sorted(glob.glob(pattern)) | |
2507 | try: | |
2508 | preset = config.get("presets", cat) | |
47 | 2509 | # Starting from Slic3r 1.3.0, preset names have no extension |
2510 | if version.split(".") >= ["1","3","0"]: preset += ".ini" | |
15 | 2511 | self.slic3r_configs[cat] = preset |
2512 | except: | |
2513 | preset = None | |
2514 | self.slic3r_configs[cat] = None | |
2515 | for f in files: | |
2516 | name = os.path.splitext(os.path.basename(f))[0] | |
2517 | item = menu.Append(-1, name, f, wx.ITEM_RADIO) | |
2518 | item.Check(os.path.basename(f) == preset) | |
2519 | self.Bind(wx.EVT_MENU, | |
2520 | lambda event, cat = cat, f = f: | |
2521 | self.set_slic3r_config(configfile, cat, f), item) | |
2522 | ||
2523 | def read_slic3r_config(self, configfile, parser = None): | |
2524 | """Helper to read a Slic3r configuration file""" | |
47 | 2525 | import configparser |
2526 | parser = configparser.RawConfigParser() | |
15 | 2527 | |
47 | 2528 | class add_header: |
15 | 2529 | def __init__(self, f): |
2530 | self.f = f | |
2531 | self.header = '[dummy]' | |
2532 | ||
2533 | def readline(self): | |
2534 | if self.header: | |
2535 | try: return self.header | |
2536 | finally: self.header = None | |
2537 | else: | |
2538 | return self.f.readline() | |
47 | 2539 | |
2540 | def __iter__(self): | |
2541 | import itertools | |
2542 | return itertools.chain([self.header], iter(self.f)) | |
2543 | ||
15 | 2544 | parser.readfp(add_header(open(configfile)), configfile) |
2545 | return parser | |
2546 | ||
2547 | def set_slic3r_config(self, configfile, cat, file): | |
2548 | """Set new preset for a given category""" | |
2549 | self.slic3r_configs[cat] = file | |
2550 | if self.settings.slic3rupdate: | |
2551 | config = self.read_slic3r_config(configfile) | |
47 | 2552 | version = config.get("dummy", "version") # Slic3r version |
2553 | preset = os.path.basename(file) | |
2554 | # Starting from Slic3r 1.3.0, preset names have no extension | |
2555 | if version.split(".") >= ["1","3","0"]: | |
2556 | preset = os.path.splitext(preset)[0] | |
2557 | config.set("presets", cat, preset) | |
15 | 2558 | f = StringIO.StringIO() |
2559 | config.write(f) | |
2560 | data = f.getvalue() | |
2561 | f.close() | |
2562 | data = data.replace("[dummy]\n", "") | |
2563 | with open(configfile, "w") as f: | |
2564 | f.write(data) | |
2565 | ||
2566 | class PronterApp(wx.App): | |
2567 | ||
2568 | mainwindow = None | |
2569 | ||
2570 | def __init__(self, *args, **kwargs): | |
2571 | super(PronterApp, self).__init__(*args, **kwargs) | |
2572 | self.SetAppName("Pronterface") | |
47 | 2573 | self.locale = wx.Locale(wx.Locale.GetSystemLanguage()) |
15 | 2574 | self.mainwindow = PronterWindow(self) |
2575 | self.mainwindow.Show() |