project
Adom Custom Electronic Load
Public Made by Adomby adom
Independent compact digitally controlled load design for 3rdPartyFab; research and AIFlow evidence in progress
main
John Lauer
R0 engineering checkpoint: checked schematic, sourcing, servo evidence and safety-policy code; PCB and qualification incomplete
10054d3
1h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
#!/usr/bin/env python3
"""Hierarchical engineering schematic; circuit sections and explicit divider chains."""
from pathlib import Path
import json,uuid,math,copy
import sexp
from sexp import Sym as S
P=Path(__file__).resolve().parents[1]; OUT=P/'kicad'; NAME='adom-custom-electronic-load'; N=json.loads((P/'design/netlist.json').read_text());C={c['ref']:c for c in N['components']}
def uid(s):return str(uuid.uuid5(uuid.NAMESPACE_URL,NAME+'/'+s))
def font(n=1.27):return [S('effects'),[S('font'),[S('size'),n,n]]]
def base(root,title):return [S('kicad_sch'),[S('version'),20250114],[S('generator'),'adom-load-design'],[S('uuid'),root],[S('paper'),'A3'],[S('title_block'),[S('title'),title],[S('company'),'Adom'],[S('rev'),'R0 ENGINEERING DRAFT']]]
def text(s,x,y):return [S('text'),s,[S('exclude_from_sim'),S('no')],[S('at'),x,y,0],font(),[S('uuid'),uid(s+str(x)+str(y))]]
libs=sexp.lib_symbols(P/'libraries/Device.kicad_sym');cache={}
def symbol(c):
ref=c['ref']
if ref.startswith('U') and ref!='U10':
f=OUT/'symbol-work'/c['mpn']/(c['mpn']+'.kicad_sym'); d=sexp.lib_symbols(f);out=sexp.flatten(c['mpn'],d,'Load')
# Adom-symbol 1.x emits functional-group offsets at 2.00 mm; snap pin tips to the required 1.27 mm connection grid.
for unit in sexp.find(out,'symbol'):
for pin in sexp.find(unit,'pin'):
at=sexp.first(pin,'at');at[1]=round(float(at[1])/1.27)*1.27;at[2]=round(float(at[2])/1.27)*1.27
return out
if ref.startswith('R') and ref!='RSH1':return sexp.flatten('R',libs,'Device')
if ref.startswith('C'):return sexp.flatten('C',libs,'Device')
if ref=='L1':return sexp.flatten('L',libs,'Device')
if ref in ('D1','D2','D4'):return sexp.flatten('D_Schottky',libs,'Device')
if ref=='F1':return sexp.flatten('Fuse',libs,'Device')
if ref=='RSH1':return sexp.flatten('R_Shunt',libs,'Device')
if ref=='D3':return sexp.flatten('D_Dual_Series_AKC',libs,'Device')
if ref in ['Q1','Q3','Q4']:
name='Q_NMOS_GDS' if ref=='Q1' else '2N7002';return sexp.flatten(name,sexp.lib_symbols(P/'libraries/Transistor_FET.kicad_sym'),'Transistor_FET')
if ref=='Q2':return sexp.flatten('MMBT3904',sexp.lib_symbols(P/'libraries/Transistor_BJT.kicad_sym'),'Transistor_BJT')
# Explicit numbered terminals for remaining package-specific parts, review pending.
name=ref;pins=list(c['pins']);h=max(5.08,len(pins)*1.27)
body=[S('symbol'),'Load:'+name,[S('pin_names'),[S('offset'),0.5]],[S('in_bom'),S('yes')],[S('on_board'),S('yes')],[S('property'),'Reference',ref,[S('at'),0,h+2.54,0],font()],[S('property'),'Value',c['value'],[S('at'),0,-h-2.54,0],font()],[S('symbol'),name+'_0_1',[S('rectangle'),[S('start'),-7.62,h],[S('end'),7.62,-h],[S('stroke'),[S('width'),0],[S('type'),S('default')]],[S('fill'),[S('type'),S('background')]]]]]
unit=[S('symbol'),name+'_1_1']
for i,n in enumerate(pins):
py=(len(pins)-1)*1.27-i*2.54
unit.append([S('pin'),S('passive'),S('line'),[S('at'),-10.16,py,0],[S('length'),2.54],[S('name'),str(n),font()],[S('number'),str(n),font()]])
body.append(unit);return body
root=uid('root');rootbody=base(root,'Custom electronic load: circuit overview');sexp.first(rootbody,'paper')[1]='A2';rootbody.append([S('lib_symbols')]);rootbody.append(text('R0 engineering draft - not approved for fabrication. Limits: 24 V, 3 A, 25 W simultaneously.\nUSB/UART control, analogue current servo, independent latched shutdown.\nUnverified: cooling, loop stability, contact ratings, complete sourcing and fault coverage.',180,30))
blocks=['power','servo','telemetry','protection','auxiliary','digital','mechanical','molecule','probe','control'];pages=[]; alldefs={}
for block in blocks:
parts=[c for c in N['components'] if c['block']==block]
if not parts:continue
sheetid=uid('sheet/'+block);sheetroot=uid('root/'+block);page=base(sheetroot,block.title()+' - custom electronic load');defs={};body=[];pos={};tips={};angles={}
# ICs occupy a row each; passives form columns underneath.
ics=[c for c in parts if c['ref'].startswith('U')];small=[c for c in parts if c not in ics]
for i,c in enumerate(ics):pos[c['ref']]=(80+i*150,75)
for i,c in enumerate(small):pos[c['ref']]=(38+(i%6)*62,160+(i//6)*30) if ics else (38+(i%6)*62,55+(i//6)*38)
if block=='digital':
for i,c in enumerate(small):pos[c['ref']]=(38+(i%6)*62,220+(i//6)*36)
pos['J8']=(450,90)
# Local dividers are connected by real vertical wires, grouped physically.
chains={'servo':[['R8','R9','R10']], 'protection':[['R18','R19'],['R20','R21'],['R22','R23'],['R27','R28']], 'auxiliary':[['R35','R36','R37']], 'telemetry':[['R24','R25']]}.get(block,[])
for j,chain in enumerate(chains):
for i,ref in enumerate(chain):pos[ref]=(35+j*85,160+i*25)
if block=='power':
pos.update({'F1':(55.88,50.8),'D1':(55.88,76.2),'Q1':(129.54,76.2),'RSH1':(132.08,106.68),'R6':(101.6,106.68)})
# Other passives are parked to the right / additional sheet height so they cannot collide with chains.
chainrefs={r for ch in chains for r in ch}
if chains:
others=[c for c in small if c['ref'] not in chainrefs]
for i,c in enumerate(others):pos[c['ref']]=(35+(i%6)*62,250+(i//6)*30)
# A2 for crowded protection/digital pages; all coordinates remain visible.
if chains or len(small)>18:
sexp.first(page,'paper')[1]='A2'
for c in parts:
ref=c['ref'];sd=symbol(c);libid=sd[1];defs[libid]=sd;alldefs[libid]=sd;x,y=[round(v/1.27)*1.27 for v in pos[ref]];pins=sexp.pins(sd)
sy=[S('symbol'),[S('lib_id'),libid],[S('at'),x,y,0],[S('unit'),1],[S('exclude_from_sim'),S('no')],[S('in_bom'),S('no') if c['block']=='probe' else S('yes')],[S('on_board'),S('yes')],[S('dnp'),S('no')],[S('uuid'),uid('sym/'+ref)]]
top=max([py for _,_,_,py,_ in pins]+[3.81]);bot=min([py for _,_,_,py,_ in pins]+[-3.81])
for key,val,px,py,hide in [('Reference',ref,x,y-top-3.81,False),('Value',c['value'],x,y-bot+3.81,False),('Footprint',c['fp'],x,y,True),('MPN',c['mpn'],x,y,True)]:
ef=font(.9 if key=='Value' else 1.27)
if hide:ef.append([S('hide'),S('yes')])
sy.append([S('property'),key,val,[S('at'),px,py,0],ef])
for n,pn,px,py,a in pins:
tips[(ref,n)]=(round(x+px,4),round(y-py,4));angles[(ref,n)]=a
sy.append([S('pin'),n,[S('uuid'),uid('pin/'+ref+'/'+n)]])
sy.append([S('instances'),[S('project'),NAME,[S('path'),'/'+root+'/'+sheetid,[S('reference'),ref],[S('unit'),1]]]])
body.append(sy)
connected=set()
def wire(a,b,key):
if a==b:return
body.append([S('wire'),[S('pts'),[S('xy'),*a],[S('xy'),*b]],[S('stroke'),[S('width'),0],[S('type'),S('default')]],[S('uuid'),uid(key)]])
def label(net,pt,key):body.append([S('global_label'),net,[S('shape'),S('bidirectional')],[S('at'),*pt,0],[S('fields_autoplaced'),S('yes')],font(.9),[S('uuid'),uid(key)],[S('property'),'Intersheetrefs','${INTERSHEET_REFS}',[S('at'),*pt,0],[S('effects'),[S('font'),[S('size'),1.27,1.27]],[S('hide'),S('yes')]]]])
for chain in chains:
for ra,rb in zip(chain,chain[1:]):
assert C[ra]['pins']['2']==C[rb]['pins']['1']
a=tips[(ra,'2')];b=tips[(rb,'1')];wire(a,b,'chain/'+ra+rb);label(C[ra]['pins']['2'],a,'chainlabel/'+ra);connected|={(ra,'2'),(rb,'1')}
if block=='power':
# Main dissipative path: input fuse -> reverse diode -> MOSFET -> four-terminal shunt.
for ra,na,rb,nb in [('F1','2','D1','2'),('D1','1','Q1','2'),('Q1','3','RSH1','1')]:
a=tips[(ra,na)];b=tips[(rb,nb)];mid=(b[0],a[1]) if ra=='F1' else (a[0],b[1]);wire(a,mid,'power-a/'+ra+rb);wire(mid,b,'power-b/'+ra+rb);label(C[ra]['pins'][na],a,'power-label/'+ra);connected|={(ra,na),(rb,nb)}
for c in parts:
for n,netname in c['pins'].items():
key=(c['ref'],n)
if key not in tips:raise ValueError('missing symbol pin '+str(key))
if key in connected:continue
a=tips[key]
if netname is None:
body.append([S('no_connect'),[S('at'),*a],[S('uuid'),uid('nc/'+str(key))]]);continue
ang=math.radians(angles[key]);b=(round(a[0]-math.cos(ang)*5.08,4),round(a[1]+math.sin(ang)*5.08,4));wire(a,b,'wire/'+str(key));label(netname,b,'label/'+str(key))
page.append([S('lib_symbols')]+list(defs.values()));page+=body;page.append(text('DRAFT: inspect pin names, actual package mapping, values and protection coverage before release.',190,15));(OUT/(block+'.kicad_sch')).write_text(sexp.dump(page)+'\n')
i=len(pages);sx=30+(i%3)*115;sy=60+(i//3)*55
rootbody.append([S('sheet'),[S('at'),sx,sy],[S('size'),85,35],[S('fields_autoplaced'),S('yes')],[S('stroke'),[S('width'),0],[S('type'),S('default')]],[S('fill'),[S('color'),0,0,0,0]],[S('uuid'),sheetid],[S('property'),'Sheetname',block.title(),[S('at'),sx,sy-1.27,0],font()],[S('property'),'Sheetfile',block+'.kicad_sch',[S('at'),sx,sy+36.27,0],font()],[S('instances'),[S('project'),NAME,[S('path'),'/'+root,[S('page'),str(i+2)]]]]]);pages.append(block)
# Off-board sources and boost output after a passive diode need ERC power flags.
powerlibs=sexp.lib_symbols(P/'references/buck-tooling/kicad/lib/power.kicad_sym')
flag=sexp.flatten('PWR_FLAG',powerlibs,'power');sexp.first(rootbody,'lib_symbols').append(flag)
for i,rail in enumerate(['5V_AUX','GND','V_GATE','SHUNT_N_SENSE']):
x=round((60+i*110)/1.27)*1.27;y=round(290/1.27)*1.27;ref='#FLG0'+str(i+1)
rootbody.append([S('symbol'),[S('lib_id'),'power:PWR_FLAG'],[S('at'),x,y,0],[S('unit'),1],[S('exclude_from_sim'),S('no')],[S('in_bom'),S('no')],[S('on_board'),S('no')],[S('dnp'),S('no')],[S('uuid'),uid(ref)],
[S('property'),'Reference',ref,[S('at'),x,y-5.08,0],font()], [S('property'),'Value','PWR_FLAG',[S('at'),x,y-2.54,0],font()],
[S('pin'),'1',[S('uuid'),uid(ref+'/1')]], [S('instances'),[S('project'),NAME,[S('path'),'/'+root,[S('reference'),ref],[S('unit'),1]]]]])
rootbody.append([S('global_label'),rail,[S('shape'),S('bidirectional')],[S('at'),x,y,0],[S('fields_autoplaced'),S('yes')],font(),[S('uuid'),uid('flaglabel/'+rail)]])
local=[]
for k,v in alldefs.items():
if k.startswith('Load:'):
d=copy.deepcopy(v);d[1]=k.split(':',1)[1];local.append(d)
(OUT/'lib/Load.kicad_sym').write_text(sexp.dump([S('kicad_symbol_lib'),[S('version'),20250114],[S('generator'),'adom-load-design']]+local)+'\n')
rootbody.append([S('sheet_instances'),[S('path'),'/',[S('page'),'1']]]);(OUT/(NAME+'.kicad_sch')).write_text(sexp.dump(rootbody)+'\n');print('Generated',len(pages)+1,'sheets and',len(C),'components')