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:
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Refresh board-local device outlines without modifying libraries or project settings.
|
||||
|
||||
Uses pcbnew and Shapely from system Python. Only owned F.SilkS graphics are
|
||||
replaced; source F.Fab outlines, copper, footprints and board edges are retained.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import math
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import LineString, Point, box
|
||||
from shapely.ops import unary_union, polygonize
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
NS = uuid.UUID("67964aa4-c3a5-45d8-9b54-250fb68e7c41")
|
||||
WIDTH = 0.12
|
||||
GAP = 0.22 # Additional clearance from mask openings and existing silk strokes.
|
||||
|
||||
|
||||
def xy(point):
|
||||
return p.ToMM(point.x), p.ToMM(point.y)
|
||||
|
||||
|
||||
def vec(point):
|
||||
return p.VECTOR2I(p.FromMM(point[0]), p.FromMM(point[1]))
|
||||
|
||||
|
||||
def outline_lines(shape):
|
||||
if shape.GetShape() == p.SHAPE_T_RECT:
|
||||
x1, y1 = xy(shape.GetStart())
|
||||
x2, y2 = xy(shape.GetEnd())
|
||||
return [LineString([(x1,y1),(x2,y1)]), LineString([(x2,y1),(x2,y2)]),
|
||||
LineString([(x2,y2),(x1,y2)]), LineString([(x1,y2),(x1,y1)])]
|
||||
if shape.GetShape() == p.SHAPE_T_SEGMENT:
|
||||
return [LineString([xy(shape.GetStart()), xy(shape.GetEnd())])]
|
||||
return []
|
||||
|
||||
|
||||
def add_outlines(board):
|
||||
owned = {str(uuid.uuid5(NS, str(i))) for i in range(4096)}
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if drawing.m_Uuid.AsString() in owned:
|
||||
board.Delete(drawing)
|
||||
footprints = {f.GetReference(): f for f in board.GetFootprints()}
|
||||
obstacles = []
|
||||
for fp in board.GetFootprints():
|
||||
for pad in fp.Pads():
|
||||
if not pad.IsOnLayer(p.F_Mask):
|
||||
continue
|
||||
assert pad.GetOrientationDegrees() % 90 == 0
|
||||
x,y = xy(pad.GetPosition())
|
||||
expansion = max(0, p.ToMM(pad.GetSolderMaskExpansion(p.F_Mask)))
|
||||
if pad.GetShape() == p.PAD_SHAPE_CIRCLE:
|
||||
geom = Point(x,y).buffer(p.ToMM(pad.GetSize().x)/2, quad_segs=32)
|
||||
else:
|
||||
bb=pad.GetBoundingBox()
|
||||
geom=box(p.ToMM(bb.GetX()),p.ToMM(bb.GetY()),p.ToMM(bb.GetRight()),p.ToMM(bb.GetBottom()))
|
||||
obstacles.append(geom.buffer(expansion+GAP+WIDTH/2))
|
||||
for graphic in fp.GraphicalItems():
|
||||
if isinstance(graphic,p.PCB_SHAPE) and graphic.GetLayer()==p.F_SilkS:
|
||||
for line in outline_lines(graphic):
|
||||
obstacles.append(line.buffer(p.ToMM(graphic.GetWidth())/2+GAP+WIDTH/2))
|
||||
# Clear even tented vias for a robust visible guide, independent of mask settings.
|
||||
for track in board.GetTracks():
|
||||
if isinstance(track,p.PCB_VIA):
|
||||
obstacles.append(Point(xy(track.GetPosition())).buffer(p.ToMM(track.GetWidth(p.F_Cu))/2+GAP+WIDTH/2,quad_segs=32))
|
||||
blocked=unary_union(obstacles)
|
||||
edges = [LineString([xy(d.GetStart()), xy(d.GetEnd())])
|
||||
for d in board.GetDrawings() if d.GetLayer() == p.Edge_Cuts]
|
||||
polygons = list(polygonize(edges))
|
||||
assert len(polygons) == 1, 'Expected a single closed carrier outline'
|
||||
# U2's bare PCB front is flush with the carrier edge; do not print across it.
|
||||
silk_area = polygons[0].buffer(-(GAP + WIDTH / 2))
|
||||
count=0
|
||||
|
||||
def add(shape):
|
||||
nonlocal count
|
||||
assert count < 4096
|
||||
shape.SetLayer(p.F_SilkS)
|
||||
shape.SetUuid(p.KIID(str(uuid.uuid5(NS,str(count)))))
|
||||
board.Add(shape)
|
||||
count+=1
|
||||
|
||||
def line(a,b):
|
||||
clipped=LineString([a,b]).intersection(silk_area).difference(blocked)
|
||||
pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped]
|
||||
for piece in pieces:
|
||||
if piece.geom_type!="LineString" or piece.length < 0.25:
|
||||
continue
|
||||
shape=p.PCB_SHAPE(board)
|
||||
shape.SetShape(p.SHAPE_T_SEGMENT)
|
||||
shape.SetStart(vec(piece.coords[0])); shape.SetEnd(vec(piece.coords[-1]))
|
||||
shape.SetWidth(p.FromMM(WIDTH))
|
||||
add(shape)
|
||||
|
||||
for ref in ("DS1","U2"):
|
||||
assert footprints[ref].GetOrientationDegrees()==0, "Review outlines for rotated modules"
|
||||
for source in footprints[ref].GraphicalItems():
|
||||
if not isinstance(source,p.PCB_SHAPE) or source.GetLayer()!=p.F_Fab:
|
||||
continue
|
||||
if source.GetShape()==p.SHAPE_T_ARC:
|
||||
# Test small spans for clearance, but retain true circular arcs
|
||||
# for each visible run rather than replacing corners with chords.
|
||||
cx,cy=xy(source.GetCenter())
|
||||
sx,sy=xy(source.GetStart()); mx,my=xy(source.GetArcMid())
|
||||
start=math.atan2(sy-cy,sx-cx)
|
||||
sweep=2*math.atan2((sx-cx)*(my-cy)-(sy-cy)*(mx-cx),
|
||||
(sx-cx)*(mx-cx)+(sy-cy)*(my-cy))
|
||||
radius=math.hypot(sx-cx,sy-cy)
|
||||
steps=max(1,math.ceil(abs(math.degrees(sweep))))
|
||||
def at(t):
|
||||
angle=start+sweep*t/steps
|
||||
return cx+radius*math.cos(angle),cy+radius*math.sin(angle)
|
||||
run=None
|
||||
for i in range(steps+1):
|
||||
span=LineString([at(i),at(i+0.5),at(i+1)])
|
||||
clear=i<steps and silk_area.covers(span) and not span.intersects(blocked)
|
||||
if clear and run is None:
|
||||
run=i
|
||||
if not clear and run is not None:
|
||||
if radius*abs(sweep)*(i-run)/steps>=0.25:
|
||||
arc=p.PCB_SHAPE(board)
|
||||
arc.SetShape(p.SHAPE_T_ARC)
|
||||
arc.SetArcGeometry(vec(at(run)),vec(at((run+i)/2)),vec(at(i)))
|
||||
arc.SetWidth(p.FromMM(WIDTH))
|
||||
add(arc)
|
||||
run=None
|
||||
continue
|
||||
for edge in outline_lines(source):
|
||||
# Display boundary is visually distinct from the PCB perimeter.
|
||||
dashed = ref=="DS1" and round(edge.bounds[0]-xy(footprints[ref].GetPosition())[0],2) in (1.0,25.5)
|
||||
if dashed:
|
||||
distance=0.0
|
||||
while distance < edge.length:
|
||||
line(edge.interpolate(distance).coords[0],edge.interpolate(min(distance+0.8,edge.length)).coords[0])
|
||||
distance+=1.3
|
||||
else:
|
||||
line(edge.coords[0],edge.coords[-1])
|
||||
# These are pad-row guides, not inferred socket/body or ESP32 PCB dimensions.
|
||||
fp=footprints["U1"]
|
||||
assert fp.GetOrientationDegrees()==0, "Review guide construction for rotated U1"
|
||||
pads={pad.GetNumber():xy(pad.GetPosition()) for pad in fp.Pads()}
|
||||
for first,last in (("1","22"),("23","44")):
|
||||
x,ytop=pads[first]; _,ybottom=pads[last]
|
||||
corners=[(x-1.2,ytop-1.2),(x+1.2,ytop-1.2),(x+1.2,ybottom+1.2),(x-1.2,ybottom+1.2)]
|
||||
for a,b in zip(corners,corners[1:]+corners[:1]):
|
||||
line(a,b)
|
||||
label=p.PCB_TEXT(board)
|
||||
label.SetText("HEADER GUIDES")
|
||||
a,b=pads["1"],pads["23"]
|
||||
label.SetPosition(vec(((a[0]+b[0])/2,a[1]+28)))
|
||||
label.SetTextSize(p.VECTOR2I(p.FromMM(0.85),p.FromMM(0.85)))
|
||||
label.SetTextThickness(p.FromMM(0.12))
|
||||
add(label)
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
lock=ROOT/"~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise SystemExit("Close PCB Editor before updating silkscreen")
|
||||
before=PCB.read_bytes()
|
||||
project=ROOT/"serial-carrier.kicad_pro"
|
||||
project_before=project.read_bytes()
|
||||
board=p.LoadBoard(str(PCB))
|
||||
count=add_outlines(board)
|
||||
temp=ROOT/"validation/pcb-routing-silk-tmp.kicad_pcb"
|
||||
try:
|
||||
assert p.SaveBoard(str(temp),board,True)
|
||||
if lock.exists() or PCB.read_bytes()!=before:
|
||||
raise RuntimeError("PCB changed or reopened; refusing to overwrite")
|
||||
assert project.read_bytes()==project_before,"Project settings changed"
|
||||
temp.replace(PCB)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
print(f"Added {count} board-local silkscreen graphics; project settings preserved.")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user