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,1073 @@
|
|
|
1
|
+
"""KiCad file-format versions: detection, and conversion between KiCad 7, 8, 9 and 10 syntax.
|
|
2
|
+
|
|
3
|
+
Every KiCad file starts with ``(version YYYYMMDD)``. The readers in this package accept
|
|
4
|
+
files from KiCad 6 through 10; this module maps a version stamp to a KiCad release and
|
|
5
|
+
rewrites node trees between the syntax of different releases, so the design compiler
|
|
6
|
+
and the edit tools can write the format of the KiCad the user actually has.
|
|
7
|
+
|
|
8
|
+
What changes between releases (only what traceforge reads or writes):
|
|
9
|
+
|
|
10
|
+
* KiCad 8 (schematic 20231120, board 20240108): ``generator_version``; ``uuid`` instead
|
|
11
|
+
of ``tstamp`` on board items; footprint ``fp_text reference/value`` became
|
|
12
|
+
``property`` fields with their own position, layer and visibility; ``Sheetfile`` /
|
|
13
|
+
``Sheetname`` footprint properties became ``(sheetfile ..)`` / ``(sheetname ..)``;
|
|
14
|
+
hidden text is ``(hide yes)``; booleans are ``yes``/``no``; ``exclude_from_sim`` on
|
|
15
|
+
symbols; ``ki_description`` became the ``Description`` field.
|
|
16
|
+
* KiCad 9 (schematic 20250114, board 20241229): ``(embedded_fonts no)`` and embedded
|
|
17
|
+
files; ``(hide yes)`` also on symbol pins and pin names/numbers; new board layer
|
|
18
|
+
numbering (B.Cu is 2, technical layers odd); component classes; complex padstacks;
|
|
19
|
+
tables; rule areas.
|
|
20
|
+
* KiCad 10 (schematic 20260306, board 20260206): board files no longer store net codes
|
|
21
|
+
(``(net "GND")``); symbol properties carry ``show_name`` / ``do_not_autoplace`` and a
|
|
22
|
+
property-level ``(hide yes)``; ``(power global|local)``; ``in_pos_files`` and
|
|
23
|
+
``(body_style N)`` (was ``convert``) on symbols; ``duplicate_pad_numbers_are_jumpers``
|
|
24
|
+
on footprints; ``(tenting (front yes) (back yes))`` instead of ``(tenting front back)``;
|
|
25
|
+
jumper pad groups; variants; symbol libraries may be directories
|
|
26
|
+
(``Name.kicad_symdir``) of one-symbol files.
|
|
27
|
+
|
|
28
|
+
KiCad opens files written by older releases, never newer ones, so the safe default is
|
|
29
|
+
the version of the KiCad installed on the machine.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from traceforge.sexpr import Node, Sym, find, find_all
|
|
39
|
+
|
|
40
|
+
# The newest file version each release writes. Index by major version.
|
|
41
|
+
SCH_VERSIONS = {6: 20211123, 7: 20230121, 8: 20231120, 9: 20250114, 10: 20260306}
|
|
42
|
+
PCB_VERSIONS = {6: 20211014, 7: 20221018, 8: 20240108, 9: 20241229, 10: 20260206}
|
|
43
|
+
SYM_VERSIONS = {6: 20211014, 7: 20220914, 8: 20231120, 9: 20241209, 10: 20251024}
|
|
44
|
+
|
|
45
|
+
# Releases traceforge can write.
|
|
46
|
+
WRITABLE = (7, 8, 9, 10)
|
|
47
|
+
DEFAULT_WRITE_VERSION = 7
|
|
48
|
+
|
|
49
|
+
# Board files stopped storing net codes from this version (KiCad 10 development).
|
|
50
|
+
PCB_NETNAME_ONLY = 20251028
|
|
51
|
+
# Board files use (uuid ...) for every item from this version (KiCad 8 normalisation).
|
|
52
|
+
PCB_UUID_VERSION = 20231014
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class FileInfo:
|
|
57
|
+
kind: str # schematic | board | footprint | symbol_library | unknown
|
|
58
|
+
version: int
|
|
59
|
+
major: int | None
|
|
60
|
+
generator: str
|
|
61
|
+
generator_version: str
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict:
|
|
64
|
+
return {
|
|
65
|
+
"kind": self.kind,
|
|
66
|
+
"version": self.version,
|
|
67
|
+
"kicad": self.major,
|
|
68
|
+
"generator": self.generator,
|
|
69
|
+
"generator_version": self.generator_version,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_KINDS = {
|
|
74
|
+
"kicad_sch": "schematic",
|
|
75
|
+
"kicad_pcb": "board",
|
|
76
|
+
"footprint": "footprint",
|
|
77
|
+
"module": "footprint",
|
|
78
|
+
"kicad_symbol_lib": "symbol_library",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _table(kind: str) -> dict[int, int]:
|
|
83
|
+
if kind == "schematic":
|
|
84
|
+
return SCH_VERSIONS
|
|
85
|
+
if kind == "symbol_library":
|
|
86
|
+
return SYM_VERSIONS
|
|
87
|
+
return PCB_VERSIONS
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def major_for(kind: str, version: int) -> int | None:
|
|
91
|
+
"""The KiCad release that writes ``version`` for this kind of file.
|
|
92
|
+
|
|
93
|
+
Development versions between two releases map to the later release, which is
|
|
94
|
+
the oldest KiCad that can open them.
|
|
95
|
+
"""
|
|
96
|
+
if not version:
|
|
97
|
+
return None
|
|
98
|
+
table = _table(kind)
|
|
99
|
+
for major in sorted(table):
|
|
100
|
+
if version <= table[major]:
|
|
101
|
+
return major
|
|
102
|
+
return max(table) + 1 # newer than anything we know: a future release
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def version_for(kind: str, major: int) -> int:
|
|
106
|
+
table = _table(kind)
|
|
107
|
+
if major not in table:
|
|
108
|
+
raise ValueError(f"unknown KiCad version {major}; known: {sorted(table)}")
|
|
109
|
+
return table[major]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_HEADER = re.compile(
|
|
113
|
+
r"^\s*\((kicad_sch|kicad_pcb|footprint|module|kicad_symbol_lib)\b"
|
|
114
|
+
r"(?:.*?\(version\s+(\d+)\))?"
|
|
115
|
+
r"(?:.*?\(generator\s+\"?([^\")]*)\"?\))?"
|
|
116
|
+
r"(?:.*?\(generator_version\s+\"?([^\")]*)\"?\))?",
|
|
117
|
+
re.S,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def file_info(path: str | Path) -> FileInfo:
|
|
122
|
+
"""Read the header of a KiCad file without parsing all of it."""
|
|
123
|
+
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
124
|
+
head = handle.read(1500)
|
|
125
|
+
return header_info(head)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def header_info(text: str) -> FileInfo:
|
|
129
|
+
match = _HEADER.match(text)
|
|
130
|
+
if not match:
|
|
131
|
+
return FileInfo("unknown", 0, None, "", "")
|
|
132
|
+
kind = _KINDS[match.group(1)]
|
|
133
|
+
version = int(match.group(2) or 0)
|
|
134
|
+
return FileInfo(
|
|
135
|
+
kind, version, major_for(kind, version), match.group(3) or "", match.group(4) or ""
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def root_info(root: Node) -> FileInfo:
|
|
140
|
+
kind = _KINDS.get(str(root[0]) if root else "", "unknown")
|
|
141
|
+
version_node = find(root, "version")
|
|
142
|
+
version = int(str(version_node[1])) if version_node and len(version_node) > 1 else 0
|
|
143
|
+
generator = find(root, "generator")
|
|
144
|
+
gen_version = find(root, "generator_version")
|
|
145
|
+
return FileInfo(
|
|
146
|
+
kind,
|
|
147
|
+
version,
|
|
148
|
+
major_for(kind, version) if kind != "unknown" else None,
|
|
149
|
+
str(generator[1]) if generator and len(generator) > 1 else "",
|
|
150
|
+
str(gen_version[1]) if gen_version and len(gen_version) > 1 else "",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def installed_major() -> int | None:
|
|
155
|
+
"""Major version of the installed ``kicad-cli``, or None when KiCad is not installed."""
|
|
156
|
+
from traceforge.kicad.cli import KicadCli, KicadCliError
|
|
157
|
+
|
|
158
|
+
cli = KicadCli()
|
|
159
|
+
if not cli.available:
|
|
160
|
+
return None
|
|
161
|
+
try:
|
|
162
|
+
return cli.version()[0]
|
|
163
|
+
except KicadCliError:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def resolve_target(requested: int | str | None) -> int:
|
|
168
|
+
"""Turn ``7``/``8``/``9``/``10``/``"auto"``/None into a version to write.
|
|
169
|
+
|
|
170
|
+
``auto`` (and None) picks the installed KiCad's own format, capped at the newest
|
|
171
|
+
format traceforge writes (newer releases open it); without KiCad it writes the
|
|
172
|
+
KiCad 7 format, which every supported release opens.
|
|
173
|
+
"""
|
|
174
|
+
if requested in (None, "", "auto"):
|
|
175
|
+
installed = installed_major()
|
|
176
|
+
if installed is None:
|
|
177
|
+
return DEFAULT_WRITE_VERSION
|
|
178
|
+
return max(min(installed, max(WRITABLE)), min(WRITABLE))
|
|
179
|
+
try:
|
|
180
|
+
major = int(str(requested).split(".")[0])
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"--kicad-version must be 7, 8, 9, 10 or auto (got {requested!r})"
|
|
184
|
+
) from exc
|
|
185
|
+
if major > max(WRITABLE):
|
|
186
|
+
return max(WRITABLE) # newer releases open the newest format we write
|
|
187
|
+
if major not in WRITABLE:
|
|
188
|
+
raise ValueError(f"traceforge writes KiCad {', '.join(map(str, WRITABLE))} files")
|
|
189
|
+
return major
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ----------------------------------------------------------------------- helpers
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def is_hidden(node: Node) -> bool:
|
|
196
|
+
"""Visibility of a text, field or pin in any KiCad syntax.
|
|
197
|
+
|
|
198
|
+
KiCad 6/7: a bare ``hide`` atom (in the item or in its ``effects``); KiCad 8+:
|
|
199
|
+
``(hide yes)`` in ``effects`` (schematic) or on the item (board, KiCad 10 symbols).
|
|
200
|
+
"""
|
|
201
|
+
for scope in (node, find(node, "effects")):
|
|
202
|
+
if scope is None:
|
|
203
|
+
continue
|
|
204
|
+
if any(isinstance(atom, Sym) and atom == "hide" for atom in scope[1:]):
|
|
205
|
+
return True
|
|
206
|
+
flag = find(scope, "hide")
|
|
207
|
+
if flag is not None:
|
|
208
|
+
return len(flag) == 1 or str(flag[1]) == "yes"
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def flag(node: Node | None, key: str, default: bool = False) -> bool:
|
|
213
|
+
"""``(key yes|no)``, a bare ``(key)``, or a bare ``key`` atom -> bool."""
|
|
214
|
+
if node is None:
|
|
215
|
+
return default
|
|
216
|
+
child = find(node, key)
|
|
217
|
+
if child is not None:
|
|
218
|
+
return len(child) == 1 or str(child[1]) in {"yes", "true"}
|
|
219
|
+
if any(isinstance(atom, Sym) and atom == key for atom in node[1:]):
|
|
220
|
+
return True
|
|
221
|
+
return default
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _remove_flag(node: Node, key: str) -> None:
|
|
225
|
+
node[:] = [
|
|
226
|
+
c
|
|
227
|
+
for i, c in enumerate(node)
|
|
228
|
+
if i == 0
|
|
229
|
+
or not ((isinstance(c, Sym) and c == key) or (isinstance(c, list) and c and c[0] == key))
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _set_hidden(node: Node, hidden: bool, *, style: str, where: str) -> None:
|
|
234
|
+
"""Rewrite visibility of ``node``.
|
|
235
|
+
|
|
236
|
+
``style`` is ``bare`` (``hide`` atom) or ``bool`` (``(hide yes)``); ``where`` is
|
|
237
|
+
``effects`` or ``item``. Visible items carry no flag.
|
|
238
|
+
"""
|
|
239
|
+
effects = find(node, "effects")
|
|
240
|
+
_remove_flag(node, "hide")
|
|
241
|
+
if effects is not None:
|
|
242
|
+
_remove_flag(effects, "hide")
|
|
243
|
+
if not hidden:
|
|
244
|
+
return
|
|
245
|
+
token: Sym | Node = Sym("hide") if style == "bare" else [Sym("hide"), Sym("yes")]
|
|
246
|
+
if where == "effects":
|
|
247
|
+
if effects is None:
|
|
248
|
+
effects = [Sym("effects"), [Sym("font"), [Sym("size"), Sym("1.27"), Sym("1.27")]]]
|
|
249
|
+
node.append(effects)
|
|
250
|
+
effects.append(token)
|
|
251
|
+
return
|
|
252
|
+
# On the item: KiCad puts it after (layer ..) on board text, before effects otherwise.
|
|
253
|
+
anchor = None
|
|
254
|
+
for index, child in enumerate(node):
|
|
255
|
+
if isinstance(child, list) and child and child[0] in {"layer", "unlocked", "at"}:
|
|
256
|
+
anchor = index
|
|
257
|
+
position = anchor + 1 if anchor is not None else len(node)
|
|
258
|
+
node.insert(position, token)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _set_bool(node: Node, key: str, value: bool, *, after: tuple[str, ...] = ()) -> None:
|
|
262
|
+
"""Set ``(key yes|no)``, inserting it after the last child named in ``after``."""
|
|
263
|
+
existing = find(node, key)
|
|
264
|
+
word = Sym("yes" if value else "no")
|
|
265
|
+
if existing is not None:
|
|
266
|
+
existing[1:] = [word]
|
|
267
|
+
return
|
|
268
|
+
position = len(node)
|
|
269
|
+
for index, child in enumerate(node):
|
|
270
|
+
if isinstance(child, list) and child and child[0] in after:
|
|
271
|
+
position = index + 1
|
|
272
|
+
node.insert(position, [Sym(key), word])
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _walk_all(node: Node):
|
|
276
|
+
yield node
|
|
277
|
+
for child in node:
|
|
278
|
+
if isinstance(child, list):
|
|
279
|
+
yield from _walk_all(child)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _strip(node: Node, keys: set[str]) -> None:
|
|
283
|
+
"""Remove every descendant node whose name is in ``keys``."""
|
|
284
|
+
node[:] = [
|
|
285
|
+
c
|
|
286
|
+
for c in node
|
|
287
|
+
if not (isinstance(c, list) and c and isinstance(c[0], str) and c[0] in keys)
|
|
288
|
+
]
|
|
289
|
+
for child in node:
|
|
290
|
+
if isinstance(child, list):
|
|
291
|
+
_strip(child, keys)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _set_header(root: Node, version: int, major: int, generator_name: str | None = None) -> None:
|
|
295
|
+
version_node = find(root, "version")
|
|
296
|
+
if version_node is None:
|
|
297
|
+
version_node = [Sym("version"), Sym("0")]
|
|
298
|
+
root.insert(1, version_node)
|
|
299
|
+
version_node[1:] = [Sym(str(version))]
|
|
300
|
+
generator = find(root, "generator")
|
|
301
|
+
if generator is not None and generator_name:
|
|
302
|
+
generator[1:] = [generator_name]
|
|
303
|
+
_remove_flag(root, "generator_version")
|
|
304
|
+
if major >= 8:
|
|
305
|
+
index = root.index(generator) + 1 if generator is not None else 2
|
|
306
|
+
root.insert(index, [Sym("generator_version"), f"{major}.0"])
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _ensure_trailing(node: Node, key: str, value: str, before: tuple[str, ...] = ()) -> None:
|
|
310
|
+
if find(node, key) is not None:
|
|
311
|
+
return
|
|
312
|
+
position = len(node)
|
|
313
|
+
for index, child in enumerate(node):
|
|
314
|
+
if isinstance(child, list) and child and child[0] in before:
|
|
315
|
+
position = index
|
|
316
|
+
break
|
|
317
|
+
node.insert(position, [Sym(key), Sym(value)])
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# -------------------------------------------------------------------- schematic
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Symbol-level attributes, oldest first, in KiCad's order. ``since`` is the first
|
|
324
|
+
# release that writes them.
|
|
325
|
+
_SYMBOL_ATTRS_SINCE = {
|
|
326
|
+
"exclude_from_sim": 8,
|
|
327
|
+
"in_pos_files": 10,
|
|
328
|
+
"duplicate_pin_numbers_are_jumpers": 10,
|
|
329
|
+
}
|
|
330
|
+
_SCH_ONLY_SINCE = {
|
|
331
|
+
"embedded_fonts": 9,
|
|
332
|
+
"embedded_files": 9,
|
|
333
|
+
"show_name": 10,
|
|
334
|
+
"do_not_autoplace": 10,
|
|
335
|
+
"jumper_pin_groups": 10,
|
|
336
|
+
"body_styles": 10,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def convert_symbol_tree(node: Node, major: int) -> None:
|
|
341
|
+
"""Rewrite a library symbol, a placed symbol, or a whole schematic/library in place."""
|
|
342
|
+
for item in list(_walk_all(node)):
|
|
343
|
+
head = str(item[0]) if item and isinstance(item[0], str) else ""
|
|
344
|
+
if head == "property":
|
|
345
|
+
_convert_sch_property(item, major)
|
|
346
|
+
elif head in {"pin_names", "pin_numbers"}:
|
|
347
|
+
_set_hidden(item, is_hidden(item), style="bool" if major >= 9 else "bare", where="item")
|
|
348
|
+
elif head == "pin" and len(item) > 2 and isinstance(item[1], Sym):
|
|
349
|
+
# library pin: (pin <type> <shape> (at ..) ...)
|
|
350
|
+
hidden = is_hidden(item)
|
|
351
|
+
_remove_flag(item, "hide")
|
|
352
|
+
if hidden:
|
|
353
|
+
token = Sym("hide") if major < 9 else [Sym("hide"), Sym("yes")]
|
|
354
|
+
position = len(item)
|
|
355
|
+
for index, child in enumerate(item):
|
|
356
|
+
if isinstance(child, list) and child and child[0] in {"at", "length"}:
|
|
357
|
+
position = index + 1
|
|
358
|
+
item.insert(position, token)
|
|
359
|
+
elif head in {"label", "global_label", "hierarchical_label", "text", "netclass_flag"}:
|
|
360
|
+
autoplaced = find(item, "fields_autoplaced")
|
|
361
|
+
if autoplaced is not None:
|
|
362
|
+
autoplaced[1:] = [Sym("yes")] if major >= 8 else []
|
|
363
|
+
effects = find(item, "effects")
|
|
364
|
+
if effects is not None:
|
|
365
|
+
_set_hidden(
|
|
366
|
+
item,
|
|
367
|
+
is_hidden(item),
|
|
368
|
+
style="bool" if major >= 8 else "bare",
|
|
369
|
+
where="effects",
|
|
370
|
+
)
|
|
371
|
+
elif head == "symbol" and (find(item, "lib_id") is not None or find(item, "in_bom")):
|
|
372
|
+
autoplaced = find(item, "fields_autoplaced")
|
|
373
|
+
if autoplaced is not None:
|
|
374
|
+
autoplaced[1:] = [Sym("yes")] if major >= 8 else []
|
|
375
|
+
if major >= 8 and find(item, "exclude_from_sim") is None:
|
|
376
|
+
_insert_before(item, [Sym("exclude_from_sim"), Sym("no")], ("in_bom",))
|
|
377
|
+
_convert_body_style(item, major)
|
|
378
|
+
if major >= 10 and find(item, "on_board") is not None:
|
|
379
|
+
_set_bool(
|
|
380
|
+
item, "in_pos_files", flag(item, "in_pos_files", True), after=("on_board",)
|
|
381
|
+
)
|
|
382
|
+
if find(item, "lib_id") is None: # library symbol
|
|
383
|
+
_set_bool(
|
|
384
|
+
item,
|
|
385
|
+
"duplicate_pin_numbers_are_jumpers",
|
|
386
|
+
flag(item, "duplicate_pin_numbers_are_jumpers"),
|
|
387
|
+
after=("in_pos_files",),
|
|
388
|
+
)
|
|
389
|
+
elif head == "power" and major < 10:
|
|
390
|
+
del item[1:] # (power global) -> (power)
|
|
391
|
+
elif head == "power" and len(item) == 1:
|
|
392
|
+
item.append(Sym("global")) # KiCad 10 names the scope of power symbols
|
|
393
|
+
# Attributes newer than the target disappear.
|
|
394
|
+
remove = {k for k, since in {**_SYMBOL_ATTRS_SINCE, **_SCH_ONLY_SINCE}.items() if since > major}
|
|
395
|
+
_strip(node, remove)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _convert_body_style(item: Node, major: int) -> None:
|
|
399
|
+
"""Placed symbols: ``(convert N)`` up to KiCad 9, ``(body_style N)`` from KiCad 10."""
|
|
400
|
+
if find(item, "lib_id") is None:
|
|
401
|
+
return
|
|
402
|
+
old, new = ("convert", "body_style") if major >= 10 else ("body_style", "convert")
|
|
403
|
+
style = find(item, old)
|
|
404
|
+
if style is not None:
|
|
405
|
+
style[0] = Sym(new)
|
|
406
|
+
style = find(item, new)
|
|
407
|
+
if major >= 10 and style is None:
|
|
408
|
+
unit = find(item, "unit")
|
|
409
|
+
index = item.index(unit) + 1 if unit is not None else len(item)
|
|
410
|
+
item.insert(index, [Sym("body_style"), Sym("1")])
|
|
411
|
+
elif major < 10 and style is not None and str(style[1]) == "1":
|
|
412
|
+
item.remove(style) # KiCad 9 omits the default body style
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _insert_before(node: Node, item: Node, before: tuple[str, ...]) -> None:
|
|
416
|
+
for index, child in enumerate(node):
|
|
417
|
+
if isinstance(child, list) and child and child[0] in before:
|
|
418
|
+
node.insert(index, item)
|
|
419
|
+
return
|
|
420
|
+
node.append(item)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _convert_sch_property(item: Node, major: int) -> None:
|
|
424
|
+
if len(item) > 1 and major >= 8 and item[1] == "ki_description":
|
|
425
|
+
item[1] = "Description"
|
|
426
|
+
elif len(item) > 1 and major < 8 and item[1] == "Description":
|
|
427
|
+
item[1] = "ki_description"
|
|
428
|
+
if len(item) > 1 and str(item[1]) in {"Sheetfile", "Sheetname", "Sheet file", "Sheet name"}:
|
|
429
|
+
pass
|
|
430
|
+
hidden = is_hidden(item)
|
|
431
|
+
if major >= 10:
|
|
432
|
+
# KiCad 10: (at) [(hide yes)] (show_name no) (do_not_autoplace no) (effects ...).
|
|
433
|
+
# Flags already on the property keep their place.
|
|
434
|
+
effects = find(item, "effects")
|
|
435
|
+
if effects is not None:
|
|
436
|
+
_remove_flag(effects, "hide")
|
|
437
|
+
if any(isinstance(atom, Sym) and atom == "hide" for atom in item[1:]):
|
|
438
|
+
_remove_flag(item, "hide")
|
|
439
|
+
positioned = find(item, "at") is not None # private ki_* fields have no position
|
|
440
|
+
for key in ("show_name", "do_not_autoplace"):
|
|
441
|
+
existing = find(item, key)
|
|
442
|
+
if existing is not None:
|
|
443
|
+
existing[1:] = [Sym("yes" if flag(item, key) else "no")]
|
|
444
|
+
elif positioned:
|
|
445
|
+
_insert_before(item, [Sym(key), Sym("no")], ("effects",))
|
|
446
|
+
existing = find(item, "hide")
|
|
447
|
+
if existing is not None and hidden:
|
|
448
|
+
existing[1:] = [Sym("yes")]
|
|
449
|
+
elif existing is not None:
|
|
450
|
+
item.remove(existing)
|
|
451
|
+
elif hidden:
|
|
452
|
+
_insert_before(item, [Sym("hide"), Sym("yes")], ("effects",))
|
|
453
|
+
return
|
|
454
|
+
if find(item, "effects") is None and not hidden:
|
|
455
|
+
return
|
|
456
|
+
# KiCad 8/9 keep (hide yes) in effects; KiCad 7 a bare hide in effects.
|
|
457
|
+
_set_hidden(item, hidden, style="bool" if major >= 8 else "bare", where="effects")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def convert_schematic(root: Node, major: int) -> Node:
|
|
461
|
+
"""Rewrite a ``kicad_sch`` tree to the syntax of KiCad ``major`` (7, 8, 9 or 10)."""
|
|
462
|
+
_check_writable(major)
|
|
463
|
+
_set_header(root, SCH_VERSIONS[major], major)
|
|
464
|
+
convert_symbol_tree(root, major)
|
|
465
|
+
if major >= 9:
|
|
466
|
+
for library_symbol in _library_symbols(root):
|
|
467
|
+
_ensure_trailing(library_symbol, "embedded_fonts", "no")
|
|
468
|
+
_ensure_trailing(root, "embedded_fonts", "no")
|
|
469
|
+
return root
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _library_symbols(root: Node) -> list[Node]:
|
|
473
|
+
library = find(root, "lib_symbols")
|
|
474
|
+
if library is None:
|
|
475
|
+
return [s for s in find_all(root, "symbol") if find(s, "lib_id") is None]
|
|
476
|
+
return find_all(library, "symbol")
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def convert_symbol_library(root: Node, major: int) -> Node:
|
|
480
|
+
_check_writable(major)
|
|
481
|
+
_set_header(root, SYM_VERSIONS[major], major)
|
|
482
|
+
convert_symbol_tree(root, major)
|
|
483
|
+
if major >= 9:
|
|
484
|
+
for symbol in find_all(root, "symbol"):
|
|
485
|
+
_ensure_trailing(symbol, "embedded_fonts", "no")
|
|
486
|
+
return root
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _check_writable(major: int) -> None:
|
|
490
|
+
if major not in WRITABLE:
|
|
491
|
+
raise ValueError(f"cannot write KiCad {major} files (supported: {WRITABLE})")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ------------------------------------------------------------------------ board
|
|
495
|
+
|
|
496
|
+
# Layer numbering. KiCad 7/8 number copper 0..31 (B.Cu = 31) and technical layers
|
|
497
|
+
# 32..49; KiCad 9 interleaves them (copper even, B.Cu = 2, technical odd).
|
|
498
|
+
LEGACY_LAYER_IDS = {
|
|
499
|
+
"F.Cu": 0,
|
|
500
|
+
**{f"In{i}.Cu": i for i in range(1, 31)},
|
|
501
|
+
"B.Cu": 31,
|
|
502
|
+
"B.Adhes": 32,
|
|
503
|
+
"F.Adhes": 33,
|
|
504
|
+
"B.Paste": 34,
|
|
505
|
+
"F.Paste": 35,
|
|
506
|
+
"B.SilkS": 36,
|
|
507
|
+
"F.SilkS": 37,
|
|
508
|
+
"B.Mask": 38,
|
|
509
|
+
"F.Mask": 39,
|
|
510
|
+
"Dwgs.User": 40,
|
|
511
|
+
"Cmts.User": 41,
|
|
512
|
+
"Eco1.User": 42,
|
|
513
|
+
"Eco2.User": 43,
|
|
514
|
+
"Edge.Cuts": 44,
|
|
515
|
+
"Margin": 45,
|
|
516
|
+
"B.CrtYd": 46,
|
|
517
|
+
"F.CrtYd": 47,
|
|
518
|
+
"B.Fab": 48,
|
|
519
|
+
"F.Fab": 49,
|
|
520
|
+
**{f"User.{i}": 49 + i for i in range(1, 10)},
|
|
521
|
+
}
|
|
522
|
+
V9_LAYER_IDS = {
|
|
523
|
+
"F.Cu": 0,
|
|
524
|
+
"B.Cu": 2,
|
|
525
|
+
**{f"In{i}.Cu": 2 + 2 * i for i in range(1, 31)},
|
|
526
|
+
"F.Mask": 1,
|
|
527
|
+
"B.Mask": 3,
|
|
528
|
+
"F.SilkS": 5,
|
|
529
|
+
"B.SilkS": 7,
|
|
530
|
+
"F.Adhes": 9,
|
|
531
|
+
"B.Adhes": 11,
|
|
532
|
+
"F.Paste": 13,
|
|
533
|
+
"B.Paste": 15,
|
|
534
|
+
"Dwgs.User": 17,
|
|
535
|
+
"Cmts.User": 19,
|
|
536
|
+
"Eco1.User": 21,
|
|
537
|
+
"Eco2.User": 23,
|
|
538
|
+
"Edge.Cuts": 25,
|
|
539
|
+
"Margin": 27,
|
|
540
|
+
"B.CrtYd": 29,
|
|
541
|
+
"F.CrtYd": 31,
|
|
542
|
+
"B.Fab": 33,
|
|
543
|
+
"F.Fab": 35,
|
|
544
|
+
**{f"User.{i}": 37 + 2 * i for i in range(1, 46)},
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
# Layers plotted by default in generated boards: copper, paste, silk, mask, outline.
|
|
548
|
+
_PLOT_LAYERS = ("Cu", "F.Paste", "B.Paste", "F.SilkS", "B.SilkS", "F.Mask", "B.Mask", "Edge.Cuts")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def layer_selection(layers: list[str], major: int) -> str:
|
|
552
|
+
"""The ``layerselection`` mask KiCad writes in ``pcbplotparams``."""
|
|
553
|
+
ids = V9_LAYER_IDS if major >= 9 else LEGACY_LAYER_IDS
|
|
554
|
+
mask = 0
|
|
555
|
+
for name in layers:
|
|
556
|
+
if name in ids:
|
|
557
|
+
mask |= 1 << ids[name]
|
|
558
|
+
if major >= 9:
|
|
559
|
+
words = [(mask >> (32 * i)) & 0xFFFFFFFF for i in range(3, -1, -1)]
|
|
560
|
+
return "0x" + "_".join(f"{w:08x}" for w in words)
|
|
561
|
+
return f"0x{mask >> 32:07x}_{mask & 0xFFFFFFFF:08x}"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def default_plot_layers(copper: list[str]) -> list[str]:
|
|
565
|
+
return [*copper, *(name for name in _PLOT_LAYERS if name != "Cu")]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
_PCB_ONLY_SINCE = {
|
|
569
|
+
"generator_version": 8,
|
|
570
|
+
"legacy_teardrops": 8,
|
|
571
|
+
"embedded_fonts": 9,
|
|
572
|
+
"embedded_files": 9,
|
|
573
|
+
"component_classes": 9,
|
|
574
|
+
"tenting": 9,
|
|
575
|
+
"padstack": 9,
|
|
576
|
+
"duplicate_pad_numbers_are_jumpers": 10,
|
|
577
|
+
"jumper_pad_groups": 10,
|
|
578
|
+
"variants": 10,
|
|
579
|
+
"variant": 10,
|
|
580
|
+
"covering": 10,
|
|
581
|
+
"plugging": 10,
|
|
582
|
+
"capping": 10,
|
|
583
|
+
"filling": 10,
|
|
584
|
+
}
|
|
585
|
+
# (units ...) is also a dimension format field, so it is only removed from footprints.
|
|
586
|
+
_FOOTPRINT_ONLY_SINCE = {"units": 10, "point": 10}
|
|
587
|
+
_MODEL_DIR = re.compile(r"\$\{KICAD(\d+)_3DMODEL_DIR\}")
|
|
588
|
+
_PLOT_BOOLEANS = {"true": "yes", "false": "no"}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def convert_board(root: Node, major: int) -> Node:
|
|
592
|
+
"""Rewrite a ``kicad_pcb`` tree to the syntax of KiCad ``major`` (7, 8, 9 or 10)."""
|
|
593
|
+
_check_writable(major)
|
|
594
|
+
source = root_info(root)
|
|
595
|
+
_set_header(root, PCB_VERSIONS[major], major)
|
|
596
|
+
general = find(root, "general")
|
|
597
|
+
if general is not None:
|
|
598
|
+
if major >= 8:
|
|
599
|
+
_set_bool(general, "legacy_teardrops", False, after=("thickness",))
|
|
600
|
+
layers = find(root, "layers")
|
|
601
|
+
if layers is not None:
|
|
602
|
+
ids = V9_LAYER_IDS if major >= 9 else LEGACY_LAYER_IDS
|
|
603
|
+
for entry in layers[1:]:
|
|
604
|
+
if isinstance(entry, list) and len(entry) > 1 and str(entry[1]) in ids:
|
|
605
|
+
entry[0] = Sym(str(ids[str(entry[1])]))
|
|
606
|
+
setup = find(root, "setup")
|
|
607
|
+
if setup is not None:
|
|
608
|
+
_convert_tenting(setup, major)
|
|
609
|
+
params = find(setup, "pcbplotparams") if setup is not None else None
|
|
610
|
+
if params is not None:
|
|
611
|
+
for item in params[1:]:
|
|
612
|
+
if (
|
|
613
|
+
isinstance(item, list)
|
|
614
|
+
and len(item) == 2
|
|
615
|
+
and str(item[1]) in {"true", "false", "yes", "no"}
|
|
616
|
+
):
|
|
617
|
+
word = str(item[1])
|
|
618
|
+
if major >= 8:
|
|
619
|
+
item[1] = Sym(_PLOT_BOOLEANS.get(word, word))
|
|
620
|
+
else:
|
|
621
|
+
item[1] = Sym({"yes": "true", "no": "false"}.get(word, word))
|
|
622
|
+
selection = find(params, "layerselection")
|
|
623
|
+
renumbered = ((source.major or 7) >= 9) != (major >= 9)
|
|
624
|
+
if selection is not None and layers is not None and renumbered:
|
|
625
|
+
copper = [
|
|
626
|
+
str(e[1])
|
|
627
|
+
for e in layers[1:]
|
|
628
|
+
if isinstance(e, list) and len(e) > 1 and str(e[1]).endswith(".Cu")
|
|
629
|
+
]
|
|
630
|
+
selection[1] = Sym(layer_selection(default_plot_layers(copper), major))
|
|
631
|
+
on_all = find(params, "plot_on_all_layers_selection")
|
|
632
|
+
if on_all is not None and renumbered:
|
|
633
|
+
on_all[1] = Sym(layer_selection([], major))
|
|
634
|
+
for footprint in [*find_all(root, "footprint"), *find_all(root, "module")]:
|
|
635
|
+
convert_footprint(footprint, major, standalone=False)
|
|
636
|
+
_convert_board_items(root, major)
|
|
637
|
+
_convert_nets(root, major, source.version)
|
|
638
|
+
remove = {k for k, since in _PCB_ONLY_SINCE.items() if since > major}
|
|
639
|
+
_strip(root, remove)
|
|
640
|
+
if major < 10: # KiCad 10 board points
|
|
641
|
+
root[:] = [c for c in root if not (isinstance(c, list) and c and c[0] == "point")]
|
|
642
|
+
if major >= 9:
|
|
643
|
+
_ensure_trailing(root, "embedded_fonts", "no")
|
|
644
|
+
return root
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
_SHAPE_FILL_V9 = {"none": "no", "solid": "yes"}
|
|
648
|
+
_SHAPE_FILL_LEGACY = {"no": "none", "yes": "solid"}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _convert_tenting(setup: Node, major: int) -> None:
|
|
652
|
+
"""Via tenting: ``(tenting front back)`` in KiCad 9, ``(tenting (front yes) (back yes))``
|
|
653
|
+
in KiCad 10, which also adds covering/plugging/capping/filling (left at defaults)."""
|
|
654
|
+
tenting = find(setup, "tenting")
|
|
655
|
+
if tenting is None:
|
|
656
|
+
return
|
|
657
|
+
sides = {}
|
|
658
|
+
for side in ("front", "back"):
|
|
659
|
+
if any(isinstance(a, Sym) and a == side for a in tenting[1:]):
|
|
660
|
+
sides[side] = True
|
|
661
|
+
else:
|
|
662
|
+
sides[side] = flag(tenting, side)
|
|
663
|
+
if major >= 10:
|
|
664
|
+
tenting[1:] = [[Sym(side), Sym("yes" if on else "no")] for side, on in sides.items()]
|
|
665
|
+
else:
|
|
666
|
+
tenting[1:] = [Sym(side) for side, on in sides.items() if on] or [Sym("none")]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _convert_shape_fill(item: Node, major: int) -> None:
|
|
670
|
+
"""Graphic shapes: ``(fill none|solid)`` up to KiCad 8, ``(fill no|yes)`` from KiCad 9."""
|
|
671
|
+
fill = find(item, "fill")
|
|
672
|
+
if fill is None or len(fill) != 2 or isinstance(fill[1], list):
|
|
673
|
+
return
|
|
674
|
+
mapping = _SHAPE_FILL_V9 if major >= 9 else _SHAPE_FILL_LEGACY
|
|
675
|
+
fill[1] = Sym(mapping.get(str(fill[1]), str(fill[1])))
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _convert_board_items(root: Node, major: int) -> None:
|
|
679
|
+
stamp_from, stamp_to = ("tstamp", "uuid") if major >= 8 else ("uuid", "tstamp")
|
|
680
|
+
for item in _walk_all(root):
|
|
681
|
+
if not item or not isinstance(item[0], str):
|
|
682
|
+
continue
|
|
683
|
+
head = str(item[0])
|
|
684
|
+
if head.startswith(("gr_", "fp_")):
|
|
685
|
+
_convert_shape_fill(item, major)
|
|
686
|
+
if head in {"footprint", "module", "kicad_pcb"}:
|
|
687
|
+
continue
|
|
688
|
+
# Top-level and nested item stamps (segments, vias, zones, gr_*, groups...).
|
|
689
|
+
for child in item:
|
|
690
|
+
if isinstance(child, list) and child and child[0] == stamp_from and len(child) > 1:
|
|
691
|
+
child[0] = Sym(stamp_to)
|
|
692
|
+
child[1] = str(child[1])
|
|
693
|
+
if head in {"gr_text", "gr_text_box"}:
|
|
694
|
+
_set_hidden(item, is_hidden(item), style="bool" if major >= 8 else "bare", where="item")
|
|
695
|
+
if head in {"via", "pad"}:
|
|
696
|
+
for key in ("remove_unused_layers", "keep_end_layers", "free", "locked"):
|
|
697
|
+
_normalise_flag(item, key, major)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _normalise_flag(node: Node, key: str, major: int) -> None:
|
|
701
|
+
"""Bare ``key`` / ``(key)`` (KiCad 7) <-> ``(key yes)`` (KiCad 8+). Absent stays absent."""
|
|
702
|
+
present_bare = any(isinstance(a, Sym) and a == key for a in node[1:])
|
|
703
|
+
child = find(node, key)
|
|
704
|
+
if not present_bare and child is None:
|
|
705
|
+
return
|
|
706
|
+
value = present_bare or child is None or len(child) == 1 or str(child[1]) == "yes"
|
|
707
|
+
if major >= 8 and child is not None and not present_bare:
|
|
708
|
+
child[1:] = [Sym("yes" if value else "no")] # keep its position
|
|
709
|
+
return
|
|
710
|
+
_remove_flag(node, key)
|
|
711
|
+
if major >= 8:
|
|
712
|
+
node.append([Sym(key), Sym("yes" if value else "no")])
|
|
713
|
+
elif value:
|
|
714
|
+
if key in {"remove_unused_layers", "keep_end_layers"}:
|
|
715
|
+
node.append([Sym(key)])
|
|
716
|
+
else:
|
|
717
|
+
node.append(Sym(key))
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _convert_nets(root: Node, major: int, source_version: int) -> None:
|
|
721
|
+
"""KiCad 10 boards store net names only; KiCad 7-9 need a net table and codes."""
|
|
722
|
+
if major >= 10:
|
|
723
|
+
_nets_by_name(root)
|
|
724
|
+
return
|
|
725
|
+
table = [n for n in find_all(root, "net") if len(n) >= 3]
|
|
726
|
+
codes = {str(n[2]): int(str(n[1])) for n in table}
|
|
727
|
+
names_in_use: list[str] = []
|
|
728
|
+
for item in _walk_all(root):
|
|
729
|
+
if item and item[0] in {"pad", "segment", "arc", "via", "zone"}:
|
|
730
|
+
net = find(item, "net")
|
|
731
|
+
if net is not None and len(net) >= 2 and not isinstance(net[1], Sym):
|
|
732
|
+
names_in_use.append(str(net[1]))
|
|
733
|
+
if not names_in_use and table:
|
|
734
|
+
return
|
|
735
|
+
if not table:
|
|
736
|
+
codes = {"": 0}
|
|
737
|
+
for name in names_in_use:
|
|
738
|
+
if name not in codes:
|
|
739
|
+
codes[name] = max(codes.values(), default=0) + 1
|
|
740
|
+
if not table:
|
|
741
|
+
# Insert the net table where KiCad keeps it: after setup, before items.
|
|
742
|
+
index = len(root)
|
|
743
|
+
for i, child in enumerate(root):
|
|
744
|
+
if (
|
|
745
|
+
isinstance(child, list)
|
|
746
|
+
and child
|
|
747
|
+
and child[0]
|
|
748
|
+
in {
|
|
749
|
+
"net",
|
|
750
|
+
"footprint",
|
|
751
|
+
"module",
|
|
752
|
+
"gr_line",
|
|
753
|
+
"gr_rect",
|
|
754
|
+
"gr_arc",
|
|
755
|
+
"gr_circle",
|
|
756
|
+
"gr_poly",
|
|
757
|
+
"gr_text",
|
|
758
|
+
"segment",
|
|
759
|
+
"via",
|
|
760
|
+
"zone",
|
|
761
|
+
}
|
|
762
|
+
):
|
|
763
|
+
index = i
|
|
764
|
+
break
|
|
765
|
+
for offset, (name, code) in enumerate(sorted(codes.items(), key=lambda kv: kv[1])):
|
|
766
|
+
root.insert(index + offset, [Sym("net"), Sym(str(code)), name])
|
|
767
|
+
for item in _walk_all(root):
|
|
768
|
+
if not item or item[0] not in {"pad", "segment", "arc", "via", "zone"}:
|
|
769
|
+
continue
|
|
770
|
+
net = find(item, "net")
|
|
771
|
+
if net is None or len(net) < 2 or isinstance(net[1], Sym):
|
|
772
|
+
continue
|
|
773
|
+
name = str(net[1])
|
|
774
|
+
if item[0] == "pad":
|
|
775
|
+
net[1:] = [Sym(str(codes[name])), name]
|
|
776
|
+
else:
|
|
777
|
+
net[1:] = [Sym(str(codes[name]))]
|
|
778
|
+
if item[0] == "zone" and find(item, "net_name") is None:
|
|
779
|
+
item.insert(item.index(net) + 1, [Sym("net_name"), name])
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _nets_by_name(root: Node) -> None:
|
|
783
|
+
"""Drop the net table and refer to nets by name, as KiCad 10 writes boards."""
|
|
784
|
+
names = {str(n[1]): str(n[2]) for n in find_all(root, "net") if len(n) >= 3}
|
|
785
|
+
root[:] = [c for c in root if not (isinstance(c, list) and c and c[0] == "net")]
|
|
786
|
+
for item in _walk_all(root):
|
|
787
|
+
if not item or item[0] not in {"pad", "segment", "arc", "via", "zone"}:
|
|
788
|
+
continue
|
|
789
|
+
net = find(item, "net")
|
|
790
|
+
if net is None:
|
|
791
|
+
continue
|
|
792
|
+
if len(net) >= 3: # pad: (net code "name")
|
|
793
|
+
name = str(net[2])
|
|
794
|
+
elif len(net) == 2 and isinstance(net[1], Sym): # (net code)
|
|
795
|
+
name = names.get(str(net[1]), "")
|
|
796
|
+
net_name = find(item, "net_name")
|
|
797
|
+
if net_name is not None and len(net_name) > 1:
|
|
798
|
+
name = name or str(net_name[1])
|
|
799
|
+
else:
|
|
800
|
+
name = str(net[1]) if len(net) == 2 else ""
|
|
801
|
+
_remove_flag(item, "net_name")
|
|
802
|
+
if name:
|
|
803
|
+
net[1:] = [name]
|
|
804
|
+
else:
|
|
805
|
+
item.remove(net) # unconnected
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def convert_footprint(node: Node, major: int, *, standalone: bool = True) -> Node:
|
|
809
|
+
"""Rewrite one footprint (a library ``.kicad_mod`` or a footprint on a board)."""
|
|
810
|
+
if standalone:
|
|
811
|
+
_check_writable(major)
|
|
812
|
+
_set_header(node, PCB_VERSIONS[major], major)
|
|
813
|
+
stamp_from, stamp_to = ("tstamp", "uuid") if major >= 8 else ("uuid", "tstamp")
|
|
814
|
+
if not standalone:
|
|
815
|
+
for child in node:
|
|
816
|
+
if isinstance(child, list) and child and child[0] == stamp_from and len(child) > 1:
|
|
817
|
+
child[0] = Sym(stamp_to)
|
|
818
|
+
layer = str(find(node, "layer")[1]) if find(node, "layer") else "F.Cu"
|
|
819
|
+
fab = "B.Fab" if layer.startswith("B.") else "F.Fab"
|
|
820
|
+
new: Node = [node[0]]
|
|
821
|
+
sheet_values: dict[str, str] = {}
|
|
822
|
+
locked = False
|
|
823
|
+
for child in node[1:]:
|
|
824
|
+
if isinstance(child, Sym) and child in {"locked", "placed"}:
|
|
825
|
+
locked = locked or child == "locked"
|
|
826
|
+
if major < 8:
|
|
827
|
+
new.append(child)
|
|
828
|
+
elif child == "placed":
|
|
829
|
+
new.append([Sym("placed"), Sym("yes")])
|
|
830
|
+
continue
|
|
831
|
+
if not isinstance(child, list) or not child:
|
|
832
|
+
new.append(child)
|
|
833
|
+
continue
|
|
834
|
+
head = str(child[0])
|
|
835
|
+
if head in {"locked", "placed"}:
|
|
836
|
+
if major >= 8:
|
|
837
|
+
new.append(child if len(child) > 1 else [Sym(head), Sym("yes")])
|
|
838
|
+
elif len(child) == 1 or str(child[1]) == "yes":
|
|
839
|
+
new.append(Sym(head))
|
|
840
|
+
continue
|
|
841
|
+
if head in {"sheetname", "sheetfile"} and len(child) > 1:
|
|
842
|
+
sheet_values[head] = str(child[1])
|
|
843
|
+
continue
|
|
844
|
+
if head == "property" and len(child) > 2 and str(child[1]) in {"Sheetfile", "Sheetname"}:
|
|
845
|
+
sheet_values["sheet" + str(child[1])[5:].lower()] = str(child[2])
|
|
846
|
+
continue
|
|
847
|
+
if head == "fp_text" and len(child) > 2 and str(child[1]) in {"reference", "value"}:
|
|
848
|
+
child = _fp_text_to_property(child) if major >= 8 else child
|
|
849
|
+
elif head == "property" and len(child) > 2 and str(child[1]) in {"Reference", "Value"}:
|
|
850
|
+
child = child if major >= 8 else _property_to_fp_text(child)
|
|
851
|
+
elif head == "property" and len(child) > 2:
|
|
852
|
+
child = _footprint_field(child, major, fab)
|
|
853
|
+
if isinstance(child, list) and child and child[0] in {"fp_text", "property"}:
|
|
854
|
+
_set_hidden(
|
|
855
|
+
child, is_hidden(child), style="bool" if major >= 8 else "bare", where="item"
|
|
856
|
+
)
|
|
857
|
+
stamp = find(child, stamp_from)
|
|
858
|
+
if stamp is not None:
|
|
859
|
+
stamp[0] = Sym(stamp_to)
|
|
860
|
+
new.append(child)
|
|
861
|
+
if sheet_values:
|
|
862
|
+
position = _after_fields(new)
|
|
863
|
+
if major >= 8:
|
|
864
|
+
entries = [
|
|
865
|
+
[Sym(key), sheet_values[key]]
|
|
866
|
+
for key in ("sheetname", "sheetfile")
|
|
867
|
+
if key in sheet_values
|
|
868
|
+
]
|
|
869
|
+
else:
|
|
870
|
+
entries = [
|
|
871
|
+
[Sym("property"), "Sheet" + key[5:], sheet_values[key]]
|
|
872
|
+
for key in ("sheetfile", "sheetname")
|
|
873
|
+
if key in sheet_values
|
|
874
|
+
]
|
|
875
|
+
path_index = next(
|
|
876
|
+
(i for i, c in enumerate(new) if isinstance(c, list) and c and c[0] == "path"), None
|
|
877
|
+
)
|
|
878
|
+
if path_index is not None:
|
|
879
|
+
position = path_index + 1 if major >= 8 else path_index
|
|
880
|
+
new[position:position] = entries
|
|
881
|
+
if locked and major >= 8 and find(new, "locked") is None:
|
|
882
|
+
new.insert(2, [Sym("locked"), Sym("yes")])
|
|
883
|
+
node[:] = new
|
|
884
|
+
for item in _walk_all(node):
|
|
885
|
+
if item and item[0] in {
|
|
886
|
+
"fp_line",
|
|
887
|
+
"fp_rect",
|
|
888
|
+
"fp_circle",
|
|
889
|
+
"fp_arc",
|
|
890
|
+
"fp_poly",
|
|
891
|
+
"pad",
|
|
892
|
+
"fp_text_box",
|
|
893
|
+
"group",
|
|
894
|
+
"zone",
|
|
895
|
+
"dimension",
|
|
896
|
+
}:
|
|
897
|
+
for child in item:
|
|
898
|
+
if isinstance(child, list) and child and child[0] == stamp_from and len(child) > 1:
|
|
899
|
+
child[0] = Sym(stamp_to)
|
|
900
|
+
if item and item[0] == "pad":
|
|
901
|
+
for key in ("remove_unused_layers", "keep_end_layers", "locked"):
|
|
902
|
+
_normalise_flag(item, key, major)
|
|
903
|
+
if item and str(item[0]).startswith("fp_"):
|
|
904
|
+
_convert_shape_fill(item, major)
|
|
905
|
+
remove = {
|
|
906
|
+
k for k, since in {**_PCB_ONLY_SINCE, **_FOOTPRINT_ONLY_SINCE}.items() if since > major
|
|
907
|
+
}
|
|
908
|
+
node[:] = [
|
|
909
|
+
c
|
|
910
|
+
for c in node
|
|
911
|
+
if not (isinstance(c, list) and c and isinstance(c[0], str) and c[0] in remove)
|
|
912
|
+
]
|
|
913
|
+
for child in node:
|
|
914
|
+
if isinstance(child, list):
|
|
915
|
+
_strip(child, {k for k in remove if k not in _FOOTPRINT_ONLY_SINCE})
|
|
916
|
+
for model in find_all(node, "model"):
|
|
917
|
+
if len(model) > 1 and isinstance(model[1], str):
|
|
918
|
+
# Library 3D-model paths name the release: ${KICAD9_3DMODEL_DIR}.
|
|
919
|
+
model[1] = _MODEL_DIR.sub(
|
|
920
|
+
lambda m: (
|
|
921
|
+
f"${{KICAD{min(int(m.group(1)), major)}_3DMODEL_DIR}}"
|
|
922
|
+
if int(m.group(1)) > major
|
|
923
|
+
else m.group(0)
|
|
924
|
+
),
|
|
925
|
+
str(model[1]),
|
|
926
|
+
)
|
|
927
|
+
if major >= 10 and find(node, "duplicate_pad_numbers_are_jumpers") is None:
|
|
928
|
+
attr = find(node, "attr")
|
|
929
|
+
index = node.index(attr) + 1 if attr is not None else _after_fields(node)
|
|
930
|
+
node.insert(index, [Sym("duplicate_pad_numbers_are_jumpers"), Sym("no")])
|
|
931
|
+
if major >= 9:
|
|
932
|
+
_ensure_trailing(node, "embedded_fonts", "no", before=("model",))
|
|
933
|
+
return node
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _after_fields(node: Node) -> int:
|
|
937
|
+
position = len(node)
|
|
938
|
+
for index, child in enumerate(node):
|
|
939
|
+
if isinstance(child, list) and child and child[0] in {"property", "fp_text", "path"}:
|
|
940
|
+
position = index + 1
|
|
941
|
+
if isinstance(child, list) and child and child[0] in {"attr", "fp_line", "pad"}:
|
|
942
|
+
break
|
|
943
|
+
return position
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _fp_text_to_property(item: Node) -> Node:
|
|
947
|
+
kind = "Reference" if item[1] == "reference" else "Value"
|
|
948
|
+
hidden = is_hidden(item)
|
|
949
|
+
rest = [c for c in item[3:] if not (isinstance(c, Sym) and c == "hide")]
|
|
950
|
+
node: Node = [Sym("property"), kind, item[2], *rest]
|
|
951
|
+
_set_hidden(node, hidden, style="bool", where="item")
|
|
952
|
+
return node
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _property_to_fp_text(item: Node) -> Node:
|
|
956
|
+
kind = "reference" if item[1] == "Reference" else "value"
|
|
957
|
+
hidden = is_hidden(item)
|
|
958
|
+
rest = [
|
|
959
|
+
c
|
|
960
|
+
for c in item[3:]
|
|
961
|
+
if not (
|
|
962
|
+
isinstance(c, list)
|
|
963
|
+
and c
|
|
964
|
+
and c[0] in {"hide", "unlocked", "show_name", "do_not_autoplace"}
|
|
965
|
+
)
|
|
966
|
+
]
|
|
967
|
+
node: Node = [Sym("fp_text"), Sym(kind), item[2], *rest]
|
|
968
|
+
_set_hidden(node, hidden, style="bare", where="item")
|
|
969
|
+
return node
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _footprint_field(item: Node, major: int, fab_layer: str) -> Node:
|
|
973
|
+
"""User fields (LCSC, MPN, Datasheet...) on a footprint.
|
|
974
|
+
|
|
975
|
+
KiCad 7 stores them as ``(property "key" "value")``. KiCad 8 made them real text
|
|
976
|
+
fields with position, layer and visibility; private ``ki_*`` keys keep the short form.
|
|
977
|
+
"""
|
|
978
|
+
key = str(item[1])
|
|
979
|
+
short = not any(isinstance(c, list) for c in item[3:])
|
|
980
|
+
if major < 8:
|
|
981
|
+
return [Sym("property"), item[1], item[2]]
|
|
982
|
+
if not short or key.startswith("ki_"):
|
|
983
|
+
return item
|
|
984
|
+
return [
|
|
985
|
+
Sym("property"),
|
|
986
|
+
item[1],
|
|
987
|
+
item[2],
|
|
988
|
+
[Sym("at"), Sym("0"), Sym("0"), Sym("0")],
|
|
989
|
+
[Sym("unlocked"), Sym("yes")],
|
|
990
|
+
[Sym("layer"), fab_layer],
|
|
991
|
+
[Sym("hide"), Sym("yes")],
|
|
992
|
+
[Sym("effects"), [Sym("font"), [Sym("size"), Sym("1.27"), Sym("1.27")]]],
|
|
993
|
+
]
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
# ------------------------------------------------------------------------- entry
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def convert(root: Node, major: int) -> Node:
|
|
1000
|
+
"""Convert any supported KiCad tree in place; returns it for chaining."""
|
|
1001
|
+
head = str(root[0]) if root else ""
|
|
1002
|
+
if head == "kicad_sch":
|
|
1003
|
+
return convert_schematic(root, major)
|
|
1004
|
+
if head == "kicad_pcb":
|
|
1005
|
+
return convert_board(root, major)
|
|
1006
|
+
if head in {"footprint", "module"}:
|
|
1007
|
+
return convert_footprint(root, major)
|
|
1008
|
+
if head == "kicad_symbol_lib":
|
|
1009
|
+
return convert_symbol_library(root, major)
|
|
1010
|
+
raise ValueError(f"not a KiCad file tree: {head!r}")
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def board_net_node(root: Node, name: str, *, pad: bool = False) -> Node:
|
|
1014
|
+
"""The ``(net ...)`` child for a new pad, track, via or zone on this board.
|
|
1015
|
+
|
|
1016
|
+
KiCad 10 boards refer to nets by name; older boards by code (pads also repeat
|
|
1017
|
+
the name). Raises KeyError when a code-based board has no such net.
|
|
1018
|
+
"""
|
|
1019
|
+
info = root_info(root)
|
|
1020
|
+
if info.version >= PCB_NETNAME_ONLY and not any(len(n) >= 3 for n in find_all(root, "net")):
|
|
1021
|
+
return [Sym("net"), name]
|
|
1022
|
+
codes = {str(n[2]): str(n[1]) for n in find_all(root, "net") if len(n) >= 3}
|
|
1023
|
+
if name not in codes:
|
|
1024
|
+
raise KeyError(name)
|
|
1025
|
+
return [Sym("net"), Sym(codes[name]), name] if pad else [Sym("net"), Sym(codes[name])]
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def board_net_names(root: Node) -> list[str]:
|
|
1029
|
+
"""Every net name on a board, from the net table or (KiCad 10) from the items."""
|
|
1030
|
+
names = {str(n[2]) for n in find_all(root, "net") if len(n) >= 3}
|
|
1031
|
+
if not names:
|
|
1032
|
+
for item in _walk_all(root):
|
|
1033
|
+
if item and item[0] in {"pad", "segment", "arc", "via", "zone"}:
|
|
1034
|
+
net = find(item, "net")
|
|
1035
|
+
if net is not None and len(net) >= 2 and not isinstance(net[1], Sym):
|
|
1036
|
+
names.add(str(net[1]))
|
|
1037
|
+
names.discard("")
|
|
1038
|
+
return sorted(names)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def stamp_key(root: Node) -> str:
|
|
1042
|
+
"""``uuid`` (KiCad 8+) or ``tstamp`` (KiCad 6/7) for new board items."""
|
|
1043
|
+
return "uuid" if root_info(root).version >= PCB_UUID_VERSION else "tstamp"
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
__all__ = [
|
|
1047
|
+
"DEFAULT_WRITE_VERSION",
|
|
1048
|
+
"FileInfo",
|
|
1049
|
+
"LEGACY_LAYER_IDS",
|
|
1050
|
+
"PCB_VERSIONS",
|
|
1051
|
+
"SCH_VERSIONS",
|
|
1052
|
+
"SYM_VERSIONS",
|
|
1053
|
+
"V9_LAYER_IDS",
|
|
1054
|
+
"WRITABLE",
|
|
1055
|
+
"board_net_names",
|
|
1056
|
+
"board_net_node",
|
|
1057
|
+
"convert",
|
|
1058
|
+
"convert_board",
|
|
1059
|
+
"convert_footprint",
|
|
1060
|
+
"convert_schematic",
|
|
1061
|
+
"convert_symbol_library",
|
|
1062
|
+
"file_info",
|
|
1063
|
+
"flag",
|
|
1064
|
+
"header_info",
|
|
1065
|
+
"installed_major",
|
|
1066
|
+
"is_hidden",
|
|
1067
|
+
"layer_selection",
|
|
1068
|
+
"major_for",
|
|
1069
|
+
"resolve_target",
|
|
1070
|
+
"root_info",
|
|
1071
|
+
"stamp_key",
|
|
1072
|
+
"version_for",
|
|
1073
|
+
]
|