new-site/docs/ink-signature-plotter.md
justin 894d989445 Add portable Line-us pen-arm support to ink-signature pipeline
Adds a second machine class (small fan-shaped reach arm) alongside the
CR-10/AxiDraw rectangular-bed plotters, so wet signatures can be produced
while away from the home station.

ink_signature_plotter.py:
- PlotterConfig gains dialect (marlin|lineus) + name; new LineUsConfig
  (native units, pen height = per-move Z, reach annulus from shoulder pivot).
- Named machine profiles (cr10 default, axidraw, lineus) via load_profile().
- bed_mm_to_lineus_units(), check_reach() (annulus for lineus, rectangle for
  marlin), compute_jig_offset_for_box() (solves jig from the ACTUAL fitted ink
  extent so a wide cell line doesn't over-constrain a small arm).
- emit_gcode() dispatches to emit_marlin_gcode()/emit_lineus_gcode().
- send_lineus(): WiFi TCP 1337 (NUL-terminated, ok-acked) or USB serial,
  dry_run=True default (same gating as the CR-10 path).

ink_signature_cli.py: --profile, --solve-jig (auto-applies jig offset),
--lineus-host/--lineus-usb, reach-check that refuses to --plot out-of-reach
on Line-us.

Tests: 43 checks (was 30) covering profiles, reach check, jig solve, lineus
emitter, dry-run sender. Docs updated with profiles + portable workflow.
2026-06-07 03:45:46 -05:00

8.6 KiB
Raw Permalink Blame History

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.

# 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

# 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

# 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