v6 T11 accelerometer (submodule 23ed361) + T11 script

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
This commit is contained in:
Leopoldo Rodríguez 2026-09-01 22:06:21 +00:00
parent 7fea0a5c11
commit 0a50ff7d21
2 changed files with 257 additions and 1 deletions

@ -1 +1 @@
Subproject commit 83ed72e2d63ef357640189df993fd8765a87f543
Subproject commit 23ed361f0d1bbb72e6ba2978d9f0834dae5c9e79

View File

@ -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 = '<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')