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.
182 lines
8.6 KiB
Markdown
182 lines
8.6 KiB
Markdown
# 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 |
|