Integrate A1 carrier PCB CAD updates

- Add pinned STEP-derived DE-9 mechanical measurements and provenance
- Refresh placement, routing, silkscreen, and nominal CAD envelopes
- Document provisional dimensions, assumptions, and validation status
This commit is contained in:
2026-09-21 12:31:24 +02:00
parent 06df47c934
commit c0f7a1ad51
13 changed files with 5825 additions and 3966 deletions
+24 -39
View File
@@ -3,11 +3,11 @@
Requires system Python + pcbnew. Refuses to overwrite an existing PCB unless
--overwrite is explicit. This is a draft generator, not a manufacturing tool.
Does not modify the schematic or footprint libraries. KiCad SaveBoard also saves
PCB design settings into the project; close the project before regeneration.
Does not modify the schematic, footprint libraries or project settings.
SaveBoard(..., True) deliberately preserves the user's existing project rules.
"""
import argparse
import json
from pathlib import Path
import re
import subprocess
@@ -18,10 +18,10 @@ ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
LIBS = Path("/usr/share/kicad/footprints")
ORIGIN = (100, 100)
OUTLINE = [(0,0), (82,0), (82,80), (36,80), (36,61), (0,61)]
OUTLINE = [(36,0), (82,0), (82,80), (0,80), (0,19), (36,19)]
PLACEMENT = {
"U1": ("Carrier:HW678_2x22_Provisional", 8, 4),
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 30),
"U1": ("Carrier:HW678_2x22_Provisional", 8, 23),
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 26.670),
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43, 36),
"R1": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 43, 34),
"R2": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 54, 34),
@@ -30,9 +30,9 @@ PLACEMENT = {
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 55, 68),
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 67, 68),
}
MOUNTS = [(3.5,3.5), (3.5,56), (78,4), (78,76)]
MOUNTS = [(3.5,22.5), (3.5,75), (78,4), (78,76)]
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
RF_RESERVE = [(10,0), (31,0), (31,13), (10,13)]
RF_RESERVE = [(10,19), (31,19), (31,32), (10,32)]
def mm(v):
return p.FromMM(v)
@@ -87,20 +87,6 @@ def copy_schematic_fields(fp, component):
fp.GetField(name).SetVisible(False)
def configure_draft_netclasses():
# SaveBoard creates the project defaults. Align future interactive routing
# with the draft's physical minimum widths and its explicit power routing.
path=ROOT/"serial-carrier.kicad_pro"
project=json.loads(path.read_text())
settings=project["net_settings"]
default=next(c for c in settings["classes"] if c["name"]=="Default")
default.update(clearance=0.25, track_width=0.25, via_diameter=0.7, via_drill=0.3)
power=dict(default, name="Power", priority=0, track_width=0.5, diff_pair_width=0.25)
settings["classes"]=[c for c in settings["classes"] if c["name"]!="Power"]+[power]
patterns=[v for v in settings.get("netclass_patterns",[]) if v["pattern"] not in ("/+3V3","/GND")]
settings["netclass_patterns"]=patterns+[{"netclass":"Power","pattern":name} for name in ("/+3V3","/GND")]
path.write_text(json.dumps(project,indent=2)+"\n")
def main():
parser=argparse.ArgumentParser(description=__doc__)
@@ -108,8 +94,8 @@ def main():
args=parser.parse_args()
if PCB.exists() and not args.overwrite:
parser.error("PCB exists; preserve manual edits or explicitly use --overwrite")
if any(ROOT.glob("~serial-carrier.*.lck")):
parser.error("Project is open in KiCad; close it before regenerating PCB/settings")
if ROOT.joinpath("~serial-carrier.kicad_pcb.lck").exists():
parser.error("PCB is open in KiCad; close it before regenerating")
netfile=ROOT / "validation/pcb-source-netlist.xml"
subprocess.run(["kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(netfile),str(ROOT/"serial-carrier.kicad_sch")],check=True)
source=ET.parse(netfile).getroot()
@@ -126,11 +112,11 @@ def main():
settings.m_HoleClearance=mm(0.25)
settings.m_TrackMinWidth=mm(0.25)
settings.m_ViasMinSize=mm(0.7)
# SaveBoard persists these proposed draft rules into the project settings.
# Proposed in-memory draft rules only; never overwrite the user's project settings.
title=board.GetTitleBlock()
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier draft")
title.SetRevision("A0 DRAFT")
title.SetComment(0,"NOT FOR FABRICATION - male module / USB / antenna geometry unverified")
title.SetRevision("A1 DRAFT")
title.SetComment(0,"NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly")
pad_nets={}
for source_net in source.findall("nets/net"):
net=p.NETINFO_ITEM(board,source_net.get("name"))
@@ -155,9 +141,9 @@ def main():
quiet_footprint(fp)
copy_schematic_fields(fp,comp)
if ref=="U1":
fp.Reference().SetPosition(xy(20.7,25))
fp.Reference().SetPosition(xy(20.7,44))
elif ref=="U2":
fp.Reference().SetPosition(xy(56.97,18))
fp.Reference().SetPosition(xy(56.97,14.670))
elif ref=="DS1":
fp.Reference().SetPosition(xy(56,49))
elif ref.startswith("SW"):
@@ -199,23 +185,22 @@ def main():
reserve.SetDoNotAllowFootprints(False) # the module itself spans this region
polygon(reserve,RF_RESERVE)
board.Add(reserve)
text(board,"RF RESERVE",20.5,7,p.Dwgs_User,1.2)
text(board,"VERIFY ANTENNA",20.5,10,p.Dwgs_User,1)
text(board,"ESP32 HW678",20.7,29)
text(board,"USB ACCESS - VERIFY",18,64,p.Dwgs_User,1)
text(board,"DE-9 OUT / VERIFY MALE",56.97,1.5,p.Dwgs_User,1)
text(board,"MAX3243 REF",56.97,21)
text(board,"RF RESERVE",20.5,26,p.Dwgs_User,1.2)
text(board,"VERIFY ANTENNA",20.5,29,p.Dwgs_User,1)
text(board,"ESP32 HW678",20.7,48)
text(board,"USB ACCESS - VERIFY",18,83,p.Dwgs_User,1)
text(board,"DE-9 OUT / NOMINAL MALE",56.97,-1.830,p.Dwgs_User,1)
text(board,"MAX3243 REF",56.97,17.670)
text(board,"26mm OLED",56,52)
for label,x in zip(("BACK","SELECT","NEXT"),(46.25,58.25,70.25)):
text(board,label,x,75, size=0.9)
text(board,"DRAFT - NOT FOR FAB",56,78,size=0.9)
text(board,"82 x 80 mm / 2 copper layers",41,-5,p.Dwgs_User,1.5)
text(board,"Carrier envelope only; module overhang and connector clearance not qualified",41,-2.5,p.Dwgs_User,0.9)
text(board,"Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending",41,-2.5,p.Dwgs_User,0.9)
board.BuildConnectivity()
p.SaveBoard(str(PCB),board)
configure_draft_netclasses()
assert p.SaveBoard(str(PCB),board,True)
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
print("Placement draft only. Board-only U2 footprint is a female-CAD reference, not male qualification.")
print("Placement draft only. U2 nominal male external metal dimensions user-confirmed; pin map / assembly still require qualification.")
if __name__=="__main__":
main()