# 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` 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. ## 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` (30 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 - `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 |