9a5828a21a
Move plugin sources into plugins/ subdirectory, add 64x64 package icon in resources/, and add metadata.json for PCM compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
249 lines
7.3 KiB
Python
249 lines
7.3 KiB
Python
"""
|
|
Parser for KiCad .kicad_sch S-expression files.
|
|
|
|
Extracts:
|
|
- pin-number → GPIO-name mapping (from the symbol library definition)
|
|
- pin-number → schematic label (by tracing wires from MCU pins to labels)
|
|
"""
|
|
|
|
import re
|
|
from collections import defaultdict
|
|
|
|
|
|
# ── low-level helpers ────────────────────────────────────────────────
|
|
|
|
def _find_matching_close(text, start, limit=200_000):
|
|
"""Return index of the ')' that closes the '(' at *start*."""
|
|
depth = 0
|
|
for i, ch in enumerate(text[start : start + limit]):
|
|
if ch == "(":
|
|
depth += 1
|
|
elif ch == ")":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return start + i
|
|
return start + limit
|
|
|
|
|
|
def _rnd(x, y):
|
|
"""Round a coordinate pair for reliable dict-key comparison."""
|
|
return (round(x, 2), round(y, 2))
|
|
|
|
|
|
# ── symbol / pin extraction ─────────────────────────────────────────
|
|
|
|
def _extract_pins(section):
|
|
"""Return {pin_number: pin_name} for every (pin ...) in *section*."""
|
|
pins = {}
|
|
pos = 0
|
|
while True:
|
|
idx = section.find("(pin ", pos)
|
|
if idx == -1:
|
|
break
|
|
end = _find_matching_close(section, idx)
|
|
block = section[idx : end + 1]
|
|
|
|
nm = re.search(r'\(name\s+"([^"]+)"', block)
|
|
nu = re.search(r'\(number\s+"(\d+)"', block)
|
|
if nm and nu:
|
|
pins[nu.group(1)] = nm.group(1)
|
|
|
|
pos = end + 1
|
|
return pins
|
|
|
|
|
|
_PIN_POS_RE = re.compile(
|
|
r"\(pin\s+\w+\s+\w+\s*\n\s*"
|
|
r"\(at\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)\)"
|
|
r".*?\(name\s+\"([^\"]+)\""
|
|
r".*?\(number\s+\"(\d+)\"",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def _extract_pin_positions(section):
|
|
"""Return {pin_number: (local_x, local_y, angle, name)}."""
|
|
pins = {}
|
|
for m in _PIN_POS_RE.finditer(section):
|
|
lx = float(m.group(1))
|
|
ly = float(m.group(2))
|
|
ang = int(m.group(3))
|
|
name = m.group(4)
|
|
num = m.group(5)
|
|
pins[num] = (lx, ly, ang, name)
|
|
return pins
|
|
|
|
|
|
def _find_lib_id(content, reference):
|
|
"""Find the lib_id string for the symbol instance with *reference*."""
|
|
ref_re = re.compile(
|
|
r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"'
|
|
)
|
|
for m in ref_re.finditer(content):
|
|
chunk = content[max(0, m.start() - 3000) : m.start()]
|
|
hits = list(
|
|
re.finditer(r'\(symbol\s*\n\s*\(lib_id\s+"([^"]+)"\)', chunk)
|
|
)
|
|
if hits:
|
|
return hits[-1].group(1)
|
|
return None
|
|
|
|
|
|
def _find_symbol_position(content, reference):
|
|
"""Return (x, y) placement of the symbol instance for *reference*."""
|
|
ref_re = re.compile(
|
|
r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"'
|
|
)
|
|
for m in ref_re.finditer(content):
|
|
chunk = content[max(0, m.start() - 3000) : m.start()]
|
|
hits = list(re.finditer(
|
|
r'\(symbol\s*\n\s*\(lib_id\s+"[^"]+"\)\s*\n\s*'
|
|
r'\(at\s+([-\d.]+)\s+([-\d.]+)\s+\d+\)',
|
|
chunk,
|
|
))
|
|
if hits:
|
|
return float(hits[-1].group(1)), float(hits[-1].group(2))
|
|
return None
|
|
|
|
|
|
def _sub_symbol_sections(content, lib_id):
|
|
"""Yield content slices for each sub-symbol unit (_0_1, _1_1, …)."""
|
|
sym_name = lib_id.split(":")[-1] if ":" in lib_id else lib_id
|
|
sub_re = re.compile(
|
|
r'\(symbol\s+"' + re.escape(sym_name) + r'_\d+_1"'
|
|
)
|
|
for hit in sub_re.finditer(content):
|
|
end = _find_matching_close(content, hit.start())
|
|
yield content[hit.start() : end + 1]
|
|
|
|
|
|
# ── schematic wire / label parsing ───────────────────────────────────
|
|
|
|
_WIRE_RE = re.compile(
|
|
r"\(wire\s*\n\s*\(pts\s*\n\s*"
|
|
r"\(xy\s+([-\d.]+)\s+([-\d.]+)\)\s+"
|
|
r"\(xy\s+([-\d.]+)\s+([-\d.]+)\)\s*\n"
|
|
)
|
|
|
|
|
|
def _parse_wires(content):
|
|
"""Return list of ((x1,y1), (x2,y2)) wire segments."""
|
|
return [
|
|
((float(m.group(1)), float(m.group(2))),
|
|
(float(m.group(3)), float(m.group(4))))
|
|
for m in _WIRE_RE.finditer(content)
|
|
]
|
|
|
|
|
|
def _parse_labels(content):
|
|
"""Return {(x,y): label_text} for all net labels and global labels."""
|
|
labels = {}
|
|
|
|
# normal net labels
|
|
for m in re.finditer(
|
|
r'\(label\s+"([^"]+)".*?\(at\s+([-\d.]+)\s+([-\d.]+)',
|
|
content, re.DOTALL,
|
|
):
|
|
txt = m.group(1)
|
|
if not txt.startswith("TODO"):
|
|
labels[(float(m.group(2)), float(m.group(3)))] = txt
|
|
|
|
# global labels
|
|
for m in re.finditer(
|
|
r'\(global_label\s+"([^"]+)".*?\(at\s+([-\d.]+)\s+([-\d.]+)',
|
|
content, re.DOTALL,
|
|
):
|
|
labels[(float(m.group(2)), float(m.group(3)))] = m.group(1)
|
|
|
|
return labels
|
|
|
|
|
|
def _build_wire_graph(wires):
|
|
"""Build adjacency dict from wire segments (rounded coordinates)."""
|
|
graph = defaultdict(set)
|
|
for (x1, y1), (x2, y2) in wires:
|
|
p1, p2 = _rnd(x1, y1), _rnd(x2, y2)
|
|
graph[p1].add(p2)
|
|
graph[p2].add(p1)
|
|
return graph
|
|
|
|
|
|
def _bfs_labels(start, graph, label_at):
|
|
"""BFS from *start* through the wire graph; return first label found."""
|
|
visited = set()
|
|
queue = [start]
|
|
while queue:
|
|
pt = queue.pop(0)
|
|
if pt in visited:
|
|
continue
|
|
visited.add(pt)
|
|
if pt in label_at:
|
|
return label_at[pt]
|
|
for nb in graph.get(pt, ()):
|
|
if nb not in visited:
|
|
queue.append(nb)
|
|
return ""
|
|
|
|
|
|
# ── public API ───────────────────────────────────────────────────────
|
|
|
|
def get_pin_mapping(sch_path, reference):
|
|
"""
|
|
Return {pad_number_str: gpio_name_str} for *reference* (e.g. "U31").
|
|
"""
|
|
with open(sch_path, "r", encoding="utf-8") as fh:
|
|
content = fh.read()
|
|
|
|
lib_id = _find_lib_id(content, reference)
|
|
if not lib_id:
|
|
return {}
|
|
|
|
all_pins = {}
|
|
for section in _sub_symbol_sections(content, lib_id):
|
|
all_pins.update(_extract_pins(section))
|
|
|
|
return all_pins
|
|
|
|
|
|
def get_pin_labels(sch_path, reference):
|
|
"""
|
|
Return {pad_number_str: schematic_label_str} for *reference*.
|
|
|
|
Traces wires from each MCU pin to the nearest connected label
|
|
(normal or global) in the schematic.
|
|
"""
|
|
with open(sch_path, "r", encoding="utf-8") as fh:
|
|
content = fh.read()
|
|
|
|
lib_id = _find_lib_id(content, reference)
|
|
if not lib_id:
|
|
return {}
|
|
|
|
sym_pos = _find_symbol_position(content, reference)
|
|
if not sym_pos:
|
|
return {}
|
|
sx, sy = sym_pos
|
|
|
|
# collect pin local positions from all sub-symbol units
|
|
pin_locals = {}
|
|
for section in _sub_symbol_sections(content, lib_id):
|
|
pin_locals.update(_extract_pin_positions(section))
|
|
|
|
# transform to absolute coordinates (rotation 0: abs = sym + local, Y inverted)
|
|
pin_abs = {}
|
|
for num, (lx, ly, _angle, _name) in pin_locals.items():
|
|
pin_abs[num] = _rnd(sx + lx, sy - ly)
|
|
|
|
# build wire connectivity graph
|
|
graph = _build_wire_graph(_parse_wires(content))
|
|
label_at = {_rnd(*k): v for k, v in _parse_labels(content).items()}
|
|
|
|
# trace from each pin
|
|
result = {}
|
|
for num, start in pin_abs.items():
|
|
lbl = _bfs_labels(start, graph, label_at)
|
|
if lbl:
|
|
result[num] = lbl
|
|
|
|
return result
|