diff --git a/README.md b/README.md index b7e43f5..aaf06fa 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ If you like it, I would be thankful about a cup of coffee :) - [x] Used/Calculated length - [x] Used weight - [x] Filament cost +- [x] Slicer Settings (look [here](https://github.com/OllisGit/OctoPrint-PrintJobHistory/wiki/Slicer-Settings) for "how to use it") + ### UI features - [x] List all printjobs diff --git a/octoprint_PrintJobHistory/DatabaseManager.py b/octoprint_PrintJobHistory/DatabaseManager.py index 1263ff5..a0010f8 100644 --- a/octoprint_PrintJobHistory/DatabaseManager.py +++ b/octoprint_PrintJobHistory/DatabaseManager.py @@ -18,7 +18,7 @@ FORCE_CREATE_TABLES = False # SQL_LOGGING = True -CURRENT_DATABASE_SCHEME_VERSION = 2 +CURRENT_DATABASE_SCHEME_VERSION = 3 # List all Models MODELS = [PluginMetaDataModel, PrintJobModel, FilamentModel, TemperatureModel] @@ -58,22 +58,62 @@ def _createOrUpgradeSchemeIfNecessary(self): self._logger.info("We need to upgrade the database scheme from: '" + str(currentDatabaseSchemeVersion) + "' to: '" + str(CURRENT_DATABASE_SCHEME_VERSION) + "'") try: - self._backupDatabaseFile() - if (currentDatabaseSchemeVersion < 2): - self._upgradeFrom1To2() + self.backupDatabaseFile(self._databasePath) + self._upgradeDatabase(currentDatabaseSchemeVersion, CURRENT_DATABASE_SCHEME_VERSION) except Exception as e: self._logger.error("Error during database upgrade!!!!") - self._logger.error(str(e)) + self._logger.exception(e) + return self._logger.info("Database-scheme successfully upgraded.") pass + def _upgradeDatabase(self,currentDatabaseSchemeVersion, targetDatabaseSchemeVersion): + + migrationFunctions = [self._upgradeFrom1To2, self._upgradeFrom2To3, self._upgradeFrom3To4, self._upgradeFrom4To5] + + for migrationMethodIndex in range(currentDatabaseSchemeVersion -1, targetDatabaseSchemeVersion -1): + self._logger.info("Database migration from '" + str(migrationMethodIndex + 1) + "' to '" + str(migrationMethodIndex + 2) + "'") + migrationFunctions[migrationMethodIndex]() + pass + pass + + def _upgradeFrom4To5(self): + self._logger.info(" Starting 4 -> 5") + + def _upgradeFrom3To4(self): + self._logger.info(" Starting 3 -> 4") + + def _upgradeFrom2To3(self): + self._logger.info(" Starting 2 -> 3") + # What is changed: + # - PrintJobModel: + # - Add Column: slicerSettingsAsText + + connection = sqlite3.connect(self._databaseFileLocation) + cursor = connection.cursor() + + sql = """ + PRAGMA foreign_keys=off; + BEGIN TRANSACTION; + + ALTER TABLE 'pjh_printjobmodel' ADD 'slicerSettingsAsText' TEXT; + + UPDATE 'pjh_pluginmetadatamodel' SET value=3 WHERE key='databaseSchemeVersion'; + COMMIT; + PRAGMA foreign_keys=on; + """ + cursor.executescript(sql) + + connection.close() + self._logger.info(" Successfully 2 -> 3") + pass + def _upgradeFrom1To2(self): + self._logger.info(" Starting 1 -> 2") # What is changed: # - PrintJobModel: Add Column fileOrigin # - FilamentModel: Several ColumnTypes were wrong - - connection = sqlite3.connect(self._databaseFileLocation) cursor = connection.cursor() @@ -115,7 +155,6 @@ def _upgradeFrom1To2(self): cursor.executescript(sql) connection.close() - pass diff --git a/octoprint_PrintJobHistory/__init__.py b/octoprint_PrintJobHistory/__init__.py index 1c02f5c..c3ae479 100644 --- a/octoprint_PrintJobHistory/__init__.py +++ b/octoprint_PrintJobHistory/__init__.py @@ -21,6 +21,7 @@ from peewee import DoesNotExist from .common.SettingsKeys import SettingsKeys +from .common.SlicerSettingsParser import SlicerSettingsParser from .api.PrintJobHistoryAPI import PrintJobHistoryAPI from .api import TransformPrintJob2JSON from .DatabaseManager import DatabaseManager @@ -302,10 +303,6 @@ def _addTemperatureToPrintModel(self, printJobModel, bedTemp, toolTemp): #### print job finished def _printJobFinished(self, printStatus, payload): - self._currentPrintJobModel.printEndDateTime = datetime.datetime.now() - self._currentPrintJobModel.duration = (self._currentPrintJobModel.printEndDateTime - self._currentPrintJobModel.printStartDateTime).total_seconds() - self._currentPrintJobModel.printStatusResult = printStatus - captureMode = self._settings.get([SettingsKeys.SETTINGS_KEY_CAPTURE_PRINTJOBHISTORY_MODE]) if (captureMode == SettingsKeys.KEY_CAPTURE_PRINTJOBHISTORY_MODE_NONE): return @@ -322,6 +319,20 @@ def _printJobFinished(self, printStatus, payload): # capture the print if (captureThePrint == True): self._logger.info("Start capturing print job") + # Core Data + self._currentPrintJobModel.printEndDateTime = datetime.datetime.now() + self._currentPrintJobModel.duration = ( + self._currentPrintJobModel.printEndDateTime - self._currentPrintJobModel.printStartDateTime).total_seconds() + self._currentPrintJobModel.printStatusResult = printStatus + + # Slicer Settings + selectedFilename = payload.get("path") + selectedFile = self._file_manager.path_on_disk(payload.get("origin"), selectedFilename) + slicerSettings = SlicerSettingsParser(self._logger).extractSlicerSettings(selectedFile, None) + if (slicerSettings.settingsAsText != None and len(slicerSettings.settingsAsText) != 0): + self._currentPrintJobModel.slicerSettingsAsText = slicerSettings.settingsAsText + + # Image if self._settings.get_boolean([SettingsKeys.SETTINGS_KEY_TAKE_SNAPSHOT_AFTER_PRINT]): self._cameraManager.takeSnapshotAsync( CameraManager.buildSnapshotFilename(self._currentPrintJobModel.printStartDateTime), diff --git a/octoprint_PrintJobHistory/common/SlicerSettingsParser.py b/octoprint_PrintJobHistory/common/SlicerSettingsParser.py new file mode 100644 index 0000000..713ad6d --- /dev/null +++ b/octoprint_PrintJobHistory/common/SlicerSettingsParser.py @@ -0,0 +1,202 @@ +# coding=utf-8 +from __future__ import absolute_import + +import logging +import os + +MAX_GCODE_LINES_BEFORE_STOP_READING = 10 +LINE_RESULT_GCODE = "LR:gcode" +LINE_RESULT_SETTINGS = "LR:settings" +LINE_RESULT_OTHERS = "LR:others" + +# Model of Slicer Settings +class SlicerSettings(object): + + def __init__(self): + self.settingsAsText = "" + self.settingsAsDict = dict() + + def isKeyAlreadyExtracted(self, key): + return key in self.settingsAsDict + + def addKeyValueSetting(self, key, value): + self.settingsAsDict.update({key:value}) + + def addKeyValueSettingsAsText(self, settingsText): + self.settingsAsText += settingsText + +########################################################### +# Parse reads all 'key = values' out of the gcode file +# - It reads to top and the bottom +# - It reads till a block of gcode-commands will be detected +# - No overlappig between the top-block and the bottom-block +class SlicerSettingsParser(object): + + def __init__(self, parentLogger): + self._logger = logging.getLogger(parentLogger.name + "." + self.__class__.__name__) + # self._logger.setLevel(logging.DEBUG) + + def extractSlicerSettings(self, gcodeFilePath, includedSettingsKeyList): + + self._logger.info("Start parsing Slicer-Settings") + # Read the file from top + # Read the file from bottom + # read key-value + + # - Make sure that the top-region is not overlappig with bottom region + # - Stop reading after you read a definied amount of gcode continusly (no interruption -> gcode-block) + slicerSettings = SlicerSettings() + + lastLineResult = None # type of line + gcodeCount = 0 + readingOrder = 0 # 0=forward; 1=reverse 2++=finished + reverseReadinStarted = False + lastTopFilePosition = 0 # needed for overlapping detection of top-region and bottom-region + lineNumber = 0 + with open(gcodeFilePath, 'r') as fileHandle: + while True: + + if (readingOrder == 0): + # Forward reading + line = fileHandle.readline() + lastTopFilePosition = fileHandle.tell() + lineNumber += 1 + pass + else: + # Reverse reading + # Jump to the end + if (reverseReadinStarted == False): + fileHandle.seek(0, os.SEEK_END) + reverseReadinStarted = True + lineNumber = 0 + gcodeCount = 0 + line = self.nextReversedLine(fileHandle, lastTopFilePosition) + lineNumber += 1 + + if (line == ''): + # EOF reached + readingOrder += 1 + + if (readingOrder == 1): + lineNumber = 0 + continue + else: + # finaly top/Bottom reading is done + break + + lineResult = self.processLine(line, slicerSettings) + # print(lineResult) + if (lineResult == LINE_RESULT_GCODE): + gcodeCount += 1 + if (lastLineResult == LINE_RESULT_GCODE): + if (gcodeCount >= MAX_GCODE_LINES_BEFORE_STOP_READING): + # forward reading finished, switch to reverse + if (reverseReadinStarted == True): + # finaly top/Bottom reading is done + break + readingOrder += 1 + continue + else: + gcodeCount = 0 + lastLineResult = lineResult + + debugInformation = "ORDER: " + str(readingOrder) + " LineNumber: " + str(lineNumber) + " GCodeFound: " + str( + gcodeCount) + # print(debugInformation) + self._logger.debug(debugInformation) + + pass + self._logger.debug(" Slicer-Settings:") + self._logger.debug(slicerSettings.settingsAsDict) + self._logger.info("Finished parsing Slicer-Settings") + return slicerSettings + + + # Process a Single-Line + def processLine(self, line, slicerSettings): + # print(line) + if (line == None or line == '' ): + # EMPTY + return LINE_RESULT_OTHERS + + line = line.lstrip() + if (len(line) == 0): + # EMPTY + return LINE_RESULT_OTHERS + + if (line[0] == ";"): + # special comments + if ("enerated" in line): + key = "generated by" + value = line[1:] + slicerSettings.addKeyValueSetting(key, value) + slicerSettings.addKeyValueSettingsAsText(line) + return LINE_RESULT_SETTINGS + # Cura put JSON fragments to SETTINGS2_ comments -> ignore it + if (";SETTING_" in line): + return LINE_RESULT_OTHERS + + # KeyValue extraction + if ('=' in line): + keyValue = line.split('=', 1) # 1 == only the first = + key = keyValue[0].strip() + value = keyValue[1].strip() + if (slicerSettings.isKeyAlreadyExtracted(key) == False): + slicerSettings.addKeyValueSetting(key, value) + slicerSettings.addKeyValueSettingsAsText(line) + return LINE_RESULT_SETTINGS + + return LINE_RESULT_OTHERS + + # Must be a gcode + return LINE_RESULT_GCODE + + def nextReversedLine(self, fileHandle, lastTopFilePosition): + line = '' + + filePosition = fileHandle.tell() + if (filePosition <=0): + return line + if (filePosition <= lastTopFilePosition): + self._logger.debug("We reached the already parsed top-region during reverse-parsing") + print("We reached the already parsed top-region during reverse-parsing") + return line + + while filePosition >= 0: + fileHandle.seek(filePosition) + current_char = fileHandle.read(1) + line += current_char + + if (filePosition == 0): + line = line[::-1] + fileHandle.seek(0) + break + + fileHandle.seek(filePosition - 1) + next_char = fileHandle.read(1) + if next_char == "\n": + line = line[::-1] + # HACK + if len(line)==0: + line = " " + fileHandle.seek(filePosition - 1) + break + filePosition -= 1 + + return line + + + + +if __name__ == '__main__': + parsingFilename = "/Users/o0632/0_Projekte/3DDruck/OctoPrint/OctoPrint-PrintJobHistory/testdata/slicer-settings/CURA_schieberdeckel2.gcode" + #parsingFilename = "/Users/o0632/0_Projekte/3DDruck/OctoPrint/OctoPrint-PrintJobHistory/testdata/slicer-settings/simple.gcode" + + + testLogger = logging.getLogger("testLogger") + settingsParser = SlicerSettingsParser(testLogger) + slicerSettings = settingsParser.extractSlicerSettings(parsingFilename, None) + + print("TEXT: "+slicerSettings.settingsAsText) + print(slicerSettings.settingsAsDict) + print("done") diff --git a/octoprint_PrintJobHistory/models/PrintJobModel.py b/octoprint_PrintJobHistory/models/PrintJobModel.py index 506cad3..6bf7c24 100644 --- a/octoprint_PrintJobHistory/models/PrintJobModel.py +++ b/octoprint_PrintJobHistory/models/PrintJobModel.py @@ -23,6 +23,7 @@ class PrintJobModel(BaseModel): noteHtml = CharField(null=True) printedLayers = CharField(null=True) printedHeight = CharField(null=True) + slicerSettingsAsText = TextField(null=True) allFilaments = None allTemperatures = None diff --git a/octoprint_PrintJobHistory/static/js/PrintJobHistory-EditJobDialog.js b/octoprint_PrintJobHistory/static/js/PrintJobHistory-EditJobDialog.js index c2859ed..440f0ba 100644 --- a/octoprint_PrintJobHistory/static/js/PrintJobHistory-EditJobDialog.js +++ b/octoprint_PrintJobHistory/static/js/PrintJobHistory-EditJobDialog.js @@ -64,6 +64,9 @@ function PrintJobHistoryEditDialog(){ function _restoreSnapshotImageSource(){ _setSnapshotImageSource(self.lastSnapshotImageSource); } + + self.isSlicerSettingsPresent = ko.observable(false); + /////////////////////////////////////////////////////////////////////////////////////////////////// INIT this.init = function(apiClient, webCamSettings){ @@ -135,8 +138,29 @@ function PrintJobHistoryEditDialog(){ self.snapshotUploadInProgress(false); } }); + + self.slicerSettingsDialog = $("#dialog_printJobHistory_slicerSettings"); + } + + + this.showSlicerSettingsDialog = function(){ + self.slicerSettingsDialog.modal({ + //minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); } + keyboard: false, + clickClose: false, + showClose: false, + backdrop: "static" + }).css({ + width: 'auto', + 'margin-left': function() { return -($(this).width() /2); } + }); + } + + this.closeSlicerSettingsDialog = function(){ + self.slicerSettingsDialog.modal('hide'); } + this.isInitialized = function() { return self.apiClient != null; } @@ -169,6 +193,11 @@ function PrintJobHistoryEditDialog(){ self.noteEditor.setContents(deltaFormat, 'api'); } + slicerSettingsPresent = self.printJobItemForEdit.slicerSettingsAsText(); + if (slicerSettingsPresent != null && slicerSettingsPresent.length != 0){ + self.isSlicerSettingsPresent(true); + } + self.editPrintJobItemDialog.modal({ //minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); } keyboard: false, @@ -179,6 +208,8 @@ function PrintJobHistoryEditDialog(){ width: 'auto', 'margin-left': function() { return -($(this).width() /2); } }); + + } diff --git a/octoprint_PrintJobHistory/static/js/PrintJobHistory.js b/octoprint_PrintJobHistory/static/js/PrintJobHistory.js index dd38b36..155d750 100644 --- a/octoprint_PrintJobHistory/static/js/PrintJobHistory.js +++ b/octoprint_PrintJobHistory/static/js/PrintJobHistory.js @@ -51,6 +51,7 @@ $(function() { this.usedCost = ko.observable(); this.snapshotFilename = ko.observable(); + this.slicerSettingsAsText = ko.observable(); /* this.successful = ko.computed(function() { return this.success() == 1; @@ -141,6 +142,7 @@ $(function() { } this.snapshotFilename(updateData.snapshotFilename); + this.slicerSettingsAsText(updateData.slicerSettingsAsText) }; @@ -202,7 +204,8 @@ $(function() { "noteText" : "Good output of Legolas", "noteDeltaFormat" : "Good output of Legolas", "noteHtml" : "

Good output of Legolas

", - "snapshotFilename" : ko.observable("20191003-123322") + "snapshotFilename" : ko.observable("20191003-123322"), + "slicerSettingsAsText" : ko.observable() },{ "databaseId" : ko.observable(2), @@ -230,8 +233,8 @@ $(function() { "noteText" : "Bad quality", "noteDeltaFormat" : "Bad quality", "noteHtml" : "

Bad quality,/h2>", - "snapshotFilename" : ko.observable("20191003-153312") - + "snapshotFilename" : ko.observable("20191003-153312"), + "slicerSettingsAsText" : ko.observable() } ]; // self.printJobHistorylistHelper.updateItems(printHistoryJobItems); diff --git a/octoprint_PrintJobHistory/templates/PrintJobHistory_tab_dialogs.jinja2 b/octoprint_PrintJobHistory/templates/PrintJobHistory_tab_dialogs.jinja2 index 36129a7..73fd6f3 100644 --- a/octoprint_PrintJobHistory/templates/PrintJobHistory_tab_dialogs.jinja2 +++ b/octoprint_PrintJobHistory/templates/PrintJobHistory_tab_dialogs.jinja2 @@ -15,12 +15,13 @@ @@ -202,8 +203,13 @@ - +
+ Show Slicer Settings + +
+ +
@@ -317,4 +323,27 @@
+ + + + + + + diff --git a/octoprint_PrintJobHistory/test/test_DatabaseManager.py b/octoprint_PrintJobHistory/test/test_DatabaseManager.py index b3d22e8..3c8b609 100644 --- a/octoprint_PrintJobHistory/test/test_DatabaseManager.py +++ b/octoprint_PrintJobHistory/test/test_DatabaseManager.py @@ -8,6 +8,7 @@ def clientOutput(message1, message2): print(message1) print(message2) +logging.basicConfig(level=logging.DEBUG) testLogger = logging.getLogger("testLogger") logging.info("Start Database-Test") databaseManager = DatabaseManager(testLogger, True) @@ -25,15 +26,21 @@ def convert(value): # result = value.encode("utf-8") # return result -printJob = databaseManager.loadPrintJob(1) -# python2 -> unicode -# python3 -> str (es gibt kein unicode) -fileName = printJob.fileName -convert(fileName) -fileSize = printJob.fileSize -convert(fileSize) -printStartDateTime = printJob.printStartDateTime -convert(printStartDateTime) -diameter = printJob.loadFilamentFromAssoziation().diameter -convert(diameter) + +# printJob = databaseManager.loadPrintJob(1) +# # python2 -> unicode +# # python3 -> str (es gibt kein unicode) +# fileName = printJob.fileName +# convert(fileName) +# fileSize = printJob.fileSize +# convert(fileSize) +# printStartDateTime = printJob.printStartDateTime +# convert(printStartDateTime) +# diameter = printJob.loadFilamentFromAssoziation().diameter +# convert(diameter) + +currentScheme = 1 +targetScheme = 5 + +databaseManager._upgradeDatabase(currentScheme, targetScheme) diff --git a/setup.py b/setup.py index 69ab381..6de3c9e 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "Print Job History" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "1.1.0" +plugin_version = "1.2.0" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module