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 afd5d36..53ae43e 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -7,9 +7,8 @@ import datetime import threading import copy import os -import subprocess -import re -import logging, logging.config + +#import logging, logging.config import octoprint.util.comm as comm import octoprint.util as util @@ -28,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 @@ -94,9 +94,6 @@ class Printer(): currentZ=None ) - self.sys_command= { "z_change": settings().get(["system_commands", "z_change"]), "cancelled" : settings().get(["system_commands", "cancelled"]), "print_done" :settings().get(["system_commands", "print_done"]), "print_started": settings().get(["system_commands", "print_started"])}; - - #~~ callback handling def registerCallback(self, callback): @@ -137,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): """ @@ -198,7 +196,7 @@ class Printer(): self._setCurrentZ(-1) - self.executeSystemCommand(self.sys_command['print_started']) + self._eventManager.FireEvent ('PrintStarted',filename) self._comm.printGCode(self._gcodeList) @@ -225,9 +223,9 @@ class Printer(): self._setProgressData(None, None, None) # mark print as failure - self._gcodeManager.printFailed(self._filename) - self.executeSystemCommand(self.sys_command['cancelled']) - + if self._filename: + self._gcodeManager.printFailed(self._filename) + #~~ state monitoring def setTimelapse(self, timelapse): @@ -370,6 +368,13 @@ 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: @@ -377,10 +382,10 @@ class Printer(): #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.executeSystemCommand(self.sys_command['print_done']) + 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.executeSystemCommand(self.sys_command['cancelled']) + 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 @@ -419,7 +424,7 @@ class Printer(): self.peakZ = newZ if self._timelapse is not None: self._timelapse.onZChange(oldZ, newZ) - self.executeSystemCommand(self.sys_command['z_change']) + self._eventManager.FireEvent ('ZChange',newZ) self._setCurrentZ(newZ) @@ -437,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()}) @@ -489,31 +494,7 @@ class Printer(): def isLoading(self): return self._gcodeLoader is not None - - def executeSystemCommand(self,command_string): - if command_string is None: - return - logger = logging.getLogger(__name__) - try: - # handle a few regex substs for job data passed to external apps - cmd_string_with_params = command_string - cmd_string_with_params = re.sub("_ZHEIGHT_",str(self._currentZ), cmd_string_with_params) - cmd_string_with_params = re.sub("_FILE_",os.path.basename(self._filename), cmd_string_with_params) - # cut down to 2 decimal places, forcing through an int to avoid the 10.320000000001 floating point thing... - if self._gcodeList and self._progress: - prog = int(10000.0 * self._progress / len(self._gcodeList))/100.0 - else: - prog = 0.0 - cmd_string_with_params = re.sub("_PROGRESS_",str(prog), cmd_string_with_params) - cmd_string_with_params = re.sub("_LINE_",str(self._comm._gcodePos), cmd_string_with_params) - logger.info ("Executing system command: %s " % cmd_string_with_params) - #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(cmd_string_with_params,shell = True) - except subprocess.CalledProcessError, e: - logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message)) - except Exception, ex: - logger.exception("Command failed") + class GcodeLoader(threading.Thread): diff --git a/octoprint/server.py b/octoprint/server.py index feaae8e..d79fefa 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -17,6 +17,9 @@ import octoprint.timelapse as timelapse import octoprint.gcodefiles as gcodefiles import octoprint.util as util + +import octoprint.events as events + SUCCESS = {} BASEURL = "/ajax/" app = Flask("octoprint") @@ -24,6 +27,7 @@ app = Flask("octoprint") # In order that threads don't start too early when running as a Daemon printer = None gcodeManager = None +eventManager =None #~~ Printer state @@ -41,12 +45,16 @@ class PrinterStateConnection(tornadio2.SocketConnection): self._messageBacklogMutex = threading.Lock() 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) @@ -103,7 +111,7 @@ def index(): webcamStream=settings().get(["webcam", "stream"]), enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), - enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 + enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 ) #~~ Printer control @@ -129,7 +137,9 @@ def connect(): @app.route(BASEURL + "control/disconnect", methods=["POST"]) def disconnect(): + global eventManager printer.disconnect() + eventManager.FireEvent("Disconnected") return jsonify(state="Offline") @app.route(BASEURL + "control/command", methods=["POST"]) @@ -162,11 +172,15 @@ def printGcode(): @app.route(BASEURL + "control/pause", methods=["POST"]) def pausePrint(): + global eventManager + eventManager.FireEvent("Paused") printer.togglePausePrint() return jsonify(SUCCESS) @app.route(BASEURL + "control/cancel", methods=["POST"]) def cancelPrint(): + global eventManager + eventManager.FireEvent("Cancelled") printer.cancelPrint() return jsonify(SUCCESS) @@ -256,6 +270,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"]) @@ -267,6 +283,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"]) @@ -317,11 +335,12 @@ def deleteTimelapse(filename): @app.route(BASEURL + "timelapse/config", methods=["POST"]) 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"): @@ -329,7 +348,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() @@ -374,16 +393,9 @@ def getSettings(): "profiles": s.get(["temperature", "profiles"]) }, "system": { - "actions": s.get(["system", "actions"]) - }, - "system_commands": { - "print_done": s.get(["system_commands", "print_done"]), - "cancelled": s.get(["system_commands", "cancelled"]), - "print_started": s.get(["system_commands", "print_started"]), - "z_change": s.get(["system_commands", "z_change"]) - - } - + "actions": s.get(["system", "actions"]), + "events": s.get(["system", "events"]) + } }) @app.route(BASEURL + "settings", methods=["POST"]) @@ -424,13 +436,7 @@ def setSettings(): if "system" in data.keys(): if "actions" in data["system"].keys(): s.set(["system", "actions"], data["system"]["actions"]) - - if "system_commands" in data.keys(): - if "z_change" in data["system_commands"].keys(): s.set(["system_commands", "z_change"], data["system_commands"]["z_change"]) - if "print_started" in data["system_commands"].keys(): s.set(["system_commands", "print_started"], data["system_commands"]["print_started"]) - if "cancelled" in data["system_commands"].keys(): s.set(["system_commands", "cancelled"], data["system_commands"]["cancelled"]) - if "print_done" in data["system_commands"].keys(): s.set(["system_commands", "print_done"], data["system_commands"]["print_done"]) - + if "events" in data["system"].keys(): s.set(["system", "events"], data["system"]["events"]) s.save() return getSettings() @@ -465,11 +471,13 @@ 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 eventManager + from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop @@ -481,14 +489,21 @@ class Server(): # then initialize logging self._initLogging(self._debug) + 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 self._host is None: self._host = settings().get(["server", "host"]) if self._port is None: self._port = settings().getInt(["server", "port"]) - logging.getLogger(__name__).info("Listening on http://%s:%d" % (self._host, self._port)) app.debug = self._debug @@ -499,6 +514,8 @@ class Server(): ]) self._server = HTTPServer(self._tornado_app) self._server.listen(self._port, address=self._host) + + eventManager.FireEvent("Startup") IOLoop.instance().start() def _initSettings(self, configfile, basedir): diff --git a/octoprint/settings.py b/octoprint/settings.py index b655f49..947466a 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -68,14 +68,9 @@ default_settings = { }, "controls": [], "system": { - "actions": [] + "actions": [], + "events": [] }, - "system_commands": { - "z_change":None, - "print_started":None, - "cancelled":None, - "print_done":None - } } valid_boolean_trues = ["true", "yes", "y", "1"] @@ -116,7 +111,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 4283323..9b5c546 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)