# CPL rotation corrections — re-bba-rb

`re-bba-rb-cpl.csv` is generated from `kicad-cli pcb export pos` (raw KiCad
rotation values), **then hand-corrected** for the parts below before each
regeneration. This file exists because that correction step is easy to lose
silently — regenerating the CPL "the normal way" wipes it out, and there is
no automated check that would catch that. Read this before touching the CPL.

## Why this exists

KiCad's per-footprint rotation is measured from an arbitrary reference baked
into whoever drew that footprint — it does not reliably match what JLCPCB's
placement machine expects. This is a well-known, generic problem (see
`cpl_rotations_db.csv` from the JLCKicadTools / kicad-jlcpcb-tools projects
for the community's attempt at a fix), but **the generic community
correction table was tried on this board and found wrong** — it claims
`LQFP-: 270°`, but on this exact board/toolchain pairing (KiCad 10 +
`kicad-cli pcb export pos`, not the dedicated plugin) the raw LQFP/QFN
rotation is already correct. The generic table's values are calibrated
against that plugin's own raw-rotation baseline, which is not the same
baseline `kicad-cli`'s raw CSV export produces (different Y-axis handling
between the two tools). **Do not reapply the community table wholesale to
this board.** Every correction below was derived per-part from direct visual
verification against JLCPCB's placement-preview render, not from that table.

## Rotation sign convention

The CPL uses JLCPCB's Y-up coordinate frame (`kicad-cli`'s csv pos format
already negates Y from KiCad's native Y-down). In that frame, **increasing
rotation angle is counter-clockwise** (standard math convention). So when
the user reports "needs N degrees clockwise", the correction applied is
**`new = (old - N) % 360`**, not `+N`. Get this backwards and every fix
lands 2×N off instead of correct.

## How to reapply — copy this exact dict, don't reconstruct it from memory

This is the complete, current correction set (all 16 parts). **Copy-paste
this block, don't retype it from the table below or from memory** — the one
time it was retyped from a section of prose instead of copied whole, U4 got
silently dropped and shipped with a wrong rotation.

```python
import csv

corrections = {
    'U7': 270,                                       # SOIC-8, confirmed 2026-07-31 (net of +90 hypothesis + 180 more) — same net correction as U10
    'U10': 270,                                      # SOIC-8, confirmed (net of +90 hypothesis + 180 more)
    'U11': -90,                                      # LQFP-64, confirmed 2026-07-31 ("90 degrees clockwise") — SUPERSEDES the earlier "all large chips are right" report, see below
    'D1': -90, 'D4': -90, 'D5': -90, 'U12': -90,     # SOT-23-6, reconstructed hypothesis
    'U1': -180,                                      # SOT-23-5, confirmed (two -90 corrections)
    'U4': -90,                                       # SOT-23-5, confirmed ("U4 is good")
    'Q1': 180, 'Q2': 180,                            # SOT-23 bottom layer, confirmed "upside down"
    'D2': -90,                                       # custom footprint, confirmed "90 CW"
    'D3': 180,                                       # SMA diode, confirmed "180"
    'C65': 180,                                      # electrolytic cap, confirmed "180"
    'U2': -90,                                       # SOT-583-8, confirmed "90 CW"
    'U3': 180,                                       # custom SOT95P280X110-6N footprint, confirmed "180"
}   # positive = CCW, negative = CW

with open('re-bba-rb-cpl.csv', newline='') as f:
    rows = list(csv.DictReader(f))
for r in rows:
    if r['Designator'] in corrections:
        old = float(r['Rotation'])
        new = (old + corrections[r['Designator']]) % 360
        r['Rotation'] = f"{new:.6f}"
with open('re-bba-rb-cpl.csv', 'w', newline='') as f:
    writer = csv.DictWriter(f, fieldnames=['Designator','Mid X','Mid Y','Layer','Rotation'])
    writer.writeheader()
    writer.writerows(rows)
```

If a new correction comes in from a placement-preview check, add it to
*this* dict (and the table below), never work from a hand-copied subset.

When regenerating the CPL from scratch (e.g. after a reroute or component
move), **re-run `kicad-cli pcb export pos` for fresh positions, then
reapply every correction below** — don't just diff/copy the old file, since
positions can legitimately change while rotations still need the same
correction offset applied on top.

## Confirmed correct as-is — apply NO correction

**These are the only parts that take zero correction.** Everything else in
this section title's spirit but requiring a nonzero offset (like U4 below)
lives in the corrections table, not here — filing U4 in this section
previously (saying "confirmed good" without also flagging its required -90°
delta) caused a real bug: a later reapplication pass built its correction
dict from this section's prose and silently skipped U4, regenerating the
CPL with U4 back at raw/wrong rotation. Don't repeat that — if a part needs
*any* nonzero delta from raw, it belongs in the table below, full stop, even
if that delta is "confirmed correct and needs no further correction beyond
it."

- **U8 (LQFP-48)**, **U9 (QFN-48)** — "all large chips are right" (user,
  direct placement-preview check). ⚠️ **That single report also covered U11,
  and U11 later turned out to need −90° after all** (direct check,
  2026-07-31). So the "large chips are fine" observation is now known to be
  wrong for at least one of the three parts it covered, and U8/U9 rest
  entirely on that same weakened report. Treat both as **unverified**, not
  settled — re-check them in the placement preview before assembly. Do not
  cite "all large chips are right" as evidence again.
- **D8, D9 (LED_0603, bottom layer)** — verified 2026-07-31 by tracing the
  circuit rather than by eye, see "D8/D9 polarity" below. No correction.
- **J2 (HR911105A magjack, rot=90° in the PCB file)** — not a CPL-rotation
  question (J2 doesn't need per-part correction below, it's not in the CPL
  correction set), but its physical orientation was independently verified:
  the RJ45 signal pins (pins 1–8, footprint local +Y) map to absolute −X
  under the board's 90° placement rotation, which correctly faces the
  board's tongue/cable-exit edge. Pin 1 appearing to "face outside the
  board" in the placement render is *expected*, not a defect — the RJ45
  contacts are always near the cable-insertion opening by construction.

## Corrections applied (all currently in `re-bba-rb-cpl.csv`)

Each row: designator, package, raw→corrected, confidence.

| Ref | Package | Raw | Corrected | Confidence |
|---|---|---|---|---|
| U7  | SOIC-8 | 90°  | **0°** (net: +90° reconstructed hypothesis, then +180° more, user-confirmed 2026-07-31) | **Confirmed.** Note this landed on exactly the same net correction as U10 (+270°), which retroactively makes the two SOIC-8 parts consistent — the original bare "+90°" hypothesis was wrong for *both*, in the same direction and by the same amount |
| U11 | LQFP-64 | -90° | **180°** | **Confirmed** ("90 degrees clockwise", 2026-07-31). Previously filed as needing NO correction on the strength of "all large chips are right" — that report was wrong for this part. See the note in the no-correction section above: U8/U9 inherit the doubt |
| U10 | SOIC-8 | 0°   | **270°** (net: +90° reconstructed hypothesis, then +180° more, user-confirmed) | Partially confirmed — the +90° hypothesis alone was WRONG/incomplete (needed another 180° on top). **This means the SOIC "+90°" hypothesis should not be trusted for U7 either** without its own direct check, since it's now proven wrong at least once. |
| U3  | custom `SOT95P280X110-6N` | 0°   | **180°** | **Confirmed** ("180°") |
| D1  | SOT-23-6 | 0°   | **270°** | Reconstructed hypothesis — not independently re-confirmed |
| D4  | SOT-23-6 | 180° | **90°**  | Reconstructed hypothesis — not independently re-confirmed |
| D5  | SOT-23-6 | 180° | **90°**  | Reconstructed hypothesis — not independently re-confirmed |
| U12 | SOT-23-6 | 180° | **90°**  | Reconstructed hypothesis — not independently re-confirmed |
| U1  | SOT-23-5 | 180° | **0°** (two corrections: -90° then another -90°, user-confirmed both times) | **Confirmed** by direct user feedback, twice |
| U4  | SOT-23-5 | 0°   | **270°** (-90°, the same family correction as D1/D4/D5/U12) | **Confirmed** — user checked and said "U4 is good" after this delta, no second correction needed (unlike U1) |
| Q1  | SOT-23 (bottom layer) | -90° | **90°** | **Confirmed** ("upside down" = 180°, direction-agnostic) |
| Q2  | SOT-23 (bottom layer) | -90° | **90°** | **Confirmed** (same as Q1) |
| D2  | custom `SON50P110X213X50-7` | 0°   | **270°** | **Confirmed** ("90° clockwise") |
| D3  | `Diode_SMD:D_SMA` | 180° | **0°**   | **Confirmed** ("180°", direction-agnostic) |
| C65 | `Capacitor_SMD:CP_Elec_6.3x7.7` | 0°   | **180°** | **Confirmed** ("180°", direction-agnostic) |
| U2  | SOT-583-8 | 180° | **90°**  | **Confirmed** ("90° clockwise") |

**"Reconstructed hypothesis" parts (U7, U10, D1, D4, D5, U12) explained:**
the user reported "SOIC and SOT chips are rotated 90°, direction unsure."
Cross-referencing against *earlier* feedback in the same session — where a
-90° correction had been applied to the top-layer SOT-23 parts and NOT
flagged as wrong, while the same -90° applied to a SOIC part (U10) WAS
flagged wrong — implies SOT-23 needs -90° and SOIC needs the opposite,
+90°. This is indirect reasoning, not a fresh direct check, and **U1 later
proved that even within one confirmed-wrong-by-90°-family, the correction
does NOT reliably generalize per package type** (U1 and U4 are both
SOT-23-5, both got the -90° family correction, but U1 needed a *second*
-90° on top while U4 was already correct) — so treat every "reconstructed
hypothesis" row above as **worth a fresh placement-preview check**, not
settled fact, despite no contradicting feedback having come in since.

## D8/D9 polarity — checked by circuit tracing, NOT by eye (2026-07-31)

D8 was reported as *possibly* upside down because "it connects with its
negative side to Q1". **That observation is correct behaviour, not a
fault** — D8's cathode is *supposed* to face Q1. Tracing the net:

```
D8:  3V3 → R41 → D8.pad2 (A) ─ D8.pad1 (K) → Q1.pad3 (C) → Q1.pad2 (E) → GND
     Q1 is an NPN low-side switch, base Q1.pad1 ← R39.
     Current can only flow if the LED cathode is the Q1 end. ✔
D9:  3V3 → Q2.pad2 (E) → Q2.pad3 (C) → R42 → D9.pad2 (A) ─ D9.pad1 (K) → GND
     Q2 is a PNP high-side switch, base Q2.pad1 ← R40.
     D9's cathode goes straight to GND; it does NOT touch Q2 directly. ✔
```

Both are `LED_SMD:LED_0603_1608Metric` on **B.Cu**, both at raw −90°, and
both have pad 1 = cathode (the footprint's B.Fab chamfer marks the pad-1 /
cathode end). Absolute pad geometry, with the back-layer X-mirror applied:

| | pad 1 (K) | pad 2 (A) | anode's partner |
|---|---|---|---|
| D8 | (167.420, **47.300**) | (167.420, **48.875**) | R41 pad 2 at (166.170, 48.875) |
| D9 | (164.940, **47.340**) | (164.940, **48.915**) | R42 pad 2 at (163.690, 48.917) |

The anode pads land at the *same Y* as their series resistor's pad — that
alignment is what confirms the back-layer mirror was applied correctly when
computing these (get the mirror wrong and pad 1/2 swap, inverting the whole
conclusion). Q1's collector pad sits at (165.350, 45.580), i.e. **above** D8
in Y — the same side as D8's cathode. So "cathode faces Q1" is exactly right.

**Both D8 and D9 are oriented identically** (cathode toward smaller Y). That
is a useful invariant: they are the same part in the same circuit role, so if
a future preview shows one of them needing a flip, *both* need it — a
correction applied to only one of the pair is a mistake.

Note this relationship is **mirror-invariant**: whether the assembler renders
the bottom layer viewed-from-below or seen-through-the-board, "D8's cathode
end is the end nearer Q1" stays true. So it is a safe thing to eyeball in a
preview, unlike absolute left/right or up/down judgements on bottom-layer
parts.

## Parts never checked at all

Every 0402/0603/0805 passive (R/C/L) except C65, J1/J3/J4 connectors, and
anything else not listed in the corrections table above is sitting at
**raw, unverified KiCad rotation** in the CPL. Passives in symmetric
packages are usually fine raw (this matched observed behavior for the ones
implicitly checked), but this has not been confirmed part-by-part the way
the table above was.

## Bottom line for whoever picks this up next

Before ordering assembly, re-check the *entire* board in JLCPCB's (or
whatever assembler's) placement preview — this file only covers what's been
individually verified so far, not a general guarantee the CPL is correct.
