From 908d39ad3972073de39179cd74236310828f70e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 1 Jan 2014 02:39:35 +0100 Subject: [PATCH] Refining temperature control REST API, also added support for multi extrusion while at it --- src/octoprint/gcodefiles.py | 4 +- src/octoprint/printer.py | 130 ++++--- src/octoprint/server/api/printer.py | 228 ++++++++---- src/octoprint/server/api/settings.py | 4 +- src/octoprint/server/util.py | 2 +- src/octoprint/settings.py | 6 +- src/octoprint/static/css/octoprint.less | 2 +- src/octoprint/static/js/app/helpers.js | 18 +- .../static/js/app/viewmodels/control.js | 68 +++- .../static/js/app/viewmodels/files.js | 4 +- .../static/js/app/viewmodels/settings.js | 5 +- .../static/js/app/viewmodels/temperature.js | 346 +++++++++++------- src/octoprint/templates/index.jinja2 | 251 +++++-------- src/octoprint/templates/settings.jinja2 | 6 + src/octoprint/util/comm.py | 134 +++++-- src/octoprint/util/virtual.py | 66 +++- 16 files changed, 804 insertions(+), 470 deletions(-) diff --git a/src/octoprint/gcodefiles.py b/src/octoprint/gcodefiles.py index c9bed3ae..32b6d9df 100644 --- a/src/octoprint/gcodefiles.py +++ b/src/octoprint/gcodefiles.py @@ -144,8 +144,8 @@ class GcodeManager: def _migrateMetadata(self): self._logger.info("Migrating metadata if necessary...") - printTimeRe = r"(\d+):(\d{2}):(\d{2})" - filamentRe = r"(\d*\.\d+)m(\s/\s(\d*\.\d+)cm.)?" + printTimeRe = re.compile("(\d+):(\d{2}):(\d{2})") + filamentRe = re.compile("(\d*\.\d+)m(\s/\s(\d*\.\d+)cm.)?") hoursToSeconds = 60 * 60 minutesToSeconds = 60 diff --git a/src/octoprint/printer.py b/src/octoprint/printer.py index cb195d9b..fe728fac 100644 --- a/src/octoprint/printer.py +++ b/src/octoprint/printer.py @@ -41,12 +41,7 @@ class Printer(): self._bedTemp = None self._targetTemp = None self._targetBedTemp = None - self._temps = { - "actual": deque([], 300), - "target": deque([], 300), - "actualBed": deque([], 300), - "targetBed": deque([], 300) - } + self._temps = deque([], 300) self._tempBacklog = [] self._latestMessage = None @@ -193,12 +188,57 @@ class Printer(): for command in commands: self._comm.sendCommand(command) - def setTemperatureOffset(self, extruder, bed): + def jog(self, axis, amount): + movementSpeed = settings().get(["printerParameters", "movementSpeed", ["x", "y", "z"]], asdict=True) + self.commands(["G91", "G1 %s%.4f F%d" % (axis.upper(), amount, movementSpeed[axis]), "G90"]) + + def home(self, axes): + self.commands(["G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), axes)), "G90"]) + + def extrude(self, amount): + extrusionSpeed = settings().get(["printerParameters", "movementSpeed", "e"]) + self.commands(["G91", "G1 E%s F%d" % (amount, extrusionSpeed), "G90"]) + + def changeTool(self, tool): + try: + toolNum = int(tool[len("tool"):]) + self.command("T%d" % toolNum) + except ValueError: + pass + + def setTemperature(self, type, value): + if type.startswith("tool"): + try: + toolNum = int(type[len("tool"):]) + self.command("M104 T%d S%f" % (toolNum, value)) + except ValueError: + pass + elif type == "bed": + self.command("M140 S%f" % value) + + def setTemperatureOffset(self, offsets={}): if self._comm is None: return - self._comm.setTemperatureOffset(extruder, bed) - self._stateMonitor.setTempOffsets(extruder, bed) + tool, bed = self._comm.getOffsets() + + validatedOffsets = {} + + for key in offsets: + value = offsets[key] + if key == "bed": + bed = value + validatedOffsets[key] = value + elif key.startswith("tool"): + try: + toolNum = int(key[len("tool"):]) + tool[toolNum] = value + validatedOffsets[key] = value + except ValueError: + pass + + self._comm.setTemperatureOffset(tool, bed) + self._stateMonitor.setTempOffsets(validatedOffsets) def selectFile(self, filename, sd, printAfterSelect=False): if self._comm is None or (self._comm.isBusy() or self._comm.isStreaming()): @@ -297,20 +337,28 @@ class Printer(): "printTimeLeft": int(self._printTimeLeft * 60) if self._printTimeLeft is not None else None }) - def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): + def _addTemperatureData(self, temp, bedTemp): currentTimeUtc = int(time.time()) - self._temps["actual"].append((currentTimeUtc, temp)) - self._temps["target"].append((currentTimeUtc, targetTemp)) - self._temps["actualBed"].append((currentTimeUtc, bedTemp)) - self._temps["targetBed"].append((currentTimeUtc, bedTargetTemp)) + data = { + "time": currentTimeUtc + } + for tool in temp.keys(): + data["tool%d" % tool] = { + "actual": temp[tool][0], + "target": temp[tool][1] + } + data["bed"] = { + "actual": bedTemp[0], + "target": bedTemp[1] + } + + self._temps.append(data) self._temp = temp self._bedTemp = bedTemp - self._targetTemp = targetTemp - self._targetBedTemp = bedTargetTemp - self._stateMonitor.addTemperature({"currentTime": currentTimeUtc, "temp": self._temp, "bedTemp": self._bedTemp, "targetTemp": self._targetTemp, "targetBedTemp": self._targetBedTemp}) + self._stateMonitor.addTemperature(data) def _setJobData(self, filename, filesize, sd): if filename is not None: @@ -352,10 +400,8 @@ class Printer(): def _sendInitialStateUpdate(self, callback): try: data = self._stateMonitor.getCurrentData() - # convert the dict of deques to a dict of lists - temps = {k: list(v) for (k,v) in self._temps.iteritems()} data.update({ - "temperatureHistory": temps, + "tempHistory": list(self._temps), "logHistory": list(self._log), "messageHistory": list(self._messages) }) @@ -387,8 +433,8 @@ class Printer(): """ self._addLog(message) - def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): - self._addTemperatureData(temp, bedTemp, targetTemp, bedTargetTemp) + def mcTempUpdate(self, temp, bedTemp): + self._addTemperatureData(temp, bedTemp) def mcStateChange(self, state): """ @@ -546,24 +592,26 @@ class Printer(): def getCurrentTemperatures(self): if self._comm is not None: - (tempOffset, bedTempOffset) = self._comm.getOffsets() + tempOffset, bedTempOffset = self._comm.getOffsets() else: - tempOffset = 0 - bedTempOffset = 0 + tempOffset = {} + bedTempOffset = None - return { - "extruder": { - "current": self._temp, - "target": self._targetTemp, - "offset": tempOffset - }, - "bed": { - "current": self._bedTemp, - "target": self._targetBedTemp, - "offset": bedTempOffset + result = {} + for tool in self._temp.keys(): + result["tool%d" % tool] = { + "actual": self._temp[tool][0], + "target": self._temp[tool][1], + "offset": tempOffset[tool] if tool in tempOffset.keys() and tempOffset[tool] is not None else 0 } + result["bed"] = { + "actual": self._bedTemp[0], + "target": self._bedTemp[1], + "offset": bedTempOffset } + return result + def getTemperatureHistory(self): return self._temps @@ -616,8 +664,7 @@ class StateMonitor(object): self._currentZ = None self._progress = None - self._tempOffset = 0 - self._bedTempOffset = 0 + self._offsets = {} self._changeEvent = threading.Event() @@ -660,11 +707,8 @@ class StateMonitor(object): self._progress = progress self._changeEvent.set() - def setTempOffsets(self, tempOffset, bedTempOffset): - if tempOffset is not None: - self._tempOffset = tempOffset - if bedTempOffset is not None: - self._bedTempOffset = bedTempOffset + def setTempOffsets(self, offsets): + self._offsets = offsets self._changeEvent.set() def _work(self): @@ -688,6 +732,6 @@ class StateMonitor(object): "job": self._jobData, "currentZ": self._currentZ, "progress": self._progress, - "offsets": (self._tempOffset, self._bedTempOffset) + "offsets": self._offsets } diff --git a/src/octoprint/server/api/printer.py b/src/octoprint/server/api/printer.py index 66e5a663..547f0422 100644 --- a/src/octoprint/server/api/printer.py +++ b/src/octoprint/server/api/printer.py @@ -3,51 +3,73 @@ __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' from flask import request, jsonify, make_response +import re -from octoprint.settings import settings +from octoprint.settings import settings, valid_boolean_trues from octoprint.server import printer, restricted_access, NO_CONTENT from octoprint.server.api import api import octoprint.util as util +#~~ Printer -#~~ Heater +@api.route("/printer", methods=["GET"]) +def printerState(): + if not printer.isOperational(): + return make_response("Printer is not operational", 409) + + result = {} + result.update(_getTemperatureData(lambda x: x)) + + return jsonify(result) -@api.route("/printer/heater", methods=["POST"]) +#~~ Tool + + +@api.route("/printer/tool", methods=["POST"]) @restricted_access -def controlPrinterHotend(): +def printerToolCommand(): if not printer.isOperational(): return make_response("Printer is not operational", 409) valid_commands = { - "temp": ["temps"], - "offset": ["offsets"] + "select": ["tool"], + "target": ["targets"], + "offset": ["offsets"], + "extrude": ["amount"] } command, data, response = util.getJsonCommandFromRequest(request, valid_commands) if response is not None: return response - valid_targets = ["hotend", "bed"] + validation_regex = re.compile("tool\d+") + + ##~~ tool selection + if command == "select": + tool = data["tool"] + if re.match(validation_regex, tool) is None: + return make_response("Invalid tool: %s" % tool, 400) + if not tool.startswith("tool"): + return make_response("Invalid tool for selection: %s" % tool, 400) + + printer.changeTool(tool) ##~~ temperature - if command == "temp": - temps = data["temps"] + elif command == "target": + targets = data["targets"] # make sure the targets are valid and the values are numbers validated_values = {} - for type, value in temps.iteritems(): - if not type in valid_targets: - return make_response("Invalid target for setting temperature: %s" % type, 400) + for tool, value in targets.iteritems(): + if re.match(validation_regex, tool) is None: + return make_response("Invalid target for setting temperature: %s" % tool, 400) if not isinstance(value, (int, long, float)): - return make_response("Not a number for %s: %r" % (type, value), 400) - validated_values[type] = value + return make_response("Not a number for %s: %r" % (tool, value), 400) + validated_values[tool] = value # perform the actual temperature commands - # TODO make this a generic method call (printer.setTemperature(type, value)) to get rid of gcode here - if "hotend" in validated_values: - printer.command("M104 S%f" % validated_values["hotend"]) - if "bed" in validated_values: - printer.command("M140 S%f" % validated_values["bed"]) + for tool in validated_values.keys(): + printer.setTemperature(tool, validated_values[tool]) ##~~ temperature offset elif command == "offset": @@ -55,32 +77,107 @@ def controlPrinterHotend(): # make sure the targets are valid, the values are numbers and in the range [-50, 50] validated_values = {} - for type, value in offsets.iteritems(): - if not type in valid_targets: - return make_response("Invalid target for setting temperature: %s" % type, 400) + for tool, value in offsets.iteritems(): + if re.match(validation_regex, tool) is None: + return make_response("Invalid target for setting temperature: %s" % tool, 400) if not isinstance(value, (int, long, float)): - return make_response("Not a number for %s: %r" % (type, value), 400) + return make_response("Not a number for %s: %r" % (tool, value), 400) if not -50 <= value <= 50: - return make_response("Offset %s not in range [-50, 50]: %f" % (type, value), 400) - validated_values[type] = value + return make_response("Offset %s not in range [-50, 50]: %f" % (tool, value), 400) + validated_values[tool] = value # set the offsets - if "hotend" in validated_values and "bed" in validated_values: - printer.setTemperatureOffset(validated_values["hotend"], validated_values["bed"]) - elif "hotend" in validated_values: - printer.setTemperatureOffset(validated_values["hotend"], None) - elif "bed" in validated_values: - printer.setTemperatureOffset(None, validated_values["bed"]) + printer.setTemperatureOffset(validated_values) + + ##~~ extrusion + elif command == "extrude": + if printer.isPrinting(): + # do not extrude when a print job is running + return make_response("Printer is currently printing", 409) + + amount = data["amount"] + if not isinstance(amount, (int, long, float)): + return make_response("Not a number for extrusion amount: %r" % amount, 400) + printer.extrude(amount) return NO_CONTENT +@api.route("/printer/tool", methods=["GET"]) +def printerToolState(): + def deleteBed(x): + data = dict(x) + + if "bed" in data.keys(): + del data["bed"] + return data + + return jsonify(_getTemperatureData(deleteBed)) + + +##~~ Heated bed + + +@api.route("/printer/bed", methods=["POST"]) +@restricted_access +def printerBedCommand(): + if not printer.isOperational(): + return make_response("Printer is not operational", 409) + + valid_commands = { + "target": ["target"], + "offset": ["offset"] + } + command, data, response = util.getJsonCommandFromRequest(request, valid_commands) + if response is not None: + return response + + ##~~ temperature + if command == "target": + target = data["target"] + + # make sure the target is a number + if not isinstance(target, (int, long, float)): + return make_response("Not a number: %r" % target, 400) + + # perform the actual temperature command + printer.setTemperature("bed", target) + + ##~~ temperature offset + elif command == "offset": + offset = data["offsets"] + + # make sure the offset is valid + if not isinstance(offset, (int, long, float)): + return make_response("Not a number: %r" % offset, 400) + if not -50 <= offset <= 50: + return make_response("Offset not in range [-50, 50]: %f" % offset, 400) + + # set the offsets + printer.setTemperatureOffset({"bed": offset}) + + return NO_CONTENT + + +@api.route("/printer/bed", methods=["GET"]) +def printerBedState(): + def deleteTools(x): + data = dict(x) + + for k in data.keys(): + if k.startswith("tool"): + del data[k] + return data + + return jsonify(_getTemperatureData(deleteTools)) + + ##~~ Print head @api.route("/printer/printhead", methods=["POST"]) @restricted_access -def controlPrinterPrinthead(): +def printerPrintheadCommand(): if not printer.isOperational() or printer.isPrinting(): # do not jog when a print job is running or we don't have a connection return make_response("Printer is not operational or currently printing", 409) @@ -93,8 +190,6 @@ def controlPrinterPrinthead(): if response is not None: return response - movementSpeed = settings().get(["printerParameters", "movementSpeed", ["x", "y", "z"]], asdict=True) - valid_axes = ["x", "y", "z"] ##~~ jog command if command == "jog": @@ -109,8 +204,7 @@ def controlPrinterPrinthead(): # execute the jog commands for axis, value in validated_values.iteritems(): - # TODO make this a generic method call (printer.jog(axis, value)) to get rid of gcode here - printer.commands(["G91", "G1 %s%.4f F%d" % (axis.upper(), value, movementSpeed[axis]), "G90"]) + printer.jog(axis, value) ##~~ home command elif command == "home": @@ -122,38 +216,7 @@ def controlPrinterPrinthead(): validated_values.append(axis) # execute the home command - # TODO make this a generic method call (printer.home(axis, ...)) to get rid of gcode here - printer.commands(["G91", "G28 %s" % " ".join(map(lambda x: "%s0" % x.upper(), validated_values)), "G90"]) - - return NO_CONTENT - - -##~~ Feeder - - -@api.route("/printer/feeder", methods=["POST"]) -@restricted_access -def controlPrinterFeeder(): - if not printer.isOperational() or printer.isPrinting(): - # do not jog when a print job is running or we don't have a connection - return make_response("Printer is not operational or currently printing", 409) - - valid_commands = { - "extrude": ["amount"] - } - command, data, response = util.getJsonCommandFromRequest(request, valid_commands) - if response is not None: - return response - - extrusionSpeed = settings().get(["printerParameters", "movementSpeed", "e"]) - - if command == "extrude": - amount = data["amount"] - if not isinstance(amount, (int, long, float)): - return make_response("Not a number for extrusion amount: %r" % amount, 400) - - # TODO make this a generic method call (printer.extruder([hotend,] amount)) to get rid of gcode here - printer.commands(["G91", "G1 E%s F%d" % (data["amount"], extrusionSpeed), "G90"]) + printer.home(validated_values) return NO_CONTENT @@ -163,7 +226,7 @@ def controlPrinterFeeder(): @api.route("/printer/sd", methods=["POST"]) @restricted_access -def sdCommand(): +def printerSdCommand(): if not settings().getBoolean(["feature", "sdSupport"]): return make_response("SD support is disabled", 404) @@ -190,7 +253,7 @@ def sdCommand(): @api.route("/printer/sd", methods=["GET"]) -def sdState(): +def printerSdState(): if not settings().getBoolean(["feature", "sdSupport"]): return make_response("SD support is disabled", 404) @@ -238,3 +301,28 @@ def getCustomControls(): return jsonify(controls=customControls) +def _getTemperatureData(filter): + if not printer.isOperational(): + return make_response("Printer is not operational", 409) + + tempData = printer.getCurrentTemperatures() + result = { + "temps": filter(tempData) + } + + if "history" in request.values.keys() and request.values["history"] in valid_boolean_trues: + tempHistory = printer.getTemperatureHistory() + + limit = 300 + if "limit" in request.values.keys() and unicode(request.values["limit"]).isnumeric(): + limit = int(request.values["limit"]) + + history = list(tempHistory) + limit = min(limit, len(history)) + + result.update({ + "history": map(lambda x: filter(x), history[-limit:]) + }) + + return result + diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 3203e827..d4c0e15b 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -39,7 +39,8 @@ def getSettings(): "movementSpeedY": movementSpeedY, "movementSpeedZ": movementSpeedZ, "movementSpeedE": movementSpeedE, - "invertAxes": s.get(["printerParameters", "invertAxes"]) + "invertAxes": s.get(["printerParameters", "invertAxes"]), + "numExtruders": s.get(["printerParameters", "numExtruders"]) }, "webcam": { "streamUrl": s.get(["webcam", "stream"]), @@ -113,6 +114,7 @@ def setSettings(): if "movementSpeedZ" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "z"], data["printer"]["movementSpeedZ"]) if "movementSpeedE" in data["printer"].keys(): s.setInt(["printerParameters", "movementSpeed", "e"], data["printer"]["movementSpeedE"]) if "invertAxes" in data["printer"].keys(): s.set(["printerParameters", "invertAxes"], data["printer"]["invertAxes"]) + if "numExtruders" in data["printer"].keys(): s.setInt(["printerParameters", "numExtruders"], data["printer"]["numExtruders"]) if "webcam" in data.keys(): if "streamUrl" in data["webcam"].keys(): s.set(["webcam", "stream"], data["webcam"]["streamUrl"]) diff --git a/src/octoprint/server/util.py b/src/octoprint/server/util.py index befa1285..3ff9389d 100644 --- a/src/octoprint/server/util.py +++ b/src/octoprint/server/util.py @@ -160,7 +160,7 @@ class PrinterStateConnection(SockJSConnection): self._messageBacklog = [] data.update({ - "temperatures": temperatures, + "temps": temperatures, "logs": logs, "messages": messages }) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 741ae963..8c9d4c3b 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -85,7 +85,8 @@ default_settings = { "e": 300 }, "pauseTriggers": [], - "invertAxes": [] + "invertAxes": [], + "numExtruders": 1 }, "appearance": { "name": "", @@ -129,7 +130,8 @@ default_settings = { "enabled": False, "okAfterResend": False, "forceChecksum": False, - "okWithLinenumber": False + "okWithLinenumber": False, + "numExtruders": 1 } } } diff --git a/src/octoprint/static/css/octoprint.less b/src/octoprint/static/css/octoprint.less index 065d2156..04918094 100644 --- a/src/octoprint/static/css/octoprint.less +++ b/src/octoprint/static/css/octoprint.less @@ -437,7 +437,7 @@ ul.dropdown-menu li a { margin-bottom: 10px; } - .btn-group > .btn { + .btn-group.distance > .btn { width: 43px; padding: 3px 0; height: 30px; diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 9663a419..a04072ae 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -279,6 +279,8 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor } function formatSize(bytes) { + if (!bytes) return "-"; + var units = ["bytes", "KB", "MB", "GB"]; for (var i = 0; i < units.length; i++) { if (bytes < 1024) { @@ -290,6 +292,8 @@ function formatSize(bytes) { } function formatDuration(seconds) { + if (!seconds) return "-"; + var s = seconds % 60; var m = (seconds % 3600) / 60; var h = seconds / 3600; @@ -298,13 +302,25 @@ function formatDuration(seconds) { } function formatDate(unixTimestamp) { + if (!unixTimestamp) return "-"; return moment.unix(unixTimestamp).format("YYYY-MM-DD HH:mm"); } function formatFilament(filament) { + if (!filament || !filament["length"]) return "-"; var result = _.sprintf("%.02fm", (filament["length"] / 1000)); - if (filament.hasOwnProperty("volume")) { + if (filament.hasOwnProperty("volume") && filament.volume) { result += " / " + _.sprintf("%.02fcm³", filament["volume"]); } return result; } + +function cleanTemperature(temp) { + if (!temp || temp < 10) return "off"; + return temp; +} + +function formatTemperature(temp) { + if (!temp || temp < 10) return "off"; + return _.sprintf("%.1f°C", temp); +} diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js index 286f299e..6fc921ad 100644 --- a/src/octoprint/static/js/app/viewmodels/control.js +++ b/src/octoprint/static/js/app/viewmodels/control.js @@ -4,6 +4,13 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) { self.loginState = loginStateViewModel; self.settings = settingsViewModel; + self._createToolEntry = function() { + return { + name: ko.observable(), + key: ko.observable() + } + }; + self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); @@ -15,8 +22,31 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) { self.extrusionAmount = ko.observable(undefined); self.controls = ko.observableArray([]); + self.tools = ko.observableArray([]); + self.feedbackControlLookup = {}; + self.settings.printer_numExtruders.subscribe(function(oldVal, newVal) { + var tools = []; + + var numExtruders = self.settings.printer_numExtruders(); + if (numExtruders > 1) { + // multiple extruders + for (var extruder = 0; extruder < numExtruders; extruder++) { + tools[extruder] = self._createToolEntry(); + tools[extruder]["name"]("Tool " + extruder); + tools[extruder]["key"]("tool" + extruder); + } + } else { + // only one extruder, no need to add numbers + tools[0] = self._createToolEntry(); + tools[0]["name"]("Hotend"); + tools[0]["key"]("tool0"); + } + + self.tools(tools); + }); + self.fromCurrentData = function(data) { self._processStateData(data.state); } @@ -115,25 +145,49 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) { self.sendExtrudeCommand = function() { self._sendECommand(1); - } + }; self.sendRetractCommand = function() { self._sendECommand(-1); - } + }; self._sendECommand = function(dir) { var length = self.extrusionAmount(); - if (!length) - length = 5; + if (!length) length = 5; + + var data = { + command: "extrude", + amount: length * dir + }; $.ajax({ - url: API_BASEURL + "printer/feeder", + url: API_BASEURL + "printer/tool", type: "POST", dataType: "json", contentType: "application/json; charset=UTF-8", - data: JSON.stringify({command: "extrude", amount: (dir * length)}) + data: JSON.stringify(data), + success: function() { + self.extrusionAmount(""); + } }); - } + }; + + self.sendSelectToolCommand = function(data) { + if (!data || !data.key()) return; + + var data = { + command: "select", + tool: data.key() + } + + $.ajax({ + url: API_BASEURL + "printer/tool", + type: "POST", + dataType: "json", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify(data) + }); + }; self.sendCustomCommand = function(command) { if (!command) diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index ce110556..8daae2fd 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -115,8 +115,8 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) { self.fromResponse = function(response, filenameToFocus, locationToFocus) { var files = response.files; _.each(files, function(element, index, list) { - if (!element.hasOwnProperty("size")) element.size = "n/a"; - if (!element.hasOwnProperty("date")) element.date = "n/a"; + if (!element.hasOwnProperty("size")) element.size = undefined; + if (!element.hasOwnProperty("date")) element.date = undefined; }); self.listHelper.updateItems(files); diff --git a/src/octoprint/static/js/app/viewmodels/settings.js b/src/octoprint/static/js/app/viewmodels/settings.js index 3a7a66e4..30413b79 100644 --- a/src/octoprint/static/js/app/viewmodels/settings.js +++ b/src/octoprint/static/js/app/viewmodels/settings.js @@ -18,6 +18,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.printer_movementSpeedZ = ko.observable(undefined); self.printer_movementSpeedE = ko.observable(undefined); self.printer_invertAxes = ko.observable(undefined); + self.printer_numExtruders = ko.observable(undefined); self.webcam_streamUrl = ko.observable(undefined); self.webcam_snapshotUrl = ko.observable(undefined); @@ -121,6 +122,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.printer_movementSpeedZ(response.printer.movementSpeedZ); self.printer_movementSpeedE(response.printer.movementSpeedE); self.printer_invertAxes(response.printer.invertAxes); + self.printer_numExtruders(response.printer.numExtruders); self.webcam_streamUrl(response.webcam.streamUrl); self.webcam_snapshotUrl(response.webcam.snapshotUrl); @@ -178,7 +180,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { "movementSpeedY": self.printer_movementSpeedY(), "movementSpeedZ": self.printer_movementSpeedZ(), "movementSpeedE": self.printer_movementSpeedE(), - "invertAxes": self.printer_invertAxes() + "invertAxes": self.printer_invertAxes(), + "numExtruders": self.printer_numExtruders() }, "webcam": { "streamUrl": self.webcam_streamUrl(), diff --git a/src/octoprint/static/js/app/viewmodels/temperature.js b/src/octoprint/static/js/app/viewmodels/temperature.js index c962ad61..c5367c9b 100644 --- a/src/octoprint/static/js/app/viewmodels/temperature.js +++ b/src/octoprint/static/js/app/viewmodels/temperature.js @@ -2,19 +2,24 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) { var self = this; self.loginState = loginStateViewModel; + self.settingsViewModel = settingsViewModel; - self.temp = ko.observable(undefined); - self.bedTemp = ko.observable(undefined); - self.targetTemp = ko.observable(undefined); - self.bedTargetTemp = ko.observable(undefined); + self._createToolEntry = function() { + return { + name: ko.observable(), + key: ko.observable(), + actual: ko.observable(0), + target: ko.observable(0), + offset: ko.observable(0), + newTarget: ko.observable(), + newOffset: ko.observable() + } + }; - self.newTemp = ko.observable(undefined); - self.newBedTemp = ko.observable(undefined); - - self.newTempOffset = ko.observable(undefined); - self.tempOffset = ko.observable(0); - self.newBedTempOffset = ko.observable(undefined); - self.bedTempOffset = ko.observable(0); + self.tools = ko.observableArray([]); + self.bedTemp = self._createToolEntry(); + self.bedTemp["name"]("Bed"); + self.bedTemp["key"]("bed"); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); @@ -26,25 +31,46 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) { self.temperature_profiles = settingsViewModel.temperature_profiles; - self.tempString = ko.computed(function() { - if (!self.temp()) - return "-"; - return self.temp() + " °C"; - }); - self.bedTempString = ko.computed(function() { - if (!self.bedTemp()) - return "-"; - return self.bedTemp() + " °C"; - }); - self.targetTempString = ko.computed(function() { - if (!self.targetTemp()) - return "-"; - return self.targetTemp() + " °C"; - }); - self.bedTargetTempString = ko.computed(function() { - if (!self.bedTargetTemp()) - return "-"; - return self.bedTargetTemp() + " °C"; + self.heaterOptions = ko.observable(undefined); + + self.settingsViewModel.printer_numExtruders.subscribe(function(oldVal, newVal) { + var graphColors = ["red", "orange", "green", "brown", "purple"]; + var heaterOptions = {}; + var tools = self.tools(); + + // tools + var numExtruders = self.settingsViewModel.printer_numExtruders(); + if (numExtruders > 1) { + // multiple extruders + for (var extruder = 0; extruder < numExtruders; extruder++) { + var color = graphColors.shift(); + if (!color) color = "black"; + heaterOptions["tool" + extruder] = {name: "T" + extruder, color: color}; + + if (tools.length <= extruder || !tools[extruder]) { + tools[extruder] = self._createToolEntry(); + } + tools[extruder]["name"]("Tool " + extruder); + tools[extruder]["key"]("tool" + extruder); + } + } else { + // only one extruder, no need to add numbers + var color = graphColors[0]; + heaterOptions["tool0"] = {name: "T", color: color}; + + if (tools.length < 1 || !tools[0]) { + tools[0] = self._createToolEntry(); + } + tools[0]["name"]("Hotend"); + tools[0]["key"]("tool0"); + } + + // print bed + heaterOptions["bed"] = {name: "Bed", color: "blue"}; + + // write back + self.heaterOptions(heaterOptions); + self.tools(tools); }); self.temperatures = []; @@ -76,19 +102,21 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) { } }, legend: { - noColumns: 4 + position: "sw", + noColumns: 2, + backgroundOpacity: 0 } } self.fromCurrentData = function(data) { self._processStateData(data.state); - self._processTemperatureUpdateData(data.temperatures); + self._processTemperatureUpdateData(data.temps); self._processOffsetData(data.offsets); } self.fromHistoryData = function(data) { self._processStateData(data.state); - self._processTemperatureHistoryData(data.temperatureHistory); + self._processTemperatureHistoryData(data.tempHistory); self._processOffsetData(data.offsets); } @@ -106,144 +134,204 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) { if (data.length == 0) return; - self.temp(data[data.length - 1].temp); - self.bedTemp(data[data.length - 1].bedTemp); - self.targetTemp(data[data.length - 1].targetTemp); - self.bedTargetTemp(data[data.length - 1].targetBedTemp); + var lastData = data[data.length - 1]; + + var tools = self.tools(); + for (var i = 0; i < tools.length; i++) { + if (lastData.hasOwnProperty("tool" + i)) { + tools[i]["actual"](lastData["tool" + i].actual); + tools[i]["target"](lastData["tool" + i].target); + } + } + + self.bedTemp["actual"](lastData.bed.actual); + self.bedTemp["target"](lastData.bed.target); if (!CONFIG_TEMPERATURE_GRAPH) return; - if (!self.temperatures) - self.temperatures = []; - if (!self.temperatures.actual) - self.temperatures.actual = []; - if (!self.temperatures.target) - self.temperatures.target = []; - if (!self.temperatures.actualBed) - self.temperatures.actualBed = []; - if (!self.temperatures.targetBed) - self.temperatures.targetBed = []; - - _.each(data, function(d) { - var time = d.currentTime; - self.temperatures.actual.push([time * 1000, d.temp]); - self.temperatures.target.push([time * 1000, d.targetTemp]); - self.temperatures.actualBed.push([time * 1000, d.bedTemp]); - self.temperatures.targetBed.push([time * 1000, d.targetBedTemp]); + self.temperatures = self._processTemperatureData(data, self.temperatures); + _.each(_.keys(self.heaterOptions()), function(d) { + self.temperatures[d].actual = self.temperatures[d].actual.slice(-300); + self.temperatures[d].target = self.temperatures[d].target.slice(-300); }); - self.temperatures.actual = self.temperatures.actual.slice(-300); - self.temperatures.target = self.temperatures.target.slice(-300); - self.temperatures.actualBed = self.temperatures.actualBed.slice(-300); - self.temperatures.targetBed = self.temperatures.targetBed.slice(-300); - self.updatePlot(); } self._processTemperatureHistoryData = function(data) { - var toJsTimestamp = function(d) { - return [d[0] * 1000, d[1]]; - } - - var processedData = { - actual: _.map(data.actual, toJsTimestamp), - target: _.map(data.target, toJsTimestamp), - actualBed: _.map(data.actualBed, toJsTimestamp), - targetBed: _.map(data.targetBed, toJsTimestamp) - }; - self.temperatures = processedData; + self.temperatures = self._processTemperatureData(data); self.updatePlot(); } self._processOffsetData = function(data) { - self.tempOffset(data[0]); - self.bedTempOffset(data[1]); + var tools = self.tools(); + for (var i = 0; i < tools.length; i++) { + if (data.hasOwnProperty("tool" + i)) { + tools[i]["offset"](data["tool" + i]); + } + } + + if (data.hasOwnProperty("bed")) { + self.bedTemp["offset"](data["bed"]); + } + } + + self._processTemperatureData = function(data, result) { + var types = _.keys(self.heaterOptions()); + + // make sure result is properly initialized + if (!result) { + result = {}; + } + _.each(types, function(type) { + if (!result.hasOwnProperty(type)) { + result[type] = {actual: [], target: []}; + } + if (!result[type].hasOwnProperty("actual")) result[type]["actual"] = []; + if (!result[type].hasOwnProperty("target")) result[type]["target"] = []; + }); + + // convert data + _.each(data, function(d) { + var time = d.time * 1000; + _.each(types, function(type) { + if (!d[type]) return; + result[type].actual.push([time, d[type].actual]); + result[type].target.push([time, d[type].target]); + }) + }); + + return result; } self.updatePlot = function() { var graph = $("#temperature-graph"); if (graph.length) { - var data = [ - {label: "Actual", color: "#FF4040", data: self.temperatures.actual}, - {label: "Target", color: "#FFA0A0", data: self.temperatures.target}, - {label: "Bed Actual", color: "#4040FF", data: self.temperatures.actualBed}, - {label: "Bed Target", color: "#A0A0FF", data: self.temperatures.targetBed} - ] + var data = []; + var heaterOptions = self.heaterOptions(); + _.each(_.keys(heaterOptions), function(type) { + var actuals = []; + var targets = []; + + if (self.temperatures[type]) { + actuals = self.temperatures[type].actual; + targets = self.temperatures[type].target; + } + + var actualTemp = actuals && actuals.length ? formatTemperature(actuals[actuals.length - 1][1]) : "-"; + var targetTemp = targets && targets.length ? formatTemperature(targets[targets.length - 1][1]) : "-"; + + data.push({ + label: "Actual " + heaterOptions[type].name + ": " + actualTemp, + color: heaterOptions[type].color, + data: actuals + }); + data.push({ + label: "Target " + heaterOptions[type].name + ": " + targetTemp, + color: pusher.color(heaterOptions[type].color).tint(0.5).html(), + data: targets + }); + }); $.plot(graph, data, self.plotOptions); } } - self.setTempFromProfile = function(profile) { - if (!profile) - return; - self._sendHotendCommand("temp", "hotend", profile.extruder); - } + self.setTarget = function(item) { + var value = item.newTarget(); + if (!value) return; - self.setTemp = function() { - self._sendHotendCommand("temp", "hotend", self.newTemp(), function() {self.targetTemp(self.newTemp()); self.newTemp("");}); + self._sendToolCommand("target", + item.key(), + item.newTarget(), + function() {item.newTarget("");} + ); }; - self.setTempToZero = function() { - self._sendHotendCommand("temp", "hotend", 0, function() {self.targetTemp(0); self.newTemp("");}); - } + self.setTargetFromProfile = function(item, profile) { + if (!profile) return; - self.setTempOffset = function() { - self._sendHotendCommand("offset", "hotend", self.newTempOffset(), function() {self.tempOffset(self.newTempOffset()); self.newTempOffset("");}); - } - - self.setBedTempFromProfile = function(profile) { - if (!profile) - return; - self._sendHotendCommand("temp", "bed", profile.bed); - } - - self.setBedTemp = function() { - self._sendHotendCommand("temp", "bed", self.newBedTemp(), function() {self.bedTargetTemp(self.newBedTemp()); self.newBedTemp("");}); - }; - - self.setBedTempToZero = function() { - self._sendHotendCommand("temp", "bed", 0, function() {self.bedTargetTemp(0); self.newBedTemp("");}); - } - - self.setBedTempOffset = function() { - self._sendHotendCommand("offset", "bed", self.newBedTempOffset(), function() {self.bedTempOffset(self.newBedTempOffset()); self.newBedTempOffset("");}); - } - - self._sendHotendCommand = function(command, type, temp, callback) { - var group; - if ("temp" == command) { - group = "temps"; - } else if ("offset" == command) { - group = "offsets"; + var value = undefined; + if (item.key() == "bed") { + value = profile.bed; } else { - return; + value = profile.extruder; } + self._sendToolCommand("target", + item.key(), + value, + function() {item.newTarget("");} + ); + }; + + self.setTargetToZero = function(item) { + self._sendToolCommand("target", + item.key(), + 0, + function() {item.newTarget("");} + ); + }; + + self.setOffset = function(item) { + self._sendToolCommand("offset", + item.key(), + item.newOffset(), + function() {item.newOffset("");} + ); + }; + + self._sendToolCommand = function(command, type, temp, successCb, errorCb) { var data = { - "command": command + command: command }; - data[group] = {}; - data[group][type] = parseInt(temp); + + var endpoint; + if (type == "bed") { + if ("target" == command) { + data["target"] = parseInt(temp); + } else if ("offset" == command) { + data["offset"] = parseInt(temp); + } else { + return; + } + + endpoint = "bed"; + } else { + var group; + if ("target" == command) { + group = "targets"; + } else if ("offset" == command) { + group = "offsets"; + } else { + return; + } + data[group] = {}; + data[group][type] = parseInt(temp); + + endpoint = "tool"; + } $.ajax({ - url: API_BASEURL + "printer/heater", + url: API_BASEURL + "printer/" + endpoint, type: "POST", dataType: "json", contentType: "application/json; charset=UTF-8", data: JSON.stringify(data), - success: function() { if (callback !== undefined) callback(); } + success: function() { if (successCb !== undefined) successCb(); }, + error: function() { if (errorCb !== undefined) errorCb(); } }); - } + }; - self.handleEnter = function(event, type) { + self.handleEnter = function(event, type, item) { if (event.keyCode == 13) { - if (type == "temp") { - self.setTemp(); - } else if (type == "bedTemp") { - self.setBedTemp(); + if (type == "target") { + self.setTarget(item); + } else if (type == "offset") { + self.setOffset(item); } } - } + }; + } diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index e6a5e5a5..f9a3c242 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -253,87 +253,55 @@
{% endif %} -
-
-

Temperature

+
- + + + + + + + + + + + +
ActualTargetOffset
- - -
- +
@@ -375,32 +343,41 @@
- - - - + + + +
@@ -446,86 +423,34 @@
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-

-
-
-
-
- -
-
-

-
-
-
-
- -
-
- Show non-extrusion moves
- Show retracts and restarts
- Move model to the center of the grid
- Show different speeds with different colors
- Emulate extrusion width
- Width modifier:
- Show +1 layer
-
-
-
- -
- -
-
- These require re-analyzing file:
- Sort layers by Z
- Hide empty layers
- Show GCode in GCode tab (memory intensive!)
-
-
-
- +

Progress indicators

+
+
+
+
+
+ + +

Model info

+

+ +

Layer info

+

+ +

Render options

+ Show non-extrusion moves
+ Show retracts and restarts
+ Move model to the center of the grid
+ Emulate extrusion width
+ Width modifier:
+ Show +1 layer
+ +

GCode analyzer options

+ These require re-analyzing file:
+ Sort layers by Z
+ Hide empty layers
+ Show GCode in GCode tab (memory intensive!)
{% endif %} @@ -650,6 +575,7 @@ + @@ -677,6 +603,7 @@ + diff --git a/src/octoprint/templates/settings.jinja2 b/src/octoprint/templates/settings.jinja2 index 027db1b8..a2f8e2ac 100644 --- a/src/octoprint/templates/settings.jinja2 +++ b/src/octoprint/templates/settings.jinja2 @@ -132,6 +132,12 @@
+
+ +
+ +
+
diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 235d4df3..8265ee61 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -133,17 +133,25 @@ class MachineCom(object): self._serial = None self._baudrateDetectList = baudrateList() self._baudrateDetectRetry = 0 - self._temp = 0 + self._temp = {} + self._targetTemp = {} + self._tempOffset = {} self._bedTemp = 0 - self._targetTemp = 0 self._bedTargetTemp = 0 - self._tempOffset = 0 self._bedTempOffset = 0 self._commandQueue = queue.Queue() self._currentZ = None self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 + # Regex matching temperature entries in line. Groups will be as follows: + # - 1: whole tool designator incl. optional toolNumber ("T", "Tn", "B") + # - 2: toolNumber, if given ("", "n", "") + # - 3: actual temperature + # - 4: whole target substring, if given (e.g. " / 22.0") + # - 5: target temperature + self._tempRegex = re.compile("(B|T(\d*)):([-+]?\d*\.?\d*)(\s*\/?\s*([-+]?\d*\.?\d*))?") + self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) self._currentLine = 1 self._resendDelta = None @@ -305,7 +313,7 @@ class MachineCom(object): return self._bedTemp def getOffsets(self): - return (self._tempOffset, self._bedTempOffset) + return self._tempOffset, self._bedTempOffset def getConnection(self): return self._port, self._baudrate @@ -335,9 +343,9 @@ class MachineCom(object): eventManager().fire(Events.PRINT_FAILED, payload) eventManager().fire(Events.DISCONNECTED) - def setTemperatureOffset(self, extruder=None, bed=None): - if extruder is not None: - self._tempOffset = extruder + def setTemperatureOffset(self, tool=None, bed=None): + if tool is not None: + self._tempOffset = tool if bed is not None: self._bedTempOffset = bed @@ -507,6 +515,53 @@ class MachineCom(object): ##~~ communication monitoring and handling + def _parseTemperatures(self, line): + result = {} + maxToolNum = 0 + for match in re.finditer(self._tempRegex, line): + tool = match.group(1) + toolNumber = int(match.group(2)) if match.group(2) and len(match.group(2)) > 0 else None + if toolNumber > maxToolNum: + maxToolNum = toolNumber + + try: + actual = float(match.group(3)) + target = None + if match.group(4) and match.group(5): + target = float(match.group(5)) + + result[tool] = (toolNumber, actual, target) + except ValueError: + # catch conversion issues, we'll rather just not get the temperature update instead of killing the connection + pass + + if "T0" in result.keys() and "T" in result.keys(): + del result["T"] + + return maxToolNum, result + + def _processTemperatures(self, line): + maxToolNum, parsedTemps = self._parseTemperatures(line) + + # extruder temperatures + if not "T0" in parsedTemps.keys() and "T" in parsedTemps.keys(): + # only single reporting, "T" is our one and only extruder temperature + toolNum, actual, target = parsedTemps["T"] + self._temp[0] = (actual, target) + elif "T0" in parsedTemps.keys(): + for n in range(maxToolNum + 1): + tool = "T%d" % n + if not tool in parsedTemps.keys(): + continue + + toolNum, actual, target = parsedTemps[tool] + self._temp[toolNum] = (actual, target) + + # bed temperature + if "B" in parsedTemps.keys(): + toolNum, actual, target = parsedTemps["B"] + self._bedTemp = (actual, target) + def _monitor(self): feedbackControls = settings().getFeedbackControls() pauseTriggers = settings().getPauseTriggers() @@ -547,15 +602,8 @@ class MachineCom(object): ##~~ Temperature processing if ' T:' in line or line.startswith('T:'): - try: - self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0)) - if ' B:' in line: - self._bedTemp = float(re.search("-?[0-9\.]*", line.split(' B:')[1]).group(0)) - - self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp) - except ValueError: - # catch conversion issues, we'll rather just not get the temperature update instead of killing the connection - pass + self._processTemperatures(line) + self._callback.mcTempUpdate(self._temp, self._bedTemp) #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate. if not 'ok' in line: @@ -1058,7 +1106,7 @@ class MachineComPrintCallback(object): def mcLog(self, message): pass - def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp): + def mcTempUpdate(self, temp, bedTemp): pass def mcStateChange(self, state): @@ -1169,12 +1217,16 @@ class PrintingGcodeFileInformation(PrintingFileInformation): def __init__(self, filename, offsetCallback): PrintingFileInformation.__init__(self, filename) - self._filehandle = None + self._filesetMenuModehandle = None self._lineCount = None self._firstLine = None + self._currentTool = 0 self._offsetCallback = offsetCallback - self._tempCommandPattern = re.compile("^\s*M(104|109|140|190)\s+S([0-9\.]+)") + self._tempCommandPattern = re.compile("M(104|109|140|190)") + self._tempCommandTemperaturePattern = re.compile("S([-+]?\d*\.?\d*)") + self._tempCommandToolPattern = re.compile("T(\d+)") + self._toolCommandPattern = re.compile("^T(\d+)") if not os.path.exists(self._filename) or not os.path.isfile(self._filename): raise IOError("File %s does not exist" % self._filename) @@ -1228,25 +1280,47 @@ class PrintingGcodeFileInformation(PrintingFileInformation): line = line[0:line.find(";")] line = line.strip() if len(line) > 0: - if self._offsetCallback is not None: - (tempOffset, bedTempOffset) = self._offsetCallback() - if tempOffset != 0 or bedTempOffset != 0: + toolMatch = self._toolCommandPattern.match(line) + if toolMatch is not None: + # track tool changes + self._currentTool = int(toolMatch.group(1)) + else: + ## apply offsets + if self._offsetCallback is not None: tempMatch = self._tempCommandPattern.match(line) if tempMatch is not None: + # if we have a temperature command, retrieve current offsets + tempOffset, bedTempOffset = self._offsetCallback() if tempMatch.group(1) == "104" or tempMatch.group(1) == "109": - offset = tempOffset + # extruder temperature, determine which one and retrieve corresponding offset + toolNum = self._currentTool + + toolNumMatch = self._tempCommandToolPattern.search(line) + if toolNumMatch is not None: + try: + toolNum = int(toolNumMatch.group(1)) + except ValueError: + pass + + offset = tempOffset[toolNum] if toolNum in tempOffset.keys() and tempOffset[toolNum] is not None else 0 elif tempMatch.group(1) == "140" or tempMatch.group(1) == "190": + # bed temperature offset = bedTempOffset else: + # unknown, should never happen offset = 0 - try: - temp = float(tempMatch.group(2)) - if temp > 0: - newTemp = temp + offset - line = line.replace("S" + tempMatch.group(2), "S%f" % newTemp) - except ValueError: - pass + if not offset == 0: + # if we have an offset != 0, we need to get the temperature to be set and apply the offset to it + tempValueMatch = self._tempCommandTemperaturePattern.search(line) + if tempValueMatch is not None: + try: + temp = float(tempValueMatch.group(1)) + if temp > 0: + newTemp = temp + offset + line = line.replace("S" + tempValueMatch.group(1), "S%f" % newTemp) + except ValueError: + pass return line else: return None diff --git a/src/octoprint/util/virtual.py b/src/octoprint/util/virtual.py index cc70578a..8f5e7f8b 100644 --- a/src/octoprint/util/virtual.py +++ b/src/octoprint/util/virtual.py @@ -15,8 +15,9 @@ from octoprint.settings import settings class VirtualPrinter(): def __init__(self): self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n', 'SD init fail\n'] # no sd card as default startup scenario - self.temp = 0.0 - self.targetTemp = 0.0 + self.currentExtruder = 0 + self.temp = [0.0] * settings().getInt(["devel", "virtualPrinter", "numExtruders"]) + self.targetTemp = [0.0] * settings().getInt(["devel", "virtualPrinter", "numExtruders"]) self.lastTempAt = time.time() self.bedTemp = 1.0 self.bedTargetTemp = 1.0 @@ -89,10 +90,7 @@ class VirtualPrinter(): #print "Send: %s" % (data.rstrip()) if 'M104' in data or 'M109' in data: - try: - self.targetTemp = float(re.search('S([0-9]+)', data).group(1)) - except: - pass + self._parseHotendCommand(data) if 'M140' in data or 'M190' in data: try: self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1)) @@ -101,7 +99,14 @@ class VirtualPrinter(): if 'M105' in data: # send simulated temperature data - self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp)) + if settings().getInt(["devel", "virtualPrinter", "numExtruders"]) > 1: + allTemps = [] + for i in range(len(self.temp)): + allTemps.append((i, self.temp[i], self.targetTemp[i])) + allTempsString = " ".join(map(lambda x: "T%d:%.2f /%.2f" % x, allTemps)) + self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f %s @:64\n" % (self.temp[self.currentExtruder], self.targetTemp[self.currentExtruder] + 1, self.bedTemp, self.bedTargetTemp, allTempsString)) + else: + self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp[0], self.targetTemp[0], self.bedTemp, self.bedTargetTemp)) elif 'M20' in data: if self._sdCardReady: self._listSd() @@ -147,6 +152,10 @@ class VirtualPrinter(): elif "M999" in data: # mirror Marlin behaviour self.readList.append("Resend: 1") + elif data.startswith("T"): + self.currentExtruder = int(re.search("T(\d+)", data).group(1)) + self._sendOk() + self.readList.append("Active Extruder: %d" % self.currentExtruder) elif len(data.strip()) > 0: self._sendOk() @@ -188,6 +197,23 @@ class VirtualPrinter(): else: self.readList.append("Not SD printing") + def _parseHotendCommand(self, line): + tool = 0 + toolMatch = re.search('T([0-9]+)', line) + if toolMatch: + try: + tool = int(toolMatch.group(1)) + except: + pass + + if tool >= settings().getInt(["devel", "virtualPrinter", "numExtruders"]): + return + + try: + self.targetTemp[tool] = float(re.search('S([0-9]+)', line).group(1)) + except: + pass + def _writeSdFile(self, filename): file = os.path.join(self._virtualSd, filename).lower() if os.path.exists(file): @@ -223,10 +249,7 @@ class VirtualPrinter(): # set target temps if 'M104' in line or 'M109' in line: - try: - self.targetTemp = float(re.search('S([0-9]+)', line).group(1)) - except: - pass + self._parseHotendCommand(line) if 'M140' in line or 'M190' in line: try: self.bedTargetTemp = float(re.search('S([0-9]+)', line).group(1)) @@ -241,9 +264,9 @@ class VirtualPrinter(): self.readList.append("Done printing file") def _deleteSdFile(self, filename): - file = os.path.join(self._virtualSd, filename) - if os.path.exists(file) and os.path.isfile(file): - os.remove(file) + f = os.path.join(self._virtualSd, filename) + if os.path.exists(f) and os.path.isfile(f): + os.remove(f) self._sendOk() def readline(self): @@ -252,12 +275,19 @@ class VirtualPrinter(): n = 0 timeDiff = self.lastTempAt - time.time() self.lastTempAt = time.time() - if abs(self.temp - self.targetTemp) > 1: - self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp) - if self.temp < 0: - self.temp = 0 + for i in range(len(self.temp)): + if abs(self.temp[i] - self.targetTemp[i]) > 1: + oldVal = self.temp[i] + self.temp[i] += math.copysign(timeDiff * 10, self.targetTemp[i] - self.temp[i]) + if math.copysign(1, self.targetTemp[i] - oldVal) != math.copysign(1, self.targetTemp[i] - self.temp[i]): + self.temp[i] = self.targetTemp[i] + if self.temp[i] < 0: + self.temp[i] = 0 if abs(self.bedTemp - self.bedTargetTemp) > 1: + oldVal = self.bedTemp self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp) + if math.copysign(1, self.bedTargetTemp - oldVal) != math.copysign(1, self.bedTargetTemp - self.bedTemp): + self.bedTemp = self.bedTargetTemp if self.bedTemp < 0: self.bedTemp = 0 while len(self.readList) < 1: