Compare commits
6 Commits
985a2daa6e
...
2b03a7ee02
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b03a7ee02 | |||
| cb244f25bf | |||
| 9367527d85 | |||
| 3014b92f08 | |||
| 31d0e43a7f | |||
| 90835738a6 |
@ -1 +1 @@
|
|||||||
Subproject commit affb84bccd06df085f25583d016c79494ac5c914
|
Subproject commit fdffd441d48a60d02cfbae93ecaae2246808e663
|
||||||
159
scripts/2026-09-25_v6_T14_sim_holder_primary.py
Normal file
159
scripts/2026-09-25_v6_T14_sim_holder_primary.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""v6 T14 — nano-SIM holder becomes the PRIMARY SIM (Telcel will not supply MFF2).
|
||||||
|
|
||||||
|
Context: T13 (17-Sep) built a dual-SIM block with the soldered MFF2 (U22) populated and
|
||||||
|
the JAE SF72S006 holder (J14) DNP. On 25-Sep Leo confirmed Telcel will NOT provide the
|
||||||
|
MFF2, so as-is the board would ship with jumpers to an empty footprint and no SIM socket.
|
||||||
|
|
||||||
|
This script (schematic only — the PCB picks it up via Update PCB from Schematic):
|
||||||
|
1. Flips population: J14 + R131..R134 -> populated ; U22 + R127..R130 -> DNP.
|
||||||
|
The MFF2 footprint stays on the board as a future option.
|
||||||
|
2. Fixes the 5 ERC errors left by T13:
|
||||||
|
- no-connect flags on J14 C6 (VPP), CSW, DSW
|
||||||
|
- PWR_FLAG on /SIMB_VDD and /SIMA_VDD (both are fed through 0R jumpers,
|
||||||
|
so ERC could not see a driver)
|
||||||
|
3. Gives J14 its LCSC field (C2977286) so it lands in the JLCPCB BOM, and rewrites its
|
||||||
|
instance Description (was "DNP alternate to U22").
|
||||||
|
|
||||||
|
Method: clone-and-transform from existing blocks in the same file (PF19 for the flags,
|
||||||
|
an existing no_connect), fresh UUIDs, invariant guards. Dry-run by default.
|
||||||
|
|
||||||
|
python3 2026-09-25_v6_T14_sim_holder_primary.py hbox-watchdog-v6.kicad_sch # dry run
|
||||||
|
python3 2026-09-25_v6_T14_sim_holder_primary.py hbox-watchdog-v6.kicad_sch --apply # writes .T14bak
|
||||||
|
"""
|
||||||
|
import re, sys, uuid, shutil
|
||||||
|
|
||||||
|
POPULATE = ['J14', 'R131', 'R132', 'R133', 'R134']
|
||||||
|
MAKE_DNP = ['U22', 'R127', 'R128', 'R129', 'R130']
|
||||||
|
NO_CONNECT = [(327.3, 487.54), (352.7, 482.46), (352.7, 485.0)] # J14 C6, CSW, DSW
|
||||||
|
FLAGS = [('PF20', 310.0, 453.81, 'SIMB_VDD'), ('PF21', 310.0, 423.81, 'SIMA_VDD')]
|
||||||
|
J14_LCSC = 'C2977286' # JAE SF72S006VBDR2500 — JLC "Pre-ordered Products": pre-order stock before PCBA
|
||||||
|
J14_DESC = ('Nano-SIM holder JAE SF72S006VBDR2500 (LCSC C2977286) - PRIMARY SIM since T14 '
|
||||||
|
'(2026-09-25, Telcel will not supply MFF2). U22 MFF2 is the DNP alternate.')
|
||||||
|
|
||||||
|
def span(s, i):
|
||||||
|
d = 0; instr = False; esc = False; j = i
|
||||||
|
while True:
|
||||||
|
c = s[j]
|
||||||
|
if esc: esc = False
|
||||||
|
elif c == '\\': esc = True
|
||||||
|
elif c == '"': instr = not instr
|
||||||
|
elif not instr:
|
||||||
|
if c == '(': d += 1
|
||||||
|
elif c == ')':
|
||||||
|
d -= 1
|
||||||
|
if d == 0: return j + 1
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
def balanced(s):
|
||||||
|
d = 0; instr = False; esc = False
|
||||||
|
for c in s:
|
||||||
|
if esc: esc = False; continue
|
||||||
|
if c == '\\': esc = True; continue
|
||||||
|
if c == '"': instr = not instr; continue
|
||||||
|
if instr: continue
|
||||||
|
if c == '(': d += 1
|
||||||
|
elif c == ')': d -= 1
|
||||||
|
if d < 0: return False
|
||||||
|
return d == 0
|
||||||
|
|
||||||
|
UUID = re.compile(r'\(uuid "([0-9a-f-]{36})"\)')
|
||||||
|
|
||||||
|
def sym_block(s, ref):
|
||||||
|
m = re.search(r'\(property "Reference" "%s"\s' % re.escape(ref), s)
|
||||||
|
if not m: raise SystemExit('reference not found: ' + ref)
|
||||||
|
a = s.rfind('\n\t(symbol', 0, m.start()) + 1
|
||||||
|
return a, span(s, a)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
path = sys.argv[1]; apply = '--apply' in sys.argv
|
||||||
|
s0 = open(path).read(); s = s0
|
||||||
|
assert balanced(s0), 'input not paren-balanced'
|
||||||
|
uuids0 = set(UUID.findall(s0))
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
# 1. population flip — only the symbol's own top-level (dnp …) token
|
||||||
|
for ref, want in [(r, 'no') for r in POPULATE] + [(r, 'yes') for r in MAKE_DNP]:
|
||||||
|
a, e = sym_block(s, ref); blk = s[a:e]
|
||||||
|
m = re.search(r'\n\t\t\(dnp (yes|no)\)', blk)
|
||||||
|
if not m: raise SystemExit('no dnp flag on ' + ref)
|
||||||
|
if m.group(1) != want:
|
||||||
|
blk2 = blk[:m.start()] + '\n\t\t(dnp %s)' % want + blk[m.end():]
|
||||||
|
s = s[:a] + blk2 + s[e:]
|
||||||
|
changes.append(f'{ref}: dnp {m.group(1)} -> {want}')
|
||||||
|
|
||||||
|
# 2a. no-connect flags (skip any already present)
|
||||||
|
tmpl = re.search(r'\n\t\(no_connect\s*\(at [^)]*\)\s*\(uuid "[^"]+"\)\s*\)', s).group(0)
|
||||||
|
existing_nc = {(round(float(x), 2), round(float(y), 2))
|
||||||
|
for x, y in re.findall(r'\(no_connect\s*\(at (-?[\d.]+) (-?[\d.]+)\)', s)}
|
||||||
|
add = ''
|
||||||
|
for x, y in NO_CONNECT:
|
||||||
|
if (round(x, 2), round(y, 2)) in existing_nc: continue
|
||||||
|
nb = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g})', tmpl, count=1)
|
||||||
|
nb = UUID.sub(f'(uuid "{uuid.uuid4()}")', nb, count=1)
|
||||||
|
add += nb; changes.append(f'no_connect @({x:g},{y:g})')
|
||||||
|
|
||||||
|
# 2b. PWR_FLAGs cloned from PF19
|
||||||
|
a, e = sym_block(s, 'PF19'); pf = s[a:e]
|
||||||
|
ox, oy = map(float, re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+) ', pf).groups())
|
||||||
|
for ref, x, y, net in FLAGS:
|
||||||
|
if re.search(r'\(property "Reference" "%s"\s' % ref, s): continue
|
||||||
|
nb = pf
|
||||||
|
def mv(m):
|
||||||
|
px, py = float(m.group(1)), float(m.group(2))
|
||||||
|
return f'(at {px - ox + x:g} {py - oy + y:g}{m.group(3)})'
|
||||||
|
nb = re.sub(r'\(at (-?[\d.]+) (-?[\d.]+)((?: -?[\d.]+)?)\)', mv, nb)
|
||||||
|
nb = nb.replace('"PF19"', f'"{ref}"')
|
||||||
|
nb = UUID.sub(lambda m: f'(uuid "{uuid.uuid4()}")', nb)
|
||||||
|
add += '\n' + nb; changes.append(f'{ref} PWR_FLAG on /{net} @({x:g},{y:g})')
|
||||||
|
|
||||||
|
if add:
|
||||||
|
cut = s.rstrip().rfind('\n)')
|
||||||
|
s = s[:cut] + add + s[cut:]
|
||||||
|
|
||||||
|
# 3. J14 becomes a JLCPCB-assembled part: give it the LCSC field (cloned from R131's
|
||||||
|
# LCSC property block) and fix its instance Description, which still said "DNP alternate".
|
||||||
|
a, e = sym_block(s, 'J14'); blk = s[a:e]
|
||||||
|
if not re.search(r'\(property "LCSC" ', blk):
|
||||||
|
ra, re_ = sym_block(s, 'R131'); rblk = s[ra:re_]
|
||||||
|
m = re.search(r'\n\t\t\(property "LCSC" "', rblk)
|
||||||
|
pa = m.start() + 3; lc = rblk[pa:span(rblk, pa)] # from '(' — indent added below
|
||||||
|
lc = re.sub(r'"LCSC" "[^"]*"', f'"LCSC" "{J14_LCSC}"', lc, count=1)
|
||||||
|
jx, jy = re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+) ', blk).groups()
|
||||||
|
lc = re.sub(r'\(at [^)]*\)', f'(at {jx} {jy} 0)', lc, count=1)
|
||||||
|
d = re.search(r'\n\t\t\(property "Description" "', blk)
|
||||||
|
da = d.start() + 1; de = span(blk, da)
|
||||||
|
blk = blk[:de] + '\n\t\t' + lc + blk[de:]
|
||||||
|
changes.append(f'J14: + LCSC {J14_LCSC}')
|
||||||
|
old_desc = '"Nano-SIM holder JAE SF72S006 (LCSC C2977286) - DNP alternate to U22"'
|
||||||
|
if old_desc in blk:
|
||||||
|
blk = blk.replace(old_desc, '"' + J14_DESC + '"', 1)
|
||||||
|
changes.append('J14: Description -> primary SIM')
|
||||||
|
s = s[:a] + blk + s[e:]
|
||||||
|
|
||||||
|
# guards
|
||||||
|
assert balanced(s), 'output not paren-balanced'
|
||||||
|
uuids1 = UUID.findall(s)
|
||||||
|
assert uuids0 <= set(uuids1), 'a pre-existing UUID disappeared'
|
||||||
|
assert len(uuids1) == len(set(uuids1)), 'duplicate UUID'
|
||||||
|
refs = re.findall(r'\n\t\t\(property "Reference" "([^"#][^"]*)"', s)
|
||||||
|
dup = {r for r in refs if refs.count(r) > 1}
|
||||||
|
assert not dup, f'duplicate references: {dup}'
|
||||||
|
for ref in POPULATE:
|
||||||
|
a, e = sym_block(s, ref); assert '\n\t\t(dnp no)' in s[a:e], ref
|
||||||
|
for ref in MAKE_DNP:
|
||||||
|
a, e = sym_block(s, ref); assert '\n\t\t(dnp yes)' in s[a:e], ref
|
||||||
|
a, e = sym_block(s, 'J14')
|
||||||
|
assert f'(property "LCSC" "{J14_LCSC}"' in s[a:e], 'J14 LCSC missing'
|
||||||
|
|
||||||
|
print(f'{len(changes)} change(s):'); [print(' ', c) for c in changes]
|
||||||
|
print(f'UUIDs: {len(uuids0)} kept, {len(uuids1) - len(uuids0)} new, 0 duplicated; paren-balanced')
|
||||||
|
if not changes:
|
||||||
|
print('nothing to do (already applied)'); return
|
||||||
|
if apply:
|
||||||
|
shutil.copy2(path, path + '.T14bak'); open(path, 'w').write(s); print('APPLIED (backup .T14bak)')
|
||||||
|
else:
|
||||||
|
print('DRY RUN — re-run with --apply')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
229
scripts/2026-09-25_v6_T15_bom_lcsc_and_pcb_attrs.py
Normal file
229
scripts/2026-09-25_v6_T15_bom_lcsc_and_pcb_attrs.py
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""v6 T15 — make the board orderable at JLCPCB: LCSC numbers + fabrication attributes.
|
||||||
|
|
||||||
|
Why a direct PCB edit: on 25-Sep "Update PCB from Schematic" did not carry the T14 SIM flip
|
||||||
|
to the board, so T15 writes the attributes into the .kicad_pcb itself (only `attr` tokens and
|
||||||
|
hidden LCSC properties; no geometry, no copper, no placement is touched).
|
||||||
|
|
||||||
|
Schematic (.kicad_sch):
|
||||||
|
1. LCSC field on the 66 populated parts that had none (+ RS1, whose value was a TBD placeholder).
|
||||||
|
Every number verified in the local JLC DB on 25-Sep (see LCSC below).
|
||||||
|
2. JP1–JP5 (solder jumpers = bare copper) -> in_bom no, in_pos_files no.
|
||||||
|
Board (.kicad_pcb):
|
||||||
|
3. Same LCSC values on the matching footprints (hidden F.Fab property).
|
||||||
|
4. SIM flip that Update PCB didn't carry (T14): J14 + R131–R134 lose `dnp`; U22 + R127–R130 get `dnp`.
|
||||||
|
5. U19 (A7670SA LGA) and L22 (SMD inductor) through_hole -> smd.
|
||||||
|
6. JP1–JP5 -> exclude_from_pos_files + exclude_from_bom.
|
||||||
|
|
||||||
|
NOT touched on purpose: F2 (vehicle-input fuse, still TBD — needs Leo's part choice),
|
||||||
|
J10/P11/P12 (DNP intent pending Leo), anything geometric.
|
||||||
|
|
||||||
|
Method: clone-and-transform (LCSC blocks cloned from R131's own), fresh UUIDs, invariant guards,
|
||||||
|
dry-run by default, .T15bak backups, idempotent.
|
||||||
|
|
||||||
|
python3 T15.py hbox-watchdog-v6.kicad_sch hbox-watchdog-v6.kicad_pcb # dry run
|
||||||
|
python3 T15.py hbox-watchdog-v6.kicad_sch hbox-watchdog-v6.kicad_pcb --apply # writes .T15bak
|
||||||
|
"""
|
||||||
|
import re, sys, uuid, shutil
|
||||||
|
|
||||||
|
LCSC = {
|
||||||
|
# 100 nF 0402 X7R 16 V (Basic)
|
||||||
|
**{r: 'C1525' for r in 'C121 C122 C123 C126 C129 C134 C137 C138 C139 C140 C144 C158 C159 C143'.split()},
|
||||||
|
# 10 uF 0805 X5R 50 V Murata (Basic) — already used 5x on the board; 50 V covers C127 on /VIN_MODEM (24 V)
|
||||||
|
**{r: 'C440198' for r in 'C119 C120 C124 C127 C160'.split()},
|
||||||
|
'C125': 'C23630', # 2.2 uF 0603 16 V (Basic) on /1V8_MODEM
|
||||||
|
'C128': 'C15849', # 1 uF 0603 X5R 50 V (Basic) on /VIN_MODEM (24 V)
|
||||||
|
'C133': 'C15849', # 1 uF 0603 50 V on /VBAT_MODEM
|
||||||
|
'C135': 'C1562', # 33 pF 0402 C0G 50 V (Basic)
|
||||||
|
'C136': 'C32949', # 10 pF 0402 C0G 50 V (Basic)
|
||||||
|
'C141': 'C1536', # 3.3 nF 0402 X7R 50 V (Ext) — modem buck compensation
|
||||||
|
'C142': 'C1567', # 47 pF 0402 C0G 50 V (Basic) — modem buck compensation
|
||||||
|
'C145': 'C1546', # 100 pF 0402 C0G 50 V (Basic)
|
||||||
|
'C149': 'C153036', # 2.2 uF 1210 X7R 100 V FH (Ext, 471k stock) on /VIN_PROT
|
||||||
|
'C150': 'C153036',
|
||||||
|
'C151': 'C116241', # 100 uF 50 V Nichicon UUD 8x10, 105 C / 5000 h (Ext)
|
||||||
|
'C152': 'C14663', # 100 nF 0603 50 V (Basic) — U25 bootstrap
|
||||||
|
'C157': 'C14663', # 100 nF 0603 on /NTC2
|
||||||
|
'C153': 'C5440143', # 47 uF 1210 X7R 16 V (Ext) — same part already on the board
|
||||||
|
'C154': 'C5440143',
|
||||||
|
'C155': 'C1542', # 6.8 nF 0402 X7R 50 V (Preferred) — U25 compensation
|
||||||
|
'C156': 'C1563', # 39 pF 0402 C0G 50 V (Ext) — U25 compensation
|
||||||
|
'D20': 'C8678', # SS34 SMA (Basic) — already used 5x
|
||||||
|
'D33': 'C80857', # BZX84C15LT1G (Ext) — already used on the board
|
||||||
|
'L20': 'C17700167', # Sunlord MWSA0804S-4R7MT 4.7 uH — same family as the other SRU8028-footprint inductors
|
||||||
|
'L21': 'C162555', # Murata LQG18HN68NJ00D 68 nH 0603 — GNSS active-antenna bias choke
|
||||||
|
'Q20': 'C8545', 'Q21': 'C8545', # 2N7002 CJ (Basic)
|
||||||
|
'Q22': 'C20526', # MMBT3904 (Basic) — already used
|
||||||
|
'Q23': 'C2624', # IRFR5305TRPBF (Ext) — already used
|
||||||
|
'R105': 'C25804', # 10k 1% (Basic)
|
||||||
|
'R126': 'C25804',
|
||||||
|
'R106': 'C22775', # 100R (Basic)
|
||||||
|
'R109': 'C23162', # 4.7k (Basic)
|
||||||
|
'R110': 'C23242', # 75k 1% (Basic) — modem buck FB top (0.8 V x (1+75/20) = 3.80 V)
|
||||||
|
'R111': 'C4184', # 20k 1% (Basic) — modem buck FB bottom
|
||||||
|
'R112': 'C14676', # 1k — same as the other 1k on the board
|
||||||
|
**{r: 'C25803' for r in 'R113 R114 R115 R119'.split()}, # 100k 1% (Basic)
|
||||||
|
'R116': 'C22803', # 14k 1% (Ext)
|
||||||
|
'R117': 'C25811', # 200k 1% (Basic)
|
||||||
|
'R118': 'C17168', # 0R 0402 (Basic)
|
||||||
|
'R120': 'C165760', # 162k 1% Yageo (Ext) — U25 RT (~620 kHz)
|
||||||
|
'R121': 'C23074', # 53.6k 1% (Ext) — U25 FB top (0.8 V x (1+53.6/10.2) = 5.00 V)
|
||||||
|
'R122': 'C22772', # 10.2k 1% (Ext) — U25 FB bottom
|
||||||
|
'R123': 'C22797', # 13k 1% (Basic) — U25 compensation
|
||||||
|
'RS1': 'C105362', # 10 mOhm 1% 1 W 1206 current-sense (Ext) — replaces "TBD-10mR-1206"
|
||||||
|
'P8': 'C158012', # JST B2B-XH-A — same as the other 2-pin XH on the board
|
||||||
|
'J14': 'C2977286', # JAE SF72S006VBDR2500 — schematic got it in T14b; the board needs it too
|
||||||
|
}
|
||||||
|
JUMPERS = ['JP1', 'JP2', 'JP3', 'JP4', 'JP5']
|
||||||
|
POPULATE = ['J14', 'R131', 'R132', 'R133', 'R134']
|
||||||
|
MAKE_DNP = ['U22', 'R127', 'R128', 'R129', 'R130']
|
||||||
|
TO_SMD = ['U19', 'L22']
|
||||||
|
ATTR_ORDER = ['board_only', 'exclude_from_pos_files', 'exclude_from_bom',
|
||||||
|
'allow_missing_courtyard', 'dnp', 'allow_soldermask_bridges']
|
||||||
|
|
||||||
|
def span(s, i):
|
||||||
|
d = 0; instr = False; esc = False; j = i
|
||||||
|
while True:
|
||||||
|
c = s[j]
|
||||||
|
if esc: esc = False
|
||||||
|
elif c == '\\': esc = True
|
||||||
|
elif c == '"': instr = not instr
|
||||||
|
elif not instr:
|
||||||
|
if c == '(': d += 1
|
||||||
|
elif c == ')':
|
||||||
|
d -= 1
|
||||||
|
if d == 0: return j + 1
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
def balanced(s):
|
||||||
|
d = 0; instr = False; esc = False
|
||||||
|
for c in s:
|
||||||
|
if esc: esc = False; continue
|
||||||
|
if c == '\\': esc = True; continue
|
||||||
|
if c == '"': instr = not instr; continue
|
||||||
|
if instr: continue
|
||||||
|
if c == '(': d += 1
|
||||||
|
elif c == ')': d -= 1
|
||||||
|
if d < 0: return False
|
||||||
|
return d == 0
|
||||||
|
|
||||||
|
UUID = re.compile(r'\(uuid "([0-9a-f-]{36})"\)')
|
||||||
|
|
||||||
|
def block(s, ref, head):
|
||||||
|
m = re.search(r'\n\t\t\(property "Reference" "%s"\n' % re.escape(ref), s)
|
||||||
|
if not m: raise SystemExit(f'{head} {ref} not found')
|
||||||
|
a = s.rfind('\n\t(' + head, 0, m.start()) + 1
|
||||||
|
return a, span(s, a)
|
||||||
|
|
||||||
|
def prop_block(blk, name):
|
||||||
|
m = re.search(r'\n\t\t\(property "%s" "' % re.escape(name), blk)
|
||||||
|
if not m: return None
|
||||||
|
a = m.start() + 3
|
||||||
|
return a, span(blk, a)
|
||||||
|
|
||||||
|
def set_lcsc(s, ref, head, value, template_ref, changes, tag):
|
||||||
|
"""Set/insert the LCSC property of symbol/footprint `ref`. Returns new s."""
|
||||||
|
a, e = block(s, ref, head); blk = s[a:e]
|
||||||
|
pb = prop_block(blk, 'LCSC')
|
||||||
|
if pb:
|
||||||
|
pa, pe = pb; cur = re.match(r'\(property "LCSC" "([^"]*)"', blk[pa:pe]).group(1)
|
||||||
|
if cur == value: return s
|
||||||
|
if cur and not cur.startswith('TBD'):
|
||||||
|
raise SystemExit(f'{tag} {ref}: has real LCSC {cur}, refusing to overwrite with {value}')
|
||||||
|
nb = blk[:pa] + blk[pa:pe].replace(f'"LCSC" "{cur}"', f'"LCSC" "{value}"', 1) + blk[pe:]
|
||||||
|
changes.append(f'{tag} {ref}: LCSC {cur} -> {value}')
|
||||||
|
else:
|
||||||
|
if head == 'symbol':
|
||||||
|
ta, te = block(s, template_ref, head); tb = s[ta:te]
|
||||||
|
pa, pe = prop_block(tb, 'LCSC'); lc = tb[pa:pe]
|
||||||
|
x, y = re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+) ', blk).groups()
|
||||||
|
lc = re.sub(r'\(at [^)]*\)', f'(at {x} {y} 0)', lc, count=1)
|
||||||
|
else: # footprint: clone this footprint's own hidden Datasheet field (right layer/rotation)
|
||||||
|
pa, pe = prop_block(blk, 'Datasheet'); lc = blk[pa:pe]
|
||||||
|
if '(hide yes)' not in lc: raise SystemExit(f'{ref}: Datasheet field not hidden, unsafe template')
|
||||||
|
lc = re.sub(r'^\(property "[^"]*" "[^"]*"', f'(property "LCSC" "{value}"', lc, count=1)
|
||||||
|
lc = UUID.sub(lambda m: f'(uuid "{uuid.uuid4()}")', lc)
|
||||||
|
da, de = prop_block(blk, 'Description')
|
||||||
|
nb = blk[:de] + '\n\t\t' + lc + blk[de:]
|
||||||
|
changes.append(f'{tag} {ref}: + LCSC {value}')
|
||||||
|
return s[:a] + nb + s[e:]
|
||||||
|
|
||||||
|
def sch_flags(s, ref, want, changes):
|
||||||
|
a, e = block(s, ref, 'symbol'); blk = s[a:e]
|
||||||
|
for k, v in want.items():
|
||||||
|
m = re.search(r'\n\t\t\(%s (yes|no)\)' % k, blk)
|
||||||
|
if not m: raise SystemExit(f'sch {ref}: no ({k} …)')
|
||||||
|
if m.group(1) != v:
|
||||||
|
blk = blk[:m.start()] + f'\n\t\t({k} {v})' + blk[m.end():]
|
||||||
|
changes.append(f'sch {ref}: {k} {m.group(1)} -> {v}')
|
||||||
|
return s[:a] + blk + s[e:]
|
||||||
|
|
||||||
|
def pcb_attr(s, ref, add=(), remove=(), set_type=None, changes=None):
|
||||||
|
a, e = block(s, ref, 'footprint'); blk = s[a:e]
|
||||||
|
m = re.search(r'\n\t\t\(attr ([^)]*)\)', blk)
|
||||||
|
if not m: # KiCad omits (attr) when the type is "unspecified" and no flags are set
|
||||||
|
i = blk.find('\n\t\t(duplicate_pad_numbers_are_jumpers')
|
||||||
|
if i < 0: i = re.search(r'\n\t\t\((fp_|pad )', blk).start()
|
||||||
|
blk = blk[:i] + '\n\t\t(attr )' + blk[i:]
|
||||||
|
m = re.search(r'\n\t\t\(attr ([^)]*)\)', blk)
|
||||||
|
toks = m.group(1).split()
|
||||||
|
typ = toks[0] if toks and toks[0] in ('smd', 'through_hole') else None
|
||||||
|
flags = [t for t in toks if t not in ('smd', 'through_hole')]
|
||||||
|
unknown = [t for t in flags if t not in ATTR_ORDER]
|
||||||
|
if unknown: raise SystemExit(f'pcb {ref}: unknown attr tokens {unknown}')
|
||||||
|
ntyp = set_type or typ
|
||||||
|
nflags = (set(flags) | set(add)) - set(remove)
|
||||||
|
new = ' '.join(([ntyp] if ntyp else []) + [t for t in ATTR_ORDER if t in nflags])
|
||||||
|
if new != m.group(1):
|
||||||
|
blk = blk[:m.start()] + f'\n\t\t(attr {new})' + blk[m.end():]
|
||||||
|
changes.append(f'pcb {ref}: attr "{m.group(1)}" -> "{new}"')
|
||||||
|
return s[:a] + blk + s[e:]
|
||||||
|
|
||||||
|
def guard(s0, s, what):
|
||||||
|
assert balanced(s), f'{what}: output not paren-balanced'
|
||||||
|
u0 = set(UUID.findall(s0)); u1 = UUID.findall(s)
|
||||||
|
assert u0 <= set(u1), f'{what}: a pre-existing UUID disappeared'
|
||||||
|
assert len(u1) == len(set(u1)), f'{what}: duplicate UUID'
|
||||||
|
return len(u0), len(u1) - len(u0)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sch, pcb = sys.argv[1], sys.argv[2]; apply = '--apply' in sys.argv
|
||||||
|
S0 = open(sch).read(); P0 = open(pcb).read(); S = S0; P = P0
|
||||||
|
assert balanced(S0) and balanced(P0), 'input not paren-balanced'
|
||||||
|
ch = []
|
||||||
|
n_fp0 = len(re.findall(r'\n\t\(footprint "', P0)); n_sym0 = len(re.findall(r'\n\t\(symbol\s*\(lib_', S0))
|
||||||
|
|
||||||
|
for ref, val in LCSC.items():
|
||||||
|
S = set_lcsc(S, ref, 'symbol', val, 'R131', ch, 'sch')
|
||||||
|
P = set_lcsc(P, ref, 'footprint', val, None, ch, 'pcb')
|
||||||
|
for ref in JUMPERS:
|
||||||
|
S = sch_flags(S, ref, {'in_bom': 'no', 'in_pos_files': 'no'}, ch)
|
||||||
|
P = pcb_attr(P, ref, add=('exclude_from_pos_files', 'exclude_from_bom'), changes=ch)
|
||||||
|
for ref in POPULATE: P = pcb_attr(P, ref, remove=('dnp',), changes=ch)
|
||||||
|
for ref in MAKE_DNP: P = pcb_attr(P, ref, add=('dnp',), changes=ch)
|
||||||
|
for ref in TO_SMD: P = pcb_attr(P, ref, set_type='smd', changes=ch)
|
||||||
|
|
||||||
|
ks, ns = guard(S0, S, 'sch'); kp, np_ = guard(P0, P, 'pcb')
|
||||||
|
assert len(re.findall(r'\n\t\(footprint "', P)) == n_fp0, 'footprint count changed'
|
||||||
|
assert len(re.findall(r'\n\t\(symbol\s*\(lib_', S)) == n_sym0, 'symbol count changed'
|
||||||
|
# geometry invariant: every original board line survives, except attr lines and LCSC values
|
||||||
|
from collections import Counter
|
||||||
|
keep = lambda t: Counter(l for l in t.split('\n')
|
||||||
|
if not re.match(r'\t*\((attr |property "LCSC" )', l))
|
||||||
|
lost = keep(P0) - keep(P)
|
||||||
|
assert not lost, f'pcb: unexpected changed lines: {list(lost)[:3]}'
|
||||||
|
for ref in LCSC: # schematic and board agree
|
||||||
|
for t, h in ((S, 'symbol'), (P, 'footprint')):
|
||||||
|
a, e = block(t, ref, h); assert f'(property "LCSC" "{LCSC[ref]}"' in t[a:e], (ref, h)
|
||||||
|
|
||||||
|
print(f'{len(ch)} change(s)'); [print(' ', c) for c in ch]
|
||||||
|
print(f'sch UUIDs: {ks} kept, {ns} new | pcb UUIDs: {kp} kept, {np_} new | 0 duplicated; paren-balanced; geometry untouched')
|
||||||
|
if not ch: print('nothing to do (already applied)'); return
|
||||||
|
if apply:
|
||||||
|
shutil.copy2(sch, sch + '.T15bak'); shutil.copy2(pcb, pcb + '.T15bak')
|
||||||
|
open(sch, 'w').write(S); open(pcb, 'w').write(P); print('APPLIED (backups .T15bak)')
|
||||||
|
else:
|
||||||
|
print('DRY RUN — re-run with --apply')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
65
scripts/2026-09-25_v6_T15b_dangling_stubs.py
Normal file
65
scripts/2026-09-25_v6_T15b_dangling_stubs.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""v6 T15b — delete the 4 dangling track stubs DRC reported on 25-Sep (warnings track_dangling).
|
||||||
|
|
||||||
|
Each target is identified by net + layer + the dangling end DRC printed + the reported length;
|
||||||
|
the script refuses to delete anything that doesn't match exactly one segment.
|
||||||
|
|
||||||
|
python3 T15b.py hbox-watchdog-v6.kicad_pcb # dry run
|
||||||
|
python3 T15b.py hbox-watchdog-v6.kicad_pcb --apply # writes .T15bbak
|
||||||
|
"""
|
||||||
|
import re, sys, math, shutil
|
||||||
|
|
||||||
|
STUBS = [ # (net, layer, dangling end x, y, length mm) — from the 25-Sep DRC report
|
||||||
|
('/3V3_ETH', 'F.Cu', 91.7375, 56.0800, 0.3889),
|
||||||
|
('/VEH5V', 'F.Cu', 187.5000, 116.1100, 0.7071),
|
||||||
|
('/RS485_B', 'B.Cu', 145.3500, 131.1682, 0.2107),
|
||||||
|
('/DOUT_4G', 'In2.Cu', 158.4000, 80.1000, 1.6000),
|
||||||
|
# second DRC pass: the RS485_B stub was a 3-segment chain off a THT pad going nowhere
|
||||||
|
('/RS485_B', 'B.Cu', 146.2000, 132.0182, 1.2021),
|
||||||
|
('/RS485_B', 'B.Cu', 146.2000, 132.3075, 0.2894),
|
||||||
|
]
|
||||||
|
|
||||||
|
def span(s, i):
|
||||||
|
d = 0; instr = False; j = i
|
||||||
|
while True:
|
||||||
|
c = s[j]
|
||||||
|
if c == '"' and s[j-1] != '\\': instr = not instr
|
||||||
|
elif not instr:
|
||||||
|
if c == '(': d += 1
|
||||||
|
elif c == ')':
|
||||||
|
d -= 1
|
||||||
|
if d == 0: return j + 1
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
def main():
|
||||||
|
path = sys.argv[1]; apply = '--apply' in sys.argv
|
||||||
|
s0 = open(path).read(); cuts = []; log = []
|
||||||
|
segs = []
|
||||||
|
for m in re.finditer(r'\n\t\(segment\b', s0):
|
||||||
|
a = m.start() + 1; e = span(s0, a); b = s0[a:e]
|
||||||
|
st = tuple(map(float, re.search(r'\(start (-?[\d.]+) (-?[\d.]+)\)', b).groups()))
|
||||||
|
en = tuple(map(float, re.search(r'\(end (-?[\d.]+) (-?[\d.]+)\)', b).groups()))
|
||||||
|
segs.append(dict(a=a, e=e, st=st, en=en, l=re.search(r'\(layer "([^"]+)"\)', b).group(1),
|
||||||
|
n=re.search(r'\(net "([^"]*)"\)', b).group(1)))
|
||||||
|
for net, lay, x, y, L in STUBS:
|
||||||
|
hit = [g for g in segs if g['n'] == net and g['l'] == lay
|
||||||
|
and any(math.hypot(px - x, py - y) < 0.005 for px, py in (g['st'], g['en']))
|
||||||
|
and abs(math.dist(g['st'], g['en']) - L) < 0.002]
|
||||||
|
if not hit:
|
||||||
|
log.append(f'{net} {lay} @({x},{y}): not found (already removed?)'); continue
|
||||||
|
if len(hit) > 1: raise SystemExit(f'{net}: {len(hit)} candidates, refusing')
|
||||||
|
cuts.append(hit[0]); log.append(f'delete {net} {lay} {hit[0]["st"]}->{hit[0]["en"]} ({L} mm)')
|
||||||
|
s = s0
|
||||||
|
for g in sorted(cuts, key=lambda g: -g['a']):
|
||||||
|
s = s[:g['a'] - 1] + s[g['e']:] # also drops the preceding newline
|
||||||
|
t = re.sub(r'"(?:[^"\\]|\\.)*"', '', s); assert t.count('(') == t.count(')'), 'unbalanced'
|
||||||
|
assert len(re.findall(r'\n\t\(footprint "', s)) == len(re.findall(r'\n\t\(footprint "', s0))
|
||||||
|
print('\n'.join(log))
|
||||||
|
if not cuts: print('nothing to do'); return
|
||||||
|
if apply:
|
||||||
|
shutil.copy2(path, path + '.T15bbak'); open(path, 'w').write(s); print('APPLIED (backup .T15bbak)')
|
||||||
|
else:
|
||||||
|
print('DRY RUN — re-run with --apply')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user