From 7fea0a5c119d69729f8bd1f23e4df5bbf1f05f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Mon, 31 Aug 2026 21:48:32 +0000 Subject: [PATCH] v6 T10 sensor set (submodule 83ed72e) + T10 script --- 03-Architecture-Hardware/kicad | 2 +- scripts/2026-08-31_v6_T10_sensors.py | 257 +++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 scripts/2026-08-31_v6_T10_sensors.py diff --git a/03-Architecture-Hardware/kicad b/03-Architecture-Hardware/kicad index b92a187..83ed72e 160000 --- a/03-Architecture-Hardware/kicad +++ b/03-Architecture-Hardware/kicad @@ -1 +1 @@ -Subproject commit b92a18710b694d80c59c840183378a00efddc3ac +Subproject commit 83ed72e2d63ef357640189df993fd8765a87f543 diff --git a/scripts/2026-08-31_v6_T10_sensors.py b/scripts/2026-08-31_v6_T10_sensors.py new file mode 100644 index 0000000..74079f0 --- /dev/null +++ b/scripts/2026-08-31_v6_T10_sensors.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +"""v6 T10 - final-version sensor set: embedded SHT40 + two cheap external NTC probes. + +- U3 SHT40-AD1B stays soldered on-board (already in the design) = tair + humidity. +- DELETE U4 TMP102 (replaced by the 2nd NTC probe channel). +- DELETE P5 TEMP_PROBE jack (external SHT40 probe; redundant vs on-board U3, and a + probe SHT40 would collide with U3 at I2C address 0x44 anyway). +- KEEP P12 + R42 10k pullup + C24 100nF -> PC5 ADC (/NTC_TEMP) = external probe #1 (tntc). +- ADD P18 (same JST-XH 2-pin) + R126 10k/1% (3V3 pullup) + C157 100nF -> /NTC2 -> PC4 + (U16 pin 24, ADC12_IN14) = external probe #2. Identical topology to P12's channel so + the firmware conversion carries over (per-probe Beta constant in firmware). + +Usage: python3 2026-08-31_v6_T10_sensors.py [--apply] +""" +import os, sys, shutil, uuid, re + +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') +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; 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_RE2= re.compile(r'\(pin \w+ \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S) + +def libpins(libsrc, name): + i = libsrc.find('(symbol "%s"' % name) + if i < 0: return None + j = libsrc.find('\n\t\t(symbol "', i+10) + body = libsrc[i: j if j>0 else len(libsrc)] + return [(num, float(x), float(y)) for x,y,rot,num in PIN_RE2.findall(body)] + +sch = open(SCH, encoding='utf-8').read() +uuids_before = set(re.findall(r'\(uuid "([0-9a-f-]+)"\)', sch)) +lib_cached = sch.split('(lib_symbols',1)[1] +def cachedpins(name): return libpins(lib_cached, name) + +# ---------- 1. locate the doomed instances ---------- +def find_inst(ref): + for b in re.finditer(r'\n\t\(symbol\n', sch): + a = b.start()+1 + e = span_from(sch, sch.index('(', a)) + blk = sch[a:e] + if '(property "Reference" "%s"' % ref in blk: + l = re.search(r'\(lib_id "([^"]+)"', blk).group(1) + m = re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)', blk) + assert m.group(3) == '0' and '(mirror' not in blk[:400], (ref,'not rot0/nomirror') + return (a, e, l, float(m.group(1)), float(m.group(2))) + sys.exit('%s not found' % ref) + +doomed = {} +for ref in ('U4','P5'): + doomed[ref] = find_inst(ref) +# sanity: expected identities +assert 'TMP102' in sch[doomed['U4'][0]:doomed['U4'][1]] +assert 'TEMP_PROBE' in sch[doomed['P5'][0]:doomed['P5'][1]] + +# refdes guards for additions +for r in ('P18','R126','C157'): + if '(property "Reference" "%s"' % r in sch: sys.exit('refdes %s already used' % r) +assert '(property "Reference" "P12"' in sch and '(property "Reference" "R42"' in sch + +# ---------- 2. delete labels / no_connects at the doomed pins, then the symbols ---------- +def pin_coords_of(inst): + a,e,l,X,Y = inst + pp = cachedpins(l) + assert pp, 'no lib pins for %s' % l + return {(round(X+px,3), round(Y-py,3)) for num,px,py in pp} + +kill_coords = set() +for ref in doomed: kill_coords |= pin_coords_of(doomed[ref]) + +removed_lab = removed_nc = 0 +out=[]; last=0 +for m in re.finditer(r'\n\t\((label|global_label) "[^"]+"\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\n\t\().)*?\n\t\)', sch, re.S): + c=(round(float(m.group(2)),3),round(float(m.group(3)),3)) + if c in kill_coords: + out.append(sch[last:m.start()]); last=m.end(); removed_lab+=1 +out.append(sch[last:]); sch=''.join(out) +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 kill_coords: + out.append(sch[last:m.start()]); last=m.end(); removed_nc+=1 +out.append(sch[last:]); sch=''.join(out) + +# wires hanging off doomed pins (U4 ADD0 has a 1.27mm wire to a GND label): chain-trace +# from the doomed pin coords, delete chained wires + labels at chain points. Guarded: abort +# if the chain grows past 3 wires (would mean eating a shared net wire) or touches a +# surviving symbol's pin. +wires=[] +for m in re.finditer(r'\n\t\(wire\s*\n\s*\(pts\s*\n((?:\s*\(xy [-\d.]+ [-\d.]+\)\s*\n?)+)(?:(?!\n\t\().)*?\n\t\)', sch, re.S): + pts=[(round(float(a),3),round(float(b),3)) for a,b in re.findall(r'\(xy ([-\d.]+) ([-\d.]+)\)', m.group(1))] + wires.append((pts, m.start()+1, m.end())) +seen=set(kill_coords); chain=[]; grew=True +while grew: + grew=False + for w in wires: + if w in chain: continue + if any(p in seen for p in w[0]): + chain.append(w); seen|=set(w[0]); grew=True +assert len(chain) <= 3, 'wire chain from doomed pins too large: %d' % len(chain) +surv_pins=set() +for b2 in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*', sch, re.S): + blk2=b2.group(0) + rr=re.search(r'\(property "Reference" "([^"]+)"',blk2) + if rr and rr.group(1) in doomed: continue + ll=re.search(r'\(lib_id "([^"]+)"',blk2); aa=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk2) + if ll and aa: + for num,px,py in (cachedpins(ll.group(1)) or []): + surv_pins.add((round(float(aa.group(1))+px,3), round(float(aa.group(2))-py,3))) +assert not (seen - kill_coords) & surv_pins, 'wire chain touches a surviving pin - refusing' +removed_wires=0 +for pts,a,e in sorted(chain, key=lambda t:-t[1]): + sch = sch[:a-1] + sch[e:]; removed_wires+=1 +# labels at chain points (e.g. the GND at the far end of the ADD0 stub) +out=[]; last=0; removed_lab2=0 +for m in re.finditer(r'\n\t\((label|global_label) "[^"]+"\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\n\t\().)*?\n\t\)', sch, re.S): + c=(round(float(m.group(2)),3),round(float(m.group(3)),3)) + if c in (seen - kill_coords): + out.append(sch[last:m.start()]); last=m.end(); removed_lab2+=1 +out.append(sch[last:]); sch=''.join(out) +removed_lab += removed_lab2 + +# re-find the symbol spans on the label-stripped text, delete (largest offset first) +spans = [] +for ref in doomed: + a,e,l,X,Y = find_inst(ref) + spans.append((a-1, e)) # a-1 swallows the leading newline +for a,e in sorted(spans, key=lambda t:-t[0]): + sch = sch[:a] + sch[e:] + +# ---------- 3. additions: P18 + R126 + C157 + PC4 label ---------- +add = '' +newlabels=[] +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 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') + +def place(lib, ref, val, x, y, fp, nets, lcsc=''): + pp = cachedpins(lib) + if not pp: sys.exit('no pins for %s' % lib) + global add + add += mksym(lib,ref,val,x,y,fp,[num for num,_,_ in pp],lcsc) + for num,px,py in pp: + rot,just = orient(px,py) + add_lab(nets[num],x+px,y-py,rot,just) + +add += ('\n\t(text "2ND EXTERNAL NTC PROBE (T10): same channel as P12/R42/C24 -> PC5. ' + 'This one -> PC4 (ADC12_IN14). TMP102 (U4) and SHT40 probe jack (P5) deleted; ' + 'on-board U3 SHT40 = tair+humidity."\n' + '\t\t(exclude_from_sim no)\n\t\t(at 376 254 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() + +place('Connector_Generic:Conn_01x02','P18','NTC-PROBE-2',384.0,266.0, + 'Connector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical',{'1':'NTC2','2':'GND'}) +place('Device:R','R126','10k/1%',396.0,258.0,'Resistor_SMD:R_0603_1608Metric', + {'1':'3V3','2':'NTC2'}) +place('Device:C','C157','100nF',404.0,258.0,'Capacitor_SMD:C_0603_1608Metric', + {'1':'NTC2','2':'GND'}) + +# PC4 = U16 pin 24 at sheet (518.16, 411.48): label + remove stale NC if present +PC4 = (518.16, 411.48) +add_lab('NTC2', PC4[0], PC4[1], 0, 'left bottom') +out=[]; last=0; pc4_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)),2),round(float(m.group(2)),2)) + if c == PC4: + out.append(sch[last:m.start()]); last=m.end(); pc4_nc+=1 +out.append(sch[last:]); sch=''.join(out) + +# ---------- 4. collision check on the post-delete text ---------- +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))) +inst = {} +for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*', sch, re.S): + blk=b.group(0) + r=re.search(r'\(property "Reference" "([^"]+)"',blk); l=re.search(r'\(lib_id "([^"]+)"',blk); a=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk) + if r and l and a: inst[r.group(1)]=(l.group(1),float(a.group(1)),float(a.group(2))) +pin_coords=set() +for ref,(lb_,X_,Y_) in inst.items(): + for num,px,py in (cachedpins(lb_) or []): + pin_coords.add((round(X_+px,3), round(Y_-py,3))) +INTENTIONAL = {(round(PC4[0],3),round(PC4[1],3))} +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)) +# the two deleted symbols and their pin/label/nc uuids are EXPECTED losses; count them +print('deleted: U4 (TMP102) + P5 (TEMP_PROBE); labels removed: %d, NCs: %d, stub wires: %d' % (removed_lab, removed_nc, removed_wires)) +print('pre-existing uuids gone: %d (expected: the 2 symbols + their %d labels + %d NCs + pin uuids)' % (len(lost), removed_lab, removed_nc)) +print('added: P18 NTC-PROBE-2 + R126 10k + C157 100nF; labels: %d; PC4 label placed, PC4 stale NC removed: %d' % (len(newlabels)+1, pc4_nc)) +print('paren: %d' % 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+'.T10bak') +open(SCH,'w',encoding='utf-8').write(sch) +print('\nAPPLIED. Backup: .T10bak')