Formlabs Developers
Download PreForm Insider to start developing with the PreForm API.

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 setting Lab arches v3, and one is on the shipped Fast Arch setting.
  • lab_arches_v3.fps and lab_arches_v4.fps are 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:

Python
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:

Python
PREFORM_URL = os.environ.get("FL_LOCAL_BASE_URL", "http://localhost:44388")

If PreForm doesn't answer, the script stops with a hint:

Python
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:

Python
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:

Python
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))
                continue

Each build volume is one row of the report. A skipped one shows the status skipped, other setting:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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.

  1. In a terminal, preview the change from the folder you unzipped:

    Terminal
    python roll_out_print_setting.py lab_arches_v4.fps queue \
        --only-from "Lab arches v3" --dry-run --print-time
    Terminal
    uv run roll_out_print_setting.py lab_arches_v4.fps queue \
        --only-from "Lab arches v3" --dry-run --print-time
  2. Roll out the .fps file:

    Terminal
    python roll_out_print_setting.py lab_arches_v4.fps queue \
        --only-from "Lab arches v3" --out queue-v4
    Terminal
    uv run roll_out_print_setting.py lab_arches_v4.fps queue \
        --only-from "Lab arches v3" --out queue-v4
  3. Roll out a shipped setting:

    Terminal
    python roll_out_print_setting.py --shipped FORM-4-0 FLFMGR01 DEFAULT 0.2 queue \
        --only-from "Lab arches v3" --out queue-v4
    Terminal
    uv 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)

What's next#

Feedback & Bug Reports