hrithik18k/open-ring-flex

A flexible wearable PCB integrating inductive battery charging, BLE control with SPI flash and 32 MHz crystal, SAM L10 capacitive touch/wear sensing, haptic MOSFET drive with a large capacitor bank, RF antenna matching, and SWD programming/test connectors.

Version
1.0.4
License
CC-BY-NC-SA-4.0
Stars
0

scripts/generate-board.py

import json,re,pathlib
Path=pathlib.Path
root=pathlib.Path('.')
parts=json.loads((root/'source-data/original-components.json').read_text())
netlist=json.loads((root/'source-data/original-netlist.json').read_text())
models={}
def assign(refs,model):
 for ref in refs.split():models[ref]=model
assign('U1','BQ25125YFPR');assign('U2','DA14531_00000FX2');assign('U3','MX25R4035FZUIL0');assign('U4','ATSAML10E16A_MUT')
assign('Y1','W3008');assign('X1','ABM11W_32_0000MHZ_6_B1U_T3');assign('J1 J2','FTSH_105_01_L_DV_K_P_TR')
assign('L1 L4','MLZ1608N2R2LT000');assign('L2 L3','LQP03TN3N3B02D');assign('Q1','PMH600UNEH');assign('NTC1','NTCG104ED104DT1X')
assign('D1 D4','VSKY02300603_G4_08');assign('D2','BZX884B5V1L_HG3_08')
assign('R1','RTT012803FTH');assign('R2','RC0201FR_072M2L');assign('R3','RC0201FR_07100RL');assign('R8 R9','CR0201FH1004G');assign('R10','CR0201FH1003G');assign('R11','RTT012493FTH');assign('R12 R13','CR0201FH1002G');assign('R16 R17','RC0201FR_07316RL')
caps={'10uF':'GRM155R61A106ME44D','2.2uF':'GRM155R60J225ME15D','100nF':'GRM033R60J104KE19D','100pF':'GRM0335C1E101JA01D','47uF':'GRM188R60J476ME15D','1.8pF':'GRM0335C1H1R8CA01D','10pF':'GRM0335C1H100JA01D'}
for p in parts:
 if p['ref'].startswith('C'):models[p['ref']]=caps[p['value']]
# Deliberate functional clusters. All coordinates mm; components on stiffened assembly islands.
pos={
'U1':(-27,0),'L4':(-23,0),'C1':(-28,-3),'C2':(-25,-3),'C3':(-30,0),'C4':(-27,3),'C5':(21,2),'C6':(20,-5),'NTC1':(-32,0),'R1':(-32,-3),'R2':(-34,-3),
'D1':(-38,1),'D4':(-36,1),'D2':(-34,1),'C7':(-38,3),'C8':(-40,3),'C10':(-34,4),'R8':(-35,6),'R9':(-32,6),'R10':(-35,-5),'R11':(-32,-5),
'U4':(-5,0),'C18':(-9,4),'C19':(-9,2),'C20':(-9,0),'R3':(-11,4),'C21':(-1,-4),'C22':(-3,-4),'C23':(-9,-4),'C24':(-7,-4),'R12':(0,3),'R13':(0,5),
'U2':(24,0),'X1':(24,4),'L1':(20,0),'C16':(29,3),'C17':(-30,-3),'C15':(27,-3),'U3':(29,0),
'L2':(28,5),'L3':(34,5),'C9':(31,5),'C11':(27,7),'C12':(30,7),'C13':(33,7),'C14':(36,7),'Y1':(40,5),
'Q1':(-40,-4),'R16':(-38,-4),'R17':(-36,-4),
'J1':(24,-14),'J2':(-5,-14),'TP3':(6,0),'TP4':(6,-3),
'PAD1':(-30,-7),'PAD2':(-27,-7),'PAD3':(-41,6),'PAD4':(-38,6),'PAD5':(-43,-7),'PAD6':(-40,-7)}
for i in range(21):pos[f'C{i+25}']=(-52+(i%3)*2.8,-6+(i//3)*2)
# Schematic coordinates are independent of the validated PCB placement.
layout=json.loads((root/'source-data/schematic-layout.json').read_text())
sections={spec['name']:[] for spec in layout['sections']}
schrot={'Y1': -90.0, 'C4': -90.0, 'C5': -90.0, 'C6': -90.0, 'C11': 90.0, 'C12': 90.0, 'C13': 90.0, 'C14': 90.0, 'C16': -90.0, 'C15': -90.0, 'C25': -90.0, 'C26': 90.0, 'C27': -90.0, 'C28': 90.0, 'C30': 90.0, 'C31': -90.0, 'C32': 90.0, 'C33': -90.0, 'C34': 90.0, 'C35': -90.0, 'C36': 90.0, 'C37': -90.0, 'C41': -90.0, 'C38': 90.0, 'C42': 90.0, 'C39': -90.0, 'C43': -90.0, 'C40': 90.0, 'C44': 90.0, 'C29': 90.0, 'L1': 90.0, 'R17': -90.0, 'R16': -90.0, 'L4': 90.0, 'C10': -90.0, 'C17': 90.0, 'C3': -90.0, 'R1': -90.0, 'R2': -90.0, 'NTC1': 90.0, 'D4': -90.0, 'D1': -90.0, 'D2': -90.0, 'C2': -90.0, 'C1': -90.0, 'C45': 90.0, 'C18': -90.0, 'C19': -90.0, 'C20': -90.0, 'R3': -90.0, 'R12': 90.0, 'R13': 90.0, 'C24': -90.0, 'C21': -90.0, 'C23': -90.0, 'C22': -90.0, 'R11': 90.0, 'R9': -90.0, 'R8': -90.0, 'R10': 90.0}
sch={}
for ref,spec in layout['components'].items():
 sch[ref]=(spec['x'],spec['y'],spec['section'])
 sections[spec['section']].append(ref)
 if 'rotation' in spec:schrot[ref]=spec['rotation']
u4={'A2':1,'A3':2,'A4':3,'B3':4,'B4':5,'A5':6,'C4':7,'B5':8,'B6':9,'C6':10,'D4':11,'D6':12,'C5':13,'D5':14,'E6':15,'E5':16,'D3':17,'F5':18,'E4':19,'E3':20,'F4':21,'F3':22,'F2':23,'E2':24,'D2':25,'C2':26,'E1':27,'D1':28,'C1':29,'B1':30,'B2':31,'C3':32}
def newpin(r,p):
 if r=='U4':return 'pin'+str(u4[p])
 if r=='U1':return 'pin'+str((ord(p[0])-65)*5+int(p[1]))
 if r=='Q1':return p
 if r in ['D1','D2','D4']:return 'A' if p=='1' else 'K'
 if r=='Y1':return 'pin'+p
 return 'pin'+p
nets=[]
for n in netlist:
 pins=list(dict.fromkeys(f".{p['ref']} > .{newpin(p['ref'],p['pin'])}" for p in n['pins']))
 name=re.sub(r'[^A-Za-z0-9_]','_',n['name']);name=('V'+name if name[0].isdigit() else name)
 nets.append({'name':name,'original':n['name'],'pins':pins})
for ref,net in [('PAD1','VBAT'),('PAD2','GND'),('PAD3','COIL+'),('PAD4','GND'),('PAD5','VHAP'),('PAD6','MOTOR-')]:
 next(n for n in nets if n['original']==net)['pins'].append(f'.{ref} > .pin1')
next(n for n in nets if n['name']=='GND')['pins'].append('.U4 > .pin33')
# Six covered copper electrodes are PCB features, not purchasable BOM components.
electrodes=['TOUCH_SLIDER_RX2','TOUCH_SLIDER_RX1','TOUCH_SLIDER_RX0','TOUCH_SLIDER_TX0','TOUCH_FINGER_RX','TOUCH_FINGER_TX']
for i,n in enumerate(electrodes):next(t for t in nets if t['name']==n)['pins'].append(f'.E{i+1} > .pin1')
priority=['VIN','VDDCORE','VDDANA','PMID','VHAP','V1V1','VBAT','V1V8','GND']
nets.sort(key=lambda n:priority.index(n['name']) if n['name'] in priority else 99)
(root/'source-data/adapted-netlist.json').write_text(json.dumps(nets,indent=2))
lines=['import { Fragment } from "react"','// Open Ring adaptation. CC BY-NC-SA-4.0. See README.md and source-data/.']
for model in sorted(set(models.values())):lines.append(f'import {{ {model} }} from "./imports/{model}"')
lines+=['import schematicLabels from "./source-data/schematic-net-labels.json"','import nativeRoutes from "./source-data/native-routed-paths.json"','import crystalPaths from "./source-data/crystal-paths.json"','import type { FanoutTracePath } from "@tscircuit/props"','import nets from "./source-data/adapted-netlist.json"','export default () => (', '<board title="Open Ring — JLCPCB flex adaptation" material="flex" layers={4} thickness={0.22}', '  outline={[{x:-55,y:9},{x:51,y:9},{x:51,y:-9},{x:30,y:-9},{x:30,y:-19},{x:18,y:-19},{x:18,y:-9},{x:1,y:-9},{x:1,y:-19},{x:-11,y:-19},{x:-11,y:-9},{x:-55,y:-9}]}', '  minTraceWidth={0.075} minTraceToPadEdgeClearance={0.075} minPadEdgeToPadEdgeClearance={0.075}', '  minViaHoleDiameter={0.1} minViaPadDiameter={0.2} minBoardEdgeClearance={0.25}', '  allowBlindAndBuriedVias isViaInPadAllowed routeRemaining={false} autorouter={{local:true}} >']
lines.append('  <autoroutingphase name="remaining-nets" phaseIndex={0} pcbTracePaths={nativeRoutes as FanoutTracePath[]} />')
lines.append('  {nets.map(n => <Fragment key={n.name}><net name={n.name} routingPhaseIndex={["XTAL32N","XTAL32P"].includes(n.name) ? 1 : 0} /></Fragment>)}')
lines.append('  <autoroutingphase name="crystal-paths" phaseIndex={1} pcbTracePaths={crystalPaths as FanoutTracePath[]} />')
# Place sections and their boundary net labels on native schematic sheets.
labels=[]
for net in nets:
 if len(net['pins'])<2:continue
 owners={layout['components'][pin.split(' > ')[0][1:]]['section'] for pin in net['pins']}
 if net['name'] in ['GND','V1V8','V1V1','VIN','VBAT','VHAP','VDDANA','VDDCORE','PMID','VLS'] or len(owners)<2:continue
 for pin in net['pins']:
  ref=pin.split(' > ')[0][1:]
  labels.append({'net':net['name'],'pin':pin,'sheet':layout['components'][ref]['sheet']})
(root/'source-data/schematic-net-labels.json').write_text(json.dumps(labels,indent=2)+'\n')
for i,sheet in enumerate(layout['sheets']):
 lines.append(f'  <schematicsheet name="{sheet["name"]}" displayName="{sheet["displayName"]}" sheetIndex={{{i}}}>')
 for spec in layout['sections']:
  if spec['sheet']==sheet['name']:
   lines.append(f'    <schematicsection name="{spec["name"]}" />')
   lines.append(f'    <schematictext text="{spec["displayName"]}" schX={{{spec["titleX"]}}} schY={{{spec["titleY"]}}} anchor="top_left" fontSize={{0.3}} />')
 lines.append('    {schematicLabels.filter(label => label.sheet === "'+sheet['name']+'").map(label => <Fragment key={label.pin}><netlabel net={label.net} connection={label.pin} /></Fragment>)}')
 lines.append('  </schematicsheet>')
for p in parts:
 r=p['ref'];x,y=pos[r];sx,sy,s=sch[r];props=f'name="{r}" pcbX={{{x}}} pcbY={{{y}}} schX={{{sx}}} schY={{{sy}}} schSectionName="{s}" schSheetName="{layout["components"][r]["sheet"]}"'
 props+=f' schRotation={{{schrot.get(r,0)}}}'
 if r in ['C6','C12','C17','R1','R2','D4','R9','R10']:props+=' pcbRotation={180}'
 if r in ['C7','C8','C9']:props+=' schOrientation="vertical"'
 if r in ['J1','J2']:props+=' schPinArrangement={{topSide:[1],bottomSide:[3,5,9],leftSide:[2,4,6],rightSide:[7,8,10]}} schWidth={0.8}'
 if r=='X1':props+=' loadCapacitance="6pF"'
 if r=='U3':props+=' schPinArrangement={{topSide:[8],bottomSide:[4,9],leftSide:[1,3,7],rightSide:[2,5,6]}} schWidth={1.6} schHeight={1.2}'
 if r=='U2':props+=' schWidth={2.34} schHeight={3.2} schPinStyle={{pin5:{marginRight:0.7}}} pcbRotation={180} schPinArrangement={{topSide:[5,7],bottomSide:[2,18,19,20,21,23],leftSide:[1,3,4,6,8,9,10,11],rightSide:[12,13,14,15,16,17,22,24]}}'
 if r=='Y1':props+=' schWidth={2.2} pinLabels={{pin1:["RF"],pin2:["GND_NEAR"],pin3:["GND_FAR"]}} footprint={<footprint><smtpad portHints={["pin1"]} shape="rect" pcbX={-1.6} pcbY={-0.475} width={0.8} height={0.65}/><smtpad portHints={["pin2"]} shape="rect" pcbX={-1.6} pcbY={0.475} width={0.8} height={0.65}/><smtpad portHints={["pin3"]} shape="rect" pcbX={1.6} pcbY={0} width={0.8} height={1.6}/></footprint>}'
 if r in models:lines.append(f'  <{models[r]} {props} />')
 else:lines.append(f'  <testpoint doNotPlace {props} padDiameter={{{1.2 if r.startswith("PAD") else .8}}} />')
for i,n in enumerate(electrodes):
 ref=f'E{i+1}'; sx,sy,section=sch[ref]; sheet=layout['components'][ref]['sheet']
 x=6+(i%3)*4;y=5 if i<3 else -5
 lines.append(f'  <chip doNotPlace name="E{i+1}" pcbX={{{x}}} pcbY={{{y}}} schX={{{sx}}} schY={{{sy}}} schSectionName="{section}" schSheetName="{sheet}" pinLabels={{{{pin1:["{n}"]}}}} footprint={{<footprint><smtpad portHints={{["pin1"]}} shape="rect" width={{2.8}} height={{2}} coveredWithSolderMask noSolderPaste /></footprint>}} />')
lines+=['  <keepout shape="rect" pcbX={40} pcbY={5} width={6.25} height={4} layers={["inner1","inner2","bottom"]} />','  <keepout shape="rect" pcbX={40} pcbY={5} width={2.35} height={4.25} layers={["top"]} />','  {nets.filter(net => net.pins.length > 1).flatMap(net => net.pins.map((pin, i) => <trace key={`${net.name}_${i}`} name={`${net.name}_${i}`} from={pin} to={"net." + net.name} schDisplayLabel={net.name} />))}', '</board>',')','']
(root/'index.circuit.tsx').write_text('\n'.join(lines),encoding='utf8')
bom=[]
for p in parts:
 r=p['ref']
 if r not in models:continue
 content=(root/'imports'/f'{models[r]}.tsx').read_text(); jlc=re.search(r'"jlcpcb":\s*\[\s*"([^"]+)"',content).group(1)
 bom.append({'ref':r,'original':p['value'],'model':models[r],'jlcpcb':jlc})
(root/'source-data/bom.json').write_text(json.dumps(bom,indent=2))
print(f'Generated {len(models)} imported parts, {len(nets)} nets, 6 electrodes, 8 solder/test pads')