app
adom-project-manager
Public Made by Adomby adom
Adom Project Manager — browse your wiki boards/projects (Personal / Organization / Public), view symbol + footprint + 3D and the per-EDA footprint layer stackup, explore the board and schematic in INT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#!/usr/bin/env python3
"""Analyze (and optionally normalize) a KiCad project for wiki publishing.
Given a project directory (already pulled from the user's machine), report the
render files, the 3D-model refs (standard vs custom — the custom ones must be
bundled into <Board>.3dshapes/), and the component list with MPN -> slug so the
library's dependencies can be filled.
With --normalize, rewrite every CUSTOM (model "...") ref in the .kicad_pcb to a
project-relative <Board>.3dshapes/<basename> path. This is REQUIRED before
publishing: machine-specific absolute paths (/home/<user>/…, C:\\Users\\<user>\\…)
resolve nowhere on the render service or on anyone else's machine. Relative paths
with forward slashes work on every OS and KiCad resolves them next to the .pcb.
Only the path string is changed; each model's offset/scale/rotate is untouched,
and ${KICAD*_3DMODEL_DIR} standard refs are left alone.
usage: kicad_board_bundle.py <project-dir> [--normalize]
prints JSON: { pro, pcb, schematics[], sym, pretty[], board_name,
models_std[], models_custom[], components[{ref,value,mpn,slug}],
normalized? }
"""
import sys, os, re, json, glob
def slugify(s):
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
def basename(path):
return re.split(r"[\\/]", path)[-1]
def normalize_pcb(pcb_path, shapes_dir):
"""Rewrite custom (model "...") refs to <shapes_dir>/<basename>. Returns the
count of refs actually changed. ${...} standard refs are left untouched."""
txt = open(pcb_path, encoding="utf-8", errors="ignore").read()
changed = [0]
def repl(m):
path = m.group(1)
if path.startswith("${"):
return m.group(0)
newp = f"{shapes_dir}/{basename(path)}"
if newp != path:
changed[0] += 1
return f'(model "{newp}"'
out = re.sub(r'\(model\s+"([^"]+)"', repl, txt)
if changed[0]:
open(pcb_path, "w", encoding="utf-8").write(out)
return changed[0]
def main(d, normalize=False):
g = lambda pat: sorted(glob.glob(os.path.join(d, "**", pat), recursive=True))
pro = (g("*.kicad_pro") or [None])[0]
pcb = (g("*.kicad_pcb") or [None])[0]
schs = g("*.kicad_sch")
sym = (g("*.kicad_sym") or [None])[0]
pretty = sorted(p for p in g("*.pretty") if os.path.isdir(p))
board_name = os.path.splitext(os.path.basename(pro or pcb or "board"))[0]
result = {
"pro": pro, "pcb": pcb, "schematics": schs, "sym": sym, "pretty": pretty,
"board_name": board_name,
}
# normalize BEFORE reporting so the reported refs reflect the fixed file
if normalize and pcb:
result["normalized"] = normalize_pcb(pcb, f"{board_name}.3dshapes")
# 3D model refs from the PCB: ${KICAD*_3DMODEL_DIR}/... resolve on the render
# service; everything else is custom and must be bundled into <Board>.3dshapes/.
models_std, models_custom = set(), set()
if pcb:
txt = open(pcb, encoding="utf-8", errors="ignore").read()
for m in re.findall(r'\(model\s+"([^"]+)"', txt):
(models_std if m.startswith("${") else models_custom).add(m)
result["models_std"] = sorted(models_std)
result["models_custom"] = sorted(models_custom)
# components: each schematic symbol instance's Reference + an MPN-like property
mpn_keys = ("MPN", "Manufacturer Part Number", "Mfr Part #", "Mfr. Part #",
"LCSC", "LCSC Part", "LCSC Part #", "Part Number", "Value")
comps = {}
for sch in schs:
t = open(sch, encoding="utf-8", errors="ignore").read()
for block in re.split(r"\n\s*\(symbol\s", t):
ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', block)
if not ref_m:
continue
ref = ref_m.group(1)
if ref.startswith("#") or ref in comps: # power symbols / dupes
continue
mpn = None
for k in mpn_keys:
mm = re.search(r'\(property\s+"' + re.escape(k) + r'"\s+"([^"]+)"', block)
if mm and mm.group(1).strip() and mm.group(1) not in ("~", ""):
mpn = mm.group(1).strip()
break
val_m = re.search(r'\(property\s+"Value"\s+"([^"]+)"', block)
comps[ref] = {
"ref": ref,
"value": val_m.group(1) if val_m else "",
"mpn": mpn,
"slug": slugify(mpn) if mpn else None,
}
result["components"] = list(comps.values())
print(json.dumps(result, indent=2))
if __name__ == "__main__":
args = [a for a in sys.argv[1:] if not a.startswith("--")]
if not args:
sys.exit("usage: kicad_board_bundle.py <project-dir> [--normalize]")
main(args[0], normalize="--normalize" in sys.argv)