mohan-bee/fitness-band

A 4-layer BLE wearable mainboard with Li-ion charging/protection, 3.3/1.8 V power regulation, motion and battery sensing, plus a folded 2-layer flex sensor board carrying MAX30102 optical and TMP117 temperature sensors.

Version
0.0.2
License
unset
Stars
0

scripts/prepare-kicad.py

"""Normalize tscircuit export metadata, deduplicate identical vias, and refill native zones.

Run with KiCad's bundled Python. No trace route is moved or DRC violation suppressed.
"""
from pathlib import Path
import json
import sys
import hashlib
import pcbnew as k

kind = sys.argv[1]
assert kind in ('main', 'flex')
root = Path(__file__).resolve().parents[1]
folder = root / 'manufacturing/rev-b' / kind
path = folder / f'{kind}.kicad_pcb'
source = root / 'dist' / ('index' if kind == 'main' else 'sensor-flex') / 'circuit.json'
b = k.LoadBoard(str(path))
mm = k.FromMM
report = {'kind':kind,'source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),'removed_duplicate_vias':0,'removed_duplicate_segments':0}

# All internal layers are ordinary through-via layers; do not change routing.
seen_vias = {}
seen_segments = set()
removed_items = []
for track in list(b.GetTracks()):
    if track.GetClass() == 'PCB_VIA':
        pos = track.GetPosition()
        key = (pos.x, pos.y, track.TopLayer(), track.BottomLayer())
        if key in seen_vias:
            assert seen_vias[key] == track.GetNetCode(), 'Different nets have coincident vias'
            b.Remove(track)
            removed_items.append(track)
            report['removed_duplicate_vias'] += 1
        else:
            seen_vias[key] = track.GetNetCode()
    elif track.GetClass() == 'PCB_TRACK':
        start,end=track.GetStart(),track.GetEnd()
        ends=tuple(sorted(((start.x,start.y),(end.x,end.y))))
        key=(track.GetNetCode(),track.GetLayer(),track.GetWidth(),ends)
        if key in seen_segments or start == end:
            b.Remove(track)
            removed_items.append(track)
            report['removed_duplicate_segments'] += 1
        else:seen_segments.add(key)

# Copper fill must be computed by KiCad from the intended boundary, rather than
# treating pre-filled polygon rings exported from another renderer as native zones.
report['replaced_exported_zones']=sum(not zone.GetIsRuleArea() for zone in b.Zones())
for zone in list(b.Zones()):
    if not zone.GetIsRuleArea():
        b.Remove(zone)
        removed_items.append(zone)
if kind == 'main':
    ground=next(net for name,net in b.GetNetsByName().items() if str(name) in ('GND','/GND'))
    zone=k.ZONE(b);zone.SetLayer(k.In1_Cu);zone.SetNetCode(ground.GetNetCode())
    zone.SetLocalClearance(mm(0.2));zone.SetPadConnection(k.ZONE_CONNECTION_FULL)
    zone.SetMinThickness(mm(0.15))
    zone.SetIslandRemovalMode(0)
    outline=zone.Outline();outline.NewOutline()
    for x,y in [(-24.2,-11.7),(24.7,-11.7),(24.7,11.7),(-24.2,11.7)]:
        outline.Append(mm(100+x),mm(100-y))
    b.Add(zone)

do_not_place={'J_SWD','J_BAT','J_CHG','J_FLEX','J_TAIL',*[f'TP{i}' for i in range(1,9)]}
for i,fp in enumerate(b.GetFootprints()):
    ref=fp.GetReference()
    if not ref:
        ref=f'AUX{i+1}'
        fp.SetReference(ref)
    fp.SetFPIDAsString(ref+'_embedded')
    if ref in do_not_place or ref.startswith('AUX'):
        fp.SetAttributes(fp.GetAttributes() | k.FP_DNP | k.FP_EXCLUDE_FROM_BOM | k.FP_EXCLUDE_FROM_POS_FILES)
    # Keep all component bodies and labels on assembly/fabrication layers. The
    # small wearable has no room for duplicate imported silk reference strings.
    for item in list(fp.GraphicalItems()):
        if item.GetLayer() == k.F_SilkS:item.SetLayer(k.F_Fab)
        if item.GetLayer() == k.B_SilkS:item.SetLayer(k.B_Fab)
        if item.GetClass() == 'PCB_TEXT': item.SetVisible(False)
    reference=fp.Reference();reference.SetLayer(k.F_Fab);reference.SetPosition(fp.GetPosition())
    reference.SetTextSize(k.VECTOR2I(mm(0.7),mm(0.7)));reference.SetTextThickness(mm(0.1));reference.SetVisible(True)
    fp.Value().SetVisible(False)
    if ref == 'U2' and kind == 'main':
        # Four 1.1 mm paste windows, 0.2 mm gutters, 61.7% EP coverage.
        center=fp.GetPosition()
        for dx in (-0.65,0.65):
            for dy in (-0.65,0.65):
                shape=k.PCB_SHAPE(b);shape.SetShape(k.SHAPE_T_RECT);shape.SetLayer(k.F_Paste)
                shape.SetStart(k.VECTOR2I(center.x+mm(dx-0.55),center.y+mm(dy-0.55)))
                shape.SetEnd(k.VECTOR2I(center.x+mm(dx+0.55),center.y+mm(dy+0.55)))
                shape.SetFilled(True);shape.SetWidth(0);b.Add(shape)

# These explicit project constraints match the intended fabrication process.
# Retain copper, hole, shorts and connectivity checks; no exclusions are added.
rules={
 'min_clearance':0.1, 'min_track_width':0.15, 'min_via_diameter':0.45,
 'min_through_hole_diameter':0.2, 'min_via_annular_width':0.1,
 'min_hole_clearance':0.1, 'min_hole_to_hole':0.2, 'min_copper_edge_clearance':0.2,
 'min_text_height':0.8, 'min_text_thickness':0.12,
}
project={'board':{'design_settings':{'rules':rules}},'meta':{'filename':f'{kind}.kicad_pro','version':3},
 'net_settings':{'meta':{'version':5},'classes':[{'name':'Default','clearance':0.1,'track_width':0.15,'via_diameter':0.45,'via_drill':0.2}]}}
k.SaveBoard(str(path),b)
path.with_suffix('.kicad_pro').write_text(json.dumps(project,indent=2)+'\n')
(folder/'export-normalization.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))