diff options
author | David Crocker <dcrocker@eschertech.com> | 2018-04-01 13:45:55 +0300 |
---|---|---|
committer | David Crocker <dcrocker@eschertech.com> | 2018-04-01 13:45:55 +0300 |
commit | 4d151a5d02a8bca3810e4a75bf225136548fa1ab (patch) | |
tree | c81234e4e1ad675310272c6ffaf74a94f9ac6438 | |
parent | de270d2a00fd83cc3961a749432bff940601f1dd (diff) |
More RTOS work
Movd the file info parser out of PrintManager into a separate class
within Storage and made it thread safe
Added "Off" status for DWC and PanelDue
Fixed bug with using G1 S1 Ennn on a delta printer
-rw-r--r-- | src/GCodes/GCodes.cpp | 12 | ||||
-rw-r--r-- | src/GCodes/GCodes2.cpp | 6 | ||||
-rw-r--r-- | src/Networking/HttpResponder.cpp | 7 | ||||
-rw-r--r-- | src/Networking/HttpResponder.h | 4 | ||||
-rw-r--r-- | src/Platform.cpp | 5 | ||||
-rw-r--r-- | src/Platform.h | 1 | ||||
-rw-r--r-- | src/PrintMonitor.cpp | 839 | ||||
-rw-r--r-- | src/PrintMonitor.h | 61 | ||||
-rw-r--r-- | src/RepRap.cpp | 78 | ||||
-rw-r--r-- | src/RepRap.h | 1 | ||||
-rw-r--r-- | src/Storage/FileInfoParser.cpp | 739 | ||||
-rw-r--r-- | src/Storage/FileInfoParser.h | 86 | ||||
-rw-r--r-- | src/Storage/MassStorage.h | 4 | ||||
-rw-r--r-- | src/Version.h | 2 |
14 files changed, 970 insertions, 875 deletions
diff --git a/src/GCodes/GCodes.cpp b/src/GCodes/GCodes.cpp index 3d82b3c7..00ada42f 100644 --- a/src/GCodes/GCodes.cpp +++ b/src/GCodes/GCodes.cpp @@ -2294,12 +2294,6 @@ const char* GCodes::DoStraightMove(GCodeBuffer& gb, bool isCoordinated) if (moveBuffer.moveType != 0) { - // Special move. If on a delta, movement must be relative. - if (!gb.MachineState().axesRelative && reprap.GetMove().GetKinematics().GetKinematicsType() == KinematicsType::linearDelta) - { - return "G0/G1: attempt to move delta motors to absolute positions"; - } - // This may be a raw motor move, in which case we need the current raw motor positions in moveBuffer.coords. // If it isn't a raw motor move, it will still be applied without axis or bed transform applied, // so make sure the initial coordinates don't have those either to avoid unwanted Z movement. @@ -2317,6 +2311,12 @@ const char* GCodes::DoStraightMove(GCodeBuffer& gb, bool isCoordinated) { if (gb.Seen(axisLetters[axis])) { + // If it is a special move on a delta, movement must be relative. + if (moveBuffer.moveType != 0 && !gb.MachineState().axesRelative && reprap.GetMove().GetKinematics().GetKinematicsType() == KinematicsType::linearDelta) + { + return "G0/G1: attempt to move individual motors of a delta machine to absolute positions"; + } + SetBit(axesMentioned, axis); const float moveArg = gb.GetFValue() * distanceScale; if (moveBuffer.moveType != 0) diff --git a/src/GCodes/GCodes2.cpp b/src/GCodes/GCodes2.cpp index 19065eab..6d21eeba 100644 --- a/src/GCodes/GCodes2.cpp +++ b/src/GCodes/GCodes2.cpp @@ -845,7 +845,7 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) String<MaxFilenameLength> filename; const bool gotFilename = gb.GetUnprecedentedString(filename.GetRef()); OutputBuffer *fileInfoResponse; - const bool done = reprap.GetPrintMonitor().GetFileInfoResponse((gotFilename) ? filename.Pointer() : nullptr, fileInfoResponse); + const bool done = reprap.GetFileInfoResponse((gotFilename) ? filename.Pointer() : nullptr, fileInfoResponse, false); if (done) { fileInfoResponse->cat('\n'); @@ -1892,9 +1892,9 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, const StringRef& reply) case 204: // Set max travel and printing accelerations { bool seen = false; - if (gb.Seen('S') && platform.Emulating() == Compatibility::marlin) + if (gb.Seen('S')) { - // For backwards compatibility e.g. with Cura, set both accelerations as Marlin does. + // For backwards compatibility with old versions of Marlin (e.g. for Cura and the Prusa fork of slic3r), set both accelerations const float acc = gb.GetFValue(); platform.SetMaxPrintingAcceleration(acc); platform.SetMaxTravelAcceleration(acc); diff --git a/src/Networking/HttpResponder.cpp b/src/Networking/HttpResponder.cpp index 88d5f6e1..58208494 100644 --- a/src/Networking/HttpResponder.cpp +++ b/src/Networking/HttpResponder.cpp @@ -116,7 +116,7 @@ bool HttpResponder::Spin() // no break case ResponderState::gettingFileInfo: - if (SendFileInfo()) + if (SendFileInfo(millis() - startedGettingFileInfoAt >= MaxFileInfoGetTime)) { fileInfoLock.Release(this); // release the lock } @@ -562,6 +562,7 @@ bool HttpResponder::GetJsonResponse(const char* request, OutputBuffer *&response // Simple rr_fileinfo call to get info about the file being printed filenameBeingProcessed[0] = 0; } + startedGettingFileInfoAt = millis(); responderState = ResponderState::gettingFileInfoLock; return false; } @@ -618,10 +619,10 @@ const char* HttpResponder::GetKeyValue(const char *key) const // Called to process a FileInfo request, which may take several calls // When we have finished, set the state back to free. -bool HttpResponder::SendFileInfo() +bool HttpResponder::SendFileInfo(bool quitEarly) { OutputBuffer *jsonResponse = nullptr; - const bool gotFileInfo = reprap.GetPrintMonitor().GetFileInfoResponse(filenameBeingProcessed, jsonResponse); + const bool gotFileInfo = reprap.GetFileInfoResponse(filenameBeingProcessed, jsonResponse, quitEarly); if (gotFileInfo) { // Got it - send the response now diff --git a/src/Networking/HttpResponder.h b/src/Networking/HttpResponder.h index 4ba99d7c..04ba362b 100644 --- a/src/Networking/HttpResponder.h +++ b/src/Networking/HttpResponder.h @@ -37,6 +37,7 @@ private: static const size_t MaxQualKeys = 5; // max number of key/value pairs in the qualifier static const size_t MaxHeaders = 30; // max number of key/value pairs in the headers static const uint32_t HttpSessionTimeout = 8000; // HTTP session timeout in milliseconds + static const uint32_t MaxFileInfoGetTime = 1800; // maximum length of time we spend getting file info, to avoid the client timing out (actual time will be a little longer than this) enum class HttpParseState { @@ -80,7 +81,7 @@ private: bool GetJsonResponse(const char* request, OutputBuffer *&response, bool& keepOpen); void ProcessMessage(); void RejectMessage(const char* s, unsigned int code = 500); - bool SendFileInfo(); + bool SendFileInfo(bool quitEarly); void DoUpload(); @@ -102,6 +103,7 @@ private: size_t numHeaderKeys; // number of keys we have found, <= maxHeaders // rr_fileinfo requests + uint32_t startedGettingFileInfoAt; // when we started trying to get file info char filenameBeingProcessed[MaxFilenameLength]; // The filename being processed (for rr_fileinfo) // Keeping track of HTTP sessions diff --git a/src/Platform.cpp b/src/Platform.cpp index 656018d6..17f25317 100644 --- a/src/Platform.cpp +++ b/src/Platform.cpp @@ -1640,6 +1640,11 @@ bool Platform::IsPowerOk() const return !autoSaveEnabled || currentVin > autoPauseReading; } +bool Platform::HasVinPower() const +{ + return driversPowered; // not quite right because drivers are disabled if we get over-voltage too, but OK for the status report +} + void Platform::EnableAutoSave(float saveVoltage, float resumeVoltage) { autoPauseReading = PowerVoltageToAdcReading(saveVoltage); diff --git a/src/Platform.h b/src/Platform.h index f1a16a35..32d3e270 100644 --- a/src/Platform.h +++ b/src/Platform.h @@ -551,6 +551,7 @@ public: void GetPowerVoltages(float& minV, float& currV, float& maxV) const; float GetCurrentPowerVoltage() const; bool IsPowerOk() const; + bool HasVinPower() const; void DisableAutoSave(); void EnableAutoSave(float saveVoltage, float resumeVoltage); bool GetAutoSaveSettings(float& saveVoltage, float&resumeVoltage); diff --git a/src/PrintMonitor.cpp b/src/PrintMonitor.cpp index 139b6d05..43f6bb02 100644 --- a/src/PrintMonitor.cpp +++ b/src/PrintMonitor.cpp @@ -28,9 +28,7 @@ Licence: GPL PrintMonitor::PrintMonitor(Platform& p, GCodes& gc) : platform(p), gCodes(gc), isPrinting(false), printStartTime(0), pauseStartTime(0), totalPauseTime(0), heatingUp(false), currentLayer(0), warmUpDuration(0.0), firstLayerDuration(0.0), firstLayerFilament(0.0), firstLayerProgress(0.0), lastLayerChangeTime(0.0), - lastLayerFilament(0.0), lastLayerZ(0.0), numLayerSamples(0), layerEstimatedTimeLeft(0.0), parseState(notParsing), - fileBeingParsed(nullptr), fileOverlapLength(0), printingFileParsed(false), accumulatedParseTime(0), - accumulatedReadTime(0), accumulatedSeekTime(0) + lastLayerFilament(0.0), lastLayerZ(0.0), numLayerSamples(0), layerEstimatedTimeLeft(0.0), printingFileParsed(false) { filenameBeingPrinted[0] = 0; } @@ -40,12 +38,46 @@ void PrintMonitor::Init() longWait = lastUpdateTime = millis(); } +// Get information for the specified file, or the currently printing file, in JSON format +bool PrintMonitor::GetPrintingFileInfoResponse(OutputBuffer *&response) const +{ + // If the file being printed hasn't been processed yet or if we cannot write the response, try again later + if (!printingFileParsed || !OutputBuffer::Allocate(response)) + { + return false; + } + + // Poll file info about a file currently being printed + response->printf("{\"err\":0,\"size\":%lu,\"height\":%.2f,\"firstLayerHeight\":%.2f,\"layerHeight\":%.2f,\"filament\":", + printingFileInfo.fileSize, (double)printingFileInfo.objectHeight, (double)printingFileInfo.firstLayerHeight, (double)printingFileInfo.layerHeight); + char ch = '['; + if (printingFileInfo.numFilaments == 0) + { + response->cat(ch); + } + else + { + for (size_t i = 0; i < printingFileInfo.numFilaments; ++i) + { + response->catf("%c%.1f", ch, (double)printingFileInfo.filamentNeeded[i]); + ch = ','; + } + } + response->cat("],\"generatedBy\":"); + response->EncodeString(printingFileInfo.generatedBy.c_str(), printingFileInfo.generatedBy.Capacity(), false); + response->catf(",\"printDuration\":%d,\"fileName\":", (int)GetPrintDuration()); + response->EncodeString(filenameBeingPrinted.c_str(), filenameBeingPrinted.Capacity(), false); + response->cat('}'); + + return true; +} + void PrintMonitor::Spin() { // File information about the file being printed must be available before layer estimations can be made if (filenameBeingPrinted[0] != 0 && !printingFileParsed) { - printingFileParsed = GetFileInfo(platform.GetGCodeDir(), filenameBeingPrinted.c_str(), printingFileInfo); + printingFileParsed = platform.GetMassStorage()->GetFileInfo(platform.GetGCodeDir(), filenameBeingPrinted.c_str(), printingFileInfo, false); if (!printingFileParsed) { platform.ClassReport(longWait); @@ -166,7 +198,7 @@ float PrintMonitor::GetWarmUpDuration() const // Notifies this class that a file has been set for printing void PrintMonitor::StartingPrint(const char* filename) { - printingFileParsed = GetFileInfo(platform.GetGCodeDir(), filename, printingFileInfo); + printingFileParsed = platform.GetMassStorage()->GetFileInfo(platform.GetGCodeDir(), filename, printingFileInfo, false); filenameBeingPrinted.copy(filename); } @@ -273,407 +305,6 @@ void PrintMonitor::StoppedPrint() lastLayerChangeTime = lastLayerFilament = lastLayerZ = 0.0; } -bool PrintMonitor::GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info) -{ - if (parseState != notParsing && !StringEquals(fileName, filenameBeingParsed.c_str())) - { - // We are already parsing a different file - if (millis() - lastFileParseTime < MaxFileParseInterval) - { - return false; // try again later - } - - // Time this client out because it has probably disconnected - fileBeingParsed->Close(); - parseState = notParsing; - } - - if (parseState == notParsing) - { - // See if we can access the file - // Webserver may call rr_fileinfo for a directory, check this case here - if (platform.GetMassStorage()->DirectoryExists(directory, fileName)) - { - info.isValid = false; - return true; - } - - fileBeingParsed = platform.OpenFile(directory, fileName, OpenMode::read); - if (fileBeingParsed == nullptr) - { - // Something went wrong - we cannot open it - info.isValid = false; - return true; - } - - // File has been opened, let's start now - filenameBeingParsed.copy(fileName); - fileOverlapLength = 0; - - // Set up the info struct - parsedFileInfo.isValid = true; - parsedFileInfo.fileSize = fileBeingParsed->Length(); - parsedFileInfo.lastModifiedTime = platform.GetMassStorage()->GetLastModifiedTime(directory, fileName); - parsedFileInfo.firstLayerHeight = 0.0; - parsedFileInfo.objectHeight = 0.0; - parsedFileInfo.layerHeight = 0.0; - parsedFileInfo.numFilaments = 0; - parsedFileInfo.generatedBy[0] = 0; - for(size_t extr = 0; extr < MaxExtruders; extr++) - { - parsedFileInfo.filamentNeeded[extr] = 0.0; - } - - // Record some debug values here - if (reprap.Debug(modulePrintMonitor)) - { - accumulatedReadTime = accumulatedParseTime = 0; - platform.MessageF(UsbMessage, "-- Parsing file %s --\n", fileName); - } - - // If the file is empty or not a G-Code file, we don't need to parse anything - if (fileBeingParsed->Length() == 0 || (!StringEndsWith(fileName, ".gcode") && !StringEndsWith(fileName, ".g") - && !StringEndsWith(fileName, ".gco") && !StringEndsWith(fileName, ".gc"))) - { - fileBeingParsed->Close(); - info = parsedFileInfo; - return true; - } - parseState = parsingHeader; - } - - // Getting file information take a few runs. Speed it up when we are not printing by calling it several times. - const uint32_t loopStartTime = millis(); - do - { - uint32_t buf32[(GCODE_READ_SIZE + GCODE_OVERLAP_SIZE + 3)/4 + 1]; // buffer should be 32-bit aligned for HSMCI (need the +1 so we can add a null terminator) - char* const buf = reinterpret_cast<char*>(buf32); - size_t sizeToRead, sizeToScan; // number of bytes we want to read and scan in this go - - switch (parseState) - { - case parsingHeader: - { - bool headerInfoComplete = true; - - // Read a chunk from the header. On the first run only process GCODE_READ_SIZE bytes, but use overlap next times. - sizeToRead = (size_t)min<FilePosition>(fileBeingParsed->Length() - fileBeingParsed->Position(), GCODE_READ_SIZE); - if (fileOverlapLength > 0) - { - memcpy(buf, fileOverlap, fileOverlapLength); - sizeToScan = sizeToRead + fileOverlapLength; - } - else - { - sizeToScan = sizeToRead; - } - - uint32_t startTime = millis(); - const int nbytes = fileBeingParsed->Read(&buf[fileOverlapLength], sizeToRead); - if (nbytes != (int)sizeToRead) - { - platform.MessageF(ErrorMessage, "Failed to read header of G-Code file \"%s\"\n", fileName); - parseState = notParsing; - fileBeingParsed->Close(); - info = parsedFileInfo; - return true; - } - buf[sizeToScan] = 0; - - // Record performance data - uint32_t now = millis(); - accumulatedReadTime += now - startTime; - startTime = now; - - // Search for filament usage (Cura puts it at the beginning of a G-code file) - if (parsedFileInfo.numFilaments == 0) - { - parsedFileInfo.numFilaments = FindFilamentUsed(buf, sizeToScan, parsedFileInfo.filamentNeeded, DRIVES - reprap.GetGCodes().GetTotalAxes()); - headerInfoComplete &= (parsedFileInfo.numFilaments != 0); - } - - // Look for first layer height - if (parsedFileInfo.firstLayerHeight == 0.0) - { - headerInfoComplete &= FindFirstLayerHeight(buf, sizeToScan, parsedFileInfo.firstLayerHeight); - } - - // Look for layer height - if (parsedFileInfo.layerHeight == 0.0) - { - headerInfoComplete &= FindLayerHeight(buf, sizeToScan, parsedFileInfo.layerHeight); - } - - // Look for slicer program - if (parsedFileInfo.generatedBy.IsEmpty()) - { - headerInfoComplete &= FindSlicerInfo(buf, sizeToScan, parsedFileInfo.generatedBy.GetRef()); - } - - // Keep track of the time stats - accumulatedParseTime += millis() - startTime; - - // Can we proceed to the footer? Don't scan more than the first 4KB of the file - FilePosition pos = fileBeingParsed->Position(); - if (headerInfoComplete || pos >= GCODE_HEADER_SIZE || pos == fileBeingParsed->Length()) - { - // Yes - see if we need to output some debug info - if (reprap.Debug(modulePrintMonitor)) - { - platform.MessageF(UsbMessage, "Header complete, processed %lu bytes, read time %.3fs, parse time %.3fs\n", - fileBeingParsed->Position(), (double)((float)accumulatedReadTime/1000.0), (double)((float)accumulatedParseTime/1000.0)); - } - - // Go to the last chunk and proceed from there on - const FilePosition seekFromEnd = ((fileBeingParsed->Length() - 1) % GCODE_READ_SIZE) + 1; - nextSeekPos = fileBeingParsed->Length() - seekFromEnd; - accumulatedSeekTime = accumulatedReadTime = accumulatedParseTime = 0; - fileOverlapLength = 0; - parseState = seeking; - } - else - { - // No - copy the last chunk of the buffer for overlapping search - fileOverlapLength = min<size_t>(sizeToRead, GCODE_OVERLAP_SIZE); - memcpy(fileOverlap, &buf[sizeToRead - fileOverlapLength], fileOverlapLength); - } - } - break; - - case seeking: - // Seeking into a large file can take a long time using the FAT file system, so do it in stages - { - FilePosition currentPos = fileBeingParsed->Position(); - const uint32_t clsize = fileBeingParsed->ClusterSize(); - if (currentPos/clsize > nextSeekPos/clsize) - { - // Seeking backwards over a cluster boundary, so in practice the seek will start from the start of the file - currentPos = 0; - } - - // Seek at most 512 clusters at a time - const FilePosition maxSeekDistance = 512 * (FilePosition)clsize; - const bool doFullSeek = (nextSeekPos <= currentPos + maxSeekDistance); - const FilePosition thisSeekPos = (doFullSeek) ? nextSeekPos : currentPos + maxSeekDistance; - - const uint32_t startTime = millis(); - if (!fileBeingParsed->Seek(thisSeekPos)) - { - platform.Message(ErrorMessage, "Could not seek from end of file!\n"); - parseState = notParsing; - fileBeingParsed->Close(); - info = parsedFileInfo; - return true; - } - accumulatedSeekTime += millis() - startTime; - if (doFullSeek) - { - parseState = parsingFooter; - } - } - break; - - case parsingFooter: - { - // Processing the footer. See how many bytes we need to read and if we can reuse the overlap - sizeToRead = (size_t)min<FilePosition>(fileBeingParsed->Length() - nextSeekPos, GCODE_READ_SIZE); - if (fileOverlapLength > 0) - { - memcpy(&buf[sizeToRead], fileOverlap, fileOverlapLength); - sizeToScan = sizeToRead + fileOverlapLength; - } - else - { - sizeToScan = sizeToRead; - } - - // Read another chunk from the footer - uint32_t startTime = millis(); - int nbytes = fileBeingParsed->Read(buf, sizeToRead); - if (nbytes != (int)sizeToRead) - { - platform.MessageF(ErrorMessage, "Failed to read footer from G-Code file \"%s\"\n", fileName); - parseState = notParsing; - fileBeingParsed->Close(); - info = parsedFileInfo; - return true; - } - buf[sizeToScan] = 0; - - // Record performance data - uint32_t now = millis(); - accumulatedReadTime += now - startTime; - startTime = now; - - bool footerInfoComplete = true; - - // Search for filament used - if (parsedFileInfo.numFilaments == 0) - { - parsedFileInfo.numFilaments = FindFilamentUsed(buf, sizeToScan, parsedFileInfo.filamentNeeded, DRIVES - reprap.GetGCodes().GetTotalAxes()); - if (parsedFileInfo.numFilaments == 0) - { - footerInfoComplete = false; - } - } - - // Search for layer height - if (parsedFileInfo.layerHeight == 0.0) - { - if (!FindLayerHeight(buf, sizeToScan, parsedFileInfo.layerHeight)) - { - footerInfoComplete = false; - } - } - - // Search for object height - if (parsedFileInfo.objectHeight == 0.0) - { - if (!FindHeight(buf, sizeToScan, parsedFileInfo.objectHeight)) - { - footerInfoComplete = false; - } - } - - // Keep track of the time stats - accumulatedParseTime += millis() - startTime; - - // If we've collected all details, scanned the last 192K of the file or if we cannot go any further, stop here. - if (footerInfoComplete || nextSeekPos == 0 || fileBeingParsed->Length() - nextSeekPos >= GCODE_FOOTER_SIZE) - { - if (reprap.Debug(modulePrintMonitor)) - { - platform.MessageF(UsbMessage, "Footer complete, processed %lu bytes, read time %.3fs, parse time %.3fs, seek time %.3fs\n", - fileBeingParsed->Length() - fileBeingParsed->Position() + GCODE_READ_SIZE, - (double)((float)accumulatedReadTime/1000.0), (double)((float)accumulatedParseTime/1000.0), (double)((float)accumulatedSeekTime/1000.0)); - } - parseState = notParsing; - fileBeingParsed->Close(); - info = parsedFileInfo; - return true; - } - - // Else go back further - fileOverlapLength = (size_t)min<FilePosition>(sizeToScan, GCODE_OVERLAP_SIZE); - memcpy(fileOverlap, buf, fileOverlapLength); - nextSeekPos = (nextSeekPos <= GCODE_READ_SIZE) ? 0 : nextSeekPos - GCODE_READ_SIZE; - parseState = seeking; - } - break; - - default: // should not get here - info = parsedFileInfo; - return true; - } - lastFileParseTime = millis(); - } while (!isPrinting && lastFileParseTime - loopStartTime < MAX_FILEINFO_PROCESS_TIME); - return false; -} - -// Get information for the specified file, or the currently printing file, in JSON format -bool PrintMonitor::GetFileInfoResponse(const char *filename, OutputBuffer *&response) -{ - // Poll file info for a specific file - if (filename != nullptr && filename[0] != 0) - { - GCodeFileInfo info; - if (!GetFileInfo(FS_PREFIX, filename, info)) - { - // This may take a few runs... - return false; - } - - if (info.isValid) - { - if (!OutputBuffer::Allocate(response)) - { - // Should never happen - return false; - } - - response->printf("{\"err\":0,\"size\":%lu,",info.fileSize); - const struct tm * const timeInfo = gmtime(&info.lastModifiedTime); - if (timeInfo->tm_year > /*19*/80) - { - response->catf("\"lastModified\":\"%04u-%02u-%02uT%02u:%02u:%02u\",", - timeInfo->tm_year + 1900, timeInfo->tm_mon + 1, timeInfo->tm_mday, - timeInfo->tm_hour, timeInfo->tm_min, timeInfo->tm_sec); - } - - response->catf("\"height\":%.2f,\"firstLayerHeight\":%.2f,\"layerHeight\":%.2f,\"filament\":", - (double)info.objectHeight, (double)info.firstLayerHeight, (double)info.layerHeight); - char ch = '['; - if (info.numFilaments == 0) - { - response->cat(ch); - } - else - { - for(size_t i = 0; i < info.numFilaments; ++i) - { - response->catf("%c%.1f", ch, (double)info.filamentNeeded[i]); - ch = ','; - } - } - response->cat("],\"generatedBy\":"); - response->EncodeString(info.generatedBy.c_str(), info.generatedBy.Capacity(), false); - response->cat("}"); - } - else - { - if (!OutputBuffer::Allocate(response)) - { - // Should never happen - return false; - } - - response->copy("{\"err\":1}"); - } - } - else if (IsPrinting()) - { - // If the file being printed hasn't been processed yet or if we - // cannot write the response, try again later - if (!printingFileParsed || !OutputBuffer::Allocate(response)) - { - return false; - } - - // Poll file info about a file currently being printed - response->printf("{\"err\":0,\"size\":%lu,\"height\":%.2f,\"firstLayerHeight\":%.2f,\"layerHeight\":%.2f,\"filament\":", - printingFileInfo.fileSize, (double)printingFileInfo.objectHeight, (double)printingFileInfo.firstLayerHeight, (double)printingFileInfo.layerHeight); - char ch = '['; - if (printingFileInfo.numFilaments == 0) - { - response->cat(ch); - } - else - { - for (size_t i = 0; i < printingFileInfo.numFilaments; ++i) - { - response->catf("%c%.1f", ch, (double)printingFileInfo.filamentNeeded[i]); - ch = ','; - } - } - response->cat("],\"generatedBy\":"); - response->EncodeString(printingFileInfo.generatedBy.GetRef(), false); - response->catf(",\"printDuration\":%d,\"fileName\":", (int)GetPrintDuration()); - response->EncodeString(filenameBeingPrinted.GetRef(), false); - response->cat('}'); - } - else - { - if (!OutputBuffer::Allocate(response)) - { - // Should never happen - return false; - } - - response->copy("{\"err\":1}"); - } - return true; -} - // Estimate the print time left in seconds on a preset estimation method float PrintMonitor::EstimateTimeLeft(PrintEstimationMethod method) const { @@ -806,402 +437,6 @@ float PrintMonitor::EstimateTimeLeft(PrintEstimationMethod method) const return 0.0; } -// Scan the buffer for a G1 Zxxx command. The buffer is null-terminated. -bool PrintMonitor::FindFirstLayerHeight(const char* buf, size_t len, float& height) const -{ - if (len < 4) - { - // Don't start if the buffer is not big enough - return false; - } - height = 0.0; - -//debugPrintf("Scanning %u bytes starting %.100s\n", len, buf); - bool inComment = false, inRelativeMode = false, foundHeight = false; - for(size_t i = 0; i < len - 4; i++) - { - if (buf[i] == ';') - { - inComment = true; - } - else if (inComment) - { - if (buf[i] == '\n') - { - inComment = false; - } - } - else if (buf[i] == 'G') - { - // See if we can switch back to absolute mode - if (inRelativeMode) - { - inRelativeMode = !(buf[i + 1] == '9' && buf[i + 2] == '0' && buf[i + 3] <= ' '); - } - // Ignore G0/G1 codes if in relative mode - else if (buf[i + 1] == '9' && buf[i + 2] == '1' && buf[i + 3] <= ' ') - { - inRelativeMode = true; - } - // Look for "G0/G1 ... Z#HEIGHT#" command - else if ((buf[i + 1] == '0' || buf[i + 1] == '1') && buf[i + 2] == ' ') - { - for(i += 3; i < len - 4; i++) - { - if (buf[i] == 'Z') - { - //debugPrintf("Found at offset %u text: %.100s\n", i, &buf[i + 1]); - float flHeight = strtod(&buf[i + 1], nullptr); - if ((height == 0.0 || flHeight < height) && (flHeight <= platform.GetNozzleDiameter() * 3.0)) - { - height = flHeight; // Only report first Z height if it's somewhat reasonable - foundHeight = true; - // NB: Don't stop here, because some slicers generate two Z moves at the beginning - } - break; - } - else if (buf[i] == ';') - { - // Ignore comments - break; - } - } - } - } - } - return foundHeight; -} - -// Scan the buffer for a G1 Zxxx command. The buffer is null-terminated. -// This parsing algorithm needs to be fast. The old one sometimes took 5 seconds or more to parse about 120K of data. -// To speed up parsing, we now parse forwards from the start of the buffer. This means we can't stop when we have found a G1 Z command, -// we have to look for a later G1 Z command in the buffer. But it is faster in the (common) case that we don't find a match in the buffer at all. -bool PrintMonitor::FindHeight(const char* buf, size_t len, float& height) const -{ - bool foundHeight = false; - bool inRelativeMode = false; - for(;;) - { - // Skip to next newline - char c; - while (len >= 6 && (c = *buf) != '\r' && c != '\n') - { - ++buf; - --len; - } - - // Skip the newline and any leading spaces - do - { - ++buf; // skip the newline - --len; - c = *buf; - } while (len >= 5 && (c == ' ' || c == '\t' || c == '\r' || c == '\n')); - - if (len < 5) - { - break; // not enough characters left for a G1 Zx.x command - } - - ++buf; // move to 1 character beyond c - --len; - - // In theory we should skip N and a line number here if they are present, but no slicers seem to generate line numbers - if (c == 'G') - { - if (inRelativeMode) - { - // We have seen a G91 in this buffer already, so we are only interested in G90 commands that switch back to absolute mode - if (buf[0] == '9' && buf[1] == '0' && (buf[2] < '0' || buf[2] > '9')) - { - // It's a G90 command so go back to absolute mode - inRelativeMode = false; - } - } - else if (*buf == '1' || *buf == '0') - { - // It could be a G0 or G1 command - ++buf; - --len; - if (*buf < '0' || *buf > '9') - { - // It is a G0 or G1 command. See if it has a Z parameter. - while (len >= 4) - { - c = *buf; - if (c == 'Z') - { - const char* zpos = buf + 1; - // Check special case of this code ending with ";E" or "; E" - ignore such codes - while (len > 2 && *buf != '\n' && *buf != '\r' && *buf != ';') - { - ++buf; - --len; - } - if ((len >= 2 && StringStartsWith(buf, ";E")) || (len >= 3 && StringStartsWith(buf, "; E"))) - { - // Ignore this G1 Z command - } - else - { - height = strtod(zpos, nullptr); - foundHeight = true; - } - break; // carry on looking for a later G1 Z command - } - if (c == ';' || c == '\n' || c == '\r') - { - break; // no Z parameter - } - ++buf; - --len; - } - } - } - else if (buf[0] == '9' && buf[1] == '1' && (buf[2] < '0' || buf[2] > '9')) - { - // It's a G91 command - inRelativeMode = true; - } - } - else if (c == ';') - { - static const char kisslicerHeightString[] = " END_LAYER_OBJECT z="; - if (len > 31 && StringStartsWith(buf, kisslicerHeightString)) - { - height = strtod(buf + sizeof(kisslicerHeightString)/sizeof(char) - 1, nullptr); - return true; - } - } - } - return foundHeight; -} - -// Scan the buffer for the layer height. The buffer is null-terminated. -bool PrintMonitor::FindLayerHeight(const char *buf, size_t len, float& layerHeight) const -{ - static const char* const layerHeightStrings[] = - { - "layer_height", // slic3r - "Layer height", // Cura - "layerHeight", // S3D - "layer_thickness_mm", // Kisslicer - "layerThickness" // Matter Control - }; - - if (*buf != 0) - { - ++buf; // make sure we can look back 1 character after we find a match - for (size_t i = 0; i < ARRAY_SIZE(layerHeightStrings); ++i) // search for each string in turn - { - const char *pos = buf; - for(;;) // loop until success or strstr returns null - { - pos = strstr(pos, layerHeightStrings[i]); - if (pos == nullptr) - { - break; // didn't find this string in the buffer, so try the next string - } - - const char c = pos[-1]; // fetch the previous character - pos += strlen(layerHeightStrings[i]); // skip the string we matched - if (c == ' ' || c == ';' || c == '\t') // check we are not in the middle of a word - { - while (strchr(" \t=:,", *pos) != nullptr) // skip the possible separators - { - ++pos; - } - char *tailPtr; - const float val = strtod(pos, &tailPtr); - if (tailPtr != pos) // if we found and converted a number - { - layerHeight = val; - return true; - } - } - } - } - } - - return false; -} - -bool PrintMonitor::FindSlicerInfo(const char* buf, size_t len, const StringRef& generatedBy) const -{ - static const char * const GeneratedByStrings[] = - { - "generated by ", // slic3r and S3D - ";Sliced by ", // ideaMaker - "; KISSlicer", // KISSlicer - ";Sliced at: ", // Cura (old) - ";Generated with " // Cura (new) - }; - - size_t index = 0; - const char* pos; - do - { - pos = strstr(buf, GeneratedByStrings[index]); - if (pos != nullptr) - { - break; - } - ++index; - } while (index < ARRAY_SIZE(GeneratedByStrings)); - - if (pos != nullptr) - { - const char* introString = ""; - switch (index) - { - default: - pos += strlen(GeneratedByStrings[index]); - break; - - case 2: // KISSlicer - pos += 2; - break; - - case 3: // Cura (old) - introString = "Cura at "; - pos += strlen(GeneratedByStrings[index]); - break; - } - - generatedBy.copy(introString); - while (*pos >= ' ') - { - generatedBy.cat(*pos++); - } - return true; - } - return false; -} - -// Scan the buffer for the filament used. The buffer is null-terminated. -// Returns the number of filaments found. -unsigned int PrintMonitor::FindFilamentUsed(const char* buf, size_t len, float *filamentUsed, size_t maxFilaments) const -{ - unsigned int filamentsFound = 0; - - // Look for filament usage as generated by Slic3r and Cura - const char* const filamentUsedStr1 = "ilament used"; // comment string used by slic3r and Cura, followed by filament used and "mm" - const char* p = buf; - while (filamentsFound < maxFilaments && (p = strstr(p, filamentUsedStr1)) != nullptr) - { - p += strlen(filamentUsedStr1); - while(strchr(" :=\t", *p) != nullptr) - { - ++p; // this allows for " = " from default slic3r comment and ": " from default Cura comment - } - while (isDigit(*p)) - { - char* q; - filamentUsed[filamentsFound] = strtod(p, &q); - p = q; - if (*p == 'm') - { - ++p; - if (*p == 'm') - { - ++p; - } - else - { - filamentUsed[filamentsFound] *= 1000.0; // Cura outputs filament used in metres not mm - } - } - ++filamentsFound; - while (strchr(", \t", *p) != nullptr) - { - ++p; - } - } - } - - // Look for filament usage string generated by Ideamaker - const char* const filamentUsedStr2 = ";Material#"; // comment string used by Ideamaker, e.g. ";Material#1 Used: 868.0" - p = buf; - while (filamentsFound < maxFilaments && (p = strstr(p, filamentUsedStr2)) != nullptr) - { - p += strlen(filamentUsedStr2); - char *q; - unsigned int num = strtoul(p, &q, 10); - if (q != p && num < maxFilaments) - { - p = q; - while(strchr(" Used:\t", *p) != nullptr) - { - ++p; // this allows for " Used: " - } - if (isDigit(*p)) - { - filamentUsed[filamentsFound] = strtod(p, &q); - ++filamentsFound; - } - } - } - - // Look for filament usage as generated by S3D - if (filamentsFound == 0) - { - const char *filamentLengthStr = "ilament length"; // comment string used by S3D - p = buf; - while (filamentsFound < maxFilaments && (p = strstr(p, filamentLengthStr)) != nullptr) - { - p += strlen(filamentLengthStr); - while(strchr(" :=\t", *p) != nullptr) - { - ++p; - } - if (isDigit(*p)) - { - filamentUsed[filamentsFound] = strtod(p, nullptr); // S3D reports filament usage in mm, no conversion needed - ++filamentsFound; - } - } - } - - // Look for filament usage as generated by recent KISSlicer versions - if (filamentsFound == 0) - { - const char *filamentLengthStr = "; Ext "; - p = buf; - while (filamentsFound < maxFilaments && (p = strstr(p, filamentLengthStr)) != nullptr) - { - p += strlen(filamentLengthStr); - while(isdigit(*p)) - { - ++p; - } - while(strchr(" :=\t", *p) != nullptr) - { - ++p; - } - - if (isDigit(*p)) - { - filamentUsed[filamentsFound] = strtod(p, nullptr); - ++filamentsFound; - } - } - } - - // Special case: Old KISSlicer only generates the filament volume, so we need to calculate the length from it - if (filamentsFound == 0) - { - const char *filamentVolumeStr = "; Estimated Build Volume: "; - p = strstr(buf, filamentVolumeStr); - if (p != nullptr) - { - const float filamentCMM = strtof(p + strlen(filamentVolumeStr), nullptr) * 1000.0; - filamentUsed[filamentsFound++] = filamentCMM / (Pi * fsquare(platform.GetFilamentWidth() / 2.0)); - } - } - - return filamentsFound; -} - // This returns the amount of time the machine has printed without interruptions (i.e. pauses) float PrintMonitor::GetPrintDuration() const { diff --git a/src/PrintMonitor.h b/src/PrintMonitor.h index 78348514..e0544970 100644 --- a/src/PrintMonitor.h +++ b/src/PrintMonitor.h @@ -21,17 +21,7 @@ Licence: GPL #define PRINTMONITOR_H #include "RepRapFirmware.h" - -const FilePosition GCODE_HEADER_SIZE = 20000uL; // How many bytes to read from the header - I (DC) have a Kisslicer file with a layer height comment 14Kb from the start -const FilePosition GCODE_FOOTER_SIZE = 400000uL; // How many bytes to read from the footer - -#if SAM4E || SAM4S || SAME70 -const size_t GCODE_READ_SIZE = 2048; // How many bytes to read in one go in GetFileInfo() (should be a multiple of 512 for read efficiency) -#else -const size_t GCODE_READ_SIZE = 1024; // How many bytes to read in one go in GetFileInfo() (should be a multiple of 512 for read efficiency) -#endif - -const size_t GCODE_OVERLAP_SIZE = 100; // Size of the overlapping buffer for searching (should be a multiple of 4) +#include "Storage/FileInfoParser.h" // for struct GCodeFileInfo const float LAYER_HEIGHT_TOLERANCE = 0.015; // Tolerance for comparing two Z heights (in mm) @@ -41,8 +31,6 @@ const float ESTIMATION_MIN_FILE_USAGE = 0.001; // Minimum per cent of the file const float FIRST_LAYER_SPEED_FACTOR = 0.25; // First layer speed factor compared to other layers (only for layer-based estimation) const uint32_t PRINTMONITOR_UPDATE_INTERVAL = 200; // Update interval in milliseconds -const uint32_t MAX_FILEINFO_PROCESS_TIME = 200; // Maximum time to spend polling for file info in each call -const uint32_t MaxFileParseInterval = 4000; // Maximum interval between repeat requests to parse a file enum PrintEstimationMethod { @@ -51,28 +39,6 @@ enum PrintEstimationMethod layerBased }; -// Struct to hold Gcode file information -struct GCodeFileInfo -{ - bool isValid; - FilePosition fileSize; - time_t lastModifiedTime; - float firstLayerHeight; - float objectHeight; - float filamentNeeded[MaxExtruders]; - unsigned int numFilaments; - float layerHeight; - String<50> generatedBy; -}; - -enum FileParseState -{ - notParsing, - parsingHeader, - seeking, - parsingFooter -}; - class PrintMonitor { public: @@ -85,10 +51,6 @@ class PrintMonitor void StartedPrint(); // Called whenever a new live print starts (see M24) void StoppedPrint(); // Called whenever a file print has stopped - // The following two methods need to be called until they return true - this may take a few runs - bool GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info); - bool GetFileInfoResponse(const char *filename, OutputBuffer *&response); - // Return an estimate in seconds based on a specific estimation method float EstimateTimeLeft(PrintEstimationMethod method) const; @@ -101,13 +63,13 @@ class PrintMonitor float GetFirstLayerHeight() const; const char *GetPrintingFilename() const { return (isPrinting) ? filenameBeingPrinted.c_str() : nullptr; } + bool GetPrintingFileInfoResponse(OutputBuffer *&response) const; private: Platform& platform; GCodes& gCodes; uint32_t longWait; uint32_t lastUpdateTime; - uint32_t lastFileParseTime; // Information/Events concerning the file being printed void WarmUpComplete(); @@ -130,28 +92,9 @@ class PrintMonitor float fileProgressPerLayer[MAX_LAYER_SAMPLES]; float layerEstimatedTimeLeft; - // We parse G-Code files in multiple stages. These variables hold the required information - FileParseState parseState; - String<MaxFilenameLength> filenameBeingParsed; - FileStore *fileBeingParsed; - FilePosition nextSeekPos; - GCodeFileInfo parsedFileInfo; - - char fileOverlap[GCODE_OVERLAP_SIZE]; - size_t fileOverlapLength; - bool printingFileParsed; GCodeFileInfo printingFileInfo; String<MaxFilenameLength> filenameBeingPrinted; - - // G-Code parser methods - bool FindHeight(const char* buf, size_t len, float& height) const; - bool FindFirstLayerHeight(const char* buf, size_t len, float& layerHeight) const; - bool FindLayerHeight(const char* buf, size_t len, float& layerHeight) const; - bool FindSlicerInfo(const char* buf, size_t len, const StringRef& generatedBy) const; - unsigned int FindFilamentUsed(const char* buf, size_t len, float *filamentUsed, size_t maxFilaments) const; - - uint32_t accumulatedParseTime, accumulatedReadTime, accumulatedSeekTime; }; inline bool PrintMonitor::IsPrinting() const { return isPrinting; } diff --git a/src/RepRap.cpp b/src/RepRap.cpp index f97932d5..a8108f72 100644 --- a/src/RepRap.cpp +++ b/src/RepRap.cpp @@ -1618,6 +1618,83 @@ OutputBuffer *RepRap::GetFilesResponse(const char *dir, bool flagsDirs) return response; } +// Get information for the specified file, or the currently printing file, in JSON format +bool RepRap::GetFileInfoResponse(const char *filename, OutputBuffer *&response, bool quitEarly) +{ + // Poll file info for a specific file + if (filename != nullptr && filename[0] != 0) + { + GCodeFileInfo info; + if (!platform->GetMassStorage()->GetFileInfo(FS_PREFIX, filename, info, quitEarly)) + { + // This may take a few runs... + return false; + } + + if (info.isValid) + { + if (!OutputBuffer::Allocate(response)) + { + // Should never happen + return false; + } + + response->printf("{\"err\":0,\"size\":%lu,",info.fileSize); + const struct tm * const timeInfo = gmtime(&info.lastModifiedTime); + if (timeInfo->tm_year > /*19*/80) + { + response->catf("\"lastModified\":\"%04u-%02u-%02uT%02u:%02u:%02u\",", + timeInfo->tm_year + 1900, timeInfo->tm_mon + 1, timeInfo->tm_mday, + timeInfo->tm_hour, timeInfo->tm_min, timeInfo->tm_sec); + } + + response->catf("\"height\":%.2f,\"firstLayerHeight\":%.2f,\"layerHeight\":%.2f,\"filament\":", + (double)info.objectHeight, (double)info.firstLayerHeight, (double)info.layerHeight); + char ch = '['; + if (info.numFilaments == 0) + { + response->cat(ch); + } + else + { + for (size_t i = 0; i < info.numFilaments; ++i) + { + response->catf("%c%.1f", ch, (double)info.filamentNeeded[i]); + ch = ','; + } + } + response->cat("],\"generatedBy\":"); + response->EncodeString(info.generatedBy.c_str(), info.generatedBy.Capacity(), false); + response->cat("}"); + } + else + { + if (!OutputBuffer::Allocate(response)) + { + // Should never happen + return false; + } + + response->copy("{\"err\":1}"); + } + } + else if (GetPrintMonitor().IsPrinting()) + { + return GetPrintMonitor().GetPrintingFileInfoResponse(response); + } + else + { + if (!OutputBuffer::Allocate(response)) + { + // Should never happen + return false; + } + + response->copy("{\"err\":1}"); + } + return true; +} + // Get a JSON-style filelist including file types and sizes OutputBuffer *RepRap::GetFilelistResponse(const char *dir) { @@ -1757,6 +1834,7 @@ char RepRap::GetStatusCharacter() const return (processingConfig) ? 'C' // Reading the configuration file : (gCodes->IsFlashing()) ? 'F' // Flashing a new firmware binary : (IsStopped()) ? 'H' // Halted + : (!platform->HasVinPower()) ? 'O' // Off i.e. powered down : (gCodes->IsPausing()) ? 'D' // Pausing / Decelerating : (gCodes->IsResuming()) ? 'R' // Resuming : (gCodes->IsDoingToolChange()) ? 'T' // Changing tool diff --git a/src/RepRap.h b/src/RepRap.h index 145d23e5..dcdc62fe 100644 --- a/src/RepRap.h +++ b/src/RepRap.h @@ -102,6 +102,7 @@ public: OutputBuffer *GetLegacyStatusResponse(uint8_t type, int seq); OutputBuffer *GetFilesResponse(const char* dir, bool flagsDirs); OutputBuffer *GetFilelistResponse(const char* dir); + bool GetFileInfoResponse(const char *filename, OutputBuffer *&response, bool quitEarly); void Beep(unsigned int freq, unsigned int ms); void SetMessage(const char *msg); diff --git a/src/Storage/FileInfoParser.cpp b/src/Storage/FileInfoParser.cpp new file mode 100644 index 00000000..fcd78500 --- /dev/null +++ b/src/Storage/FileInfoParser.cpp @@ -0,0 +1,739 @@ +/* + * FileInfoParser.cpp + * + * Created on: 31 Mar 2018 + * Author: David + */ + +#include "FileInfoParser.h" +#include "OutputMemory.h" +#include "RepRap.h" +#include "Platform.h" +#include "PrintMonitor.h" +#include "GCodes/GCodes.h" + +void GCodeFileInfo::Init() +{ + isValid = false; + incomplete = true; + firstLayerHeight = 0.0; + objectHeight = 0.0; + layerHeight = 0.0; + numFilaments = 0; + generatedBy.Clear(); + for (size_t extr = 0; extr < MaxExtruders; extr++) + { + filamentNeeded[extr] = 0.0; + } +} + +FileInfoParser::FileInfoParser() + : parseState(notParsing), fileBeingParsed(nullptr), accumulatedParseTime(0), accumulatedReadTime(0), accumulatedSeekTime(0), fileOverlapLength(0) +{ + parsedFileInfo.Init(); + parserMutexHandle = RTOSIface::CreateMutex(parserMutexStorage); +} + +bool FileInfoParser::GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info, bool quitEarly) +{ + Locker lock(parserMutexHandle, MAX_FILEINFO_PROCESS_TIME); + if (!lock) + { + return false; + } + + if (parseState != notParsing && !StringEquals(fileName, filenameBeingParsed.c_str())) + { + // We are already parsing a different file + if (millis() - lastFileParseTime < MaxFileParseInterval) + { + return false; // try again later + } + + // Time this client out because it has probably disconnected + fileBeingParsed->Close(); + parseState = notParsing; + } + + if (parseState == notParsing) + { + // See if we can access the file + // Webserver may call rr_fileinfo for a directory, check this case here + if (reprap.GetPlatform().GetMassStorage()->DirectoryExists(directory, fileName)) + { + info.isValid = false; + return true; + } + + fileBeingParsed = reprap.GetPlatform().OpenFile(directory, fileName, OpenMode::read); + if (fileBeingParsed == nullptr) + { + // Something went wrong - we cannot open it + info.isValid = false; + return true; + } + + // File has been opened, let's start now + filenameBeingParsed.copy(fileName); + fileOverlapLength = 0; + + // Set up the info struct + parsedFileInfo.Init(); + parsedFileInfo.fileSize = fileBeingParsed->Length(); + parsedFileInfo.lastModifiedTime = reprap.GetPlatform().GetMassStorage()->GetLastModifiedTime(directory, fileName); + parsedFileInfo.isValid = true; + + // Record some debug values here + if (reprap.Debug(modulePrintMonitor)) + { + accumulatedReadTime = accumulatedParseTime = 0; + reprap.GetPlatform().MessageF(UsbMessage, "-- Parsing file %s --\n", fileName); + } + + // If the file is empty or not a G-Code file, we don't need to parse anything + if (fileBeingParsed->Length() == 0 || (!StringEndsWith(fileName, ".gcode") && !StringEndsWith(fileName, ".g") + && !StringEndsWith(fileName, ".gco") && !StringEndsWith(fileName, ".gc"))) + { + fileBeingParsed->Close(); + parsedFileInfo.incomplete = false; + info = parsedFileInfo; + return true; + } + parseState = parsingHeader; + } + + // Getting file information take a few runs. Speed it up when we are not printing by calling it several times. + const uint32_t loopStartTime = millis(); + do + { + uint32_t buf32[(GCODE_READ_SIZE + GCODE_OVERLAP_SIZE + 3)/4 + 1]; // buffer should be 32-bit aligned for HSMCI (need the +1 so we can add a null terminator) + char* const buf = reinterpret_cast<char*>(buf32); + size_t sizeToRead, sizeToScan; // number of bytes we want to read and scan in this go + + switch (parseState) + { + case parsingHeader: + { + bool headerInfoComplete = true; + + // Read a chunk from the header. On the first run only process GCODE_READ_SIZE bytes, but use overlap next times. + sizeToRead = (size_t)min<FilePosition>(fileBeingParsed->Length() - fileBeingParsed->Position(), GCODE_READ_SIZE); + if (fileOverlapLength > 0) + { + memcpy(buf, fileOverlap, fileOverlapLength); + sizeToScan = sizeToRead + fileOverlapLength; + } + else + { + sizeToScan = sizeToRead; + } + + uint32_t startTime = millis(); + const int nbytes = fileBeingParsed->Read(&buf[fileOverlapLength], sizeToRead); + if (nbytes != (int)sizeToRead) + { + reprap.GetPlatform().MessageF(ErrorMessage, "Failed to read header of G-Code file \"%s\"\n", fileName); + parseState = notParsing; + fileBeingParsed->Close(); + info = parsedFileInfo; + return true; + } + buf[sizeToScan] = 0; + + // Record performance data + uint32_t now = millis(); + accumulatedReadTime += now - startTime; + startTime = now; + + // Search for filament usage (Cura puts it at the beginning of a G-code file) + if (parsedFileInfo.numFilaments == 0) + { + parsedFileInfo.numFilaments = FindFilamentUsed(buf, sizeToScan, parsedFileInfo.filamentNeeded, DRIVES - reprap.GetGCodes().GetTotalAxes()); + headerInfoComplete &= (parsedFileInfo.numFilaments != 0); + } + + // Look for first layer height + if (parsedFileInfo.firstLayerHeight == 0.0) + { + headerInfoComplete &= FindFirstLayerHeight(buf, sizeToScan, parsedFileInfo.firstLayerHeight); + } + + // Look for layer height + if (parsedFileInfo.layerHeight == 0.0) + { + headerInfoComplete &= FindLayerHeight(buf, sizeToScan, parsedFileInfo.layerHeight); + } + + // Look for slicer program + if (parsedFileInfo.generatedBy.IsEmpty()) + { + headerInfoComplete &= FindSlicerInfo(buf, sizeToScan, parsedFileInfo.generatedBy.GetRef()); + } + + // Keep track of the time stats + accumulatedParseTime += millis() - startTime; + + // Can we proceed to the footer? Don't scan more than the first 4KB of the file + FilePosition pos = fileBeingParsed->Position(); + if (headerInfoComplete || pos >= GCODE_HEADER_SIZE || pos == fileBeingParsed->Length()) + { + // Yes - see if we need to output some debug info + if (reprap.Debug(modulePrintMonitor)) + { + reprap.GetPlatform().MessageF(UsbMessage, "Header complete, processed %lu bytes, read time %.3fs, parse time %.3fs\n", + fileBeingParsed->Position(), (double)((float)accumulatedReadTime/1000.0), (double)((float)accumulatedParseTime/1000.0)); + } + + // Go to the last chunk and proceed from there on + const FilePosition seekFromEnd = ((fileBeingParsed->Length() - 1) % GCODE_READ_SIZE) + 1; + nextSeekPos = fileBeingParsed->Length() - seekFromEnd; + accumulatedSeekTime = accumulatedReadTime = accumulatedParseTime = 0; + fileOverlapLength = 0; + parseState = seeking; + } + else + { + // No - copy the last chunk of the buffer for overlapping search + fileOverlapLength = min<size_t>(sizeToRead, GCODE_OVERLAP_SIZE); + memcpy(fileOverlap, &buf[sizeToRead - fileOverlapLength], fileOverlapLength); + } + } + break; + + case seeking: + // Seeking into a large file can take a long time using the FAT file system, so do it in stages + { + FilePosition currentPos = fileBeingParsed->Position(); + const uint32_t clsize = fileBeingParsed->ClusterSize(); + if (currentPos/clsize > nextSeekPos/clsize) + { + // Seeking backwards over a cluster boundary, so in practice the seek will start from the start of the file + currentPos = 0; + } + + // Seek at most 512 clusters at a time + const FilePosition maxSeekDistance = 512 * (FilePosition)clsize; + const bool doFullSeek = (nextSeekPos <= currentPos + maxSeekDistance); + const FilePosition thisSeekPos = (doFullSeek) ? nextSeekPos : currentPos + maxSeekDistance; + + const uint32_t startTime = millis(); + if (!fileBeingParsed->Seek(thisSeekPos)) + { + reprap.GetPlatform().Message(ErrorMessage, "Could not seek from end of file!\n"); + parseState = notParsing; + fileBeingParsed->Close(); + info = parsedFileInfo; + return true; + } + accumulatedSeekTime += millis() - startTime; + if (doFullSeek) + { + parseState = parsingFooter; + } + } + break; + + case parsingFooter: + { + // Processing the footer. See how many bytes we need to read and if we can reuse the overlap + sizeToRead = (size_t)min<FilePosition>(fileBeingParsed->Length() - nextSeekPos, GCODE_READ_SIZE); + if (fileOverlapLength > 0) + { + memcpy(&buf[sizeToRead], fileOverlap, fileOverlapLength); + sizeToScan = sizeToRead + fileOverlapLength; + } + else + { + sizeToScan = sizeToRead; + } + + // Read another chunk from the footer + uint32_t startTime = millis(); + int nbytes = fileBeingParsed->Read(buf, sizeToRead); + if (nbytes != (int)sizeToRead) + { + reprap.GetPlatform().MessageF(ErrorMessage, "Failed to read footer from G-Code file \"%s\"\n", fileName); + parseState = notParsing; + fileBeingParsed->Close(); + info = parsedFileInfo; + return true; + } + buf[sizeToScan] = 0; + + // Record performance data + uint32_t now = millis(); + accumulatedReadTime += now - startTime; + startTime = now; + + bool footerInfoComplete = true; + + // Search for filament used + if (parsedFileInfo.numFilaments == 0) + { + parsedFileInfo.numFilaments = FindFilamentUsed(buf, sizeToScan, parsedFileInfo.filamentNeeded, DRIVES - reprap.GetGCodes().GetTotalAxes()); + if (parsedFileInfo.numFilaments == 0) + { + footerInfoComplete = false; + } + } + + // Search for layer height + if (parsedFileInfo.layerHeight == 0.0) + { + if (!FindLayerHeight(buf, sizeToScan, parsedFileInfo.layerHeight)) + { + footerInfoComplete = false; + } + } + + // Search for object height + if (parsedFileInfo.objectHeight == 0.0) + { + if (!FindHeight(buf, sizeToScan, parsedFileInfo.objectHeight)) + { + footerInfoComplete = false; + } + } + + // Keep track of the time stats + accumulatedParseTime += millis() - startTime; + + // If we've collected all details, scanned the last 192K of the file or if we cannot go any further, stop here. + if (footerInfoComplete || nextSeekPos == 0 || fileBeingParsed->Length() - nextSeekPos >= GCODE_FOOTER_SIZE) + { + if (reprap.Debug(modulePrintMonitor)) + { + reprap.GetPlatform().MessageF(UsbMessage, "Footer complete, processed %lu bytes, read time %.3fs, parse time %.3fs, seek time %.3fs\n", + fileBeingParsed->Length() - fileBeingParsed->Position() + GCODE_READ_SIZE, + (double)((float)accumulatedReadTime/1000.0), (double)((float)accumulatedParseTime/1000.0), (double)((float)accumulatedSeekTime/1000.0)); + } + parseState = notParsing; + fileBeingParsed->Close(); + parsedFileInfo.incomplete = false; + info = parsedFileInfo; + return true; + } + + // Else go back further + fileOverlapLength = (size_t)min<FilePosition>(sizeToScan, GCODE_OVERLAP_SIZE); + memcpy(fileOverlap, buf, fileOverlapLength); + nextSeekPos = (nextSeekPos <= GCODE_READ_SIZE) ? 0 : nextSeekPos - GCODE_READ_SIZE; + parseState = seeking; + } + break; + + default: // should not get here + parsedFileInfo.incomplete = false; + info = parsedFileInfo; + parseState = notParsing; + return true; + } + lastFileParseTime = millis(); + } while (!reprap.GetPrintMonitor().IsPrinting() && lastFileParseTime - loopStartTime < MAX_FILEINFO_PROCESS_TIME); + + if (quitEarly) + { + info = parsedFileInfo; // note that the 'incomplete' flag is still set + parseState = notParsing; + return true; + } + return false; +} + +// Scan the buffer for a G1 Zxxx command. The buffer is null-terminated. +bool FileInfoParser::FindFirstLayerHeight(const char* buf, size_t len, float& height) const +{ + if (len < 4) + { + // Don't start if the buffer is not big enough + return false; + } + height = 0.0; + +//debugPrintf("Scanning %u bytes starting %.100s\n", len, buf); + bool inComment = false, inRelativeMode = false, foundHeight = false; + for(size_t i = 0; i < len - 4; i++) + { + if (buf[i] == ';') + { + inComment = true; + } + else if (inComment) + { + if (buf[i] == '\n') + { + inComment = false; + } + } + else if (buf[i] == 'G') + { + // See if we can switch back to absolute mode + if (inRelativeMode) + { + inRelativeMode = !(buf[i + 1] == '9' && buf[i + 2] == '0' && buf[i + 3] <= ' '); + } + // Ignore G0/G1 codes if in relative mode + else if (buf[i + 1] == '9' && buf[i + 2] == '1' && buf[i + 3] <= ' ') + { + inRelativeMode = true; + } + // Look for "G0/G1 ... Z#HEIGHT#" command + else if ((buf[i + 1] == '0' || buf[i + 1] == '1') && buf[i + 2] == ' ') + { + for(i += 3; i < len - 4; i++) + { + if (buf[i] == 'Z') + { + //debugPrintf("Found at offset %u text: %.100s\n", i, &buf[i + 1]); + float flHeight = strtod(&buf[i + 1], nullptr); + if ((height == 0.0 || flHeight < height) && (flHeight <= reprap.GetPlatform().GetNozzleDiameter() * 3.0)) + { + height = flHeight; // Only report first Z height if it's somewhat reasonable + foundHeight = true; + // NB: Don't stop here, because some slicers generate two Z moves at the beginning + } + break; + } + else if (buf[i] == ';') + { + // Ignore comments + break; + } + } + } + } + } + return foundHeight; +} + +// Scan the buffer for a G1 Zxxx command. The buffer is null-terminated. +// This parsing algorithm needs to be fast. The old one sometimes took 5 seconds or more to parse about 120K of data. +// To speed up parsing, we now parse forwards from the start of the buffer. This means we can't stop when we have found a G1 Z command, +// we have to look for a later G1 Z command in the buffer. But it is faster in the (common) case that we don't find a match in the buffer at all. +bool FileInfoParser::FindHeight(const char* buf, size_t len, float& height) const +{ + bool foundHeight = false; + bool inRelativeMode = false; + for(;;) + { + // Skip to next newline + char c; + while (len >= 6 && (c = *buf) != '\r' && c != '\n') + { + ++buf; + --len; + } + + // Skip the newline and any leading spaces + do + { + ++buf; // skip the newline + --len; + c = *buf; + } while (len >= 5 && (c == ' ' || c == '\t' || c == '\r' || c == '\n')); + + if (len < 5) + { + break; // not enough characters left for a G1 Zx.x command + } + + ++buf; // move to 1 character beyond c + --len; + + // In theory we should skip N and a line number here if they are present, but no slicers seem to generate line numbers + if (c == 'G') + { + if (inRelativeMode) + { + // We have seen a G91 in this buffer already, so we are only interested in G90 commands that switch back to absolute mode + if (buf[0] == '9' && buf[1] == '0' && (buf[2] < '0' || buf[2] > '9')) + { + // It's a G90 command so go back to absolute mode + inRelativeMode = false; + } + } + else if (*buf == '1' || *buf == '0') + { + // It could be a G0 or G1 command + ++buf; + --len; + if (*buf < '0' || *buf > '9') + { + // It is a G0 or G1 command. See if it has a Z parameter. + while (len >= 4) + { + c = *buf; + if (c == 'Z') + { + const char* zpos = buf + 1; + // Check special case of this code ending with ";E" or "; E" - ignore such codes + while (len > 2 && *buf != '\n' && *buf != '\r' && *buf != ';') + { + ++buf; + --len; + } + if ((len >= 2 && StringStartsWith(buf, ";E")) || (len >= 3 && StringStartsWith(buf, "; E"))) + { + // Ignore this G1 Z command + } + else + { + height = strtod(zpos, nullptr); + foundHeight = true; + } + break; // carry on looking for a later G1 Z command + } + if (c == ';' || c == '\n' || c == '\r') + { + break; // no Z parameter + } + ++buf; + --len; + } + } + } + else if (buf[0] == '9' && buf[1] == '1' && (buf[2] < '0' || buf[2] > '9')) + { + // It's a G91 command + inRelativeMode = true; + } + } + else if (c == ';') + { + static const char kisslicerHeightString[] = " END_LAYER_OBJECT z="; + if (len > 31 && StringStartsWith(buf, kisslicerHeightString)) + { + height = strtod(buf + sizeof(kisslicerHeightString)/sizeof(char) - 1, nullptr); + return true; + } + } + } + return foundHeight; +} + +// Scan the buffer for the layer height. The buffer is null-terminated. +bool FileInfoParser::FindLayerHeight(const char *buf, size_t len, float& layerHeight) const +{ + static const char* const layerHeightStrings[] = + { + "layer_height", // slic3r + "Layer height", // Cura + "layerHeight", // S3D + "layer_thickness_mm", // Kisslicer + "layerThickness" // Matter Control + }; + + if (*buf != 0) + { + ++buf; // make sure we can look back 1 character after we find a match + for (size_t i = 0; i < ARRAY_SIZE(layerHeightStrings); ++i) // search for each string in turn + { + const char *pos = buf; + for(;;) // loop until success or strstr returns null + { + pos = strstr(pos, layerHeightStrings[i]); + if (pos == nullptr) + { + break; // didn't find this string in the buffer, so try the next string + } + + const char c = pos[-1]; // fetch the previous character + pos += strlen(layerHeightStrings[i]); // skip the string we matched + if (c == ' ' || c == ';' || c == '\t') // check we are not in the middle of a word + { + while (strchr(" \t=:,", *pos) != nullptr) // skip the possible separators + { + ++pos; + } + char *tailPtr; + const float val = strtod(pos, &tailPtr); + if (tailPtr != pos) // if we found and converted a number + { + layerHeight = val; + return true; + } + } + } + } + } + + return false; +} + +bool FileInfoParser::FindSlicerInfo(const char* buf, size_t len, const StringRef& generatedBy) const +{ + static const char * const GeneratedByStrings[] = + { + "generated by ", // slic3r and S3D + ";Sliced by ", // ideaMaker + "; KISSlicer", // KISSlicer + ";Sliced at: ", // Cura (old) + ";Generated with " // Cura (new) + }; + + size_t index = 0; + const char* pos; + do + { + pos = strstr(buf, GeneratedByStrings[index]); + if (pos != nullptr) + { + break; + } + ++index; + } while (index < ARRAY_SIZE(GeneratedByStrings)); + + if (pos != nullptr) + { + const char* introString = ""; + switch (index) + { + default: + pos += strlen(GeneratedByStrings[index]); + break; + + case 2: // KISSlicer + pos += 2; + break; + + case 3: // Cura (old) + introString = "Cura at "; + pos += strlen(GeneratedByStrings[index]); + break; + } + + generatedBy.copy(introString); + while (*pos >= ' ') + { + generatedBy.cat(*pos++); + } + return true; + } + return false; +} + +// Scan the buffer for the filament used. The buffer is null-terminated. +// Returns the number of filaments found. +unsigned int FileInfoParser::FindFilamentUsed(const char* buf, size_t len, float *filamentUsed, size_t maxFilaments) const +{ + unsigned int filamentsFound = 0; + + // Look for filament usage as generated by Slic3r and Cura + const char* const filamentUsedStr1 = "ilament used"; // comment string used by slic3r and Cura, followed by filament used and "mm" + const char* p = buf; + while (filamentsFound < maxFilaments && (p = strstr(p, filamentUsedStr1)) != nullptr) + { + p += strlen(filamentUsedStr1); + while(strchr(" :=\t", *p) != nullptr) + { + ++p; // this allows for " = " from default slic3r comment and ": " from default Cura comment + } + while (isDigit(*p)) + { + char* q; + filamentUsed[filamentsFound] = strtod(p, &q); + p = q; + if (*p == 'm') + { + ++p; + if (*p == 'm') + { + ++p; + } + else + { + filamentUsed[filamentsFound] *= 1000.0; // Cura outputs filament used in metres not mm + } + } + ++filamentsFound; + while (strchr(", \t", *p) != nullptr) + { + ++p; + } + } + } + + // Look for filament usage string generated by Ideamaker + const char* const filamentUsedStr2 = ";Material#"; // comment string used by Ideamaker, e.g. ";Material#1 Used: 868.0" + p = buf; + while (filamentsFound < maxFilaments && (p = strstr(p, filamentUsedStr2)) != nullptr) + { + p += strlen(filamentUsedStr2); + char *q; + unsigned int num = strtoul(p, &q, 10); + if (q != p && num < maxFilaments) + { + p = q; + while(strchr(" Used:\t", *p) != nullptr) + { + ++p; // this allows for " Used: " + } + if (isDigit(*p)) + { + filamentUsed[filamentsFound] = strtod(p, &q); + ++filamentsFound; + } + } + } + + // Look for filament usage as generated by S3D + if (filamentsFound == 0) + { + const char *filamentLengthStr = "ilament length"; // comment string used by S3D + p = buf; + while (filamentsFound < maxFilaments && (p = strstr(p, filamentLengthStr)) != nullptr) + { + p += strlen(filamentLengthStr); + while(strchr(" :=\t", *p) != nullptr) + { + ++p; + } + if (isDigit(*p)) + { + filamentUsed[filamentsFound] = strtod(p, nullptr); // S3D reports filament usage in mm, no conversion needed + ++filamentsFound; + } + } + } + + // Look for filament usage as generated by recent KISSlicer versions + if (filamentsFound == 0) + { + const char *filamentLengthStr = "; Ext "; + p = buf; + while (filamentsFound < maxFilaments && (p = strstr(p, filamentLengthStr)) != nullptr) + { + p += strlen(filamentLengthStr); + while(isdigit(*p)) + { + ++p; + } + while(strchr(" :=\t", *p) != nullptr) + { + ++p; + } + + if (isDigit(*p)) + { + filamentUsed[filamentsFound] = strtod(p, nullptr); + ++filamentsFound; + } + } + } + + // Special case: Old KISSlicer only generates the filament volume, so we need to calculate the length from it + if (filamentsFound == 0) + { + const char *filamentVolumeStr = "; Estimated Build Volume: "; + p = strstr(buf, filamentVolumeStr); + if (p != nullptr) + { + const float filamentCMM = strtof(p + strlen(filamentVolumeStr), nullptr) * 1000.0; + filamentUsed[filamentsFound++] = filamentCMM / (Pi * fsquare(reprap.GetPlatform().GetFilamentWidth() / 2.0)); + } + } + + return filamentsFound; +} + +// End diff --git a/src/Storage/FileInfoParser.h b/src/Storage/FileInfoParser.h new file mode 100644 index 00000000..059d775c --- /dev/null +++ b/src/Storage/FileInfoParser.h @@ -0,0 +1,86 @@ +/* + * FileInfoParser.h + * + * Created on: 31 Mar 2018 + * Author: David + */ + +#ifndef SRC_STORAGE_FILEINFOPARSER_H_ +#define SRC_STORAGE_FILEINFOPARSER_H_ + +#include "RepRapFirmware.h" +#include "RTOSIface.h" + +const FilePosition GCODE_HEADER_SIZE = 20000uL; // How many bytes to read from the header - I (DC) have a Kisslicer file with a layer height comment 14Kb from the start +const FilePosition GCODE_FOOTER_SIZE = 400000uL; // How many bytes to read from the footer + +#if SAM4E || SAM4S || SAME70 +const size_t GCODE_READ_SIZE = 2048; // How many bytes to read in one go in GetFileInfo() (should be a multiple of 512 for read efficiency) +#else +const size_t GCODE_READ_SIZE = 1024; // How many bytes to read in one go in GetFileInfo() (should be a multiple of 512 for read efficiency) +#endif + +const size_t GCODE_OVERLAP_SIZE = 100; // Size of the overlapping buffer for searching (should be a multiple of 4) + +const uint32_t MAX_FILEINFO_PROCESS_TIME = 200; // Maximum time to spend polling for file info in each call +const uint32_t MaxFileParseInterval = 4000; // Maximum interval between repeat requests to parse a file + +// Struct to hold Gcode file information +struct GCodeFileInfo +{ + FilePosition fileSize; + time_t lastModifiedTime; + float layerHeight; + float firstLayerHeight; + float objectHeight; + float filamentNeeded[MaxExtruders]; + unsigned int numFilaments; + bool isValid; + bool incomplete; + String<50> generatedBy; + + void Init(); +}; + +enum FileParseState +{ + notParsing, + parsingHeader, + seeking, + parsingFooter +}; + +class FileInfoParser +{ +public: + FileInfoParser(); + + // The following method needs to be called until it returns true - this may take a few runs + bool GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info, bool quitEarly); + +private: + + // G-Code parser methods + bool FindHeight(const char* buf, size_t len, float& height) const; + bool FindFirstLayerHeight(const char* buf, size_t len, float& layerHeight) const; + bool FindLayerHeight(const char* buf, size_t len, float& layerHeight) const; + bool FindSlicerInfo(const char* buf, size_t len, const StringRef& generatedBy) const; + unsigned int FindFilamentUsed(const char* buf, size_t len, float *filamentUsed, size_t maxFilaments) const; + + // We parse G-Code files in multiple stages. These variables hold the required information + MutexHandle parserMutexHandle; + MutexStorage parserMutexStorage; + + FileParseState parseState; + String<MaxFilenameLength> filenameBeingParsed; + FileStore *fileBeingParsed; + FilePosition nextSeekPos; + GCodeFileInfo parsedFileInfo; + uint32_t lastFileParseTime; + uint32_t accumulatedParseTime, accumulatedReadTime, accumulatedSeekTime; + + size_t fileOverlapLength; + char fileOverlap[GCODE_OVERLAP_SIZE]; +}; + +#endif /* SRC_STORAGE_FILEINFOPARSER_H_ */ diff --git a/src/Storage/MassStorage.h b/src/Storage/MassStorage.h index b19c9c2a..e73716f3 100644 --- a/src/Storage/MassStorage.h +++ b/src/Storage/MassStorage.h @@ -7,6 +7,8 @@ #include "Libraries/Fatfs/ff.h" #include "GCodes/GCodeResult.h" #include "FileStore.h" +#include "FileInfoParser.h" + #include <ctime> #include "RTOSIface.h" @@ -50,6 +52,7 @@ public: unsigned int GetNumFreeFiles() const; void Spin(); MutexHandle GetVolumeMutexHandle(size_t vol) const { return info[vol].volMutexHandle; } + bool GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info, bool quitEarly) { return infoParser.GetFileInfo(directory, fileName, info, quitEarly); } enum class InfoResult : uint8_t { @@ -102,6 +105,7 @@ private: MutexStorage fsMutexStorage; MutexStorage dirMutexStorage; + FileInfoParser infoParser; DIR findDir; FileWriteBuffer *freeWriteBuffers; FileStore files[MAX_FILES]; diff --git a/src/Version.h b/src/Version.h index 6bbe1f7a..9165a6e9 100644 --- a/src/Version.h +++ b/src/Version.h @@ -19,7 +19,7 @@ #endif #ifndef DATE -# define DATE "2018-03-30b1" +# define DATE "2018-04-01b1" #endif #define AUTHORS "reprappro, dc42, chrishamm, t3p3, dnewman" |