Print setting rollout
When a new version of a print setting is approved, every job already prepared on the old version has to move to the new one. In PreForm, you would open each job, change the print setting in Job Setup, and save it again. This script does that for a whole folder of .form files. It changes only the jobs on the setting you name, saves the changed jobs to a new folder, and reports the layer count and print time of each job before and after the change.
View on GitHub Download print-setting-rollout.zip
A dry run on the sample jobs prints:
rolling out: Lab arches v4 (FORM-4-0, FLFMGR01, F4_FAST_ARCHES at 0.2 mm)
dry run, nothing saved
file current setting layers print time status
case-1041.form Lab arches v3 150 -> 120 0h 15m -> 0h 12m changed
case-1042.form Lab arches v3 156 -> 125 0h 16m -> 0h 13m changed
case-1043.form Fast Arch 156 skipped, other setting
2 build volume(s) changed in 3 file(s)The example contains the script, roll_out_print_setting.py, and its sample input:
queue/holds three supported jobs. Two are on the custom settingLab arches v3, and one is on the shippedFast Archsetting.lab_arches_v3.fpsandlab_arches_v4.fpsare two versions of a custom setting. Both are copies of the Fast Arch setting for Fast Model V1 on Form 4 with a changed squish wait, and v4 also prints at 0.2 mm layers.
Choose the new setting#
You can give the new setting as an .fps file, or as the four values of a shipped setting with --shipped:
paths = args.paths
if args.shipped:
machine, material, setting, thickness = args.shipped
target = dict(
machine_type=machine,
material_code=material,
print_setting=setting,
layer_thickness_mm=float(thickness),
)
elif paths[0].suffix.lower() == ".fps":
target = {"fps_file": str(paths[0].resolve())}
paths = paths[1:]
else:
sys.exit("Give an .fps file or --shipped MACHINE MATERIAL SETTING THICKNESS_MM")Both end up as keyword arguments for the scene calls, so the rest of the script treats them the same way.
Connect to PreForm#
Make sure PreForm is ready to accept connections.
The script reads the PreForm address from FL_LOCAL_BASE_URL, and uses http://localhost:44388 if it isn't set:
PREFORM_URL = os.environ.get("FL_LOCAL_BASE_URL", "http://localhost:44388")If PreForm doesn't answer, the script stops with a hint:
with FormlabsClient() as client:
try:
preform = client.preform.connect(base_url=PREFORM_URL)
except PreFormApiError as exc:
sys.exit(
f"{exc}\nNo PreForm at {PREFORM_URL}; "
"open PreForm and start its API server"
)Check the new setting#
Before it opens any job, check_target makes sure the new setting works. It checks a shipped setting against PreForm's list, then creates an empty scene on the new setting and reads its name:
def check_target(preform: PreFormApi, target: dict) -> str:
"""Create an empty scene on the target setting, so a bad setting stops the run."""
if "fps_file" not in target and not preform.validate_print_setting(**target).valid:
sys.exit(f"No shipped print setting matches {target}")
scene = preform.create_scene(**target)
try:
return describe(preform, scene.scene_id)
finally:
preform.delete_scene(scene_id=scene.scene_id)If the .fps file is missing or can't be read, the script stops here with INPUT_ERROR. The name on the first line of the report comes from the .fps file.
Skip jobs on other settings#
roll_out opens each .form file. Each build volume in the file opens as its own scene, and the script reads the name of its print setting from the job metadata. With --only-from, it skips the build volumes on any other setting:
def roll_out(
preform: PreFormApi,
form: Path,
target: dict,
only_from: str | None,
out: Path | None,
with_time: bool,
) -> list[Row]:
"""Change each build volume in a .form file to the target, then save the file."""
opened = preform.open_form_file(file=str(form.resolve()))
rows: list[Row] = []
try:
for scene in opened.scenes:
label = form.name
if len(opened.scenes) > 1:
label = f"{form.name} / {scene.scene_name}"
before = setting_name(preform, scene.scene_id)
layers_before = preform.get_scene(scene_id=scene.scene_id).layer_count
if only_from and before != only_from:
rows.append(Row(label, before, str(layers_before), status=SKIPPED))
continueEach build volume is one row of the report. A skipped one shows the status skipped, other setting:
PREFORM_URL = os.environ.get("FL_LOCAL_BASE_URL", "http://localhost:44388")
SKIPPED = "skipped, other setting"
@dataclass
class Row:
label: str
setting: str
layers: str
print_time: str = ""
status: str = ""The script matches the setting by the name PreForm shows. For a custom setting, that's the name stored in its .fps file, so give each version of a custom setting its own name.
Change the setting#
update_scene takes the same arguments as create_scene. After the update, the script reads the scene again for its layer count and models:
time_before = print_time(preform, scene.scene_id) if with_time else ""
# Models, supports, and layout stay. On another printer's setting, the
# models move into its build volume, and any that don't fit are out of
# bounds.
preform.update_scene(scene_id=scene.scene_id, **target)
after = preform.get_scene(scene_id=scene.scene_id)
outside = [model.name for model in after.models if not model.in_bounds]
rows.append(
Row(
label,
before,
f"{layers_before} -> {after.layer_count}",
f"{time_before} -> {print_time(preform, scene.scene_id)}" if with_time else "",
f"{len(outside)} model(s) outside the build volume" if outside else "changed",
)
)The layer count changes when the new setting has another layer thickness, as lab_arches_v4.fps does. Models, supports, and layout stay as they are. If a model ends up outside the build volume, the report says so, and the job is still saved.
Compare print times#
With --print-time, the script calculates the print time before and after the change:
def print_time(preform: PreFormApi, scene_id: UUID) -> str:
seconds = preform.calculate_print_time(scene_id=scene_id).total_print_time_s
return f"{seconds // 3600}h {seconds % 3600 // 60:02d}m"The calculation slices each build, which adds time to the run. Leave out --print-time to skip it.
Save the job#
The script saves the changed job to the --out folder, under its original file name, and the report shows where:
if out and any(row.status != SKIPPED for row in rows):
out.mkdir(parents=True, exist_ok=True)
saved = out / form.name
preform.save_form_file(
file=str(saved.resolve()),
scene_ids=[scene.scene_id for scene in opened.scenes],
project_name=opened.project_name or form.stem,
)
for row in rows:
if row.status != SKIPPED:
row.status = f"saved to {saved}" if row.status == "changed" else f"{row.status}, saved to {saved}"The original files are never changed. With --dry-run, nothing is saved.
Delete the scenes#
The finally block deletes every scene the file opened, including when a call fails:
finally:
for scene in opened.scenes:
preform.delete_scene(scene_id=scene.scene_id)Run the example#
With pip, install the Formlabs SDK first; with uv, uv run installs it from the example's pyproject.toml.
In a terminal, preview the change from the folder you unzipped:
Terminalpython roll_out_print_setting.py lab_arches_v4.fps queue \ --only-from "Lab arches v3" --dry-run --print-timeTerminaluv run roll_out_print_setting.py lab_arches_v4.fps queue \ --only-from "Lab arches v3" --dry-run --print-timeRoll out the
.fpsfile:Terminalpython roll_out_print_setting.py lab_arches_v4.fps queue \ --only-from "Lab arches v3" --out queue-v4Terminaluv run roll_out_print_setting.py lab_arches_v4.fps queue \ --only-from "Lab arches v3" --out queue-v4Roll out a shipped setting:
Terminalpython roll_out_print_setting.py --shipped FORM-4-0 FLFMGR01 DEFAULT 0.2 queue \ --only-from "Lab arches v3" --out queue-v4Terminaluv run roll_out_print_setting.py --shipped FORM-4-0 FLFMGR01 DEFAULT 0.2 queue \ --only-from "Lab arches v3" --out queue-v4
The script reads the .fps file and every .form file in queue/, and writes the changed jobs to queue-v4/. To roll out another setting, pass its .fps file or its four --shipped values, and pass the name of the setting it replaces to --only-from.
The preview prints the output at the top of this page. The .fps rollout saves both jobs:
rolling out: Lab arches v4 (FORM-4-0, FLFMGR01, F4_FAST_ARCHES at 0.2 mm)
file current setting layers status
case-1041.form Lab arches v3 150 -> 120 saved to queue-v4/case-1041.form
case-1042.form Lab arches v3 156 -> 125 saved to queue-v4/case-1042.form
case-1043.form Fast Arch 156 skipped, other setting
2 build volume(s) changed in 3 file(s)The shipped setting rollout saves both jobs on the shipped setting:
rolling out: Default (FORM-4-0, FLFMGR01, DEFAULT at 0.2 mm)
file current setting layers status
case-1041.form Lab arches v3 150 -> 120 saved to queue-v4/case-1041.form
case-1042.form Lab arches v3 156 -> 125 saved to queue-v4/case-1042.form
case-1043.form Fast Arch 156 skipped, other setting
2 build volume(s) changed in 3 file(s)