Wed, 20 Jan 2021 10:15:13 +0100
updated and added new files for printrun
15 | 1 | # This file is part of the Printrun suite. |
2 | # | |
3 | # Printrun is free software: you can redistribute it and/or modify | |
4 | # it under the terms of the GNU General Public License as published by | |
5 | # the Free Software Foundation, either version 3 of the License, or | |
6 | # (at your option) any later version. | |
7 | # | |
8 | # Printrun is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU General Public License | |
14 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
15 | ||
16 | import logging | |
17 | import traceback | |
46 | 18 | import os |
19 | import sys | |
15 | 20 | |
21 | from functools import wraps | |
22 | ||
23 | from .utils import parse_build_dimensions | |
24 | ||
25 | def setting_add_tooltip(func): | |
26 | @wraps(func) | |
27 | def decorator(self, *args, **kwargs): | |
28 | widget = func(self, *args, **kwargs) | |
29 | helptxt = self.help or "" | |
30 | sep, deftxt = "", "" | |
31 | if len(helptxt): | |
32 | sep = "\n" | |
33 | if helptxt.find("\n") >= 0: | |
34 | sep = "\n\n" | |
46 | 35 | if self.default != "": |
15 | 36 | deftxt = _("Default: ") |
37 | resethelp = _("(Control-doubleclick to reset to default value)") | |
38 | if len(repr(self.default)) > 10: | |
39 | deftxt += "\n " + repr(self.default).strip("'") + "\n" + resethelp | |
40 | else: | |
41 | deftxt += repr(self.default) + " " + resethelp | |
42 | helptxt += sep + deftxt | |
43 | if len(helptxt): | |
46 | 44 | widget.SetToolTip(helptxt) |
15 | 45 | return widget |
46 | return decorator | |
47 | ||
46 | 48 | class Setting: |
15 | 49 | |
50 | DEFAULT_GROUP = "Printer" | |
51 | ||
52 | hidden = False | |
53 | ||
54 | def __init__(self, name, default, label = None, help = None, group = None): | |
55 | self.name = name | |
56 | self.default = default | |
57 | self._value = default | |
58 | self.label = label | |
59 | self.help = help | |
60 | self.group = group if group else Setting.DEFAULT_GROUP | |
61 | ||
62 | def _get_value(self): | |
63 | return self._value | |
64 | ||
65 | def _set_value(self, value): | |
66 | raise NotImplementedError | |
67 | value = property(_get_value, _set_value) | |
68 | ||
69 | @setting_add_tooltip | |
70 | def get_label(self, parent): | |
71 | import wx | |
72 | widget = wx.StaticText(parent, -1, self.label or self.name) | |
73 | widget.set_default = self.set_default | |
74 | return widget | |
75 | ||
76 | @setting_add_tooltip | |
77 | def get_widget(self, parent): | |
78 | return self.get_specific_widget(parent) | |
79 | ||
80 | def get_specific_widget(self, parent): | |
81 | raise NotImplementedError | |
82 | ||
83 | def update(self): | |
84 | raise NotImplementedError | |
85 | ||
46 | 86 | def validate(self, value): pass |
87 | ||
15 | 88 | def __str__(self): |
89 | return self.name | |
90 | ||
91 | def __repr__(self): | |
92 | return self.name | |
93 | ||
94 | class HiddenSetting(Setting): | |
95 | ||
96 | hidden = True | |
97 | ||
98 | def _set_value(self, value): | |
99 | self._value = value | |
100 | value = property(Setting._get_value, _set_value) | |
101 | ||
102 | class wxSetting(Setting): | |
103 | ||
104 | widget = None | |
105 | ||
106 | def _set_value(self, value): | |
107 | self._value = value | |
108 | if self.widget: | |
109 | self.widget.SetValue(value) | |
110 | value = property(Setting._get_value, _set_value) | |
111 | ||
112 | def update(self): | |
113 | self.value = self.widget.GetValue() | |
114 | ||
46 | 115 | def set_default(self, e): |
116 | if e.CmdDown() and e.ButtonDClick() and self.default != "": | |
117 | self.widget.SetValue(self.default) | |
118 | else: | |
119 | e.Skip() | |
120 | ||
15 | 121 | class StringSetting(wxSetting): |
122 | ||
123 | def get_specific_widget(self, parent): | |
124 | import wx | |
125 | self.widget = wx.TextCtrl(parent, -1, str(self.value)) | |
126 | return self.widget | |
127 | ||
46 | 128 | def wxColorToStr(color, withAlpha = True): |
129 | # including Alpha seems to be non standard in CSS | |
130 | format = '#{0.red:02X}{0.green:02X}{0.blue:02X}' \ | |
131 | + ('{0.alpha:02X}' if withAlpha else '') | |
132 | return format.format(color) | |
133 | ||
134 | class ColorSetting(wxSetting): | |
135 | def __init__(self, name, default, label = None, help = None, group = None, isRGBA=True): | |
136 | super().__init__(name, default, label, help, group) | |
137 | self.isRGBA = isRGBA | |
138 | ||
139 | def validate(self, value): | |
140 | from .utils import check_rgb_color, check_rgba_color | |
141 | validate = check_rgba_color if self.isRGBA else check_rgb_color | |
142 | validate(value) | |
143 | ||
144 | def get_specific_widget(self, parent): | |
145 | import wx | |
146 | self.widget = wx.ColourPickerCtrl(parent, colour=wx.Colour(self.value), style=wx.CLRP_USE_TEXTCTRL) | |
147 | self.widget.SetValue = self.widget.SetColour | |
148 | self.widget.LayoutDirection = wx.Layout_RightToLeft | |
149 | return self.widget | |
150 | def update(self): | |
151 | self._value = wxColorToStr(self.widget.Colour, self.isRGBA) | |
152 | ||
15 | 153 | class ComboSetting(wxSetting): |
154 | ||
155 | def __init__(self, name, default, choices, label = None, help = None, group = None): | |
156 | super(ComboSetting, self).__init__(name, default, label, help, group) | |
157 | self.choices = choices | |
158 | ||
159 | def get_specific_widget(self, parent): | |
160 | import wx | |
46 | 161 | readonly = isinstance(self.choices, tuple) |
162 | if readonly: | |
163 | # wx.Choice drops its list on click, no need to click down arrow | |
164 | # which is far to the right because of wx.EXPAND | |
165 | self.widget = wx.Choice(parent, -1, choices = self.choices) | |
166 | self.widget.GetValue = lambda: self.choices[self.widget.Selection] | |
167 | self.widget.SetValue = lambda v: self.widget.SetSelection(self.choices.index(v)) | |
168 | self.widget.SetValue(self.value) | |
169 | else: | |
170 | self.widget = wx.ComboBox(parent, -1, str(self.value), choices = self.choices, style = wx.CB_DROPDOWN) | |
15 | 171 | return self.widget |
172 | ||
173 | class SpinSetting(wxSetting): | |
174 | ||
175 | def __init__(self, name, default, min, max, label = None, help = None, group = None, increment = 0.1): | |
46 | 176 | super().__init__(name, default, label, help, group) |
15 | 177 | self.min = min |
178 | self.max = max | |
179 | self.increment = increment | |
180 | ||
181 | def get_specific_widget(self, parent): | |
46 | 182 | import wx |
183 | self.widget = wx.SpinCtrlDouble(parent, -1, min = self.min, max = self.max) | |
184 | self.widget.SetDigits(0) | |
15 | 185 | self.widget.SetValue(self.value) |
186 | orig = self.widget.GetValue | |
187 | self.widget.GetValue = lambda: int(orig()) | |
188 | return self.widget | |
189 | ||
46 | 190 | def MySpin(parent, digits, *args, **kw): |
191 | # in GTK 3.[01], spinner is not large enough to fit text | |
192 | # Could be a class, but use function to avoid load errors if wx | |
193 | # not installed | |
194 | # If native wx.SpinCtrlDouble has problems in different platforms | |
195 | # try agw | |
196 | # from wx.lib.agw.floatspin import FloatSpin | |
197 | import wx | |
198 | sp = wx.SpinCtrlDouble(parent, *args, **kw) | |
199 | # sp = FloatSpin(parent) | |
200 | sp.SetDigits(digits) | |
201 | # sp.SetValue(kw['initial']) | |
202 | def fitValue(ev): | |
203 | text = '%%.%df'% digits % sp.Max | |
204 | # native wx.SpinCtrlDouble does not return good size | |
205 | # in GTK 3.0 | |
206 | tex = sp.GetTextExtent(text) | |
207 | tsz = sp.GetSizeFromTextSize(tex.x) | |
208 | ||
209 | if sp.MinSize.x < tsz.x: | |
210 | # print('fitValue', getattr(sp, 'setting', None), sp.Value, sp.Digits, tsz.x) | |
211 | sp.MinSize = tsz | |
212 | # sp.Size = tsz | |
213 | # sp.Bind(wx.EVT_TEXT, fitValue) | |
214 | fitValue(None) | |
215 | return sp | |
216 | ||
15 | 217 | class FloatSpinSetting(SpinSetting): |
218 | ||
219 | def get_specific_widget(self, parent): | |
46 | 220 | self.widget = MySpin(parent, 2, initial = self.value, min = self.min, max = self.max, inc = self.increment) |
15 | 221 | return self.widget |
222 | ||
223 | class BooleanSetting(wxSetting): | |
224 | ||
225 | def _get_value(self): | |
226 | return bool(self._value) | |
227 | ||
228 | def _set_value(self, value): | |
229 | self._value = value | |
230 | if self.widget: | |
231 | self.widget.SetValue(bool(value)) | |
232 | ||
233 | value = property(_get_value, _set_value) | |
234 | ||
235 | def get_specific_widget(self, parent): | |
236 | import wx | |
237 | self.widget = wx.CheckBox(parent, -1) | |
238 | self.widget.SetValue(bool(self.value)) | |
239 | return self.widget | |
240 | ||
241 | class StaticTextSetting(wxSetting): | |
242 | ||
243 | def __init__(self, name, label = " ", text = "", help = None, group = None): | |
244 | super(StaticTextSetting, self).__init__(name, "", label, help, group) | |
245 | self.text = text | |
246 | ||
247 | def update(self): | |
248 | pass | |
249 | ||
250 | def _get_value(self): | |
251 | return "" | |
252 | ||
253 | def _set_value(self, value): | |
254 | pass | |
255 | ||
256 | def get_specific_widget(self, parent): | |
257 | import wx | |
258 | self.widget = wx.StaticText(parent, -1, self.text) | |
259 | return self.widget | |
260 | ||
261 | class BuildDimensionsSetting(wxSetting): | |
262 | ||
263 | widgets = None | |
264 | ||
265 | def _set_value(self, value): | |
266 | self._value = value | |
267 | if self.widgets: | |
268 | self._set_widgets_values(value) | |
269 | value = property(wxSetting._get_value, _set_value) | |
270 | ||
271 | def _set_widgets_values(self, value): | |
272 | build_dimensions_list = parse_build_dimensions(value) | |
273 | for i in range(len(self.widgets)): | |
274 | self.widgets[i].SetValue(build_dimensions_list[i]) | |
275 | ||
276 | def get_widget(self, parent): | |
277 | from wx.lib.agw.floatspin import FloatSpin | |
278 | import wx | |
279 | build_dimensions = parse_build_dimensions(self.value) | |
280 | self.widgets = [] | |
46 | 281 | def w(val, m, M): |
282 | self.widgets.append(MySpin(parent, 2, initial = val, min = m, max = M)) | |
283 | def addlabel(name, pos): | |
284 | self.widget.Add(wx.StaticText(parent, -1, name), pos = pos, flag = wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border = 5) | |
285 | def addwidget(*pos): | |
286 | self.widget.Add(self.widgets[-1], pos = pos, flag = wx.RIGHT | wx.EXPAND, border = 5) | |
15 | 287 | self.widget = wx.GridBagSizer() |
288 | addlabel(_("Width"), (0, 0)) | |
289 | w(build_dimensions[0], 0, 2000) | |
290 | addwidget(0, 1) | |
291 | addlabel(_("Depth"), (0, 2)) | |
292 | w(build_dimensions[1], 0, 2000) | |
293 | addwidget(0, 3) | |
294 | addlabel(_("Height"), (0, 4)) | |
295 | w(build_dimensions[2], 0, 2000) | |
296 | addwidget(0, 5) | |
297 | addlabel(_("X offset"), (1, 0)) | |
298 | w(build_dimensions[3], -2000, 2000) | |
299 | addwidget(1, 1) | |
300 | addlabel(_("Y offset"), (1, 2)) | |
301 | w(build_dimensions[4], -2000, 2000) | |
302 | addwidget(1, 3) | |
303 | addlabel(_("Z offset"), (1, 4)) | |
304 | w(build_dimensions[5], -2000, 2000) | |
305 | addwidget(1, 5) | |
306 | addlabel(_("X home pos."), (2, 0)) | |
307 | w(build_dimensions[6], -2000, 2000) | |
46 | 308 | addwidget(2, 1) |
15 | 309 | addlabel(_("Y home pos."), (2, 2)) |
310 | w(build_dimensions[7], -2000, 2000) | |
46 | 311 | addwidget(2, 3) |
15 | 312 | addlabel(_("Z home pos."), (2, 4)) |
313 | w(build_dimensions[8], -2000, 2000) | |
46 | 314 | addwidget(2, 5) |
15 | 315 | return self.widget |
316 | ||
317 | def update(self): | |
318 | values = [float(w.GetValue()) for w in self.widgets] | |
319 | self.value = "%.02fx%.02fx%.02f%+.02f%+.02f%+.02f%+.02f%+.02f%+.02f" % tuple(values) | |
320 | ||
46 | 321 | class Settings: |
15 | 322 | def __baudrate_list(self): return ["2400", "9600", "19200", "38400", "57600", "115200", "250000"] |
323 | ||
324 | def __init__(self, root): | |
325 | # defaults here. | |
326 | # the initial value determines the type | |
327 | self._add(StringSetting("port", "", _("Serial port"), _("Port used to communicate with printer"))) | |
328 | self._add(ComboSetting("baudrate", 115200, self.__baudrate_list(), _("Baud rate"), _("Communications Speed"))) | |
329 | self._add(BooleanSetting("tcp_streaming_mode", False, _("TCP streaming mode"), _("When using a TCP connection to the printer, the streaming mode will not wait for acks from the printer to send new commands. This will break things such as ETA prediction, but can result in smoother prints.")), root.update_tcp_streaming_mode) | |
330 | self._add(BooleanSetting("rpc_server", True, _("RPC server"), _("Enable RPC server to allow remotely querying print status")), root.update_rpc_server) | |
331 | self._add(BooleanSetting("dtr", True, _("DTR"), _("Disabling DTR would prevent Arduino (RAMPS) from resetting upon connection"), "Printer")) | |
46 | 332 | if sys.platform != "win32": |
333 | self._add(StringSetting("devicepath", "", _("Device name pattern"), _("Custom device pattern: for example /dev/3DP_* "), "Printer")) | |
334 | self._add(SpinSetting("bedtemp_abs", 110, 0, 400, _("Bed temperature for ABS"), _("Heated Build Platform temp for ABS (deg C)"), "Printer"), root.set_temp_preset) | |
335 | self._add(SpinSetting("bedtemp_pla", 60, 0, 400, _("Bed temperature for PLA"), _("Heated Build Platform temp for PLA (deg C)"), "Printer"), root.set_temp_preset) | |
336 | self._add(SpinSetting("temperature_abs", 230, 0, 400, _("Extruder temperature for ABS"), _("Extruder temp for ABS (deg C)"), "Printer"), root.set_temp_preset) | |
337 | self._add(SpinSetting("temperature_pla", 185, 0, 400, _("Extruder temperature for PLA"), _("Extruder temp for PLA (deg C)"), "Printer"), root.set_temp_preset) | |
15 | 338 | self._add(SpinSetting("xy_feedrate", 3000, 0, 50000, _("X && Y manual feedrate"), _("Feedrate for Control Panel Moves in X and Y (mm/min)"), "Printer")) |
339 | self._add(SpinSetting("z_feedrate", 100, 0, 50000, _("Z manual feedrate"), _("Feedrate for Control Panel Moves in Z (mm/min)"), "Printer")) | |
340 | self._add(SpinSetting("e_feedrate", 100, 0, 1000, _("E manual feedrate"), _("Feedrate for Control Panel Moves in Extrusions (mm/min)"), "Printer")) | |
46 | 341 | defaultslicerpath = "" |
342 | if getattr(sys, 'frozen', False): | |
343 | if sys.platform == "darwin": | |
344 | defaultslicerpath = "/Applications/Slic3r.app/Contents/MacOS/" | |
345 | elif sys.platform == "win32": | |
346 | defaultslicerpath = ".\\slic3r\\" | |
347 | self._add(StringSetting("slicecommandpath", defaultslicerpath, _("Path to slicer"), _("Path to slicer"), "External")) | |
348 | slicer = 'slic3r-console' if sys.platform == 'win32' else 'slic3r' | |
349 | self._add(StringSetting("slicecommand", slicer + ' $s --output $o', _("Slice command"), _("Slice command"), "External")) | |
350 | self._add(StringSetting("sliceoptscommand", "slic3r", _("Slicer options command"), _("Slice settings command"), "External")) | |
15 | 351 | self._add(StringSetting("start_command", "", _("Start command"), _("Executable to run when the print is started"), "External")) |
352 | self._add(StringSetting("final_command", "", _("Final command"), _("Executable to run when the print is finished"), "External")) | |
353 | self._add(StringSetting("error_command", "", _("Error command"), _("Executable to run when an error occurs"), "External")) | |
354 | self._add(StringSetting("log_path", "", _("Log path"), _("Path to the log file. An empty path will log to the console."), "UI")) | |
355 | ||
356 | self._add(HiddenSetting("project_offset_x", 0.0)) | |
357 | self._add(HiddenSetting("project_offset_y", 0.0)) | |
358 | self._add(HiddenSetting("project_interval", 2.0)) | |
359 | self._add(HiddenSetting("project_pause", 2.5)) | |
360 | self._add(HiddenSetting("project_scale", 1.0)) | |
361 | self._add(HiddenSetting("project_x", 1024)) | |
362 | self._add(HiddenSetting("project_y", 768)) | |
363 | self._add(HiddenSetting("project_projected_x", 150.0)) | |
364 | self._add(HiddenSetting("project_direction", "Top Down")) | |
365 | self._add(HiddenSetting("project_overshoot", 3.0)) | |
366 | self._add(HiddenSetting("project_z_axis_rate", 200)) | |
367 | self._add(HiddenSetting("project_layer", 0.1)) | |
368 | self._add(HiddenSetting("project_prelift_gcode", "")) | |
369 | self._add(HiddenSetting("project_postlift_gcode", "")) | |
370 | self._add(HiddenSetting("pause_between_prints", True)) | |
371 | self._add(HiddenSetting("default_extrusion", 5.0)) | |
372 | self._add(HiddenSetting("last_extrusion", 5.0)) | |
373 | self._add(HiddenSetting("total_filament_used", 0.0)) | |
46 | 374 | self._add(HiddenSetting("spool_list", "")) |
15 | 375 | |
376 | _settings = [] | |
377 | ||
378 | def __setattr__(self, name, value): | |
379 | if name.startswith("_"): | |
380 | return object.__setattr__(self, name, value) | |
381 | if isinstance(value, Setting): | |
382 | if not value.hidden: | |
383 | self._settings.append(value) | |
384 | object.__setattr__(self, "_" + name, value) | |
385 | elif hasattr(self, "_" + name): | |
386 | getattr(self, "_" + name).value = value | |
387 | else: | |
388 | setattr(self, name, StringSetting(name = name, default = value)) | |
389 | ||
390 | def __getattr__(self, name): | |
391 | if name.startswith("_"): | |
392 | return object.__getattribute__(self, name) | |
393 | return getattr(self, "_" + name).value | |
394 | ||
46 | 395 | def _add(self, setting, callback = None, |
15 | 396 | alias = None, autocomplete_list = None): |
397 | setattr(self, setting.name, setting) | |
398 | if callback: | |
399 | setattr(self, "__" + setting.name + "_cb", callback) | |
400 | if alias: | |
401 | setattr(self, "__" + setting.name + "_alias", alias) | |
402 | if autocomplete_list: | |
403 | setattr(self, "__" + setting.name + "_list", autocomplete_list) | |
404 | ||
405 | def _set(self, key, value): | |
406 | try: | |
407 | value = getattr(self, "__%s_alias" % key)()[value] | |
408 | except KeyError: | |
409 | pass | |
410 | except AttributeError: | |
411 | pass | |
46 | 412 | setting = getattr(self, '_'+key) |
413 | setting.validate(value) | |
15 | 414 | t = type(getattr(self, key)) |
46 | 415 | if t == bool and value == "False": |
416 | value = False | |
417 | setattr(self, key, t(value)) | |
15 | 418 | try: |
46 | 419 | cb = getattr(self, "__%s_cb" % key, None) |
420 | if cb is not None: | |
421 | cb(key, value) | |
15 | 422 | except: |
423 | logging.warning((_("Failed to run callback after setting \"%s\":") % key) + | |
424 | "\n" + traceback.format_exc()) | |
425 | return value | |
426 | ||
427 | def _tabcomplete(self, key): | |
428 | try: | |
429 | return getattr(self, "__%s_list" % key)() | |
430 | except AttributeError: | |
431 | pass | |
432 | try: | |
46 | 433 | return list(getattr(self, "__%s_alias" % key)().keys()) |
15 | 434 | except AttributeError: |
435 | pass | |
436 | return [] | |
437 | ||
438 | def _all_settings(self): | |
439 | return self._settings |