221 行
12 KiB
Python
221 行
12 KiB
Python
"""Scoped migration cleanup; preserve all untouched S-expression source spans."""
|
||
import argparse
|
||
from collections import Counter
|
||
import json
|
||
from pathlib import Path
|
||
import re
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
TOKEN = re.compile(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+')
|
||
# Pin numbers/types cross-checked against KiCad 74xx:74HC595.
|
||
HC595_PINS = {
|
||
**{str(i): (f'Q{i}', 'tri_state') for i in range(1, 8)},
|
||
'8': ('GND', 'power_in'), '9': ('Q7S', 'output'),
|
||
'10': ('MR#', 'input'), '11': ('SHCP', 'input'),
|
||
'12': ('STCP', 'input'), '13': ('OE#', 'input'),
|
||
'14': ('DS', 'input'), '15': ('Q0', 'tri_state'),
|
||
'16': ('VCC', 'power_in'),
|
||
}
|
||
# Reviewed dangling extensions from the migration baseline, not arbitrary ERC errors.
|
||
WIRE_STUBS = {
|
||
'a65a7700-a243-55e0-8007-fd0b3d962b61', '0a207484-be28-5acb-b11a-f92435941381',
|
||
'6cd4551d-b945-53d0-80d0-21d019ad50b8', '503168b2-0b79-59b7-bc16-76fc375faae5',
|
||
'441e621d-54a9-5730-ba9d-e935bb1fb902', 'b8b87b0a-b722-5042-99c4-f48791a7a13a',
|
||
'f0891bf2-0ec0-5b8f-a2e7-8aae61bc25c3', '543b8815-4995-5fb6-808d-969700a6774a',
|
||
'8a9d3b65-768d-59a5-a13f-19fae717ebc4', '32a92a88-d05a-56aa-9132-fde8e08df99b',
|
||
'5c2de7b9-896a-5a8b-9cfa-054a4be9c2c4', '33ec1b87-42b3-5371-95f4-c14859255644',
|
||
'15fea150-9f17-5ddd-97f0-f3618ed282ae', '56ba2d24-026a-56c4-a8b9-f2fe385f63aa',
|
||
'0a7ffeba-9293-5255-8d6d-e59c113ecf11', '4d8be1e0-5a90-519d-bd30-3b2092db6667',
|
||
'5939369d-292c-5284-aba7-35b9e8c034eb',
|
||
}
|
||
|
||
|
||
class Node(list):
|
||
def __init__(self, start):
|
||
super().__init__()
|
||
self.start = start
|
||
self.end = None
|
||
|
||
|
||
def parse(source):
|
||
stack = []
|
||
root = None
|
||
for match in TOKEN.finditer(source):
|
||
token = match.group()
|
||
if token == '(':
|
||
node = Node(match.start())
|
||
if stack:
|
||
stack[-1].append(node)
|
||
else:
|
||
if root is not None:
|
||
raise ValueError('Multiple roots')
|
||
root = node
|
||
stack.append(node)
|
||
elif token == ')':
|
||
stack.pop().end = match.end()
|
||
else:
|
||
stack[-1].append(json.loads(token) if token.startswith('"') else token)
|
||
if stack or root is None:
|
||
raise ValueError('Unbalanced S-expression')
|
||
return root
|
||
|
||
|
||
def children(node, key):
|
||
return [x for x in node if isinstance(x, Node) and x[0] == key]
|
||
|
||
|
||
def child(node, key):
|
||
return next(iter(children(node, key)), None)
|
||
|
||
|
||
def properties(node):
|
||
return {x[1]: x for x in children(node, 'property')}
|
||
|
||
|
||
def cleanup(path, apply=False):
|
||
source = path.read_text(encoding='utf-8')
|
||
root = parse(source)
|
||
edits = []
|
||
counts = Counter()
|
||
replace_fan_notes = (path.name == 'P6-FAN-EDL.kicad_sch'
|
||
and '风扇电路审查:FR-10 尚未满足' not in source
|
||
and 'TWO-FAN PROTOTYPE: INTAKE + EXHAUST' not in source
|
||
and 'FOUR-WIRE FAN PROTOTYPE R1' not in source
|
||
and 'SANYO FAN SELECTION R2' not in source)
|
||
replace_site_notes = path.name.startswith('C4-SITE') and '侧键采用裸铜压接,非采购元件' not in source
|
||
replace_keys_notes = path.name == 'C6-KEYS.kicad_sch' and '控制链核查:四个未用输出已逐脚标 NC' not in source
|
||
|
||
def replace(node, replacement, reason):
|
||
old = source[node.start:node.end]
|
||
if replacement != old:
|
||
edits.append((node.start, node.end, replacement))
|
||
counts[reason] += 1
|
||
|
||
libraries = children(root, 'symbol') if root[0] == 'kicad_symbol_lib' else children(child(root, 'lib_symbols'), 'symbol')
|
||
for symbol in libraries:
|
||
props = properties(symbol)
|
||
ref = props['Reference'][2]
|
||
if symbol[1].removeprefix('PhoneRail:') == '74HC595D,118':
|
||
pins = [p for unit in children(symbol, 'symbol') for p in children(unit, 'pin')]
|
||
assert {child(p, 'number')[1] for p in pins} == set(HC595_PINS)
|
||
for pin in pins:
|
||
name, kind = HC595_PINS[child(pin, 'number')[1]]
|
||
assert child(pin, 'name')[1] == name
|
||
replace(pin, re.sub(r'^\(pin\s+\S+', '(pin ' + kind, source[pin.start:pin.end]), 'hc595_pin_types')
|
||
if ref not in {'R', 'Rs', 'RT', 'C', 'L', 'F', 'Y', 'J', 'D', 'DZ'}:
|
||
continue
|
||
for unit in children(symbol, 'symbol'):
|
||
for pin in children(unit, 'pin'):
|
||
raw = source[pin.start:pin.end]
|
||
replace(pin, re.sub(r'^\(pin\s+\S+', '(pin passive', raw), 'passive_pins')
|
||
|
||
if root[0] == 'kicad_sch':
|
||
for wire in children(root, 'wire'):
|
||
if child(wire, 'uuid')[1] in WIRE_STUBS:
|
||
replace(wire, '', 'reviewed_wire_stubs')
|
||
for symbol in children(root, 'symbol'):
|
||
props = properties(symbol)
|
||
ref = props['Reference'][2]
|
||
if replace_site_notes and props.get('Value', ['', '', ''])[2].startswith('6.3TPV2200'):
|
||
p = props['Value']
|
||
replace(p, re.sub(r'^(\(property "Value"\s+)"[^"]*"', r'\1"2200uF / 6.3V"', source[p.start:p.end]), 'capacitor_label')
|
||
if ref.startswith('#PWR'):
|
||
for key in ('in_bom', 'on_board'):
|
||
replace(child(symbol, key), f'({key} no)', 'power_metadata')
|
||
if re.fullmatch(r'J(?:4[1-9]|5[0-9]|60)', ref) and 'Board2' in str(path):
|
||
assert props['Footprint'][2] == 'PhoneRail:PAD-D1.5_MI8-KEY'
|
||
replace(child(symbol, 'in_bom'), '(in_bom no)', 'contact_bom')
|
||
for key, value in {'Value': 'KEY_PAD_D1.5', 'MPN': '', 'LCSC': '', 'Manufacturer': '', 'Datasheet': '', 'Description': 'ENIG contact, no paste; PCB feature, not a purchased part'}.items():
|
||
if key in props:
|
||
p = props[key]
|
||
raw = source[p.start:p.end]
|
||
updated = re.sub(r'^(\(property\s+"(?:\\.|[^"\\])*"\s+)"(?:\\.|[^"\\])*"', lambda m: m[1] + json.dumps(value), raw)
|
||
replace(p, updated, 'contact_metadata')
|
||
|
||
# Only identical text at identical coordinates is redundant. Different revisions
|
||
# are retained except the fan page, whose full annotation is replaced below.
|
||
seen = set()
|
||
for text in children(root, 'text'):
|
||
signature = (text[1], tuple(child(text, 'at')[1:]))
|
||
if replace_fan_notes or replace_site_notes or replace_keys_notes:
|
||
replace(text, '', 'keys_stale_notes' if replace_keys_notes else 'fan_stale_notes' if replace_fan_notes else 'site_stale_notes')
|
||
elif signature in seen:
|
||
replace(text, '', 'duplicate_notes')
|
||
else:
|
||
seen.add(signature)
|
||
if path.name.startswith('C4-SITE'):
|
||
value = text[1]
|
||
if 'YTC1P-2510-01' in value:
|
||
value = ' 侧键为 PCB 顶层 Ø1.5 ENIG 裸铜触点;不开钢网、不装顶针。'
|
||
elif '冠径只有' in value:
|
||
value = ' 主板弹片压接触点,装配间隙 1.0 mm;触点位置须实物复测。'
|
||
else:
|
||
value = value.replace('四根顶针顶主板反面的', '四个裸铜触点连接主板的').replace('侧键顶针', '侧键裸铜触点')
|
||
raw = source[text.start:text.end]
|
||
replace(text, re.sub(r'^\(text\s+"(?:\\.|[^"\\])*"', lambda m: '(text ' + json.dumps(value, ensure_ascii=False), raw), 'contact_notes')
|
||
|
||
if replace_fan_notes or replace_site_notes or replace_keys_notes:
|
||
import uuid
|
||
notes = [
|
||
(12.7, 13.716, '风扇电路审查:FR-10 尚未满足,禁止按本页直接投板。'),
|
||
(12.7, 19.05, 'FAN_PWM = GPIO18 / U20.11;FAN_TACH = GPIO16 / U20.9。'),
|
||
(12.7, 86.36, 'Q6 低边调速,D2 续流;五个风扇共用供电回路。'),
|
||
(12.7, 92.456, 'R730/R731/R732 = 10k/10k/3.3k:开漏释放时高电平仅 1.70V。'),
|
||
(12.7, 98.552, '计算:12 x 3.3 / (10 + 10 + 3.3),低于 GPIO 保证高阈值 2.475V。'),
|
||
(12.7, 104.648, '五路 TACH 并联不能保证识别单风扇停转;不得按频率下降 1/5 判定。'),
|
||
(12.7, 110.744, '测速需独立采集或故障汇总电路;低边关断时须校核浮地和 GPIO 钳位。'),
|
||
(12.7, 170.18, 'J26~J30:1=+12V,2=风扇回流/Q6.D,3=TACH(当前并联,待整改)。'),
|
||
(12.7, 176.276, '型号、电平接口与装配高度待实物确认。详见 HARDWARE_REVIEW.md。'),
|
||
]
|
||
if replace_site_notes:
|
||
ch = int(path.stem[-1])
|
||
notes = [
|
||
(15, 17, f'CH{ch}:电池直供、电容组、侧键触点与 EDL'),
|
||
(15, 23, f'J8{ch}:1=P+,2=P-,3=TEMP,4=ID;旁路电池保护板,保留排线 BTB。'),
|
||
(15, 29, f'R78{ch}=10k 模拟温度;R79{ch}=68k 模拟 ID。2200uF 使用指定普通铝电解,勿换低 ESR 聚合物。'),
|
||
(15, 86, '侧键采用裸铜压接,非采购元件:Ø1.5 ENIG,钢网不开口,不安装顶针。'),
|
||
(15, 92, '主板弹片直压触点,间隙 1.0mm;接触位置、压力及绝缘须实物确认。'),
|
||
(15, 98, f'Q{ch}03~Q{ch}05:栅极 1k 串阻、100k 下拉;595 高阻期间保持按键释放。'),
|
||
(15, 167, f'J6{ch} 连接主板 TP1204/TP1205,U4{ch} 导通时短接两点进入 EDL。'),
|
||
(15, 173, f'R74{ch}=100k 下拉:复位或连接器未插时 EDL 不触发。'),
|
||
(15, 179, '装配与未完成检查见 HARDWARE_REVIEW.md。'),
|
||
]
|
||
if replace_keys_notes:
|
||
notes = [
|
||
(15, 16, 'C6-KEYS:三片 74HC595,15 路侧键 + 5 路 EDL + HUB_RST'),
|
||
(15, 22, '控制链核查:四个未用输出已逐脚标 NC;电气参数与电源时序仍待复核。'),
|
||
(15, 28, '级联 U21 → U22 → U33;固件发送 U33 → U22 → U21,每字节高位先发。'),
|
||
(185, 44, '三片 VCC / MR# 接 +3V3_HUB。'),
|
||
(185, 51, 'R609 100k 将 OE# 拉高,复位时输出高阻。'),
|
||
(185, 58, '先锁存安全值,再拉低 OE# 使能输出。'),
|
||
(185, 65, 'HUB_RST 低有效;安全值为 1 << 23。'),
|
||
(185, 72, 'U33.Q7 → U32.43;R716 为复位上拉。'),
|
||
(185, 82, 'NC:U21.4、U22.4、U22.15、U33.9。'),
|
||
(185, 89, '前三脚为预留并行输出;末脚为链尾输出。'),
|
||
(185, 99, 'KEY_SER / SRCLK / RCLK / OE# 经板间连接。'),
|
||
(185, 106, '跨电源域上掉电、注入电流及阈值待审。'),
|
||
(185, 116, 'K*_PWR / VUP / VDN 驱动 C4 页按键 MOS。'),
|
||
(185, 123, 'EDL_EN1~5 控制各路 TS5A3157。'),
|
||
]
|
||
new = ''.join('\n\t(text ' + json.dumps(value, ensure_ascii=False) + f' (at {x} {y} 0) (effects (font (size 1.587 1.587)) (justify left bottom)) (uuid "{uuid.uuid5(uuid.NAMESPACE_URL, value)}"))' for x, y, value in notes)
|
||
edits.append((root.end - 1, root.end - 1, new + '\n'))
|
||
|
||
for start, end, replacement in sorted(edits, reverse=True):
|
||
source = source[:start] + replacement + source[end:]
|
||
if apply and edits:
|
||
path.write_text(source, encoding='utf-8', newline='\n')
|
||
return counts
|
||
|
||
|
||
if __name__ == '__main__':
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument('--apply', action='store_true')
|
||
args = parser.parse_args()
|
||
total = Counter()
|
||
for path in [ROOT / 'lib/PhoneRail.kicad_sym', *sorted(ROOT.glob('PhoneRail-*/*.kicad_sch'))]:
|
||
counts = cleanup(path, args.apply)
|
||
if counts:
|
||
print(path.name, dict(counts))
|
||
total.update(counts)
|
||
print('TOTAL', dict(total))
|