
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
1164 lines
35 KiB
C++
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
|