From 0a50ff7d21b58b08eaac868a6077c39441cdc576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Tue, 1 Sep 2026 22:06:21 +0000 Subject: [PATCH] v6 T11 accelerometer (submodule 23ed361) + T11 script Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt --- 03-Architecture-Hardware/kicad | 2 +- scripts/2026-09-01_v6_T11_accel.py | 256 +++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 scripts/2026-09-01_v6_T11_accel.py diff --git a/03-Architecture-Hardware/kicad b/03-Architecture-Hardware/kicad index 83ed72e..23ed361 160000 --- a/03-Architecture-Hardware/kicad +++ b/03-Architecture-Hardware/kicad @@ -1 +1 @@ -Subproject commit 83ed72e2d63ef357640189df993fd8765a87f543 +Subproject commit 23ed361f0d1bbb72e6ba2978d9f0834dae5c9e79 diff --git a/scripts/2026-09-01_v6_T11_accel.py b/scripts/2026-09-01_v6_T11_accel.py new file mode 100644 index 0000000..dda2aaf --- /dev/null +++ b/scripts/2026-09-01_v6_T11_accel.py @@ -0,0 +1,256 @@ +#!/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')