Drogheda/scripts/2026-08-25_v6_T6_rf_block.py

235 lines
12 KiB
Python

#!/usr/bin/env python3
"""v6 T6 - Block S: RF front ends (J12 GNSS + bias tee, J13 LTE + pi-pad), RF ESD,
P17 u-center USB port, and the RF / PWR_MODEM netclasses.
Parts: U.FL = C88374 (fp RF-SMD_FRF05002-JSS103M, 1=SIG 2/3=GND shield);
RF ESD = ESD9L5.0ST5G C82326 (0.5 pF - the PESD used elsewhere is ~70 pF, NOT RF-safe).
Usage: python3 2026-08-25_v6_T6_rf_block.py [--apply]
"""
import os, sys, shutil, uuid, re, json, glob
KI = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '03-Architecture-Hardware', 'kicad')
SCH = os.path.join(KI, 'hbox-watchdog-v6.kicad_sch')
VSYM= os.path.join(KI, 'Drogheda-v6.kicad_sym')
PRO = os.path.join(KI, 'hbox-watchdog-v6.kicad_pro')
APPLY = '--apply' in sys.argv
ROOT = '/561403b3-895e-4153-a7be-3876ae930d43'
def f(v):
v = 0.0 if abs(v) < 1e-9 else v
return '%.10g' % v
def bal(t):
d=0;i=False;e=False
for ch in t:
if e: e=False; continue
if ch=='\\': e=True; continue
if ch=='"': i=not i; continue
if i: continue
if ch=='(': d+=1
elif ch==')': d-=1
return d
def span_from(text,i):
d=0;instr=False;esc=False;j=i
while j<len(text):
ch=text[j]
if esc: esc=False
elif ch=='\\': esc=True
elif ch=='"': instr=not instr
elif not instr:
if ch=='(': d+=1
elif ch==')':
d-=1
if d==0: return j+1
j+=1
raise ValueError('unbalanced')
PIN_RE = re.compile(r'\(pin \w+ \w+\s*\(?at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(name "([^"]*)"(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S)
PIN_RE2= re.compile(r'\(pin \w+ \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(name "([^"]*)"(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S)
def libpins(libsrc,name):
i=libsrc.find('(symbol "%s"'%name)
if i<0: return None
j=libsrc.find('\n (symbol "',i+10)
if j<0: j=libsrc.find('\n\t\t(symbol "',i+10)
body=libsrc[i: j if j>0 else len(libsrc)]
pins=PIN_RE2.findall(body) or PIN_RE.findall(body)
return [(num,nm,float(x),float(y)) for x,y,rot,nm,num in pins]
sch=open(SCH,encoding='utf-8').read()
vsym=open(VSYM,encoding='utf-8').read()
# --- prerequisites ---
esd_syms=[n for n in re.findall(r'\n \(symbol "([^"]+)"',vsym) if 'ESD9L' in n]
if not esd_syms: sys.exit('ESD9L5.0ST5G not in Drogheda-v6.kicad_sym - run easyeda2kicad C82326 first')
ESDN=esd_syms[0]
esd_fp_files=[os.path.basename(p)[:-10] for p in glob.glob(os.path.join(KI,'Drogheda-v6.pretty','*.kicad_mod'))
if 'SOD' in os.path.basename(p).upper() and '128' not in os.path.basename(p)]
if not esd_fp_files: sys.exit('no SOD-9xx footprint found for the ESD9L part')
ESDFP='Drogheda-v6:'+esd_fp_files[0]
esdp=libpins(vsym,ESDN)
print('ESD symbol %s pins: %s fp=%s'%(ESDN,[(n,nm) for n,nm,_,_ in esdp],ESDFP))
# cathode pad = the one NOT named A/anode; EasyEDA usually 1=A? assert explicit naming
kpin=[n for n,nm,_,_ in esdp if nm.upper() in ('K','C','CATHODE','2','1') and nm.upper() in ('K','C','CATHODE')]
apin=[n for n,nm,_,_ in esdp if nm.upper() in ('A','ANODE')]
if not (kpin and apin):
# fall back: unidirectional SOD-923 convention on EasyEDA symbols: pin named by number
sys.exit('ESD9L pin names ambiguous: %s - inspect and hardcode before applying'%[(n,nm) for n,nm,_,_ in esdp])
KP,AP=kpin[0],apin[0]
UFL=libpins(vsym,'U.FL-R-SMT-1')
if not UFL: sys.exit('U.FL-R-SMT-1 symbol missing')
lib_cached=sch.split('(lib_symbols',1)[1]
def cachedpins(name):
pp=libpins(lib_cached,name)
return pp
# --- cache new symbols in sch ---
def grab(libsrc,name,prefix):
i=libsrc.find(' (symbol "%s"\n'%name)
if i<0: sys.exit('symbol %s not found'%name)
j=libsrc.index('(',i); k=span_from(libsrc,j)
return libsrc[j:k].replace('(symbol "%s"'%name,'(symbol "%s:%s"'%(prefix,name),1)
cache=''
for nm in ('U.FL-R-SMT-1',ESDN):
if '(symbol "Drogheda-v6:%s"'%nm not in sch:
cache+='\n\t\t'+grab(vsym,nm,'Drogheda-v6')
m=re.search(r'\n\t\(lib_symbols\n',sch)
k=span_from(sch,sch.index('(',m.start()))
sch=sch[:k-1]+cache+'\n\t'+sch[k-1:]
# --- helpers ---
add=''; nlab=0; newlabels=[]
def add_lab(net,x,y,rot,just):
global add,nlab
add+=('\n\t(label "%s"\n\t\t(at %s %s %d)\n\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.27 1.27)\n'
'\t\t\t)\n\t\t\t(justify %s)\n\t\t)\n\t\t(uuid "%s")\n\t)')%(net,f(x),f(y),rot,just,uuid.uuid4())
nlab+=1; newlabels.append((net,round(x,3),round(y,3)))
def orient(px,py):
if abs(px)<0.01: return 90,'left bottom'
return (0,'left bottom') if px>0 else (180,'right bottom')
def mksym(lib_id,ref,val,x,y,fp,npins,lcsc='',dnp=False):
props=[('Reference',ref,x-3,y-3,False),('Value',val,x+3,y-3,False),('Footprint',fp,x,y,True),
('Datasheet','~',x,y,True),('Description','',x,y,True)]
if lcsc: props.append(('LCSC',lcsc,x,y,True))
p=''
for nm,v,px_,py_,hide in props:
h='\n\t\t\t(hide yes)' if hide else ''
p+=('\t\t(property "%s" "%s"\n\t\t\t(at %s %s 0)%s\n\t\t\t(show_name no)\n\t\t\t(do_not_autoplace no)\n'
'\t\t\t(effects\n\t\t\t\t(font\n\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n')%(nm,v,f(px_),f(py_),h)
pn=''.join('\t\t(pin "%s"\n\t\t\t(uuid "%s")\n\t\t)\n'%(n,uuid.uuid4()) for n in npins)
return ('\n\t(symbol\n\t\t(lib_id "%s")\n\t\t(at %s %s 0)\n\t\t(unit 1)\n\t\t(body_style 1)\n'
'\t\t(exclude_from_sim no)\n\t\t(in_bom yes)\n\t\t(on_board yes)\n\t\t(in_pos_files yes)\n'
'\t\t(dnp %s)\n\t\t(uuid "%s")\n%s%s\t\t(instances\n\t\t\t(project "hbox-watchdog-v6"\n'
'\t\t\t\t(path "%s"\n\t\t\t\t\t(reference "%s")\n\t\t\t\t\t(unit 1)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t)'
)%(lib_id,f(x),f(y),'yes' if dnp else 'no',uuid.uuid4(),p,pn,ROOT,ref)
def place(lib_id,ref,val,x,y,fp,netmap,lcsc='',dnp=False,src=None):
global add
pp=libpins(src,lib_id.split(':',1)[1]) if src is not None else cachedpins(lib_id)
if not pp: sys.exit('no pins for %s'%lib_id)
add+=mksym(lib_id,ref,val,x,y,fp,[n for n,_,_,_ in pp],lcsc,dnp)
for num,_,px,py in pp:
rot,just=orient(px,py)
add_lab(netmap[num],x+px,y-py,rot,just)
UFLFP='Drogheda-v6:RF-SMD_FRF05002-JSS103M'
# ----- S1: GNSS chain -----
place('Drogheda-v6:U.FL-R-SMT-1','J12','UFL-GNSS',400.0,155.0,UFLFP,{'1':'RF_ANT_GNSS','2':'GND','3':'GND'},'C88374',src=vsym)
place('Device:C','C143','100nF-RF',418.0,150.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'RF_ANT_GNSS','2':'RF_GNSS'})
place('Device:L','L21','68nH',424.0,160.0,'Inductor_SMD:L_0603_1608Metric',{'1':'RF_ANT_GNSS','2':'VCC_RF_GNSS'})
place('Device:C','C144','100nF',432.0,160.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'VCC_RF_GNSS','2':'GND'})
place('Device:C','C145','100pF',438.0,160.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'VCC_RF_GNSS','2':'GND'})
place('Drogheda-v6:'+ESDN,'D27','ESD9L5.0ST5G',406.0,165.0,ESDFP,{KP:'RF_ANT_GNSS',AP:'GND'},'C82326',src=vsym)
# ----- S2: LTE chain -----
place('Device:C','C146','DNP-pi',296.0,262.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'RF_LTE','2':'GND'},dnp=True)
place('Device:R','R118','0R',302.0,262.0,'Resistor_SMD:R_0402_1005Metric',{'1':'RF_LTE','2':'RF_ANT_LTE'})
place('Device:C','C147','DNP-pi',308.0,262.0,'Capacitor_SMD:C_0402_1005Metric',{'1':'RF_ANT_LTE','2':'GND'},dnp=True)
place('Drogheda-v6:U.FL-R-SMT-1','J13','UFL-LTE',330.0,262.0,UFLFP,{'1':'RF_ANT_LTE','2':'GND','3':'GND'},'C88374',src=vsym)
place('Drogheda-v6:'+ESDN,'D28','ESD9L5.0ST5G',344.0,262.0,ESDFP,{KP:'RF_ANT_LTE',AP:'GND'},'C82326',src=vsym)
# ----- P17: u-center USB port -----
place('Connector_Generic:Conn_01x04','P17','GNSS-USB-UCENTER',410.0,185.0,
'Connector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical',
{'1':'GNSS_VBUS','2':'GNSS_USB_DM','3':'GNSS_USB_DP','4':'GND'})
place('Device:C','C148','1uF',428.0,185.0,'Capacitor_SMD:C_0603_1608Metric',{'1':'GNSS_VBUS','2':'GND'})
d14lib=None
for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*',sch,re.S):
blk=b.group(0)
if re.search(r'\(property "Reference" "D14"',blk):
d14lib=re.search(r'\(lib_id "([^"]+)"',blk).group(1); break
for ref,sig,x in [('D29','GNSS_USB_DM',436.0),('D30','GNSS_USB_DP',442.0),('D31','GNSS_VBUS',448.0)]:
place(d14lib,ref,'PESD5V0S1BA',x,185.0,'Diode_SMD:D_SOD-323',{'1':sig,'2':'GND'},'C2827694')
# labels on U18 USB pins (replacing the no_connects removed below)
add_lab('GNSS_VBUS',436.98,107.3,180,'right bottom')
add_lab('GNSS_USB_DM',503.02,116.19,0,'left bottom')
add_lab('GNSS_USB_DP',503.02,118.73,0,'left bottom')
# --- remove the 3 no_connects on U18 V_USB / USB_DM / USB_DP ---
STALE={(436.98,107.3),(503.02,116.19),(503.02,118.73)}
removed=0; out=[]; last=0
for m in re.finditer(r'\n\t\(no_connect\s*\n\s*\(at ([-\d.]+) ([-\d.]+)\)(?:(?!\n\t\().)*?\n\t\)',sch,re.S):
c=(round(float(m.group(1)),3),round(float(m.group(2)),3))
if c in STALE:
out.append(sch[last:m.start()]); last=m.end(); removed+=1
out.append(sch[last:]); sch=''.join(out)
# --- collision guard ---
labs=set()
for m in re.finditer(r'\((?:label|global_label) "[^"]+"\s*\n\s*\(at ([-\d.]+) ([-\d.]+)',sch):
labs.add((round(float(m.group(1)),3),round(float(m.group(2)),3)))
wire_vts=set()
for m in re.finditer(r'\(wire\s*\n\s*\(pts\s*\n((?:\s*\(xy [-\d.]+ [-\d.]+\)\s*\n?)+)',sch):
for a,b in re.findall(r'\(xy ([-\d.]+) ([-\d.]+)\)',m.group(1)):
wire_vts.add((round(float(a),3),round(float(b),3)))
pins=set()
for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*',sch,re.S):
blk=b.group(0)
l=re.search(r'\(lib_id "([^"]+)"',blk); a=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk)
if not(l and a): continue
for pp in (cachedpins(l.group(1)) or []):
pins.add((round(float(a.group(1))+pp[2],3),round(float(a.group(2))-pp[3],3)))
INTENTIONAL={(436.98,107.3),(503.02,116.19),(503.02,118.73)}
existing=(labs|wire_vts|pins)-INTENTIONAL
coll=[(n,x,y) for n,x,y in newlabels if (x,y) in existing]
if coll: sys.exit('COLLISION: %s'%coll[:6])
end=sch.rstrip(); assert end.endswith(')')
sch=end[:-1].rstrip('\n')+add+'\n)\n'
# --- netclasses in the .kicad_pro (JSON) ---
pro=json.load(open(PRO,encoding='utf-8'))
ns=pro.setdefault('net_settings',{})
classes=ns.setdefault('classes',[])
names=[c.get('name') for c in classes]
proto=None
for c in classes:
if c.get('name') not in ('Default',None): proto=c; break
if proto is None and classes: proto=classes[0]
def mkclass(name,width,clearance):
base=dict(proto) if proto else {}
base.update({'name':name,'track_width':width,'clearance':clearance})
base.pop('nets',None)
return base
added_classes=[]
if 'RF' not in names:
classes.append(mkclass('RF',0.35,0.25)); added_classes.append('RF')
if 'PWR_MODEM' not in names:
classes.append(mkclass('PWR_MODEM',1.5,0.2)); added_classes.append('PWR_MODEM')
ncp=ns.setdefault('netclass_patterns',[])
pats={p.get('pattern') for p in ncp}
for net,cls in [('/RF_GNSS','RF'),('/RF_ANT_GNSS','RF'),('/RF_LTE','RF'),('/RF_ANT_LTE','RF'),
('/VBAT_MODEM','PWR_MODEM'),('/VIN_MODEM','PWR_MODEM'),('/SW_MODEM','PWR_MODEM')]:
if net not in pats:
ncp.append({'netclass':cls,'pattern':net})
print('added: J12+bias tee (C143 DC block, L21 68nH, C144/C145), D27 | J13+pi (C146/C147 DNP, R118 0R), D28')
print('P17 u-center port + C148 + D29/D30/D31 PESD | U18 USB labels on, %d no_connects removed'%removed)
print('netclasses added: %s + 7 net patterns'%added_classes)
print('labels added: %d | paren: %d'%(nlab,bal(sch)))
if not APPLY:
print('\nDRY RUN - nothing written. Re-run with --apply.'); sys.exit(0)
if bal(sch)!=0: sys.exit('REFUSING: paren imbalance')
shutil.copy2(SCH,SCH+'.T6bak'); shutil.copy2(PRO,PRO+'.T6bak')
open(SCH,'w',encoding='utf-8').write(sch)
json.dump(pro,open(PRO,'w',encoding='utf-8'),indent=2)
print('\nAPPLIED. Backups: *.T6bak')