Integrate A1 carrier PCB CAD updates
- Add pinned STEP-derived DE-9 mechanical measurements and provenance - Refresh placement, routing, silkscreen, and nominal CAD envelopes - Document provisional dimensions, assumptions, and validation status
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), [architecture](architecture.md) and [decisions](design-decisions.md).
|
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), [architecture](architecture.md) and [decisions](design-decisions.md).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- Adafruit STEP metal geometry extracted and validated; provenance/dimensions in `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.
|
||||||
|
|
||||||
## Dual-stack diagnostics follow-up — implemented, device validation pending
|
## Dual-stack diagnostics follow-up — implemented, device validation pending
|
||||||
|
|
||||||
- User explicitly requested IPv6 ping/traceroute/nslookup on all surfaces after accepting Phase12. Shared `network_console.*` now supports one `-4`/`-6` anywhere after command, preserves count/hop bounds, root and `wifi` aliases. Default probes try IPv4 then IPv6 on resolution failure only; nslookup performs A+AAAA queries separately (one selected address/family), displays unscoped link-local records. Link-local probes require ESP-local `%sta`/`%ap`/index/name; no client-interface guess or IPv4-mapped fallback. Help/completion updated; permissions unchanged.
|
- User explicitly requested IPv6 ping/traceroute/nslookup on all surfaces after accepting Phase12. Shared `network_console.*` now supports one `-4`/`-6` anywhere after command, preserves count/hop bounds, root and `wifi` aliases. Default probes try IPv4 then IPv6 on resolution failure only; nslookup performs A+AAAA queries separately (one selected address/family), displays unscoped link-local records. Link-local probes require ESP-local `%sta`/`%ap`/index/name; no client-interface guess or IPv4-mapped fallback. Help/completion updated; permissions unchanged.
|
||||||
|
|||||||
+10
-3
@@ -4,7 +4,8 @@ Open **`serial-carrier.kicad_pro`** in KiCad 10, then open
|
|||||||
**`serial-carrier.kicad_sch`**. A readable export is **`serial-carrier.pdf`**.
|
**`serial-carrier.kicad_sch`**. A readable export is **`serial-carrier.pdf`**.
|
||||||
This is a native, editable schematic with actual connected nets, not an image
|
This is a native, editable schematic with actual connected nets, not an image
|
||||||
or a SKiDL-only netlist. **`serial-carrier.kicad_pcb` now contains a routed,
|
or a SKiDL-only netlist. **`serial-carrier.kicad_pcb` now contains a routed,
|
||||||
82 × 80 mm two-copper-layer carrier draft.** It is **not fabrication-ready**.
|
82 × 80 mm two-copper-layer A1 carrier draft with a top-left antenna notch,
|
||||||
|
USB toward the bottom and RS-232 toward the top.** It is **not fabrication-ready**.
|
||||||
See **`pcb-draft-notes.md`** for placement, chosen dimensions/rules, provisional
|
See **`pcb-draft-notes.md`** for placement, chosen dimensions/rules, provisional
|
||||||
mechanical assumptions and validation. A review drawing is `pcb-placement-draft.pdf`.
|
mechanical assumptions and validation. A review drawing is `pcb-placement-draft.pdf`.
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@ 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 |
|
||||||
|
| `db9-mechanical-notes.md`, `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation |
|
||||||
|
| `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 |
|
||||||
| `oled-fit-check.svg` | User-fit-verified OLED mechanical template |
|
| `oled-fit-check.svg` | User-fit-verified OLED mechanical template |
|
||||||
@@ -124,7 +128,7 @@ Repository sources: `../../docs/wiring.md`, `../../src/board_pins.h`,
|
|||||||
| Assembly | Repository identification | Footprint status |
|
| 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. |
|
| 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 | Published female CAD pin map and geometry verified; male mapping/fit remains provisional. Reference footprint used on PCB only; schematic assignment remains blank. |
|
| 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. |
|
||||||
| 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. |
|
| 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. External 2.2 kΩ pull-ups included. Live distributor stock remains unverified. |
|
||||||
|
|
||||||
@@ -135,7 +139,10 @@ Published female-module Eagle CAD and its header map were retrieved and verified
|
|||||||
see `adafruit-research.md` for pinned revision, hashes, attribution and geometry.
|
see `adafruit-research.md` for pinned revision, hashes, attribution and geometry.
|
||||||
The original files and CC BY-SA 3.0 notices are retained in `reference/`; they
|
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
|
remain under their upstream license. Omron reference documents retain their
|
||||||
manufacturer notices. No male-specific module footprint is claimed verified.
|
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.
|
||||||
|
|
||||||
## Verified logical connections for the existing firmware profile
|
## Verified logical connections for the existing firmware profile
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Add nominal STEP-derived metal bounding projections on Dwgs.User only.
|
||||||
|
|
||||||
|
Not silkscreen, a courtyard, or a case cutout. See db9-mechanical-notes.md for
|
||||||
|
source attribution, selected face subsets and the user-confirmed male/female
|
||||||
|
external-geometry equivalence. No module height or case-wall thickness assumed.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
import pcbnew as p
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parent
|
||||||
|
PCB=ROOT/"serial-carrier.kicad_pcb"
|
||||||
|
NS=uuid.UUID("ba7274a8-c35d-46bc-908d-cd86708fc6c4")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
lock=ROOT/"~serial-carrier.kicad_pcb.lck"
|
||||||
|
if lock.exists():
|
||||||
|
raise SystemExit("Close PCB Editor before refreshing the CAD envelopes")
|
||||||
|
before=PCB.read_bytes()
|
||||||
|
project=ROOT/"serial-carrier.kicad_pro"
|
||||||
|
project_before=project.read_bytes()
|
||||||
|
board=p.LoadBoard(str(PCB))
|
||||||
|
u2=next(f for f in board.GetFootprints() if f.GetReference()=="U2")
|
||||||
|
assert u2.GetOrientationDegrees()==0, "Review transform for rotated module"
|
||||||
|
x0,y0=p.ToMM(u2.GetPosition().x),p.ToMM(u2.GetPosition().y)
|
||||||
|
data=json.loads((ROOT/"reference/db9-cad/measurements.json").read_text())
|
||||||
|
owned={str(uuid.uuid5(NS,str(i))) for i in range(64)}
|
||||||
|
for drawing in list(board.GetDrawings()):
|
||||||
|
if drawing.m_Uuid.AsString() in owned:
|
||||||
|
board.Delete(drawing)
|
||||||
|
count=0
|
||||||
|
|
||||||
|
def point(x,y):
|
||||||
|
# STEP/Eagle axes +Y toward connector; KiCad local Y is downward.
|
||||||
|
return p.VECTOR2I(p.FromMM(x0+x-1.905),p.FromMM(y0+2.667-y))
|
||||||
|
|
||||||
|
def add(item):
|
||||||
|
nonlocal count
|
||||||
|
assert count<64
|
||||||
|
item.SetLayer(p.Dwgs_User)
|
||||||
|
item.SetUuid(p.KIID(str(uuid.uuid5(NS,str(count)))))
|
||||||
|
board.Add(item)
|
||||||
|
count+=1
|
||||||
|
|
||||||
|
for feature in ("front_flange_exposed_faces", "shell_outer_including_root_and_lip",
|
||||||
|
"left_front_hex_post", "right_front_hex_post"):
|
||||||
|
xmin,ymin,_,xmax,ymax,_=data[feature]["bounds"]
|
||||||
|
rect=p.PCB_SHAPE(board)
|
||||||
|
rect.SetShape(p.SHAPE_T_RECT)
|
||||||
|
rect.SetStart(point(xmin,ymax)); rect.SetEnd(point(xmax,ymin))
|
||||||
|
rect.SetWidth(p.FromMM(0.1))
|
||||||
|
add(rect)
|
||||||
|
for axis in data["front_lock_axes"]:
|
||||||
|
x=axis["location"][0]
|
||||||
|
# These mark front-facing Y axes, not holes through the carrier PCB.
|
||||||
|
line=p.PCB_SHAPE(board)
|
||||||
|
line.SetShape(p.SHAPE_T_SEGMENT)
|
||||||
|
line.SetStart(point(x,26.907)); line.SetEnd(point(x,32.707))
|
||||||
|
line.SetWidth(p.FromMM(0.08))
|
||||||
|
add(line)
|
||||||
|
label=p.PCB_TEXT(board)
|
||||||
|
label.SetText("CAD METAL ENVELOPES - NOT CASE CUTOUT")
|
||||||
|
label.SetPosition(p.VECTOR2I(p.FromMM(x0+13.97),p.FromMM(y0-17.67)))
|
||||||
|
label.SetTextSize(p.VECTOR2I(p.FromMM(0.65),p.FromMM(0.65)))
|
||||||
|
label.SetTextThickness(p.FromMM(0.1))
|
||||||
|
add(label)
|
||||||
|
for drawing in board.GetDrawings():
|
||||||
|
if not isinstance(drawing,p.PCB_TEXT):
|
||||||
|
continue
|
||||||
|
value=drawing.GetText()
|
||||||
|
if value.startswith("DE-9 OUT /"):
|
||||||
|
drawing.SetPosition(p.VECTOR2I(p.FromMM(x0+13.97),p.FromMM(y0-20.67)))
|
||||||
|
elif value=="82 x 80 mm / 2 copper layers":
|
||||||
|
drawing.SetPosition(p.VECTOR2I(p.FromMM(141),p.FromMM(91)))
|
||||||
|
elif value.startswith("Nominal RS-232 metal verified;"):
|
||||||
|
drawing.SetPosition(p.VECTOR2I(p.FromMM(141),p.FromMM(93.5)))
|
||||||
|
temp=ROOT/"serial-carrier-envelope-tmp.kicad_pcb"
|
||||||
|
try:
|
||||||
|
assert p.SaveBoard(str(temp),board,True)
|
||||||
|
if lock.exists() or PCB.read_bytes()!=before:
|
||||||
|
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||||
|
assert project.read_bytes()==project_before
|
||||||
|
temp.replace(PCB)
|
||||||
|
finally:
|
||||||
|
temp.unlink(missing_ok=True)
|
||||||
|
print(f"Added {count} nominal CAD projection graphics on Dwgs.User; no copper/project changes")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Refresh board-local device outlines without modifying libraries or project settings.
|
||||||
|
|
||||||
|
Uses pcbnew and Shapely from system Python. Only owned F.SilkS graphics are
|
||||||
|
replaced; source F.Fab outlines, copper, footprints and board edges are retained.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import math
|
||||||
|
import uuid
|
||||||
|
import pcbnew as p
|
||||||
|
from shapely.geometry import LineString, Point, box
|
||||||
|
from shapely.ops import unary_union, polygonize
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||||
|
NS = uuid.UUID("67964aa4-c3a5-45d8-9b54-250fb68e7c41")
|
||||||
|
WIDTH = 0.12
|
||||||
|
GAP = 0.22 # Additional clearance from mask openings and existing silk strokes.
|
||||||
|
|
||||||
|
|
||||||
|
def xy(point):
|
||||||
|
return p.ToMM(point.x), p.ToMM(point.y)
|
||||||
|
|
||||||
|
|
||||||
|
def vec(point):
|
||||||
|
return p.VECTOR2I(p.FromMM(point[0]), p.FromMM(point[1]))
|
||||||
|
|
||||||
|
|
||||||
|
def outline_lines(shape):
|
||||||
|
if shape.GetShape() == p.SHAPE_T_RECT:
|
||||||
|
x1, y1 = xy(shape.GetStart())
|
||||||
|
x2, y2 = xy(shape.GetEnd())
|
||||||
|
return [LineString([(x1,y1),(x2,y1)]), LineString([(x2,y1),(x2,y2)]),
|
||||||
|
LineString([(x2,y2),(x1,y2)]), LineString([(x1,y2),(x1,y1)])]
|
||||||
|
if shape.GetShape() == p.SHAPE_T_SEGMENT:
|
||||||
|
return [LineString([xy(shape.GetStart()), xy(shape.GetEnd())])]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def add_outlines(board):
|
||||||
|
owned = {str(uuid.uuid5(NS, str(i))) for i in range(4096)}
|
||||||
|
for drawing in list(board.GetDrawings()):
|
||||||
|
if drawing.m_Uuid.AsString() in owned:
|
||||||
|
board.Delete(drawing)
|
||||||
|
footprints = {f.GetReference(): f for f in board.GetFootprints()}
|
||||||
|
obstacles = []
|
||||||
|
for fp in board.GetFootprints():
|
||||||
|
for pad in fp.Pads():
|
||||||
|
if not pad.IsOnLayer(p.F_Mask):
|
||||||
|
continue
|
||||||
|
assert pad.GetOrientationDegrees() % 90 == 0
|
||||||
|
x,y = xy(pad.GetPosition())
|
||||||
|
expansion = max(0, p.ToMM(pad.GetSolderMaskExpansion(p.F_Mask)))
|
||||||
|
if pad.GetShape() == p.PAD_SHAPE_CIRCLE:
|
||||||
|
geom = Point(x,y).buffer(p.ToMM(pad.GetSize().x)/2, quad_segs=32)
|
||||||
|
else:
|
||||||
|
bb=pad.GetBoundingBox()
|
||||||
|
geom=box(p.ToMM(bb.GetX()),p.ToMM(bb.GetY()),p.ToMM(bb.GetRight()),p.ToMM(bb.GetBottom()))
|
||||||
|
obstacles.append(geom.buffer(expansion+GAP+WIDTH/2))
|
||||||
|
for graphic in fp.GraphicalItems():
|
||||||
|
if isinstance(graphic,p.PCB_SHAPE) and graphic.GetLayer()==p.F_SilkS:
|
||||||
|
for line in outline_lines(graphic):
|
||||||
|
obstacles.append(line.buffer(p.ToMM(graphic.GetWidth())/2+GAP+WIDTH/2))
|
||||||
|
# Clear even tented vias for a robust visible guide, independent of mask settings.
|
||||||
|
for track in board.GetTracks():
|
||||||
|
if isinstance(track,p.PCB_VIA):
|
||||||
|
obstacles.append(Point(xy(track.GetPosition())).buffer(p.ToMM(track.GetWidth(p.F_Cu))/2+GAP+WIDTH/2,quad_segs=32))
|
||||||
|
blocked=unary_union(obstacles)
|
||||||
|
edges = [LineString([xy(d.GetStart()), xy(d.GetEnd())])
|
||||||
|
for d in board.GetDrawings() if d.GetLayer() == p.Edge_Cuts]
|
||||||
|
polygons = list(polygonize(edges))
|
||||||
|
assert len(polygons) == 1, 'Expected a single closed carrier outline'
|
||||||
|
# U2's bare PCB front is flush with the carrier edge; do not print across it.
|
||||||
|
silk_area = polygons[0].buffer(-(GAP + WIDTH / 2))
|
||||||
|
count=0
|
||||||
|
|
||||||
|
def add(shape):
|
||||||
|
nonlocal count
|
||||||
|
assert count < 4096
|
||||||
|
shape.SetLayer(p.F_SilkS)
|
||||||
|
shape.SetUuid(p.KIID(str(uuid.uuid5(NS,str(count)))))
|
||||||
|
board.Add(shape)
|
||||||
|
count+=1
|
||||||
|
|
||||||
|
def line(a,b):
|
||||||
|
clipped=LineString([a,b]).intersection(silk_area).difference(blocked)
|
||||||
|
pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped]
|
||||||
|
for piece in pieces:
|
||||||
|
if piece.geom_type!="LineString" or piece.length < 0.25:
|
||||||
|
continue
|
||||||
|
shape=p.PCB_SHAPE(board)
|
||||||
|
shape.SetShape(p.SHAPE_T_SEGMENT)
|
||||||
|
shape.SetStart(vec(piece.coords[0])); shape.SetEnd(vec(piece.coords[-1]))
|
||||||
|
shape.SetWidth(p.FromMM(WIDTH))
|
||||||
|
add(shape)
|
||||||
|
|
||||||
|
for ref in ("DS1","U2"):
|
||||||
|
assert footprints[ref].GetOrientationDegrees()==0, "Review outlines for rotated modules"
|
||||||
|
for source in footprints[ref].GraphicalItems():
|
||||||
|
if not isinstance(source,p.PCB_SHAPE) or source.GetLayer()!=p.F_Fab:
|
||||||
|
continue
|
||||||
|
if source.GetShape()==p.SHAPE_T_ARC:
|
||||||
|
# Test small spans for clearance, but retain true circular arcs
|
||||||
|
# for each visible run rather than replacing corners with chords.
|
||||||
|
cx,cy=xy(source.GetCenter())
|
||||||
|
sx,sy=xy(source.GetStart()); mx,my=xy(source.GetArcMid())
|
||||||
|
start=math.atan2(sy-cy,sx-cx)
|
||||||
|
sweep=2*math.atan2((sx-cx)*(my-cy)-(sy-cy)*(mx-cx),
|
||||||
|
(sx-cx)*(mx-cx)+(sy-cy)*(my-cy))
|
||||||
|
radius=math.hypot(sx-cx,sy-cy)
|
||||||
|
steps=max(1,math.ceil(abs(math.degrees(sweep))))
|
||||||
|
def at(t):
|
||||||
|
angle=start+sweep*t/steps
|
||||||
|
return cx+radius*math.cos(angle),cy+radius*math.sin(angle)
|
||||||
|
run=None
|
||||||
|
for i in range(steps+1):
|
||||||
|
span=LineString([at(i),at(i+0.5),at(i+1)])
|
||||||
|
clear=i<steps and silk_area.covers(span) and not span.intersects(blocked)
|
||||||
|
if clear and run is None:
|
||||||
|
run=i
|
||||||
|
if not clear and run is not None:
|
||||||
|
if radius*abs(sweep)*(i-run)/steps>=0.25:
|
||||||
|
arc=p.PCB_SHAPE(board)
|
||||||
|
arc.SetShape(p.SHAPE_T_ARC)
|
||||||
|
arc.SetArcGeometry(vec(at(run)),vec(at((run+i)/2)),vec(at(i)))
|
||||||
|
arc.SetWidth(p.FromMM(WIDTH))
|
||||||
|
add(arc)
|
||||||
|
run=None
|
||||||
|
continue
|
||||||
|
for edge in outline_lines(source):
|
||||||
|
# Display boundary is visually distinct from the PCB perimeter.
|
||||||
|
dashed = ref=="DS1" and round(edge.bounds[0]-xy(footprints[ref].GetPosition())[0],2) in (1.0,25.5)
|
||||||
|
if dashed:
|
||||||
|
distance=0.0
|
||||||
|
while distance < edge.length:
|
||||||
|
line(edge.interpolate(distance).coords[0],edge.interpolate(min(distance+0.8,edge.length)).coords[0])
|
||||||
|
distance+=1.3
|
||||||
|
else:
|
||||||
|
line(edge.coords[0],edge.coords[-1])
|
||||||
|
# These are pad-row guides, not inferred socket/body or ESP32 PCB dimensions.
|
||||||
|
fp=footprints["U1"]
|
||||||
|
assert fp.GetOrientationDegrees()==0, "Review guide construction for rotated U1"
|
||||||
|
pads={pad.GetNumber():xy(pad.GetPosition()) for pad in fp.Pads()}
|
||||||
|
for first,last in (("1","22"),("23","44")):
|
||||||
|
x,ytop=pads[first]; _,ybottom=pads[last]
|
||||||
|
corners=[(x-1.2,ytop-1.2),(x+1.2,ytop-1.2),(x+1.2,ybottom+1.2),(x-1.2,ybottom+1.2)]
|
||||||
|
for a,b in zip(corners,corners[1:]+corners[:1]):
|
||||||
|
line(a,b)
|
||||||
|
label=p.PCB_TEXT(board)
|
||||||
|
label.SetText("HEADER GUIDES")
|
||||||
|
a,b=pads["1"],pads["23"]
|
||||||
|
label.SetPosition(vec(((a[0]+b[0])/2,a[1]+28)))
|
||||||
|
label.SetTextSize(p.VECTOR2I(p.FromMM(0.85),p.FromMM(0.85)))
|
||||||
|
label.SetTextThickness(p.FromMM(0.12))
|
||||||
|
add(label)
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
lock=ROOT/"~serial-carrier.kicad_pcb.lck"
|
||||||
|
if lock.exists():
|
||||||
|
raise SystemExit("Close PCB Editor before updating silkscreen")
|
||||||
|
before=PCB.read_bytes()
|
||||||
|
project=ROOT/"serial-carrier.kicad_pro"
|
||||||
|
project_before=project.read_bytes()
|
||||||
|
board=p.LoadBoard(str(PCB))
|
||||||
|
count=add_outlines(board)
|
||||||
|
temp=ROOT/"validation/pcb-routing-silk-tmp.kicad_pcb"
|
||||||
|
try:
|
||||||
|
assert p.SaveBoard(str(temp),board,True)
|
||||||
|
if lock.exists() or PCB.read_bytes()!=before:
|
||||||
|
raise RuntimeError("PCB changed or reopened; refusing to overwrite")
|
||||||
|
assert project.read_bytes()==project_before,"Project settings changed"
|
||||||
|
temp.replace(PCB)
|
||||||
|
finally:
|
||||||
|
temp.unlink(missing_ok=True)
|
||||||
|
print(f"Added {count} board-local silkscreen graphics; project settings preserved.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# DE-9 mechanical reference — pinned Adafruit 5988 STEP
|
||||||
|
|
||||||
|
Research: 2026-09-21. **Nominal CAD geometry, not a toleranced connector drawing or physical fit qualification.** No board, schematic, footprint, application dependency or main README changes.
|
||||||
|
|
||||||
|
**User-confirmed assumption:** male and female modules have identical external dimensions; use the external geometry below for mechanical planning of the male module on that basis. This is not independent male-CAD verification and **does not verify the male pin map**. The source explicitly identifies `DE9-F` / `F09HP` (5988). Do not infer contact numbering from gender, the model, or the front view.
|
||||||
|
|
||||||
|
## Sources, attribution and licensing
|
||||||
|
|
||||||
|
- User's link: https://github.com/adafruit/Adafruit_CAD_Parts/tree/main/5988%20RS-232%20Level%20Shifter
|
||||||
|
- Pinned repository revision: **`5b8a21fb5e2e48478faf8901077be812f5224066`**, 2024-09-05, Noe Ruiz, “Adding 5988 RS-232 Level Shifter”. GitHub path history returned this as the latest change to that directory at retrieval.
|
||||||
|
- Immutable directory: https://github.com/adafruit/Adafruit_CAD_Parts/tree/5b8a21fb5e2e48478faf8901077be812f5224066/5988%20RS-232%20Level%20Shifter
|
||||||
|
- Unmodified originals retained in `reference/db9-cad/`: STEP, repository-root `README.md`, repository-root `LICENSE`. `sources.json` records **full immutable raw URLs and SHA-256 hashes**.
|
||||||
|
|
||||||
|
| Original | SHA-256 |
|
||||||
|
|---|---|
|
||||||
|
| `5988 RS-232 Level Shifter.step` | `8eab9dc591a985fc987ceec2d7534e5e259263c73bebc64c0c3cd847e135906f` |
|
||||||
|
| `LICENSE` | `f60436644e066301d4c2e8f1f0674a157b1f60e4fd5586717b99dde255cc18cd` |
|
||||||
|
| `README.md` | `9ed9beac7600623b7b9bd0d02e1fdcd86610ce22966723179e6c6b618e399dce` |
|
||||||
|
|
||||||
|
The CAD repository root license is **MIT, Copyright (c) 2016 Adafruit Industries**; root README merely describes STEP/Fusion/STL parts. No part-specific license file is present in the four-file directory. Retain the original MIT notice and warranty disclaimer with redistribution. This assessment follows the repository's stated license, not an independent audit of any imported vendor geometry. F3D, STL and JPG were listed but not downloaded or used; no image-derived dimensions or mesh fallback were needed.
|
||||||
|
|
||||||
|
The separate Eagle board reference is from `Adafruit-RS-232-Level-Shifter-Breakout-PCB`, commit `63c6200bb3ef17d491089cfa6441b1810d79bb20`; its local `.brd` hash was rechecked (`6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870`). Eagle-derived registration/hole information is attributed to **Limor Fried/Ladyada for Adafruit Industries**, under **CC BY-SA 3.0 Unported**; retain `reference/README.md` and `reference/license.txt`. The Eagle-derived portions of these notes use that license. Do not silently substitute the CAD-parts repository's MIT license for the Eagle design license.
|
||||||
|
|
||||||
|
## Extraction method and confidence
|
||||||
|
|
||||||
|
Used Python/OpenCascade (`cadquery-ocp==7.9.3.1.1`) in the isolated project `.cache/cad-tools/venv`; no application dependencies changed. The initially attempted older OCP wheel was unavailable for this Python; the pinned installed version succeeded.
|
||||||
|
|
||||||
|
- `reference/db9-cad/extract.py`: STEPControl import with assembly placements applied; `BRepBndLib.AddOptimal` with triangulation and shape-tolerance enlargement disabled; analytic planes/cylinders via `BRepAdaptor_Surface`. No tessellation used for measurement.
|
||||||
|
- `geometry.json`: all 15 solids and their faces, bounds, validity, analytic cylinder radii/axes, plane normals. All 15 pass `BRepCheck_Analyzer`.
|
||||||
|
- `validate.py`: checks original hashes, all 12 header centres against actual Eagle XML, board bounds, mounting holes and selected front-face geometry; writes `measurements.json` containing explicit face selections and their bounds.
|
||||||
|
- For unique PCB/connector bodies, STEP `CLOSED_SHELL` face order is associated with import face order, checking face counts, every surface type and every cylinder radius. Appearance assignments corroborate feature identification; **colour is not a material certification**.
|
||||||
|
- Imported solid 12 = STEP `#101` / `Body2`, the main connector. It fuses housing/insert, contacts, flange, shell and front hex posts; its whole-solid bounds are **not metal-only dimensions**. Solids 13/14 = `#102`/`#103` (`Body3`/`Body4`), separate anchor brackets.
|
||||||
|
- Front shell/flange/hex-post faces have the shared `Opaque(245,245,246)` appearance and the expected geometric continuity. The report measures those **face subsets**, excluding plastic, contacts, PCB and ICs. There is no separate hollow metal shell solid from which to certify wall thickness.
|
||||||
|
|
||||||
|
**High confidence:** numerical nominal positions/bounds/radii of identified modeled features and STEP-to-Eagle board registration. **Moderate confidence:** physical fidelity of the simplified connector model and material boundaries; the contact/tail geometry differs from Eagle. **Unverified:** manufacturing tolerances, actual purchased-part fit, threads, male electrical mapping, cable/backshell envelope and final enclosure cutout. Extra decimal places below support reproducibility, not manufacturing accuracy.
|
||||||
|
|
||||||
|
## Coordinate system and orientation
|
||||||
|
|
||||||
|
All dimensions are **mm**. Use `(X,Y,Z)` in the imported STEP assembly:
|
||||||
|
|
||||||
|
- X rightward in component/top view; Y toward the connector/cable, Z above the PCB.
|
||||||
|
- Bare breakout PCB: **X 0…31.750, Y 0…29.337, Z 0…1.570**. PCB front edge is **Y = 29.337**; underside is Z = 0, component-side surface Z = 1.570.
|
||||||
|
- Logic header hole row: **Y = 2.667**, X = `1.905 + 2.540 × (N−1)`, N = 1…12. No installed header/socket body is modeled; this is the hole-axis datum, not a plastic-header edge.
|
||||||
|
- STEP XY is already Eagle board XY: **identity transform**, no scale, rotation or translation. Verified by all 12 header holes, both mounting holes and PCB bounds. STEP connector symmetry alone would not establish this registration.
|
||||||
|
- Existing carrier footprint coordinates (header pad 1 origin, Y downward): **`x = X − 1.905; y = 2.667 − Y`**. Height above module PCB top: **`h = Z − 1.570`**. Add the chosen module-top elevation later to get case/carrier height; socket/standoff stack is not yet selected.
|
||||||
|
- Connector front/mating approach is along Y, **parallel to the PCB**, not down into it. A cable approaches from +Y and inserts toward −Y. A front view looks along −Y with +Z up; +X then appears to the viewer's **left**, unlike component/top view. Do not reuse top-view left/right pin numbering as front-view numbering.
|
||||||
|
|
||||||
|
## Metal feature dimensions — selected subsets, not assembly bounds
|
||||||
|
|
||||||
|
The Z centre of the D profile and front screw-lock axes is **7.84762024**, or **6.27762024 above the PCB top**. Connector centre X = **15.875** (carrier-local x = **13.970**).
|
||||||
|
|
||||||
|
| Feature | X bounds | Y bounds | Z bounds | Nominal dimensions / evidence |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Front metal flange, exposed faces | 0.475…31.275 | 27.007…27.407 | 1.572620…14.122620 | **30.800 wide × 12.550 high × 0.400 axial thickness**; solid 12 faces 8, 11–14 |
|
||||||
|
| D-shell straight outer wall region | 8.035981…23.714019 | 28.007…32.607 | 3.897620…11.797620 | **15.678038 wide × 7.900 high**, straight axial length **4.600**; faces 18–25 |
|
||||||
|
| D-shell outer surface including root blend and rounded front lip | 7.435981…24.314019 | 27.407…33.207 | 3.297620…12.397620 | **16.878038 wide × 9.100 high × 5.800 axial extent**; faces 18–25, 81–88, 103–110 |
|
||||||
|
| Left front hex screw-lock post | 0.719189…6.030811 | 27.407…32.207 | 5.547620…10.147620 | **4.600 across flats**, **5.311622 across corners**, **4.800 long**; faces 53–61, 89 |
|
||||||
|
| Right front hex screw-lock post | 25.719189…31.030811 | 27.407…32.207 | 5.547620…10.147620 | Same dimensions; faces 62–70, 90 |
|
||||||
|
|
||||||
|
The flange is centred laterally on the PCB, inset **0.475** from each side. Its lower edge is only **0.002620** above the modeled PCB top, upper edge **12.552620** above it. Preserve this tiny source offset in registration; do not interpret it as a designed assembly clearance.
|
||||||
|
|
||||||
|
The D-shell has sloping side planes at **10° from vertical** in the front cross-section (`|normal X| = cos 10°`, `|normal Z| = sin 10°`), corner-cylinder radii **1.700**, root/lip rounding radius **0.600**. The wider side is upper (+Z). The maximum 16.878038 × 9.100 envelope occurs at the **root blend**, not uniformly along the projecting nose. These are bounding dimensions of a D shape, not a rectangular cutout prescription. Shell wall thickness / actual hollow cavity geometry is **not verified** by this fused representation.
|
||||||
|
|
||||||
|
### Front cable screw-lock holes: actual modeled cylinders, not thread specifications
|
||||||
|
|
||||||
|
- Axes **parallel to Y**, at `(X,Z) = (3.375,7.84762024)` and `(28.375,7.84762024)`.
|
||||||
|
- **Axis spacing = 25.000**, symmetric ±12.500 about X = 15.875.
|
||||||
|
- Carrier-local axis x = **1.470 and 26.470**; post front plane y = **−29.540**.
|
||||||
|
- Solid 12 faces **60 / 69**, raw STEP faces **#13822 / #13831**, are inward-facing cylindrical hole surfaces: **radius 1.500, diameter 3.000**, Y = **27.407…32.007** (straight cylindrical length **4.600**).
|
||||||
|
- Mouth cones (faces **89 / 90**) run Y = **32.007…32.207**, growing to **diameter 3.400** at the front: **0.200 axial × 0.200 radial** chamfer. Front mouth plane **Y = 32.207**.
|
||||||
|
- Flat modeled bottoms at Y = **27.407** (faces 61/70): total modeled mouth-to-bottom depth **4.800** including chamfer.
|
||||||
|
- **No helical threads modeled. Do not call this M3, 4-40, a tap drill, minor/major thread diameter, or usable engagement depth.** Actual screw standard, pitch, engagement and tolerances require a connector drawing or physical check.
|
||||||
|
|
||||||
|
## Front planes, PCB overhang and header offsets
|
||||||
|
|
||||||
|
There is no single interchangeable “front plane”. The flange, hex-post mouth and shell lip differ. The shell's foremost geometric plane is a useful **mating-face reference**, but the source does not establish a manufacturer's functional fully-mated datum or panel seating specification.
|
||||||
|
|
||||||
|
| Reference plane | STEP/Eagle Y | Forward of PCB front edge Y=29.337 | Forward of header row Y=2.667 | Carrier-local y |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| Flange rear face | 27.007 | −2.330 | 24.340 | −24.340 |
|
||||||
|
| Flange front face / shell root | **27.407** | **−1.930** | **24.740** | **−24.740** |
|
||||||
|
| Straight shell begins | 28.007 | −1.330 | 25.340 | −25.340 |
|
||||||
|
| Breakout PCB front edge | **29.337** | 0 | **26.670** | **−26.670** |
|
||||||
|
| Front hex-post mouth faces | **32.207** | **+2.870** | **29.540** | **−29.540** |
|
||||||
|
| Straight shell ends / rounded lip begins | 32.607 | +3.270 | 29.940 | −29.940 |
|
||||||
|
| Foremost shell lip / modeled insert face | **33.207** | **+3.870** | **30.540** | **−30.540** |
|
||||||
|
|
||||||
|
Thus the modeled shell nose projects **3.870 beyond the breakout PCB**; the hex posts project **2.870**, ending **1.000 behind the shell nose**. The flange itself is **1.930 behind the PCB front edge**. Do not position a case wall at the flange while ignoring that the PCB extends 1.930 beyond it.
|
||||||
|
|
||||||
|
For later unrotated, component-up carrier placement with header pad 1 at `(x0,y0)`:
|
||||||
|
|
||||||
|
- Breakout front edge: `y0 − 26.670`.
|
||||||
|
- Flange front: `y0 − 24.740`.
|
||||||
|
- Hex mouths: `y0 − 29.540`.
|
||||||
|
- Shell nose: `y0 − 30.540`.
|
||||||
|
- If the **breakout PCB front edge** is aligned with carrier top edge `ytop`, choose `y0 = ytop + 26.670`; shell nose is then at `ytop − 3.870`, hex mouths at `ytop − 2.870`.
|
||||||
|
- If instead the **shell nose** is aligned with that edge, choose `y0 = ytop + 30.540`; breakout front edge is then 3.870 inward. These are different choices, not a recommended final layout.
|
||||||
|
|
||||||
|
## Three distinct mechanical systems — do not interchange
|
||||||
|
|
||||||
|
| System | Axes / coordinates | Spacing | Diameter or geometry | Purpose |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| Front cable screw-lock holes | Y axes; X 3.375 / 28.375, Z 7.847620 | **25.000** | Modeled bore Ø3.000, mouth Ø3.400; no thread qualification | Retain mating cable at connector front |
|
||||||
|
| Breakout PCB mounting holes | Z axes; `(3.810,11.430)` / `(27.940,11.430)` | **24.130** | Eagle drill Ø3.000 / plated copper Ø3.600; STEP hole cylinders Ø3.000 | Mount module to carrier/standoffs |
|
||||||
|
| Connector shell/anchor PCB holes G1/G2 | Z axes; Eagle `(3.3528,17.907)` / `(28.3972,17.907)` | **25.0444** | Eagle drill Ø3.302 / copper Ø5.080; STEP rounds X to 3.353 / 28.397 | Receive connector anchor legs, **not** carrier mounting or cable screws |
|
||||||
|
|
||||||
|
### Separate metal anchor brackets and underside clearance
|
||||||
|
|
||||||
|
Solids **13/14** represent the bent anchor brackets/legs, not the front hex posts. Per bracket whole-solid bounds (leg-only components, not whole assembly):
|
||||||
|
|
||||||
|
- Right: **X 25.875…30.875, Y 17.907…24.307, Z −2.230…10.122620**.
|
||||||
|
- Left: **X 0.875…5.875**, same Y and Z.
|
||||||
|
- Each bracket envelope: **5.000 wide × 6.400 deep × 12.352620 high**. Bent sheet regions are **0.500 thick**; the lower two fingers occupy Y = **17.907…18.407**, not a circular screw shank.
|
||||||
|
- Tips reach **2.230 below the module underside**, i.e. **3.800 below the module top**. This needs carrier/case underside clearance; do not turn these bounds into a mounting-hole drill recommendation.
|
||||||
|
- Bracket symmetry centres X = **3.375 / 28.375**, lower sheet midplane Y = **18.157**. Relative to Eagle G1/G2, centre offsets are respectively **+0.0222 / −0.0222 in X** and **+0.250 in Y**. These are source-model differences, not a reason to move the Eagle pads.
|
||||||
|
- Main fused body's signal tails separately reach Z = **−1.607380**. Their modeled rows are Y = **16.467 / 19.307**, versus Eagle **16.637 / 19.177**; horizontal pitch also differs (model 2.770 versus Eagle 2.7432). The 3D model is **not suitable as electrical pin/drill authority**.
|
||||||
|
|
||||||
|
No complete all-metal rear housing reconstruction is claimed: only the explicitly selected front surfaces and separate anchor brackets above. Main solid 12's 30.800 × 18.200 × 15.730 bounds include plastic/contact tails and must not be relabeled as a shell-only envelope.
|
||||||
|
|
||||||
|
## Validation, reproduction and release boundary
|
||||||
|
|
||||||
|
From project root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.cache/cad-tools/venv/bin/python -B hardware/PCB/reference/db9-cad/extract.py
|
||||||
|
python3 -B hardware/PCB/reference/db9-cad/validate.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Both successfully ran; validation reported PASS for original hashes, 15 valid solids, all 12 Eagle header centres, PCB bounds, mounting holes, metal-appearance subsets and front lock cylinders. Extraction artifacts retain unrounded floating-point values. No firmware build, board DRC, hardware measurement or physical mating test was performed (research-only task).
|
||||||
|
|
||||||
|
**Ready for nominal placement planning:** metal flange dimensions, shell outer envelope, front hex-post dimensions and bore geometry, their distinct front planes, 25.000 front-axis spacing, PCB/header registration and overhang, anchor-leg underside extent. The A1 PCB now aligns the breakout front edge with the carrier top edge. Nominal metal bounding projections and screw-lock axes appear on Dwgs.User; companion layout/footprint notes have been updated. Neither projection nor nominal bound is an enclosure cutout or clearance qualification.
|
||||||
|
|
||||||
|
**Before fabrication / final case design:** verify actual male pin map; check physical connector profile and overhang against these nominal values; select header/socket and standoff heights; establish manufacturing/assembly clearances, cable backshell/tool access, wall thickness and cutout shape; verify thread standard and retention; perform a 1:1 fit/mating check. In particular, do not use exact-fit nominal bounds as an enclosure clearance or assume the CAD's Ø3.000 lock bore specifies a real screw thread.
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
Requires system Python + pcbnew. Refuses to overwrite an existing PCB unless
|
Requires system Python + pcbnew. Refuses to overwrite an existing PCB unless
|
||||||
--overwrite is explicit. This is a draft generator, not a manufacturing tool.
|
--overwrite is explicit. This is a draft generator, not a manufacturing tool.
|
||||||
Does not modify the schematic or footprint libraries. KiCad SaveBoard also saves
|
Does not modify the schematic, footprint libraries or project settings.
|
||||||
PCB design settings into the project; close the project before regeneration.
|
SaveBoard(..., True) deliberately preserves the user's existing project rules.
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -18,10 +18,10 @@ ROOT = Path(__file__).resolve().parent
|
|||||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||||
LIBS = Path("/usr/share/kicad/footprints")
|
LIBS = Path("/usr/share/kicad/footprints")
|
||||||
ORIGIN = (100, 100)
|
ORIGIN = (100, 100)
|
||||||
OUTLINE = [(0,0), (82,0), (82,80), (36,80), (36,61), (0,61)]
|
OUTLINE = [(36,0), (82,0), (82,80), (0,80), (0,19), (36,19)]
|
||||||
PLACEMENT = {
|
PLACEMENT = {
|
||||||
"U1": ("Carrier:HW678_2x22_Provisional", 8, 4),
|
"U1": ("Carrier:HW678_2x22_Provisional", 8, 23),
|
||||||
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 30),
|
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 26.670),
|
||||||
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43, 36),
|
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43, 36),
|
||||||
"R1": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 43, 34),
|
"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),
|
"R2": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 54, 34),
|
||||||
@@ -30,9 +30,9 @@ PLACEMENT = {
|
|||||||
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 55, 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),
|
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 67, 68),
|
||||||
}
|
}
|
||||||
MOUNTS = [(3.5,3.5), (3.5,56), (78,4), (78,76)]
|
MOUNTS = [(3.5,22.5), (3.5,75), (78,4), (78,76)]
|
||||||
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
|
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
|
||||||
RF_RESERVE = [(10,0), (31,0), (31,13), (10,13)]
|
RF_RESERVE = [(10,19), (31,19), (31,32), (10,32)]
|
||||||
|
|
||||||
def mm(v):
|
def mm(v):
|
||||||
return p.FromMM(v)
|
return p.FromMM(v)
|
||||||
@@ -87,20 +87,6 @@ def copy_schematic_fields(fp, component):
|
|||||||
fp.GetField(name).SetVisible(False)
|
fp.GetField(name).SetVisible(False)
|
||||||
|
|
||||||
|
|
||||||
def configure_draft_netclasses():
|
|
||||||
# SaveBoard creates the project defaults. Align future interactive routing
|
|
||||||
# with the draft's physical minimum widths and its explicit power routing.
|
|
||||||
path=ROOT/"serial-carrier.kicad_pro"
|
|
||||||
project=json.loads(path.read_text())
|
|
||||||
settings=project["net_settings"]
|
|
||||||
default=next(c for c in settings["classes"] if c["name"]=="Default")
|
|
||||||
default.update(clearance=0.25, track_width=0.25, via_diameter=0.7, via_drill=0.3)
|
|
||||||
power=dict(default, name="Power", priority=0, track_width=0.5, diff_pair_width=0.25)
|
|
||||||
settings["classes"]=[c for c in settings["classes"] if c["name"]!="Power"]+[power]
|
|
||||||
patterns=[v for v in settings.get("netclass_patterns",[]) if v["pattern"] not in ("/+3V3","/GND")]
|
|
||||||
settings["netclass_patterns"]=patterns+[{"netclass":"Power","pattern":name} for name in ("/+3V3","/GND")]
|
|
||||||
path.write_text(json.dumps(project,indent=2)+"\n")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser=argparse.ArgumentParser(description=__doc__)
|
parser=argparse.ArgumentParser(description=__doc__)
|
||||||
@@ -108,8 +94,8 @@ def main():
|
|||||||
args=parser.parse_args()
|
args=parser.parse_args()
|
||||||
if PCB.exists() and not args.overwrite:
|
if PCB.exists() and not args.overwrite:
|
||||||
parser.error("PCB exists; preserve manual edits or explicitly use --overwrite")
|
parser.error("PCB exists; preserve manual edits or explicitly use --overwrite")
|
||||||
if any(ROOT.glob("~serial-carrier.*.lck")):
|
if ROOT.joinpath("~serial-carrier.kicad_pcb.lck").exists():
|
||||||
parser.error("Project is open in KiCad; close it before regenerating PCB/settings")
|
parser.error("PCB is open in KiCad; close it before regenerating")
|
||||||
netfile=ROOT / "validation/pcb-source-netlist.xml"
|
netfile=ROOT / "validation/pcb-source-netlist.xml"
|
||||||
subprocess.run(["kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(netfile),str(ROOT/"serial-carrier.kicad_sch")],check=True)
|
subprocess.run(["kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(netfile),str(ROOT/"serial-carrier.kicad_sch")],check=True)
|
||||||
source=ET.parse(netfile).getroot()
|
source=ET.parse(netfile).getroot()
|
||||||
@@ -126,11 +112,11 @@ def main():
|
|||||||
settings.m_HoleClearance=mm(0.25)
|
settings.m_HoleClearance=mm(0.25)
|
||||||
settings.m_TrackMinWidth=mm(0.25)
|
settings.m_TrackMinWidth=mm(0.25)
|
||||||
settings.m_ViasMinSize=mm(0.7)
|
settings.m_ViasMinSize=mm(0.7)
|
||||||
# SaveBoard persists these proposed draft rules into the 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 draft")
|
||||||
title.SetRevision("A0 DRAFT")
|
title.SetRevision("A1 DRAFT")
|
||||||
title.SetComment(0,"NOT FOR FABRICATION - male module / USB / antenna geometry unverified")
|
title.SetComment(0,"NOT FOR FABRICATION - 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"))
|
||||||
@@ -155,9 +141,9 @@ def main():
|
|||||||
quiet_footprint(fp)
|
quiet_footprint(fp)
|
||||||
copy_schematic_fields(fp,comp)
|
copy_schematic_fields(fp,comp)
|
||||||
if ref=="U1":
|
if ref=="U1":
|
||||||
fp.Reference().SetPosition(xy(20.7,25))
|
fp.Reference().SetPosition(xy(20.7,44))
|
||||||
elif ref=="U2":
|
elif ref=="U2":
|
||||||
fp.Reference().SetPosition(xy(56.97,18))
|
fp.Reference().SetPosition(xy(56.97,14.670))
|
||||||
elif ref=="DS1":
|
elif ref=="DS1":
|
||||||
fp.Reference().SetPosition(xy(56,49))
|
fp.Reference().SetPosition(xy(56,49))
|
||||||
elif ref.startswith("SW"):
|
elif ref.startswith("SW"):
|
||||||
@@ -199,23 +185,22 @@ def main():
|
|||||||
reserve.SetDoNotAllowFootprints(False) # the module itself spans this region
|
reserve.SetDoNotAllowFootprints(False) # the module itself spans this region
|
||||||
polygon(reserve,RF_RESERVE)
|
polygon(reserve,RF_RESERVE)
|
||||||
board.Add(reserve)
|
board.Add(reserve)
|
||||||
text(board,"RF RESERVE",20.5,7,p.Dwgs_User,1.2)
|
text(board,"RF RESERVE",20.5,26,p.Dwgs_User,1.2)
|
||||||
text(board,"VERIFY ANTENNA",20.5,10,p.Dwgs_User,1)
|
text(board,"VERIFY ANTENNA",20.5,29,p.Dwgs_User,1)
|
||||||
text(board,"ESP32 HW678",20.7,29)
|
text(board,"ESP32 HW678",20.7,48)
|
||||||
text(board,"USB ACCESS - VERIFY",18,64,p.Dwgs_User,1)
|
text(board,"USB ACCESS - VERIFY",18,83,p.Dwgs_User,1)
|
||||||
text(board,"DE-9 OUT / VERIFY MALE",56.97,1.5,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,21)
|
text(board,"MAX3243 REF",56.97,17.670)
|
||||||
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,"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,"Carrier envelope only; module overhang and connector clearance not qualified",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()
|
||||||
p.SaveBoard(str(PCB),board)
|
assert p.SaveBoard(str(PCB),board,True)
|
||||||
configure_draft_netclasses()
|
|
||||||
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
|
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
|
||||||
print("Placement draft only. Board-only U2 footprint is a female-CAD reference, not male qualification.")
|
print("Placement draft only. U2 nominal male external metal dimensions user-confirmed; pin map / assembly still require qualification.")
|
||||||
|
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Compact two-layer PCB — A0 draft
|
# Compact two-layer PCB — A1 draft
|
||||||
|
|
||||||
Open **`serial-carrier.kicad_pcb`** in KiCad 10. The routed board belongs to the
|
Open **`serial-carrier.kicad_pcb`** in KiCad 10. The routed board belongs to the
|
||||||
existing `serial-carrier.kicad_pro` / `.kicad_sch` project. **Not for fabrication.**
|
existing `serial-carrier.kicad_pro` / `.kicad_sch` project. **Not for fabrication.**
|
||||||
@@ -9,8 +9,10 @@ No Gerbers, production drills or manufacturing archive were generated.
|
|||||||
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
|
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
|
||||||
- **Two copper layers**, F.Cu and B.Cu. All components mount on the front.
|
- **Two copper layers**, F.Cu and B.Cu. All components mount on the front.
|
||||||
- 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 lower-left cutaway** leaves cable space below
|
- Stepped outline: a **36 × 19 mm top-left antenna cutaway** fits within a simple
|
||||||
the ESP32 while the OLED/buttons occupy the deeper right-hand section.
|
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
|
||||||
|
registration remains provisional pending the user's measured ESP32 geometry.
|
||||||
- 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.
|
||||||
|
|
||||||
@@ -22,17 +24,18 @@ component body corners.
|
|||||||
|
|
||||||
| Reference | Local origin (X, Y) | Placement |
|
| Reference | Local origin (X, Y) | Placement |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| U1 | (8, 4) | ESP32 left header pin 1; antenna toward upper edge, USB toward cutaway |
|
| U1 | (8, 23) | ESP32 left header pin 1; antenna toward top-left cutaway, USB toward bottom edge |
|
||||||
| U2 | (43, 30) | MAX3243 header pin 1; DE-9 toward upper edge |
|
| U2 | (43, 26.670) | MAX3243 header pin 1; bare module PCB front exactly flush with carrier top edge |
|
||||||
| DS1 | (43, 36) | OLED module upper-left PCB corner; display faces up |
|
| DS1 | (43, 36) | OLED module upper-left PCB corner; display faces up |
|
||||||
| R1 / R2 / R3 | (43 / 54 / 65, 34) | Axial pull-ups in the gap between RS-232 and OLED |
|
| R1 / R2 / R3 | (43 / 54 / 65, 34) | Axial pull-ups in the gap between RS-232 and OLED |
|
||||||
| SW1 / SW2 / SW3 | (43 / 55 / 67, 68) | Back, select, next below display; switch pad 1 origins |
|
| SW1 / SW2 / SW3 | (43 / 55 / 67, 68) | Back, select, next below display; switch pad 1 origins |
|
||||||
| H1 | (3.5, 3.5) | Proposed case mounting hole |
|
| H1 | (3.5, 22.5) | Proposed case mounting hole below antenna notch |
|
||||||
| H2 | (3.5, 56) | Proposed case mounting hole |
|
| H2 | (3.5, 75) | Proposed case mounting hole near bottom-left edge |
|
||||||
| H3 | (78, 4) | Proposed case mounting hole |
|
| H3 | (78, 4) | Proposed case mounting hole |
|
||||||
| H4 | (78, 76) | Proposed case mounting hole |
|
| H4 | (78, 76) | Proposed case mounting hole |
|
||||||
|
|
||||||
Outline vertices: (0,0), (82,0), (82,80), (36,80), (36,61), (0,61), closed.
|
Outline vertices: (36,0), (82,0), (82,80), (0,80), (0,19), (36,19), closed.
|
||||||
|
The previous A0 board is retained in `validation/pcb-a0-before-reposition.kicad_pcb`.
|
||||||
The board area is **58.76 cm²**; its bounding rectangle is 65.6 cm².
|
The board area is **58.76 cm²**; its bounding rectangle is 65.6 cm².
|
||||||
|
|
||||||
H1–H4 are proposed **Ø3.2 mm NPTH M3 clearance holes**, not a previously agreed
|
H1–H4 are proposed **Ø3.2 mm NPTH M3 clearance holes**, not a previously agreed
|
||||||
@@ -48,8 +51,8 @@ The PCB contains actual copper, not just ratsnest placement:
|
|||||||
|
|
||||||
- All **38 required physical-pad spanning connections** across **17 connected
|
- All **38 required physical-pad spanning connections** across **17 connected
|
||||||
nets** routed, including both duplicated pads of each switch contact.
|
nets** routed, including both duplicated pads of each switch contact.
|
||||||
- **569 segments**, signal width **0.25 mm**, 3.3 V and GND width **0.50 mm**.
|
- **565 segments**, signal width **0.25 mm**, 3.3 V and GND width **0.50 mm**.
|
||||||
- **20 through vias**, **0.70 mm copper / 0.30 mm drill**.
|
- **22 through vias**, **0.70 mm copper / 0.30 mm drill**.
|
||||||
- **No copper pours**; ground is explicitly routed. This is an initial routing
|
- **No copper pours**; ground is explicitly routed. This is an initial routing
|
||||||
solution, not a reviewed return-current/EMI or power-integrity design.
|
solution, not a reviewed return-current/EMI or power-integrity design.
|
||||||
- Proposed minimum different-net clearance **0.25 mm** and copper-edge clearance
|
- Proposed minimum different-net clearance **0.25 mm** and copper-edge clearance
|
||||||
@@ -69,7 +72,7 @@ current-carrying capacity, supply stability, I²C signal quality or EMC complian
|
|||||||
### ESP32 and RF
|
### ESP32 and RF
|
||||||
|
|
||||||
A two-layer **no-tracks/no-vias/no-pads/no-zone-fill rule area** reserves local
|
A two-layer **no-tracks/no-vias/no-pads/no-zone-fill rule area** reserves local
|
||||||
X=10…31 mm, Y=0…13 mm toward the antenna. This is a deliberate carrier design
|
X=10…31 mm, Y=19…32 mm beneath the antenna-side end of U1, adjacent to the notch. This is a deliberate carrier design
|
||||||
allocation, **not a measured antenna envelope or a manufacturer RF keepout**.
|
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.
|
||||||
@@ -83,9 +86,39 @@ Missing RF clearance cannot be signed off from the current footprint alone.
|
|||||||
|
|
||||||
U2 uses the new **`Carrier:MAX3243_Reference_Provisional`** footprint **on the PCB
|
U2 uses the new **`Carrier:MAX3243_Reference_Provisional`** footprint **on the PCB
|
||||||
only**. Its bare-module outline/header/holes are adapted from published female
|
only**. Its bare-module outline/header/holes are adapted from published female
|
||||||
5988 CAD for the intended male 6253 module. The user reports identical dimensions,
|
5988 CAD for the intended male 6253 module. The user explicitly confirms identical
|
||||||
but the male header map and assembled DE-9 envelope are still unverified.
|
male/female external dimensions. The linked Adafruit STEP has now been measured
|
||||||
See `rs232-footprint-notes.md` and its CC BY-SA attribution.
|
with OpenCascade; its nominal metal geometry is used on that basis. Male header
|
||||||
|
mapping, actual fit, tolerances and the mounting stack still need qualification.
|
||||||
|
See `rs232-footprint-notes.md` and `db9-mechanical-notes.md` for evidence/licenses.
|
||||||
|
|
||||||
|
The module's **bare PCB front is flush with carrier Y=0**, as requested. The
|
||||||
|
connector has distinct front planes, not one universal case-wall datum:
|
||||||
|
|
||||||
|
| Feature | Position relative to carrier top edge | Meaning |
|
||||||
|
|---|---:|---|
|
||||||
|
| Flange front / shell root | 1.930 mm inside | Behind the PCB edge |
|
||||||
|
| Breakout PCB front | 0 | Flush with the carrier edge |
|
||||||
|
| Hex screw-lock mouth faces | 2.870 mm outside | Front cable-retention hole mouths |
|
||||||
|
| Shell nose | 3.870 mm outside | Foremost modeled shell surface |
|
||||||
|
|
||||||
|
The front screw-lock axes are **25.000 mm apart**, with centres **6.277620 mm
|
||||||
|
above the module PCB top**. They are not the two 24.130 mm-spaced PCB mounting
|
||||||
|
holes. The source models Ø3.000 mm bores but **no threads**; this does not establish
|
||||||
|
M3 or 4-40 hardware. The main metal flange is 30.800 × 12.550 × 0.400 mm; the
|
||||||
|
shell envelope including its root/lip is 16.878 × 9.100 × 5.800 mm.
|
||||||
|
|
||||||
|
`add_db9_envelope.py` places the measured metal bounding projections and screw-lock
|
||||||
|
axes on **Dwgs.User**, relative to U2. They are not silkscreen, a courtyard or a
|
||||||
|
case cutout. Run the helper after moving U2; it preserves copper and project
|
||||||
|
settings. The original STEP and its extraction/validation scripts are retained.
|
||||||
|
|
||||||
|
**Case-wall positioning remains a design decision:** PCB-edge alignment does not
|
||||||
|
put the flange or screw-lock mouths at that same plane. For example, making the
|
||||||
|
hex mouths flush with the case exterior would place that exterior 2.870 mm beyond
|
||||||
|
the carrier edge. No wall thickness, cable-backshell clearance or sealing scheme
|
||||||
|
has been assumed. The model's anchor legs extend **2.230 mm below the module
|
||||||
|
underside**; select a stand-off/header stack or clearance solution before assembly.
|
||||||
|
|
||||||
The schematic footprint remains intentionally blank, preserving its caution
|
The schematic footprint remains intentionally blank, preserving its caution
|
||||||
and avoiding edits to the schematic that was open in KiCad. Consequently there
|
and avoiding edits to the schematic that was open in KiCad. Consequently there
|
||||||
@@ -103,6 +136,39 @@ header/socket bodies, button actuation and finger/plunger clearance are not
|
|||||||
represented by complete assembly courtyards. In particular the axial resistors
|
represented by complete assembly courtyards. In particular the axial resistors
|
||||||
are tightly packed between the module outlines; confirm clearance and height.
|
are tightly packed between the module outlines; confirm clearance and height.
|
||||||
|
|
||||||
|
## Device outlines on front silkscreen
|
||||||
|
|
||||||
|
The PCB now has **0.12 mm front-silkscreen outlines** for the OLED PCB and the
|
||||||
|
published RS-232 reference module, plus a dashed OLED display boundary. The
|
||||||
|
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
|
||||||
|
remains unchanged.
|
||||||
|
|
||||||
|
U1 instead has **header-row guides**, labelled `HEADER GUIDES`: guide edges are
|
||||||
|
1.20 mm outside the outer pin centres. These are graphic alignment aids, **not
|
||||||
|
claimed socket-body dimensions or an inferred ESP32 PCB outline**. The actual
|
||||||
|
U1 body/antenna/USB outline still requires measured offsets. Existing button and
|
||||||
|
resistor silkscreen remains unchanged.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script requires pcbnew and Shapely, replaces only its own generated graphics,
|
||||||
|
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
|
||||||
|
routing regeneration; moving a footprint does not move these board-local drawings.
|
||||||
|
It currently supports the draft's unrotated module placements.
|
||||||
|
|
||||||
|
The original A0 silkscreen-only change left its routing unchanged. A1 deliberately
|
||||||
|
repositions U1/U2 and reroutes all connections, then refreshes these outlines.
|
||||||
|
Silkscreen is clipped away from the new flush U2 front board edge. Physical DRC
|
||||||
|
remains **0 violations**, **0 unconnected**, with the same U2 footprint-parity
|
||||||
|
warning. PDF and both previews have been refreshed.
|
||||||
|
|
||||||
## Review outputs
|
## Review outputs
|
||||||
|
|
||||||
- `validation/pcb-draft.svg` / `pcb-draft.png`: combined copper/outline/fab review.
|
- `validation/pcb-draft.svg` / `pcb-draft.png`: combined copper/outline/fab review.
|
||||||
@@ -111,7 +177,8 @@ are tightly packed between the module outlines; confirm clearance and height.
|
|||||||
the render is not an assembled-device or enclosure fit check.
|
the render is not an assembled-device or enclosure fit check.
|
||||||
- `pcb-placement-draft.pdf`: **1:1** front placement/pad/outline drawing. Print at
|
- `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 only known module outlines; no guessed U1 body or DE-9 envelope.
|
It includes the known module outlines and nominal STEP-derived DE-9 metal
|
||||||
|
bounding projections; no guessed U1 body or case-wall geometry.
|
||||||
|
|
||||||
## Validation and limits
|
## Validation and limits
|
||||||
|
|
||||||
@@ -138,9 +205,10 @@ Results with KiCad **10.0.6**:
|
|||||||
|
|
||||||
- **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**.
|
||||||
- Independent minimum different-net/copper-to-NPTH gap **0.275 mm**; minimum
|
- Independent minimum different-net/copper-to-NPTH gap **0.270544 mm**; minimum
|
||||||
copper/NPTH-to-board-edge gap **0.525 mm**.
|
copper/NPTH-to-board-edge gap **1.900 mm**.
|
||||||
- All 9 electrical footprints, 4 board-only mounts, outline and RF area retained.
|
- All 9 electrical footprints and 4 board-only mounts retained; positions, outline
|
||||||
|
and RF area revised as documented, with electrical linkage preserved.
|
||||||
- Startup `PROPERTY_ENUM` assertions from the local pcbnew binding are still
|
- Startup `PROPERTY_ENUM` assertions from the local pcbnew binding are still
|
||||||
emitted, but the checks complete successfully.
|
emitted, but the checks complete successfully.
|
||||||
|
|
||||||
@@ -154,11 +222,13 @@ complete capability set. No electrical/hardware/fabrication tests were performed
|
|||||||
## Regeneration and project settings
|
## Regeneration and project settings
|
||||||
|
|
||||||
Normal use is to **edit the native PCB in KiCad**, not regenerate it. The optional
|
Normal use is to **edit the native PCB in KiCad**, not regenerate it. The optional
|
||||||
`generate_pcb_draft.py` recreates the *unrouted* initial placement and discards
|
`generate_pcb_draft.py` recreates the *unrouted* A1 placement and discards manual
|
||||||
manual PCB work only with explicit `--overwrite`; it refuses an open-project lock.
|
PCB work only with explicit `--overwrite`; it refuses an open-project lock. A1 was
|
||||||
KiCad's initial SaveBoard populated the previously minimal `.kicad_pro` with PCB
|
made by mutating the existing board, not running that generator. All A1 saves use
|
||||||
settings and draft clearance/width rules. The schematic was not modified. If the
|
`SaveBoard(..., True)` to preserve user project settings. The original A0 creation
|
||||||
project was already open, reopen it to load the saved PCB settings before review.
|
populated the minimal project with rules; this revision leaves those settings and
|
||||||
|
the schematic unchanged. Refresh device silkscreen and CAD envelope helpers after
|
||||||
|
any intentional regeneration or placement/routing changes.
|
||||||
|
|
||||||
`route_pcb_draft.py` requires NumPy and pcbnew. By default it never saves; replacing
|
`route_pcb_draft.py` requires NumPy and pcbnew. By default it never saves; replacing
|
||||||
existing tracks requires explicit `--route --overwrite --replace-routes`. Do not
|
existing tracks requires explicit `--route --overwrite --replace-routes`. Do not
|
||||||
|
|||||||
Binary file not shown.
@@ -109,10 +109,10 @@ def boundary(radius):
|
|||||||
edge = 0.5 + radius + MARGIN
|
edge = 0.5 + radius + MARGIN
|
||||||
mask = np.broadcast_to((X < 100 + edge) | (X > 182 - edge) |
|
mask = np.broadcast_to((X < 100 + edge) | (X > 182 - edge) |
|
||||||
(Y < 100 + edge) | (Y > 180 - edge), (NY, NX)).copy()
|
(Y < 100 + edge) | (Y > 180 - edge), (NY, NX)).copy()
|
||||||
mask |= (X < 136 + edge) & (Y > 161 - edge)
|
mask |= (X < 136 + edge) & (Y < 119 + edge)
|
||||||
# Expand the RF reservation by copper radius plus numerical safety margin.
|
# Expand the RF reservation by copper radius plus numerical safety margin.
|
||||||
r = radius + MARGIN
|
r = radius + MARGIN
|
||||||
mask |= (X >= 110 - r) & (X <= 131 + r) & (Y <= 113 + r)
|
mask |= (X >= 110 - r) & (X <= 131 + r) & (Y >= 119 - r) & (Y <= 132 + r)
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
|
|
||||||
@@ -275,13 +275,15 @@ def main():
|
|||||||
if ROOT.joinpath('~serial-carrier.kicad_pcb.lck').exists():
|
if ROOT.joinpath('~serial-carrier.kicad_pcb.lck').exists():
|
||||||
parser.error('PCB editor lock exists; close the PCB before routing')
|
parser.error('PCB editor lock exists; close the PCB before routing')
|
||||||
original_bytes = PCB.read_bytes()
|
original_bytes = PCB.read_bytes()
|
||||||
|
project = ROOT / 'serial-carrier.kicad_pro'
|
||||||
|
project_before = project.read_bytes()
|
||||||
board = p.LoadBoard(str(PCB))
|
board = p.LoadBoard(str(PCB))
|
||||||
before = identity(board)
|
before = identity(board)
|
||||||
if board.GetCopperLayerCount() != 2 or len(board.GetFootprints()) != 13:
|
if board.GetCopperLayerCount() != 2 or len(board.GetFootprints()) != 13:
|
||||||
parser.error('unexpected board structure; review router assumptions')
|
parser.error('unexpected board structure; review router assumptions')
|
||||||
if len(board.Zones()) != 1 or not board.Zones()[0].GetIsRuleArea():
|
if len(board.Zones()) != 1 or not board.Zones()[0].GetIsRuleArea():
|
||||||
parser.error('unexpected zones; router supports only the original RF rule area')
|
parser.error('unexpected zones; router supports only the original RF rule area')
|
||||||
outline = [(100, 100), (182, 100), (182, 180), (136, 180), (136, 161), (100, 161)]
|
outline = [(136, 100), (182, 100), (182, 180), (100, 180), (100, 119), (136, 119)]
|
||||||
expected_edges = {frozenset((a, b)) for a, b in zip(outline, outline[1:] + outline[:1])}
|
expected_edges = {frozenset((a, b)) for a, b in zip(outline, outline[1:] + outline[:1])}
|
||||||
actual_edges = {frozenset(((p.ToMM(e.GetStart().x), p.ToMM(e.GetStart().y)),
|
actual_edges = {frozenset(((p.ToMM(e.GetStart().x), p.ToMM(e.GetStart().y)),
|
||||||
(p.ToMM(e.GetEnd().x), p.ToMM(e.GetEnd().y))))
|
(p.ToMM(e.GetEnd().x), p.ToMM(e.GetEnd().y))))
|
||||||
@@ -292,7 +294,7 @@ def main():
|
|||||||
contour = reserve.Outline().COutline(0)
|
contour = reserve.Outline().COutline(0)
|
||||||
rf_points = {(p.ToMM(contour.CPoint(i).x), p.ToMM(contour.CPoint(i).y))
|
rf_points = {(p.ToMM(contour.CPoint(i).x), p.ToMM(contour.CPoint(i).y))
|
||||||
for i in range(contour.PointCount())}
|
for i in range(contour.PointCount())}
|
||||||
if (rf_points != {(110, 100), (131, 100), (131, 113), (110, 113)} or
|
if (rf_points != {(110, 119), (131, 119), (131, 132), (110, 132)} or
|
||||||
not all(reserve.IsOnLayer(layer) for layer in LAYERS) or
|
not all(reserve.IsOnLayer(layer) for layer in LAYERS) or
|
||||||
not all((reserve.GetDoNotAllowTracks(), reserve.GetDoNotAllowVias(),
|
not all((reserve.GetDoNotAllowTracks(), reserve.GetDoNotAllowVias(),
|
||||||
reserve.GetDoNotAllowZoneFills()))):
|
reserve.GetDoNotAllowZoneFills()))):
|
||||||
@@ -313,8 +315,9 @@ def main():
|
|||||||
if len(netpads) > 1:
|
if len(netpads) > 1:
|
||||||
name = netpads[0].GetNetname()
|
name = netpads[0].GetNetname()
|
||||||
for length, a, b in pairs_for_net(netpads):
|
for length, a, b in pairs_for_net(netpads):
|
||||||
# Short local links first, then the long signal fanout; ground last.
|
# Reserve short ground escapes before signal fanout can box them in.
|
||||||
priority = (name == '/GND', length)
|
# Long ground links still run last, preserving space for signal paths.
|
||||||
|
priority = (name == '/GND' and length > 15, length)
|
||||||
work.append((priority, net, name, a, b))
|
work.append((priority, net, name, a, b))
|
||||||
work.sort(key=lambda w: (w[0], w[2]))
|
work.sort(key=lambda w: (w[0], w[2]))
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
@@ -354,7 +357,8 @@ def main():
|
|||||||
raise RuntimeError('PCB changed or editor opened during routing; refusing overwrite')
|
raise RuntimeError('PCB changed or editor opened during routing; refusing overwrite')
|
||||||
# Check a serialized candidate before replacing the user's board.
|
# Check a serialized candidate before replacing the user's board.
|
||||||
candidate = ROOT / 'validation/pcb-routing-candidate.kicad_pcb'
|
candidate = ROOT / 'validation/pcb-routing-candidate.kicad_pcb'
|
||||||
p.SaveBoard(str(candidate), board)
|
assert p.SaveBoard(str(candidate), board, True)
|
||||||
|
assert project.read_bytes() == project_before, 'project settings changed'
|
||||||
assert identity(p.LoadBoard(str(candidate))) == before, 'saved identity mismatch'
|
assert identity(p.LoadBoard(str(candidate))) == before, 'saved identity mismatch'
|
||||||
candidate.replace(PCB)
|
candidate.replace(PCB)
|
||||||
ROOT.joinpath('validation/pcb-routing-run.json').write_text(json.dumps(report, indent=2)+'\n')
|
ROOT.joinpath('validation/pcb-routing-run.json').write_text(json.dumps(report, indent=2)+'\n')
|
||||||
|
|||||||
@@ -83,11 +83,13 @@ when high. This project's logic interface requires 3.3 V Vin.
|
|||||||
|
|
||||||
## Intentionally omitted / release gates
|
## Intentionally omitted / release gates
|
||||||
|
|
||||||
**No verified DE-9 mechanical envelope is available, so none is drawn.** No
|
The footprint F.Fab outline still describes only the bare module PCB. Subsequent
|
||||||
male overhang, shell, mating plug, cable bend, enclosure cutout, height, underside
|
STEP B-rep measurements now establish nominal shell/flange/hex-post envelopes and
|
||||||
keepout, mounting-hardware clearance, 3D model or courtyard is invented. Female
|
registration; see `db9-mechanical-notes.md`. The user explicitly confirms identical
|
||||||
library silkscreen and product bounding dimensions are not male clearance data.
|
male/female external dimensions. A1 board-local Dwgs.User projections show those
|
||||||
The F.Fab outline describes only the bare module PCB, not the assembled envelope.
|
nominal bounds, without changing this footprint's pads or deriving drills from
|
||||||
|
the 3D model. Mating-plug/cable, case wall/cutout, assembly height and actual fit
|
||||||
|
remain unqualified. No guessed complete courtyard or 3D mounting offset is added.
|
||||||
There is **no fake courtyard**; courtyard checks cannot establish assembly fit.
|
There is **no fake courtyard**; courtyard checks cannot establish assembly fit.
|
||||||
Short silkscreen labels identify PROVISIONAL / FEMALE CAD REF / VERIFY MALE.
|
Short silkscreen labels identify PROVISIONAL / FEMALE CAD REF / VERIFY MALE.
|
||||||
|
|
||||||
|
|||||||
+4875
-3883
File diff suppressed because it is too large
Load Diff
@@ -259,6 +259,230 @@
|
|||||||
"cvpcb": {
|
"cvpcb": {
|
||||||
"equivalence_files": []
|
"equivalence_files": []
|
||||||
},
|
},
|
||||||
|
"erc": {
|
||||||
|
"erc_exclusions": [],
|
||||||
|
"meta": {
|
||||||
|
"version": 0
|
||||||
|
},
|
||||||
|
"pin_map": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"rule_severities": {
|
||||||
|
"bus_definition_conflict": "error",
|
||||||
|
"bus_entry_needed": "error",
|
||||||
|
"bus_to_bus_conflict": "error",
|
||||||
|
"bus_to_net_conflict": "error",
|
||||||
|
"different_unit_footprint": "error",
|
||||||
|
"different_unit_net": "error",
|
||||||
|
"duplicate_reference": "error",
|
||||||
|
"duplicate_sheet_names": "error",
|
||||||
|
"endpoint_off_grid": "warning",
|
||||||
|
"extra_units": "error",
|
||||||
|
"field_name_whitespace": "warning",
|
||||||
|
"footprint_filter": "ignore",
|
||||||
|
"footprint_link_issues": "warning",
|
||||||
|
"four_way_junction": "ignore",
|
||||||
|
"ground_pin_not_ground": "warning",
|
||||||
|
"hier_label_mismatch": "error",
|
||||||
|
"isolated_pin_label": "warning",
|
||||||
|
"label_dangling": "error",
|
||||||
|
"label_multiple_wires": "warning",
|
||||||
|
"lib_symbol_issues": "warning",
|
||||||
|
"lib_symbol_mismatch": "warning",
|
||||||
|
"missing_bidi_pin": "warning",
|
||||||
|
"missing_input_pin": "warning",
|
||||||
|
"missing_power_pin": "error",
|
||||||
|
"missing_unit": "warning",
|
||||||
|
"multiple_net_names": "warning",
|
||||||
|
"net_not_bus_member": "warning",
|
||||||
|
"no_connect_connected": "warning",
|
||||||
|
"no_connect_dangling": "warning",
|
||||||
|
"pin_not_connected": "error",
|
||||||
|
"pin_not_driven": "error",
|
||||||
|
"pin_to_pin": "warning",
|
||||||
|
"power_pin_not_driven": "error",
|
||||||
|
"same_local_global_label": "warning",
|
||||||
|
"similar_label_and_power": "warning",
|
||||||
|
"similar_labels": "warning",
|
||||||
|
"similar_power": "warning",
|
||||||
|
"simulation_model_issue": "ignore",
|
||||||
|
"single_global_label": "ignore",
|
||||||
|
"stacked_pin_name": "warning",
|
||||||
|
"unannotated": "error",
|
||||||
|
"unconnected_wire_endpoint": "warning",
|
||||||
|
"undefined_netclass": "error",
|
||||||
|
"unit_value_mismatch": "error",
|
||||||
|
"unresolved_variable": "error",
|
||||||
|
"wire_dangling": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
"libraries": {
|
"libraries": {
|
||||||
"pinned_footprint_libs": [],
|
"pinned_footprint_libs": [],
|
||||||
"pinned_symbol_libs": []
|
"pinned_symbol_libs": []
|
||||||
@@ -314,8 +538,14 @@
|
|||||||
"net_colors": null,
|
"net_colors": null,
|
||||||
"netclass_assignments": null,
|
"netclass_assignments": null,
|
||||||
"netclass_patterns": [
|
"netclass_patterns": [
|
||||||
{ "netclass": "Power", "pattern": "/+3V3" },
|
{
|
||||||
{ "netclass": "Power", "pattern": "/GND" }
|
"netclass": "Power",
|
||||||
|
"pattern": "/+3V3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"netclass": "Power",
|
||||||
|
"pattern": "/GND"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pcbnew": {
|
"pcbnew": {
|
||||||
@@ -330,16 +560,137 @@
|
|||||||
"page_layout_descr_file": ""
|
"page_layout_descr_file": ""
|
||||||
},
|
},
|
||||||
"schematic": {
|
"schematic": {
|
||||||
|
"annotate_start_num": 0,
|
||||||
|
"annotation": {
|
||||||
|
"method": 0,
|
||||||
|
"sort_order": 0
|
||||||
|
},
|
||||||
|
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||||
|
"bom_fmt_presets": [],
|
||||||
|
"bom_fmt_settings": {
|
||||||
|
"field_delimiter": ",",
|
||||||
|
"keep_line_breaks": false,
|
||||||
|
"keep_tabs": false,
|
||||||
|
"name": "CSV",
|
||||||
|
"ref_delimiter": ",",
|
||||||
|
"ref_range_delimiter": "",
|
||||||
|
"string_delimiter": "\""
|
||||||
|
},
|
||||||
|
"bom_presets": [],
|
||||||
|
"bom_settings": {
|
||||||
|
"exclude_dnp": false,
|
||||||
|
"fields_ordered": [
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Reference",
|
||||||
|
"name": "Reference",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Qty",
|
||||||
|
"name": "${QUANTITY}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Value",
|
||||||
|
"name": "Value",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "DNP",
|
||||||
|
"name": "${DNP}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Exclude from BOM",
|
||||||
|
"name": "${EXCLUDE_FROM_BOM}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Exclude from Board",
|
||||||
|
"name": "${EXCLUDE_FROM_BOARD}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Exclude from Simulation",
|
||||||
|
"name": "${EXCLUDE_FROM_SIM}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Exclude from Position Files",
|
||||||
|
"name": "${EXCLUDE_FROM_POS_FILES}",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": true,
|
||||||
|
"label": "Footprint",
|
||||||
|
"name": "Footprint",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Datasheet",
|
||||||
|
"name": "Datasheet",
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filter_string": "",
|
||||||
|
"group_symbols": true,
|
||||||
|
"include_excluded_from_bom": true,
|
||||||
|
"name": "Default Editing",
|
||||||
|
"sort_asc": true,
|
||||||
|
"sort_field": "Referenz"
|
||||||
|
},
|
||||||
"bus_aliases": {},
|
"bus_aliases": {},
|
||||||
|
"connection_grid_size": 50.0,
|
||||||
|
"drawing": {
|
||||||
|
"dashed_lines_dash_length_ratio": 12.0,
|
||||||
|
"dashed_lines_gap_length_ratio": 3.0,
|
||||||
|
"default_line_thickness": 6.0,
|
||||||
|
"default_text_size": 50.0,
|
||||||
|
"field_names": [],
|
||||||
|
"hop_over_size_choice": 0,
|
||||||
|
"intersheets_ref_own_page": false,
|
||||||
|
"intersheets_ref_prefix": "",
|
||||||
|
"intersheets_ref_short": false,
|
||||||
|
"intersheets_ref_show": false,
|
||||||
|
"intersheets_ref_suffix": "",
|
||||||
|
"junction_size_choice": 3,
|
||||||
|
"label_size_ratio": 0.375,
|
||||||
|
"operating_point_overlay_i_precision": 3,
|
||||||
|
"operating_point_overlay_i_range": "~A",
|
||||||
|
"operating_point_overlay_v_precision": 3,
|
||||||
|
"operating_point_overlay_v_range": "~V",
|
||||||
|
"overbar_offset_ratio": 1.23,
|
||||||
|
"pin_symbol_size": 25.0,
|
||||||
|
"text_offset_ratio": 0.15
|
||||||
|
},
|
||||||
"legacy_lib_dir": "",
|
"legacy_lib_dir": "",
|
||||||
"legacy_lib_list": [],
|
"legacy_lib_list": [],
|
||||||
|
"meta": {
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"page_layout_descr_file": "",
|
||||||
|
"plot_directory": "",
|
||||||
|
"reuse_designators": true,
|
||||||
|
"subpart_first_id": 65,
|
||||||
|
"subpart_id_separator": 0,
|
||||||
"top_level_sheets": [
|
"top_level_sheets": [
|
||||||
{
|
{
|
||||||
"filename": "serial-carrier.kicad_sch",
|
"filename": "serial-carrier.kicad_sch",
|
||||||
"name": "serial-carrier",
|
"name": "serial-carrier",
|
||||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"used_designators": "",
|
||||||
|
"variants": []
|
||||||
},
|
},
|
||||||
"sheets": [],
|
"sheets": [],
|
||||||
"text_variables": {},
|
"text_variables": {},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
Requires KiCad's pcbnew binding in system Python. The known U2 footprint parity
|
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.
|
warning is reported explicitly, not hidden or added to KiCad's exclusions.
|
||||||
"""
|
"""
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -29,7 +30,33 @@ assert p.ToMM(board.GetDesignSettings().GetBoardThickness())==1.6
|
|||||||
outline=[d for d in board.GetDrawings() if d.GetLayer()==p.Edge_Cuts]
|
outline=[d for d in board.GetDrawings() if d.GetLayer()==p.Edge_Cuts]
|
||||||
assert len(outline)==6
|
assert len(outline)==6
|
||||||
points={(round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5)) for d in outline for v in (d.GetStart(),d.GetEnd())}
|
points={(round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5)) for d in outline for v in (d.GetStart(),d.GetEnd())}
|
||||||
assert points=={(0,0),(82,0),(82,80),(36,80),(36,61),(0,61)}
|
expected_outline=[(36,0),(82,0),(82,80),(0,80),(0,19),(36,19)]
|
||||||
|
assert points==set(expected_outline)
|
||||||
|
expected_edges={frozenset((a,b)) for a,b in zip(expected_outline,expected_outline[1:]+expected_outline[:1])}
|
||||||
|
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),
|
||||||
|
"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()
|
||||||
|
assert position.x==p.FromMM(x+100) and position.y==p.FromMM(y+100),ref
|
||||||
|
assert footprints[ref].GetOrientationDegrees()==0,ref
|
||||||
|
assert len(board.Zones())==1
|
||||||
|
reserve=board.Zones()[0]
|
||||||
|
assert reserve.GetIsRuleArea() and reserve.IsOnLayer(p.F_Cu) and reserve.IsOnLayer(p.B_Cu)
|
||||||
|
assert reserve.GetDoNotAllowTracks() and reserve.GetDoNotAllowVias() and reserve.GetDoNotAllowZoneFills()
|
||||||
|
contour=reserve.Outline().COutline(0)
|
||||||
|
assert {(p.ToMM(contour.CPoint(i).x),p.ToMM(contour.CPoint(i).y)) for i in range(contour.PointCount())}=={(110,119),(131,119),(131,132),(110,132)}
|
||||||
|
# Reference bare-PCB front is 26.670 mm above the U2 pad-row origin.
|
||||||
|
u2_front=p.ToMM(footprints['U2'].GetPosition().y)-26.670
|
||||||
|
assert abs(u2_front-100)<1e-6
|
||||||
|
assert any(isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and
|
||||||
|
g.GetShape()==p.SHAPE_T_SEGMENT and
|
||||||
|
g.GetStart().y==p.FromMM(100) and g.GetEnd().y==p.FromMM(100)
|
||||||
|
for g in footprints['U2'].GraphicalItems()), 'U2 bare-PCB front not flush'
|
||||||
assert max(x for x,y in points)-min(x for x,y in points)==82
|
assert max(x for x,y in points)-min(x for x,y in points)==82
|
||||||
assert max(y for x,y in points)-min(y for x,y in points)==80
|
assert max(y for x,y in points)-min(y for x,y in points)==80
|
||||||
padnets={}
|
padnets={}
|
||||||
@@ -77,6 +104,13 @@ 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 len(parity[0]["items"])==1 and parity[0]["items"][0]["uuid"]==footprints["U2"].m_Uuid.AsString()
|
||||||
summary={
|
summary={
|
||||||
"carrier_bounds_mm":[82,80],
|
"carrier_bounds_mm":[82,80],
|
||||||
|
"outline_local_mm":expected_outline,
|
||||||
|
"placements_local_mm":placements,
|
||||||
|
"rf_reserve_absolute_mm":[110,119,131,132],
|
||||||
|
"u2_bare_pcb_front_local_y_mm":round(u2_front-100,6),
|
||||||
|
"nominal_user_confirmed_rs232_metal_mm":{"shell_overhang":3.870,"hex_mouth_overhang":2.870,"flange_front_inset":1.930},
|
||||||
|
"tracks":sum(not isinstance(t,p.PCB_VIA) for t in tracks),
|
||||||
|
"vias":sum(isinstance(t,p.PCB_VIA) for t in tracks),
|
||||||
"copper_layers":2,
|
"copper_layers":2,
|
||||||
"draft_thickness_mm":1.6,
|
"draft_thickness_mm":1.6,
|
||||||
"electrical_footprints":len(comps),
|
"electrical_footprints":len(comps),
|
||||||
@@ -84,9 +118,10 @@ summary={
|
|||||||
"physical_DRC_violations":0,
|
"physical_DRC_violations":0,
|
||||||
"unconnected_items":0,
|
"unconnected_items":0,
|
||||||
"schematic_pad_nets_match":True,
|
"schematic_pad_nets_match":True,
|
||||||
"known_parity_warning":"U2 provisional female-reference footprint assigned on PCB only; schematic footprint intentionally blank",
|
"known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
|
||||||
|
"project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(),
|
||||||
"ignored_DRC_checks":report["ignored_checks"],
|
"ignored_DRC_checks":report["ignored_checks"],
|
||||||
"not_validated":["male module pin map/fit", "assembly envelope and 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 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"],
|
||||||
}
|
}
|
||||||
(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.")
|
||||||
|
|||||||
Reference in New Issue
Block a user