
Added chrishamm's FTP fix Fix spurious extra command word in http parser Minor improvements to Heat and PrintMonitor
1094 lines
31 KiB
C++
1094 lines
31 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), isHeating(false),
|
|
printStartTime(0), pauseStartTime(0.0), totalPauseTime(0.0), currentLayer(0), warmUpDuration(0.0),
|
|
firstLayerDuration(0.0), firstLayerFilament(0.0), firstLayerProgress(0.0), lastLayerChangeTime(0.0),
|
|
lastLayerFilament(0.0), numLayerSamples(0), layerEstimatedTimeLeft(0.0), parseState(notParsing),
|
|
fileBeingParsed(nullptr), fileOverlapLength(0), printingFileParsed(false), accumulatedParseTime(0.0),
|
|
accumulatedReadTime(0.0)
|
|
{
|
|
filenameBeingPrinted[0] = 0;
|
|
}
|
|
|
|
void PrintMonitor::Init()
|
|
{
|
|
longWait = platform->Time();
|
|
}
|
|
|
|
void PrintMonitor::Spin()
|
|
{
|
|
// We might have started a file print while another G-Code file is being parsed.
|
|
// So we need to start this process once the other file has been processed.
|
|
if (filenameBeingPrinted[0] != 0 && !printingFileParsed)
|
|
{
|
|
printingFileParsed = GetFileInfo(platform->GetGCodeDir(), filenameBeingPrinted, printingFileInfo);
|
|
if (!printingFileParsed)
|
|
{
|
|
platform->ClassReport(longWait);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Don't update the print time estimations if there is no file info or if the print has been paused
|
|
if (gCodes->IsPausing() || gCodes->IsPaused() || gCodes->IsResuming())
|
|
{
|
|
if (pauseStartTime == 0.0)
|
|
{
|
|
pauseStartTime = platform->Time();
|
|
}
|
|
platform->ClassReport(longWait);
|
|
return;
|
|
}
|
|
|
|
// Otherwise try to update them
|
|
if (IsPrinting() && !reprap.GetRoland()->Active())
|
|
{
|
|
// We might need to adjust the actual print time if it 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 (warmUpDuration == 0.0)
|
|
{
|
|
// Check if at least one nozzle heater is active and set
|
|
bool heatersAtHighTemperature = false;
|
|
for(size_t heater = E0_HEATER; heater < HEATERS; heater++)
|
|
{
|
|
if (reprap.GetHeat()->GetStatus(heater) == Heat::HS_active &&
|
|
reprap.GetHeat()->GetActiveTemperature(heater) > TEMPERATURE_LOW_SO_DONT_CARE)
|
|
{
|
|
isHeating = true;
|
|
if (reprap.GetHeat()->HeaterAtSetTemperature(heater))
|
|
{
|
|
heatersAtHighTemperature = true;
|
|
isHeating = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Yes - do we have live momement?
|
|
if (heatersAtHighTemperature && !reprap.GetMove()->NoLiveMovement())
|
|
{
|
|
// Yes - we're actually starting the print
|
|
WarmUpComplete();
|
|
}
|
|
}
|
|
// Print is in progress...
|
|
else if (currentLayer > 0 && !gCodes->DoingFileMacro())
|
|
{
|
|
float liveCoords[DRIVES + 1];
|
|
reprap.GetMove()->LiveCoordinates(liveCoords);
|
|
|
|
// See if we need to determine the first layer height (usually smaller than the nozzle diameter)
|
|
if (printingFileInfo.firstLayerHeight == 0.0)
|
|
{
|
|
if (liveCoords[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 = liveCoords[Z_AXIS];
|
|
}
|
|
}
|
|
// Then check if we've finished the first layer
|
|
else if (firstLayerDuration == 0.0)
|
|
{
|
|
if (HeightMatches(liveCoords[Z_AXIS], printingFileInfo.firstLayerHeight + printingFileInfo.layerHeight))
|
|
{
|
|
// First layer is complete
|
|
FirstLayerComplete();
|
|
}
|
|
}
|
|
// We have enough values to estimate the following layer heights
|
|
else if (printingFileInfo.objectHeight > 0.0)
|
|
{
|
|
// Check for layer change
|
|
float nextLayerZ = printingFileInfo.firstLayerHeight + currentLayer * printingFileInfo.layerHeight;
|
|
if (HeightMatches(liveCoords[Z_AXIS], nextLayerZ))
|
|
{
|
|
// A new layer is now being printed
|
|
LayerComplete();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
platform->ClassReport(longWait);
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
warmUpDuration = GetPrintDuration();
|
|
if (printingFileInfo.layerHeight > 0.0) {
|
|
currentLayer = 1;
|
|
}
|
|
}
|
|
|
|
// Called when the first layer has been finished
|
|
void PrintMonitor::FirstLayerComplete()
|
|
{
|
|
firstLayerFilament = gCodes->GetTotalRawExtrusion();
|
|
firstLayerDuration = GetPrintDuration() - warmUpDuration;
|
|
firstLayerProgress = gCodes->FractionOfFilePrinted();
|
|
}
|
|
|
|
// This is called whenever another layer has been finished
|
|
void PrintMonitor::LayerComplete()
|
|
{
|
|
// Use untainted extruder positions for filament-based estimation
|
|
const float extrRawTotal = gCodes->GetTotalRawExtrusion();
|
|
|
|
// Record a new set of layer, filament and file stats
|
|
if (currentLayer > 1)
|
|
{
|
|
// Record a new set
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Update layer-based estimation times
|
|
unsigned int remainingLayers;
|
|
remainingLayers = round((printingFileInfo.objectHeight - printingFileInfo.firstLayerHeight) / printingFileInfo.layerHeight) + 1;
|
|
remainingLayers -= currentLayer;
|
|
|
|
float avgLayerTime, avgLayerDelta = 0.0;
|
|
if (numLayerSamples)
|
|
{
|
|
avgLayerTime = 0.0;
|
|
for(size_t layer = 0; layer < numLayerSamples; layer++)
|
|
{
|
|
avgLayerTime += layerDurations[layer];
|
|
if (layer)
|
|
{
|
|
avgLayerDelta += layerDurations[layer] - layerDurations[layer - 1];
|
|
}
|
|
}
|
|
avgLayerTime /= numLayerSamples;
|
|
avgLayerDelta /= numLayerSamples;
|
|
}
|
|
else
|
|
{
|
|
avgLayerTime = firstLayerDuration * FIRST_LAYER_SPEED_FACTOR;
|
|
}
|
|
|
|
layerEstimatedTimeLeft = (avgLayerTime * remainingLayers) - (avgLayerDelta * remainingLayers);
|
|
if (layerEstimatedTimeLeft < 0.0)
|
|
{
|
|
layerEstimatedTimeLeft = avgLayerTime * remainingLayers;
|
|
}
|
|
|
|
// Set new layer values
|
|
currentLayer++;
|
|
lastLayerChangeTime = GetPrintDuration();
|
|
lastLayerFilament = extrRawTotal;
|
|
}
|
|
|
|
void PrintMonitor::StoppedPrint()
|
|
{
|
|
isPrinting = printingFileParsed = false;
|
|
currentLayer = numLayerSamples = 0;
|
|
pauseStartTime = totalPauseTime = 0.0;
|
|
firstLayerDuration = firstLayerFilament = firstLayerProgress = 0.0;
|
|
layerEstimatedTimeLeft = printStartTime = warmUpDuration = 0.0;
|
|
lastLayerChangeTime = lastLayerFilament = 0.0;
|
|
}
|
|
|
|
bool PrintMonitor::GetFileInfo(const char *directory, const char *fileName, GCodeFileInfo& info)
|
|
{
|
|
// Webserver may call rr_fileinfo for a directory, check this case here
|
|
if (reprap.GetPlatform()->GetMassStorage()->DirectoryExists(directory, fileName))
|
|
{
|
|
info.isValid = false;
|
|
return true;
|
|
}
|
|
|
|
// Are we still parsing a file?
|
|
if (parseState != notParsing)
|
|
{
|
|
if (!StringEquals(fileName, filenameBeingParsed))
|
|
{
|
|
// Yes - but it's not the file we're processing. Try again later
|
|
return false;
|
|
}
|
|
}
|
|
else if (parseState == notParsing)
|
|
{
|
|
// No - see if we can access the file
|
|
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.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 - AXES; extr++)
|
|
{
|
|
parsedFileInfo.filamentNeeded[extr] = 0.0;
|
|
}
|
|
|
|
// Record some debug values here
|
|
if (reprap.Debug(modulePrintMonitor))
|
|
{
|
|
accumulatedReadTime = accumulatedParseTime = 0.0;
|
|
platform->MessageF(GENERIC_MESSAGE, "-- Parsing file %s --\n", fileName);
|
|
}
|
|
|
|
// If the file is empty or no 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;
|
|
}
|
|
|
|
// First try to process the header of the file
|
|
float startTime = platform->Time();
|
|
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 1024 bytes, but use overlap (total 1124 bytes) 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;
|
|
}
|
|
|
|
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
|
|
if (reprap.Debug(modulePrintMonitor))
|
|
{
|
|
const float now = platform->Time();
|
|
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 - AXES);
|
|
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 ";
|
|
char* pos = strstr(buf, generatedByString);
|
|
if (pos != nullptr)
|
|
{
|
|
pos += strlen(generatedByString);
|
|
size_t i = 0;
|
|
while (i < ARRAY_SIZE(parsedFileInfo.generatedBy) - 1 && *pos >= ' ')
|
|
{
|
|
char c = *pos++;
|
|
if (c == '"' || c == '\\')
|
|
{
|
|
// Need to escape the quote-mark for JSON
|
|
if (i > ARRAY_SIZE(parsedFileInfo.generatedBy) - 3)
|
|
{
|
|
break;
|
|
}
|
|
parsedFileInfo.generatedBy[i++] = '\\';
|
|
}
|
|
parsedFileInfo.generatedBy[i++] = c;
|
|
}
|
|
parsedFileInfo.generatedBy[i] = 0;
|
|
}
|
|
|
|
// Cura
|
|
const char* slicedAtString = ";Sliced at: ";
|
|
pos = strstr(buf, slicedAtString);
|
|
if (pos != nullptr)
|
|
{
|
|
pos += strlen(slicedAtString);
|
|
strcpy(parsedFileInfo.generatedBy, "Cura at ");
|
|
size_t i = 8;
|
|
while (i < ARRAY_SIZE(parsedFileInfo.generatedBy) - 1 && *pos >= ' ')
|
|
{
|
|
char c = *pos++;
|
|
if (c == '"' || c == '\\')
|
|
{
|
|
if (i > ARRAY_SIZE(parsedFileInfo.generatedBy) - 3)
|
|
{
|
|
break;
|
|
}
|
|
parsedFileInfo.generatedBy[i++] = '\\';
|
|
}
|
|
parsedFileInfo.generatedBy[i++] = c;
|
|
}
|
|
parsedFileInfo.generatedBy[i] = 0;
|
|
}
|
|
|
|
// KISSlicer
|
|
const char* kisslicerStart = "; KISSlicer";
|
|
if (StringStartsWith(buf, kisslicerStart))
|
|
{
|
|
size_t stringLength = 0;
|
|
for(size_t i = 2; i < ARRAY_UPB(parsedFileInfo.generatedBy); i++)
|
|
{
|
|
if (buf[i] == '\r' || buf[i] == '\n')
|
|
{
|
|
break;
|
|
}
|
|
|
|
parsedFileInfo.generatedBy[stringLength++] = buf[i];
|
|
}
|
|
parsedFileInfo.generatedBy[stringLength] = 0;
|
|
}
|
|
}
|
|
headerInfoComplete &= (parsedFileInfo.generatedBy[0] != 0);
|
|
|
|
// Keep track of the time stats
|
|
if (reprap.Debug(modulePrintMonitor))
|
|
{
|
|
accumulatedParseTime += platform->Time() - 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(GENERIC_MESSAGE, "Header complete, processed %lu bytes total\n", fileBeingParsed->Position());
|
|
platform->MessageF(GENERIC_MESSAGE, "Accumulated file read time: %fs, accumulated parsing time: %fs\n", accumulatedReadTime, accumulatedParseTime);
|
|
accumulatedReadTime = accumulatedParseTime = 0.0;
|
|
}
|
|
|
|
// Go to the last sector and proceed from there on
|
|
const FilePosition seekFromEnd = ((fileBeingParsed->Length() - 1) % GCODE_READ_SIZE) + 1;
|
|
fileBeingParsed->Seek(fileBeingParsed->Length() - seekFromEnd);
|
|
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);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Processing the footer. See how many bytes we need to read and if we can reuse the overlap
|
|
bool footerInfoComplete = true;
|
|
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
|
|
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
|
|
if (reprap.Debug(modulePrintMonitor))
|
|
{
|
|
const float now = platform->Time();
|
|
accumulatedReadTime += now - startTime;
|
|
startTime = now;
|
|
}
|
|
|
|
// Search for filament used
|
|
if (parsedFileInfo.numFilaments == 0)
|
|
{
|
|
parsedFileInfo.numFilaments = FindFilamentUsed(buf, sizeToScan, parsedFileInfo.filamentNeeded, DRIVES - AXES);
|
|
footerInfoComplete &= (parsedFileInfo.numFilaments != 0);
|
|
}
|
|
|
|
// Search for layer height
|
|
if (parsedFileInfo.layerHeight == 0.0)
|
|
{
|
|
footerInfoComplete &= FindLayerHeight(buf, sizeToScan, parsedFileInfo.layerHeight);
|
|
}
|
|
|
|
// Search for object height
|
|
if (parsedFileInfo.objectHeight == 0.0)
|
|
{
|
|
footerInfoComplete &= FindHeight(buf, sizeToScan, parsedFileInfo.objectHeight);
|
|
}
|
|
|
|
// Keep track of the time stats
|
|
if (reprap.Debug(modulePrintMonitor))
|
|
{
|
|
accumulatedParseTime += platform->Time() - startTime;
|
|
}
|
|
|
|
// If we've collected all details, scanned the last 128K 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(GENERIC_MESSAGE, "Footer complete, processed %lu bytes total\n", fileBeingParsed->Length() - fileBeingParsed->Position() + GCODE_READ_SIZE);
|
|
platform->MessageF(GENERIC_MESSAGE, "Accumulated file read time: %fs, accumulated parsing time: %fs\n", accumulatedReadTime, accumulatedParseTime);
|
|
}
|
|
parseState = notParsing;
|
|
fileBeingParsed->Close();
|
|
info = parsedFileInfo;
|
|
return true;
|
|
}
|
|
|
|
// Else go back further
|
|
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;
|
|
}
|
|
|
|
fileOverlapLength = (size_t)min<FilePosition>(sizeToScan, GCODE_OVERLAP_SIZE);
|
|
memcpy(fileOverlap, buf, fileOverlapLength);
|
|
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)
|
|
{
|
|
GCodeFileInfo info;
|
|
if (!GetFileInfo("0:/", filename, info))
|
|
{
|
|
// This may take a few runs...
|
|
return false;
|
|
}
|
|
|
|
if (info.isValid)
|
|
{
|
|
if (!reprap.AllocateOutput(response))
|
|
{
|
|
// Should never happen
|
|
return false;
|
|
}
|
|
|
|
response->printf("{\"err\":0,\"size\":%lu,\"height\":%.2f,\"firstLayerHeight\":%.2f,\"layerHeight\":%.2f,\"filament\":",
|
|
info.fileSize, 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->catf("],\"generatedBy\":\"%s\"}", info.generatedBy);
|
|
}
|
|
else
|
|
{
|
|
if (!reprap.AllocateOutput(response))
|
|
{
|
|
// Should never happen
|
|
return false;
|
|
}
|
|
|
|
response->copy("{\"err\":1}");
|
|
}
|
|
}
|
|
else if (IsPrinting())
|
|
{
|
|
if (!reprap.AllocateOutput(response))
|
|
{
|
|
// Should never happen
|
|
return false;
|
|
}
|
|
|
|
// If we are still busy processing the file, return err code 2 so the web interface knows what's going on
|
|
if (!printingFileParsed)
|
|
{
|
|
response->copy("{\"err\":2}");
|
|
return true;
|
|
}
|
|
|
|
// 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->catf("],\"generatedBy\":\"%s\",\"printDuration\":%d,\"fileName\":\"%s\"}",
|
|
printingFileInfo.generatedBy, (int)GetPrintDuration(), filenameBeingPrinted);
|
|
}
|
|
else
|
|
{
|
|
if (!reprap.AllocateOutput(response))
|
|
{
|
|
// Should never happen
|
|
return false;
|
|
}
|
|
|
|
response->copy("{\"err\":1}");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void PrintMonitor::StopParsing(const char *filename)
|
|
{
|
|
if (parseState == notParsing)
|
|
{
|
|
// We're not parsing anything, stop here
|
|
return;
|
|
}
|
|
|
|
if (filenameBeingPrinted[0] != 0 && !printingFileParsed)
|
|
{
|
|
if (StringEquals(filename, filenameBeingPrinted))
|
|
{
|
|
// If this is the file we're parsing for internal purposes, don't bother with this request
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (StringEquals(filenameBeingParsed, filename))
|
|
{
|
|
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're not printing (yet)
|
|
if (!printingFileParsed && warmUpDuration == 0.0)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
// How long have we been printing continuously?
|
|
float realPrintDuration = GetPrintDuration() - warmUpDuration;
|
|
if (numLayerSamples != 0)
|
|
{
|
|
// Take into account the first layer time only if we haven't got any other samples
|
|
realPrintDuration -= firstLayerDuration;
|
|
}
|
|
|
|
// Actual estimations
|
|
switch (method)
|
|
{
|
|
case fileBased:
|
|
{
|
|
const float fractionPrinted = gCodes->FractionOfFilePrinted();
|
|
|
|
// Provide rough estimation only if we haven't collected at least 2 layer samples
|
|
if (numLayerSamples < 2 || !printingFileParsed || printingFileInfo.objectHeight == 0.0)
|
|
{
|
|
return (fractionPrinted < 0.01)
|
|
? 0.0
|
|
: realPrintDuration * (1.0 / fractionPrinted) - realPrintDuration;
|
|
}
|
|
|
|
// Work out how much progress we made in the layers we have data for, and how long it took.
|
|
// Can't use the first layer sample in the table because we don't know the fraction printed at the start.
|
|
float duration = 0.0;
|
|
for(size_t layer = 1; layer < numLayerSamples; layer++)
|
|
{
|
|
duration += layerDurations[layer];
|
|
}
|
|
const float fractionPrintedInLayers = fileProgressPerLayer[numLayerSamples - 1] - fileProgressPerLayer[0];
|
|
return (fractionPrintedInLayers < 0.01)
|
|
? 0.0
|
|
: duration * (1.0 - fractionPrinted)/fractionPrintedInLayers;
|
|
}
|
|
|
|
case filamentBased:
|
|
{
|
|
// Need some file information, otherwise this method won't work
|
|
if (!printingFileParsed || printingFileInfo.numFilaments == 0 || reprap.GetRoland()->Active())
|
|
{
|
|
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 - AXES; 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)
|
|
{
|
|
if (firstLayerFilament == 0.0)
|
|
{
|
|
return realPrintDuration * (totalFilamentNeeded - extrRawTotal) / extrRawTotal;
|
|
}
|
|
if (extrRawTotal >= totalFilamentNeeded)
|
|
{
|
|
return 0.0; // Avoid division by zero, else the web interface will report AJAX errors
|
|
}
|
|
|
|
float filamentRate;
|
|
if (numLayerSamples)
|
|
{
|
|
filamentRate = 0.0;
|
|
for(size_t i = 0; i < numLayerSamples; i++)
|
|
{
|
|
filamentRate += filamentUsagePerLayer[i] / layerDurations[i];
|
|
}
|
|
filamentRate /= numLayerSamples;
|
|
}
|
|
else
|
|
{
|
|
filamentRate = firstLayerFilament / firstLayerDuration;
|
|
}
|
|
|
|
return (totalFilamentNeeded - extrRawTotal) / filamentRate;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case layerBased:
|
|
if (layerEstimatedTimeLeft > 0.0)
|
|
{
|
|
float timeLeft = layerEstimatedTimeLeft - (GetPrintDuration() - lastLayerChangeTime);
|
|
if (timeLeft > 0.0)
|
|
{
|
|
return timeLeft;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
//debugPrintf("Scanning %u bytes starting %.100s\n", len, buf);
|
|
bool inComment = false, inRelativeMode = 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 first "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 (flHeight <= platform->GetNozzleDiameter() * 3.0)
|
|
{
|
|
height = flHeight; // Only report first Z height if it's somewhat reasonable
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
else if (buf[i] == ';')
|
|
{
|
|
// Ignore comments
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
// Scan the buffer in reverse 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, inRelativeMode = false;
|
|
unsigned int zPos;
|
|
for(int i = (int)len - 5; i > 0; i--)
|
|
{
|
|
if (inRelativeMode)
|
|
{
|
|
inRelativeMode = !(buf[i] == 'G' && buf[i + 1] == '9' && buf[i + 2] == '1' && buf[i + 3] <= ' ');
|
|
}
|
|
else if (buf[i] == 'G')
|
|
{
|
|
// Ignore G0/G1 codes if absolute mode was switched back using G90 (typical for Cura files)
|
|
if (buf[i + 1] == '9' && buf[i + 2] == '0' && buf[i + 3] <= ' ')
|
|
{
|
|
inRelativeMode = true;
|
|
}
|
|
// Look for last "G0/G1 ... Z#HEIGHT#" command as generated by common slicers
|
|
else if ((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 < (int)len - 2; j++)
|
|
{
|
|
char c = buf[j];
|
|
if (c < ' ')
|
|
{
|
|
// Skip all whitespaces...
|
|
while (j < (int)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], nullptr);
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
else if (c == ';')
|
|
{
|
|
// Ignore comments
|
|
break;
|
|
}
|
|
else if (c == 'Z')
|
|
{
|
|
zPos = j;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Special case: KISSlicer generates object height as a comment
|
|
else
|
|
{
|
|
const char *kisslicerHeightString = "; END_LAYER_OBJECT z=";
|
|
if (i < (int)len - 32 && StringStartsWith(buf + i, kisslicerHeightString))
|
|
{
|
|
height = strtod(buf + i + strlen(kisslicerHeightString), nullptr);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
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 ";
|
|
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))
|
|
{
|
|
char* q;
|
|
filamentUsed[filamentsFound] = strtod(p, &q); // S3D reports filament usage in mm, no conversion needed
|
|
++filamentsFound;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special case: 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 for the time estimation
|
|
printDuration -= platform->Time() - pauseStartTime;
|
|
}
|
|
return printDuration;
|
|
}
|
|
|
|
// vim: ts=4:sw=4
|