Refining temperature control REST API, also added support for multi extrusion while at it
This commit is contained in:
parent
084ffb0dc1
commit
908d39ad39
16 changed files with 804 additions and 470 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class PrinterStateConnection(SockJSConnection):
|
|||
self._messageBacklog = []
|
||||
|
||||
data.update({
|
||||
"temperatures": temperatures,
|
||||
"temps": temperatures,
|
||||
"logs": logs,
|
||||
"messages": messages
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">°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) + '°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 + '°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">°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">°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 + '°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">°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> 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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue