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]>
This commit is contained in:
commit
4e957509cc
11 files changed
+695
No files matched your search
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user