/**************************************************************************************************** RepRapFirmware - PrintMonitor This class provides methods to obtain print end-time estimations and file information from generated G-Code files, which may be reported to auxiliary devices and to the web interface using status responses. ----------------------------------------------------------------------------------------------------- Version 0.1 Created on: Feb 24, 2015 Christian Hammacher Licence: GPL ****************************************************************************************************/ #include "RepRapFirmware.h" PrintMonitor::PrintMonitor(Platform *p, GCodes *gc) : platform(p), gCodes(gc), fileInfoDetected(false), printStartTime(0.0), currentLayer(0), firstLayerDuration(0.0), firstLayerHeight(0.0), firstLayerFilament(0.0), firstLayerProgress(0.0), warmUpDuration(0.0), layerEstimatedTimeLeft(0.0), lastLayerTime(0.0), lastLayerFilament(0.0), numLayerSamples(0) { } void PrintMonitor::Init() { longWait = platform->Time(); } void PrintMonitor::Spin() { if (gCodes->IsPausing() || gCodes->IsPaused() || gCodes->IsResuming()) { // TODO: maybe incorporate pause durations in print estimations in the future? platform->ClassReport(longWait); return; } if (gCodes->PrintingAFile()) { // May have just started a print, see if we're heating up if (warmUpDuration == 0.0) { // When a new print starts, the total (raw) extruder positions are zeroed float totalRawFilament = 0.0; for(size_t extruder=0; extruder < DRIVES - AXES; extruder++) { totalRawFilament += gCodes->GetRawExtruderPosition(extruder); } // See if at least one heater is active and set bool heatersAtHighTemperature = false; for(size_t heater=E0_HEATER; heaterGetStatus(heater) == Heat::HS_active && reprap.GetHeat()->GetActiveTemperature(heater) > TEMPERATURE_LOW_SO_DONT_CARE && reprap.GetHeat()->HeaterAtSetTemperature(heater)) { heatersAtHighTemperature = true; break; } } if (heatersAtHighTemperature && totalRawFilament != 0.0) { lastLayerTime = platform->Time(); warmUpDuration = lastLayerTime - printStartTime; if (fileInfoDetected && currentFileInfo.layerHeight > 0.0) { currentLayer = 1; } } } // Looks like the print has started else if (currentLayer > 0) { float liveCoords[DRIVES + 1]; reprap.GetMove()->LiveCoordinates(liveCoords); // See if we can determine the first layer height (must be smaller than the nozzle diameter) if (firstLayerHeight == 0.0) { if (liveCoords[Z_AXIS] < NOZZLE_DIAMETER && !gCodes->DoingFileMacro()) { firstLayerHeight = liveCoords[Z_AXIS]; } } // Then check if we've finished the first layer else if (firstLayerDuration == 0.0) { if (liveCoords[Z_AXIS] > firstLayerHeight * 1.05) // allow some tolerance for transform operations { firstLayerFilament = 0.0; for(size_t extruder=0; extruderGetRawExtruderPosition(extruder); } firstLayerDuration = platform->Time() - lastLayerTime; firstLayerProgress = gCodes->FractionOfFilePrinted(); } } // We have enough values to estimate the following layer heights else if (currentFileInfo.objectHeight > 0.0) { unsigned int estimatedLayer = round((liveCoords[Z_AXIS] - firstLayerHeight) / currentFileInfo.layerHeight) + 1; if (estimatedLayer == currentLayer + 1) // on layer change { // Record untainted extruder positions for filament-based estimation float extrRawTotal = 0.0; for(size_t extruder=0; extruder < DRIVES - AXES; extruder++) { extrRawTotal += gCodes->GetRawExtruderPosition(extruder); } const float now = platform->Time(); unsigned int remainingLayers; remainingLayers = round((currentFileInfo.objectHeight - firstLayerHeight) / currentFileInfo.layerHeight) + 1; remainingLayers -= currentLayer; if (currentLayer > 1) { // Record a new set if (numLayerSamples < MAX_LAYER_SAMPLES) { layerDurations[numLayerSamples] = now - lastLayerTime; if (!numLayerSamples) { filamentUsagePerLayer[numLayerSamples] = extrRawTotal - firstLayerFilament; } else { filamentUsagePerLayer[numLayerSamples] = extrRawTotal - lastLayerFilament; } fileProgressPerLayer[numLayerSamples] = gCodes->FractionOfFilePrinted(); numLayerSamples++; } else { for(size_t i=1; iFractionOfFilePrinted(); } } // Update layer-based estimation times float avgLayerTime, avgLayerDelta = 0.0; if (numLayerSamples) { avgLayerTime = 0.0; for(size_t layer=0; layerClassReport(longWait); } void PrintMonitor::StartingPrint(const char* filename) { fileInfoDetected = GetFileInfo(platform->GetGCodeDir(), filename, currentFileInfo); strncpy(fileBeingPrinted, filename, ARRAY_SIZE(fileBeingPrinted)); fileBeingPrinted[ARRAY_UPB(fileBeingPrinted)] = 0; } void PrintMonitor::StartedPrint() { printStartTime = platform->Time(); } void PrintMonitor::StoppedPrint() { currentLayer = numLayerSamples = 0; firstLayerDuration = firstLayerHeight = firstLayerFilament = firstLayerProgress = 0.0; layerEstimatedTimeLeft = printStartTime = warmUpDuration = 0.0; lastLayerTime = lastLayerFilament = 0.0; } bool PrintMonitor::GetFileInfo(const char *directory, const char *fileName, GcodeFileInfo& info) const { if (reprap.GetPlatform()->GetMassStorage()->PathExists(directory, fileName)) { // Webserver can use this method to determine if a file was passed or not return false; } FileStore *f = reprap.GetPlatform()->GetFileStore(directory, fileName, false); if (f != NULL) { // Try to find the object height by looking for the last G1 Zxxx command in the file info.fileSize = f->Length(); info.objectHeight = 0.0; info.layerHeight = 0.0; info.numFilaments = 0; info.generatedBy[0] = 0; for(size_t extr=0; extr(info.fileSize, readSize + overlap); int nbytes = f->Read(buf, sizeToRead); if (nbytes != (int)sizeToRead) { break; // read failed so give up } else { buf[sizeToRead] = 0; // Search for filament usage (Cura puts it at the beginning of a G-code file) if (!filamentsFound) { nFilaments = FindFilamentUsed(buf, sizeToRead, filaments, DRIVES - AXES); if (nFilaments != 0 && nFilaments >= filamentsFound) { filamentsFound = nFilaments; for (unsigned int i = 0; i < filamentsFound; ++i) { info.filamentNeeded[i] = filaments[i]; } } } // Look for layer height if (!foundLayerHeight) { foundLayerHeight = FindLayerHeight(buf, sizeToRead, info.layerHeight); } // Look for slicer program if (!info.generatedBy[0]) { // Slic3r and S3D const char* generatedByString = "generated by "; char* pos = strstr(buf, generatedByString); size_t generatedByLength = ARRAY_SIZE(info.generatedBy); if (pos != NULL) { pos += strlen(generatedByString); size_t i = 0; while (i < ARRAY_SIZE(info.generatedBy) - 1 && *pos >= ' ') { char c = *pos++; if (c == '"' || c == '\\') { // Need to escape the quote-mark for JSON if (i > ARRAY_SIZE(info.generatedBy) - 3) { break; } info.generatedBy[i++] = '\\'; } info.generatedBy[i++] = c; } info.generatedBy[i] = 0; } // Cura const char* slicedAtString = ";Sliced at: "; pos = strstr(buf, slicedAtString); if (pos != NULL) { pos += strlen(slicedAtString); strcpy(info.generatedBy, "Cura at "); size_t i = 8; while (i < ARRAY_SIZE(info.generatedBy) - 1 && *pos >= ' ') { char c = *pos++; if (c == '"' || c == '\\') { // Need to escape the quote-mark for JSON if (i > ARRAY_SIZE(info.generatedBy) - 3) { break; } info.generatedBy[i++] = '\\'; } info.generatedBy[i++] = c; } info.generatedBy[i] = 0; } } // Add code to look for other values here... } // Have we collected everything? if (filamentsFound && foundLayerHeight && info.generatedBy[0]) { break; } } // Now get the object height and filament used by reading the end of the file { size_t sizeToRead; if (info.fileSize <= readSize + overlap) { sizeToRead = info.fileSize; // read the whole file in one go } else { sizeToRead = info.fileSize % readSize; if (sizeToRead <= overlap) { sizeToRead += readSize; } } unsigned long seekPos = info.fileSize - sizeToRead; // read on a 512b boundary size_t sizeToScan = sizeToRead; for (;;) { if (!f->Seek(seekPos)) { break; } int nbytes = f->Read(buf, sizeToRead); if (nbytes != (int)sizeToRead) { break; // read failed so give up } // Search for filament used if (!filamentsFound) { nFilaments = FindFilamentUsed(buf, sizeToScan, filaments, DRIVES - AXES); if (nFilaments != 0 && nFilaments >= filamentsFound) { filamentsFound = nFilaments; for (unsigned int i = 0; i < filamentsFound; ++i) { info.filamentNeeded[i] = filaments[i]; } } } // Search for layer height if (!foundLayerHeight) { foundLayerHeight = FindLayerHeight(buf, sizeToScan, info.layerHeight); } // Search for object height if (FindHeight(buf, sizeToScan, info.objectHeight)) { break; // quit if found height } if (seekPos == 0 || info.fileSize - seekPos >= 200000uL) // scan up to about the last 200K of the file (32K wasn't enough) { break; // quit if reached start of file or already scanned the last 32K of the file } seekPos -= readSize; sizeToRead = readSize; sizeToScan = readSize + overlap; memcpy(buf + sizeToRead, buf, overlap); } info.numFilaments = filamentsFound; } } f->Close(); //debugPrintf("Set height %f and filament %f\n", height, filamentUsed); return true; } return false; } void PrintMonitor::GetFileInfoResponse(StringRef& response, const char* filename) const { // Poll file info for a specific file if (filename != NULL) { GcodeFileInfo info; bool found = GetFileInfo("0:/", filename, info); if (found) { response.printf("{\"err\":0,\"size\":%lu,\"height\":%.2f,\"layerHeight\":%.2f,\"filament\":", info.fileSize, info.objectHeight, info.layerHeight); char ch = '['; if (info.numFilaments == 0) { response.catf("%c", ch); } else { for (unsigned int i = 0; i < info.numFilaments; ++i) { response.catf("%c%.1f", ch, info.filamentNeeded[i]); ch = ','; } } response.catf("],\"generatedBy\":\"%s\"}", info.generatedBy); } else { response.copy("{\"err\":1}"); } } else if (gCodes->PrintingAFile() && fileInfoDetected) { // Poll file info about a file currently being printed response.printf("{\"err\":0,\"size\":%lu,\"height\":%.2f,\"layerHeight\":%.2f,\"filament\":", currentFileInfo.fileSize, currentFileInfo.objectHeight, currentFileInfo.layerHeight); char ch = '['; if (currentFileInfo.numFilaments == 0) { response.catf("%c", ch); } else { for (unsigned int i = 0; i < currentFileInfo.numFilaments; ++i) { response.catf("%c%.1f", ch, currentFileInfo.filamentNeeded[i]); ch = ','; } } response.catf("],\"generatedBy\":\"%s\",\"printDuration\":%d,\"fileName\":\"%s\"}", currentFileInfo.generatedBy, (int)((platform->Time() - printStartTime) * 1000.0), fileBeingPrinted); } else { response.copy("{\"err\":1}"); } } float PrintMonitor::EstimateTimeLeft(PrintEstimationMethod method) const { // We can't provide an estimation if we're not printing (yet) if (!gCodes->PrintingAFile() || (fileInfoDetected && currentFileInfo.numFilaments != 0 && warmUpDuration == 0.0)) { return 0.0; } // Take into account the first layer time only if we haven't got any other samples float realPrintDuration = (platform->Time() - printStartTime) - warmUpDuration; if (numLayerSamples != 0) { realPrintDuration -= firstLayerDuration; } // Actual estimations switch (method) { case fileBased: { // Provide rough estimation only if we haven't collected any layer samples float fractionPrinted = gCodes->FractionOfFilePrinted(); if (numLayerSamples == 0 || !fileInfoDetected || currentFileInfo.objectHeight == 0.0) { return realPrintDuration * (1.0 / fractionPrinted) - realPrintDuration; } // Each layer takes time to achieve more file progress, so take an average over our samples float avgSecondsByProgress = 0.0, lastLayerProgress = 0.0; for (unsigned int layer=0; layerGetRawExtruderPosition(extruder); } // If we have a reasonable amount of filament extruded, calculate estimated times left if (totalFilamentNeeded > 0.0 && extrRawTotal > totalFilamentNeeded * ESTIMATION_MIN_FILAMENT_USAGE) { if (firstLayerFilament == 0.0) { return realPrintDuration * (totalFilamentNeeded - extrRawTotal) / extrRawTotal; } float filamentRate; if (numLayerSamples != 0) { filamentRate = 0.0; for (unsigned int i=0; i 0.0) { float timeLeft = layerEstimatedTimeLeft - (platform->Time() - lastLayerTime); if (timeLeft > 0.0) { return timeLeft; } } break; } return 0.0; } // Get information for the specified file, or the currently printing file, in JSON format // Get information for a file on the SD card // Scan the buffer for a G1 Zxxx command. The buffer is null-terminated. bool PrintMonitor::FindHeight(const char* buf, size_t len, float& height) const { //debugPrintf("Scanning %u bytes starting %.100s\n", len, buf); bool inComment; unsigned int zPos; for(size_t i = len - 5; i > 0; i--) { // Look for last "G0/G1 ... Z#HEIGHT#" command as generated by common slicers if (buf[i] == 'G' && (buf[i + 1] == '0' || buf[i + 1] == '1') && buf[i + 2] == ' ') { // Looks like we found a controlled move, however it could be in a comment, especially when using slic3r 1.1.1 inComment = false; size_t j = i; while (j != 0) { --j; char c = buf[j]; if (c == '\n' || c == '\r') { // It's not in a comment break; } if (c == ';') { // It is in a comment, so give up on this one inComment = true; break; } } if (inComment) continue; // Find 'Z' position and grab that value zPos = 0; for(int j=i +3; j < len - 2; j++) { char c = buf[j]; if (c < ' ') { // Skip all whitespaces... while (j < len - 2 && c <= ' ') { c = buf[++j]; } // ...to make sure ";End" doesn't follow G0 .. Z#HEIGHT# if (zPos != 0 && (buf[j] != ';' || buf[j + 1] != 'E')) { //debugPrintf("Found at offset %u text: %.100s\n", zPos, &buf[zPos + 1]); height = strtod(&buf[zPos + 1], NULL); return true; } break; } else if (c == ';') { // Ignore comments break; } else if (c == 'Z') { zPos = j; } } } } return false; } // Scan the buffer for the layer height. The buffer is null-terminated. bool PrintMonitor::FindLayerHeight(const char *buf, size_t len, float& layerHeight) const { // Look for layer_height as generated by Slic3r const char* layerHeightStringSlic3r = "; layer_height "; char *pos = strstr(buf, layerHeightStringSlic3r); if (pos != NULL) { pos += strlen(layerHeightStringSlic3r); while (strchr(" \t=:", *pos)) { ++pos; } layerHeight = strtod(pos, NULL); return true; } // Look for layer height as generated by Cura const char* layerHeightStringCura = "Layer height: "; pos = strstr(buf, layerHeightStringCura); if (pos != NULL) { pos += strlen(layerHeightStringCura); while (strchr(" \t=:", *pos)) { ++pos; } layerHeight = strtod(pos, NULL); return true; } // Look for layer height as generated by S3D const char* layerHeightStringS3D = "layerHeight,"; pos = strstr(buf, layerHeightStringS3D); if (pos != NULL) { pos += strlen(layerHeightStringS3D); layerHeight = strtod(pos, NULL); 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, unsigned int maxFilaments) const { unsigned int filamentsFound = 0; // Look for filament usage as generated by Slic3r and Cura const char* filamentUsedStr = "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, filamentUsedStr)) != NULL) { p += strlen(filamentUsedStr); while(strchr(" :=\t", *p) != NULL) { ++p; // this allows for " = " from default slic3r comment and ": " from default Cura comment } if (isDigit(*p)) { char* q; filamentUsed[filamentsFound] = strtod(p, &q); if (*q == 'm' && *(q + 1) != 'm') { filamentUsed[filamentsFound] *= 1000.0; // Cura outputs filament used in metres not mm } ++filamentsFound; } } // Look for filament usage as generated by S3D if (!filamentsFound) { const char *filamentLengthStr = "ilament length:"; // comment string used by S3D p = buf; while (filamentsFound < maxFilaments && (p = strstr(p, filamentLengthStr)) != NULL) { p += strlen(filamentLengthStr); while(strchr(" :=\t", *p) != NULL) { ++p; // this allows for " = " from default slic3r comment and ": " from default Cura comment } if (isDigit(*p)) { char* q; filamentUsed[filamentsFound] = strtod(p, &q); // S3D reports filament usage in mm, no conversion needed ++filamentsFound; } } } return filamentsFound; }