This repository has been archived on 2025-02-01. You can view files and clone it, but cannot push or open issues or pull requests.
reprapfirmware-dc42/src/PrintMonitor.cpp
David Crocker fdcef50706 Version 1.17RC3
Bed compensation taper is now applied to 3/4/5-point compensation
methods too
Recognise generated-with comment in new Cura gcode files
Recognise filament-used info min new kisslicer files
When uploading files, create the full path if necessary
Duet 0.8.5 webserver looks for gzipped files first
A second controlled fan on the Duet 0.6 is no longer inverted
2016-12-21 17:17:57 +00:00

1164 lines
35 KiB
C++

/****************************************************************************************************
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), isPrinting(false),
printStartTime(0), pauseStartTime(0.0), totalPauseTime(0.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)
{
filenameBeingPrinted[0] = 0;
}
void PrintMonitor::Init()
{
longWait = platform->Time();
lastUpdateTime = millis();
}
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, printingFileInfo);
if (!printingFileParsed)
{
platform->ClassReport(longWait);
return;
}
}
// Don't do any updates if the print has been paused
if (!gCodes->IsRunning())
{
if (pauseStartTime == 0.0)
{
pauseStartTime = platform->Time();
}
platform->ClassReport(longWait);
return;
}
// Otherwise collect some stats after a certain period of time
uint32_t now = millis();
if (IsPrinting()
#if SUPPORT_ROLAND
&& !reprap.GetRoland()->Active()
#endif
&& now - lastUpdateTime > PRINTMONITOR_UPDATE_INTERVAL)
{
// Adjust the actual print time if the print was paused before
if (pauseStartTime != 0.0)
{
totalPauseTime += platform->Time() - pauseStartTime;
pauseStartTime = 0.0;
}
// Have we just started a print? See if we're heating up
if (currentLayer == 0)
{
// Check if there are any active heaters
bool nozzleAtHighTemperature = false;
for(int heater = 0; heater < HEATERS; heater++)
{
if (reprap.GetHeat()->GetStatus(heater) == Heat::HS_active && reprap.GetHeat()->GetActiveTemperature(heater) > TEMPERATURE_LOW_SO_DONT_CARE)
{
heatingUp = true;
// Check if this heater is assigned to a tool and if it has reached its set temperature yet
if (reprap.IsHeaterAssignedToTool(heater))
{
if (!reprap.GetHeat()->HeaterAtSetTemperature(heater, false))
{
nozzleAtHighTemperature = false;
break;
}
nozzleAtHighTemperature = true;
}
}
}
// Yes - do we have live movement?
if (nozzleAtHighTemperature && !reprap.GetMove()->NoLiveMovement())
{
// Yes - we're actually starting the print
WarmUpComplete();
currentLayer = 1;
}
}
// Print is in progress and filament is being extruded
else if (!gCodes->DoingFileMacro() && reprap.GetMove()->IsExtruding())
{
float liveCoordinates[DRIVES];
reprap.GetMove()->LiveCoordinates(liveCoordinates, reprap.GetCurrentXAxes());
// See if we need to determine the first layer height (usually smaller than the nozzle diameter)
if (printingFileInfo.firstLayerHeight == 0.0)
{
if (liveCoordinates[Z_AXIS] < platform->GetNozzleDiameter() * 1.5)
{
// This shouldn't be needed because we parse the first layer height anyway, but it won't harm
printingFileInfo.firstLayerHeight = liveCoordinates[Z_AXIS];
}
}
// Check if we've finished the first layer
else if (currentLayer == 1)
{
if (liveCoordinates[Z_AXIS] > printingFileInfo.firstLayerHeight + LAYER_HEIGHT_TOLERANCE)
{
FirstLayerComplete();
currentLayer++;
lastLayerZ = liveCoordinates[Z_AXIS];
lastLayerChangeTime = GetPrintDuration();
}
}
// Check for following layer changes
else if (liveCoordinates[Z_AXIS] > lastLayerZ + LAYER_HEIGHT_TOLERANCE)
{
LayerComplete();
currentLayer++;
lastLayerZ = liveCoordinates[Z_AXIS];
lastLayerChangeTime = GetPrintDuration();
}
}
lastUpdateTime = now;
}
platform->ClassReport(longWait);
}
float PrintMonitor::GetWarmUpDuration() const
{
if (currentLayer > 0)
{
return warmUpDuration;
}
return heatingUp ? GetPrintDuration() : 0.0;
}
// Notifies this class that a file has been set for printing
void PrintMonitor::StartingPrint(const char* filename)
{
printingFileParsed = GetFileInfo(platform->GetGCodeDir(), filename, printingFileInfo);
strncpy(filenameBeingPrinted, filename, ARRAY_SIZE(filenameBeingPrinted));
filenameBeingPrinted[ARRAY_UPB(filenameBeingPrinted)] = 0;
}
// Tell this class that the file set for printing is now actually processed
void PrintMonitor::StartedPrint()
{
isPrinting = true;
printStartTime = platform->Time();
}
// This is called as soon as the heaters are at temperature and the actual print has started
void PrintMonitor::WarmUpComplete()
{
heatingUp = false;
warmUpDuration = GetPrintDuration();
}
// Called when the first layer has been finished
void PrintMonitor::FirstLayerComplete()
{
firstLayerFilament = gCodes->GetTotalRawExtrusion();
firstLayerDuration = GetPrintDuration() - warmUpDuration;
firstLayerProgress = gCodes->FractionOfFilePrinted();
// Update layer-based estimation time (if the object and layer heights are known)
// This won't be very accurate, but at least something can be sent the web interface and to PanelDue
if (printingFileInfo.objectHeight > 0.0 && printingFileInfo.layerHeight > 0.0)
{
unsigned int layersToPrint = round((printingFileInfo.objectHeight - printingFileInfo.firstLayerHeight) / printingFileInfo.layerHeight) + 1;
layerEstimatedTimeLeft = firstLayerDuration * FIRST_LAYER_SPEED_FACTOR * (layersToPrint - 1);
}
}
// This is called whenever a layer greater than 2 has been finished
void PrintMonitor::LayerComplete()
{
// Record a new set of layer, filament and file stats
const float extrRawTotal = gCodes->GetTotalRawExtrusion();
if (numLayerSamples < MAX_LAYER_SAMPLES)
{
if (numLayerSamples == 0)
{
filamentUsagePerLayer[numLayerSamples] = extrRawTotal - firstLayerFilament;
layerDurations[numLayerSamples] = GetPrintDuration() - warmUpDuration;
}
else
{
filamentUsagePerLayer[numLayerSamples] = extrRawTotal - lastLayerFilament;
layerDurations[numLayerSamples] = GetPrintDuration() - lastLayerChangeTime;
}
fileProgressPerLayer[numLayerSamples] = gCodes->FractionOfFilePrinted();
numLayerSamples++;
}
else
{
for(size_t i = 1; i < MAX_LAYER_SAMPLES; i++)
{
layerDurations[i - 1] = layerDurations[i];
filamentUsagePerLayer[i - 1] = filamentUsagePerLayer[i];
fileProgressPerLayer[i - 1] = fileProgressPerLayer[i];
}
layerDurations[MAX_LAYER_SAMPLES - 1] = GetPrintDuration() - lastLayerChangeTime;
filamentUsagePerLayer[MAX_LAYER_SAMPLES - 1] = extrRawTotal - lastLayerFilament;
fileProgressPerLayer[MAX_LAYER_SAMPLES - 1] = gCodes->FractionOfFilePrinted();
}
lastLayerFilament = extrRawTotal;
// Update layer-based estimation time (if the object and layer heights are known)
if (printingFileInfo.objectHeight > 0.0 && printingFileInfo.layerHeight > 0.0)
{
// Calculate the average layer time and include the first layer if possible
float avgLayerTime = (numLayerSamples < MAX_LAYER_SAMPLES)
? firstLayerDuration * FIRST_LAYER_SPEED_FACTOR
: 0.0;
for(size_t layer = 0; layer < numLayerSamples; layer++)
{
avgLayerTime += layerDurations[layer];
}
avgLayerTime /= (numLayerSamples < MAX_LAYER_SAMPLES) ? numLayerSamples + 1 : numLayerSamples;
// Estimate the layer-based time left
unsigned int totalLayers;
totalLayers = round((printingFileInfo.objectHeight - printingFileInfo.firstLayerHeight) / printingFileInfo.layerHeight) + 1;
if (currentLayer < totalLayers)
{
// Current layer is within reasonable boundaries, so an estimation can be made
layerEstimatedTimeLeft = avgLayerTime * (totalLayers - currentLayer);
}
else
{
// Current layer is higher than the maximum number of layers. Assume the print has almost finished
layerEstimatedTimeLeft = 0.1;
}
}
}
void PrintMonitor::StoppedPrint()
{
isPrinting = heatingUp = printingFileParsed = false;
currentLayer = numLayerSamples = 0;
pauseStartTime = totalPauseTime = 0.0;
firstLayerDuration = firstLayerFilament = firstLayerProgress = 0.0;
layerEstimatedTimeLeft = printStartTime = warmUpDuration = 0.0;
lastLayerChangeTime = lastLayerFilament = lastLayerZ = 0.0;
}
bool PrintMonitor::GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info)
{
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 = platform->GetFileStore(directory, fileName, false);
if (fileBeingParsed == nullptr)
{
// Something went wrong - we cannot open it
info.isValid = false;
return true;
}
// File has been opened, let's start now
strncpy(filenameBeingParsed, fileName, ARRAY_SIZE(filenameBeingParsed));
filenameBeingParsed[ARRAY_UPB(filenameBeingParsed)] = 0;
fileOverlapLength = 0;
// Set up the info struct
parsedFileInfo.isValid = true;
parsedFileInfo.fileSize = fileBeingParsed->Length();
parsedFileInfo.lastModifiedTime = reprap.GetPlatform()->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 < DRIVES - MIN_AXES; extr++)
{
parsedFileInfo.filamentNeeded[extr] = 0.0;
}
// Record some debug values here
if (reprap.Debug(modulePrintMonitor))
{
accumulatedReadTime = accumulatedParseTime = 0;
platform->MessageF(HOST_MESSAGE, "-- 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;
}
else if (!StringEquals(fileName, filenameBeingParsed))
{
// We are already parsing a different file. Try again later.
return false;
}
// 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
if (parseState == 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();
int nbytes = fileBeingParsed->Read(&buf[fileOverlapLength], sizeToRead);
if (nbytes != (int)sizeToRead)
{
platform->MessageF(HOST_MESSAGE, "Error: 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()->GetNumAxes());
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[0] == 0)
{
// Slic3r and S3D
const char* generatedByString = "generated by ";
const char* introString = "";
const char* pos = strstr(buf, generatedByString);
if (pos != nullptr)
{
pos += strlen(generatedByString);
}
else
{
// KISSlicer
pos = strstr(buf, "; KISSlicer");
if (pos != nullptr)
{
pos += 2;
}
else
{
// Cura (old)
const char* slicedAtString = ";Sliced at: ";
pos = strstr(buf, slicedAtString);
if (pos != nullptr)
{
pos += strlen(slicedAtString);
introString = "Cura at ";
}
else
{
// Cura (new)
const char* generatedWithString = ";Generated with ";
pos = strstr(buf, generatedWithString);
if (pos != nullptr)
{
pos += strlen(generatedWithString);
}
}
}
}
if (pos != nullptr)
{
strcpy(parsedFileInfo.generatedBy, introString);
size_t i = strlen(introString);
while (i < ARRAY_SIZE(parsedFileInfo.generatedBy) - 1 && *pos >= ' ')
{
parsedFileInfo.generatedBy[i++] = *pos++;
}
parsedFileInfo.generatedBy[i] = 0;
}
}
headerInfoComplete &= (parsedFileInfo.generatedBy[0] != 0);
// 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(HOST_MESSAGE, "Header complete, processed %lu bytes, read time %.3fs, parse time %.3fs\n",
fileBeingParsed->Position(), (float)accumulatedReadTime/1000.0, (float)accumulatedParseTime/1000.0);
}
// Go to the last chunk and proceed from there on
startTime = millis();
const FilePosition seekFromEnd = ((fileBeingParsed->Length() - 1) % GCODE_READ_SIZE) + 1;
fileBeingParsed->Seek(fileBeingParsed->Length() - seekFromEnd);
accumulatedSeekTime = millis() - startTime;
accumulatedReadTime = accumulatedParseTime = 0;
fileOverlapLength = 0;
parseState = parsingFooter;
}
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);
}
}
if (parseState == parsingFooter)
{
// Processing the footer. See how many bytes we need to read and if we can reuse the overlap
FilePosition pos = fileBeingParsed->Position();
sizeToRead = (size_t)min<FilePosition>(fileBeingParsed->Length() - pos, 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(HOST_MESSAGE, "Error: 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()->GetNumAxes());
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 || pos == 0 || fileBeingParsed->Length() - pos >= GCODE_FOOTER_SIZE)
{
if (reprap.Debug(modulePrintMonitor))
{
platform->MessageF(HOST_MESSAGE, "Footer complete, processed %lu bytes, read time %.3fs, parse time %.3fs, seek time %.3fs\n",
fileBeingParsed->Length() - fileBeingParsed->Position() + GCODE_READ_SIZE,
(float)accumulatedReadTime/1000.0, (float)accumulatedParseTime/1000.0, (float)accumulatedSeekTime/1000.0);
}
parseState = notParsing;
fileBeingParsed->Close();
info = parsedFileInfo;
return true;
}
// Else go back further
startTime = millis();
size_t seekOffset = (size_t)min<FilePosition>(pos, GCODE_READ_SIZE);
if (!fileBeingParsed->Seek(pos - seekOffset))
{
platform->Message(HOST_MESSAGE, "Error: Could not seek from end of file!\n");
parseState = notParsing;
fileBeingParsed->Close();
info = parsedFileInfo;
return true;
}
accumulatedSeekTime += millis() - startTime;
fileOverlapLength = (size_t)min<FilePosition>(sizeToScan, GCODE_OVERLAP_SIZE);
memcpy(fileOverlap, buf, fileOverlapLength);
}
} while (!isPrinting && millis() - 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\":",
info.objectHeight, info.firstLayerHeight, 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, info.filamentNeeded[i]);
ch = ',';
}
}
response->cat("],\"generatedBy\":");
response->EncodeString(info.generatedBy, ARRAY_SIZE(info.generatedBy), 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, printingFileInfo.objectHeight, printingFileInfo.firstLayerHeight, 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, printingFileInfo.filamentNeeded[i]);
ch = ',';
}
}
response->cat("],\"generatedBy\":");
response->EncodeString(printingFileInfo.generatedBy, ARRAY_SIZE(printingFileInfo.generatedBy), false);
response->catf(",\"printDuration\":%d,\"fileName\":", (int)GetPrintDuration());
response->EncodeString(filenameBeingPrinted, ARRAY_SIZE(filenameBeingPrinted), false);
response->cat('}');
}
else
{
if (!OutputBuffer::Allocate(response))
{
// Should never happen
return false;
}
response->copy("{\"err\":1}");
}
return true;
}
// May be called from ISR
void PrintMonitor::StopParsing(const char *filename)
{
if (parseState != notParsing && StringEquals(filenameBeingParsed, filename))
{
if (filenameBeingPrinted[0] != 0 && !printingFileParsed)
{
// If this is the file we're parsing for internal purposes, don't bother with this request
return;
}
parseState = notParsing;
fileBeingParsed->Close();
}
}
// Estimate the print time left in seconds on a preset estimation method
float PrintMonitor::EstimateTimeLeft(PrintEstimationMethod method) const
{
// We can't provide an estimation if we don't have any information about the file
if (!printingFileParsed)
{
return 0.0;
}
// How long have we been printing continuously?
float realPrintDuration = GetPrintDuration() - warmUpDuration;
switch (method)
{
case fileBased:
{
// Can we provide an estimation at all?
const float fractionPrinted = gCodes->FractionOfFilePrinted();
if (fractionPrinted < ESTIMATION_MIN_FILE_USAGE || heatingUp)
{
// No, we haven't printed enough of the file yet. We can't provide an estimation at this moment
return 0.0;
}
if (fractionPrinted == 1.0)
{
// No, but the file has been processed entirely. It won't take long until the print finishes
return 0.1;
}
// See how long it takes per progress
float duration, fractionPrintedInLayers;
if (numLayerSamples == 0)
{
duration = firstLayerDuration;
fractionPrintedInLayers = firstLayerProgress;
}
else if (numLayerSamples == 1)
{
duration = layerDurations[0];
fractionPrintedInLayers = fileProgressPerLayer[0] - firstLayerProgress;
}
else if (numLayerSamples > 1)
{
duration = 0.0;
for(size_t sample = 1; sample < numLayerSamples; sample++)
{
duration += layerDurations[sample];
}
fractionPrintedInLayers = fileProgressPerLayer[numLayerSamples - 1] - fileProgressPerLayer[0];
}
// Can we use these values?
if (fractionPrintedInLayers < ESTIMATION_MIN_FILE_USAGE)
{
// No - only provide a rough estimation
return max<float>(realPrintDuration * (1.0 / fractionPrinted) - realPrintDuration, 0.1);
}
// Yes...
return max<float>(duration * (1.0 - fractionPrinted) / fractionPrintedInLayers, 0.1);
}
case filamentBased:
{
// Need some file information, otherwise this method won't work
if (currentLayer == 0 || printingFileInfo.numFilaments == 0
#if SUPPORT_ROLAND
|| reprap.GetRoland()->Active()
#endif
)
{
return 0.0;
}
// Sum up the filament usage and the filament needed
float totalFilamentNeeded = 0.0;
const float extrRawTotal = gCodes->GetTotalRawExtrusion();
for(size_t extruder = 0; extruder < DRIVES - reprap.GetGCodes()->GetNumAxes(); extruder++)
{
totalFilamentNeeded += printingFileInfo.filamentNeeded[extruder];
}
// If we have a reasonable amount of filament extruded, calculate estimated times left
if (totalFilamentNeeded > 0.0 && extrRawTotal > totalFilamentNeeded * ESTIMATION_MIN_FILAMENT_USAGE)
{
// Do we have more total filament extruded than reported by the file
if (extrRawTotal >= totalFilamentNeeded)
{
// Yes - assume the print has almost finished
return 0.1;
}
// Get filament usage per layer
float filamentRate = 0.0;
if (numLayerSamples > 0)
{
for(size_t i = 0; i < numLayerSamples; i++)
{
filamentRate += filamentUsagePerLayer[i] / layerDurations[i];
}
filamentRate /= numLayerSamples;
}
else if (firstLayerDuration > 0.0)
{
filamentRate = firstLayerFilament / firstLayerDuration;
}
// Can we provide a good estimation?
if (filamentRate == 0.0)
{
// No - calculate time left based on the filament we have extruded so far
return realPrintDuration * (totalFilamentNeeded - extrRawTotal) / extrRawTotal;
}
return (totalFilamentNeeded - extrRawTotal) / filamentRate;
}
break;
}
case layerBased:
// Layer-based estimations are made after each layer change, only reflect this value
if (layerEstimatedTimeLeft > 0.0)
{
float timeLeft = layerEstimatedTimeLeft - (GetPrintDuration() - lastLayerChangeTime);
return (timeLeft > 0.0) ? timeLeft : 0.1;
}
break;
}
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
{
// Look for layer_height as generated by Slic3r
const char* layerHeightStringSlic3r = "; layer_height ";
const char *pos = strstr(buf, layerHeightStringSlic3r);
if (pos != nullptr)
{
pos += strlen(layerHeightStringSlic3r);
while (strchr(" \t=:", *pos))
{
++pos;
}
layerHeight = strtod(pos, nullptr);
return true;
}
// Look for layer height as generated by Cura
const char* layerHeightStringCura = "Layer height: ";
pos = strstr(buf, layerHeightStringCura);
if (pos != nullptr)
{
pos += strlen(layerHeightStringCura);
while (strchr(" \t=:", *pos))
{
++pos;
}
layerHeight = strtod(pos, nullptr);
return true;
}
// Look for layer height as generated by S3D
const char* layerHeightStringS3D = "layerHeight,";
pos = strstr(buf, layerHeightStringS3D);
if (pos != nullptr)
{
pos += strlen(layerHeightStringS3D);
layerHeight = strtod(pos, nullptr);
return true;
}
// Look for layer height as generated by KISSlicer
const char* layerHeightStringKisslicer = "layer_thickness_mm = ";
pos = strstr(buf, layerHeightStringKisslicer);
if (pos != nullptr)
{
pos += strlen(layerHeightStringKisslicer);
layerHeight = strtod(pos, nullptr);
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)) != nullptr)
{
p += strlen(filamentUsedStr);
while(strchr(" :=\t", *p) != nullptr)
{
++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)) != nullptr)
{
p += strlen(filamentLengthStr);
while(strchr(" :=\t", *p) != nullptr)
{
++p; // this allows for " = " from default slic3r comment and ": " from default Cura comment
}
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)
{
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)
{
const char *filamentVolumeStr = "; Estimated Build Volume: ";
p = strstr(buf, filamentVolumeStr);
if (p != nullptr)
{
float filamentCMM = strtod(p + strlen(filamentVolumeStr), nullptr) * 1000.0;
filamentUsed[filamentsFound++] = filamentCMM / (PI * (platform->GetFilamentWidth() / 2.0) * (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
{
if (!isPrinting)
{
// Can't provide a valid print duration if we don't know when it started
return 0.0;
}
float printDuration = platform->Time() - printStartTime - totalPauseTime;
if (pauseStartTime != 0.0)
{
// Take into account how long the machine has been paused
printDuration -= platform->Time() - pauseStartTime;
}
return printDuration;
}
// vim: ts=4:sw=4