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.
Files changed (219) hide show
  1. traceforge/__init__.py +3 -0
  2. traceforge/__main__.py +3 -0
  3. traceforge/acp_server.py +570 -0
  4. traceforge/agent.py +680 -0
  5. traceforge/altium/__init__.py +6 -0
  6. traceforge/altium/cfb.py +70 -0
  7. traceforge/altium/cli.py +60 -0
  8. traceforge/altium/model.py +163 -0
  9. traceforge/altium/outputs.py +24 -0
  10. traceforge/altium/pcb.py +301 -0
  11. traceforge/altium/project.py +328 -0
  12. traceforge/altium/schematic.py +400 -0
  13. traceforge/altium/tools.py +166 -0
  14. traceforge/approval.py +219 -0
  15. traceforge/attachments.py +346 -0
  16. traceforge/background.py +253 -0
  17. traceforge/bench/__init__.py +5 -0
  18. traceforge/bench/adapters.py +337 -0
  19. traceforge/bench/checks.py +410 -0
  20. traceforge/bench/cli.py +121 -0
  21. traceforge/bench/design.py +439 -0
  22. traceforge/bench/kicad.py +99 -0
  23. traceforge/bench/partscheck.py +159 -0
  24. traceforge/bench/review.py +100 -0
  25. traceforge/bench/runner.py +226 -0
  26. traceforge/bench/score.py +204 -0
  27. traceforge/bench/tasks.py +102 -0
  28. traceforge/chat.py +1185 -0
  29. traceforge/checks.py +328 -0
  30. traceforge/checks_sides.py +118 -0
  31. traceforge/cli.py +2224 -0
  32. traceforge/clipboard.py +107 -0
  33. traceforge/config.py +299 -0
  34. traceforge/context.py +276 -0
  35. traceforge/design/__init__.py +1 -0
  36. traceforge/design/constraints.py +397 -0
  37. traceforge/design/generate.py +1789 -0
  38. traceforge/design/outline.py +905 -0
  39. traceforge/design/schematic_layout.py +932 -0
  40. traceforge/design/schematic_lint.py +329 -0
  41. traceforge/design/schematic_route.py +207 -0
  42. traceforge/design/sheets.py +854 -0
  43. traceforge/design/sides.py +328 -0
  44. traceforge/design/spec.py +462 -0
  45. traceforge/design/specsheet.py +2351 -0
  46. traceforge/design/specsheet_render.py +183 -0
  47. traceforge/desktop/__init__.py +109 -0
  48. traceforge/desktop/keys.py +90 -0
  49. traceforge/desktop/project.py +443 -0
  50. traceforge/desktop/review.py +20 -0
  51. traceforge/desktop/server.py +1277 -0
  52. traceforge/desktop/static/app.css +223 -0
  53. traceforge/desktop/static/app.js +683 -0
  54. traceforge/desktop/static/highlight.js +77 -0
  55. traceforge/desktop/static/index.html +89 -0
  56. traceforge/desktop/static/md.js +90 -0
  57. traceforge/desktop/static/viewer.js +213 -0
  58. traceforge/desktop/symbols.py +212 -0
  59. traceforge/desktop/update.py +65 -0
  60. traceforge/diagnostics.py +67 -0
  61. traceforge/errata.py +219 -0
  62. traceforge/fab.py +108 -0
  63. traceforge/fab_layers.py +44 -0
  64. traceforge/features/__init__.py +6 -0
  65. traceforge/features/candidates.py +117 -0
  66. traceforge/features/cli.py +200 -0
  67. traceforge/features/package.py +433 -0
  68. traceforge/features/revdiff.py +224 -0
  69. traceforge/features/sarif.py +145 -0
  70. traceforge/features/viewer.py +265 -0
  71. traceforge/firmware/__init__.py +3 -0
  72. traceforge/firmware/cli.py +174 -0
  73. traceforge/firmware/drivers.py +372 -0
  74. traceforge/firmware/drivers_data.py +256 -0
  75. traceforge/firmware/mcus.py +177 -0
  76. traceforge/firmware/netlist.py +55 -0
  77. traceforge/firmware/pinmap.py +296 -0
  78. traceforge/firmware/scaffold.py +466 -0
  79. traceforge/firmware/toolchain.py +262 -0
  80. traceforge/hooks.py +237 -0
  81. traceforge/kicad/__init__.py +0 -0
  82. traceforge/kicad/cli.py +519 -0
  83. traceforge/kicad/diffpair.py +174 -0
  84. traceforge/kicad/edgegeom.py +191 -0
  85. traceforge/kicad/edit.py +405 -0
  86. traceforge/kicad/escape.py +1410 -0
  87. traceforge/kicad/formats.py +1073 -0
  88. traceforge/kicad/impedance.py +320 -0
  89. traceforge/kicad/install.py +75 -0
  90. traceforge/kicad/libraries.py +490 -0
  91. traceforge/kicad/models3d.py +942 -0
  92. traceforge/kicad/outputs.py +81 -0
  93. traceforge/kicad/pcb.py +787 -0
  94. traceforge/kicad/planes.py +1242 -0
  95. traceforge/kicad/project.py +136 -0
  96. traceforge/kicad/route.py +2698 -0
  97. traceforge/kicad/routegeom.py +164 -0
  98. traceforge/kicad/schematic.py +268 -0
  99. traceforge/kicad/sides.py +34 -0
  100. traceforge/kicad/stackup.py +424 -0
  101. traceforge/kicad_plugin/__init__.py +12 -0
  102. traceforge/kicad_plugin/actions.py +92 -0
  103. traceforge/kicad_plugin/cli.py +66 -0
  104. traceforge/kicad_plugin/client.py +86 -0
  105. traceforge/kicad_plugin/context.py +257 -0
  106. traceforge/kicad_plugin/discovery.py +81 -0
  107. traceforge/kicad_plugin/install.py +203 -0
  108. traceforge/kicad_plugin/overlay.py +60 -0
  109. traceforge/kicad_plugin/payload/action_plugin.py +65 -0
  110. traceforge/kicad_plugin/payload/ipc_main.py +42 -0
  111. traceforge/kicad_plugin/payload/plugin.json +90 -0
  112. traceforge/kicad_plugin/payload/requirements.txt +1 -0
  113. traceforge/kicad_plugin/views.py +202 -0
  114. traceforge/langchain.py +57 -0
  115. traceforge/local.py +580 -0
  116. traceforge/mcp_server.py +304 -0
  117. traceforge/models.py +310 -0
  118. traceforge/next_steps.py +110 -0
  119. traceforge/parts/__init__.py +52 -0
  120. traceforge/parts/alternates.py +488 -0
  121. traceforge/parts/bom.py +423 -0
  122. traceforge/parts/brief.py +705 -0
  123. traceforge/parts/catalog.py +297 -0
  124. traceforge/parts/cli_sourcing.py +156 -0
  125. traceforge/parts/data/duty_rates.yaml +37 -0
  126. traceforge/parts/data/hs_codes.yaml +103 -0
  127. traceforge/parts/datasheet.py +278 -0
  128. traceforge/parts/fx.py +100 -0
  129. traceforge/parts/model.py +185 -0
  130. traceforge/parts/net.py +187 -0
  131. traceforge/parts/offers.py +46 -0
  132. traceforge/parts/packages.py +397 -0
  133. traceforge/parts/pinout.py +714 -0
  134. traceforge/parts/providers/__init__.py +17 -0
  135. traceforge/parts/providers/base.py +100 -0
  136. traceforge/parts/providers/jlc.py +364 -0
  137. traceforge/parts/providers/keyed.py +661 -0
  138. traceforge/parts/report.py +508 -0
  139. traceforge/parts/supply.py +330 -0
  140. traceforge/parts/verify.py +455 -0
  141. traceforge/parts/web.py +575 -0
  142. traceforge/plans.py +70 -0
  143. traceforge/preferences.py +127 -0
  144. traceforge/prices.py +130 -0
  145. traceforge/prices.yaml +28 -0
  146. traceforge/prompts/__init__.py +54 -0
  147. traceforge/prompts/system.md +103 -0
  148. traceforge/providers.py +764 -0
  149. traceforge/remote/__init__.py +33 -0
  150. traceforge/remote/client.py +422 -0
  151. traceforge/remote/oidc.py +240 -0
  152. traceforge/remote/orgs.py +510 -0
  153. traceforge/remote/server.py +690 -0
  154. traceforge/remote/store.py +100 -0
  155. traceforge/review_changes.py +187 -0
  156. traceforge/routing.py +99 -0
  157. traceforge/runtime.py +435 -0
  158. traceforge/sessions.py +324 -0
  159. traceforge/sexpr.py +279 -0
  160. traceforge/sim/__init__.py +17 -0
  161. traceforge/sim/acmath.py +234 -0
  162. traceforge/sim/circuit.py +499 -0
  163. traceforge/sim/cli.py +162 -0
  164. traceforge/sim/engine.py +172 -0
  165. traceforge/sim/ltspice.py +175 -0
  166. traceforge/sim/measure.py +123 -0
  167. traceforge/sim/models.py +76 -0
  168. traceforge/sim/project.py +218 -0
  169. traceforge/sim/rawfile.py +123 -0
  170. traceforge/sim/results.py +298 -0
  171. traceforge/sim/tune.py +172 -0
  172. traceforge/skill_commands.py +165 -0
  173. traceforge/skill_library/circuit-simulation/SKILL.md +30 -0
  174. traceforge/skill_library/compliance-review/SKILL.md +51 -0
  175. traceforge/skill_library/design-review/SKILL.md +47 -0
  176. traceforge/skill_library/errata-hunt/SKILL.md +30 -0
  177. traceforge/skill_library/firmware/SKILL.md +26 -0
  178. traceforge/skill_library/power-budget/SKILL.md +27 -0
  179. traceforge/skill_library/release-package/SKILL.md +29 -0
  180. traceforge/skill_library/sourcing-brief/SKILL.md +24 -0
  181. traceforge/skill_library/thermal-review/SKILL.md +15 -0
  182. traceforge/skills.py +131 -0
  183. traceforge/thermal.py +557 -0
  184. traceforge/tools/__init__.py +48 -0
  185. traceforge/tools/agent_tools.py +213 -0
  186. traceforge/tools/base.py +303 -0
  187. traceforge/tools/batch.py +155 -0
  188. traceforge/tools/connections.py +245 -0
  189. traceforge/tools/files.py +777 -0
  190. traceforge/tools/firmware_tools.py +218 -0
  191. traceforge/tools/kicad_tools.py +1072 -0
  192. traceforge/tools/models3d_tools.py +141 -0
  193. traceforge/tools/parts_tools.py +473 -0
  194. traceforge/tools/processes.py +246 -0
  195. traceforge/tools/review_tools.py +178 -0
  196. traceforge/tools/sim_tools.py +230 -0
  197. traceforge/tui.py +620 -0
  198. traceforge/updates.py +124 -0
  199. traceforge/validate/__init__.py +77 -0
  200. traceforge/validate/builtins.py +1091 -0
  201. traceforge/validate/circuits.py +1240 -0
  202. traceforge/validate/config.py +410 -0
  203. traceforge/validate/default.yaml +113 -0
  204. traceforge/validate/design.py +191 -0
  205. traceforge/validate/explain.py +112 -0
  206. traceforge/validate/grounding.py +169 -0
  207. traceforge/validate/ipc2221.py +350 -0
  208. traceforge/validate/multilayer.py +805 -0
  209. traceforge/validate/netmodel.py +261 -0
  210. traceforge/validate/partcheck.py +472 -0
  211. traceforge/validate/report.py +280 -0
  212. traceforge/validate/review.py +322 -0
  213. traceforge/validate/runner.py +662 -0
  214. traceforge/workspace.py +171 -0
  215. traceforge_pcb-1.0.0.dist-info/METADATA +463 -0
  216. traceforge_pcb-1.0.0.dist-info/RECORD +219 -0
  217. traceforge_pcb-1.0.0.dist-info/WHEEL +4 -0
  218. traceforge_pcb-1.0.0.dist-info/entry_points.txt +2 -0
  219. 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"