# 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 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 ` | 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 ` | 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 ` | "Total doc number". Read **0** with 49 files stored. Don't trust it. | | `E8 01 ` | `E8 01 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 ` | Select document n: **starts** it | | `E8 00 ...` | Delete document(s); `E8 00 00 00 00 00` deletes all | | `E8 04 ` | 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.