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