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/PrintMonitor.cpp
David Crocker f5c4bdc770 Version 1.09r
Added timeout to output buffers destined for USB
Fixed bugs in thermocouple code
Reallocated thermocouple pin numbers
Made Roland mill and inkjet support conditional and normally disabled
Fixed issue with thermocouple code messing up the timekeeping system,
which was suspected of causing the network to be unreliable
Added support for M577 (thanks chrishamm)
2016-01-16 23:10:36 +00:00

1101 lines
32 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()
#if SUPPORT_ROLAND
&& !reprap.GetRoland()->Active()
#endif
)
{
// 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 (!OutputBuffer::Allocate(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 (!OutputBuffer::Allocate(response))
{
// Should never happen
return false;
}
response->copy("{\"err\":1}");
}
}
else if (IsPrinting())
{
if (!OutputBuffer::Allocate(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 (!OutputBuffer::Allocate(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:
{
// Provide rough estimation only if we haven't collected at least 2 layer samples
const float fractionPrinted = gCodes->FractionOfFilePrinted();
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
#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 - 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