Also added error popup in such a case so the error will be known even if the terminal tab scrolled past it.
372 lines
17 KiB
JavaScript
372 lines
17 KiB
JavaScript
function DataUpdater(allViewModels) {
|
|
var self = this;
|
|
|
|
self.allViewModels = allViewModels;
|
|
|
|
self._socket = undefined;
|
|
self._autoReconnecting = false;
|
|
self._autoReconnectTrial = 0;
|
|
self._autoReconnectTimeouts = [0, 1, 1, 2, 3, 5, 8, 13, 20, 40, 100];
|
|
self._autoReconnectDialogIndex = 1;
|
|
|
|
self._pluginHash = undefined;
|
|
|
|
self._throttleFactor = 1;
|
|
self._baseProcessingLimit = 500.0;
|
|
self._lastProcessingTimes = [];
|
|
self._lastProcessingTimesSize = 20;
|
|
|
|
self.connect = function() {
|
|
var options = {};
|
|
if (SOCKJS_DEBUG) {
|
|
options["debug"] = true;
|
|
}
|
|
|
|
self._socket = new SockJS(SOCKJS_URI, undefined, options);
|
|
self._socket.onopen = self._onconnect;
|
|
self._socket.onclose = self._onclose;
|
|
self._socket.onmessage = self._onmessage;
|
|
};
|
|
|
|
self.reconnect = function() {
|
|
self._socket.close();
|
|
delete self._socket;
|
|
self.connect();
|
|
};
|
|
|
|
self.increaseThrottle = function() {
|
|
self.setThrottle(self._throttleFactor + 1);
|
|
};
|
|
|
|
self.decreaseThrottle = function() {
|
|
if (self._throttleFactor <= 1) {
|
|
return;
|
|
}
|
|
self.setThrottle(self._throttleFactor - 1);
|
|
};
|
|
|
|
self.setThrottle = function(throttle) {
|
|
self._throttleFactor = throttle;
|
|
|
|
self._send("throttle", self._throttleFactor);
|
|
log.debug("DataUpdater: New SockJS throttle factor:", self._throttleFactor, " new processing limit:", self._baseProcessingLimit * self._throttleFactor);
|
|
};
|
|
|
|
self._send = function(message, data) {
|
|
var payload = {};
|
|
payload[message] = data;
|
|
self._socket.send(JSON.stringify(payload));
|
|
};
|
|
|
|
self._onconnect = function() {
|
|
self._autoReconnecting = false;
|
|
self._autoReconnectTrial = 0;
|
|
};
|
|
|
|
self._onclose = function(e) {
|
|
if (e.code == SOCKJS_CLOSE_NORMAL) {
|
|
return;
|
|
}
|
|
if (self._autoReconnectTrial >= self._autoReconnectDialogIndex) {
|
|
// Only consider it a real disconnect if the trial number has exceeded our threshold.
|
|
|
|
var handled = false;
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (handled == true) {
|
|
return;
|
|
}
|
|
|
|
if (viewModel.hasOwnProperty("onServerDisconnect")) {
|
|
var result = viewModel.onServerDisconnect();
|
|
if (result !== undefined && !result) {
|
|
handled = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (handled) {
|
|
return;
|
|
}
|
|
|
|
showOfflineOverlay(
|
|
gettext("Server is offline"),
|
|
gettext("The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect automatically <strong>over the next couple of minutes</strong>, however you are welcome to try a manual reconnect anytime using the button below."),
|
|
self.reconnect
|
|
);
|
|
}
|
|
|
|
if (self._autoReconnectTrial < self._autoReconnectTimeouts.length) {
|
|
var timeout = self._autoReconnectTimeouts[self._autoReconnectTrial];
|
|
log.info("Reconnect trial #" + self._autoReconnectTrial + ", waiting " + timeout + "s");
|
|
setTimeout(self.reconnect, timeout * 1000);
|
|
self._autoReconnectTrial++;
|
|
} else {
|
|
self._onreconnectfailed();
|
|
}
|
|
};
|
|
|
|
self._onreconnectfailed = function() {
|
|
var handled = false;
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (handled == true) {
|
|
return;
|
|
}
|
|
|
|
if (viewModel.hasOwnProperty("onServerDisconnect")) {
|
|
var result = viewModel.onServerDisconnect();
|
|
if (result !== undefined && !result) {
|
|
handled = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (handled) {
|
|
return;
|
|
}
|
|
|
|
$("#offline_overlay_title").text(gettext("Server is offline"));
|
|
$("#offline_overlay_message").html(gettext("The server appears to be offline, at least I'm not getting any response from it. I <strong>could not reconnect automatically</strong>, but you may try a manual reconnect using the button below."));
|
|
};
|
|
|
|
self._onmessage = function(e) {
|
|
for (var prop in e.data) {
|
|
if (!e.data.hasOwnProperty(prop)) {
|
|
continue;
|
|
}
|
|
|
|
var data = e.data[prop];
|
|
|
|
var gcodeUploadProgress = $("#gcode_upload_progress");
|
|
var gcodeUploadProgressBar = $(".bar", gcodeUploadProgress);
|
|
|
|
var start = new Date().getTime();
|
|
switch (prop) {
|
|
case "connected": {
|
|
// update the current UI API key and send it with any request
|
|
UI_API_KEY = data["apikey"];
|
|
$.ajaxSetup({
|
|
headers: {"X-Api-Key": UI_API_KEY}
|
|
});
|
|
|
|
var oldVersion = VERSION;
|
|
VERSION = data["version"];
|
|
DISPLAY_VERSION = data["display_version"];
|
|
BRANCH = data["branch"];
|
|
$("span.version").text(DISPLAY_VERSION);
|
|
|
|
var oldPluginHash = self._pluginHash;
|
|
self._pluginHash = data["plugin_hash"];
|
|
|
|
if ($("#offline_overlay").is(":visible")) {
|
|
hideOfflineOverlay();
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onServerReconnect")) {
|
|
viewModel.onServerReconnect();
|
|
} else if (viewModel.hasOwnProperty("onDataUpdaterReconnect")) {
|
|
viewModel.onDataUpdaterReconnect();
|
|
}
|
|
});
|
|
|
|
if ($('#tabs li[class="active"] a').attr("href") == "#control") {
|
|
$("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime());
|
|
}
|
|
} else {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onServerConnect")) {
|
|
viewModel.onServerConnect();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (oldVersion != VERSION || (oldPluginHash != undefined && oldPluginHash != self._pluginHash)) {
|
|
showReloadOverlay();
|
|
}
|
|
|
|
self.setThrottle(1);
|
|
|
|
break;
|
|
}
|
|
case "history": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("fromHistoryData")) {
|
|
viewModel.fromHistoryData(data);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "current": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("fromCurrentData")) {
|
|
viewModel.fromCurrentData(data);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "slicingProgress": {
|
|
gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: Math.round(data["progress"])}));
|
|
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onSlicingProgress")) {
|
|
viewModel.onSlicingProgress(data["slicer"], data["model_path"], data["machinecode_path"], data["progress"]);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "event": {
|
|
var type = data["type"];
|
|
var payload = data["payload"];
|
|
var html = "";
|
|
var format = {};
|
|
|
|
log.debug("Got event " + type + " with payload: " + JSON.stringify(payload));
|
|
|
|
if (type == "MovieRendering") {
|
|
new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)});
|
|
} else if (type == "MovieDone") {
|
|
new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)});
|
|
} else if (type == "MovieFailed") {
|
|
html = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failed with return code %(returncode)s"), payload) + "</p>";
|
|
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + payload.error + '</pre>');
|
|
new PNotify({
|
|
title: gettext("Rendering failed"),
|
|
text: html,
|
|
type: "error",
|
|
hide: false
|
|
});
|
|
} else if (type == "PostRollStart") {
|
|
var title = gettext("Capturing timelapse postroll");
|
|
|
|
var text;
|
|
if (!payload.postroll_duration) {
|
|
text = _.sprintf(gettext("Now capturing timelapse post roll, this will take only a moment..."), format);
|
|
} else {
|
|
if (payload.postroll_duration > 60) {
|
|
format = {duration: _.sprintf(gettext("%(minutes)d min"), {minutes: payload.postroll_duration / 60})};
|
|
} else {
|
|
format = {duration: _.sprintf(gettext("%(seconds)d sec"), {seconds: payload.postroll_duration})};
|
|
}
|
|
text = _.sprintf(gettext("Now capturing timelapse post roll, this will take approximately %(duration)s..."), format);
|
|
}
|
|
|
|
new PNotify({
|
|
title: title,
|
|
text: text
|
|
});
|
|
} else if (type == "SlicingStarted") {
|
|
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
|
gcodeUploadProgressBar.css("width", "100%");
|
|
if (payload.progressAvailable) {
|
|
gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0}));
|
|
} else {
|
|
gcodeUploadProgressBar.text(gettext("Slicing ..."));
|
|
}
|
|
} else if (type == "SlicingDone") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload), type: "success"});
|
|
} else if (type == "SlicingCancelled") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
} else if (type == "SlicingFailed") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
|
|
html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload);
|
|
new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false});
|
|
} else if (type == "TransferStarted") {
|
|
gcodeUploadProgress.addClass("progress-striped").addClass("active");
|
|
gcodeUploadProgressBar.css("width", "100%");
|
|
gcodeUploadProgressBar.text(gettext("Streaming ..."));
|
|
} else if (type == "TransferDone") {
|
|
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
|
|
gcodeUploadProgressBar.css("width", "0%");
|
|
gcodeUploadProgressBar.text("");
|
|
new PNotify({
|
|
title: gettext("Streaming done"),
|
|
text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload),
|
|
type: "success"
|
|
});
|
|
} else if (type == "PrintCancelled") {
|
|
if (payload.firmwareError) {
|
|
new PNotify({
|
|
title: gettext("Unhandled firmware error"),
|
|
text: _.sprintf(gettext("The firmware reported an unhandled error. Due to that the ongoing print job was cancelled. Error: %(firmwareError)s"), payload),
|
|
type: "error",
|
|
hide: false
|
|
});
|
|
}
|
|
} else if (type == "Error") {
|
|
new PNotify({
|
|
title: gettext("Unhandled firmware error"),
|
|
text: _.sprintf(gettext("The firmware reported an unhandled error. Due to that OctoPrint disconnected. Error: %(error)s"), payload),
|
|
type: "error",
|
|
hide: false
|
|
});
|
|
}
|
|
|
|
var legacyEventHandlers = {
|
|
"UpdatedFiles": "onUpdatedFiles",
|
|
"MetadataStatisticsUpdated": "onMetadataStatisticsUpdated",
|
|
"MetadataAnalysisFinished": "onMetadataAnalysisFinished",
|
|
"SlicingDone": "onSlicingDone",
|
|
"SlicingCancelled": "onSlicingCancelled",
|
|
"SlicingFailed": "onSlicingFailed"
|
|
};
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onEvent" + type)) {
|
|
viewModel["onEvent" + type](payload);
|
|
} else if (legacyEventHandlers.hasOwnProperty(type) && viewModel.hasOwnProperty(legacyEventHandlers[type])) {
|
|
// there might still be code that uses the old callbacks, make sure those still get called
|
|
// but log a warning
|
|
log.warn("View model " + viewModel.name + " is using legacy event handler " + legacyEventHandlers[type] + ", new handler is called " + legacyEventHandlers[type]);
|
|
viewModel[legacyEventHandlers[type]](payload);
|
|
}
|
|
});
|
|
|
|
break;
|
|
}
|
|
case "timelapse": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("fromTimelapseData")) {
|
|
viewModel.fromTimelapseData(data);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "plugin": {
|
|
_.each(self.allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onDataUpdaterPluginMessage")) {
|
|
viewModel.onDataUpdaterPluginMessage(data.plugin, data.data);
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var end = new Date().getTime();
|
|
var difference = end - start;
|
|
|
|
while (self._lastProcessingTimes.length >= self._lastProcessingTimesSize) {
|
|
self._lastProcessingTimes.shift();
|
|
}
|
|
self._lastProcessingTimes.push(difference);
|
|
|
|
var processingLimit = self._throttleFactor * self._baseProcessingLimit;
|
|
if (difference > processingLimit) {
|
|
self.increaseThrottle();
|
|
log.debug("We are slow (" + difference + " > " + processingLimit + "), reducing refresh rate");
|
|
} else if (self._throttleFactor > 1) {
|
|
var maxProcessingTime = Math.max.apply(null, self._lastProcessingTimes);
|
|
var lowerProcessingLimit = (self._throttleFactor - 1) * self._baseProcessingLimit;
|
|
if (maxProcessingTime < lowerProcessingLimit) {
|
|
self.decreaseThrottle();
|
|
log.debug("We are fast (" + maxProcessingTime + " < " + lowerProcessingLimit + "), increasing refresh rate");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
self.connect();
|
|
}
|