Metadata-Version: 2.4
Name: gantrix
Version: 0.1.0
Summary: A framework for controlling gantry-based (GRBL) devices with swappable tool heads — laser, solenoid, pen, and more.
Author: devk-op
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyserial>=3.5
Provides-Extra: gui
Requires-Dist: fastapi>=0.110; extra == "gui"
Requires-Dist: uvicorn>=0.29; extra == "gui"
Provides-Extra: design
Requires-Dist: Pillow>=10.0; extra == "design"
Requires-Dist: qrcode>=7.4; extra == "design"
Requires-Dist: svgpathtools>=1.6; extra == "design"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Dynamic: license-file

# gantrix

A small framework for driving GRBL-based gantry devices with swappable tool
heads — laser engraver today, whatever else you bolt to the carriage later
(solenoid stamp, pen plotter, spray head, ...). Grew out of a one-off laser
text-engraving script; generalized so the same motion/fill planning works
for any tool, without being locked to the laser.

## Layout

- `gantrix.transport` — serial connection to GRBL, command/ack handshake,
  homes with the tool forced off first (a previous crashed run can leave a
  tool physically engaged even after the host process dies). A command
  that gets no reply at all (dead link, controller reset) raises `GrblError`
  rather than being silently swallowed; a mid-command controller reboot is
  detected from its boot banner instead of waiting out the full timeout.
  `emergency_stop()` sends GRBL's real-time soft-reset byte directly,
  bypassing the normal command queue, so it works even while another
  thread is blocked waiting on a stuck command.
- `gantrix.tools` — the `Tool` plugin interface (`engage`/`disengage` +
  feed rates). `LaserTool` (M3/M5 with power) is the only concrete
  implementation shipped so far; new device types are added by subclassing
  `Tool` (see `src/gantrix/tools/laser.py` as the reference).
- `gantrix.design` — turns a design into `Shape`s the planner can sweep.
  `design.text` is a small stroke font (rects + tapered diagonals).
  `design.shapes.RasterShape` is a generic bitmap-grid `Shape`, backing both
  `design.qrcode` (any QR-encodable data) and `design.image` (any image —
  logo, silhouette, photo — thresholded to black/white at a configurable
  resolution). True vector/SVG import (arbitrary curves, not just bitmaps)
  is the natural next addition here.
- `gantrix.planner.fill` — generic boustrophedon (zigzag raster) fill:
  given anything exposing `spans_at_y(y)`, sweeps scanlines and emits
  direction-alternating segments that minimize travel.
- `gantrix.job` — wires a design + tool + transport into a runnable job.
  Accepts either a port string (opens/closes its own connection — CLI
  usage) or an already-connected transport (keeps it open — GUI usage).
- `gantrix.gui.app` — preview a design as SVG, connect/disconnect a serial
  port, and run a job — all from the browser. `gantrix.gui.connection`
  holds the one live serial connection the process manages;
  `gantrix.gui.job_runner` runs a `Job` in a background thread (so the
  HTTP request returns immediately) and exposes progress + cancellation
  for the page to poll. Only one job at a time — it's a single physical
  device, not a queue. Two ways to stop: **Cancel** is cooperative (checked
  between fill segments — fine for "I want a different design," not fast
  enough to rely on if something's actually wrong); **EMERGENCY STOP** hits
  the hardware directly via `emergency_stop()` and works regardless of
  whether a command is stuck. **Home** manually re-homes ($H) when
  connected and no job is running.

## Quickstart

```bash
pip install -e ".[dev,gui,design]"
pytest                                  # no hardware needed

# preview a design in the browser (no hardware needed)
uvicorn gantrix.gui.app:app --reload
# open http://127.0.0.1:8000

# run it for real
gantrix text HAND --port /dev/cu.usbserial-XXXXXX
gantrix qr "https://example.com" --port /dev/cu.usbserial-XXXXXX --cell-size 1.5
gantrix image logo.png --port /dev/cu.usbserial-XXXXXX --height 40 --cell-size 0.3
```

## Adding a new device

Swapping tools without touching the design or planner is the point of the
`Tool` split. To support a new actuator, subclass `Tool` and implement
`engage`/`disengage` (see `src/gantrix/tools/laser.py`):

```python
class MyTool(Tool):
    def engage(self, transport):
        transport.send("M62 P1")   # e.g. a GRBL digital output pin

    def disengage(self, transport):
        transport.send("M63 P1")
```

Then use it exactly like `LaserTool` — same `layout_text`/`layout_qr`
output, same `Job`, just a different tool instance passed in.

## Status

Early scaffold. Laser path is validated against a working engrave script.
QR fill segments have been round-trip verified (reconstructing the module
grid from planned segments reproduces the source matrix exactly) and
rendered to confirm the code is well-formed. Only one concrete `Tool`
(laser) ships so far — the interface is there for more, none built yet.
Vector/SVG import and shape-drawing in the GUI (not just text/QR forms) are
not built yet either. The GUI can connect/disconnect, run a job (text or
QR), Cancel, Home, and hardware-level EMERGENCY STOP. A real incident
during testing (the controller stopped responding mid-job) surfaced that
failures weren't being surfaced at all — `send()` used to return `False`
on a dead link and nothing checked it, so the job just kept retrying into
the void with no visible error. Fixed: failures now raise and show up in
the job's `error` field instead of hanging silently.
