mohan-bee/rc-link-breakaway-panel
A four-layer RC link panel combining an STM32/E28 2.4‑GHz transmitter with USB-RP2040 socket and an STM32/E28 receiver providing six buffered PWM servo outputs, regulated 3.3‑V power, headers, controls, and protection/decoupling.
- Version
- 0.1.5
- License
- unset
- Stars
- 0
tools/audit-panel.py
import contextlib, io, json, os, runpy
from pathlib import Path
from shapely.geometry import Polygon, Point, box
from shapely.affinity import translate
from shapely.ops import unary_union
from shapely.validation import make_valid
root = Path(__file__).resolve().parents[1]
def load(path, report):
os.environ["PHONE_TX_AUDIT_CIRCUIT"] = str(path)
os.environ["PHONE_TX_AUDIT_REPORT_DIR"] = str(root / "validation" / report)
with contextlib.redirect_stdout(io.StringIO()):
env = runpy.run_path(str(root / "tools/audit-copper.py"))
shapes = env["shapes"]
for e in env["c"]:
if e["type"] != "pcb_copper_pour": continue
b = e["brep_shape"]
p = make_valid(Polygon([(v["x"],v["y"]) for v in b["outer_ring"]["vertices"]], [[(v["x"],v["y"]) for v in r["vertices"]] for r in b["inner_rings"]]))
net=next(n['subcircuit_connectivity_map_key'] for n in env['c'] if n['type']=='source_net' and n['source_net_id']==e['source_net_id'])
shapes.append(dict(geom=p, layers={e["layer"]}, kind="pour", net=net))
return env["c"], shapes
c, actual = load(root / "dist/index/circuit.json", "panel")
layers = ["top", "inner1", "inner2", "bottom"]
actual_layers = {layer: unary_union([s["geom"] for s in actual if layer in s["layers"]]) for layer in layers}
results = {}
units = []
for name, dx, dy in [("transmitter", -23.5, 0), ("receiver", 36.5, -3)]:
original, expected = load(root / "boards" / f"{name}.circuit.json", name)
b = next(e for e in original if e["type"] == "pcb_board")
outline = translate(Polygon([(v["x"], v["y"]) for v in b["outline"]]), dx, dy)
units.append(outline)
results[name] = {}
# Only the receiver's servo supply and ground were intentionally rerouted.
excluded_original={e['subcircuit_connectivity_map_key'] for e in original if e['type']=='source_net' and name=='receiver' and e['name'] in ['GND','V_SERVO']}
excluded_actual={e['subcircuit_connectivity_map_key'] for e in c if e['type']=='source_net' and name=='receiver' and e['name'] in ['RX_GND','RX_V_SERVO']}
for layer in layers:
target = translate(unary_union([s["geom"] for s in expected if layer in s["layers"] and s['net'] not in excluded_original]), dx, dy)
measured = unary_union([s['geom'] for s in actual if layer in s['layers'] and s['net'] not in excluded_actual]).intersection(outline)
missing = target.difference(measured.buffer(.00001)).area
added = measured.difference(target.buffer(.00001)).area
results[name][layer] = dict(missing_mm2=missing, added_mm2=added)
assert missing < .001 and added < .001, (name, layer, missing, added)
g = json.loads((root / "panel-geometry.json").read_text())
for layer, copper in actual_layers.items():
outside = copper.difference(unary_union(units))
fiducials = unary_union([Point(p['x'],p['y']).buffer(.501) for p in g['fiducials']]) if layer == 'top' else Polygon()
assert outside.difference(fiducials).area < .00001, (layer, 'copper crosses a breakaway tab or rail')
slots = unary_union([Polygon([(p["x"],p["y"]) for p in pts]) for pts in g["slots"]])
material = box(-66.5,-38,66.5,38).difference(slots)
assert material.geom_type == "Polygon", "Panel must be one connected piece"
for hole in (e for e in c if e["type"] == "pcb_hole"):
material = material.difference(Point(hole["x"], hole["y"]).buffer(hole["hole_diameter"]/2))
assert material.geom_type == "Polygon", "Mouse bites must leave connecting webs"
slot_gap = {layer: slots.distance(copper) for layer, copper in actual_layers.items()}
assert min(slot_gap.values()) >= .499, slot_gap
hole_gap = min(Point(p["x"],p["y"]).distance(copper) - .3 for p in g["mouseBites"] for copper in actual_layers.values())
assert hole_gap >= .249, hole_gap
assert len([e for e in c if e["type"] == "pcb_hole"]) == 96
assert len([e for e in c if e["type"] == "pcb_plated_hole"]) == 54
assert len([e for e in c if e["type"] == "pcb_via"]) == 268
for e in c:
if e["type"] in ["pcb_plated_hole", "pcb_via"]:
assert set(e["layers"]) == set(layers), e
report = dict(status="pass", preserved_copper=results, comparison_scope={'transmitter':'all copper','receiver':'all copper except intentionally rerouted RX_GND and RX_V_SERVO; these are verified by servo-power-audit and copper-connectivity'}, router_slot_copper_gap_mm=slot_gap,
mouse_bite_copper_gap_mm=hole_gap, npth_count=96, pth_count=322,
connected_before_clipping=True, scope="Geometry and connectivity only; no physical hardware qualification")
(root / "validation/panel-audit.json").write_text(json.dumps(report, indent=2)+"\n")
print(json.dumps(report, indent=2))