Validate MeerK40t hatch planning on mock spooler
This commit is contained in:
parent
a63a4ee4a6
commit
ceb70be30e
6 changed files with 226 additions and 3 deletions
|
|
@ -89,6 +89,9 @@ Subsequent offline evidence:
|
|||
|
||||
- [MeerK40t mock MOPA spike](../research/meerk40t-mock-mopa-spike.md):
|
||||
deterministic controller-level frequency, pulse-width and square encoding;
|
||||
- [MeerK40t hatch cut-plan and mock-spooler spike](../research/meerk40t-hatch-cutplan-spike.md):
|
||||
stock planning and spooling preserve a synthetic hatch and MOPA settings on
|
||||
forced mock transport;
|
||||
- [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):
|
||||
|
|
|
|||
36
docs/research/meerk40t-hatch-cutplan-spike.md
Normal file
36
docs/research/meerk40t-hatch-cutplan-spike.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# MeerK40t hatch cut-plan and mock-spooler spike
|
||||
|
||||
- Date: 2026-10-18
|
||||
- MeerK40t commit: `d511944e08788e88077727f682957759884ff5ca`
|
||||
- Scope: synthetic hatch through stock planning and mock spooling
|
||||
|
||||
## Result
|
||||
|
||||
A synthetic 2 mm square was assigned MeerK40t's default hatch effect and run
|
||||
through the stock `copy`, `preprocess`, `validate`, `blob`, `preopt` and
|
||||
`optimize` stages. The resulting plan contained one hatch mark line for this
|
||||
small fixture and preserved 50 percent power, 250 mm/s speed, 40 kHz frequency
|
||||
and 200 ns pulse width in the Balor command list.
|
||||
|
||||
The same plan was submitted to the stock spooler. It completed using
|
||||
`MockConnection`, and mock packets were observed. A PyUSB trap was installed
|
||||
before MeerK40t imports; any USB discovery or disposal attempt would fail the
|
||||
run. Two runs produced identical golden output with SHA-256
|
||||
`52c20ea7ef7221ff0321386e26e200dbec3b13d0223488b9d2e83236fd713f47`.
|
||||
|
||||
## What this proves
|
||||
|
||||
- Stock MeerK40t can turn a hatch effect into Balor mark geometry.
|
||||
- MOPA frequency and pulse width survive the cut-plan and LaserJob boundary.
|
||||
- The stock spooler can complete the synthetic job on forced mock transport.
|
||||
|
||||
## Remaining limits
|
||||
|
||||
- The fixture uses MeerK40t's default synthetic hatch configuration; it does
|
||||
not establish a production hatch recipe or machine-safe parameter range.
|
||||
- It does not test the graphical workflow, project import, correction files,
|
||||
framing, cancellation, status or recovery.
|
||||
- It establishes no compatibility with physical hardware.
|
||||
|
||||
No USB enumeration, controller command, galvo motion or laser emission was
|
||||
attempted.
|
||||
|
|
@ -107,9 +107,14 @@ The first work is entirely offline:
|
|||
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.
|
||||
through BalorMK's forced mock transport. That first spike left cut-plan,
|
||||
spooler and hatch coverage open and did not settle ADR 0002.
|
||||
|
||||
[The hatch cut-plan and mock-spooler follow-up](research/meerk40t-hatch-cutplan-spike.md)
|
||||
now confirms that stock planning turns a synthetic hatch into Balor geometry,
|
||||
preserves frequency and pulse width, and completes through `MockConnection`
|
||||
with USB access trapped. Graphical workflow, import, correction, cancellation
|
||||
and recovery coverage remain open.
|
||||
|
||||
The first Rayforge addon proof is recorded in
|
||||
[the external binary-driver addon spike](research/rayforge-addon-spike.md).
|
||||
|
|
|
|||
128
scripts/spike_meerk40t_hatch_cutplan.py
Normal file
128
scripts/spike_meerk40t_hatch_cutplan.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""Run a synthetic MOPA hatch through MeerK40t planning without hardware."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from spike_meerk40t_mock_mopa import PINNED_COMMIT, _install_usb_trap
|
||||
|
||||
|
||||
def _commit_of(source: Path) -> str:
|
||||
return subprocess.run(
|
||||
["git", "-C", str(source), "rev-parse", "HEAD"],
|
||||
check=True, capture_output=True, text=True,
|
||||
).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 test import bootstrap
|
||||
from meerk40t.balormk.mock_connection import MockConnection
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
job_path = Path(temporary) / "synthetic-hatch.lmc"
|
||||
kernel = bootstrap.bootstrap(profile="ThePirateRay_HATCH")
|
||||
try:
|
||||
kernel.console("service device start -i balor 0\n")
|
||||
device = kernel.device
|
||||
device.mock = True
|
||||
device.pulse_width_enabled = True
|
||||
device.default_pulse_width = 200
|
||||
kernel.console("rect 54mm 54mm 2mm 2mm\n")
|
||||
kernel.console("operation* delete\n")
|
||||
kernel.console("hatch\n")
|
||||
operation = list(kernel.elements.ops())[0]
|
||||
hatch_effect = operation.children[0]
|
||||
if hatch_effect.type != "effect hatch":
|
||||
raise AssertionError(
|
||||
f"hatch command created unexpected node: {hatch_effect.type}"
|
||||
)
|
||||
element = list(kernel.elements.elems())[0]
|
||||
hatch_effect.add_reference(element)
|
||||
operation.speed = 250.0
|
||||
operation.power = 500
|
||||
operation.frequency = 40.0
|
||||
operation.pulse_width_enabled = True
|
||||
operation.pulse_width = 200
|
||||
kernel.console(
|
||||
f'plan clear copy preprocess validate blob preopt optimize save_job "{job_path}"\n'
|
||||
)
|
||||
saved = job_path.read_text(encoding="utf-8")
|
||||
|
||||
controller = device.driver.connection
|
||||
controller.connect_if_needed()
|
||||
if not isinstance(controller.connection, MockConnection):
|
||||
raise AssertionError("spooler did not select MockConnection")
|
||||
packets: list[str] = []
|
||||
controller.connection.send = packets.append
|
||||
controller.connection.set_implied_response(
|
||||
struct.pack("<4H", 0, 0, 0, 0x20)
|
||||
)
|
||||
kernel.console("plan spool\n")
|
||||
deadline = time.monotonic() + 10.0
|
||||
while device.spooler.queue and time.monotonic() < deadline:
|
||||
time.sleep(0.02)
|
||||
if device.spooler.queue:
|
||||
raise TimeoutError("mock spooler did not become idle")
|
||||
finally:
|
||||
kernel()
|
||||
|
||||
lines = saved.splitlines()
|
||||
mark_lines = [line for line in lines if line.startswith("listMarkTo")]
|
||||
required = ("listQSwitchPeriod", "listFiberYLPMPulseWidth", "listEndOfList")
|
||||
if any(not any(line.startswith(item) for line in lines) for item in required):
|
||||
raise AssertionError("planned hatch lost required MOPA/list commands")
|
||||
if not mark_lines:
|
||||
raise AssertionError(f"planned output contains no mark geometry: {lines}")
|
||||
return {
|
||||
"upstream": "meerk40t/meerk40t",
|
||||
"commit": commit,
|
||||
"transport": "MockConnection",
|
||||
"usb_access": "trapped; none attempted",
|
||||
"input": {
|
||||
"geometry": "synthetic 2 mm square with default hatch effect",
|
||||
"power_per_mille": 500,
|
||||
"speed_mm_per_s": 250.0,
|
||||
"frequency_khz": 40.0,
|
||||
"pulse_width_ns": 200,
|
||||
},
|
||||
"cutplan": {
|
||||
"command_count": len(lines),
|
||||
"mark_count": len(mark_lines),
|
||||
"contains_frequency": any("listQSwitchPeriod" in line for line in lines),
|
||||
"contains_pulse_width": any("listFiberYLPMPulseWidth" in line for line in lines),
|
||||
},
|
||||
"spooler": {
|
||||
"completed": True,
|
||||
"mock_packets_observed": bool(packets),
|
||||
},
|
||||
"trace": lines,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source", type=Path)
|
||||
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", newline="\n")
|
||||
else:
|
||||
print(rendered, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
40
tests/fixtures/meerk40t-hatch-cutplan.json
vendored
Normal file
40
tests/fixtures/meerk40t-hatch-cutplan.json
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"upstream": "meerk40t/meerk40t",
|
||||
"commit": "d511944e08788e88077727f682957759884ff5ca",
|
||||
"transport": "MockConnection",
|
||||
"usb_access": "trapped; none attempted",
|
||||
"input": {
|
||||
"geometry": "synthetic 2 mm square with default hatch effect",
|
||||
"power_per_mille": 500,
|
||||
"speed_mm_per_s": 250.0,
|
||||
"frequency_khz": 40.0,
|
||||
"pulse_width_ns": 200
|
||||
},
|
||||
"cutplan": {
|
||||
"command_count": 15,
|
||||
"mark_count": 1,
|
||||
"contains_frequency": true,
|
||||
"contains_pulse_width": true
|
||||
},
|
||||
"spooler": {
|
||||
"completed": true,
|
||||
"mock_packets_observed": true
|
||||
},
|
||||
"trace": [
|
||||
"listReadyMark 0000 0000 0000 0000 0000",
|
||||
"listDelayTime 0320 0000 0000 0000 0000",
|
||||
"listWritePort 0001 0000 0000 0000 0000",
|
||||
"listJumpSpeed 04a7 0000 0000 0000 0000",
|
||||
"listFiberYLPMPulseWidth 00c8 0000 0000 0000 0000",
|
||||
"listQSwitchPeriod 01f4 0000 0000 0000 0000",
|
||||
"listMarkCurrent 0800 0000 0000 0000 0000",
|
||||
"listMarkSpeed 0094 0000 0000 0000 0000",
|
||||
"listLaserOnDelay 0064 0000 0000 0000 0000",
|
||||
"listLaserOffDelay 0064 0000 0000 0000 0000",
|
||||
"listPolygonDelay 000a 0000 0000 0000 0000",
|
||||
"listJumpTo 7fff 8253 0000 0253 0000",
|
||||
"listMarkTo 7fff 7dac 0000 04a7 0000",
|
||||
"listDelayTime 001e 0000 0000 0000 0000",
|
||||
"listEndOfList 0000 0000 0000 0000 0000"
|
||||
]
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ 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"
|
||||
HATCH_FIXTURE = Path(__file__).parent / "fixtures" / "meerk40t-hatch-cutplan.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
|
||||
|
|
@ -16,6 +17,16 @@ SPEC.loader.exec_module(MODULE)
|
|||
|
||||
|
||||
class MockSpikeSafetyTests(unittest.TestCase):
|
||||
def test_hatch_cutplan_and_spooler_stay_on_mock_transport(self):
|
||||
result = json.loads(HATCH_FIXTURE.read_text(encoding="utf-8"))
|
||||
self.assertEqual("MockConnection", result["transport"])
|
||||
self.assertEqual("trapped; none attempted", result["usb_access"])
|
||||
self.assertTrue(result["spooler"]["completed"])
|
||||
self.assertTrue(result["spooler"]["mock_packets_observed"])
|
||||
self.assertEqual(1, result["cutplan"]["mark_count"])
|
||||
self.assertTrue(result["cutplan"]["contains_frequency"])
|
||||
self.assertTrue(result["cutplan"]["contains_pulse_width"])
|
||||
|
||||
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"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue