More work on the API + documentation

Changed some endpoints again (removed "/control" path element) and made API spit out only raw data (e.g. seconds, millimeters, unix timestamps etc) instead of formatted versions. Modified frontend to take care of formatting this data itself.
This commit is contained in:
Gina Häußge 2013-12-22 02:01:48 +01:00
parent 097e398efc
commit 538338abfe
28 changed files with 740 additions and 377 deletions

View file

@ -11,7 +11,7 @@ Connection handling
Get connection settings
=======================
.. http:get:: /api/control/connection
.. http:get:: /api/connection
Retrieve the current connection settings, including information regarding the available baudrates and
serial ports and the current connection state.
@ -54,7 +54,7 @@ Get connection settings
Issue a connection command
==========================
.. http:post:: /api/control/connection
.. http:post:: /api/connection
Issue a connection command. Currently available command are:

View file

@ -36,23 +36,25 @@ Retrieve all files
"files": [
{
"name": "whistle_v2.gcode",
"bytes": 1468987,
"size": "1.4MB",
"date": "2013-05-21 23:15",
"size": 1468987,
"date": 1378847754,
"origin": "local",
"refs": {
"resource": "http://example.com/api/files/local/whistle_v2.gcode",
"download": "http://example.com/downloads/files/local/whistle_v2.gcode"
},
"gcodeAnalysis": {
"estimatedPrintTime": "00:31:40",
"filament": "0.79m"
"estimatedPrintTime": 1188,
"filament": {
"length": 810,
"volume": 5.36
}
},
"print": {
"failure": 4,
"success": 23,
"last": {
"date": "2013-11-18 18:00",
"date": 1387144346,
"success": true
}
}
@ -70,7 +72,7 @@ Retrieve all files
:statuscode 200: No error
.. _sec-api-fileops-retrievespecific:
.. _sec-api-fileops-retrievelocation:
Retrieve files from specific location
=====================================
@ -100,23 +102,25 @@ Retrieve files from specific location
"files": [
{
"name": "whistle_v2.gcode",
"bytes": 1468987,
"size": "1.4MB"
"date": "2013-05-21 23:15",
"size": 1468987,
"date": 1378847754,
"origin": "local",
"refs": {
"resource": "http://example.com/api/files/local/whistle_v2.gcode",
"download": "http://example.com/downloads/files/local/whistle_v2.gcode"
},
"gcodeAnalysis": {
"estimatedPrintTime": "00:31:40",
"filament": "0.79m"
"estimatedPrintTime": 1188,
"filament": {
"length": 810,
"volume": 5.36
}
},
"print": {
"failure": 4,
"success": 23,
"last": {
"date": "2013-11-18 18:00",
"date": 1387144346,
"success": true
}
}
@ -259,23 +263,25 @@ Retrieve a specific file's information
{
"name": "whistle_v2.gcode",
"bytes": 1468987,
"size": "1.4MB"
"date": "2013-05-21 23:15",
"size": 1468987,
"date": 1378847754,
"origin": "local",
"refs": {
"resource": "http://example.com/api/files/local/whistle_v2.gcode",
"download": "http://example.com/downloads/files/local/whistle_v2.gcode"
},
"gcodeAnalysis": {
"estimatedPrintTime": "00:31:40",
"filament": "0.79m"
"estimatedPrintTime": 1188,
"filament": {
"length": 810,
"volume": 5.36
}
},
"print": {
"failure": 4,
"success": 23,
"last": {
"date": "2013-11-18 18:00",
"date": 1387144346,
"success": true
}
}
@ -383,7 +389,7 @@ Retrieve response
* - ``free``
- 0..1
- String
- The amount of disk space available in the local disk space (refers to OctoPrint's ``uploads`` folder). Only
- The amount of disk space in bytes available in the local disk space (refers to OctoPrint's ``uploads`` folder). Only
returned if file list was requested for origin ``local`` or all origins.
.. _sec-api-fileops-datamodel-uploadresponse:
@ -438,18 +444,14 @@ File information
- 1
- String
- The name of the file
* - ``bytes``
* - ``size``
- 0..1
- Number
- The size of the file in bytes. Only available for ``local`` files.
* - ``size``
- 0..1
- String
- The size of the file in a human readable format. Only available for ``local`` files.
* - ``date``
- 0..1
- String representing a date and time in the format ``YYYY-MM-DD HH:mm``
- The date and time this files was uploaded. Only available for ``local`` files.
- Unix timestamp
- The timestamp when this file was uploaded. Only available for ``local`` files.
* - ``origin``
- 1
- String, either ``local`` or ``sdcard``
@ -457,7 +459,7 @@ File information
printer's SD card (if available)
* - ``refs``
- 0..1
- :ref:`<sec-api-fileops-datamodel-ref>`
- :ref:`sec-api-fileops-datamodel-ref`
- References relevant to this file
* - ``gcodeAnalysis``
- 0..1
@ -482,14 +484,21 @@ GCODE analysis information
- Type
- Description
* - ``estimatedPrintTime``
- 1
- String representing a duration in the format ``HH:mm:ss``
- The estimated print time of the file
- 0..1
- Integer
- The estimated print time of the file, in seconds
* - ``filament``
- 1
- String
- The estimated usage of filament (length in meters and volume in cubic centimeters) in a human readable format.
Example: ``1.89m / 11.90cm³``
- 0..1
- Object
- The estimated usage of filament
* - ``filament.length``
- 0..1
- Integer
- The length of filament used, in mm
* - ``filament.volume``
- 0..1
- Float
- The volume of filament used, in cm³
.. _sec-api-fileops-datamodel-prints:
@ -519,8 +528,8 @@ Print information
- Information regarding the last print on record for the file
* - ``last.date``
- 1
- String representing a date and time in the format ``YYYY-MM-DD HH:mm``
- Date and time when the file was printed last
- Unix timestamp
- Timestamp when this file was printed last
* - ``last.success``
- 1
- Boolean

View file

@ -11,5 +11,5 @@ API Documentation
fileops.rst
connection.rst
printer.rst
jobs.rst
job.rst

248
docs/api/job.rst Normal file
View file

@ -0,0 +1,248 @@
.. _sec-api-jobs:
**************
Job operations
**************
.. contents::
.. _sec-api-jobs-command:
Issue a job command
===================
.. http:post:: /api/job
Job commands allow starting, pausing and cancelling print jobs. Available commands are:
start
Starts the print of the currently selected file. For selecting a file, see :ref:`Issue a file command <sec-api-fileops-filecommand>`.
If a print job is already active, a :http:statuscode:`409` will be returned.
restart
Restart the print of the currently selected file from the beginning. There must be an active print job for this to work
and the print job must currently be paused. If either is not the case, a :http:statuscode:`409` will be returned.
pause
Pauses/unpauses the current print job. If no print job is active (either paused or printing), a :http:statuscode:`409`
will be returned.
cancel
Cancels the current print job. If no print job is active (either paused or printing), a :http:statuscode:`409`
will be returned.
Upon success, a status code of :http:statuscode:`204` and an empty body is returned.
**Example Start Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "start"
}
**Example Restart Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "restart"
}
**Example Pause Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "pause"
}
**Example Cancel Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "cancel"
}
:json string command: The command to issue, either ``start``, ``restart``, ``pause`` or ``cancel``
:statuscode 204: No error
:statuscode 409: If the printer is not operational or the current print job state does not match the preconditions
for the command.
.. _sec-api-job-information:
Retrieve information about the current job
==========================================
.. http:get:: /api/job
Retrieve information about the current job (if there is one).
Returns a :http:statuscode:`200` with a :ref:`sec-api-job-datamodel-response` in the body.
**Example Request**
.. sourcecode:: http
GET /api/job HTTP/1.1
Host: example.com
**Example Response**
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"job": {
"file": {
"name": "whistle_v2.gcode",
"origin": "local",
"size": 1468987,
"date": 1378847754
},
"estimatedPrintTime": 8811,
"filament": {
"length": 810,
"volume": 5.36
}
},
"progress": {
"completion": 23,
"filepos": 337942,
"printTime": 276,
"printTimeLeft": 912
}
}
:statuscode 200: No error
.. _sec-api-job-datamodel:
Datamodel
=========
.. _sec-api-job-datamodel-response:
Job information response
------------------------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``job``
- 1
- :ref:`sec-api-job-datamodel-job`
- Information regarding the target of the current print job
* - ``progress``
- 1
- :ref:`sec-api-job-datamodel-progress`
- Information regarding the progress of the current print job
.. _sec-api-job-datamodel-job:
Job information
---------------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``file``
- 1
- Object
- The file that is the target of the current print job
* - ``file.name``
- 1
- String
- The file's name
* - ``file.origin``
- 1
- String, either ``local`` or ``sdcard``
- The file's origin, either ``local`` or ``sdcard``
* - ``file.size``
- 0..1
- Integer
- The file's size, in bytes. Only available for files stored locally.
* - ``file.date``
- 0..1
- Unix timestamp
- The file's upload date. Only available for files stored locally.
* - ``estimatedPrintTime``
- 0..1
- Integer
- The estimated print time for the file, in seconds.
* - ``filament``
- 0..1
- Object
- Information regarding the estimated filament usage of the print job
* - ``filament.length``
- 0..1
- Integer
- Length of filament used, in mm
* - ``filament.volume``
- 0..1
- Float
- Volume of filament used, in cm³
.. _sec-api-job-datamodel-progress:
Progress information
--------------------
.. list-table::
:widths: 15 5 10 30
:header-rows: 1
* - Name
- Multiplicity
- Type
- Description
* - ``completion``
- 1
- Integer
- Percentage of completion of the current print job
* - ``filepos``
- 1
- Integer
- Current position in the file being printed, in bytes from the beginning
* - ``printTime``
- 1
- Integer
- Time already spent printing, in seconds
* - ``printTimeLeft``
- 1
- Integer
- Estimate of time left to print, in seconds

View file

@ -1,91 +0,0 @@
.. _sec-api-jobs:
***********
Job Control
***********
.. contents::
.. _sec-api-jobs-command:
Issue a job command
===================
.. http:post:: /api/control/job
Job commands allow starting, pausing and cancelling print jobs. Available commands are:
start
Starts the print of the currently selected file. For selecting a file, see :ref:`Issue a file command <sec-api-fileops-filecommand>`.
If a print job is already active, a :http:statuscode:`409` will be returned.
restart
Restart the print of the currently selected file from the beginning. There must be an active print job for this to work
and the print job must currently be paused. If either is not the case, a :http:statuscode:`409` will be returned.
pause
Pauses/unpauses the current print job. If no print job is active (either paused or printing), a :http:statuscode:`409`
will be returned.
cancel
Cancels the current print job. If no print job is active (either paused or printing), a :http:statuscode:`409`
will be returned.
Upon success, a status code of :http:statuscode:`204` and an empty body is returned.
**Example Start Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "start"
}
**Example Restart Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "restart"
}
**Example Pause Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "pause"
}
**Example Cancel Request**
.. sourcecode:: http
POST /api/control/job HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
{
"command": "cancel"
}
:json string command: The command to issue, either ``start``, ``restart``, ``pause`` or ``cancel``
:statuscode 204: No error
:statuscode 409: If the printer is not operational or the current print job state does not match the preconditions
for the command.

View file

@ -1,8 +1,8 @@
.. _sec-api-printer:
***************
Printer Control
***************
******************
Printer operations
******************
.. contents::
@ -25,7 +25,7 @@ SD card
Issue a print head command
==========================
.. http:post:: /api/control/printer/printhead
.. http:post:: /api/printer/printhead
Print head commands allow jogging and homing the print head in all three axes. Available commands are:
@ -52,7 +52,7 @@ Issue a print head command
.. sourcecode:: http
POST /api/control/printer/printhead HTTP/1.1
POST /api/printer/printhead HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -70,7 +70,7 @@ Issue a print head command
.. sourcecode:: http
POST /api/control/printer/printhead HTTP/1.1
POST /api/printer/printhead HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -95,7 +95,7 @@ Issue a print head command
Issue a heater command
======================
.. http:post:: /api/control/printer/heater
.. http:post:: /api/printer/heater
Heater commands allow setting the temperature and temperature offsets for the printer's hotend and bed. Available
commands are:
@ -127,7 +127,7 @@ Issue a heater command
.. sourcecode:: http
POST /api/control/printer/heater HTTP/1.1
POST /api/printer/heater HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -146,7 +146,7 @@ Issue a heater command
.. sourcecode:: http
POST /api/control/printer/heater HTTP/1.1
POST /api/printer/heater HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -173,7 +173,7 @@ Issue a heater command
Issue a feeder command
======================
.. http:post:: /api/control/printer/feeder
.. http:post:: /api/printer/feeder
Feeder commands allow extrusion/extraction of filament. Available commands are:
@ -193,7 +193,7 @@ Issue a feeder command
.. sourcecode:: http
POST /api/control/printer/feeder HTTP/1.1
POST /api/printer/feeder HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -209,7 +209,7 @@ Issue a feeder command
.. sourcecode:: http
POST /api/control/printer/feeder HTTP/1.1
POST /api/printer/feeder HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -230,7 +230,7 @@ Issue a feeder command
Issue a SD command
==================
.. http:post:: /api/control/printer/sd
.. http:post:: /api/printer/sd
SD commands allow initialization, refresh and release of the printer's SD card (if available).
@ -239,7 +239,7 @@ Issue a SD command
init
Initializes the printer's SD card, making it available for use. This also includes an initial retrieval of the
list of files currently stored on the SD card, so after issueing that command a :ref:`retrieval of the files
on SD card <sec-api-fileops-retrieveorigin>` will return a successful result.
on SD card <sec-api-fileops-retrievelocation>` will return a successful result.
.. note::
If OctoPrint detects the availability of a SD card on the printer during connection, it will automatically attempt
@ -260,7 +260,7 @@ Issue a SD command
.. sourcecode:: http
POST /api/control/printer/sd HTTP/1.1
POST /api/printer/sd HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -273,7 +273,7 @@ Issue a SD command
.. sourcecode:: http
POST /api/control/printer/sd HTTP/1.1
POST /api/printer/sd HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -286,7 +286,7 @@ Issue a SD command
.. sourcecode:: http
POST /api/control/printer/sd HTTP/1.1
POST /api/printer/sd HTTP/1.1
Host: example.com
Content-Type: application/json
X-Api-Key: abcdef...
@ -305,7 +305,7 @@ Issue a SD command
Retrieve the current SD state
=============================
.. http:get:: /api/control/printer/sd
.. http:get:: /api/printer/sd
Retrieves the current state of the printer's SD card. For this request no authentication is needed.
@ -318,7 +318,7 @@ Retrieve the current SD state
.. sourcecode:: http
GET /api/control/printer/sd HTTP/1.1
GET /api/printer/sd HTTP/1.1
Host: example.com
**Example Response**

View file

@ -254,7 +254,7 @@ class CommandTrigger(GenericEventListener):
try:
processedCommand = self._processCommand(command, payload)
self.executeCommand(processedCommand)
except KeyError:
except KeyError, e:
self._logger.warn("There was an error processing one or more placeholders in the following command: %s" % command)
def executeCommand(self, command):
@ -292,7 +292,7 @@ class CommandTrigger(GenericEventListener):
params["__currentZ"] = str(currentData["currentZ"])
if "job" in currentData.keys() and currentData["job"] is not None:
params["__filename"] = currentData["job"]["filename"]
params["__filename"] = currentData["job"]["file"]["name"]
if "progress" in currentData.keys() and currentData["progress"] is not None \
and "progress" in currentData["progress"].keys() and currentData["progress"]["progress"] is not None:
params["__progress"] = str(round(currentData["progress"]["progress"] * 100))

View file

@ -1,11 +1,12 @@
# coding=utf-8
import re
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
import os
import Queue
import threading
import datetime
import yaml
import time
import logging
@ -14,6 +15,7 @@ import octoprint.util.gcodeInterpreter as gcodeInterpreter
from octoprint.settings import settings
from octoprint.events import eventManager, Events
from octoprint.filemanager.destinations import FileDestinations
from werkzeug.utils import secure_filename
@ -107,12 +109,16 @@ class GcodeManager:
analysisResult = {}
dirty = False
if gcode.totalMoveTimeMinute:
analysisResult["estimatedPrintTime"] = util.getFormattedTimeDelta(datetime.timedelta(minutes=gcode.totalMoveTimeMinute))
analysisResult["estimatedPrintTime"] = gcode.totalMoveTimeMinute * 60
dirty = True
if gcode.extrusionAmount:
analysisResult["filament"] = "%.2fm" % (gcode.extrusionAmount / 1000)
analysisResult["filament"] = {
"length": gcode.extrusionAmount
}
if gcode.calculateVolumeCm3():
analysisResult["filament"] += " / %.2fcm³" % gcode.calculateVolumeCm3()
analysisResult["filament"].update({
"volume": gcode.calculateVolumeCm3()
})
dirty = True
if dirty:
@ -128,9 +134,58 @@ class GcodeManager:
with self._metadataFileAccessMutex:
with open(self._metadataFile, "r") as f:
self._metadata = yaml.safe_load(f)
if self._metadata is None:
self._metadata = {}
# TODO: Remove in a couple of versions (2013-12-21)
self._migrateMetadata()
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.)?"
hoursToSeconds = 60 * 60
minutesToSeconds = 60
updateCount = 0
for metadata in self._metadata.values():
if not "gcodeAnalysis" in metadata:
continue
updated = False
if "estimatedPrintTime" in metadata["gcodeAnalysis"]:
estimatedPrintTime = metadata["gcodeAnalysis"]["estimatedPrintTime"]
if isinstance(estimatedPrintTime, (str, unicode)):
match = re.match(printTimeRe, estimatedPrintTime)
if match:
metadata["gcodeAnalysis"]["estimatedPrintTime"] = int(match.group(1)) * hoursToSeconds + int(match.group(2)) * minutesToSeconds + int(match.group(3))
self._metadataDirty = True
updated = True
if "filament" in metadata["gcodeAnalysis"]:
filament = metadata["gcodeAnalysis"]["filament"]
if isinstance(filament, (str, unicode)):
match = re.match(filamentRe, filament)
if match:
metadata["gcodeAnalysis"]["filament"] = {
"length": int(float(match.group(1)) * 1000)
}
if match.group(3) is not None:
metadata["gcodeAnalysis"]["filament"].update({
"volume": float(match.group(3))
})
self._metadataDirty = True
updated = True
if updated:
updateCount += 1
self._saveMetadata()
self._logger.info("Updated %d sets of metadata to new format" % updateCount)
def _saveMetadata(self, force=False):
if not self._metadataDirty and not force:
return
@ -330,9 +385,9 @@ class GcodeManager:
statResult = os.stat(absolutePath)
fileData = {
"name": filename,
"size": util.getFormattedSize(statResult.st_size),
"bytes": statResult.st_size,
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
"size": statResult.st_size,
"origin": FileDestinations.LOCAL,
"date": int(statResult.st_ctime)
}
# enrich with additional metadata from analysis if available
@ -340,18 +395,18 @@ class GcodeManager:
for key in self._metadata[filename].keys():
if key == "prints":
val = self._metadata[filename][key]
formattedLast = None
last = None
if "last" in val and val["last"] is not None:
formattedLast = {
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(val["last"]["date"])),
last = {
"date": val["last"]["date"],
"success": val["last"]["success"]
}
formattedPrints = {
prints = {
"success": val["success"],
"failure": val["failure"],
"last": formattedLast
"last": last
}
fileData["prints"] = formattedPrints
fileData["prints"] = prints
else:
fileData[key] = self._metadata[filename][key]

View file

@ -92,8 +92,20 @@ class Printer():
)
self._stateMonitor.reset(
state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()},
jobData={"filename": None, "filesize": None, "estimatedPrintTime": None, "filament": None},
progress={"progress": None, "filepos": None, "printTime": None, "printTimeLeft": None},
jobData={
"file": {
"name": None,
"size": None,
"origin": None,
"date": None
},
"estimatedPrintTime": None,
"filament": {
"length": None,
"volume": None
}
},
progress={"completion": None, "filepos": None, "printTime": None, "printTimeLeft": None},
currentZ=None
)
@ -259,11 +271,7 @@ class Printer():
def _setCurrentZ(self, currentZ):
self._currentZ = currentZ
formattedCurrentZ = None
if self._currentZ:
formattedCurrentZ = "%.2f mm" % (self._currentZ)
self._stateMonitor.setCurrentZ(formattedCurrentZ)
self._stateMonitor.setCurrentZ(self._currentZ)
def _setState(self, state):
self._state = state
@ -282,22 +290,15 @@ class Printer():
self._printTime = printTime
self._printTimeLeft = printTimeLeft
formattedPrintTime = None
if (self._printTime):
formattedPrintTime = util.getFormattedTimeDelta(datetime.timedelta(seconds=self._printTime))
formattedPrintTimeLeft = None
if (self._printTimeLeft):
formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft))
formattedFilePos = None
if (filepos):
formattedFilePos = util.getFormattedSize(filepos)
self._stateMonitor.setProgress({"progress": self._progress, "filepos": formattedFilePos, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft})
self._stateMonitor.setProgress({
"completion": int(self._progress * 100) if self._progress is not None else None,
"filepos": filepos,
"printTime": int(self._printTime) if self._printTime is not None else None,
"printTimeLeft": int(self._printTimeLeft * 60) if self._printTimeLeft is not None else None
})
def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp):
currentTimeUtc = int(time.time() * 1000)
currentTimeUtc = int(time.time())
self._temps["actual"].append((currentTimeUtc, temp))
self._temps["target"].append((currentTimeUtc, targetTemp))
@ -321,21 +322,14 @@ class Printer():
else:
self._selectedFile = None
formattedFilename = None
formattedFilesize = None
estimatedPrintTime = None
fileMTime = None
date = None
filament = None
if filename:
formattedFilename = os.path.basename(filename)
# Use a string for mtime because it could be float and the
# javascript needs to exact match
if not sd:
fileMTime = str(os.stat(filename).st_mtime)
if filesize:
formattedFilesize = util.getFormattedSize(filesize)
date = int(os.stat(filename).st_ctime)
fileData = self._gcodeManager.getFileData(filename)
if fileData is not None and "gcodeAnalysis" in fileData.keys():
@ -344,7 +338,16 @@ class Printer():
if "filament" in fileData["gcodeAnalysis"].keys():
filament = fileData["gcodeAnalysis"]["filament"]
self._stateMonitor.setJobData({"filename": formattedFilename, "filesize": formattedFilesize, "estimatedPrintTime": estimatedPrintTime, "filament": filament, "sd": sd, "mtime": fileMTime})
self._stateMonitor.setJobData({
"file": {
"name": os.path.basename(filename),
"origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL,
"size": filesize,
"date": date
},
"estimatedPrintTime": estimatedPrintTime,
"filament": filament,
})
def _sendInitialStateUpdate(self, callback):
try:

View file

@ -19,7 +19,9 @@ from octoprint.settings import settings as s, valid_boolean_trues
api = Blueprint("api", __name__)
from . import control as api_control
from . import printer as api_printer
from . import job as api_job
from . import connection as api_connection
from . import files as api_files
from . import settings as api_settings
from . import timelapse as api_timelapse

View file

@ -0,0 +1,62 @@
# coding=utf-8
__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
from octoprint.settings import settings
from octoprint.printer import getConnectionOptions
from octoprint.server import printer, restricted_access, NO_CONTENT
from octoprint.server.api import api
import octoprint.util as util
@api.route("/connection", methods=["GET"])
def connectionState():
state, port, baudrate = printer.getCurrentConnection()
current = {
"state": state,
"port": port,
"baudrate": baudrate
}
return jsonify({"current": current, "options": getConnectionOptions()})
@api.route("/connection", methods=["POST"])
@restricted_access
def connectionCommand():
valid_commands = {
"connect": ["autoconnect"],
"disconnect": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if command == "connect":
options = getConnectionOptions()
port = None
baudrate = None
if "port" in data.keys():
port = data["port"]
if port not in options["ports"]:
return make_response("Invalid port: %s" % port, 400)
if "baudrate" in data.keys():
baudrate = data["baudrate"]
if baudrate not in options["baudrates"]:
return make_response("Invalid baudrate: %d" % baudrate, 400)
if "save" in data.keys() and data["save"]:
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
if "autoconnect" in data.keys():
settings().setBoolean(["serial", "autoconnect"], data["autoconnect"])
settings().save()
printer.connect(port=port, baudrate=baudrate)
elif command == "disconnect":
printer.disconnect()
return NO_CONTENT

View file

@ -22,7 +22,7 @@ from octoprint.server.api import api
def readGcodeFiles():
files = _getFileList(FileDestinations.LOCAL)
files.extend(_getFileList(FileDestinations.SDCARD))
return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads")))
@api.route("/files/<string:origin>", methods=["GET"])
@ -33,7 +33,7 @@ def readGcodeFilesForOrigin(origin):
files = _getFileList(origin)
if origin == FileDestinations.LOCAL:
return jsonify(files=files, free=util.getFormattedSize(util.getFreeBytes(settings().getBaseFolder("uploads"))))
return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads")))
else:
return jsonify(files=files)
@ -64,7 +64,6 @@ def _getFileList(origin):
files = gcodeManager.getAllFileData()
for file in files:
file.update({
"origin": FileDestinations.LOCAL,
"refs": {
"resource": url_for(".readGcodeFile", target=FileDestinations.LOCAL, filename=file["name"], _external=True),
"download": urlForDownload(FileDestinations.LOCAL, file["name"])

View file

@ -0,0 +1,57 @@
# coding=utf-8
__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, make_response, jsonify
from octoprint.server import printer, restricted_access, NO_CONTENT
from octoprint.server.api import api
import octoprint.util as util
@api.route("/job", methods=["POST"])
@restricted_access
def controlJob():
if not printer.isOperational():
return make_response("Printer is not operational", 409)
valid_commands = {
"start": [],
"restart": [],
"pause": [],
"cancel": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
activePrintjob = printer.isPrinting() or printer.isPaused()
if command == "start":
if activePrintjob:
return make_response("Printer already has an active print job, did you mean 'restart'?", 409)
printer.startPrint()
elif command == "restart":
if not printer.isPaused():
return make_response("Printer does not have an active print job or is not paused", 409)
printer.startPrint()
elif command == "pause":
if not activePrintjob:
return make_response("Printer is neither printing nor paused, 'pause' command cannot be performed", 409)
printer.togglePausePrint()
elif command == "cancel":
if not activePrintjob:
return make_response("Printer is neither printing nor paused, 'cancel' command cannot be performed", 409)
printer.cancelPrint()
return NO_CONTENT
@api.route("/job", methods=["GET"])
def jobState():
currentData = printer.getCurrentData()
return jsonify({
"job": currentData["job"],
"progress": currentData["progress"],
"state": currentData["state"]["stateString"]
})

View file

@ -11,128 +11,10 @@ from octoprint.server.api import api
import octoprint.util as util
#~~ Printer control
#~~ Heater
@api.route("/control/connection", methods=["GET"])
def connectionState():
state, port, baudrate = printer.getCurrentConnection()
current = {
"state": state,
"port": port,
"baudrate": baudrate
}
return jsonify({"current": current, "options": getConnectionOptions()})
@api.route("/control/connection", methods=["POST"])
@restricted_access
def connectionCommand():
valid_commands = {
"connect": ["autoconnect"],
"disconnect": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
if command == "connect":
options = getConnectionOptions()
port = None
baudrate = None
if "port" in data.keys():
port = data["port"]
if port not in options["ports"]:
return make_response("Invalid port: %s" % port, 400)
if "baudrate" in data.keys():
baudrate = data["baudrate"]
if baudrate not in options["baudrates"]:
return make_response("Invalid baudrate: %d" % baudrate, 400)
if "save" in data.keys() and data["save"]:
settings().set(["serial", "port"], port)
settings().setInt(["serial", "baudrate"], baudrate)
if "autoconnect" in data.keys():
settings().setBoolean(["serial", "autoconnect"], data["autoconnect"])
settings().save()
printer.connect(port=port, baudrate=baudrate)
elif command == "disconnect":
printer.disconnect()
return NO_CONTENT
@api.route("/control/printer/command", methods=["POST"])
@restricted_access
def printerCommand():
# TODO: document me
if not printer.isOperational():
return make_response("Printer is not operational", 409)
if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content type JSON", 400)
data = request.json
parameters = {}
if "parameters" in data.keys(): parameters = data["parameters"]
commands = []
if "command" in data.keys(): commands = [data["command"]]
elif "commands" in data.keys(): commands = data["commands"]
commandsToSend = []
for command in commands:
commandToSend = command
if len(parameters) > 0:
commandToSend = command % parameters
commandsToSend.append(commandToSend)
printer.commands(commandsToSend)
return NO_CONTENT
@api.route("/control/job", methods=["POST"])
@restricted_access
def controlJob():
if not printer.isOperational():
return make_response("Printer is not operational", 409)
valid_commands = {
"start": [],
"restart": [],
"pause": [],
"cancel": []
}
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
if response is not None:
return response
activePrintjob = printer.isPrinting() or printer.isPaused()
if command == "start":
if activePrintjob:
return make_response("Printer already has an active print job, did you mean 'restart'?", 409)
printer.startPrint()
elif command == "restart":
if not printer.isPaused():
return make_response("Printer does not have an active print job or is not paused", 409)
printer.startPrint()
elif command == "pause":
if not activePrintjob:
return make_response("Printer is neither printing nor paused, 'pause' command cannot be performed", 409)
printer.togglePausePrint()
elif command == "cancel":
if not activePrintjob:
return make_response("Printer is neither printing nor paused, 'cancel' command cannot be performed", 409)
printer.cancelPrint()
return NO_CONTENT
@api.route("/control/printer/heater", methods=["POST"])
@api.route("/printer/heater", methods=["POST"])
@restricted_access
def controlPrinterHotend():
if not printer.isOperational():
@ -194,7 +76,10 @@ def controlPrinterHotend():
return NO_CONTENT
@api.route("/control/printer/printhead", methods=["POST"])
##~~ Print head
@api.route("/printer/printhead", methods=["POST"])
@restricted_access
def controlPrinterPrinthead():
if not printer.isOperational() or printer.isPrinting():
@ -244,7 +129,10 @@ def controlPrinterPrinthead():
return NO_CONTENT
@api.route("/control/printer/feeder", methods=["POST"])
##~~ Feeder
@api.route("/printer/feeder", methods=["POST"])
@restricted_access
def controlPrinterFeeder():
if not printer.isOperational() or printer.isPrinting():
@ -270,14 +158,11 @@ def controlPrinterFeeder():
return NO_CONTENT
@api.route("/control/custom", methods=["GET"])
def getCustomControls():
# TODO: document me
customControls = settings().get(["controls"])
return jsonify(controls=customControls)
##~~ SD Card
@api.route("/control/printer/sd", methods=["POST"])
@api.route("/printer/sd", methods=["POST"])
@restricted_access
def sdCommand():
if not settings().getBoolean(["feature", "sdSupport"]):
@ -304,10 +189,52 @@ def sdCommand():
return NO_CONTENT
@api.route("/control/printer/sd", methods=["GET"])
@api.route("/printer/sd", methods=["GET"])
def sdState():
if not settings().getBoolean(["feature", "sdSupport"]):
return make_response("SD support is disabled", 404)
return jsonify(ready=printer.isSdReady())
##~~ Commands
@api.route("/printer/command", methods=["POST"])
@restricted_access
def printerCommand():
# TODO: document me
if not printer.isOperational():
return make_response("Printer is not operational", 409)
if not "application/json" in request.headers["Content-Type"]:
return make_response("Expected content type JSON", 400)
data = request.json
parameters = {}
if "parameters" in data.keys(): parameters = data["parameters"]
commands = []
if "command" in data.keys(): commands = [data["command"]]
elif "commands" in data.keys(): commands = data["commands"]
commandsToSend = []
for command in commands:
commandToSend = command
if len(parameters) > 0:
commandToSend = command % parameters
commandsToSend.append(commandToSend)
printer.commands(commandsToSend)
return NO_CONTENT
@api.route("/printer/command/custom", methods=["GET"])
def getCustomControls():
# TODO: document me
customControls = settings().get(["controls"])
return jsonify(controls=customControls)

View file

@ -277,3 +277,34 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
self._loadCurrentFiltersFromLocalStorage();
self._loadCurrentSortingFromLocalStorage();
}
function formatSize(bytes) {
var units = ["bytes", "KB", "MB", "GB"];
for (var i = 0; i < units.length; i++) {
if (bytes < 1024) {
return _.sprintf("%3.1f%s", bytes, units[i]);
}
bytes /= 1024;
}
return _.sprintf("%.1f%s", bytes, "TB");
}
function formatDuration(seconds) {
var s = seconds % 60;
var m = (seconds % 3600) / 60;
var h = seconds / 3600;
return _.sprintf("%02d:%02d:%02d", h, m, s);
}
function formatDate(unixTimestamp) {
return moment.unix(unixTimestamp).format("YYYY-MM-DD HH:mm");
}
function formatFilament(filament) {
var result = _.sprintf("%.02fm", (filament["length"] / 1000));
if (filament.hasOwnProperty("volume")) {
result += " / " + _.sprintf("%.02fcm³", filament["volume"]);
}
return result;
}

View file

@ -264,6 +264,10 @@ $(function() {
//~~ Offline overlay
$("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()});
//~~ Underscore setup
_.mixin(_.str.exports());
//~~ knockout.js bindings
ko.bindingHandlers.popover = {

View file

@ -30,7 +30,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
self.requestData = function() {
$.ajax({
url: API_BASEURL + "control/connection",
url: API_BASEURL + "connection",
method: "GET",
dataType: "json",
success: function(response) {
@ -100,7 +100,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
data["save"] = true;
$.ajax({
url: API_BASEURL + "control/connection",
url: API_BASEURL + "connection",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -112,7 +112,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
} else {
self.requestData();
$.ajax({
url: API_BASEURL + "control/connection",
url: API_BASEURL + "connection",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -43,7 +43,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
self.requestData = function() {
$.ajax({
url: API_BASEURL + "control/custom",
url: API_BASEURL + "printer/command/custom",
method: "GET",
dataType: "json",
success: function(response) {
@ -90,7 +90,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
data[axis] = distance * multiplier;
$.ajax({
url: API_BASEURL + "control/printer/printhead",
url: API_BASEURL + "printer/printhead",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -105,7 +105,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
}
$.ajax({
url: API_BASEURL + "control/printer/printhead",
url: API_BASEURL + "printer/printhead",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -127,7 +127,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
length = 5;
$.ajax({
url: API_BASEURL + "control/printer/feeder",
url: API_BASEURL + "printer/feeder",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -160,7 +160,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
return;
$.ajax({
url: API_BASEURL + "control/printer/command",
url: API_BASEURL + "printer/command",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -14,6 +14,11 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
self.isSdReady = ko.observable(undefined);
self.freeSpace = ko.observable(undefined);
self.freeSpaceString = ko.computed(function() {
if (!self.freeSpace())
return "-";
return formatSize(self.freeSpace());
});
// initialize list helper
self.listHelper = new ItemListHelper(
@ -174,7 +179,7 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
self._sendSdCommand = function(command) {
$.ajax({
url: API_BASEURL + "control/printer/sd",
url: API_BASEURL + "printer/sd",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",
@ -183,16 +188,18 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
}
self.getPopoverContent = function(data) {
var output = "<p><strong>Uploaded:</strong> " + data["date"] + "</p>";
var output = "<p><strong>Uploaded:</strong> " + formatDate(data["date"]) + "</p>";
if (data["gcodeAnalysis"]) {
output += "<p>";
output += "<strong>Filament:</strong> " + data["gcodeAnalysis"]["filament"] + "<br>";
output += "<strong>Estimated Print Time:</strong> " + data["gcodeAnalysis"]["estimatedPrintTime"];
if (data["gcodeAnalysis"]["filament"]) {
output += "<strong>Filament:</strong> " + formatFilament(data["gcodeAnalysis"]["filament"]) + "<br>";
}
output += "<strong>Estimated Print Time:</strong> " + formatDuration(data["gcodeAnalysis"]["estimatedPrintTime"]);
output += "</p>";
}
if (data["prints"] && data["prints"]["last"]) {
output += "<p>";
output += "<strong>Last Print:</strong> <span class=\"" + (data["prints"]["last"]["success"] ? "text-success" : "text-error") + "\">" + data["prints"]["last"]["date"] + "</span>";
output += "<strong>Last Print:</strong> <span class=\"" + (data["prints"]["last"]["success"] ? "text-success" : "text-error") + "\">" + formatDate(data["prints"]["last"]["date"]) + "</span>";
output += "</p>";
}
return output;

View file

@ -4,7 +4,7 @@ function GcodeViewModel(loginStateViewModel) {
self.loginState = loginStateViewModel;
self.loadedFilename = undefined;
self.loadedFileMTime = undefined;
self.loadedFileDate = undefined;
self.status = 'idle';
self.enabled = false;
@ -15,18 +15,18 @@ function GcodeViewModel(loginStateViewModel) {
GCODE.ui.initHandlers();
}
self.loadFile = function(filename, mtime){
self.loadFile = function(filename, date){
if (self.status == 'idle' && self.errorCount < 3) {
self.status = 'request';
$.ajax({
url: BASEURL + "downloads/files/local/" + filename,
data: { "mtime": mtime },
data: { "ctime": date },
type: "GET",
success: function(response, rstatus) {
if(rstatus === 'success'){
self.showGCodeViewer(response, rstatus);
self.loadedFilename=filename;
self.loadedFileMTime=mtime;
self.loadedFilename = filename;
self.loadedFileDate = date;
self.status = 'idle';
}
},
@ -55,20 +55,20 @@ function GcodeViewModel(loginStateViewModel) {
self._processData = function(data) {
if (!self.enabled) return;
if (!data.job.filename) return;
if (!data.job.file || !data.job.file.name) return;
if(self.loadedFilename && self.loadedFilename == data.job.filename &&
self.loadedFileMTime == data.job.mtime) {
if(self.loadedFilename && self.loadedFilename == data.job.file.name &&
self.loadedFileDate == data.job.file.date) {
if (data.state.flags && (data.state.flags.printing || data.state.flags.paused)) {
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.progress * 100);
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.completion);
if(cmdIndex){
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer);
}
}
self.errorCount = 0
} else if (data.job.filename && !data.job.sd) {
self.loadFile(data.job.filename, data.job.mtime);
} else if (data.job.file.name && data.job.file.origin != "sdcard") {
self.loadFile(data.job.file.name, data.job.file.date);
}
}

View file

@ -27,16 +27,36 @@ function PrinterStateViewModel(loginStateViewModel) {
self.currentHeight = ko.observable(undefined);
self.estimatedPrintTimeString = ko.computed(function() {
if (!self.estimatedPrintTime())
return "-";
return formatDuration(self.estimatedPrintTime());
});
self.filamentString = ko.computed(function() {
if (!self.filament())
return "-";
return formatFilament(self.filament());
});
self.byteString = ko.computed(function() {
if (!self.filesize())
return "-";
var filepos = self.filepos() ? self.filepos() : "-";
return filepos + " / " + self.filesize();
var filepos = self.filepos() ? formatSize(self.filepos()) : "-";
return filepos + " / " + formatSize(self.filesize());
});
self.heightString = ko.computed(function() {
if (!self.currentHeight())
return "-";
return self.currentHeight();
return _.sprintf("%.02fmm", self.currentHeight());
});
self.printTimeString = ko.computed(function() {
if (!self.printTime())
return "-";
return formatDuration(self.printTime());
});
self.printTimeLeftString = ko.computed(function() {
if (!self.printTimeLeft())
return "-";
return formatDuration(self.printTimeLeft());
})
self.progressString = ko.computed(function() {
if (!self.progress())
@ -97,16 +117,22 @@ function PrinterStateViewModel(loginStateViewModel) {
}
self._processJobData = function(data) {
self.filename(data.filename);
self.filesize(data.filesize);
if (data.file) {
self.filename(data.file.name);
self.filesize(data.file.size);
self.sd(data.file.origin == "sdcard");
} else {
self.filename(undefined);
self.filesize(undefined);
self.sd(undefined);
}
self.estimatedPrintTime(data.estimatedPrintTime);
self.filament(data.filament);
self.sd(data.sd);
}
self._processProgressData = function(data) {
if (data.progress) {
self.progress(Math.round(data.progress * 100));
if (data.completion) {
self.progress(data.completion);
} else {
self.progress(undefined);
}
@ -145,7 +171,7 @@ function PrinterStateViewModel(loginStateViewModel) {
self._jobCommand = function(command) {
$.ajax({
url: API_BASEURL + "control/job",
url: API_BASEURL + "job",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -126,10 +126,10 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
_.each(data, function(d) {
var time = d.currentTime;
self.temperatures.actual.push([time, d.temp]);
self.temperatures.target.push([time, d.targetTemp]);
self.temperatures.actualBed.push([time, d.bedTemp]);
self.temperatures.targetBed.push([time, d.targetBedTemp]);
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.actual = self.temperatures.actual.slice(-300);
@ -141,7 +141,17 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
}
self._processTemperatureHistoryData = function(data) {
self.temperatures = 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.updatePlot();
}
@ -217,7 +227,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
data[group][type] = parseInt(temp);
$.ajax({
url: API_BASEURL + "control/printer/heater",
url: API_BASEURL + "printer/heater",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

View file

@ -99,7 +99,7 @@ function TerminalViewModel(loginStateViewModel, settingsViewModel) {
if (command) {
$.ajax({
url: API_BASEURL + "control/printer/command",
url: API_BASEURL + "printer/command",
type: "POST",
dataType: "json",
contentType: "application/json; charset=UTF-8",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -115,12 +115,12 @@
<div class="accordion-inner">
Machine State: <strong data-bind="text: stateString"></strong><br>
File: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
Filament: <strong data-bind="text: filament"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTime"></strong><br>
Filament: <strong data-bind="text: filamentString"></strong><br>
Estimated Print Time: <strong data-bind="text: estimatedPrintTimeString"></strong><br>
Timelapse: <strong data-bind="text: timelapseString"></strong><br>
Height: <strong data-bind="text: heightString"></strong><br>
Print Time: <strong data-bind="text: printTime"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeft"></strong><br>
Print Time: <strong data-bind="text: printTimeString"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeftString"></strong><br>
Printed: <strong data-bind="text: byteString"></strong><br>
<div class="progress">
@ -183,7 +183,7 @@
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="css: $root.getSuccessClass($data), style: { 'font-weight': $root.listHelper.isSelected($data) ? 'bold' : 'normal' }, popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
<td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td>
<td class="gcode_files_size" data-bind="text: formatSize(size)"></td>
<td class="gcode_files_action">
<a href="#" class="icon-trash" title="Remove" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"></a>&nbsp;|&nbsp;<a href="#" class="icon-folder-open" title="Load" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"></a>&nbsp;|&nbsp;<a href="#" class="icon-print" title="Load and Print" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"></a>
</td>
@ -191,7 +191,7 @@
</tbody>
</table>
<div class="muted text-right">
<small>Free: <span data-bind="text: freeSpace"></span></small>
<small>Free: <span data-bind="text: freeSpaceString"></span></small>
</div>
<div class="pagination pagination-mini pagination-centered">
<ul>
@ -633,7 +633,8 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore-min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.string.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
@ -648,6 +649,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
<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>
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->