AnasSarkiz/muse-voice-remote

Muse Voice Remote — bare ESP32-S3R8, 155 TOP JLCPCB parts with real supplier CAD, native 3D assembly and GLB exports. Revised 17 mm case allocation; 0.30 mm via drill /0.45 mm pad. Checked prototype PCB; mechanical, RF and MUSE qualification pending.

Version
1.3.2
License
unset
Stars
0

scripts/save-routed-copper.py

#!/usr/bin/env python3
"""Retain the verified native copper as a tscircuit visualization interchange."""
import hashlib
import json
import pathlib
import shutil
import pcbnew
import sys
sys.path.insert(0,"/workspace/.cache/native-python")
from shapely.geometry import LineString, box
root = pathlib.Path(__file__).resolve().parents[1]
board_path=root / "dist/MuseVoiceRemote-library.kicad_pcb"
board=pcbnew.LoadBoard(str(board_path))
catalog=json.loads((root / "docs/component-catalog.json").read_text())
populated={p["reference"]:p for p in catalog if p.get("library")}
native={f.GetReference():f for f in board.GetFootprints() if f.GetReference() in populated}
anchor_path = root / "docs/jlcpcb-native-anchor-offsets.json"
anchors = json.loads(anchor_path.read_text()) if anchor_path.exists() else {}
if set(native)!=set(populated):
    raise RuntimeError("Native CAD component references differ from current catalog")
for ref,p in populated.items():
    footprint=native[ref]
    pos=footprint.GetPosition()
    offset = anchors.get(ref,{"dx":0,"dy":0}) if p["library"].startswith("JLCPCB/") else {"dx":0,"dy":0}
    # SPECCTRA resolution is 10 ticks per micrometre (0.0001 mm).
    tolerance=0.0001 if p["library"].startswith("JLCPCB/") else 0.000001
    if abs(pcbnew.ToMM(pos.x)-(p["x"]+offset["dx"]))>tolerance or abs(pcbnew.ToMM(pos.y)-(-p["y"]+offset["dy"]))>tolerance:
        raise RuntimeError(f"Stale native placement for {ref}")
    if abs((footprint.GetOrientationDegrees()-p["rotation"]+180)%360-180)>0.000001:
        raise RuntimeError(f"Stale native rotation for {ref}")
    actual={pad.GetNumber():pad.GetNetname() for pad in footprint.Pads() if pad.GetNetname()}
    if p["library"].startswith("JLCPCB/"):
        lcsc=p["library"].split("/")[-1]
        ports=[e for e in json.loads((root/f"vendor/jlcpcb/{lcsc}.circuit.json").read_text()) if e["type"]=="source_port"]
        translated={}
        for key,net in actual.items():
            if ref=="J_BAT" and key=="0": key="4"
            if not key.isdigit():
                port=next((e for e in ports if key in e.get("port_hints",[])),None)
                if not port: raise RuntimeError(f"Unknown native imported pad: {ref}.{key}")
                key=str(port["pin_number"])
            translated[key]=net
        actual=translated
    if actual!=p["padNets"]:
        raise RuntimeError(f"Stale native pin nets for {ref}")
layers={pcbnew.F_Cu:"top",pcbnew.In1_Cu:"inner1",pcbnew.In2_Cu:"inner2",pcbnew.B_Cu:"bottom"}
def xy(point): return [pcbnew.ToMM(point.x),-pcbnew.ToMM(point.y)]
tracks=[];vias=[]
for track in board.GetTracks():
    if isinstance(track,pcbnew.PCB_VIA):
        vias.append({"net":track.GetNetname(),"at":xy(track.GetPosition()),
                     "outerDiameter":pcbnew.ToMM(track.GetWidth(pcbnew.F_Cu)),"holeDiameter":pcbnew.ToMM(track.GetDrillValue())})
    else:
        tracks.append({"net":track.GetNetname(),"start":xy(track.GetStart()),"end":xy(track.GetEnd()),
                       "width":pcbnew.ToMM(track.GetWidth()),"layer":layers[track.GetLayer()]})
reference_regions = {"RF":(-15,-7,0,-3),"crystal":(-4,-12,4,-5)}
for track in board.GetTracks():
    if isinstance(track,pcbnew.PCB_VIA) or track.GetLayer()!=pcbnew.In1_Cu:continue
    if track.GetNetname()!="GND":
        raise RuntimeError("Layer 2 is reserved entirely for ground; signal tracks are prohibited")
ground_planes=[{"net":zone.GetNetname(),"layer":layers[zone.GetLayer()],"filled":bool(zone.IsFilled())} for zone in board.Zones() if not zone.GetIsRuleArea()]
pours=[]
def ring_points(chain):
    return [{"x":pcbnew.ToMM(chain.CPoint(i).x),"y":-pcbnew.ToMM(chain.CPoint(i).y)} for i in range(chain.PointCount())]
for zone in board.Zones():
    if zone.GetIsRuleArea():continue
    # KiCad serializes filled holes using zero-width linked cuts. Unfracture
    # a COPY into explicit BRep inner rings without altering native copper.
    polygon=pcbnew.SHAPE_POLY_SET(zone.GetFilledPolysList(zone.GetLayer()))
    original_area=polygon.Area()
    polygon.Unfracture()
    # Unfracture removes nanometre-wide serialization cuts. Allow only
    # 0.0001 mm² of quantization difference; retain every clearance hole.
    if abs(polygon.Area()-original_area)>1e8:
        raise RuntimeError("Plane BRep conversion changed native filled area")
    for index in range(polygon.OutlineCount()):
        outer=polygon.COutline(index)
        holes=[]
        for hole_index in range(polygon.HoleCount(index)):
            hole=polygon.CHole(index,hole_index)
            holes.append({"vertices":ring_points(hole)})
        pours.append({"net":zone.GetNetname(),"layer":layers[zone.GetLayer()],
                      "brep_shape":{"outer_ring":{"vertices":ring_points(outer)},"inner_rings":holes}})
if not any(z=={"net":"GND","layer":"inner1","filled":True} for z in ground_planes):
    raise RuntimeError("Bare chip requires its filled inner ground reference")
snapshot={"ground_reference_regions_native_mm":reference_regions,"ground_planes":ground_planes,"pours":pours,"input_sha256":hashlib.sha256((root / "docs/routing-input.json").read_bytes()).hexdigest(),
          "cad_sha256":hashlib.sha256(board_path.read_bytes()).hexdigest(),
          "router":"FreeRouting 2.5.0 using JLCPCB/EasyEDA imports; native KiCad 9 independent DRC",
          "drc":{"violations":0,"unconnected":0,"footprint_errors":0},"tracks":tracks,"vias":vias}
report=(root / "dist/native-routed-drc.txt").read_text()
if not all(s in report for s in ["Found 0 DRC violations","Found 0 unconnected pads","Found 0 Footprint errors"]):
    raise RuntimeError("Native DRC is not clean; refusing to save verified route state")
(root / "design/routed-copper.json").write_text(json.dumps(snapshot,indent=2)+"\n")
(root / "hardware").mkdir(exist_ok=True)
shutil.copyfile(board_path,root / "hardware/MuseVoiceRemote.kicad_pcb")
shutil.copyfile(root / "dist/MuseVoiceRemote-library.kicad_pro",root / "hardware/MuseVoiceRemote.kicad_pro")
shutil.copyfile(root / "dist/native-routed-drc.txt",root / "hardware/drc-report.txt")
print(f"Saved {len(tracks)} copper segments, {len(vias)} vias and verified native CAD.")