Fix nets, breakaway layout, add schematic

This commit is contained in:
zyphlar
2026-07-04 03:28:12 -07:00
parent 181558e7aa
commit 3e7d12aa76
6 changed files with 1659 additions and 1066 deletions
+720
View File
@@ -0,0 +1,720 @@
#!/usr/bin/env python3
"""
Baby Mobile v5 — Integrated Schematic + PCB Generator
Fixes from v4 feedback:
(a) Generates BOTH .kicad_sch and .kicad_pcb
(b) XIAO USB-C on TOP edge, pin rows 15mm apart,
carrier pads extended to 20mm with castellated edges
(c) Each sub-board has unique net names:
- Carrier: C_S1..C_S8, C_VBAT, C_3V3, SPI_*, FLASH_CS, BOOST_SW, AUDIO_FILT
- SOP-8 adapter: A8_P1..A8_P8, A8_VBAT, A8_GND
- SOP-16N adapter: A16N_P1..A16N_P16, A16N_VBAT, A16N_GND
- SOP-16W adapter: A16W_P1..A16W_P16, A16W_VBAT, A16W_GND
FPC connects carrier nets to adapter nets (separate pins, separate nets)
(d) Key-shaped board: wide carrier head, narrow adapter stem
Adapters stacked vertically in stem, v-scored on narrow (short) edges
Board shape:
┌────────────────────────────┐
│ CARRIER (26×22mm) │
│ XIAO, Flash, Boost, │ wide "head"
│ FPC connector │
├──────┬─────────────┬───────┤
│ ADAPTER │ narrow "stem"
│ STRIP │
│ (12×30mm) │
│ SOP-8 │←v-score
│ SOP-16N │←v-score
│ SOP-16W │
└─────────────┘
"""
import uuid, os, json, sys
LIB = "bm"
def uid():
return str(uuid.uuid4())
# ============================================================
# NET DEFINITIONS — unique per sub-board
# ============================================================
# We'll build a flat net list. Each net gets a unique integer ID.
NETS = {}
_net_counter = [0]
def net(name):
"""Register a net name and return its ID."""
if name not in NETS:
_net_counter[0] += 1
NETS[name] = _net_counter[0]
return NETS[name]
def net_decl():
"""Generate all (net N "name") declarations."""
lines = [' (net 0 "")']
for name, idx in sorted(NETS.items(), key=lambda x: x[1]):
lines.append(f' (net {idx} "{name}")')
return "\n".join(lines)
# Pre-register all nets
# Carrier power
net("GND"); net("C_VBAT"); net("C_3V3"); net("BOOST_SW"); net("AUDIO_FILT")
# Carrier SPI (on-board, XIAO to flash)
net("SPI_MOSI"); net("SPI_MISO"); net("SPI_SCK"); net("FLASH_CS")
# Carrier signal nets (XIAO GPIOs that connect to FPC)
for i in range(1, 9):
net(f"C_S{i}")
# Carrier FPC pins (carrier side of cable)
for i in range(1, 21):
net(f"FPC_C{i}")
# SOP-8 adapter nets
for i in range(1, 9):
net(f"A8_P{i}")
net("A8_VBAT"); net("A8_GND")
for i in range(1, 11):
net(f"FPC_A8_{i}")
# SOP-16N adapter nets
for i in range(1, 17):
net(f"A16N_P{i}")
net("A16N_VBAT"); net("A16N_GND")
for i in range(1, 21):
net(f"FPC_A16N_{i}")
# SOP-16W adapter nets
for i in range(1, 17):
net(f"A16W_P{i}")
net("A16W_VBAT"); net("A16W_GND")
for i in range(1, 21):
net(f"FPC_A16W_{i}")
# ============================================================
# XIAO DIMENSIONS
# ============================================================
XIAO_PCB_W = 17.5 # board width (the short dimension when USB is up)
XIAO_PCB_H = 21.0 # board height (USB to antenna end)
XIAO_ROW_SPAN = 15.0 # center-to-center between pin rows
XIAO_USB_W = 8.9
XIAO_USB_H = 3.2
CARRIER_W = 26.0 # wide enough for 20mm pad span + margin
CARRIER_H = 24.0 # tall enough for XIAO + components below
CARRIER_PAD_SPAN = 20.0 # extended pad span for castellated hand soldering
STEM_W = 14.0 # narrow stem for adapters
SOP_PITCH = 1.27
FPC_PITCH = 0.5
# ============================================================
# KiCad primitives (same as before, abbreviated)
# ============================================================
def gl(x1,y1,x2,y2,layer="Edge.Cuts",w=0.1):
return f' (gr_line (start {x1:.3f} {y1:.3f}) (end {x2:.3f} {y2:.3f}) (layer "{layer}") (width {w}) (uuid "{uid()}"))'
def gr(x1,y1,x2,y2,layer="F.SilkS",w=0.15):
return f' (gr_rect (start {x1:.3f} {y1:.3f}) (end {x2:.3f} {y2:.3f}) (layer "{layer}") (width {w}) (uuid "{uid()}"))'
def gt(s,x,y,sz=0.8,layer="F.SilkS"):
th=max(0.08,sz*0.13)
return f' (gr_text "{s}" (at {x:.3f} {y:.3f}) (layer "{layer}") (uuid "{uid()}") (effects (font (size {sz} {sz}) (thickness {th:.3f}))))'
def gc(x,y,r=0.3,layer="F.SilkS"):
return f' (gr_circle (center {x:.3f} {y:.3f}) (end {x+r:.3f} {y:.3f}) (layer "{layer}") (width 0.1) (fill solid) (uuid "{uid()}"))'
def fp(name,x,y,layer="F.Cu"):
return f"""
(footprint "bm:{name}" (layer "{layer}")
(at {x:.3f} {y:.3f})
(uuid "{uid()}")
(fp_text reference "{name}" (at 0 0) (layer "{layer[0]}.Fab") (uuid "{uid()}")
(effects (font (size 0.5 0.5) (thickness 0.08)) hide))
(fp_text value "" (at 0 0) (layer "{layer[0]}.Fab") (uuid "{uid()}")
(effects (font (size 0.5 0.5) (thickness 0.08)) hide))"""
def ps(name,x,y,w,h,nid,nn,layer="F.Cu"):
ls=f'"{layer}" "{layer[0]}.Paste" "{layer[0]}.Mask"'
return f' (pad "{name}" smd rect (at {x:.3f} {y:.3f}) (size {w:.3f} {h:.3f}) (layers {ls}) (net {nid} "{nn}") (uuid "{uid()}"))'
def pt(name,x,y,pd,dd,nid,nn):
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size {pd} {pd}) (drill {dd}) (layers "*.Cu" "*.Mask") (net {nid} "{nn}") (uuid "{uid()}"))'
def pc(name,x,y,nid,nn):
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size 1.0 1.0) (drill 0.6) (layers "*.Cu" "*.Mask") (net {nid} "{nn}") (uuid "{uid()}"))'
# ============================================================
# PCB GENERATOR
# ============================================================
def generate_pcb():
ox, oy = 100.0, 100.0
# Adapter heights in the stem
a8_h = 8.0 # SOP-8 adapter board height (= row span 3.9mm + FPC tab)
a16n_h = 10.0 # SOP-16 narrow
a16w_h = 14.0 # SOP-16 wide (10.3mm span + FPC tab)
stem_h = a8_h + a16n_h + a16w_h
total_h = CARRIER_H + stem_h
pcb = []
pcb.append(f"""(kicad_pcb (version 20221018) (generator "bm_v5")
(general (thickness 1.6))
(paper "A4")
(title_block
(title "Baby Mobile v5 — Key-Shaped Panel")
(date "2026-03-11") (rev "5.0")
(comment 1 "Carrier head + breakaway adapter stem")
(comment 2 "Unique nets per sub-board, FPC interconnect")
)
(layers
(0 "F.Cu" signal) (31 "B.Cu" signal)
(34 "B.Paste" user) (35 "F.Paste" user)
(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen")
(38 "B.Mask" user "B.Mask") (39 "F.Mask" user "F.Mask")
(44 "Edge.Cuts" user)
(48 "B.Fab" user "B.Fab") (49 "F.Fab" user "F.Fab")
)
(setup (pad_to_mask_clearance 0.05)
(pcbplotparams (layerselection 0x00010fc_ffffffff) (outputdirectory "gerbers/")))
""")
# ---- KEY-SHAPED OUTLINE ----
# Carrier head: ox to ox+CARRIER_W, oy to oy+CARRIER_H
# Stem: centered under carrier, STEM_W wide
stem_l = ox + (CARRIER_W - STEM_W) / 2
stem_r = stem_l + STEM_W
stem_top = oy + CARRIER_H
stem_bot = stem_top + stem_h
# Clockwise outline
pcb.append(gl(ox, oy, ox+CARRIER_W, oy)) # top
pcb.append(gl(ox+CARRIER_W, oy, ox+CARRIER_W, stem_top)) # right carrier
pcb.append(gl(ox+CARRIER_W, stem_top, stem_r, stem_top)) # step right
pcb.append(gl(stem_r, stem_top, stem_r, stem_bot)) # right stem
pcb.append(gl(stem_r, stem_bot, stem_l, stem_bot)) # bottom stem
pcb.append(gl(stem_l, stem_bot, stem_l, stem_top)) # left stem
pcb.append(gl(stem_l, stem_top, ox, stem_top)) # step left
pcb.append(gl(ox, stem_top, ox, oy)) # left carrier
# V-score lines between adapters (horizontal, across stem width)
vs1_y = stem_top + a8_h
vs2_y = vs1_y + a16n_h
pcb.append(gl(stem_l, vs1_y, stem_r, vs1_y))
pcb.append(gl(stem_l, vs2_y, stem_r, vs2_y))
pcb.append(gt("SNAP", (stem_l+stem_r)/2, vs1_y-0.3, 0.4))
pcb.append(gt("SNAP", (stem_l+stem_r)/2, vs2_y-0.3, 0.4))
# ---- XIAO (USB on top edge, centered) ----
xiao_cx = ox + CARRIER_W / 2
xiao_top = oy + 1.5 # small margin from carrier top
xiao_bot = xiao_top + XIAO_PCB_H
# XIAO board outline
pcb.append(gr(xiao_cx - XIAO_PCB_W/2, xiao_top,
xiao_cx + XIAO_PCB_W/2, xiao_bot, "F.SilkS", 0.2))
# USB-C on top
pcb.append(gr(xiao_cx - XIAO_USB_W/2, xiao_top - XIAO_USB_H,
xiao_cx + XIAO_USB_W/2, xiao_top, "F.SilkS", 0.2))
pcb.append(gt("USB-C", xiao_cx, xiao_top - XIAO_USB_H/2, 0.45))
pcb.append(gt("XIAO nRF52840", xiao_cx, (xiao_top+xiao_bot)/2, 0.6))
pcb.append(gt("RST", xiao_cx - XIAO_PCB_W/2 + 2, xiao_top + 1.5, 0.35))
# Pin rows: 7 pins each, 2.54mm pitch, running vertically
# Left row at xiao_cx - 7.5mm, right row at xiao_cx + 7.5mm
# Pin 1 (D0) at top of left row
# Extended pads: each pad extends from 15mm span to carrier edge (20mm or board edge)
left_x = xiao_cx - XIAO_ROW_SPAN / 2
right_x = xiao_cx + XIAO_ROW_SPAN / 2
pin1_y = xiao_top + 3.0 # first pin position (some margin from USB)
left_pins = [
("D0", "C_S1"), ("D1", "C_S2"), ("D2", "C_S3"), ("D3", "C_S4"),
("D4", "C_S5"), ("D5", "C_S6"), ("D6", "C_S7"),
]
right_pins = [
("D10", "C_S8"), # audio PWM
("D9", "SPI_SCK"),
("D8", "SPI_MOSI"),
("D7", "SPI_MISO"),
("3V3", "C_3V3"),
("GND", "GND"),
("5V", ""), # not connected
]
# Left pin row with extended pads
pcb.append(fp("XIAO_L", left_x, pin1_y))
for i, (label, nn) in enumerate(left_pins):
y = i * 2.54
nid = net(nn) if nn else 0
# Through-hole at XIAO position
pcb.append(pt(label, 0, y, 1.7, 1.0, nid, nn))
# Extended SMD pad toward board left edge for castellated soldering
ext_x = -(CARRIER_PAD_SPAN/2 - XIAO_ROW_SPAN/2) # offset toward left edge
pcb.append(ps(f"{label}_ext", ext_x/2, y, abs(ext_x), 1.2, nid, nn))
pcb.append(" )")
# Right pin row with extended pads
pcb.append(fp("XIAO_R", right_x, pin1_y))
for i, (label, nn) in enumerate(right_pins):
y = i * 2.54
nid = net(nn) if nn else 0
if nid > 0:
pcb.append(pt(label, 0, y, 1.7, 1.0, nid, nn))
ext_x = (CARRIER_PAD_SPAN/2 - XIAO_ROW_SPAN/2)
pcb.append(ps(f"{label}_ext", ext_x/2, y, abs(ext_x), 1.2, nid, nn))
else:
pcb.append(f' (pad "{label}" thru_hole circle (at 0 {y:.2f}) (size 1.7 1.7) (drill 1.0) (layers "*.Cu" "*.Mask") (uuid "{uid()}"))')
pcb.append(" )")
# Pin labels
for i, (label, _) in enumerate(left_pins):
pcb.append(gt(label, left_x - 3.5, pin1_y + i*2.54, 0.4))
for i, (label, _) in enumerate(right_pins):
pcb.append(gt(label, right_x + 3.5, pin1_y + i*2.54, 0.4))
# ---- IS25LP128F (SOIC-8, below XIAO on carrier) ----
fl_cx = ox + 5
fl_cy = oy + CARRIER_H - 5
soic_span = 5.4
pcb.append(gr(fl_cx-2.5, fl_cy-soic_span/2-0.5, fl_cx+2.5, fl_cy+soic_span/2+0.5))
pcb.append(gc(fl_cx-1.5, fl_cy+soic_span/2-0.5, 0.25)) # pin 1
pcb.append(gt("U2 IS25LP128F", fl_cx, fl_cy-soic_span/2-1.5, 0.4))
pcb.append(gt("●1", fl_cx-2.2, fl_cy+soic_span/2+1, 0.3))
flash_pins = [
(1,-1.905, soic_span/2, "FLASH_CS"),
(2,-0.635, soic_span/2, "SPI_MISO"),
(3, 0.635, soic_span/2, "C_3V3"),
(4, 1.905, soic_span/2, "GND"),
(5, 1.905,-soic_span/2, "SPI_MOSI"),
(6, 0.635,-soic_span/2, "SPI_SCK"),
(7,-0.635,-soic_span/2, "C_3V3"),
(8,-1.905,-soic_span/2, "C_3V3"),
]
pcb.append(fp("U2", fl_cx, fl_cy))
for pin, x, y, nn in flash_pins:
pcb.append(ps(str(pin), x, y, 0.6, 1.5, net(nn), nn))
pcb.append(" )")
# C3 flash decoupling
pcb.append(fp("C3", fl_cx+5, fl_cy))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_3V3"),"C_3V3"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
pcb.append(" )")
pcb.append(gt("C3 100n", fl_cx+5, fl_cy-1.3, 0.35))
# R2 CS pullup
pcb.append(fp("R2", fl_cx+5, fl_cy+3))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("FLASH_CS"),"FLASH_CS"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("C_3V3"),"C_3V3"))
pcb.append(" )")
pcb.append(gt("R2 10K", fl_cx+5, fl_cy+1.7, 0.35))
# ---- TPS61220 + passives (bottom-right of carrier) ----
bx = ox + CARRIER_W - 6
by = oy + CARRIER_H - 5
pcb.append(gr(bx-1.2, by-1.1, bx+1.2, by+1.1))
pcb.append(gc(bx-0.8, by+0.6, 0.2))
pcb.append(gt("U1 TPS61220", bx, by-2.2, 0.4))
pcb.append(gt("●1", bx-1.5, by+0.7, 0.3))
pcb.append(fp("U1", bx, by))
pcb.append(ps("1",-0.65, 0.7, 0.4,0.55, net("C_VBAT"),"C_VBAT"))
pcb.append(ps("2", 0, 0.7, 0.4,0.55, net("GND"),"GND"))
pcb.append(ps("3", 0.65, 0.7, 0.4,0.55, net("C_VBAT"),"C_VBAT"))
pcb.append(ps("4", 0.65,-0.7, 0.4,0.55, net("C_3V3"),"C_3V3"))
pcb.append(ps("5",-0.65,-0.7, 0.4,0.55, net("BOOST_SW"),"BOOST_SW"))
pcb.append(" )")
pcb.append(fp("L1", bx-4, by))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("BOOST_SW"),"BOOST_SW"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("C_VBAT"),"C_VBAT"))
pcb.append(" )")
pcb.append(gt("L1 4.7u", bx-4, by-1.3, 0.35))
pcb.append(fp("C1", bx-4, by+3))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_VBAT"),"C_VBAT"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
pcb.append(" )")
pcb.append(gt("C1 10u", bx-4, by+1.7, 0.35))
pcb.append(fp("C2", bx+4, by))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_3V3"),"C_3V3"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
pcb.append(" )")
pcb.append(gt("C2 10u", bx+4, by-1.3, 0.35))
# R1 + C5 audio filter
pcb.append(fp("R1", bx, by-6))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_S8"),"C_S8"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("AUDIO_FILT"),"AUDIO_FILT"))
pcb.append(" )")
pcb.append(gt("R1 4.7K", bx, by-7.3, 0.35))
pcb.append(fp("C5", bx+4, by-6))
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("AUDIO_FILT"),"AUDIO_FILT"))
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
pcb.append(" )")
pcb.append(gt("C5 10n", bx+4, by-7.3, 0.35))
# ---- Carrier FPC-20 (at bottom of carrier, connecting to stem) ----
fpc_cx = ox + CARRIER_W / 2
fpc_cy = oy + CARRIER_H - 1.5
n_fpc = 20
pcb.append(gr(fpc_cx - n_fpc*FPC_PITCH/2 - 1, fpc_cy-1.5,
fpc_cx + n_fpc*FPC_PITCH/2 + 1, fpc_cy+1.5))
pcb.append(gt("J1 FPC-20 0.5mm", fpc_cx, fpc_cy-2.5, 0.4))
pcb.append(gc(fpc_cx - (n_fpc-1)*FPC_PITCH/2 - 0.6, fpc_cy, 0.15))
# FPC pin mapping: carrier side uses FPC_C1..FPC_C20
# 1=C_VBAT 2=GND 3-10=C_S1..C_S8 11-18=spare 19-20=GND
fpc_c_map = [
("FPC_C1","C_VBAT"), ("FPC_C2","GND"),
] + [(f"FPC_C{i+3}", f"C_S{i+1}") for i in range(8)] + [
(f"FPC_C{i}", "") for i in range(11,19)
] + [("FPC_C19","GND"), ("FPC_C20","GND")]
pcb.append(fp("J1_FPC", fpc_cx, fpc_cy))
for j in range(n_fpc):
x = -((n_fpc-1)*FPC_PITCH/2) + j*FPC_PITCH
nn = fpc_c_map[j][0]
nid = net(nn)
pcb.append(ps(str(j+1), x, 0, 0.3, 1.2, nid, nn))
pcb.append(ps("SA", -(n_fpc*FPC_PITCH/2+1), 2, 1.5, 1.0, 0, ""))
pcb.append(ps("SB", (n_fpc*FPC_PITCH/2+1), 2, 1.5, 1.0, 0, ""))
pcb.append(" )")
# ---- Mounting holes ----
for mx,my in [(ox+2, oy+2), (ox+CARRIER_W-2, oy+2)]:
pcb.append(fp("MH",mx,my))
pcb.append(f' (pad "" thru_hole circle (at 0 0) (size 3.5 3.5) (drill 2.2) (layers "*.Cu" "*.Mask") (uuid "{uid()}"))')
pcb.append(" )")
# ================================================================
# ADAPTER: SOP-8
# ================================================================
def make_adapter(prefix, center_x, top_y, height, pins_per_side, row_span, n_fpc_pins):
total_pins = pins_per_side * 2
row_len = (pins_per_side - 1) * SOP_PITCH
cy = top_y + height / 2
pcb.append(gt(prefix, center_x, cy, 0.5))
# Castellation pads on long (left/right) edges
# Bottom row: pins 1..N at cy + row_span/2
# Top row: pins 2N..N+1 at cy - row_span/2
pcb.append(fp(f"{prefix}_cast", center_x, cy, "B.Cu"))
for i in range(pins_per_side):
pin = i + 1
x = -row_len/2 + i * SOP_PITCH
nn = f"{prefix}_P{pin}"
pcb.append(pc(str(pin), x, row_span/2, net(nn), nn))
top_pin = total_pins - i
nn2 = f"{prefix}_P{top_pin}"
pcb.append(pc(str(top_pin), x, -row_span/2, net(nn2), nn2))
pcb.append(" )")
# Pin labels
for i in range(pins_per_side):
x = center_x - row_len/2 + i * SOP_PITCH
pcb.append(gt(str(i+1), x, cy + row_span/2 + 0.7, 0.3))
pcb.append(gt(str(total_pins-i), x, cy - row_span/2 - 0.7, 0.3))
pcb.append(gc(center_x - row_len/2 - 0.5, cy + row_span/2 + 0.5, 0.2))
# FPC connector centered
fpc_y = cy # centered vertically
pcb.append(fp(f"{prefix}_FPC", center_x, fpc_y))
for j in range(n_fpc_pins):
x = -((n_fpc_pins-1)*FPC_PITCH/2) + j*FPC_PITCH
if j < total_pins:
sop_pin = j + 1
nn = f"FPC_{prefix}_{j+1}"
elif j == n_fpc_pins - 2:
nn = f"{prefix}_GND"
elif j == n_fpc_pins - 1:
nn = f"{prefix}_VBAT"
else:
nn = f"FPC_{prefix}_{j+1}"
nid = net(nn)
pcb.append(ps(f"F{j+1}", x, 0, 0.3, 1.0, nid, nn))
pcb.append(ps("SA", -(n_fpc_pins*FPC_PITCH/2+1), 1.5, 1.2, 0.8, 0, ""))
pcb.append(ps("SB", (n_fpc_pins*FPC_PITCH/2+1), 1.5, 1.2, 0.8, 0, ""))
pcb.append(" )")
pcb.append(gt("FPC", center_x, fpc_y - 1.5, 0.3))
pcb.append(gc(center_x - (n_fpc_pins-1)*FPC_PITCH/2 - 0.5, fpc_y, 0.15))
stem_cx = (stem_l + stem_r) / 2
make_adapter("A8", stem_cx, stem_top, a8_h, 4, 3.9, 10)
make_adapter("A16N", stem_cx, vs1_y, a16n_h, 8, 6.0, 20)
make_adapter("A16W", stem_cx, vs2_y, a16w_h, 8, 10.3, 20)
# Back silk instructions
pcb.append(gt("Baby Mobile v5 — github.com/you/baby-mobile", ox+CARRIER_W/2, oy+CARRIER_H/2, 0.4, "B.SilkS"))
# ---- Insert net declarations at the top ----
# We need to do this after all nets are registered
net_block = net_decl()
pcb.insert(1, "") # placeholder, we'll join later
pcb.append(")")
# Splice net declarations in after the setup block
result = "\n".join(pcb)
result = result.replace("(setup (pad_to_mask_clearance 0.05)",
f"{net_block}\n\n (setup (pad_to_mask_clearance 0.05)")
return result
# ============================================================
# SCHEMATIC GENERATOR
# ============================================================
def generate_schematic():
"""Generate a matching .kicad_sch with all components and nets."""
sch = []
sch.append(f"""(kicad_sch (version 20230121) (generator "bm_v5_sch")
(uuid "{uid()}")
(paper "A3")
(title_block
(title "Baby Mobile v5 — Schematic")
(date "2026-03-11") (rev "5.0")
(comment 1 "XIAO nRF52840 + IS25LP128F + TPS61220")
(comment 2 "Key-shaped panel with breakaway SOP adapters")
)
""")
# Lib symbols (using the v2 fix: no library prefix on sub-symbols)
sch.append(" (lib_symbols")
# Generic 2-pin (for R, C, L)
for name, ref in [("R","R"), ("C","C"), ("L","L")]:
sch.append(f"""
(symbol "{LIB}:{name}" (in_bom yes) (on_board yes)
(property "Reference" "{ref}" (at 2 0 0) (effects (font (size 1.27 1.27))))
(property "Value" "{name}" (at 2 -2 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "{name}_0_1"
(rectangle (start -1 2.5) (end 1 -2.5) (stroke (width 0.254) (type default)) (fill (type background)))
)
(symbol "{name}_1_1"
(pin passive line (at 0 5 270) (length 2.5) (name "1" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
(pin passive line (at 0 -5 90) (length 2.5) (name "2" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
)
)""")
# TPS61220
sch.append(f"""
(symbol "{LIB}:TPS61220" (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 6 0) (effects (font (size 1.27 1.27))))
(property "Value" "TPS61220" (at 0 -6 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "Package_TO_SOT_SMD:SOT-353_SC-70-5" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "TPS61220_0_1"
(rectangle (start -7 5) (end 7 -5) (stroke (width 0.254) (type default)) (fill (type background)))
)
(symbol "TPS61220_1_1"
(pin power_in line (at -10 3 0) (length 3) (name "VIN" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
(pin power_in line (at -10 -3 0) (length 3) (name "GND" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
(pin input line (at 10 -3 180) (length 3) (name "EN" (effects (font (size 1 1)))) (number "3" (effects (font (size 1 1)))))
(pin power_out line (at 10 3 180) (length 3) (name "VOUT" (effects (font (size 1 1)))) (number "4" (effects (font (size 1 1)))))
(pin passive line (at 0 8 270) (length 3) (name "L" (effects (font (size 1 1)))) (number "5" (effects (font (size 1 1)))))
)
)""")
# IS25LP128F
sch.append(f"""
(symbol "{LIB}:IS25LP128F" (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 9 0) (effects (font (size 1.27 1.27))))
(property "Value" "IS25LP128F" (at 0 7 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "IS25LP128F_0_1"
(rectangle (start -7 6) (end 7 -6) (stroke (width 0.254) (type default)) (fill (type background)))
)
(symbol "IS25LP128F_1_1"
(pin input line (at -10 4 0) (length 3) (name "~{{CS}}" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
(pin output line (at -10 1.5 0) (length 3) (name "SO" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
(pin input line (at -10 -1.5 0) (length 3) (name "~{{WP}}" (effects (font (size 1 1)))) (number "3" (effects (font (size 1 1)))))
(pin power_in line (at -10 -4 0) (length 3) (name "GND" (effects (font (size 1 1)))) (number "4" (effects (font (size 1 1)))))
(pin input line (at 10 -4 180) (length 3) (name "SI" (effects (font (size 1 1)))) (number "5" (effects (font (size 1 1)))))
(pin input line (at 10 -1.5 180) (length 3) (name "SCLK" (effects (font (size 1 1)))) (number "6" (effects (font (size 1 1)))))
(pin input line (at 10 1.5 180) (length 3) (name "~{{HOLD}}" (effects (font (size 1 1)))) (number "7" (effects (font (size 1 1)))))
(pin power_in line (at 10 4 180) (length 3) (name "VCC" (effects (font (size 1 1)))) (number "8" (effects (font (size 1 1)))))
)
)""")
# FPC connector (generic N-pin)
for npins in [10, 20]:
sch.append(f"""
(symbol "{LIB}:FPC_{npins}" (in_bom yes) (on_board yes)
(property "Reference" "J" (at 0 {npins+2} 0) (effects (font (size 1.27 1.27))))
(property "Value" "FPC-{npins}" (at 0 {-npins-2} 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "FPC_{npins}_0_1"
(rectangle (start -3 {npins}) (end 3 {-npins}) (stroke (width 0.254) (type default)) (fill (type background)))
)
(symbol "FPC_{npins}_1_1"
""")
for i in range(npins):
y = npins - 1 - i * 2
sch.append(f' (pin passive line (at -6 {y} 0) (length 3) (name "P{i+1}" (effects (font (size 1 1)))) (number "{i+1}" (effects (font (size 1 1)))))')
sch.append(" )\n )")
# XIAO module symbol
sch.append(f"""
(symbol "{LIB}:XIAO_nRF52840" (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 22 0) (effects (font (size 1.27 1.27))))
(property "Value" "XIAO_nRF52840" (at 0 20 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
(symbol "XIAO_nRF52840_0_1"
(rectangle (start -12 19) (end 12 -19) (stroke (width 0.254) (type default)) (fill (type background)))
)
(symbol "XIAO_nRF52840_1_1"
(pin bidirectional line (at -15 15 0) (length 3) (name "D0" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
(pin bidirectional line (at -15 12.5 0) (length 3) (name "D1" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
(pin bidirectional line (at -15 10 0) (length 3) (name "D2" (effects (font (size 1 1)))) (number "3" (effects (font (size 1 1)))))
(pin bidirectional line (at -15 7.5 0) (length 3) (name "D3" (effects (font (size 1 1)))) (number "4" (effects (font (size 1 1)))))
(pin bidirectional line (at -15 5 0) (length 3) (name "D4" (effects (font (size 1 1)))) (number "5" (effects (font (size 1 1)))))
(pin bidirectional line (at -15 2.5 0) (length 3) (name "D5" (effects (font (size 1 1)))) (number "6" (effects (font (size 1 1)))))
(pin bidirectional line (at -15 0 0) (length 3) (name "D6" (effects (font (size 1 1)))) (number "7" (effects (font (size 1 1)))))
(pin bidirectional line (at 15 15 180) (length 3) (name "D10" (effects (font (size 1 1)))) (number "8" (effects (font (size 1 1)))))
(pin bidirectional line (at 15 12.5 180) (length 3) (name "D9/SCK" (effects (font (size 1 1)))) (number "9" (effects (font (size 1 1)))))
(pin bidirectional line (at 15 10 180) (length 3) (name "D8/MOSI" (effects (font (size 1 1)))) (number "10" (effects (font (size 1 1)))))
(pin bidirectional line (at 15 7.5 180) (length 3) (name "D7/MISO" (effects (font (size 1 1)))) (number "11" (effects (font (size 1 1)))))
(pin power_in line (at 15 2.5 180) (length 3) (name "3V3" (effects (font (size 1 1)))) (number "12" (effects (font (size 1 1)))))
(pin power_in line (at 15 0 180) (length 3) (name "GND" (effects (font (size 1 1)))) (number "13" (effects (font (size 1 1)))))
(pin power_in line (at 15 -2.5 180) (length 3) (name "5V" (effects (font (size 1 1)))) (number "14" (effects (font (size 1 1)))))
)
)""")
sch.append(" )") # end lib_symbols
# Component instances (placed with net labels for connectivity)
def inst(lib_id, ref, value, footprint, x, y, angle=0):
return f""" (symbol (lib_id "{LIB}:{lib_id}") (at {x} {y} {angle}) (unit 1)
(in_bom yes) (on_board yes) (dnp no)
(uuid "{uid()}")
(property "Reference" "{ref}" (at {x+3} {y-3} 0) (effects (font (size 1.27 1.27))))
(property "Value" "{value}" (at {x+3} {y+3} 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "{footprint}" (at {x} {y} 0) (effects (font (size 1.27 1.27)) hide))
(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) hide))
)"""
def label(name, x, y):
return f' (label "{name}" (at {x} {y} 0) (effects (font (size 1.27 1.27))) (uuid "{uid()}"))'
# Place components
sch.append(inst("XIAO_nRF52840", "U3", "XIAO_nRF52840", "", 80, 80))
sch.append(inst("IS25LP128F", "U2", "IS25LP128F", "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", 180, 60))
sch.append(inst("TPS61220", "U1", "TPS61220", "Package_TO_SOT_SMD:SOT-353_SC-70-5", 180, 130))
sch.append(inst("FPC_20", "J1", "FPC-20 Carrier", "", 80, 160))
sch.append(inst("R", "R1", "4.7K", "Resistor_SMD:R_0805_2012Metric", 140, 80))
sch.append(inst("C", "C5", "10nF", "Capacitor_SMD:C_0805_2012Metric", 150, 90))
sch.append(inst("R", "R2", "10K", "Resistor_SMD:R_0805_2012Metric", 210, 50))
sch.append(inst("C", "C3", "100nF", "Capacitor_SMD:C_0805_2012Metric", 210, 70))
sch.append(inst("L", "L1", "4.7uH", "Inductor_SMD:L_0805_2012Metric", 180, 115))
sch.append(inst("C", "C1", "10uF", "Capacitor_SMD:C_0805_2012Metric", 160, 140))
sch.append(inst("C", "C2", "10uF", "Capacitor_SMD:C_0805_2012Metric", 200, 140))
# Adapter schematics (separate sections for clarity)
sch.append(inst("FPC_10", "J2", "FPC-10 SOP-8", "", 280, 80))
sch.append(inst("FPC_20", "J3", "FPC-20 SOP-16N", "", 280, 140))
sch.append(inst("FPC_20", "J4", "FPC-20 SOP-16W", "", 280, 200))
# Net labels (connecting everything logically)
# XIAO to SPI flash
labels = [
("SPI_SCK", 95, 67.5), ("SPI_SCK", 190, 58.5),
("SPI_MOSI", 95, 70), ("SPI_MOSI", 190, 56),
("SPI_MISO", 95, 72.5), ("SPI_MISO", 170, 61.5),
("FLASH_CS", 170, 64), ("FLASH_CS", 210, 45),
("C_3V3", 95, 77.5), ("C_3V3", 190, 64), ("C_3V3", 190, 133),
("GND", 95, 80), ("GND", 170, 56), ("GND", 170, 127),
# XIAO signals to carrier FPC
("C_S1", 65, 65), ("C_S2", 65, 67.5), ("C_S3", 65, 70),
("C_S4", 65, 72.5), ("C_S5", 65, 75), ("C_S6", 65, 77.5),
("C_S7", 65, 80), ("C_S8", 95, 65),
("C_S8", 130, 80), # to R1
("AUDIO_FILT", 150, 80),
# Boost
("C_VBAT", 170, 133), ("C_VBAT", 180, 110),
("BOOST_SW", 180, 122), ("BOOST_SW", 180, 120),
]
for name, x, y in labels:
sch.append(label(name, x, y))
# Text notes
notes = [
(40, 30, "CARRIER: XIAO nRF52840 + IS25LP128F (16MB) + TPS61220 (3.3V boost)"),
(40, 35, "FPC-20 cable connects carrier to one of the SOP adapter boards"),
(250, 30, "ADAPTERS: Snap-off castellated SOP footprints"),
(250, 35, "Each has its own FPC connector + unique net names"),
]
for x, y, text in notes:
sch.append(f' (text "{text}" (at {x} {y} 0) (effects (font (size 1.5 1.5)) (justify left)) (uuid "{uid()}"))')
sch.append(")")
return "\n".join(sch)
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
outdir = os.path.dirname(os.path.abspath(__file__))
# Generate PCB
pcb_text = generate_pcb()
pcb_path = os.path.join(outdir, "baby-mobile-v5.kicad_pcb")
with open(pcb_path, "w") as f:
f.write(pcb_text)
print(f"PCB: {pcb_path} ({len(pcb_text)} bytes)")
# Generate Schematic
sch_text = generate_schematic()
sch_path = os.path.join(outdir, "baby-mobile-v5.kicad_sch")
with open(sch_path, "w") as f:
f.write(sch_text)
print(f"SCH: {sch_path} ({len(sch_text)} bytes)")
# Generate Project
proj = json.dumps({
"meta": {"filename": "baby-mobile-v5.kicad_pro", "version": 1},
"board": {"design_settings": {"rules": {"min_track_width": 0.15, "min_via_diameter": 0.4}}},
"net_settings": {"classes": [
{"name": "Default", "clearance": 0.15, "track_width": 0.25},
{"name": "Power", "clearance": 0.2, "track_width": 0.5},
]},
}, indent=2)
with open(os.path.join(outdir, "baby-mobile-v5.kicad_pro"), "w") as f:
f.write(proj)
print(f"\n{len(NETS)} unique nets registered:")
print(f" Carrier: C_S1-8, C_VBAT, C_3V3, SPI_*, FLASH_CS, BOOST_SW, AUDIO_FILT")
print(f" SOP-8: A8_P1-8, A8_VBAT, A8_GND, FPC_A8_1-10")
print(f" SOP-16N: A16N_P1-16, A16N_VBAT, A16N_GND, FPC_A16N_1-20")
print(f" SOP-16W: A16W_P1-16, A16W_VBAT, A16W_GND, FPC_A16W_1-20")
print(f" No shared nets between carrier and adapters (FPC bridges them)")
print(f"\nKey-shaped board:")
print(f" Head (carrier): {CARRIER_W}×{CARRIER_H}mm")
print(f" Stem (adapters): {STEM_W}mm wide, stacked SOP-8 + SOP-16N + SOP-16W")
print(f" XIAO: USB-C on top, rows {XIAO_ROW_SPAN}mm apart, pads extended to {CARRIER_PAD_SPAN}mm")