Revamped the events slightly

Payload data can now be properly injected into event handlers such as command triggers. Added a couple of new events to use for update triggers to the frontend instead of custom code, further decoupling the application. Movie rendering now also causes a frontend notification.
This commit is contained in:
Gina Häußge 2013-12-02 17:04:00 +01:00
parent 9f7fc73441
commit 0b662b7211
22 changed files with 118 additions and 104 deletions

View file

@ -135,7 +135,8 @@ class DebugEventListener(GenericEventListener):
events = ["Startup", "Connected", "Disconnected", "ClientOpen", "ClientClosed", "PowerOn", "PowerOff", "Upload",
"FileSelected", "TransferStarted", "TransferDone", "PrintStarted", "PrintDone", "PrintFailed",
"Cancelled", "Home", "ZChange", "Paused", "Waiting", "Cooling", "Alert", "Conveyor", "Eject",
"CaptureStart", "CaptureDone", "MovieDone", "EStop", "Error"]
"CaptureStart", "CaptureDone", "MovieRendering", "MovieDone", "MovieFailed", "EStop", "Error",
"SlicingStarted", "SlicingDone", "SlicingFailed", "UpdatedFiles"]
self.subscribe(events)
def eventCallback(self, event, payload):
@ -191,8 +192,11 @@ class CommandTrigger(GenericEventListener):
return
for command in self._subscriptions[event]:
processedCommand = self._processCommand(command, payload)
self.executeCommand(processedCommand)
try:
processedCommand = self._processCommand(command, payload)
self.executeCommand(processedCommand)
except KeyError:
self._logger.warn("There was an error processing one or more placeholders in the following command: %s" % command)
def executeCommand(self, command):
"""
@ -206,33 +210,39 @@ class CommandTrigger(GenericEventListener):
The following substitutions are currently supported:
- %(currentZ)s : current Z position of the print head, or -1 if not available
- %(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
- {__currentZ} : current Z position of the print head, or -1 if not available
- {__filename} : current selected filename, or "NO FILE" if no file is selected
- {__progress} : current print progress in percent, 0 if no print is in progress
- {__data} : the string representation of the event's payload
- {__now} : ISO 8601 representation of the current date and time
Additionally, the keys of the event's payload can also be used as placeholder.
"""
params = {
"currentZ": "-1",
"filename": "NO FILE",
"progress": "0",
"data": str(payload),
"now": datetime.datetime.now().isoformat()
"__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"])
params["__currentZ"] = str(currentData["currentZ"])
if "job" in currentData.keys() and currentData["job"] is not None:
params["filename"] = currentData["job"]["filename"]
params["__filename"] = currentData["job"]["filename"]
if "progress" in currentData.keys() and currentData["progress"] is not None \
and "progress" in currentData["progress"].keys() and currentData["progress"]["progress"] is not None:
params["progress"] = str(round(currentData["progress"]["progress"] * 100))
params["__progress"] = str(round(currentData["progress"]["progress"] * 100))
return command % params
# now add the payload keys as well
if isinstance(payload, dict):
params.update(payload)
return command.format(**params)
class SystemCommandTrigger(CommandTrigger):
"""

View file

@ -121,6 +121,7 @@ class GcodeManager:
self._metadata[basename] = metadata
self._metadataDirty = True
self._saveMetadata()
eventManager().fire("MetadataAnalysisFinished", {"filename": basename, "result": analysisResult})
def _loadMetadata(self):
if os.path.exists(self._metadataFile) and os.path.isfile(self._metadataFile):
@ -141,7 +142,6 @@ class GcodeManager:
util.safeRename(self._metadataTempFile, self._metadataFile)
self._loadMetadata()
self._sendUpdateTrigger("gcodeFiles")
def _getBasicFilename(self, filename):
if filename.startswith(self._uploadFolder):
@ -160,7 +160,7 @@ class GcodeManager:
def _sendUpdateTrigger(self, type):
for callback in self._callbacks:
try: callback.sendUpdateTrigger(type)
try: callback.sendEvent(type)
except: pass
#~~ file handling

View file

@ -129,7 +129,7 @@ class Printer():
def _sendTriggerUpdateCallbacks(self, type):
for callback in self._callbacks:
try: callback.sendUpdateTrigger(type)
try: callback.sendEvent(type)
except: pass
def _sendFeedbackCommandOutput(self, name, output):
@ -436,7 +436,7 @@ class Printer():
self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
def mcSdFiles(self, files):
self._sendTriggerUpdateCallbacks("gcodeFiles")
eventManager().fire("UpdatedFiles", {"type": "gcode", "files": files})
self._sdFilelistAvailable.set()
def mcFileSelected(self, filename, filesize, sd):

View file

@ -4,7 +4,7 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
from flask import request, jsonify, make_response
from octoprint.settings import settings, valid_boolean_trues
from octoprint.settings import settings
from octoprint.printer import getConnectionOptions
from octoprint.server import printer, restricted_access, SUCCESS
from octoprint.server.ajax import ajax

View file

@ -83,6 +83,9 @@ def api_access(func):
class PrinterStateConnection(SockJSConnection):
EVENTS = ["UpdatedFiles", "MetadataAnalysisFinished", "MovieRendering", "MovieDone",
"MovieFailed", "SlicingStarted", "SlicingDone", "SlicingFailed"]
def __init__(self, printer, gcodeManager, userManager, eventManager, session):
SockJSConnection.__init__(self, session)
@ -113,10 +116,8 @@ class PrinterStateConnection(SockJSConnection):
octoprint.timelapse.registerCallback(self)
self._eventManager.fire("ClientOpened")
self._eventManager.subscribe("MovieDone", self._onMovieDone)
self._eventManager.subscribe("SlicingStarted", self._onSlicingStarted)
self._eventManager.subscribe("SlicingDone", self._onSlicingDone)
self._eventManager.subscribe("SlicingFailed", self._onSlicingFailed)
for event in PrinterStateConnection.EVENTS:
self._eventManager.subscribe(event, self._onEvent)
octoprint.timelapse.notifyCallbacks(octoprint.timelapse.current)
@ -127,10 +128,8 @@ class PrinterStateConnection(SockJSConnection):
octoprint.timelapse.unregisterCallback(self)
self._eventManager.fire("ClientClosed")
self._eventManager.unsubscribe("MovieDone", self._onMovieDone)
self._eventManager.unsubscribe("SlicingStarted", self._onSlicingStarted)
self._eventManager.unsubscribe("SlicingDone", self._onSlicingDone)
self._eventManager.unsubscribe("SlicingFailed", self._onSlicingFailed)
for event in PrinterStateConnection.EVENTS:
self._eventManager.unsubscribe(event, self._onEvent)
def on_message(self, message):
pass
@ -150,17 +149,17 @@ class PrinterStateConnection(SockJSConnection):
self._messageBacklog = []
data.update({
"temperatures": temperatures,
"logs": logs,
"messages": messages
"temperatures": temperatures,
"logs": logs,
"messages": messages
})
self._emit("current", data)
def sendHistoryData(self, data):
self._emit("history", data)
def sendUpdateTrigger(self, type, payload=None):
self._emit("updateTrigger", {"type": type, "payload": payload})
def sendEvent(self, type, payload=None):
self._emit("event", {"type": type, "payload": payload})
def sendFeedbackCommandOutput(self, name, output):
self._emit("feedbackCommandOutput", {"name": name, "output": output})
@ -180,17 +179,8 @@ class PrinterStateConnection(SockJSConnection):
with self._temperatureBacklogMutex:
self._temperatureBacklog.append(data)
def _onMovieDone(self, event, payload):
self.sendUpdateTrigger("timelapseFiles")
def _onSlicingStarted(self, event, payload):
self.sendUpdateTrigger("slicingStarted", payload)
def _onSlicingDone(self, event, payload):
self.sendUpdateTrigger("slicingDone", payload)
def _onSlicingFailed(self, event, payload):
self.sendUpdateTrigger("slicingFailed", payload)
def _onEvent(self, event, payload):
self.sendEvent(event, payload)
def _emit(self, type, payload):
self.send({type: payload})

View file

@ -134,7 +134,7 @@ default_settings = {
}
}
valid_boolean_trues = ["true", "yes", "y", "1"]
valid_boolean_trues = [True, "true", "yes", "y", "1"]
class Settings(object):

View file

@ -102,27 +102,36 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
self.gcodeFilesViewModel.fromCurrentData(data);
break;
}
case "updateTrigger": {
case "event": {
var type = data["type"];
var payload = data["payload"];
if (type == "gcodeFiles") {
var gcodeUploadProgress = $("#gcode_upload_progress");
var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress);
if ((type == "UpdatedFiles" && payload.type == "gcode") || type == "MetadataAnalysisFinished") {
gcodeFilesViewModel.requestData();
} else if (type == "timelapseFiles") {
} else if (type == "MovieRendering") {
$.pnotify({title: "Rendering timelapse", text: "Now rendering timelapse " + payload.movie_basename});
} else if (type == "MovieDone") {
$.pnotify({title: "Timelapse ready", text: "New timelapse " + payload.movie_basename + " is done rendering."});
timelapseViewModel.requestData();
} else if (type == "slicingStarted") {
$("#gcode_upload_progress .bar").css("width", "100%");
$("#gcode_upload_progress").addClass("progress-striped").addClass("active");
$("#gcode_upload_progress .bar").text("Slicing ...");
} else if (type == "slicingDone") {
$("#gcode_upload_progress .bar").css("width", "0%");
$("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
$("#gcode_upload_progress .bar").text("");
} else if (type == "MovieFailed") {
$.pnotify({title: "Rendering failed", text: "Rendering of timelapse " + payload.movie_basename + " failed, return code " + payload.returncode, type: "error"});
} else if (type == "SlicingStarted") {
gcodeUploadProgress.addClass("progress-striped").addClass("active");
gcodeUploadProgressBar.css("width", "100%");
gcodeUploadProgressBar.text("Slicing ...");
} else if (type == "SlicingDone") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
$.pnotify({title: "Slicing done", text: "Sliced " + payload.stl + " to " + payload.gcode + ", took " + payload.time + " seconds"});
gcodeFilesViewModel.requestData(payload.gcode);
} else if (type == "slicingFailed") {
$("#gcode_upload_progress .bar").css("width", "0%");
$("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
$("#gcode_upload_progress .bar").text("");
} else if (type == "SlicingFailed") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
$.pnotify({title: "Slicing failed", text: "Could not slice " + payload.stl + " to " + payload.gcode + ": " + payload.reason, type: "error"});
}
break;

View file

@ -107,7 +107,7 @@ $(function() {
function enable_local_dropzone() {
$("#gcode_upload").fileupload({
url: AJAX_BASEURL + "gcodefiles/local",
url: API_BASEURL + "gcodefiles/local",
dataType: "json",
dropZone: localTarget,
done: gcode_upload_done,
@ -118,7 +118,7 @@ $(function() {
function disable_local_dropzone() {
$("#gcode_upload").fileupload({
url: AJAX_BASEURL + "gcodefiles/local",
url: API_BASEURL + "gcodefiles/local",
dataType: "json",
dropZone: null,
done: gcode_upload_done,
@ -129,7 +129,7 @@ $(function() {
function enable_sd_dropzone() {
$("#gcode_upload_sd").fileupload({
url: AJAX_BASEURL + "gcodefiles/sdcard",
url: API_BASEURL + "gcodefiles/sdcard",
dataType: "json",
dropZone: $("#drop_sd"),
done: gcode_upload_done,
@ -140,7 +140,7 @@ $(function() {
function disable_sd_dropzone() {
$("#gcode_upload_sd").fileupload({
url: AJAX_BASEURL + "gcodefiles/sdcard",
url: API_BASEURL + "gcodefiles/sdcard",
dataType: "json",
dropZone: null,
formData: {target: "sd"},

View file

@ -30,7 +30,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/connection",
url: API_BASEURL + "control/connection",
method: "GET",
dataType: "json",
success: function(response) {
@ -100,7 +100,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
data["save"] = true;
$.ajax({
url: AJAX_BASEURL + "control/connection",
url: API_BASEURL + "control/connection",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -112,7 +112,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
} else {
self.requestData();
$.ajax({
url: AJAX_BASEURL + "control/connection",
url: API_BASEURL + "control/connection",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -43,7 +43,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "control/custom",
url: API_BASEURL + "control/custom",
method: "GET",
dataType: "json",
success: function(response) {
@ -90,7 +90,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
data[axis] = distance * multiplier;
$.ajax({
url: AJAX_BASEURL + "control/printer/printhead",
url: API_BASEURL + "control/printer/printhead",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -105,7 +105,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
}
$.ajax({
url: AJAX_BASEURL + "control/printer/printhead",
url: API_BASEURL + "control/printer/printhead",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -127,7 +127,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
length = 5;
$.ajax({
url: AJAX_BASEURL + "control/printer/feeder",
url: API_BASEURL + "control/printer/feeder",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -160,7 +160,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
return;
$.ajax({
url: AJAX_BASEURL + "control/printer/command",
url: API_BASEURL + "control/printer/command",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -54,7 +54,7 @@ function FirstRunViewModel() {
self._sendData = function(data, callback) {
$.ajax({
url: AJAX_BASEURL + "setup",
url: API_BASEURL + "setup",
type: "POST",
dataType: "json",
data: data,

View file

@ -98,7 +98,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
self.requestData = function(filenameOverride) {
$.ajax({
url: AJAX_BASEURL + "gcodefiles",
url: API_BASEURL + "gcodefiles",
method: "GET",
dataType: "json",
success: function(response) {
@ -135,7 +135,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
}
$.ajax({
url: AJAX_BASEURL + "gcodefiles/" + origin + "/" + filename,
url: API_BASEURL + "gcodefiles/" + origin + "/" + filename,
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -155,7 +155,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
}
$.ajax({
url: AJAX_BASEURL + "gcodefiles/" + origin + "/" + filename,
url: API_BASEURL + "gcodefiles/" + origin + "/" + filename,
type: "DELETE",
success: self.fromResponse
})
@ -175,7 +175,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
self._sendSdCommand = function(command) {
$.ajax({
url: AJAX_BASEURL + "control/sd",
url: API_BASEURL + "control/sd",
type: "POST",
dataType: "json",
data: {command: command}

View file

@ -24,7 +24,7 @@ function LoginStateViewModel() {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "login",
url: API_BASEURL + "login",
type: "POST",
data: {"passive": true},
success: self.fromResponse
@ -63,7 +63,7 @@ function LoginStateViewModel() {
$("#login_remember").prop("checked", false);
$.ajax({
url: AJAX_BASEURL + "login",
url: API_BASEURL + "login",
type: "POST",
data: {"user": username, "pass": password, "remember": remember},
success: function(response) {
@ -78,7 +78,7 @@ function LoginStateViewModel() {
self.logout = function() {
$.ajax({
url: AJAX_BASEURL + "logout",
url: API_BASEURL + "logout",
type: "POST",
success: function(response) {
$.pnotify({title: "Logout successful", text: "You are now logged out", type: "success"});

View file

@ -9,7 +9,7 @@ function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsV
self.triggerAction = function(action) {
var callback = function() {
$.ajax({
url: AJAX_BASEURL + "system",
url: API_BASEURL + "system",
type: "POST",
dataType: "json",
data: "action=" + action.action,
@ -22,14 +22,17 @@ function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsV
})
}
if (action.confirm) {
$("#confirmation_dialog .confirmation_dialog_message").text(action.confirm);
$("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click");
$("#confirmation_dialog .confirmation_dialog_acknowledge").bind("click", function(e) {
var confirmationDialog = $("#confirmation_dialog");
var confirmationDialogAck = $(".confirmation_dialog_acknowledge", confirmationDialog);
$(".confirmation_dialog_message", confirmationDialog).text(action.confirm);
confirmationDialogAck.unbind("click");
confirmationDialogAck.bind("click", function(e) {
e.preventDefault();
$("#confirmation_dialog").modal("hide");
callback();
});
$("#confirmation_dialog").modal("show");
confirmationDialog.modal("show");
} else {
callback();
}

View file

@ -144,7 +144,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self._jobCommand = function(command) {
$.ajax({
url: AJAX_BASEURL + "control/job",
url: API_BASEURL + "control/job",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -102,7 +102,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "settings",
url: API_BASEURL + "settings",
type: "GET",
dataType: "json",
success: self.fromResponse
@ -227,7 +227,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
}
$.ajax({
url: AJAX_BASEURL + "settings",
url: API_BASEURL + "settings",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -217,7 +217,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
data[group][type] = parseInt(temp);
$.ajax({
url: AJAX_BASEURL + "control/printer/hotend",
url: API_BASEURL + "control/printer/hotend",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -99,7 +99,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
if (command) {
$.ajax({
url: AJAX_BASEURL + "control/printer/command",
url: API_BASEURL + "control/printer/command",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -68,7 +68,7 @@ function TimelapseViewModel(loginStateViewModel) {
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "timelapse",
url: API_BASEURL + "timelapse",
type: "GET",
dataType: "json",
success: self.fromResponse
@ -109,7 +109,7 @@ function TimelapseViewModel(loginStateViewModel) {
self.removeFile = function(filename) {
$.ajax({
url: AJAX_BASEURL + "timelapse/" + filename,
url: API_BASEURL + "timelapse/" + filename,
type: "DELETE",
dataType: "json",
success: self.requestData
@ -127,7 +127,7 @@ function TimelapseViewModel(loginStateViewModel) {
}
$.ajax({
url: AJAX_BASEURL + "timelapse",
url: API_BASEURL + "timelapse",
type: "POST",
dataType: "json",
data: data,

View file

@ -56,7 +56,7 @@ function UsersViewModel(loginStateViewModel) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users",
url: API_BASEURL + "users",
type: "GET",
dataType: "json",
success: self.fromResponse
@ -153,7 +153,7 @@ function UsersViewModel(loginStateViewModel) {
if (user === undefined) return;
$.ajax({
url: AJAX_BASEURL + "users",
url: API_BASEURL + "users",
type: "POST",
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(user),
@ -175,7 +175,7 @@ function UsersViewModel(loginStateViewModel) {
}
$.ajax({
url: AJAX_BASEURL + "users/" + user.name,
url: API_BASEURL + "users/" + user.name,
type: "DELETE",
success: function(response) {
self.fromResponse(response);
@ -189,7 +189,7 @@ function UsersViewModel(loginStateViewModel) {
if (user === undefined) return;
$.ajax({
url: AJAX_BASEURL + "users/" + user.name,
url: API_BASEURL + "users/" + user.name,
type: "PUT",
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(user),
@ -204,7 +204,7 @@ function UsersViewModel(loginStateViewModel) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users/" + username + "/password",
url: API_BASEURL + "users/" + username + "/password",
type: "PUT",
contentType: "application/json; charset=UTF-8",
data: JSON.stringify({password: password}),
@ -216,7 +216,7 @@ function UsersViewModel(loginStateViewModel) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users/" + username + "/apikey",
url: API_BASEURL + "users/" + username + "/apikey",
type: "POST",
success: callback
});
@ -226,7 +226,7 @@ function UsersViewModel(loginStateViewModel) {
if (!CONFIG_ACCESS_CONTROL) return;
$.ajax({
url: AJAX_BASEURL + "users/" + username + "/apikey",
url: API_BASEURL + "users/" + username + "/apikey",
type: "DELETE",
success: callback
});

View file

@ -18,7 +18,7 @@
<script lang="javascript">
var BASEURL = "{{ url_for('index') }}";
var AJAX_BASEURL = BASEURL + "ajax/";
var API_BASEURL = BASEURL + "ajax/";
var CONFIG_GCODEFILESPERPAGE = 5;
var CONFIG_TIMELAPSEFILESPERPAGE = 10;

View file

@ -252,11 +252,13 @@ class Timelapse(object):
# finalize command with output file
self._logger.debug("Rendering movie to %s" % output)
command.append(output)
eventManager().fire("MovieRendering", {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
try:
subprocess.check_call(command)
eventManager().fire("MovieDone", output)
eventManager().fire("MovieDone", {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output)})
except subprocess.CalledProcessError as (e):
self._logger.warn("Could not render movie, got return code %r" % e.returncode)
eventManager().fire("MovieFailed", {"gcode": self._gcodeFile, "movie": output, "movie_basename": os.path.basename(output), "returncode": e.returncode})
def cleanCaptureDir(self):
if not os.path.isdir(self._captureDir):