Refining temperature control REST API, also added support for multi extrusion while at it

This commit is contained in:
Gina Häußge 2014-01-01 02:39:35 +01:00
parent 084ffb0dc1
commit 908d39ad39
16 changed files with 804 additions and 470 deletions

View file

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

View file

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

View file

@ -3,51 +3,73 @@ __author__ = "Gina Häußge <osd@foosel.net>"
__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

View file

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

View file

@ -160,7 +160,7 @@ class PrinterStateConnection(SockJSConnection):
self._messageBacklog = []
data.update({
"temperatures": temperatures,
"temps": temperatures,
"logs": logs,
"messages": messages
})

View file

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

View file

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

View file

@ -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&deg;C", temp);
}

View file

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

View file

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

View file

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

View file

@ -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() + " &deg;C";
});
self.bedTempString = ko.computed(function() {
if (!self.bedTemp())
return "-";
return self.bedTemp() + " &deg;C";
});
self.targetTempString = ko.computed(function() {
if (!self.targetTemp())
return "-";
return self.targetTemp() + " &deg;C";
});
self.bedTargetTempString = ko.computed(function() {
if (!self.bedTargetTemp())
return "-";
return self.bedTargetTemp() + " &deg;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);
}
}
}
};
}

View file

@ -253,87 +253,55 @@
<div id="temperature-graph"></div>
</div>
{% endif %}
<div class="row-fluid" style="margin-bottom: 20px">
<div class="form-horizontal span6">
<h1>Temperature</h1>
<div class="row-fluid">
<label title="Current extruder temperature">Current: <strong data-bind="html: tempString"></strong></label>
<table class="table table-bordered table-hover" style="table-layout: fixed; width: 100%; margin-top: 20px">
<tr>
<th style="width: 18%"></th>
<th style="text-align: right" style="width: 12%">Actual</th>
<th style="width: 35%">Target</th>
<th style="width: 35%">Offset</th>
</tr>
<!-- ko foreach: tools -->
<tr data-bind="template: { name: 'temprow-template' }"></tr>
<!-- /ko -->
<tr data-bind="template: { name: 'temprow-template', data: bedTemp }"></tr>
</table>
<label title="Target extruder temperature">Target: <strong data-bind="html: targetTempString"></strong></label>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newTemp" title="Sets the new target temperature for the extruder">New Target</label>
<script type="text/html" id="temprow-template">
<th style="vertical-align: middle" data-bind="text: name"></th>
<td style="text-align: right; vertical-align: middle" data-bind="html: formatTemperature(actual())"></td>
<td style="vertical-align: middle; overflow: visible">
<div class="input-append">
<input type="text" class="input-mini text-right" data-bind="value: newTemp, valueUpdate: 'afterkeydown', attr: {placeholder: targetTemp}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'temp');} }" class="tempInput">
<input type="text" class="input-mini text-right tempInput" data-bind="attr: {placeholder: cleanTemperature(target()) }, value: newTarget, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'target', $data);} }">
<span class="add-on">&deg;C</span>
<div class="btn-group">
<button type="submit" data-bind="click: $parent.setTarget, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: $root.isOperational() && $root.loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: $root.temperature_profiles -->
<li>
<a href="#" data-bind="click: function() {$root.setTargetFromProfile($parent, $data);}, text: 'Set ' + name + ' (' + ($parent.key() == 'bed' ? bed : extruder) + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: $root.setTargetToZero">Off</a>
</li>
</ul>
</div>
</div>
<div class="btn-group">
<button type="submit" class="btn" data-bind="click: setTemp, enable: isOperational() && loginState.isUser() && newTemp()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setTempFromProfile, text: 'Set ' + name + ' (' + extruder + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function() { $root.setTempToZero(); }">Off</a>
</li>
</ul>
</div>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label title="Sets a temperature offset to apply to temperatures set via streamed GCODE, may be positive or negative, will not persist across restarts of OctoPrint">Offset</label>
</td>
<td style="vertical-align: middle">
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right" data-bind="value: newTempOffset, valueUpdate: 'afterkeydown', attr: {placeholder: tempOffset}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'tempOffset');} }" class="tempInput">
<input type="number" min="-50" max="50" class="input-mini text-right tempInput" data-bind="attr: {placeholder: offset}, value: newOffset, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'offset', $data);} }">
<span class="add-on">&deg;C</span>
<button type="submit" data-bind="click: $root.setOffset, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">Set</button>
</div>
<button type="submit" class="btn" data-bind="click: setTempOffset, enable: newTempOffset() && isOperational() && loginState.isUser()">Set</button>
</div>
</div>
<div class="form-horizontal span6">
<h1>Bed Temperature</h1>
<label title="Current bed temperature">Current: <strong data-bind="html: bedTempString"></strong></label>
<label title="Target bed temperature">Target: <strong data-bind="html: bedTargetTempString"></strong></label>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label for="temp_newBedTemp" title="Sets the new target temperature for the bed">New Target</label>
<div class="input-append">
<input type="text" class="input-mini text-right" data-bind="value: newBedTemp, valueUpdate: 'afterkeydown', attr: {placeholder: bedTargetTemp}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(event, 'bedTemp');} }" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<div class="btn-group">
<button type="submit" class="btn" data-bind="click: setBedTemp, enable: isOperational() && loginState.isUser() && newBedTemp()">Set</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && loginState.isUser()">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<!-- ko foreach: temperature_profiles -->
<li>
<a href="#" data-bind="click: $parent.setBedTempFromProfile, text: 'Set ' + name + ' (' + bed + '&deg;C)'"></a>
</li>
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: function(){ $root.setBedTempToZero(); }">Off</a>
</li>
</ul>
</div>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<label title="Sets a temperature offset to apply to bed temperatures set via streamed GCODE, may be positive or negative, will not persist across restarts of OctoPrint">Offset</label>
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right" data-bind="value: newBedTempOffset, valueUpdate: 'afterkeydown', attr: {placeholder: bedTempOffset}, enable: isOperational() && loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'bedTempOffset');} }" class="tempInput">
<span class="add-on">&deg;C</span>
</div>
<button type="submit" class="btn" data-bind="click: setBedTempOffset, enable: newBedTempOffset() && isOperational() && loginState.isUser()">Set</button>
</div>
</div>
</td>
</script>
</div>
</div>
<div class="tab-pane" id="control">
@ -375,32 +343,41 @@
<!-- Jog distance -->
<div class="distance">
<div class="btn-group" data-toggle="buttons-radio" id="jog_distance">
<button type="button" class="btn" data-distance="0.1" data-bind="enable: loginState.isUser()">0.1</button>
<button type="button" class="btn" data-distance="1" data-bind="enable: loginState.isUser()">1</button>
<button type="button" class="btn active" data-distance="10" data-bind="enable: loginState.isUser()">10</button>
<button type="button" class="btn" data-distance="100" data-bind="enable: loginState.isUser()">100</button>
<button type="button" class="btn distance" data-distance="0.1" data-bind="enable: loginState.isUser()">0.1</button>
<button type="button" class="btn distance" data-distance="1" data-bind="enable: loginState.isUser()">1</button>
<button type="button" class="btn distance active" data-distance="10" data-bind="enable: loginState.isUser()">10</button>
<button type="button" class="btn distance" data-distance="100" data-bind="enable: loginState.isUser()">100</button>
</div>
</div>
</div>
<!-- Extrusion control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>E</h1>
<h1>Tool (E)</h1>
<div>
<div class="btn-group control-box">
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && !isPrinting() && !isPaused() && loginState.isUser()">
Select Tool...
<span class="caret"></span>
</button>
<ul class="dropdown-menu" data-bind="foreach: tools">
<li><a href="#" data-bind="click: $root.sendSelectToolCommand, text: 'Select ' + name(), enable: $root.isOperational() && !$root.isPrinting() && !$root.isPaused() && $root.loginState.isUser()"></a></li>
</ul>
</div>
<div class="input-append control-box">
<input type="text" class="input-mini text-right" data-bind="value: extrusionAmount, enable: isOperational() && !isPrinting() && loginState.isUser(), attr: {placeholder: 5}">
<span class="add-on">mm</span>
</div>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">Extrude</button>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">Retract</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">Extrude</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">Retract</button>
</div>
</div>
<!-- General control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>General</h1>
<div>
<button class="btn control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }"><i class="icon-off"></i>&nbsp;Motors off</button>
<button class="btn control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">Fans on</button>
<button class="btn control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">Fans off</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }">Motors off</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">Fans on</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">Fans off</button>
</div>
</div>
@ -446,86 +423,34 @@
<div id="slider-vertical"></div>
<div id="slider-horizontal"></div>
<div id="gcode_accordion" class="accordion" style="margin-top: 20px">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#progressAccordionTab">
Progress indicators
</a>
</div>
<div id="progressAccordionTab" class="accordion-body collapse in">
<div class="accordion-inner">
<div id="progressBlock">
<div class="progress" >
<div id="loadProgress" class="bar" style="width: 0%;"></div>
</div>
<div class="progress" >
<div id="analyzeProgress" class="bar" style="width: 0%;"></div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#infoAccordionTab">
Model info
</a>
</div>
<div id="infoAccordionTab" class="accordion-body collapse in">
<div class="accordion-inner">
<p id="list"></p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#layerAccordionTab">
Layer Info
</a>
</div>
<div id="layerAccordionTab" class="accordion-body collapse in">
<div class="accordion-inner">
<p id="layerInfo"></p>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#options2DAccordionTab">
2D Render options
</a>
</div>
<div id="options2DAccordionTab" class="accordion-body collapse in">
<div class="accordion-inner">
<input type="checkbox" id="showMovesCheckbox" value="1" onclick="GCODE.ui.processOptions()" checked>Show non-extrusion moves</input><br>
<input type="checkbox" id="showRetractsCheckbox" value="2" onclick="GCODE.ui.processOptions()" checked>Show retracts and restarts</input><br>
<input type="checkbox" id="moveModelCheckbox" value="3" onclick="GCODE.ui.processOptions()" checked>Move model to the center of the grid</input><br>
<input type="checkbox" id="differentiateColorsCheckbox" value="7" onclick="GCODE.ui.processOptions()" checked>Show different speeds with different colors</input><br>
<input type="checkbox" id="thickExtrusionCheckbox" value="8" onclick="GCODE.ui.processOptions()">Emulate extrusion width</input><br>
Width modifier: <input type="text" value="2" id="widthModifier" onchange="GCODE.ui.processOptions()"/><br>
<input type="checkbox" id="showNextLayer" value="9" onclick="GCODE.ui.processOptions()" >Show +1 layer</input><br>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#analyzeOptionsAccordioinTab">
GCode analyzer options
</a>
</div>
<div id="analyzeOptionsAccordioinTab" class="accordion-body collapse in">
<div class="accordion-inner">
These require re-analyzing file:<br>
<input type="checkbox" id="sortLayersCheckbox" value="4" onclick="GCODE.ui.processOptions()" checked>Sort layers by Z</input><br>
<input type="checkbox" id="purgeEmptyLayersCheckbox" value="5" onclick="GCODE.ui.processOptions()" checked>Hide empty layers</input><br>
<input type="checkbox" id="showGCodeCheckbox" value="6" onclick="GCODE.ui.processOptions()" checked>Show GCode in GCode tab (memory intensive!)</input><br>
</div>
</div>
</div>
<h1>Progress indicators</h1>
<div class="progress" >
<div id="loadProgress" class="bar" style="width: 0%;"></div>
</div>
<div class="progress" >
<div id="analyzeProgress" class="bar" style="width: 0%;"></div>
</div>
<h1>Model info</h1>
<p id="list"></p>
<h1>Layer info</h1>
<p id="layerInfo"></p>
<h1>Render options</h1>
<input type="checkbox" id="showMovesCheckbox" value="1" onclick="GCODE.ui.processOptions()" checked>Show non-extrusion moves</input><br>
<input type="checkbox" id="showRetractsCheckbox" value="2" onclick="GCODE.ui.processOptions()" checked>Show retracts and restarts</input><br>
<input type="checkbox" id="moveModelCheckbox" value="3" onclick="GCODE.ui.processOptions()" checked>Move model to the center of the grid</input><br>
<input type="checkbox" id="thickExtrusionCheckbox" value="8" onclick="GCODE.ui.processOptions()">Emulate extrusion width</input><br>
Width modifier: <input type="text" value="2" id="widthModifier" onchange="GCODE.ui.processOptions()"/><br>
<input type="checkbox" id="showNextLayer" value="9" onclick="GCODE.ui.processOptions()" >Show +1 layer</input><br>
<h1>GCode analyzer options</h1>
These require re-analyzing file:<br>
<input type="checkbox" id="sortLayersCheckbox" value="4" onclick="GCODE.ui.processOptions()" checked>Sort layers by Z</input><br>
<input type="checkbox" id="purgeEmptyLayersCheckbox" value="5" onclick="GCODE.ui.processOptions()" checked>Hide empty layers</input><br>
<input type="checkbox" id="showGCodeCheckbox" value="6" onclick="GCODE.ui.processOptions()" checked>Show GCode in GCode tab (memory intensive!)</input><br>
</div>
{% endif %}
@ -650,6 +575,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/moment.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pusher.color.min.js') }}"></script>
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->
@ -677,6 +603,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/Worker.js') }}"></script>
</body>
</html>

View file

@ -132,6 +132,12 @@
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-numExtruders">Number of Extruders</label>
<div class="controls">
<input type="number" class="input-mini text-right" min="1" max="5" data-bind="value: printer_numExtruders" id="settings-numExtruders">
</div>
</div>
</form>
</div>
<div class="tab-pane" id="settings_webcam">

View file

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

View file

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