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