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())