diff --git a/octoprint/printer.py b/octoprint/printer.py
index e944ba6..db41c8d 100644
--- a/octoprint/printer.py
+++ b/octoprint/printer.py
@@ -348,6 +348,11 @@ class Printer():
pass
def _getStateFlags(self):
+ if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None:
+ sdReady = False
+ else:
+ sdReady = self._comm.isSdReady()
+
return {
"operational": self.isOperational(),
"printing": self.isPrinting(),
@@ -355,7 +360,8 @@ class Printer():
"error": self.isError(),
"loading": self.isLoading(),
"paused": self.isPaused(),
- "ready": self.isReady()
+ "ready": self.isReady(),
+ "sdReady": sdReady
}
#~~ callbacks triggered from self._comm
@@ -434,6 +440,9 @@ class Printer():
self._setCurrentZ(newZ)
+ def mcSdStateChange(self, sdReady):
+ self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()})
+
def mcSdFiles(self, files):
self._sendTriggerUpdateCallbacks("gcodeFiles")
@@ -480,6 +489,21 @@ class Printer():
self._sdPrintAfterSelect = printAfterSelect
self._comm.selectSdFile(filename)
+ def initSdCard(self):
+ if not self._comm:
+ return
+ self._comm.initSdCard()
+
+ def releaseSdCard(self):
+ if not self._comm:
+ return
+ self._comm.releaseSdCard()
+
+ def refreshSdFiles(self):
+ if not self._comm:
+ return
+ self._comm.refreshSdFiles()
+
#~~ callbacks triggered by sdFileStreamer
def _onSdFileStreamProgress(self, filename, progress):
diff --git a/octoprint/server.py b/octoprint/server.py
index 96fe006..f7ee412 100644
--- a/octoprint/server.py
+++ b/octoprint/server.py
@@ -254,6 +254,23 @@ def getCustomControls():
customControls = settings().get(["controls"])
return jsonify(controls=customControls)
+@app.route(BASEURL + "control/sd", methods=["POST"])
+@login_required
+def sdCommand():
+ if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting():
+ return jsonify(SUCCESS)
+
+ if "command" in request.values.keys():
+ command = request.values["command"]
+ if command == "init":
+ printer.initSdCard()
+ elif command == "refresh":
+ printer.refreshSdFiles()
+ elif command == "release":
+ printer.releaseSdCard()
+
+ return jsonify(SUCCESS)
+
#~~ GCODE file handling
@app.route(BASEURL + "gcodefiles", methods=["GET"])
@@ -315,6 +332,11 @@ def deleteGcodeFile():
gcodeManager.removeFile(filename)
return readGcodeFiles()
+@app.route(BASEURL + "gcodefiles/refresh", methods=["POST"])
+def refreshFiles():
+ printer.updateSdFiles()
+ return jsonify(SUCCESS)
+
#~~ timelapse handling
@app.route(BASEURL + "timelapse", methods=["GET"])
diff --git a/octoprint/static/css/octoprint.less b/octoprint/static/css/octoprint.less
index 29c0bb5..b61e3bb 100644
--- a/octoprint/static/css/octoprint.less
+++ b/octoprint/static/css/octoprint.less
@@ -111,10 +111,10 @@ body {
.octoprint-container {
.accordion-heading {
- .settings-trigger {
+ .accordion-heading-button {
float: right;
- .dropdown-toggle {
+ a {
display: inline-block;
padding: 8px 15px;
font-size: 14px;
diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js
index e1c7b75..01a01d8 100644
--- a/octoprint/static/js/ui.js
+++ b/octoprint/static/js/ui.js
@@ -213,6 +213,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self.isError = ko.observable(undefined);
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
+ self.isSdReady = ko.observable(undefined);
self.filename = ko.observable(undefined);
self.filament = ko.observable(undefined);
@@ -268,6 +269,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
+ self.isSdReady(data.flags.sdReady);
}
self._processJobData = function(data) {
@@ -756,6 +758,7 @@ function GcodeFilesViewModel(loginStateViewModel) {
self.isError = ko.observable(undefined);
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
+ self.isSdReady = ko.observable(undefined);
// initialize list helper
self.listHelper = new ItemListHelper(
@@ -821,6 +824,7 @@ function GcodeFilesViewModel(loginStateViewModel) {
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
+ self.isSdReady(data.flags.sdReady);
}
self.requestData = function() {
@@ -868,6 +872,27 @@ function GcodeFilesViewModel(loginStateViewModel) {
})
}
+ self.initSdCard = function() {
+ self._sendSdCommand("init");
+ }
+
+ self.releaseSdCard = function() {
+ self._sendSdCommand("release");
+ }
+
+ self.refreshSdFiles = function() {
+ self._sendSdCommand("refresh");
+ }
+
+ self._sendSdCommand = function(command) {
+ $.ajax({
+ url: AJAX_BASEURL + "control/sd",
+ type: "POST",
+ dataType: "json",
+ data: {command: command}
+ });
+ }
+
self.getPopoverContent = function(data) {
var output = "
Uploaded: " + data["date"] + "
";
if (data["gcodeAnalysis"]) {
diff --git a/octoprint/templates/index.jinja2 b/octoprint/templates/index.jinja2
index 71c5a1d..7b3c28c 100644
--- a/octoprint/templates/index.jinja2
+++ b/octoprint/templates/index.jinja2
@@ -132,7 +132,7 @@
Files
-
+
+
+ {% if enableSdSupport %}
+
+ {% endif %}
diff --git a/octoprint/templates/settings.jinja2 b/octoprint/templates/settings.jinja2
index 721b6a2..c6a3652 100644
--- a/octoprint/templates/settings.jinja2
+++ b/octoprint/templates/settings.jinja2
@@ -100,6 +100,13 @@
+
+
+
+
+
-
-
-
-
-
diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py
index 6ca2c89..8863129 100644
--- a/octoprint/util/comm.py
+++ b/octoprint/util/comm.py
@@ -57,7 +57,7 @@ def baudrateList():
class VirtualPrinter():
def __init__(self):
- self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
+ 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.lastTempAt = time.time()
@@ -65,6 +65,7 @@ class VirtualPrinter():
self.bedTargetTemp = 1.0
self._virtualSd = settings().getBaseFolder("virtualSd")
+ self._sdCardReady = False
self._sdPrinter = None
self._sdPrintingSemaphore = threading.Event()
self._selectedSdFile = None
@@ -104,27 +105,41 @@ class VirtualPrinter():
# send simulated temperature data
self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
elif 'M20' in data:
- self._listSd()
+ if self._sdCardReady:
+ self._listSd()
+ elif 'M21' in data:
+ self._sdCardReady = True
+ self.readList.append("SD card ok")
+ elif 'M22' in data:
+ self._sdCardReady = False
elif 'M23' in data:
- filename = data.split(None, 1)[1].strip()
- self._selectSdFile(filename)
+ if self._sdCardReady:
+ filename = data.split(None, 1)[1].strip()
+ self._selectSdFile(filename)
elif 'M24' in data:
- self._startSdPrint()
+ if self._sdCardReady:
+ self._startSdPrint()
elif 'M25' in data:
- self._pauseSdPrint()
+ if self._sdCardReady:
+ self._pauseSdPrint()
elif 'M26' in data:
- pos = int(re.search("S([0-9]+)", data).group(1))
- self._setSdPos(pos)
+ if self._sdCardReady:
+ pos = int(re.search("S([0-9]+)", data).group(1))
+ self._setSdPos(pos)
elif 'M27' in data:
- self._reportSdStatus()
+ if self._sdCardReady:
+ self._reportSdStatus()
elif 'M28' in data:
- filename = data.split(None, 1)[1].strip()
- self._writeSdFile(filename)
+ if self._sdCardReady:
+ filename = data.split(None, 1)[1].strip()
+ self._writeSdFile(filename)
elif 'M29' in data:
- self._finishSdFile()
+ if self._sdCardReady:
+ self._finishSdFile()
elif 'M30' in data:
- filename = data.split(None, 1)[1].strip()
- self._deleteSdFile(filename)
+ if self._sdCardReady:
+ filename = data.split(None, 1)[1].strip()
+ self._deleteSdFile(filename)
elif "M110" in data:
# reset current line
self.currentLine = int(re.search('N([0-9]+)', data).group(1))
@@ -196,7 +211,7 @@ class VirtualPrinter():
def _sdPrintingWorker(self):
self._selectedSdFilePos = 0
- with open(self._selectedSdFile, "rb") as f:
+ with open(self._selectedSdFile, "r") as f:
for line in f:
# reset position if requested by client
if self._newSdFilePos is not None:
@@ -284,6 +299,9 @@ class MachineComPrintCallback(object):
def mcZChange(self, newZ):
pass
+ def mcSdStateChange(self, sdReady):
+ pass
+
def mcSdFiles(self, files):
pass
@@ -355,6 +373,7 @@ class MachineCom(object):
self.thread.daemon = True
self.thread.start()
+ self._sdAvailable = False
self._sdPrinting = False
self._sdFileList = False
self._sdFile = None
@@ -443,6 +462,9 @@ class MachineCom(object):
def isBusy(self):
return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE
+ def isSdReady(self):
+ return self._sdAvailable
+
def getPrintPos(self):
if self._sdPrinting:
return self._sdFilePos
@@ -462,10 +484,11 @@ class MachineCom(object):
if self._sdPrinting:
printTime = (time.time() - self._printStartTime) / 60
if self._sdFilePos > 0:
- printTimeTotal = printTime * (self._sdFileSize / self._sdFilePos)
+ printTimeTotal = printTime * self._sdFileSize / self._sdFilePos
else:
printTimeTotal = printTime * self._sdFileSize
printTimeLeft = printTimeTotal - printTime
+ self._logger.info("printTime: %f, sdFileSize: %f, sdFilePos: %f, printTimeTotal: %f, printTimeLeft: %f" % (printTime, self._sdFileSize, self._sdFilePos, printTimeTotal, printTimeLeft))
return printTimeLeft
else:
# for host printing we only start counting the print time at gcode line 100, so we need to calculate stuff
@@ -593,6 +616,14 @@ class MachineCom(object):
self._heatupWaitStartTime = t
##~~ SD Card handling
+ elif 'SD init fail' in line:
+ self._sdAvailable = False
+ self._sdFiles = []
+ self._callback.mcSdStateChange(self._sdAvailable)
+ elif 'SD card ok' in line:
+ self._sdAvailable = True
+ self.refreshSdFiles()
+ self._callback.mcSdStateChange(self._sdAvailable)
elif 'Begin file list' in line:
self._sdFiles = []
self._sdFileList = True
@@ -671,8 +702,6 @@ class MachineCom(object):
startSeen = True
elif "ok" in line and startSeen:
self._changeState(self.STATE_OPERATIONAL)
- if settings().get(["feature", "sdSupport"]):
- self._sendCommand("M20")
elif time.time() > timeout:
self.close()
@@ -697,24 +726,28 @@ class MachineCom(object):
self._log("Communication timeout during printing, forcing a line")
line = 'ok'
- # Even when printing request the temperature every 5 seconds.
- if time.time() > tempRequestTimeout:
- self._commandQueue.put("M105")
- tempRequestTimeout = time.time() + 5
-
if self._sdPrinting:
+ if time.time() > tempRequestTimeout:
+ self._sendCommand("M105")
+ tempRequestTimeout = time.time() + 5
+
if time.time() > sdStatusRequestTimeout:
- self._commandQueue.put("M27")
+ self._sendCommand("M27")
sdStatusRequestTimeout = time.time() + 1
- if 'ok' in line and not self._commandQueue.empty():
- self._sendCommand(self._commandQueue.get())
+ if 'ok' or 'SD printing byte' in line:
+ timeout = time.time() + 5
else:
+ # Even when printing request the temperature every 5 seconds.
+ if time.time() > tempRequestTimeout:
+ self._commandQueue.put("M105")
+ tempRequestTimeout = time.time() + 5
+
if 'ok' in line:
timeout = time.time() + 5
- if self._resendDelta is not None:
- self._resendNextCommand()
- elif not self._commandQueue.empty():
+ if self._resendDelta is not None:
+ self._resendNextCommand()
+ elif not self._commandQueue.empty():
self._sendCommand(self._commandQueue.get())
else:
self._sendNext()
@@ -908,7 +941,7 @@ class MachineCom(object):
self._log("Unexpected error: %s" % (getExceptionString()))
self._sendCommand(line, True)
self._gcodePos += 1
- self._callback.mcProgress(self._gcodePos)
+ self._callback.mcProgress()
def sendCommand(self, cmd):
cmd = cmd.encode('ascii', 'replace')
@@ -929,14 +962,6 @@ class MachineCom(object):
self._printStartTime = time.time()
self._sendNext()
- def selectSdFile(self, filename):
- if not self.isOperational() or self.isPrinting():
- return
- self._sdFile = None
- self._sdFilePos = 0
-
- self.sendCommand("M23 %s" % filename.lower())
-
def printSdFile(self):
if not self.isOperational() or self.isPrinting():
return
@@ -979,24 +1004,64 @@ class MachineCom(object):
result.update(self._feedRateModifier)
return result
+ ##~~ SD card
def getSdFiles(self):
return self._sdFiles
def startSdFileTransfer(self, filename):
- if self.isPrinting() or self.isPaused():
+ if not self.isOperational() or self.isPrinting() or self.isPaused():
return
+
self._changeState(self.STATE_RECEIVING_FILE)
self.sendCommand("M28 %s" % filename.lower())
def endSdFileTransfer(self, filename):
+ if not self.isOperational() or self.isPrinting() or self.isPaused():
+ return
+
self.sendCommand("M29 %s" % filename.lower())
self._changeState(self.STATE_OPERATIONAL)
- self.sendCommand("M20")
+ self.refreshSdFiles()
+
+ def selectSdFile(self, filename):
+ if not self.isOperational() or self.isPrinting() or self.isPaused():
+ return
+
+ self._sdFile = None
+ self._sdFilePos = 0
+
+ self.sendCommand("M23 %s" % filename.lower())
def deleteSdFile(self, filename):
+ if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdFile == filename.lower()):
+ # do not delete a file from sd we are currently printing from
+ return
+
self.sendCommand("M30 %s" % filename.lower())
+ self.refreshSdFiles()
+
+ def refreshSdFiles(self):
+ if not self.isOperational() or self.isPrinting() or self.isPaused():
+ return
self.sendCommand("M20")
+ def initSdCard(self):
+ if not self.isOperational():
+ return
+ self.sendCommand("M21")
+
+ def releaseSdCard(self):
+ if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdPrinting):
+ # do not release the sd card if we are currently printing from it
+ return
+
+ self.sendCommand("M22")
+ self._sdAvailable = False
+ self._sdFiles = []
+
+ self._callback.mcSdStateChange(self._sdAvailable)
+ self._callback.mcSdFiles(self._sdFiles)
+
def getExceptionString():
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])