# @leumas/machines

Drive a machine that makes something — 3D printers, CNC mills, routers and laser engravers. An isomorphic core (G-code lexer, dialect table, ONE credit-based streamer covering both Marlin's ok-acking...


Drive a machine that makes something — a 3D printer, a CNC mill or router, a laser engraver.

Studio's Devices domain could already *see* almost every kind of device a customer owns and could drive
none of them. This is the layer underneath the **Printers**, **CNC / PCB** and **Serial** tabs: a G-code
lexer, a firmware dialect table, one streamer that covers both flow-control models, a job runner and the
safety interlocks. It does no I/O; transports are injected, so the same session runs over a host serial
port, over a LeumasNode, and over a mock.

Roadmap: [`ops/todos/devices-machines-roadmap.md`](../../../ops/todos/devices-machines-roadmap.md).

## The one idea

A printer, a mill and a laser are the **same code path plus a small table**. Everything that differs
between firmwares is declared in a dialect; everything above the dialect is written once.

```js
import { createStreamer } from '@leumas/machines';
import { dialectFor } from '@leumas/machines/dialects';

const streamer = createStreamer({ dialect: dialectFor('grbl'), write, writeBytes });
streamer.enqueue('G1 X10 Y10 F1000');
streamer.feed(dialect.parseLine(lineFromTheWire));   // an ack frees a slot and sends the next line
await streamer.realtime('estop');                    // bypasses the queue entirely
```

`can` is **derived** from what a dialect implements, never declared — so a surface renders a temperature
control because `can.setTemp` is true, and there is no `kind === 'fdm'` test anywhere above this layer.
Delete `encode.setTemp` from a dialect and the temperature controls disappear.

## Layout

| | |
|---|---|
| `src/vocab.js` · `limits.js` | the frozen vocabularies and every bound, with the measurement behind each |
| `src/gcode/{lex,lines,modal,scan}.js` | one line classified; chunks → lines; modal state; the preflight |
| `src/dialect.js` | `defineDialect` / `extendDialect` — validated at **definition** time |
| `src/dialects/` | `marlin` · `grbl` · `fluidnc` (≈20 lines) · `reprap` (≈25 lines) |
| `src/streamer.js` | **the one hard file** — see below |
| `src/interlocks.js` | temperature ceiling, jog-while-running, envelope, the laser gate |
| `src/host/` | Node transports over `@leumas/serial` (lazy — absent is `unavailable`, never a crash) |
| `src/mock/` | virtual Marlin and GRBL with a **real** 128-byte buffer |

## The streamer, and why it is one file

Marlin acks line-by-line; GRBL counts characters against a 128-byte receive buffer. Two senders would
mean pause, resume, cancel, progress, checkpoints and the watchdog written twice. Instead both reduce
to *may I send the next line?* and the difference is three declared fields.

Seven traps are written into that file, each a bug that has shipped in real G-code senders:

1. **`error:` credits exactly like `ok`** — GRBL frees the slot for a *rejected* line too. Credit only
   on `ok` and the stream stalls forever after the first bad line. The most common bug in the genre.
2. `busy:` and status reports credit **nothing** — they renew the watchdog, that is all.
3. A line larger than the buffer **deadlocks**; it is refused up front instead.
4. `Resend:` is **fatal, not a retry** — lines are not numbered, so it cannot be honoured honestly.
5. Blocking commands withhold their `ok` for **minutes** (`M109` heat soak, `G28` homing). One fixed
   ack timeout kills every print at its first heat-up, so the timeout is per-line.
6. **Realtime bytes bypass the queue and are never charged.** An emergency stop modelled as a queued
   command waits behind every buffered move.
7. Writes are **serialised** — fire-and-forget interleaves two lines on the wire.

## Testing without hardware

`src/mock/` ships a virtual Marlin and a virtual GRBL. The GRBL board **throws on a buffer overrun**
rather than silently corrupting, so a sender that miscounts fails a test instead of ruining a workpiece.
Its replies are copied from a real capture — including `ok T:0.0 /0.0 B:0.0 /0.0 @:0 B@:0` (the
temperature rides on the ack) and `X:0.00 … Count X: 0 Y:0` (inconsistent spacing), both of which break
a parser written against the tidy documentation examples.

```sh
pnpm --filter @leumas/machines test
```

## What is deliberately absent

- **Klipper.** A Klipper machine's USB port speaks Klipper's own binary MCU protocol, not G-code; it
  cannot be driven over serial at all. A `klipper` dialect would connect, say nothing, and never ack.
- **Line numbering and checksums.** See trap 4.
- **A slicer.** `@leumas/adapter-fabrication-plan` emits a toolchain descriptor and executes nothing.


---
Source: shared/packages/machines/README.md
Canonical: https://docs.leumas.tech/p/packages/machines
