Event Manager

Added event manager to trigger system and gcode commands.
master
Lars Norpchen 2013-05-20 20:04:21 -07:00
parent 7b214d3f16
commit 525d73a20b
7 changed files with 343 additions and 84 deletions

150
octoprint/events.py Normal file
View File

@ -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")

63
octoprint/events.txt Normal file
View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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:

View File

@ -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)