← Commit history

Add adompkg installer files

Kyle Bergstedt ·29af6492d5 ·4mo ago ·parent 3b1d22f
4 files changed +342
SKILL.mdadded+312
@@ -0,0 +1,312 @@+---+name: altium-pcblib+description: >+  Native Rust crate that parses Altium .PcbLib (and .IntLib) binary footprint+  libraries — OLE2 Compound File container with Altium's reverse-engineered+  binary record format inside. Emits KiCad-shaped pad JSON so chipsmith,+  concur, and any other Adom tool that needs to read Altium binary footprints+  pulls from one source of truth instead of re-implementing OLE2 walking ++  binary record decoding.+  Public Rust API: parse_altium_pcblib_path(&Path), parse_altium_pcblib_bytes(&[u8]),+  render_footprint_svg(&Value), render_all_footprints_svg(&Value).+  Bundled CLI: altium-pcblib-svg <path> [--all] [-o out.svg] — emits the+  authoritative parser-side SVG so a downstream renderer disagreeing with+  the SVG is provably the bug, not the parser.+  Source is private (github.com/adom-inc/altium-pcblib); depend via+  Cargo path in the same workspace, or git URL on a build container.+  Trigger words: altium pcblib, altium intlib, parse altium binary footprint,+  altium ole2, altium compound file, altium binary record, .PcbLib, .IntLib,+  altium pad parser, read altium footprint in rust, altium-pcblib, altium+  cross-source, footprint consensus altium, altium svg debug, altium-pcblib-svg,+  authoritative footprint render, debug altium footprint, why is my altium pad+  rotated wrong, altium rotation convention, altium fat overshoot, altium pcblib+  malformed fat, multi-variant altium footprint, MFG vs IPC variant, RGF0040E.+---++# altium-pcblib — Adom's shared Altium .PcbLib parser (Rust crate)++`altium-pcblib` is a private Adom Rust crate that reads Altium Designer's+binary footprint libraries (`.PcbLib`, and the PCB stream embedded in+`.IntLib`) and emits per-pad JSON shaped like KiCad's `parse_kicad_mod`+output. Used by `adom-chipsmith` (Tab 4 cross-source agreement) and+`concur` (cross-source consensus check) so neither has to re-implement+OLE2 / CFBF walking or Altium's reverse-engineered binary record format.++> **This is a skill, not a library page.** The wiki's `library` type means+> "EDA part bundle = symbol + footprint + 3D + variants + pad↔pin mapping".+> `altium-pcblib` is a Rust crate, so it lives as a skill that points at the+> private GH repo. See `adom-wiki` skill → "What each `<type>` means in Adom".++## Where the source lives++- Private GitHub repo: <https://github.com/adom-inc/altium-pcblib>+- Local workspace path (Adom container): `/home/adom/project/altium-pcblib`+- Cargo description (matches what's in `Cargo.toml`):+  > "Native Rust parser for Altium .PcbLib (and .IntLib) footprint libraries — extracts pads as KiCad-shaped JSON"++## Depending on it from another Adom Rust tool++In the workspace (chipsmith, concur, anything else under `/home/adom/project/`):++```toml+[dependencies]+altium-pcblib = { path = "../altium-pcblib" }+```++On a build container that doesn't have the workspace checkout:++```toml+[dependencies]+altium-pcblib = { git = "https://github.com/adom-inc/altium-pcblib", branch = "main" }+```++Single direct dep (`cfb`, the OLE2 reader) plus `anyhow` and `serde_json`.++## Public Rust API++```rust+// Parse from a file on disk.+pub fn parse_altium_pcblib_path(path: &Path) -> anyhow::Result<serde_json::Value>;++// Parse from raw bytes (handy for include_bytes! tests and HTTP bodies).+pub fn parse_altium_pcblib_bytes(bytes: &[u8]) -> anyhow::Result<serde_json::Value>;++// Render the parser's view of the footprint as standalone SVG. THIS IS THE+// AUTHORITATIVE GROUND TRUTH — if a downstream tool draws something different,+// the downstream tool is the bug.+pub fn render_footprint_svg(footprint: &serde_json::Value) -> String;++// Render primary + every alternate variant, stacked vertically in one SVG.+pub fn render_all_footprints_svg(parsed: &serde_json::Value) -> String;+```++The returned `Value` is shaped exactly like+`adom-chipsmith::view_library::parse_kicad_mod_text` output:++```json+{+  "module_name": "D8",+  "pad_count": 8,+  "pads": [{+    "number": "1", "type": "smd", "shape": "rect",+    "at": [-2.4638, -1.905, 0.0],+    "size": [1.9812, 0.5588],+    "layers": ["F.Cu", "F.Paste", "F.Mask"],+    "drill": null+  }],+  "silk_markers": [],+  "pin1_silk": { "found": false },+  "_source": "altium_pcblib",+  "_alternates": [ /* other footprints in the lib, sorted by pad_count desc */ ]+}+```++## CLI bin: `altium-pcblib-svg`++Single binary that ships with the crate. Use it any time you suspect a+downstream renderer is drawing pads wrong:++```bash+# Primary footprint, SVG to stdout.+altium-pcblib-svg /path/to/Foo.PcbLib > foo.svg++# Primary + every alternate variant stacked vertically.+altium-pcblib-svg /path/to/Foo.PcbLib --all -o foo-all.svg+```++Conventions baked into the SVG:++- 1 SVG unit = 1 mm. Open it in any browser at 50 px/mm by default.+- Y-down (matches both KiCad and our emitted JSON). Altium's Y-up is flipped at parse time.+- Rotation applied via `transform="rotate(deg cx cy)"` clockwise on screen — same direction KiCad displays.+- Pad numbers stay un-rotated for readability regardless of pad rotation.+- F.Cu pads = orange. B.Cu pads = blue. Thru-hole = green. Drill holes = white circle on top.+- Origin crosshair at (0, 0) with 1 mm arms — sanity-check pad placement.+- Footprint name + pad count in the top-left corner.++## Integration patterns++### chipsmith Tab 4 — cross-source agreement matrix++`adom-chipsmith/src/cli/view_library.rs` calls into the parser when it sees a+`<MPN>.PcbLib` sibling next to the `.kicad_mod`:++```rust+let altium_pcblib = args.dir.join(format!("{mpn}.PcbLib"));+if altium_pcblib.exists() && !sources.contains_key("altium") {+    match altium_pcblib::parse_altium_pcblib_path(&altium_pcblib) {+        Ok(parsed) => sources.insert("altium".to_string(), parsed),+        Err(e) => err(format!("altium parse failed: {e}")),+    }+}+```++The result drops straight into the same map that holds the KiCad / Fusion /+Datasheet entries, so the Tab 4 matrix lights up a fourth column with no+adapter needed. Alternates ride along under `_alternates` so the UI can+offer a variant picker.++### concur — adapter to lean Part shape++`concur/src/parsers/altium.rs` adapts the rich pad JSON down to concur's+lean `Part` shape (concur cares about pad designators + module name only):++```rust+pub fn parse(path: &Path) -> Result<Part> {+    let parsed = altium_pcblib::parse_altium_pcblib_path(path)?;+    let module_name = parsed.get("module_name").and_then(|v| v.as_str()).map(String::from);+    let pads: Vec<Pad> = parsed.get("pads").and_then(|v| v.as_array()).map(|arr| {+        arr.iter()+            .filter_map(|p| p.get("number").and_then(|v| v.as_str()).map(String::from))+            .map(|number| Pad { number })+            .collect()+    }).unwrap_or_default();+    Ok(Part {+        source: "altium".into(),+        source_part_name: module_name.clone(),+        pads,+        pins: Vec::new(),+        package: module_name.map(|raw_name| PackageInfo { raw_name, body_x_mm: None, body_y_mm: None }),+    })+}+```++For multi-variant `.PcbLib`s (DRV8316RRGFR has IPC_A / IPC_B / IPC_C / MFG),+concur's `parse_with_consensus_hint` walks primary + every alternate and picks+the variant whose pad count is closest to what the other parsed sources agree+on, then tiebreaks against names containing `MFG` / `DEBUG` / `TEST` / `FIDUCIAL`+(manufacturing layouts with extra non-pinout pads).++## Format quirks worth knowing++These are the things that bit us during reverse-engineering — they're+encoded in the parser, but if you extend it or debug a weird chip, here's+what's going on.++### 1. Some Altium .PcbLibs have FAT overshoot++INA226AIDGSR's PcbLib declares 256 FAT entries but only has 213 sectors.+The `cfb` crate refuses with `Malformed FAT … FAT has N entries, but file+has only M sectors`. The fix the parser uses: pad the input bytes to 1 MiB+with `0xFF` (CFBF "free sector" sentinel) before opening. The unreferenced+trailing sectors don't hold real data, so this is safe.++### 2. Per-record-type sub-block count++Altium records aren't uniform `[type][len][payload]`. Different record+types carry different numbers of `[u32 len][bytes]` sub-blocks:++- Pad (0x02): **6** sub-blocks (designator, layer-name pstring, four metadata blocks).+- Text (0x05): **2** sub-blocks (main + style trailer).+- Track / Arc / Via / Fill / Region / ComponentBody: **1** sub-block.++If you hit an unknown type byte mid-stream, the parser stops walking+gracefully — pads always come first, so by then we already have what we need.++### 3. Pad block-5 layout (the 194-byte v3 / 202-byte v4 pad data)++```+[0]      u8 layer (1 = TOP_CU, 32 = BOTTOM_CU, 21 = TOP_OVERLAY, ...)+[1]      u8 net/flag (0x08 in v3, 0x0c in v4 — version probe)+[2]      u8 reserved+[3..13]  10 bytes of 0xFF (component / net id sentinels)+[13..17] i32 LE x position (units = 1 / 10000 mil = 2.54e-6 mm)+[17..21] i32 LE y position (Altium Y-up — flipped to KiCad Y-down at parse)+[21..29] u32 LE x size, y size — top layer+[29..37] u32 LE x size, y size — mid layer (multi-layer thru-hole pads)+[37..45] u32 LE x size, y size — bottom layer+[45..49] u32 LE hole size (0 → SMD; > 0 → thru-hole)+[49..52] u8 × 3 shape codes (1 round, 2 rect, 3 octagonal, 9 rounded-rect)+[52..60] f64 LE rotation in degrees — ONLY in 202-byte v4 layout+```++Past offset 60 the layout drifts between Altium releases (paste / mask+offsets, plated bool, GUIDs, primitive index). The parser stops at 60.++### 4. Two equally valid rotation conventions in the wild++This is the one that *looks* like a parser bug but isn't. Altium gives+library authors freedom in how to encode a rotated pad:++- **Convention A (matches KiCad):** store the pad as a "vertical stick"+  (xsize < ysize), set rotation = 90° on the side pads. DRV8316's IPC_A+  variant uses this.+- **Convention B (post-rotation):** store the physical post-rotation+  dimensions (xsize > ysize for a horizontal pad), set rotation = 0.+  DRV8316's MFG variant + INA226's v3 layout both use this.++Both render to the same physical pad on the PCB. A downstream tool that+compares `size[]` element-wise without applying rotation will see a 90°+mismatch in convention B → KiCad. The fix lives in the consumer (compute+the post-rotation bbox before comparing), **not** in this parser.++When in doubt about whether your downstream is the bug, run+`altium-pcblib-svg` on the file — that's the parser's authoritative view.++### 5. Y-axis flip++Altium Y-up vs KiCad Y-down. The parser flips Y at parse time so emitted+`at[1]` is in KiCad convention. **Do not flip again downstream.**++### 6. Round shape with unequal w/h → "oval"++KiCad's `circle` implies w == h. Altium's "round" (shape byte 1) covers+both true circles and stadium-shaped SMD pads with rounded ends. The+parser specializes: shape 1 with `(xsize - ysize).abs() < 1e-3` → `circle`,+otherwise → `oval` (KiCad's stadium).++## Visual proof — what "right" looks like++The crate ships three reference SVGs from the test corpus, generated by+`altium-pcblib-svg` (and reproducible any time):++- `playbooks/lm358d-soic8.svg` — SOIC-8 with 8 horizontal pads, two+  columns, rot=0 throughout. Sanity baseline.+- `playbooks/ina226-vssop10.svg` — VSSOP-10 v3 layout. xsize=1.4 mm,+  ysize=0.3 mm, rot=0 (Convention B above). Same physical pad as KiCad's+  rot=90 + size (0.3, 1.4) encoding.+- `playbooks/drv8316-all-variants.svg` — DRV8316RRGFR rendered with+  `--all`, showing the 41-pad IPC_A / IPC_B / IPC_C variants and the+  57-pad MFG variant stacked. The IPC variants match KiCad pin-for-pin+  (Convention A); MFG has extra fiducials, test points, and EP heatsink+  vias that don't appear in the chip's electrical pinout.++### chipsmith integration screenshot — DRV8316 in the 3D viewer++When the parser feeds chipsmith Tab 4, the Altium footprint appears as a+first-class Outline citizen alongside the KiCad and Fusion footprints. The+chipsmith view of `DRV8316RRGFR.step` shows:++- **Outline panel:** `KiCad footprint (41 pads)`, `Fusion (.lbr) footprint+  (41 pads)`, `Altium (.PcbLib) footprint (57)` — selected, drawing+  the MFG variant — plus `Heatsink (15 vias + EP pour)` and `Faux PCB+  (1.6 mm FR4)`.+- **3D canvas:** the full QFN-40 chip body with all 40 perimeter pads in+  their correct positions, the EP centred under the body, and the heatsink+  via grid drilled through the 1.6 mm FR4 plate. Tooltip on the FR4 plate+  shows `size 15.40 × 1.60 × 17.05 mm`.++The visual confirms three things at once: pads land in the right spots,+the multi-variant picker surfaces all four package options, and the+overlay layers stack correctly in the chipsmith Outline.++## Trouble-shooting checklist when a downstream renders pads wrong++1. **Run the authoritative SVG first.** `altium-pcblib-svg <path>.PcbLib > /tmp/auth.svg`. If pads are right in the SVG, the bug is downstream — not in this crate.+2. **Check which variant got picked.** `parsed.module_name` plus `_alternates[*].module_name`. Manufacturing variants often have extra pads vs the IPC pinout variant; concur uses `parse_with_consensus_hint` to pick the right one.+3. **Check rotation convention.** If pad 1 looks correct but pad 13 looks rotated 90° wrong, you're probably hitting Convention B vs A (see Format quirks #4) — your downstream is comparing or rendering size literally without applying rotation.+4. **Check Y direction.** If pads are mirrored top-to-bottom, your downstream double-flipped Y. Parser already emits KiCad Y-down.+5. **Scale.** SVG units are mm. If the rendered size is off by 1000×, the consumer is treating mm as µm or similar.++## Adding a new chip to the test corpus++When a new Altium PcbLib comes through chip-fetcher and you want to add+it as a regression test:++1. Drop the `.PcbLib` next to the existing test inputs (or use+   `chip-fetcher/library/<MPN>/`).+2. Run `altium-pcblib-svg <path> -o /tmp/new-chip.svg` and eyeball it.+3. Compare to the chip's `.kicad_mod` (or vendor-published IPC drawing).+4. If correct, add an `include_bytes!` test in `src/lib.rs::tests` like+   `lm358d_decodes_eight_pads_pad1_at_top_left` — assert pad count + a+   couple of representative positions.
install.shadded+15
@@ -0,0 +1,15 @@+#!/bin/bash+set -e+PKG_DIR="$(cd "$(dirname "$0")" && pwd)"+SKILLS_DIR="$HOME/.claude/skills"+mkdir -p "$SKILLS_DIR/altium-pcblib"+if [ -f "$PKG_DIR/SKILL.md" ]; then+  cp "$PKG_DIR/SKILL.md" "$SKILLS_DIR/altium-pcblib/SKILL.md"+fi+for skill_dir in "$PKG_DIR/skills"/*/; do+  [ -d "$skill_dir" ] || continue+  skill_name=$(basename "$skill_dir")+  mkdir -p "$SKILLS_DIR/$skill_name"+  cp -r "$skill_dir"* "$SKILLS_DIR/$skill_name/"+done+echo "Installed altium-pcblib"
package.jsonadded+11
@@ -0,0 +1,11 @@+{+  "slug": "altium-pcblib",+  "version": "1.0.0",+  "type": "skill",+  "description": "Native Rust parser for Altium .PcbLib binary footprint libraries — used by chipsmith + concur for cross-source agreement.",+  "dependencies": {},+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  }+}
uninstall.shadded+4
@@ -0,0 +1,4 @@+#!/bin/bash+set -e+rm -rf "$HOME/.claude/skills/altium-pcblib"+echo "Uninstalled altium-pcblib"