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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
#!/usr/bin/env python3
"""Generate kicad/ocp-oculink-riser.kicad_sch from design/netlist.json.
Every pin gets a short wire and a net label (a 168-pin card edge and four 42-pin receptacles read best as labelled
blocks); unused pins get no-connect flags; power nets get PWR_FLAG. Symbols are embedded (lib_symbols): standard
ones copied verbatim from KiCad 10's own libraries, project ones (OCP edge, OCuLink, the ICs) generated here with the
datasheet pin numbers and names."""
import json, os, re, sys, uuid, datetime
sys.path.insert(0, os.path.dirname(__file__))
from footprints import ocp_pinout
HERE = os.path.dirname(__file__)
ROOT = os.path.join(HERE, "..")
KSYM = "/mnt/c/Users/john/AppData/Local/Programs/KiCad/10.0/share/kicad/symbols"
OUT = os.path.join(ROOT, "kicad", "ocp-oculink-riser.kicad_sch")
NS = uuid.UUID("0b8b4a1e-7f6d-4f0e-9d3a-2a6a1c5b9e71")
nl = json.load(open(os.path.join(ROOT, "design", "netlist.json")))
pin_net = {p: n for n, ps in nl["nets"].items() for p in ps}
comp = {c["ref"]: c for c in nl["components"]}
ROOT_UUID = str(uuid.uuid5(NS, "root"))
def U(*k):
return str(uuid.uuid5(NS, "/".join(map(str, k))))
def f(v):
s = f"{v:.4f}".rstrip("0").rstrip(".")
return "0" if s in ("", "-0") else s
def q(s):
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
# ------------------------------------------------------------------ s-expression extraction from KiCad libs
def extract(libfile, name):
txt = open(libfile).read()
m = re.search(r'\n[ \t]*\(symbol "' + re.escape(name) + r'"\s*\n', txt)
if not m:
raise SystemExit(f"{name} not in {libfile}")
i = txt.index("(", m.start())
depth, j, n = 0, i, len(txt)
while j < n:
c = txt[j]
if c == '"':
j += 1
while j < n and txt[j] != '"':
j += 2 if txt[j] == "\\" else 1
elif c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
return txt[i:j + 1]
j += 1
raise SystemExit("unbalanced")
def lib_symbol(lib, name):
s = extract(os.path.join(KSYM, lib + ".kicad_sym"), name)
return s.replace(f'(symbol "{name}"', f'(symbol "{lib}:{name}"', 1)
# ------------------------------------------------------------------ generated symbols
def gen_symbol(libid, refp, left, right, top=(), bottom=(), width=None, units=None, extra_text=None):
"""left/right: lists of (number, name) or None for a gap. Returns (sexp, pin_positions{number:(x,y,angle)})."""
name = libid.split(":")[1]
P = 2.54
if units is None:
units = [(left, right, top, bottom)]
out = [f'(symbol {q(libid)} (pin_names (offset 1.016)) (exclude_from_sim no) (in_bom yes) (on_board yes)',
f' (property "Reference" {q(refp)} (at 0 0 0) (effects (font (size 1.27 1.27))))',
f' (property "Value" {q(name)} (at 0 0 0) (effects (font (size 1.27 1.27))))',
f' (property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))',
f' (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))']
pos = {}
for ui, (lf, rt, tp, bt) in enumerate(units, start=1):
n = max(len(lf), len(rt))
h = (n + 1) * P
maxlen = max([len(x[1]) for x in lf + rt if x] + [4])
w = width or max(20.32, round((maxlen * 1.27 * 2 + 6) / P) * P)
tw = max(len(tp), len(bt))
w = max(w, (tw + 1) * P)
x0, y0 = -w / 2, h / 2
out.append(f' (symbol {q(name + f"_{ui}_1")}')
out.append(f' (rectangle (start {f(x0)} {f(y0)}) (end {f(-x0)} {f(-y0)}) (stroke (width 0.254) (type default)) (fill (type background)))')
for side, lst in (("L", lf), ("R", rt)):
for k, pn in enumerate(lst):
if pn is None:
continue
num, pname = pn
y = y0 - (k + 1) * P
if side == "L":
x, ang = x0 - P, 0
else:
x, ang = -x0 + P, 180
out.append(f' (pin passive line (at {f(x)} {f(y)} {ang}) (length 2.54) (name {q(pname)} (effects (font (size 1.016 1.016)))) (number {q(num)} (effects (font (size 1.016 1.016)))))')
pos[(ui, num)] = (x, y, ang)
for side, lst in (("T", tp), ("B", bt)):
for k, pn in enumerate(lst):
num, pname = pn
x = x0 + (k + 1) * P
if side == "T":
y, ang = y0 + P, 270
else:
y, ang = -y0 - P, 90
out.append(f' (pin passive line (at {f(x)} {f(y)} {ang}) (length 2.54) (name {q(pname)} (effects (font (size 1.016 1.016)))) (number {q(num)} (effects (font (size 1.016 1.016)))))')
pos[(ui, num)] = (x, y, ang)
out.append(' )')
out.append(')')
return "\n".join(out), pos
edge = ocp_pinout()
def edge_units():
def pins(prefix, rng):
return [(f"{prefix}{n}", edge[f"{prefix}{n}"].replace("+3.3V_EDGE", "+3V3_EDGE")) for n in rng]
u1L = [(f"OCP_B{n}", edge[f"OCP_B{n}"]) for n in range(1, 15)] + [None] + pins("B", range(1, 17)) + [None] + pins("B", (42, 68, 69, 70))
u1R = [(f"OCP_A{n}", edge[f"OCP_A{n}"]) for n in range(1, 15)] + [None] + pins("A", range(1, 17)) + [None] + pins("A", (42, 68, 69, 70))
u2L = pins("B", [n for n in range(17, 42)]); u2R = pins("A", [n for n in range(17, 42)])
u3L = pins("B", range(43, 68)); u3R = pins("A", range(43, 68))
return [(u1L, u1R, (), ()), (u2L, u2R, (), ()), (u3L, u3R, (), ())]
def oculink_pins():
A = {1: "PWR_3V3_RX", 2: "GND", 3: "PERp0", 4: "PERn0", 5: "GND", 6: "PERp1", 7: "PERn1", 8: "GND", 9: "BP_TYPE",
10: "CWAKE#", 11: "GND", 12: "REFCLK+", 13: "REFCLK-", 14: "GND", 15: "PERp2", 16: "PERn2", 17: "GND",
18: "PERp3", 19: "PERn3", 20: "GND", 21: "PWR_5V_2"}
Bm = {1: "PWR_5V_1", 2: "GND", 3: "PETp0", 4: "PETn0", 5: "GND", 6: "PETp1", 7: "PETn1", 8: "GND", 9: "2W_CLK",
10: "2W_DATA", 11: "GND", 12: "PERST#", 13: "CPRSNT#", 14: "GND", 15: "PETp2", 16: "PETn2", 17: "GND",
18: "PETp3", 19: "PETn3", 20: "GND", 21: "PWR_3V3_TX"}
return [(f"A{n}", A[n]) for n in range(1, 22)], [(f"B{n}", Bm[n]) for n in range(1, 22)], (), [("SH", "SHIELD")]
SYMS = {}
POS = {}
SYMS["Riser:OCP_NIC3_SFF_4C+"], POS["Riser:OCP_NIC3_SFF_4C+"] = gen_symbol("Riser:OCP_NIC3_SFF_4C+", "J", None, None, units=edge_units(), width=50.8)
l, r, t, b = oculink_pins()
SYMS["Riser:OCuLink_4i_Root"], POS["Riser:OCuLink_4i_Root"] = gen_symbol("Riser:OCuLink_4i_Root", "J", l, r, t, b, width=30.48)
SYMS["Riser:SN74LV165A"], POS["Riser:SN74LV165A"] = gen_symbol(
"Riser:SN74LV165A", "U",
[("1", "SH/~{LD}"), ("2", "CLK"), ("15", "CLK_INH"), ("10", "SER"), None, ("11", "A"), ("12", "B"), ("13", "C"),
("14", "D"), ("3", "E"), ("4", "F"), ("5", "G"), ("6", "H")],
[("9", "QH"), ("7", "~{QH}")], [("16", "VCC")], [("8", "GND")], width=17.78)
SYMS["Riser:BR24H64FJ"], POS["Riser:BR24H64FJ"] = gen_symbol(
"Riser:BR24H64FJ", "U", [("1", "A0"), ("2", "A1"), ("3", "A2"), ("7", "WP")], [("5", "SDA"), ("6", "SCL")],
[("8", "VCC")], [("4", "GND")], width=15.24)
SYMS["Riser:TPS3808G33"], POS["Riser:TPS3808G33"] = gen_symbol(
"Riser:TPS3808G33", "U", [("5", "SENSE"), ("3", "~{MR}"), ("4", "CT")], [("1", "~{RESET}")],
[("6", "VDD")], [("2", "GND")], width=15.24)
SYMS["Riser:SN74LVC125A"], POS["Riser:SN74LVC125A"] = gen_symbol(
"Riser:SN74LVC125A", "U",
[("2", "1A"), ("1", "~{1OE}"), ("5", "2A"), ("4", "~{2OE}"), ("9", "3A"), ("10", "~{3OE}"), ("12", "4A"), ("13", "~{4OE}")],
[("3", "1Y"), None, ("6", "2Y"), None, ("8", "3Y"), None, ("11", "4Y")], [("14", "VCC")], [("7", "GND")], width=15.24)
SYMS["Riser:TPD4E05U06"], POS["Riser:TPD4E05U06"] = gen_symbol(
"Riser:TPD4E05U06", "U", [("1", "D1+"), ("2", "D1-"), ("4", "D2+"), ("5", "D2-")],
[("10", "NC"), ("9", "NC"), ("7", "NC"), ("6", "NC")], (), [("3", "GND"), ("8", "GND")], width=12.7)
# Adom-style IC symbols (gen/ic_symbols.py -> adom-symbol create, chip outline baked in)
ADOMSYM = os.path.join(ROOT, "kicad", "lib", "adom-symbols")
for name in ("SN74LV165APWR", "BR24H64FJ-5ACE2", "TPS3808G33DBV", "SN74LVC125APWR", "TPD4E05U06QDQARQ1"):
txt = extract(os.path.join(ADOMSYM, name, name + ".kicad_sym"), name)
txt = txt.replace(f'(symbol "{name}"', f'(symbol "Riser:{name}"', 1)
SYMS[f"Riser:{name}"] = txt
POS[f"Riser:{name}"] = None # filled below by std_pins
STD = {"R": ("Device", "R"), "C": ("Device", "C"), "LED": ("Device", "LED"), "TP": ("Connector", "TestPoint"),
"JP": ("Jumper", "SolderJumper_2_Open"), "H": ("Mechanical", "MountingHole_Pad"), "PWR": ("power", "PWR_FLAG")}
# pin positions of standard symbols (read from the library text: (pin ... (at x y a) ... (number "n"
def std_pins(text):
pos = {}
for m in re.finditer(r'\(pin \w+ \w+\s*\(at ([-\d.]+) ([-\d.]+) ([-\d.]+)\).*?\(number "([^"]+)"', text, re.S):
pos[(1, m.group(4))] = (float(m.group(1)), float(m.group(2)), float(m.group(3)))
return pos
for key, (lib, name) in STD.items():
txt = lib_symbol(lib, name)
SYMS[f"{lib}:{name}"] = txt
POS[f"{lib}:{name}"] = std_pins(txt)
for k in list(POS):
if POS[k] is None:
POS[k] = std_pins(SYMS[k])
SYMOF = {}
for c in nl["components"]:
r = c["ref"]
if r == "J1": SYMOF[r] = "Riser:OCP_NIC3_SFF_4C+"
elif r in ("J2", "J3", "J4", "J5"): SYMOF[r] = "Riser:OCuLink_4i_Root"
elif r == "U1": SYMOF[r] = "Riser:SN74LV165APWR"
elif r == "U2": SYMOF[r] = "Riser:BR24H64FJ-5ACE2"
elif r == "U3": SYMOF[r] = "Riser:TPS3808G33DBV"
elif r == "U4": SYMOF[r] = "Riser:SN74LVC125APWR"
elif r in ("U5", "U6", "U7", "U8"): SYMOF[r] = "Riser:TPD4E05U06QDQARQ1"
elif r.startswith("R"): SYMOF[r] = "Device:R"
elif r.startswith("C"): SYMOF[r] = "Device:C"
elif r.startswith("D"): SYMOF[r] = "Device:LED"
elif r.startswith("TP"): SYMOF[r] = "Connector:TestPoint"
elif r.startswith("JP"): SYMOF[r] = "Jumper:SolderJumper_2_Open"
elif r.startswith("H"): SYMOF[r] = "Mechanical:MountingHole_Pad"
# ------------------------------------------------------------------ placement on an A1 sheet (mm, y down; 2.54 grid)
G = 2.54
def g(v):
return round(v / G) * G
PLACE = {} # ref -> list of (unit, x, y, rot)
PLACE["J1"] = [(1, g(60), g(110), 0), (2, g(60), g(285), 0), (3, g(60), g(430), 0)]
for k in range(4):
PLACE[f"J{2+k}"] = [(1, g(470 + 95 * k), g(105), 0)]
PLACE["U1"] = [(1, g(210), g(95), 0)]
PLACE["U2"] = [(1, g(210), g(190), 0)]
PLACE["U3"] = [(1, g(320), g(95), 0)]
PLACE["U4"] = [(1, g(320), g(190), 0)]
for k in range(4):
PLACE[f"U{5+k}"] = [(1, g(470 + 95 * k), g(235), 0)]
# two-pin parts: vertical (rot 90) in rows under their blocks
row = {"scan": (g(175), g(150)), "fru": (g(175), g(240)), "power": (g(285), g(140)), "perst": (g(285), g(255)),
"host": (g(175), g(285)), "test": (g(285), g(320))}
two = [c for c in nl["components"] if c["ref"] not in PLACE and c["ref"] not in ("H1", "H2")]
cursor = {}
for c in two:
blk = c["block"]
if blk.startswith("port"):
k = int(blk[-1]); base = (g(452 + 95 * k), g(285))
elif blk == "mech":
base = (g(285), g(380))
else:
base = row.get(blk, (g(285), g(380)))
n = cursor.get(base, 0); cursor[base] = n + 1
PLACE[c["ref"]] = [(1, base[0] + g(15.24) * (n % 6), base[1] + g(22.86) * (n // 6), 0)]
PLACE["H1"] = [(1, g(400), g(400), 0)]
PLACE["H2"] = [(1, g(420), g(400), 0)]
# ------------------------------------------------------------------ emit
items = []
labels_at = set()
def pin_world(libid, unit, num, x, y, rot):
px, py, ang = POS[libid][(unit, num)] if (unit, num) in POS[libid] else POS[libid][(1, num)]
# symbol coords are y-up; rotation not used (all 0)
return x + px, y - py, ang
def wire(x1, y1, x2, y2):
items.append(f' (wire (pts (xy {f(x1)} {f(y1)}) (xy {f(x2)} {f(y2)})) (stroke (width 0) (type default)) (uuid {q(U("w", x1, y1, x2, y2))}))')
def label(name, x, y, ang):
just = "left" if ang in (0, 270) else "right"
items.append(f' (global_label {q(name)} (shape passive) (at {f(x)} {f(y)} {ang}) (fields_autoplaced yes) (effects (font (size 1.016 1.016)) (justify {just})) (uuid {q(U("l", name, x, y))})\n (property "Intersheetrefs" "${{INTERSHEET_REFS}}" (at {f(x)} {f(y)} 0) (effects (font (size 1.016 1.016)) (hide yes))))')
def noconn(x, y):
items.append(f' (no_connect (at {f(x)} {f(y)}) (uuid {q(U("nc", x, y))}))')
syms_out = []
for c in nl["components"]:
ref = c["ref"]; libid = SYMOF[ref]
for (unit, x, y, rot) in PLACE[ref]:
# snap: shift the instance so its pin ends land on the 2.54 mm grid (world x = X + px, world y = Y - py)
_pp = [v for (u_, n_), v in POS[libid].items() if u_ == unit]
if _pp:
px0, py0 = _pp[0][0], _pp[0][1]
x = x + ((-px0) % G if abs((-px0) % G - G) > 1e-6 else 0.0) - 0.0
y = y + (py0 % G if abs(py0 % G - G) > 1e-6 else 0.0)
x, y = round(x, 4), round(y, 4)
props = [f' (property "Reference" {q(ref)} (at {f(x)} {f(y - 3)} 0) (effects (font (size 1.27 1.27))))',
f' (property "Value" {q(c["value"])} (at {f(x)} {f(y + 3)} 0) (effects (font (size 1.27 1.27))))',
f' (property "Footprint" {q(c["fp"])} (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))',
f' (property "Datasheet" "" (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))',
f' (property "MPN" {q(c.get("mpn",""))} (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))',
f' (property "Manufacturer" {q(c.get("mfr",""))} (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))',
f' (property "Description" {q(c.get("desc",""))} (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))']
if libid.startswith(("Device:", "Connector:", "Jumper:", "Mechanical:")):
props[0] = f' (property "Reference" {q(ref)} (at {f(x + 2.54)} {f(y - 1.27)} 0) (effects (font (size 1.27 1.27)) (justify left)))'
props[1] = f' (property "Value" {q(c["value"])} (at {f(x + 2.54)} {f(y + 1.27)} 0) (effects (font (size 1.27 1.27)) (justify left)))'
pins_here = [num for (u_, num) in POS[libid] if u_ == unit]
pinlines = "\n".join(f' (pin {q(num)} (uuid {q(U("pin", ref, num))}))' for num in sorted(set(pins_here)))
inbom = "no" if ref.startswith(("TP", "JP", "H")) or ref == "J1" else "yes"
syms_out.append(f' (symbol (lib_id {q(libid)}) (at {f(x)} {f(y)} {rot}) (unit {unit}) (exclude_from_sim no) (in_bom {inbom}) (on_board yes) (dnp no) (fields_autoplaced yes) (uuid {q(U("sym", ref, unit))})\n'
+ "\n".join(props) + "\n" + pinlines +
f'\n (instances (project "ocp-oculink-riser" (path {q("/" + ROOT_UUID)} (reference {q(ref)}) (unit {unit}))))\n )')
for num in sorted(set(pins_here)):
wx, wy, ang = pin_world(libid, unit, num, x, y, rot)
net = pin_net.get(f"{ref}.{num}")
# direction away from the body
d = {0: (-1, 0), 180: (1, 0), 90: (0, 1), 270: (0, -1)}[int(ang) % 360]
ex, ey = wx + d[0] * G, wy + d[1] * G
if net is None:
noconn(wx, wy)
continue
wire(wx, wy, ex, ey)
lang = {(-1, 0): 180, (1, 0): 0, (0, 1): 270, (0, -1): 90}[d]
label(net, ex, ey, lang)
# PWR_FLAG on the off-board rails
pw = []
for i, net in enumerate(("GND", "+3V3_EDGE", "+12V_EDGE")):
x, y = g(400 + 20 * i), g(440)
syms_out.append(f' (symbol (lib_id "power:PWR_FLAG") (at {f(x)} {f(y)} 0) (unit 1) (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {q(U("pwr", net))})\n'
f' (property "Reference" {q(f"#FLG0{i+1}")} (at {f(x)} {f(y - 3)} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
f' (property "Value" "PWR_FLAG" (at {f(x)} {f(y - 4.5)} 0) (effects (font (size 1.27 1.27))))\n'
f' (property "Footprint" "" (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
f' (property "Datasheet" "" (at {f(x)} {f(y)} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
f' (pin "1" (uuid {q(U("pwrpin", net))}))\n'
f' (instances (project "ocp-oculink-riser" (path {q("/" + ROOT_UUID)} (reference {q(f"#FLG0{i+1}")}) (unit 1))))\n )')
wire(x, y, x, y + G)
label(net, x, y + G, 270)
# section titles and the design note
notes = [
(g(30), g(20), "OCP NIC 3.0 SFF (Supermicro AIOM) to 4x OCuLink 4i riser - passive, PCIe Gen4 x16 as 4 x4", 3.0),
(g(30), g(30), "Card edge J1: OCP NIC 3.0 v1.6.0 Table 20 (4C+). PRSNTB[3:0]# = 0b0011 -> 4 x4 card (Table 45). Host BIOS: set the AIOM slot to x4x4x4x4.", 1.8),
(g(30), g(36), "Port k = PCIe lanes 4k..4k+3 with REFCLKk and PERSTk# (Table 47). OCuLink pinout: SFF-9402 Table 6-1, root side.", 1.8),
(g(30), g(42), "No AC caps: host TX caps are on the baseboard, device TX caps are on the device. 85 ohm pairs, JLC06161H-3313 stack, see docs.", 1.8),
(g(175), g(70), "Scan chain SR0 (mandatory, OCP 4.5.6)", 2.0),
(g(175), g(170), "FRU EEPROM at 0xA0 + SLOT_ID (OCP 5.10)", 2.0),
(g(285), g(70), "NIC_PWR_GOOD (OCP 4.5.7.6)", 2.0),
(g(285), g(170), "PERST# buffers to the ports", 2.0),
(g(470), g(70), "OCuLink ports (SFF-8611/8621), root side", 2.0),
(g(470), g(215), "Port sideband ESD and termination", 2.0),
(g(30), g(70), "Gold fingers - sideband and power", 2.0),
(g(30), g(250), "Gold fingers - PCIe lanes 0-7", 2.0),
(g(30), g(395), "Gold fingers - PCIe lanes 8-15", 2.0),
]
for (x, y, s, sz) in notes:
items.append(f' (text {q(s)} (exclude_from_sim no) (at {f(x)} {f(y)} 0) (effects (font (size {sz} {sz}) (thickness {f(sz*0.15)}) (bold yes)) (justify left bottom)) (uuid {q(U("t", s))}))')
today = datetime.date(2026, 10, 12).isoformat()
doc = ['(kicad_sch (version 20231120) (generator "riser-gen") (generator_version "1.0")',
f' (uuid {q(ROOT_UUID)})', ' (paper "A1")',
f' (title_block (title "OCP NIC 3.0 SFF to 4x OCuLink riser") (date {q(today)}) (rev "A") (company "Adom") (comment 1 "Generated from design/netlist.json by gen/gen_schematic.py"))',
' (lib_symbols']
for k, v in SYMS.items():
doc.append(" " + v.replace("\n", "\n "))
doc.append(' )')
doc += items
doc += syms_out
doc.append(' (sheet_instances (path "/" (page "1")))')
doc.append(')')
open(OUT, "w").write("\n".join(doc) + "\n")
print("wrote", OUT, len(syms_out), "symbol instances")
# ------------------------------------------------------------------ project symbol library and lib tables
lib = ['(kicad_symbol_lib (version 20231120) (generator "riser-gen") (generator_version "1.0")']
for k, v in SYMS.items():
if k.startswith("Riser:"):
lib.append(" " + v.replace(f'(symbol "{k}"', f'(symbol "{k.split(":")[1]}"', 1).replace("\n", "\n "))
lib.append(")")
os.makedirs(os.path.join(ROOT, "kicad", "lib"), exist_ok=True)
open(os.path.join(ROOT, "kicad", "lib", "Riser.kicad_sym"), "w").write("\n".join(lib) + "\n")
open(os.path.join(ROOT, "kicad", "sym-lib-table"), "w").write(
'(sym_lib_table\n (version 7)\n (lib (name "Riser")(type "KiCad")(uri "${KIPRJMOD}/lib/Riser.kicad_sym")(options "")(descr "OCP OCuLink riser project symbols"))\n)\n')
open(os.path.join(ROOT, "kicad", "fp-lib-table"), "w").write(
'(fp_lib_table\n (version 7)\n (lib (name "Riser")(type "KiCad")(uri "${KIPRJMOD}/lib/Riser.pretty")(options "")(descr "OCP OCuLink riser project footprints"))\n)\n')
print("wrote Riser.kicad_sym and lib tables")