#!/usr/bin/env python3 """v6 T11 - Block U: SC7A20TR 3-axis accelerometer (vibration telemetry). U26 SC7A20TR (Silan, LGA-12 2x2, LCSC C5126709, ~$0.25; register-compatible with ST LIS3DH driver). I2C mode on the existing bus: CS->3V3, SDO/SA0->GND (addr 0x18 - no clash: SHT40 0x44, PCF8574 0x20+). INT1 -> /ACC_INT1 -> U16 PB9 (pin 62, EXTI9). INT2 left NC. Decoupling 100nF x2 + 10uF. Pin mapping is resolved FROM THE VENDOR SYMBOL'S PIN NAMES (Drogheda-v6.kicad_sym, easyeda2kicad C5126709) and asserted - nothing is guessed by pin number. Review the printed pin->net table in the dry run before --apply. Prereq on the Mac: easyeda2kicad --full --lcsc_id C5126709 --output ~/Documents/Claude/Projects/Drogheda/03-Architecture-Hardware/kicad/Drogheda-v6 Usage: python3 2026-09-01_v6_T11_accel.py [--apply] """ import os, sys, shutil, uuid, re, 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') APPLY = '--apply' in sys.argv ROOT = '/561403b3-895e-4153-a7be-3876ae930d43' NC = '' def f(v): v = 0.0 if abs(v) < 1e-9 else v return '%.10g' % v def bal(t): d=0; instr=False; esc=False for ch in t: if esc: esc=False; continue if ch=='\\': esc=True; continue if ch=='"': instr = not instr; continue if instr: 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 extractor that also captures the pin NAME PIN_NAMED = re.compile(r'\(pin \w+ \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?' r'\(name "([^"]*)"(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S) def libpins_named(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)] return [(num, nm, float(x), float(y)) for x,y,rot,nm,num in PIN_NAMED.findall(body)] sch = open(SCH, encoding='utf-8').read() vsym = open(VSYM, encoding='utf-8').read() uuids_before = set(re.findall(r'\(uuid "([0-9a-f-]+)"\)', sch)) # ---------- 1. find the vendor symbol + footprint ---------- symname = None for m in re.finditer(r'\(symbol "((?:SC7A20|Sc7a20)[^"]*)"', vsym): symname = m.group(1); break if not symname: sys.exit('SC7A20 symbol not found in Drogheda-v6.kicad_sym - run on the Mac first:\n' ' easyeda2kicad --full --lcsc_id C5126709 --output ~/Documents/Claude/Projects/Drogheda/03-Architecture-Hardware/kicad/Drogheda-v6') fp_hits = [p for p in glob.glob(os.path.join(KI,'Drogheda-v6.pretty','*.kicad_mod')) if re.search(r'LGA-12_L2|SC7A20', os.path.basename(p), re.I)] if not fp_hits: sys.exit('SC7A20 LGA-12 footprint not found in Drogheda-v6.pretty - run easyeda2kicad C5126709 first') u26_fp = 'Drogheda-v6:' + os.path.basename(fp_hits[0])[:-len('.kicad_mod')] pins = libpins_named(vsym, symname) if not pins or len(pins) < 10: sys.exit('pin extraction failed for %s: got %s' % (symname, pins)) # ---------- 2. map pins to nets BY NAME ---------- RULES = [(r'^VDD_?IO$','3V3'), (r'^VDD$','3V3'), (r'^(GND|RES|RESERVED)',' GND'.strip()), (r'(SDO.*SA0|SA0.*SDO|^SDO$|^SA0$)','GND'), # addr select -> 0x18 (before SDA rule) (r'(SCL|SPC|^SCX$)','I2C_SCL'), (r'(SDA|SDI|^SDX$)','I2C_SDA'), (r'^CS','3V3'), (r'INT_?1','ACC_INT1'), (r'INT_?2',NC), (r'^(NC|N\.C\.?)$',NC)] mapping = {} for num, nm, px, py in pins: nmu = nm.upper().replace('~','').strip() net = None for pat, tgt in RULES: if re.search(pat, nmu): net = tgt; break if net is None: sys.exit('unmapped pin: number %s name %r - extend RULES' % (num, nm)) mapping[num] = (nm, net, px, py) print('U26 %s (fp %s) pin -> net:' % (symname, u26_fp)) for num in sorted(mapping, key=lambda n: int(re.sub(r'\D','',n) or 0)): nm, net, _, _ = mapping[num] print(' %3s %-12s -> %s' % (num, nm, net)) nets_used = [net for _,(nm,net,_,_) in mapping.items()] for req, cnt in (('I2C_SCL',1),('I2C_SDA',1),('ACC_INT1',1)): assert nets_used.count(req) == cnt, 'expected exactly %d %s pin, got %d' % (cnt, req, nets_used.count(req)) assert nets_used.count('3V3') >= 2, 'expected VDD+VDDIO/CS on 3V3' # ---------- 3. guards ---------- for r in ('U26','C158','C159','C160'): if '(property "Reference" "%s"' % r in sch: sys.exit('refdes %s already used' % r) lib_cached = sch.split('(lib_symbols',1)[1] PIN_RE2= re.compile(r'\(pin \w+ \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S) def cachedpins(name): i = lib_cached.find('(symbol "%s"' % name) if i < 0: return None j = lib_cached.find('\n\t\t(symbol "', i+10) body = lib_cached[i: j if j>0 else len(lib_cached)] return [(num, float(x), float(y)) for x,y,rot,num in PIN_RE2.findall(body)] # U16 PB9 = pin 62: coords + existing NC u16 = None for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*', sch, re.S): blk=b.group(0) if '(property "Reference" "U16"' in blk: l=re.search(r'\(lib_id "([^"]+)"',blk).group(1) a=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk) assert a.group(3)=='0' and '(mirror' not in blk[:400] u16=(l,float(a.group(1)),float(a.group(2))); break assert u16, 'U16 not found' pb9 = [(px,py) for num,px,py in cachedpins(u16[0]) if num=='62'] assert len(pb9)==1, 'U16 pin 62 (PB9) not found' PB9 = (round(u16[1]+pb9[0][0],3), round(u16[2]-pb9[0][1],3)) print('PB9 sheet coord:', PB9) # ---------- 4. cache vendor symbol into schematic ---------- def grab(libsrc, name, prefix): i = libsrc.find(' (symbol "%s"\n' % name) if i < 0: sys.exit('symbol %s not found for caching' % name) j = libsrc.index('(', i) k = span_from(libsrc, j) return libsrc[j:k].replace('(symbol "%s"' % name, '(symbol "%s:%s"' % (prefix,name), 1) if '(symbol "Drogheda-v6:%s"' % symname not in sch: m = re.search(r'\n\t\(lib_symbols\n', sch) k = span_from(sch, sch.index('(', m.start())) sch = sch[:k-1] + '\n\t\t' + grab(vsym, symname, 'Drogheda-v6') + '\n\t' + sch[k-1:] lib_cached = sch.split('(lib_symbols',1)[1] # ---------- 5. build additions ---------- add = '' newlabels=[]; nnc=0 def add_lab(net,x,y,rot,just): global add 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()) newlabels.append((net,round(x,3),round(y,3))) def add_nc(x,y): global add,nnc add += '\n\t(no_connect\n\t\t(at %s %s)\n\t\t(uuid "%s")\n\t)' % (f(x),f(y),uuid.uuid4()) nnc += 1 def mksym(lib_id, ref, val, x, y, fp, npins, lcsc=''): props=[('Reference',ref,x-3,y-3,''),('Value',val,x+3,y-3,''),('Footprint',fp,x,y,'\n\t\t\t(hide yes)'), ('Datasheet','~',x,y,'\n\t\t\t(hide yes)'),('Description','',x,y,'\n\t\t\t(hide yes)')] if lcsc: props.append(('LCSC',lcsc,x,y,'\n\t\t\t(hide yes)')) p='' for nm,v,px,py,hide in props: 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),hide) 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 no)\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),uuid.uuid4(),p,pn,ROOT,ref) def orient(px,py): if abs(px) < 0.01: return 90,'left bottom' return (0,'left bottom') if px>0 else (180,'right bottom') add += ('\n\t(text "BLOCK U - VIBRATION (T11): U26 SC7A20 3-axis accel, I2C addr 0x18 ' '(CS=3V3, SA0=GND), INT1 -> PB9. LAYOUT: place near a mounting hole for rigid ' 'chassis coupling; not on a flexy board edge."\n' '\t\t(exclude_from_sim no)\n\t\t(at 40 390 0)\n\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.6 1.6)\n' '\t\t\t)\n\t\t\t(justify left bottom)\n\t\t)\n\t\t(uuid "%s")\n\t)') % uuid.uuid4() U26X,U26Y = 60.0, 408.0 add += mksym('Drogheda-v6:%s'%symname,'U26','SC7A20TR',U26X,U26Y,u26_fp, [num for num,_,_,_ in pins],'C5126709') for num,(nm,net,px,py) in mapping.items(): rot,just = orient(px,py) x,y = U26X+px, U26Y-py if net==NC: add_nc(x,y) else: add_lab(net,x,y,rot,just) for ref,val,fp,x,y,n1,n2 in ( ('C158','100nF','Capacitor_SMD:C_0402_1005Metric',85.0,404.0,'3V3','GND'), ('C159','100nF','Capacitor_SMD:C_0402_1005Metric',91.0,404.0,'3V3','GND'), ('C160','10uF','Capacitor_SMD:C_0805_2012Metric',97.0,404.0,'3V3','GND')): pp={num:(px,py) for num,px,py in cachedpins('Device:C')} add += mksym('Device:C',ref,val,x,y,fp,['1','2']) add_lab(n1,x+pp['1'][0],y-pp['1'][1],90,'left bottom') add_lab(n2,x+pp['2'][0],y-pp['2'][1],90,'left bottom') # PB9 label + stale NC removal add_lab('ACC_INT1', PB9[0], PB9[1], 0, 'left bottom') out=[]; last=0; pb9_nc=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 == PB9: out.append(sch[last:m.start()]); last=m.end(); pb9_nc+=1 out.append(sch[last:]); sch=''.join(out) # ---------- 6. collision check ---------- labs=set() for m in re.finditer(r'\n\t\((label|global_label) "[^"]+"\s*\n\s*\(at ([-\d.]+) ([-\d.]+)', sch): labs.add((round(float(m.group(2)),3), round(float(m.group(3)),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))) pin_coords=set() for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*', sch, re.S): blk=b.group(0) ll=re.search(r'\(lib_id "([^"]+)"',blk); aa=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk) if ll and aa: for num,px,py in (cachedpins(ll.group(1)) or []): pin_coords.add((round(float(aa.group(1))+px,3), round(float(aa.group(2))-py,3))) INTENTIONAL = {PB9} existing = (labs | wire_vts | pin_coords) - INTENTIONAL coll = [(n,x,y) for n,x,y in newlabels if (x,y) in existing] if coll: sys.exit('LABEL COLLISION: %s' % coll[:8]) end = sch.rstrip(); assert end.endswith(')') sch = end[:-1].rstrip('\n') + add + '\n)\n' lost = uuids_before - set(re.findall(r'\(uuid "([0-9a-f-]+)"\)', sch)) if len(lost) > pb9_nc: sys.exit('REFUSING: %d unexpected pre-existing UUIDs vanished' % len(lost)) print('added: U26 + C158/C159/C160; labels %d, NCs %d; PB9 label placed, stale NC removed: %d' % (len(newlabels), nnc, pb9_nc)) print('paren: %d' % bal(sch)) if not APPLY: print('\nDRY RUN - review the pin->net table above, then re-run with --apply.'); sys.exit(0) if bal(sch)!=0: sys.exit('REFUSING: paren imbalance') shutil.copy2(SCH,SCH+'.T11bak') open(SCH,'w',encoding='utf-8').write(sch) print('\nAPPLIED. Backup: .T11bak')