diff --git a/README.md b/README.md index eeeb585..a0fbdae 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,21 @@ Phase 0 has established a reproducible evidence and research baseline: correction data, material libraries, and relevant configuration files; - the physical controller has been passively identified by Windows as `USBLMCV4`, USB `9588:9899`, with device revision descriptor `0005`; +- LightBurn `2.1.03` exports now confirm the JPT `JCZFiber`/`LibUSB` profile, + 110 x 110 mm work area, Q-pulse-width support, and the exact serialized + setting name `QPulseWidth`; +- the Eduard X1550 export confirms a Ruida/Serial profile and 1500 x 1000 mm + work area; - the development computer has no compatible controller driver installed; - no USB endpoint has been opened and no controller command, galvo movement, framing, marking, or laser output has occurred; - the public tests, provenance checks, leak guard, and clean-clone validation pass. -Phase 0 remains open while we collect the LightBurn bundle, preferences, -known-working projects, device screenshots, machine photos, and operator notes -directly from the actual control computer. +Phase 0 remains open while we collect known-working LightBurn projects, device +screenshots, the closed-application preferences folder, machine photos, and +operator notes directly from the actual control computer. The bundle and +exported preferences are already preserved and analyzed. ## The important discovery @@ -55,16 +61,17 @@ BalorMK. This remains a proposed decision until the offline proofs in ## Next steps -The next work is deliberately offline and hardware-free: +The initial offline architecture spikes have passed. The next work remains +hardware-free: -1. run a synthetic MOPA job through MeerK40t's mock controller; -2. prove that an external Rayforge addon can register a binary no-device - driver on Windows; -3. translate a small Rayforge operation stream into a deterministic JCZ trace - and compare it with BalorMK; -4. test Rayforge's existing LightBurn import against galvo/MOPA fixtures; -5. accept, revise, or reject the proposed reuse architecture from evidence; -6. write and review a separate hardware protocol before any live controller +1. collect known-working `.lbrn2` projects and the remaining screenshots, + photos, preferences folder and operator notes; +2. extend the synthetic import tests with the now-observed `QPulseWidth` + evidence, without assuming its `.lbrn2` representation; +3. define the minimum Rayforge JCZ device-profile/import extension and finish + controller-list buffering and safe state ordering offline; +4. accept, revise, or reject the proposed reuse architecture from evidence; +5. write and review a separate hardware protocol before any live controller communication. An independent desktop application is the fallback only if both existing @@ -86,6 +93,7 @@ project workspace or development destination. - [Open-source reuse landscape](docs/research/open-source-landscape.md) - [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md) - [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md) +- [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md) - [Sanitized machine profile](docs/research/machine-profile.yaml) - [110/300 configuration comparison](docs/research/config-comparison.md) - [Data handling policy](docs/data-handling.md) diff --git a/docs/research/lightburn-device-exports-2026-10-19.md b/docs/research/lightburn-device-exports-2026-10-19.md new file mode 100644 index 0000000..5dc8a14 --- /dev/null +++ b/docs/research/lightburn-device-exports-2026-10-19.md @@ -0,0 +1,114 @@ +# LightBurn device-export analysis + +- Date: 2026-10-19 +- Scope: four user-provided LightBurn exports, analyzed offline +- LightBurn version recorded by all three bundle manifests: `2.1.03` +- Safety boundary: no executable, driver, USB endpoint or machine command was + used + +## Evidence + +Raw exports remain in PrivateStash. This public record contains hashes, +structure and independently derived findings only. + +| Artifact ID | Size | Role | +| --- | ---: | --- | +| `sha256-e16fdb0aa7c374c4c94d` | 20,309 bytes | Complete bundle containing three device profiles, preferences and libraries | +| `sha256-fc6cf52f4ef815dd5037` | 5,720 bytes | Eduard device bundle | +| `sha256-88c762adcb8e9693c913` | 97,356 bytes | Exported LightBurn preferences | +| `sha256-d422e9886e29ed666c07` | 8,798 bytes | JPT MOPA device bundle | + +Every digest and size is recorded in +[`evidence/public-lightburn-exports.jsonl`](../../evidence/public-lightburn-exports.jsonl). +Private paths, device GUIDs and local account data are intentionally omitted. + +## Bundle structure + +The complete bundle has eight entries: preferences, material-test presets, +three LightBurn device profiles, two material libraries and a bundle manifest. +Each device-specific bundle contains preferences, one device profile and a +manifest. All manifests declare bundle format `1.0` and LightBurn `2.1.03`. + +## Device findings + +| Display name | LightBurn backend | Connection | Work area | Attribution | +| --- | --- | --- | ---: | --- | +| `100 JPT E2 MOPA` | `JCZFiber` | `LibUSB` | 110 × 110 mm | Focus machine | +| `Eduard X1550` | `Ruida` | `Serial` | 1500 × 1000 mm | Makerspace CO2 machine | +| `Ruida 644XS` | `Ruida` | `CDM` | 500 × 300 mm | Unassigned; physical machine unknown | + +The preferences contain four device records because the unassigned Ruida +profile occurs twice. This is not evidence for a fourth physical machine, and +the 500 × 300 mm profile must not be attributed to the Bodor or any other +machine without independent confirmation. + +The JPT profile additionally records: + +- a numeric laser type of `1` and fiber type of `3`; +- enabled Q-pulse-width control; +- profile frequency bounds stored as `1` and `4000`; +- `RequireFrameBeforeStart` enabled; +- 60 cut-list entries. + +Numeric enums are preserved as observations. Their semantic labels are not +guessed from the numbers alone. + +## MOPA process-setting evidence + +Across the 60 exported cut-list entries: + +- 35 contain `frequency`; +- 25 contain `QPulseWidth`; +- all 25 pulse-width entries also contain frequency; +- observed raw frequency values range from 30,000 to 1,000,000; +- observed `QPulseWidth` values range from 2 to 180. + +LightBurn's official galvo cut-setting documentation presents frequency in +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 +field without an explicit conversion contract. The exact field name and unit +are now evidenced for a device-library export, but the representation inside +a `.lbrn2` project remains unconfirmed. + +These observed recipe values and profile fields are **not certified hardware +limits or safe operating parameters**. They must not be used to probe the +machine. LightBurn likewise warns that incorrect galvo source settings can +damage a laser. + +Sources: + +- [LightBurn galvo-specific cut settings](https://docs.lightburnsoftware.com/2.2/Reference/CutSettingsEditor/GalvoSpecificCutSettings/) +- [LightBurn galvo source settings](https://docs.lightburnsoftware.com/latest/Reference/DeviceSettings/GalvoPorts/) + +## Reuse impact + +Rayforge's current LightBurn **project** importer retains raw frequency +metadata but maps neither frequency nor pulse width into workflow settings. +Its separate LightBurn **device** importer recognizes common profile fields, +but its driver map does not cover this observed `LibUSB`/`JCZFiber` pairing +and it does not map the JPT-specific settings or cut list. + +The narrowest useful implementation is therefore an extension rather than a +new general parser: + +1. add evidence-backed JCZ device-profile recognition; +2. preserve source units and raw values at the import boundary; +3. map frequency and `QPulseWidth` only after a real `.lbrn2` fixture confirms + project serialization; +4. validate imported values against a separately verified machine profile; +5. fail closed when units, profile identity or ranges are unknown. + +## Remaining collection gaps + +The exports settle the exact LightBurn release and the two named device +profiles. 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; +- mounted-lens, connection, startup and interlock notes; +- the stock driver's identity and version on the control computer. + +An About screenshot is still useful as visual corroboration, but the bundle +manifests already establish version `2.1.03`. diff --git a/docs/research/machine-profile.yaml b/docs/research/machine-profile.yaml index 79ac139..90197da 100644 --- a/docs/research/machine-profile.yaml +++ b/docs/research/machine-profile.yaml @@ -19,9 +19,14 @@ optics: - width: 300 height: 300 lightburn: - version: null + version: 2.1.03 build: null - status: pending-collection + device_name: 100 JPT E2 MOPA + backend: JCZFiber + connection: LibUSB + q_pulse_width_enabled: true + require_frame_before_start: true + status: bundle-export-observed usb: vid: "9588" pid: "9899" @@ -45,8 +50,16 @@ evidence: - artifact_id: sha256-ebfe631958b9522533ff sha256: ebfe631958b9522533ffb1e78c8961073e3549039053ea0d56e4c82cfe8bff83 role: jcz-windows-driver-metadata + - artifact_id: sha256-d422e9886e29ed666c07 + sha256: d422e9886e29ed666c077a0e7bab9a6c3d53fc938365b7b773d79c529b55c0f0 + role: lightburn-jpt-device-bundle + - artifact_id: sha256-88c762adcb8e9693c913 + sha256: 88c762adcb8e9693c9132473a9d90c69eaf768b96f12e8af71e89adfb10b4cfa + role: lightburn-preferences-export notes: - No complete serial numbers or private source locators are stored here. - Incorrect galvo or source configuration can damage hardware; values are not inferred by trial and error. - USB identity was confirmed by passive Windows PnP enumeration on 2026-10-17; no device endpoint was opened. - USB revision 0005 is a descriptor value, not a proven board or firmware version. + - LightBurn bundle manifests establish version 2.1.03 but do not expose a separate build identifier. + - Exported recipe values are observations, not certified hardware limits or safe operating parameters. diff --git a/docs/research/rayforge-lightburn-mopa-import-spike.md b/docs/research/rayforge-lightburn-mopa-import-spike.md index da24fa5..5b5d457 100644 --- a/docs/research/rayforge-lightburn-mopa-import-spike.md +++ b/docs/research/rayforge-lightburn-mopa-import-spike.md @@ -14,9 +14,12 @@ speed and one pass. LightBurn's observed `frequency` and However, the importer does not map frequency into the workflow layer settings. It maps only power, speed and passes from this fixture. The inspected Rayforge -LightBurn fixtures and importer contain no evidence-backed mapping for a MOPA -pulse-width field. Consequently, the imported job is explicitly classified as -unsafe for JCZ encoding rather than being completed with a guessed value. +LightBurn project fixtures and importer contain no evidence-backed mapping for +a MOPA pulse-width field. A later private LightBurn device-library export +establishes the field name `QPulseWidth` and its nanosecond UI contract, but it +does not establish how that value is serialized in a `.lbrn2` project. +Consequently, the imported job is explicitly classified as unsafe for JCZ +encoding rather than being completed with a guessed mapping. Two runs produced identical output with SHA-256 `9ebce7b5b3388abfeed024e25f8f46001260bf814da19808bfc83fb6554fc26e`. @@ -28,8 +31,12 @@ The golden result is `tests/fixtures/rayforge-lightburn-mopa-import.json`. `overrideFrequency` elements. - **Observed:** generic parsing retains these elements in source metadata. - **Observed:** `_build_step_config` does not map frequency or pulse width. -- **Unknown:** the exact LightBurn field and unit for JPT MOPA pulse width in a - known-working galvo project. +- **Observed after this spike:** a LightBurn `2.1.03` device-library export + uses `QPulseWidth`; official LightBurn documentation defines Q-pulse width + in nanoseconds. See the + [device-export analysis](lightburn-device-exports-2026-10-19.md). +- **Unknown:** the exact `QPulseWidth` representation and omission/default + behavior in a known-working galvo `.lbrn2` project. - **Not inferred:** no pulse-width tag, unit, default or machine limit was invented for the public fixture. @@ -38,9 +45,8 @@ The golden result is `tests/fixtures/rayforge-lightburn-mopa-import.json`. Rayforge's existing LightBurn importer is useful for ordinary vector geometry and common process settings, but it does not yet satisfy M1's MOPA parameter survival requirement. The narrowest likely extension is an importer mapping -for frequency plus a separately evidenced pulse-width field, followed by an -explicit validation gate. A private known-working LightBurn project is still -needed to establish the pulse-width representation before implementing that -mapping. +for frequency plus `QPulseWidth`, followed by an explicit unit conversion and +validation gate. A private known-working LightBurn project is still needed to +establish project serialization before implementing that mapping. No USB enumeration, controller command, motion or emission was attempted. diff --git a/docs/roadmap.md b/docs/roadmap.md index c13fa6b..e4771e3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Product and research roadmap -Last updated: 2026-10-18 +Last updated: 2026-10-19 ## Destination @@ -46,7 +46,8 @@ evidence. The collected installer snapshot establishes: - the JCZ/EZCAD2 controller and `Lmcv2u` driver family; -- vendor-declared USB VID/PID values, pending live confirmation; +- vendor-declared USB VID/PID values, subsequently confirmed by passive live + Windows enumeration; - known-good 110 mm and 300 mm configuration trees; - exact hashes for the opaque, lens-specific `markcfg7` profiles; - a shared correction file and observable workspace differences; @@ -60,7 +61,13 @@ It does **not** yet establish: - which values are controller settings versus laser-source settings; - status, interlock, error, abort, and recovery behavior; - whether an apparently harmless query can move a galvo or arm emission; -- the exact LightBurn version and known-working LightBurn device profile. +- how a known-working `.lbrn2` project serializes JPT frequency and + `QPulseWidth` settings. + +The later LightBurn export collection now establishes version `2.1.03`, a +110 x 110 mm `JCZFiber`/`LibUSB` JPT profile, and the `QPulseWidth` device +library field. See the +[device-export analysis](research/lightburn-device-exports-2026-10-19.md). 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. @@ -147,10 +154,11 @@ controller-safe state ordering remains an explicit evidence gap. [The synthetic LightBurn MOPA import spike](research/rayforge-lightburn-mopa-import-spike.md) confirms that Rayforge preserves geometry, power, speed and passes and retains an observed LightBurn frequency value in raw source metadata. Frequency is not -mapped into workflow settings, and no evidence-backed pulse-width field was -found, so the job deliberately fails the JCZ encoding gate. A private -known-working galvo fixture is required before implementing the missing MOPA -mapping. +mapped into workflow settings. A later real device export identifies +`QPulseWidth` and its nanosecond UI contract, but project serialization remains +unknown, so the job still deliberately fails the JCZ encoding gate. A private +known-working `.lbrn2` fixture is required before implementing the missing +MOPA mapping. The architecture decision remains proposed until these spikes produce evidence. An independent app requires a documented gap analysis showing why @@ -251,17 +259,22 @@ imply compatible safety or control behavior on either CO2 machine. The next work package should be small enough to review in a few commits: -1. collect the pending LightBurn data from the control PC as one local archive; -2. run the stock MeerK40t mock-controller evaluation; -3. prove the Rayforge external-addon and binary-driver registration seam; -4. build one no-I/O Rayforge-to-JCZ encoder spike and golden trace comparison; -5. test Rayforge's existing LightBurn import against galvo/MOPA fixtures; -6. accept, amend or reject ADR 0002 from the results; -7. separately produce a written M3 hardware-research protocol before any USB - access or controller command. +1. collect two or three known-working `.lbrn2` projects plus the remaining + device screenshots, preferences folder, photos and operator notes; +2. add a sanitized synthetic JCZ `.lbdev` fixture and characterize the exact + gaps in Rayforge's device importer; +3. implement and test the smallest evidence-backed JCZ profile-import seam, + keeping project `QPulseWidth` mapping gated until a real project confirms + its serialization; +4. extend the offline JCZ encoder from semantic records to controller-list + buffering and explicit safe state ordering, still without transport; +5. exercise cancellation/recovery and the graphical workflow against mock + backends, then accept, amend or reject ADR 0002; +6. separately produce a written M3 hardware-research protocol before any USB + endpoint access or controller command. -Items 2–6 can proceed while item 1 is pending. Hardware work cannot proceed -until item 7 is reviewed and the physical setup is ready. +Hardware work cannot proceed until item 6 is reviewed and the physical setup +is ready. ## Decision rules diff --git a/docs/status/phase-0.md b/docs/status/phase-0.md index 3e8f960..7e4aa43 100644 --- a/docs/status/phase-0.md +++ b/docs/status/phase-0.md @@ -1,6 +1,6 @@ # Phase 0 status -Last updated: 2026-10-17 +Last updated: 2026-10-19 ## Completed @@ -22,29 +22,36 @@ Last updated: 2026-10-17 description `USBLMCV4`, USB VID/PID `9588:9899`, vendor-specific class, and USB revision descriptor `0005`. No compatible function driver is installed on the development computer; no endpoint was opened. +- Four LightBurn exports are preserved privately with SHA-256 manifests and + Git LFS. Their public evidence records contain no private source locators. +- All three bundle manifests identify LightBurn `2.1.03`. +- The JPT export confirms a `JCZFiber`/`LibUSB` device profile, 110 x 110 mm + work area, enabled Q-pulse-width control, `QPulseWidth` cut-list fields and + a require-frame-before-start setting. +- The Eduard export confirms a Ruida/Serial profile with a 1500 x 1000 mm work + area. A separate 500 x 300 mm Ruida profile remains physically unassigned. - No vendor executable, plugin, DLL, or driver was run. No USB capture, framing, machine command, or laser output was performed. ## Pending external acquisition -The current development computer is not the LightBurn control computer: no -LightBurn executable or local LightBurn preferences directory was present. -Consequently, these items remain pending from the actual control machine: +These items remain pending from the actual control machine: -- Help → About screenshot; -- full `.lbzip` bundle and `.lbprefs` export; - closed-application preferences-folder archive; - Device Settings screenshots; - known-working `.lbrn2` examples; - safe exterior hardware/label photographs and startup/interlock notes; - stock-driver identity and version from the actual LightBurn control computer. +An About screenshot is useful corroboration, but the imported bundle manifests +already establish version `2.1.03`. + Collect them directly into a dated directory under PrivateStash by following the [LightBurn checklist](../collection/lightburn.md). Do not use Google Drive as an intermediate project workspace and do not generate Support Data. -Phase 0 remains open until these control-machine artifacts are hashed, -committed, pushed, and represented by sanitized public evidence. +Phase 0 remains open until the safety- and interoperability-relevant remainder +is hashed, committed, pushed, and represented by sanitized public evidence. The work that does not depend on this acquisition can proceed in parallel. The [product and research roadmap](../roadmap.md) defines the offline application diff --git a/evidence/public-lightburn-exports.jsonl b/evidence/public-lightburn-exports.jsonl new file mode 100644 index 0000000..02f4977 --- /dev/null +++ b/evidence/public-lightburn-exports.jsonl @@ -0,0 +1,4 @@ +{"analysis_refs":["docs/research/lightburn-device-exports-2026-10-19.md"],"artifact_id":"sha256-e16fdb0aa7c374c4c94d","classification":"private-sensitive","media_type":"application/zip","observed_at_utc":"2026-10-19T08:06:14Z","origin":"user-provided-local-lightburn-export","sha256":"e16fdb0aa7c374c4c94de9b4ecf4f0be1e1167f91c8f51517b060dde900880de","size_bytes":20309} +{"analysis_refs":["docs/research/lightburn-device-exports-2026-10-19.md"],"artifact_id":"sha256-fc6cf52f4ef815dd5037","classification":"private-sensitive","media_type":"application/zip","observed_at_utc":"2026-10-19T08:06:14Z","origin":"user-provided-local-lightburn-export","sha256":"fc6cf52f4ef815dd5037e11a893cefce4009b4965c11776d931e501ccf10a713","size_bytes":5720} +{"analysis_refs":["docs/research/lightburn-device-exports-2026-10-19.md"],"artifact_id":"sha256-88c762adcb8e9693c913","classification":"private-sensitive","media_type":"application/octet-stream","observed_at_utc":"2026-10-19T08:06:14Z","origin":"user-provided-local-lightburn-export","sha256":"88c762adcb8e9693c9132473a9d90c69eaf768b96f12e8af71e89adfb10b4cfa","size_bytes":97356} +{"analysis_refs":["docs/research/lightburn-device-exports-2026-10-19.md"],"artifact_id":"sha256-d422e9886e29ed666c07","classification":"private-sensitive","media_type":"application/zip","observed_at_utc":"2026-10-19T08:06:14Z","origin":"user-provided-local-lightburn-export","sha256":"d422e9886e29ed666c077a0e7bab9a6c3d53fc938365b7b773d79c529b55c0f0","size_bytes":8798} diff --git a/tests/test_public_evidence.py b/tests/test_public_evidence.py index 2dd2f0d..000a88c 100644 --- a/tests/test_public_evidence.py +++ b/tests/test_public_evidence.py @@ -8,6 +8,17 @@ ROOT = Path(__file__).resolve().parents[1] class PublicEvidenceTests(unittest.TestCase): + def _manifest_paths(self) -> list[Path]: + return sorted((ROOT / "evidence").glob("public-*.jsonl")) + + def _entries(self) -> list[dict]: + return [ + json.loads(line) + for path in self._manifest_paths() + for line in path.read_text(encoding="utf-8").splitlines() + if line + ] + def test_public_evidence_matches_manifest_contract(self) -> None: schema = json.loads( (ROOT / "schemas" / "artifact-manifest.schema.json").read_text(encoding="utf-8") @@ -18,13 +29,10 @@ class PublicEvidenceTests(unittest.TestCase): digest_pattern = re.compile(schema["properties"]["sha256"]["pattern"]) classifications = set(schema["properties"]["classification"]["enum"]) - lines = (ROOT / "evidence" / "public-installer-configs.jsonl").read_text( - encoding="utf-8" - ).splitlines() - self.assertEqual(len(lines), 9) + paths = self._manifest_paths() + self.assertGreaterEqual(len(paths), 2) - for line in lines: - entry = json.loads(line) + for entry in self._entries(): self.assertFalse(required - set(entry)) self.assertFalse(set(entry) - allowed) self.assertRegex(entry["artifact_id"], artifact_pattern) @@ -35,15 +43,21 @@ class PublicEvidenceTests(unittest.TestCase): self.assertNotIn("source_locator", entry) def test_public_evidence_has_unique_content_ids(self) -> None: - entries = [ - json.loads(line) - for line in (ROOT / "evidence" / "public-installer-configs.jsonl") - .read_text(encoding="utf-8") - .splitlines() - ] + entries = self._entries() ids = [entry["artifact_id"] for entry in entries] self.assertEqual(len(ids), len(set(ids))) + def test_expected_manifest_entry_counts(self) -> None: + expected = { + "public-installer-configs.jsonl": 9, + "public-lightburn-exports.jsonl": 4, + } + actual = { + path.name: len(path.read_text(encoding="utf-8").splitlines()) + for path in self._manifest_paths() + } + self.assertEqual(actual, expected) + if __name__ == "__main__": unittest.main()