From 3370e224b2a81f2a7a3755048032c90c8868c894 Mon Sep 17 00:00:00 2001 From: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> Date: Tue, 19 May 2026 09:40:53 +0100 Subject: [PATCH] Cooling: add per-printer non-zero fan PWM floor and fix N=0 fan ramp override (#13715) * Cooling: add per-printer non-zero fan PWM floor and fix N=0 ramp override * updated parameter naming --- src/libslic3r/GCode/CoolingBuffer.cpp | 29 +++++++++++++++------------ src/libslic3r/GCode/FanMover.cpp | 5 ++++- src/libslic3r/GCodeWriter.cpp | 10 +++++++-- src/libslic3r/GCodeWriter.hpp | 4 +++- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintConfig.cpp | 22 ++++++++++++++++++++ src/libslic3r/PrintConfig.hpp | 4 ++++ src/slic3r/GUI/Tab.cpp | 2 ++ 9 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 41e612fdab..8cfb99a46b 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -719,6 +719,9 @@ std::string CoolingBuffer::apply_layer_cooldown( // Second generate the adjusted G-code. std::string new_gcode; new_gcode.reserve(gcode.size() * 2); + // ORCA: per-printer minimum non-zero fan PWM. Applied at every part-cooling fan emission below so any + // non-zero command is raised to at least this percent (0 disables the clamp). + const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); bool overhang_fan_control= false; int overhang_fan_speed = 0; bool internal_bridge_fan_control= false; // ORCA: Add support for separate internal bridge fan speed control @@ -727,7 +730,7 @@ std::string CoolingBuffer::apply_layer_cooldown( int supp_interface_fan_speed = 0; bool ironing_fan_control= false; // ORCA: Add support for ironing fan speed control int ironing_fan_speed = 0; // ORCA: Add support for ironing fan speed control - auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode, + auto change_extruder_set_fan = [ this, layer_id, layer_time, &new_gcode, part_cooling_fan_min_pwm, &overhang_fan_control, &overhang_fan_speed, &internal_bridge_fan_control, &internal_bridge_fan_speed, &supp_interface_fan_control, &supp_interface_fan_speed, @@ -743,11 +746,11 @@ std::string CoolingBuffer::apply_layer_cooldown( int full_fan_speed_layer = EXTRUDER_CONFIG(full_fan_speed_layer); supp_interface_fan_speed = EXTRUDER_CONFIG(support_material_interface_fan_speed); - if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) { - // When ramping up fan speed from close_fan_the_first_x_layers to full_fan_speed_layer, force close_fan_the_first_x_layers above zero, - // so there will be a zero fan speed at least at the 1st layer. - close_fan_the_first_x_layers = 1; - } + // ORCA: previously a silent override forced `close_fan_the_first_x_layers` from 0 up to 1 whenever a ramp + // was configured (`full_fan_speed_layer > 0`), so the first printed layer always had the fan disabled. + // That hid the user's literal "no cooling for the first 0 layers" setting and produced a non-zero starting + // factor on the ramp denominator. The override has been removed: with N=0 and M>0 the ramp now genuinely + // starts on layer 0 at a factor of 1/M and reaches 100% at layer M-1, matching the intent of the option. if (int(layer_id) >= close_fan_the_first_x_layers) { float fan_max_speed = EXTRUDER_CONFIG(fan_max_speed); float slow_down_layer_time = float(EXTRUDER_CONFIG(slow_down_layer_time)); @@ -806,7 +809,7 @@ std::string CoolingBuffer::apply_layer_cooldown( m_fan_speed = fan_speed_new; m_current_fan_speed = fan_speed_new; if (immediately_apply) - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm); } //BBS if (additional_fan_speed_new != m_additional_fan_speed) { @@ -980,26 +983,26 @@ std::string CoolingBuffer::apply_layer_cooldown( if (need_set_fan) { if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed, part_cooling_fan_min_pwm); m_current_fan_speed = overhang_fan_speed; } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed, part_cooling_fan_min_pwm); m_current_fan_speed = internal_bridge_fan_speed; } else if (fan_speed_change_requests[CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed, part_cooling_fan_min_pwm); m_current_fan_speed = supp_interface_fan_speed; } else if (fan_speed_change_requests[CoolingLine::TYPE_IRONING_FAN_START]){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed, part_cooling_fan_min_pwm); m_current_fan_speed = ironing_fan_speed; } else if(fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] && m_current_fan_speed != -1){ - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm); fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; } else - new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed); + new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm); need_set_fan = false; } pos = line_end; diff --git a/src/libslic3r/GCode/FanMover.cpp b/src/libslic3r/GCode/FanMover.cpp index 63e47f3f73..112066bb85 100644 --- a/src/libslic3r/GCode/FanMover.cpp +++ b/src/libslic3r/GCode/FanMover.cpp @@ -230,7 +230,10 @@ void FanMover::_remove_slow_fan(int16_t min_speed, float past_sec) { 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); + // ORCA: apply the per-printer non-zero fan PWM floor so reposted fan commands respect the clamp too. + const int floor_pct = m_writer.config.part_cooling_fan_min_pwm.value; + const unsigned int part_cooling_fan_min_pwm = floor_pct > 0 ? static_cast(floor_pct) : 0u; + return GCodeWriter::set_fan(m_writer.config.gcode_flavor.value, speed, part_cooling_fan_min_pwm); } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 0c0bbcd3b6..3029f1f89c 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1092,9 +1092,13 @@ std::string GCodeWriter::unlift() return gcode; } -std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, unsigned int speed) +std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, unsigned int speed, unsigned int part_cooling_fan_min_pwm) { std::ostringstream gcode; + // ORCA: clamp non-zero fan commands up to the configured PWM floor so fans that can't spool at low duty + // cycles still start reliably. Zero (fan off) is preserved exactly so disable-fan commands are never altered. + if (speed > 0 && part_cooling_fan_min_pwm > 0 && speed < part_cooling_fan_min_pwm) + speed = part_cooling_fan_min_pwm; if (speed == 0) { switch (gcode_flavor) { case gcfTeacup: @@ -1129,7 +1133,9 @@ std::string GCodeWriter::set_fan(const GCodeFlavor gcode_flavor, unsigned int sp std::string GCodeWriter::set_fan(unsigned int speed) const { //BBS - return GCodeWriter::set_fan(this->config.gcode_flavor, speed); + // ORCA: pick up the per-printer PWM floor from the active config. + return GCodeWriter::set_fan(this->config.gcode_flavor, speed, + static_cast(std::max(0, this->config.part_cooling_fan_min_pwm.value))); } //BBS: set additional fan speed for BBS machine only diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index dbc64694e2..3fd7a7668b 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -98,7 +98,9 @@ public: void set_xy_offset(double x, double y) { m_x_offset = x; m_y_offset = y; } Vec2f get_xy_offset() { return Vec2f{m_x_offset, m_y_offset}; }; // To be called by the CoolingBuffer from another thread. - static std::string set_fan(const GCodeFlavor gcode_flavor, unsigned int speed); + // ORCA: `part_cooling_fan_min_pwm` (0-100, default 0) is a floor applied only when `speed` is non-zero, used to overcome + // PWM start-up thresholds on fans that won't spool below a certain duty cycle. A `speed` of 0 is always honoured. + static std::string set_fan(const GCodeFlavor gcode_flavor, unsigned int speed, unsigned int part_cooling_fan_min_pwm = 0); // To be called by the main thread. It always emits the G-code, it does not remember the previous state. // Keeping the state is left to the CoolingBuffer, which runs asynchronously on another thread. std::string set_fan(unsigned int speed) const; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 6780cb6188..68f103fdc2 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1326,7 +1326,7 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor", - "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", + "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", "printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index d5a4296dcc..1853e249b8 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -133,6 +133,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "fan_cooling_layer_time", "full_fan_speed_layer", "fan_kickstart", + "part_cooling_fan_min_pwm", "fan_speedup_overhangs", "fan_speedup_time", "filament_colour", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 0c0549f5c1..393198351b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3724,6 +3724,28 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); + // ORCA: minimum non-zero part cooling fan speed. + def = this->add("part_cooling_fan_min_pwm", coInt); + def->label = L("Minimum non-zero part cooling fan speed"); + def->tooltip = L("Some part-cooling fans cannot start spinning when commanded below a certain PWM duty cycle. " + "When set above 0, any non-zero part-cooling fan command will be raised to at least this percentage " + "so the fan reliably starts. A fan command of 0 (fan off) is always honoured exactly. " + "This clamp is applied after every other fan calculation (first-layer ramp, layer-time interpolation, " + "overhang/bridge/support-interface/ironing overrides), so scaling still operates within the range " + "[this value, 100%]." + "\nIf your firmware already disables the fan below a threshold (for example Klipper's " + "[fan] off_below: 0.10 shuts the fan off whenever the commanded duty cycle is below 10%), " + "this option and the firmware threshold should ideally be set to the same value. Matching them " + "(e.g. off_below: 0.10 in Klipper and 10% here) guarantees the slicer never emits a non-zero " + "value that the firmware would silently drop, and the fan never receives a value below the one " + "you know it can actually spool at." + "\nSet to 0 to deactivate."); + def->sidetext = L("%"); + def->min = 0; + def->max = 100; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(0)); + def = this->add("time_cost", coFloat); def->label = L("Time cost"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a7f29bda07..b84e227c4c 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1309,6 +1309,10 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, fan_kickstart)) ((ConfigOptionBool, fan_speedup_overhangs)) ((ConfigOptionFloat, fan_speedup_time)) + // ORCA: minimum PWM (as a percent 0-100) emitted when the part-cooling fan is asked for a non-zero speed. + // Used to overcome the PWM start-up threshold on fans that cannot spool below a certain duty cycle. + // A value of 0 (the default) leaves behaviour unchanged. A fan command of 0 (off) is always honoured. + ((ConfigOptionInt, part_cooling_fan_min_pwm)) ((ConfigOptionFloats, filament_diameter)) ((ConfigOptionBoolsNullable, filament_adaptive_volumetric_speed)) ((ConfigOptionStrings, volumetric_speed_coefficients)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 44b34e590e..3bfdc39983 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4500,6 +4500,8 @@ void TabPrinter::build_fff() line.append_option(optgroup->get_option("fan_speedup_overhangs")); optgroup->append_line(line); optgroup->append_single_option_line("fan_kickstart", "printer_basic_information_cooling_fan#fan-kick-start-time"); + // ORCA: PWM floor for fans that won't spool at low duty cycles. + optgroup->append_single_option_line("part_cooling_fan_min_pwm", "printer_basic_information_cooling_fan#minimum-non-zero-part-cooling-fan-speed"); optgroup = page->new_optgroup(L("Extruder Clearance"), "param_extruder_clearance"); optgroup->append_single_option_line("extruder_clearance_radius", "printer_basic_information_extruder_clearance#radius");