restructure repo for KiCad PCM packaging
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>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user