ink-signature: pen-plotter pipeline for original wet-ink CMS signatures

The Standard no-login CMS path needs an ORIGINAL ink signature on paper
(CMS-10114: 'Stamped, faxed or copied signatures will not be accepted'). This
adds a pipeline to redraw the provider's own captured strokes in real ink with a
pen on a CR-10 V2 (or any Marlin/GRBL machine) — original, in ink, never copied.

- migration 090: esign_records.signature_vector (JSONB stroke paths, 0..1).
- signing page now captures normalized stroke paths alongside the PNG; API
  stores a size-bounded vector for drawn signatures.
- ink_signature_plotter.py (hardware-independent): fit strokes to the signature
  anchor box, PDF-pt -> bed-mm via jig offset, emit Marlin/GRBL G-code (Z pen or
  M280 servo/BLTouch), SVG toolpath preview, and render_signature_on_pdf (a
  digital twin that proves the toolpath lands on the cert line). Gated serial
  sender (dry_run default).
- ink_signature_cli.py: end-to-end load-record -> gcode+preview, --test-box jig
  calibration, --plot to stream over USB.
- Corrected CMS-10114 signature anchor to sit inside the Section 4A signing cell
  (above the bottom rule, below the label).
- docs/ink-signature-plotter.md documents the CR-10 retrofit + interpretive risk.

Tests: test_ink_signature.py 30/30, test_cms10114.py 27/27, test_paper_batch.py
15/15, API tsc clean, Astro build 58 pages.
This commit is contained in:
justin 2026-06-07 02:34:17 -05:00
parent e6a630ada1
commit b0a8563a93
8 changed files with 994 additions and 19 deletions

View file

@ -82,12 +82,13 @@ CERT_NAME_POS = {
"last": {"x": 429, "y": 372},
}
# Signature line for Section 4A (individual). The label "Signature (First,
# Middle, Last...)" sits at pdf_y ~458; the blank signing line is the row above
# it. The e-sign stamper places the provider's signature here.
# Signature line for Section 4A (individual). The cell "1. Practitioner's
# Signature" has its label at pdf_y ~458 and its bottom rule at pdf_y ~441; the
# signer writes in the band between them (x 36..483). We anchor the signature to
# rest just above the bottom rule. The e-sign stamper / pen plotter use this box.
SIGNATURE_FIELDS = [
{"field": "signer", "page": PAGE_CERT,
"rect": [60.0, 470.0, 470.0, 490.0], "page_w": PAGE_W, "page_h": PAGE_H},
"rect": [44.0, 442.0, 474.0, 456.0], "page_w": PAGE_W, "page_h": PAGE_H},
]
VALID_REASONS = ("initial", "change", "deactivation", "reactivation")
@ -212,7 +213,7 @@ def fill_cms10114(intake: dict, reason: str = "change",
"x": float(llx) + 4,
"y": float(lly) + 1,
"w": float(urx - llx) - 8,
"h": max(float(ury - lly), 18.0),
"h": max(float(ury - lly), 12.0),
"page_w": sig["page_w"],
"page_h": sig["page_h"],
})