Add offline Rayforge and BalorMK validation spikes

This commit is contained in:
Codex 2026-10-18 17:12:30 +02:00
parent b8fb3ee658
commit 4742099be8
23 changed files with 1535 additions and 10 deletions

View file

@ -141,8 +141,8 @@
}, },
{ {
"path": "ios/AppIcon.appiconset/Contents.json", "path": "ios/AppIcon.appiconset/Contents.json",
"sha256": "89af6a0409542706ccb6cf406a288e7ce65d4f4b3a463930674bb1569225cbd1", "sha256": "3275a59b6f06b7690e1923669a19088b42930310bdc725b7004f51f36ad61bb8",
"bytes": 2298 "bytes": 2182
}, },
{ {
"path": "ios/AppIcon.appiconset/icon-1024.png", "path": "ios/AppIcon.appiconset/icon-1024.png",

View file

@ -85,3 +85,23 @@ workflow. Record evidence before choosing a standalone app.
See [Open-source landscape](../research/open-source-landscape.md) for the See [Open-source landscape](../research/open-source-landscape.md) for the
candidate matrix, inspected commits, risks and detailed spike plan. candidate matrix, inspected commits, risks and detailed spike plan.
Subsequent offline evidence:
- [MeerK40t mock MOPA spike](../research/meerk40t-mock-mopa-spike.md):
deterministic controller-level frequency, pulse-width and square encoding;
- [Rayforge addon spike](../research/rayforge-addon-spike.md): external binary
driver load and lifecycle are viable, with a documented registry API gap.
- [Rayforge-to-JCZ equivalence spike](../research/rayforge-jcz-equivalence-spike.md):
the required MOPA and geometry semantics survive a real Rayforge `Ops`
stream and match the pinned BalorMK mock trace.
- [JCZ negative cases](../research/rayforge-jcz-negative-cases.md): the encoder
boundary rejects profile mismatch, invalid values, out-of-field geometry and
unsupported commands without a transport.
- [JCZ 12-byte record spike](../research/rayforge-jcz-packet-spike.md): the
external encoder deterministically packs controller-shaped command records
matching the pinned BalorMK mock values, while retaining no transport.
These results support continuing toward complete list buffering and production
integration, but they do not yet satisfy every proof obligation or change this
ADR's proposed status.

View file

@ -0,0 +1,67 @@
# MeerK40t mock-controller MOPA spike
- Date: 2026-10-18
- Upstream: `meerk40t/meerk40t`
- Commit: `d511944e08788e88077727f682957759884ff5ca`
- Scope: offline command-list encoding only
## Result
The pinned BalorMK controller selected its `MockConnection` and encoded a
synthetic square with explicit power, speed, 40 kHz frequency and 200 ns MOPA
pulse width. Two consecutive runs produced identical parsed command traces.
The harness supplies a synthetic 110 mm field solely for BalorMK's conversion
from mm/s to galvo units; it is not evidence that the physical lens is 110 mm.
The trace contains:
- `listQSwitchPeriod` value `0x01f4` (500), matching BalorMK's internal
fiber-source conversion `20000 / 40 kHz`;
- `listFiberYLPMPulseWidth` value `0x00c8` (200);
- jump and mark commands for the synthetic square; and
- a one millisecond `listLaserOnPoint` dwell represented as 100 ten-microsecond
controller units; and
- an explicit end-of-list command.
The harness replaces PyUSB with a trap before BalorMK is imported. Any call to
USB discovery or resource disposal fails the run. The controller is also
constructed with `force_mock=True`. No USB enumeration or hardware access was
attempted.
## Reproduce
Check out the pinned MeerK40t commit outside this repository, then run:
```powershell
python scripts/spike_meerk40t_mock_mopa.py C:\path\to\meerk40t
```
The script refuses to run against any other commit. Its JSON output includes
the upstream revision, inputs, selected transport, USB status and parsed trace.
The checked result is stored as
`tests/fixtures/meerk40t-mock-mopa-trace.json` for later golden comparisons.
## What this proves
- BalorMK's mock path can be forced without controller discovery.
- Operation settings preserve frequency in kHz and pulse width as the integer
value emitted by `listFiberYLPMPulseWidth` through command-list encoding.
- The tested square produces a deterministic mock trace.
## What this does not prove
- It does not validate the units or safe range expected by the makerspace's
specific JPT E2/M7 source.
- It does not exercise MeerK40t's graphical UI, hatch planner, correction file,
job spooler, framing, cancellation or status behavior.
- It does not establish compatibility with the physical controller, mounted
lens or correction profile.
- It does not authorize USB access, galvo motion or laser output.
## Decision impact
This is positive evidence for the BalorMK command encoder and mock transport,
but it is not enough to select stock MeerK40t as the workshop application.
The next useful extension of Spike A is a full cut-plan/spooler test containing
synthetic hatch geometry. Spike B, the external Rayforge no-device driver
registration proof, can proceed independently.

View file

@ -158,8 +158,9 @@ Source inspection found particularly relevant implementation details:
- imported LightBurn cut settings are mapped into Rayforge workflows, - imported LightBurn cut settings are mapped into Rayforge workflows,
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 and `pulse_width` in - laser steps and recipes serialize `frequency` in Hz; the laser-step UI
microseconds; describes `pulse_width` as nanoseconds, while the Ruida encoder calls the
same Ops value microseconds, so the unit contract requires clarification;
- 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

View file

@ -0,0 +1,82 @@
# Rayforge external binary-driver addon spike
- Date: 2026-10-18
- Upstream: `barebaric/rayforge`
- Commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
- Rayforge addon API: 23
- Scope: Windows, external addon lifecycle, no device or transport
## Result
An external addon loaded through Rayforge's real `AddonManager` and registered
`PirateRayJCZNoDeviceDriver`. The driver declares `uses_gcode = False`, has no
discovery specification or transport, and returns a recognizable binary test
payload from an `OpsEncoder`.
The following lifecycle completed twice with identical JSON results:
1. load the external addon;
2. resolve the driver by class name;
3. create its binary encoder and verify its payload;
4. disable the addon and verify driver removal;
5. re-enable it and verify re-registration;
6. reload it and verify there is exactly one registry entry;
7. package its three source/manifest files into a ZIP; and
8. disable it cleanly.
The two result files had SHA-256
`610f8f191526afe4685f8659c1ef2b34fac3c68a8f8088d77ca5e1fdd6080bfc`.
The checked result is `tests/fixtures/rayforge-addon-spike.json`.
## Registration seam finding
Rayforge's addon hook specification still exposes
`register_machines(machine_manager)`, but the inspected startup registration
table does not invoke that hook, and `MachineManager` has no method for adding
a driver class. The source-level driver registry instead exposes
`rayforge.machine.driver.register_driver(driver)`.
The working spike therefore registers during import of the addon's worker
entry point and removes its class in `on_unload`. This survives load, disable,
enable and reload, but it relies on mutable module-level registry internals for
cleanup. Before a production addon, Rayforge should ideally expose a dedicated
addon-aware driver registry hook with supported unregister behavior.
## Reproduce
Create an isolated environment containing Rayforge's non-GUI runtime driver
dependencies, then run:
```powershell
python scripts/spike_rayforge_addon.py C:\path\to\rayforge
```
The script refuses any Rayforge revision other than the pinned commit.
## What this proves
- An external addon can load a binary, non-G-code driver without patching the
Rayforge source tree.
- Its worker entry point is sufficient in headless mode.
- The driver can remain completely free of discovery and transport.
- Addon load, disable, enable and reload work when registration and cleanup are
handled explicitly.
- The addon itself can be packaged as a deterministic set of files on Windows.
## What remains open
- The ZIP check is addon packaging, not a proof that a complete PyInstaller or
MSIX Rayforge build discovers an externally installed addon.
- The current registration seam is functional but not a stable, addon-aware
API. Upstream feasibility should be assessed before relying on it.
- No JCZ command encoding, machine profile, UI, project reload, discovery,
transport, controller status or cancellation is implemented here.
- Nothing in this spike authorizes USB access, galvo movement or emission.
## Decision impact
Spike B is a qualified go: the external binary-driver boundary is viable
without a Rayforge core fork, but registration lifecycle support should be
made explicit upstream. The next technical proof is Spike C: translate a tiny
Rayforge `Ops` stream into a deterministic no-I/O JCZ trace and compare its
semantics with the pinned BalorMK mock trace.

View file

@ -0,0 +1,64 @@
# Rayforge-to-JCZ semantic equivalence spike
- Date: 2026-10-18
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
- MeerK40t commit: `d511944e08788e88077727f682957759884ff5ca`
- Scope: deterministic semantic encoding with no transport
## Result
A real Rayforge `raygeo.ops.Ops` stream was constructed with job boundaries,
50 percent power, 250 mm/s feed, 40 kHz frequency, 200 ns pulse width, a
synthetic square, a one millisecond dwell and job end. The external addon
encoded it into a deterministic semantic trace without discovery or transport.
The encoded values matched the pinned BalorMK mock golden trace for all six
checked semantics:
| Concept | Rayforge input | JCZ/Balor semantic value |
| --- | ---: | ---: |
| Power | `0.5` fraction | `0x0800` |
| Mark speed | `15000` mm/min | `0x0094` for synthetic 110 mm field |
| Frequency | `40000` Hz | Q-switch period `0x01f4` |
| Pulse width | `200` ns | `0x00c8` |
| Geometry | four marks | matching `0x7000`/`0x7100` coordinates |
| Dwell | `1` ms | `0x0064` ten-microsecond units |
Two runs produced identical results with SHA-256
`6507c887b624a28820a79cf92a49b12b518a47aca7612a17af1f4ba5302b1e8d`.
The checked result is `tests/fixtures/rayforge-jcz-equivalence.json`.
## Unit finding
Rayforge's laser-step UI contract labels pulse width as nanoseconds, matching
BalorMK's MOPA UI. However, the current Ruida RPA encoder docstring calls the
same `Ops.pulse_width` value microseconds. The spike follows the laser-step
contract and records this disagreement as an upstream unit-clarity gap. A
production adapter must not infer the unit from a driver-specific comment.
## What this proves
- Rayforge `Ops` carries the required frequency and pulse-width state into an
external binary encoder.
- The selected values can map deterministically to the same controller-level
semantics emitted by the pinned BalorMK mock implementation.
- Coordinate, speed and dwell conversions can remain inside a small encoder
boundary with no USB dependency.
## What this does not prove
- The semantic trace is now accompanied by separately documented 12-byte
command records; neither artifact is a complete production command list.
- The 110 mm field is synthetic and applies no lens correction.
- No machine/source/lens profile validation is implemented.
- Unsupported Ops, cancellation, status, list buffering and error recovery
still need explicit behavior and negative tests.
- No USB enumeration, controller command, motion or emission was attempted.
## Decision impact
Spike C passes at the semantic boundary. The result supports a Rayforge JCZ
addon architecture. A follow-up spike now covers transport-free 12-byte
command records, but ADR 0002 remains proposed until complete list behavior,
addon packaging in a complete Windows build, and the remaining stock
MeerK40t workflow assessment are addressed.

View file

@ -0,0 +1,37 @@
# Rayforge JCZ negative safety cases
- Date: 2026-10-18
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
- Scope: fail-closed encoder boundary, no discovery or transport
## Result
The transport-free JCZ semantic encoder rejected all six synthetic negative
cases before producing output:
| Case | Rejection |
| --- | --- |
| Profile mismatch | Encoder profile identity did not match the required identity |
| Out-of-field geometry | Coordinate exceeded the synthetic 110 mm field |
| Power above one | Power was outside the Rayforge `0..1` fraction |
| Zero frequency | Frequency was not positive |
| Zero pulse width | Pulse width was not positive |
| Unsupported command | A synthetic `CUSTOM` Ops command had no explicit mapping |
Two runs produced identical results with SHA-256
`0adab8fb1060fde1e172c674da3d0d4ae5936b01490891b898f86cc4f7ca3c3f`.
The checked result is `tests/fixtures/rayforge-jcz-negative-cases.json`.
## Boundary
The profile identifier and 110 mm field are explicitly synthetic. This test
does not claim the actual machine, lens or correction identity. It proves only
that the encoder boundary can require exact identity and reject mismatches
without guessing values.
The current validations are intentionally narrower than a production policy.
Real minimum/maximum frequency and pulse-width limits require verified source
and machine evidence. Until then, the spike rejects non-positive values but
does not invent an upper limit.
No controller discovery, USB access, motion, framing or emission occurred.

View file

@ -0,0 +1,47 @@
# Rayforge-to-JCZ 12-byte record spike
- Date: 2026-10-18
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
- MeerK40t commit: `d511944e08788e88077727f682957759884ff5ca`
- Scope: deterministic command-record packing with no transport
## Result
The external Rayforge addon now packs the synthetic operation stream into
little-endian JCZ command records of six unsigned 16-bit words (12 bytes).
The opcode and value fields agree with the pinned BalorMK mock trace for power,
mark and jump speed, frequency, MOPA pulse width, the square coordinates and
distances, one millisecond dwell, and end-of-list.
Two independent runs produced identical output with SHA-256
`b3d34600daac7060c3d3b28edf1627813d3bc518b1a64be11367f8e2106a51c6`.
The checked records are in
`tests/fixtures/rayforge-jcz-equivalence.json` and are asserted byte-for-byte by
the standard-library test suite.
## Safety boundary
This is an independently written, transport-free encoder spike. It imports no
USB, serial or socket transport, performs no discovery, and cannot send the
records anywhere. A synthetic profile identity must match before encoding.
Coordinates outside the synthetic 110 mm field and invalid or unsupported
operations continue to fail closed.
## Remaining gaps
- Records are emitted in Rayforge operation order. A production adapter must
define controller-safe state ordering from evidence.
- List framing, fixed-size buffers, padding, upload, status, cancellation and
recovery are absent.
- The 110 mm coordinate mapping is synthetic and has no lens correction.
- Machine-specific bounds and the pulse-width unit contract still require
authoritative evidence; they are not guessed here.
- No hardware compatibility or safe marking behavior is established.
## Decision impact
The result advances the Rayforge addon route beyond semantic JSON traces: the
same operation model can produce deterministic controller-shaped records at a
narrow encoder boundary. ADR 0002 remains proposed because stock application
workflow coverage, complete packet/list behavior, packaging in a full Windows
build and the remaining safety proof obligations are still open.

View file

@ -1,6 +1,6 @@
# Product and research roadmap # Product and research roadmap
Last updated: 2026-10-17 Last updated: 2026-10-18
## Destination ## Destination
@ -104,6 +104,41 @@ The first work is entirely offline:
- identify only the missing profile, validation, diagnostic and safety - identify only the missing profile, validation, diagnostic and safety
concepts instead of designing replacements for existing ones. concepts instead of designing replacements for existing ones.
The first controller-level part of the MeerK40t mock evaluation is recorded in
[the mock MOPA spike](research/meerk40t-mock-mopa-spike.md). It confirms
deterministic frequency, pulse-width and square-geometry command-list encoding
through BalorMK's forced mock transport. Full cut-plan/spooler and hatch
coverage remains open, so this does not yet complete the stock-application
evaluation or settle ADR 0002.
The first Rayforge addon proof is recorded in
[the external binary-driver addon spike](research/rayforge-addon-spike.md).
An external addon can register a no-discovery, no-transport binary driver and
survive load, disable, enable and reload without patching Rayforge core. The
working registration path is an import-time call to the module-level driver
registry plus explicit unload cleanup, not the documented
`register_machines(machine_manager)` hook. Full application packaging and a
stable addon-aware unregister API remain open.
[The Rayforge-to-JCZ equivalence spike](research/rayforge-jcz-equivalence-spike.md)
confirms deterministic semantic agreement with the pinned BalorMK mock trace
for power, speed, frequency, MOPA pulse width, square coordinates and a one
millisecond dwell. It deliberately stops before production packet encoding,
lens correction or transport and records an upstream pulse-width unit
documentation conflict that must be resolved explicitly.
[Negative JCZ encoder cases](research/rayforge-jcz-negative-cases.md) now prove
fail-closed handling for a synthetic profile mismatch, out-of-field geometry,
power outside Rayforge's fraction range, non-positive frequency and pulse
width, and unsupported Ops. Hardware-specific upper limits remain unknown and
are deliberately not guessed.
[The transport-free JCZ record spike](research/rayforge-jcz-packet-spike.md)
packs the same operation stream into deterministic little-endian 12-byte
records whose opcodes, values, coordinates and distances match the pinned
BalorMK mock evidence. It deliberately omits list buffering and all transport;
controller-safe state ordering remains an explicit evidence gap.
The architecture decision remains proposed until these spikes produce The architecture decision remains proposed until these spikes produce
evidence. An independent app requires a documented gap analysis showing why evidence. An independent app requires a documented gap analysis showing why
neither existing platform can be extended responsibly. neither existing platform can be extended responsibly.

View file

@ -316,7 +316,9 @@ def generate_ios(source: Image.Image) -> None:
contents = {"images": images, "info": {"author": "xcode", "version": 1}} contents = {"images": images, "info": {"author": "xcode", "version": 1}}
directory.mkdir(parents=True, exist_ok=True) directory.mkdir(parents=True, exist_ok=True)
(directory / "Contents.json").write_text( (directory / "Contents.json").write_text(
json.dumps(contents, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" json.dumps(contents, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
newline="\n",
) )
@ -348,8 +350,12 @@ def generate_android(source: Image.Image) -> None:
""" """
anydpi = OUTPUT / "android" / "mipmap-anydpi-v26" anydpi = OUTPUT / "android" / "mipmap-anydpi-v26"
anydpi.mkdir(parents=True, exist_ok=True) anydpi.mkdir(parents=True, exist_ok=True)
(anydpi / "ic_launcher.xml").write_text(adaptive_xml, encoding="utf-8") (anydpi / "ic_launcher.xml").write_text(
(anydpi / "ic_launcher_round.xml").write_text(adaptive_xml, encoding="utf-8") adaptive_xml, encoding="utf-8", newline="\n"
)
(anydpi / "ic_launcher_round.xml").write_text(
adaptive_xml, encoding="utf-8", newline="\n"
)
values = OUTPUT / "android" / "values" values = OUTPUT / "android" / "values"
values.mkdir(parents=True, exist_ok=True) values.mkdir(parents=True, exist_ok=True)
(values / "ic_launcher_background.xml").write_text( (values / "ic_launcher_background.xml").write_text(
@ -359,6 +365,7 @@ def generate_android(source: Image.Image) -> None:
</resources> </resources>
""", """,
encoding="utf-8", encoding="utf-8",
newline="\n",
) )
@ -434,7 +441,9 @@ def generate() -> None:
"display": "standalone", "display": "standalone",
} }
(web / "site.webmanifest").write_text( (web / "site.webmanifest").write_text(
json.dumps(webmanifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" json.dumps(webmanifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
newline="\n",
) )
generate_windows(source) generate_windows(source)
@ -466,7 +475,9 @@ def generate() -> None:
"files": files, "files": files,
} }
(OUTPUT / "manifest.json").write_text( (OUTPUT / "manifest.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
newline="\n",
) )
write_branding_readme(files) write_branding_readme(files)

View file

@ -0,0 +1,201 @@
"""Run a deterministic, hardware-free BalorMK MOPA command-list spike.
The script imports a separately checked-out MeerK40t tree. It installs an
in-process USB trap before importing BalorMK, forces the controller's mock
transport, and never enumerates USB devices.
"""
from __future__ import annotations
import argparse
import json
import struct
import subprocess
import sys
import types
from pathlib import Path
PINNED_COMMIT = "d511944e08788e88077727f682957759884ff5ca"
class _Channel:
def __init__(self, name: str, log: list[str]):
self.name = name
self.log = log
self._ = lambda value: value
def __call__(self, value: str) -> None:
self.log.append(f"{self.name}: {value}")
def watch(self, _handler) -> None:
return None
class _Service:
safe_label = "offline-mopa-spike"
source = "fiber"
pulse_width_enabled = True
default_pulse_width = 4
default_rapid_speed = 2000.0
default_power = 100.0
default_frequency = 30.0
default_fpk = 10.0
default_speed = 100.0
delay_laser_on = 100
delay_laser_off = 100
delay_polygon = 100
signal_updates = False
class _SyntheticView:
"""Deterministic 110 mm field used only for speed conversion."""
@staticmethod
def position(_x, _y, *, vector=False, margins=True):
del vector, margins
galvos_per_mm = 65535.0 / 110.0
return galvos_per_mm, galvos_per_mm
def __init__(self) -> None:
self.log: list[str] = []
self.channels: dict[str, _Channel] = {}
self.view = self._SyntheticView()
def channel(self, name: str, **_kwargs) -> _Channel:
return self.channels.setdefault(name, _Channel(name, self.log))
def signal(self, *_args) -> None:
return None
def setting(self, _kind, name: str, default):
return getattr(self, name, default)
def _install_usb_trap() -> None:
class USBError(Exception):
backend_error_code = None
class NoBackendError(USBError):
pass
def forbidden(*_args, **_kwargs):
raise AssertionError("real USB access attempted during mock-only spike")
usb = types.ModuleType("usb")
core = types.ModuleType("usb.core")
util = types.ModuleType("usb.util")
backend = types.ModuleType("usb.backend")
libusb1 = types.ModuleType("usb.backend.libusb1")
core.USBError = USBError
core.NoBackendError = NoBackendError
core.find = forbidden
util.dispose_resources = forbidden
libusb1.LIBUSB_ERROR_ACCESS = -3
libusb1.LIBUSB_ERROR_NOT_FOUND = -5
usb.core = core
usb.util = util
sys.modules.update(
{
"usb": usb,
"usb.core": core,
"usb.util": util,
"usb.backend": backend,
"usb.backend.libusb1": libusb1,
}
)
def _commit_of(source: Path) -> str:
result = subprocess.run(
["git", "-C", str(source), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def run(source: Path) -> dict[str, object]:
commit = _commit_of(source)
if commit != PINNED_COMMIT:
raise RuntimeError(f"expected MeerK40t {PINNED_COMMIT}, found {commit}")
_install_usb_trap()
sys.path.insert(0, str(source))
from meerk40t.balormk.controller import GalvoController
from meerk40t.balormk.mock_connection import MockConnection
service = _Service()
controller = GalvoController(service, force_mock=True)
controller.init_laser = lambda: None
controller.connect_if_needed()
if not isinstance(controller.connection, MockConnection):
raise AssertionError("controller did not select MockConnection")
parsed_packets: list[str] = []
controller.connection.send = parsed_packets.append
controller._list_new()
controller.set_settings(
{
"power": 500,
"speed": 250.0,
"frequency": 40.0,
"pulse_width_enabled": True,
"pulse_width": 200,
}
)
controller.goto(0x7000, 0x7000)
controller.mark(0x7100, 0x7000)
controller.mark(0x7100, 0x7100)
controller.mark(0x7000, 0x7100)
controller.mark(0x7000, 0x7000)
controller.list_laser_on_point(100) # 1 ms in BalorMK's 10 us units.
controller.list_end_of_list()
# Make the mock's status reply deterministic and ready. Without this,
# upstream MockConnection returns random bytes when no response is implied.
controller.connection.set_implied_response(struct.pack("<4H", 0, 0, 0, 0x20))
controller._list_end()
trace = "\n".join(parsed_packets)
required = {
"frequency_40_khz": "801b:01f4:0000:0000:0000:0000 listQSwitchPeriod",
"pulse_width_200_ns": "8026:00c8:0000:0000:0000:0000 listFiberYLPMPulseWidth",
"geometry": "listMarkTo",
"dwell_1_ms": "8003:0064:0000:0000:0000:0000 listLaserOnPoint",
"end": "listEndOfList",
}
for label, marker in required.items():
if marker not in trace:
raise AssertionError(f"missing {label}: {marker}\nactual trace:\n{trace}")
return {
"upstream": "meerk40t/meerk40t",
"commit": commit,
"transport": type(controller.connection).__name__,
"usb_access": "trapped; none attempted",
"input": {
"power_per_mille": 500,
"speed_mm_per_s": 250.0,
"frequency_khz": 40.0,
"pulse_width_ns": 200,
"geometry": "closed synthetic square in controller coordinates",
},
"trace": trace.splitlines(),
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, help="pinned MeerK40t checkout")
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = run(args.source.resolve())
rendered = json.dumps(result, indent=2) + "\n"
if args.output:
args.output.write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,143 @@
"""Exercise the external Rayforge addon and binary-driver lifecycle."""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
from unittest.mock import MagicMock
PINNED_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
ADDON_NAME = "the_pirate_ray_jcz"
DRIVER_NAME = "PirateRayJCZNoDeviceDriver"
def _commit_of(source: Path) -> str:
result = subprocess.run(
["git", "-C", str(source), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def run(source: Path, addon_path: Path) -> dict[str, object]:
commit = _commit_of(source)
if commit != PINNED_COMMIT:
raise RuntimeError(f"expected Rayforge {PINNED_COMMIT}, found {commit}")
sys.path.insert(0, str(source))
from rayforge.addon_mgr.addon import Addon
from rayforge.addon_mgr.addon_manager import AddonManager
from rayforge.core.addon_config import AddonConfig
from rayforge.core.hooks import RayforgeSpecs
from rayforge.machine import driver as driver_registry
import pluggy
parsed = Addon.load_from_directory(addon_path, version="0.1.0")
parsed.validate()
scratch = Path.cwd() / "reports"
scratch.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(
prefix="tpr-rayforge-addon-", dir=scratch
) as temp:
root = Path(temp)
config = AddonConfig(root / "config")
plugin_mgr = pluggy.PluginManager("rayforge")
plugin_mgr.add_hookspecs(RayforgeSpecs)
manager = AddonManager(
[addon_path.parent],
addon_path.parent,
plugin_mgr,
MagicMock(),
addon_config=config,
registries={},
)
manager.load_addon(addon_path, worker_only=True, version="0.1.0")
driver = driver_registry.get_driver_cls(DRIVER_NAME)
if driver.__name__ != DRIVER_NAME or driver.uses_gcode:
raise AssertionError("external binary driver was not registered")
encoded = driver.create_encoder(None).encode(None)
if not encoded.driver_data.get("binary"):
raise AssertionError("binary encoder payload missing")
if encoded.driver_data.get("transport") is not None:
raise AssertionError("offline encoder unexpectedly has transport")
if not manager.disable_addon(ADDON_NAME):
raise AssertionError("addon did not disable cleanly")
if DRIVER_NAME in driver_registry.driver_by_classname:
raise AssertionError("driver remained registered after disable")
if not manager.enable_addon(ADDON_NAME):
raise AssertionError("addon did not re-enable cleanly")
if driver_registry.get_driver_cls(DRIVER_NAME).__name__ != DRIVER_NAME:
raise AssertionError("driver did not re-register after enable")
if not manager.reload_addon(ADDON_NAME):
raise AssertionError("addon did not reload cleanly")
matches = [
cls for cls in driver_registry.drivers if cls.__name__ == DRIVER_NAME
]
if len(matches) != 1:
raise AssertionError("driver registry duplicated on reload")
package = root / "the_pirate_ray_jcz-0.1.0.zip"
with zipfile.ZipFile(package, "w", zipfile.ZIP_DEFLATED) as archive:
for path in sorted(addon_path.rglob("*")):
if path.is_file() and "__pycache__" not in path.parts:
archive.write(path, path.relative_to(addon_path))
with zipfile.ZipFile(package) as archive:
packaged_files = sorted(archive.namelist())
manager.disable_addon(ADDON_NAME)
return {
"upstream": "barebaric/rayforge",
"commit": commit,
"rayforge_api_version": parsed.metadata.api_version,
"addon": ADDON_NAME,
"driver": DRIVER_NAME,
"uses_gcode": False,
"discovery": None,
"transport": None,
"binary_payload_hex": (
b'[{"command":"offline_probe"}]'.hex()
),
"lifecycle": ["load", "disable", "enable", "reload", "disable"],
"packaged_files": packaged_files,
"registration_seam": "worker import side effect + on_unload cleanup",
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, help="pinned Rayforge checkout")
parser.add_argument(
"--addon",
type=Path,
default=(
Path(__file__).parents[1]
/ "spikes"
/ "rayforge-jcz-no-device-addon"
),
)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = run(args.source.resolve(), args.addon.resolve())
rendered = json.dumps(result, indent=2) + "\n"
if args.output:
args.output.write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,152 @@
"""Compare a synthetic Rayforge Ops stream with the BalorMK golden trace."""
from __future__ import annotations
import argparse
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
RAYFORGE_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
def _commit_of(source: Path) -> str:
result = subprocess.run(
["git", "-C", str(source), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def _load_encoder(addon: Path):
worker = addon / "pirate_ray_jcz" / "worker.py"
spec = importlib.util.spec_from_file_location("tpr_jcz_worker", worker)
if spec is None or spec.loader is None:
raise RuntimeError("could not load JCZ spike encoder")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.OfflineJCZEncoder
def run(source: Path, addon: Path, balor_fixture: Path) -> dict[str, object]:
commit = _commit_of(source)
if commit != RAYFORGE_COMMIT:
raise RuntimeError(f"expected Rayforge {RAYFORGE_COMMIT}, found {commit}")
sys.path.insert(0, str(source))
from raygeo.ops import Ops
Encoder = _load_encoder(addon)
to_mm = lambda word: word * Encoder.FIELD_MM / 65535.0
ops = Ops()
ops.job_start()
ops.set_power(0.5)
ops.set_feed_rate(250.0 * 60.0)
ops.set_rapid_rate(2000.0 * 60.0)
ops.set_frequency(40_000)
ops.set_pulse_width(200)
ops.move_to(to_mm(0x7000), to_mm(0x7000), 0.0)
ops.line_to(to_mm(0x7100), to_mm(0x7000), 0.0)
ops.line_to(to_mm(0x7100), to_mm(0x7100), 0.0)
ops.line_to(to_mm(0x7000), to_mm(0x7100), 0.0)
ops.line_to(to_mm(0x7000), to_mm(0x7000), 0.0)
ops.dwell(1.0)
ops.job_end()
encoded = Encoder().encode(ops)
semantic = encoded.driver_data["semantic_trace"]
by_command: dict[str, list[dict]] = {}
for item in semantic:
by_command.setdefault(item["command"], []).append(item)
balor = json.loads(balor_fixture.read_text(encoding="utf-8"))
balor_trace = "\n".join(balor["trace"])
checks = {
"power_50_percent": (
by_command["power"][0]["balor_word"] == 0x0800
and "8012:0800" in balor_trace
),
"speed_250_mm_s": (
by_command["mark_speed"][0]["balor_word"] == 0x0094
and "800c:0094" in balor_trace
),
"frequency_40_khz": (
by_command["frequency"][0]["balor_qswitch_period"] == 0x01F4
and "801b:01f4" in balor_trace
),
"pulse_width_200_ns": (
by_command["pulse_width"][0]["nanoseconds"] == 200
and "8026:00c8" in balor_trace
),
"square_coordinates": (
[(item["x"], item["y"]) for item in by_command["mark"]]
== [
(0x7100, 0x7000),
(0x7100, 0x7100),
(0x7000, 0x7100),
(0x7000, 0x7000),
]
and balor_trace.count("listMarkTo") == 4
),
"dwell_1_ms": (
by_command["laser_on_point"][0]["balor_10us_units"] == 100
and "8003:0064" in balor_trace
),
}
if not all(checks.values()):
raise AssertionError(f"semantic mismatch: {checks}")
return {
"rayforge_commit": commit,
"balormk_commit": balor["commit"],
"transport": None,
"synthetic_field_mm": Encoder.FIELD_MM,
"rayforge_ops_count": ops.len(),
"semantic_trace": semantic,
"command_records_hex": encoded.driver_data["command_records_hex"],
"balormk_equivalence": checks,
"limitations": [
"12-byte command records only; no list buffering or transport",
"record ordering follows the Rayforge Ops stream",
"synthetic field mapping; no lens correction",
"pulse width treated as nanoseconds from LaserStep UI contract",
],
}
def main() -> None:
root = Path(__file__).parents[1]
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, help="pinned Rayforge checkout")
parser.add_argument(
"--addon",
type=Path,
default=root / "spikes" / "rayforge-jcz-no-device-addon",
)
parser.add_argument(
"--balor-fixture",
type=Path,
default=root / "tests" / "fixtures" / "meerk40t-mock-mopa-trace.json",
)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = run(
args.source.resolve(),
args.addon.resolve(),
args.balor_fixture.resolve(),
)
rendered = json.dumps(result, indent=2) + "\n"
if args.output:
args.output.write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,127 @@
"""Run fail-closed cases against the transport-free Rayforge JCZ encoder."""
from __future__ import annotations
import argparse
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
RAYFORGE_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
def _commit_of(source: Path) -> str:
result = subprocess.run(
["git", "-C", str(source), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def _load_encoder(addon: Path):
path = addon / "pirate_ray_jcz" / "worker.py"
spec = importlib.util.spec_from_file_location("tpr_jcz_negative", path)
if spec is None or spec.loader is None:
raise RuntimeError("could not load JCZ spike encoder")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.OfflineJCZEncoder
def run(source: Path, addon: Path) -> dict[str, object]:
commit = _commit_of(source)
if commit != RAYFORGE_COMMIT:
raise RuntimeError(f"expected Rayforge {RAYFORGE_COMMIT}, found {commit}")
sys.path.insert(0, str(source))
from raygeo.ops import Ops
Encoder = _load_encoder(addon)
cases = []
def expect_rejection(name, encoder, ops, expected_message):
try:
encoder.encode(ops)
except ValueError as error:
message = str(error)
if expected_message not in message:
raise AssertionError(
f"{name} rejected for unexpected reason: {message}"
) from error
cases.append(
{"case": name, "result": "rejected", "reason": message}
)
return
raise AssertionError(f"{name} was not rejected")
empty = Ops()
expect_rejection(
"profile_mismatch",
Encoder(profile_id="synthetic-wrong-profile"),
empty,
"profile mismatch",
)
outside = Ops()
outside.move_to(Encoder.FIELD_MM + 0.001, 0.0, 0.0)
expect_rejection(
"out_of_field_geometry", Encoder(), outside, "outside"
)
power = Ops()
power.set_power(1.001)
expect_rejection("power_above_one", Encoder(), power, "power")
frequency = Ops()
frequency.set_frequency(0)
expect_rejection(
"non_positive_frequency", Encoder(), frequency, "frequency"
)
pulse = Ops()
pulse.set_pulse_width(0)
expect_rejection(
"non_positive_pulse_width", Encoder(), pulse, "pulse width"
)
unsupported = Ops()
unsupported.custom("synthetic-unsupported-command")
expect_rejection(
"unsupported_ops_command", Encoder(), unsupported, "unsupported"
)
return {
"rayforge_commit": commit,
"profile": Encoder.SYNTHETIC_PROFILE_ID,
"transport": None,
"cases": cases,
"all_failed_closed": len(cases) == 6,
}
def main() -> None:
root = Path(__file__).parents[1]
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, help="pinned Rayforge checkout")
parser.add_argument(
"--addon",
type=Path,
default=root / "spikes" / "rayforge-jcz-no-device-addon",
)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = run(args.source.resolve(), args.addon.resolve())
rendered = json.dumps(result, indent=2) + "\n"
if args.output:
args.output.write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
if __name__ == "__main__":
main()

View file

@ -0,0 +1 @@
"""The Pirate Ray JCZ offline Rayforge spike."""

View file

@ -0,0 +1,245 @@
"""Hardware-free Rayforge binary driver registration proof.
This is deliberately not a controller implementation. It has no discovery
specification, transport, USB import, or machine I/O.
"""
import json
import math
import struct
from typing import Any
from rayforge.core.hooks import hookimpl
from rayforge.machine import driver as driver_registry
from rayforge.machine.driver import NoDeviceDriver
from rayforge.pipeline.encoder.base import (
EncodedOutput,
MachineCodeOpMap,
OpsEncoder,
)
class OfflineJCZEncoder(OpsEncoder):
"""Encode Rayforge Ops to a deterministic, transport-free JCZ trace."""
FIELD_MM = 110.0
SYNTHETIC_PROFILE_ID = "synthetic-jpt-mopa-110-v1"
OPCODES = {
"jump": 0x8001,
"end_of_list": 0x8002,
"laser_on_point": 0x8003,
"mark": 0x8005,
"jump_speed": 0x8006,
"frequency": 0x801B,
"power": 0x8012,
"mark_speed": 0x800C,
"pulse_width": 0x8026,
}
def __init__(
self,
profile_id: str = SYNTHETIC_PROFILE_ID,
expected_profile_id: str = SYNTHETIC_PROFILE_ID,
) -> None:
self.profile_id = profile_id
self.expected_profile_id = expected_profile_id
def _validate_profile(self) -> None:
if self.profile_id != self.expected_profile_id:
raise ValueError(
"profile mismatch: encoder refuses to produce a JCZ trace"
)
@staticmethod
def _record(command: int, *values: int) -> bytes:
words = (command, *values, 0, 0, 0, 0, 0)[:6]
if any(not 0 <= value <= 0xFFFF for value in words):
raise ValueError("JCZ command word is outside the unsigned 16-bit range")
return struct.pack("<6H", *words)
def _encode_records(self, trace: list[dict[str, Any]]) -> list[bytes]:
records = []
last_x = 0x8000
last_y = 0x8000
for item in trace:
command = item["command"]
if command == "job_start":
continue
if command == "job_end":
records.append(self._record(self.OPCODES["end_of_list"]))
elif command == "pulse_width":
records.append(
self._record(
self.OPCODES[command], round(item["nanoseconds"])
)
)
elif command == "frequency":
records.append(
self._record(
self.OPCODES[command], item["balor_qswitch_period"]
)
)
elif command in ("power", "mark_speed", "jump_speed"):
records.append(
self._record(self.OPCODES[command], item["balor_word"])
)
elif command in ("jump", "mark"):
x = item["x"]
y = item["y"]
distance = min(0xFFFF, int(math.hypot(x - last_x, y - last_y)))
records.append(self._record(self.OPCODES[command], x, y, 0, distance))
last_x, last_y = x, y
elif command == "laser_on_point":
records.append(
self._record(self.OPCODES[command], item["balor_10us_units"])
)
elif command == "offline_probe":
continue
else:
raise ValueError(f"cannot packetize semantic command: {command}")
return records
@classmethod
def _coordinate(cls, value_mm: float) -> int:
if not 0.0 <= value_mm <= cls.FIELD_MM:
raise ValueError("coordinate is outside the synthetic 110 mm field")
return round(value_mm * 65535.0 / cls.FIELD_MM)
def encode(self, ops: Any, *args, **kwargs) -> EncodedOutput:
del args, kwargs
self._validate_profile()
if ops is None:
trace = [{"command": "offline_probe"}]
op_count = 0
else:
trace = []
for command in ops.to_dict()["commands"]:
kind = command["type"]
if kind in ("JOB_START", "JOB_END"):
trace.append({"command": kind.lower()})
elif kind == "SET_POWER":
fraction = float(command["power"])
if not 0.0 <= fraction <= 1.0:
raise ValueError("power must be between 0 and 1")
trace.append(
{
"command": "power",
"fraction": fraction,
"balor_word": round(fraction * 0xFFF),
}
)
elif kind in ("SET_FEED_RATE", "SET_RAPID_RATE"):
speed_mm_s = float(command["speed"]) / 60.0
trace.append(
{
"command": (
"mark_speed"
if kind == "SET_FEED_RATE"
else "jump_speed"
),
"mm_per_s": speed_mm_s,
"balor_word": int(
speed_mm_s * (65535.0 / self.FIELD_MM) / 1000.0
),
}
)
elif kind == "SET_FREQUENCY":
frequency_hz = float(command["frequency"])
if frequency_hz <= 0:
raise ValueError("frequency must be positive")
trace.append(
{
"command": "frequency",
"hz": frequency_hz,
"balor_qswitch_period": round(
20_000_000.0 / frequency_hz
),
}
)
elif kind == "SET_PULSE_WIDTH":
width_ns = float(command["pulse_width"])
if width_ns <= 0:
raise ValueError("pulse width must be positive")
trace.append(
{"command": "pulse_width", "nanoseconds": width_ns}
)
elif kind in ("MOVE_TO", "LINE_TO"):
x, y, _z = command["end"]
trace.append(
{
"command": "jump" if kind == "MOVE_TO" else "mark",
"x": self._coordinate(float(x)),
"y": self._coordinate(float(y)),
}
)
elif kind == "DWELL":
duration_ms = float(command["duration_ms"])
trace.append(
{
"command": "laser_on_point",
"duration_ms": duration_ms,
"balor_10us_units": round(duration_ms * 100.0),
}
)
else:
raise ValueError(f"unsupported Ops command: {kind}")
op_count = len(trace)
records = self._encode_records(trace)
payload = b"".join(records)
if ops is None:
payload = json.dumps(
trace, sort_keys=True, separators=(",", ":")
).encode("ascii")
return EncodedOutput(
text=payload.hex(),
op_map=MachineCodeOpMap.from_lists(
[(index, 1) for index in range(op_count)],
list(range(op_count)),
),
driver_data={
"binary": payload,
"command_records_hex": [record.hex() for record in records],
"semantic_trace": trace,
"transport": None,
},
)
class PirateRayJCZNoDeviceDriver(NoDeviceDriver):
label = "The Pirate Ray JCZ (offline)"
subtitle = "No discovery or hardware transport"
uses_gcode = False
DISCOVERY = None
@classmethod
def create_encoder(cls, machine) -> OpsEncoder:
del machine
return OfflineJCZEncoder()
def _register() -> None:
name = PirateRayJCZNoDeviceDriver.__name__
if name not in driver_registry.driver_by_classname:
driver_registry.register_driver(PirateRayJCZNoDeviceDriver)
def _unregister() -> None:
name = PirateRayJCZNoDeviceDriver.__name__
registered = driver_registry.driver_by_classname.get(name)
if registered is PirateRayJCZNoDeviceDriver:
del driver_registry.driver_by_classname[name]
driver_registry.drivers[:] = [
item for item in driver_registry.drivers if item is not registered
]
# Rayforge currently has no driver registry in call_registration_hooks().
# Registration therefore has to happen when the worker entry point is loaded.
_register()
@hookimpl
def on_unload() -> None:
_unregister()

View file

@ -0,0 +1,12 @@
name: the_pirate_ray_jcz
display_name: The Pirate Ray JCZ (offline spike)
description: Hardware-free binary-driver registration proof for Rayforge.
version: 0.1.0
api_version: 23
depends:
- rayforge>=1.0.0
author:
name: The Pirate Ray contributors
provides:
worker: pirate_ray_jcz.worker
maturity: experimental

View file

@ -0,0 +1,33 @@
{
"upstream": "meerk40t/meerk40t",
"commit": "d511944e08788e88077727f682957759884ff5ca",
"transport": "MockConnection",
"usb_access": "trapped; none attempted",
"input": {
"power_per_mille": 500,
"speed_mm_per_s": 250.0,
"frequency_khz": 40.0,
"pulse_width_ns": 200,
"geometry": "closed synthetic square in controller coordinates"
},
"trace": [
"0007:0000:0000:0000:0000:0000 GetVersion",
"8026:00c8:0000:0000:0000:0000 listFiberYLPMPulseWidth",
"8006:04a7:0000:0000:0000:0000 listJumpSpeed",
"801b:01f4:0000:0000:0000:0000 listQSwitchPeriod",
"8012:0800:0000:0000:0000:0000 listMarkCurrent",
"800c:0094:0000:0000:0000:0000 listMarkSpeed",
"8007:0064:0000:0000:0000:0000 listLaserOnDelay",
"8008:0064:0000:0000:0000:0000 listLaserOffDelay",
"800f:000a:0000:0000:0000:0000 listPolygonDelay",
"8001:7000:7000:0000:16a0:0000 listJumpTo",
"8005:7100:7000:0000:0100:0000 listMarkTo",
"8005:7100:7100:0000:0100:0000 listMarkTo",
"8005:7000:7100:0000:0100:0000 listMarkTo",
"8005:7000:7000:0000:0100:0000 listMarkTo",
"8003:0064:0000:0000:0000:0000 listLaserOnPoint",
"8002:0000:0000:0000:0000:0000 listEndOfList",
"... repeated 241 times ...",
"0019:0000:0000:0000:0000:0000 SetEndOfList"
]
}

View file

@ -0,0 +1,24 @@
{
"upstream": "barebaric/rayforge",
"commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"rayforge_api_version": 23,
"addon": "the_pirate_ray_jcz",
"driver": "PirateRayJCZNoDeviceDriver",
"uses_gcode": false,
"discovery": null,
"transport": null,
"binary_payload_hex": "5b7b22636f6d6d616e64223a226f66666c696e655f70726f6265227d5d",
"lifecycle": [
"load",
"disable",
"enable",
"reload",
"disable"
],
"packaged_files": [
"pirate_ray_jcz/__init__.py",
"pirate_ray_jcz/worker.py",
"rayforge-addon.yaml"
],
"registration_seam": "worker import side effect + on_unload cleanup"
}

View file

@ -0,0 +1,50 @@
{
"rayforge_commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"balormk_commit": "d511944e08788e88077727f682957759884ff5ca",
"transport": null,
"synthetic_field_mm": 110.0,
"rayforge_ops_count": 13,
"semantic_trace": [
{"command": "job_start"},
{"command": "power", "fraction": 0.5, "balor_word": 2048},
{"command": "mark_speed", "mm_per_s": 250.0, "balor_word": 148},
{"command": "jump_speed", "mm_per_s": 2000.0, "balor_word": 1191},
{"command": "frequency", "hz": 40000.0, "balor_qswitch_period": 500},
{"command": "pulse_width", "nanoseconds": 200.0},
{"command": "jump", "x": 28672, "y": 28672},
{"command": "mark", "x": 28928, "y": 28672},
{"command": "mark", "x": 28928, "y": 28928},
{"command": "mark", "x": 28672, "y": 28928},
{"command": "mark", "x": 28672, "y": 28672},
{"command": "laser_on_point", "duration_ms": 1.0, "balor_10us_units": 100},
{"command": "job_end"}
],
"command_records_hex": [
"128000080000000000000000",
"0c8094000000000000000000",
"0680a7040000000000000000",
"1b80f4010000000000000000",
"2680c8000000000000000000",
"0180007000700000a0160000",
"058000710070000000010000",
"058000710071000000010000",
"058000700071000000010000",
"058000700070000000010000",
"038064000000000000000000",
"028000000000000000000000"
],
"balormk_equivalence": {
"power_50_percent": true,
"speed_250_mm_s": true,
"frequency_40_khz": true,
"pulse_width_200_ns": true,
"square_coordinates": true,
"dwell_1_ms": true
},
"limitations": [
"12-byte command records only; no list buffering or transport",
"record ordering follows the Rayforge Ops stream",
"synthetic field mapping; no lens correction",
"pulse width treated as nanoseconds from LaserStep UI contract"
]
}

View file

@ -0,0 +1,38 @@
{
"rayforge_commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"profile": "synthetic-jpt-mopa-110-v1",
"transport": null,
"cases": [
{
"case": "profile_mismatch",
"result": "rejected",
"reason": "profile mismatch: encoder refuses to produce a JCZ trace"
},
{
"case": "out_of_field_geometry",
"result": "rejected",
"reason": "coordinate is outside the synthetic 110 mm field"
},
{
"case": "power_above_one",
"result": "rejected",
"reason": "power must be between 0 and 1"
},
{
"case": "non_positive_frequency",
"result": "rejected",
"reason": "frequency must be positive"
},
{
"case": "non_positive_pulse_width",
"result": "rejected",
"reason": "pulse width must be positive"
},
{
"case": "unsupported_ops_command",
"result": "rejected",
"reason": "unsupported Ops command: CUSTOM"
}
],
"all_failed_closed": true
}

View file

@ -0,0 +1,44 @@
import importlib.util
import json
import sys
import types
import unittest
from pathlib import Path
from unittest.mock import patch
SCRIPT = Path(__file__).parents[1] / "scripts" / "spike_meerk40t_mock_mopa.py"
FIXTURE = Path(__file__).parent / "fixtures" / "meerk40t-mock-mopa-trace.json"
SPEC = importlib.util.spec_from_file_location("spike_meerk40t_mock_mopa", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(MODULE)
class MockSpikeSafetyTests(unittest.TestCase):
def test_golden_trace_records_pin_mock_and_mopa_values(self):
result = json.loads(FIXTURE.read_text(encoding="utf-8"))
trace = "\n".join(result["trace"])
self.assertEqual(MODULE.PINNED_COMMIT, result["commit"])
self.assertEqual("MockConnection", result["transport"])
self.assertEqual("trapped; none attempted", result["usb_access"])
self.assertIn("801b:01f4", trace)
self.assertIn("8026:00c8", trace)
self.assertEqual(4, trace.count("listMarkTo"))
def test_usb_trap_rejects_enumeration(self):
with patch.dict(sys.modules, clear=False):
MODULE._install_usb_trap()
import usb.core
with self.assertRaisesRegex(AssertionError, "real USB access"):
usb.core.find(idVendor=0x9588, idProduct=0x9899)
def test_rejects_unpinned_checkout(self):
with patch.object(MODULE, "_commit_of", return_value="wrong"):
with self.assertRaisesRegex(RuntimeError, "expected MeerK40t"):
MODULE.run(Path("unused"))
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,91 @@
import json
import unittest
from pathlib import Path
ROOT = Path(__file__).parents[1]
ADDON = ROOT / "spikes" / "rayforge-jcz-no-device-addon"
FIXTURE = ROOT / "tests" / "fixtures" / "rayforge-addon-spike.json"
EQUIVALENCE_FIXTURE = (
ROOT / "tests" / "fixtures" / "rayforge-jcz-equivalence.json"
)
NEGATIVE_FIXTURE = (
ROOT / "tests" / "fixtures" / "rayforge-jcz-negative-cases.json"
)
class RayforgeAddonSpikeTests(unittest.TestCase):
def test_golden_result_is_hardware_free_binary_driver(self):
result = json.loads(FIXTURE.read_text(encoding="utf-8"))
self.assertFalse(result["uses_gcode"])
self.assertIsNone(result["discovery"])
self.assertIsNone(result["transport"])
self.assertEqual(
["load", "disable", "enable", "reload", "disable"],
result["lifecycle"],
)
self.assertEqual(23, result["rayforge_api_version"])
def test_addon_contains_no_hardware_transport_import(self):
worker = (ADDON / "pirate_ray_jcz" / "worker.py").read_text(
encoding="utf-8"
)
compact = worker.lower()
for forbidden in ("import usb", "import serial", "import socket"):
self.assertNotIn(forbidden, compact)
self.assertIn("discovery = none", compact)
self.assertIn("uses_gcode = false", compact)
def test_manifest_and_golden_package_agree(self):
result = json.loads(FIXTURE.read_text(encoding="utf-8"))
actual = sorted(
path.relative_to(ADDON).as_posix()
for path in ADDON.rglob("*")
if path.is_file() and "__pycache__" not in path.parts
)
self.assertEqual(result["packaged_files"], actual)
def test_semantic_equivalence_fixture_has_no_failed_mapping(self):
result = json.loads(EQUIVALENCE_FIXTURE.read_text(encoding="utf-8"))
self.assertIsNone(result["transport"])
self.assertEqual(13, result["rayforge_ops_count"])
self.assertTrue(all(result["balormk_equivalence"].values()))
commands = [item["command"] for item in result["semantic_trace"]]
self.assertEqual(4, commands.count("mark"))
self.assertIn("pulse_width", commands)
self.assertIn("laser_on_point", commands)
def test_packet_records_match_pinned_balor_values(self):
result = json.loads(EQUIVALENCE_FIXTURE.read_text(encoding="utf-8"))
records = result["command_records_hex"]
self.assertTrue(all(len(record) == 24 for record in records))
self.assertEqual(
[
"128000080000000000000000",
"0c8094000000000000000000",
"0680a7040000000000000000",
"1b80f4010000000000000000",
"2680c8000000000000000000",
"0180007000700000a0160000",
"058000710070000000010000",
"058000710071000000010000",
"058000700071000000010000",
"058000700070000000010000",
"038064000000000000000000",
"028000000000000000000000",
],
records,
)
def test_negative_cases_all_fail_closed_without_transport(self):
result = json.loads(NEGATIVE_FIXTURE.read_text(encoding="utf-8"))
self.assertTrue(result["all_failed_closed"])
self.assertIsNone(result["transport"])
self.assertEqual(6, len(result["cases"]))
self.assertEqual(
{"rejected"}, {case["result"] for case in result["cases"]}
)
if __name__ == "__main__":
unittest.main()