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 os | |
17 | import sys | |
18 | import re | |
19 | import gettext | |
20 | import datetime | |
21 | import subprocess | |
22 | import shlex | |
46 | 23 | import locale |
15 | 24 | import logging |
25 | ||
46 | 26 | DATADIR = os.path.join(sys.prefix, 'share') |
27 | ||
28 | ||
29 | def set_utf8_locale(): | |
30 | """Make sure we read/write all text files in UTF-8""" | |
31 | lang, encoding = locale.getlocale() | |
32 | if encoding != 'UTF-8': | |
33 | locale.setlocale(locale.LC_CTYPE, (lang, 'UTF-8')) | |
34 | ||
15 | 35 | # Set up Internationalization using gettext |
36 | # searching for installed locales on /usr/share; uses relative folder if not | |
37 | # found (windows) | |
38 | def install_locale(domain): | |
46 | 39 | shared_locale_dir = os.path.join(DATADIR, 'locale') |
40 | if os.path.exists(shared_locale_dir): | |
41 | gettext.install(domain, shared_locale_dir) | |
15 | 42 | else: |
46 | 43 | gettext.install(domain, './locale') |
15 | 44 | |
45 | class LogFormatter(logging.Formatter): | |
46 | def __init__(self, format_default, format_info): | |
47 | super(LogFormatter, self).__init__(format_info) | |
48 | self.format_default = format_default | |
49 | self.format_info = format_info | |
50 | ||
51 | def format(self, record): | |
52 | if record.levelno == logging.INFO: | |
53 | self._fmt = self.format_info | |
54 | else: | |
55 | self._fmt = self.format_default | |
56 | return super(LogFormatter, self).format(record) | |
57 | ||
58 | def setup_logging(out, filepath = None, reset_handlers = False): | |
59 | logger = logging.getLogger() | |
60 | logger.setLevel(logging.INFO) | |
61 | if reset_handlers: | |
62 | logger.handlers = [] | |
63 | formatter = LogFormatter("[%(levelname)s] %(message)s", "%(message)s") | |
64 | logging_handler = logging.StreamHandler(out) | |
65 | logging_handler.setFormatter(formatter) | |
66 | logger.addHandler(logging_handler) | |
67 | if filepath: | |
68 | if os.path.isdir(filepath): | |
69 | filepath = os.path.join(filepath, "printrun.log") | |
70 | formatter = LogFormatter("%(asctime)s - [%(levelname)s] %(message)s", "%(asctime)s - %(message)s") | |
71 | logging_handler = logging.FileHandler(filepath) | |
72 | logging_handler.setFormatter(formatter) | |
73 | logger.addHandler(logging_handler) | |
74 | ||
75 | def iconfile(filename): | |
76 | if hasattr(sys, "frozen") and sys.frozen == "windows_exe": | |
77 | return sys.executable | |
78 | else: | |
79 | return pixmapfile(filename) | |
80 | ||
81 | def imagefile(filename): | |
46 | 82 | shared_pronterface_images_dir = os.path.join(DATADIR, 'pronterface/images') |
83 | candidate = os.path.join(shared_pronterface_images_dir, filename) | |
84 | if os.path.exists(candidate): | |
85 | return candidate | |
15 | 86 | local_candidate = os.path.join(os.path.dirname(sys.argv[0]), |
87 | "images", filename) | |
88 | if os.path.exists(local_candidate): | |
89 | return local_candidate | |
46 | 90 | frozen_candidate=os.path.join(getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))),"images",filename) |
91 | if os.path.exists(frozen_candidate): | |
92 | return frozen_candidate | |
15 | 93 | else: |
94 | return os.path.join("images", filename) | |
95 | ||
96 | def lookup_file(filename, prefixes): | |
97 | local_candidate = os.path.join(os.path.dirname(sys.argv[0]), filename) | |
98 | if os.path.exists(local_candidate): | |
99 | return local_candidate | |
46 | 100 | if getattr(sys,"frozen",False): prefixes+=[getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))),] |
15 | 101 | for prefix in prefixes: |
102 | candidate = os.path.join(prefix, filename) | |
103 | if os.path.exists(candidate): | |
104 | return candidate | |
105 | return filename | |
106 | ||
107 | def pixmapfile(filename): | |
46 | 108 | shared_pixmaps_dir = os.path.join(DATADIR, 'pixmaps') |
109 | return lookup_file(filename, [shared_pixmaps_dir]) | |
15 | 110 | |
111 | def sharedfile(filename): | |
46 | 112 | shared_pronterface_dir = os.path.join(DATADIR, 'pronterface') |
113 | return lookup_file(filename, [shared_pronterface_dir]) | |
15 | 114 | |
115 | def configfile(filename): | |
116 | return lookup_file(filename, [os.path.expanduser("~/.printrun/"), ]) | |
117 | ||
118 | def decode_utf8(s): | |
119 | try: | |
120 | s = s.decode("utf-8") | |
121 | except: | |
122 | pass | |
123 | return s | |
124 | ||
125 | def format_time(timestamp): | |
126 | return datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") | |
127 | ||
128 | def format_duration(delta): | |
129 | return str(datetime.timedelta(seconds = int(delta))) | |
130 | ||
131 | def prepare_command(command, replaces = None): | |
46 | 132 | command = shlex.split(command.replace("\\", "\\\\")) |
15 | 133 | if replaces: |
134 | replaces["$python"] = sys.executable | |
135 | for pattern, rep in replaces.items(): | |
136 | command = [bit.replace(pattern, rep) for bit in command] | |
137 | return command | |
138 | ||
46 | 139 | def run_command(command, replaces = None, stdout = subprocess.STDOUT, stderr = subprocess.STDOUT, blocking = False, universal_newlines = False): |
15 | 140 | command = prepare_command(command, replaces) |
141 | if blocking: | |
46 | 142 | return subprocess.call(command, universal_newlines = universal_newlines) |
15 | 143 | else: |
46 | 144 | return subprocess.Popen(command, stderr = stderr, stdout = stdout, universal_newlines = universal_newlines) |
15 | 145 | |
146 | def get_command_output(command, replaces): | |
147 | p = run_command(command, replaces, | |
148 | stdout = subprocess.PIPE, stderr = subprocess.STDOUT, | |
46 | 149 | blocking = False, universal_newlines = True) |
15 | 150 | return p.stdout.read() |
151 | ||
152 | def dosify(name): | |
153 | return os.path.split(name)[1].split(".")[0][:8] + ".g" | |
154 | ||
46 | 155 | class RemainingTimeEstimator: |
15 | 156 | |
157 | drift = None | |
158 | gcode = None | |
159 | ||
160 | def __init__(self, gcode): | |
161 | self.drift = 1 | |
162 | self.previous_layers_estimate = 0 | |
163 | self.current_layer_estimate = 0 | |
164 | self.current_layer_lines = 0 | |
165 | self.gcode = gcode | |
166 | self.remaining_layers_estimate = sum(layer.duration for layer in gcode.all_layers) | |
167 | if len(gcode) > 0: | |
168 | self.update_layer(0, 0) | |
169 | ||
170 | def update_layer(self, layer, printtime): | |
171 | self.previous_layers_estimate += self.current_layer_estimate | |
172 | if self.previous_layers_estimate > 1. and printtime > 1.: | |
173 | self.drift = printtime / self.previous_layers_estimate | |
174 | self.current_layer_estimate = self.gcode.all_layers[layer].duration | |
175 | self.current_layer_lines = len(self.gcode.all_layers[layer]) | |
176 | self.remaining_layers_estimate -= self.current_layer_estimate | |
177 | self.last_idx = -1 | |
178 | self.last_estimate = None | |
179 | ||
180 | def __call__(self, idx, printtime): | |
181 | if not self.current_layer_lines: | |
182 | return (0, 0) | |
183 | if idx == self.last_idx: | |
184 | return self.last_estimate | |
185 | layer, line = self.gcode.idxs(idx) | |
186 | layer_progress = (1 - (float(line + 1) / self.current_layer_lines)) | |
187 | remaining = layer_progress * self.current_layer_estimate + self.remaining_layers_estimate | |
188 | estimate = self.drift * remaining | |
189 | total = estimate + printtime | |
190 | self.last_idx = idx | |
191 | self.last_estimate = (estimate, total) | |
192 | return self.last_estimate | |
193 | ||
194 | def parse_build_dimensions(bdim): | |
195 | # a string containing up to six numbers delimited by almost anything | |
196 | # first 0-3 numbers specify the build volume, no sign, always positive | |
197 | # remaining 0-3 numbers specify the coordinates of the "southwest" corner of the build platform | |
198 | # "XXX,YYY" | |
199 | # "XXXxYYY+xxx-yyy" | |
200 | # "XXX,YYY,ZZZ+xxx+yyy-zzz" | |
201 | # etc | |
202 | bdl = re.findall("([-+]?[0-9]*\.?[0-9]*)", bdim) | |
203 | defaults = [200, 200, 100, 0, 0, 0, 0, 0, 0] | |
46 | 204 | bdl = [b for b in bdl if b] |
15 | 205 | bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)] |
206 | if len(bdl_float) < len(defaults): | |
207 | bdl_float += [defaults[i] for i in range(len(bdl_float), len(defaults))] | |
208 | for i in range(3): # Check for nonpositive dimensions for build volume | |
209 | if bdl_float[i] <= 0: bdl_float[i] = 1 | |
210 | return bdl_float | |
211 | ||
212 | def get_home_pos(build_dimensions): | |
213 | return build_dimensions[6:9] if len(build_dimensions) >= 9 else None | |
214 | ||
215 | def hexcolor_to_float(color, components): | |
216 | color = color[1:] | |
217 | numel = len(color) | |
46 | 218 | ndigits = numel // components |
15 | 219 | div = 16 ** ndigits - 1 |
220 | return tuple(round(float(int(color[i:i + ndigits], 16)) / div, 2) | |
221 | for i in range(0, numel, ndigits)) | |
222 | ||
223 | def check_rgb_color(color): | |
224 | if len(color[1:]) % 3 != 0: | |
225 | ex = ValueError(_("Color must be specified as #RGB")) | |
226 | ex.from_validator = True | |
227 | raise ex | |
228 | ||
229 | def check_rgba_color(color): | |
230 | if len(color[1:]) % 4 != 0: | |
231 | ex = ValueError(_("Color must be specified as #RGBA")) | |
232 | ex.from_validator = True | |
233 | raise ex | |
234 | ||
235 | tempreport_exp = re.compile("([TB]\d*):([-+]?\d*\.?\d*)(?: ?\/)?([-+]?\d*\.?\d*)") | |
236 | def parse_temperature_report(report): | |
237 | matches = tempreport_exp.findall(report) | |
238 | return dict((m[0], (m[1], m[2])) for m in matches) | |
46 | 239 | |
240 | def compile_file(filename): | |
241 | with open(filename) as f: | |
242 | return compile(f.read(), filename, 'exec') | |
243 | ||
244 | def read_history_from(filename): | |
245 | history=[] | |
246 | if os.path.exists(filename): | |
247 | _hf=open(filename,encoding="utf-8") | |
248 | for i in _hf: | |
249 | history.append(i.rstrip()) | |
250 | return history | |
251 | ||
252 | def write_history_to(filename, hist): | |
253 | _hf=open(filename,"w",encoding="utf-8") | |
254 | for i in hist: | |
255 | _hf.write(i+"\n") | |
256 | _hf.close() |