traceforge-pcb 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traceforge/__init__.py +3 -0
- traceforge/__main__.py +3 -0
- traceforge/acp_server.py +570 -0
- traceforge/agent.py +680 -0
- traceforge/altium/__init__.py +6 -0
- traceforge/altium/cfb.py +70 -0
- traceforge/altium/cli.py +60 -0
- traceforge/altium/model.py +163 -0
- traceforge/altium/outputs.py +24 -0
- traceforge/altium/pcb.py +301 -0
- traceforge/altium/project.py +328 -0
- traceforge/altium/schematic.py +400 -0
- traceforge/altium/tools.py +166 -0
- traceforge/approval.py +219 -0
- traceforge/attachments.py +346 -0
- traceforge/background.py +253 -0
- traceforge/bench/__init__.py +5 -0
- traceforge/bench/adapters.py +337 -0
- traceforge/bench/checks.py +410 -0
- traceforge/bench/cli.py +121 -0
- traceforge/bench/design.py +439 -0
- traceforge/bench/kicad.py +99 -0
- traceforge/bench/partscheck.py +159 -0
- traceforge/bench/review.py +100 -0
- traceforge/bench/runner.py +226 -0
- traceforge/bench/score.py +204 -0
- traceforge/bench/tasks.py +102 -0
- traceforge/chat.py +1185 -0
- traceforge/checks.py +328 -0
- traceforge/checks_sides.py +118 -0
- traceforge/cli.py +2224 -0
- traceforge/clipboard.py +107 -0
- traceforge/config.py +299 -0
- traceforge/context.py +276 -0
- traceforge/design/__init__.py +1 -0
- traceforge/design/constraints.py +397 -0
- traceforge/design/generate.py +1789 -0
- traceforge/design/outline.py +905 -0
- traceforge/design/schematic_layout.py +932 -0
- traceforge/design/schematic_lint.py +329 -0
- traceforge/design/schematic_route.py +207 -0
- traceforge/design/sheets.py +854 -0
- traceforge/design/sides.py +328 -0
- traceforge/design/spec.py +462 -0
- traceforge/design/specsheet.py +2351 -0
- traceforge/design/specsheet_render.py +183 -0
- traceforge/desktop/__init__.py +109 -0
- traceforge/desktop/keys.py +90 -0
- traceforge/desktop/project.py +443 -0
- traceforge/desktop/review.py +20 -0
- traceforge/desktop/server.py +1277 -0
- traceforge/desktop/static/app.css +223 -0
- traceforge/desktop/static/app.js +683 -0
- traceforge/desktop/static/highlight.js +77 -0
- traceforge/desktop/static/index.html +89 -0
- traceforge/desktop/static/md.js +90 -0
- traceforge/desktop/static/viewer.js +213 -0
- traceforge/desktop/symbols.py +212 -0
- traceforge/desktop/update.py +65 -0
- traceforge/diagnostics.py +67 -0
- traceforge/errata.py +219 -0
- traceforge/fab.py +108 -0
- traceforge/fab_layers.py +44 -0
- traceforge/features/__init__.py +6 -0
- traceforge/features/candidates.py +117 -0
- traceforge/features/cli.py +200 -0
- traceforge/features/package.py +433 -0
- traceforge/features/revdiff.py +224 -0
- traceforge/features/sarif.py +145 -0
- traceforge/features/viewer.py +265 -0
- traceforge/firmware/__init__.py +3 -0
- traceforge/firmware/cli.py +174 -0
- traceforge/firmware/drivers.py +372 -0
- traceforge/firmware/drivers_data.py +256 -0
- traceforge/firmware/mcus.py +177 -0
- traceforge/firmware/netlist.py +55 -0
- traceforge/firmware/pinmap.py +296 -0
- traceforge/firmware/scaffold.py +466 -0
- traceforge/firmware/toolchain.py +262 -0
- traceforge/hooks.py +237 -0
- traceforge/kicad/__init__.py +0 -0
- traceforge/kicad/cli.py +519 -0
- traceforge/kicad/diffpair.py +174 -0
- traceforge/kicad/edgegeom.py +191 -0
- traceforge/kicad/edit.py +405 -0
- traceforge/kicad/escape.py +1410 -0
- traceforge/kicad/formats.py +1073 -0
- traceforge/kicad/impedance.py +320 -0
- traceforge/kicad/install.py +75 -0
- traceforge/kicad/libraries.py +490 -0
- traceforge/kicad/models3d.py +942 -0
- traceforge/kicad/outputs.py +81 -0
- traceforge/kicad/pcb.py +787 -0
- traceforge/kicad/planes.py +1242 -0
- traceforge/kicad/project.py +136 -0
- traceforge/kicad/route.py +2698 -0
- traceforge/kicad/routegeom.py +164 -0
- traceforge/kicad/schematic.py +268 -0
- traceforge/kicad/sides.py +34 -0
- traceforge/kicad/stackup.py +424 -0
- traceforge/kicad_plugin/__init__.py +12 -0
- traceforge/kicad_plugin/actions.py +92 -0
- traceforge/kicad_plugin/cli.py +66 -0
- traceforge/kicad_plugin/client.py +86 -0
- traceforge/kicad_plugin/context.py +257 -0
- traceforge/kicad_plugin/discovery.py +81 -0
- traceforge/kicad_plugin/install.py +203 -0
- traceforge/kicad_plugin/overlay.py +60 -0
- traceforge/kicad_plugin/payload/action_plugin.py +65 -0
- traceforge/kicad_plugin/payload/ipc_main.py +42 -0
- traceforge/kicad_plugin/payload/plugin.json +90 -0
- traceforge/kicad_plugin/payload/requirements.txt +1 -0
- traceforge/kicad_plugin/views.py +202 -0
- traceforge/langchain.py +57 -0
- traceforge/local.py +580 -0
- traceforge/mcp_server.py +304 -0
- traceforge/models.py +310 -0
- traceforge/next_steps.py +110 -0
- traceforge/parts/__init__.py +52 -0
- traceforge/parts/alternates.py +488 -0
- traceforge/parts/bom.py +423 -0
- traceforge/parts/brief.py +705 -0
- traceforge/parts/catalog.py +297 -0
- traceforge/parts/cli_sourcing.py +156 -0
- traceforge/parts/data/duty_rates.yaml +37 -0
- traceforge/parts/data/hs_codes.yaml +103 -0
- traceforge/parts/datasheet.py +278 -0
- traceforge/parts/fx.py +100 -0
- traceforge/parts/model.py +185 -0
- traceforge/parts/net.py +187 -0
- traceforge/parts/offers.py +46 -0
- traceforge/parts/packages.py +397 -0
- traceforge/parts/pinout.py +714 -0
- traceforge/parts/providers/__init__.py +17 -0
- traceforge/parts/providers/base.py +100 -0
- traceforge/parts/providers/jlc.py +364 -0
- traceforge/parts/providers/keyed.py +661 -0
- traceforge/parts/report.py +508 -0
- traceforge/parts/supply.py +330 -0
- traceforge/parts/verify.py +455 -0
- traceforge/parts/web.py +575 -0
- traceforge/plans.py +70 -0
- traceforge/preferences.py +127 -0
- traceforge/prices.py +130 -0
- traceforge/prices.yaml +28 -0
- traceforge/prompts/__init__.py +54 -0
- traceforge/prompts/system.md +103 -0
- traceforge/providers.py +764 -0
- traceforge/remote/__init__.py +33 -0
- traceforge/remote/client.py +422 -0
- traceforge/remote/oidc.py +240 -0
- traceforge/remote/orgs.py +510 -0
- traceforge/remote/server.py +690 -0
- traceforge/remote/store.py +100 -0
- traceforge/review_changes.py +187 -0
- traceforge/routing.py +99 -0
- traceforge/runtime.py +435 -0
- traceforge/sessions.py +324 -0
- traceforge/sexpr.py +279 -0
- traceforge/sim/__init__.py +17 -0
- traceforge/sim/acmath.py +234 -0
- traceforge/sim/circuit.py +499 -0
- traceforge/sim/cli.py +162 -0
- traceforge/sim/engine.py +172 -0
- traceforge/sim/ltspice.py +175 -0
- traceforge/sim/measure.py +123 -0
- traceforge/sim/models.py +76 -0
- traceforge/sim/project.py +218 -0
- traceforge/sim/rawfile.py +123 -0
- traceforge/sim/results.py +298 -0
- traceforge/sim/tune.py +172 -0
- traceforge/skill_commands.py +165 -0
- traceforge/skill_library/circuit-simulation/SKILL.md +30 -0
- traceforge/skill_library/compliance-review/SKILL.md +51 -0
- traceforge/skill_library/design-review/SKILL.md +47 -0
- traceforge/skill_library/errata-hunt/SKILL.md +30 -0
- traceforge/skill_library/firmware/SKILL.md +26 -0
- traceforge/skill_library/power-budget/SKILL.md +27 -0
- traceforge/skill_library/release-package/SKILL.md +29 -0
- traceforge/skill_library/sourcing-brief/SKILL.md +24 -0
- traceforge/skill_library/thermal-review/SKILL.md +15 -0
- traceforge/skills.py +131 -0
- traceforge/thermal.py +557 -0
- traceforge/tools/__init__.py +48 -0
- traceforge/tools/agent_tools.py +213 -0
- traceforge/tools/base.py +303 -0
- traceforge/tools/batch.py +155 -0
- traceforge/tools/connections.py +245 -0
- traceforge/tools/files.py +777 -0
- traceforge/tools/firmware_tools.py +218 -0
- traceforge/tools/kicad_tools.py +1072 -0
- traceforge/tools/models3d_tools.py +141 -0
- traceforge/tools/parts_tools.py +473 -0
- traceforge/tools/processes.py +246 -0
- traceforge/tools/review_tools.py +178 -0
- traceforge/tools/sim_tools.py +230 -0
- traceforge/tui.py +620 -0
- traceforge/updates.py +124 -0
- traceforge/validate/__init__.py +77 -0
- traceforge/validate/builtins.py +1091 -0
- traceforge/validate/circuits.py +1240 -0
- traceforge/validate/config.py +410 -0
- traceforge/validate/default.yaml +113 -0
- traceforge/validate/design.py +191 -0
- traceforge/validate/explain.py +112 -0
- traceforge/validate/grounding.py +169 -0
- traceforge/validate/ipc2221.py +350 -0
- traceforge/validate/multilayer.py +805 -0
- traceforge/validate/netmodel.py +261 -0
- traceforge/validate/partcheck.py +472 -0
- traceforge/validate/report.py +280 -0
- traceforge/validate/review.py +322 -0
- traceforge/validate/runner.py +662 -0
- traceforge/workspace.py +171 -0
- traceforge_pcb-1.0.0.dist-info/METADATA +463 -0
- traceforge_pcb-1.0.0.dist-info/RECORD +219 -0
- traceforge_pcb-1.0.0.dist-info/WHEEL +4 -0
- traceforge_pcb-1.0.0.dist-info/entry_points.txt +2 -0
- traceforge_pcb-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Altium projects (``.PrjPcb``): load sheets and board, netlist, BOM, KiCad-side models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import date
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from traceforge.altium.cfb import AltiumError
|
|
13
|
+
from traceforge.altium.pcb import AltPcb, parse_pcb
|
|
14
|
+
from traceforge.altium.schematic import (
|
|
15
|
+
AltComponent,
|
|
16
|
+
AltPin,
|
|
17
|
+
AltSheet,
|
|
18
|
+
Netlist,
|
|
19
|
+
extract_nets,
|
|
20
|
+
natural_key,
|
|
21
|
+
parse_sheet,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_SKIP = {".git", "node_modules", "__pycache__", ".traceforge", "History"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clean_pin_name(name: str) -> str:
|
|
28
|
+
"""Altium marks overbars with a backslash after each barred character (``R\\S\\T\\``)."""
|
|
29
|
+
out: list[str] = []
|
|
30
|
+
barred: list[str] = []
|
|
31
|
+
i = 0
|
|
32
|
+
while i < len(name):
|
|
33
|
+
ch = name[i]
|
|
34
|
+
if i + 1 < len(name) and name[i + 1] == "\\":
|
|
35
|
+
barred.append(ch)
|
|
36
|
+
i += 2
|
|
37
|
+
continue
|
|
38
|
+
if barred:
|
|
39
|
+
out.append("~{" + "".join(barred) + "}")
|
|
40
|
+
barred = []
|
|
41
|
+
out.append(ch)
|
|
42
|
+
i += 1
|
|
43
|
+
if barred:
|
|
44
|
+
out.append("~{" + "".join(barred) + "}")
|
|
45
|
+
return "".join(out)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _alias(params: dict[str, str], pattern: str) -> str:
|
|
49
|
+
for key, val in params.items():
|
|
50
|
+
if val and re.search(pattern, key, re.I):
|
|
51
|
+
return val
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class AltiumProject:
|
|
57
|
+
root: Path
|
|
58
|
+
prjpcb: Path | None
|
|
59
|
+
sheets: list[AltSheet] = field(default_factory=list)
|
|
60
|
+
pcb: AltPcb | None = None
|
|
61
|
+
netlist: Netlist = field(default_factory=lambda: Netlist({}))
|
|
62
|
+
warnings: list[str] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def name(self) -> str:
|
|
66
|
+
return (self.prjpcb or self.root).stem
|
|
67
|
+
|
|
68
|
+
def components(self) -> dict[str, AltComponent]:
|
|
69
|
+
"""Real parts by designator; parts split over several placed symbols are merged."""
|
|
70
|
+
merged: dict[str, AltComponent] = {}
|
|
71
|
+
for sheet in self.sheets:
|
|
72
|
+
for comp in sheet.components:
|
|
73
|
+
if comp.is_power or not comp.designator.strip("*") or comp.designator.endswith("?"):
|
|
74
|
+
continue
|
|
75
|
+
known = merged.get(comp.designator)
|
|
76
|
+
if known is None:
|
|
77
|
+
merged[comp.designator] = AltComponent(
|
|
78
|
+
**{**comp.__dict__, "pins": list(comp.pins), "params": dict(comp.params)}
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
have = {p.number for p in known.pins}
|
|
82
|
+
known.pins.extend(p for p in comp.pins if p.number not in have)
|
|
83
|
+
return dict(sorted(merged.items(), key=lambda kv: natural_key(kv[0])))
|
|
84
|
+
|
|
85
|
+
def find_pin(self, designator: str, number: str) -> tuple[AltComponent, AltPin] | None:
|
|
86
|
+
comp = self.components().get(designator)
|
|
87
|
+
if comp is None:
|
|
88
|
+
return None
|
|
89
|
+
pin = comp.pin(number)
|
|
90
|
+
if pin is None:
|
|
91
|
+
for p in comp.pins:
|
|
92
|
+
if clean_pin_name(p.name).lower() == number.lower():
|
|
93
|
+
pin = p
|
|
94
|
+
break
|
|
95
|
+
return (comp, pin) if pin else None
|
|
96
|
+
|
|
97
|
+
def net_of(self, designator: str, number: str) -> str | None:
|
|
98
|
+
for name, pins in self.netlist.nets.items():
|
|
99
|
+
if (designator, number) in pins:
|
|
100
|
+
return name
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def summary(self) -> dict:
|
|
104
|
+
comps = self.components()
|
|
105
|
+
out: dict = {
|
|
106
|
+
"project": self.name,
|
|
107
|
+
"prjpcb": self.prjpcb.name if self.prjpcb else None,
|
|
108
|
+
"sheets": [s.name for s in self.sheets],
|
|
109
|
+
"components": len(comps),
|
|
110
|
+
"pins": sum(len(c.pins) for c in comps.values()),
|
|
111
|
+
"nets": len(self.netlist.nets),
|
|
112
|
+
"wires": sum(len(s.wires) for s in self.sheets),
|
|
113
|
+
"net_labels": sum(len(s.net_labels) for s in self.sheets),
|
|
114
|
+
"power_ports": sum(len(s.power_ports) for s in self.sheets),
|
|
115
|
+
"warnings": self.warnings + self.netlist.warnings,
|
|
116
|
+
}
|
|
117
|
+
if self.pcb:
|
|
118
|
+
p = self.pcb
|
|
119
|
+
box = p.outline_bbox()
|
|
120
|
+
out["pcb"] = {
|
|
121
|
+
"file": p.path.name,
|
|
122
|
+
"components": len(p.components),
|
|
123
|
+
"pads": sum(len(c.pads) for c in p.components) + len(p.free_pads),
|
|
124
|
+
"nets": len(p.nets),
|
|
125
|
+
"tracks": len(p.tracks),
|
|
126
|
+
"tracks_per_layer": p.tracks_per_layer(),
|
|
127
|
+
"vias": len(p.vias),
|
|
128
|
+
"layers": p.layers,
|
|
129
|
+
"outline_mm": [round(v, 3) for v in box] if box else None,
|
|
130
|
+
}
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _find(root: Path, suffixes: set[str], depth: int = 4) -> list[Path]:
|
|
135
|
+
found: list[Path] = []
|
|
136
|
+
stack = [(root, 0)]
|
|
137
|
+
while stack:
|
|
138
|
+
d, n = stack.pop()
|
|
139
|
+
try:
|
|
140
|
+
entries = sorted(d.iterdir())
|
|
141
|
+
except OSError:
|
|
142
|
+
continue
|
|
143
|
+
for e in entries:
|
|
144
|
+
if e.is_dir():
|
|
145
|
+
if n < depth and e.name not in _SKIP and not e.name.startswith("."):
|
|
146
|
+
stack.append((e, n + 1))
|
|
147
|
+
elif e.suffix.lower() in suffixes:
|
|
148
|
+
found.append(e)
|
|
149
|
+
return sorted(found, key=lambda p: (len(p.parts), str(p)))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def find_projects(root: str | Path) -> list[Path]:
|
|
153
|
+
return _find(Path(root), {".prjpcb"})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def project_documents(prjpcb: Path) -> list[Path]:
|
|
157
|
+
"""``DocumentPath`` entries of the INI-style ``.PrjPcb``, resolved against its folder."""
|
|
158
|
+
docs: list[Path] = []
|
|
159
|
+
text = prjpcb.read_text(encoding="utf-8", errors="replace")
|
|
160
|
+
for line in text.splitlines():
|
|
161
|
+
if line.lower().startswith("documentpath="):
|
|
162
|
+
rel = line.split("=", 1)[1].strip().replace("\\", "/")
|
|
163
|
+
path = (prjpcb.parent / rel).resolve()
|
|
164
|
+
if path.exists():
|
|
165
|
+
docs.append(path)
|
|
166
|
+
else: # tolerate case differences on case-sensitive systems
|
|
167
|
+
match = next(
|
|
168
|
+
(
|
|
169
|
+
p
|
|
170
|
+
for p in prjpcb.parent.rglob("*")
|
|
171
|
+
if p.name.lower() == Path(rel).name.lower()
|
|
172
|
+
),
|
|
173
|
+
None,
|
|
174
|
+
)
|
|
175
|
+
if match:
|
|
176
|
+
docs.append(match)
|
|
177
|
+
return docs
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_project(path: str | Path) -> AltiumProject:
|
|
181
|
+
"""Load a ``.PrjPcb`` (or a folder / loose ``.SchDoc`` / ``.PcbDoc`` files)."""
|
|
182
|
+
path = Path(path).resolve()
|
|
183
|
+
if not path.exists():
|
|
184
|
+
raise AltiumError(f"{path} does not exist")
|
|
185
|
+
warnings: list[str] = []
|
|
186
|
+
prjpcb: Path | None = None
|
|
187
|
+
if path.is_file() and path.suffix.lower() == ".prjpcb":
|
|
188
|
+
prjpcb = path
|
|
189
|
+
elif path.is_dir():
|
|
190
|
+
found = find_projects(path)
|
|
191
|
+
prjpcb = found[0] if found else None
|
|
192
|
+
root = prjpcb.parent if prjpcb else (path if path.is_dir() else path.parent)
|
|
193
|
+
if prjpcb:
|
|
194
|
+
docs = project_documents(prjpcb)
|
|
195
|
+
elif path.is_file():
|
|
196
|
+
docs = [path]
|
|
197
|
+
else:
|
|
198
|
+
docs = _find(root, {".schdoc", ".pcbdoc"})
|
|
199
|
+
sch_paths = [d for d in docs if d.suffix.lower() == ".schdoc"]
|
|
200
|
+
pcb_paths = [d for d in docs if d.suffix.lower() == ".pcbdoc"]
|
|
201
|
+
if path.is_file() and path.suffix.lower() == ".schdoc":
|
|
202
|
+
pcb_paths = []
|
|
203
|
+
if not sch_paths and not pcb_paths:
|
|
204
|
+
raise AltiumError(f"no .SchDoc or .PcbDoc found for {path}")
|
|
205
|
+
sheets: list[AltSheet] = []
|
|
206
|
+
for p in sch_paths:
|
|
207
|
+
try:
|
|
208
|
+
sheets.append(parse_sheet(p))
|
|
209
|
+
except AltiumError as exc:
|
|
210
|
+
warnings.append(str(exc))
|
|
211
|
+
pcb = None
|
|
212
|
+
if pcb_paths:
|
|
213
|
+
try:
|
|
214
|
+
pcb = parse_pcb(pcb_paths[0])
|
|
215
|
+
except AltiumError as exc:
|
|
216
|
+
warnings.append(str(exc))
|
|
217
|
+
if len(pcb_paths) > 1:
|
|
218
|
+
warnings.append(f"{len(pcb_paths)} boards in the project; read {pcb_paths[0].name}")
|
|
219
|
+
netlist = extract_nets(sheets) if sheets else Netlist({})
|
|
220
|
+
if not sheets and pcb:
|
|
221
|
+
netlist = Netlist(pcb.pad_nets())
|
|
222
|
+
return AltiumProject(root, prjpcb, sheets, pcb, netlist, warnings)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# --- outputs ---------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def bom_rows(project: AltiumProject) -> list[dict[str, str]]:
|
|
229
|
+
"""Altium-style BOM: identical parts grouped, with MPN / LCSC when the parameters have them."""
|
|
230
|
+
groups: dict[tuple, list[AltComponent]] = {}
|
|
231
|
+
for comp in project.components().values():
|
|
232
|
+
mpn = _alias(comp.params, r"^(mpn|manufacturer.?part|mfr.?part|part.?number)")
|
|
233
|
+
lcsc = _alias(comp.params, r"lcsc")
|
|
234
|
+
key = (comp.comment, comp.footprint, comp.lib_ref, comp.description, mpn, lcsc)
|
|
235
|
+
groups.setdefault(key, []).append(comp)
|
|
236
|
+
rows = []
|
|
237
|
+
for (comment, footprint, lib_ref, description, mpn, lcsc), comps in groups.items():
|
|
238
|
+
comps.sort(key=lambda c: natural_key(c.designator))
|
|
239
|
+
rows.append(
|
|
240
|
+
{
|
|
241
|
+
"Comment": comment,
|
|
242
|
+
"Description": description,
|
|
243
|
+
"Designator": ", ".join(c.designator for c in comps),
|
|
244
|
+
"Footprint": footprint,
|
|
245
|
+
"LibRef": lib_ref,
|
|
246
|
+
"Quantity": str(len(comps)),
|
|
247
|
+
"MPN": mpn,
|
|
248
|
+
"LCSC": lcsc,
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
rows.sort(key=lambda r: natural_key(r["Designator"].split(",")[0]))
|
|
252
|
+
return rows
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def bom_csv(project: AltiumProject) -> str:
|
|
256
|
+
rows = bom_rows(project)
|
|
257
|
+
cols = ["Comment", "Description", "Designator", "Footprint", "LibRef", "Quantity"]
|
|
258
|
+
for extra in ("MPN", "LCSC"):
|
|
259
|
+
if any(r[extra] for r in rows):
|
|
260
|
+
cols.append(extra)
|
|
261
|
+
buf = io.StringIO()
|
|
262
|
+
writer = csv.DictWriter(buf, fieldnames=cols, extrasaction="ignore", lineterminator="\n")
|
|
263
|
+
writer.writeheader()
|
|
264
|
+
writer.writerows(rows)
|
|
265
|
+
return buf.getvalue()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _q(text: str) -> str:
|
|
269
|
+
return '"' + str(text).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
_KICAD_PIN_TYPE = {
|
|
273
|
+
"input": "input",
|
|
274
|
+
"output": "output",
|
|
275
|
+
"bidirectional": "bidirectional",
|
|
276
|
+
"passive": "passive",
|
|
277
|
+
"power": "power_in",
|
|
278
|
+
"tri_state": "tri_state",
|
|
279
|
+
"open_collector": "open_collector",
|
|
280
|
+
"open_emitter": "open_emitter",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def kicad_netlist(project: AltiumProject) -> str:
|
|
285
|
+
"""A KiCad netlist (``.net``, export format E) that KiCad's PCB editor and our tools read.
|
|
286
|
+
|
|
287
|
+
Footprints keep their Altium names: KiCad only links them when a footprint of the same
|
|
288
|
+
name exists in its libraries (see docs/altium.md).
|
|
289
|
+
"""
|
|
290
|
+
comps = project.components()
|
|
291
|
+
lines = [
|
|
292
|
+
'(export (version "E")',
|
|
293
|
+
f" (design (source {_q(project.name)}) (date {_q(date.today().isoformat())})",
|
|
294
|
+
' (tool "Traceforge altium"))',
|
|
295
|
+
" (components",
|
|
296
|
+
]
|
|
297
|
+
for ref, c in comps.items():
|
|
298
|
+
lines.append(f" (comp (ref {_q(ref)})")
|
|
299
|
+
lines.append(f" (value {_q(c.comment)})")
|
|
300
|
+
if c.footprint:
|
|
301
|
+
lines.append(f" (footprint {_q(c.footprint)})")
|
|
302
|
+
lines.append(f" (libsource (lib {_q('altium')}) (part {_q(c.lib_ref)})")
|
|
303
|
+
lines.append(f" (description {_q(c.description)}))")
|
|
304
|
+
fields = {k: v for k, v in c.params.items() if not k.startswith("_") and v}
|
|
305
|
+
if fields:
|
|
306
|
+
lines.append(" (fields")
|
|
307
|
+
for k, v in sorted(fields.items()):
|
|
308
|
+
lines.append(f" (field (name {_q(k)}) {_q(v)})")
|
|
309
|
+
lines.append(" )")
|
|
310
|
+
lines.append(" )")
|
|
311
|
+
lines.append(" )")
|
|
312
|
+
lines.append(" (nets")
|
|
313
|
+
for code, (name, pins) in enumerate(project.netlist.nets.items(), 1):
|
|
314
|
+
lines.append(f" (net (code {_q(str(code))}) (name {_q(name)})")
|
|
315
|
+
for ref, number in pins:
|
|
316
|
+
comp = comps.get(ref)
|
|
317
|
+
pin = comp.pin(number) if comp else None
|
|
318
|
+
extra = ""
|
|
319
|
+
if pin:
|
|
320
|
+
extra = (
|
|
321
|
+
f" (pinfunction {_q(clean_pin_name(pin.name))})"
|
|
322
|
+
f" (pintype {_q(_KICAD_PIN_TYPE.get(pin.electrical, 'passive'))})"
|
|
323
|
+
)
|
|
324
|
+
lines.append(f" (node (ref {_q(ref)}) (pin {_q(number)}){extra})")
|
|
325
|
+
lines.append(" )")
|
|
326
|
+
lines.append(" )")
|
|
327
|
+
lines.append(")")
|
|
328
|
+
return "\n".join(lines) + "\n"
|