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/finish-mic-data.py
#!/usr/bin/env python3
"""Finish the two microphone data branches left by FreeRouting.
Route on the back copper with a conservative grid search around existing
copper and imported pad bounds; original component geometry is unchanged.
The independent KiCad DRC is the acceptance check for the resulting copper.
"""
import sys
sys.path.insert(0,"/workspace/.cache/native-python")
import heapq
import math
import pathlib
import pcbnew
from shapely.geometry import Point, LineString, box
from shapely.ops import unary_union
from shapely.prepared import prep
root = pathlib.Path(__file__).resolve().parents[1]
path = root / "dist/MuseVoiceRemote-library.kicad_pcb"
board = pcbnew.LoadBoard(str(path))
net = next(n for n in board.GetNetsByNetcode().values() if n.GetNetname()=="MIC_SD")
def xy(point): return pcbnew.ToMM(point.x),pcbnew.ToMM(point.y)
def vector(point): return pcbnew.VECTOR2I(pcbnew.FromMM(point[0]),pcbnew.FromMM(point[1]))
obstacles = []
layer = pcbnew.B_Cu
clearance = 0.175 # 0.15 mm required, plus margin for grid interpolation.
width = 0.15
for track in board.GetTracks():
if track.GetNetname()=="MIC_SD": continue
if isinstance(track,pcbnew.PCB_VIA):
obstacles.append(Point(xy(track.GetPosition())).buffer(pcbnew.ToMM(track.GetWidth(pcbnew.B_Cu))/2+clearance+width/2))
elif track.GetLayer()==layer:
obstacles.append(LineString([xy(track.GetStart()),xy(track.GetEnd())]).buffer(pcbnew.ToMM(track.GetWidth())/2+clearance+width/2))
for footprint in board.GetFootprints():
for pad in footprint.Pads():
if pad.GetNetname()=="MIC_SD" or not pad.GetLayerSet().Contains(layer): continue
bounds = pad.GetBoundingBox()
lo=xy(bounds.GetOrigin());hi=xy(bounds.GetEnd())
obstacles.append(box(*lo,*hi).buffer(clearance+width/2))
occupied=prep(unary_union(obstacles))
step=0.125
def snap(point): return round(point[0]/step),round(point[1]/step)
def point(node): return node[0]*step,node[1]*step
def search(start,end):
start_node,end_node=snap(start),snap(end)
todo=[(0,0,start_node)];cost={start_node:0};previous={}
directions=[(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]
while todo:
_,g,node=heapq.heappop(todo)
if g>cost[node]:continue
if node==end_node:
path=[node]
while path[-1] in previous:path.append(previous[path[-1]])
path.reverse();return [start]+[point(n) for n in path]+[end]
for dx,dy in directions:
nxt=(node[0]+dx,node[1]+dy);pos=point(nxt)
if abs(pos[0])>32.3 or abs(pos[1])>23.3:continue
if occupied.intersects(LineString([point(node),pos])):continue
new=g+math.hypot(dx,dy)
if new>=cost.get(nxt,float("inf")):continue
cost[nxt]=new;previous[nxt]=node
h=math.hypot(nxt[0]-end_node[0],nxt[1]-end_node[1])
heapq.heappush(todo,(new+h*1.05,new,nxt))
raise RuntimeError("No clear back-copper route for microphone data")
def add_track(a,b,layer):
if math.dist(a,b)<0.0001:return
track=pcbnew.PCB_TRACK(board);track.SetStart(vector(a));track.SetEnd(vector(b))
track.SetLayer(layer);track.SetWidth(pcbnew.FromMM(width));track.SetNet(net);board.Add(track)
for x in [-28,28]:
landing=(x,-23.1 if x<0 else -23.2)
via=pcbnew.PCB_VIA(board);via.SetPosition(vector(landing))
for copper_layer in [pcbnew.F_Cu,pcbnew.In1_Cu,pcbnew.In2_Cu,pcbnew.B_Cu]: via.SetWidth(copper_layer,pcbnew.FromMM(0.6))
via.SetDrill(pcbnew.FromMM(0.3));via.SetViaType(pcbnew.VIATYPE_THROUGH)
via.SetLayerPair(pcbnew.F_Cu,pcbnew.B_Cu);via.SetNet(net);board.Add(via)
add_track((x,-22.864),landing,pcbnew.F_Cu)
route=search(landing,(-15.7367,-19.0))
for a,b in zip(route,route[1:]):add_track(a,b,layer)
print(f"Finished MIC_SD branch at X={x}: {len(route)-1} grid segments")
pcbnew.SaveBoard(str(path),board)