#!/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")
