mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 22:31:02 +00:00
Belt: drive the first-layer fan band from the generator, not from parsed moves
The cooling buffer's band pass rebuilt positions from the layer's G-code and tested them against the first-layer plane. The G-code is in machine coordinates and the plane is in slicing coordinates, so on the shipped profiles the nearest move was over 100 mm from a 0.2 mm band and the pass never changed the fan. GCode::_extrude() already knows each path's height above the belt, so it now tags the band changes and the buffer applies and strips the tags. The pass also took the S of every M106 as the part fan, whatever its P index, and stored that 0..255 value where a percentage was expected (an auxiliary fan line came back as M106 S651); it now uses FanMover's parser, which ignores other fans, and converts to percent. It no longer overwrites the layer's intended speed, only the fan's actual state. Raised in Hanif Koh's review of #14394.
This commit is contained in:
@@ -8507,6 +8507,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// the speed fade tracks perpendicular distance from the plane on
|
||||
// belt printers; otherwise this falls back to the slicing layer id.
|
||||
const int _layer = this->effective_layer_index_for_point(path_point_mm);
|
||||
// Belt printers: tell the cooling buffer which band above the belt this path starts in,
|
||||
// once per layer and at every change (CoolingBuffer::apply_belt_band_fan consumes it).
|
||||
if (m_enable_cooling_markers && m_config.belt_printer.value &&
|
||||
(m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != _layer)) {
|
||||
gcode += ";_BELT_BAND:" + std::to_string(_layer) + "\n";
|
||||
m_belt_band_tag = _layer;
|
||||
m_belt_band_tag_layer = m_layer_index;
|
||||
}
|
||||
if (path_on_first_layer || object_layer_over_raft()) {
|
||||
//BBS: for solid infill of first layer, speed can be higher as long as
|
||||
//wall lines have be attached
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define slic3r_GCode_hpp_
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include <limits>
|
||||
#include "ExPolygon.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "GCode/BeltKinematics.hpp"
|
||||
@@ -855,6 +856,10 @@ protected:
|
||||
~BeltFloorObjectGuard() { slot = nullptr; }
|
||||
};
|
||||
|
||||
// Last ";_BELT_BAND" tag written and the layer it was written on (see _extrude()).
|
||||
int m_belt_band_tag{std::numeric_limits<int>::min()};
|
||||
int m_belt_band_tag_layer{std::numeric_limits<int>::min()};
|
||||
|
||||
std::set<unsigned int> m_initial_layer_extruders;
|
||||
std::vector<std::vector<unsigned int>> m_sorted_layer_filaments;
|
||||
// BBS
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include "../GCode.hpp"
|
||||
#include "../FirstLayerPlane.hpp"
|
||||
#include "CoolingBuffer.hpp"
|
||||
#include "FanMover.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <float.h>
|
||||
#include <string_view>
|
||||
@@ -33,15 +35,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t
|
||||
m_extruder_ids.emplace_back(ex.id());
|
||||
}
|
||||
|
||||
// Borrow the first-layer plane from the GCode generator. When inactive
|
||||
// (non-belt printers and belt printers without Z shear), per-line fan
|
||||
// re-evaluation is skipped and behavior is bit-identical to the legacy
|
||||
// per-layer path.
|
||||
m_first_layer_plane = gcodegen.first_layer_plane();
|
||||
}
|
||||
|
||||
void CoolingBuffer::reset(const Vec3d &position)
|
||||
{
|
||||
m_belt_band_fan = -1;
|
||||
m_belt_band_layer_fan = -1;
|
||||
// BBS: add I and J axis to store center of arc
|
||||
m_current_pos.assign(7, 0.f);
|
||||
m_current_pos[0] = float(position.x());
|
||||
@@ -335,16 +334,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b
|
||||
if (flush) {
|
||||
// This is either an object layer or the very last print layer. Calculate cool down over the collected support layers
|
||||
// and one object layer.
|
||||
const unsigned int extruder_at_start = m_current_extruder;
|
||||
std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos);
|
||||
float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments);
|
||||
out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments);
|
||||
// First-layer plane: per-segment fan re-evaluation post-pass. Walks
|
||||
// the cooled-down gcode and inserts inline M106 commands at band
|
||||
// crossings (where the path's perpendicular distance to the plane
|
||||
// crosses close_fan_the_first_x_layers thresholds). No-op when
|
||||
// the evaluator is inactive.
|
||||
if (m_first_layer_plane && m_first_layer_plane->is_active())
|
||||
out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched);
|
||||
// Belt printers: the fan follows each path's height above the belt (see apply_belt_band_fan).
|
||||
if (out.find(";_BELT_BAND:") != std::string::npos)
|
||||
out = this->apply_belt_band_fan(std::move(out), layer_time_stretched, extruder_at_start);
|
||||
m_gcode.clear();
|
||||
}
|
||||
return out;
|
||||
@@ -1078,7 +1074,7 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
|
||||
// Pure helper: compute the main fan speed for a given effective layer index.
|
||||
// Mirrors the inline logic in change_extruder_set_fan but is callable from
|
||||
// per-line code in apply_first_layer_plane_fan_eval.
|
||||
// per-tag code in apply_belt_band_fan.
|
||||
int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time,
|
||||
unsigned int extruder_id) const
|
||||
{
|
||||
@@ -1116,173 +1112,74 @@ int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_ti
|
||||
return int(fan_speed_new);
|
||||
}
|
||||
|
||||
// Post-pass: walk the cooled-down gcode line by line, track XYZ position,
|
||||
// and insert M106 commands at first-layer-plane band crossings so the fan
|
||||
// follows perpendicular distance to the plane rather than the slicing-layer
|
||||
// index. Only invoked when the FirstLayerPlane evaluator is active.
|
||||
// Belt printers: a layer is a tilted slab, so "the first layer" is not a slicing layer but
|
||||
// whatever lies within a layer height of the belt. GCode::_extrude() knows each path's
|
||||
// height above the belt in the slicing frame and tags every change of it with
|
||||
// ";_BELT_BAND:<effective layer index>". This pass turns the tags into part-fan changes
|
||||
// and strips them. (The G-code itself is in machine coordinates, which is why the band
|
||||
// is not worked out from the moves here.)
|
||||
//
|
||||
// This implementation is intentionally minimal: it overrides only the MAIN
|
||||
// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc
|
||||
// special fans remain at their layer-level values from apply_layer_cooldown.
|
||||
// That keeps the per-line logic small while still giving the user precise
|
||||
// fan control near the belt surface, which is the main quality concern.
|
||||
std::string CoolingBuffer::apply_first_layer_plane_fan_eval(
|
||||
std::string &&gcode_in, size_t /*layer_id*/, float layer_time)
|
||||
// Only the main part-cooling fan is touched; overhang and bridge fans keep the values
|
||||
// apply_layer_cooldown() gave them.
|
||||
std::string CoolingBuffer::apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start)
|
||||
{
|
||||
if (!m_first_layer_plane || !m_first_layer_plane->is_active())
|
||||
return std::move(gcode_in);
|
||||
|
||||
static constexpr std::string_view band_tag = ";_BELT_BAND:";
|
||||
const std::string &gcode = gcode_in;
|
||||
std::string out;
|
||||
out.reserve(gcode.size() + 256);
|
||||
out.reserve(gcode.size());
|
||||
|
||||
// Match the PWM floor applied at every other set_fan call in this file so
|
||||
// band-crossing M106 emissions start the fan reliably at low speeds.
|
||||
// Match the PWM floor applied at every other set_fan call in this file.
|
||||
const unsigned int part_cooling_fan_min_pwm = static_cast<unsigned int>(std::max(0, m_config.part_cooling_fan_min_pwm.value));
|
||||
unsigned int active_extruder = extruder_at_start;
|
||||
|
||||
// Track position in slicing-frame mm. Seed from m_current_pos which the
|
||||
// CoolingBuffer keeps up-to-date across layers.
|
||||
Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]);
|
||||
|
||||
// Track current main fan speed by parsing M106 commands as we walk so
|
||||
// we can restore it after a band exit.
|
||||
int current_main_fan = m_fan_speed;
|
||||
int pre_band_main_fan = current_main_fan;
|
||||
// Implicit initial state: assume the layer started "out of the band"
|
||||
// (i.e., the layer-level fan setting from apply_layer_cooldown is in
|
||||
// effect). The first movement we encounter will reconcile this.
|
||||
bool in_first_layer_band = false;
|
||||
unsigned int active_extruder = m_current_extruder;
|
||||
|
||||
auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) {
|
||||
if (line_sv.size() < 3) return false;
|
||||
if (line_sv[0] != 'G') return false;
|
||||
if (line_sv[1] != '0' && line_sv[1] != '1') return false;
|
||||
if (line_sv[2] != ' ' && line_sv[2] != '\t') return false;
|
||||
const char *c = line_sv.data() + 3;
|
||||
const char *end = line_sv.data() + line_sv.size();
|
||||
bool any = false;
|
||||
while (c < end && *c != ';') {
|
||||
while (c < end && (*c == ' ' || *c == '\t')) ++c;
|
||||
if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break;
|
||||
char axis = *c;
|
||||
++c;
|
||||
if (axis == 'X' || axis == 'Y' || axis == 'Z') {
|
||||
char *next;
|
||||
double v = std::strtod(c, &next);
|
||||
if (next != c) {
|
||||
if (axis == 'X') p.x() = v;
|
||||
else if (axis == 'Y') p.y() = v;
|
||||
else p.z() = v;
|
||||
c = next;
|
||||
any = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Skip unrecognized word.
|
||||
while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n')
|
||||
++c;
|
||||
}
|
||||
return any;
|
||||
};
|
||||
|
||||
auto parse_m106 = [](const std::string_view &line_sv) -> int {
|
||||
// Returns -1 if not an M106, otherwise the S value (0..255).
|
||||
if (line_sv.size() < 4 || line_sv[0] != 'M') return -1;
|
||||
if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6'))
|
||||
return -1;
|
||||
// Find S<value>
|
||||
size_t s_pos = line_sv.find('S');
|
||||
if (s_pos == std::string_view::npos) return -1;
|
||||
const char *c = line_sv.data() + s_pos + 1;
|
||||
char *next;
|
||||
long v = std::strtol(c, &next, 10);
|
||||
if (next == c) return -1;
|
||||
return int(std::clamp<long>(v, 0, 255));
|
||||
};
|
||||
|
||||
auto parse_m107 = [](const std::string_view &line_sv) -> bool {
|
||||
return line_sv.size() >= 4 && line_sv[0] == 'M' &&
|
||||
line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7';
|
||||
};
|
||||
|
||||
auto parse_tool_change = [this](const std::string_view &line_sv) -> int {
|
||||
// Returns the new extruder id, or -1 if not a toolchange.
|
||||
if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1;
|
||||
if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0)
|
||||
return -1;
|
||||
const char *c = line_sv.data() + m_toolchange_prefix.size();
|
||||
char *next;
|
||||
long v = std::strtol(c, &next, 10);
|
||||
if (next == c) return -1;
|
||||
return int(v);
|
||||
};
|
||||
|
||||
const char *p = gcode.c_str();
|
||||
const char *end = gcode.c_str() + gcode.size();
|
||||
const char *p = gcode.data();
|
||||
const char *end = p + gcode.size();
|
||||
while (p < end) {
|
||||
const char *line_end = p;
|
||||
while (line_end < end && *line_end != '\n') ++line_end;
|
||||
const char *next_line = line_end;
|
||||
if (next_line < end) ++next_line; // include the '\n'
|
||||
const char *line_end = static_cast<const char*>(std::memchr(p, '\n', end - p));
|
||||
if (line_end == nullptr)
|
||||
line_end = end;
|
||||
const char *next_line = line_end < end ? line_end + 1 : end;
|
||||
const std::string_view line(p, line_end - p);
|
||||
|
||||
std::string_view line_sv(p, line_end - p);
|
||||
|
||||
// Track tool changes so the per-line fan eval uses the right extruder.
|
||||
int new_tool = parse_tool_change(line_sv);
|
||||
if (new_tool >= 0)
|
||||
active_extruder = unsigned(new_tool);
|
||||
|
||||
// Track existing fan commands so we can restore the right value when
|
||||
// exiting a band.
|
||||
int m106_speed = parse_m106(line_sv);
|
||||
if (m106_speed >= 0) {
|
||||
current_main_fan = m106_speed;
|
||||
if (!in_first_layer_band)
|
||||
pre_band_main_fan = m106_speed;
|
||||
} else if (parse_m107(line_sv)) {
|
||||
current_main_fan = 0;
|
||||
if (!in_first_layer_band)
|
||||
pre_band_main_fan = 0;
|
||||
if (line.size() > band_tag.size() && line.compare(0, band_tag.size(), band_tag) == 0) {
|
||||
const int eff_idx = std::atoi(std::string(line.substr(band_tag.size())).c_str());
|
||||
const bool in_band = eff_idx < std::max(m_config.close_fan_the_first_x_layers.get_at(active_extruder), 1);
|
||||
int target = this->compute_main_fan_speed(eff_idx, layer_time, active_extruder);
|
||||
// Clear of the belt and nothing to add: fall back to what the layer asked for.
|
||||
if (! in_band && target == 0 && m_belt_band_layer_fan >= 0)
|
||||
target = m_belt_band_layer_fan;
|
||||
if (target != m_belt_band_fan) {
|
||||
out += GCodeWriter::set_fan(m_config.gcode_flavor, target, part_cooling_fan_min_pwm);
|
||||
m_belt_band_fan = target;
|
||||
}
|
||||
// The tag itself is dropped.
|
||||
p = next_line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Movement line: parse XYZ, evaluate plane, possibly emit a fan
|
||||
// change BEFORE this line.
|
||||
bool moved = parse_xyz_into(line_sv, cur_pos_mm);
|
||||
if (moved) {
|
||||
const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm);
|
||||
const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder);
|
||||
const bool now_in_band = eff_idx < std::max(close_n, 1);
|
||||
if (now_in_band != in_first_layer_band) {
|
||||
// Band crossing: emit a M106 with the appropriate speed.
|
||||
int target_fan;
|
||||
if (now_in_band) {
|
||||
// Entering the first-layer band: fan off.
|
||||
pre_band_main_fan = current_main_fan;
|
||||
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
|
||||
} else {
|
||||
// Exiting the band: restore the layer's normal fan speed.
|
||||
// Use compute_main_fan_speed with the effective index so
|
||||
// the linear ramp factor (close_fan→full_fan_speed_layer)
|
||||
// also follows distance from the plane.
|
||||
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
|
||||
if (target_fan == 0)
|
||||
target_fan = pre_band_main_fan;
|
||||
}
|
||||
if (target_fan != current_main_fan) {
|
||||
out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm);
|
||||
current_main_fan = target_fan;
|
||||
m_fan_speed = target_fan;
|
||||
m_current_fan_speed = target_fan;
|
||||
}
|
||||
in_first_layer_band = now_in_band;
|
||||
if (! line.empty() && line.front() == 'M') {
|
||||
// Follow the fan commands of the layer-level cooling. FanMover's parser ignores
|
||||
// auxiliary and chamber fans (M106 P2, P3...), which must not be taken for the part fan.
|
||||
const int16_t raw = get_fan_speed(std::string(line), m_config.gcode_flavor);
|
||||
if (raw >= 0) {
|
||||
m_belt_band_fan = int(std::lround(double(std::min<int16_t>(raw, 255)) * 100. / 255.));
|
||||
m_belt_band_layer_fan = m_belt_band_fan;
|
||||
}
|
||||
} else if (line.size() > m_toolchange_prefix.size() && line.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) == 0) {
|
||||
char *num_end = nullptr;
|
||||
const std::string num(line.substr(m_toolchange_prefix.size()));
|
||||
const long tool = std::strtol(num.c_str(), &num_end, 10);
|
||||
if (num_end != num.c_str() && tool >= 0)
|
||||
active_extruder = unsigned(tool);
|
||||
}
|
||||
|
||||
out.append(p, next_line - p);
|
||||
p = next_line;
|
||||
}
|
||||
|
||||
// The fan's real state, so the next layer's cooling re-issues its own speed if it differs.
|
||||
if (m_belt_band_fan >= 0)
|
||||
m_current_fan_speed = m_belt_band_fan;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace Slic3r {
|
||||
|
||||
class GCode;
|
||||
class Layer;
|
||||
class FirstLayerPlane;
|
||||
struct PerExtruderAdjustments;
|
||||
|
||||
// A standalone G-code filter, to control cooling of the print.
|
||||
@@ -37,13 +36,10 @@ private:
|
||||
// Returns the adjusted G-code.
|
||||
std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
|
||||
|
||||
// First-layer plane: per-line fan re-evaluation post-pass. Walks the
|
||||
// post-cooldown gcode, tracks XYZ position, and inserts M106 commands at
|
||||
// band-crossing transitions in slicing-frame coordinates. Only runs
|
||||
// when m_first_layer_plane is active.
|
||||
std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in,
|
||||
size_t layer_id,
|
||||
float layer_time);
|
||||
// Belt printers: turn the ";_BELT_BAND:<n>" tags GCode::_extrude() leaves in the
|
||||
// layer's G-code into part-fan changes, so the fan follows a path's height above the
|
||||
// belt rather than the slicing layer index, and strip the tags.
|
||||
std::string apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start);
|
||||
|
||||
// Pure helper: compute the main fan speed for a given effective layer
|
||||
// index (layer-id units, mapped through the plane evaluator) and the
|
||||
@@ -74,9 +70,10 @@ private:
|
||||
unsigned int m_current_nozzle;
|
||||
//BBS: current fan speed
|
||||
int m_current_fan_speed;
|
||||
// First-layer plane evaluator, borrowed from GCode. Null = inactive
|
||||
// (legacy per-layer fan control).
|
||||
const FirstLayerPlane *m_first_layer_plane = nullptr;
|
||||
// Belt band pass state, kept across layers. The part fan as this pass last saw or set
|
||||
// it (percent, -1 unknown), and the last value the layer-level cooling asked for.
|
||||
int m_belt_band_fan = -1;
|
||||
int m_belt_band_layer_fan = -1;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// The part-cooling fan value a G-code line sets: the raw S (0..255) of an M106 that
|
||||
// addresses the part fan, 0 for a fan-off command, -1 for any other line (auxiliary
|
||||
// and chamber fans included).
|
||||
int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor);
|
||||
|
||||
class FanMover
|
||||
{
|
||||
private:
|
||||
|
||||
@@ -596,3 +596,58 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P
|
||||
CHECK_FALSE(print.validate().string.empty());
|
||||
}
|
||||
}
|
||||
|
||||
// On a belt every tilted layer starts on the belt, so "first layer" cooling is a band along
|
||||
// the belt, not the first slicing layers: the part fan goes off for the paths that start
|
||||
// within a layer height of the belt and back on above it, on every layer. The G-code is in
|
||||
// machine coordinates, so the generator tags the band changes and the cooling buffer
|
||||
// applies them; before that the buffer compared machine-frame moves with a slicing-frame
|
||||
// plane and never switched the fan at all.
|
||||
TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print][belt][Cooling]")
|
||||
{
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "belt_printer", 1 },
|
||||
{ "belt_slice_rotation", "x" },
|
||||
{ "belt_slice_rotation_angle", 45 },
|
||||
{ "belt_slice_rotation_global", 1 },
|
||||
{ "gcode_remap_x", "rev_x" },
|
||||
{ "gcode_remap_y", "pos_z" },
|
||||
{ "gcode_remap_z", "pos_y" },
|
||||
{ "layer_height", 0.2 },
|
||||
{ "initial_layer_print_height", 0.2 },
|
||||
{ "skirt_loops", 0 },
|
||||
{ "z_hop", 0 },
|
||||
{ "close_fan_the_first_x_layers", 1 },
|
||||
{ "full_fan_speed_layer", 0 },
|
||||
{ "fan_min_speed", 100 },
|
||||
{ "fan_max_speed", 100 },
|
||||
{ "slow_down_layer_time", 1000 },
|
||||
{ "fan_cooling_layer_time", 1001 },
|
||||
{ "reduce_fan_stop_start_freq", 0 },
|
||||
{ "machine_start_gcode", "T[initial_tool]\n" },
|
||||
{ "layer_change_gcode", "G92 E0\n" },
|
||||
});
|
||||
const std::string gcode = slice({ cube(20) }, config);
|
||||
REQUIRE(! gcode.empty());
|
||||
|
||||
// The tags are consumed by the cooling buffer and never reach the file.
|
||||
CHECK(gcode.find(";_BELT_BAND") == std::string::npos);
|
||||
|
||||
size_t fan_off = 0, fan_on = 0;
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) {
|
||||
if (line.cmd_is("M107"))
|
||||
++ fan_off;
|
||||
else if (line.cmd_is("M106")) {
|
||||
float s = 0.f;
|
||||
if (line.has_value('S', s) && s <= 0.f)
|
||||
++ fan_off;
|
||||
else
|
||||
++ fan_on;
|
||||
}
|
||||
});
|
||||
// A flat-bed print turns the fan on once. Here it cycles with the layers.
|
||||
CHECK(fan_off > 10);
|
||||
CHECK(fan_on > 10);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user