kicad:EasyEDA→KiCad 工程(Board1/Board2)、库、脚本与评审文档(IC 引脚、风扇、热、EDL 供电、布局规则)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,487 @@
|
||||
"""Create unrouted KiCad boards with verified net import and mechanical anchors.
|
||||
|
||||
Run with the Python shipped with KiCad. Refuse to overwrite edited boards.
|
||||
Electrical parts awaiting layout are grouped outside the board, not silently
|
||||
presented as a completed placement. No EasyEDA tracks or copper are imported.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pcbnew as pcb
|
||||
|
||||
from check_hardware import Board
|
||||
from schematic_maintenance import ROOT, parse, child, children
|
||||
|
||||
CONFIG = json.loads((ROOT / 'pcb-mechanical.json').read_text(encoding='utf-8'))
|
||||
OUT = ROOT / 'out/pcb-start'
|
||||
MECH_LIB = ROOT / 'lib/PhoneRail-Mechanical.pretty'
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def xy(x, y):
|
||||
return pcb.VECTOR2I(pcb.FromMM(x), pcb.FromMM(y))
|
||||
|
||||
|
||||
def top(x, y):
|
||||
ox, oy = CONFIG['origin_kicad_mm']
|
||||
return ox + x, oy + CONFIG['size_mm'][1] - y
|
||||
|
||||
|
||||
def site(n, u, v):
|
||||
x, y, angle = CONFIG['phone_sites'][str(n)]
|
||||
if angle == 180:
|
||||
u, v = CONFIG['phone_size_mm'][0] - u, CONFIG['phone_size_mm'][1] - v
|
||||
return top(x + u, y + v)
|
||||
|
||||
|
||||
def layer_set(*layers):
|
||||
result = pcb.LSET()
|
||||
for layer in layers:
|
||||
result.AddLayer(layer)
|
||||
return result
|
||||
|
||||
|
||||
def draw_line(board, a, b, layer=pcb.Edge_Cuts, width=.05):
|
||||
item = pcb.PCB_SHAPE()
|
||||
item.SetShape(pcb.SHAPE_T_SEGMENT)
|
||||
item.SetStart(xy(*a))
|
||||
item.SetEnd(xy(*b))
|
||||
item.SetLayer(layer)
|
||||
item.SetWidth(pcb.FromMM(width))
|
||||
board.Add(item)
|
||||
|
||||
|
||||
def rounded_rect(board, x, y, w, h, r, layer=pcb.Edge_Cuts):
|
||||
points = [(x+r,y), (x+w-r,y), (x+w,y+r), (x+w,y+h-r),
|
||||
(x+w-r,y+h), (x+r,y+h), (x,y+h-r), (x,y+r)]
|
||||
for i in (0, 2, 4, 6):
|
||||
draw_line(board, points[i], points[i+1], layer)
|
||||
for i, center, mid_angle in [(1,(x+w-r,y+r),-45), (3,(x+w-r,y+h-r),45),
|
||||
(5,(x+r,y+h-r),135), (7,(x+r,y+r),225)]:
|
||||
mid = (center[0]+r*math.cos(math.radians(mid_angle)),
|
||||
center[1]+r*math.sin(math.radians(mid_angle)))
|
||||
arc = pcb.PCB_SHAPE()
|
||||
arc.SetShape(pcb.SHAPE_T_ARC)
|
||||
arc.SetArcGeometry(xy(*points[i]), xy(*mid), xy(*points[(i+1) % 8]))
|
||||
arc.SetLayer(layer)
|
||||
arc.SetWidth(pcb.FromMM(.05))
|
||||
board.Add(arc)
|
||||
|
||||
|
||||
def label(board, text, x, y, size=1, layer=pcb.Dwgs_User):
|
||||
item = pcb.PCB_TEXT(board)
|
||||
item.SetText(text)
|
||||
item.SetPosition(xy(x, y))
|
||||
item.SetTextSize(xy(size, size))
|
||||
item.SetTextThickness(pcb.FromMM(size / 7))
|
||||
item.SetLayer(layer)
|
||||
board.Add(item)
|
||||
|
||||
|
||||
def keepout(board, name, rect, layers, copper=False):
|
||||
zone = pcb.ZONE(board)
|
||||
zone.SetIsRuleArea(True)
|
||||
zone.SetZoneName(name)
|
||||
zone.SetLayerSet(layer_set(*layers))
|
||||
zone.SetDoNotAllowFootprints(True)
|
||||
zone.SetDoNotAllowTracks(copper)
|
||||
zone.SetDoNotAllowVias(copper)
|
||||
zone.SetDoNotAllowPads(copper)
|
||||
zone.SetDoNotAllowZoneFills(copper)
|
||||
x0, y0, x1, y1 = rect
|
||||
outline = zone.Outline()
|
||||
outline.NewOutline()
|
||||
for x, y in [(x0,y0), (x1,y0), (x1,y1), (x0,y1)]:
|
||||
outline.Append(pcb.FromMM(x), pcb.FromMM(y))
|
||||
board.Add(zone)
|
||||
|
||||
|
||||
def mechanical_library():
|
||||
MECH_LIB.mkdir(exist_ok=True)
|
||||
for name, drill, pad, radius, plated in [
|
||||
('M3_GND_D3.2', 3.2, 6, 3.5, True),
|
||||
('M1.6_NPTH_D1.8', 1.8, 1.8, CONFIG['phone_hole_courtyard_radius_mm'], False)]:
|
||||
fp = pcb.FOOTPRINT(None)
|
||||
fp.SetFPIDAsString('PhoneRail-Mechanical:' + name)
|
||||
fp.SetReference('H**')
|
||||
fp.SetValue(name)
|
||||
fp.SetAttributes(pcb.FP_BOARD_ONLY | pcb.FP_EXCLUDE_FROM_BOM | pcb.FP_EXCLUDE_FROM_POS_FILES)
|
||||
hole = pcb.PAD(fp)
|
||||
hole.SetNumber('1' if plated else '')
|
||||
hole.SetAttribute(pcb.PAD_ATTRIB_PTH if plated else pcb.PAD_ATTRIB_NPTH)
|
||||
hole.SetShape(pcb.PAD_SHAPE_CIRCLE)
|
||||
hole.SetSize(xy(pad, pad))
|
||||
hole.SetDrillSize(xy(drill, drill))
|
||||
layers = pcb.LSET.AllCuMask()
|
||||
layers.AddLayer(pcb.F_Mask)
|
||||
layers.AddLayer(pcb.B_Mask)
|
||||
hole.SetLayerSet(layers)
|
||||
fp.Add(hole)
|
||||
for layer in (pcb.F_CrtYd, pcb.B_CrtYd):
|
||||
shape = pcb.PCB_SHAPE(fp)
|
||||
shape.SetShape(pcb.SHAPE_T_CIRCLE)
|
||||
shape.SetCenter(xy(0, 0))
|
||||
shape.SetEnd(xy(radius, 0))
|
||||
shape.SetWidth(pcb.FromMM(.05))
|
||||
shape.SetLayer(layer)
|
||||
fp.Add(shape)
|
||||
fp.Reference().SetVisible(False)
|
||||
fp.Value().SetVisible(False)
|
||||
pcb.PCB_IO_MGR.FindPlugin(pcb.PCB_IO_MGR.KICAD_SEXP).FootprintSave(str(MECH_LIB), fp)
|
||||
path = MECH_LIB/(name+'.kicad_mod')
|
||||
path.write_text(pth_apertures(path.read_text(encoding='utf-8')),encoding='utf-8')
|
||||
|
||||
|
||||
def mount(board, name, ref, position, gnd=None):
|
||||
fp = pcb.FootprintLoad(str(MECH_LIB), name)
|
||||
fp.SetReference(ref)
|
||||
fp.SetPosition(xy(*position))
|
||||
fp.SetLocked(True)
|
||||
if gnd:
|
||||
for pad in fp.Pads():
|
||||
pad.SetNet(gnd)
|
||||
board.Add(fp)
|
||||
|
||||
|
||||
def place(fp, position, rotation=0, bottom=False, locked=True):
|
||||
fp.SetPosition(xy(*position))
|
||||
if bottom:
|
||||
fp.Flip(fp.GetPosition(), pcb.FLIP_DIRECTION_TOP_BOTTOM)
|
||||
fp.SetOrientationDegrees(rotation)
|
||||
fp.SetLocked(locked)
|
||||
fp.SetField('Placement', 'MECHANICAL_ANCHOR' if locked else 'STAGED_NOT_PLACED')
|
||||
for field in fp.GetFields():
|
||||
if field.GetName() not in {'Reference', 'Value'}:
|
||||
field.SetVisible(False)
|
||||
field.SetLayer(pcb.B_Fab if bottom else pcb.F_Fab)
|
||||
field.SetMirrored(bottom)
|
||||
fp.Value().SetVisible(False)
|
||||
fp.Reference().SetTextSize(xy(.8, .8))
|
||||
fp.Reference().SetTextThickness(pcb.FromMM(.12))
|
||||
box = fp.GetBoundingBox(False, False)
|
||||
fp.Reference().SetPosition(pcb.VECTOR2I(fp.GetPosition().x, box.GetTop()-pcb.FromMM(1)))
|
||||
|
||||
|
||||
def stage(board, parts, source, number):
|
||||
groups = {}
|
||||
for ref, fp in parts.items():
|
||||
if fp.IsLocked():
|
||||
continue
|
||||
groups.setdefault(source.parts[ref]['page'], []).append(fp)
|
||||
xstart = CONFIG['origin_kicad_mm'][0] + CONFIG['size_mm'][0] + 15
|
||||
ystart = CONFIG['origin_kicad_mm'][1]
|
||||
column_y = [ystart] * 3
|
||||
for index, (page, fps) in enumerate(sorted(groups.items())):
|
||||
col = index % 3
|
||||
left, y = xstart + col*77, column_y[col] + 6
|
||||
label(board, page.removesuffix('.kicad_sch'), left+32, y-4, 1.2)
|
||||
cursor, row_height = left, 0
|
||||
for fp in sorted(fps, key=lambda f: f.GetReference()):
|
||||
place(fp, (0, 0), bottom=number == 2, locked=False)
|
||||
box = fp.GetBoundingBox(False, False)
|
||||
w, h = pcb.ToMM(box.GetWidth()), pcb.ToMM(box.GetHeight())
|
||||
if cursor+w > left+70 and cursor > left:
|
||||
cursor, y, row_height = left, y+row_height+4, 0
|
||||
offset = (cursor-pcb.ToMM(box.GetLeft()), y-pcb.ToMM(box.GetTop()))
|
||||
fp.Move(xy(*offset))
|
||||
cursor += w+3
|
||||
row_height = max(row_height, h)
|
||||
column_y[col] = y+row_height+7
|
||||
|
||||
|
||||
def add_stackup(path):
|
||||
source = path.read_text(encoding='utf-8')
|
||||
setup = child(parse(source), 'setup')
|
||||
layers = ['F.Cu','In1.Cu','In2.Cu','B.Cu']
|
||||
items = ['(stackup']
|
||||
for i, layer in enumerate(layers):
|
||||
items.append(f'(layer "{layer}" (type "copper") (thickness {CONFIG["copper_mm"][i]}))')
|
||||
if i < 3:
|
||||
kind = 'core' if i == 1 else 'prepreg'
|
||||
items.append(f'(layer "dielectric {i+1}" (type "{kind}") '
|
||||
f'(thickness {CONFIG["dielectric_mm"][i]}) (material "FR4") '
|
||||
'(epsilon_r 4.3) (loss_tangent 0.02))')
|
||||
items.append('(copper_finish "ENIG") (dielectric_constraints no))')
|
||||
old = child(setup, 'stackup')
|
||||
start, end = (old.start, old.end) if old else (setup.end-1, setup.end-1)
|
||||
source = source[:start]+'\n'.join(items)+'\n'+source[end:]
|
||||
path.write_text(pth_apertures(source), encoding='utf-8')
|
||||
|
||||
|
||||
def pth_apertures(source):
|
||||
# Explicit no-tenting survives a native load/save cycle in KiCad 10.
|
||||
edits = []
|
||||
root = parse(source)
|
||||
footprints = [root] if root[0] == 'footprint' else children(root,'footprint')
|
||||
for footprint in footprints:
|
||||
for pad in children(footprint,'pad'):
|
||||
if pad[2] in {'thru_hole','np_thru_hole'}:
|
||||
layers = child(pad,'layers')
|
||||
edits.append((layers.start,layers.end,'(layers "*.Cu" "*.Mask")'))
|
||||
if not child(pad,'tenting'):
|
||||
edits.append((pad.end-1,pad.end-1,' (tenting (front no) (back no))\n'))
|
||||
for start,end,replacement in sorted(edits,reverse=True):
|
||||
source = source[:start]+replacement+source[end:]
|
||||
return source
|
||||
|
||||
|
||||
def native_net_names(source):
|
||||
if hasattr(source, 'pcb_net_names'):
|
||||
return source.pcb_net_names
|
||||
path = OUT/f'Board{source.number}.net'
|
||||
result = subprocess.run(['D:/KiCad/bin/kicad-cli.exe','sch','export','netlist',
|
||||
'--format','kicadsexpr','-o',str(path),str(source.path)],
|
||||
capture_output=True,encoding='utf-8',errors='replace',timeout=60)
|
||||
path.with_suffix('.net.log').write_text(result.stdout+result.stderr,encoding='utf-8')
|
||||
if result.returncode:
|
||||
raise RuntimeError(f'Native netlist failed: {path}')
|
||||
nets = child(parse(path.read_text(encoding='utf-8')),'nets')
|
||||
by_members = {frozenset((child(node,'ref')[1],child(node,'pin')[1]) for node in children(net,'node')):
|
||||
child(net,'name')[1] for net in children(nets,'net')}
|
||||
# XML is human-readable; the native netlist retains KiCad's escaped names.
|
||||
source.pcb_net_names = {name:by_members[frozenset(members)] for name,members in source.net_members.items()}
|
||||
return source.pcb_net_names
|
||||
|
||||
|
||||
def project_settings(number):
|
||||
path = ROOT / f'PhoneRail-Board{number}/PhoneRail-Board{number}.kicad_pro'
|
||||
project = json.loads(path.read_text(encoding='utf-8'))
|
||||
net = project.setdefault('net_settings', {})
|
||||
base = dict(net['classes'][0])
|
||||
base.update(clearance=.15, track_width=.25, via_diameter=.6, via_drill=.3,
|
||||
diff_pair_width=.28, diff_pair_gap=.18, diff_pair_via_gap=.25)
|
||||
classes = [dict(base, name='Default')]
|
||||
for name, width, clearance in [('USB90',.28,.15), ('PWR4A',1.2,.2),
|
||||
('PWR5V',.8,.2), ('PWR12V',2,.2)]:
|
||||
classes.append(dict(base, name=name, track_width=width, clearance=clearance))
|
||||
net['classes'] = classes
|
||||
patterns = [(n, 'PWR4A') for n in ['VO*_P', *[f'VO{i}' for i in range(1,6)],
|
||||
*[f'Net-(U{i}-VIN+)' for i in range(8,13)]]]
|
||||
patterns += [(n, 'PWR12V') for n in ['+12V', '*/FAN_12V_SW', 'Net-(J1-+)', 'Net-(D1-C)']]
|
||||
patterns += [(n, 'PWR5V') for n in ['+5V', *[f'VBUS{i}' for i in range(1,6)]]]
|
||||
patterns += [(n, 'USB90') for n in ['USB*_DP','USB*_DM','USB*_DPJ','USB*_DMJ','UP_DP','UP_DM']]
|
||||
net['netclass_patterns'] = [{'pattern': p, 'netclass': c} for p, c in patterns]
|
||||
settings = project['board'].setdefault('design_settings', {})
|
||||
settings.setdefault('rules', {}).update(min_clearance=.127, min_track_width=.127,
|
||||
min_via_diameter=.6, min_through_hole_diameter=.3, min_hole_to_hole=.25,
|
||||
min_copper_edge_clearance=.3, solder_mask_clearance=.05, solder_mask_min_width=.1)
|
||||
return path, json.dumps(project, indent=2, ensure_ascii=False)+'\n'
|
||||
|
||||
|
||||
def build(number, source):
|
||||
board = pcb.BOARD()
|
||||
board.SetCopperLayerCount(4)
|
||||
board.GetDesignSettings().SetBoardThickness(pcb.FromMM(CONFIG['thickness_mm']))
|
||||
board.GetTitleBlock().SetTitle(f'PhoneRail Board{number} R1 - placement start')
|
||||
board.GetTitleBlock().SetRevision('R1-PLACEMENT-START')
|
||||
origin = CONFIG['origin_kicad_mm']
|
||||
rounded_rect(board, *origin, *CONFIG['size_mm'], CONFIG['corner_radius_mm'])
|
||||
nets = {}
|
||||
names = native_net_names(source)
|
||||
for name in source.net_members:
|
||||
net = pcb.NETINFO_ITEM(board, names[name])
|
||||
board.Add(net)
|
||||
nets[name] = net
|
||||
tree = ET.parse(OUT / f'Board{number}.xml')
|
||||
root_uuid = child(parse(source.path.read_text(encoding='utf-8')), 'uuid')[1]
|
||||
parts = {}
|
||||
for c in tree.findall('components/comp'):
|
||||
ref, fpname = c.get('ref'), c.findtext('footprint')
|
||||
if not fpname:
|
||||
raise ValueError(f'{ref}: no footprint')
|
||||
lib, name = fpname.split(':')
|
||||
if lib == 'PhoneRail':
|
||||
assert 'phonerail-y-axis-corrected-r1' in (ROOT/f'lib/{lib}.pretty/{name}.kicad_mod').read_text(encoding='utf-8'), 'Run repair_footprint_handedness.py before importing the migrated library'
|
||||
fp = pcb.FootprintLoad(str(ROOT / f'lib/{lib}.pretty'), name)
|
||||
if fp is None:
|
||||
raise ValueError(f'{ref}: missing {fpname}')
|
||||
fp.SetFPIDAsString(fpname)
|
||||
fp.SetReference(ref)
|
||||
fp.SetValue(c.findtext('value'))
|
||||
fp.SetSheetname(source.parts[ref]['page'].removesuffix('.kicad_sch'))
|
||||
fp.SetSheetfile(source.parts[ref]['page'])
|
||||
fp.SetExcludedFromBOM(source.parts[ref]['in_bom'] != 'yes')
|
||||
fp.SetExcludedFromPosFiles(source.parts[ref]['in_bom'] != 'yes')
|
||||
fp.SetDNP(source.parts[ref]['dnp'] == 'yes')
|
||||
for field in c.findall('fields/field'):
|
||||
if field.get('name') not in {'Reference','Value','Footprint'}:
|
||||
fp.SetField(field.get('name'), field.text or '')
|
||||
path = pcb.KIID_PATH()
|
||||
ids = [root_uuid, *c.find('sheetpath').get('tstamps').strip('/').split('/'), c.findtext('tstamps')]
|
||||
for uid in filter(None, ids):
|
||||
path.push_back(pcb.KIID(uid))
|
||||
fp.SetPath(path)
|
||||
board.Add(fp)
|
||||
for pad in fp.Pads():
|
||||
net_name = source.pins.get((ref, pad.GetNumber()))
|
||||
if net_name:
|
||||
pad.SetNet(nets[net_name])
|
||||
parts[ref] = fp
|
||||
for side, position in CONFIG['headers_source_mm'].items():
|
||||
ref = {1: {'left':'J24','right':'J25'}, 2: {'left':'J31','right':'J32'}}[number][side]
|
||||
place(parts[ref], top(*position), bottom=number == 2)
|
||||
holes = list(CONFIG['mounting_holes_common_mm'])
|
||||
if number == 2:
|
||||
holes += CONFIG['mounting_holes_carrier_only_mm']
|
||||
for i, point in enumerate(holes, 1):
|
||||
mount(board, 'M3_GND_D3.2', f'MH{i}', top(*point), nets['GND'])
|
||||
slot_rectangles = []
|
||||
if number == 2:
|
||||
place(parts['J23'], top(*CONFIG['usb_upstream_source_mm']), rotation=270, bottom=True)
|
||||
for n in range(1, 6):
|
||||
x, y, angle = CONFIG['phone_sites'][str(n)]
|
||||
w, h = CONFIG['phone_size_mm']
|
||||
lx, ly = top(x, y+h)
|
||||
rounded_rect(board, lx, ly, w, h, .5, pcb.Dwgs_User)
|
||||
label(board, f'CH{n} PHONE - 1mm GAP', lx+w/2, ly+h/2, 1.2)
|
||||
for name, pos in CONFIG['phone_holes_mm'].items():
|
||||
if name not in CONFIG['phone_holes_skipped']:
|
||||
mount(board, 'M1.6_NPTH_D1.8', f'P{n}{name}', site(n, *pos))
|
||||
for index, pos in enumerate(CONFIG['phone_key_contacts_mm']):
|
||||
ref = f'J{41+(n-1)*4+index}'
|
||||
place(parts[ref], site(n, *pos), rotation=angle)
|
||||
# A separate label outside the contact cluster prevents ink on contacts.
|
||||
parts[ref].Reference().SetVisible(False)
|
||||
for prefix, key in [('J8','battery_connector_local_mm'), ('J7','tail_connector_local_mm')]:
|
||||
place(parts[f'{prefix}{n}'], site(n, *CONFIG[key]), rotation=angle, bottom=True)
|
||||
for name, rect in CONFIG['slots_local_mm'].items():
|
||||
a, b = site(n, *rect[:2]), site(n, *rect[2:])
|
||||
x0,y0,x1,y1 = min(a[0],b[0]),min(a[1],b[1]),max(a[0],b[0]),max(a[1],b[1])
|
||||
rounded_rect(board, x0,y0,x1-x0,y1-y0,CONFIG['slot_corner_radius_mm'])
|
||||
margin = CONFIG['slot_clearance_mm']
|
||||
keepout(board, f'CH{n}_{name}_CUTOUT', (x0-margin,y0-margin,x1+margin,y1+margin),
|
||||
[pcb.F_Cu,pcb.In1_Cu,pcb.In2_Cu,pcb.B_Cu], copper=True)
|
||||
slot_rectangles.append([x0,y0,x1,y1])
|
||||
for name, rect in CONFIG['ribbon_lanes_local_mm'].items():
|
||||
a,b = site(n,*rect[:2]),site(n,*rect[2:])
|
||||
keepout(board, f'CH{n}_{name}_RIBBON',
|
||||
(min(a[0],b[0]),min(a[1],b[1]),max(a[0],b[0]),max(a[1],b[1])), [pcb.B_Cu])
|
||||
label(board, f'BOARD {number} R1 - UNROUTED', origin[0]+110, origin[1]-5, 1.8)
|
||||
stage(board, parts, source, number)
|
||||
destination = OUT / f'PhoneRail-Board{number}.kicad_pcb'
|
||||
pcb.SaveBoard(str(destination), board)
|
||||
add_stackup(destination)
|
||||
reloaded = pcb.LoadBoard(str(destination))
|
||||
report = verify_import(reloaded, source, number)
|
||||
report['internal_slots'] = len(slot_rectangles)
|
||||
return destination, reloaded, report
|
||||
|
||||
|
||||
def verify_import(board, source, number, allow_routes=False):
|
||||
parts = {f.GetReference(): f for f in board.GetFootprints()}
|
||||
electrical = {r: f for r, f in parts.items() if r in source.parts}
|
||||
assert set(electrical) == set(source.parts), 'Component import mismatch'
|
||||
checked = 0
|
||||
names = native_net_names(source)
|
||||
for ref, fp in electrical.items():
|
||||
expected = {pin: net for (r, pin), net in source.pins.items() if r == ref}
|
||||
pads = {}
|
||||
for pad in fp.Pads():
|
||||
pads.setdefault(pad.GetNumber(), []).append(pad)
|
||||
assert set(expected) <= set(pads), (ref, 'Missing pads', set(expected)-set(pads))
|
||||
for pin, net in expected.items():
|
||||
assert all(p.GetNetname() == names[net] for p in pads[pin]), (ref, pin, net)
|
||||
checked += len(pads[pin])
|
||||
for pad in fp.Pads():
|
||||
if pad.GetAttribute() in {pcb.PAD_ATTRIB_PTH,pcb.PAD_ATTRIB_NPTH}:
|
||||
assert pad.GetLayerSet().Contains(pcb.F_Mask) and pad.GetLayerSet().Contains(pcb.B_Mask), (ref,'missing PTH mask openings')
|
||||
assert fp.GetPath().size() >= 2, (ref, 'Missing schematic UUID path')
|
||||
assert fp.GetFPIDAsString() == source.parts[ref]['Footprint']
|
||||
if number == 2 and ref.startswith('J') and ref[1:].isdigit() and 41 <= int(ref[1:]) <= 60:
|
||||
assert all(not p.GetLayerSet().Contains(pcb.F_Paste) for p in fp.Pads()), ref
|
||||
assert fp.IsExcludedFromBOM() and fp.IsExcludedFromPosFiles(), ref
|
||||
assert board.GetCopperLayerCount() == 4
|
||||
assert abs(pcb.ToMM(board.GetDesignSettings().GetBoardThickness())-1.6) < .001
|
||||
if not allow_routes:
|
||||
assert len(list(board.GetTracks())) == 0
|
||||
if number == 2:
|
||||
usb = electrical['J23']
|
||||
assert all(p.GetPosition().x < usb.GetPosition().x for p in usb.Pads()
|
||||
if p.GetNumber() in {'A5','B5','A6','B6','A7','B7'}), 'USB mouth faces inward'
|
||||
for n in range(1,6):
|
||||
fp = electrical[f'J7{n}']
|
||||
pads = {p.GetNumber():p for p in fp.Pads()}
|
||||
# In local phone coordinates, VBUS anchor is -u, even row is +v.
|
||||
sign = -1 if CONFIG['phone_sites'][str(n)][2] == 180 else 1
|
||||
assert (pads['51'].GetPosition().x-fp.GetPosition().x)*sign < 0
|
||||
assert (pads['2'].GetPosition().y-fp.GetPosition().y)*sign < 0
|
||||
return {'electrical_footprints':len(electrical), 'mechanical_footprints':len(parts)-len(electrical),
|
||||
'pad_net_assignments_checked':checked, 'net_count':len(source.net_members),
|
||||
'anchored_electrical_footprints':sum(f.IsLocked() for f in electrical.values()),
|
||||
'staged_electrical_footprints':sum(any(field.GetName() == 'Placement' and field.GetText() == 'STAGED_NOT_PLACED'
|
||||
for field in f.GetFields()) for f in electrical.values()),
|
||||
'tracks':len(list(board.GetTracks())), 'copper_layers':4, 'thickness_mm':1.6}
|
||||
|
||||
|
||||
def mating_check(lower, upper):
|
||||
boards = [{f.GetReference():f for f in b.GetFootprints()} for b in (lower, upper)]
|
||||
count = 0
|
||||
for lref, uref in [('J24','J31'),('J25','J32')]:
|
||||
lp = {p.GetNumber():p for p in boards[0][lref].Pads()}
|
||||
up = {p.GetNumber():p for p in boards[1][uref].Pads()}
|
||||
for pin in range(1,41):
|
||||
a, b = lp[str(pin)], up[str(pin+1 if pin % 2 else pin-1)]
|
||||
assert (a.GetPosition()-b.GetPosition()).EuclideanNorm() < pcb.FromMM(.001), (lref,pin,'misaligned')
|
||||
assert a.GetNetname() == b.GetNetname(), (lref,pin,'wrong net')
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(exist_ok=True)
|
||||
manifest_path = OUT / 'pcb-start.json'
|
||||
old = json.loads(manifest_path.read_text(encoding='utf-8')) if manifest_path.exists() else {}
|
||||
for n in (1,2):
|
||||
name = f'PhoneRail-Board{n}'
|
||||
for suffix in ('.kicad_pcb', '.kicad_dru'):
|
||||
target = ROOT / name / (name+suffix)
|
||||
if target.exists() and digest(target) != old.get('generated_files', {}).get(str(target.relative_to(ROOT))):
|
||||
raise RuntimeError(f'Refusing to overwrite an edited or untracked file: {target}')
|
||||
mechanical_library()
|
||||
results, boards, artifacts = {}, [], []
|
||||
for n in (1,2):
|
||||
source = Board(n)
|
||||
source.compile(r'D:\KiCad\bin\kicad-cli.exe', OUT)
|
||||
temporary, board, report = build(n, source)
|
||||
results[f'Board{n}'] = report
|
||||
boards.append(board)
|
||||
artifacts.append((temporary, ROOT/source.name/(source.name+'.kicad_pcb')))
|
||||
results['mated_pins_checked'] = mating_check(*boards)
|
||||
results['generated_files'] = {}
|
||||
rules = '''(version 1)
|
||||
(rule "USB90 geometry target"
|
||||
(condition "A.NetClass == 'USB90'")
|
||||
(constraint diff_pair_gap (min 0.15) (opt 0.18) (max 0.21))
|
||||
(constraint diff_pair_uncoupled (max 5)))
|
||||
'''
|
||||
for temporary, target in artifacts:
|
||||
shutil.copyfile(temporary, target)
|
||||
n = 1 if 'Board1' in target.stem else 2
|
||||
project_path, content = project_settings(n)
|
||||
project_path.write_text(content, encoding='utf-8')
|
||||
rule_path = target.with_suffix('.kicad_dru')
|
||||
rule_path.write_text(rules, encoding='utf-8')
|
||||
table_path = target.parent / 'fp-lib-table'
|
||||
table = table_path.read_text(encoding='utf-8')
|
||||
if 'PhoneRail-Mechanical' not in table:
|
||||
node = parse(table)
|
||||
entry = '(lib (name "PhoneRail-Mechanical") (type "KiCad") (uri "${KIPRJMOD}/../lib/PhoneRail-Mechanical.pretty") (options "") (descr "R1 mechanical anchors"))\n'
|
||||
table_path.write_text(table[:node.end-1]+entry+table[node.end-1:], encoding='utf-8')
|
||||
for path in (target, rule_path):
|
||||
results['generated_files'][str(path.relative_to(ROOT))] = digest(path)
|
||||
results['status'] = 'PLACEMENT_START_NOT_ROUTED_NOT_RELEASED'
|
||||
results['mechanical_config_sha256'] = digest(ROOT/'pcb-mechanical.json')
|
||||
manifest_path.write_text(json.dumps(results,indent=2)+'\n',encoding='utf-8')
|
||||
print(json.dumps(results, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户