#!/usr/bin/env python3 """Generate the initial native KiCad carrier schematic (Python stdlib only). Explicit bootstrap/regeneration tool: overwrites the generated schematic, symbol library and library tables. Do not run over subsequent manual KiCad edits without reviewing/backing them up. It does not regenerate footprints or the fit template. """ import json from pathlib import Path import uuid ROOT = Path(__file__).resolve().parent PROJECT = "serial-carrier" NS = uuid.UUID("b6f75dd1-a843-42f2-9067-57c995968145") def uid(key): return str(uuid.uuid5(NS, key)) def q(value): return json.dumps(str(value), ensure_ascii=False) def n(value): return f"{value:.4f}".rstrip("0").rstrip(".") if value else "0" def effects(size=1.27, extra=""): return f"(effects (font (size {size} {size})) {extra})" def prop(name, value, x=0, y=0, hidden=False): return f'(property {q(name)} {q(value)} (at {n(x)} {n(y)} 0) {effects(extra="(hide yes)" if hidden else "")})' # Physical module numbering is a project convention, not ESP32 GPIO numbering: # component side, antenna up / USB down; left row 1..22, right row 23..44. left = ["3V3", "3V3", "EN", "GPIO4", "GPIO5", "GPIO6", "GPIO7", "GPIO15", "GPIO16", "GPIO17", "GPIO18", "GPIO8", "GPIO3", "GPIO46", "GPIO9", "GPIO10", "GPIO11", "GPIO12", "GPIO13", "GPIO14", "5V", "GND"] right = ["GND", "GPIO43", "GPIO44", "GPIO1", "GPIO2", "GPIO42", "GPIO41", "GPIO40", "GPIO39", "GPIO38", "GPIO37", "GPIO36", "GPIO35", "GPIO0", "GPIO45", "GPIO48", "GPIO47", "GPIO21", "GPIO20", "GPIO19", "GND", "GND"] GPIO_NET = {4:"RS_DCD",5:"RS_DSR",6:"RS_RI",7:"RS_DTR",8:"RS_VALID",9:"RS_OFF_N",10:"BTN_PREVIOUS",11:"OLED_SDA",12:"OLED_SCL",13:"BTN_SELECT",14:"BTN_NEXT",15:"RS_RTS",16:"RS_CTS",17:"RS_TX",18:"RS_RX"} OUTPUTS = {7, 12, 15, 17} INPUTS = {4, 5, 6, 8, 10, 13, 14, 16, 18} def gpio_type(name): if not name.startswith("GPIO"): return "input" if name == "EN" else "passive" number = int(name[4:]) if number == 9: # Model the programmable MCU pad, not a discrete open-collector device. # The required firmware open-drain mode and module pull-up are noted on sheet. return "bidirectional" return "output" if number in OUTPUTS else "input" if number in INPUTS else "bidirectional" # Pin tuples: physical number, name, electrical type, local X, local Y, angle. MCU = [] for side, names in enumerate((left, right)): for index, name in enumerate(names): number = index + 1 + side * 22 kind = "power_out" if number in (1, 22) else gpio_type(name) MCU.append((str(number), name, kind, -22.86 if side == 0 else 22.86, 26.67-index*2.54, 0 if side == 0 else 180)) # Published Adafruit 5988 CAD reference; male 6253 correspondence is provisional. RS_NAMES = ["Vin", "GND", "DCD", "RX", "TX", "DTR", "DSR", "RTS", "CTS", "RI", "VLD", "OFF_N"] RS = [(str(i+1), name, "power_in" if i < 2 else "input" if name in ("TX","DTR","RTS","OFF_N") else "output", -20.32, 13.97-i*2.54, 0) for i,name in enumerate(RS_NAMES)] OLED = [(str(i+1),name,kind,-15.24,3.81-i*2.54,0) for i,(name,kind) in enumerate((("GND","power_in"),("VCC","power_in"),("SCL","input"),("SDA","bidirectional")))] TWO = [("1","~","passive",-5.08,0,0),("2","~","passive",5.08,0,180)] SPECS = { "HW678_N16R8": (MCU, 17.78, 29.21, "U", "Carrier:HW678_2x22_Provisional", "HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained"), "MAX3243_Module_Reference": (RS, 15.24, 17.78, "U", "", "Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE"), "OLED_I2C_26mm": (OLED, 10.16, 7.62, "DS", "Carrier:OLED_26mm_I2C_Provisional", "User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA"), "SW_NO": (TWO, 2.54, 2.54, "SW", "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", "Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"), "R": (TWO, 2.54, 1.016, "R", "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", "Resistor, axial DIN0207 footprint; exact MPN pending") } def symbol_definition(name, embedded=False): pins, halfwidth, halfheight, ref, footprint, description = SPECS[name] out = [f'(symbol {q("Carrier:"+name if embedded else name)} (pin_names (offset 0.762)) (in_bom yes) (on_board yes)', prop("Reference", ref, 0, halfheight+5.08), prop("Value", name, 0, halfheight+2.54), prop("Footprint", footprint, hidden=True), prop("Datasheet", "", hidden=True), prop("Description",description,hidden=True)] if name == "SW_NO": shape = '(polyline (pts (xy -2.54 0) (xy 2.54 1.524)) (stroke (width 0.254) (type default)) (fill (type none)))' shape += ''.join(f'(circle (center {x} 0) (radius 0.35) (stroke (width 0.1524) (type default)) (fill (type none)))' for x in (-2.54,2.54)) else: shape = f'(rectangle (start {-halfwidth} {halfheight}) (end {halfwidth} {-halfheight}) (stroke (width 0.254) (type default)) (fill (type background)))' out.append(f'(symbol {q(name+"_0_1")} {shape})') out.append(f'(symbol {q(name+"_1_1")}') for number,label,kind,x,y,angle in pins: length = abs(x)-halfwidth out.append(f'(pin {kind} line (at {n(x)} {n(y)} {angle}) (length {n(length)}) (name {q(label)} {effects(1.016)}) (number {q(number)} {effects(1.016)}))') out.extend([')', ')']) return '\n'.join(out) ROOT.joinpath("Carrier.kicad_sym").write_text('(kicad_symbol_lib (version 20231120) (generator "kicad_symbol_editor")\n'+'\n'.join(symbol_definition(name) for name in SPECS)+'\n)\n') ROOT.joinpath("sym-lib-table").write_text('(sym_lib_table (version 7)\n (lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.kicad_sym")(options "")(descr "Project-local module interfaces"))\n)\n') ROOT.joinpath("fp-lib-table").write_text('(fp_lib_table (version 7)\n (lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.pretty")(options "")(descr "Provisional module carrier footprints; see footprint-notes.md"))\n (lib (name "Button_Switch_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Button_Switch_THT.pretty")(options "")(descr "KiCad standard THT switches"))\n (lib (name "Resistor_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Resistor_THT.pretty")(options "")(descr "KiCad standard axial resistors"))\n (lib (name "MountingHole")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr "KiCad standard mounting holes"))\n)\n') # Preserve project settings on regeneration. project_file = ROOT / (PROJECT + ".kicad_pro") if not project_file.exists(): project_file.write_text(json.dumps({"meta":{"filename":project_file.name,"version":3}},indent=2)+"\n") root_uuid = uid("root") items = [f'(kicad_sch (version 20250114) (generator "eeschema") (uuid {q(root_uuid)}) (paper "A3")', '(title_block (title "ESP32 Serial Swiss Army Knife - Module Carrier") (date "2026-09-20") (rev "A0 DRAFT") (comment 1 "Not fabrication-ready: provisional module footprints and male RS-232 mapping"))', '(lib_symbols '+'\n'.join(symbol_definition(name,True) for name in SPECS)+')'] expected = {} def wire(x1,y1,x2,y2,key): items.append(f'(wire (pts (xy {n(x1)} {n(y1)}) (xy {n(x2)} {n(y2)})) (stroke (width 0) (type default)) (uuid {q(uid("wire:"+key))}))') def label(net,x,y,key): items.append(f'(label {q(net)} (at {n(x)} {n(y)} 0) {effects(1.016,"(justify left bottom)")} (uuid {q(uid("label:"+key))}))') def note(text,x,y,size=1.27): items.append(f'(text {q(text)} (at {n(x)} {n(y)} 0) {effects(size,"(justify left top)")} (uuid {q(uid("text:"+text))}))') def place(name,ref,value,x,y,nets,extra=None): pins,hw,hh,_,footprint,description = SPECS[name] instance = [f'(symbol (lib_id {q("Carrier:"+name)}) (at {n(x)} {n(y)} 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid {q(uid(ref))})', prop("Reference",ref,x,y-hh-5.08),prop("Value",value,x,y-hh-2.54),prop("Footprint",footprint,x,y,True),prop("Datasheet","",x,y,True)] for k,v in (extra or {}).items(): instance.append(prop(k,v,x,y,True)) instance.extend(f'(pin {q(p[0])} (uuid {q(uid(ref+":"+p[0]))}))' for p in pins) instance.append(f'(instances (project {q(PROJECT)} (path {q("/"+root_uuid)} (reference {q(ref)}) (unit 1)))))') items.append('\n'.join(instance)) for number,_,_,px,py,angle in pins: ax,ay=x+px,y-py net=nets.get(number) key=ref+":"+number if net is None: items.append(f'(no_connect (at {n(ax)} {n(ay)}) (uuid {q(uid("nc:"+key))}))') else: end=ax-12.7 if angle==0 else ax+12.7 wire(ax,ay,end,ay,key) label(net,end,ay,key) expected.setdefault(net,[]).append([ref,number]) mcu_nets={} for number,name,*_ in MCU: if name == "3V3": mcu_nets[number]="+3V3" elif name == "GND": mcu_nets[number]="GND" elif name.startswith("GPIO") and int(name[4:]) in GPIO_NET: mcu_nets[number]=GPIO_NET[int(name[4:])] place("HW678_N16R8","U1","HW678 / S3-N16R8",95.25,109.22,mcu_nets,{"Status":"Header geometry nominal; outline/antenna clearance pending","Pin_numbering":"Left 1-22; right 23-44; both top-to-bottom, antenna up"}) rs_nets=dict(zip(map(str,range(1,13)),["+3V3","GND","RS_DCD","RS_RX","RS_TX","RS_DTR","RS_DSR","RS_RTS","RS_CTS","RS_RI","RS_VALID","RS_OFF_N"])) place("MAX3243_Module_Reference","U2","Adafruit MAX3243 / male 6253",228.6,96.52,rs_nets,{"Status":"VERIFY male header numbering; footprint intentionally unassigned","Datasheet_source":"adafruit-research.md; reference female 5988 CAD"}) place("OLED_I2C_26mm","DS1","128x64 I2C OLED / 0x3C",228.6,152.4,{"1":"GND","2":"+3V3","3":"OLED_SCL","4":"OLED_SDA"},{"Status":"Module outline/header/display user fit-verified; pad/drill fit provisional"}) for index,(net,function) in enumerate((("BTN_PREVIOUS","Previous / back"),("BTN_SELECT","Select / confirm"),("BTN_NEXT","Next")),1): y=88.9+(index-1)*35.56 place("R",f"R{index}","2.2k 1%",327.66,y,{"1":"+3V3","2":net},{"Rating":"At least 0.125 W; exact resistor MPN pending"}) place("SW_NO",f"SW{index}","B3F-1000",327.66,y+15.24,{"1":net,"2":"GND"},{"Manufacturer":"Omron","MPN":"B3F-1000","Function":function,"Procurement":"Live stock not verified"}) note(function,302.26,y+21.59,1.016) note("MODULE CARRIER - INITIAL SCHEMATIC",15.24,15.24,2.54) note("Native KiCad 10 schematic. Named wire labels join matching nets on this sheet.\nRead hardware/PCB/README.md before layout or fabrication.",15.24,24.13) note("U1: complete development board",53.34,57.15,1.778) note("Antenna up / USB down: left row 1-22, right row 23-44.\nNC marks mean unused on carrier, not unused inside module.",53.34,64.77,1.016) note("U2: complete RS-232 breakout",177.8,57.15,1.778) note("Male 6253 selected. Numbers from published 5988 female CAD.\nVERIFY male header before routing; no footprint assigned.\nDE-9 and charge pump are already on the module.",177.8,64.77,1.016) note("Controls: active-low, firmware-debounced",292.1,57.15,1.778) note("External 2.2k pull-ups: ~1.5 mA pressed at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending.",292.1,64.77,1.016) note("DS1: front view GND / VCC / SCL / SDA",177.8,128.27,1.524) note("Power at 3.3 V only. Verify on-module I2C pull-ups;\nexternal values intentionally not guessed.",177.8,165.1,1.016) note("POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nVerify USB power isolation before attaching two powered hosts.\nKeep both USB connectors and reset/boot buttons accessible.",15.24,185.42) note("RS-232 / ELECTRICAL LIMITS\nTX/RX and modem labels are at the ESP32 logic side.\nOFF_N is active low; GPIO9 must remain open-drain.\nBreakout provides its OFF pull-up. No galvanic isolation.\nNo carrier connection to raw RS-232 voltages.\nCarrier GND, USB GND and RS-232 signal ground are common.\nNever power OLED or MAX3243 logic from 5 V.",152.4,185.42) note("BEFORE PCB RELEASE\nVerify male breakout pin order/footprint.\nConfirm header/socket finished-hole requirements.\nComplete module courtyards and antenna clearance.\nVerify 3.3 V budget, OLED pull-ups and USB power path.\nChoose mounting hardware and confirm button stock.\nERC is not hardware or fabrication approval.",292.1,185.42) items.append(')') ROOT.joinpath(PROJECT+".kicad_sch").write_text('\n'.join(items)+'\n') ROOT.joinpath("validation/expected-nets.json").write_text(json.dumps(expected,indent=2,sort_keys=True)+'\n') print("Generated native KiCad schematic, project-local symbol library and tables.")