Refactoring of event management

master
Gina Häußge 2013-05-27 00:56:57 +02:00
parent c836c79179
commit be99930021
7 changed files with 305 additions and 239 deletions

View File

@ -1,149 +1,228 @@
# coding=utf-8
__author__ = "Lars Norpchen"
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import datetime
import re
import logging
import subprocess
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
from octoprint.settings import settings
class event_record(object):
def __init__(self, what, who, action):
self.what = what
self.who = who
self.action = action
# singleton
_instance = None
def eventManager():
global _instance
if _instance is None:
_instance = EventManager()
return _instance
class EventManager(object):
"""
Handles receiving events and dispatching them to listeners
Handles receiving events and dispatching them to subscribers
"""
def __init__(self):
self.registered_events = []
self.logger = logging.getLogger(__name__)
def fire(self, name, payload=None):
"""
Fire an event to anyone listening.
self._registeredListeners = {}
self._logger = logging.getLogger(__name__)
Any object can generate an event and any object can listen pass in the event_name as a string (arbitrary, but
def fire(self, event, payload=None):
"""
Fire an event to anyone subscribed to it
Any object can generate an event and any object can subscribe to the event's name as a string (arbitrary, but
case sensitive) and any extra payload data that may pertain to the event.
Callbacks must implement the signature "callback(event, payload)", with "event" being the event's name and
payload being a payload object specific to the event.
"""
self.logger.debug("Firing event: %s (%r)" % (name, payload))
for event in self.registered_events:
(who, what, action) = event
if name == what:
self.logger.debug("Sending action to %r" % who)
if action is not None:
action(name, payload)
if not event in self._registeredListeners.keys():
return
self._logger.debug("Firing event: %s (%r)" % (event, payload))
eventListeners = self._registeredListeners[event]
for listener in eventListeners:
self._logger.debug("Sending action to %r" % listener)
listener(event, payload)
def subscribe(self, name, target, action):
def subscribe(self, event, callback):
"""
Subscribe a listener to an event -- pass in the event name (as a string), the target object
and the callback object
Subscribe a listener to an event -- pass in the event name (as a string) and the callback object
"""
newEvent = (name, target, action)
self.registered_events = self.registered_events.append(newEvent)
self.logger.debug("Registered event \"%s\" to invoke \"%r\" on %r" % (name, action, target))
if not event in self._registeredListeners.keys():
self._registeredListeners[event] = []
def unsubscribe (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
class EventResponse(object):
"""
Hooks the event manager to system events, gcode, etc. Creates listeners to any events defined in the settings.
"""
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 event in availableEvents:
name = event["event"].strip()
action = event["type"].strip()
data = event["command"]
self._eventManager.subscribe(event.event_string, self, self.eventRec)
self.registered_responses = self.registered_responses.append(event)
self.logger.debug("Registered %s event \"%s\" to execute \"%s\"" % (event.type, event.event_string, event.command_data))
self.logger.debug("Registered %d events" % len(self.registered_responses))
def eventRec (self,event_name, event_data):
self.logger.debug("Received event: %s (%r)" % (event_name, 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)
def doStringProcessing (self, command_string):
"""
Handles 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._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.debug("GCode command: " + command_string)
self._printer.commands(command_string.split(','))
def executeSystemCommand(self, command_string):
if command_string is None:
if callback in self._registeredListeners[event]:
# callback is already subscribed to the event
return
self._registeredListeners[event].append(callback)
self._logger.debug("Subscribed listener %r for event %s" % (callback, event))
def unsubscribe (self, event, callback):
if not event in self._registeredListeners:
# no callback registered for callback, just return
return
if not callback in self._registeredListeners[event]:
# callback not subscribed to event, just return
return
self._registeredListeners[event].remove(callback)
self._logger.debug("Unsubscribed listener %r for event %s" % (callback, event))
class GenericEventListener(object):
"""
The GenericEventListener can be subclassed to easily create custom event listeners.
"""
def __init__(self):
self._logger = logging.getLogger(__name__)
def subscribe(self, events):
"""
Subscribes the eventCallback method for all events in the given list.
"""
for event in events:
eventManager().subscribe(event, self.eventCallback)
def unsubscribe(self, events):
"""
Unsubscribes the eventCallback method for all events in the given list
"""
for event in events:
eventManager().unsubscribe(event, self.eventCallback)
def eventCallback(self, event, payload):
"""
Actual event callback called with name of event and optional payload. Not implemented here, override in
child classes.
"""
pass
class CommandTrigger(GenericEventListener):
def __init__(self, triggerType, printer):
GenericEventListener.__init__(self)
self._printer = printer
self._subscriptions = {}
self._initSubscriptions(triggerType)
def _initSubscriptions(self, triggerType):
"""
Subscribes all events as defined in "events > $triggerType > subscriptions" in the settings with their
respective commands.
"""
if not settings().get(["events", triggerType]):
return
if not settings().getBoolean(["events", triggerType, "enabled"]):
return
eventsToSubscribe = []
for subscription in settings().get(["events", triggerType, "subscriptions"]):
if not "event" in subscription.keys() or not "command" in subscription.keys():
self._logger.info("Invalid %s, missing either event or command: %r" % (triggerType, subscription))
continue
event = subscription["event"]
command = subscription["command"]
if not event in self._subscriptions.keys():
self._subscriptions[event] = []
self._subscriptions[event].append(command)
if not event in eventsToSubscribe:
eventsToSubscribe.append(event)
self.subscribe(eventsToSubscribe)
def eventCallback(self, event, payload):
"""
Event callback, iterates over all subscribed commands for the given event, processes the command
string and then executes the command via the abstract executeCommand method.
"""
GenericEventListener.eventCallback(self, event, payload)
if not event in self._subscriptions:
return
for command in self._subscriptions[event]:
processedCommand = self._processCommand(command, payload)
self.executeCommand(processedCommand)
def executeCommand(self, command):
"""
Not implemented, override in child classes
"""
pass
def _processCommand(self, command, payload):
"""
Performs string substitutions in the command string based on a couple of current parameters.
The following substitutions are currently supported:
- %(currentZ)s : current Z position of the print head
- %(filename)s : current selected filename, or "NO FILE" if no file is selected
- %(progress)s : current print progress in percent, 0 if no print is in progress
- %(data)s : the string representation of the event's payload
- %(now)s : ISO 8601 representation of the current date and time
"""
params = {
"currentZ": "-1",
"filename": "NO FILE",
"progress": "0",
"data": str(payload),
"now": datetime.datetime.now().isoformat()
}
currentData = self._printer.getCurrentData()
if "currentZ" in currentData.keys() and currentData["currentZ"] is not None:
params["currentZ"] = str(currentData["currentZ"])
if "jobData" in currentData.keys() and currentData["jobData"] is not None:
params["filename"] = currentData["jobData"]["filename"]
if "progress" in currentData.keys() and currentData["progress"] is not None and currentData["jobData"]["lines"] is not None:
params["progress"] = str(round(currentData["progress"] * 100 / currentData["jobData"]["lines"]))
return command % params
class SystemCommandTrigger(CommandTrigger):
"""
Performs configured system commands for configured events.
"""
def __init__(self, printer):
CommandTrigger.__init__(self, "systemCommandTrigger", printer)
def executeCommand(self, command):
try:
command_string = self.doStringProcessing(command_string)
self.logger.info("Executing system command: %s" % 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)
self._logger.info("Executing system command: %s" % command)
subprocess.Popen(command, shell=True)
except subprocess.CalledProcessError, e:
self.logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
self._logger.warn("Command failed with return code %i: %s" % (e.returncode, e.message))
except Exception, ex:
self.logger.exception("Command failed")
self._logger.exception("Command failed")
class GcodeCommandTrigger(CommandTrigger):
"""
Sends configured GCODE commands to the printer for configured events.
"""
def __init__(self, printer):
CommandTrigger.__init__(self, "gcodeCommandTrigger", printer)
def executeCommand(self, command):
self._logger.debug("Executing GCode command: %s" % command)
self._printer.commands(command.split(","))

View File

@ -14,6 +14,7 @@ import octoprint.util.comm as comm
import octoprint.util as util
from octoprint.settings import settings
from octoprint.events import eventManager
def getConnectionOptions():
"""
@ -27,9 +28,8 @@ def getConnectionOptions():
}
class Printer():
def __init__(self, gcodeManager,eventManager):
def __init__(self, gcodeManager):
self._gcodeManager = gcodeManager
self._eventManager = eventManager
# state
self._temp = None
@ -124,7 +124,7 @@ class Printer():
try: callback.sendCurrentData(copy.deepcopy(data))
except: pass
#~~ printer commands
#~~ printer commands
def connect(self, port=None, baudrate=None):
"""
@ -134,7 +134,6 @@ 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):
"""
@ -143,6 +142,7 @@ class Printer():
if self._comm is not None:
self._comm.close()
self._comm = None
eventManager().fire("Disconnected")
def command(self, command):
"""
@ -196,8 +196,6 @@ class Printer():
self._setCurrentZ(-1)
#self._eventManager.fire('PrintStarted', self.filename)
self._comm.printGCode(self._gcodeList)
def togglePausePrint(self):
@ -225,6 +223,7 @@ class Printer():
# mark print as failure
if self._filename:
self._gcodeManager.printFailed(self._filename)
eventManager().fire("PrintFailed", self._filename)
#~~ state monitoring
@ -344,6 +343,9 @@ class Printer():
"ready": self.isReady()
}
def getCurrentData(self):
return self._stateMonitor.getCurrentData()
#~~ callbacks triggered from self._comm
def mcLog(self, message):
@ -368,24 +370,12 @@ 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.fire('PrintStarted', self._filename)
if state == self._comm.STATE_OPERATIONAL and (oldState <= self._comm.STATE_CONNECTING or oldState >=self._comm.STATE_CLOSED):
self._eventManager.fire('Connected',self._comm._port+" at " +self._comm._baudrate)
if state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR:
self._eventManager.fire('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.fire('PrintDone', self._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.fire('PrintFailed', self._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
@ -424,7 +414,7 @@ class Printer():
self.peakZ = newZ
if self._timelapse is not None:
self._timelapse.onZChange(oldZ, newZ)
self._eventManager.FireEvent ('ZChange',newZ)
eventManager().fire("ZChange", newZ)
self._setCurrentZ(newZ)
@ -442,10 +432,11 @@ class Printer():
self._setCurrentZ(None)
self._setProgressData(None, None, None)
self._gcodeLoader = None
self._eventManager.fire("LoadDone", filename)
self._stateMonitor.setGcodeData({"filename": None, "progress": None})
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
eventManager().fire("LoadDone", filename)
def _onGcodeLoadedToPrint(self, filename, gcodeList):
self._onGcodeLoaded(filename, gcodeList)
self.startPrint()

View File

@ -20,7 +20,6 @@ import octoprint.gcodefiles as gcodefiles
import octoprint.util as util
import octoprint.users as users
import octoprint.events as events
SUCCESS = {}
@ -32,7 +31,7 @@ app = Flask("octoprint")
printer = None
gcodeManager = None
userManager = None
eventManager =None
eventManager = None
principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
@ -41,7 +40,7 @@ user_permission = Permission(RoleNeed("user"))
#~~ Printer state
class PrinterStateConnection(tornadio2.SocketConnection):
def __init__(self, printer, gcodeManager, userManager, session, endpoint=None):
def __init__(self, printer, gcodeManager, userManager, eventManager, session, endpoint=None):
tornadio2.SocketConnection.__init__(self, session, endpoint)
self._logger = logging.getLogger(__name__)
@ -56,23 +55,24 @@ class PrinterStateConnection(tornadio2.SocketConnection):
self._printer = printer
self._gcodeManager = gcodeManager
self._userManager = userManager
self._eventManager = eventManager
def on_open(self, info):
global eventManager
eventManager.fire("ClientOpen")
self._logger.info("New connection from client")
# Use of global here is smelly
printer.registerCallback(self)
gcodeManager.registerCallback(self)
self._eventManager.fire("ClientOpened")
def on_close(self):
global eventManager
eventManager.fire("ClientClosed")
self._logger.info("Closed client connection")
# Use of global here is smelly
printer.unregisterCallback(self)
gcodeManager.unregisterCallback(self)
self._eventManager.fire("ClientClosed")
def on_message(self, message):
pass
@ -152,7 +152,6 @@ def connect():
printer.connect(port=port, baudrate=baudrate)
elif "command" in request.values.keys() and request.values["command"] == "disconnect":
printer.disconnect()
eventManager.fire("Disconnected")
return jsonify(SUCCESS)
@ -188,10 +187,8 @@ def printJobControl():
printer.startPrint()
elif request.values["command"] == "pause":
printer.togglePausePrint()
eventManager.fire("Paused")
elif request.values["command"] == "cancel":
printer.cancelPrint()
eventManager.fire("Cancelled")
return jsonify(SUCCESS)
@app.route(BASEURL + "control/temperature", methods=["POST"])
@ -281,8 +278,9 @@ def uploadGcodeFile():
if "gcode_file" in request.files.keys():
file = request.files["gcode_file"]
filename = gcodeManager.addFile(file)
global eventManager
eventManager.fire("Upload",filename)
eventManager.fire("Upload", filename)
return jsonify(files=gcodeManager.getAllFileData(), filename=filename)
@app.route(BASEURL + "gcodefiles/load", methods=["POST"])
@ -295,8 +293,9 @@ def loadGcodeFile():
filename = gcodeManager.getAbsolutePath(request.values["filename"])
if filename is not None:
printer.loadGcode(filename, printAfterLoading)
global eventManager
eventManager.fire("LoadStart",filename)
eventManager.fire("LoadStart", filename)
return jsonify(SUCCESS)
@app.route(BASEURL + "gcodefiles/delete", methods=["POST"])
@ -350,12 +349,11 @@ 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(eventManager)
lapse = timelapse.ZTimelapse()
elif "timed" == type:
interval = 10
if request.values.has_key("interval"):
@ -363,7 +361,7 @@ def setTimelapseConfig():
interval = int(request.values["interval"])
except ValueError:
pass
lapse = timelapse.TimedTimelapse( eventManager,interval)
lapse = timelapse.TimedTimelapse(interval)
printer.setTimelapse(lapse)
return getTimelapseData()
@ -671,16 +669,13 @@ class Server():
self._initLogging(self._debug)
logger = logging.getLogger(__name__)
eventManager = events.EventManager()
eventManager = events.eventManager()
gcodeManager = gcodefiles.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")
printer = Printer(gcodeManager)
# setup system and gcode command triggers
events.SystemCommandTrigger(printer)
events.GcodeCommandTrigger(printer)
if settings().getBoolean(["accessControl", "enabled"]):
userManagerName = settings().get(["accessControl", "userManager"])
@ -719,8 +714,8 @@ class Server():
IOLoop.instance().start()
def _createSocketConnection(self, session, endpoint=None):
global printer, gcodeManager, userManager
return PrinterStateConnection(printer, gcodeManager, userManager, session, endpoint)
global printer, gcodeManager, userManager, eventManager
return PrinterStateConnection(printer, gcodeManager, userManager, eventManager, session, endpoint)
def _initSettings(self, configfile, basedir):
s = settings(init=True, basedir=basedir, configfile=configfile)

View File

@ -68,13 +68,20 @@ default_settings = {
},
"controls": [],
"system": {
"actions": [],
"events": []
"actions": []
},
"accessControl": {
"enabled": False,
"userManager": "octoprint.users.FilebasedUserManager",
"userfile": None
},
"events": {
"systemCommandTrigger": {
"enabled": False
},
"gcodeCommandTrigger": {
"enabled": False
}
}
}

View File

@ -1,12 +1,9 @@
# coding=utf-8
import logging
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from octoprint.settings import settings
import octoprint.util as util
import logging
import os
import threading
import urllib
@ -14,10 +11,13 @@ import time
import subprocess
import fnmatch
import datetime
import octoprint.events as events
import sys
import octoprint.util as util
from octoprint.settings import settings
from octoprint.events import eventManager
def getFinishedTimelapses():
files = []
basedir = settings().getBaseFolder("timelapse")
@ -34,9 +34,8 @@ def getFinishedTimelapses():
return files
class Timelapse(object):
def __init__(self,ev):
def __init__(self):
self._logger = logging.getLogger(__name__)
self._eventManager = ev
self._imageNumber = None
self._inTimelapse = False
self._gcodeFile = None
@ -86,15 +85,15 @@ 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.fire("CaptureStart",filename);
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
captureThread.daemon = True
captureThread.start()
def _captureWorker(self, filename):
eventManager().fire("CaptureStart", filename);
urllib.urlretrieve(self._snapshotUrl, filename)
self._logger.debug("Image %s captured from %s" % (filename, self._snapshotUrl))
self._eventManager.fire("CaptureDone",filename);
eventManager().fire("CaptureDone", filename);
def _createMovie(self):
ffmpeg = settings().get(["webcam", "ffmpeg"])
@ -121,10 +120,10 @@ class Timelapse(object):
command.extend(['-vf', 'movie=%s [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]' % watermark])
# finalize command with output file
self._logger.debug("Rendering movie to %s" % output)
command.append(output)
subprocess.call(command)
self._logger.debug("Rendering movie to %s" % output)
self._eventManager.fire("MovieDone",output);
eventManager().fire("MovieDone", output);
def cleanCaptureDir(self):
if not os.path.isdir(self._captureDir):
@ -137,8 +136,8 @@ class Timelapse(object):
os.remove(os.path.join(self._captureDir, filename))
class ZTimelapse(Timelapse):
def __init__(self,ev):
Timelapse.__init__(self,ev)
def __init__(self):
Timelapse.__init__(self)
self._logger.debug("ZTimelapse initialized")
def onZChange(self, oldZ, newZ):
@ -146,8 +145,8 @@ class ZTimelapse(Timelapse):
self.captureImage()
class TimedTimelapse(Timelapse):
def __init__(self, ev,interval=1):
Timelapse.__init__(self,ev)
def __init__(self, interval=1):
Timelapse.__init__(self)
self._interval = interval
if self._interval < 1:

View File

@ -2,6 +2,8 @@
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import re
def getFormattedSize(num):
"""
Taken from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
@ -38,4 +40,7 @@ def getClass(name):
m = __import__(module)
for comp in parts[1:]:
m = getattr(m, comp)
return m
return m
def matchesGcode(line, gcode):
return re.search("^\s*%s\D" % gcode, line, re.I)

View File

@ -17,6 +17,7 @@ from octoprint.util.avr_isp import stk500v2
from octoprint.util.avr_isp import ispBase
from octoprint.settings import settings
from octoprint.events import eventManager
try:
import _winreg
@ -55,6 +56,21 @@ def baudrateList():
ret.insert(0, prev)
return ret
gcodeToEvent = {
"M226": "Waiting", # pause for user input
"M0": "Waiting",
"M1": "Waiting",
"M245": "Cooling", # part cooler
"M240": "Conveyor", # part conveyor
"M40": "Eject", # part ejector
"M300": "Alert", # user alert
"G28": "Home", # home print head
"M112": "EStop",
"M80": "PowerOn",
"M81": "PowerOff",
"M25": "Paused" # SD Card pause
}
class VirtualPrinter():
def __init__(self):
self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
@ -178,15 +194,11 @@ 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
@ -312,6 +324,7 @@ class MachineCom(object):
self._log("Failed to open serial port (%s)" % (self._port))
self._errorValue = 'Failed to autodetect serial port.'
self._changeState(self.STATE_ERROR)
eventManager().fire("Error", self.getErrorString())
return
self._log("Connected to: %s, starting monitor" % (self._serial))
if self._baudrate == 0:
@ -340,8 +353,12 @@ class MachineCom(object):
if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line or 'No Checksum with line number' in line:
pass
elif not self.isError():
if self.isPrinting():
eventManager().fire("PrintFailed")
self._errorValue = line[6:]
self._changeState(self.STATE_ERROR)
eventManager().fire("Error", self.getErrorString())
if ' T:' in line or line.startswith('T:'):
self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0))
if ' B:' in line:
@ -361,6 +378,7 @@ class MachineCom(object):
self.close()
self._errorValue = "No more baudrates to test, and no suitable baudrate found."
self._changeState(self.STATE_ERROR)
eventManager().fire("Error", self.getErrorString())
elif self._baudrateDetectRetry > 0:
self._baudrateDetectRetry -= 1
self._serial.write('\n')
@ -390,6 +408,7 @@ class MachineCom(object):
self._sendCommand("M999")
self._serial.timeout = 2
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
else:
self._testingBaudrate = False
elif self._state == self.STATE_CONNECTING:
@ -399,6 +418,7 @@ class MachineCom(object):
startSeen = True
elif 'ok' in line and startSeen:
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire("Connected", "%s at %s baud" % (self._port, self._baudrate))
elif time.time() > timeout:
self.close()
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
@ -458,6 +478,7 @@ class MachineCom(object):
return ret
def close(self, isError = False):
printing = self.isPrinting() or self.isPaused()
if self._serial != None:
self._serial.close()
if isError:
@ -465,53 +486,21 @@ class MachineCom(object):
else:
self._changeState(self.STATE_CLOSED)
self._serial = None
if printing:
eventManager().fire("PrintFailed")
eventManager().fire("Disconnected")
def __del__(self):
self.close()
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.fire ('Waiting')
# part cooler started
if re.search ("^\s*M245\D",t_cmd,re.I):
self._eventManager.fire ('Cooling')
# part conveyor started
if re.search ("^\s*M240\D",t_cmd,re.I):
self._eventManager.fire ('Conveyor')
# part ejector
if re.search ("^\s*M40\D",t_cmd,re.I):
self._eventManager.fire ('Eject')
# user alert issued by sending beep command to printer...
if re.search ("^\s*M300\D",t_cmd,re.I):
self._eventManager.fire ('Alert')
# Print head has moved to home
if re.search ("^\s*G28\D",t_cmd,re.I):
self._eventManager.fire ('Home')
if re.search ("^\s*M112\D",t_cmd,re.I):
self._eventManager.fire ('EStop')
if re.search ("^\s*M80\D",t_cmd,re.I):
self._eventManager.fire ('PowerOn')
if re.search ("^\s*M81\D",t_cmd,re.I):
self._eventManager.fire ('PowerOff')
if re.search ("^\s*M25\D",t_cmd,re.I): # SD Card pause
self._eventManager.fire ('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)
for gcode in gcodeToEvent.keys():
if gcode in cmd:
eventManager().fire(gcodeToEvent[gcode])
if 'M109' in cmd or 'M190' in cmd:
self._heatupWaitStartTime = time.time()
@ -544,7 +533,9 @@ class MachineCom(object):
def _sendNext(self):
if self._gcodePos >= len(self._gcodeList):
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire('PrintDone')
return
if self._gcodePos == 100:
self._printStartTime100 = time.time()
line = self._gcodeList[self._gcodePos]
@ -552,13 +543,9 @@ class MachineCom(object):
self._printSection = line[1]
line = line[0]
try:
if line == 'M0' or line == 'M1' or line=='M112': # M112 is also an LCD pause
if line == 'M0' or line == 'M1':
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.
# 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:
@ -591,11 +578,13 @@ class MachineCom(object):
self._printStartTime = time.time()
for i in xrange(0, 6):
self._sendNext()
eventManager().fire("PrintStarted")
def cancelPrint(self):
if self.isOperational():
self._changeState(self.STATE_OPERATIONAL)
eventManager().fire("PrintCancelled")
def setPause(self, pause):
if not pause and self.isPaused():
self._changeState(self.STATE_PRINTING)
@ -603,6 +592,7 @@ class MachineCom(object):
self._sendNext()
if pause and self.isPrinting():
self._changeState(self.STATE_PAUSED)
eventManager().fire("Paused")
def setFeedrateModifier(self, type, value):
self._feedRateModifier[type] = value