pixalynx/pixal-gps
These files define two separate printed circuit boards: a small, two-layer battery cartridge with protection circuitry and contact pads for a pouch cell, and a larger, two-layer wireless charging dock featuring USB-C input, a resonant coil, and wireless power transmission components.
- Version
- 0.1.2
- License
- unset
- Stars
- 0
review/inspect-design.py
import json,collections,math,pathlib
root=pathlib.Path(__file__).resolve().parents[1]
def inside(x,y,verts):
hit=False
for a,b in zip(verts,verts[1:]+verts[:1]):
if (a['y']>y)!=(b['y']>y) and x<(b['x']-a['x'])*(y-a['y'])/(b['y']-a['y'])+a['x']:hit=not hit
return hit
out={}
for name in ['tracker','battery-pack','dock']:
c=json.loads((root/f'dist/{name}/circuit.json').read_text());src={e['source_component_id']:e for e in c if e['type']=='source_component'};sports={e['source_port_id']:e for e in c if e['type']=='source_port'};ports={e['pcb_port_id']:e for e in c if e['type']=='pcb_port'};nets={e['source_net_id']:e['name'] for e in c if e['type']=='source_net'};traces={e['source_trace_id']:e for e in c if e['type']=='source_trace'}
keynames={}
for t in traces.values():
for n in t.get('connected_source_net_ids',[]):
for pid in t.get('connected_source_port_ids',[]):keynames[sports[pid].get('subcircuit_connectivity_map_key')]=nets[n]
pads=[e for e in c if e['type']=='pcb_smtpad'];pours=[e for e in c if e['type']=='pcb_copper_pour'];stats=collections.defaultdict(lambda:{'segments':0,'length_mm':0,'widths':set(),'layers':set()})
for t in c:
if t['type']!='pcb_trace':continue
st=traces.get(t.get('source_trace_id'),{});pids=st.get('connected_source_port_ids',[]);k=sports[pids[0]].get('subcircuit_connectivity_map_key') if pids else None;n=keynames.get(k,t.get('connection_name','unknown'))
for a,b in zip(t['route'],t['route'][1:]):
if a['route_type']!='wire' or b['route_type']!='wire' or a['layer']!=b['layer']:continue
stt=stats[n];stt['segments']+=1;stt['length_mm']+=math.hypot(a['x']-b['x'],a['y']-b['y']);stt['widths'].add(b.get('width',a.get('width')));stt['layers'].add(a['layer'])
for st in stats.values():st['widths']=sorted(st['widths']);st['layers']=sorted(st['layers']);st['length_mm']=round(st['length_mm'],3)
padpour=[]
for p in pads:
sp=sports.get(ports.get(p.get('pcb_port_id'),{}).get('source_port_id'),{});n=src.get(sp.get('source_component_id'),{}).get('name','')+'.'+sp.get('name','')
if n not in ['U1.P0_31','U3.VBUS','U6.SCL','U6.INT1','C41.pin2','R13.pin1'] or name!='tracker':continue
touching=[]
for pour in pours:
if pour['layer']!=p['layer']:continue
sh=pour['brep_shape'];covers=inside(p['x'],p['y'],sh['outer_ring']['vertices']) and not any(inside(p['x'],p['y'],h['vertices']) for h in sh.get('inner_rings',[]))
if covers:touching.append({'pour':pour['pcb_copper_pour_id'],'net':nets.get(pour.get('source_net_id'))})
padpour.append({'pad':n,'x':p['x'],'y':p['y'],'net':keynames.get(sp.get('subcircuit_connectivity_map_key')),'center_in_pour':touching})
holes=collections.Counter()
for h in c:
if h['type']!='pcb_plated_hole':continue
for p in pads:
if h.get('pcb_component_id')==p.get('pcb_component_id') and p['shape']=='rect' and abs(h['x']-p['x'])<p['width']/2 and abs(h['y']-p['y'])<p['height']/2:
sp=sports.get(ports.get(p.get('pcb_port_id'),{}).get('source_port_id'),{});holes[src.get(sp.get('source_component_id'),{}).get('name','?')]+=1;break
out[name]={'errors':collections.Counter(e['type'] for e in c if e['type'].endswith('_error')),'footprint_warning_count':sum(e['type']=='supplier_footprint_mismatch_warning' for e in c),'power_copper':{n:stats[n] for n in ['VIN','PGND','LX1','LX2','COIL_A','VBAT','VSYS','VDD_NRF','VBUS_QI','AC1','AC2','RECT'] if n in stats},'missing_endpoint_pour_check':padpour,'plated_holes_inside_smt_lands':dict(holes)}
(root/'review/evidence/design-metrics.json').write_text(json.dumps(out,indent=2)+'\n');print(json.dumps(out,indent=2))