Files
OrcaSlicer/src/libslic3r/GCode/FanMover.cpp
Kiss Lorand 7de3031121 Fix fan stuck at wrong speed (#13336)
Fix fan speed staying high

When fan speed-up time is enabled, layers can inherit a higher fan speed than intended.

If a layer ended with a role-driven fan change, the last applied fan speed
was not properly tracked. As a result, the next layer could start with an
incorrect fan state and keep the higher speed until another fan command
overrode it.

Ensure the emitted fan speed is always tracked correctly, including when
flushing buffered commands at the end of a layer.
2026-04-26 20:19:40 +08:00

519 lines
23 KiB
C++

#include "FanMover.hpp"
#include "GCodeReader.hpp"
#include <iomanip>
/*
#include <memory.h>
#include <string.h>
#include <float.h>
#include "../libslic3r.h"
#include "../PrintConfig.hpp"
#include "../Utils.hpp"
#include "Print.hpp"
#include <boost/log/trivial.hpp>
*/
namespace Slic3r {
const std::string& FanMover::process_gcode(const std::string& gcode, bool flush)
{
m_process_output = "";
// recompute buffer time to recover from rounding
m_buffer_time_size = 0;
for (auto& data : m_buffer) m_buffer_time_size += data.time;
if(!gcode.empty())
m_parser.parse_buffer(gcode,
[this](GCodeReader& reader, const GCodeReader::GCodeLine& line) { /*m_process_output += line.raw() + "\n";*/ this->_process_gcode_line(reader, line); });
if (flush) {
while (!m_buffer.empty()) {
BufferData &front = m_buffer.front();
m_process_output += front.raw + "\n";
// Orca: Keep the emitted fan state in sync when flushing buffered fan commands.
if (front.fan_speed >= 0)
m_front_buffer_fan_speed = front.fan_speed;
remove_from_buffer(m_buffer.begin());
}
}
return m_process_output;
}
bool is_end_of_word(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == 0;
}
float get_axis_value(const std::string& line, char axis)
{
char match[3] = " X";
match[1] = axis;
size_t pos = line.find(match);
if (pos == std::string::npos) {
return NAN;
}
pos += 2;
//size_t end = std::min(line.find(' ', pos + 1), line.find(';', pos + 1));
// Try to parse the numeric value.
const char* c = line.c_str();
char* pend = nullptr;
errno = 0;
double v = strtod(c + pos, &pend);
if (pend != nullptr && errno == 0 && pend != c) {
// The axis value has been parsed correctly.
return float(v);
}
return NAN;
}
void change_axis_value(std::string& line, char axis, const float new_value, const int decimal_digits)
{
std::ostringstream ss;
ss << std::fixed << std::setprecision(decimal_digits) << new_value;
char match[3] = " X";
match[1] = axis;
size_t pos = line.find(match) + 2;
size_t end = std::min(line.find(' ', pos + 1), line.find(';', pos + 1));
line = line.replace(pos, end - pos, ss.str());
}
int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor) {
if (line.compare(0, 4, "M106") == 0) {
if (flavor == (gcfMach3) || flavor == (gcfMachinekit)) {
return (int16_t)get_axis_value(line, 'P');
} else {
// Bambu machines use both M106 P1(not P0!) and M106 for part cooling fan.
// Non-bambu machines usually use M106 (without P parameter) for part cooling fan.
// P2 is reserved for auxiliary fan regardless of bambu or not.
// To keep compatibility with Bambu machines, we accept M106 and M106 P1 as the only two valid form
// of gcode that control the part cooling fan. Any other command will be ignored!
const auto idx = get_axis_value(line, 'P');
if (!isnan(idx) && idx != 1.0f) {
return -1;
}
return (int16_t)get_axis_value(line, 'S');
}
} else if (line.compare(0, 4, "M127") == 0 || line.compare(0, 4, "M107") == 0) {
return 0;
} else if ((flavor == (gcfMakerWare) || flavor == (gcfSailfish)) && line.compare(0, 4, "M126") == 0) {
return (int16_t)get_axis_value(line, 'T');
} else {
return -1;
}
}
void FanMover::_put_in_middle_G1(std::list<BufferData>::iterator item_to_split, float nb_sec_since_itemtosplit_start, BufferData &&line_to_write) {
assert(item_to_split != m_buffer.end());
if (nb_sec_since_itemtosplit_start > item_to_split->time * 0.9) {
// doesn't really need to be split, print it after
m_buffer.insert(next(item_to_split), line_to_write);
} else if (nb_sec_since_itemtosplit_start < item_to_split->time * 0.1) {
// doesn't really need to be split, print it before
//will also print before if line_to_split.time == 0
m_buffer.insert(item_to_split, line_to_write);
} else if (item_to_split->raw.size() > 2
&& item_to_split->raw[0] == 'G' && item_to_split->raw[1] == '1' && item_to_split->raw[2] == ' ') {
float percent = nb_sec_since_itemtosplit_start / item_to_split->time;
BufferData before = *item_to_split;
before.time *= percent;
item_to_split->time *= (1-percent);
if (item_to_split->dx != 0) {
before.dx = item_to_split->dx * percent;
item_to_split->x += before.dx;
item_to_split->dx = item_to_split->dx * (1-percent);
change_axis_value(before.raw, 'X', before.x + before.dx, 3);
}
if (item_to_split->dy != 0) {
before.dy = item_to_split->dy * percent;
item_to_split->y += before.dy;
item_to_split->dy = item_to_split->dy * (1 - percent);
change_axis_value(before.raw, 'Y', before.y + before.dy, 3);
}
if (item_to_split->dz != 0) {
before.dz = item_to_split->dz * percent;
item_to_split->z += before.dz;
item_to_split->dz = item_to_split->dz * (1 - percent);
change_axis_value(before.raw, 'Z', before.z + before.dz, 3);
}
if (item_to_split->de != 0) {
if (relative_e) {
before.de = item_to_split->de * percent;
change_axis_value(before.raw, 'E', before.de, 5);
item_to_split->de = item_to_split->de * (1 - percent);
change_axis_value(item_to_split->raw, 'E', item_to_split->de, 5);
} else {
before.de = item_to_split->de * percent;
item_to_split->e += before.de;
item_to_split->de = item_to_split->de * (1 - percent);
change_axis_value(before.raw, 'E', before.e + before.de, 5);
}
}
//add before then line_to_write, then there is the modified data.
m_buffer.insert(item_to_split, before);
m_buffer.insert(item_to_split, line_to_write);
} else {
//not a G1, print it before
m_buffer.insert(item_to_split, line_to_write);
}
}
void FanMover::_print_in_middle_G1(BufferData& line_to_split, float nb_sec, const std::string &line_to_write) {
if (nb_sec < line_to_split.time * 0.1) {
// doesn't really need to be split, print it after
m_process_output += line_to_split.raw + "\n";
m_process_output += line_to_write + (line_to_write.back() == '\n'?"":"\n");
} else if (nb_sec > line_to_split.time * 0.9) {
// doesn't really need to be split, print it before
//will also print before if line_to_split.time == 0
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
m_process_output += line_to_split.raw + "\n";
}else if(line_to_split.raw.size() > 2
&& line_to_split.raw[0] == 'G' && line_to_split.raw[1] == '1' && line_to_split.raw[2] == ' ') {
float percent = nb_sec / line_to_split.time;
std::string before = line_to_split.raw;
std::string& after = line_to_split.raw;
if (line_to_split.dx != 0) {
change_axis_value(before, 'X', line_to_split.x + line_to_split.dx * percent, 3);
}
if (line_to_split.dy != 0) {
change_axis_value(before, 'Y', line_to_split.y + line_to_split.dy * percent, 3);
}
if (line_to_split.dz != 0) {
change_axis_value(before, 'Z', line_to_split.z + line_to_split.dz * percent, 3);
}
if (line_to_split.de != 0) {
if (relative_e) {
change_axis_value(before, 'E', line_to_split.de * percent, 5);
change_axis_value(after, 'E', line_to_split.de * (1 - percent), 5);
} else {
change_axis_value(before, 'E', line_to_split.e + line_to_split.de * percent, 5);
}
}
m_process_output += before + "\n";
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
m_process_output += line_to_split.raw + "\n";
} else {
//not a G1, print it before
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
m_process_output += line_to_split.raw + "\n";
}
}
void FanMover::_remove_slow_fan(int16_t min_speed, float past_sec) {
//erase fan in the buffer -> don't slowdown if you are in the process of step-up.
//we began at the "recent" side , and remove as long as we don't push past_sec to 0
auto it = m_buffer.begin();
while (it != m_buffer.end() && past_sec > 0) {
past_sec -= it->time;
if (it->fan_speed >= 0 && it->fan_speed < min_speed){
//found something that is lower than us
it = remove_from_buffer(it);
} else {
++it;
}
}
}
std::string FanMover::_set_fan(int16_t speed) {
//const Tool* tool = m_writer.get_tool(m_currrent_extruder < 20 ? m_currrent_extruder : 0);
return GCodeWriter::set_fan(m_writer.config.gcode_flavor.value, speed);
}
bool parse_number(const std::string_view sv, int& out)
{
{
// Legacy conversion, which is costly due to having to make a copy of the string before conversion.
try {
assert(sv.size() < 1024);
assert(sv.data() != nullptr);
std::string str{ sv };
size_t read = 0;
out = std::stoi(str, &read);
return str.size() == read;
}
catch (...) {
return false;
}
}
}
//FIXME: add other firmware
// or just create that damn new gcode writer arch
void FanMover::_process_T(const std::string_view command)
{
if (command.length() > 1) {
int eid = 0;
if (!parse_number(command.substr(1), eid) || eid < 0 || eid > 255) {
GCodeFlavor flavor = m_writer.config.gcode_flavor;
// Specific to the MMU2 V2 (see https://www.help.prusa3d.com/en/article/prusa-specific-g-codes_112173):
if ((flavor == gcfMarlinLegacy || flavor == gcfMarlinFirmware) && (command == "Tx" || command == "Tc" || command == "T?"))
return;
// T-1 is a valid gcode line for RepRap Firmwares (used to deselects all tools) see https://github.com/prusa3d/PrusaSlicer/issues/5677
if ((flavor != gcfRepRapFirmware && flavor != gcfRepRapSprinter) || eid != -1)
m_currrent_extruder = static_cast<uint16_t>(0);
} else {
m_currrent_extruder = static_cast<uint16_t>(eid);
}
}
}
void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCodeLine& line)
{
// processes 'normal' gcode lines
bool need_flush = false;
std::string cmd(line.cmd());
double time = 0;
int16_t fan_speed = -1;
if (cmd.length() > 1) {
if (line.has_f())
m_current_speed = line.f() / 60.0f;
switch (::toupper(cmd[0])) {
case 'T':
case 't':
_process_T(cmd);
break;
case 'G':
{
if (::atoi(&cmd[1]) == 1 || ::atoi(&cmd[1]) == 0) {
double distx = line.dist_X(reader);
double disty = line.dist_Y(reader);
double distz = line.dist_Z(reader);
double dist = distx * distx + disty * disty + distz * distz;
if (dist > 0) {
dist = std::sqrt(dist);
time = dist / m_current_speed;
}
}
break;
}
case 'M':
{
fan_speed = get_fan_speed(line.raw(), m_writer.config.gcode_flavor);
if (fan_speed >= 0) {
const auto fan_baseline = 255.0;
fan_speed = 100 * fan_speed / fan_baseline;
//speed change: stop kickstart reverting if any
m_current_kickstart.time = -1;
if (!m_is_custom_gcode) {
// if slow down => put in the queue. if not =>
if (m_back_buffer_fan_speed < fan_speed) {
if (nb_seconds_delay > 0 && (!only_overhangs || current_role == ExtrusionRole::erOverhangPerimeter)) {
//don't put this command in the queue
time = -1;
// this M106 need to go in the past
//check if we have ( kickstart and not in slowdown )
if (kickstart > 0 && fan_speed > m_front_buffer_fan_speed) {
//stop current kickstart , it's not relevant anymore
if (m_current_kickstart.time > 0) {
m_current_kickstart.time = (-1);
}
//if kickstart
// first erase everything lower that that value
_remove_slow_fan(fan_speed, m_buffer_time_size + 1);
// then erase everything lower that kickstart
_remove_slow_fan(fan_baseline, kickstart);
// print me
if (!m_buffer.empty() && (m_buffer_time_size - m_buffer.front().time * 0.1) > nb_seconds_delay) {
_print_in_middle_G1(m_buffer.front(), m_buffer_time_size - nb_seconds_delay, _set_fan(100));//m_writer.set_fan(100, true)); //FIXME extruder id (or use the gcode writer, but then you have to disable the multi-thread thing
remove_from_buffer(m_buffer.begin());
} else {
m_process_output += _set_fan(100);//m_writer.set_fan(100, true)); //FIXME extruder id (or use the gcode writer, but then you have to disable the multi-thread thing
}
//write it in the queue if possible
const float kickstart_duration = kickstart * float(fan_speed - m_front_buffer_fan_speed) / 100.f;
float time_count = kickstart_duration;
auto it = m_buffer.begin();
while (it != m_buffer.end() && time_count > 0) {
time_count -= it->time;
if (time_count< 0) {
//found something that is lower than us
_put_in_middle_G1(it, it->time + time_count, BufferData(std::string(line.raw()), 0, fan_speed, true));
//found, stop
break;
}
++it;
}
if (time_count > 0) {
//can't place it in the buffer, use m_current_kickstart
m_current_kickstart.fan_speed = fan_speed;
m_current_kickstart.time = time_count;
m_current_kickstart.raw = line.raw();
}
m_front_buffer_fan_speed = fan_speed;
} else {
// first erase everything lower that that value
_remove_slow_fan(fan_speed, m_buffer_time_size + 1);
// then write the fan command
if (!m_buffer.empty() && (m_buffer_time_size - m_buffer.front().time * 0.1) > nb_seconds_delay) {
_print_in_middle_G1(m_buffer.front(), m_buffer_time_size - nb_seconds_delay, line.raw());
remove_from_buffer(m_buffer.begin());
} else {
m_process_output += line.raw() + "\n";
}
m_front_buffer_fan_speed = fan_speed;
}
} else {
if (kickstart <= 0) {
//nothing to do
//we don't put time = -1; so it will printed in the buffer as other line are done
} else if (m_current_kickstart.time > 0) {
//cherry-pick this one
if (m_back_buffer_fan_speed >= fan_speed) {
//stop kickstart
m_current_kickstart.time = -1;
//this will print me just after as time >=0
} else {
// add some duration to the kickstart and use it for me.
float kickstart_duration = kickstart * float(fan_speed - m_back_buffer_fan_speed) / 100.f;
m_current_kickstart.fan_speed = fan_speed;
m_current_kickstart.time += kickstart_duration;
m_current_kickstart.raw = line.raw();
//i'm printed by the m_current_kickstart
time = -1;
}
} else if(m_back_buffer_fan_speed < fan_speed - 10){ //only kickstart if more than 10% change
//don't write this line, as it will need to be delayed
time = -1;
//get the duration of kickstart
float kickstart_duration = kickstart * float(fan_speed - m_back_buffer_fan_speed) / 100.f;
//if kickstart, write the M106 S[fan_baseline] first
//set the target speed and set the kickstart flag
put_in_buffer(BufferData(_set_fan(100)//m_writer.set_fan(100, true)); //FIXME extruder id (or use the gcode writer, but then you have to disable the multi-thread thing
, 0, fan_speed, true));
//kickstart!
//m_process_output += m_writer.set_fan(100, true);
//add the normal speed line for the future
m_current_kickstart.fan_speed = fan_speed;
m_current_kickstart.time = kickstart_duration;
m_current_kickstart.raw = line.raw();
}
}
}
//update back buffer fan speed
m_back_buffer_fan_speed = fan_speed;
} else {
// have to flush the buffer to avoid erasing a fan command.
need_flush = true;
}
}
break;
}
}
} else {
if(!line.raw().empty() && line.raw().front() == ';')
{
if (line.raw().size() > 10 && line.raw().rfind(";TYPE:", 0) == 0) {
// get the type of the next extrusions
std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6);
current_role = ExtrusionEntity::string_to_role(extrusion_string);
}
if (line.raw().size() > 16) {
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
m_is_custom_gcode = false;
else
m_is_custom_gcode = true;
}
}
}
}
if (time >= 0) {
BufferData& new_data = put_in_buffer(BufferData(line.raw(), time, fan_speed));
if (line.has(Axis::X)) {
new_data.x = reader.x();
new_data.dx = line.dist_X(reader);
}
if (line.has(Axis::Y)) {
new_data.y = reader.y();
new_data.dy = line.dist_Y(reader);
}
if (line.has(Axis::Z)) {
new_data.z = reader.z();
new_data.dz = line.dist_Z(reader);
}
if (line.has(Axis::E)) {
new_data.e = reader.e();
if (relative_e)
new_data.de = line.e();
else
new_data.de = line.dist_E(reader);
}
if (m_current_kickstart.time > 0 && time > 0) {
m_current_kickstart.time -= time;
if (m_current_kickstart.time < 0) {
//prev is possible because we just do a emplace_back.
_put_in_middle_G1(prev(m_buffer.end()), time + m_current_kickstart.time, BufferData{ m_current_kickstart.raw, 0, m_current_kickstart.fan_speed, true });
}
}
}/* else {
BufferData& new_data = put_in_buffer(BufferData("; del? "+line.raw(), 0, fan_speed));
if (line.has(Axis::X)) {
new_data.x = reader.x();
new_data.dx = line.dist_X(reader);
}
if (line.has(Axis::Y)) {
new_data.y = reader.y();
new_data.dy = line.dist_Y(reader);
}
if (line.has(Axis::Z)) {
new_data.z = reader.z();
new_data.dz = line.dist_Z(reader);
}
if (line.has(Axis::E)) {
new_data.e = reader.e();
if (relative_e)
new_data.de = line.e();
else
new_data.de = line.dist_E(reader);
}
}*/
// puts the line back into the gcode
//if buffer too big, flush it.
if (time >= 0) {
while (!m_buffer.empty() && (need_flush || m_buffer_time_size - m_buffer.front().time > nb_seconds_delay - EPSILON) ){
BufferData& frontdata = m_buffer.front();
if (frontdata.fan_speed < 0 || frontdata.fan_speed != m_front_buffer_fan_speed || frontdata.is_kickstart) {
if (frontdata.is_kickstart && frontdata.fan_speed < m_front_buffer_fan_speed) {
//you have to slow down! not kickstart! rewrite the fan speed.
m_process_output += _set_fan(frontdata.fan_speed);//m_writer.set_fan(frontdata.fan_speed,true); //FIXME extruder id (or use the gcode writer, but then you have to disable the multi-thread thing
m_front_buffer_fan_speed = frontdata.fan_speed;
} else {
m_process_output += frontdata.raw + "\n";
if (frontdata.fan_speed >= 0) {
//note that this is the only place where the fan_speed is set and we print from the buffer, as if the fan_speed >= 0 => time == 0
//and as this flush all time == 0 lines from the back of the queue...
m_front_buffer_fan_speed = frontdata.fan_speed;
}
}
}
remove_from_buffer(m_buffer.begin());
}
}
double sum = 0;
for (auto& data : m_buffer) sum += data.time;
assert( std::abs(m_buffer_time_size - sum) < 0.01);
}
} // namespace Slic3r