Files
wrickertandClaude Opus 5.5 4e957509cc Ruida laser skill: prepare and load jobs, never run them
FreeCAD/DXF to .rd conversion, named upload to controller memory with
idle check and read-back verification, read-only status/file listing,
and a fake controller for dry runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 10:24:05 -05:00

88 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""dxf2rd.py — DXF (mm, LINE entities) -> Ruida .rd job FILE. Offline only.
Writes bytes to disk. Contains NO networking: it never talks to the laser.
A person loads/runs jobs at the laser (see SKILL.md, "Safety").
usage: dxf2rd.py in.dxf out.rd --speed 25 --max 65 --min 55 --passes 2 [--air]
"""
import argparse, sys
from meerk40t.ruida.rdjob import RDJob, swizzles_lut
UM = 1000 # Ruida native units are micrometres
def read_lines(path):
t = open(path).read().split("\n")
out = []
for i, v in enumerate(t):
if v.strip() == "LINE":
g = dict(zip([s.strip() for s in t[i + 1:i + 17:2]], [s.strip() for s in t[i + 2:i + 18:2]]))
out.append(((float(g["10"]), float(g["20"])), (float(g["11"]), float(g["21"]))))
return out
def chain(segs, tol=1e-3):
key = lambda p: (round(p[0] / tol), round(p[1] / tol))
segs, loops = segs[:], []
while segs:
a, b = segs.pop(0)
path = [a, b]
while key(path[-1]) != key(path[0]):
for k, (p, q) in enumerate(segs):
if key(p) == key(path[-1]): path.append(q); segs.pop(k); break
if key(q) == key(path[-1]): path.append(p); segs.pop(k); break
else:
break # open path: cut as-is
loops.append(path)
return loops
class Box:
def __init__(self, pts):
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
self._b = (min(xs), min(ys), max(xs), max(ys))
def left(self): return self._b[0]
def upper(self): return self._b[1]
def right(self): return self._b[2]
def lower(self): return self._b[3]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("dxf"); ap.add_argument("rd")
ap.add_argument("--speed", type=float, required=True, help="mm/s")
ap.add_argument("--max", type=float, required=True, help="max power %%")
ap.add_argument("--min", type=float, required=True, help="min power %%")
ap.add_argument("--passes", type=int, default=1)
ap.add_argument("--air", action="store_true")
ap.add_argument("--offset", type=float, nargs=2, default=(10, 10), help="mm from origin")
a = ap.parse_args()
assert 10 <= a.min <= a.max <= 80, "power outside 10-80% (shop rule)"
assert a.speed >= 5, "speed below 5 mm/s is a fire risk on wood"
loops = chain(read_lines(a.dxf))
ox, oy = a.offset
loops = [[(int(round((x + ox) * UM)), int(round((y + oy) * UM))) for x, y in lp] for lp in loops]
job = RDJob(magic=0x88)
allpts = [p for lp in loops for p in lp]
job.write_header([Box(allpts)])
job.write_settings({"speed": a.speed, "power": a.max * 10, "coolant": 1 if a.air else 2})
job.min_power_1(a.min); job.min_power_2(a.min) # Ruida min power: fires during accel/decel
cx = cy = None
for _ in range(a.passes):
for lp in loops:
x0, y0 = lp[0]
job.move_abs_xy(x0, y0); cx, cy = x0, y0
for x, y in lp[1:]:
job.mark(x, y, x - cx, y - cy); cx, cy = x, y
job.write_tail()
data = job.get_contents()
sw, _ = swizzles_lut(0x88)
open(a.rd, "wb").write(bytes(sw[b] for b in data))
print(f"{a.rd}: {len(loops)} paths x {a.passes} passes, {len(data)} bytes")
if __name__ == "__main__":
sys.exit(main())