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:
wrickertandClaude Opus 5.5 committed 2026-10-04 10:24:05 -05:00
commit 4e957509cc
11 files changed
+695

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
# 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.