# Ink-signature pen-plotter pipeline

Produces a **genuine original ink signature** on a printed CMS form from a
signature the provider drew online, by redrawing their own stroke paths with a
pen mounted on a 3-axis motion system (a Creality CR-10 V2 here; any
Marlin/GRBL machine works).

## Why this exists

The Standard (no-login) CMS filing path mails a paper form that must carry an
**original ink signature** — the CMS-10114 states verbatim: *"All signatures
must be original and signed in ink. Applications with signatures deemed not
original will not be processed. Stamped, faxed or copied signatures will not be
accepted."* A printed/stamped digital signature image is a copy. A pen plotter
drawing real ink onto the one original sheet is **original, in ink, never
copied** — categorically different from the three banned methods (stamp / fax /
photocopy).

> **Interpretive risk (read this):** CMS guidance does not explicitly bless or
> ban robotic/autopen signatures for the 855/10114. A reviewer could argue a
> machine-applied mark isn't the provider's own hand. Treat the plotter as the
> *fast* path and keep a true wet-signature mail-out as the conservative default
> for filings where a rejection is costly, until real-world acceptance is
> confirmed on a few live filings.

## Data flow

```
signature pad (online)           esign_records
  capture strokes (0..1) ───────► signature_vector (JSONB)   [migration 090]
  + raster PNG          ───────► signature_data  (digital stamp / audit)
                                  signature_anchors (form's signing box)
        │
        ▼
ink_signature_plotter.py  (hardware-independent, fully tested)
  fit_strokes_to_box()  strokes -> PDF points, fit anchor box, flip Y, rest on rule
  pdf_points_to_bed_mm() PDF pt -> bed mm via PlotterConfig (jig offset, 1pt=0.3528mm)
  emit_gcode()          -> Marlin/GRBL G-code (Z pen lift, or M280 servo/BLTouch)
  render_signature_on_pdf() -> stamp strokes onto the real PDF (visual proof)
  render_preview_svg()  -> toolpath preview
  send_gcode_serial()   -> stream to /dev/ttyUSB0 (gated, dry_run=True default)
        │
        ▼
ink_signature_cli.py     end-to-end: load record -> gcode + preview [+ --plot]
```

## Coordinate frames

| Frame | Origin | Units |
|---|---|---|
| Capture box (signature pad) | top-left | fraction 0..1 |
| PDF / signature anchors | bottom-left | points (1in = 72pt) |
| Plotter bed (CR-10) | homed corner (front-left) | mm (1pt = 0.35278mm) |

A **paper jig** (corner stop on the bed) fixes the sheet so PDF (0,0) maps to
`(jig_x_mm, jig_y_mm)`. The signature anchor box (same one the digital stamper
uses) places the ink exactly on the cert line.

## CR-10 V2 retrofit (reversible)

1. **Pen holder:** print a spring-loaded CR-10 pen holder (clips to the X
   carriage / hotend shroud). The spring keeps even contact pressure.
2. **Pen-up/down:**
   - *Default (Z mode):* pen fixed; G-code lifts/drops Z (`pen_up_mm` /
     `pen_down_mm`). The spring forgives over-press.
   - *Optional (servo/BLTouch mode):* `--servo-pen` emits `M280 P0 S<angle>` to
     deploy/stow instead of moving Z. Useful if you actuate the pen with a servo
     or repurpose the BLTouch.
3. **Registration:** tape a corner jig to the bed so every sheet sits in the
   same place. The BLTouch can probe a fixed point to set a repeatable pen-touch
   Z reference.
4. **Pen:** a smooth gel/rollerball refill gives the most hand-written, wet-ink
   look. Avoid dry fiber tips.
5. **Linux:** the CR-10 enumerates as a CH340 serial device (`/dev/ttyUSB0`,
   115200 baud). No drivers needed — we stream G-code ourselves.

## Machine profiles

The pipeline is machine-agnostic via named profiles (`--profile`):

| Profile | Machine | Dialect | Pen lift | Bed/reach |
|---|---|---|---|---|
| `cr10` (default) | Creality CR-10 V2 (home station) | Marlin | Z move | 300×300 mm rectangular |
| `axidraw` | AxiDraw / iDraw A4 pen plotter | Marlin/GRBL | servo (M280) | 210×297 mm rectangular |
| `lineus` | Line-us folding pen-arm (pocket) | Line-us GCode | pen Z per move | small fan-shaped annulus |

`load_profile()` returns a `PlotterConfig`; CLI flags (`--jig-x/-y`,
`--pen-down/-up`, `--servo-pen`) override individual fields.

## Portable option: Line-us (pocket pen-arm)

When away from the home CR-10, the **Line-us** is a palm-size folding 2-link arm
that draws with a real pen. It speaks its own GCode subset over **WiFi (TCP
1337)** or **USB serial**, acks each command with `"ok"`, and encodes pen height
as the **Z value of each move** (low Z = pen down, high Z = pen up), not mm.

Because the arm's reachable area is a **small fan-shaped annulus** in front of
its shoulder pivot (not a rectangular bed), two things differ from the CR-10:

1. **Reach checking.** `check_reach()` validates every planned point against the
   annulus (`reach_min_units..reach_max_units` from the shoulder). The CLI prints
   the report and **refuses to `--plot` on Line-us if any point is out of reach**
   (an unreachable point would distort the signature).
2. **Jig solving.** The signature must be slid into the sweet spot. `--solve-jig`
   calls `compute_jig_offset_for_box(box, cfg, vector=...)`, which aims the centre
   of the **actual fitted ink extent** (not the full cell, which can be far wider
   than the signature) at the annulus mid-radius and reach-checks the result. It
   then applies the computed `jig_x_mm/jig_y_mm` automatically.

A small registration jig (a corner stop + a marked cell on a card) holds the
paper so the solved offset is repeatable.

```bash
# Generate gcode + preview for Line-us, auto-solving the jig (safe, dry-run):
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114 \
    --profile lineus --solve-jig

# Plot over WiFi (default host line-us.local:1337), sheet in the jig:
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114 \
    --profile lineus --solve-jig --plot --lineus-host line-us.local

# Or plot over USB serial instead of WiFi:
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114 \
    --profile lineus --solve-jig --plot --lineus-usb --port /dev/ttyACM0
```

`send_lineus()` defaults to `dry_run=True` (same gating as the CR-10 path); it
only moves the arm with `--plot`.

## Calibration

```bash
# 1. Draw just the signature-box outline on a blank sheet (dry-run first to
#    inspect the gcode/preview, then --plot with a sheet in the jig):
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114 --test-box
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114 --test-box \
    --plot --home --port /dev/ttyUSB0

# 2. Adjust --jig-x / --jig-y until the rectangle lands on the cert line, and
#    --pen-down until the pen draws cleanly without digging in.
```

## Plotting a signature

```bash
# Generate gcode + SVG preview (safe anywhere, no hardware):
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114

# Plot it (sheet loaded in jig, calibrated):
python scripts/workers/ink_signature_cli.py --order CO-XXXX --doc cms10114 \
    --plot --home --port /dev/ttyUSB0 \
    --jig-x 22 --jig-y 24 --pen-down -0.2
```

`send_gcode_serial` defaults to `dry_run=True`; the CLI only plots with `--plot`.

## Verification (no hardware required)

`scripts/tests/test_ink_signature.py` (43 checks) proves the geometry:
- strokes fit inside the anchor box, aspect preserved, Y flipped, ink on the rule
- PDF-pt → bed-mm uses the jig offset + correct unit scale, stays on the bed
- G-code framing (pen up/down, feeds, park), servo mode, over-bed warning
- machine profiles (cr10 / axidraw / lineus) load with the right dialect/bed
- Line-us reach check rejects out-of-reach plots; `--solve-jig` brings the
  signature into reach; the Line-us emitter homes, encodes pen Z, and warns
  on out-of-reach; `send_lineus()` is dry-run safe over both TCP and USB
- `render_signature_on_pdf` stamps the strokes onto the **real CMS-10114 cert
  page** inside the signature cell (label ≤ y, ink ≥ bottom rule)

Because the PDF preview and the G-code share the same `fit_strokes_to_box`
geometry, the preview is a faithful digital twin of what the pen will draw.

## Files

| File | Role |
|---|---|
| `api/migrations/090_esign_signature_vector.sql` | `signature_vector` JSONB column |
| `site/public/portal/esign/index.html` | captures stroke paths alongside the PNG |
| `api/src/routes/portal-esign-generic.ts` | stores the (size-bounded) vector |
| `scripts/workers/services/ink_signature_plotter.py` | geometry + G-code + preview + serial |
| `scripts/workers/ink_signature_cli.py` | end-to-end CLI (generate / calibrate / plot) |
| `scripts/tests/test_ink_signature.py` | hardware-free correctness tests |
