wiring/validate.py
7.2 KB · 117 lines · python · Download from the repository page
# Structural + rendered-output checks for the wiring set. Writes PU_Validation_Rev-A.md. Exit 1 on FAIL.import json, os, re, sys, xml.etree.ElementTree as ETsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))import model, sheet as S HERE = os.path.dirname(os.path.abspath(__file__))results = [] # (check, status, detail)def rec(check, status, detail=""): results.append((check, status, detail)) # ---- structural -------------------------------------------------------------------------------ids = [c["id"] for c in model.CONNECTIONS] + [p[0] for p in model.POWER_LINKS] + [o[0] for o in model.OPEN_ITEMS]dups = sorted({i for i in ids if ids.count(i) > 1})rec("Unique connection / link / open-item IDs", "FAIL" if dups else "PASS", f"duplicates: {dups}" if dups else f"{len(ids)} IDs") bad = []for c in model.CONNECTIONS: for end in (c["frm"], c["to"]): eq, conn = end if eq not in model.EQUIPMENT: bad.append(f"{c['id']}: unknown equipment {eq}") elif conn not in ("—","GND","PACK−","shield","J4/J5/J6/J7 shells") and (eq,conn) not in model.CONNECTORS: bad.append(f"{c['id']}: unknown connector {eq}.{conn}")rec("Every connection references existing equipment and connectors", "FAIL" if bad else "PASS", "; ".join(bad) or f"{len(model.CONNECTIONS)} connections") sheet_nos = {n for n,_ in model.SHEETS}bad = [f"{c['id']}→{s}" for c in model.CONNECTIONS for s in c["sheets"] if s not in sheet_nos]rec("Connection sheet cross-references resolve", "FAIL" if bad else "PASS", "; ".join(bad) or "all resolve") mezz = sum(p[2] for p in model.MEZZ_PINS)rec("Mezzanine allocation totals 80 pins", "PASS" if mezz == 80 else "FAIL", f"{mezz} pins across {len(model.MEZZ_PINS)} groups") conn_pins = model.CONNECTORS[("MB","J12")][2]rec("Pack pinout matches J12 position count", "PASS" if len(model.PINOUTS["J12 Battery pack (MB) / J1 (PB)"]["pins"]) == conn_pins else "FAIL", f"{conn_pins} positions") # ---- rendered SVG checks ------------------------------------------------------------------------svgs = sorted(f for f in os.listdir(os.path.join(HERE,"sheets")) if f.endswith(".svg"))expected = [f"{model.sheet_no(n)}.svg" for n,_ in model.SHEETS]rec("Every sheet in the index exists as an SVG", "PASS" if svgs == expected else "FAIL", f"{len(svgs)} files; missing {sorted(set(expected)-set(svgs))}, extra {sorted(set(svgs)-set(expected))}") def num(v): try: return float(v) except: return Noneoob_total = 0; overlap_total = 0; per_sheet = []id_presence_fail = []for f in svgs: p = os.path.join(HERE,"sheets",f); root = ET.parse(p).getroot() ns = {"s":"http://www.w3.org/2000/svg"} oob = 0 for el in root.iter(): tag = el.tag.split("}")[-1] if tag == "rect": x,y,w,h = (num(el.get(a)) for a in ("x","y","width","height")) if None in (x,y,w,h): continue if el.get("width") == "100%": continue if x < 0 or y < 0 or x+w > S.W+0.5 or y+h > S.H+0.5: oob += 1 elif tag == "line": for a in ("x1","x2"): v = num(el.get(a)); if v is not None and (v < 0 or v > S.W+0.5): oob += 1 for a in ("y1","y2"): v = num(el.get(a)); if v is not None and (v < 0 or v > S.H+0.5): oob += 1 elif tag == "text": x,y = num(el.get("x")), num(el.get("y")) if x is None or y is None or x < 0 or x > S.W or y < 0 or y > S.H: oob += 1 meta = json.load(open(os.path.join(HERE,"preview",f.replace(".svg",".meta.json")))) # text-overlap heuristic on estimated bboxes (ignores table cell text against its own row, which cannot overlap by construction) T = [t for t in meta["texts"] if t[5] not in ("BORDER",)] ov = [] for i in range(len(T)): x0,y0,x1,y1,ta,la = T[i] for j in range(i+1, len(T)): u0,v0,u1,v1,tb,lb = T[j] if x0 < u1-1 and u0 < x1-1 and y0 < v1-1 and v0 < y1-1: ov.append((ta[:40], tb[:40])) fx0,fy0,fx1,fy1 = meta["frame"] outside_frame = [t[4][:40] for t in T if t[5] not in ("BORDER","TITLE") and (t[0] < fx0-1 or t[2] > fx1+1 or t[1] < fy0-1 or t[3] > fy1+1)] # every connection that claims this sheet must be mentioned by ID on it n = f.split("-")[-1].replace(".svg","") missing = [c["id"] for c in model.CONNECTIONS if n in c["sheets"] and c["id"] not in meta["ids"]] if missing: id_presence_fail.append(f"{f}: {missing}") per_sheet.append((f, oob, len(ov), ov[:12], outside_frame[:8])) oob_total += oob; overlap_total += len(ov) rec("Geometry inside the sheet bounds (1587 × 1123)", "PASS" if oob_total == 0 else "FAIL", f"{oob_total} out-of-bounds primitives")rec("Connection IDs appear on every sheet they are assigned to", "FAIL" if id_presence_fail else "PASS", "; ".join(id_presence_fail) or "all present")rec("Estimated text-bbox overlaps (heuristic, 0.53 em advance)", "PASS" if overlap_total == 0 else "WARN", f"{overlap_total} candidate overlaps — see per-sheet list; confirmed visually on PNG previews")of = sum(len(x[4]) for x in per_sheet)rec("Annotation text inside the drawing frame", "PASS" if of == 0 else "FAIL", f"{of} texts outside the frame") pngs = sorted(f for f in os.listdir(os.path.join(HERE,"preview")) if f.endswith(".png"))rec("PNG preview rendered for every sheet (headless Chrome)", "PASS" if len(pngs) == len(svgs) else "FAIL", f"{len(pngs)} PNG / {len(svgs)} SVG")pdf = os.path.join(HERE, "PU_Wiring-Set_Rev-A.pdf")if os.path.exists(pdf): n = len(re.findall(rb"/Type\s*/Page[^s]", open(pdf,"rb").read())) rec("Combined PDF page count equals sheet count", "PASS" if n == len(svgs) else "FAIL", f"{n} pages / {len(svgs)} sheets, {os.path.getsize(pdf)//1024} kB")else: rec("Combined PDF page count equals sheet count", "NOT RUN", "PDF not found — run render.mjs without arguments")rec("Electrical rule check / netlist", "NOT APPLICABLE", "SVG diagrams carry no netlist; ERC belongs to the EVT schematic capture")rec("Native CAD (DXF/DWG) export", "NOT RUN", "no CAD tool or ezdxf available in this environment; SVG is the editable source") # ---- report -----------------------------------------------------------------------------------lines = [f"# PoppyUp wiring set — validation report (Rev {model.REV}, {model.DATE})", "", f"Baseline {model.BASELINE}. Status **{model.STATUS}**. Generated by `wiring/validate.py` after `build.py` and `render.mjs`.", "", "| # | Check | Result | Detail |", "|---|---|---|---|"]for i,(c,s,d) in enumerate(results, 1): lines.append(f"| {i} | {c} | **{s}** | {d} |")lines += ["", "## Per-sheet render checks", "", "| Sheet | Out-of-bounds primitives | Text-overlap candidates | Examples |", "|---|---|---|---|"]for f,oob,nov,ex,of in per_sheet: exs = "; ".join(f"“{a}” ↔ “{b}”" for a,b in ex) or "—" if of: exs += f"; outside frame: {of}" lines.append(f"| {f} | {oob} | {nov} | {exs} |")lines += ["", "## Open items carried on PU-W-000", ""]for oid,t,d,src in model.OPEN_ITEMS: lines.append(f"- **{oid} {t}** — {d} (docs {src})")lines += ["", "Result key: PASS / FAIL / WARN / NOT RUN / NOT APPLICABLE. A WARN on the text-overlap heuristic is not a defect by itself; the PNG previews in `wiring/preview/` are the visual record."]open(os.path.join(HERE, "PU_Validation_Rev-A.md"),"w").write("\n".join(lines)+"\n")fails = [r for r in results if r[1] == "FAIL"]print("\n".join(f"{s:15s} {c} — {d}" for c,s,d in results))sys.exit(1 if fails else 0)