Files
baby-mobile/generate_panel.py
T

544 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Baby Mobile — Combined Panel v4
PANEL LAYOUT:
┌───────────────────────────────────────────────┐
│ CARRIER BOARD (44×26mm) │
│ ┌───────USB-C──┐ │
│ │ █████████████ │ │
│ │ │ [U2 IS25LP128F] [C3][R2] │
│ │ XIAO BLE │ ●1 │
│ │ nRF52840 │ [R1][C5] │
│ │ │ │
│ │ D0 D10│ [U1 TPS61220] │
│ │ D1 D9│ ●1 [L1][C1][C2] │
│ │ D2 D8│ │
│ │ D3 D7│ ┌────────────────┐ │
│ │ D4 3V3│ │ J1 FPC-20 0.5mm│ │
│ │ D5 GND│ └────────────────┘ │
│ │ D6 5V│ │
│ └───────────────┘ (M2) (M2) │
├ ─ ─ ─ ─ ─ SNAP ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
│ ┌──────┐ ┃ ┌──────────┐ ┃ ┌──────────┐ │
│ │SOP-8 │ ┃ │SOP-16 N │ ┃ │SOP-16 W │ │
│ C│ │C┃C│ │C┃C│ │C │
│ A│[FPC] │A┃A│ [FPC] │A┃A│ [FPC] │A │
│ S│ │S┃S│ │S┃S│ │S │
│ T│ │T┃T│ │T┃T│ │T │
│ └──────┘ ┃ └──────────┘ ┃ └──────────┘ │
└───────────┸──────────────┸─────────────────────┘
C=castellated edges ┃=v-score snap lines
Each SOP adapter:
- Long edges (top/bottom): castellated half-holes = SOP pads
- Short edges: v-score snap lines
- Center top: FPC 0.5mm connector (20-pin for SOP-16, 10-pin for SOP-8)
- All SOP pins netted to corresponding FPC pins
"""
import uuid, os, json
def uid():
return str(uuid.uuid4())
# ============================================================
# KiCad drawing primitives
# ============================================================
def gr_line(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_rect(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 gr_text(s, x, y, sz=0.8, layer="F.SilkS", mirror=False):
th = max(0.08, sz * 0.13)
mir = " (justify mirror)" if mirror else ""
return f' (gr_text "{s}" (at {x:.3f} {y:.3f}) (layer "{layer}") (uuid "{uid()}") (effects (font (size {sz} {sz}) (thickness {th:.3f})){mir}))'
def gr_circle(x, y, r=0.3, layer="F.SilkS", fill="solid"):
return f' (gr_circle (center {x:.3f} {y:.3f}) (end {x+r:.3f} {y:.3f}) (layer "{layer}") (width 0.1) (fill {fill}) (uuid "{uid()}"))'
def fp_hdr(name, x, y, layer="F.Cu"):
return f"""
(footprint "panel:{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 pad_smd(name, x, y, w, h, net, nname, layer="F.Cu"):
layers = 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 {layers}) (net {net} "{nname}") (uuid "{uid()}"))'
def pad_thru(name, x, y, pd, dd, net, nname):
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size {pd} {pd}) (drill {dd}) (layers "*.Cu" "*.Mask") (net {net} "{nname}") (uuid "{uid()}"))'
def pad_cast(name, x, y, net, nname):
"""Castellated half-hole on board edge."""
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size 1.0 1.0) (drill 0.6) (layers "*.Cu" "*.Mask") (net {net} "{nname}") (uuid "{uid()}"))'
# ============================================================
# XIAO nRF52840 Dimensions (from Seeed docs)
# ============================================================
# PCB: 21.0mm x 17.5mm
# USB-C: centered on one 17.5mm short edge, ~9mm wide, protrudes ~2.5mm
# Pin headers: 2x7, 2.54mm pitch, rows 17.78mm apart (center-center)
# Pin 1 (D0) at top-left when USB faces left
# Reset button on USB side, LED on opposite side
XIAO_W = 21.0
XIAO_H = 17.5
XIAO_ROW_SPAN = 17.78 # center to center of pin rows
XIAO_USB_W = 8.9
XIAO_USB_H = 3.2
XIAO_USB_PROTRUDE = 1.5 # how far USB sticks past PCB edge
# ============================================================
# Main generator
# ============================================================
def generate():
# Panel dimensions
CARRIER_W = 44.0
CARRIER_H = 26.0
ADAPTER_STRIP_H = 14.0
PANEL_W = 48.0
PANEL_H = CARRIER_H + ADAPTER_STRIP_H + 1.0 # 1mm v-score gap
ox, oy = 100.0, 100.0 # KiCad origin
pcb = []
# ---- File header ----
pcb.append(f"""(kicad_pcb (version 20221018) (generator "panel_v4")
(general (thickness 1.6))
(paper "A4")
(title_block
(title "Baby Mobile Combined Panel v4")
(date "2026-03-11")
(rev "4.0")
(comment 1 "Carrier: XIAO nRF52840 + IS25LP128F + TPS61220 + FPC-20")
(comment 2 "Adapters: SOP-8, SOP-16N, SOP-16W — castellated, FPC connected")
(comment 3 "Order: JLCPCB, 2-layer, castellated holes=yes, v-score=yes")
)
(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/"))
)
(net 0 "")
(net 1 "GND") (net 2 "VBAT") (net 3 "3V3")
(net 4 "SPI_MOSI") (net 5 "SPI_MISO") (net 6 "SPI_SCK") (net 7 "FLASH_CS")
(net 8 "BOOST_SW") (net 9 "AUDIO_FILT")
""")
# 16 signal nets for SOP pins / FPC
for i in range(1, 17):
pcb.append(f' (net {i+9} "S{i}")')
# Total nets: 0-25
# ================================================================
# PANEL OUTLINE
# ================================================================
# Outer rectangle
pcb.append(gr_line(ox, oy, ox+PANEL_W, oy))
pcb.append(gr_line(ox+PANEL_W, oy, ox+PANEL_W, oy+PANEL_H))
pcb.append(gr_line(ox+PANEL_W, oy+PANEL_H, ox, oy+PANEL_H))
pcb.append(gr_line(ox, oy+PANEL_H, ox, oy))
# V-score: carrier / adapter strip
vs_y = oy + CARRIER_H + 0.5
pcb.append(gr_line(ox, vs_y, ox+PANEL_W, vs_y))
pcb.append(gr_text("▼ SNAP ▼", ox + PANEL_W/2, vs_y - 0.3, 0.5))
# V-score: between adapters
adapt_base = vs_y + 0.5
# SOP-8: 10mm wide, SOP-16N: 16mm wide, SOP-16W: 16mm wide
a8_w = 12.0
a16n_w = 16.0
a16w_w = 16.0
gap = 0.0 # v-score is zero-width
vs_x1 = ox + a8_w + 1.0
vs_x2 = vs_x1 + a16n_w
pcb.append(gr_line(vs_x1, adapt_base, vs_x1, oy + PANEL_H))
pcb.append(gr_line(vs_x2, adapt_base, vs_x2, oy + PANEL_H))
# ================================================================
# CARRIER: XIAO SOCKET with accurate outline
# ================================================================
# Place XIAO with USB-C facing left edge of carrier
xiao_cx = ox + 3 + XIAO_W/2 # center of XIAO
xiao_cy = oy + CARRIER_H/2
# Board outline rectangle
xb_l = xiao_cx - XIAO_W/2 # left edge of XIAO PCB
xb_r = xiao_cx + XIAO_W/2 # right edge
xb_t = xiao_cy - XIAO_H/2 # top
xb_b = xiao_cy + XIAO_H/2 # bottom
pcb.append(gr_rect(xb_l, xb_t, xb_r, xb_b, "F.SilkS", 0.2))
# USB-C rectangle protruding from left edge
usb_l = xb_l - XIAO_USB_PROTRUDE
usb_t = xiao_cy - XIAO_USB_W/2
usb_b = xiao_cy + XIAO_USB_W/2
pcb.append(gr_rect(usb_l, usb_t, xb_l, usb_b, "F.SilkS", 0.2))
# Fill USB area
pcb.append(gr_rect(usb_l + 0.3, usb_t + 0.3, xb_l - 0.1, usb_b - 0.3, "F.SilkS", 0.15))
pcb.append(gr_text("USB-C", usb_l + 2, xiao_cy, 0.5))
pcb.append(gr_text("XIAO nRF52840", xiao_cx + 1, xiao_cy, 0.8))
# Reset button indicator (top-left corner of XIAO near USB)
pcb.append(gr_text("RST", xb_l + 2, xb_t + 1.5, 0.4))
pcb.append(gr_circle(xb_l + 1, xb_t + 1.5, 0.5, "F.SilkS", "none"))
# Pin labels
left_labels = ["D0","D1","D2","D3","D4","D5","D6"]
right_labels = ["D10","D9","D8","D7","3V3","GND","5V"]
# Left row nets: SIG_1..7 → S1..S7 (from toy via FPC)
left_nets = [(10,"S1"),(11,"S2"),(12,"S3"),(13,"S4"),(14,"S5"),(15,"S6"),(16,"S7")]
# Right row: D10→S8(audio), D9→SCK, D8→MOSI, D7→MISO, 3V3, GND, 5V(NC)
right_nets = [(17,"S8"),(6,"SPI_SCK"),(4,"SPI_MOSI"),(5,"SPI_MISO"),(3,"3V3"),(1,"GND"),(0,"")]
# Left header
pcb.append(fp_hdr("J2_XIAO_L", xiao_cx - XIAO_ROW_SPAN/2, xiao_cy))
for i in range(7):
y = -7.62 + i * 2.54
net_id, net_name = left_nets[i]
pcb.append(pad_thru(left_labels[i], 0, y, 1.7, 1.0, net_id, net_name))
pcb.append(" )")
for i, lbl in enumerate(left_labels):
pcb.append(gr_text(lbl, xiao_cx - XIAO_ROW_SPAN/2 - 2.5, xiao_cy - 7.62 + i*2.54, 0.45))
# Right header
pcb.append(fp_hdr("J3_XIAO_R", xiao_cx + XIAO_ROW_SPAN/2, xiao_cy))
for i in range(7):
y = -7.62 + i * 2.54
net_id, net_name = right_nets[i]
if net_id > 0:
pcb.append(pad_thru(right_labels[i], 0, y, 1.7, 1.0, net_id, net_name))
else:
pcb.append(f' (pad "{right_labels[i]}" thru_hole circle (at 0 {y:.2f}) (size 1.7 1.7) (drill 1.0) (layers "*.Cu" "*.Mask") (uuid "{uid()}"))')
pcb.append(" )")
for i, lbl in enumerate(right_labels):
pcb.append(gr_text(lbl, xiao_cx + XIAO_ROW_SPAN/2 + 2.8, xiao_cy - 7.62 + i*2.54, 0.45))
# ================================================================
# CARRIER: IS25LP128F (SOIC-8)
# ================================================================
fl_cx = ox + 32
fl_cy = oy + 5.5
soic_span = 5.4
# SOIC-8 body outline on silk
pcb.append(gr_rect(fl_cx - 2.5, fl_cy - soic_span/2 - 0.5,
fl_cx + 2.5, fl_cy + soic_span/2 + 0.5))
# Pin 1 dot
pcb.append(gr_circle(fl_cx - 1.5, fl_cy + soic_span/2 - 0.5, 0.25))
# Notch on pin 1 end
pcb.append(gr_circle(fl_cx, fl_cy + soic_span/2 + 0.5, 0.4, "F.SilkS", "none"))
pcb.append(gr_text("U2", fl_cx, fl_cy - soic_span/2 - 1.5, 0.5))
pcb.append(gr_text("IS25LP128F", fl_cx, fl_cy + soic_span/2 + 2, 0.4))
# Pin numbers on silk
for i, num in enumerate([1,2,3,4]):
pcb.append(gr_text(str(num), fl_cx - 1.905 + i*1.27, fl_cy + soic_span/2 + 1.2, 0.3))
for i, num in enumerate([8,7,6,5]):
pcb.append(gr_text(str(num), fl_cx - 1.905 + i*1.27, fl_cy - soic_span/2 - 1.2, 0.3))
flash_pins = [
(1, -1.905, soic_span/2, 7, "FLASH_CS"),
(2, -0.635, soic_span/2, 5, "SPI_MISO"),
(3, 0.635, soic_span/2, 3, "3V3"), # ~WP tie high
(4, 1.905, soic_span/2, 1, "GND"),
(5, 1.905, -soic_span/2, 4, "SPI_MOSI"),
(6, 0.635, -soic_span/2, 6, "SPI_SCK"),
(7, -0.635, -soic_span/2, 3, "3V3"), # ~HOLD tie high
(8, -1.905, -soic_span/2, 3, "3V3"), # VCC
]
pcb.append(fp_hdr("U2_Flash", fl_cx, fl_cy))
for pin, x, y, net, nn in flash_pins:
pcb.append(pad_smd(str(pin), x, y, 0.6, 1.5, net, nn))
pcb.append(" )")
# Flash decoupling C3
pcb.append(fp_hdr("C3", fl_cx + 5.5, fl_cy - 1))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 3, "3V3"))
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
pcb.append(" )")
pcb.append(gr_text("C3", fl_cx + 5.5, fl_cy - 2.3, 0.4))
pcb.append(gr_text("100n", fl_cx + 5.5, fl_cy + 0.3, 0.35))
# Flash CS pullup R2
pcb.append(fp_hdr("R2", fl_cx + 5.5, fl_cy + 2))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 7, "FLASH_CS"))
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 3, "3V3"))
pcb.append(" )")
pcb.append(gr_text("R2", fl_cx + 5.5, fl_cy + 0.8, 0.4))
pcb.append(gr_text("10K", fl_cx + 5.5, fl_cy + 3.3, 0.35))
# ================================================================
# CARRIER: Audio RC filter (R1 + C5)
# ================================================================
rc_x = ox + 32
rc_y = oy + 14
pcb.append(fp_hdr("R1", rc_x, rc_y))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 17, "S8")) # from XIAO D10
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 9, "AUDIO_FILT"))
pcb.append(" )")
pcb.append(gr_text("R1 4.7K", rc_x, rc_y - 1.3, 0.4))
pcb.append(fp_hdr("C5", rc_x, rc_y + 3))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 9, "AUDIO_FILT"))
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
pcb.append(" )")
pcb.append(gr_text("C5 10n", rc_x, rc_y + 1.7, 0.4))
# ================================================================
# CARRIER: TPS61220 Boost Converter
# ================================================================
bx = ox + 32
by = oy + 21
# SC-70-5 outline
pcb.append(gr_rect(bx - 1.2, by - 1.1, bx + 1.2, by + 1.1))
pcb.append(gr_circle(bx - 0.8, by + 0.6, 0.2)) # pin 1 dot
pcb.append(gr_text("U1", bx, by - 2, 0.5))
pcb.append(gr_text("TPS61220", bx, by + 2, 0.4))
pcb.append(gr_text("●1", bx - 1.5, by + 0.7, 0.35))
pcb.append(fp_hdr("U1_Boost", bx, by))
pcb.append(pad_smd("1", -0.65, 0.7, 0.4, 0.55, 2, "VBAT")) # VIN
pcb.append(pad_smd("2", 0, 0.7, 0.4, 0.55, 1, "GND")) # GND
pcb.append(pad_smd("3", 0.65, 0.7, 0.4, 0.55, 2, "VBAT")) # EN=VIN
pcb.append(pad_smd("4", 0.65, -0.7, 0.4, 0.55, 3, "3V3")) # VOUT
pcb.append(pad_smd("5", -0.65, -0.7, 0.4, 0.55, 8, "BOOST_SW")) # L
pcb.append(" )")
# L1 (0805)
pcb.append(fp_hdr("L1", bx - 4, by))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 8, "BOOST_SW"))
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 2, "VBAT"))
pcb.append(" )")
pcb.append(gr_text("L1 4.7u", bx - 4, by - 1.3, 0.4))
# C1 input (0805)
pcb.append(fp_hdr("C1", bx - 4, by + 3))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 2, "VBAT"))
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
pcb.append(" )")
pcb.append(gr_text("C1 10u", bx - 4, by + 1.7, 0.4))
# C2 output (0805)
pcb.append(fp_hdr("C2", bx + 4, by))
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 3, "3V3"))
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
pcb.append(" )")
pcb.append(gr_text("C2 10u", bx + 4, by - 1.3, 0.4))
# ================================================================
# CARRIER: FPC-20 Connector (0.5mm pitch, bottom-contact)
# ================================================================
fpc_cx = ox + 38
fpc_cy = oy + 14
fpc_pitch = 0.5
n_fpc = 20
# FPC connector outline
fpc_w = (n_fpc - 1) * fpc_pitch + 2.0
pcb.append(gr_rect(fpc_cx - fpc_w/2, fpc_cy - 2, fpc_cx + fpc_w/2, fpc_cy + 2))
pcb.append(gr_text("J1 FPC-20", fpc_cx, fpc_cy - 3, 0.5))
pcb.append(gr_text("0.5mm TO TOY", fpc_cx, fpc_cy + 3, 0.35))
pcb.append(gr_circle(fpc_cx - (n_fpc-1)*fpc_pitch/2 - 0.8, fpc_cy, 0.2)) # pin 1
# FPC pin assignment:
# 1=VBAT 2=GND 3=S1 4=S2 ... 10=S8 11-16=S9-S14(spare) 17-20=spare/GND
fpc_nets = [
(1, 2, "VBAT"), (2, 1, "GND"),
(3, 10, "S1"), (4, 11, "S2"), (5, 12, "S3"), (6, 13, "S4"),
(7, 14, "S5"), (8, 15, "S6"), (9, 16, "S7"), (10, 17, "S8"),
(11, 18, "S9"), (12, 19, "S10"), (13, 20, "S11"), (14, 21, "S12"),
(15, 22, "S13"), (16, 23, "S14"), (17, 24, "S15"), (18, 25, "S16"),
(19, 1, "GND"), (20, 1, "GND"),
]
pcb.append(fp_hdr("J1_FPC20", fpc_cx, fpc_cy))
for pin, net, nn in fpc_nets:
x = -((n_fpc - 1) * fpc_pitch / 2) + (pin - 1) * fpc_pitch
pcb.append(pad_smd(str(pin), x, 0, 0.3, 1.2, net, nn))
# Anchor/lock pads
pcb.append(pad_smd("S1", -(fpc_w/2 - 0.5), 2.5, 1.5, 1.0, 0, ""))
pcb.append(pad_smd("S2", (fpc_w/2 - 0.5), 2.5, 1.5, 1.0, 0, ""))
pcb.append(" )")
# ================================================================
# CARRIER: Mounting holes
# ================================================================
for mx, my in [(ox + 2, oy + CARRIER_H - 2), (ox + CARRIER_W - 3, oy + CARRIER_H - 2)]:
pcb.append(fp_hdr("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(" )")
# Carrier back silk
pcb.append(gr_text("XIAO: D0-D6→SIG1-7 D7=MISO D8=MOSI D9=SCK D10=AUDIO(S8)", ox + CARRIER_W/2, oy + CARRIER_H/2, 0.4, "B.SilkS"))
pcb.append(gr_text("FPC: 1=VBAT 2=GND 3-10=S1-S8 19-20=GND", ox + CARRIER_W/2, oy + CARRIER_H/2 + 2, 0.4, "B.SilkS"))
# ================================================================
# ADAPTERS: Castellated SOP breakouts with FPC connectors
# ================================================================
SOP_PITCH = 1.27
def make_adapter(name, board_cx, board_cy, pins_per_side, row_span, n_fpc_pins):
"""
Adapter board with:
- Castellated half-holes on TOP and BOTTOM long edges (SOP pads)
- FPC connector centered on F.Cu
- Board edges through SOP pad centers
"""
total_pins = pins_per_side * 2
row_len = (pins_per_side - 1) * SOP_PITCH
bw = row_len + 3.0 # board width (along pin rows)
bh = row_span # board height = exactly SOP row spacing
# Board outline — edges at castellation centers
bl = board_cx - bw/2
br = board_cx + bw/2
bt = board_cy - bh/2 # top edge (pins N+1..2N)
bb = board_cy + bh/2 # bottom edge (pins 1..N)
# Don't draw left/right edges if they coincide with panel v-score
# (they'll be part of the panel outline or v-score already)
# Draw top and bottom edges
pcb.append(gr_line(bl, bt, br, bt))
pcb.append(gr_line(br, bt, br, bb))
pcb.append(gr_line(br, bb, bl, bb))
pcb.append(gr_line(bl, bb, bl, bt))
# Label
pcb.append(gr_text(name, board_cx, board_cy, 0.6))
# Castellated pads footprint
pcb.append(fp_hdr(f"{name}_cast", board_cx, board_cy, "B.Cu"))
for i in range(pins_per_side):
pin = i + 1
x = -row_len/2 + i * SOP_PITCH
net_id = pin + 9 # S1=net10, S2=net11, etc.
# Bottom edge: pins 1..N
pcb.append(pad_cast(str(pin), x, bh/2, net_id, f"S{pin}"))
# Top edge: pins 2N..N+1 (reversed, SOP convention)
top_pin = total_pins - i
top_net = top_pin + 9
pcb.append(pad_cast(str(top_pin), x, -bh/2, top_net, f"S{top_pin}"))
pcb.append(" )")
# Pin labels on front silk
for i in range(pins_per_side):
x = board_cx - row_len/2 + i * SOP_PITCH
pcb.append(gr_text(str(i+1), x, bb + 0.7, 0.3))
pcb.append(gr_text(str(total_pins - i), x, bt - 0.7, 0.3))
# Pin 1 dot
pcb.append(gr_circle(board_cx - row_len/2 - 0.5, bb + 0.5, 0.2))
pcb.append(gr_text("●1", board_cx - row_len/2 - 0.5, bb + 1.3, 0.3))
# Pin labels on back silk (mirror, for soldering reference)
pcb.append(gr_text(f"{name} ●1→", board_cx, board_cy, 0.5, "B.SilkS", mirror=True))
# FPC connector centered
fpc_pitch_a = 0.5
pcb.append(fp_hdr(f"{name}_FPC", board_cx, board_cy))
for j in range(n_fpc_pins):
x = -((n_fpc_pins - 1) * fpc_pitch_a / 2) + j * fpc_pitch_a
# Map FPC pins to SOP pin nets
if j < total_pins:
sop_pin = j + 1
net_id = sop_pin + 9
nn = f"S{sop_pin}"
elif j == n_fpc_pins - 2:
net_id = 1; nn = "GND" # second-to-last = GND
elif j == n_fpc_pins - 1:
net_id = 2; nn = "VBAT" # last = VBAT
else:
net_id = 0; nn = ""
pcb.append(pad_smd(f"F{j+1}", x, 0, 0.3, 1.0, net_id, nn))
# Anchor pads
aw = (n_fpc_pins - 1) * fpc_pitch_a / 2 + 1.5
pcb.append(pad_smd("SA", -aw, 1.5, 1.2, 0.8, 0, ""))
pcb.append(pad_smd("SB", aw, 1.5, 1.2, 0.8, 0, ""))
pcb.append(" )")
pcb.append(gr_text("FPC", board_cx, board_cy - 1.8, 0.35))
pcb.append(gr_circle(board_cx - (n_fpc_pins-1)*fpc_pitch_a/2 - 0.6, board_cy, 0.15))
# Place adapters in the strip
strip_cy = vs_y + 0.5 + ADAPTER_STRIP_H / 2
# SOP-8: 4+4 pins, 3.9mm span, 10-pin FPC (8 signals + GND + VBAT)
make_adapter("SOP-8", ox + a8_w/2 + 0.5, strip_cy, 4, 3.9, 10)
# SOP-16 narrow: 8+8 pins, 6.0mm span, 20-pin FPC (16 signals + GND + VBAT + 2 spare)
make_adapter("SOP-16N", vs_x1 + a16n_w/2, strip_cy, 8, 6.0, 20)
# SOP-16 wide: 8+8 pins, 10.3mm span, 20-pin FPC
make_adapter("SOP-16W", vs_x2 + a16w_w/2, strip_cy, 8, 10.3, 20)
# ================================================================
# Panel title
# ================================================================
pcb.append(gr_text("Baby Mobile v4 — JLCPCB Panel", ox + PANEL_W/2, oy - 2, 1.0))
pcb.append(gr_text("Castellated Holes=YES V-Score=YES 2-Layer 1.6mm", ox + PANEL_W/2, oy + PANEL_H + 2, 0.5))
pcb.append(")")
return "\n".join(pcb)
if __name__ == "__main__":
outdir = os.path.dirname(os.path.abspath(__file__))
pcb_text = generate()
pcb_path = os.path.join(outdir, "baby-mobile-panel.kicad_pcb")
with open(pcb_path, "w") as f:
f.write(pcb_text)
print(f"Written: {pcb_path} ({len(pcb_text)} bytes)")
proj = json.dumps({
"meta": {"filename": "baby-mobile-panel.kicad_pro", "version": 1},
"board": {"design_settings": {"rules": {"min_track_width": 0.15}}},
"net_settings": {"classes": [{"name":"Default","clearance":0.15,"track_width":0.25}]},
}, indent=2)
with open(os.path.join(outdir, "baby-mobile-panel.kicad_pro"), "w") as f:
f.write(proj)
print("\nPanel:")
print(" Carrier (44×26mm): XIAO(accurate outline+USB-C), IS25LP128F(pin1 dot+notch), TPS61220(pin1 dot), FPC-20")
print(" SOP-8 adapter: castellation on long edges, 10-pin FPC center")
print(" SOP-16N adapter: castellation on long edges, 20-pin FPC center")
print(" SOP-16W adapter: castellation on long edges, 20-pin FPC center")
print(" V-score between carrier and adapter strip, and between each adapter")
print(" All polarized parts have ●1 markers")