Integrate Measured ESP32 PCB Geometry

- Add measured F.Fab bounds and USB mouth markers
- Add guarded preservation and regression validation
- Refresh review exports and remove draft presentation labels
This commit is contained in:
2026-09-21 22:54:37 +02:00
parent ab6a8010ba
commit 901afbe754
25 changed files with 20490 additions and 850 deletions
+15 -1
View File
@@ -8,9 +8,23 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
- README was refined from the user's already-thinned version, not restored from Git. New `docs/getting_started.md`, `docs/user_administration.md`, and `docs/firmware_update.md` provide operating procedures; `docs/web_administration.md` is now a user guide and its original contract is preserved under `docs/devel/`. Flash layout and PCB research moved into the developer archive; contextual test/hardware/provenance READMEs remain beside artifacts. - README was refined from the user's already-thinned version, not restored from Git. New `docs/getting_started.md`, `docs/user_administration.md`, and `docs/firmware_update.md` provide operating procedures; `docs/web_administration.md` is now a user guide and its original contract is preserved under `docs/devel/`. Flash layout and PCB research moved into the developer archive; contextual test/hardware/provenance READMEs remain beside artifacts.
- Documentation relocation adds no new device evidence and does not change historical acceptance limits. Firmware, generated assets, CAD and historical hash JSON are untouched. Hardware scripts have only relocated-note reference/protection adjustments; Python syntax was checked without executing generators. Documentation links/anchors and `git diff --check` were checked; no firmware build, upload, erase, or hardware test was performed. - Documentation relocation adds no new device evidence and does not change historical acceptance limits. Firmware, generated assets, CAD and historical hash JSON are untouched. Hardware scripts have only relocated-note reference/protection adjustments; Python syntax was checked without executing generators. Documentation links/anchors and `git diff --check` were checked; no firmware build, upload, erase, or hardware test was performed.
## PCB presentation labels — removed and exports refreshed
- Removed PCB title suffix `draft`, revision suffix `DRAFT`, front-silkscreen `DRAFT - NOT FOR FAB`, and title-comment prefix `NOT FOR FABRICATION -` (also in the PDF exporter). Generator no longer emits them. Specific technical cautions, hidden metadata, footprint identifiers, filenames and schematic remain unchanged; this is not fabrication approval.
- `remove_draft_markers.py` guards editor locks/concurrent edits, saves with `SaveBoard(..., True)`, and audits the exact parsed-content delta and unchanged project settings. Two separate cleanup runs passed; same-process repeated board access hit a KiCad SWIG binding failure before any source overwrite.
- 1:1 placement PDF and SVG/PNG/3D previews regenerated and visually reviewed. U1 preservation audit permits only the exact label delta in addition to prior measured-U1/device-silk changes; original baseline retained. U1/silk regression, two footprint tests and fresh PCB checks pass: 565 segments, 22 vias, zero physical DRC/unrouted, expected U2 parity warning. Existing mechanical/electrical release gates and historical A0 settings-hash limitation remain; no firmware/device tests or fabrication outputs.
## ESP32 mechanical measurements — board and review exports integrated
- User supplied main PCB 56.07 × 28.09 mm excluding 6.89 mm antenna projection; antenna left/right insets 5.41/5.05 mm. Overall length 62.96 mm. Both USB shells 8.87 mm wide, outer-edge offsets 4.16 mm left / 3.66 mm right, mouth faces flush with main PCB bottom.
- Preserve 2.54 mm pitch / 25.40 mm row spacing. User accepted approximate 1.365 mm top/bottom pin-centre offset and same left offset; derived right margin 1.325 mm. Library HW678 F.Fab now has main PCB/antenna rectangles and USB mouth spans; no guessed USB depth, RF keepout, courtyard or heights. Footprint regression updated.
- Embedded U1 now has the four measured F.Fab shapes and two measurement-limit annotations. `update_u1_graphics.py` adds only owned graphics; pads/nets/existing UUIDs/placement/routing/project settings and existing footprint fields remain unchanged. Old hidden status/description fields are historical, not current measurement evidence. `add_device_silkscreen.py` replaces header guides with carrier/mask-clipped measured outlines and 0.30 mm width-only USB mouth markers. PDF now verifies saved U1 instead of overlaying library geometry; PDF/SVG/PNG/3D exports refreshed.
- New `validate_u1_update.py` audits exact pre-update preservation, library geometry, silk idempotence/clipping/mouth positions and stale-geometry rejection; snapshot/report retained in hardware/PCB/validation. Fresh PCB, independent routing, footprint and DB9 CAD checks pass: 565 segments, 22 vias, no physical DRC/unrouted, one expected U2 parity warning. Historical A0 preservation script fails its old `serial-carrier.kicad_prl` hash gate; not weakened or rebaselined. New A1-specific preservation passes.
- At unchanged A1 placement USB faces sit 2.295 mm inside carrier bottom edge; plug clearance and physical 1:1 fit remain pending. Antenna geometry is not RF guidance; no keepout, USB depth, courtyard or height was invented. No firmware/assets changes, rerouting, hardware tests or fabrication outputs.
## Carrier PCB A1 — revised placement and CAD integration ## Carrier PCB A1 — revised placement and CAD integration
- `hardware/PCB/serial-carrier.kicad_pcb`: routed 82 × 80 mm two-layer draft; 36 × 19 mm top-left notch, ESP32 shifted down for bottom USB access, RS-232 bare PCB front flush with carrier top. Exact ESP32 body/USB/antenna envelope remains pending user measurements; RF reserve is provisional. - `hardware/PCB/serial-carrier.kicad_pcb`: routed 82 × 80 mm two-layer draft; 36 × 19 mm top-left notch, ESP32 shifted down for bottom USB access, RS-232 bare PCB front flush with carrier top. Measured ESP32 planar body/antenna and USB mouths are integrated; full connector/assembly envelopes and RF qualification remain pending.
- Adafruit STEP metal geometry extracted and validated; provenance/dimensions in `docs/devel/hardware/PCB/db9-mechanical-notes.md`. Flange front sits 1.930 mm inside carrier edge, screw-lock mouths 2.870 mm outside, shell nose 3.870 mm outside. Case wall plane is not yet fixed. Seven board-local Dwgs.User projections added; rerun `add_db9_envelope.py` and `add_device_silkscreen.py` after moving modules. - Adafruit STEP metal geometry extracted and validated; provenance/dimensions in `docs/devel/hardware/PCB/db9-mechanical-notes.md`. Flange front sits 1.930 mm inside carrier edge, screw-lock mouths 2.870 mm outside, shell nose 3.870 mm outside. Case wall plane is not yet fixed. Seven board-local Dwgs.User projections added; rerun `add_db9_envelope.py` and `add_device_silkscreen.py` after moving modules.
- Placement PDF and SVG/PNG/3D previews refreshed. Fresh PCB, independent routing, preservation and CAD validators pass: 38/38 connections, 565 segments, 22 vias, 0 physical DRC violations/unrouted, one expected provisional U2 footprint parity warning. Not fabrication-ready; no hardware qualification or Gerbers. Existing project/schematic/library work preserved; board saves must use `SaveBoard(..., True)` and require checking editor lock. Prior A0 preserved in validation backup. - Placement PDF and SVG/PNG/3D previews refreshed. Fresh PCB, independent routing, preservation and CAD validators pass: 38/38 connections, 565 segments, 22 vias, 0 physical DRC violations/unrouted, one expected provisional U2 footprint parity warning. Not fabrication-ready; no hardware qualification or Gerbers. Existing project/schematic/library work preserved; board saves must use `SaveBoard(..., True)` and require checking editor lock. Prior A0 preserved in validation backup.
+35 -5
View File
@@ -78,10 +78,38 @@ centre (pad 1), **not a PCB corner**.
- Numbering follows the project's explicit convention, not a generic dual-row - Numbering follows the project's explicit convention, not a generic dual-row
connector's alternating or counterclockwise numbering. Physical numbers do connector's alternating or counterclockwise numbering. Physical numbers do
**not** establish GPIO, supply, or ground assignments. **not** establish GPIO, supply, or ground assignments.
- Only pads and annotation text are present. **No PCB outline, antenna keepout, - User-measured planar geometry is now drawn on `F.Fab`, relative to pad 1:
courtyard, USB envelope, mounting holes, or 3D model is guessed.** Text positions
are annotations, not dimensional evidence. Outline registration, antenna extent, | Feature | Start (X, Y), mm | End (X, Y), mm |
connector offsets/overhang, and assembly height remain unknown. |---|---|---|
| Main PCB rectangle | (-1.365, -1.365) | (26.725, 54.705) |
| Projecting antenna rectangle | (4.045, -8.255) | (21.675, -1.365) |
| Left USB-C shell mouth span | (2.795, 54.705) | (11.665, 54.705) |
| Right USB-C shell mouth span | (14.195, 54.705) | (23.065, 54.705) |
Main PCB is 28.09 mm wide × 56.07 mm long; antenna projection adds 6.89 mm,
giving 62.96 mm overall. Antenna left/right insets are 5.41/5.05 mm, giving
17.63 mm width and a centre 0.18 mm right of the PCB centre. Both shell widths
are 8.87 mm, with outer-edge offsets 4.16 mm left / 3.66 mm right; mouths are
flush with the bottom PCB edge. Lines indicate mouth spans, not shell depth.
- User accepted approximate 1.365 mm top/bottom pin-centre offsets and supplied
approximately the same left offset. Nominal pin pitch/row spacing are retained;
resulting right margin is 1.325 mm. This 0.04 mm asymmetry follows the selected
modelling datum, not a precision measurement of header asymmetry.
- **No RF keepout, courtyard, full USB envelope, mounting holes or 3D model is
inferred.** Shell depth/height, component/underside heights and plug clearances
remain unknown. The antenna rectangle is physical geometry, not RF guidance.
- Routed embedded U1 now contains the four measured F.Fab shapes and the two
antenna/USB measurement-limit annotations. Generic library captions are omitted
to avoid overlapping existing board labels. Existing footprint fields (including
the historical hidden status/description), reference/value positions, pads,
nets and UUIDs are deliberately retained; this is not a full footprint replacement.
The library is authoritative for measured geometry, not those old status strings.
- Measured board-local silkscreen and PDF/SVG/PNG/3D previews are refreshed.
`update_u1_graphics.py` synchronizes only owned graphics, with deterministic new
UUIDs and lock/concurrent-change guards; `validate_u1_update.py` checks exact
preservation against the pre-update A1 snapshot. A physical 1:1 fit check is
still pending; USB mouths sit 2.295 mm inside the carrier bottom edge.
## Validation and release gates ## Validation and release gates
@@ -89,7 +117,9 @@ Validation performed with KiCad 10.0.6 `pcbnew.FootprintLoad`: both footprints
parsed successfully. Assertions passed for pad counts, all numbered pad positions, parsed successfully. Assertions passed for pad counts, all numbered pad positions,
pad shapes/types/sizes/drills, OLED NPTH positions and diameters, both OLED `F.Fab` pad shapes/types/sizes/drills, OLED NPTH positions and diameters, both OLED `F.Fab`
rectangle bounds, and absence of courtyards, `Edge.Cuts`, and keepout zones. rectangle bounds, and absence of courtyards, `Edge.Cuts`, and keepout zones.
HW678 was also checked to contain no graphical shapes. The local Python binding The HW678 regression now checks the two measured rectangles and two USB mouth
spans, while retaining exact pad geometry checks and prohibiting guessed
courtyards, zones and 3D models. The local Python binding
emitted three startup `PROPERTY_ENUM` assertions (also seen on a version-only emitted three startup `PROPERTY_ENUM` assertions (also seen on a version-only
import); these did not prevent loading or the checks from passing. import); these did not prevent loading or the checks from passing.
+99 -12
View File
@@ -7,6 +7,26 @@ Open **`../../../../hardware/PCB/serial-carrier.kicad_pcb`** in KiCad 10. The ro
existing `../../../../hardware/PCB/serial-carrier.kicad_pro` / `../../../../hardware/PCB/serial-carrier.kicad_sch` project. **Not for fabrication.** existing `../../../../hardware/PCB/serial-carrier.kicad_pro` / `../../../../hardware/PCB/serial-carrier.kicad_sch` project. **Not for fabrication.**
No Gerbers, production drills or manufacturing archive were generated. No Gerbers, production drills or manufacturing archive were generated.
## Measurement update — embedded U1, silkscreen and exports
The user has now supplied ESP32 main-PCB, antenna and USB mouth geometry; see
`footprint-notes.md` and the hardware README. The library HW678 footprint contains
these measured F.Fab bounds. Embedded U1 now has the same four shapes and two
measurement-limit annotations; generic library captions are omitted to avoid
colliding with existing board labels. Silkscreen, PDF and SVG/PNG/3D previews are
refreshed. Existing footprint fields, all existing non-generated UUIDs, pads/nets,
placement, routing, edges, RF reserve and project settings are unchanged. Old
hidden U1 status/description fields are retained historical metadata, not evidence
that planar measurements are missing. RF requirements, full USB envelopes and
assembly heights remain pending; the antenna rectangle is not an RF keepout.
At unchanged A1 U1 placement, the measured main PCB occupies carrier-local
X=6.635…34.725, Y=21.635…77.705 mm; the projecting antenna occupies
X=12.045…29.675, Y=14.745…21.635 mm. USB mouths are at Y=77.705 mm,
**2.295 mm inside the carrier bottom edge**, so flush-with-module USB does not
mean flush-with-carrier access. Review mating-plug clearance before accepting
placement; no relocation, rerouting or RF sign-off is implied by this update.
## Size and placement ## Size and placement
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm. - **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
@@ -14,8 +34,8 @@ No Gerbers, production drills or manufacturing archive were generated.
- Proposed **1.6 mm** carrier thickness; fabrication stackup/process not selected. - Proposed **1.6 mm** carrier thickness; fabrication stackup/process not selected.
- Stepped outline: a **36 × 19 mm top-left antenna cutaway** fits within a simple - Stepped outline: a **36 × 19 mm top-left antenna cutaway** fits within a simple
rectangular/square case. The lower edge is now straight, with ESP32 USB toward rectangular/square case. The lower edge is now straight, with ESP32 USB toward
the bottom and RS-232 toward the top. U1 moved down by 19 mm; its exact USB/body the bottom and RS-232 toward the top. A1 moved U1 down by 19 mm; this measurement
registration remains provisional pending the user's measured ESP32 geometry. integration retains that placement and uses the user-accepted approximate header datum.
- Module and mating-plug overhangs are not included in the carrier dimensions. - Module and mating-plug overhangs are not included in the carrier dimensions.
The final complete assembly/enclosure envelope still needs measurement. The final complete assembly/enclosure envelope still needs measurement.
@@ -80,8 +100,8 @@ allocation, **not a measured antenna envelope or a manufacturer RF keepout**.
The module itself may span the rule area; footprint placement is not forbidden. The module itself may span the rule area; footprint placement is not forbidden.
No carrier copper enters the reserve, but copper runs close to its boundary. No carrier copper enters the reserve, but copper runs close to its boundary.
U1's exact PCB/antenna/USB offsets are still unmeasured. The intended antenna U1's planar PCB/antenna/USB-mouth offsets are now recorded. Antenna clearance
overhang and USB access must be checked with the actual board and a printed and USB access must still be checked with the actual board and a printed
placement template. Move U1, change the edge or enlarge the reserve if required. placement template. Move U1, change the edge or enlarge the reserve if required.
Missing RF clearance cannot be signed off from the current footprint alone. Missing RF clearance cannot be signed off from the current footprint alone.
@@ -147,11 +167,13 @@ RS-232's visible rounded corners retain true arcs. Small gaps clear pad/mask
openings, vias and adjacent resistor markings; the original full F.Fab geometry openings, vias and adjacent resistor markings; the original full F.Fab geometry
remains unchanged. remains unchanged.
U1 instead has **header-row guides**, labelled `HEADER GUIDES`: guide edges are U1 now has **measured body/antenna outlines**, replacing the header-row guides
1.20 mm outside the outer pin centres. These are graphic alignment aids, **not and `HEADER GUIDES` label. Silkscreen is clipped inside the stepped carrier with
claimed socket-body dimensions or an inferred ESP32 PCB outline**. The actual 0.22 mm additional edge/mask clearance; the off-carrier antenna projection remains
U1 body/antenna/USB outline still requires measured offsets. Existing button and visible only on F.Fab. Two **0.30 mm strokes** mark the 8.87 mm USB mouth spans,
resistor silkscreen remains unchanged. replacing coincident 0.12 mm body-edge strokes. Wider strokes receive correspondingly
wider clearance. No USB depth or RF keepout is inferred. Existing button/resistor
silkscreen remains unchanged.
These are board-local drawings, not edits to the footprint libraries. If the These are board-local drawings, not edits to the footprint libraries. If the
module placement or routing changes, close PCB Editor and refresh them with: module placement or routing changes, close PCB Editor and refresh them with:
@@ -160,7 +182,8 @@ module placement or routing changes, close PCB Editor and refresh them with:
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py /usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
``` ```
The script requires pcbnew and Shapely, replaces only its own generated graphics, The script first verifies the saved U1 measured graphics against the library.
It requires pcbnew and Shapely, replaces only its own generated graphics,
and refuses an open PCB or concurrent board change. It leaves project settings, and refuses an open PCB or concurrent board change. It leaves project settings,
copper, pads and source F.Fab outlines unchanged. Run it **after** placement or copper, pads and source F.Fab outlines unchanged. Run it **after** placement or
routing regeneration; moving a footprint does not move these board-local drawings. routing regeneration; moving a footprint does not move these board-local drawings.
@@ -174,6 +197,21 @@ warning. PDF and both previews have been refreshed.
## Review outputs ## Review outputs
PCB presentation labels have been removed: title suffix `draft`, revision suffix
`DRAFT`, front-silkscreen `DRAFT - NOT FOR FAB`, and the title-comment prefix
`NOT FOR FABRICATION -` (including the PDF exporter). The generator no longer
emits these labels. Specific technical cautions, dimensions, geometry, hidden
metadata and footprint identifiers remain; artifact filenames are unchanged.
This is a presentation change, **not fabrication approval**. The schematic and
its exports are outside this PCB-only cleanup.
`remove_draft_markers.py` performs the guarded, idempotent cleanup and audits
that no other parsed board content or project settings change. The U1 regression
now permits exactly these label edits against its unchanged historical baseline,
without relaxing geometry/pad/net/routing checks. Fresh PCB DRC, U1/silkscreen
regressions and both footprint tests pass; the expected U2 parity warning remains.
PDF/SVG/PNG/3D outputs were regenerated and visually reviewed after cleanup.
- `../../../../hardware/PCB/validation/pcb-draft.svg` / `../../../../hardware/PCB/validation/pcb-draft.png`: combined copper/outline/fab review. - `../../../../hardware/PCB/validation/pcb-draft.svg` / `../../../../hardware/PCB/validation/pcb-draft.png`: combined copper/outline/fab review.
- `../../../../hardware/PCB/validation/pcb-draft-3d.png`: KiCad render of the carrier and available standard - `../../../../hardware/PCB/validation/pcb-draft-3d.png`: KiCad render of the carrier and available standard
component models. **ESP32, OLED and RS-232 assembled module models are absent**; component models. **ESP32, OLED and RS-232 assembled module models are absent**;
@@ -181,7 +219,35 @@ warning. PDF and both previews have been refreshed.
- `../../../../hardware/PCB/pcb-placement-draft.pdf`: **1:1** front placement/pad/outline drawing. Print at - `../../../../hardware/PCB/pcb-placement-draft.pdf`: **1:1** front placement/pad/outline drawing. Print at
100%, disable fit-to-page, and confirm the outer width is 82 mm before use. 100%, disable fit-to-page, and confirm the outer width is 82 mm before use.
It includes the known module outlines and nominal STEP-derived DE-9 metal It includes the known module outlines and nominal STEP-derived DE-9 metal
bounding projections; no guessed U1 body or case-wall geometry. bounding projections and measured U1 planar geometry; no guessed USB depth,
RF keepout or case-wall geometry.
Refresh the embedded measurement graphics and PDF without replacing the footprint:
```sh
/usr/bin/python3 -B hardware/PCB/update_u1_graphics.py
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
/usr/bin/python3 -B hardware/PCB/export_placement_pdf.py
```
Both board-writing helpers refuse PCB Editor locks and concurrent board changes,
use `SaveBoard(..., True)`, and check unchanged project settings. The PDF exporter
verifies embedded geometry instead of overlaying the library; only review captions
change on its temporary plotting board. Print at 100%, not fit-to-page.
Preview refresh commands (repository root, KiCad and Inkscape installed):
```sh
kicad-cli pcb export svg --mode-single --layers F.Cu,B.Cu,F.SilkS,F.Fab,Dwgs.User,Edge.Cuts --sketch-pads-on-fab-layers --page-size-mode 2 --output hardware/PCB/validation/pcb-draft.svg hardware/PCB/serial-carrier.kicad_pcb
inkscape hardware/PCB/validation/pcb-draft.svg --export-type=png --export-width=1800 --export-filename=hardware/PCB/validation/pcb-draft.png
kicad-cli pcb render --width 1600 --height 1200 --side top --quality high --output hardware/PCB/validation/pcb-draft-3d.png hardware/PCB/serial-carrier.kicad_pcb
```
KiCad's SVG trailing whitespace was normalized after export (`git diff --check`
passes); the rendered geometry is unchanged. PDF and previews were visually reviewed.
The board-area SVG crop omits outside-board review captions/parts of the DE-9
projection; use the full-page PDF for the complete placement drawing.
## Validation and limits ## Validation and limits
@@ -190,6 +256,9 @@ Run from the repository root with the system Python containing KiCad bindings:
```sh ```sh
/usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py /usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py
/usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py /usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
/usr/bin/python3 -B hardware/PCB/validate_u1_update.py
/usr/bin/python3 -B hardware/PCB/reference/db9-cad/validate.py
``` ```
The first script exports a **fresh saved-schematic netlist** and checks the actual The first script exports a **fresh saved-schematic netlist** and checks the actual
@@ -204,7 +273,25 @@ placement/net/UUID preservation against the initial unrouted snapshot. It needs
locally installed NumPy and Shapely in addition to pcbnew. It is not a substitute locally installed NumPy and Shapely in addition to pcbnew. It is not a substitute
for current-schematic net validation and is specific to this initial layout. for current-schematic net validation and is specific to this initial layout.
Results with KiCad **10.0.6**: Measurement-integration validation with KiCad **10.0.6**:
- All five commands above pass. New `validate_u1_update.py` compares the complete
parsed board against `validation/pcb-before-u1-measured.kicad_pcb`, excluding
only owned silk and six new U1 graphics. Snapshot and project hashes are pinned;
full pad/net/UUID/placement/routing/edge/zone/settings preservation passes.
- Measured embedded/library geometry, silk regeneration idempotence, carrier/notch
clipping, two exact USB mouth markers and a stale-geometry negative control pass;
**91 generated silk items**. U1 refresh byte-idempotence also passed; an already
current U1 now returns without writing. Evidence: `hardware/PCB/validation/pcb-u1-update.json`.
- The historical A0 `pcb-routing-preservation.py` does **not** pass: it stops on
its historical `serial-carrier.kicad_prl` protected hash, already stale for this
working state. Its older exact-footprint contract also predates measured U1
additions. Historical script/reports were not weakened or rebaselined; the new
A1 pre-update comparison is the preservation evidence for this change.
- PCB, project and schematic retain their existing content except the authorized
board graphics. No firmware/assets, reroute, upload, erase or hardware test.
Current results:
- **0 physical DRC errors/warnings; 0 unconnected items.** - **0 physical DRC errors/warnings; 0 unconnected items.**
- All saved-schematic pad nets match the PCB; **1 expected U2 footprint parity warning**. - All saved-schematic pad nets match the PCB; **1 expected U2 footprint parity warning**.
@@ -2,13 +2,19 @@
(version 20260206) (version 20260206)
(generator "pcbnew") (generator "pcbnew")
(layer "F.Cu") (layer "F.Cu")
(descr "HW-678, component/top view antenna up USB down; origin pad 1; left top-bottom 1..22, right top-bottom 23..44; only header centres verified; no GPIO mapping, outline, courtyard or antenna keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet") (descr "HW-678, component/top view antenna up USB down; origin pad 1; left top-bottom 1..22, right top-bottom 23..44; nominal header centres with user-measured main PCB/antenna bounds and USB shell mouth spans on F.Fab; approximate 1.365 mm top/left registration; no GPIO mapping, complete USB envelope, courtyard or RF keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet")
(tags "module provisional THT") (tags "module provisional THT")
(attr through_hole) (attr through_hole)
(property "Reference" "REF**" (at 12.7 -3) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15)))) (property "Reference" "REF**" (at 12.7 -3) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
(property "Value" "HW678_2x22_Provisional" (at 12.7 57) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15)))) (property "Value" "HW678_2x22_Provisional" (at 12.7 57) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
(fp_text user "PROVISIONAL CONNECTOR FIT" (at 12.7 25) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15)))) (fp_text user "PROVISIONAL CONNECTOR FIT" (at 12.7 25) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
(fp_text user "ANTENNA UP / USB DOWN" (at 12.7 28) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15)))) (fp_text user "ANTENNA UP / USB DOWN" (at 12.7 28) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
(fp_rect (start -1.365 -1.365) (end 26.725 54.705) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
(fp_rect (start 4.045 -8.255) (end 21.675 -1.365) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
(fp_line (start 2.795 54.705) (end 11.665 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
(fp_line (start 14.195 54.705) (end 23.065 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
(fp_text user "ANTENNA BOUNDS - NOT RF KEEPOUT" (at 12.7 -9.5) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
(fp_text user "USB SHELL MOUTHS ONLY" (at 12.7 59) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
(pad "1" thru_hole rect (at 0.00 0.00) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no)) (pad "1" thru_hole rect (at 0.00 0.00) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "2" thru_hole circle (at 0.00 2.54) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no)) (pad "2" thru_hole circle (at 0.00 2.54) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "3" thru_hole circle (at 0.00 5.08) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no)) (pad "3" thru_hole circle (at 0.00 5.08) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
+25 -6
View File
@@ -26,8 +26,11 @@ the carrier. All 3.3 V and ground connections are explicit named nets.
| `validation/pcb-draft.png`, `validation/pcb-draft-3d.png` | Copper/fab preview and carrier-only 3D render | | `validation/pcb-draft.png`, `validation/pcb-draft-3d.png` | Copper/fab preview and carrier-only 3D render |
| `validate_pcb_draft.py` | Fresh schematic-to-PCB net checks, bounds/layers and DRC/parity report | | `validate_pcb_draft.py` | Fresh schematic-to-PCB net checks, bounds/layers and DRC/parity report |
| `generate_pcb_draft.py`, `route_pcb_draft.py` | Optional destructive-regeneration tools with explicit overwrite guards | | `generate_pcb_draft.py`, `route_pcb_draft.py` | Optional destructive-regeneration tools with explicit overwrite guards |
| `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 outlines and ESP32 header guides after placement/routing; preserves copper and project settings | | `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 and measured ESP32 outlines/USB mouth markers after placement/routing; preserves copper and project settings |
| [db9-mechanical-notes.md](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation | | [db9-mechanical-notes.md](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation |
| `update_u1_graphics.py`, `validate_u1_update.py` | Guarded measured-U1 graphics integration and exact pre-update preservation / geometry / silkscreen regressions |
| `export_placement_pdf.py` | 1:1 PDF from saved board geometry; refuses stale embedded U1 graphics |
| `remove_draft_markers.py` | Guarded presentation-label cleanup with exact-content preservation audit; does not qualify fabrication |
| `add_db9_envelope.py` | Refresh nominal connector metal projections on Dwgs.User; not a case cutout or assembly clearance | | `add_db9_envelope.py` | Refresh nominal connector metal projections on Dwgs.User; not a case cutout or assembly clearance |
| `Carrier.kicad_sym`, `sym-lib-table` | Project-local module, switch and resistor symbols | | `Carrier.kicad_sym`, `sym-lib-table` | Project-local module, switch and resistor symbols |
| `Carrier.pretty/`, `fp-lib-table` | OLED/ESP32/RS-232-reference provisional footprints; standard KiCad 10 switch/resistor/mounting-hole library references | | `Carrier.pretty/`, `fp-lib-table` | OLED/ESP32/RS-232-reference provisional footprints; standard KiCad 10 switch/resistor/mounting-hole library references |
@@ -217,9 +220,24 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
rows fit a standard breadboard simultaneously without forcing. Use nominal rows fit a standard breadboard simultaneously without forcing. Use nominal
2.54 mm pitch (53.34 mm over 21 intervals) and 25.40 mm row spacing based on 2.54 mm pitch (53.34 mm over 21 intervals) and 25.40 mm row spacing based on
that fit check, not the raw caliper measurements. Verify with a 1:1 print before fabrication. that fit check, not the raw caliper measurements. Verify with a 1:1 print before fabrication.
- ESP32 seller drawing states 28 mm width and annotates 63 mm length; its 20 mm - ESP32 actual user measurements supersede the seller's 28 × 63 mm drawing:
arrow is not header row spacing. Antenna extent and header-to-outline offsets main PCB **56.07 × 28.09 mm**, excluding the antenna projection. Top/component
are not fully dimensioned. view, antenna up and USB down: antenna projects **6.89 mm** above the main PCB,
inset **5.41 mm left / 5.05 mm right**. Derived antenna width **17.63 mm**,
overall length **62.96 mm**, antenna centre **0.18 mm right** of PCB centre.
- Header end offsets were initially approximately 1.5 mm; user accepted **1.365 mm**
top/bottom to preserve the breadboard-confirmed 53.34 mm span. Left-edge-to-left-row
centre distance is also approximately **1.365 mm**. Preserve 25.40 mm row spacing:
the resulting right margin is **1.325 mm**, not an independently measured offset.
These modelling offsets are not a claim of micron-level measurement accuracy.
- Both USB-C metal shells are **8.87 mm wide**, with opening faces flush with the
main PCB bottom edge. Left shell's outside left edge is **4.16 mm** from the PCB
left edge; right shell's outside right edge is **3.66 mm** from the PCB right edge.
Shell depth/height and mating-plug clearance remain unmeasured.
- The library and routed board's embedded U1 now record these bounds on F.Fab;
USB mouths are width spans only, not complete connector outlines. Measured
board-local silkscreen, 1:1 placement PDF and SVG/PNG/3D previews are refreshed. See
`../../docs/devel/hardware/PCB/footprint-notes.md` for coordinates and limits.
- OLED actual PCB outline measured by user: 26 × 26 mm, superseding the seller's - OLED actual PCB outline measured by user: 26 × 26 mm, superseding the seller's
28.7 × 27.8 mm dimensions. Seller's 4.1 mm height remains unverified. 28.7 × 27.8 mm dimensions. Seller's 4.1 mm height remains unverified.
- OLED front view, connector at top: GND, VCC, SCL, SDA from left to right, - OLED front view, connector at top: GND, VCC, SCL, SDA from left to right,
@@ -258,8 +276,9 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
1. OLED outline, mounting-hole placement, revised header position and display 1. OLED outline, mounting-hole placement, revised header position and display
outline are user fit-verified. Select carrier header/socket pads and drills outline are user fit-verified. Select carrier header/socket pads and drills
from the actual connector drawings. from the actual connector drawings.
2. Before final module outlines/courtyards, establish their registration to header 2. ESP32 planar body/antenna bounds and USB mouth positions are now recorded in
centres, connector overhang and display position from drawings or measurements. 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. 3. Verify power-path schematic/regulator capacity and OLED pull-up implementation.
Photos alone do not establish those electrical properties. Photos alone do not establish those electrical properties.
+21 -19
View File
@@ -10,6 +10,7 @@ import uuid
import pcbnew as p import pcbnew as p
from shapely.geometry import LineString, Point, box from shapely.geometry import LineString, Point, box
from shapely.ops import unary_union, polygonize from shapely.ops import unary_union, polygonize
from update_u1_graphics import verify
ROOT = Path(__file__).resolve().parent ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb" PCB = ROOT / "serial-carrier.kicad_pcb"
@@ -38,6 +39,7 @@ def outline_lines(shape):
def add_outlines(board): def add_outlines(board):
verify(board)
owned = {str(uuid.uuid5(NS, str(i))) for i in range(4096)} owned = {str(uuid.uuid5(NS, str(i))) for i in range(4096)}
for drawing in list(board.GetDrawings()): for drawing in list(board.GetDrawings()):
if drawing.m_Uuid.AsString() in owned: if drawing.m_Uuid.AsString() in owned:
@@ -82,8 +84,11 @@ def add_outlines(board):
board.Add(shape) board.Add(shape)
count+=1 count+=1
def line(a,b): def line(a,b,width=WIDTH):
clipped=LineString([a,b]).intersection(silk_area).difference(blocked) extra = max(0, (width-WIDTH)/2)
area = silk_area.buffer(-extra) if extra else silk_area
obstacles = blocked.buffer(extra) if extra else blocked
clipped=LineString([a,b]).intersection(area).difference(obstacles)
pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped] pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped]
for piece in pieces: for piece in pieces:
if piece.geom_type!="LineString" or piece.length < 0.25: if piece.geom_type!="LineString" or piece.length < 0.25:
@@ -91,7 +96,7 @@ def add_outlines(board):
shape=p.PCB_SHAPE(board) shape=p.PCB_SHAPE(board)
shape.SetShape(p.SHAPE_T_SEGMENT) shape.SetShape(p.SHAPE_T_SEGMENT)
shape.SetStart(vec(piece.coords[0])); shape.SetEnd(vec(piece.coords[-1])) shape.SetStart(vec(piece.coords[0])); shape.SetEnd(vec(piece.coords[-1]))
shape.SetWidth(p.FromMM(WIDTH)) shape.SetWidth(p.FromMM(width))
add(shape) add(shape)
for ref in ("DS1","U2"): for ref in ("DS1","U2"):
@@ -137,22 +142,19 @@ def add_outlines(board):
distance+=1.3 distance+=1.3
else: else:
line(edge.coords[0],edge.coords[-1]) line(edge.coords[0],edge.coords[-1])
# These are pad-row guides, not inferred socket/body or ESP32 PCB dimensions. # Union removes the shared body/antenna edge duplication. Mouth spans replace
fp=footprints["U1"] # the coincident bottom-edge strokes, with their measured library line weight;
assert fp.GetOrientationDegrees()==0, "Review guide construction for rotated U1" # this marks width only, without inventing connector depth or an RF keepout.
pads={pad.GetNumber():xy(pad.GetPosition()) for pad in fp.Pads()} sources=[g for g in footprints["U1"].GraphicalItems()
for first,last in (("1","22"),("23","44")): if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab]
x,ytop=pads[first]; _,ybottom=pads[last] mouths=[g for g in sources if g.GetShape()==p.SHAPE_T_SEGMENT]
corners=[(x-1.2,ytop-1.2),(x+1.2,ytop-1.2),(x+1.2,ybottom+1.2),(x-1.2,ybottom+1.2)] mouth_lines=unary_union([edge for g in mouths for edge in outline_lines(g)])
for a,b in zip(corners,corners[1:]+corners[:1]): perimeter=unary_union([edge for g in sources if g.GetShape()==p.SHAPE_T_RECT
line(a,b) for edge in outline_lines(g)]).difference(mouth_lines)
label=p.PCB_TEXT(board) for edge in perimeter.geoms:
label.SetText("HEADER GUIDES") line(edge.coords[0],edge.coords[-1])
a,b=pads["1"],pads["23"] for mouth in mouths:
label.SetPosition(vec(((a[0]+b[0])/2,a[1]+28))) line(xy(mouth.GetStart()),xy(mouth.GetEnd()),p.ToMM(mouth.GetWidth()))
label.SetTextSize(p.VECTOR2I(p.FromMM(0.85),p.FromMM(0.85)))
label.SetTextThickness(p.FromMM(0.12))
add(label)
return count return count
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Export a 1:1 placement PDF with measured HW678 geometry, without editing CAD.
Export the saved board's verified measured U1 graphics. Only review captions
change on a temporary plotting board; no library overlay hides stale CAD.
"""
from pathlib import Path
import hashlib
import subprocess
import tempfile
import pcbnew as p
from update_u1_graphics import verify
ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
PDF = ROOT / "pcb-placement-draft.pdf"
def export():
original = PCB.read_bytes()
board = p.LoadBoard(str(PCB))
verify(board)
for drawing in board.GetDrawings():
if isinstance(drawing, p.PCB_TEXT):
if drawing.GetText() == "Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending":
drawing.SetText("Measured ESP32 outline / USB mouths; RF / plug / assembly clearance pending")
board.GetTitleBlock().SetComment(0, "Measured planar outlines; verify RF / plug / assembly clearance")
with tempfile.TemporaryDirectory(prefix="carrier-placement-") as temporary:
draft = Path(temporary) / PCB.name
result = Path(temporary) / PDF.name
if not p.SaveBoard(str(draft), board, True):
raise RuntimeError("Cannot save temporary plotting board")
subprocess.run([
"kicad-cli", "pcb", "export", "pdf", "--mode-single",
"--layers", "F.Fab,F.SilkS,Dwgs.User,Edge.Cuts",
"--sketch-pads-on-fab-layers", "--black-and-white", "--scale", "1",
"--output", str(result), str(draft),
], check=True)
if PCB.read_bytes() != original:
raise RuntimeError("Source PCB changed during export; PDF not replaced")
PDF.write_bytes(result.read_bytes())
print(f"Exported {PDF}; scale 1:1; source PCB unchanged")
print(f"Source PCB SHA256: {hashlib.sha256(original).hexdigest()}")
if __name__ == "__main__":
export()
+4 -4
View File
@@ -114,9 +114,9 @@ def main():
settings.m_ViasMinSize=mm(0.7) settings.m_ViasMinSize=mm(0.7)
# Proposed in-memory draft rules only; never overwrite the user's project settings. # Proposed in-memory draft rules only; never overwrite the user's project settings.
title=board.GetTitleBlock() title=board.GetTitleBlock()
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier draft") title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier")
title.SetRevision("A1 DRAFT") title.SetRevision("A1")
title.SetComment(0,"NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly") title.SetComment(0,"USB / antenna geometry pending; verify module pinout / assembly")
pad_nets={} pad_nets={}
for source_net in source.findall("nets/net"): for source_net in source.findall("nets/net"):
net=p.NETINFO_ITEM(board,source_net.get("name")) net=p.NETINFO_ITEM(board,source_net.get("name"))
@@ -194,7 +194,7 @@ def main():
text(board,"26mm OLED",56,52) text(board,"26mm OLED",56,52)
for label,x in zip(("BACK","SELECT","NEXT"),(46.25,58.25,70.25)): for label,x in zip(("BACK","SELECT","NEXT"),(46.25,58.25,70.25)):
text(board,label,x,75, size=0.9) text(board,label,x,75, size=0.9)
text(board,"DRAFT - NOT FOR FAB",56,78,size=0.9)
text(board,"82 x 80 mm / 2 copper layers",41,-5,p.Dwgs_User,1.5) text(board,"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) text(board,"Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending",41,-2.5,p.Dwgs_User,0.9)
board.BuildConnectivity() board.BuildConnectivity()
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Remove presentation-only PCB draft labels; retain all technical cautions."""
from pathlib import Path
import tempfile
import pcbnew as p
ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
TITLE = "Serial Swiss Army Knife - 82x80 mm carrier"
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
SILK = "DRAFT - NOT FOR FAB"
def remove_labels(board):
title = board.GetTitleBlock()
assert title.GetTitle() in (TITLE, TITLE + " draft")
assert title.GetRevision() in ("A1", "A1 DRAFT")
assert title.GetComment(0) in (COMMENT, "NOT FOR FABRICATION - " + COMMENT)
title.SetTitle(TITLE)
title.SetRevision("A1")
title.SetComment(0, COMMENT)
for drawing in list(board.GetDrawings()):
if isinstance(drawing, p.PCB_TEXT) and drawing.GetText() == SILK:
assert drawing.GetLayer() == p.F_SilkS
board.Remove(drawing)
def expected_tree(tree):
"""Exact permitted delta for preservation audits, independent of pcbnew edits."""
import copy
result = copy.deepcopy(tree)
for item in list(result):
if not isinstance(item, list):
continue
if item[0] == "title_block":
for field in item[1:]:
replacements = {
('title', '"' + TITLE + ' draft"'): '"' + TITLE + '"',
('rev', '"A1 DRAFT"'): '"A1"',
('comment', '"NOT FOR FABRICATION - ' + COMMENT + '"'): '"' + COMMENT + '"',
}
replacement = replacements.get((field[0], field[-1]))
if replacement is not None:
field[-1] = replacement
elif item[0] == "gr_text" and item[1] == '"' + SILK + '"':
assert ['layer', '"F.SilkS"'] in item
result.remove(item)
return result
def main():
from validate_u1_update import parse, canonical
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
if lock.exists():
raise RuntimeError("Close PCB Editor before changing labels")
original = PCB.read_bytes()
settings = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
expected = expected_tree(parse(PCB))
board = p.LoadBoard(str(PCB))
remove_labels(board)
with tempfile.TemporaryDirectory(prefix="carrier-labels-", dir=ROOT) as temporary:
result = Path(temporary) / PCB.name
assert p.SaveBoard(str(result), board, True)
assert canonical(parse(result)) == canonical(expected), "Change beyond exact label edits"
assert canonical(expected_tree(parse(result))) == canonical(expected), "Label cleanup not idempotent"
if lock.exists() or PCB.read_bytes() != original:
raise RuntimeError("PCB changed or reopened; refusing overwrite")
assert all(path.read_bytes() == data for path, data in settings.items()), "Project settings changed"
result.replace(PCB)
print("Removed draft presentation labels; exact preservation and idempotence passed.")
if __name__ == "__main__":
main()
+175 -123
View File
@@ -8,9 +8,9 @@
) )
(paper "A4") (paper "A4")
(title_block (title_block
(title "Serial Swiss Army Knife - 82x80 mm carrier draft") (title "Serial Swiss Army Knife - 82x80 mm carrier")
(rev "A1 DRAFT") (rev "A1")
(comment 1 "NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly") (comment 1 "USB / antenna geometry pending; verify module pinout / assembly")
) )
(layers (layers
(0 "F.Cu" signal) (0 "F.Cu" signal)
@@ -562,6 +562,70 @@
(sheetfile "serial-carrier.kicad_sch") (sheetfile "serial-carrier.kicad_sch")
(attr through_hole) (attr through_hole)
(duplicate_pad_numbers_are_jumpers no) (duplicate_pad_numbers_are_jumpers no)
(fp_line
(start 2.795 54.705)
(end 11.665 54.705)
(stroke
(width 0.3)
(type solid)
)
(layer "F.Fab")
(uuid "6434f4c9-cb7e-5b60-902d-bd13b5c57f13")
)
(fp_line
(start 14.195 54.705)
(end 23.065 54.705)
(stroke
(width 0.3)
(type solid)
)
(layer "F.Fab")
(uuid "a4082d84-0a5f-5edd-b97f-afbb68b73336")
)
(fp_rect
(start -1.365 -1.365)
(end 26.725 54.705)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.Fab")
(uuid "53b4b62c-705f-584c-9987-8e20cbcb0eae")
)
(fp_rect
(start 4.045 -8.255)
(end 21.675 -1.365)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.Fab")
(uuid "1a629168-d420-520b-820e-a4de5828228b")
)
(fp_text user "ANTENNA BOUNDS - NOT RF KEEPOUT"
(at 12.7 -9.5 0)
(layer "F.Fab")
(uuid "acabe363-725d-5486-8c68-8e06b5758ceb")
(effects
(font
(size 0.8 0.8)
(thickness 0.12)
)
)
)
(fp_text user "USB SHELL MOUTHS ONLY"
(at 12.7 59 0)
(layer "F.Fab")
(uuid "4af1d9b9-5ec1-5ef3-a72d-b72b93ecc391")
(effects
(font
(size 0.8 0.8)
(thickness 0.12)
)
)
)
(pad "1" thru_hole rect (pad "1" thru_hole rect
(at 0 0) (at 0 0)
(size 1.7 1.7) (size 1.7 1.7)
@@ -3025,8 +3089,8 @@
(embedded_fonts no) (embedded_fonts no)
) )
(gr_line (gr_line
(start 106.8 121.8) (start 106.635 121.635)
(end 109.2 121.8) (end 112.045 121.635)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
@@ -3035,78 +3099,8 @@
(uuid "cdd870c2-6daf-560d-a619-6903566202a1") (uuid "cdd870c2-6daf-560d-a619-6903566202a1")
) )
(gr_line (gr_line
(start 106.8 177.54) (start 106.635 177.705)
(end 106.8 121.8) (end 106.635 121.635)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "1576658b-4eb9-5105-9817-e374c6ef271e")
)
(gr_line
(start 109.2 121.8)
(end 109.2 177.54)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "8f746121-d8e6-5a89-9328-da46f2da8ac0")
)
(gr_line
(start 109.2 177.54)
(end 106.8 177.54)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "bfbd0b33-54e8-590f-87da-4935db08a121")
)
(gr_line
(start 132.2 121.8)
(end 134.6 121.8)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "fef8b02b-8a5c-5e27-8e75-fc116ddbd526")
)
(gr_line
(start 132.2 136.509273)
(end 132.2 121.8)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "abb27fb8-ae0c-56d1-9e35-c66b92681547")
)
(gr_line
(start 132.2 162.671905)
(end 132.2 137.390726)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "03740f02-7c63-574b-ad1d-eeaa2c77d2ea")
)
(gr_line
(start 132.2 170.202783)
(end 132.2 163.828094)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "2cecac83-165e-5e49-a695-c0b09f3b631c")
)
(gr_line
(start 132.2 177.54)
(end 132.2 171.397216)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
@@ -3115,48 +3109,48 @@
(uuid "5473bcda-98ee-5a28-9fda-f27d137d534c") (uuid "5473bcda-98ee-5a28-9fda-f27d137d534c")
) )
(gr_line (gr_line
(start 134.6 121.8) (start 110.795 177.705)
(end 134.6 130.421905) (end 106.635 177.705)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
) )
(layer "F.SilkS") (layer "F.SilkS")
(uuid "0445e79c-43e4-5609-90ad-67bffdf64341") (uuid "2441022c-c903-5a57-a84c-86f0a5143cb5")
) )
(gr_line (gr_line
(start 134.6 131.578094) (start 110.795 177.705)
(end 134.6 140.571905) (end 119.665 177.705)
(stroke
(width 0.3)
(type default)
)
(layer "F.SilkS")
(uuid "abb27fb8-ae0c-56d1-9e35-c66b92681547")
)
(gr_line
(start 112.045 121.635)
(end 112.045 119.28)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
) )
(layer "F.SilkS") (layer "F.SilkS")
(uuid "6405d812-d377-52d9-8013-8c19bd4cdc2b") (uuid "03740f02-7c63-574b-ad1d-eeaa2c77d2ea")
) )
(gr_line (gr_line
(start 134.6 141.728094) (start 112.045 121.635)
(end 134.6 160.952783) (end 129.675 121.635)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
) )
(layer "F.SilkS") (layer "F.SilkS")
(uuid "f938fe4f-5ff2-577d-9bff-2827fb32ce22") (uuid "8f746121-d8e6-5a89-9328-da46f2da8ac0")
) )
(gr_line (gr_line
(start 134.6 162.147216) (start 122.195 177.705)
(end 134.6 169.559273) (end 119.665 177.705)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "d472d222-21c6-5a78-ab7f-18799bceab8a")
)
(gr_line
(start 134.6 170.440726)
(end 134.6 177.54)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
@@ -3165,14 +3159,94 @@
(uuid "bfa82c3d-e8e2-58ea-8b46-f0db41e84458") (uuid "bfa82c3d-e8e2-58ea-8b46-f0db41e84458")
) )
(gr_line (gr_line
(start 134.6 177.54) (start 122.195 177.705)
(end 132.2 177.54) (end 131.065 177.705)
(stroke
(width 0.3)
(type default)
)
(layer "F.SilkS")
(uuid "2b9b8e99-a473-51bc-8241-448388f63fcb")
)
(gr_line
(start 129.675 119.28)
(end 129.675 121.635)
(stroke (stroke
(width 0.12) (width 0.12)
(type default) (type default)
) )
(layer "F.SilkS") (layer "F.SilkS")
(uuid "2441022c-c903-5a57-a84c-86f0a5143cb5") (uuid "2cecac83-165e-5e49-a695-c0b09f3b631c")
)
(gr_line
(start 129.675 121.635)
(end 134.725 121.635)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "bfbd0b33-54e8-590f-87da-4935db08a121")
)
(gr_line
(start 134.725 121.635)
(end 134.725 130.382575)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "1576658b-4eb9-5105-9817-e374c6ef271e")
)
(gr_line
(start 134.725 131.617424)
(end 134.725 140.532575)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "fef8b02b-8a5c-5e27-8e75-fc116ddbd526")
)
(gr_line
(start 134.725 141.767424)
(end 134.725 160.924667)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "0445e79c-43e4-5609-90ad-67bffdf64341")
)
(gr_line
(start 134.725 162.175332)
(end 134.725 169.460337)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "6405d812-d377-52d9-8013-8c19bd4cdc2b")
)
(gr_line
(start 134.725 170.539662)
(end 134.725 177.705)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "f938fe4f-5ff2-577d-9bff-2827fb32ce22")
)
(gr_line
(start 134.725 177.705)
(end 131.065 177.705)
(stroke
(width 0.12)
(type default)
)
(layer "F.SilkS")
(uuid "d472d222-21c6-5a78-ab7f-18799bceab8a")
) )
(gr_line (gr_line
(start 141.095 100.28) (start 141.095 100.28)
@@ -4050,17 +4124,6 @@
(layer "Edge.Cuts") (layer "Edge.Cuts")
(uuid "17ad1955-d7db-47eb-a736-baff736aac39") (uuid "17ad1955-d7db-47eb-a736-baff736aac39")
) )
(gr_text "HEADER GUIDES"
(at 120.7 151 0)
(layer "F.SilkS")
(uuid "2b9b8e99-a473-51bc-8241-448388f63fcb")
(effects
(font
(size 0.85 0.85)
(thickness 0.12)
)
)
)
(gr_text "BACK" (gr_text "BACK"
(at 146.25 175 0) (at 146.25 175 0)
(layer "F.SilkS") (layer "F.SilkS")
@@ -4072,17 +4135,6 @@
) )
) )
) )
(gr_text "DRAFT - NOT FOR FAB"
(at 156 178 0)
(layer "F.SilkS")
(uuid "875ba70f-7139-4924-a69c-d2e171f3f526")
(effects
(font
(size 0.9 0.9)
(thickness 0.15)
)
)
)
(gr_text "SELECT" (gr_text "SELECT"
(at 158.25 175 0) (at 158.25 175 0)
(layer "F.SilkS") (layer "F.SilkS")
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Synchronize measured U1 F.Fab graphics only; never replace the footprint."""
from pathlib import Path
import uuid
import pcbnew as p
ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
NS = uuid.UUID("36693251-e1f8-44e9-a8a9-e201bd658ceb")
def library_graphics():
fp = p.FootprintLoad(str(ROOT / "Carrier.pretty"), "HW678_2x22_Provisional")
if fp is None:
raise RuntimeError("Cannot load measured HW678 footprint")
return fp
def signature(g, origin):
def point(v):
return v.x-origin.x, v.y-origin.y
if isinstance(g, p.PCB_SHAPE):
return ("shape", g.GetShape(), point(g.GetStart()), point(g.GetEnd()), g.GetWidth())
return ("text", g.GetText(), point(g.GetPosition()),
(g.GetTextSize().x, g.GetTextSize().y), g.GetTextThickness())
def measured_items(fp):
# Generic library captions would overlap existing board labels.
return sorted((g for g in fp.GraphicalItems() if g.GetLayer() == p.F_Fab
and not (isinstance(g, p.PCB_TEXT) and g.GetText() in (
"PROVISIONAL CONNECTOR FIT", "ANTENNA UP / USB DOWN"))),
key=lambda g: signature(g, fp.GetPosition()))
def verify(board):
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
assert len([g for g in u1.GraphicalItems() if g.GetLayer() == p.F_Fab]) == 6
library = library_graphics()
expected = [signature(g, library.GetPosition()) for g in measured_items(library)]
actual = [signature(g, u1.GetPosition()) for g in measured_items(u1)]
assert actual == expected, "Embedded U1 F.Fab differs from measured library; run update_u1_graphics.py"
def main():
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
if lock.exists():
raise SystemExit("Close PCB Editor before updating U1 graphics")
before = PCB.read_bytes()
project = ROOT / "serial-carrier.kicad_pro"
project_before = project.read_bytes()
board = p.LoadBoard(str(PCB))
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
library = library_graphics()
sources = measured_items(library)
assert len(sources) == 6, "Review changed measured library before integration"
owned = {str(uuid.uuid5(NS, str(i))) for i in range(len(sources))}
existing = list(u1.GraphicalItems())
assert all(g.m_Uuid.AsString() in owned for g in existing if g.GetLayer() == p.F_Fab), "Unowned U1 fabrication graphics; review manually"
if [signature(g, u1.GetPosition()) for g in measured_items(u1)] == [
signature(g, library.GetPosition()) for g in sources]:
verify(board)
print("Embedded measured U1 graphics already current; no board write")
return
for g in existing:
if g.m_Uuid.AsString() in owned:
u1.Remove(g)
for i, source in enumerate(sources):
graphic = source.Duplicate()
graphic.SetParent(u1)
graphic.SetUuid(p.KIID(str(uuid.uuid5(NS, str(i)))))
graphic.Move(u1.GetPosition() - library.GetPosition())
u1.Add(graphic)
verify(board)
temp = ROOT / "validation/pcb-u1-tmp.kicad_pcb"
try:
assert p.SaveBoard(str(temp), board, True)
verify(p.LoadBoard(str(temp)))
if lock.exists() or PCB.read_bytes() != before:
raise RuntimeError("PCB changed or reopened; refusing to overwrite")
assert project.read_bytes() == project_before, "Project settings changed"
temp.replace(PCB)
finally:
temp.unlink(missing_ok=True)
print("Embedded measured U1 graphics; existing footprint fields, pads and routing retained")
if __name__ == "__main__":
main()
+17 -3
View File
@@ -117,9 +117,23 @@ class FootprintRegression(unittest.TestCase):
for row, x in enumerate((0, 25.4)) for index in range(22) for row, x in enumerate((0, 25.4)) for index in range(22)
} }
self.check_header(footprint, positions) self.check_header(footprint, positions)
self.assertFalse(any(isinstance(g, k.PCB_SHAPE) shapes = [g for g in footprint.GraphicalItems() if isinstance(g, k.PCB_SHAPE)]
for g in footprint.GraphicalItems()), expected = [
"HW678 must not acquire guessed outlines or envelopes") (k.SHAPE_T_RECT, mm(-1.365, -1.365), mm(26.725, 54.705), k.FromMM(0.1)),
(k.SHAPE_T_RECT, mm(4.045, -8.255), mm(21.675, -1.365), k.FromMM(0.1)),
(k.SHAPE_T_SEGMENT, mm(2.795, 54.705), mm(11.665, 54.705), k.FromMM(0.3)),
(k.SHAPE_T_SEGMENT, mm(14.195, 54.705), mm(23.065, 54.705), k.FromMM(0.3)),
]
self.assertEqual(Counter((g.GetShape(), xy(g.GetStart()), xy(g.GetEnd()),
g.GetWidth()) for g in shapes), Counter(expected))
for shape in shapes:
self.assertEqual(shape.GetLayer(), k.F_Fab)
self.assertFalse(shape.IsAnyFill())
# Only measured planar bounds/mouth spans: no invented shell depth or RF area.
self.assertAlmostEqual(26.725 - (-1.365), 28.09)
self.assertAlmostEqual(54.705 - (-1.365), 56.07)
self.assertAlmostEqual(54.705 - (-8.255), 62.96)
self.assertAlmostEqual(26.725 - 25.4, 1.325)
if __name__ == "__main__": if __name__ == "__main__":
+3 -1
View File
@@ -10,6 +10,7 @@ from pathlib import Path
import subprocess import subprocess
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import pcbnew as p import pcbnew as p
from update_u1_graphics import verify
ROOT=Path(__file__).resolve().parent ROOT=Path(__file__).resolve().parent
PCB=ROOT/"serial-carrier.kicad_pcb" PCB=ROOT/"serial-carrier.kicad_pcb"
@@ -21,6 +22,7 @@ def run(*args):
run("kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(OUT/"pcb-source-netlist.xml"),str(ROOT/"serial-carrier.kicad_sch")) run("kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(OUT/"pcb-source-netlist.xml"),str(ROOT/"serial-carrier.kicad_sch"))
run("kicad-cli","pcb","drc","--format","json","--all-track-errors","--severity-all","--schematic-parity","-o",str(OUT/"pcb-parity-drc.json"),str(PCB)) 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)) board=p.LoadBoard(str(PCB))
verify(board)
source=ET.parse(OUT/"pcb-source-netlist.xml").getroot() source=ET.parse(OUT/"pcb-source-netlist.xml").getroot()
comps={c.get("ref"):c for c in source.findall("components/comp")} comps={c.get("ref"):c for c in source.findall("components/comp")}
footprints={f.GetReference():f for f in board.GetFootprints()} footprints={f.GetReference():f for f in board.GetFootprints()}
@@ -121,7 +123,7 @@ summary={
"known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed", "known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
"project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(), "project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(),
"ignored_DRC_checks":report["ignored_checks"], "ignored_DRC_checks":report["ignored_checks"],
"not_validated":["male module pin map/header fit", "ESP32 geometry and complete assembly envelope/heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"], "not_validated":["male module pin map/header fit", "ESP32 measured-planar 1:1 fit and complete assembly envelope/heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"],
} }
(OUT/"pcb-validation-summary.json").write_text(json.dumps(summary,indent=2)+"\n") (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("PCB draft verified: 82 x 80 mm; 2 copper layers; 0 physical DRC violations; 0 unrouted.")
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Measured U1 integration regression and exact pre-update preservation audit."""
import hashlib
import json
from pathlib import Path
import re
import tempfile
import uuid
import pcbnew as p
from shapely.geometry import Polygon
from add_device_silkscreen import add_outlines, NS as SILK_NS, xy, outline_lines
from update_u1_graphics import verify, NS as U1_NS
from remove_draft_markers import expected_tree
ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
BASE = ROOT / "validation/pcb-before-u1-measured.kicad_pcb"
def parse(path):
tokens = iter(re.findall(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+', path.read_text()))
def group():
result = []
for token in tokens:
if token == ')':
return result
result.append(group() if token == '(' else token)
raise ValueError("Unterminated S-expression")
assert next(tokens) == '('
return group()
def ident(item):
return next((v[1].strip('"') for v in item if isinstance(v,list) and v[0]=='uuid'), None)
def canonical(item, excluded=frozenset()):
# KiCad can reorder independent board items when saving; compare their full
# parsed contents, not byte order. Pad coordinates/netnames/UUIDs remain exact.
if not isinstance(item, list):
return item
values = [canonical(v, excluded) for v in item
if not isinstance(v,list) or ident(v) not in excluded]
if item and item[0] in ("kicad_pcb", "footprint"):
return ([v for v in values if not isinstance(v,list)] +
sorted((v for v in values if isinstance(v,list)), key=repr))
# Coordinates, polygon vertex order, text and other ordered values must
# remain ordered: sorting these could conceal an X/Y swap or zone edit.
return values
def main():
assert hashlib.sha256(BASE.read_bytes()).hexdigest() == "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045", "Pre-update snapshot changed"
assert hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest() == "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516", "Project settings changed"
before = PCB.read_bytes()
board = p.LoadBoard(str(PCB))
verify(board)
silk_ids = {str(uuid.uuid5(SILK_NS,str(i))) for i in range(4096)}
u1_ids = {str(uuid.uuid5(U1_NS,str(i))) for i in range(6)}
# The original baseline retains historical draft labels. Permit only the
# exact requested title/revision/comment edits and removal of the silk label.
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(PCB), silk_ids | u1_ids), "Change outside owned U1 additions / device silk / exact presentation labels"
generated = [g for g in board.GetDrawings() if g.m_Uuid.AsString() in silk_ids]
assert not any(isinstance(g,p.PCB_TEXT) and g.GetText()=="HEADER GUIDES" for g in board.GetDrawings())
carrier = Polygon([(136,100),(182,100),(182,180),(100,180),(100,119),(136,119)])
mouths = []
for g in generated:
assert g.GetLayer() == p.F_SilkS
for edge in outline_lines(g):
assert carrier.covers(edge.buffer(p.ToMM(g.GetWidth())/2)), "Silk outside carrier/notch"
if g.GetWidth() == p.FromMM(0.3):
mouths.append(g)
assert len(mouths) == 2
assert sorted((xy(g.GetStart()),xy(g.GetEnd())) for g in mouths) == [
((110.795,177.705),(119.665,177.705)),
((122.195,177.705),(131.065,177.705))]
# A second refresh must reproduce all drawing geometry and UUIDs.
add_outlines(board)
with tempfile.TemporaryDirectory(prefix="u1-regression-") as temporary:
result = Path(temporary)/PCB.name
assert p.SaveBoard(str(result),board,True)
assert canonical(parse(result)) == canonical(parse(PCB)), "Silkscreen refresh not idempotent"
u1 = next(f for f in board.GetFootprints() if f.GetReference()=="U1")
shape = next(g for g in u1.GraphicalItems() if isinstance(g,p.PCB_SHAPE))
shape.SetWidth(shape.GetWidth()+1)
try:
verify(board)
except AssertionError:
pass
else:
raise AssertionError("Stale embedded geometry was not rejected")
assert PCB.read_bytes() == before
assert not (ROOT/"~serial-carrier.kicad_pcb.lck").exists()
summary = dict(measured_library_matches_embedded_u1=True,
existing_uuids_pads_nets_placement_routing_settings_and_unrelated_graphics_preserved=True,
presentation_label_cleanup=True,
silk_refresh_idempotent=True, stale_geometry_negative_control=True,
silk_clipped_to_carrier=True, usb_mouth_markers=2,
generated_silk_items=len(generated),
baseline_sha256=hashlib.sha256(BASE.read_bytes()).hexdigest(),
board_sha256=hashlib.sha256(before).hexdigest(),
project_sha256=hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest())
(ROOT/"validation/pcb-u1-update.json").write_text(json.dumps(summary,indent=2)+"\n")
print(json.dumps(summary,indent=2))
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 345 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 368 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schemas.kicad.org/drc.v1.json", "$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm", "coordinate_units": "mm",
"date": "2026-09-21T11:39:10", "date": "2026-09-21T22:52:26",
"ignored_checks": [ "ignored_checks": [
{ {
"description": "Footprint has no courtyard defined", "description": "Footprint has no courtyard defined",
@@ -1,6 +1,6 @@
{ {
"immutable_geometry_nets_pads_linkage_preserved": true, "immutable_geometry_nets_pads_linkage_preserved": true,
"board_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045", "board_sha256": "94e941625b3b9fc9a08b9f2d532eb35003ea8da41d456a1c67269b9651200e05",
"independent_min_different_net_or_npth_clearance_mm": 0.2705441169852849, "independent_min_different_net_or_npth_clearance_mm": 0.2705441169852849,
"closest_items": [ "closest_items": [
"U1.38:4b8eabef-8493-437d-bd02-cb9ff4afd7cd", "U1.38:4b8eabef-8493-437d-bd02-cb9ff4afd7cd",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schemas.kicad.org/drc.v1.json", "$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm", "coordinate_units": "mm",
"date": "2026-09-21T11:39:13", "date": "2026-09-21T22:33:53",
"ignored_checks": [ "ignored_checks": [
{ {
"description": "Footprint has no courtyard defined", "description": "Footprint has no courtyard defined",
@@ -2,7 +2,7 @@
<export version="E"> <export version="E">
<design> <design>
<source>/home/mscholz/Repos/ESP32_serial_swiss_army_knife/hardware/PCB/serial-carrier.kicad_sch</source> <source>/home/mscholz/Repos/ESP32_serial_swiss_army_knife/hardware/PCB/serial-carrier.kicad_sch</source>
<date>2026-09-21T11:39:08</date> <date>2026-09-21T22:52:19</date>
<tool>Eeschema 10.0.6-1.fc44</tool> <tool>Eeschema 10.0.6-1.fc44</tool>
<sheet number="1" name="/" tstamps="/"> <sheet number="1" name="/" tstamps="/">
<title_block> <title_block>
@@ -0,0 +1,13 @@
{
"measured_library_matches_embedded_u1": true,
"existing_uuids_pads_nets_placement_routing_settings_and_unrelated_graphics_preserved": true,
"presentation_label_cleanup": true,
"silk_refresh_idempotent": true,
"stale_geometry_negative_control": true,
"silk_clipped_to_carrier": true,
"usb_mouth_markers": 2,
"generated_silk_items": 91,
"baseline_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
"board_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
"project_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516"
}
@@ -130,7 +130,7 @@
], ],
"not_validated": [ "not_validated": [
"male module pin map/header fit", "male module pin map/header fit",
"ESP32 geometry and complete assembly envelope/heights", "ESP32 measured-planar 1:1 fit and complete assembly envelope/heights",
"header/socket fit", "header/socket fit",
"RF performance and antenna clearance", "RF performance and antenna clearance",
"power/return-path integrity", "power/return-path integrity",