Record Accepted PCB Placement And Fit

Align component groups, repair affected routing, and add preservation
audits.
Assign the confirmed U2 footprint and update pull-ups to 2 kΩ across
design files.
Document user acceptance, switch to an A3 worksheet, and refresh PCB
exports.
This commit is contained in:
2026-09-22 10:28:08 +02:00
parent 901afbe754
commit c257158ada
42 changed files with 29500 additions and 17205 deletions
+58 -43
View File
@@ -10,11 +10,26 @@ See **[pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md)** f
mechanical assumptions and validation. A review drawing is `pcb-placement-draft.pdf`.
The schematic contains the complete ESP32 development-board interface, MAX3243
breakout logic interface, I²C OLED, three Omron B3F-1000 buttons and three 2.2 kΩ
breakout logic interface, I²C OLED, three Omron B3F-1000 buttons and three 2 kΩ
button pull-ups. USB connectors, regulator, reset/boot circuit, DE-9 and MAX3243
charge pump are already on their respective modules and are not duplicated on
the carrier. All 3.3 V and ground connections are explicit named nets.
## Current user acceptance and placement
See [user-acceptance.md](../../docs/devel/hardware/PCB/user-acceptance.md) for the exact group placements, nominal body clearances and evidence limits. Common body centreline is carrier-local X=56.970 mm; resistor bodies are centred in the RS-232/OLED gap with 2.0815 mm nominal clearance to each PCB. U1, U2, outline, carrier holes and USB placement are unchanged. R1–R3 are purchased 2 kΩ 1% metal-film parts; Omron B3F-1000 switches are also purchased.
The user accepts visual fit/USB clearance, male module mapping, M3 carrier/daughterboard and M2 display hardware, current RF reserve and power/ground routing. Intended/reported assembled module elevation is 10 mm, not an independently verified sum of the 8.5 mm female and described 2.5 mm male contributions. USB nonisolation is accepted; backfeed safety, regulator margin and RF qualification are not established. OLED backside pull-ups are confirmed by user, but their values/rail are unmeasured. Earlier pending-status language below is historical where superseded by this record.
PCB presentation uses **A3 landscape (420 × 297 mm)** instead of A4; the
carrier stays at absolute X=100…182, Y=100…180 mm. No origin, footprint,
routing or zone was moved. The 1:1 placement PDF includes the worksheet/title
block, clear of the PCB; `validation/pcb-placement-page.png` previews the full
page. Print at actual size on A3 for the 1:1 fit check, not “fit to A4”.
`update_presentation.py` removes the accepted USB/geometry/pinout/assembly
pending labels with lock/concurrent-edit guards and an exact-content audit.
RF qualification and connector/case-cutout cautions remain.
## Files and use
| File | Purpose |
@@ -76,12 +91,11 @@ modify footprints. The generated KiCad files are the editable design artifacts.
matching board markings. This is photo/label evidence, not a continuity test:
verify actual pin-to-function mapping before release, especially on clones.
- DS1 pins 1–4 are GND, VCC, SCL, SDA, front/display view with header at top.
- U2 pin numbering follows published Adafruit **5988 female** CAD as a provisional
reference for the selected **6253 male** module. Its footprint is deliberately
**unassigned in the schematic** until the actual male header and mechanics are
verified. The PCB draft assigns `Carrier:MAX3243_Reference_Provisional` on the
PCB only, causing one documented footprint-parity warning. Its reference nets
are routed for review, not male-board qualification. See [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md)
- U2 pin numbering follows published Adafruit **5988 female** CAD for the selected
**6253 male** module. The user confirmed the same pinout, header mapping and
footprint as the reference. Both schematic and PCB now assign
`Carrier:MAX3243_Reference_Provisional`; clean footprint parity is required.
The identifier is unchanged; this assignment is not hardware or fabrication qualification. See [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md)
and [pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md); this is not a generic DE-9 footprint.
- No-connect crosses mean unused on the carrier, not disconnected internally.
Native USB GPIO19/20, UART0 GPIO43/44, memory-conflicting GPIO35–37 and the 5 V
@@ -92,8 +106,8 @@ modify footprints. The generated KiCad files are the editable design artifacts.
bidirectional pad; firmware **must use open-drain mode** for OFF. ERC does not
enforce that configuration or model the breakout's internal OFF pull-up.
- Selected buttons are Omron B3F-1000 with their verified KiCad THT footprint.
Added R1–R3, **2.2 kΩ, 1%, at least 0.125 W**, from each button input to 3.3 V.
They provide nominal 1.5 mA contact current / 4.95 mW dissipation while pressed,
Added R1–R3, **2 kΩ, 1%, at least 0.125 W**, from each button input to 3.3 V.
They provide nominal 1.65 mA contact current / 5.445 mW dissipation while pressed,
rather than relying on the internal pull-up for the silver-contact rated load.
Firmware's internal pull-ups and debouncing remain enabled; no firmware changes
are required. Final supply tolerance/load budget still needs checking.
@@ -118,9 +132,9 @@ the user. This does **not** validate connector drills, male RS-232 geometry,
electrical operation, power budget, procurement or fabrication.
The routed PCB draft has **0 physical DRC violations and 0 unconnected items**;
a fresh netlist-to-PCB check passes. One deliberate U2 footprint-parity warning
remains (PCB reference footprint versus blank schematic assignment). No added
DRC exclusions hide this warning. Missing courtyards are ignored by default DRC
a fresh netlist-to-PCB check passes with **zero schematic-parity issues**,
including the user-confirmed U2 footprint assignment. No DRC exclusions were
added. Missing courtyards are ignored by default DRC
and remain a manual release check. Run `/usr/bin/python3 -B
hardware/PCB/validate_pcb_draft.py` from the repository root; see
[pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md) for the complete verification scope and remaining gates.
@@ -133,10 +147,10 @@ Repository sources: `../../docs/wiring.md`, `../../src/board_pins.h`,
| Assembly | Repository identification | Footprint status |
|---|---|---|
| ESP32 development board | User confirmed HW-678 V0.0.0 markings and S3-N16R8 shield, two USB-C ports | Two 22-pin rows; nominal 2.54 mm pitch and 25.40 mm row spacing supported by simultaneous breadboard fit. Mechanical outline registration and socket/drill selection remain pending. |
| RS-232 module | Adafruit MAX3243 full-pinout breakout; male is product 6253, female is 5988 | Eagle pin map/PCB geometry and STEP metal dimensions extracted. User confirms identical male/female external dimensions; actual fit and male electrical map remain release checks. Reference footprint used on PCB only. |
| ESP32 development board | User confirmed HW-678 V0.0.0 markings and S3-N16R8 shield, two USB-C ports | Two 22-pin rows; nominal 2.54 mm pitch and 25.40 mm row spacing supported by simultaneous breadboard fit. Measured planar outline and visual fit accepted by user; finished-hole process remains unqualified. |
| RS-232 module | Adafruit MAX3243 full-pinout breakout; male is product 6253, female is 5988 | Eagle pin map/PCB geometry and STEP metal dimensions extracted. User confirms identical male pinout/header/footprint, visual fit and pin-by-pin review. Reference footprint assigned in schematic and PCB. |
| Display | 128×64 yellow/blue I²C OLED, address 0x3c; Amazon ASIN B0F13SSSFB | 26 mm square board, 22.50 mm hole spacing, Ø2 mm holes and centred header 1.50 mm from top edge are user-fit-verified. Local footprint assigned; header pad/drill selection remains provisional. |
| Three buttons | Omron B3F-1000; previous, select, next; normally open to ground, active low | Datasheet and `Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x` pad mapping verified. External 2.2 kΩ pull-ups included. Live distributor stock remains unverified. |
| Three buttons | Omron B3F-1000; previous, select, next; normally open to ground, active low | Datasheet and `Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x` pad mapping verified. Purchased per user; external purchased 2 kΩ 1% metal-film pull-ups included. |
Adafruit manufacturer CAD starting point:
https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/downloads
@@ -147,8 +161,9 @@ The original files and CC BY-SA 3.0 notices are retained in `reference/`; they
remain under their upstream license. Omron reference documents retain their
manufacturer notices. The CAD-parts repository's STEP and MIT license are separately
retained under `reference/db9-cad/`. External metal geometry is used for the male
module on the user's explicit equivalence confirmation; this does not validate
its electrical map, connector threads, tolerances, assembly fit or case cutout.
module on the user's explicit equivalence confirmation. The user separately
confirmed electrical mapping and visual fit; connector threads, tolerances and
final case cutout are not independently qualified.
## Verified logical connections for the existing firmware profile
@@ -181,11 +196,11 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
- Keep UART0 / GPIO43 and GPIO44 available through the onboard USB-UART bridge.
- Reserve GPIO19 and GPIO20 for onboard native USB.
- Existing firmware profile powers RS-232 Vin and OLED VCC from 3.3 V, not 5 V.
- Confirm the exact development board's regulator capacity and USB power-path
behavior before finalizing the power design, especially with both USB ports attached.
- Display pull-up presence, resistance and termination rail must be checked on
the actual module. Do not select external pull-up values without this information.
- Firmware enables internal button pull-ups; this carrier additionally uses 2.2 kΩ
- User reports successful 3.3 V module operation and accepts nonisolated USB power.
Regulator margin and USB backfeed safety remain unverified, especially with two powered hosts.
- User confirms backside OLED I²C pull-ups and operation without extras.
Resistance and termination rail are unmeasured; no external values are invented.
- Firmware enables internal button pull-ups; this carrier additionally uses 2 kΩ
external pull-ups for the selected buttons' contact-current requirement.
- RS-232 OFF uses the breakout's onboard pull-up; GPIO9 releases it to enable.
- The breakout is not galvanically isolated; signal grounds are common.
@@ -198,17 +213,18 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
- OLED purchase: https://www.amazon.de/dp/B0F13SSSFB — I²C, not SPI.
- Both Amazon URLs returned an access-check page on inspection; the ASINs alone
do not establish a verified manufacturer drawing or footprint.
- Use the male Adafruit breakout. User reports male/female board dimensions match;
verify the selected male board against its manufacturer CAD.
- Use the male Adafruit breakout. User confirms dimensions, pinout/header/footprint
equivalence and completed a pin-by-pin module connection review.
- Carrier for complete modules: soldered module headers for final assembly,
sockets for beta testing without a case. Header pitch, row spacing, drill size
and socket clearance must be validated against selected hardware.
- Power only through the ESP32 development board's USB connectors, typically a
powerbank or the serial peer's USB port, with common ground. Do not introduce
another carrier power input or assume simultaneous USB supplies are isolated.
- Select readily available THT buttons; the existing random switches need not be reused.
- Case will be 3D-printed after PCB finalization. Include mounting provisions in
the layout, but screw size, hole diameter and insert bosses are not yet selected.
- User purchased Omron B3F-1000 THT buttons and 2 kΩ 1% metal-film resistors.
- Case will be 3D-printed after PCB finalization. User approves M3 carrier holes,
10 mm brass standoffs into heat inserts, M3 daughterboard screws and M2 display
hardware. Exact insert/boss dimensions and screw lengths are not supplied.
## User-supplied mechanical evidence
@@ -233,7 +249,7 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
- 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.
User accepts USB placement and mating-plug clearance visually; shell depth/height 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
@@ -271,20 +287,19 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
Crosshairs denote header centres, not specified pad or drill diameters. This
template is not a KiCad footprint or a fabrication file.
## Remaining mechanical/electrical checks
## Current acceptance checklist and evidence limits
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. 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.
- [x] Component dimensions and visual physical fit accepted by user, including USB placement/clearance and RS-232 prong clearance.
- [x] Male Adafruit pinout/header/footprint equivalence and pin-by-pin module connection review confirmed by user; existing U2 schematic assignment retained with clean parity.
- [x] M3 carrier holes, 10 mm brass standoffs into heat inserts, M3 daughterboard screws and M2 display hardware approved.
- [x] Omron B3F-1000 and 2 kΩ 1% metal-film resistors purchased per user.
- [x] Existing 3.3 V supply operation and OLED onboard pull-ups reported; no extra I²C pull-ups needed in reported operation.
- [x] Nonisolated USB power, existing RF reserve and power/ground routing accepted by user.
- [ ] Independently verified regulator margin, USB backfeed safety, OLED pull-up resistance/rail and manufacturer RF/EMC qualification are not available.
- [ ] Detailed fastener lengths/insert dimensions, final case cutout, tolerance/height envelopes and fabrication-process qualification remain outside this acceptance.
The native schematic, local libraries and routed two-layer PCB draft are present.
Before fabrication, finish the above checks, qualify the male RS-232 module map
and footprint, and confirm procurement. The carrier now proposes an 82 × 80 mm
stepped outline and four Ø3.2 mm M3 mounting holes; case hardware is not yet fixed.
The PCB's RF reserve is a draft design allocation, not a verified module antenna
keepout. Module/USB/DE-9 clearances must still be established from actual hardware.
The intended/reported 10 mm assembled elevation is not independently measured;
8.5 + 2.5 = 11 mm is not evidence of actual mated height. User acceptance closes
the corresponding review gates, not these factual evidence limits. Missing
module courtyards still limit automated assembly checks. No new hardware tests,
firmware changes, Gerbers, order or certification are implied.
+8 -7
View File
@@ -1,8 +1,9 @@
References,Quantity,Description,Manufacturer,MPN or identification,Footprint,Status
U1,1,ESP32-S3 N16R8 development board,User supplied,HW-678 V0.0.0 / ASIN B0F3XMYYQY,Carrier:HW678_2x22_Provisional,Header pitch/spacing confirmed; connector drill fit and module envelope pending
U2,1,MAX3243 full-pinout RS-232 breakout male,Adafruit,6253,Carrier:MAX3243_Reference_Provisional,PCB-only reference footprint; schematic assignment blank; male header/geometry verification required
DS1,1,128x64 yellow-blue I2C OLED 26mm PCB,User supplied,ASIN B0F13SSSFB,Carrier:OLED_26mm_I2C_Provisional,Module geometry user-fit-verified; carrier header drill fit pending
SW1 SW2 SW3,3,Normally-open 6mm THT tactile switch,Omron,B3F-1000,Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x,Datasheet/footprint checked; stock unverified
R1 R2 R3,3,2.2k ohm 1 percent axial resistor at least 0.125W,TBD,TBD,Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,Design value selected; exact MPN and body/lead fit pending
H1 H2 H3 H4,4,Proposed M3 case mounting locations with 3.2mm NPTH,TBD,TBD,MountingHole:MountingHole_3.2mm_M3,Board-only hole locations; screws and insert/standoff dimensions not selected
Assembly hardware,TBD,Module headers and optional beta-test sockets plus mounting hardware,TBD,TBD,,Not yet selected; not included in electrical component count
U1,1,ESP32-S3 N16R8 development board,User supplied,HW-678 V0.0.0 / ASIN B0F3XMYYQY,Carrier:HW678_2x22_Provisional,Dimensions and visual fit including USB placement/clearance user accepted
U2,1,MAX3243 full-pinout RS-232 breakout male,Adafruit,6253,Carrier:MAX3243_Reference_Provisional,Assigned in schematic and PCB; male pinout/header/footprint equivalence and pin-by-pin review user confirmed
DS1,1,128x64 yellow-blue I2C OLED 26mm PCB,User supplied,ASIN B0F13SSSFB,Carrier:OLED_26mm_I2C_Provisional,Module geometry and visual fit user confirmed; onboard I2C pull-ups present but values/rail unmeasured
SW1 SW2 SW3,3,Normally-open 6mm THT tactile switch,Omron,B3F-1000,Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x,User reports purchased; datasheet/footprint mapping checked
R1 R2 R3,3,2k ohm 1 percent metal-film axial resistor,TBD,User purchased; exact MPN and wattage unspecified,Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,Visual dimensions accepted; retained nominal 6.3x2.5mm body and 7.62mm pitch; design rating at least 0.125W
H1 H2 H3 H4,4,M3 carrier mounting locations with 3.2mm NPTH,User selected,M3 screws and 10mm brass standoffs into heat inserts,MountingHole:MountingHole_3.2mm_M3,User approved; board-only holes excluded from electrical component count
Module mounting hardware,TBD,M3 daughterboard screws and M2 OLED hardware,User selected,Exact insert and screw lengths unspecified,,User approved; case wall and detailed fastener specifications not defined
Module headers,TBD,8.5mm female headers and standard male headers,User selected,Male described as adding 2.5mm,,User intended/reported assembled elevation 10mm; not independently measured; arithmetic sum 11mm is not verified mating height; RS232 prongs clear per user
1 References Quantity Description Manufacturer MPN or identification Footprint Status
2 U1 1 ESP32-S3 N16R8 development board User supplied HW-678 V0.0.0 / ASIN B0F3XMYYQY Carrier:HW678_2x22_Provisional Header pitch/spacing confirmed; connector drill fit and module envelope pending Dimensions and visual fit including USB placement/clearance user accepted
3 U2 1 MAX3243 full-pinout RS-232 breakout male Adafruit 6253 Carrier:MAX3243_Reference_Provisional PCB-only reference footprint; schematic assignment blank; male header/geometry verification required Assigned in schematic and PCB; male pinout/header/footprint equivalence and pin-by-pin review user confirmed
4 DS1 1 128x64 yellow-blue I2C OLED 26mm PCB User supplied ASIN B0F13SSSFB Carrier:OLED_26mm_I2C_Provisional Module geometry user-fit-verified; carrier header drill fit pending Module geometry and visual fit user confirmed; onboard I2C pull-ups present but values/rail unmeasured
5 SW1 SW2 SW3 3 Normally-open 6mm THT tactile switch Omron B3F-1000 Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x Datasheet/footprint checked; stock unverified User reports purchased; datasheet/footprint mapping checked
6 R1 R2 R3 3 2.2k ohm 1 percent axial resistor at least 0.125W 2k ohm 1 percent metal-film axial resistor TBD TBD User purchased; exact MPN and wattage unspecified Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal Design value selected; exact MPN and body/lead fit pending Visual dimensions accepted; retained nominal 6.3x2.5mm body and 7.62mm pitch; design rating at least 0.125W
7 H1 H2 H3 H4 4 Proposed M3 case mounting locations with 3.2mm NPTH M3 carrier mounting locations with 3.2mm NPTH TBD User selected TBD M3 screws and 10mm brass standoffs into heat inserts MountingHole:MountingHole_3.2mm_M3 Board-only hole locations; screws and insert/standoff dimensions not selected User approved; board-only holes excluded from electrical component count
8 Assembly hardware Module mounting hardware TBD Module headers and optional beta-test sockets plus mounting hardware M3 daughterboard screws and M2 OLED hardware TBD User selected TBD Exact insert and screw lengths unspecified Not yet selected; not included in electrical component count User approved; case wall and detailed fastener specifications not defined
9 Module headers TBD 8.5mm female headers and standard male headers User selected Male described as adding 2.5mm User intended/reported assembled elevation 10mm; not independently measured; arithmetic sum 11mm is not verified mating height; RS232 prongs clear per user
+6 -8
View File
@@ -1,8 +1,8 @@
#!/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.
Export the saved board's verified measured U1 graphics and A3 worksheet.
No library overlay or temporary caption hides stale CAD.
"""
from pathlib import Path
import hashlib
@@ -21,18 +21,16 @@ 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")
from update_presentation import expected_tree
from validate_u1_update import parse, canonical
assert canonical(expected_tree(parse(PCB))) == canonical(parse(PCB)), "Update PCB presentation first"
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",
"kicad-cli", "pcb", "export", "pdf", "--mode-single", "--include-border-title",
"--layers", "F.Fab,F.SilkS,Dwgs.User,Edge.Cuts",
"--sketch-pads-on-fab-layers", "--black-and-white", "--scale", "1",
"--output", str(result), str(draft),
+17 -14
View File
@@ -22,13 +22,13 @@ OUTLINE = [(36,0), (82,0), (82,80), (0,80), (0,19), (36,19)]
PLACEMENT = {
"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),
"R3": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 65, 34),
"SW1": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 43, 68),
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 55, 68),
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 67, 68),
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43.97, 36),
"R1": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 42.16, 32.6685),
"R2": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 53.16, 32.6685),
"R3": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 64.16, 32.6685),
"SW1": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 41.72, 68),
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 53.72, 68),
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 65.72, 68),
}
MOUNTS = [(3.5,22.5), (3.5,75), (78,4), (78,76)]
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
@@ -116,7 +116,7 @@ def main():
title=board.GetTitleBlock()
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier")
title.SetRevision("A1")
title.SetComment(0,"USB / antenna geometry pending; verify module pinout / assembly")
title.SetComment(0,"")
pad_nets={}
for source_net in source.findall("nets/net"):
net=p.NETINFO_ITEM(board,source_net.get("name"))
@@ -125,8 +125,7 @@ def main():
pad_nets[node.get("ref"),node.get("pin")]=(net,node)
for ref,(lib_id,x,y) in PLACEMENT.items():
comp=components[ref]
if ref != "U2":
assert comp.findtext("footprint")==lib_id,(ref,"Schematic footprint changed")
assert comp.findtext("footprint")==lib_id,(ref,"Schematic footprint changed")
fp=load(lib_id)
fp.SetReference(ref)
fp.SetValue(comp.findtext("value"))
@@ -145,7 +144,7 @@ def main():
elif ref=="U2":
fp.Reference().SetPosition(xy(56.97,14.670))
elif ref=="DS1":
fp.Reference().SetPosition(xy(56,49))
fp.Reference().SetPosition(xy(56.97,49))
elif ref.startswith("SW"):
fp.Reference().SetPosition(xy(x+3.25,y-2))
else:
@@ -188,7 +187,7 @@ def main():
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)
@@ -196,11 +195,15 @@ def main():
text(board,label,x,75, 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)
from update_presentation import CAPTION
text(board,CAPTION,41,-2.5,p.Dwgs_User,0.9)
board.BuildConnectivity()
assert p.SaveBoard(str(PCB),board,True)
# Separate process avoids repeated-load SWIG lifetime issues in pcbnew.
import sys
subprocess.run([sys.executable, "-B", str(ROOT/"update_presentation.py")], check=True)
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
print("Placement draft only. U2 nominal male external metal dimensions user-confirmed; pin map / assembly still require qualification.")
print("Placement draft only. User-confirmed fit and pin map; RF/electrical/fabrication qualification not established.")
if __name__=="__main__":
main()
+10 -8
View File
@@ -111,6 +111,8 @@ def note(text,x,y,size=1.27):
def place(name,ref,value,x,y,nets,extra=None):
pins,hw,hh,_,footprint,description = SPECS[name]
if ref == "U2":
footprint = "Carrier:MAX3243_Reference_Provisional"
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():
@@ -140,12 +142,12 @@ for number,name,*_ in MCU:
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":"../../docs/devel/hardware/PCB/adafruit-research.md; reference female 5988 CAD"})
place("MAX3243_Module_Reference","U2","Adafruit MAX3243 / male 6253",228.6,96.52,rs_nets,{"Status":"User confirmed male pinout/header/footprint and pin-by-pin connections","Datasheet_source":"../../docs/devel/hardware/PCB/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"})
place("R",f"R{index}","2k 1%",327.66,y,{"1":"+3V3","2":net},{"Rating":"Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"})
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":"User reports purchased"})
note(function,302.26,y+21.59,1.016)
note("MODULE CARRIER - INITIAL SCHEMATIC",15.24,15.24,2.54)
@@ -153,14 +155,14 @@ note("Native KiCad 10 schematic. Named wire labels join matching nets on this sh
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("Male 6253 selected. Numbers from published 5988 female CAD.\nMale mapping user-reviewed; reference 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("External 2k 1% pull-ups: ~1.65 mA 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("Power at 3.3 V only. Onboard I2C pull-ups user-confirmed;\nresistance and rail unmeasured; no external pull-ups.",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.\nUSB nonisolated accepted; backfeed safety not verified.\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)
note("USER ACCEPTANCE / EVIDENCE LIMITS\nModule mapping and visual fit accepted by user.\nB3F-1000 and 2k 1% metal-film resistors purchased.\nM3 carrier/module hardware; M2 display hardware.\nRF/power routing accepted, not qualified.\nRegulator margin and USB backfeed safety unverified.\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')
Binary file not shown.
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/python3
"""One-shot body-centred placement with incremental copper repair, not regeneration.
Writes a candidate only. Review its DRC before promoting it to the native board.
Requires the retained pre-alignment snapshot and no editor locks.
"""
import hashlib
import json
import time
import subprocess
from pathlib import Path
import pcbnew as p
from shapely.geometry import Point, LineString, box
import route_pcb_draft as r
ROOT = Path(__file__).resolve().parent
BASE = ROOT/'validation/pcb-before-group-alignment.kicad_pcb'
MOVES = {'DS1': ((143,136),(143.97,136)),
**{f'R{i+1}': ((143+11*i,134),(142.16+11*i,132.6685)) for i in range(3)},
**{f'SW{i+1}': ((143+12*i,168),(141.72+12*i,168)) for i in range(3)}}
def unlocked():
assert not list(ROOT.glob('*.lck')), 'KiCad editor lock exists'
assert not list(ROOT.glob('.~lock.*')), 'Document editor lock exists'
def geom(item):
if isinstance(item,p.PAD):
if item.GetShape()==p.PAD_SHAPE_CIRCLE:
return Point(r.pos(item)).buffer(p.ToMM(item.GetSize().x)/2,quad_segs=32)
bb=item.GetBoundingBox()
return box(p.ToMM(bb.GetX()),p.ToMM(bb.GetY()),p.ToMM(bb.GetRight()),p.ToMM(bb.GetBottom()))
if isinstance(item,p.PCB_VIA):
return Point(r.pos(item)).buffer(.35,quad_segs=32)
return LineString([xy(item.GetStart()),xy(item.GetEnd())]).buffer(p.ToMM(item.GetWidth())/2,quad_segs=32)
def xy(v):
return (p.ToMM(v.x),p.ToMM(v.y))
def layers(item):
return set(r.LAYERS) if isinstance(item,(p.PAD,p.PCB_VIA)) else {item.GetLayer()}
def groups(board, net):
items=[a for f in board.GetFootprints() for a in f.Pads() if a.GetNetCode()==net]
items += [t for t in board.GetTracks() if t.GetNetCode()==net]
shapes=[geom(i) for i in items]
parents=list(range(len(items)))
def root(i):
while parents[i]!=i:
i=parents[i]
return i
for i,a in enumerate(items):
for j in range(i):
if layers(a)&layers(items[j]) and shapes[i].distance(shapes[j])<1e-6:
parents[root(i)]=root(j)
result={}
for i,a in enumerate(items):
result.setdefault(root(i),[]).append(a)
return list(result.values())
def anchors(group):
result=[]
for a in group:
points=[r.pos(a)] if isinstance(a,(p.PAD,p.PCB_VIA)) else [xy(a.GetStart()),xy(a.GetEnd())]
result.extend((point,layers(a)) for point in points)
return result
def main():
unlocked()
before=r.PCB.read_bytes()
assert before==BASE.read_bytes(), 'Input differs from preserved baseline'
protected={name:(ROOT/name).read_bytes() for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl','serial-carrier.kicad_sch')}
board=p.LoadBoard(str(r.PCB))
fps={f.GetReference():f for f in board.GetFootprints()}
for ref,(old,new) in MOVES.items():
assert r.pos(fps[ref])==old
fps[ref].SetPosition(r.point(new))
if ref.startswith('R'):
fps[ref].SetValue('2k 1%')
pads=[a for f in board.GetFootprints() for a in f.Pads()]
removed=[]
# Only remove copper made illegal by a moved pad/hole. Everything else is
# initially retained, including all unrelated nets and manual routing.
moved_pads=[a for ref in MOVES for a in fps[ref].Pads()]
for t in list(board.GetTracks()):
if any((not a.GetNetCode() or a.GetNetCode()!=t.GetNetCode()) and
geom(t).distance(geom(a))<.25-1e-6 for a in moved_pads):
removed.append(t.m_Uuid.AsString())
board.Delete(t)
repaired=[]
deadline=time.monotonic()+600
for ref,(old,new) in MOVES.items():
for pad in fps[ref].Pads():
if not pad.GetNumber(): continue
target=r.pos(pad)
start=(round(target[0]+old[0]-new[0],6),round(target[1]+old[1]-new[1],6))
if geom(pad).covers(Point(start)): continue
attached=[t for t in board.GetTracks() if t.GetNetCode()==pad.GetNetCode() and start in (xy(t.GetStart()),xy(t.GetEnd()))]
if not attached: continue
width=.5 if pad.GetNetname() in ('/+3V3','/GND') else .25
blocked,via=r.masks(board,pads,pad.GetNetCode(),width)
allowed=set.union(*(layers(t) for t in attached))
for z,layer in enumerate(r.LAYERS):
if layer not in allowed: blocked[r.node(start,z)]=True
path,_=r.astar(blocked,via,start,target,deadline,500000)
assert path, ('Cannot extend moved pad',ref,pad.GetNumber())
r.add_path(board,path,start,target,pad.GetNetCode(),width)
repaired.append(dict(net=pad.GetNetname(),start=start,end=target))
nets=sorted({a.GetNetCode() for a in pads if a.GetNumber() and a.GetNetCode()}, key=lambda n: (board.FindNet(n).GetNetname() in ('/+3V3','/GND'), not board.FindNet(n).GetNetname().startswith('/OLED'), n))
for net in nets:
while len(parts:=groups(board,net))>1:
options=[]
for i,aa in enumerate(parts):
for bb in parts[i+1:]:
for a,la in anchors(aa):
for b,lb in anchors(bb):
options.append(((a[0]-b[0])**2+(a[1]-b[1])**2,a,la,b,lb))
options.sort(key=lambda v:v[0])
name=parts[0][0].GetNetname()
width=.5 if name in ('/+3V3','/GND') else .25
blocked,via=r.masks(board,pads,net,width)
success=False
for _,a,la,b,lb in options[:400]:
mask=blocked.copy()
for z,layer in enumerate(r.LAYERS):
if layer not in la: mask[r.node(a,z)]=True
if layer not in lb: mask[r.node(b,z)]=True
path,_=r.astar(mask,via,a,b,deadline,250000)
if path:
r.add_path(board,path,a,b,net,width)
repaired.append(dict(net=name,start=a,end=b))
print(repaired[-1],flush=True)
success=True
break
assert time.monotonic()<deadline, 'Bounded routing deadline reached'
assert success, ('Cannot reconnect retained copper',name)
unlocked()
assert r.PCB.read_bytes()==before
assert all((ROOT/n).read_bytes()==data for n,data in protected.items())
out=ROOT/'validation/pcb-group-candidate.kicad_pcb'
# Let KiCad identify true dangling stubs after repair. Do not prune valid
# retained routes before reconnecting the moved pads.
from add_device_silkscreen import add_outlines
drc=ROOT/'validation/pcb-group-candidate-drc.json'
for attempt in range(20):
add_outlines(board)
assert p.SaveBoard(str(out),board,True)
subprocess.run(['kicad-cli','pcb','drc','--format','json','--all-track-errors','--severity-all','-o',str(drc),str(out)],check=True,timeout=60)
results=json.loads(drc.read_text())
ids={i['uuid'] for v in results['violations'] if v['type']=='track_dangling' for i in v['items']}
if not ids:
assert not results['unconnected_items'], results['unconnected_items']
assert all(v['type']=='lib_footprint_issues' for v in results['violations']), results['violations']
break
for t in list(board.GetTracks()):
if t.m_Uuid.AsString() in ids:
removed.append(t.m_Uuid.AsString())
board.Delete(t)
else:
raise AssertionError('Dangling-stub cleanup did not converge')
report=dict(baseline_sha256=hashlib.sha256(before).hexdigest(),moves_absolute_mm=MOVES,
removed_track_uuids=removed,repaired_connections=repaired,
retained_tracks=sum(t.m_Uuid.AsString() not in removed for t in p.LoadBoard(str(BASE)).GetTracks()),
protected_sha256={n:hashlib.sha256(data).hexdigest() for n,data in protected.items()})
(ROOT/'validation/pcb-group-migration.json').write_text(json.dumps(report,indent=2)+'\n')
if __name__=='__main__':
main()
+3 -2
View File
@@ -15,10 +15,11 @@ 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)
assert title.GetComment(0) in ("", COMMENT, "NOT FOR FABRICATION - " + COMMENT)
title.SetTitle(TITLE)
title.SetRevision("A1")
title.SetComment(0, COMMENT)
if title.GetComment(0):
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
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -234,9 +234,9 @@
(symbol (lib_id "Carrier:MAX3243_Module_Reference") (at 228.6 96.52 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "1127195e-e393-5d1d-9119-b25d10556144")
(property "Reference" "U2" (at 228.6 73.66 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "Adafruit MAX3243 / male 6253" (at 228.6 76.2 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Footprint" "Carrier:MAX3243_Reference_Provisional" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Status" "VERIFY male header numbering; footprint intentionally unassigned" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Status" "User confirmed male pinout/header/footprint and pin-by-pin connections" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet_source" "adafruit-research.md; reference female 5988 CAD" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "14d1ea7b-f53a-592f-9305-545890b94099"))
(pin "2" (uuid "b2027da1-66f1-5c08-b29b-b63e30a160ff"))
@@ -296,10 +296,10 @@
(label "OLED_SDA" (at 200.66 156.21 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "dc86b9ed-3904-50d0-8423-c02004518d88"))
(symbol (lib_id "Carrier:R") (at 327.66 88.9 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "defb78eb-632f-5f9d-a59d-dffbb1d31dec")
(property "Reference" "R1" (at 327.66 82.804 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "2.2k 1%" (at 327.66 85.344 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "2k 1%" (at 327.66 85.344 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Rating" "At least 0.125 W; exact resistor MPN pending" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Rating" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "79a88116-1a52-5fcb-855e-da54defa9400"))
(pin "2" (uuid "b56a5773-68f5-5c6d-936a-8289c11714de"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R1") (unit 1)))))
@@ -315,7 +315,7 @@
(property "Manufacturer" "Omron" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "MPN" "B3F-1000" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Function" "Previous / back" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Procurement" "Live stock not verified" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Procurement" "User reports purchased" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "0e490c78-3450-5a5b-b7d3-d55c32de988b"))
(pin "2" (uuid "6bbc82d1-8cfb-5fd5-832c-8df2775a89c9"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW1") (unit 1)))))
@@ -326,10 +326,10 @@
(text "Previous / back" (at 302.26 110.49 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "d84df288-be52-5de4-899d-bf29ba78f4f2"))
(symbol (lib_id "Carrier:R") (at 327.66 124.46 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "405055c0-35ab-5334-ad4d-30d45c5ef6f2")
(property "Reference" "R2" (at 327.66 118.364 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "2.2k 1%" (at 327.66 120.904 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "2k 1%" (at 327.66 120.904 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Rating" "At least 0.125 W; exact resistor MPN pending" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Rating" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "c23dbef2-d61d-56fa-a158-bb9912ef48ac"))
(pin "2" (uuid "f60eb205-60d1-5d8f-ac84-ca04f66e7106"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R2") (unit 1)))))
@@ -345,7 +345,7 @@
(property "Manufacturer" "Omron" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "MPN" "B3F-1000" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Function" "Select / confirm" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Procurement" "Live stock not verified" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Procurement" "User reports purchased" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "19bafbc6-666f-5e53-a4ef-265057c6dc8a"))
(pin "2" (uuid "9e3f47ba-92ac-5503-9d87-6b5590a70ccb"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW2") (unit 1)))))
@@ -356,10 +356,10 @@
(text "Select / confirm" (at 302.26 146.05 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "c28f2639-f392-552b-858e-b031aabc8c0a"))
(symbol (lib_id "Carrier:R") (at 327.66 160.02 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "78f9a74f-5d91-5687-b50e-68176333df23")
(property "Reference" "R3" (at 327.66 153.924 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "2.2k 1%" (at 327.66 156.464 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "2k 1%" (at 327.66 156.464 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Rating" "At least 0.125 W; exact resistor MPN pending" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Rating" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "d871ee2e-bc82-5f23-a436-7b7ab74b73a9"))
(pin "2" (uuid "96528b84-ba49-5f38-8445-b6ca2cadc93b"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R3") (unit 1)))))
@@ -375,7 +375,7 @@
(property "Manufacturer" "Omron" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "MPN" "B3F-1000" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Function" "Next" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Procurement" "Live stock not verified" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Procurement" "User reports purchased" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "85beb3b7-e16f-5450-ba94-5711245a7b25"))
(pin "2" (uuid "300c4196-eb91-5e58-a9aa-c84b8f7c4219"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW3") (unit 1)))))
@@ -389,12 +389,12 @@
(text "U1: complete development board" (at 53.34 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "320a8f4e-ff8b-5100-845b-dfe249b39a2e"))
(text "Antenna up / USB down: left row 1-22, right row 23-44.\nNC marks mean unused on carrier, not unused inside module." (at 53.34 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "649df1fa-48c0-580d-aeb8-d019ffd0b61d"))
(text "U2: complete RS-232 breakout" (at 177.8 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "5226007c-1904-547d-8ddb-008d14b83d51"))
(text "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." (at 177.8 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "9b62e7ca-695a-51af-bbbe-e7ee4ee45c08"))
(text "Male 6253 selected. Numbers from published 5988 female CAD.\nMale mapping user-reviewed; reference footprint assigned.\nDE-9 and charge pump are already on the module." (at 177.8 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "9b62e7ca-695a-51af-bbbe-e7ee4ee45c08"))
(text "Controls: active-low, firmware-debounced" (at 292.1 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "9d91985c-7375-5177-9a5e-8280e3bcaff6"))
(text "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." (at 292.1 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "b2dd72f5-86fc-5ce1-b09a-67a6fe9404d4"))
(text "External 2k 1% pull-ups: ~1.65 mA at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending." (at 292.1 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "b2dd72f5-86fc-5ce1-b09a-67a6fe9404d4"))
(text "DS1: front view GND / VCC / SCL / SDA" (at 177.8 128.27 0) (effects (font (size 1.524 1.524)) (justify left top)) (uuid "7b57748d-38ca-5712-97f9-bf6dab6dafaf"))
(text "Power at 3.3 V only. Verify on-module I2C pull-ups;\nexternal values intentionally not guessed." (at 177.8 165.1 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "fb23ec11-40f8-56c7-b78b-0da2d40e4b72"))
(text "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." (at 15.24 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "cadf6b77-e888-5f04-b4f6-3873114844ca"))
(text "Power at 3.3 V only. Onboard I2C pull-ups user-confirmed;\nresistance and rail unmeasured; no external pull-ups." (at 177.8 165.1 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "fb23ec11-40f8-56c7-b78b-0da2d40e4b72"))
(text "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.\nUSB nonisolated accepted; backfeed safety not verified.\nKeep both USB connectors and reset/boot buttons accessible." (at 15.24 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "cadf6b77-e888-5f04-b4f6-3873114844ca"))
(text "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." (at 152.4 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "0f7da388-3edb-5cc3-9937-13492397a221"))
(text "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." (at 292.1 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "1df2bd9e-fbff-5047-92a9-09f33d7cd7ac"))
(text "USER ACCEPTANCE / EVIDENCE LIMITS\nModule mapping and visual fit accepted by user.\nB3F-1000 and 2k 1% metal-film resistors purchased.\nM3 carrier/module hardware; M2 display hardware.\nRF/power routing accepted, not qualified.\nRegulator margin and USB backfeed safety unverified.\nERC is not hardware or fabrication approval." (at 292.1 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "1df2bd9e-fbff-5047-92a9-09f33d7cd7ac"))
)
Binary file not shown.
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Guarded accepted-fit label cleanup and A3 worksheet; never move board items."""
import copy
from pathlib import Path
import tempfile
import pcbnew as p
ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
USB = "USB ACCESS - VERIFY"
OLD_CAPTION = "Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending"
CAPTION = "Nominal RS-232 metal verified; RF qualification not established"
def expected_tree(tree):
result = copy.deepcopy(tree)
for item in list(result):
if not isinstance(item, list):
continue
if item[0] == "paper":
assert item in (["paper", '"A4"'], ["paper", '"A3"'])
item[1] = '"A3"'
elif item[0] == "title_block":
for field in list(item[1:]):
if field == ["comment", "1", '"' + COMMENT + '"']:
item.remove(field)
elif item[0] == "gr_text":
if item[1] == '"' + USB + '"':
assert ['layer', '"Dwgs.User"'] in item
result.remove(item)
elif item[1] == '"' + OLD_CAPTION + '"':
assert ['layer', '"Dwgs.User"'] in item
item[1] = '"' + CAPTION + '"'
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 presentation")
original = PCB.read_bytes()
protected = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
expected = expected_tree(parse(PCB))
with tempfile.TemporaryDirectory(prefix="carrier-presentation-", dir=ROOT) as temporary:
candidate = Path(temporary) / PCB.name
# PAGE_INFO is opaque in this pcbnew binding. Load the exact paper-only
# substitution, then use the native saver for all label changes.
text = original.decode()
assert text.count('(paper "A4")') + text.count('(paper "A3")') == 1
candidate.write_text(text.replace('(paper "A4")', '(paper "A3")'))
board = p.LoadBoard(str(candidate))
title = board.GetTitleBlock()
assert title.GetComment(0) in (COMMENT, "")
title.SetComment(0, "")
for drawing in list(board.GetDrawings()):
if isinstance(drawing, p.PCB_TEXT):
if drawing.GetText() == USB:
assert drawing.GetLayer() == p.Dwgs_User
board.Remove(drawing)
elif drawing.GetText() == OLD_CAPTION:
drawing.SetText(CAPTION)
assert p.SaveBoard(str(candidate), board, True)
assert canonical(parse(candidate)) == canonical(expected), "Unexpected physical/content change"
assert canonical(expected_tree(parse(candidate))) == canonical(expected), "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 protected.items()), "Settings changed"
candidate.replace(PCB)
print("A3 worksheet / accepted-fit labels updated; exact preservation and idempotence PASS")
if __name__ == "__main__":
main()
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/python3
"""Exact authorized placement/value delta, retained copper and body geometry audit."""
import copy
import hashlib
import json
import uuid
import pcbnew as p
from validate_u1_update import parse, canonical, ident
from add_device_silkscreen import NS
from refine_group_placement import ROOT, BASE, MOVES
METADATA = {
**{f'R{i}': ('Rating', 'Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified') for i in range(1,4)},
**{f'SW{i}': ('Procurement', 'User reports purchased') for i in range(1,4)},
'U2': ('Status', 'User confirmed male pinout/header/footprint and pin-by-pin connections'),
}
def expected_identity(board):
"""Apply only the seven authorized placements and three resistor values."""
fps={f.GetReference():f for f in board.GetFootprints()}
for ref,(old,new) in MOVES.items():
fps[ref].SetPosition(p.VECTOR2I(p.FromMM(new[0]),p.FromMM(new[1])))
if ref.startswith('R'): fps[ref].SetValue('2k 1%')
return board
def audit():
assert hashlib.sha256(BASE.read_bytes()).hexdigest()=='b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192'
old=parse(BASE)
new=parse(ROOT/'serial-carrier.kicad_pcb')
from update_presentation import expected_tree
wanted=expected_tree(old)
for f in wanted:
if not isinstance(f,list) or f[0]!='footprint': continue
ref=next(v[2].strip('"') for v in f if isinstance(v,list) and v[:2]==['property','"Reference"'])
if ref in METADATA:
key,value=METADATA[ref]
next(v for v in f if isinstance(v,list) and v[:2]==['property',json.dumps(key)])[2]=json.dumps(value)
if ref in MOVES:
at=next(v for v in f if isinstance(v,list) and v[0]=='at')
at[1:3]=[format(x,'.10g') for x in MOVES[ref][1]]
if ref.startswith('R'):
next(v for v in f if isinstance(v,list) and v[:2]==['property','"Value"'])[2]='"2k 1%"'
silk={str(uuid.uuid5(NS,str(i))) for i in range(4096)}
def without_copper(tree):
return [v for v in tree if not isinstance(v,list) or v[0] not in ('segment','via')]
assert canonical(without_copper(wanted),silk)==canonical(without_copper(new),silk), 'Unexpected non-copper edit'
before={ident(v):v for v in old if isinstance(v,list) and v[0] in ('segment','via')}
after={ident(v):v for v in new if isinstance(v,list) and v[0] in ('segment','via')}
migration=json.loads((ROOT/'validation/pcb-group-migration.json').read_text())
removed=set(before)-set(after)
assert removed==set(before)&set(migration['removed_track_uuids'])
assert all(before[k]==after[k] for k in before.keys()&after.keys()), 'Retained copper changed'
assert len(before.keys()&after.keys())==476
for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl'):
assert hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==migration['protected_sha256'][name]
board=p.LoadBoard(str(ROOT/'serial-carrier.kicad_pcb'))
fps={f.GetReference():f for f in board.GetFootprints()}
def xy(v): return p.ToMM(v.x),p.ToMM(v.y)
# Published U2 F.Fab outline includes the bare PCB beyond the header row.
u2pts=[xy(v) for g in fps['U2'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
axis=(min(x for x,y in u2pts)+max(x for x,y in u2pts))/2
bottom=max(y for x,y in u2pts)
oled=next(g for g in fps['DS1'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetStart()==fps['DS1'].GetPosition())
assert abs((xy(oled.GetStart())[0]+xy(oled.GetEnd())[0])/2-axis)<1e-6
resistor_boxes=[g for i in range(1,4) for g in fps[f'R{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetShape()==p.SHAPE_T_RECT]
assert len(resistor_boxes)==3
left=min(xy(g.GetStart())[0] for g in resistor_boxes)
right=max(xy(g.GetEnd())[0] for g in resistor_boxes)
assert abs((left+right)/2-axis)<1e-6
topgap=min(xy(g.GetStart())[1] for g in resistor_boxes)-bottom
bottomgap=xy(oled.GetStart())[1]-max(xy(g.GetEnd())[1] for g in resistor_boxes)
assert abs(topgap-2.0815)<1e-6 and abs(bottomgap-topgap)<1e-6
switchpts=[xy(v) for i in range(1,4) for g in fps[f'SW{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
assert abs((min(x for x,y in switchpts)+max(x for x,y in switchpts))/2-axis)<1e-6
switchgap=min(y for x,y in switchpts)-xy(oled.GetEnd())[1]
assert abs(switchgap-5.25)<1e-6
result=dict(common_axis_absolute_x_mm=axis,resistor_to_rs232_body_gap_mm=round(topgap,6),resistor_to_oled_body_gap_mm=round(bottomgap,6),oled_to_button_body_gap_mm=switchgap,
original_copper_items=len(before),retained_copper_items=476,removed_original_copper_items=len(removed),new_copper_items=len(after.keys()-before.keys()),
exact_unrelated_content_preserved=True,project_settings_preserved=True,baseline_sha256=migration['baseline_sha256'],board_sha256=hashlib.sha256((ROOT/'serial-carrier.kicad_pcb').read_bytes()).hexdigest())
(ROOT/'validation/pcb-group-alignment.json').write_text(json.dumps(result,indent=2)+'\n')
return result
if __name__=='__main__': print(json.dumps(audit(),indent=2))
+19 -11
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Read-only PCB checks and fresh KiCad reports; does not regenerate or save PCB.
Requires KiCad's pcbnew binding in system Python. The known U2 footprint parity
warning is reported explicitly, not hidden or added to KiCad's exclusions.
Requires KiCad's pcbnew binding in system Python. Schematic footprint parity
must be clean, including the user-confirmed U2 reference footprint assignment.
"""
import hashlib
import json
@@ -23,6 +23,13 @@ run("kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(OUT/"pcb
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)
from update_presentation import expected_tree, CAPTION
from validate_u1_update import parse, canonical
tree=parse(PCB)
assert ['paper', '"A3"'] in tree, "A3 worksheet required to clear the title block"
assert canonical(expected_tree(tree)) == canonical(tree), "Stale presentation labels"
assert board.GetTitleBlock().GetComment(0) == ""
assert any(isinstance(g,p.PCB_TEXT) and g.GetText()==CAPTION for g in board.GetDrawings())
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()}
@@ -38,9 +45,9 @@ expected_edges={frozenset((a,b)) for a,b in zip(expected_outline,expected_outlin
actual_edges={frozenset((round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5))
for v in (d.GetStart(),d.GetEnd())) for d in outline}
assert actual_edges==expected_edges
placements={"U1":(8,23),"U2":(43,26.670),"DS1":(43,36),
"R1":(43,34),"R2":(54,34),"R3":(65,34),
"SW1":(43,68),"SW2":(55,68),"SW3":(67,68),
placements={"U1":(8,23),"U2":(43,26.670),"DS1":(43.97,36),
"R1":(42.16,32.6685),"R2":(53.16,32.6685),"R3":(64.16,32.6685),
"SW1":(41.72,68),"SW2":(53.72,68),"SW3":(65.72,68),
"H1":(3.5,22.5),"H2":(3.5,75),"H3":(78,4),"H4":(78,76)}
for ref,(x,y) in placements.items():
position=footprints[ref].GetPosition()
@@ -72,7 +79,7 @@ for ref,comp in comps.items():
assert fp.GetPath().AsString().endswith("/"+comp.findtext("tstamps")),ref
wanted=comp.findtext("footprint")
if ref=="U2":
assert not wanted
assert wanted=="Carrier:MAX3243_Reference_Provisional"
assert fp.GetFPIDAsString()=="Carrier:MAX3243_Reference_Provisional"
else:
assert fp.GetFPIDAsString()==wanted,ref
@@ -102,8 +109,7 @@ report=json.loads((OUT/"pcb-parity-drc.json").read_text())
assert not report["violations"],report["violations"]
assert not report["unconnected_items"],report["unconnected_items"]
parity=report["schematic_parity"]
assert len(parity)==1 and parity[0]["type"]=="footprint_symbol_mismatch",parity
assert len(parity[0]["items"])==1 and parity[0]["items"][0]["uuid"]==footprints["U2"].m_Uuid.AsString()
assert not parity,parity
summary={
"carrier_bounds_mm":[82,80],
"outline_local_mm":expected_outline,
@@ -120,12 +126,14 @@ summary={
"physical_DRC_violations":0,
"unconnected_items":0,
"schematic_pad_nets_match":True,
"known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
"schematic_parity_violations":0,
"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 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"],
"user_accepted":["module dimensions/visual fit and USB clearance", "male pinout/header/footprint and pin-by-pin review", "M3 carrier/module and M2 display hardware", "RF keepout and power/ground routing"],
"not_independently_verified":["reported 10 mm assembled module elevation", "regulator margin", "USB backfeed safety", "OLED onboard pull-up resistance/rail", "manufacturer RF qualification", "fabrication process/certification"],
"new_hardware_tests":False,
}
(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.")
print("All schematic pad nets match. One expected U2 footprint parity warning remains visible.")
print("All schematic pad nets and footprint assignments match. Zero schematic parity violations.")
print("Not fabrication-ready; see ../../docs/devel/hardware/PCB/pcb-draft-notes.md and ignored checks in the DRC report.")
+6 -5
View File
@@ -57,7 +57,7 @@ for i, net in enumerate(("BTN_PREVIOUS", "BTN_SELECT", "BTN_NEXT"), 1):
assert len(nets[net]) == 3
assert (f"SW{i}", "1") in members[net] and (f"R{i}", "2") in members[net]
assert components[f"SW{i}"].findtext("value") == "B3F-1000"
assert components[f"R{i}"].findtext("value") == "2.2k 1%"
assert components[f"R{i}"].findtext("value") == "2k 1%"
assert members["+3V3"] == sorted([("U1","1"),("U1","2"),("U2","1"),("DS1","2"),("R1","1"),("R2","1"),("R3","1")])
assert members["GND"] == sorted([("U1","22"),("U1","23"),("U1","43"),("U1","44"),("U2","2"),("DS1","1"),("SW1","2"),("SW2","2"),("SW3","2")])
active = {name for name in nets if not name.startswith("unconnected-")}
@@ -70,8 +70,8 @@ assert sorted(int(node.get("pin")) for nodes in nets.values() for node in nodes
# USB/UART0, 5 V and PSRAM-conflicting pins must never be carrier nets.
for name in active:
assert not any(node.get("ref") == "U1" and node.get("pin") in ("21","24","25","33","34","35","41","42") for node in nets[name])
# U2's physical male footprint must stay explicitly unresolved in this draft.
assert not components["U2"].findtext("footprint")
# User confirmed the male module shares the reference pinout/header/footprint.
assert components["U2"].findtext("footprint") == "Carrier:MAX3243_Reference_Provisional"
expected = json.loads((OUT / "expected-nets.json").read_text())
assert set(expected) == active
for net, nodes in expected.items():
@@ -87,8 +87,9 @@ summary = "\n".join([
"15 firmware GPIO definitions independently checked against exported pin functions.",
"All 44 MCU header pins accounted for; USB/UART0/5V/PSRAM pins unused on carrier.",
"PDF and SVG exported successfully.",
"Not validated: male-module physical mapping, power budget/path, OLED pull-ups,",
"selected connector fit, PCB DRC/layout, procurement, or assembled hardware.",
"User accepted module mapping/fit and reports purchased switches/resistors.",
"No new hardware tests; regulator margin, USB backfeed safety, RF performance",
"and fabrication qualification are not established by this schematic check.",
]) + "\n"
(OUT / "summary.txt").write_text(summary)
print(summary)
+7 -3
View File
@@ -59,7 +59,11 @@ def main():
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"
# Keep proving the original integration against its original evidence, then
# audit the explicitly authorized subsequent placement/routing delta.
from validate_group_alignment import audit, BASE as GROUP_BASE
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(GROUP_BASE), silk_ids | u1_ids), "Historical U1 integration changed"
audit()
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)])
@@ -92,7 +96,7 @@ def main():
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,
historical_u1_integration_and_subsequent_authorized_group_delta_verified=True,
presentation_label_cleanup=True,
silk_refresh_idempotent=True, stale_geometry_negative_control=True,
silk_clipped_to_carrier=True, usb_mouth_markers=2,
@@ -100,7 +104,7 @@ def main():
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")
(ROOT/"validation/pcb-u1-group-regression.json").write_text(json.dumps(summary,indent=2)+"\n")
print(json.dumps(summary,indent=2))
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schemas.kicad.org/erc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T20:14:47",
"date": "2026-09-22T09:55:18",
"ignored_checks": [
{
"description": "Global label only appears once in the schematic",
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 337 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 347 KiB

@@ -0,0 +1,14 @@
{
"common_axis_absolute_x_mm": 156.97,
"resistor_to_rs232_body_gap_mm": 2.0815,
"resistor_to_oled_body_gap_mm": 2.0815,
"oled_to_button_body_gap_mm": 5.25,
"original_copper_items": 587,
"retained_copper_items": 476,
"removed_original_copper_items": 111,
"new_copper_items": 503,
"exact_unrelated_content_preserved": true,
"project_settings_preserved": true,
"baseline_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5"
}
@@ -0,0 +1,639 @@
{
"baseline_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
"moves_absolute_mm": {
"DS1": [
[
143,
136
],
[
143.97,
136
]
],
"R1": [
[
143,
134
],
[
142.16,
132.6685
]
],
"R2": [
[
154,
134
],
[
153.16,
132.6685
]
],
"R3": [
[
165,
134
],
[
164.16,
132.6685
]
],
"SW1": [
[
143,
168
],
[
141.72,
168
]
],
"SW2": [
[
155,
168
],
[
153.72,
168
]
],
"SW3": [
[
167,
168
],
[
165.72,
168
]
]
},
"removed_track_uuids": [
"38b14c4a-1117-48f1-84c2-69df406efd45",
"0436d2d5-7e3c-4343-a104-b27263f50378",
"0cd2f1ba-dd20-43d0-88bf-b78d25caa7f2",
"0df43fc8-4848-4075-9ee2-84440d3d4c52",
"114df41e-6104-445f-b52b-b623673294c3",
"2f38905d-78e9-4853-b3d0-50b257179e7b",
"5b48705f-d547-4a77-84fa-6e4149256b50",
"68657908-8a9e-41e0-832d-7e05a448acea",
"6b91cd8a-246d-4cfc-9398-242d2912fc37",
"88c50c34-fa4a-4fb9-89c7-fb8e40fcaad2",
"9e80e1c0-4aaa-4ebc-9689-497f9e2d3e92",
"b88df4ea-9af3-402e-b7ca-59ce0b2065bb",
"e1a92e9c-97e5-4400-b977-42536a8d2f2d",
"073a79bf-6f12-4c76-ba49-fbbfb977d470",
"18bac46c-c87a-4575-b8af-1934ea6110c4",
"20378a0c-b433-42ba-a643-c5af22ce09ed",
"28595a1b-947e-4fdd-be3e-d725cdabbcd4",
"49bb5dc7-5ca7-4d9c-8bb0-79ddb15d928f",
"79379f9c-e912-4b19-af4c-1638bb948703",
"97355a54-7801-4a02-8b6f-e1d31d364035",
"a2b2baa5-e50d-449a-ab05-53aaaf15c06c",
"da453fcc-84cc-470b-bc5a-49aa69700fa2",
"eab2ad52-ceaa-4fcb-ba9d-336bbc8745de",
"27bfb939-d170-4d17-9aab-8d68ea6995ca",
"6bb98a8c-ace7-48cb-8836-47b54273ebc3",
"8f0d756a-6380-4e82-b65c-17d2f12cf462",
"eb39c8eb-56cd-4331-b798-e769bfd19c05",
"16c19532-1b7d-4251-a3f2-07d08811abe4",
"45f61cb8-ad1d-4ca5-bb0d-3d26df7a61eb",
"4dc2388f-7f09-4efc-ae8d-5593cfd0efc5",
"527da12c-1892-4783-8e1e-bbb23491cf13",
"643622f8-985f-41ef-9f60-97b85df04a4b",
"7201c8b8-a924-4a78-8718-8639e1b84375",
"82d24494-1863-4798-8dab-d6acaa6af1ef",
"9d886175-0a22-4800-bacf-d027a2594149",
"b87e55c6-667a-4320-b42e-5311956f3869",
"ca3aebf5-89f6-4d95-920c-7f751b4e7227",
"d43c4432-114e-4488-b457-1015c677dd58",
"d5c6f4dc-918d-453a-a950-6fa7ff814a53",
"db004298-a5ea-4295-a340-99861f12affa",
"e058474e-eea8-44c5-9566-227ab00f9713",
"ebf07a5b-1b77-4da4-9009-b5456894c1a6",
"0348920f-ac8a-4881-925c-2379c1c042a3",
"1376e497-a31b-44ba-8756-a0c3909ae497",
"1ae9db6c-00ce-498f-a69a-4917829179a3",
"3385b064-faba-489a-ab8f-e2efd7bc6078",
"41b7e5dc-e873-4e78-add1-4a8aef2cf0f2",
"4a9ea5ca-9548-464c-90aa-d5a11171700b",
"53084fac-b12d-4262-8e8a-326bb1598e1d",
"6278694c-ab84-4d18-966d-cdd87569b736",
"70ecf107-6be8-4c81-944d-b9195d0e907b",
"7d370000-a728-42ad-8fb3-07bd56f55834",
"8a26d3a6-0e4e-47ac-914a-4e2ac03e80b8",
"a4794925-fe72-46c4-bea5-8cdf2ead8bbe",
"c8f89618-ed14-47ff-8e60-c489062cce99",
"d42d944c-5031-40e7-bab4-578d2a05e07c",
"fc51bdbd-326c-4620-b799-b1fcbfd0abfa",
"08eda9a1-b0e4-4ed1-8d7d-cec706f0162c",
"56f6e44c-d9f4-4144-8950-869e365e09d5",
"575729f7-8e91-43fc-ba36-47bd1927aebb",
"58b8f7f8-efff-41d9-a224-6c87461ab440",
"847e753c-6cc0-4300-8d7c-a3aba8914103",
"8b3de05f-9c1d-4e9f-82ab-64f1d17763e7",
"c0c4c166-7646-41f9-b005-9fe5c7ded558",
"c972b3de-08dc-4ad9-a684-3ff40523f29a",
"e0902a82-2700-41ac-b7e3-328984d82703",
"efb34c99-e6f3-4563-b01e-7d740f4bb0d1",
"5a6d5bb5-ab3e-4f95-9afc-8e9b5e1651b3",
"679beec8-cb5f-4c3c-9fab-d7d370b03f50",
"70fb66f6-deb5-4699-bb7d-effa571a4758",
"736e4133-a876-4d18-83ad-2a0649b0a9bf",
"785f6aab-ebe2-4476-b49f-a75d98b64c44",
"7a2820fc-35e9-408e-8e6d-1fa62e0f3416",
"870bf561-8eb8-4dbe-a63b-64a573c40a28",
"b519d784-ca35-488d-b0d2-5f4c344e7519",
"0225087c-1bcb-4bdc-ac0b-11fe1bff8a27",
"0237718c-7284-4cf8-8958-2e7e2e829e16",
"18a95172-f9ce-43f8-b9dd-ab56b48eaaa8",
"1ca667c6-9ebc-4ed5-9aeb-90c176a57a9f",
"241e7690-286a-467b-b825-70d989d4cb66",
"2a05aea8-421b-4c13-a82e-d49ae2a0c82e",
"2d7c81b1-9664-4486-bb9e-46db89cc5b02",
"310d85fb-84d9-440e-80a3-b2824fcd4d15",
"3fbbbb13-12d9-42e8-9b0a-7c40cbeef8a5",
"4260b587-cd60-4ca0-8a41-71e2e49f46a4",
"52415179-03b2-49b7-9e57-1fd6965ef4f4",
"7239518a-93d0-46dc-9114-703f779efb33",
"7bbda78d-630f-429b-816c-5b3db4ef39d0",
"7df7c029-9651-4323-bd67-afbf86fe336b",
"7e68296a-607b-446e-91e0-4fd4e30f18c9",
"a88c6fe8-c782-4db0-9c1c-afc4a1bc3f4b",
"c058ecbb-3e79-457e-a16c-77c319e0107c",
"ce63f9cd-80a2-422e-9d7b-251b8f54bbf7",
"d1d00ff3-b284-45e3-836a-0f5cfb666142",
"da267940-8d82-4b44-9d44-b28614c250cf",
"dbfbcc12-4305-4cd7-ba73-6f6dade960e4",
"e125d718-5926-473f-b25b-b9419465b0b1",
"ec4f2ef8-ca69-4520-aee7-6b12e42625b1",
"eca47b24-175d-4f12-9300-8770ec274a4d",
"08f34dfb-69ce-4ce7-aeb3-9b04878939bd",
"75e60ab2-4a98-4ae8-8155-068279b73dd6",
"98485ed4-b71e-46b1-aebe-781d40674d9f",
"9cbac9b7-9f29-4742-9e3b-8817a22ad4af",
"ab28a160-1194-460a-bcd9-cb165a51a47e",
"bac74aef-8326-4288-ac02-51ae39830b3d",
"d8112662-ca8b-4828-8ad4-34d71ca66db7",
"55f54e2b-ba8d-4c59-97e8-2e3c26ec1d98",
"a9579dde-c548-4e39-a3a9-6de3be621548",
"30c97d70-ce85-48b4-b576-22926efcb4ae",
"2ca96e04-1f1e-4466-b4ed-78484f82acd2",
"f120e834-5db9-4ce4-9e4b-3b2b0757fa3f",
"15c5caea-afd8-4f13-ad41-65aa162be77b",
"64cefc0c-db46-4fe9-bba7-c43c8901e416",
"0806ce46-3896-4c68-ab6b-e07f137928b3"
],
"repaired_connections": [
{
"net": "/GND",
"start": [
152.19,
137.5
],
"end": [
153.16,
137.5
]
},
{
"net": "/+3V3",
"start": [
154.73,
137.5
],
"end": [
155.7,
137.5
]
},
{
"net": "/OLED_SCL",
"start": [
157.27,
137.5
],
"end": [
158.24,
137.5
]
},
{
"net": "/OLED_SDA",
"start": [
159.81,
137.5
],
"end": [
160.78,
137.5
]
},
{
"net": "/+3V3",
"start": [
143.0,
134.0
],
"end": [
142.16,
132.6685
]
},
{
"net": "/BTN_PREVIOUS",
"start": [
150.62,
134.0
],
"end": [
149.78,
132.6685
]
},
{
"net": "/+3V3",
"start": [
154.0,
134.0
],
"end": [
153.16,
132.6685
]
},
{
"net": "/BTN_SELECT",
"start": [
161.62,
134.0
],
"end": [
160.78,
132.6685
]
},
{
"net": "/+3V3",
"start": [
165.0,
134.0
],
"end": [
164.16,
132.6685
]
},
{
"net": "/BTN_NEXT",
"start": [
172.62,
134.0
],
"end": [
171.78,
132.6685
]
},
{
"net": "/BTN_PREVIOUS",
"start": [
143.0,
168.0
],
"end": [
141.72,
168.0
]
},
{
"net": "/BTN_PREVIOUS",
"start": [
149.5,
168.0
],
"end": [
148.22,
168.0
]
},
{
"net": "/GND",
"start": [
143.0,
172.5
],
"end": [
141.72,
172.5
]
},
{
"net": "/GND",
"start": [
149.5,
172.5
],
"end": [
148.22,
172.5
]
},
{
"net": "/BTN_SELECT",
"start": [
155.0,
168.0
],
"end": [
153.72,
168.0
]
},
{
"net": "/BTN_SELECT",
"start": [
161.5,
168.0
],
"end": [
160.22,
168.0
]
},
{
"net": "/GND",
"start": [
155.0,
172.5
],
"end": [
153.72,
172.5
]
},
{
"net": "/GND",
"start": [
161.5,
172.5
],
"end": [
160.22,
172.5
]
},
{
"net": "/BTN_NEXT",
"start": [
167.0,
168.0
],
"end": [
165.72,
168.0
]
},
{
"net": "/BTN_NEXT",
"start": [
173.5,
168.0
],
"end": [
172.22,
168.0
]
},
{
"net": "/GND",
"start": [
167.0,
172.5
],
"end": [
165.72,
172.5
]
},
{
"net": "/GND",
"start": [
173.5,
172.5
],
"end": [
172.22,
172.5
]
},
{
"net": "/OLED_SDA",
"start": [
135.75,
161.55
],
"end": [
159.8,
137.5
]
},
{
"net": "/OLED_SCL",
"start": [
134.9,
159.85
],
"end": [
157.25,
137.5
]
},
{
"net": "/BTN_SELECT",
"start": [
161.5,
168.0
],
"end": [
161.5,
134.1
]
},
{
"net": "/RS_DCD",
"start": [
133.1,
131.9
],
"end": [
148.08,
126.67
]
},
{
"net": "/RS_RI",
"start": [
155.25,
138.7
],
"end": [
165.86,
126.67
]
},
{
"net": "/RS_DTR",
"start": [
148.6,
133.1
],
"end": [
155.7,
126.67
]
},
{
"net": "/RS_CTS",
"start": [
153.45,
138.75
],
"end": [
154.75,
135.65
]
},
{
"net": "/RS_RX",
"start": [
135.75,
139.5
],
"end": [
144.45,
132.8
]
},
{
"net": "/RS_VALID",
"start": [
157.75,
138.7
],
"end": [
165.9,
127.95
]
},
{
"net": "/RS_OFF_N",
"start": [
160.3,
138.7
],
"end": [
162.4,
136.75
]
},
{
"net": "/BTN_PREVIOUS",
"start": [
148.22,
168.0
],
"end": [
149.5,
168.0
]
},
{
"net": "/BTN_NEXT",
"start": [
172.22,
168.0
],
"end": [
173.5,
168.0
]
},
{
"net": "/GND",
"start": [
148.22,
172.5
],
"end": [
149.5,
172.5
]
},
{
"net": "/GND",
"start": [
145.54,
126.67
],
"end": [
150.25,
135.8
]
},
{
"net": "/GND",
"start": [
149.5,
170.0
],
"end": [
148.1,
156.55
]
},
{
"net": "/+3V3",
"start": [
143.0,
133.5
],
"end": [
143.0,
134.0
]
},
{
"net": "/+3V3",
"start": [
154.0,
134.0
],
"end": [
156.35,
136.4
]
},
{
"net": "/+3V3",
"start": [
148.7,
134.0
],
"end": [
152.65,
132.65
]
}
],
"retained_tracks": 476,
"protected_sha256": {
"serial-carrier.kicad_pro": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516",
"serial-carrier.kicad_prl": "f7c452b3be0ad43b4557f4d9c24a38124cd88268edfc2c9bbd59e3f2429e3772",
"serial-carrier.kicad_sch": "621acfd9ed8e025a90442e5f82f73d7d5e3228d7a48459a0fa4b0c46e763912a"
}
}
+2 -18
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-21T22:52:26",
"date": "2026-09-22T10:24:36",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
@@ -30,23 +30,7 @@
"exclusion"
],
"kicad_version": "10.0.6",
"schematic_parity": [
{
"description": "Carrier:MAX3243_Reference_Provisional stimmt nicht mit dem Footprint des Symbols () überein.",
"items": [
{
"description": "Footprint U2",
"pos": {
"x": 143.0,
"y": 126.67
},
"uuid": "812387f6-6524-459e-8102-fe9c55122668"
}
],
"severity": "warning",
"type": "footprint_symbol_mismatch"
}
],
"schematic_parity": [],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [],
"violations": []
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+24 -24
View File
@@ -1,39 +1,39 @@
{
"immutable_geometry_nets_pads_linkage_preserved": true,
"board_sha256": "94e941625b3b9fc9a08b9f2d532eb35003ea8da41d456a1c67269b9651200e05",
"independent_min_different_net_or_npth_clearance_mm": 0.2705441169852849,
"geometry_nets_pads_linkage_match_authorized_group_moves_and_values": true,
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5",
"independent_min_different_net_or_npth_clearance_mm": 0.2527621919573916,
"closest_items": [
"U1.38:4b8eabef-8493-437d-bd02-cb9ff4afd7cd",
"d95f45cb-7fa4-4203-a786-0a069425e2b8"
"DS1.4:91a4dcef-1b36-4e3d-a15c-c4578077c27b",
"265214f3-776f-4707-be6e-993749f3c8a7"
],
"min_copper_or_npth_edge_gap_mm": 1.9,
"min_rf_gap_mm": 0.025000000000005684,
"min_via_to_other_drilled_hole_gap_mm": 0.8205441169852848,
"min_via_to_other_drilled_hole_gap_mm": 0.47246725698856695,
"track_width_counts": {
"0.25": 451,
"0.5": 114
"0.25": 642,
"0.5": 296
},
"routed_length_mm_by_net": {
"/BTN_SELECT": 88.94832552628598,
"/GND": 141.68412246223386,
"/+3V3": 90.29604731966161,
"/RS_DCD": 43.068490178879834,
"/BTN_SELECT": 93.73257585313998,
"/GND": 167.13877028476702,
"/+3V3": 110.38340361450781,
"/RS_DCD": 43.09677445013352,
"/RS_DSR": 55.09398706495997,
"/RS_RI": 67.91373841554615,
"/RS_DTR": 52.53487761643567,
"/RS_RI": 75.93696129246699,
"/RS_DTR": 53.18208599609399,
"/RS_RTS": 60.056714985368814,
"/RS_CTS": 63.65082753507114,
"/RS_CTS": 81.83108365614024,
"/RS_TX": 53.135260491559045,
"/RS_RX": 51.637429252872906,
"/RS_VALID": 72.10630197626116,
"/RS_OFF_N": 78.04100118378895,
"/BTN_PREVIOUS": 78.83370879187974,
"/OLED_SDA": 62.65168524184884,
"/OLED_SCL": 61.1779295330028,
"/BTN_NEXT": 101.24898628384834
"/RS_RX": 52.38890111544552,
"/RS_VALID": 73.53056575207658,
"/RS_OFF_N": 78.70963033389047,
"/BTN_PREVIOUS": 84.48232420012336,
"/OLED_SDA": 63.88671049501825,
"/OLED_SCL": 62.3836654642909,
"/BTN_NEXT": 106.89760169209195
},
"tracks": 565,
"vias": 22,
"tracks": 938,
"vias": 41,
"copper_zones": 0,
"drc_freshly_run": true,
"drc_violations": 0,
+6 -2
View File
@@ -21,7 +21,11 @@ r = importlib.util.module_from_spec(spec)
spec.loader.exec_module(r)
board = p.LoadBoard(str(r.PCB))
original = p.LoadBoard(str(ROOT / 'validation/pcb-routing-input.kicad_pcb'))
assert r.identity(board) == r.identity(original), 'immutable source geometry/linkage changed'
import sys
sys.path.insert(0, str(ROOT))
from validate_group_alignment import expected_identity, audit
audit()
assert r.identity(board) == r.identity(expected_identity(original)), 'source geometry/linkage differs beyond authorized group moves/values'
items, holes = [], []
for f in board.GetFootprints():
for pad in f.Pads():
@@ -106,7 +110,7 @@ subprocess.run(['kicad-cli', 'pcb', 'drc', '--format', 'json', '--all-track-erro
assert r.PCB.read_bytes() == before, 'board changed during audit'
drc = json.loads(ROOT.joinpath('validation/pcb-routing-drc.json').read_text())
assert not drc['violations'] and not drc['unconnected_items']
report = dict(immutable_geometry_nets_pads_linkage_preserved=True,
report = dict(geometry_nets_pads_linkage_match_authorized_group_moves_and_values=True,
board_sha256=hashlib.sha256(before).hexdigest(),
independent_min_different_net_or_npth_clearance_mm=min_clearance,
closest_items=closest, min_copper_or_npth_edge_gap_mm=min_edge,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-21T22:33:53",
"date": "2026-09-22T10:21:48",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
+20 -19
View File
@@ -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-21T22:52:19</date>
<date>2026-09-22T10:24:34</date>
<tool>Eeschema 10.0.6-1.fc44</tool>
<sheet number="1" name="/" tstamps="/">
<title_block>
@@ -51,16 +51,16 @@
</units>
</comp>
<comp ref="R1">
<value>2.2k 1%</value>
<value>2k 1%</value>
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
<fields>
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -75,16 +75,16 @@
</units>
</comp>
<comp ref="R2">
<value>2.2k 1%</value>
<value>2k 1%</value>
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
<fields>
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -99,16 +99,16 @@
</units>
</comp>
<comp ref="R3">
<value>2.2k 1%</value>
<value>2k 1%</value>
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
<fields>
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -129,7 +129,7 @@
<field name="Manufacturer">Omron</field>
<field name="MPN">B3F-1000</field>
<field name="Function">Previous / back</field>
<field name="Procurement">Live stock not verified</field>
<field name="Procurement">User reports purchased</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
@@ -138,7 +138,7 @@
<property name="Manufacturer" value="Omron"/>
<property name="MPN" value="B3F-1000"/>
<property name="Function" value="Previous / back"/>
<property name="Procurement" value="Live stock not verified"/>
<property name="Procurement" value="User reports purchased"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -159,7 +159,7 @@
<field name="Manufacturer">Omron</field>
<field name="MPN">B3F-1000</field>
<field name="Function">Select / confirm</field>
<field name="Procurement">Live stock not verified</field>
<field name="Procurement">User reports purchased</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
@@ -168,7 +168,7 @@
<property name="Manufacturer" value="Omron"/>
<property name="MPN" value="B3F-1000"/>
<property name="Function" value="Select / confirm"/>
<property name="Procurement" value="Live stock not verified"/>
<property name="Procurement" value="User reports purchased"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -189,7 +189,7 @@
<field name="Manufacturer">Omron</field>
<field name="MPN">B3F-1000</field>
<field name="Function">Next</field>
<field name="Procurement">Live stock not verified</field>
<field name="Procurement">User reports purchased</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
@@ -198,7 +198,7 @@
<property name="Manufacturer" value="Omron"/>
<property name="MPN" value="B3F-1000"/>
<property name="Function" value="Next"/>
<property name="Procurement" value="Live stock not verified"/>
<property name="Procurement" value="User reports purchased"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -282,15 +282,16 @@
</comp>
<comp ref="U2">
<value>Adafruit MAX3243 / male 6253</value>
<footprint>Carrier:MAX3243_Reference_Provisional</footprint>
<fields>
<field name="Status">VERIFY male header numbering; footprint intentionally unassigned</field>
<field name="Status">User confirmed male pinout/header/footprint and pin-by-pin connections</field>
<field name="Datasheet_source">adafruit-research.md; reference female 5988 CAD</field>
<field name="Footprint"/>
<field name="Footprint">Carrier:MAX3243_Reference_Provisional</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="MAX3243_Module_Reference" description="Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE"/>
<property name="Status" value="VERIFY male header numbering; footprint intentionally unassigned"/>
<property name="Status" value="User confirmed male pinout/header/footprint and pin-by-pin connections"/>
<property name="Datasheet_source" value="adafruit-research.md; reference female 5988 CAD"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
@@ -0,0 +1,13 @@
{
"measured_library_matches_embedded_u1": true,
"historical_u1_integration_and_subsequent_authorized_group_delta_verified": 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": 94,
"baseline_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5",
"project_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516"
}
@@ -39,31 +39,31 @@
26.67
],
"DS1": [
43,
43.97,
36
],
"R1": [
43,
34
42.16,
32.6685
],
"R2": [
54,
34
53.16,
32.6685
],
"R3": [
65,
34
64.16,
32.6685
],
"SW1": [
43,
41.72,
68
],
"SW2": [
55,
53.72,
68
],
"SW3": [
67,
65.72,
68
],
"H1": [
@@ -95,8 +95,8 @@
"hex_mouth_overhang": 2.87,
"flange_front_inset": 1.93
},
"tracks": 565,
"vias": 22,
"tracks": 938,
"vias": 41,
"copper_layers": 2,
"draft_thickness_mm": 1.6,
"electrical_footprints": 9,
@@ -104,7 +104,7 @@
"physical_DRC_violations": 0,
"unconnected_items": 0,
"schematic_pad_nets_match": true,
"known_parity_warning": "U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
"schematic_parity_violations": 0,
"project_settings_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516",
"ignored_DRC_checks": [
{
@@ -128,15 +128,19 @@
"key": "footprint_type_mismatch"
}
],
"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"
]
"user_accepted": [
"module dimensions/visual fit and USB clearance",
"male pinout/header/footprint and pin-by-pin review",
"M3 carrier/module and M2 display hardware",
"RF keepout and power/ground routing"
],
"not_independently_verified": [
"reported 10 mm assembled module elevation",
"regulator margin",
"USB backfeed safety",
"OLED onboard pull-up resistance/rail",
"manufacturer RF qualification",
"fabrication process/certification"
],
"new_hardware_tests": false
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 232 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+22 -21
View File
@@ -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-20T20:14:45</date>
<date>2026-09-22T09:55:17</date>
<tool>Eeschema 10.0.6-1.fc44</tool>
<sheet number="1" name="/" tstamps="/">
<title_block>
@@ -51,16 +51,16 @@
</units>
</comp>
<comp ref="R1">
<value>2.2k 1%</value>
<value>2k 1%</value>
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
<fields>
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -75,16 +75,16 @@
</units>
</comp>
<comp ref="R2">
<value>2.2k 1%</value>
<value>2k 1%</value>
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
<fields>
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -99,16 +99,16 @@
</units>
</comp>
<comp ref="R3">
<value>2.2k 1%</value>
<value>2k 1%</value>
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
<fields>
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -129,7 +129,7 @@
<field name="Manufacturer">Omron</field>
<field name="MPN">B3F-1000</field>
<field name="Function">Previous / back</field>
<field name="Procurement">Live stock not verified</field>
<field name="Procurement">User reports purchased</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
@@ -138,7 +138,7 @@
<property name="Manufacturer" value="Omron"/>
<property name="MPN" value="B3F-1000"/>
<property name="Function" value="Previous / back"/>
<property name="Procurement" value="Live stock not verified"/>
<property name="Procurement" value="User reports purchased"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -159,7 +159,7 @@
<field name="Manufacturer">Omron</field>
<field name="MPN">B3F-1000</field>
<field name="Function">Select / confirm</field>
<field name="Procurement">Live stock not verified</field>
<field name="Procurement">User reports purchased</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
@@ -168,7 +168,7 @@
<property name="Manufacturer" value="Omron"/>
<property name="MPN" value="B3F-1000"/>
<property name="Function" value="Select / confirm"/>
<property name="Procurement" value="Live stock not verified"/>
<property name="Procurement" value="User reports purchased"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -189,7 +189,7 @@
<field name="Manufacturer">Omron</field>
<field name="MPN">B3F-1000</field>
<field name="Function">Next</field>
<field name="Procurement">Live stock not verified</field>
<field name="Procurement">User reports purchased</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
@@ -198,7 +198,7 @@
<property name="Manufacturer" value="Omron"/>
<property name="MPN" value="B3F-1000"/>
<property name="Function" value="Next"/>
<property name="Procurement" value="Live stock not verified"/>
<property name="Procurement" value="User reports purchased"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
@@ -282,15 +282,16 @@
</comp>
<comp ref="U2">
<value>Adafruit MAX3243 / male 6253</value>
<footprint>Carrier:MAX3243_Reference_Provisional</footprint>
<fields>
<field name="Status">VERIFY male header numbering; footprint intentionally unassigned</field>
<field name="Status">User confirmed male pinout/header/footprint and pin-by-pin connections</field>
<field name="Datasheet_source">adafruit-research.md; reference female 5988 CAD</field>
<field name="Footprint"/>
<field name="Footprint">Carrier:MAX3243_Reference_Provisional</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="MAX3243_Module_Reference" description="Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE"/>
<property name="Status" value="VERIFY male header numbering; footprint intentionally unassigned"/>
<property name="Status" value="User confirmed male pinout/header/footprint and pin-by-pin connections"/>
<property name="Datasheet_source" value="adafruit-research.md; reference female 5988 CAD"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
@@ -448,7 +449,7 @@
</library>
</libraries>
<nets>
<net code="1" name="/+3V3" class="Default">
<net code="1" name="/+3V3" class="Power">
<node ref="DS1" pin="2" pinfunction="VCC_2" pintype="power_in"/>
<node ref="R1" pin="1" pintype="passive"/>
<node ref="R2" pin="1" pintype="passive"/>
@@ -472,7 +473,7 @@
<node ref="SW2" pin="1" pintype="passive"/>
<node ref="U1" pin="19" pinfunction="GPIO13_19" pintype="input"/>
</net>
<net code="5" name="/GND" class="Default">
<net code="5" name="/GND" class="Power">
<node ref="DS1" pin="1" pinfunction="GND_1" pintype="power_in"/>
<node ref="SW1" pin="2" pintype="passive"/>
<node ref="SW2" pin="2" pintype="passive"/>
+3 -2
View File
@@ -4,5 +4,6 @@ KiCad ERC: zero reported errors/warnings; no project exclusions added.
15 firmware GPIO definitions independently checked against exported pin functions.
All 44 MCU header pins accounted for; USB/UART0/5V/PSRAM pins unused on carrier.
PDF and SVG exported successfully.
Not validated: male-module physical mapping, power budget/path, OLED pull-ups,
selected connector fit, PCB DRC/layout, procurement, or assembled hardware.
User accepted module mapping/fit and reports purchased switches/resistors.
No new hardware tests; regulator margin, USB backfeed safety, RF performance
and fabrication qualification are not established by this schematic check.