ThePirateRay/scripts/spike_rayforge_jcz_cancel_recovery.py
2026-10-19 19:35:08 +02:00

130 lines
3.9 KiB
Python

"""Exercise Rayforge-style async cancellation on the offline JCZ driver."""
from __future__ import annotations
import argparse
import asyncio
import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock
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 _ops(Ops, line_count: int):
ops = Ops()
ops.job_start()
ops.set_power(0.5)
ops.set_feed_rate(15_000)
ops.set_rapid_rate(120_000)
ops.set_frequency(40_000)
ops.set_pulse_width(200)
ops.move_to(50, 50, 0)
for index in range(line_count):
ops.line_to(51 + index % 2, 50 + index % 2, 0)
ops.job_end()
return ops
async def _exercise(Driver, Ops) -> dict[str, object]:
machine = MagicMock()
machine.coordinate_systems = {}
driver = Driver(MagicMock(), machine)
encoder = driver.create_encoder(machine)
long_ops = _ops(Ops, 260)
encoded = encoder.encode(long_ops)
buffered_before_cancel = sum(
len(packet) for packet in encoded.driver_data["list_buffers"]
)
run_task = asyncio.create_task(driver.run(encoded, None, long_ops))
await asyncio.sleep(0)
await driver.set_hold(True)
held_state = driver.offline_session.state.value
await driver.set_hold(False)
resumed_state = driver.offline_session.state.value
await driver.cancel()
await asyncio.wait_for(run_task, timeout=2)
cancelled_state = driver.offline_session.state.value
buffered_after_cancel = driver.offline_session.buffered_bytes
restart_rejected = False
try:
driver.offline_session.start()
except RuntimeError:
restart_rejected = True
driver.offline_session.recover()
recovered_state = driver.offline_session.state.value
recovery_ops = _ops(Ops, 2)
recovery_encoded = encoder.encode(recovery_ops)
await asyncio.wait_for(
driver.run(recovery_encoded, None, recovery_ops), timeout=2
)
return {
"transport": None,
"long_job_buffer_count": len(encoded.driver_data["list_buffers"]),
"buffered_bytes_before_cancel": buffered_before_cancel,
"held_state": held_state,
"resumed_state": resumed_state,
"cancelled_state": cancelled_state,
"buffered_bytes_after_cancel": buffered_after_cancel,
"restart_without_recovery_rejected": restart_rejected,
"recovered_state": recovered_state,
"second_job_state": driver.offline_session.state.value,
"events": driver.offline_session.events,
"hardware_commands": [],
}
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))
sys.path.insert(0, str(addon))
from pirate_ray_jcz.worker import PirateRayJCZNoDeviceDriver
from raygeo.ops import Ops
result = asyncio.run(_exercise(PirateRayJCZNoDeviceDriver, Ops))
result["rayforge_commit"] = commit
return result
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", newline="\n")
else:
print(rendered, end="")
if __name__ == "__main__":
main()