Repository navigation
Expand file tree
/
Copy pathstackup_parts.py
More file actions
135 lines (112 loc) · 4.23 KB
/
Copy pathstackup_parts.py
File metadata and controls
135 lines (112 loc) · 4.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""Read purchasing choices from Stackup's BOM and edit reviewed placement fields."""
from __future__ import annotations
import csv
import io
import json
import os
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PARTS_FILE = ROOT / "PCB" / "v1.x" / "stackup" / "board.kdl"
PCB_FILE = ROOT / "PCB" / "v1.x" / "snips_controller.kicad_pcb"
@dataclass(frozen=True)
class Part:
ref: str
value: str
footprint: str
manufacturer: str
mpn: str
lcsc: str
hand: bool
dnp: bool
PLACEMENT = re.compile(r'^ place \S+ "([A-Za-z0-9_]+)" .*$', re.M)
def _string(block: str, pattern: str) -> str:
match = re.search(pattern + r'("(?:[^"\\]|\\.)*")', block, re.M)
return json.loads(match.group(1)) if match else ""
def read_bom_rows() -> list[dict[str, str]]:
"""Use the Cargo-locked Stackup engine, including library ordering defaults."""
binary = os.environ.get("STACKUP_BIN")
command = ([binary] if binary else [
"cargo", "+1.96.0", "run", "--quiet", "--locked", "--manifest-path",
str(ROOT / "ci" / "stackup" / "Cargo.toml"), "--",
]) + ["bom", str(PARTS_FILE), "--locked"]
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
if result.returncode:
raise RuntimeError(f"stackup bom failed:\n{result.stderr or result.stdout}")
rows = list(csv.DictReader(io.StringIO(result.stdout)))
if not rows:
raise ValueError(f"no components in {PARTS_FILE}")
return rows
def read_parts() -> dict[str, Part]:
"""Expand grouped BOM rows by designator for scripts that edit placements."""
parts = {}
for row in read_bom_rows():
refs = [ref.strip() for ref in row["Refs"].split(",")]
if len(refs) != int(row["Quantity"]):
raise ValueError(f"bad Stackup BOM quantity for {row['Refs']}")
for ref in refs:
if ref in parts:
raise ValueError(f"duplicate Stackup BOM reference {ref}")
parts[ref] = Part(
ref=ref,
value=row["Value"],
footprint=row["Footprint"],
manufacturer=row["MF"],
mpn=row["MPN"],
lcsc=row["LCSC"],
hand=row["Hand"] == "Yes",
dnp=row["DNP"] == "Yes",
)
if not parts:
raise ValueError(f"no components in {PARTS_FILE}")
return parts
def quoted(value: str) -> str:
return json.dumps(value, ensure_ascii=False)
def replace_placement_property(text: str, ref: str, key: str, value: str) -> str:
for match in PLACEMENT.finditer(text):
if match.group(1) != ref:
continue
line = match.group()
pattern = re.compile(r"\b" + re.escape(key) + r'="(?:[^"\\]|\\.)*"')
new, count = pattern.subn(key + "=" + quoted(value), line, count=1)
if count == 0:
cut = line.find(" { ")
if cut < 0:
cut = len(line)
new = line[:cut] + " " + key + "=" + quoted(value) + line[cut:]
return text[:match.start()] + new + text[match.end():]
raise KeyError(ref)
def balanced(text: str, start: int) -> int:
"""Return the byte after the closing parenthesis, ignoring quoted strings."""
depth = 0
quoted = False
escaped = False
for index in range(start, len(text)):
char = text[index]
if quoted:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
quoted = False
elif char == '"':
quoted = True
elif char == "(":
depth += 1
elif char == ")":
depth -= 1
if depth == 0:
return index + 1
raise ValueError("unbalanced PCB expression")
def pcb_footprints(text: str):
for match in re.finditer(r'\n\t\(footprint "', text):
start = match.start() + 2
end = balanced(text, start)
block = text[start:end]
ref = _string(block, r'\(property "Reference" ')
yield ref, start, end, block
def pcb_property(block: str, name: str) -> str:
return _string(block, r'\(property "' + re.escape(name) + r'" ')