Bind LightBurn MOPA units to verified evidence
This commit is contained in:
parent
862d5a3cca
commit
8ab25827db
24 changed files with 328 additions and 126 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -4,6 +4,8 @@
|
||||||
*.py text eol=lf
|
*.py text eol=lf
|
||||||
*.json text eol=lf
|
*.json text eol=lf
|
||||||
*.jsonl text eol=lf
|
*.jsonl text eol=lf
|
||||||
|
*.webmanifest text eol=lf
|
||||||
|
*.xml text eol=lf
|
||||||
*.yaml text eol=lf
|
*.yaml text eol=lf
|
||||||
*.yml text eol=lf
|
*.yml text eol=lf
|
||||||
*.png binary
|
*.png binary
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ operator-verified projects are preserved and analyzed. The operator confirmed a
|
||||||
and [control-machine observation](docs/research/lightburn-control-machine-observation-2026-10-24.md).
|
and [control-machine observation](docs/research/lightburn-control-machine-observation-2026-10-24.md).
|
||||||
|
|
||||||
Three operator-verified projects now establish project-level `frequency` and
|
Three operator-verified projects now establish project-level `frequency` and
|
||||||
`QPulseWidth` serialization; see the
|
`QPulseWidth` serialization. Correlation with the running LightBurn 2.1.03 UI
|
||||||
|
establishes raw Hz to displayed kHz and direct ns pulse width; see the
|
||||||
[verified-project analysis](docs/research/lightburn-verified-projects-2026-10-24.md).
|
[verified-project analysis](docs/research/lightburn-verified-projects-2026-10-24.md).
|
||||||
Missing pulse width still requires explicit resolution before executable import.
|
Missing pulse width still requires explicit resolution before executable import.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,10 @@ gates remain:
|
||||||
1. A Windows artifact for the pinned Rayforge revision must be produced on a
|
1. A Windows artifact for the pinned Rayforge revision must be produced on a
|
||||||
reviewed build runner, verified without execution, and pass the manual
|
reviewed build runner, verified without execution, and pass the manual
|
||||||
no-device desktop checklist.
|
no-device desktop checklist.
|
||||||
2. Promote only complete vector MOPA imports after an explicit unit contract,
|
2. The known-working private JPT `.lbrn2` serialization and unit-contract gates
|
||||||
absent-pulse-width policy and exact machine-context validation. The private
|
are met for LightBurn 2.1.03 vector settings. Promotion still requires
|
||||||
serialization-evidence gate itself is complete.
|
explicit resolution of absent pulse width, image equivalence and complete
|
||||||
|
machine-context validation.
|
||||||
|
|
||||||
Raw projects, screenshots containing identifiers, machine data and build
|
Raw projects, screenshots containing identifiers, machine data and build
|
||||||
binaries stay out of the public repository.
|
binaries stay out of the public repository.
|
||||||
|
|
@ -89,13 +90,13 @@ binaries stay out of the public repository.
|
||||||
An offline project MOPA seam, disabled-output Rayforge workflow bridge and
|
An offline project MOPA seam, disabled-output Rayforge workflow bridge and
|
||||||
synthetic tests now exist; see
|
synthetic tests now exist; see
|
||||||
`docs/research/lightburn-project-mopa-import.md`. It preserves both process
|
`docs/research/lightburn-project-mopa-import.md`. It preserves both process
|
||||||
element types and requires an explicit unit contract for normalization. The
|
element types and applies the evidence-bound Hz/ns contract only to LightBurn
|
||||||
bridge binds source layer IDs, uses the host MOPA setters, serializes review
|
2.1.03. The bridge binds source layer IDs, uses the host MOPA setters,
|
||||||
metadata and disables every staged step. Document preflight presents those
|
serializes exact unit provenance and disables every staged step. Document
|
||||||
blockers and the offline driver rejects them before buffer preparation. It
|
preflight presents the remaining blockers and the offline driver rejects them
|
||||||
never authorizes encoding. The next import task is GTK presentation plus an
|
before buffer preparation. It never authorizes encoding. The next import task
|
||||||
independently established unit contract; absent pulse width and image
|
is packaged GTK presentation; absent pulse width and image equivalence remain
|
||||||
equivalence remain blockers.
|
blockers.
|
||||||
|
|
||||||
If a Windows artifact is available, keep the machine disconnected and run, in
|
If a Windows artifact is available, keep the machine disconnected and run, in
|
||||||
order:
|
order:
|
||||||
|
|
@ -108,8 +109,9 @@ order:
|
||||||
|
|
||||||
If no artifact is available, send the external-builder handoff in
|
If no artifact is available, send the external-builder handoff in
|
||||||
`docs/research/m1-no-device-acceptance.md`; do not start another research
|
`docs/research/m1-no-device-acceptance.md`; do not start another research
|
||||||
branch or install a build environment here. Kaz's explicit decision on ADR
|
branch or install a build environment here. The evidence-backed vector MOPA
|
||||||
0003 is also outstanding.
|
import mapping and synthetic tests are complete; Kaz's explicit decision on
|
||||||
|
ADR 0003 is also outstanding.
|
||||||
The M3 protocol can also be reviewed offline, but its
|
The M3 protocol can also be reviewed offline, but its
|
||||||
operation allowlist must stay empty until exact read-only request evidence is
|
operation allowlist must stay empty until exact read-only request evidence is
|
||||||
available. ADR 0002 is accepted for the offline architecture. Until the
|
available. ADR 0002 is accepted for the offline architecture. Until the
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,9 @@ Subsequent offline evidence:
|
||||||
external encoder deterministically packs controller-shaped command records
|
external encoder deterministically packs controller-shaped command records
|
||||||
matching the pinned BalorMK mock values, while retaining no transport.
|
matching the pinned BalorMK mock values, while retaining no transport.
|
||||||
- [Synthetic LightBurn MOPA import spike](../research/rayforge-lightburn-mopa-import-spike.md):
|
- [Synthetic LightBurn MOPA import spike](../research/rayforge-lightburn-mopa-import-spike.md):
|
||||||
geometry and common settings survive, but frequency is not mapped to the
|
geometry and common settings survive; the external addon now maps complete
|
||||||
workflow and pulse-width representation still lacks fixture evidence.
|
vector frequency and pulse width with a LightBurn 2.1.03 evidence contract,
|
||||||
|
while keeping every staged step disabled.
|
||||||
- [Graphical workflow model spike](../research/rayforge-graphical-workflow-spike.md):
|
- [Graphical workflow model spike](../research/rayforge-graphical-workflow-spike.md):
|
||||||
Rayforge's generated settings contract exposes frequency and pulse width,
|
Rayforge's generated settings contract exposes frequency and pulse width,
|
||||||
preserves them through step serialization and supplies preview Ops without
|
preserves them through step serialization and supplies preview Ops without
|
||||||
|
|
@ -130,7 +131,9 @@ Subsequent offline evidence:
|
||||||
|
|
||||||
Three operator-verified private projects subsequently established the
|
Three operator-verified private projects subsequently established the
|
||||||
project-level `frequency` and `QPulseWidth` serialization needed to implement
|
project-level `frequency` and `QPulseWidth` serialization needed to implement
|
||||||
the disabled-output import seam. Together, the evidence satisfies the host
|
the disabled-output import seam. The running LightBurn 2.1.03 UI further binds
|
||||||
architecture decision. It does not authorize hardware access or claim that the
|
those values to raw Hz/display kHz and direct ns. Together, the evidence
|
||||||
packaged Windows interaction and import-promotion release gates have passed.
|
satisfies the accepted host architecture decision. It does not authorize
|
||||||
|
hardware access or claim that the packaged Windows interaction and
|
||||||
|
import-promotion release gates have passed.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,10 @@ Across the 60 exported cut-list entries:
|
||||||
LightBurn's official galvo cut-setting documentation presents frequency in
|
LightBurn's official galvo cut-setting documentation presents frequency in
|
||||||
kHz and Q-Pulse Width in nanoseconds. The larger integers stored by the export
|
kHz and Q-Pulse Width in nanoseconds. The larger integers stored by the export
|
||||||
show that the serialized representation cannot simply be copied into a UI
|
show that the serialized representation cannot simply be copied into a UI
|
||||||
field without an explicit conversion contract. The exact field name and unit
|
field without an explicit conversion contract. Subsequent operator-verified
|
||||||
are now evidenced for a device-library export, but the representation inside
|
LightBurn 2.1.03 project/UI correlation establishes raw Hz to display kHz and
|
||||||
a `.lbrn2` project remains unconfirmed.
|
direct-ns `QPulseWidth`; see the
|
||||||
|
[control-machine observation](lightburn-control-machine-observation-2026-10-24.md).
|
||||||
|
|
||||||
These observed recipe values and profile fields are **not certified hardware
|
These observed recipe values and profile fields are **not certified hardware
|
||||||
limits or safe operating parameters**. They must not be used to probe the
|
limits or safe operating parameters**. They must not be used to probe the
|
||||||
|
|
@ -93,19 +94,16 @@ new general parser:
|
||||||
|
|
||||||
1. add evidence-backed JCZ device-profile recognition;
|
1. add evidence-backed JCZ device-profile recognition;
|
||||||
2. preserve source units and raw values at the import boundary;
|
2. preserve source units and raw values at the import boundary;
|
||||||
3. map frequency and `QPulseWidth` only after a real `.lbrn2` fixture confirms
|
3. bind the now-confirmed LightBurn 2.1.03 Hz/ns contract to exact provenance;
|
||||||
project serialization;
|
|
||||||
4. validate imported values against a separately verified machine profile;
|
4. validate imported values against a separately verified machine profile;
|
||||||
5. fail closed when units, profile identity or ranges are unknown.
|
5. fail closed when units, profile identity or ranges are unknown.
|
||||||
|
|
||||||
## Remaining collection gaps
|
## Remaining collection gaps
|
||||||
|
|
||||||
The exports settle the exact LightBurn release and the two named device
|
The exports settle the exact LightBurn release and the two named device
|
||||||
profiles. Phase 0 still needs:
|
profiles. The known-working projects, Device Settings screenshots and closed
|
||||||
|
preferences snapshot were subsequently collected. Phase 0 still needs:
|
||||||
|
|
||||||
- two or three known-working `.lbrn2` projects, especially a MOPA/hatch job;
|
|
||||||
- Device Settings and device-list screenshots;
|
|
||||||
- the closed-application LightBurn preferences directory;
|
|
||||||
- exterior machine, source, controller and installed-lens photographs;
|
- exterior machine, source, controller and installed-lens photographs;
|
||||||
- mounted-lens, connection, startup and interlock notes;
|
- mounted-lens, connection, startup and interlock notes;
|
||||||
- the stock driver's identity and version on the control computer.
|
- the stock driver's identity and version on the control computer.
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,11 @@ unverified test files and instructed us not to use them. Nine temporary copies
|
||||||
were removed from this acquisition before analysis or commit. Their originals
|
were removed from this acquisition before analysis or commit. Their originals
|
||||||
were unchanged; no project content was analyzed or retained as evidence.
|
were unchanged; no project content was analyzed or retained as evidence.
|
||||||
|
|
||||||
This collection closes the missing closed-application settings snapshot. It
|
This collection closes the missing closed-application settings snapshot. The
|
||||||
does **not** close the known-working JPT project gate: project-level frequency
|
gate was subsequently closed by three separately supplied, operator-verified
|
||||||
and pulse-width serialization still require independently identified,
|
projects plus the running LightBurn 2.1.03 UI. The excluded candidates remain
|
||||||
operator-verified project evidence. Do not derive an executable import mapping
|
unverified and are still not evidence. The resulting vector import mapping
|
||||||
from these settings or the excluded test projects.
|
stays disabled and review-gated; it is not executable machine authorization.
|
||||||
|
|
||||||
The reviewed Windows artifact and manual no-device desktop acceptance also
|
The reviewed Windows artifact and manual no-device desktop acceptance also
|
||||||
remain outstanding. No controller, USB endpoint, framing, motion or laser
|
remain outstanding. No controller, USB endpoint, framing, motion or laser
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ The addon's `project_import.py` reads observed `CutSetting` and
|
||||||
It preserves raw `frequency`, `QPulseWidth`, layer order and image dither mode.
|
It preserves raw `frequency`, `QPulseWidth`, layer order and image dither mode.
|
||||||
The synthetic unit tests contain no private geometry or complete recipes.
|
The synthetic unit tests contain no private geometry or complete recipes.
|
||||||
|
|
||||||
Normalization requires the caller to declare the serialized-unit contract
|
Normalization requires the caller to opt into the evidence-backed serialized
|
||||||
`frequency=Hz;QPulseWidth=ns`. This is an explicit input, not an inference from
|
unit contract `frequency=Hz;QPulseWidth=ns`. The contract is restricted to
|
||||||
numeric magnitude or an authorization claim. With that contract, frequency is
|
LightBurn `AppVersion="2.1.03"`; a missing or different version fails closed.
|
||||||
represented in Hz and kHz (division by 1000), and pulse width in ns. Without
|
It comes from correlating an operator-verified project's raw
|
||||||
it, only raw values are returned. The seam does not establish the contract on
|
`frequency=1000000` and `QPulseWidth=10` with LightBurn's visible `1000 kHz`
|
||||||
behalf of the caller or read machine limits from these values.
|
and `10 ns` controls. This is not an inference from magnitude or an
|
||||||
|
authorization claim. Frequency is represented in Hz and kHz (division by
|
||||||
|
1000), while pulse width remains in ns. Without explicit contract selection,
|
||||||
|
only raw values are returned.
|
||||||
|
|
||||||
Absent fields remain absent, never zero or inherited defaults. Malformed,
|
Absent fields remain absent, never zero or inherited defaults. Malformed,
|
||||||
non-finite, non-positive, duplicate and excessively large values are rejected.
|
non-finite, non-positive, duplicate and excessively large values are rejected.
|
||||||
|
|
@ -23,17 +26,19 @@ Every result has `encoding_allowed=False`, `hardware_use_allowed=False` and
|
||||||
offline metadata seam. The companion workflow bridge matches the normalized
|
offline metadata seam. The companion workflow bridge matches the normalized
|
||||||
LightBurn layer index to Rayforge's source-segment layer ID and stages complete
|
LightBurn layer index to Rayforge's source-segment layer ID and stages complete
|
||||||
vector MOPA values through the host's setters. Every staged step is disabled
|
vector MOPA values through the host's setters. Every staged step is disabled
|
||||||
through `Step.set_visible(False)`, and its raw values, normalized values,
|
through `Step.set_visible(False)`, and its raw values, normalized values, exact
|
||||||
review status and blockers are saved in the serializable `Step.extra` data.
|
unit-contract provenance, review status and blockers are saved in the
|
||||||
|
serializable `Step.extra` data. Tampered or missing provenance is rejected
|
||||||
|
before any step is mutated.
|
||||||
Image settings and incomplete settings remain disabled without applying MOPA
|
Image settings and incomplete settings remain disabled without applying MOPA
|
||||||
values. Multi-step staging is preflighted and rolled back on setter failure.
|
values. Multi-step staging is preflighted and rolled back on setter failure.
|
||||||
|
|
||||||
The bridge deliberately has no function that enables output or removes a
|
The bridge deliberately has no function that enables output or removes a
|
||||||
blocker. Host UI and preflight integration must still establish serialized
|
blocker. Host UI and preflight integration must still resolve absent pulse
|
||||||
units from independent evidence, resolve absent pulse width and validate exact
|
width and validate exact machine, lens, correction and parameter limits before
|
||||||
machine, lens, correction and parameter limits before a separate reviewed
|
a separate reviewed promotion path can exist. Image-layer equivalence remains
|
||||||
promotion path can exist. Rayforge's relevant Step and LaserStep files are now
|
unverified. Rayforge's relevant Step and LaserStep files are included in the
|
||||||
included in the pinned upstream contract manifest.
|
pinned upstream contract manifest.
|
||||||
|
|
||||||
The addon now also exposes a transport-free document preflight report with
|
The addon now also exposes a transport-free document preflight report with
|
||||||
stable issue codes and human-readable blocker messages. The offline driver
|
stable issue codes and human-readable blocker messages. The offline driver
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ required by real workshop jobs and supported by evidence.
|
||||||
| Hide geometry without disabling output, and disable output without hiding geometry | LightBurn separates `Show` and `Output` | Rayforge separates `Layer.visible` canvas state from `Step.visible` job participation and serializes both; output is operation-level rather than a layer-level control | Proven offline at model/source level; aggregate UI untested | Add or propose a tri-state layer Output control over existing step states, then verify it in the packaged no-device GUI |
|
| Hide geometry without disabling output, and disable output without hiding geometry | LightBurn separates `Show` and `Output` | Rayforge separates `Layer.visible` canvas state from `Step.visible` job participation and serializes both; output is operation-level rather than a layer-level control | Proven offline at model/source level; aggregate UI untested | Add or propose a tri-state layer Output control over existing step states, then verify it in the packaged no-device GUI |
|
||||||
| Edit common settings quickly | The layer list exposes mode, speed, power, passes and applicable galvo fields | Rayforge's generated laser-step settings expose power, speed, frequency and pulse width; serialization is proven offline | Proven offline at the model/UI-contract level | Confirm editing and save/reload in the packaged no-device GUI |
|
| Edit common settings quickly | The layer list exposes mode, speed, power, passes and applicable galvo fields | Rayforge's generated laser-step settings expose power, speed, frequency and pulse width; serialization is proven offline | Proven offline at the model/UI-contract level | Confirm editing and save/reload in the packaged no-device GUI |
|
||||||
| Reveal complexity progressively | LightBurn Beginner Mode hides some advanced settings; PirateRay calls for Beginner, Advanced and Expert levels without moving the canvas | Rayforge capability sections can hide machine-dependent fields, but three user-facing disclosure levels are not proven | Product requirement | Define the smallest beginner field set from real job needs before adding controls |
|
| Reveal complexity progressively | LightBurn Beginner Mode hides some advanced settings; PirateRay calls for Beginner, Advanced and Expert levels without moving the canvas | Rayforge capability sections can hide machine-dependent fields, but three user-facing disclosure levels are not proven | Product requirement | Define the smallest beginner field set from real job needs before adding controls |
|
||||||
| Preserve MOPA units and values | Galvo frequency is presented in kHz and Q-pulse width in ns | Rayforge's workflow uses Hz and ns and preserves both; LightBurn project import does not yet map frequency or pulse width into workflow settings | Partly proven; import gated | Keep explicit conversion at the import boundary and wait for a known-working `.lbrn2` before mapping `QPulseWidth` |
|
| Preserve MOPA units and values | Galvo frequency is presented in kHz and Q-pulse width in ns | Rayforge's workflow uses Hz and ns; the addon maps complete LightBurn 2.1.03 vector settings with an evidence-bound raw-Hz/display-kHz and direct-ns contract | Proven offline for complete vector settings; image/absent-pulse cases gated | Verify editing and persistence in the packaged no-device GUI while retaining the version and provenance checks |
|
||||||
| Apply a reusable material recipe | LightBurn libraries organize presets by material, thickness and description and apply/link them to layers | Rayforge recipes already match machine, head, material, thickness and step type and preserve Hz/ns settings; the external addon now validates versioned provenance plus exact source/lens/correction compatibility while withholding hardware authorization | Proven offline at model/source level; approval policy and portable collections remain | Integrate the validated metadata into recipe selection before changing the existing recipe UI |
|
| Apply a reusable material recipe | LightBurn libraries organize presets by material, thickness and description and apply/link them to layers | Rayforge recipes already match machine, head, material, thickness and step type and preserve Hz/ns settings; the external addon now validates versioned provenance plus exact source/lens/correction compatibility while withholding hardware authorization | Proven offline at model/source level; approval policy and portable collections remain | Integrate the validated metadata into recipe selection before changing the existing recipe UI |
|
||||||
| Inspect planned behavior before any run | LightBurn Preview reflects enabled layers and planned order | Rayforge produced a populated `JobArtifact` with Ops, path length and no machine code | Proven offline; human interaction untested | Verify layer filtering, order, hatch and warnings in the packaged no-device GUI |
|
| Inspect planned behavior before any run | LightBurn Preview reflects enabled layers and planned order | Rayforge produced a populated `JobArtifact` with Ops, path length and no machine code | Proven offline; human interaction untested | Verify layer filtering, order, hatch and warnings in the packaged no-device GUI |
|
||||||
| Reject a visibly invalid job | Preview/preflight should reveal unintended output; the target product must reject profile mismatch and out-of-field geometry | Existing JCZ negative tests reject profile mismatch, out-of-field geometry, invalid values and unsupported Ops | Proven offline at encoder boundary | Surface these failures as understandable preflight messages without weakening the fail-closed gate |
|
| Reject a visibly invalid job | Preview/preflight should reveal unintended output; the target product must reject profile mismatch and out-of-field geometry | Existing JCZ negative tests reject profile mismatch, out-of-field geometry, invalid values and unsupported Ops | Proven offline at encoder boundary | Surface these failures as understandable preflight messages without weakening the fail-closed gate |
|
||||||
|
|
@ -102,9 +102,10 @@ The PirateRay-specific work should remain narrow:
|
||||||
|
|
||||||
## Gaps that must not be papered over
|
## Gaps that must not be papered over
|
||||||
|
|
||||||
1. Three known-working private galvo projects establish project-level
|
1. Three known-working private galvo projects and the running LightBurn 2.1.03
|
||||||
`QPulseWidth` serialization. Omission/default behavior still requires an
|
UI establish the vector Hz/ns serialization contract. Missing
|
||||||
explicit policy before promotion to executable output.
|
`QPulseWidth` remains absence, never a guessed default; image-processing
|
||||||
|
equivalence is also unproven.
|
||||||
2. The current Windows environment has not launched the pinned Rayforge GTK
|
2. The current Windows environment has not launched the pinned Rayforge GTK
|
||||||
application; the headless UI contract is not a human interaction test.
|
application; the headless UI contract is not a human interaction test.
|
||||||
3. `RequireFrameBeforeStart` is observed in a LightBurn device export and
|
3. `RequireFrameBeforeStart` is observed in a LightBurn device export and
|
||||||
|
|
|
||||||
|
|
@ -163,10 +163,13 @@ ISO-8601 review timestamp with timezone. No status authorizes hardware access.
|
||||||
|
|
||||||
## Remaining release gates
|
## Remaining release gates
|
||||||
|
|
||||||
|
- The known-working project gate and LightBurn 2.1.03 Hz/ns serialization
|
||||||
|
contract are complete. Missing pulse width and image equivalence remain
|
||||||
|
explicit blockers.
|
||||||
- Human interaction results from the packaged build must be recorded before
|
- Human interaction results from the packaged build must be recorded before
|
||||||
M1 can be called a packaged desktop checkpoint.
|
M1 can be called a packaged desktop checkpoint.
|
||||||
- The three known-working private projects establish serialized MOPA fields.
|
- The three known-working private projects establish serialized MOPA fields.
|
||||||
Promotion from disabled staging still requires the explicit unit contract,
|
Promotion from disabled staging still requires an absent-pulse-width policy,
|
||||||
absent-pulse-width policy and exact machine-context validation already
|
image equivalence and exact machine-context validation already enforced by
|
||||||
enforced by preflight.
|
preflight.
|
||||||
- Physical controller behavior remains outside M1.
|
- Physical controller behavior remains outside M1.
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,9 @@ Source inspection found particularly relevant implementation details:
|
||||||
including speed, power, passes, offsets, raster interval/angle and dither;
|
including speed, power, passes, offsets, raster interval/angle and dither;
|
||||||
- `.lbdev` device profiles have a separate importer;
|
- `.lbdev` device profiles have a separate importer;
|
||||||
- laser steps and recipes serialize `frequency` in Hz; the laser-step UI
|
- laser steps and recipes serialize `frequency` in Hz; the laser-step UI
|
||||||
describes `pulse_width` as nanoseconds, while the Ruida encoder calls the
|
describes `pulse_width` as nanoseconds. Later LightBurn 2.1.03 evidence binds
|
||||||
same Ops value microseconds, so the unit contract requires clarification;
|
the JPT import seam to raw Hz/display kHz and direct ns without changing the
|
||||||
|
separate Ruida encoder question;
|
||||||
- the neutral `Ops` command set contains frequency and pulse-width commands;
|
- the neutral `Ops` command set contains frequency and pulse-width commands;
|
||||||
- binary rather than G-code drivers are supported through an `OpsEncoder`;
|
- binary rather than G-code drivers are supported through an `OpsEncoder`;
|
||||||
- driver state, cancel/hold, discovery, machine profiles, simulator and
|
- driver state, cancel/hold, discovery, machine profiles, simulator and
|
||||||
|
|
@ -224,8 +225,9 @@ stability and binary-wheel review.
|
||||||
## Finding 4: LightBurn migration is mostly an extension problem
|
## Finding 4: LightBurn migration is mostly an extension problem
|
||||||
|
|
||||||
Rayforge's existing LightBurn import removes the need to create a general
|
Rayforge's existing LightBurn import removes the need to create a general
|
||||||
`.lbrn2` parser. The focused task is to build a private, known-working fixture
|
`.lbrn2` parser. The private known-working fixture matrix now identifies the
|
||||||
matrix and identify which galvo-specific fields are currently dropped.
|
vector MOPA fields and units; the focused remaining work is packaged GUI
|
||||||
|
verification plus unsupported image and omitted-value behavior.
|
||||||
|
|
||||||
Likely gaps to test include:
|
Likely gaps to test include:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ operations continue to fail closed.
|
||||||
- Port-state commands are deliberately omitted because their machine-specific
|
- Port-state commands are deliberately omitted because their machine-specific
|
||||||
bit semantics can arm output and have not yet passed the hardware gate.
|
bit semantics can arm output and have not yet passed the hardware gate.
|
||||||
- The 110 mm coordinate mapping is synthetic and has no lens correction.
|
- The 110 mm coordinate mapping is synthetic and has no lens correction.
|
||||||
- Machine-specific bounds and the pulse-width unit contract still require
|
- The LightBurn 2.1.03 import unit contract is now evidenced, but certified
|
||||||
authoritative evidence; they are not guessed here.
|
machine-specific bounds and physical behavior remain unknown and unguessed.
|
||||||
- No hardware compatibility or safe marking behavior is established.
|
- No hardware compatibility or safe marking behavior is established.
|
||||||
|
|
||||||
## Decision impact
|
## Decision impact
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,13 @@ for JCZ use rather than receiving guessed defaults.
|
||||||
|
|
||||||
The external addon now supplies the narrow profile seam missing from Rayforge
|
The external addon now supplies the narrow profile seam missing from Rayforge
|
||||||
core. It recognizes only the explicit public synthetic profile identity,
|
core. It recognizes only the explicit public synthetic profile identity,
|
||||||
requires `JCZFiber`, positive work-area dimensions and
|
requires LightBurn 2.1.03, `JCZFiber`, the observed 110 x 110 mm work area,
|
||||||
`RequireFrameBeforeStart: true`, and associates the result only with
|
Q-pulse-width enablement, fiber-type code 3, a valid 1-4000 kHz profile range,
|
||||||
`PirateRayJCZNoDeviceDriver`. Frequency and `QPulseWidth` remain raw
|
and `RequireFrameBeforeStart: true`. It associates the result only with
|
||||||
observations with unverified serialized-unit conversion. Encoding remains
|
`PirateRayJCZNoDeviceDriver`. Cut-list frequency is normalized from serialized
|
||||||
blocked because the physical profile, lens/correction match, project
|
Hz to display kHz and `QPulseWidth` remains ns. Encoding stays blocked because
|
||||||
serialization and machine-specific limits are not yet verified.
|
physical identity, lens/correction match, certified limits and promotion
|
||||||
|
review are not verified.
|
||||||
|
|
||||||
The checked result is
|
The checked result is
|
||||||
`tests/fixtures/rayforge-lightburn-device-import.json`. Two runs produced the
|
`tests/fixtures/rayforge-lightburn-device-import.json`. Two runs produced the
|
||||||
|
|
@ -52,8 +53,8 @@ python scripts/spike_rayforge_lightburn_device_import.py C:\path\to\rayforge
|
||||||
|
|
||||||
## Next seam
|
## Next seam
|
||||||
|
|
||||||
The next extension is offline controller-list buffering and explicit safe
|
Offline controller-list buffering and explicit state ordering are complete.
|
||||||
state ordering. Cut-list recipes and project-layer `QPulseWidth` mapping remain
|
The next software gate is packaged Windows GTK interaction. Image-layer
|
||||||
gated until a known-working `.lbrn2` fixture establishes project serialization.
|
equivalence and absent `QPulseWidth` remain blocked rather than defaulted.
|
||||||
|
|
||||||
No USB enumeration, controller command, motion, framing or emission occurred.
|
No USB enumeration, controller command, motion, framing or emission occurred.
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,13 @@ square and preserved its layer name, geometry, 50 percent power, 250 mm/s
|
||||||
speed and one pass. LightBurn's observed `frequency` and
|
speed and one pass. LightBurn's observed `frequency` and
|
||||||
`overrideFrequency` elements also survive in the source asset's raw metadata.
|
`overrideFrequency` elements also survive in the source asset's raw metadata.
|
||||||
|
|
||||||
However, the importer does not map frequency into the workflow layer settings.
|
The pinned upstream importer does not map frequency into workflow settings; it
|
||||||
It maps only power, speed and passes from this fixture. The inspected Rayforge
|
maps only power, speed and passes from this fixture. The external addon now
|
||||||
LightBurn project fixtures and importer contain no evidence-backed mapping for
|
supplies that narrow mapping for LightBurn 2.1.03. Subsequent operator-verified
|
||||||
a MOPA pulse-width field. A later private LightBurn device-library export
|
evidence correlates raw `frequency=1000000` and `QPulseWidth=10` with visible
|
||||||
establishes the field name `QPulseWidth` and its nanosecond UI contract, but it
|
`1000 kHz` and `10 ns`, establishing the version-bound Hz/ns serialization
|
||||||
does not establish how that value is serialized in a `.lbrn2` project.
|
contract. The staged job nevertheless remains unsafe for JCZ encoding because
|
||||||
Consequently, the imported job is explicitly classified as unsafe for JCZ
|
machine, lens, correction and promotion review are still unresolved.
|
||||||
encoding rather than being completed with a guessed mapping.
|
|
||||||
|
|
||||||
Two runs produced identical output with SHA-256
|
Two runs produced identical output with SHA-256
|
||||||
`9ebce7b5b3388abfeed024e25f8f46001260bf814da19808bfc83fb6554fc26e`.
|
`9ebce7b5b3388abfeed024e25f8f46001260bf814da19808bfc83fb6554fc26e`.
|
||||||
|
|
@ -35,18 +34,18 @@ The golden result is `tests/fixtures/rayforge-lightburn-mopa-import.json`.
|
||||||
uses `QPulseWidth`; official LightBurn documentation defines Q-pulse width
|
uses `QPulseWidth`; official LightBurn documentation defines Q-pulse width
|
||||||
in nanoseconds. See the
|
in nanoseconds. See the
|
||||||
[device-export analysis](lightburn-device-exports-2026-10-19.md).
|
[device-export analysis](lightburn-device-exports-2026-10-19.md).
|
||||||
- **Unknown:** the exact `QPulseWidth` representation and omission/default
|
- **Observed after this spike:** three known-working galvo projects use the
|
||||||
behavior in a known-working galvo `.lbrn2` project.
|
case-sensitive `QPulseWidth` field. Missing fields remain absent; their
|
||||||
|
effective default is still unknown and is never invented.
|
||||||
- **Not inferred:** no pulse-width tag, unit, default or machine limit was
|
- **Not inferred:** no pulse-width tag, unit, default or machine limit was
|
||||||
invented for the public fixture.
|
invented for the public fixture.
|
||||||
|
|
||||||
## Decision impact
|
## Decision impact
|
||||||
|
|
||||||
Rayforge's existing LightBurn importer is useful for ordinary vector geometry
|
Rayforge's existing LightBurn importer remains useful for ordinary vector
|
||||||
and common process settings, but it does not yet satisfy M1's MOPA parameter
|
geometry and common process settings. The external addon now implements the
|
||||||
survival requirement. The narrowest likely extension is an importer mapping
|
narrow frequency/`QPulseWidth` mapping with a version- and evidence-bound unit
|
||||||
for frequency plus `QPulseWidth`, followed by an explicit unit conversion and
|
contract, stages values on disabled workflow steps, and retains all blockers.
|
||||||
validation gate. A private known-working LightBurn project is still needed to
|
Image equivalence, absent pulse width and physical compatibility remain gated.
|
||||||
establish project serialization before implementing that mapping.
|
|
||||||
|
|
||||||
No USB enumeration, controller command, motion or emission was attempted.
|
No USB enumeration, controller command, motion or emission was attempted.
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,15 @@ It does **not** yet establish:
|
||||||
- status, interlock, error, abort, and recovery behavior;
|
- status, interlock, error, abort, and recovery behavior;
|
||||||
- whether an apparently harmless query can move a galvo or arm emission;
|
- whether an apparently harmless query can move a galvo or arm emission;
|
||||||
- the effective pulse-width default for settings that omit `QPulseWidth`.
|
- the effective pulse-width default for settings that omit `QPulseWidth`.
|
||||||
|
- safe controller discovery, state and recovery behavior beyond the offline
|
||||||
|
mock evidence.
|
||||||
|
|
||||||
The later LightBurn export collection now establishes version `2.1.03`, a
|
The later LightBurn export collection now establishes version `2.1.03`, a
|
||||||
110 x 110 mm `JCZFiber`/`LibUSB` JPT profile, and the `QPulseWidth` device
|
110 x 110 mm `JCZFiber`/`LibUSB` JPT profile, and the `QPulseWidth` device
|
||||||
library field. See the
|
library field. See the
|
||||||
[device-export analysis](research/lightburn-device-exports-2026-10-19.md).
|
[device-export analysis](research/lightburn-device-exports-2026-10-19.md).
|
||||||
|
The later verified-project and live-UI collection establishes LightBurn 2.1.03
|
||||||
|
project frequency as serialized Hz/displayed kHz and `QPulseWidth` as ns.
|
||||||
|
|
||||||
Consequently, treating a vendor DLL or opaque profile as a callable black box
|
Consequently, treating a vendor DLL or opaque profile as a callable black box
|
||||||
would be premature and would make the safety boundary impossible to audit.
|
would be premature and would make the safety boundary impossible to audit.
|
||||||
|
|
@ -154,11 +158,12 @@ and explicit state-before-motion ordering. Upload, execution and physical
|
||||||
transport remain outside M1.
|
transport remain outside M1.
|
||||||
|
|
||||||
[The synthetic LightBurn MOPA import spike](research/rayforge-lightburn-mopa-import-spike.md)
|
[The synthetic LightBurn MOPA import spike](research/rayforge-lightburn-mopa-import-spike.md)
|
||||||
confirms that Rayforge preserves geometry, power, speed and passes. Three later
|
confirms that Rayforge preserves geometry, power, speed and passes. Three
|
||||||
operator-verified projects establish project-level `frequency` and
|
operator-verified LightBurn 2.1.03 projects plus the running UI establish the
|
||||||
`QPulseWidth` fields. The addon now stages complete vector settings into
|
serialized Hz/display-kHz relationship and direct ns `QPulseWidth` contract.
|
||||||
disabled Rayforge steps and rejects them at preflight until units, defaults and
|
The external addon maps complete vector settings into disabled workflow steps
|
||||||
machine context are explicitly approved.
|
with exact evidence provenance. Missing pulse width, image equivalence and
|
||||||
|
every hardware gate still fail closed.
|
||||||
|
|
||||||
ADR 0002 accepts the Rayforge-addon architecture with BalorMK as the pinned
|
ADR 0002 accepts the Rayforge-addon architecture with BalorMK as the pinned
|
||||||
behavioral oracle. Packaged Windows interaction is a release gate, not an open
|
behavioral oracle. Packaged Windows interaction is a release gate, not an open
|
||||||
|
|
@ -271,8 +276,8 @@ the M3 protocol draft are all present. Work now converges on two deliverables:
|
||||||
1. produce the pinned Windows Rayforge checkpoint on a reviewed external build
|
1. produce the pinned Windows Rayforge checkpoint on a reviewed external build
|
||||||
runner, verify the transferred artifact set and complete the no-device
|
runner, verify the transferred artifact set and complete the no-device
|
||||||
desktop interaction checklist;
|
desktop interaction checklist;
|
||||||
2. promote complete vector MOPA imports only after recording the explicit unit
|
2. promote complete vector MOPA imports only after an absent-pulse-width
|
||||||
contract, absent-pulse-width policy and exact machine-context validation.
|
policy, image equivalence and exact machine-context validation.
|
||||||
|
|
||||||
Exterior photographs, additional screenshots and speculative protocol research
|
Exterior photographs, additional screenshots and speculative protocol research
|
||||||
are not M1 blockers. Hardware work remains separately gated by M3.
|
are not M1 blockers. Hardware work remains separately gated by M3.
|
||||||
|
|
@ -291,10 +296,10 @@ Rayforge preserves the common name and work-area fields, but maps no
|
||||||
profile model. The fixture and regression test remain transport-free.
|
profile model. The fixture and regression test remain transport-free.
|
||||||
|
|
||||||
Item 3 now has a minimal external-addon seam: it recognizes only the synthetic
|
Item 3 now has a minimal external-addon seam: it recognizes only the synthetic
|
||||||
`JCZFiber` fixture with explicit provenance and frame-before-start, binds it to
|
LightBurn 2.1.03 `JCZFiber` fixture with explicit provenance, the observed
|
||||||
the no-device JCZ driver, preserves process values as raw observations with
|
110 x 110 mm/Q-pulse/frequency/frame contract, and binds it to the no-device
|
||||||
unit uncertainty, and keeps encoding disabled until the unit/default and exact
|
JCZ driver. Vector project values use the evidence-backed Hz/ns mapping, while
|
||||||
machine-context review passes.
|
encoding remains disabled pending physical compatibility and review.
|
||||||
|
|
||||||
Item 4 now covers transport-free fixed-size list buffering and an explicit
|
Item 4 now covers transport-free fixed-size list buffering and an explicit
|
||||||
state-before-motion order derived from pinned BalorMK. Port commands, upload,
|
state-before-motion order derived from pinned BalorMK. Port commands, upload,
|
||||||
|
|
|
||||||
|
|
@ -110,13 +110,17 @@ evidence for:
|
||||||
|
|
||||||
The remaining M1 release gates are deliberately narrow:
|
The remaining M1 release gates are deliberately narrow:
|
||||||
|
|
||||||
|
- vector project frequency and `QPulseWidth` now use a LightBurn 2.1.03,
|
||||||
|
evidence-bound Hz/ns contract; absent pulse width and image equivalence stay
|
||||||
|
blocked;
|
||||||
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
||||||
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
|
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
|
||||||
entry point fetches pinned source but never installs that runtime;
|
entry point fetches pinned source but never installs that runtime;
|
||||||
- three operator-verified projects establish project-level `frequency` and
|
- three operator-verified projects establish project-level `frequency` and
|
||||||
`QPulseWidth` serialization, and the offline bridge stages complete vector
|
`QPulseWidth` serialization, and the offline bridge stages complete vector
|
||||||
settings with output disabled; promotion still requires an explicit unit
|
settings with output disabled; promotion still requires an
|
||||||
contract, absent-pulse-width policy and exact machine-context validation;
|
absent-pulse-width policy, image equivalence and exact machine-context
|
||||||
|
validation;
|
||||||
- graphical workflow, lens correction and controller behavior remain unproven
|
- graphical workflow, lens correction and controller behavior remain unproven
|
||||||
at the physical boundary and belong to later gated milestones.
|
at the physical boundary and belong to later gated milestones.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,12 +60,18 @@ def run(source: Path, fixture: Path, addon: Path) -> dict[str, object]:
|
||||||
"fixture": fixture.name,
|
"fixture": fixture.name,
|
||||||
"fixture_provenance": "synthetic public data",
|
"fixture_provenance": "synthetic public data",
|
||||||
"raw_profile": {
|
"raw_profile": {
|
||||||
|
"app_version": document["AppVersion"],
|
||||||
"name": raw["DisplayName"],
|
"name": raw["DisplayName"],
|
||||||
"type": raw["Type"],
|
"type": raw["Type"],
|
||||||
"work_area_mm": [raw["Width"], raw["Height"]],
|
"work_area_mm": [raw["Width"], raw["Height"]],
|
||||||
"require_frame_before_start": settings[
|
"require_frame_before_start": settings[
|
||||||
"RequireFrameBeforeStart"
|
"RequireFrameBeforeStart"
|
||||||
],
|
],
|
||||||
|
"q_pulse_width_enabled": settings["Laser_QPulseWidthEnabled"],
|
||||||
|
"fiber_type_raw": settings["Laser_FiberType"],
|
||||||
|
"frequency_ui_range_khz": [
|
||||||
|
settings["Laser_MinFreq"], settings["Laser_MaxFreq"]
|
||||||
|
],
|
||||||
"cut_entry": {
|
"cut_entry": {
|
||||||
"frequency": cut_entry["frequency"],
|
"frequency": cut_entry["frequency"],
|
||||||
"QPulseWidth": cut_entry["QPulseWidth"],
|
"QPulseWidth": cut_entry["QPulseWidth"],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,11 @@ from typing import Any
|
||||||
|
|
||||||
DRIVER_NAME = "PirateRayJCZNoDeviceDriver"
|
DRIVER_NAME = "PirateRayJCZNoDeviceDriver"
|
||||||
SYNTHETIC_PROFILE_ID = "synthetic-jpt-mopa-110-v1"
|
SYNTHETIC_PROFILE_ID = "synthetic-jpt-mopa-110-v1"
|
||||||
|
EVIDENCE_APP_VERSION = "2.1.03"
|
||||||
|
EVIDENCE_WORK_AREA_MM = (110, 110)
|
||||||
|
EVIDENCE_FIBER_TYPE_RAW = 3
|
||||||
|
EVIDENCE_FIBER_TYPE_UI_LABEL = "IPG_YLPM"
|
||||||
|
FREQUENCY_HZ_PER_KHZ = 1000
|
||||||
|
|
||||||
|
|
||||||
class JCZProfileImportError(ValueError):
|
class JCZProfileImportError(ValueError):
|
||||||
|
|
@ -24,9 +29,10 @@ def _is_positive_number(value: Any) -> bool:
|
||||||
def import_lightburn_jcz_profile(document: dict[str, Any]) -> dict[str, Any]:
|
def import_lightburn_jcz_profile(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Recognize the public synthetic profile without enabling encoding.
|
"""Recognize the public synthetic profile without enabling encoding.
|
||||||
|
|
||||||
The returned process values remain serialized raw observations. This
|
The returned process values use the independently observed LightBurn
|
||||||
function does not infer LightBurn project fields, units, machine limits,
|
2.1.03 Hz/ns serialization contract. This function does not infer physical
|
||||||
lens correction, controller transport, or hardware compatibility.
|
machine identity, safe operating limits, lens correction, controller
|
||||||
|
transport, or hardware compatibility.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if document.get("FixtureProvenance") != (
|
if document.get("FixtureProvenance") != (
|
||||||
|
|
@ -35,6 +41,8 @@ def import_lightburn_jcz_profile(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
raise JCZProfileImportError("profile provenance is absent or unsupported")
|
raise JCZProfileImportError("profile provenance is absent or unsupported")
|
||||||
if document.get("SyntheticProfileId") != SYNTHETIC_PROFILE_ID:
|
if document.get("SyntheticProfileId") != SYNTHETIC_PROFILE_ID:
|
||||||
raise JCZProfileImportError("synthetic profile identity mismatch")
|
raise JCZProfileImportError("synthetic profile identity mismatch")
|
||||||
|
if document.get("AppVersion") != EVIDENCE_APP_VERSION:
|
||||||
|
raise JCZProfileImportError("unsupported LightBurn AppVersion")
|
||||||
|
|
||||||
devices = document.get("DeviceList")
|
devices = document.get("DeviceList")
|
||||||
if not isinstance(devices, list) or len(devices) != 1:
|
if not isinstance(devices, list) or len(devices) != 1:
|
||||||
|
|
@ -47,12 +55,24 @@ def import_lightburn_jcz_profile(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
height = device.get("Height")
|
height = device.get("Height")
|
||||||
if not all(_is_positive_number(value) for value in (width, height)):
|
if not all(_is_positive_number(value) for value in (width, height)):
|
||||||
raise JCZProfileImportError("positive work-area dimensions are required")
|
raise JCZProfileImportError("positive work-area dimensions are required")
|
||||||
|
if (width, height) != EVIDENCE_WORK_AREA_MM:
|
||||||
|
raise JCZProfileImportError("synthetic JPT profile must use 110 x 110 mm")
|
||||||
|
|
||||||
settings = device.get("Settings")
|
settings = device.get("Settings")
|
||||||
if not isinstance(settings, dict) or settings.get(
|
if not isinstance(settings, dict) or settings.get(
|
||||||
"RequireFrameBeforeStart"
|
"RequireFrameBeforeStart"
|
||||||
) is not True:
|
) is not True:
|
||||||
raise JCZProfileImportError("RequireFrameBeforeStart must be explicitly true")
|
raise JCZProfileImportError("RequireFrameBeforeStart must be explicitly true")
|
||||||
|
if settings.get("Laser_QPulseWidthEnabled") is not True:
|
||||||
|
raise JCZProfileImportError("Q-pulse-width control must be explicitly enabled")
|
||||||
|
if settings.get("Laser_FiberType") != EVIDENCE_FIBER_TYPE_RAW:
|
||||||
|
raise JCZProfileImportError("observed fiber-type code mismatch")
|
||||||
|
min_frequency = settings.get("Laser_MinFreq")
|
||||||
|
max_frequency = settings.get("Laser_MaxFreq")
|
||||||
|
if not all(
|
||||||
|
_is_positive_number(value) for value in (min_frequency, max_frequency)
|
||||||
|
) or min_frequency > max_frequency:
|
||||||
|
raise JCZProfileImportError("valid observed frequency range is required")
|
||||||
|
|
||||||
cut_list = device.get("CutList")
|
cut_list = device.get("CutList")
|
||||||
if not isinstance(cut_list, list) or not cut_list:
|
if not isinstance(cut_list, list) or not cut_list:
|
||||||
|
|
@ -70,17 +90,24 @@ def import_lightburn_jcz_profile(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
raise JCZProfileImportError(
|
raise JCZProfileImportError(
|
||||||
"frequency and QPulseWidth must be positive raw values"
|
"frequency and QPulseWidth must be positive raw values"
|
||||||
)
|
)
|
||||||
|
frequency_khz = frequency / FREQUENCY_HZ_PER_KHZ
|
||||||
|
if not min_frequency <= frequency_khz <= max_frequency:
|
||||||
|
raise JCZProfileImportError(
|
||||||
|
"cut-list frequency is outside the observed profile range"
|
||||||
|
)
|
||||||
preserved_entries.append(
|
preserved_entries.append(
|
||||||
{
|
{
|
||||||
"name": entry.get("Name", ""),
|
"name": entry.get("Name", ""),
|
||||||
"frequency": {
|
"frequency": {
|
||||||
"raw_value": frequency,
|
"raw_value": frequency,
|
||||||
"unit_status": "serialized-unit-conversion-unverified",
|
"serialized_unit": "Hz",
|
||||||
|
"value_hz": frequency,
|
||||||
|
"value_khz": frequency_khz,
|
||||||
},
|
},
|
||||||
"q_pulse_width": {
|
"q_pulse_width": {
|
||||||
"raw_value": pulse_width,
|
"raw_value": pulse_width,
|
||||||
"ui_unit_contract": "nanoseconds",
|
"serialized_unit": "ns",
|
||||||
"serialized_unit_conversion": "unverified",
|
"value_ns": pulse_width,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -91,20 +118,31 @@ def import_lightburn_jcz_profile(document: dict[str, Any]) -> dict[str, Any]:
|
||||||
"transport": None,
|
"transport": None,
|
||||||
"source": {
|
"source": {
|
||||||
"format": "LightBurn synthetic .lbdev",
|
"format": "LightBurn synthetic .lbdev",
|
||||||
|
"app_version": document["AppVersion"],
|
||||||
"provenance": document["FixtureProvenance"],
|
"provenance": document["FixtureProvenance"],
|
||||||
"backend": "JCZFiber",
|
"backend": "JCZFiber",
|
||||||
},
|
},
|
||||||
"work_area_mm": [float(width), float(height)],
|
"work_area_mm": [float(width), float(height)],
|
||||||
"safety": {"require_frame_before_start": True},
|
"safety": {"require_frame_before_start": True},
|
||||||
|
"observed_device_contract": {
|
||||||
|
"q_pulse_width_enabled": True,
|
||||||
|
"frequency_ui_range_khz": [min_frequency, max_frequency],
|
||||||
|
"frequency_serialized_unit": "Hz",
|
||||||
|
"q_pulse_width_serialized_unit": "ns",
|
||||||
|
"fiber_type_raw": EVIDENCE_FIBER_TYPE_RAW,
|
||||||
|
"fiber_type_ui_label": EVIDENCE_FIBER_TYPE_UI_LABEL,
|
||||||
|
"fiber_type_identity_status": "label-is-not-manufacturer-identity",
|
||||||
|
},
|
||||||
"process_metadata": {
|
"process_metadata": {
|
||||||
"status": "preserved-raw-not-mapped",
|
"status": "normalized-offline-not-authorized",
|
||||||
"cut_list": preserved_entries,
|
"cut_list": preserved_entries,
|
||||||
},
|
},
|
||||||
"encoding_allowed": False,
|
"encoding_allowed": False,
|
||||||
"blockers": [
|
"blockers": [
|
||||||
"synthetic profile is not physical machine identity",
|
"synthetic profile is not physical machine identity",
|
||||||
"lens and correction profile are unverified",
|
"lens and correction profile are unverified",
|
||||||
"LightBurn project QPulseWidth serialization is unverified",
|
"IPG_YLPM UI label is not manufacturer identity",
|
||||||
"machine-specific parameter limits are unverified",
|
"observed profile ranges are not certified safe machine limits",
|
||||||
|
"image settings with absent QPulseWidth remain unresolved",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,19 @@ import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
UNIT_CONTRACT = "frequency=Hz;QPulseWidth=ns"
|
UNIT_CONTRACT = "frequency=Hz;QPulseWidth=ns"
|
||||||
|
UNIT_CONTRACT_APP_VERSION = "2.1.03"
|
||||||
|
UNIT_CONTRACT_EVIDENCE = {
|
||||||
|
"status": "operator-verified-ui-project-correlation",
|
||||||
|
"lightburn_app_version": UNIT_CONTRACT_APP_VERSION,
|
||||||
|
"frequency_serialized_unit": "Hz",
|
||||||
|
"frequency_ui_unit": "kHz",
|
||||||
|
"frequency_serialized_units_per_ui_unit": 1000,
|
||||||
|
"q_pulse_width_serialized_unit": "ns",
|
||||||
|
"q_pulse_width_ui_unit": "ns",
|
||||||
|
"analysis_ref": (
|
||||||
|
"docs/research/lightburn-control-machine-observation-2026-10-24.md"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProjectImportError(ValueError):
|
class ProjectImportError(ValueError):
|
||||||
|
|
@ -44,9 +57,10 @@ def _layer_id(element: ET.Element) -> str | None:
|
||||||
def import_project_mopa(data: bytes, *, unit_contract: str | None = None) -> dict:
|
def import_project_mopa(data: bytes, *, unit_contract: str | None = None) -> dict:
|
||||||
"""Preserve observed XML fields and optionally normalize declared units.
|
"""Preserve observed XML fields and optionally normalize declared units.
|
||||||
|
|
||||||
The caller must supply an independently established serialized-unit contract.
|
The caller must opt into the independently established LightBurn 2.1.03
|
||||||
No unit is inferred from magnitude. Missing pulse width is never defaulted.
|
serialized-unit contract. No unit is inferred from magnitude. Missing pulse
|
||||||
Results are offline metadata, not executable Rayforge workflow settings.
|
width is never defaulted. Results are offline metadata, not executable
|
||||||
|
Rayforge workflow settings.
|
||||||
"""
|
"""
|
||||||
if unit_contract not in (None, UNIT_CONTRACT):
|
if unit_contract not in (None, UNIT_CONTRACT):
|
||||||
raise ProjectImportError("unsupported serialized-unit contract")
|
raise ProjectImportError("unsupported serialized-unit contract")
|
||||||
|
|
@ -62,6 +76,12 @@ def import_project_mopa(data: bytes, *, unit_contract: str | None = None) -> dic
|
||||||
raise ProjectImportError("invalid project XML") from exc
|
raise ProjectImportError("invalid project XML") from exc
|
||||||
if root.tag != "LightBurnProject" or root.get("FormatVersion") != "1":
|
if root.tag != "LightBurnProject" or root.get("FormatVersion") != "1":
|
||||||
raise ProjectImportError("unsupported project format")
|
raise ProjectImportError("unsupported project format")
|
||||||
|
app_version = root.get("AppVersion")
|
||||||
|
if unit_contract is not None and app_version != UNIT_CONTRACT_APP_VERSION:
|
||||||
|
raise ProjectImportError(
|
||||||
|
"unit contract requires LightBurn AppVersion "
|
||||||
|
+ UNIT_CONTRACT_APP_VERSION
|
||||||
|
)
|
||||||
layers = []
|
layers = []
|
||||||
layer_ids: set[str] = set()
|
layer_ids: set[str] = set()
|
||||||
for setting in root:
|
for setting in root:
|
||||||
|
|
@ -102,7 +122,10 @@ def import_project_mopa(data: bytes, *, unit_contract: str | None = None) -> dic
|
||||||
"normalized": normalized, "blockers": blockers})
|
"normalized": normalized, "blockers": blockers})
|
||||||
if not layers:
|
if not layers:
|
||||||
raise ProjectImportError("project has no process settings")
|
raise ProjectImportError("project has no process settings")
|
||||||
return {"layers": layers, "unit_contract": unit_contract, "transport": None,
|
evidence = dict(UNIT_CONTRACT_EVIDENCE) if unit_contract else None
|
||||||
|
return {"layers": layers, "unit_contract": unit_contract,
|
||||||
|
"unit_contract_evidence": evidence,
|
||||||
|
"lightburn_app_version": app_version, "transport": None,
|
||||||
"encoding_allowed": False, "hardware_use_allowed": False,
|
"encoding_allowed": False, "hardware_use_allowed": False,
|
||||||
"blockers": ["machine identity, correction and limits unverified",
|
"blockers": ["machine identity, correction and limits unverified",
|
||||||
"offline import seam is not wired to host workflow"]}
|
"offline import seam is not wired to host workflow"]}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ from collections.abc import Mapping
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .project_import import UNIT_CONTRACT, UNIT_CONTRACT_EVIDENCE
|
||||||
|
|
||||||
|
|
||||||
METADATA_KEY = "pirateray_import"
|
METADATA_KEY = "pirateray_import"
|
||||||
|
|
||||||
|
|
@ -59,6 +61,8 @@ def stage_mopa_workflow_steps(
|
||||||
isinstance(item, str) and item for item in global_blockers
|
isinstance(item, str) and item for item in global_blockers
|
||||||
):
|
):
|
||||||
raise WorkflowImportError("global blockers are missing")
|
raise WorkflowImportError("global blockers are missing")
|
||||||
|
unit_contract = imported.get("unit_contract")
|
||||||
|
unit_contract_evidence = imported.get("unit_contract_evidence")
|
||||||
|
|
||||||
plans = []
|
plans = []
|
||||||
step_identities: set[int] = set()
|
step_identities: set[int] = set()
|
||||||
|
|
@ -86,6 +90,10 @@ def stage_mopa_workflow_steps(
|
||||||
apply_values = normalized is not None and not layer_blockers
|
apply_values = normalized is not None and not layer_blockers
|
||||||
frequency = pulse_width = None
|
frequency = pulse_width = None
|
||||||
if apply_values:
|
if apply_values:
|
||||||
|
if unit_contract != UNIT_CONTRACT:
|
||||||
|
raise WorkflowImportError("normalized units lack the supported contract")
|
||||||
|
if unit_contract_evidence != UNIT_CONTRACT_EVIDENCE:
|
||||||
|
raise WorkflowImportError("normalized units lack exact evidence provenance")
|
||||||
if not callable(getattr(step, "set_frequency", None)) or not callable(
|
if not callable(getattr(step, "set_frequency", None)) or not callable(
|
||||||
getattr(step, "set_pulse_width", None)
|
getattr(step, "set_pulse_width", None)
|
||||||
):
|
):
|
||||||
|
|
@ -114,6 +122,8 @@ def stage_mopa_workflow_steps(
|
||||||
"source_layer_id": layer_id,
|
"source_layer_id": layer_id,
|
||||||
"raw": deepcopy(layer.get("raw")),
|
"raw": deepcopy(layer.get("raw")),
|
||||||
"normalized": deepcopy(layer.get("normalized")),
|
"normalized": deepcopy(layer.get("normalized")),
|
||||||
|
"unit_contract": unit_contract,
|
||||||
|
"unit_contract_evidence": deepcopy(unit_contract_evidence),
|
||||||
"blockers": blockers,
|
"blockers": blockers,
|
||||||
"encoding_allowed": False,
|
"encoding_allowed": False,
|
||||||
"hardware_use_allowed": False,
|
"hardware_use_allowed": False,
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,49 @@
|
||||||
"blockers": [
|
"blockers": [
|
||||||
"synthetic profile is not physical machine identity",
|
"synthetic profile is not physical machine identity",
|
||||||
"lens and correction profile are unverified",
|
"lens and correction profile are unverified",
|
||||||
"LightBurn project QPulseWidth serialization is unverified",
|
"IPG_YLPM UI label is not manufacturer identity",
|
||||||
"machine-specific parameter limits are unverified"
|
"observed profile ranges are not certified safe machine limits",
|
||||||
|
"image settings with absent QPulseWidth remain unresolved"
|
||||||
],
|
],
|
||||||
"driver": "PirateRayJCZNoDeviceDriver",
|
"driver": "PirateRayJCZNoDeviceDriver",
|
||||||
"encoding_allowed": false,
|
"encoding_allowed": false,
|
||||||
|
"observed_device_contract": {
|
||||||
|
"fiber_type_identity_status": "label-is-not-manufacturer-identity",
|
||||||
|
"fiber_type_raw": 3,
|
||||||
|
"fiber_type_ui_label": "IPG_YLPM",
|
||||||
|
"frequency_serialized_unit": "Hz",
|
||||||
|
"frequency_ui_range_khz": [
|
||||||
|
1,
|
||||||
|
4000
|
||||||
|
],
|
||||||
|
"q_pulse_width_enabled": true,
|
||||||
|
"q_pulse_width_serialized_unit": "ns"
|
||||||
|
},
|
||||||
"process_metadata": {
|
"process_metadata": {
|
||||||
"cut_list": [
|
"cut_list": [
|
||||||
{
|
{
|
||||||
"frequency": {
|
"frequency": {
|
||||||
"raw_value": 40000,
|
"raw_value": 40000,
|
||||||
"unit_status": "serialized-unit-conversion-unverified"
|
"serialized_unit": "Hz",
|
||||||
|
"value_hz": 40000,
|
||||||
|
"value_khz": 40.0
|
||||||
},
|
},
|
||||||
"name": "Synthetic MOPA entry",
|
"name": "Synthetic MOPA entry",
|
||||||
"q_pulse_width": {
|
"q_pulse_width": {
|
||||||
"raw_value": 100,
|
"raw_value": 100,
|
||||||
"serialized_unit_conversion": "unverified",
|
"serialized_unit": "ns",
|
||||||
"ui_unit_contract": "nanoseconds"
|
"value_ns": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "preserved-raw-not-mapped"
|
"status": "normalized-offline-not-authorized"
|
||||||
},
|
},
|
||||||
"profile_id": "synthetic-jpt-mopa-110-v1",
|
"profile_id": "synthetic-jpt-mopa-110-v1",
|
||||||
"safety": {
|
"safety": {
|
||||||
"require_frame_before_start": true
|
"require_frame_before_start": true
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
|
"app_version": "2.1.03",
|
||||||
"backend": "JCZFiber",
|
"backend": "JCZFiber",
|
||||||
"format": "LightBurn synthetic .lbdev",
|
"format": "LightBurn synthetic .lbdev",
|
||||||
"provenance": "synthetic public data; not a machine export"
|
"provenance": "synthetic public data; not a machine export"
|
||||||
|
|
@ -61,11 +77,18 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"raw_profile": {
|
"raw_profile": {
|
||||||
|
"app_version": "2.1.03",
|
||||||
"cut_entry": {
|
"cut_entry": {
|
||||||
"QPulseWidth": 100,
|
"QPulseWidth": 100,
|
||||||
"frequency": 40000
|
"frequency": 40000
|
||||||
},
|
},
|
||||||
|
"fiber_type_raw": 3,
|
||||||
|
"frequency_ui_range_khz": [
|
||||||
|
1,
|
||||||
|
4000
|
||||||
|
],
|
||||||
"name": "Synthetic JCZ MOPA",
|
"name": "Synthetic JCZ MOPA",
|
||||||
|
"q_pulse_width_enabled": true,
|
||||||
"require_frame_before_start": true,
|
"require_frame_before_start": true,
|
||||||
"type": "JCZFiber",
|
"type": "JCZFiber",
|
||||||
"work_area_mm": [
|
"work_area_mm": [
|
||||||
|
|
|
||||||
7
tests/fixtures/synthetic-jcz-device.lbdev
vendored
7
tests/fixtures/synthetic-jcz-device.lbdev
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"FixtureProvenance": "synthetic public data; not a machine export",
|
"FixtureProvenance": "synthetic public data; not a machine export",
|
||||||
"SyntheticProfileId": "synthetic-jpt-mopa-110-v1",
|
"SyntheticProfileId": "synthetic-jpt-mopa-110-v1",
|
||||||
|
"AppVersion": "2.1.03",
|
||||||
"DeviceList": [
|
"DeviceList": [
|
||||||
{
|
{
|
||||||
"DisplayName": "Synthetic JCZ MOPA",
|
"DisplayName": "Synthetic JCZ MOPA",
|
||||||
|
|
@ -10,7 +11,11 @@
|
||||||
"Height": 110,
|
"Height": 110,
|
||||||
"HomeOnStartup": false,
|
"HomeOnStartup": false,
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"RequireFrameBeforeStart": true
|
"RequireFrameBeforeStart": true,
|
||||||
|
"Laser_QPulseWidthEnabled": true,
|
||||||
|
"Laser_FiberType": 3,
|
||||||
|
"Laser_MinFreq": 1,
|
||||||
|
"Laser_MaxFreq": 4000
|
||||||
},
|
},
|
||||||
"CutList": [
|
"CutList": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ from pathlib import Path
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "spikes/rayforge-jcz-no-device-addon"))
|
sys.path.insert(0, str(Path(__file__).parents[1] / "spikes/rayforge-jcz-no-device-addon"))
|
||||||
from pirate_ray_jcz.project_import import (
|
from pirate_ray_jcz.project_import import (
|
||||||
ProjectImportError, UNIT_CONTRACT, import_project_mopa,
|
ProjectImportError, UNIT_CONTRACT, UNIT_CONTRACT_EVIDENCE,
|
||||||
|
import_project_mopa,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def project(fields, tag="CutSetting", kind="Scan", layer_id="0"):
|
def project(fields, tag="CutSetting", kind="Scan", layer_id="0"):
|
||||||
return (f'<LightBurnProject FormatVersion="1"><{tag} type="{kind}">'
|
return (f'<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
|
f'<{tag} type="{kind}">'
|
||||||
f'<index Value="{layer_id}"/>{fields}</{tag}></LightBurnProject>').encode()
|
f'<index Value="{layer_id}"/>{fields}</{tag}></LightBurnProject>').encode()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,6 +24,8 @@ class ProjectMopaTests(unittest.TestCase):
|
||||||
self.assertEqual(result["layers"][0]["normalized"], {
|
self.assertEqual(result["layers"][0]["normalized"], {
|
||||||
"frequency_hz": 125000, "frequency_khz": 125, "pulse_width_ns": 25,
|
"frequency_hz": 125000, "frequency_khz": 125, "pulse_width_ns": 25,
|
||||||
})
|
})
|
||||||
|
self.assertEqual(result["unit_contract_evidence"], UNIT_CONTRACT_EVIDENCE)
|
||||||
|
self.assertEqual(result["lightburn_app_version"], "2.1.03")
|
||||||
self.assertFalse(result["encoding_allowed"])
|
self.assertFalse(result["encoding_allowed"])
|
||||||
self.assertFalse(result["hardware_use_allowed"])
|
self.assertFalse(result["hardware_use_allowed"])
|
||||||
self.assertIsNone(result["transport"])
|
self.assertIsNone(result["transport"])
|
||||||
|
|
@ -36,6 +40,23 @@ class ProjectMopaTests(unittest.TestCase):
|
||||||
with self.assertRaises(ProjectImportError):
|
with self.assertRaises(ProjectImportError):
|
||||||
import_project_mopa(project(FIELDS), unit_contract="kHz/us")
|
import_project_mopa(project(FIELDS), unit_contract="kHz/us")
|
||||||
|
|
||||||
|
def test_contract_is_version_bound_and_matches_verified_ui_correlation(self):
|
||||||
|
verified = '<frequency Value="1000000"/><QPulseWidth Value="10"/>'
|
||||||
|
layer = import_project_mopa(
|
||||||
|
project(verified), unit_contract=UNIT_CONTRACT
|
||||||
|
)["layers"][0]
|
||||||
|
self.assertEqual(layer["normalized"], {
|
||||||
|
"frequency_hz": 1000000,
|
||||||
|
"frequency_khz": 1000,
|
||||||
|
"pulse_width_ns": 10,
|
||||||
|
})
|
||||||
|
for app_version in ("2.0.00", ""):
|
||||||
|
data = project(verified).replace(b'AppVersion="2.1.03"',
|
||||||
|
f'AppVersion="{app_version}"'.encode())
|
||||||
|
with self.subTest(app_version=app_version):
|
||||||
|
with self.assertRaisesRegex(ProjectImportError, "AppVersion 2.1.03"):
|
||||||
|
import_project_mopa(data, unit_contract=UNIT_CONTRACT)
|
||||||
|
|
||||||
def test_missing_fields_remain_absent(self):
|
def test_missing_fields_remain_absent(self):
|
||||||
for fields in ('<frequency Value="125000"/>', '<QPulseWidth Value="25"/>', ''):
|
for fields in ('<frequency Value="125000"/>', '<QPulseWidth Value="25"/>', ''):
|
||||||
with self.subTest(fields=fields):
|
with self.subTest(fields=fields):
|
||||||
|
|
@ -73,7 +94,7 @@ class ProjectMopaTests(unittest.TestCase):
|
||||||
def test_layer_indices_are_normalized_and_must_be_unique(self):
|
def test_layer_indices_are_normalized_and_must_be_unique(self):
|
||||||
layer = import_project_mopa(project(FIELDS, layer_id="007"))["layers"][0]
|
layer = import_project_mopa(project(FIELDS, layer_id="007"))["layers"][0]
|
||||||
self.assertEqual(layer["source_layer_id"], "7")
|
self.assertEqual(layer["source_layer_id"], "7")
|
||||||
duplicate = ('<LightBurnProject FormatVersion="1">'
|
duplicate = ('<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
||||||
'<CutSetting type="Scan"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Scan"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
||||||
'</LightBurnProject>').encode()
|
'</LightBurnProject>').encode()
|
||||||
|
|
@ -81,7 +102,7 @@ class ProjectMopaTests(unittest.TestCase):
|
||||||
import_project_mopa(duplicate)
|
import_project_mopa(duplicate)
|
||||||
|
|
||||||
def test_multiple_layers_preserve_order_and_missing_pulse(self):
|
def test_multiple_layers_preserve_order_and_missing_pulse(self):
|
||||||
data = ('<LightBurnProject FormatVersion="1">'
|
data = ('<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
'<CutSetting type="Cut"><index Value="0"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="0"/>'+FIELDS+'</CutSetting>'
|
||||||
'<CutSetting_Img type="Image"><index Value="1"/><frequency Value="125000"/>'
|
'<CutSetting_Img type="Image"><index Value="1"/><frequency Value="125000"/>'
|
||||||
'</CutSetting_Img></LightBurnProject>').encode()
|
'</CutSetting_Img></LightBurnProject>').encode()
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ class Step:
|
||||||
|
|
||||||
|
|
||||||
def imported(fields, layer_id="3", kind="Cut", tag="CutSetting"):
|
def imported(fields, layer_id="3", kind="Cut", tag="CutSetting"):
|
||||||
data = (f'<LightBurnProject FormatVersion="1"><{tag} type="{kind}">'
|
data = (f'<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
|
f'<{tag} type="{kind}">'
|
||||||
f'<index Value="{layer_id}"/>{fields}</{tag}></LightBurnProject>').encode()
|
f'<index Value="{layer_id}"/>{fields}</{tag}></LightBurnProject>').encode()
|
||||||
return import_project_mopa(data, unit_contract=UNIT_CONTRACT)
|
return import_project_mopa(data, unit_contract=UNIT_CONTRACT)
|
||||||
|
|
||||||
|
|
@ -50,6 +51,10 @@ class WorkflowImportTests(unittest.TestCase):
|
||||||
self.assertEqual(gate["status"], "staged-review-required")
|
self.assertEqual(gate["status"], "staged-review-required")
|
||||||
self.assertFalse(gate["encoding_allowed"])
|
self.assertFalse(gate["encoding_allowed"])
|
||||||
self.assertTrue(gate["blockers"])
|
self.assertTrue(gate["blockers"])
|
||||||
|
self.assertEqual(gate["unit_contract"], UNIT_CONTRACT)
|
||||||
|
self.assertEqual(
|
||||||
|
gate["unit_contract_evidence"], imported(FIELDS)["unit_contract_evidence"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_missing_pulse_and_image_mode_do_not_apply_parameters(self):
|
def test_missing_pulse_and_image_mode_do_not_apply_parameters(self):
|
||||||
for result in (
|
for result in (
|
||||||
|
|
@ -74,7 +79,7 @@ class WorkflowImportTests(unittest.TestCase):
|
||||||
stage_mopa_workflow_steps(imported(FIELDS), {"3": step})
|
stage_mopa_workflow_steps(imported(FIELDS), {"3": step})
|
||||||
|
|
||||||
two_layers = import_project_mopa(
|
two_layers = import_project_mopa(
|
||||||
('<LightBurnProject FormatVersion="1">'
|
('<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
||||||
'<CutSetting type="Cut"><index Value="2"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="2"/>'+FIELDS+'</CutSetting>'
|
||||||
'</LightBurnProject>').encode(), unit_contract=UNIT_CONTRACT)
|
'</LightBurnProject>').encode(), unit_contract=UNIT_CONTRACT)
|
||||||
|
|
@ -84,7 +89,7 @@ class WorkflowImportTests(unittest.TestCase):
|
||||||
|
|
||||||
def test_preflight_failure_does_not_mutate_any_step(self):
|
def test_preflight_failure_does_not_mutate_any_step(self):
|
||||||
result = import_project_mopa(
|
result = import_project_mopa(
|
||||||
('<LightBurnProject FormatVersion="1">'
|
('<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
||||||
'<CutSetting type="Cut"><index Value="2"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="2"/>'+FIELDS+'</CutSetting>'
|
||||||
'</LightBurnProject>').encode(), unit_contract=UNIT_CONTRACT)
|
'</LightBurnProject>').encode(), unit_contract=UNIT_CONTRACT)
|
||||||
|
|
@ -94,9 +99,19 @@ class WorkflowImportTests(unittest.TestCase):
|
||||||
self.assertTrue(first.visible)
|
self.assertTrue(first.visible)
|
||||||
self.assertEqual((first.frequency, first.pulse_width), (1, 2))
|
self.assertEqual((first.frequency, first.pulse_width), (1, 2))
|
||||||
|
|
||||||
|
def test_tampered_unit_evidence_is_rejected_before_mutation(self):
|
||||||
|
result = imported(FIELDS)
|
||||||
|
result["unit_contract_evidence"]["frequency_ui_unit"] = "MHz"
|
||||||
|
step = Step()
|
||||||
|
with self.assertRaisesRegex(WorkflowImportError, "exact evidence provenance"):
|
||||||
|
stage_mopa_workflow_steps(result, {"3": step})
|
||||||
|
self.assertTrue(step.visible)
|
||||||
|
self.assertEqual((step.frequency, step.pulse_width), (1, 2))
|
||||||
|
self.assertEqual(step.extra, {"preserved": True})
|
||||||
|
|
||||||
def test_setter_failure_rolls_back_every_step(self):
|
def test_setter_failure_rolls_back_every_step(self):
|
||||||
result = import_project_mopa(
|
result = import_project_mopa(
|
||||||
('<LightBurnProject FormatVersion="1">'
|
('<LightBurnProject AppVersion="2.1.03" FormatVersion="1">'
|
||||||
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="1"/>'+FIELDS+'</CutSetting>'
|
||||||
'<CutSetting type="Cut"><index Value="2"/>'+FIELDS+'</CutSetting>'
|
'<CutSetting type="Cut"><index Value="2"/>'+FIELDS+'</CutSetting>'
|
||||||
'</LightBurnProject>').encode(), unit_contract=UNIT_CONTRACT)
|
'</LightBurnProject>').encode(), unit_contract=UNIT_CONTRACT)
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,20 @@ class RayforgeLightBurnDeviceImportSpikeTests(unittest.TestCase):
|
||||||
self.assertEqual("PirateRayJCZNoDeviceDriver", seam["driver"])
|
self.assertEqual("PirateRayJCZNoDeviceDriver", seam["driver"])
|
||||||
self.assertIsNone(seam["transport"])
|
self.assertIsNone(seam["transport"])
|
||||||
self.assertFalse(seam["encoding_allowed"])
|
self.assertFalse(seam["encoding_allowed"])
|
||||||
self.assertEqual("preserved-raw-not-mapped", seam["process_metadata"]["status"])
|
self.assertEqual("normalized-offline-not-authorized", seam["process_metadata"]["status"])
|
||||||
entry = seam["process_metadata"]["cut_list"][0]
|
entry = seam["process_metadata"]["cut_list"][0]
|
||||||
self.assertEqual(40000, entry["frequency"]["raw_value"])
|
self.assertEqual(40000, entry["frequency"]["raw_value"])
|
||||||
|
self.assertEqual(40000, entry["frequency"]["value_hz"])
|
||||||
|
self.assertEqual(40, entry["frequency"]["value_khz"])
|
||||||
self.assertEqual(100, entry["q_pulse_width"]["raw_value"])
|
self.assertEqual(100, entry["q_pulse_width"]["raw_value"])
|
||||||
self.assertEqual("unverified", entry["q_pulse_width"]["serialized_unit_conversion"])
|
self.assertEqual(100, entry["q_pulse_width"]["value_ns"])
|
||||||
|
contract = seam["observed_device_contract"]
|
||||||
|
self.assertEqual([1, 4000], contract["frequency_ui_range_khz"])
|
||||||
|
self.assertEqual("IPG_YLPM", contract["fiber_type_ui_label"])
|
||||||
|
self.assertEqual(
|
||||||
|
"label-is-not-manufacturer-identity",
|
||||||
|
contract["fiber_type_identity_status"],
|
||||||
|
)
|
||||||
|
|
||||||
def test_addon_seam_rejects_wrong_backend_and_missing_frame_gate(self):
|
def test_addon_seam_rejects_wrong_backend_and_missing_frame_gate(self):
|
||||||
source = json.loads(SOURCE.read_text(encoding="utf-8"))
|
source = json.loads(SOURCE.read_text(encoding="utf-8"))
|
||||||
|
|
@ -86,6 +95,32 @@ class RayforgeLightBurnDeviceImportSpikeTests(unittest.TestCase):
|
||||||
with self.assertRaises(JCZProfileImportError):
|
with self.assertRaises(JCZProfileImportError):
|
||||||
import_lightburn_jcz_profile(ambiguous_frequency)
|
import_lightburn_jcz_profile(ambiguous_frequency)
|
||||||
|
|
||||||
|
for frequency in (500, 4_001_000):
|
||||||
|
outside_range = deepcopy(source)
|
||||||
|
outside_range["DeviceList"][0]["CutList"][0]["frequency"] = frequency
|
||||||
|
with self.subTest(frequency=frequency):
|
||||||
|
with self.assertRaisesRegex(JCZProfileImportError, "outside"):
|
||||||
|
import_lightburn_jcz_profile(outside_range)
|
||||||
|
|
||||||
|
def test_observed_profile_contract_fails_closed_on_mismatch(self):
|
||||||
|
source = json.loads(SOURCE.read_text(encoding="utf-8"))
|
||||||
|
mutations = (
|
||||||
|
("wrong version", lambda doc: doc.update(AppVersion="2.2.00")),
|
||||||
|
("wrong width", lambda doc: doc["DeviceList"][0].update(Width=300)),
|
||||||
|
("q pulse disabled", lambda doc: doc["DeviceList"][0]["Settings"].update(
|
||||||
|
Laser_QPulseWidthEnabled=False)),
|
||||||
|
("wrong fiber code", lambda doc: doc["DeviceList"][0]["Settings"].update(
|
||||||
|
Laser_FiberType=2)),
|
||||||
|
("reversed frequency range", lambda doc: doc["DeviceList"][0]["Settings"].update(
|
||||||
|
Laser_MinFreq=4000, Laser_MaxFreq=1)),
|
||||||
|
)
|
||||||
|
for label, mutate in mutations:
|
||||||
|
candidate = deepcopy(source)
|
||||||
|
mutate(candidate)
|
||||||
|
with self.subTest(label=label):
|
||||||
|
with self.assertRaises(JCZProfileImportError):
|
||||||
|
import_lightburn_jcz_profile(candidate)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue