diff --git a/P-face.png b/P-face.png new file mode 100644 index 0000000..1adcb14 Binary files /dev/null and b/P-face.png differ diff --git a/gcoder.py b/gcoder.py index 79c95bc..3b981ee 100755 --- a/gcoder.py +++ b/gcoder.py @@ -16,6 +16,17 @@ import sys import re +import math + +def deltalen(a,b): + d = object() + d.x = b.x - a.x + d.y = b.y - a.y + d.z = b.z - a.z + + return math.sqrt((d.x*d.x)+(d.y*d.y)+(d.z*d.z)) + + class Line(object): def __init__(self,l): @@ -29,6 +40,7 @@ class Line(object): self.raw = l.upper().lstrip() self.imperial = False self.relative = False + self.relative_e = False if ";" in self.raw: self.raw = self.raw.split(";")[0] @@ -77,53 +89,55 @@ class Line(object): return "" def _get_float(self,which): - return float(self.regex.findall(self.raw.split(which)[1])[0]) - + try: + return float(self.regex.findall(self.raw.split(which)[1])[0]) + except: + return None def _parse_coordinates(self): - if "X" in self.raw: - self._x = self._get_float("X") + try: + if "X" in self.raw: + self._x = self._get_float("X") + except: + pass + + try: + if "Y" in self.raw: + self._y = self._get_float("Y") + except: + pass + + try: + if "Z" in self.raw: + self._z = self._get_float("Z") + except: + pass + + try: + if "E" in self.raw: + self.e = self._get_float("E") + except: + pass + + try: + if "F" in self.raw: + self.f = self._get_float("F") + except: + pass - if "Y" in self.raw: - self._y = self._get_float("Y") - - if "Z" in self.raw: - self._z = self._get_float("Z") - - if "E" in self.raw: - self.e = self._get_float("E") - - if "F" in self.raw: - self.f = self._get_float("F") - def is_move(self): - return "G1" in self.raw or "G0" in self.raw + return self.command() and ("G1" in self.raw or "G0" in self.raw) + + + def __str__(self): + return self.raw + +class Layer(object): + def __init__(self,lines): + self.lines = lines + - -class GCode(object): - def __init__(self,data): - self.lines = [Line(i) for i in data] - self._preprocess() - - def _preprocess(self): - #checks for G20, G21, G90 and G91, sets imperial and relative flags - imperial = False - relative = False - for line in self.lines: - if line.command() == "G20": - imperial = True - elif line.command() == "G21": - imperial = False - elif line.command() == "G90": - relative = False - elif line.command() == "G91": - relative = True - elif line.is_move(): - line.imperial = imperial - line.relative = relative - - def measure(self): xmin = 999999999 ymin = 999999999 @@ -132,7 +146,8 @@ class GCode(object): ymax = -999999999 zmax = -999999999 relative = False - + relative_e = False + current_x = 0 current_y = 0 current_z = 0 @@ -147,13 +162,13 @@ class GCode(object): x = line.x y = line.y z = line.z - + if line.relative: x = current_x + (x or 0) y = current_y + (y or 0) z = current_z + (z or 0) - - + + if x and line.e: if x < xmin: xmin = x @@ -169,22 +184,137 @@ class GCode(object): zmin = z if z > zmax: zmax = z - + current_x = x or current_x current_y = y or current_y current_z = z or current_z - - self.xmin = xmin - self.ymin = ymin - self.zmin = zmin - self.xmax = xmax - self.ymax = ymax - self.zmax = zmax - - self.width = xmax-xmin - self.depth = ymax-ymin - self.height = zmax-zmin + + return ( (xmin,xmax),(ymin,ymax),(zmin,zmax) ) + +class GCode(object): + def __init__(self,data): + self.lines = [Line(i) for i in data] + self._preprocess() + self._create_layers() + + def _preprocess(self): + #checks for G20, G21, G90 and G91, sets imperial and relative flags + imperial = False + relative = False + relative_e = False + for line in self.lines: + if line.command() == "G20": + imperial = True + elif line.command() == "G21": + imperial = False + elif line.command() == "G90": + relative = False + relative_e = False + elif line.command() == "G91": + relative = True + relative_e = True + elif line.command() == "M82": + relative_e = False + elif line.command() == "M83": + relative_e = True + elif line.is_move(): + line.imperial = imperial + line.relative = relative + line.relative_e = relative_e + + def _create_layers(self): + self.layers = [] + + prev_z = None + cur_z = 0 + cur_lines = [] + layer_index = [] + + temp_layers = {} + for line in self.lines: + if line.command() == "G92" and line.z != None: + cur_z = line.z + elif line.is_move(): + if line.z != None: + if line.relative: + cur_z += line.z + else: + cur_z = line.z + + if cur_z != prev_z: + old_lines = temp_layers.pop(prev_z,[]) + old_lines += cur_lines + temp_layers[prev_z] = old_lines + + if not prev_z in layer_index: + layer_index.append(prev_z) + + cur_lines = [] + + cur_lines.append(line) + prev_z = cur_z + + + old_lines = temp_layers.pop(prev_z,[]) + old_lines += cur_lines + temp_layers[prev_z] = old_lines + + if not prev_z in layer_index: + layer_index.append(prev_z) + + layer_index.sort() + + for idx in layer_index: + cur_lines = temp_layers[idx] + has_movement = False + for l in cur_lines: + if l.is_move() and l.e != None: + has_movement = True + break + + if has_movement: + self.layers.append(Layer(cur_lines)) + + + def num_layers(self): + return len(self.layers) + + + def measure(self): + xmin = 999999999 + ymin = 999999999 + zmin = 0 + xmax = -999999999 + ymax = -999999999 + zmax = -999999999 + + for l in self.layers: + xd,yd,zd = l.measure() + if xd[0] < xmin: + xmin = xd[0] + if xd[1] > xmax: + xmax = xd[1] + + if yd[0] < ymin: + ymin = yd[0] + if yd[1] > ymax: + ymax = yd[1] + + if zd[0] < zmin: + zmin = zd[0] + if zd[1] > zmax: + zmax = zd[1] + + self.xmin = xmin + self.xmax = xmax + self.ymin = ymin + self.ymax = ymax + self.zmin = zmin + self.zmax = zmax + self.width = xmax - xmin + self.depth = ymax - ymin + self.height = zmax - zmin def filament_length(self): total_e = 0 @@ -196,7 +326,7 @@ class GCode(object): total_e += cur_e cur_e = line.e elif line.is_move() and line.e: - if line.relative: + if line.relative_e: cur_e += line.e else: cur_e = line.e @@ -210,6 +340,8 @@ def main(): print "usage: %s filename.gcode" % sys.argv[0] return +# d = [i.replace("\n","") for i in open(sys.argv[1])] +# gcode = GCode(d) gcode = GCode(list(open(sys.argv[1]))) gcode.measure() @@ -219,6 +351,8 @@ def main(): print "\tY: %0.02f - %0.02f (%0.02f)" % (gcode.ymin,gcode.ymax,gcode.depth) print "\tZ: %0.02f - %0.02f (%0.02f)" % (gcode.zmin,gcode.zmax,gcode.height) print "Filament used: %0.02fmm" % gcode.filament_length() + print "Number of layers: %d" % gcode.num_layers() + if __name__ == '__main__': main() diff --git a/plater.png b/plater.png new file mode 100644 index 0000000..a4ef9e5 Binary files /dev/null and b/plater.png differ diff --git a/printcore.py b/printcore.py index bb6501c..3350761 100755 --- a/printcore.py +++ b/printcore.py @@ -222,6 +222,7 @@ class printcore(): def pause(self): """Pauses the print, saving the current position. """ + if not self.printing: return False self.paused = True self.printing = False self.print_thread.join() @@ -230,6 +231,7 @@ class printcore(): def resume(self): """Resumes a paused print. """ + if not self.paused: return False self.paused = False self.printing = True self.print_thread = Thread(target = self._print) @@ -287,6 +289,8 @@ class printcore(): self.sentlines = {} self.log = [] self.sent = [] + self.print_thread.join() + self.print_thread = None if self.endcb: #callback for printing done try: self.endcb() diff --git a/printrun/gui.py b/printrun/gui.py index 09f7908..7e261d5 100644 --- a/printrun/gui.py +++ b/printrun/gui.py @@ -223,7 +223,7 @@ class MainToolbar(wx.BoxSizer): root.serialport = wx.ComboBox(root.panel, -1, choices = root.scanserial(), - style = wx.CB_DROPDOWN, size = (100, 25)) + style = wx.CB_DROPDOWN, size = (150, 25)) root.serialport.SetToolTip(wx.ToolTip("Select Port Printer is connected to")) root.rescanports() self.Add(root.serialport) diff --git a/printrun/webinterface.py b/printrun/webinterface.py index bc0d564..bb9690b 100644 --- a/printrun/webinterface.py +++ b/printrun/webinterface.py @@ -23,7 +23,7 @@ from printrun.printrun_utils import configfile, imagefile, sharedfile users = {} def PrintHeader(): - return '\n\nPronterface-Web\n\n\n\n' + return '\n\nPronterface-Web\n\n\n\n\n' def PrintMenu(): return '' @@ -284,14 +284,14 @@ class WebInterface(object): pageText = PrintHeader()+self.name+PrintMenu() pageText+="
\n" pageText+="
\n" - pageText+="
\n" #endxy pageText+="
" pageText+="" pageText+='' - pageText+='Z 10' - pageText+='Z 1' - pageText+='Z .1' - pageText+='Z -.1' - pageText+='Z -1' - pageText+='Z -10' + pageText+='Z 10' + pageText+='Z 1' + pageText+='Z .1' + pageText+='Z -.1' + pageText+='Z -1' + pageText+='Z -10' pageText+="" #TODO Map Z Moves pageText+="
\n" #endz @@ -374,11 +374,14 @@ def KillWebInterfaceThread(): cherrypy.engine.exit() def StartWebInterfaceThread(webInterface): - current_dir = os.path.dirname(os.path.abspath(__file__)) + current_dir = os.path.dirname(os.path.abspath(__file__)) cherrypy.config.update({'engine.autoreload_on':False}) cherrypy.config.update(configfile(webInterface.pface.web_config or "http.config")) conf = {'/css/style.css': {'tools.staticfile.on': True, - 'tools.staticfile.filename': sharedfile('css/style.css'), + 'tools.staticfile.filename': sharedfile('web/css/style.css'), + }, + '/js/asyncCommand.js': {'tools.staticfile.on': True, + 'tools.staticfile.filename': sharedfile('web/js/asyncCommand.js'), }, '/images/control_xy.png': {'tools.staticfile.on': True, 'tools.staticfile.filename': imagefile('control_xy.png'), diff --git a/pronsole.png b/pronsole.png new file mode 100644 index 0000000..d0c745d Binary files /dev/null and b/pronsole.png differ diff --git a/pronsole.py b/pronsole.py index 982665b..6bcae83 100755 --- a/pronsole.py +++ b/pronsole.py @@ -131,6 +131,14 @@ def estimate_duration(g): #print "Total Duration: " #, time.strftime('%H:%M:%S', time.gmtime(totalduration)) return "{0:d} layers, ".format(int(layercount)) + str(datetime.timedelta(seconds = int(totalduration))) +def confirm(): + y_or_n = raw_input("y/n: ") + if y_or_n == "y": + return True + elif y_or_n != "n": + return confirm() + return False + class Settings: #def _temperature_alias(self): return {"pla":210, "abs":230, "off":0} #def _temperature_validate(self, v): @@ -183,15 +191,46 @@ class Settings: def _all_settings(self): return dict([(k, getattr(self, k)) for k in self.__dict__.keys() if not k.startswith("_")]) +class Status: + + def __init__(self): + self.extruder_temp = 0 + self.extruder_temp_target = 0 + self.bed_temp = 0 + self.bed_temp_target = 0 + self.print_job = None + self.print_job_progress = 1.0 + + def update_tempreading(self, tempstr): + r = tempstr.split() + # eg. r = ["ok", "T:20.5", "/0.0", "B:0.0", "/0.0", "@:0"] + if len(r) == 6: + self.extruder_temp = float(r[1][2:]) + self.extruder_temp_target = float(r[2][1:]) + self.bed_temp = float(r[3][2:]) + self.bed_temp_target = float(r[4][1:]) + + @property + def bed_enabled(self): + return self.bed_temp != 0 + + @property + def extruder_enabled(self): + return self.extruder_temp != 0 + + + class pronsole(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) if not READLINE: self.completekey = None + self.status = Status() + self.dynamic_temp = False self.p = printcore.printcore() self.p.recvcb = self.recvcb self.recvlisteners = [] - self.prompt = "PC>" + self.in_macro = False self.p.onlinecb = self.online self.f = None self.listing = 0 @@ -230,6 +269,55 @@ class pronsole(cmd.Cmd): self.webrequested = False self.web_config = None self.web_auth_config = None + self.promptstrs = {"offline" : "%(bold)suninitialized>%(normal)s ", + "fallback" : "%(bold)sPC>%(normal)s ", + "macro" : "%(bold)s..>%(normal)s ", + "online" : "%(bold)sT:%(extruder_temp_fancy)s %(progress_fancy)s >%(normal)s "} + + def promptf(self): + """A function to generate prompts so that we can do dynamic prompts. """ + if self.in_macro: + promptstr = self.promptstrs["macro"] + elif not self.p.online: + promptstr = self.promptstrs["offline"] + elif self.status.extruder_enabled: + promptstr = self.promptstrs["online"] + else: + promptstr = self.promptstrs["fallback"] + if not "%" in promptstr: + return promptstr + else: + specials = {} + specials["extruder_temp"] = str(int(self.status.extruder_temp)) + specials["extruder_temp_target"] = str(int(self.status.extruder_temp_target)) + if self.status.extruder_temp_target == 0: + specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp)) + else: + specials["extruder_temp_fancy"] = "%s/%s" % (str(int(self.status.extruder_temp)), str(int(self.status.extruder_temp_target))) + if self.p.printing: + progress = int(1000*float(self.p.queueindex)/len(self.p.mainqueue)) / 10 + elif self.sdprinting: + progress = self.percentdone + else: + progress = 0.0 + specials["progress"] = str(progress) + if self.p.printing or self.sdprinting: + specials["progress_fancy"] = str(progress) +"%" + else: + specials["progress_fancy"] = "?%" + specials["bold"] = "\033[01m" + specials["normal"] = "\033[00m" + return promptstr % specials + + def postcmd(self, stop, line): + """ A hook we override to generate prompts after + each command is executed, for the next prompt. + We also use it to send M105 commands so that + temp info gets updated for the prompt.""" + if self.p.online and self.dynamic_temp: + self.p.send_now("M105") + self.prompt = self.promptf() + return stop def set_temp_preset(self, key, value): if not key.startswith("bed"): @@ -257,8 +345,8 @@ class pronsole(cmd.Cmd): return baselist+glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') +glob.glob("/dev/tty.*")+glob.glob("/dev/cu.*")+glob.glob("/dev/rfcomm*") def online(self): - print "Printer is now online" - sys.stdout.write(self.prompt) + print "\rPrinter is now online" + sys.stdout.write(self.promptf()) sys.stdout.flush() def help_help(self, l): @@ -290,7 +378,8 @@ class pronsole(cmd.Cmd): def end_macro(self): if self.__dict__.has_key("onecmd"): del self.onecmd # remove override - self.prompt = "PC>" + self.in_macro = False + self.prompt = self.promptf() if self.cur_macro_def!="": self.macros[self.cur_macro_name] = self.cur_macro_def macro = self.compile_macro(self.cur_macro_name, self.cur_macro_def) @@ -342,7 +431,8 @@ class pronsole(cmd.Cmd): self.cur_macro_name = macro_name self.cur_macro_def = "" self.onecmd = self.hook_macro # override onecmd temporarily - self.prompt = "..>" + self.in_macro = False + self.prompt = self.promptf() def delete_macro(self, macro_name): if macro_name in self.macros.keys(): @@ -520,6 +610,7 @@ class pronsole(cmd.Cmd): def preloop(self): print "Welcome to the printer console! Type \"help\" for a list of available commands." + self.prompt = self.promptf() cmd.Cmd.preloop(self) def do_connect(self, l): @@ -813,10 +904,13 @@ class pronsole(cmd.Cmd): def recvcb(self, l): if "T:" in l: self.tempreadings = l + self.status.update_tempreading(l) tstring = l.rstrip() if(tstring!="ok" and not tstring.startswith("ok T") and not tstring.startswith("T:") and not self.listing and not self.monitoring): - print tstring - sys.stdout.write(self.prompt) + if tstring[:5] == "echo:": + tstring = tstring[5:].lstrip() + print "\r" + tstring.ljust(15) + sys.stdout.write(self.promptf()) sys.stdout.flush() for i in self.recvlisteners: i(l) @@ -848,16 +942,17 @@ class pronsole(cmd.Cmd): def help_help(self): self.do_help("") - def tempcb(self, l): - if "T:" in l: - print l.replace("\r", "").replace("T", "Hotend").replace("B", "Bed").replace("\n", "").replace("ok ", "") - def do_gettemp(self, l): + if "dynamic" in l: + self.dynamic_temp = True if self.p.online: - self.recvlisteners+=[self.tempcb] self.p.send_now("M105") time.sleep(0.75) - self.recvlisteners.remove(self.tempcb) + if not self.status.bed_enabled: + print "Hotend: %s/%s" % (self.status.extruder_temp, self.status.extruder_temp_target) + else: + print "Hotend: %s/%s" % (self.status.extruder_temp, self.status.extruder_temp_target) + print "Bed: %s/%s" % (self.status.bed_temp, self.status.bed_temp_target) def help_gettemp(self): print "Read the extruder and bed temperature." @@ -869,6 +964,10 @@ class pronsole(cmd.Cmd): l = l.replace(i, self.temps[i]) f = float(l) if f>=0: + if f > 250: + print f, " is a high temperature to set your extruder to. Are you sure you want to do that?" + if not confirm(): + return if self.p.online: self.p.send_now("M104 S"+l) print "Setting hotend temperature to ", f, " degrees Celsius." @@ -1045,9 +1144,22 @@ class pronsole(cmd.Cmd): print "reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)" def do_exit(self, l): + if self.status.extruder_temp_target != 0: + print "Setting extruder temp to 0" + self.p.send_now("M104 S0.0") + if self.status.bed_enabled: + if self.status.bed_temp_taret != 0: + print "Setting bed temp to 0" + self.p.send_now("M140 S0.0") print "Disconnecting from printer..." - self.p.disconnect() + print self.p.printing + if self.p.printing: + print "Are you sure you want to exit while printing?" + print "(this will terminate the print)." + if not confirm(): + return False print "Exiting program. Goodbye!" + self.p.disconnect() return True def help_exit(self): @@ -1058,6 +1170,9 @@ class pronsole(cmd.Cmd): if not self.p.online: print "Printer is not online. Please connect first." return + if not (self.p.printing or self.sdprinting): + print "Printer not printing. Please print something before monitoring." + return print "Monitoring printer, use ^C to interrupt." if len(l): try: @@ -1066,22 +1181,27 @@ class pronsole(cmd.Cmd): print "Invalid period given." print "Updating values every %f seconds."%(interval,) self.monitoring = 1 + prev_msg_len = 0 try: - while(1): + while True: self.p.send_now("M105") if(self.sdprinting): self.p.send_now("M27") time.sleep(interval) #print (self.tempreadings.replace("\r", "").replace("T", "Hotend").replace("B", "Bed").replace("\n", "").replace("ok ", "")) - if(self.p.printing): - print "Print progress: ", 100*float(self.p.queueindex)/len(self.p.mainqueue), "%" - - if(self.sdprinting): - print "SD print progress: ", self.percentdone, "%" - - except: + if self.p.printing: + preface = "Print progress: " + progress = 100*float(self.p.queueindex)/len(self.p.mainqueue) + elif self.sdprinting: + preface = "Print progress: " + progress = self.percentdone + progress = int(progress*10)/10.0 #limit precision + prev_msg = preface + str(progress) + "%" + sys.stdout.write("\r" + prev_msg.ljust(prev_msg_len)) + sys.stdout.flush() + prev_msg_len = len(prev_msg) + except KeyboardInterrupt: print "Done monitoring." - pass self.monitoring = 0 def help_monitor(self): @@ -1193,6 +1313,69 @@ class pronsole(cmd.Cmd): self.onecmd(a) self.processing_args = False + + # We replace this function, defined in cmd.py . + # It's default behavior with reagrds to Ctr-C + # and Ctr-D doesn't make much sense... + + def cmdloop(self, intro=None): + """Repeatedly issue a prompt, accept input, parse an initial prefix + off the received input, and dispatch to action methods, passing them + the remainder of the line as argument. + + """ + + self.preloop() + if self.use_rawinput and self.completekey: + try: + import readline + self.old_completer = readline.get_completer() + readline.set_completer(self.complete) + readline.parse_and_bind(self.completekey+": complete") + except ImportError: + pass + try: + if intro is not None: + self.intro = intro + if self.intro: + self.stdout.write(str(self.intro)+"\n") + stop = None + while not stop: + if self.cmdqueue: + line = self.cmdqueue.pop(0) + else: + if self.use_rawinput: + try: + line = raw_input(self.prompt) + except EOFError: + print "" + should_exit = self.do_exit("") + if should_exit: + exit() + except KeyboardInterrupt: + print "" + line = "" + else: + self.stdout.write(self.prompt) + self.stdout.flush() + line = self.stdin.readline() + if not len(line): + line = "" + else: + line = line.rstrip('\r\n') + line = self.precmd(line) + stop = self.onecmd(line) + stop = self.postcmd(stop, line) + self.postloop() + finally: + if self.use_rawinput and self.completekey: + try: + import readline + readline.set_completer(self.old_completer) + except ImportError: + pass + + if __name__ == "__main__": interp = pronsole() diff --git a/css/style.css b/web/css/style.css similarity index 100% rename from css/style.css rename to web/css/style.css diff --git a/web/js/asyncCommand.js b/web/js/asyncCommand.js new file mode 100644 index 0000000..f37ec8c --- /dev/null +++ b/web/js/asyncCommand.js @@ -0,0 +1,79 @@ +function pronterfaceWebInterface_setup(){ + pronterfaceWebInterface_attachAsync(); +} + +function pronterfaceWebInterface_attachAsync(){ + + var list = []; + if(document.getElementsByClassName){ + list = document.getElementsByClassName('command'); + }else if(document.getElementsByTagName){ + list = document.getElementsByTagName('a'); + list.concat( document.getElementsByTagName('area') ); + //TODO filter list via checking the className attributes + }else{ + console && console.error && console.error('unable to gather list of elements'); + return false; + } + + for(var i=0; i < list.length; i++){ + list[i].addEventListener && list[i].addEventListener( 'click', function(e){return pronterfaceWebInterface_asyncCommand(null, e);}, true ); + list[i].attachEvent && list[i].attachEvent( 'onclick', function(e){return pronterfaceWebInterface_asyncCommand(null, e);} ); + } + + return true; +} + + +function pronterfaceWebInterface_asyncCommand( urlOrElement, event ){ + + if( ! urlOrElement && event.target) + urlOrElement = event.target; + + var url = null; + if( typeof urlOrElement == 'string' ){ + url = urlOrElement; + }else{ + url = urlOrElement&&urlOrElement.href; + } + + if( typeof url != 'string' ){ + console && console.error && console.error('url not a string', urlOrElement, url); + return true; + } + + var httpRequest; + if (window.XMLHttpRequest) { // Mozilla, Safari, ... + httpRequest = new XMLHttpRequest(); + } else if (window.ActiveXObject) { // IE 8 and older + httpRequest = new ActiveXObject("Microsoft.XMLHTTP"); + } + + if( ! httpRequest ){ + alert('no AJAX available?'); + // follow link + return true; + } + + //onreadystatechange + //onerror + httpRequest.open( 'GET', url, true); + httpRequest.send(null); + + // don't follow link + if( event ){ + event.stopImmediatePropagation && event.stopImmediatePropagation(); + event.defaultPrevented = true; + event.preventDefault && event.preventDefault(); + } + return false; +} + + +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", pronterfaceWebInterface_setup, false); +} else if (document.attachEvent) { + document.attachEvent("onreadystatechange", pronterfaceWebInterface_setup); +} else { + document.onload = pronterfaceWebInterface_setup; +}