board
OCuLink Riser for the Supermicro AIOM Slot
Public Unreviewedby John Lauer
Passive OCP NIC 3.0 SFF card that breaks a Supermicro AIOM slot's PCIe x16 into four OCuLink 4i ports (4 x4, Gen4): 6-layer, 85 ohm, length-matched, DRC-clean KiCad project with Gerbers, BOM and CPL.
main
John Lauer
README: component table per the wiki format, open-source section, no duplicate hero; generators at gen/
39a59ef
7d ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
#!/usr/bin/env python3
"""Land design/silk_plan.json on the live board through the KiCad bridge, in small groups (one undo step each), so
the silkscreen step's clip shows the labels going on. Writes design/silk_events.json (time, group, ids, reason)."""
import json, os, subprocess, sys, time
ROOT = os.path.join(os.path.dirname(__file__), "..")
BOARD = "C:/Users/john/Documents/adom-aiflow/ocp-oculink-riser/ocp-oculink-riser.kicad_pcb"
THREAD = "OCP OCuLink riser"
plan = json.load(open(os.path.join(ROOT, "design", "silk_plan.json")))
events = []
def bridge(verb, args):
args = dict(args, reason=args.get("reason", "silkscreen step: land the reviewed label plan"))
out = subprocess.run(["adom-bridge", "--ai-thread", THREAD, verb, json.dumps(args)], capture_output=True, text=True, timeout=300)
try:
d = json.loads(out.stdout)
except Exception:
raise SystemExit(f"{verb}: unreadable reply {out.stdout[:300]} {out.stderr[:300]}")
r = d.get("result", d)
if isinstance(r, dict) and "output" in r and isinstance(r["output"], str):
try:
r = json.loads(r["output"]).get("data", r)
except Exception:
pass
return d, r
def revision():
_, r = bridge("kicad_placement_state", {"filePath": BOARD})
return r.get("revision")
def group_of(item):
if item.get("field"):
ref = item["ref"]
y = item["y"]
return f"refs {'faceplate' if y < 20 else 'logic' if y < 75 else 'edge'}"
t = item["text"]
if item["layer"] == "B.SilkS":
return "bottom face"
if t.startswith(("OCP_B", "B")) and item["y"] > 105:
return "finger pins"
if t.startswith("J") or "PORT" in t:
return "ports"
return "labels"
ORDER = ["finger pins", "ports", "refs faceplate", "refs logic", "labels", "refs edge", "bottom face"]
# KiCad 10.0.5's IPC server cannot update footprint fields: references/values go in by a disclosed saved-file edit
# (gen/silk.py apply on the saved board); this script lands the standalone texts live. ONLY limits a rerun.
ONLY = sys.argv[1].split(",") if len(sys.argv) > 1 else ORDER
groups = {g: [] for g in ORDER}
for f in plan["fields"]:
groups[group_of(f)].append(("field", f))
for t in plan["texts"]:
groups[group_of(t)].append(("text", t))
rev = revision()
for g in ORDER:
if g not in ONLY:
continue
items = [it for it in groups[g] if it[0] == "text"]
# small batches: a ref/value pair or a few labels at a time, in reading order
items.sort(key=lambda it: (round(it[1]["y"] / 6), it[1]["x"]))
for k in range(0, len(items), 4):
chunk = items[k:k + 4]
fields = [dict(id=it["id"], x=it["x"], y=it["y"], rotation=it["rotation"], layer=it["layer"], height=it["height"],
width=it["height"], stroke=it["stroke"], visible=True) for kind, it in chunk if kind == "field"]
texts = [dict(text=it["text"], layer=it["layer"], x=it["x"], y=it["y"], height=it["height"], width=it["height"],
stroke=it["stroke"], rotation=it["rotation"], align=it["align"]) for kind, it in chunk if kind == "text"]
t0 = time.time()
if fields:
d, r = bridge("kicad_silk_fields_batch", {"filePath": BOARD, "expectedRevision": rev, "update": fields})
if d.get("status") != "ok" and not r.get("revision"):
print("fields batch refused:", json.dumps(d)[:600]); sys.exit(1)
rev = r.get("revision") or revision()
if texts:
d, r = bridge("kicad_silk_text_batch", {"filePath": BOARD, "expectedRevision": rev, "create": texts})
if d.get("status") != "ok" and not r.get("revision"):
print("text batch refused:", json.dumps(d)[:600]); sys.exit(1)
rev = r.get("revision") or revision()
events.append(dict(t=round(t0, 1), group=g, op="place",
items=[(it.get("ref") or "") + ("." + it["field"] if kind == "field" else "") + (it.get("text", "") if kind == "text" else "")
for kind, it in chunk],
reason=[it.get("why") or f"{it['ref']} {it['field'].lower()} beside its part" for kind, it in chunk]))
print(f"{g}: {', '.join(events[-1]['items'])}", flush=True)
time.sleep(1.2)
_ev = os.path.join(ROOT, "design", "silk_events.json")
prev = json.load(open(_ev)) if os.path.exists(_ev) and len(sys.argv) > 1 else []
json.dump(prev + events, open(_ev, "w"), indent=1)
print("landed", sum(len(e["items"]) for e in events), "labels in", len(events), "steps; revision", rev)