diff --git a/octoprint/events.py b/octoprint/events.py new file mode 100644 index 0000000..01a07a9 --- /dev/null +++ b/octoprint/events.py @@ -0,0 +1,150 @@ +import sys +import datetime +import time +import math +import re +import logging, logging.config +import subprocess +import octoprint.printer as printer +import os + +# right now we're logging a lot of extra information for testing +# we might want to comment out some of the logging eventually + +class event_record(object): + what = None + who = None + action = None + + + # object that handles receiving events and dispatching them to listeners + # +class EventManager(object): + def __init__(self): + self.registered_events = [] + self.logger = logging.getLogger(__name__) + + # Fire an event to anyone listening + # any object can generate an event and any object can listen + # pass in the event_name as a string (arbitrary, but case sensitive) + # and any extra data that may pertain to the event + def FireEvent (self,event_name,extra_data=None): + self.logger.info ( "Firing event: " + event_name + " (" +str (extra_data)+")") + for ev in self.registered_events: + if event_name == ev.what: + self.logger.info ( "Sending action to " + str(ev.who)) + if ev.action != None : + ev.action (event_name,extra_data) +# else: +# self.logger.info ( "events don't match " + str(ev.what)+ " and " + event_name) + + + # register a listener to an event -- pass in + # the event name (as a string), the target object + # and the function to call + def Register (self,event_name, target, action): + new_ev =event_record() + new_ev.what = event_name + new_ev.who = target + new_ev.action= action + self.registered_events=self.registered_events+[new_ev] + self.logger.info ("Registered event '"+new_ev.what+"' to invoke '"+str(new_ev.action)+"' on "+str(new_ev.who) ) + + + def unRegister (self,event_name, target, action): + self.registered_events[:] = [e for e in self.registered_events if event_name != e.what or e.action != action or e.who!=target] + + + #sample event receiver + # def event_rec(self,event_name,extra_data): + # print str(self) + " Receieved event ", event_name ," (", str (extra_data),")" + + # and registering it: + # eventManager.Register("Startup",self,self.event_rec) + + +class event_dispatch(object): + type = None + event_string = None + command_data = None + +# object that hooks the event manager to system events, gcode, etc. +# creates listeners to any events defined in the config.yaml settings +class EventResponse(object): + + def __init__(self, eventManager,printer): + self.registered_responses= [] + self._eventManager = eventManager + self._printer = printer + self.logger = logging.getLogger(__name__) + self._event_data = "" + + def setupEvents(self,s): + availableEvents = s.get(["system", "events"]) + for ev in availableEvents: + event = event_dispatch() + event.type = ev["type"].strip() + event.event_string = ev["event"].strip() + event.command_data = ev["command"] + self._eventManager.Register ( event.event_string ,self,self.eventRec) + self.registered_responses = self.registered_responses+[event] + self.logger.info ("Registered "+event.type +" event '"+event.event_string+"' to execute '"+event.command_data+"'" ) + self.logger.info ( "Registered "+ str(len(self.registered_responses))+" events") + + def eventRec (self,event_name, event_data): + self.logger.info ( "Receieved event: " + event_name + " (" + str(event_data) + ")") + self._event_data = event_data + for ev in self.registered_responses: + if ev.event_string == event_name: + if ev.type == "system": + self.executeSystemCommand (ev.command_data) + if ev.type == "gcode": + self.executeGCode(ev.command_data) + + # handle a few regex substs for job data passed to external apps + def doStringProcessing (self, command_string): + cmd_string_with_params = command_string + cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._printer._currentZ), cmd_string_with_params) + if self._printer._filename: + cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._printer._filename), cmd_string_with_params) + else: + cmd_string_with_params = re.sub("_FILE_","NO FILE", cmd_string_with_params) + # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... + if self._printer._gcodeList and self._printer._progress: + prog = int(10000.0 * self._printer._progress / len(self._printer._gcodeList))/100.0 + else: + prog = 0.0 + cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) + if self._printer._comm: + cmd_string_with_params = re.sub("_LINE_",str(self._printer._comm._gcodePos), cmd_string_with_params) + else: + cmd_string_with_params = re.sub("_LINE_","0", cmd_string_with_params) + if self._event_data: + cmd_string_with_params = re.sub("_DATA_",str(self._event_data), cmd_string_with_params) + else: + cmd_string_with_params = re.sub("_DATA_","", cmd_string_with_params) + cmd_string_with_params = re.sub("_NOW_",str(datetime.datetime.now()), cmd_string_with_params) + return cmd_string_with_params + + + def executeGCode(self,command_string): + command_string = self.doStringProcessing(command_string) + self.logger.info ("GCode command: " + command_string) + self._printer.commands(command_string.split(',')) + + def executeSystemCommand(self,command_string): + if command_string is None: + return + try: + command_string = self.doStringProcessing(command_string) + self.logger.info ("Executing system command: "+ command_string) + #use Popen here since it won't wait for the shell to return...and we send some of these + # commands during a print job, we don't want to block! + subprocess.Popen(command_string,shell = True) + except subprocess.CalledProcessError, e: + self.logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) + except Exception, ex: + self.logger.exception("Command failed") + + + \ No newline at end of file diff --git a/octoprint/events.txt b/octoprint/events.txt new file mode 100644 index 0000000..d6801e2 --- /dev/null +++ b/octoprint/events.txt @@ -0,0 +1,63 @@ +Add to the "system:" section of config.yaml + +There are two types of event handlers at the moment: + system: invokes an external command without waiting for the result + gcode: sends some gcode to the printer. Separate multiple commands with a comma + +Example: + system: + events: + - event: Connected + type: gcode + command: M115,M17 printer connected!,G28 + - event: Disconnected + type: system + command: python ~/growl.py -t mygrowlserver -d "Lost connection to printer" -a OctoPrint -i http://rasppi:8080/Octoprint_logo.png + - event: PrintStarted + type: system + command: python ~/growl.py -t mygrowlserver -d "Starting _FILE_" -a OctoPrint -i http://rasppi:8080/Octoprint_logo.png + - event: PrintDone + type: system + command: python ~/growl.py -t mygrowlserver -d "Completed _FILE_" -a OctoPrint -i http://rasppi:8080/Octoprint_logo.png + +command values support the following dynamic tokens: + _DATA_ - the data associated with the event (not all events have data, when they do, it's often a filename) + _FILE_ - filename of the current print (not always the same as _DATA_ filename) + _LINE_ - the current GCode line + _PROGRESS_ - the percent complete + _ZHEIGHT_ - the current Z position of the head + _NOW_ - the date and time of the event + + +Available Events: + + Startup -- the server has started + Connected -- the server has connected to the printer (data is port and baudrate) + Disconnected -- the server has disconnected from the printer + ClientOpen -- a client has connected to the web server + ClientClosed -- a client has disconnected from the web server + PowerOn -- the GCode has turned on the printer power via M80 + PowerOff -- the GCode has turned on the printer power via M81 + Upload -- a gcode file upload has been uploaded (data is filename) + LoadStart -- a gcode file load has started (data is filename) + LoadDone -- a gcode file load has finished (data is filename) + PrintStarted -- a print has started (data is filename) + PrintFailed -- a print failed (data is filename) + PrintDone -- a print completed successfully (data is filename) + Cancelled -- the print has been cancelled via the cancel button (data is filename) + Home -- the head has gone home via G28 + ZChange -- the printer's Z-Height has changed (new layer) + Paused -- the print has been paused + Waiting -- the print is paused due to a gcode wait command + Cooling -- the GCode has enabled the platform cooler via M245 + Alert -- the GCode has issued a user alert (beep) via M300 + Conveyor -- the GCode has enabled the conveyor belt via M240 + Eject -- the GCode has enabled the part ejector via M40 + CaptureStart -- a timelapse image is starting to be captured (data is image filename) + CaptureDone -- a timelapse image has completed being captured (data is image filename) + MovieDone -- the timelapse movie is completed (data is movie filename) + EStop -- the GCode has issued a panic stop via M112 + Error -- an error has occurred (data is error string) + + + \ No newline at end of file diff --git a/octoprint/printer.py b/octoprint/printer.py index be080b2..53ae43e 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -8,6 +8,8 @@ import threading import copy import os +#import logging, logging.config + import octoprint.util.comm as comm import octoprint.util as util @@ -25,8 +27,9 @@ def getConnectionOptions(): } class Printer(): - def __init__(self, gcodeManager): + def __init__(self, gcodeManager,eventManager): self._gcodeManager = gcodeManager + self._eventManager = eventManager # state self._temp = None @@ -53,6 +56,7 @@ class Printer(): self._currentZ = None + self.peakZ = -1 self._progress = None self._printTime = None self._printTimeLeft = None @@ -130,6 +134,7 @@ class Printer(): if self._comm is not None: self._comm.close() self._comm = comm.MachineCom(port, baudrate, callbackObject=self) + self._comm.setEventManager(self._eventManager) def disconnect(self): """ @@ -190,6 +195,9 @@ class Printer(): return self._setCurrentZ(-1) + + self._eventManager.FireEvent ('PrintStarted',filename) + self._comm.printGCode(self._gcodeList) def togglePausePrint(self): @@ -215,8 +223,9 @@ class Printer(): self._setProgressData(None, None, None) # mark print as failure - self._gcodeManager.printFailed(self._filename) - + if self._filename: + self._gcodeManager.printFailed(self._filename) + #~~ state monitoring def setTimelapse(self, timelapse): @@ -359,12 +368,24 @@ class Printer(): elif state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: self._timelapse.onPrintjobStarted(self._filename) + if state == self._comm.STATE_PRINTING and oldState != self._comm.STATE_PAUSED: + self._eventManager.FireEvent ('PrintStarted',filename) + if state == self._comm.STATE_OPERATIONAL and (oldState <= self._comm.STATE_CONNECTING or oldState >=self._comm.STATE_CLOSED): + self._eventManager.FireEvent ('Connected',self._comm._port+" at " +self._comm._baudrate) + if state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: + self._eventManager.FireEvent ('Error',self._comm.getErrorString()) + # forward relevant state changes to gcode manager if self._comm is not None and oldState == self._comm.STATE_PRINTING: if state == self._comm.STATE_OPERATIONAL: self._gcodeManager.printSucceeded(self._filename) + #hrm....we seem to hit this state and THEN the next failed state on a cancel request? + # oh well, add a check to see if we're really done before sending the success event external command + if self._printTimeLeft < 1: + self._eventManager.FireEvent ('PrintDone',filename) elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: self._gcodeManager.printFailed(self._filename) + self._eventManager.FireEvent ('PrintFailed',filename) self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing elif self._comm is not None and state == self._comm.STATE_PRINTING: self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use @@ -397,9 +418,14 @@ class Printer(): Callback method for the comm object, called upon change of the z-layer. """ oldZ = self._currentZ - if self._timelapse is not None: - self._timelapse.onZChange(oldZ, newZ) - + # only do this if we hit a new Z peak level. Some slicers do a Z-lift when retracting / moving without printing + # and some do ananti-backlash up-then-down movement when advancing layers + if newZ > self.peakZ: + self.peakZ = newZ + if self._timelapse is not None: + self._timelapse.onZChange(oldZ, newZ) + self._eventManager.FireEvent ('ZChange',newZ) + self._setCurrentZ(newZ) #~~ callbacks triggered by gcodeLoader @@ -416,7 +442,7 @@ class Printer(): self._setCurrentZ(None) self._setProgressData(None, None, None) self._gcodeLoader = None - + self.eventManager.FireEvent("LoadDone",filename) self._stateMonitor.setGcodeData({"filename": None, "progress": None}) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) @@ -469,6 +495,8 @@ class Printer(): def isLoading(self): return self._gcodeLoader is not None + + class GcodeLoader(threading.Thread): """ The GcodeLoader takes care of loading a gcode-File from disk and parsing it into a gcode object in a separate @@ -526,6 +554,7 @@ class StateMonitor(object): self._jobData = None self._gcodeData = None self._currentZ = None + self._peakZ = -1 self._progress = None self._changeEvent = threading.Event() diff --git a/octoprint/server.py b/octoprint/server.py index 3a6249a..ec6f77b 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -20,6 +20,9 @@ import octoprint.gcodefiles as gcodefiles import octoprint.util as util import octoprint.users as users + +import octoprint.events as events + SUCCESS = {} BASEURL = "/ajax/" @@ -29,6 +32,7 @@ app = Flask("octoprint") printer = None gcodeManager = None userManager = None +eventManager =None principals = Principal(app) admin_permission = Permission(RoleNeed("admin")) @@ -54,12 +58,16 @@ class PrinterStateConnection(tornadio2.SocketConnection): self._userManager = userManager def on_open(self, info): + global eventManager + eventManager.FireEvent("ClientOpen") self._logger.info("New connection from client") # Use of global here is smelly printer.registerCallback(self) gcodeManager.registerCallback(self) def on_close(self): + global eventManager + eventManager.FireEvent("ClientClosed") self._logger.info("Closed client connection") # Use of global here is smelly printer.unregisterCallback(self) @@ -144,6 +152,7 @@ def connect(): printer.connect(port=port, baudrate=baudrate) elif "command" in request.values.keys() and request.values["command"] == "disconnect": printer.disconnect() + eventManager.FireEvent("Disconnected") return jsonify(SUCCESS) @@ -179,8 +188,10 @@ def printJobControl(): printer.startPrint() elif request.values["command"] == "pause": printer.togglePausePrint() + eventManager.FireEvent("Paused") elif request.values["command"] == "cancel": printer.cancelPrint() + eventManager.FireEvent("Cancelled") return jsonify(SUCCESS) @app.route(BASEURL + "control/temperature", methods=["POST"]) @@ -270,6 +281,8 @@ def uploadGcodeFile(): if "gcode_file" in request.files.keys(): file = request.files["gcode_file"] filename = gcodeManager.addFile(file) + global eventManager + eventManager.FireEvent("Upload",filename) return jsonify(files=gcodeManager.getAllFileData(), filename=filename) @app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @@ -282,6 +295,8 @@ def loadGcodeFile(): filename = gcodeManager.getAbsolutePath(request.values["filename"]) if filename is not None: printer.loadGcode(filename, printAfterLoading) + global eventManager + eventManager.FireEvent("LoadStart",filename) return jsonify(SUCCESS) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @@ -335,11 +350,12 @@ def deleteTimelapse(filename): @app.route(BASEURL + "timelapse", methods=["POST"]) @login_required def setTimelapseConfig(): + global eventManager if request.values.has_key("type"): type = request.values["type"] lapse = None if "zchange" == type: - lapse = timelapse.ZTimelapse() + lapse = timelapse.ZTimelapse(eventManager) elif "timed" == type: interval = 10 if request.values.has_key("interval"): @@ -347,7 +363,7 @@ def setTimelapseConfig(): interval = int(request.values["interval"]) except ValueError: pass - lapse = timelapse.TimedTimelapse(interval) + lapse = timelapse.TimedTimelapse( eventManager,interval) printer.setTimelapse(lapse) return getTimelapseData() @@ -392,8 +408,9 @@ def getSettings(): "profiles": s.get(["temperature", "profiles"]) }, "system": { - "actions": s.get(["system", "actions"]) - } + "actions": s.get(["system", "actions"]), + "events": s.get(["system", "events"]) + } }) @app.route(BASEURL + "settings", methods=["POST"]) @@ -436,7 +453,7 @@ def setSettings(): if "system" in data.keys(): if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) - + if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"]) s.save() return getSettings() @@ -634,12 +651,14 @@ class Server(): self._port = port self._debug = debug + def run(self): # Global as I can't work out a way to get it into PrinterStateConnection global printer global gcodeManager global userManager - + global eventManager + from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop @@ -652,8 +671,16 @@ class Server(): self._initLogging(self._debug) logger = logging.getLogger(__name__) + eventManager = events.EventManager() gcodeManager = gcodefiles.GcodeManager() - printer = Printer(gcodeManager) + printer = Printer(gcodeManager, eventManager) + self.event_dispatcher = events.EventResponse (eventManager,printer) + self.event_dispatcher.setupEvents(settings()) +# a few test commands to test the event manager is working... + # eventManager.Register("Startup",self,self.event_rec) + # eventManager.unRegister("Startup",self,self.event_rec) + # eventManager.FireEvent("Startup") + if settings().getBoolean(["accessControl", "enabled"]): userManagerName = settings().get(["accessControl", "userManager"]) @@ -687,6 +714,8 @@ class Server(): ]) self._server = HTTPServer(self._tornado_app) self._server.listen(self._port, address=self._host) + + eventManager.FireEvent("Startup") IOLoop.instance().start() def _createSocketConnection(self, session, endpoint=None): diff --git a/octoprint/settings.py b/octoprint/settings.py index 7c385f8..7d2d470 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -68,7 +68,8 @@ default_settings = { }, "controls": [], "system": { - "actions": [] + "actions": [], + "events": [] }, "accessControl": { "enabled": False, @@ -115,7 +116,8 @@ class Settings(object): if os.path.exists(self._configfile) and os.path.isfile(self._configfile): with open(self._configfile, "r") as f: self._config = yaml.safe_load(f) - else: + # chamged from else to handle cases where the file exists, but is empty / 0 bytes + if not self._config: self._config = {} def save(self, force=False): diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 2db8984..5056219 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -14,6 +14,7 @@ import time import subprocess import fnmatch import datetime +import octoprint.events as events import sys @@ -33,9 +34,9 @@ def getFinishedTimelapses(): return files class Timelapse(object): - def __init__(self): + def __init__(self,ev): self._logger = logging.getLogger(__name__) - + self._eventManager = ev self._imageNumber = None self._inTimelapse = False self._gcodeFile = None @@ -85,7 +86,7 @@ class Timelapse(object): filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber)) self._imageNumber += 1 self._logger.debug("Capturing image to %s" % filename) - + self._eventManager.FireEvent("CaptureStart",filename); captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename}) captureThread.daemon = True captureThread.start() @@ -93,6 +94,7 @@ class Timelapse(object): def _captureWorker(self, filename): urllib.urlretrieve(self._snapshotUrl, filename) self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl)) + self._eventManager.FireEvent("CaptureDone",filename); def _createMovie(self): ffmpeg = settings().get(["webcam", "ffmpeg"]) @@ -122,6 +124,7 @@ class Timelapse(object): command.append(output) subprocess.call(command) self._logger.debug("Rendering movie to %s" % output) + self._eventManager.FireEvent("MovieDone",output); def cleanCaptureDir(self): if not os.path.isdir(self._captureDir): @@ -134,8 +137,8 @@ class Timelapse(object): os.remove(os.path.join(self._captureDir, filename)) class ZTimelapse(Timelapse): - def __init__(self): - Timelapse.__init__(self) + def __init__(self,ev): + Timelapse.__init__(self,ev) self._logger.debug("ZTimelapse initialized") def onZChange(self, oldZ, newZ): @@ -143,8 +146,8 @@ class ZTimelapse(Timelapse): self.captureImage() class TimedTimelapse(Timelapse): - def __init__(self, interval=1): - Timelapse.__init__(self) + def __init__(self, ev,interval=1): + Timelapse.__init__(self,ev) self._interval = interval if self._interval < 1: diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 9ef8893..29ff801 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -178,11 +178,15 @@ class MachineCom(object): self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._printStartTime100 = None + self._eventManager = None self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() + def setEventManager(self,em): + self._eventManager = em + def _changeState(self, newState): if self._state == newState: return @@ -328,8 +332,8 @@ class MachineCom(object): if line.startswith('Error:'): #Oh YEAH, consistency. # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" - # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" - # So we can have an extra newline in the most common case. Awesome work people. + # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" + # So we can have an extra newline in the most common case. Awesome work people. if re.match('Error:[0-9]\n', line): line = line.rstrip() + self._readline() #Skip the communication errors, as those get corrected. @@ -468,6 +472,47 @@ class MachineCom(object): def _sendCommand(self, cmd): if self._serial is None: return + if self._eventManager: + t_cmd = cmd+' ' + t_cmd = cmd+' ' + # some useful event triggered from GCode commands + # pause for user input. M0 in Marlin and M1 in G-code standard RS274NGC + if re.search ("^\s*M226\D",t_cmd,re.I) or re.search ("^\s*M[01]\D",t_cmd,re.I): + self._eventManager.FireEvent ('Waiting') + # part cooler started + if re.search ("^\s*M245\D",t_cmd,re.I): + self._eventManager.FireEvent ('Cooling') + # part conveyor started + if re.search ("^\s*M240\D",t_cmd,re.I): + self._eventManager.FireEvent ('Conveyor') + # part ejector + if re.search ("^\s*M40\D",t_cmd,re.I): + self._eventManager.FireEvent ('Eject') + # user alert issued by sending beep command to printer... + if re.search ("^\s*M300\D",t_cmd,re.I): + self._eventManager.FireEvent ('Alert') + # Print head has moved to home + if re.search ("^\s*G28\D",t_cmd,re.I): + self._eventManager.FireEvent ('Home') + if re.search ("^\s*M112\D",t_cmd,re.I): + self._eventManager.FireEvent ('EStop') + if re.search ("^\s*M80\D",t_cmd,re.I): + self._eventManager.FireEvent ('PowerOn') + if re.search ("^\s*M81\D",t_cmd,re.I): + self._eventManager.FireEvent ('PowerOff') + if re.search ("^\s*M25\D",t_cmd,re.I): # SD Card pause + self._eventManager.FireEvent ('Paused') + + +# these comparisons assume that the searched-for string is not in a comment or a parameter, for example +# GCode lines like this: +# G0 X100 ; let's not do an M109 here!!! +# M420 R000 E000 B000 ; set LED color on makerbot to RGB (note the G is replaced with an E) +# M1090 ; some command > 999 +# could potentially trip us up here.... +# this can be avoided by checking only the START of the string for the command code +# and checking for whitespace after the command (after trimming any leading whitespace, as necessary) + if 'M109' in cmd or 'M190' in cmd: self._heatupWaitStartTime = time.time() if 'M104' in cmd or 'M109' in cmd: @@ -507,9 +552,13 @@ class MachineCom(object): self._printSection = line[1] line = line[0] try: - if line == 'M0' or line == 'M1': + if line == 'M0' or line == 'M1' or line=='M112': # M112 is also an LCD pause self.setPause(True) - line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. + line = 'M105' #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. + + # LCD / user response pause can be used for things like mid-print filament changes, so + # always removing them may not be so good. Something to consider as a user preference? + if self._printSection in self._feedRateModifier: line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line) if ('G0' in line or 'G1' in line) and 'Z' in line: @@ -519,8 +568,8 @@ class MachineCom(object): self._callback.mcZChange(z) except: self._log("Unexpected error: %s" % (getExceptionString())) - checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line))) - self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum)) + checksum = reduce(lambda x,y:x^y, map(ord, "N%d %s " % (self._gcodePos, line))) + self._sendCommand("N%d %s *%d" % (self._gcodePos, line, checksum)) # added spaces between line # and command and checksum, because some firmware needs it and it's more readable in the terminal window self._gcodePos += 1 self._callback.mcProgress(self._gcodePos)