#!/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 j0 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')