This repository has been archived on 2025-02-01. You can view files and clone it, but cannot push or open issues or pull requests.
reprapfirmware-dc42/src/PrintMonitor.cpp
David Crocker efcb1e5ce6 Version 1.17c
New features:

The layer height comment in gcode files produced by Matter Control is
now recognised
M101 and M103 (firmware retraction commands generated by Simplify3D) are
now recognised
G20 bed probing now prints the mean height error and RMS deviation to 3
decimal places

Bug fixes:

G30 with no parameters now sets the machine position to the Z probe
trigger height instead of the user position. This means that running G28
(home) and G29 (bed probe) alternately no longer causes the bed height
map to creep up or down in average height when Z homing uses the Z
probe. However, it means that the Z height displayed after a G30 command
is no longer always the same as the Z probe trigger height.
In firmware 1.17 the speed change command by the M220 command got
delayed until the next G1 or G0 command with an F parameter was read.
In firmware 1.16 and 1.17 the acceleration used in G11 un-retraction
commands was sometimes incorrectly reduced if pressure advance was
configured on a different extruder.
In firmware 1.16 and 1.17 when pressure advance was configured, the
extruder acceleration limit implied by pressure advance used the
pressure advance value for the wrong extruder.
Firmware retraction did not work if additional axes had been configured
e.g. for an IDEX printer.
G29 commands using the P parameter with an upper case M in the filename
were recognised as M commands instead
2017-01-14 18:52:12 +00:00

1165 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 "PrintMonitor.h"
#include "GCodes/GCodes.h"
#include "Heating/Heat.h"
#include "Movement/Move.h"
#include "Platform.h"
#include "RepRap.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();
const 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
{
static const char* const layerHeightStrings[] =
{
"layer_height", // slic3r
"Layer height", // Cura
"layerHeight", // S3D
"layer_thickness_mm", // Kisslicer
"layerThickness" // Matter Control
};
if (*buf != 0)
{
++buf; // make sure we can look back 1 character after we find a match
for (size_t i = 0; i < ARRAY_SIZE(layerHeightStrings); ++i) // search for each string in turn
{
const char *pos = buf;
for(;;) // loop until success or strstr returns null
{
pos = strstr(pos, layerHeightStrings[i]);
if (pos == nullptr)
{
break; // didn't find this string in the buffer, so try the next string
}
const char c = pos[-1]; // fetch the previous character
pos += strlen(layerHeightStrings[i]); // skip the string we matched
if (c == ' ' || c == ';' || c == '\t') // check we are not in the middle of a word
{
while (strchr(" \t=:,", *pos) != nullptr) // skip the possible separators
{
++pos;
}
char *tailPtr;
const float val = strtod(pos, &tailPtr);
if (tailPtr != pos) // if we found and converted a number
{
layerHeight = val;
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