Characterize Rayforge JCZ device import gaps
This commit is contained in:
parent
202e756071
commit
131ca4b737
6 changed files with 274 additions and 0 deletions
53
docs/research/rayforge-lightburn-device-import-spike.md
Normal file
53
docs/research/rayforge-lightburn-device-import-spike.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Rayforge synthetic LightBurn JCZ device-import spike
|
||||||
|
|
||||||
|
- Date: 2026-10-19
|
||||||
|
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
|
||||||
|
- Fixture: synthetic public `.lbdev`; no machine identifiers
|
||||||
|
- Scope: device-profile importer only, with no discovery or transport
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
Pinned Rayforge parses the synthetic `JCZFiber` profile and preserves its
|
||||||
|
display name, 110 x 110 mm work area and generic home-on-start value. It does
|
||||||
|
not select a driver because its LightBurn driver map has no `JCZFiber` entry.
|
||||||
|
|
||||||
|
The imported `DeviceProfile` also has no representation of the observed
|
||||||
|
`RequireFrameBeforeStart` setting or the synthetic cut-list `frequency` and
|
||||||
|
`QPulseWidth` fields. The import is therefore explicitly classified as unsafe
|
||||||
|
for JCZ use rather than receiving guessed defaults.
|
||||||
|
|
||||||
|
The checked result is
|
||||||
|
`tests/fixtures/rayforge-lightburn-device-import.json`. Two runs produced the
|
||||||
|
same SHA-256 digest,
|
||||||
|
`3c26f43be6b06e9b17a5decbe4ba7bb633eb1b8ac7bb0361449d7498a3b9279c`.
|
||||||
|
|
||||||
|
## Evidence boundary
|
||||||
|
|
||||||
|
The fixture uses field names already recorded by the sanitized LightBurn
|
||||||
|
device-export analysis. Its values are synthetic examples inside observed
|
||||||
|
ranges, not a copy of a private export and not certified machine limits or
|
||||||
|
material settings. It deliberately omits private GUIDs, source paths and
|
||||||
|
unpublished connection serialization.
|
||||||
|
|
||||||
|
This spike characterizes the current importer; it does not implement a driver
|
||||||
|
mapping. A future `JCZFiber` mapping must target the separately tested
|
||||||
|
no-transport addon driver and require an exact compatible profile. It must not
|
||||||
|
silently map `JCZFiber` to a G-code or Ruida driver.
|
||||||
|
|
||||||
|
## Reproduce
|
||||||
|
|
||||||
|
Run against the pinned Rayforge checkout:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts/spike_rayforge_lightburn_device_import.py C:\path\to\rayforge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next seam
|
||||||
|
|
||||||
|
The smallest evidence-backed extension is to recognize `JCZFiber` as a
|
||||||
|
distinct, fail-closed profile kind, preserve its raw settings with explicit
|
||||||
|
units/provenance, and associate it only with the external no-device JCZ driver.
|
||||||
|
Cut-list recipes and project-layer `QPulseWidth` mapping remain gated until a
|
||||||
|
known-working `.lbrn2` fixture establishes project serialization.
|
||||||
|
|
||||||
|
No USB enumeration, controller command, motion, framing or emission occurred.
|
||||||
|
|
@ -276,6 +276,12 @@ The next work package should be small enough to review in a few commits:
|
||||||
Hardware work cannot proceed until item 6 is reviewed and the physical setup
|
Hardware work cannot proceed until item 6 is reviewed and the physical setup
|
||||||
is ready.
|
is ready.
|
||||||
|
|
||||||
|
Item 2 is now covered by the
|
||||||
|
[synthetic LightBurn JCZ device-import spike](research/rayforge-lightburn-device-import-spike.md):
|
||||||
|
Rayforge preserves the common name and work-area fields, but maps no
|
||||||
|
`JCZFiber` driver and drops the JCZ-specific safety and MOPA fields from its
|
||||||
|
profile model. The fixture and regression test remain transport-free.
|
||||||
|
|
||||||
## Decision rules
|
## Decision rules
|
||||||
|
|
||||||
- A simulator test precedes every equivalent hardware behavior.
|
- A simulator test precedes every equivalent hardware behavior.
|
||||||
|
|
|
||||||
111
scripts/spike_rayforge_lightburn_device_import.py
Normal file
111
scripts/spike_rayforge_lightburn_device_import.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
"""Characterize a synthetic JCZ LightBurn device import in pinned Rayforge.
|
||||||
|
|
||||||
|
The fixture contains only public, synthetic values. This spike performs no
|
||||||
|
device discovery or transport and deliberately does not add a JCZ driver.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
RAYFORGE_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
|
||||||
|
|
||||||
|
|
||||||
|
def _commit_of(source: Path) -> str:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "-C", str(source), "rev-parse", "HEAD"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def run(source: Path, fixture: Path) -> dict[str, object]:
|
||||||
|
commit = _commit_of(source)
|
||||||
|
if commit != RAYFORGE_COMMIT:
|
||||||
|
raise RuntimeError(f"expected Rayforge {RAYFORGE_COMMIT}, found {commit}")
|
||||||
|
sys.path.insert(0, str(source))
|
||||||
|
|
||||||
|
from rayforge.machine.device.lightburn_importer import (
|
||||||
|
convert_to_profile,
|
||||||
|
parse_lbdev,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw = parse_lbdev(fixture)
|
||||||
|
profile, summary = convert_to_profile(fixture)
|
||||||
|
settings = raw["Settings"]
|
||||||
|
cut_entry = raw["CutList"][0]
|
||||||
|
|
||||||
|
if raw["Type"] != "JCZFiber":
|
||||||
|
raise AssertionError("synthetic fixture is not a JCZFiber profile")
|
||||||
|
if profile.machine_config.axis_extents != (110.0, 110.0):
|
||||||
|
raise AssertionError("work area did not survive import")
|
||||||
|
if profile.machine_config.driver is not None or summary.driver is not None:
|
||||||
|
raise AssertionError("pinned Rayforge unexpectedly mapped a JCZ driver")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rayforge_commit": commit,
|
||||||
|
"transport": None,
|
||||||
|
"fixture": fixture.name,
|
||||||
|
"fixture_provenance": "synthetic public data",
|
||||||
|
"raw_profile": {
|
||||||
|
"name": raw["DisplayName"],
|
||||||
|
"type": raw["Type"],
|
||||||
|
"work_area_mm": [raw["Width"], raw["Height"]],
|
||||||
|
"require_frame_before_start": settings[
|
||||||
|
"RequireFrameBeforeStart"
|
||||||
|
],
|
||||||
|
"cut_entry": {
|
||||||
|
"frequency": cut_entry["frequency"],
|
||||||
|
"QPulseWidth": cut_entry["QPulseWidth"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mapped_profile": {
|
||||||
|
"name": profile.meta.name,
|
||||||
|
"driver": profile.machine_config.driver,
|
||||||
|
"work_area_mm": list(profile.machine_config.axis_extents),
|
||||||
|
"home_on_start": profile.machine_config.home_on_start,
|
||||||
|
},
|
||||||
|
"gap_matrix": {
|
||||||
|
"jczfiber_driver_mapped": profile.machine_config.driver is not None,
|
||||||
|
"require_frame_before_start_mapped": False,
|
||||||
|
"cut_list_mapped": False,
|
||||||
|
"frequency_mapped": False,
|
||||||
|
"q_pulse_width_mapped": False,
|
||||||
|
"safe_for_jcz_use": False,
|
||||||
|
},
|
||||||
|
"conclusion": (
|
||||||
|
"name and work area survive, but JCZFiber has no driver mapping; "
|
||||||
|
"RequireFrameBeforeStart, CutList, frequency and QPulseWidth are "
|
||||||
|
"not represented in the imported Rayforge profile"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
root = Path(__file__).parents[1]
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source", type=Path, help="pinned Rayforge checkout")
|
||||||
|
parser.add_argument(
|
||||||
|
"--fixture",
|
||||||
|
type=Path,
|
||||||
|
default=root / "tests" / "fixtures" / "synthetic-jcz-device.lbdev",
|
||||||
|
)
|
||||||
|
parser.add_argument("--output", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
result = run(args.source.resolve(), args.fixture.resolve())
|
||||||
|
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
||||||
|
if args.output:
|
||||||
|
args.output.write_text(rendered, encoding="utf-8", newline="\n")
|
||||||
|
else:
|
||||||
|
print(rendered, end="")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
37
tests/fixtures/rayforge-lightburn-device-import.json
vendored
Normal file
37
tests/fixtures/rayforge-lightburn-device-import.json
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"conclusion": "name and work area survive, but JCZFiber has no driver mapping; RequireFrameBeforeStart, CutList, frequency and QPulseWidth are not represented in the imported Rayforge profile",
|
||||||
|
"fixture": "synthetic-jcz-device.lbdev",
|
||||||
|
"fixture_provenance": "synthetic public data",
|
||||||
|
"gap_matrix": {
|
||||||
|
"cut_list_mapped": false,
|
||||||
|
"frequency_mapped": false,
|
||||||
|
"jczfiber_driver_mapped": false,
|
||||||
|
"q_pulse_width_mapped": false,
|
||||||
|
"require_frame_before_start_mapped": false,
|
||||||
|
"safe_for_jcz_use": false
|
||||||
|
},
|
||||||
|
"mapped_profile": {
|
||||||
|
"driver": null,
|
||||||
|
"home_on_start": false,
|
||||||
|
"name": "Synthetic JCZ MOPA",
|
||||||
|
"work_area_mm": [
|
||||||
|
110.0,
|
||||||
|
110.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raw_profile": {
|
||||||
|
"cut_entry": {
|
||||||
|
"QPulseWidth": 100,
|
||||||
|
"frequency": 40000
|
||||||
|
},
|
||||||
|
"name": "Synthetic JCZ MOPA",
|
||||||
|
"require_frame_before_start": true,
|
||||||
|
"type": "JCZFiber",
|
||||||
|
"work_area_mm": [
|
||||||
|
110,
|
||||||
|
110
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rayforge_commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
|
||||||
|
"transport": null
|
||||||
|
}
|
||||||
23
tests/fixtures/synthetic-jcz-device.lbdev
vendored
Normal file
23
tests/fixtures/synthetic-jcz-device.lbdev
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"FixtureProvenance": "synthetic public data; not a machine export",
|
||||||
|
"DeviceList": [
|
||||||
|
{
|
||||||
|
"DisplayName": "Synthetic JCZ MOPA",
|
||||||
|
"Name": "JCZ",
|
||||||
|
"Type": "JCZFiber",
|
||||||
|
"Width": 110,
|
||||||
|
"Height": 110,
|
||||||
|
"HomeOnStartup": false,
|
||||||
|
"Settings": {
|
||||||
|
"RequireFrameBeforeStart": true
|
||||||
|
},
|
||||||
|
"CutList": [
|
||||||
|
{
|
||||||
|
"Name": "Synthetic MOPA entry",
|
||||||
|
"frequency": 40000,
|
||||||
|
"QPulseWidth": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
44
tests/test_rayforge_lightburn_device_import_spike.py
Normal file
44
tests/test_rayforge_lightburn_device_import_spike.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parents[1]
|
||||||
|
RESULT = ROOT / "tests" / "fixtures" / "rayforge-lightburn-device-import.json"
|
||||||
|
SOURCE = ROOT / "tests" / "fixtures" / "synthetic-jcz-device.lbdev"
|
||||||
|
|
||||||
|
|
||||||
|
class RayforgeLightBurnDeviceImportSpikeTests(unittest.TestCase):
|
||||||
|
def test_public_fixture_is_synthetic_and_contains_no_private_identity(self):
|
||||||
|
source = json.loads(SOURCE.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
"synthetic public data; not a machine export",
|
||||||
|
source["FixtureProvenance"],
|
||||||
|
)
|
||||||
|
rendered = SOURCE.read_text(encoding="utf-8").lower()
|
||||||
|
self.assertNotIn("serial", rendered)
|
||||||
|
self.assertNotIn("guid", rendered)
|
||||||
|
|
||||||
|
def test_common_profile_fields_survive(self):
|
||||||
|
result = json.loads(RESULT.read_text(encoding="utf-8"))
|
||||||
|
self.assertIsNone(result["transport"])
|
||||||
|
self.assertEqual("JCZFiber", result["raw_profile"]["type"])
|
||||||
|
self.assertEqual([110, 110], result["mapped_profile"]["work_area_mm"])
|
||||||
|
self.assertEqual(
|
||||||
|
"Synthetic JCZ MOPA", result["mapped_profile"]["name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_jcz_specific_fields_fail_closed(self):
|
||||||
|
result = json.loads(RESULT.read_text(encoding="utf-8"))
|
||||||
|
gaps = result["gap_matrix"]
|
||||||
|
self.assertIsNone(result["mapped_profile"]["driver"])
|
||||||
|
self.assertFalse(gaps["jczfiber_driver_mapped"])
|
||||||
|
self.assertFalse(gaps["require_frame_before_start_mapped"])
|
||||||
|
self.assertFalse(gaps["cut_list_mapped"])
|
||||||
|
self.assertFalse(gaps["frequency_mapped"])
|
||||||
|
self.assertFalse(gaps["q_pulse_width_mapped"])
|
||||||
|
self.assertFalse(gaps["safe_for_jcz_use"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Reference in a new issue