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

3.6 KiB

Ruida UDP protocol notes

What these scripts rely on, where it came from, and what was confirmed against a real RDC644x-class controller (60 W CO2 laser, firmware reporting 49 stored files).

Transport

  • UDP. Host sends from port 40200 to the controller's port 50200.
  • Every byte in both directions is swizzled with a per-model "magic" value. Ours is 0x88 (the common value for 644x boards over UDP). meerk40t.ruida.rdjob.swizzles_lut(magic) returns the encode/decode tables. If the controller never ACKs anything, the magic may differ; RDWorks .rd files from the same machine use the same magic, and MeerK40t can guess it from one.
  • Host datagram = 2-byte big-endian sum of the swizzled payload bytes, then the payload. At most 998 payload bytes per datagram.
  • The controller answers each datagram with a single byte: ACK CC (C6 on the wire), NAK CF, ENQ CE (keep-alive). Wait for the ACK before sending the next datagram (3 s timeout).
  • Read commands get an ACK followed by a separate, swizzled reply datagram (no checksum prefix).

Commands are bytes ≥ 0x80

A command starts with a byte ≥ 0x80; its arguments are all < 0x80 (7-bit encodings). That makes a stream self-delimiting: split at every byte ≥ 0x80. Integers are big-endian 7-bit groups: encode14 (2 bytes), encode32/decodeu35 (5 bytes). Coordinates are micrometres.

Storing a job in memory (rdupload.py)

Follows LibLaserCut / VisiCut's Ruida driver. Insert two commands right after the job's preamble (D8 12, E6 01, F0):

E8 02                 file transfer: store, don't run
E7 01 <name> 00       file name, ASCII, 1-8 chars, null-terminated

then send the whole job (ending D7, end of file) as datagrams. Drop any E5 05 (file sum); LibLaserCut sends none and the controller accepted the file without it.

Confirmed: the file appears in the panel's file list and runs correctly (clean cut, settings as encoded). Confirmed failure mode: uploading while a job is running gets every packet ACKed and the file silently discarded.

Read-only queries (rdlist.py)

Send Reply Notes
DA 00 04 00 DA 01 04 00 <u35> Status bits. Bit 0 = job running: confirmed (0x20018401 mid-cut, 0x2001x600/0x2403x600 idle). Bit 24 = moving, bit 1 = part end (from MeerK40t, unconfirmed). Other bits change while idle; ignore them.
DA 00 02 00 DA 01 02 00 <u35> MeerK40t's emulator says 21/22/23 = running/idle/paused. Ours read 33313 whether cutting or idle: useless here.
DA 00 02 05 DA 01 02 05 <u35> "Total doc number". Read 0 with 49 files stored. Don't trust it.
E8 01 <n14> E8 01 <n14> NAME 00 Name of stored file n, 1-based, oldest first. Past the last file the controller doesn't reply at all; that timeout is the end-of-list marker. Confirmed.

Commands this skill must never send

Bytes Effect
D8 00 Start process (runs the current job)
E8 03 <n14> Select document n: starts it
E8 00 ... Delete document(s); E8 00 00 00 00 00 deletes all
E8 04 <n14> Calculate document time (harmless, but not needed)
A5 ... Panel keys (frame, origin, jog, pulse)
D9 ..., 88, 89 Moves

rdlist.py enforces this with an exact-match whitelist checked before the socket.

Sources

  • MeerK40t (meerk40t/ruida/): command table, swizzle, emulator, status bits. MIT.
  • LibLaserCut (VisiCut) Ruida driver: file-transfer header and UDP framing.
  • Everything marked "confirmed" was observed on the real controller; the rest is from those sources and is labelled as such.