Spaces:
Sleeping
Editable pressure variable in G-code files, with a fallback checkbox
Browse filesPressure-owning files now start with a hand-editable variable line
({preset}{aux_command}globals().update(pressureN = X)) that every
pressure command references — the preset is setpress(pressureN) and each
ramp step is round(min(pressureN + k, 100), 1) — so a print's pressure
can be changed by editing one number in the file, no regeneration
needed. The line's shape is dictated by the TCode host: {aux_command}
keeps it off the motion timeline (routing is by substring), presets are
exec'd as one list literal so only expressions survive, and the name is
per-port because all files share one exec namespace.
An "Editable Pressure Variable" checkbox (on by default) falls back to
the plain numeric format in case a host cannot run the variable form;
it participates in the staleness banner and settings export/import.
Verified against a simulation of TCode.py's routing/exec pipeline,
including hand-edit rebasing and multi-port independence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README.md +3 -2
- app.py +26 -1
- tests/test_nozzle_spacing.py +6 -4
- tests/test_vector_gcode.py +118 -9
- vector_gcode.py +97 -15
|
@@ -123,9 +123,10 @@ The **Multi-Nozzle Split** accordion on the **Shapes & G-Code** tab can split on
|
|
| 123 |
- Generated G-code starts in relative coordinate mode (`G91`).
|
| 124 |
- The app opens the valve where the tool path is inside a shape's layer polygons and closes it elsewhere.
|
| 125 |
- Generated files include pressure preset commands and WAGO valve commands based on the selected pressure, valve, and port; the nozzle number controls layout/spacing assignment.
|
| 126 |
-
- Pressure commands are written **once per port**: when shapes share a serial port, only the first shape's file carries the preset, toggle, and per-layer ramp — the print host compiles every file onto one timeline, and duplicated toggles would flip the regulator on/off/on at start. Valve commands stay per shape in every file.
|
|
|
|
| 127 |
- Pressure increases by `0.1` psi per layer by default.
|
| 128 |
-
- Pressures are bounded to the regulator's range: table values clamp to 0–100 psi and snap to the tenths place (the serial protocol encodes `pressure × 10`), the
|
| 129 |
- Every movement line is emitted as `G1` at one constant speed (no `G0` rapid travel); the WAGO valve commands mark where material is dispensed vs where the head just travels.
|
| 130 |
- **Lead In**: enabled per shape via the **Lead In** column in Shape Settings; the Lead In Options accordion sets the patch geometry. Prints a purge patch before layer 1: **Lead In Position** (Left/Right/Up/Down) picks which side of the shape it sits on, **Lead In Clearance** how far away, and **Lead In Line Direction** which way the purge strokes run — Auto points them at the shape (the historical behavior), or force Horizontal/Vertical (e.g. a patch below the shape with horizontal strokes). The purge always finishes at the patch corner nearest the toolpath start, whatever the line count's parity: strokes running across the approach print the farthest line first and step back toward the shape, and the zigzag starts from whichever end makes the last stroke land on the start corner — so the freshly primed nozzle travels only the short clearance lane to the print, never back across the wet purge lines. For grid-split pieces the clearance is automatically extended by the assembly's remaining extent along the purge axis (reported in the G-code status), so under shared reference motion every nozzle's purge patch lands clear of the whole assembled part instead of on a neighbor's print area. The **Lead In** column in Shape Settings controls dispensing per shape: an opted-out head still travels the shared patch (keeping parallel heads in sync) but keeps its valve shut, and skips the lead-in moves entirely when printing without shared motion.
|
| 131 |
- **Combined reference outline for motion** (always on): every shape's *motion* is taken from the combined reference layer union while each shape's *valve/dispensing* comes from its own layer polygons — so parallel print heads share one synchronized nozzle path and each deposits only its own geometry. The reference union is rebuilt automatically whenever shapes are sliced or G-code is generated. Contour tracing stays synchronized too: every shape traces every traced shape's contour, opening its valve only on its own outline.
|
|
|
|
| 123 |
- Generated G-code starts in relative coordinate mode (`G91`).
|
| 124 |
- The app opens the valve where the tool path is inside a shape's layer polygons and closes it elsewhere.
|
| 125 |
- Generated files include pressure preset commands and WAGO valve commands based on the selected pressure, valve, and port; the nozzle number controls layout/spacing assignment.
|
| 126 |
+
- Pressure commands are written **once per port**: when shapes share a serial port, only the first shape's file carries the pressure variable, preset, toggle, and per-layer ramp — the print host compiles every file onto one timeline, and duplicated toggles would flip the regulator on/off/on at start. Valve commands stay per shape in every file.
|
| 127 |
+
- **Editable pressure variable** (checkbox, on by default): each pressure-owning file starts with `{preset}{aux_command}globals().update(pressure3 = 25)` (the value from the Shape Settings table, the variable named after the file's serial port), and every pressure command references that variable — the preset is `setpress(pressure3)` and each ramp step is `setpress(round(min(pressure3 + 0.1, 100), 1))`. To print the same file at a different pressure, edit the one number on the first line; the preset and the entire ramp follow, no regeneration needed. The line's exact shape is dictated by the TCode print host: `{aux_command}` keeps it off the motion timeline (only lines containing `{aux_command}`/`serialPort` are routed as commands), and the host execs all `{preset}` commands as one list literal, so the line must be a pure expression — `globals().update(...)` is the expression form of the assignment. Naming the variable per port keeps multi-port prints independent, since the host runs every file in one shared namespace. Unchecking **Editable Pressure Variable** writes plain numeric pressure commands instead (the pre-variable format, e.g. `setpress(25)` with ramp values baked in), as a fallback in case a host cannot run the variable form.
|
| 128 |
- Pressure increases by `0.1` psi per layer by default.
|
| 129 |
+
- Pressures are bounded to the regulator's range: table values clamp to 0–100 psi and snap to the tenths place (the serial protocol encodes `pressure × 10`), the ramp expressions clamp at 100 psi even for hand-edited variable values, and the generation status warns when a ramp would hit the ceiling before the last layer.
|
| 130 |
- Every movement line is emitted as `G1` at one constant speed (no `G0` rapid travel); the WAGO valve commands mark where material is dispensed vs where the head just travels.
|
| 131 |
- **Lead In**: enabled per shape via the **Lead In** column in Shape Settings; the Lead In Options accordion sets the patch geometry. Prints a purge patch before layer 1: **Lead In Position** (Left/Right/Up/Down) picks which side of the shape it sits on, **Lead In Clearance** how far away, and **Lead In Line Direction** which way the purge strokes run — Auto points them at the shape (the historical behavior), or force Horizontal/Vertical (e.g. a patch below the shape with horizontal strokes). The purge always finishes at the patch corner nearest the toolpath start, whatever the line count's parity: strokes running across the approach print the farthest line first and step back toward the shape, and the zigzag starts from whichever end makes the last stroke land on the start corner — so the freshly primed nozzle travels only the short clearance lane to the print, never back across the wet purge lines. For grid-split pieces the clearance is automatically extended by the assembly's remaining extent along the purge axis (reported in the G-code status), so under shared reference motion every nozzle's purge patch lands clear of the whole assembled part instead of on a neighbor's print area. The **Lead In** column in Shape Settings controls dispensing per shape: an opted-out head still travels the shared patch (keeping parallel heads in sync) but keeps its valve shut, and skips the lead-in moves entirely when printing without shared motion.
|
| 132 |
- **Combined reference outline for motion** (always on): every shape's *motion* is taken from the combined reference layer union while each shape's *valve/dispensing* comes from its own layer polygons — so parallel print heads share one synchronized nozzle path and each deposits only its own geometry. The reference union is rebuilt automatically whenever shapes are sliced or G-code is generated. Contour tracing stays synchronized too: every shape traces every traced shape's contour, opening its valve only on its own outline.
|
|
@@ -4806,6 +4806,7 @@ def _gcode_settings_snapshot(
|
|
| 4806 |
sweep_buffer: float = 0.8,
|
| 4807 |
lead_in_orientation: str | None = None,
|
| 4808 |
contour_order: str | None = None,
|
|
|
|
| 4809 |
) -> dict:
|
| 4810 |
"""Fingerprint of every setting that shapes this record's G-code.
|
| 4811 |
|
|
@@ -4839,6 +4840,7 @@ def _gcode_settings_snapshot(
|
|
| 4839 |
"scale_mode": _normalize_scale_mode(scale_mode),
|
| 4840 |
"sweep_buffer": round(_coerce_float(sweep_buffer, 0.8), 6),
|
| 4841 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
|
|
|
| 4842 |
}
|
| 4843 |
|
| 4844 |
|
|
@@ -4863,6 +4865,7 @@ def check_gcode_staleness(
|
|
| 4863 |
sweep_buffer: float = 0.8,
|
| 4864 |
lead_in_orientation: str | None = None,
|
| 4865 |
contour_order: str | None = None,
|
|
|
|
| 4866 |
) -> str:
|
| 4867 |
"""Warning banner text when generated G-code no longer matches the settings."""
|
| 4868 |
records = _apply_shape_settings(records or [], settings_table)
|
|
@@ -4881,6 +4884,7 @@ def check_gcode_staleness(
|
|
| 4881 |
sweep_buffer,
|
| 4882 |
lead_in_orientation,
|
| 4883 |
contour_order,
|
|
|
|
| 4884 |
)
|
| 4885 |
for record in records:
|
| 4886 |
if not record.get("gcode_path"):
|
|
@@ -4927,6 +4931,7 @@ def export_project_settings(
|
|
| 4927 |
lead_in_orientation: str | None,
|
| 4928 |
nozzle_speed: Any,
|
| 4929 |
contour_order: str | None = None,
|
|
|
|
| 4930 |
split_columns: Any = None,
|
| 4931 |
split_rows: Any = None,
|
| 4932 |
split_overlapping_layers: Any = None,
|
|
@@ -4970,6 +4975,7 @@ def export_project_settings(
|
|
| 4970 |
"lead_in_orientation": str(lead_in_orientation or LEAD_IN_LINE_AUTO),
|
| 4971 |
"nozzle_speed": _coerce_float(nozzle_speed, 10.0),
|
| 4972 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
|
|
|
| 4973 |
"split_columns": max(1, _coerce_int(split_columns, 2)),
|
| 4974 |
"split_rows": max(1, _coerce_int(split_rows, 1)),
|
| 4975 |
"split_overlapping_layers": bool(split_overlapping_layers),
|
|
@@ -5001,7 +5007,7 @@ def import_project_settings(
|
|
| 5001 |
"""
|
| 5002 |
|
| 5003 |
def _skip_options() -> tuple:
|
| 5004 |
-
return tuple(gr.skip() for _ in range(
|
| 5005 |
|
| 5006 |
paths = _uploaded_file_paths(settings_upload)
|
| 5007 |
if not paths:
|
|
@@ -5064,6 +5070,7 @@ def import_project_settings(
|
|
| 5064 |
option("scale_mode"),
|
| 5065 |
option("nozzle_speed"),
|
| 5066 |
option("contour_order"),
|
|
|
|
| 5067 |
option("split_columns"),
|
| 5068 |
option("split_rows"),
|
| 5069 |
option("split_overlapping_layers"),
|
|
@@ -5111,6 +5118,7 @@ def generate_dynamic_gcode(
|
|
| 5111 |
sweep_buffer: float = 0.8,
|
| 5112 |
lead_in_orientation: str | None = None,
|
| 5113 |
contour_order: str | None = None,
|
|
|
|
| 5114 |
progress: gr.Progress = gr.Progress(),
|
| 5115 |
) -> tuple:
|
| 5116 |
records = _apply_shape_settings(records or [], settings_table)
|
|
@@ -5217,6 +5225,7 @@ def generate_dynamic_gcode(
|
|
| 5217 |
infill=_coerce_float(record.get("infill", 100.0), 100.0) / 100.0,
|
| 5218 |
motion_infill_fractions=motion_infill_fractions,
|
| 5219 |
emit_pressure_commands=owns_port_pressure,
|
|
|
|
| 5220 |
# Same buffer for every shape: the shared motion must match.
|
| 5221 |
sweep_buffer=max(0.0, _coerce_float(sweep_buffer, 0.8)),
|
| 5222 |
lead_in_enabled=bool(lead_in_enabled),
|
|
@@ -5250,6 +5259,7 @@ def generate_dynamic_gcode(
|
|
| 5250 |
sweep_buffer,
|
| 5251 |
lead_in_orientation,
|
| 5252 |
contour_order,
|
|
|
|
| 5253 |
)
|
| 5254 |
messages.append(f"Shape {record['idx']}: wrote `{gcode_path.name}`.")
|
| 5255 |
except Exception as exc:
|
|
@@ -5799,6 +5809,16 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 5799 |
"""
|
| 5800 |
)
|
| 5801 |
gcode_pressure_ramp_enabled = gr.Checkbox(label="Increase Pressure Each Layer", value=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5802 |
with gr.Row(elem_id="gcode-raster-row"):
|
| 5803 |
gcode_raster_pattern = gr.Dropdown(
|
| 5804 |
label="Raster Pattern",
|
|
@@ -6234,6 +6254,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6234 |
gcode_lead_in_orientation,
|
| 6235 |
viz_nozzle_speed,
|
| 6236 |
gcode_contour_order,
|
|
|
|
| 6237 |
split_columns,
|
| 6238 |
split_rows,
|
| 6239 |
split_overlapping_layers,
|
|
@@ -6264,6 +6285,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6264 |
scale_mode,
|
| 6265 |
viz_nozzle_speed,
|
| 6266 |
gcode_contour_order,
|
|
|
|
| 6267 |
split_columns,
|
| 6268 |
split_rows,
|
| 6269 |
split_overlapping_layers,
|
|
@@ -6291,6 +6313,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6291 |
gcode_sweep_buffer,
|
| 6292 |
gcode_lead_in_orientation,
|
| 6293 |
gcode_contour_order,
|
|
|
|
| 6294 |
]
|
| 6295 |
shape_settings.change(
|
| 6296 |
fn=check_gcode_staleness,
|
|
@@ -6311,6 +6334,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6311 |
gcode_sweep_buffer,
|
| 6312 |
gcode_lead_in_orientation,
|
| 6313 |
gcode_contour_order,
|
|
|
|
| 6314 |
):
|
| 6315 |
stale_control.change(
|
| 6316 |
fn=check_gcode_staleness,
|
|
@@ -6400,6 +6424,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6400 |
gcode_sweep_buffer,
|
| 6401 |
gcode_lead_in_orientation,
|
| 6402 |
gcode_contour_order,
|
|
|
|
| 6403 |
],
|
| 6404 |
outputs=[shape_records, ref_layers, gcode_downloads, gcode_status, gcode_text_source, gcode_source, gcode_download_all],
|
| 6405 |
).then(
|
|
|
|
| 4806 |
sweep_buffer: float = 0.8,
|
| 4807 |
lead_in_orientation: str | None = None,
|
| 4808 |
contour_order: str | None = None,
|
| 4809 |
+
pressure_variable: bool = True,
|
| 4810 |
) -> dict:
|
| 4811 |
"""Fingerprint of every setting that shapes this record's G-code.
|
| 4812 |
|
|
|
|
| 4840 |
"scale_mode": _normalize_scale_mode(scale_mode),
|
| 4841 |
"sweep_buffer": round(_coerce_float(sweep_buffer, 0.8), 6),
|
| 4842 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
| 4843 |
+
"pressure_variable": bool(pressure_variable),
|
| 4844 |
}
|
| 4845 |
|
| 4846 |
|
|
|
|
| 4865 |
sweep_buffer: float = 0.8,
|
| 4866 |
lead_in_orientation: str | None = None,
|
| 4867 |
contour_order: str | None = None,
|
| 4868 |
+
pressure_variable: bool = True,
|
| 4869 |
) -> str:
|
| 4870 |
"""Warning banner text when generated G-code no longer matches the settings."""
|
| 4871 |
records = _apply_shape_settings(records or [], settings_table)
|
|
|
|
| 4884 |
sweep_buffer,
|
| 4885 |
lead_in_orientation,
|
| 4886 |
contour_order,
|
| 4887 |
+
pressure_variable,
|
| 4888 |
)
|
| 4889 |
for record in records:
|
| 4890 |
if not record.get("gcode_path"):
|
|
|
|
| 4931 |
lead_in_orientation: str | None,
|
| 4932 |
nozzle_speed: Any,
|
| 4933 |
contour_order: str | None = None,
|
| 4934 |
+
pressure_variable: bool = True,
|
| 4935 |
split_columns: Any = None,
|
| 4936 |
split_rows: Any = None,
|
| 4937 |
split_overlapping_layers: Any = None,
|
|
|
|
| 4975 |
"lead_in_orientation": str(lead_in_orientation or LEAD_IN_LINE_AUTO),
|
| 4976 |
"nozzle_speed": _coerce_float(nozzle_speed, 10.0),
|
| 4977 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
| 4978 |
+
"pressure_variable": bool(pressure_variable),
|
| 4979 |
"split_columns": max(1, _coerce_int(split_columns, 2)),
|
| 4980 |
"split_rows": max(1, _coerce_int(split_rows, 1)),
|
| 4981 |
"split_overlapping_layers": bool(split_overlapping_layers),
|
|
|
|
| 5007 |
"""
|
| 5008 |
|
| 5009 |
def _skip_options() -> tuple:
|
| 5010 |
+
return tuple(gr.skip() for _ in range(20))
|
| 5011 |
|
| 5012 |
paths = _uploaded_file_paths(settings_upload)
|
| 5013 |
if not paths:
|
|
|
|
| 5070 |
option("scale_mode"),
|
| 5071 |
option("nozzle_speed"),
|
| 5072 |
option("contour_order"),
|
| 5073 |
+
option("pressure_variable"),
|
| 5074 |
option("split_columns"),
|
| 5075 |
option("split_rows"),
|
| 5076 |
option("split_overlapping_layers"),
|
|
|
|
| 5118 |
sweep_buffer: float = 0.8,
|
| 5119 |
lead_in_orientation: str | None = None,
|
| 5120 |
contour_order: str | None = None,
|
| 5121 |
+
pressure_variable: bool = True,
|
| 5122 |
progress: gr.Progress = gr.Progress(),
|
| 5123 |
) -> tuple:
|
| 5124 |
records = _apply_shape_settings(records or [], settings_table)
|
|
|
|
| 5225 |
infill=_coerce_float(record.get("infill", 100.0), 100.0) / 100.0,
|
| 5226 |
motion_infill_fractions=motion_infill_fractions,
|
| 5227 |
emit_pressure_commands=owns_port_pressure,
|
| 5228 |
+
pressure_variable=bool(pressure_variable),
|
| 5229 |
# Same buffer for every shape: the shared motion must match.
|
| 5230 |
sweep_buffer=max(0.0, _coerce_float(sweep_buffer, 0.8)),
|
| 5231 |
lead_in_enabled=bool(lead_in_enabled),
|
|
|
|
| 5259 |
sweep_buffer,
|
| 5260 |
lead_in_orientation,
|
| 5261 |
contour_order,
|
| 5262 |
+
pressure_variable,
|
| 5263 |
)
|
| 5264 |
messages.append(f"Shape {record['idx']}: wrote `{gcode_path.name}`.")
|
| 5265 |
except Exception as exc:
|
|
|
|
| 5809 |
"""
|
| 5810 |
)
|
| 5811 |
gcode_pressure_ramp_enabled = gr.Checkbox(label="Increase Pressure Each Layer", value=True)
|
| 5812 |
+
gcode_pressure_variable = gr.Checkbox(
|
| 5813 |
+
label="Editable Pressure Variable",
|
| 5814 |
+
value=True,
|
| 5815 |
+
info=(
|
| 5816 |
+
"Start each pressure-owning file with a pressureN variable that every "
|
| 5817 |
+
"pressure command references, so the print pressure can be changed by "
|
| 5818 |
+
"editing one number in the file — no regeneration needed. Uncheck to "
|
| 5819 |
+
"write plain numeric pressure commands instead."
|
| 5820 |
+
),
|
| 5821 |
+
)
|
| 5822 |
with gr.Row(elem_id="gcode-raster-row"):
|
| 5823 |
gcode_raster_pattern = gr.Dropdown(
|
| 5824 |
label="Raster Pattern",
|
|
|
|
| 6254 |
gcode_lead_in_orientation,
|
| 6255 |
viz_nozzle_speed,
|
| 6256 |
gcode_contour_order,
|
| 6257 |
+
gcode_pressure_variable,
|
| 6258 |
split_columns,
|
| 6259 |
split_rows,
|
| 6260 |
split_overlapping_layers,
|
|
|
|
| 6285 |
scale_mode,
|
| 6286 |
viz_nozzle_speed,
|
| 6287 |
gcode_contour_order,
|
| 6288 |
+
gcode_pressure_variable,
|
| 6289 |
split_columns,
|
| 6290 |
split_rows,
|
| 6291 |
split_overlapping_layers,
|
|
|
|
| 6313 |
gcode_sweep_buffer,
|
| 6314 |
gcode_lead_in_orientation,
|
| 6315 |
gcode_contour_order,
|
| 6316 |
+
gcode_pressure_variable,
|
| 6317 |
]
|
| 6318 |
shape_settings.change(
|
| 6319 |
fn=check_gcode_staleness,
|
|
|
|
| 6334 |
gcode_sweep_buffer,
|
| 6335 |
gcode_lead_in_orientation,
|
| 6336 |
gcode_contour_order,
|
| 6337 |
+
gcode_pressure_variable,
|
| 6338 |
):
|
| 6339 |
stale_control.change(
|
| 6340 |
fn=check_gcode_staleness,
|
|
|
|
| 6424 |
gcode_sweep_buffer,
|
| 6425 |
gcode_lead_in_orientation,
|
| 6426 |
gcode_contour_order,
|
| 6427 |
+
gcode_pressure_variable,
|
| 6428 |
],
|
| 6429 |
outputs=[shape_records, ref_layers, gcode_downloads, gcode_status, gcode_text_source, gcode_source, gcode_download_all],
|
| 6430 |
).then(
|
|
@@ -2011,6 +2011,7 @@ def test_project_settings_export_import_round_trip(tmp_path) -> None:
|
|
| 2011 |
settings_path, status = export_project_settings(
|
| 2012 |
records, None, 0.4, 0.4, None, "Circle Spiral raster", False, 1.2,
|
| 2013 |
6.0, 7.0, 4, "Down", "Horizontal", 15.0,
|
|
|
|
| 2014 |
split_columns=3, split_rows=2, split_overlapping_rows=True,
|
| 2015 |
split_overlap=1.5,
|
| 2016 |
)
|
|
@@ -2033,10 +2034,11 @@ def test_project_settings_export_import_round_trip(tmp_path) -> None:
|
|
| 2033 |
assert option_updates[2]["value"] == 1.2
|
| 2034 |
assert option_updates[8]["value"] == 0.4 # layer height
|
| 2035 |
assert option_updates[11]["value"] == 15.0 # nozzle speed
|
| 2036 |
-
assert option_updates[13]["value"]
|
| 2037 |
-
assert option_updates[14]["value"] ==
|
| 2038 |
-
assert option_updates[
|
| 2039 |
-
assert option_updates[
|
|
|
|
| 2040 |
|
| 2041 |
# A file listed in the export but not loaded is reported.
|
| 2042 |
partial = import_project_settings([settings_path], _records_from_files(["egg_inside.stl"], None), None)
|
|
|
|
| 2011 |
settings_path, status = export_project_settings(
|
| 2012 |
records, None, 0.4, 0.4, None, "Circle Spiral raster", False, 1.2,
|
| 2013 |
6.0, 7.0, 4, "Down", "Horizontal", 15.0,
|
| 2014 |
+
pressure_variable=False,
|
| 2015 |
split_columns=3, split_rows=2, split_overlapping_rows=True,
|
| 2016 |
split_overlap=1.5,
|
| 2017 |
)
|
|
|
|
| 2034 |
assert option_updates[2]["value"] == 1.2
|
| 2035 |
assert option_updates[8]["value"] == 0.4 # layer height
|
| 2036 |
assert option_updates[11]["value"] == 15.0 # nozzle speed
|
| 2037 |
+
assert option_updates[13]["value"] is False # pressure variable off
|
| 2038 |
+
assert option_updates[14]["value"] == 3 # split columns
|
| 2039 |
+
assert option_updates[15]["value"] == 2 # split rows
|
| 2040 |
+
assert option_updates[17]["value"] is True # overlapping rows
|
| 2041 |
+
assert option_updates[19]["value"] == 1.5 # overlap mm
|
| 2042 |
|
| 2043 |
# A file listed in the export but not loaded is reported.
|
| 2044 |
partial = import_project_settings([settings_path], _records_from_files(["egg_inside.stl"], None), None)
|
|
@@ -209,16 +209,23 @@ def test_gcode_header_writes_presets_before_initial_aux_commands(tmp_path) -> No
|
|
| 209 |
if line.strip()
|
| 210 |
]
|
| 211 |
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
# No metadata comments: the file starts straight with machine commands
|
| 214 |
# (the toolpath's world anchor is returned via origin_sink instead).
|
| 215 |
assert not any("PathOrigin" in line for line in lines)
|
| 216 |
-
assert lines[
|
| 217 |
# {preset} marks pressure setup for the Aerotech host runtime.
|
| 218 |
-
assert lines[
|
| 219 |
-
assert lines[
|
| 220 |
# The header ends there: no dummy valve-100 init, no duplicate close.
|
| 221 |
-
assert lines[
|
| 222 |
assert not any("WAGO_ValveCommands(100" in line for line in lines)
|
| 223 |
|
| 224 |
|
|
@@ -304,6 +311,36 @@ def test_sweep_buffer_is_adjustable_and_can_be_disabled(tmp_path) -> None:
|
|
| 304 |
assert none[0]["start"] == (0.0, 0.0, 0.0)
|
| 305 |
|
| 306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
def test_pressure_ramp_clamps_at_the_regulator_limit(tmp_path) -> None:
|
| 308 |
import re
|
| 309 |
|
|
@@ -319,11 +356,18 @@ def test_pressure_ramp_clamps_at_the_regulator_limit(tmp_path) -> None:
|
|
| 319 |
pressure_ramp_enabled=True,
|
| 320 |
output_dir=tmp_path,
|
| 321 |
)
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
| 325 |
# Preset, then the ramp climbing 0.1/layer but never past 100 psi.
|
| 326 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
|
| 329 |
def test_port_sharing_files_emit_pressure_commands_once(tmp_path) -> None:
|
|
@@ -356,10 +400,75 @@ def test_port_sharing_files_emit_pressure_commands_once(tmp_path) -> None:
|
|
| 356 |
assert "serialPort3" not in follower
|
| 357 |
assert _pressure_set_count(follower) == 0
|
| 358 |
assert "togglepress" not in follower
|
|
|
|
|
|
|
|
|
|
| 359 |
# Valve control is per shape and unaffected.
|
| 360 |
assert follower.count("WAGO_ValveCommands(7, 1)") == owner.count("WAGO_ValveCommands(7, 1)")
|
| 361 |
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
def test_gcode_lead_in_runs_once_before_first_layer(tmp_path) -> None:
|
| 364 |
gcode_path = generate_vector_gcode(
|
| 365 |
_stack(box(0.0, 0.0, 0.5, 0.5), box(0.0, 0.0, 0.5, 0.5)),
|
|
|
|
| 209 |
if line.strip()
|
| 210 |
]
|
| 211 |
|
| 212 |
+
# The hand-editable pressure variable leads the file; every pressure
|
| 213 |
+
# command below references it, so editing this one number retunes the
|
| 214 |
+
# whole print without regenerating. The odd shape is host-dictated:
|
| 215 |
+
# {aux_command} routes the line off the motion timeline, and the host
|
| 216 |
+
# execs presets as one list literal, so the line must be an expression
|
| 217 |
+
# (a bare `pressure3 = 25` assignment would be a syntax error there).
|
| 218 |
+
assert lines[0] == "{preset}{aux_command}globals().update(pressure3 = 25)"
|
| 219 |
+
assert lines[1] == "G91"
|
| 220 |
# No metadata comments: the file starts straight with machine commands
|
| 221 |
# (the toolpath's world anchor is returned via origin_sink instead).
|
| 222 |
assert not any("PathOrigin" in line for line in lines)
|
| 223 |
+
assert lines[2] == "{aux_command}WAGO_ValveCommands(7, 0)"
|
| 224 |
# {preset} marks pressure setup for the Aerotech host runtime.
|
| 225 |
+
assert lines[3] == "{preset}serialPort3.write(eval(setpress(pressure3)))"
|
| 226 |
+
assert lines[4] == "{preset}serialPort3.write(eval(togglepress()))"
|
| 227 |
# The header ends there: no dummy valve-100 init, no duplicate close.
|
| 228 |
+
assert lines[5].startswith("G")
|
| 229 |
assert not any("WAGO_ValveCommands(100" in line for line in lines)
|
| 230 |
|
| 231 |
|
|
|
|
| 311 |
assert none[0]["start"] == (0.0, 0.0, 0.0)
|
| 312 |
|
| 313 |
|
| 314 |
+
def _pressure_values(gcode_text: str, override: float | None = None) -> list[float]:
|
| 315 |
+
"""Evaluate every setpress(...) expression the way the print host does.
|
| 316 |
+
|
| 317 |
+
The TCode host routes lines containing {aux_command}/serialPort as
|
| 318 |
+
commands, strips the markers, execs the {preset} commands as ONE list
|
| 319 |
+
literal in its module namespace, and execs ramp commands individually
|
| 320 |
+
in that same namespace. This mimics exactly that: the file's own
|
| 321 |
+
variable line defines `pressureN`, then each setpress argument is
|
| 322 |
+
evaluated in the shared namespace. `override` simulates the user
|
| 323 |
+
hand-editing the variable's value before printing.
|
| 324 |
+
"""
|
| 325 |
+
namespace: dict = {}
|
| 326 |
+
values = []
|
| 327 |
+
for line in gcode_text.splitlines():
|
| 328 |
+
line = line.strip()
|
| 329 |
+
if "{aux_command}" not in line and "serialPort" not in line:
|
| 330 |
+
continue # the host would parse this as a motion line
|
| 331 |
+
command = line.replace("{aux_command}", "").replace("{preset}", "")
|
| 332 |
+
if "globals().update(" in command:
|
| 333 |
+
expr = command.split("globals().update(", 1)[1].rsplit(")", 1)[0]
|
| 334 |
+
name, value = (part.strip() for part in expr.split("="))
|
| 335 |
+
namespace[name] = float(value) if override is None else override
|
| 336 |
+
elif "setpress(" in command:
|
| 337 |
+
# The command ends `...setpress(EXPR)))` — closing setpress,
|
| 338 |
+
# eval, and write; the expression itself may contain parens.
|
| 339 |
+
expr = command.split("setpress(", 1)[1].rsplit(")))", 1)[0]
|
| 340 |
+
values.append(float(eval(expr, namespace)))
|
| 341 |
+
return values
|
| 342 |
+
|
| 343 |
+
|
| 344 |
def test_pressure_ramp_clamps_at_the_regulator_limit(tmp_path) -> None:
|
| 345 |
import re
|
| 346 |
|
|
|
|
| 356 |
pressure_ramp_enabled=True,
|
| 357 |
output_dir=tmp_path,
|
| 358 |
)
|
| 359 |
+
text = gcode_path.read_text()
|
| 360 |
+
|
| 361 |
+
# The file ships with the configured pressure as its variable default.
|
| 362 |
+
default = re.search(r"globals\(\)\.update\(pressure3 = ([\d.]+)\)", text)
|
| 363 |
+
assert default is not None and float(default.group(1)) == 99.8
|
| 364 |
+
|
| 365 |
# Preset, then the ramp climbing 0.1/layer but never past 100 psi.
|
| 366 |
+
assert _pressure_values(text) == [99.8, 99.9, 100.0, 100.0]
|
| 367 |
+
|
| 368 |
+
# Hand-editing the variable rebases the ENTIRE ramp — the point of the
|
| 369 |
+
# feature: no regeneration needed to print the same file at 50 psi.
|
| 370 |
+
assert _pressure_values(text, override=50.0) == [50.0, 50.1, 50.2, 50.3]
|
| 371 |
|
| 372 |
|
| 373 |
def test_port_sharing_files_emit_pressure_commands_once(tmp_path) -> None:
|
|
|
|
| 400 |
assert "serialPort3" not in follower
|
| 401 |
assert _pressure_set_count(follower) == 0
|
| 402 |
assert "togglepress" not in follower
|
| 403 |
+
# The pressure variable lives only in the port-owning file too.
|
| 404 |
+
assert "globals().update(pressure3 = 25)" in owner
|
| 405 |
+
assert "globals().update" not in follower
|
| 406 |
# Valve control is per shape and unaffected.
|
| 407 |
assert follower.count("WAGO_ValveCommands(7, 1)") == owner.count("WAGO_ValveCommands(7, 1)")
|
| 408 |
|
| 409 |
|
| 410 |
+
def test_pressure_variable_is_namespaced_per_port(tmp_path) -> None:
|
| 411 |
+
# The print host execs every file's commands in ONE shared namespace:
|
| 412 |
+
# if two port-owning files used the same variable name, each file's
|
| 413 |
+
# ramp would read whichever file defined it LAST. Naming the variable
|
| 414 |
+
# by port keeps each regulator on its own knob.
|
| 415 |
+
layer = box(0.0, 0.0, 2.0, 2.0)
|
| 416 |
+
|
| 417 |
+
def _generate(port: int) -> str:
|
| 418 |
+
path = generate_vector_gcode(
|
| 419 |
+
_stack(layer, layer),
|
| 420 |
+
shape_name=f"port{port}",
|
| 421 |
+
pressure=25,
|
| 422 |
+
valve=7,
|
| 423 |
+
port=port,
|
| 424 |
+
fil_width=1.0,
|
| 425 |
+
layer_height=1.0,
|
| 426 |
+
pressure_ramp_enabled=True,
|
| 427 |
+
output_dir=tmp_path / f"port{port}",
|
| 428 |
+
)
|
| 429 |
+
return path.read_text()
|
| 430 |
+
|
| 431 |
+
on_port2 = _generate(2)
|
| 432 |
+
on_port3 = _generate(3)
|
| 433 |
+
|
| 434 |
+
assert "globals().update(pressure2 = 25)" in on_port2
|
| 435 |
+
assert "pressure3" not in on_port2
|
| 436 |
+
assert "globals().update(pressure3 = 25)" in on_port3
|
| 437 |
+
assert "pressure2" not in on_port3
|
| 438 |
+
# Every setpress in each file references its own port's variable.
|
| 439 |
+
assert "setpress(pressure2)" in on_port2
|
| 440 |
+
assert "pressure2 + 0.1" in on_port2
|
| 441 |
+
assert "setpress(pressure3)" in on_port3
|
| 442 |
+
assert "pressure3 + 0.1" in on_port3
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def test_pressure_variable_can_be_disabled(tmp_path) -> None:
|
| 446 |
+
# The "Editable Pressure Variable" checkbox off: files fall back to the
|
| 447 |
+
# plain numeric format (pre-variable), in case the host chokes on the
|
| 448 |
+
# variable form.
|
| 449 |
+
layer = box(0.0, 0.0, 2.0, 2.0)
|
| 450 |
+
gcode_path = generate_vector_gcode(
|
| 451 |
+
_stack(layer, layer, layer),
|
| 452 |
+
shape_name="no_variable",
|
| 453 |
+
pressure=25,
|
| 454 |
+
valve=7,
|
| 455 |
+
port=3,
|
| 456 |
+
fil_width=1.0,
|
| 457 |
+
layer_height=1.0,
|
| 458 |
+
pressure_ramp_enabled=True,
|
| 459 |
+
pressure_variable=False,
|
| 460 |
+
output_dir=tmp_path,
|
| 461 |
+
)
|
| 462 |
+
text = gcode_path.read_text()
|
| 463 |
+
|
| 464 |
+
assert "globals().update" not in text
|
| 465 |
+
assert "pressure3" not in text
|
| 466 |
+
assert text.startswith("G91\n")
|
| 467 |
+
assert "{preset}serialPort3.write(eval(setpress(25)))" in text
|
| 468 |
+
# Ramp values are baked in, clamped and rounded at generation time.
|
| 469 |
+
assert _pressure_values(text) == [25.0, 25.1, 25.2]
|
| 470 |
+
|
| 471 |
+
|
| 472 |
def test_gcode_lead_in_runs_once_before_first_layer(tmp_path) -> None:
|
| 473 |
gcode_path = generate_vector_gcode(
|
| 474 |
_stack(box(0.0, 0.0, 0.5, 0.5), box(0.0, 0.0, 0.5, 0.5)),
|
|
@@ -53,19 +53,73 @@ __all__ = [
|
|
| 53 |
]
|
| 54 |
|
| 55 |
|
| 56 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
"""Pressure preset in the readable host dialect.
|
| 58 |
|
| 59 |
Every pressure command uses the same `eval(setpress(...))` form (the
|
| 60 |
-
print host defines `setpress`, which builds the serial byte string)
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
"""
|
| 66 |
if start:
|
| 67 |
-
return f"\n\r{{preset}}{port}.write(eval(setpress({
|
| 68 |
-
return f"\n\r{port}.write(eval(setpress({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def _toggle_cmd(port: str, start: bool) -> str:
|
|
@@ -102,29 +156,43 @@ def write_gcode_file(
|
|
| 102 |
pressure_ramp_enabled: bool,
|
| 103 |
all_g1: bool,
|
| 104 |
emit_pressure_commands: bool = True,
|
|
|
|
| 105 |
) -> None:
|
| 106 |
"""Write the move list as a G-code file.
|
| 107 |
|
| 108 |
-
`emit_pressure_commands` gates EVERY pressure command (
|
| 109 |
-
per-layer ramp, closing toggle):
|
|
|
|
| 110 |
device, so when several shapes share a serial port only ONE of their
|
| 111 |
files may own it — the print host compiles all files onto one timeline,
|
| 112 |
and duplicated toggles would flip the regulator on/off/on at start.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
"""
|
| 114 |
off_color = 0
|
| 115 |
com_port = f"serialPort{port}"
|
| 116 |
color_dict: dict[int, int] = {0: 100, 255: valve}
|
| 117 |
|
| 118 |
# The regulator tops out at MAX_PRESSURE_PSI and resolves tenths: clamp
|
| 119 |
-
# the
|
|
|
|
| 120 |
pressure = round(min(max(float(pressure), 0.0), MAX_PRESSURE_PSI), 1)
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
| 122 |
pressure_on_lines = [_toggle_cmd(com_port, start=True)]
|
| 123 |
pressure_off_lines = [_toggle_cmd(com_port, start=False)]
|
| 124 |
|
|
|
|
| 125 |
pressure_cur = float(pressure)
|
| 126 |
|
| 127 |
with open(gcode_path, "w") as f:
|
|
|
|
|
|
|
| 128 |
f.write("G91\n")
|
| 129 |
f.write(_valve_cmd(valve, 0))
|
| 130 |
if emit_pressure_commands:
|
|
@@ -156,10 +224,22 @@ def write_gcode_file(
|
|
| 156 |
f"Z{_coord(move['Z'])} ; Color {move['Color']}"
|
| 157 |
)
|
| 158 |
if pressure_ramp_enabled and emit_pressure_commands:
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
)
|
| 162 |
-
pressure_next = _setpress_cmd(com_port, pressure_cur, start=False)
|
| 163 |
else:
|
| 164 |
pressure_next = None
|
| 165 |
else:
|
|
@@ -197,6 +277,7 @@ def generate_vector_gcode(
|
|
| 197 |
infill: float = 1.0,
|
| 198 |
motion_infill_fractions: list[float] | None = None,
|
| 199 |
emit_pressure_commands: bool = True,
|
|
|
|
| 200 |
sweep_buffer: float | None = None,
|
| 201 |
increase_pressure_per_layer: float = 0.1,
|
| 202 |
pressure_ramp_enabled: bool = True,
|
|
@@ -385,5 +466,6 @@ def generate_vector_gcode(
|
|
| 385 |
pressure_ramp_enabled=bool(pressure_ramp_enabled),
|
| 386 |
all_g1=bool(all_g1),
|
| 387 |
emit_pressure_commands=bool(emit_pressure_commands),
|
|
|
|
| 388 |
)
|
| 389 |
return gcode_path
|
|
|
|
| 53 |
]
|
| 54 |
|
| 55 |
|
| 56 |
+
def _pressure_variable_name(port: int) -> str:
|
| 57 |
+
"""The hand-editable pressure variable, one per serial port.
|
| 58 |
+
|
| 59 |
+
The name carries the port number because the print host execs every
|
| 60 |
+
file's commands in ONE shared namespace: with several port-owning files
|
| 61 |
+
in a print, a single shared name would leave every ramp reading the
|
| 62 |
+
LAST file's base pressure. Pressure is a port property, so `pressureN`
|
| 63 |
+
is also the natural granularity.
|
| 64 |
+
"""
|
| 65 |
+
return f"pressure{port}"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _pressure_variable_cmd(port: int, pressure: float) -> str:
|
| 69 |
+
"""The one hand-editable knob: `pressureN = X` on the file's first line.
|
| 70 |
+
|
| 71 |
+
The exact form is dictated by how the TCode print host consumes files:
|
| 72 |
+
|
| 73 |
+
- Only lines containing `{aux_command}` or `serialPort` are routed as
|
| 74 |
+
commands; anything else is parsed as a G-code move. The `{aux_command}`
|
| 75 |
+
marker (stripped by the host) is what keeps this line off the motion
|
| 76 |
+
timeline.
|
| 77 |
+
- The host joins all `{preset}` commands into ONE list literal and
|
| 78 |
+
exec()s it at the controller's START signal, so the line must be a
|
| 79 |
+
pure EXPRESSION — a bare `pressure = X` assignment would be a syntax
|
| 80 |
+
error there. `globals().update(pressureN = X)` is an expression that
|
| 81 |
+
still reads like the assignment, and it lands the name in the host's
|
| 82 |
+
module globals where the setpress preset (same exec, evaluated next)
|
| 83 |
+
and the mid-file ramp commands (exec'd individually) can see it.
|
| 84 |
+
|
| 85 |
+
Editing this single number retunes every pressure command in the file —
|
| 86 |
+
no need to regenerate the G-code just to print at a different pressure.
|
| 87 |
+
"""
|
| 88 |
+
return (
|
| 89 |
+
f"{{preset}}{{aux_command}}globals().update("
|
| 90 |
+
f"{_pressure_variable_name(port)} = {pressure:g})"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _setpress_cmd(port: str, expression: str, start: bool) -> str:
|
| 95 |
"""Pressure preset in the readable host dialect.
|
| 96 |
|
| 97 |
Every pressure command uses the same `eval(setpress(...))` form (the
|
| 98 |
+
print host defines `setpress`, which builds the serial byte string).
|
| 99 |
+
`expression` is evaluated by the host — it references the `pressureN`
|
| 100 |
+
variable declared at the top of the file rather than a baked-in number,
|
| 101 |
+
so hand-editing that variable retunes the preset and the whole ramp.
|
| 102 |
+
`start` adds the {preset} marker: the host executes presets at the
|
| 103 |
+
controller's START signal, before the initial toggles; ramp commands
|
| 104 |
+
mid-file run at their scheduled times instead.
|
| 105 |
"""
|
| 106 |
if start:
|
| 107 |
+
return f"\n\r{{preset}}{port}.write(eval(setpress({expression})))"
|
| 108 |
+
return f"\n\r{port}.write(eval(setpress({expression})))"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _ramp_expression(port: int, offset: float) -> str:
|
| 112 |
+
"""Ramp step as an expression of the top-of-file `pressureN` variable.
|
| 113 |
+
|
| 114 |
+
The regulator tops out at MAX_PRESSURE_PSI and resolves tenths, so the
|
| 115 |
+
clamp and rounding travel inside the expression — they must hold for
|
| 116 |
+
whatever value the user hand-edits the variable to, which rules out
|
| 117 |
+
pre-computing them here.
|
| 118 |
+
"""
|
| 119 |
+
return (
|
| 120 |
+
f"round(min({_pressure_variable_name(port)} + {offset:g}, "
|
| 121 |
+
f"{MAX_PRESSURE_PSI:g}), 1)"
|
| 122 |
+
)
|
| 123 |
|
| 124 |
|
| 125 |
def _toggle_cmd(port: str, start: bool) -> str:
|
|
|
|
| 156 |
pressure_ramp_enabled: bool,
|
| 157 |
all_g1: bool,
|
| 158 |
emit_pressure_commands: bool = True,
|
| 159 |
+
pressure_variable: bool = True,
|
| 160 |
) -> None:
|
| 161 |
"""Write the move list as a G-code file.
|
| 162 |
|
| 163 |
+
`emit_pressure_commands` gates EVERY pressure command (the top-of-file
|
| 164 |
+
`pressure` variable, preset, toggle, per-layer ramp, closing toggle):
|
| 165 |
+
the pressure regulator is a PORT
|
| 166 |
device, so when several shapes share a serial port only ONE of their
|
| 167 |
files may own it — the print host compiles all files onto one timeline,
|
| 168 |
and duplicated toggles would flip the regulator on/off/on at start.
|
| 169 |
+
|
| 170 |
+
`pressure_variable` picks the pressure command style: True writes the
|
| 171 |
+
hand-editable `pressureN` variable on the first line with every
|
| 172 |
+
setpress referencing it; False writes plain numeric setpress commands
|
| 173 |
+
(the pre-variable format) with the ramp values baked in.
|
| 174 |
"""
|
| 175 |
off_color = 0
|
| 176 |
com_port = f"serialPort{port}"
|
| 177 |
color_dict: dict[int, int] = {0: 100, 255: valve}
|
| 178 |
|
| 179 |
# The regulator tops out at MAX_PRESSURE_PSI and resolves tenths: clamp
|
| 180 |
+
# the default the file ships with; the ramp expressions carry their own
|
| 181 |
+
# clamp so hand-edited values stay legal too.
|
| 182 |
pressure = round(min(max(float(pressure), 0.0), MAX_PRESSURE_PSI), 1)
|
| 183 |
+
preset_expression = (
|
| 184 |
+
_pressure_variable_name(port) if pressure_variable else f"{pressure:g}"
|
| 185 |
+
)
|
| 186 |
+
setpress_lines = [_setpress_cmd(com_port, preset_expression, start=True)]
|
| 187 |
pressure_on_lines = [_toggle_cmd(com_port, start=True)]
|
| 188 |
pressure_off_lines = [_toggle_cmd(com_port, start=False)]
|
| 189 |
|
| 190 |
+
ramp_layer = 0
|
| 191 |
pressure_cur = float(pressure)
|
| 192 |
|
| 193 |
with open(gcode_path, "w") as f:
|
| 194 |
+
if emit_pressure_commands and pressure_variable:
|
| 195 |
+
f.write(f"{_pressure_variable_cmd(port, pressure)}\n")
|
| 196 |
f.write("G91\n")
|
| 197 |
f.write(_valve_cmd(valve, 0))
|
| 198 |
if emit_pressure_commands:
|
|
|
|
| 224 |
f"Z{_coord(move['Z'])} ; Color {move['Color']}"
|
| 225 |
)
|
| 226 |
if pressure_ramp_enabled and emit_pressure_commands:
|
| 227 |
+
if pressure_variable:
|
| 228 |
+
ramp_layer += 1
|
| 229 |
+
offset = round(increase_pressure_per_layer * ramp_layer, 6)
|
| 230 |
+
ramp_expression = _ramp_expression(port, offset)
|
| 231 |
+
else:
|
| 232 |
+
pressure_cur = round(
|
| 233 |
+
min(
|
| 234 |
+
pressure_cur + increase_pressure_per_layer,
|
| 235 |
+
MAX_PRESSURE_PSI,
|
| 236 |
+
),
|
| 237 |
+
1,
|
| 238 |
+
)
|
| 239 |
+
ramp_expression = f"{pressure_cur:g}"
|
| 240 |
+
pressure_next = _setpress_cmd(
|
| 241 |
+
com_port, ramp_expression, start=False
|
| 242 |
)
|
|
|
|
| 243 |
else:
|
| 244 |
pressure_next = None
|
| 245 |
else:
|
|
|
|
| 277 |
infill: float = 1.0,
|
| 278 |
motion_infill_fractions: list[float] | None = None,
|
| 279 |
emit_pressure_commands: bool = True,
|
| 280 |
+
pressure_variable: bool = True,
|
| 281 |
sweep_buffer: float | None = None,
|
| 282 |
increase_pressure_per_layer: float = 0.1,
|
| 283 |
pressure_ramp_enabled: bool = True,
|
|
|
|
| 466 |
pressure_ramp_enabled=bool(pressure_ramp_enabled),
|
| 467 |
all_g1=bool(all_g1),
|
| 468 |
emit_pressure_commands=bool(emit_pressure_commands),
|
| 469 |
+
pressure_variable=bool(pressure_variable),
|
| 470 |
)
|
| 471 |
return gcode_path
|