# MeerK40t galvo calibration on black construction paper (ComMarker B4 60 W MOPA)

Folder: `/home/hank/laser/projects_shared/calibration_meerk40t/` · prepared 2026-10-31 · MeerK40t 0.9.8100 · JCZ/BJJCZ board (USB 9588:9899) · 110 mm lens (F-theta)

**Safety rule:** these files only prepare jobs. Hank presses Start himself. Nothing here starts a burn, framing or the red dot. Never launch MeerK40t with `-a`, because it auto-starts the laser.

## What this calibrates

MeerK40t has never been calibrated on this machine. Three sheets of 8.5×11 in black construction paper are used:

| Sheet | Burn | Purpose |
|---|---|---|
| S1 | power ladder (`mk_cal_S1_ladder_L131.5.svg`), then the target (`mk_cal_S1_L131.5.svg`) | choose a clean line setting; baseline distortion at the current settings (lens 131.5, stock `jcz110.cor`) |
| S2 | `mk_cal_S2_L131.5.svg`, using the new lens size + new .cor from the S1 analysis | apply the correction; measure how the board responded (gamma) |
| S3 | `mk_cal_S3_L131.5.svg`, using the S2 result | verify, or apply a fine-tune |

Target: worst point within the 100 mm square ≤ 0.2 mm, which matches LightBurn round 6 (0.186 mm).

## Files

| File | What |
|---|---|
| `jobs/mk_cal_S1_ladder_L131.5.svg` | MeerK40t project. 18 cells: 6 powers (3/5/8/12/16/20 %) × 3 speeds (2000/1500/1000 mm/s), 50 kHz, 200 ns, 1 pass. Each cell is a 6 mm square plus a diagonal. The ops are ordered from lowest to highest energy. Labels burn at 8 %/2000. The FRAME op is output off. |
| `jobs/mk_cal_S{1,2,3}_L131.5.svg` | MeerK40t project for the 100 mm target. Ops: `1 GEOM` (lines), `2 TEXT` (outline labels), `3 FRAME` (100 mm reference, **output off**). Default 8 %, 2000 mm/s, 50 kHz, 200 ns, ×1. |
| `jobs/*_plain.svg`, `*_preview.png` | the same geometry as plain SVG, and a picture of it |
| `jobs/jobs_L131.5.json` | every op setting in machine-readable form |
| `mk_cal_analyze.py` | analyses a 600 dpi scan and writes the report, overlay, new lens size, new flags and a corrected .cor |
| `mk_cal_design.py`, `mk_view_table.py` | shared geometry, and the MeerK40t view-flag matrices (generated from MeerK40t's own `View` class) |
| `gen_mk_cal.py` | regenerates the jobs (`--lens --power --speed --freq --pw`). Needs matplotlib, so run it on the box, not in the laptop venv. |
| `mk_cal_synth.py`, `selftest.sh` | synthetic burn/scan simulator plus an end-to-end self test |
| `selftest/` | box self-test evidence (reports + JSON) |
| `ref/jcz110.cor` | read-only copy of the stock table, used for tests |
| `scans/` | put the scans here (`sheet1_scan_600.png` …) |
| `results/` | the analysis writes here |
| `PROPOSED_CONFIG.md` | the settings that will change, and how to change them safely |

Target geometry (mm, centred on the field, y down): outer square 100. Inner square 78.49 with both diagonals. Axis rulers ±46 with ticks at ±10/20/30/44. Circles: Ø70, Ø15, and four Ø8 corner circles at (±44.6, ±44.6). Orientation pips by the corner circles count 3 (top-left), 2 (top-right), 1 (bottom-left) and 0 (bottom-right). Text: "TOP-LEFT" with an arrow, "X+", "Y+", and the sheet label. The pips let the analysis detect a mirrored or rotated burn even without the paper snip.

## Lens size: why 131.5 and not 110

- In MeerK40t, `Lens Size` (Device settings → General → **Width**) is the **only** global scale control. Its scale is galvo ticks/mm = 65535 / lens_size, so the burned size is proportional to 1 / lens_size.
- The physical lens is 110 mm, but the stock `jcz110.cor` and this galvo give about 498 ticks/mm at the centre, which LightBurn round 6 measured. 65536 / 498 ≈ **131.6 mm**. That is why the config already says `lens_size = 131.5mm`.
- Typing 110 would make every burn about **19.6 % too big**. So S1 runs at the current 131.5, and the analysis computes the exact value: new = old × measured / nominal.
- **Gotcha:** when you select a .cor file in the GUI, MeerK40t resets Width to 65536 / (cor header scale), which is **125.002** for jcz110-type files. After selecting a .cor, **always type the lens size back in**.
- There is an alternative table, Claude's `jcz110_r6_meerk40t.cor` with lens 125.001, built from the LightBurn round-6 model. It has not been tested in MeerK40t. Keep it as a fallback only. Do not mix it with the S1 → S2 chain.

## MeerK40t 0.9.8100 knobs (Balor/JCZ device)

| Knob | GUI location | Current | What it does |
|---|---|---|---|
| Lens Size | Device settings → General → **Width** | 131.5 mm | Global scale for both axes. It cannot fix X and Y separately. |
| Correction file | General → Correction File → **Enable** + **File** | on, `/home/hank/laser/meerk40t/jcz110.cor` | A 65×65 table that the board applies. It is the **only** way to fix per-axis scale, skew, trapezoid, barrel/pincushion and off-centre error. |
| Flip X / Flip Y / Swap XY | Parameters → Axis corrections | F / T / T | Mirrors or rotates the design-to-galvo mapping. These are right if the burn reads correctly (TOP-LEFT at the back-left, text not mirrored). |
| Rotate View | Parameters → Axis corrections | 0 | Further 90° steps |
| Laserspot, Margins, redlight offsets | General / Red Dot | – | Do **not** affect burn geometry (the redlight offset affects the red dot only) |
| Correction tab (Device window) | – | – | A **stub** in 0.9.8100 that does nothing. Use a .cor file instead. |

These are the settings the analysis prints and writes into new .cor files. Format: JCZ_COR_2_1, 65×65 signed int32 (dx, dy), step 1024 ticks. How the board applies the table (sign) is checked by the gamma test on sheet 2. New tables are clamped to |v| < 32768.

## Before you start (once)

1. Snip about **8 mm off one corner** of each sheet with scissors, at 45°. That corner is the "back-left" reference, so the analysis knows how the paper sat.
2. Put a flat **non-flammable** backing under the paper: aluminium sheet, steel plate or a ceramic tile. Tape all 4 edges flat with masking tape. Paper must lie flat, because a wave in the paper is a focus and scale error.
3. Focus the head on the paper surface the usual way (focus gauge / measured height for the 110 mm lens).
4. Load files with **File → Open** in the running MeerK40t. Do **not** use `stand_center.bat`, because it shifts everything by 38.25/23.25 mm. Do **not** use `cal_run.bat`, because it contains spool commands. After opening, press **Ctrl+A** then **Ctrl+Shift+C** to centre on the bed. The files are already centred at 65.75 mm for lens 131.5. Do this every time the lens size has changed.

## Fire safety (every burn)

- Fume extraction on, enclosure or shield as usual, laser safety glasses on.
- Never leave a paper burn unattended. Stay at the machine with a hand on **Stop / E-stop**.
- Keep a wet cloth or a small extinguisher within reach. Black paper can glow or smoulder after a pass. Watch for 30 s after the job ends, then check for embers before you pull the paper.
- If there is any flame, any spreading glow, or a cell that ignites: hit Stop, and smother it with the wet cloth. Never blow on it.
- Only Hank presses Start. Framing or the red dot is Hank's choice. The files do not trigger them.

## Sheet 1, step by step

1. Prepare sheet 1 (snip at the back-left, tape it flat on the non-flammable backing). Slide it so its **back third** is under the centre of the lens. The ladder is about 66 × 37 mm.
2. In MeerK40t: File → Open `jobs/mk_cal_S1_ladder_L131.5.svg` → Ctrl+A → Ctrl+Shift+C. Check the op list: 18 red ops `01 … 18` (low to high energy), `99 LABELS`, and a `FRAME` op with output off. Device settings must be unchanged: Width 131.5 mm, jcz110.cor enabled, Flip X F, Flip Y T, Swap XY T, Rotate 0.
3. Hank presses Start and watches the ladder burn. **If a cell flames, stop.** The settings after it are hotter.
4. Let it cool, then look at the ladder. The rows are speed (top 2000, middle 1500, bottom 1000 mm/s) and the columns are power (3/5/8/12/16/20 %). Pick the **lowest-energy cell whose square and diagonal are continuous, crisp and clearly lighter or darker than the paper**, without burn-through, charring or flame. That is the line setting. Rule from the laser lessons: stay at **≤12 %** if a cell there is clean.
5. Open `jobs/mk_cal_S1_L131.5.svg` (Ctrl+A, Ctrl+Shift+C). Double-click op `1 GEOM` and op `2 TEXT` and set the chosen power and speed (keep 50 kHz, 200 ns, 1 pass). `3 FRAME` stays output off.
6. Untape and slide the paper **without rotating it** (snip still at the back-left) so a clean area at least 110 mm square is centred under the lens and is clear of the ladder. Tape it flat again.
7. Hank presses Start. The target takes well under a minute. Watch it, then let it cool and check for embers.
8. Scan (next section) and run the analysis.

## Scan (HP PhotoSmart 2600 on the laptop's USB)

Place the sheet **face down**, with the snipped corner fully on the glass (portrait, against the top-left of the glass). Then run on the laptop:

```bash
pkill -x scanimage; mkdir -p /home/hank/laser/projects_shared/calibration_meerk40t/scans && nohup scanimage -d 'hpaio:/usb/Photosmart_2600_series?serial=YOUR_SERIAL' --mode Color --resolution 600 --compression None -x 215.9 -y 297 --format=png -o /home/hank/laser/projects_shared/calibration_meerk40t/scans/sheet1_scan_600.png > /tmp/scan.log 2>&1 < /dev/null &
```

For later sheets, change `sheet1` to `sheet2` / `sheet3`. A scan takes about 2.5 min and is about 53 MB. Check it with `cat /tmp/scan.log; ls -la …/scans/`. The scanner's measured scale is 600.0 dpi in X and **599.70 dpi in Y**, and the analysis uses those values by default. A face-down flatbed scan is not mirrored.

## Analysis

```bash
cd /home/hank/laser/projects_shared/calibration_meerk40t
PY=/home/hank/.venvs/mk40t/bin/python
# sheet 1 (baseline: lens 131.5 + stock cor)
$PY mk_cal_analyze.py scans/sheet1_scan_600.png --lens 131.5 --cor-in /home/hank/laser/meerk40t/jcz110.cor --tag sheet1 --out results
# sheet 2 (use the lens and cor that sheet 1 produced; --prev measures how the board responded)
$PY mk_cal_analyze.py scans/sheet2_scan_600.png --lens <S1 lens> --cor-in results/sheet1_corrected_L<S1 lens>.cor --prev results/sheet1_result.json --tag sheet2 --out results
# sheet 3: same pattern with sheet2 files
```

If the flags were changed after a sheet, add `--flip-x 0/1 --flip-y 0/1 --swap-xy 0/1 --rotate 0..3` with the flags that were **in use for that burn**. The defaults are the current 0/1/1/0.

Each run takes a few seconds and writes to `results/`:

- `<tag>_REPORT.txt` covers:
  - orientation (mirrored/rotated?) and recommended flags
  - scale X/Y/mean, plus rulers vs square edges
  - skew, trapezoid, and side bow (barrel/pincushion)
  - centre offset
  - circle ovality
  - ruler tick errors
  - mark strength by region (a weak corner suggests a focus/tilt problem)
  - fit RMS
  - worst point (before and after the lens-only fix)
- `<tag>_corrected_L<lens>.cor` and `<tag>_corrected_L<lens>_half.cor` are new tables at full and half strength
- `<tag>_overlay.jpg` shows the residual arrows ×5
- `<tag>_result.json` holds the data. It is the input for `--prev`.

Response check on sheet 2+: **gamma** ≈ 1 means the correction acted as predicted. If gamma < 0, the sign is inverted, and the script automatically writes a sign-flipped table. If gamma is near 0, the table was ignored or transposed, and no automatic .cor is written. Stop and check that the cor was enabled and the lens was re-typed.

## Applying the sheet-1 result (before sheet 2)

See `PROPOSED_CONFIG.md`. In short:

1. Back up the config.
2. Device settings → Correction File → File → choose `results/sheet1_corrected_L<lens>.cor`. Keep Enable ticked.
3. **Type the new Width (lens size) from the report back in**, because selecting the cor reset it to 125.002.
4. Change Flip/Swap/Rotate **only** if the report says the burn was mirrored or rotated.
5. Open `mk_cal_S2_L131.5.svg` with the same op power and speed as S1, then Ctrl+A, Ctrl+Shift+C.

If the S1 worst point was very large (> 3 mm) or the overlay looks wrong, use the `_half.cor` for S2.

## Self test (no laser, no scanner)

`./selftest.sh /home/hank/.venvs/mk40t/bin/python` (the laptop venv has no matplotlib; the simulator then skips the text labels and prints harmless numpy/matplotlib import warnings) simulates a distorted machine, then runs a sheet-1 scan, the correction, a sheet-2 scan and the analysis. Box result (2026-10-31):

- sheet 1 worst point 1.61 mm. Lens 131.5 → 133.509.
- sheet 2 worst point **0.043 mm**, gamma 1.04.

Earlier box tests also passed: a mirrored machine (the flags were detected), and a sign-inverted board (gamma −1.02, auto-flipped, sheet 3 at 0.061 mm).
