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