Integrate Measured ESP32 PCB Geometry
- Add measured F.Fab bounds and USB mouth markers - Add guarded preservation and regression validation - Refresh review exports and remove draft presentation labels
This commit is contained in:
@@ -2,13 +2,19 @@
|
||||
(version 20260206)
|
||||
(generator "pcbnew")
|
||||
(layer "F.Cu")
|
||||
(descr "HW-678, component/top view antenna up USB down; origin pad 1; left top-bottom 1..22, right top-bottom 23..44; only header centres verified; no GPIO mapping, outline, courtyard or antenna keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet")
|
||||
(descr "HW-678, component/top view antenna up USB down; origin pad 1; left top-bottom 1..22, right top-bottom 23..44; nominal header centres with user-measured main PCB/antenna bounds and USB shell mouth spans on F.Fab; approximate 1.365 mm top/left registration; no GPIO mapping, complete USB envelope, courtyard or RF keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet")
|
||||
(tags "module provisional THT")
|
||||
(attr through_hole)
|
||||
(property "Reference" "REF**" (at 12.7 -3) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(property "Value" "HW678_2x22_Provisional" (at 12.7 57) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_text user "PROVISIONAL CONNECTOR FIT" (at 12.7 25) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_text user "ANTENNA UP / USB DOWN" (at 12.7 28) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_rect (start -1.365 -1.365) (end 26.725 54.705) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
|
||||
(fp_rect (start 4.045 -8.255) (end 21.675 -1.365) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
|
||||
(fp_line (start 2.795 54.705) (end 11.665 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
|
||||
(fp_line (start 14.195 54.705) (end 23.065 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
|
||||
(fp_text user "ANTENNA BOUNDS - NOT RF KEEPOUT" (at 12.7 -9.5) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(fp_text user "USB SHELL MOUTHS ONLY" (at 12.7 59) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(pad "1" thru_hole rect (at 0.00 0.00) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(pad "2" thru_hole circle (at 0.00 2.54) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(pad "3" thru_hole circle (at 0.00 5.08) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
|
||||
+25
-6
@@ -26,8 +26,11 @@ the carrier. All 3.3 V and ground connections are explicit named nets.
|
||||
| `validation/pcb-draft.png`, `validation/pcb-draft-3d.png` | Copper/fab preview and carrier-only 3D render |
|
||||
| `validate_pcb_draft.py` | Fresh schematic-to-PCB net checks, bounds/layers and DRC/parity report |
|
||||
| `generate_pcb_draft.py`, `route_pcb_draft.py` | Optional destructive-regeneration tools with explicit overwrite guards |
|
||||
| `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 outlines and ESP32 header guides after placement/routing; preserves copper and project settings |
|
||||
| `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 and measured ESP32 outlines/USB mouth markers after placement/routing; preserves copper and project settings |
|
||||
| [db9-mechanical-notes.md](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation |
|
||||
| `update_u1_graphics.py`, `validate_u1_update.py` | Guarded measured-U1 graphics integration and exact pre-update preservation / geometry / silkscreen regressions |
|
||||
| `export_placement_pdf.py` | 1:1 PDF from saved board geometry; refuses stale embedded U1 graphics |
|
||||
| `remove_draft_markers.py` | Guarded presentation-label cleanup with exact-content preservation audit; does not qualify fabrication |
|
||||
| `add_db9_envelope.py` | Refresh nominal connector metal projections on Dwgs.User; not a case cutout or assembly clearance |
|
||||
| `Carrier.kicad_sym`, `sym-lib-table` | Project-local module, switch and resistor symbols |
|
||||
| `Carrier.pretty/`, `fp-lib-table` | OLED/ESP32/RS-232-reference provisional footprints; standard KiCad 10 switch/resistor/mounting-hole library references |
|
||||
@@ -217,9 +220,24 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
|
||||
rows fit a standard breadboard simultaneously without forcing. Use nominal
|
||||
2.54 mm pitch (53.34 mm over 21 intervals) and 25.40 mm row spacing based on
|
||||
that fit check, not the raw caliper measurements. Verify with a 1:1 print before fabrication.
|
||||
- ESP32 seller drawing states 28 mm width and annotates 63 mm length; its 20 mm
|
||||
arrow is not header row spacing. Antenna extent and header-to-outline offsets
|
||||
are not fully dimensioned.
|
||||
- ESP32 actual user measurements supersede the seller's 28 × 63 mm drawing:
|
||||
main PCB **56.07 × 28.09 mm**, excluding the antenna projection. Top/component
|
||||
view, antenna up and USB down: antenna projects **6.89 mm** above the main PCB,
|
||||
inset **5.41 mm left / 5.05 mm right**. Derived antenna width **17.63 mm**,
|
||||
overall length **62.96 mm**, antenna centre **0.18 mm right** of PCB centre.
|
||||
- Header end offsets were initially approximately 1.5 mm; user accepted **1.365 mm**
|
||||
top/bottom to preserve the breadboard-confirmed 53.34 mm span. Left-edge-to-left-row
|
||||
centre distance is also approximately **1.365 mm**. Preserve 25.40 mm row spacing:
|
||||
the resulting right margin is **1.325 mm**, not an independently measured offset.
|
||||
These modelling offsets are not a claim of micron-level measurement accuracy.
|
||||
- Both USB-C metal shells are **8.87 mm wide**, with opening faces flush with the
|
||||
main PCB bottom edge. Left shell's outside left edge is **4.16 mm** from the PCB
|
||||
left edge; right shell's outside right edge is **3.66 mm** from the PCB right edge.
|
||||
Shell depth/height and mating-plug clearance remain unmeasured.
|
||||
- The library and routed board's embedded U1 now record these bounds on F.Fab;
|
||||
USB mouths are width spans only, not complete connector outlines. Measured
|
||||
board-local silkscreen, 1:1 placement PDF and SVG/PNG/3D previews are refreshed. See
|
||||
`../../docs/devel/hardware/PCB/footprint-notes.md` for coordinates and limits.
|
||||
- OLED actual PCB outline measured by user: 26 × 26 mm, superseding the seller's
|
||||
28.7 × 27.8 mm dimensions. Seller's 4.1 mm height remains unverified.
|
||||
- OLED front view, connector at top: GND, VCC, SCL, SDA from left to right,
|
||||
@@ -258,8 +276,9 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
|
||||
1. OLED outline, mounting-hole placement, revised header position and display
|
||||
outline are user fit-verified. Select carrier header/socket pads and drills
|
||||
from the actual connector drawings.
|
||||
2. Before final module outlines/courtyards, establish their registration to header
|
||||
centres, connector overhang and display position from drawings or measurements.
|
||||
2. ESP32 planar body/antenna bounds and USB mouth positions are now recorded in
|
||||
the library and embedded board footprint. Verify a 1:1 fit; complete module heights, USB depth/height, mating-plug clearances and
|
||||
assembly courtyards. Measured antenna bounds do not establish an RF keepout.
|
||||
3. Verify power-path schematic/regulator capacity and OLED pull-up implementation.
|
||||
Photos alone do not establish those electrical properties.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import LineString, Point, box
|
||||
from shapely.ops import unary_union, polygonize
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
@@ -38,6 +39,7 @@ def outline_lines(shape):
|
||||
|
||||
|
||||
def add_outlines(board):
|
||||
verify(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:
|
||||
@@ -82,8 +84,11 @@ def add_outlines(board):
|
||||
board.Add(shape)
|
||||
count+=1
|
||||
|
||||
def line(a,b):
|
||||
clipped=LineString([a,b]).intersection(silk_area).difference(blocked)
|
||||
def line(a,b,width=WIDTH):
|
||||
extra = max(0, (width-WIDTH)/2)
|
||||
area = silk_area.buffer(-extra) if extra else silk_area
|
||||
obstacles = blocked.buffer(extra) if extra else blocked
|
||||
clipped=LineString([a,b]).intersection(area).difference(obstacles)
|
||||
pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped]
|
||||
for piece in pieces:
|
||||
if piece.geom_type!="LineString" or piece.length < 0.25:
|
||||
@@ -91,7 +96,7 @@ def add_outlines(board):
|
||||
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))
|
||||
shape.SetWidth(p.FromMM(width))
|
||||
add(shape)
|
||||
|
||||
for ref in ("DS1","U2"):
|
||||
@@ -137,22 +142,19 @@ def add_outlines(board):
|
||||
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)
|
||||
# Union removes the shared body/antenna edge duplication. Mouth spans replace
|
||||
# the coincident bottom-edge strokes, with their measured library line weight;
|
||||
# this marks width only, without inventing connector depth or an RF keepout.
|
||||
sources=[g for g in footprints["U1"].GraphicalItems()
|
||||
if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab]
|
||||
mouths=[g for g in sources if g.GetShape()==p.SHAPE_T_SEGMENT]
|
||||
mouth_lines=unary_union([edge for g in mouths for edge in outline_lines(g)])
|
||||
perimeter=unary_union([edge for g in sources if g.GetShape()==p.SHAPE_T_RECT
|
||||
for edge in outline_lines(g)]).difference(mouth_lines)
|
||||
for edge in perimeter.geoms:
|
||||
line(edge.coords[0],edge.coords[-1])
|
||||
for mouth in mouths:
|
||||
line(xy(mouth.GetStart()),xy(mouth.GetEnd()),p.ToMM(mouth.GetWidth()))
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export a 1:1 placement PDF with measured HW678 geometry, without editing CAD.
|
||||
|
||||
Export the saved board's verified measured U1 graphics. Only review captions
|
||||
change on a temporary plotting board; no library overlay hides stale CAD.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pcbnew as p
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
PDF = ROOT / "pcb-placement-draft.pdf"
|
||||
|
||||
|
||||
def export():
|
||||
original = PCB.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
for drawing in board.GetDrawings():
|
||||
if isinstance(drawing, p.PCB_TEXT):
|
||||
if drawing.GetText() == "Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending":
|
||||
drawing.SetText("Measured ESP32 outline / USB mouths; RF / plug / assembly clearance pending")
|
||||
board.GetTitleBlock().SetComment(0, "Measured planar outlines; verify RF / plug / assembly clearance")
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-placement-") as temporary:
|
||||
draft = Path(temporary) / PCB.name
|
||||
result = Path(temporary) / PDF.name
|
||||
if not p.SaveBoard(str(draft), board, True):
|
||||
raise RuntimeError("Cannot save temporary plotting board")
|
||||
subprocess.run([
|
||||
"kicad-cli", "pcb", "export", "pdf", "--mode-single",
|
||||
"--layers", "F.Fab,F.SilkS,Dwgs.User,Edge.Cuts",
|
||||
"--sketch-pads-on-fab-layers", "--black-and-white", "--scale", "1",
|
||||
"--output", str(result), str(draft),
|
||||
], check=True)
|
||||
if PCB.read_bytes() != original:
|
||||
raise RuntimeError("Source PCB changed during export; PDF not replaced")
|
||||
PDF.write_bytes(result.read_bytes())
|
||||
print(f"Exported {PDF}; scale 1:1; source PCB unchanged")
|
||||
print(f"Source PCB SHA256: {hashlib.sha256(original).hexdigest()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export()
|
||||
@@ -114,9 +114,9 @@ def main():
|
||||
settings.m_ViasMinSize=mm(0.7)
|
||||
# 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("A1 DRAFT")
|
||||
title.SetComment(0,"NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly")
|
||||
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier")
|
||||
title.SetRevision("A1")
|
||||
title.SetComment(0,"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"))
|
||||
@@ -194,7 +194,7 @@ def main():
|
||||
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,"Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending",41,-2.5,p.Dwgs_User,0.9)
|
||||
board.BuildConnectivity()
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove presentation-only PCB draft labels; retain all technical cautions."""
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
TITLE = "Serial Swiss Army Knife - 82x80 mm carrier"
|
||||
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
|
||||
SILK = "DRAFT - NOT FOR FAB"
|
||||
|
||||
|
||||
def remove_labels(board):
|
||||
title = board.GetTitleBlock()
|
||||
assert title.GetTitle() in (TITLE, TITLE + " draft")
|
||||
assert title.GetRevision() in ("A1", "A1 DRAFT")
|
||||
assert title.GetComment(0) in (COMMENT, "NOT FOR FABRICATION - " + COMMENT)
|
||||
title.SetTitle(TITLE)
|
||||
title.SetRevision("A1")
|
||||
title.SetComment(0, COMMENT)
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if isinstance(drawing, p.PCB_TEXT) and drawing.GetText() == SILK:
|
||||
assert drawing.GetLayer() == p.F_SilkS
|
||||
board.Remove(drawing)
|
||||
|
||||
|
||||
def expected_tree(tree):
|
||||
"""Exact permitted delta for preservation audits, independent of pcbnew edits."""
|
||||
import copy
|
||||
result = copy.deepcopy(tree)
|
||||
for item in list(result):
|
||||
if not isinstance(item, list):
|
||||
continue
|
||||
if item[0] == "title_block":
|
||||
for field in item[1:]:
|
||||
replacements = {
|
||||
('title', '"' + TITLE + ' draft"'): '"' + TITLE + '"',
|
||||
('rev', '"A1 DRAFT"'): '"A1"',
|
||||
('comment', '"NOT FOR FABRICATION - ' + COMMENT + '"'): '"' + COMMENT + '"',
|
||||
}
|
||||
replacement = replacements.get((field[0], field[-1]))
|
||||
if replacement is not None:
|
||||
field[-1] = replacement
|
||||
elif item[0] == "gr_text" and item[1] == '"' + SILK + '"':
|
||||
assert ['layer', '"F.SilkS"'] in item
|
||||
result.remove(item)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
from validate_u1_update import parse, canonical
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise RuntimeError("Close PCB Editor before changing labels")
|
||||
original = PCB.read_bytes()
|
||||
settings = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
|
||||
expected = expected_tree(parse(PCB))
|
||||
board = p.LoadBoard(str(PCB))
|
||||
remove_labels(board)
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-labels-", dir=ROOT) as temporary:
|
||||
result = Path(temporary) / PCB.name
|
||||
assert p.SaveBoard(str(result), board, True)
|
||||
assert canonical(parse(result)) == canonical(expected), "Change beyond exact label edits"
|
||||
assert canonical(expected_tree(parse(result))) == canonical(expected), "Label cleanup not idempotent"
|
||||
if lock.exists() or PCB.read_bytes() != original:
|
||||
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||
assert all(path.read_bytes() == data for path, data in settings.items()), "Project settings changed"
|
||||
result.replace(PCB)
|
||||
print("Removed draft presentation labels; exact preservation and idempotence passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -8,9 +8,9 @@
|
||||
)
|
||||
(paper "A4")
|
||||
(title_block
|
||||
(title "Serial Swiss Army Knife - 82x80 mm carrier draft")
|
||||
(rev "A1 DRAFT")
|
||||
(comment 1 "NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly")
|
||||
(title "Serial Swiss Army Knife - 82x80 mm carrier")
|
||||
(rev "A1")
|
||||
(comment 1 "USB / antenna geometry pending; verify module pinout / assembly")
|
||||
)
|
||||
(layers
|
||||
(0 "F.Cu" signal)
|
||||
@@ -562,6 +562,70 @@
|
||||
(sheetfile "serial-carrier.kicad_sch")
|
||||
(attr through_hole)
|
||||
(duplicate_pad_numbers_are_jumpers no)
|
||||
(fp_line
|
||||
(start 2.795 54.705)
|
||||
(end 11.665 54.705)
|
||||
(stroke
|
||||
(width 0.3)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "6434f4c9-cb7e-5b60-902d-bd13b5c57f13")
|
||||
)
|
||||
(fp_line
|
||||
(start 14.195 54.705)
|
||||
(end 23.065 54.705)
|
||||
(stroke
|
||||
(width 0.3)
|
||||
(type solid)
|
||||
)
|
||||
(layer "F.Fab")
|
||||
(uuid "a4082d84-0a5f-5edd-b97f-afbb68b73336")
|
||||
)
|
||||
(fp_rect
|
||||
(start -1.365 -1.365)
|
||||
(end 26.725 54.705)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(fill no)
|
||||
(layer "F.Fab")
|
||||
(uuid "53b4b62c-705f-584c-9987-8e20cbcb0eae")
|
||||
)
|
||||
(fp_rect
|
||||
(start 4.045 -8.255)
|
||||
(end 21.675 -1.365)
|
||||
(stroke
|
||||
(width 0.1)
|
||||
(type solid)
|
||||
)
|
||||
(fill no)
|
||||
(layer "F.Fab")
|
||||
(uuid "1a629168-d420-520b-820e-a4de5828228b")
|
||||
)
|
||||
(fp_text user "ANTENNA BOUNDS - NOT RF KEEPOUT"
|
||||
(at 12.7 -9.5 0)
|
||||
(layer "F.Fab")
|
||||
(uuid "acabe363-725d-5486-8c68-8e06b5758ceb")
|
||||
(effects
|
||||
(font
|
||||
(size 0.8 0.8)
|
||||
(thickness 0.12)
|
||||
)
|
||||
)
|
||||
)
|
||||
(fp_text user "USB SHELL MOUTHS ONLY"
|
||||
(at 12.7 59 0)
|
||||
(layer "F.Fab")
|
||||
(uuid "4af1d9b9-5ec1-5ef3-a72d-b72b93ecc391")
|
||||
(effects
|
||||
(font
|
||||
(size 0.8 0.8)
|
||||
(thickness 0.12)
|
||||
)
|
||||
)
|
||||
)
|
||||
(pad "1" thru_hole rect
|
||||
(at 0 0)
|
||||
(size 1.7 1.7)
|
||||
@@ -3025,8 +3089,8 @@
|
||||
(embedded_fonts no)
|
||||
)
|
||||
(gr_line
|
||||
(start 106.8 121.8)
|
||||
(end 109.2 121.8)
|
||||
(start 106.635 121.635)
|
||||
(end 112.045 121.635)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
@@ -3035,78 +3099,8 @@
|
||||
(uuid "cdd870c2-6daf-560d-a619-6903566202a1")
|
||||
)
|
||||
(gr_line
|
||||
(start 106.8 177.54)
|
||||
(end 106.8 121.8)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "1576658b-4eb9-5105-9817-e374c6ef271e")
|
||||
)
|
||||
(gr_line
|
||||
(start 109.2 121.8)
|
||||
(end 109.2 177.54)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "8f746121-d8e6-5a89-9328-da46f2da8ac0")
|
||||
)
|
||||
(gr_line
|
||||
(start 109.2 177.54)
|
||||
(end 106.8 177.54)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "bfbd0b33-54e8-590f-87da-4935db08a121")
|
||||
)
|
||||
(gr_line
|
||||
(start 132.2 121.8)
|
||||
(end 134.6 121.8)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "fef8b02b-8a5c-5e27-8e75-fc116ddbd526")
|
||||
)
|
||||
(gr_line
|
||||
(start 132.2 136.509273)
|
||||
(end 132.2 121.8)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "abb27fb8-ae0c-56d1-9e35-c66b92681547")
|
||||
)
|
||||
(gr_line
|
||||
(start 132.2 162.671905)
|
||||
(end 132.2 137.390726)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "03740f02-7c63-574b-ad1d-eeaa2c77d2ea")
|
||||
)
|
||||
(gr_line
|
||||
(start 132.2 170.202783)
|
||||
(end 132.2 163.828094)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "2cecac83-165e-5e49-a695-c0b09f3b631c")
|
||||
)
|
||||
(gr_line
|
||||
(start 132.2 177.54)
|
||||
(end 132.2 171.397216)
|
||||
(start 106.635 177.705)
|
||||
(end 106.635 121.635)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
@@ -3115,48 +3109,48 @@
|
||||
(uuid "5473bcda-98ee-5a28-9fda-f27d137d534c")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.6 121.8)
|
||||
(end 134.6 130.421905)
|
||||
(start 110.795 177.705)
|
||||
(end 106.635 177.705)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "0445e79c-43e4-5609-90ad-67bffdf64341")
|
||||
(uuid "2441022c-c903-5a57-a84c-86f0a5143cb5")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.6 131.578094)
|
||||
(end 134.6 140.571905)
|
||||
(start 110.795 177.705)
|
||||
(end 119.665 177.705)
|
||||
(stroke
|
||||
(width 0.3)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "abb27fb8-ae0c-56d1-9e35-c66b92681547")
|
||||
)
|
||||
(gr_line
|
||||
(start 112.045 121.635)
|
||||
(end 112.045 119.28)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "6405d812-d377-52d9-8013-8c19bd4cdc2b")
|
||||
(uuid "03740f02-7c63-574b-ad1d-eeaa2c77d2ea")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.6 141.728094)
|
||||
(end 134.6 160.952783)
|
||||
(start 112.045 121.635)
|
||||
(end 129.675 121.635)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "f938fe4f-5ff2-577d-9bff-2827fb32ce22")
|
||||
(uuid "8f746121-d8e6-5a89-9328-da46f2da8ac0")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.6 162.147216)
|
||||
(end 134.6 169.559273)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "d472d222-21c6-5a78-ab7f-18799bceab8a")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.6 170.440726)
|
||||
(end 134.6 177.54)
|
||||
(start 122.195 177.705)
|
||||
(end 119.665 177.705)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
@@ -3165,14 +3159,94 @@
|
||||
(uuid "bfa82c3d-e8e2-58ea-8b46-f0db41e84458")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.6 177.54)
|
||||
(end 132.2 177.54)
|
||||
(start 122.195 177.705)
|
||||
(end 131.065 177.705)
|
||||
(stroke
|
||||
(width 0.3)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "2b9b8e99-a473-51bc-8241-448388f63fcb")
|
||||
)
|
||||
(gr_line
|
||||
(start 129.675 119.28)
|
||||
(end 129.675 121.635)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "2441022c-c903-5a57-a84c-86f0a5143cb5")
|
||||
(uuid "2cecac83-165e-5e49-a695-c0b09f3b631c")
|
||||
)
|
||||
(gr_line
|
||||
(start 129.675 121.635)
|
||||
(end 134.725 121.635)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "bfbd0b33-54e8-590f-87da-4935db08a121")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.725 121.635)
|
||||
(end 134.725 130.382575)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "1576658b-4eb9-5105-9817-e374c6ef271e")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.725 131.617424)
|
||||
(end 134.725 140.532575)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "fef8b02b-8a5c-5e27-8e75-fc116ddbd526")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.725 141.767424)
|
||||
(end 134.725 160.924667)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "0445e79c-43e4-5609-90ad-67bffdf64341")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.725 162.175332)
|
||||
(end 134.725 169.460337)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "6405d812-d377-52d9-8013-8c19bd4cdc2b")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.725 170.539662)
|
||||
(end 134.725 177.705)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "f938fe4f-5ff2-577d-9bff-2827fb32ce22")
|
||||
)
|
||||
(gr_line
|
||||
(start 134.725 177.705)
|
||||
(end 131.065 177.705)
|
||||
(stroke
|
||||
(width 0.12)
|
||||
(type default)
|
||||
)
|
||||
(layer "F.SilkS")
|
||||
(uuid "d472d222-21c6-5a78-ab7f-18799bceab8a")
|
||||
)
|
||||
(gr_line
|
||||
(start 141.095 100.28)
|
||||
@@ -4050,17 +4124,6 @@
|
||||
(layer "Edge.Cuts")
|
||||
(uuid "17ad1955-d7db-47eb-a736-baff736aac39")
|
||||
)
|
||||
(gr_text "HEADER GUIDES"
|
||||
(at 120.7 151 0)
|
||||
(layer "F.SilkS")
|
||||
(uuid "2b9b8e99-a473-51bc-8241-448388f63fcb")
|
||||
(effects
|
||||
(font
|
||||
(size 0.85 0.85)
|
||||
(thickness 0.12)
|
||||
)
|
||||
)
|
||||
)
|
||||
(gr_text "BACK"
|
||||
(at 146.25 175 0)
|
||||
(layer "F.SilkS")
|
||||
@@ -4072,17 +4135,6 @@
|
||||
)
|
||||
)
|
||||
)
|
||||
(gr_text "DRAFT - NOT FOR FAB"
|
||||
(at 156 178 0)
|
||||
(layer "F.SilkS")
|
||||
(uuid "875ba70f-7139-4924-a69c-d2e171f3f526")
|
||||
(effects
|
||||
(font
|
||||
(size 0.9 0.9)
|
||||
(thickness 0.15)
|
||||
)
|
||||
)
|
||||
)
|
||||
(gr_text "SELECT"
|
||||
(at 158.25 175 0)
|
||||
(layer "F.SilkS")
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Synchronize measured U1 F.Fab graphics only; never replace the footprint."""
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
NS = uuid.UUID("36693251-e1f8-44e9-a8a9-e201bd658ceb")
|
||||
|
||||
|
||||
def library_graphics():
|
||||
fp = p.FootprintLoad(str(ROOT / "Carrier.pretty"), "HW678_2x22_Provisional")
|
||||
if fp is None:
|
||||
raise RuntimeError("Cannot load measured HW678 footprint")
|
||||
return fp
|
||||
|
||||
|
||||
def signature(g, origin):
|
||||
def point(v):
|
||||
return v.x-origin.x, v.y-origin.y
|
||||
if isinstance(g, p.PCB_SHAPE):
|
||||
return ("shape", g.GetShape(), point(g.GetStart()), point(g.GetEnd()), g.GetWidth())
|
||||
return ("text", g.GetText(), point(g.GetPosition()),
|
||||
(g.GetTextSize().x, g.GetTextSize().y), g.GetTextThickness())
|
||||
|
||||
|
||||
def measured_items(fp):
|
||||
# Generic library captions would overlap existing board labels.
|
||||
return sorted((g for g in fp.GraphicalItems() if g.GetLayer() == p.F_Fab
|
||||
and not (isinstance(g, p.PCB_TEXT) and g.GetText() in (
|
||||
"PROVISIONAL CONNECTOR FIT", "ANTENNA UP / USB DOWN"))),
|
||||
key=lambda g: signature(g, fp.GetPosition()))
|
||||
|
||||
|
||||
def verify(board):
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
|
||||
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
|
||||
assert len([g for g in u1.GraphicalItems() if g.GetLayer() == p.F_Fab]) == 6
|
||||
library = library_graphics()
|
||||
expected = [signature(g, library.GetPosition()) for g in measured_items(library)]
|
||||
actual = [signature(g, u1.GetPosition()) for g in measured_items(u1)]
|
||||
assert actual == expected, "Embedded U1 F.Fab differs from measured library; run update_u1_graphics.py"
|
||||
|
||||
|
||||
def main():
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise SystemExit("Close PCB Editor before updating U1 graphics")
|
||||
before = PCB.read_bytes()
|
||||
project = ROOT / "serial-carrier.kicad_pro"
|
||||
project_before = project.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
|
||||
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
|
||||
library = library_graphics()
|
||||
sources = measured_items(library)
|
||||
assert len(sources) == 6, "Review changed measured library before integration"
|
||||
owned = {str(uuid.uuid5(NS, str(i))) for i in range(len(sources))}
|
||||
existing = list(u1.GraphicalItems())
|
||||
assert all(g.m_Uuid.AsString() in owned for g in existing if g.GetLayer() == p.F_Fab), "Unowned U1 fabrication graphics; review manually"
|
||||
if [signature(g, u1.GetPosition()) for g in measured_items(u1)] == [
|
||||
signature(g, library.GetPosition()) for g in sources]:
|
||||
verify(board)
|
||||
print("Embedded measured U1 graphics already current; no board write")
|
||||
return
|
||||
for g in existing:
|
||||
if g.m_Uuid.AsString() in owned:
|
||||
u1.Remove(g)
|
||||
for i, source in enumerate(sources):
|
||||
graphic = source.Duplicate()
|
||||
graphic.SetParent(u1)
|
||||
graphic.SetUuid(p.KIID(str(uuid.uuid5(NS, str(i)))))
|
||||
graphic.Move(u1.GetPosition() - library.GetPosition())
|
||||
u1.Add(graphic)
|
||||
verify(board)
|
||||
temp = ROOT / "validation/pcb-u1-tmp.kicad_pcb"
|
||||
try:
|
||||
assert p.SaveBoard(str(temp), board, True)
|
||||
verify(p.LoadBoard(str(temp)))
|
||||
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("Embedded measured U1 graphics; existing footprint fields, pads and routing retained")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -117,9 +117,23 @@ class FootprintRegression(unittest.TestCase):
|
||||
for row, x in enumerate((0, 25.4)) for index in range(22)
|
||||
}
|
||||
self.check_header(footprint, positions)
|
||||
self.assertFalse(any(isinstance(g, k.PCB_SHAPE)
|
||||
for g in footprint.GraphicalItems()),
|
||||
"HW678 must not acquire guessed outlines or envelopes")
|
||||
shapes = [g for g in footprint.GraphicalItems() if isinstance(g, k.PCB_SHAPE)]
|
||||
expected = [
|
||||
(k.SHAPE_T_RECT, mm(-1.365, -1.365), mm(26.725, 54.705), k.FromMM(0.1)),
|
||||
(k.SHAPE_T_RECT, mm(4.045, -8.255), mm(21.675, -1.365), k.FromMM(0.1)),
|
||||
(k.SHAPE_T_SEGMENT, mm(2.795, 54.705), mm(11.665, 54.705), k.FromMM(0.3)),
|
||||
(k.SHAPE_T_SEGMENT, mm(14.195, 54.705), mm(23.065, 54.705), k.FromMM(0.3)),
|
||||
]
|
||||
self.assertEqual(Counter((g.GetShape(), xy(g.GetStart()), xy(g.GetEnd()),
|
||||
g.GetWidth()) for g in shapes), Counter(expected))
|
||||
for shape in shapes:
|
||||
self.assertEqual(shape.GetLayer(), k.F_Fab)
|
||||
self.assertFalse(shape.IsAnyFill())
|
||||
# Only measured planar bounds/mouth spans: no invented shell depth or RF area.
|
||||
self.assertAlmostEqual(26.725 - (-1.365), 28.09)
|
||||
self.assertAlmostEqual(54.705 - (-1.365), 56.07)
|
||||
self.assertAlmostEqual(54.705 - (-8.255), 62.96)
|
||||
self.assertAlmostEqual(26.725 - 25.4, 1.325)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
import pcbnew as p
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
PCB=ROOT/"serial-carrier.kicad_pcb"
|
||||
@@ -21,6 +22,7 @@ def run(*args):
|
||||
run("kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(OUT/"pcb-source-netlist.xml"),str(ROOT/"serial-carrier.kicad_sch"))
|
||||
run("kicad-cli","pcb","drc","--format","json","--all-track-errors","--severity-all","--schematic-parity","-o",str(OUT/"pcb-parity-drc.json"),str(PCB))
|
||||
board=p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
source=ET.parse(OUT/"pcb-source-netlist.xml").getroot()
|
||||
comps={c.get("ref"):c for c in source.findall("components/comp")}
|
||||
footprints={f.GetReference():f for f in board.GetFootprints()}
|
||||
@@ -121,7 +123,7 @@ summary={
|
||||
"known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
|
||||
"project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(),
|
||||
"ignored_DRC_checks":report["ignored_checks"],
|
||||
"not_validated":["male module pin map/header fit", "ESP32 geometry and complete assembly envelope/heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"],
|
||||
"not_validated":["male module pin map/header fit", "ESP32 measured-planar 1:1 fit and complete assembly envelope/heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"],
|
||||
}
|
||||
(OUT/"pcb-validation-summary.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print("PCB draft verified: 82 x 80 mm; 2 copper layers; 0 physical DRC violations; 0 unrouted.")
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measured U1 integration regression and exact pre-update preservation audit."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import Polygon
|
||||
from add_device_silkscreen import add_outlines, NS as SILK_NS, xy, outline_lines
|
||||
from update_u1_graphics import verify, NS as U1_NS
|
||||
from remove_draft_markers import expected_tree
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
BASE = ROOT / "validation/pcb-before-u1-measured.kicad_pcb"
|
||||
|
||||
|
||||
def parse(path):
|
||||
tokens = iter(re.findall(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+', path.read_text()))
|
||||
def group():
|
||||
result = []
|
||||
for token in tokens:
|
||||
if token == ')':
|
||||
return result
|
||||
result.append(group() if token == '(' else token)
|
||||
raise ValueError("Unterminated S-expression")
|
||||
assert next(tokens) == '('
|
||||
return group()
|
||||
|
||||
|
||||
def ident(item):
|
||||
return next((v[1].strip('"') for v in item if isinstance(v,list) and v[0]=='uuid'), None)
|
||||
|
||||
|
||||
def canonical(item, excluded=frozenset()):
|
||||
# KiCad can reorder independent board items when saving; compare their full
|
||||
# parsed contents, not byte order. Pad coordinates/netnames/UUIDs remain exact.
|
||||
if not isinstance(item, list):
|
||||
return item
|
||||
values = [canonical(v, excluded) for v in item
|
||||
if not isinstance(v,list) or ident(v) not in excluded]
|
||||
if item and item[0] in ("kicad_pcb", "footprint"):
|
||||
return ([v for v in values if not isinstance(v,list)] +
|
||||
sorted((v for v in values if isinstance(v,list)), key=repr))
|
||||
# Coordinates, polygon vertex order, text and other ordered values must
|
||||
# remain ordered: sorting these could conceal an X/Y swap or zone edit.
|
||||
return values
|
||||
|
||||
|
||||
def main():
|
||||
assert hashlib.sha256(BASE.read_bytes()).hexdigest() == "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045", "Pre-update snapshot changed"
|
||||
assert hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest() == "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516", "Project settings changed"
|
||||
before = PCB.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
silk_ids = {str(uuid.uuid5(SILK_NS,str(i))) for i in range(4096)}
|
||||
u1_ids = {str(uuid.uuid5(U1_NS,str(i))) for i in range(6)}
|
||||
# The original baseline retains historical draft labels. Permit only the
|
||||
# exact requested title/revision/comment edits and removal of the silk label.
|
||||
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(PCB), silk_ids | u1_ids), "Change outside owned U1 additions / device silk / exact presentation labels"
|
||||
generated = [g for g in board.GetDrawings() if g.m_Uuid.AsString() in silk_ids]
|
||||
assert not any(isinstance(g,p.PCB_TEXT) and g.GetText()=="HEADER GUIDES" for g in board.GetDrawings())
|
||||
carrier = Polygon([(136,100),(182,100),(182,180),(100,180),(100,119),(136,119)])
|
||||
mouths = []
|
||||
for g in generated:
|
||||
assert g.GetLayer() == p.F_SilkS
|
||||
for edge in outline_lines(g):
|
||||
assert carrier.covers(edge.buffer(p.ToMM(g.GetWidth())/2)), "Silk outside carrier/notch"
|
||||
if g.GetWidth() == p.FromMM(0.3):
|
||||
mouths.append(g)
|
||||
assert len(mouths) == 2
|
||||
assert sorted((xy(g.GetStart()),xy(g.GetEnd())) for g in mouths) == [
|
||||
((110.795,177.705),(119.665,177.705)),
|
||||
((122.195,177.705),(131.065,177.705))]
|
||||
# A second refresh must reproduce all drawing geometry and UUIDs.
|
||||
add_outlines(board)
|
||||
with tempfile.TemporaryDirectory(prefix="u1-regression-") as temporary:
|
||||
result = Path(temporary)/PCB.name
|
||||
assert p.SaveBoard(str(result),board,True)
|
||||
assert canonical(parse(result)) == canonical(parse(PCB)), "Silkscreen refresh not idempotent"
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference()=="U1")
|
||||
shape = next(g for g in u1.GraphicalItems() if isinstance(g,p.PCB_SHAPE))
|
||||
shape.SetWidth(shape.GetWidth()+1)
|
||||
try:
|
||||
verify(board)
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Stale embedded geometry was not rejected")
|
||||
assert PCB.read_bytes() == before
|
||||
assert not (ROOT/"~serial-carrier.kicad_pcb.lck").exists()
|
||||
summary = dict(measured_library_matches_embedded_u1=True,
|
||||
existing_uuids_pads_nets_placement_routing_settings_and_unrelated_graphics_preserved=True,
|
||||
presentation_label_cleanup=True,
|
||||
silk_refresh_idempotent=True, stale_geometry_negative_control=True,
|
||||
silk_clipped_to_carrier=True, usb_mouth_markers=2,
|
||||
generated_silk_items=len(generated),
|
||||
baseline_sha256=hashlib.sha256(BASE.read_bytes()).hexdigest(),
|
||||
board_sha256=hashlib.sha256(before).hexdigest(),
|
||||
project_sha256=hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest())
|
||||
(ROOT/"validation/pcb-u1-update.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print(json.dumps(summary,indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 231 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 345 KiB |
+10804
-670
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 368 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schemas.kicad.org/drc.v1.json",
|
||||
"coordinate_units": "mm",
|
||||
"date": "2026-09-21T11:39:10",
|
||||
"date": "2026-09-21T22:52:26",
|
||||
"ignored_checks": [
|
||||
{
|
||||
"description": "Footprint has no courtyard defined",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"immutable_geometry_nets_pads_linkage_preserved": true,
|
||||
"board_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
|
||||
"board_sha256": "94e941625b3b9fc9a08b9f2d532eb35003ea8da41d456a1c67269b9651200e05",
|
||||
"independent_min_different_net_or_npth_clearance_mm": 0.2705441169852849,
|
||||
"closest_items": [
|
||||
"U1.38:4b8eabef-8493-437d-bd02-cb9ff4afd7cd",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schemas.kicad.org/drc.v1.json",
|
||||
"coordinate_units": "mm",
|
||||
"date": "2026-09-21T11:39:13",
|
||||
"date": "2026-09-21T22:33:53",
|
||||
"ignored_checks": [
|
||||
{
|
||||
"description": "Footprint has no courtyard defined",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<export version="E">
|
||||
<design>
|
||||
<source>/home/mscholz/Repos/ESP32_serial_swiss_army_knife/hardware/PCB/serial-carrier.kicad_sch</source>
|
||||
<date>2026-09-21T11:39:08</date>
|
||||
<date>2026-09-21T22:52:19</date>
|
||||
<tool>Eeschema 10.0.6-1.fc44</tool>
|
||||
<sheet number="1" name="/" tstamps="/">
|
||||
<title_block>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"measured_library_matches_embedded_u1": true,
|
||||
"existing_uuids_pads_nets_placement_routing_settings_and_unrelated_graphics_preserved": true,
|
||||
"presentation_label_cleanup": true,
|
||||
"silk_refresh_idempotent": true,
|
||||
"stale_geometry_negative_control": true,
|
||||
"silk_clipped_to_carrier": true,
|
||||
"usb_mouth_markers": 2,
|
||||
"generated_silk_items": 91,
|
||||
"baseline_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
|
||||
"board_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
|
||||
"project_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516"
|
||||
}
|
||||
@@ -130,7 +130,7 @@
|
||||
],
|
||||
"not_validated": [
|
||||
"male module pin map/header fit",
|
||||
"ESP32 geometry and complete assembly envelope/heights",
|
||||
"ESP32 measured-planar 1:1 fit and complete assembly envelope/heights",
|
||||
"header/socket fit",
|
||||
"RF performance and antenna clearance",
|
||||
"power/return-path integrity",
|
||||
|
||||
Reference in New Issue
Block a user