Cold Pull G-code (for us lazy bastards)

Here’s a Cold Pull G-code I tried out today. I ran it many times, it didn’t fail a single time. This version doesn’t retract the blob back to the AMS, I ran it with the tubing disconnected from the toolhead.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Cold pull G-code for Bambu, the Raz, v2024-02-16
;
; Some observations from the X1C:
; 35 mm from top of toolhead to the extruder gears.
; 50 mm from cutter to nozzle tip.
; 1 mm of filament is 2.41 mm³.
; One push of extrude button is 5 mm (12 mm³).
; 11 pushes (55 mm, 132 mm³) from empty hotend to
; extruding from nozzle.  You get about 85 mm of
; thinned filament up/out with the cold pull.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Original in Prusa forum with following credits:
; 3dviking.wordpress.com
; use at your own risk.
; modified by C. Rajek
;
; ESUN Cleaning filament: Heat to 255, cool to 95.
; ABS: Heat to 255, cool to 170.
; PLA: Heat to 255, cool to 90.
; PA:  Heat to 240, cool to 140.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

M1002 gcode_claim_action : 14 ; "Cleaning nozzle tip"

G28 X Y        ; Home XY
G91            ; Relative positioning
G0 Z10         ; Lower bed a little
G90            ; Absolute positioning
M83            ; ...but extruder relative
G0 X128 Y128   ; Position toolhead in the center
M302 S70 P1    ; Allow cold extrusion

M1002 gcode_claim_action : 7 ; "Heating hotend"
M109 S255      ; Heat/wait until 255°C reached
M1002 gcode_claim_action : 14 ; "Cleaning nozzle tip"

G1 E50 F100    ; Extrude 50 mm to fill the nozzle
M106 P1 S255   ; 100% part fan to cool faster
M106 P2 S255   ; 100% aux fan as well
M109 S190      ; Cool/wait to 190°C
G1 E10 F100    ; Extrude another 10 mm in case some oozed out
M109 S70       ; Cool/wait to 70°C
M106 P1 S0     ; Turn off part fan
M106 P2 S0     ; Turn off aux fan

G1 E-200 F1800 ; Retract 20 mm at max. speed (user should pull too)

M104 S0        ; Turn hotend off

M1002 gcode_claim_action : 0 ; Clear message

M84            ; disable steppers - done!

Save this to a file, eg. “Cold_Pull.gcode” and put it in the root directory of the SD card. This way it will always be available for “printing” from the touch screen (I keep a copy of hotbed_tramming.gcode there as well).

It’s easy enough to do this manually but given this seems reliable, I’ll be using it. I assume this would work on P1/A1 series as well but I only tested on X1C.

Edits:
Decreased retraction speed to F1800 (30 mm/s) which is the spec. max. speed for X1C.
Added XY homing for good measure.
Lowered cold temperature to 70°C for better strength of pull.

OK please someone tell me if I’m senile or if Bambu did something idiotic. I’m now running 1.9.0.1 (which isn’t a beta, right? It doesn’t say so) and this gcode no longer works. And worse, even a manual cold pull doesn’t work because the printer ridiculously requires 170°C in order to run the extruder. Doing it at that temp, I obviously don’t get all the filament out.

Tried it from printer and from the app. From Bambu Studio, it admittedly “can” be done but for EACH press for retracting I have to confirm that I know what I’m doing, which slows down the process and doesn’t always work.

So Nozzle/Hotend Unclogging Procedure for X1/P1 | Bambu Lab Wiki says 100°C is a good temperature but the stupid printer doesn’t let me use it.

Do I need to downgrade? At the very least, the printer and the app should also have that “do it anyway” button - AND after pressing it once, you shouldn’t need to press it again in that session. EDIT: That would still not let me use this gcode, which has NEVER FAILED ME ONCE.

I contacted support on this, apparently it slipped into one of the firmware upgrades. They said it will be fixed in a future release.

Meanwhile my H2D came with a similar (and better assisted) cold pull right from the menu on the printer. I tried it and it worked like a champ. I suppose this will come to the older models as well, as there’s no technical reason it wouldn’t… although I can imagine business reasons :roll_eyes:

Both nozzles were see-through on first try (btw they were hi-flo, which makes it harder). It’s so quick and easy I’ll always do a cold pull before changing nozzles, from now on.

Thanks! I belong to the lazy crowd. Reading gcode, overwriting numbers and using sdcards exceeded my threshold so I had Claude AI write me a wrapper to parameterize everything and name the commands according to what they did. I’m not sure if I have it right, but it seems to be working. In case anyone wants it… Again thanks!

#!/usr/bin/env python3
“”“Generate a Bambu P1S cold-pull G-code program and push it to printers over FTPS.”“”

import argparse
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass, field
from pathlib import Path

============================================================

Profile – edit these

============================================================

@dataclass
class ColdPullProfile:
label: str = “3D-Fuel 3D Clean cleaning filament”

purge_temp: int = 255          # melt/fill the nozzle
intermediate_temp: int = 190   # first cool step
pull_temp: int = 90            # 3D-Fuel cleaning ~85-110, ABS ~170, PA ~140
soak_seconds: int = 0          # extra dwell at purge_temp (0 = none)

fill_mm: int = 60              # ~55mm fills an empty hotend; 60 leaves margin
ooze_mm: int = 10              # top-up after the intermediate cool
break_mm: int = 20             # gentle motor retract; finish the pull by hand

fill_feed: int = 100           # mm/min for the fill/ooze extrudes
break_feed: int = 300          # mm/min (~5 mm/s) for the break-free retract

beep: bool = True              # M300 chime at the pause (no-op on some firmware)
use_chamber_fan: bool = True   # run the chamber fan to speed cooling

center_x: int = 128
center_y: int = 128

cold_extrusion_below: int = 70 # allow extrusion below this temp

annotate: bool = True          # emit a name beside each command in the output

@dataclass
class Printer:
name: str
ip: str
access_code: str

PRINTERS = [

Printer(name=“P1S-2”, ip=“192.168.1.■■■”, access_code=“xxxxxxxx”),

]

REMOTE_FILENAME = “cold-pull.gcode”
LOCAL_COPY = “cold-pull.gcode”

============================================================

G-code builder

============================================================

class GCodeProgram:
“”“Fluent builder. Each method names the operation; the G-code is the output.”“”

def __init__(self, annotate: bool = True):
    self._ops: list[tuple[str, str]] = []
    self._annotate = annotate

def _emit(self, command: str, name: str = "") -> "GCodeProgram":
    self._ops.append((command, name))
    return self

# --- on-screen status (hides the M1002 action codes) ---
def show_heating_status(self):        return self._emit("M1002 gcode_claim_action : 7", "show heating status")
def show_cleaning_status(self):       return self._emit("M1002 gcode_claim_action : 14", "show cleaning-nozzle status")
def clear_status(self):               return self._emit("M1002 gcode_claim_action : 0", "clear status")

# --- motion ---
def home_xy(self):                    return self._emit("G28 X Y", "home X and Y")
def use_relative_positioning(self):   return self._emit("G91", "relative positioning")
def use_absolute_positioning(self):   return self._emit("G90", "absolute positioning")
def use_relative_extrusion(self):     return self._emit("M83", "relative extrusion")
def lower_bed(self, mm: int):         return self._emit(f"G0 Z{mm}", f"lower bed {mm}mm")
def park_center(self, x: int, y: int): return self._emit(f"G0 X{x} Y{y}", "park toolhead at center")

# --- temperature ---
def allow_cold_extrusion(self, below_c: int): return self._emit(f"M302 S{below_c} P1", f"allow extrusion below {below_c}C")
def heat_and_wait(self, c: int):      return self._emit(f"M109 S{c}", f"heat and wait for {c}C")
def cool_and_wait(self, c: int):      return self._emit(f"M109 S{c}", f"cool and wait for {c}C")
def turn_off_hotend(self):            return self._emit("M104 S0", "hotend off")

# --- extrusion ---
def extrude(self, mm: int, feed: int):   return self._emit(f"G1 E{mm} F{feed}", f"extrude {mm}mm")
def retract(self, mm: int, feed: int):   return self._emit(f"G1 E-{abs(mm)} F{feed}", f"retract {mm}mm to break the plug free")

# --- fans (P1 part, P2 aux, P3 chamber) ---
def part_fan_full(self):     return self._emit("M106 P1 S255", "part fan 100%")
def aux_fan_full(self):      return self._emit("M106 P2 S255", "aux fan 100%")
def chamber_fan_full(self):  return self._emit("M106 P3 S255", "chamber fan 100%")
def part_fan_off(self):      return self._emit("M106 P1 S0", "part fan off")
def aux_fan_off(self):       return self._emit("M106 P2 S0", "aux fan off")
def chamber_fan_off(self):   return self._emit("M106 P3 S0", "chamber fan off")

# --- misc ---
def soak(self, seconds: int):         return self._emit(f"G4 S{seconds}", f"soak {seconds}s")
def beep(self, hz: int = 880, ms: int = 300): return self._emit(f"M300 S{hz} P{ms}", "beep")
def wait_for_resume(self):            return self._emit("M400 U1", "HOLD -- wait for Resume")
def disable_steppers(self):           return self._emit("M84", "disable steppers")

def render(self) -> str:
    out = []
    for command, name in self._ops:
        if self._annotate and name:
            out.append(f"{command:<28} ; {name}")
        else:
            out.append(command)
    return "\n".join(out) + "\n"

def build_cold_pull(p: ColdPullProfile) → str:
g = GCodeProgram(annotate=p.annotate)

(g.show_cleaning_status()
  .home_xy()
  .use_relative_positioning()
  .lower_bed(10)
  .use_absolute_positioning()
  .use_relative_extrusion()
  .park_center(p.center_x, p.center_y)
  .allow_cold_extrusion(p.cold_extrusion_below)
  .show_heating_status()
  .heat_and_wait(p.purge_temp))

if p.soak_seconds:
    g.soak(p.soak_seconds)

(g.show_cleaning_status()
  .extrude(p.fill_mm, p.fill_feed)
  .part_fan_full()
  .aux_fan_full())

if p.use_chamber_fan:
    g.chamber_fan_full()

(g.cool_and_wait(p.intermediate_temp)
  .extrude(p.ooze_mm, p.fill_feed)
  .cool_and_wait(p.pull_temp)
  .part_fan_off()
  .aux_fan_off())

if p.use_chamber_fan:
    g.chamber_fan_off()

if p.beep:
    g.beep()

(g.wait_for_resume()
  .retract(p.break_mm, p.break_feed)
  .turn_off_hotend()
  .clear_status()
  .disable_steppers())

return g.render()

============================================================

Transport (shells out to curl, matching the working command)

============================================================

def is_configured(printer: Printer) → bool:
fields = (printer.ip, printer.access_code)
if not all(fields):
return False
return not any(c in value.lower() for value in fields for c in “x?”)

def push(local_path: Path, printer: Printer) → bool:
url = f"ftps://{printer.ip}:990/{REMOTE_FILENAME}"
cmd = [“curl”, “–ftp-pasv”, “–insecure”, “-T”, str(local_path), url,
“–user”, f"bblp:{printer.access_code}“]
print(f” → {printer.name} ({printer.ip})")
return subprocess.run(cmd).returncode == 0

def main() → int:
parser = argparse.ArgumentParser(description=“Generate and push a cold-pull G-code.”)
parser.add_argument(“–dry-run”, action=“store_true”, help=“print the G-code, do not push”)
args = parser.parse_args()

profile = ColdPullProfile()
gcode = build_cold_pull(profile)

if args.dry_run:
    print(gcode)
    return 0

if shutil.which("curl") is None:
    print("ERROR: curl not found on PATH.", file=sys.stderr)
    return 1

here = Path(__file__).resolve().parent
(here / LOCAL_COPY).write_text(gcode)
print(f"Wrote {here / LOCAL_COPY}")

with tempfile.NamedTemporaryFile("w", suffix=".gcode", delete=False) as tmp:
    tmp.write(gcode)
    tmp_path = Path(tmp.name)

for skipped in (p for p in PRINTERS if not is_configured(p)):
    print(f"Skipping {skipped.name}: missing/placeholder ip or access code.")

configured = [p for p in PRINTERS if is_configured(p)]
if not configured:
    print("No fully configured printers. Edit the PRINTERS list.", file=sys.stderr)
    return 1

print(f"Pushing to {len(configured)} printer(s):")
failures = sum(not push(tmp_path, p) for p in configured)
tmp_path.unlink(missing_ok=True)

if failures:
    print(f"Done with {failures} failure(s).", file=sys.stderr)
    return 1
print("All uploads succeeded.")
return 0

if name == “main”:
sys.exit(main())