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,155 @@
|
|
|
1
|
+
"""Run the tool calls of one model step concurrently where that is safe.
|
|
2
|
+
|
|
3
|
+
A model step can ask for several tools at once. Each call gets a scheduling class:
|
|
4
|
+
|
|
5
|
+
* ``read``: changes nothing (file reads, searches, KiCad queries, read-only shell
|
|
6
|
+
commands). Reads run concurrently, at most ``MAX_CONCURRENT_READS`` at a time.
|
|
7
|
+
* ``write``: changes one known file. It waits for every earlier call that touches the
|
|
8
|
+
same file (or an unknown set of files), so edits apply in the order the model gave.
|
|
9
|
+
* ``exclusive``: anything else (other shell commands, questions to the user, unknown
|
|
10
|
+
tools, writes whose target is unknown). It waits for every earlier call, and every
|
|
11
|
+
later call waits for it.
|
|
12
|
+
|
|
13
|
+
Results are returned in the order of the calls, whatever order they finish in.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
import shlex
|
|
21
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, TypeVar
|
|
24
|
+
|
|
25
|
+
MAX_CONCURRENT_READS = 8
|
|
26
|
+
|
|
27
|
+
READ = "read"
|
|
28
|
+
WRITE = "write"
|
|
29
|
+
EXCLUSIVE = "exclusive"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class CallPlan:
|
|
34
|
+
kind: str # read | write | exclusive
|
|
35
|
+
# Normalised file the call reads or writes; None means "not scoped to one file".
|
|
36
|
+
path: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def path_key(raw: Any, workspace: str | None = None) -> str | None:
|
|
40
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
41
|
+
return None
|
|
42
|
+
path = raw.strip().replace("\\", "/")
|
|
43
|
+
if not os.path.isabs(path) and workspace:
|
|
44
|
+
path = os.path.join(workspace, path)
|
|
45
|
+
return os.path.normpath(path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_READ_PROGRAMS = {
|
|
49
|
+
"ls",
|
|
50
|
+
"cat",
|
|
51
|
+
"head",
|
|
52
|
+
"wc",
|
|
53
|
+
"grep",
|
|
54
|
+
"rg",
|
|
55
|
+
"find",
|
|
56
|
+
"pwd",
|
|
57
|
+
"stat",
|
|
58
|
+
"file",
|
|
59
|
+
"du",
|
|
60
|
+
"diff",
|
|
61
|
+
"which",
|
|
62
|
+
}
|
|
63
|
+
_FIND_ACTIONS = {"-exec", "-execdir", "-ok", "-okdir", "-delete", "-fprint", "-fprintf", "-fls"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_only_command(command: Any) -> bool:
|
|
67
|
+
"""Conservatively decide that a shell command only reads."""
|
|
68
|
+
if not isinstance(command, str) or not command.strip():
|
|
69
|
+
return False
|
|
70
|
+
if any(token in command for token in ("\n", ";", "&", "|", "`", "<", ">", "$(", "${")):
|
|
71
|
+
return False
|
|
72
|
+
try:
|
|
73
|
+
words = shlex.split(command)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return False
|
|
76
|
+
if not words or os.path.basename(words[0]) not in _READ_PROGRAMS:
|
|
77
|
+
return False
|
|
78
|
+
program, rest = os.path.basename(words[0]), words[1:]
|
|
79
|
+
if program == "find":
|
|
80
|
+
return not any(word in _FIND_ACTIONS for word in rest)
|
|
81
|
+
if program == "rg":
|
|
82
|
+
return not any(word.startswith("--pre") for word in rest)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _overlaps(left: CallPlan, right: CallPlan) -> bool:
|
|
87
|
+
if WRITE not in (left.kind, right.kind):
|
|
88
|
+
return False
|
|
89
|
+
if left.path is None or right.path is None:
|
|
90
|
+
return True
|
|
91
|
+
return left.path == right.path
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def dependencies(plans: Sequence[CallPlan]) -> list[set[int]]:
|
|
95
|
+
"""For each call, the indexes of earlier calls it has to wait for."""
|
|
96
|
+
waits: list[set[int]] = []
|
|
97
|
+
last_exclusive: int | None = None
|
|
98
|
+
for index, plan in enumerate(plans):
|
|
99
|
+
mine: set[int] = set()
|
|
100
|
+
if plan.kind == EXCLUSIVE:
|
|
101
|
+
mine.update(range(index))
|
|
102
|
+
last_exclusive = index
|
|
103
|
+
else:
|
|
104
|
+
if last_exclusive is not None:
|
|
105
|
+
mine.add(last_exclusive)
|
|
106
|
+
mine.update(i for i in range(index) if _overlaps(plans[i], plan))
|
|
107
|
+
waits.append(mine)
|
|
108
|
+
return waits
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
T = TypeVar("T")
|
|
112
|
+
R = TypeVar("R")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def run_batch(
|
|
116
|
+
calls: Sequence[T],
|
|
117
|
+
plans: Sequence[CallPlan],
|
|
118
|
+
handler: Callable[[T], Awaitable[R]],
|
|
119
|
+
*,
|
|
120
|
+
max_concurrent_reads: int = MAX_CONCURRENT_READS,
|
|
121
|
+
) -> list[R]:
|
|
122
|
+
"""Run ``handler`` on every call under the rules above; results in call order.
|
|
123
|
+
|
|
124
|
+
``handler`` must not raise for an ordinary tool failure (return an error result
|
|
125
|
+
instead). If it does raise, the remaining calls are cancelled and the error
|
|
126
|
+
propagates. Cancelling the batch cancels every call still running.
|
|
127
|
+
"""
|
|
128
|
+
if len(calls) != len(plans):
|
|
129
|
+
raise ValueError("one plan per call")
|
|
130
|
+
waits = dependencies(plans)
|
|
131
|
+
finished = [asyncio.Event() for _ in calls]
|
|
132
|
+
reads = asyncio.Semaphore(max(1, max_concurrent_reads))
|
|
133
|
+
results: list[Any] = [None] * len(calls)
|
|
134
|
+
|
|
135
|
+
async def one(index: int) -> None:
|
|
136
|
+
try:
|
|
137
|
+
for earlier in sorted(waits[index]):
|
|
138
|
+
await finished[earlier].wait()
|
|
139
|
+
if plans[index].kind == READ:
|
|
140
|
+
async with reads:
|
|
141
|
+
results[index] = await handler(calls[index])
|
|
142
|
+
else:
|
|
143
|
+
results[index] = await handler(calls[index])
|
|
144
|
+
finally:
|
|
145
|
+
finished[index].set()
|
|
146
|
+
|
|
147
|
+
tasks = [asyncio.create_task(one(index)) for index in range(len(calls))]
|
|
148
|
+
try:
|
|
149
|
+
await asyncio.gather(*tasks)
|
|
150
|
+
finally:
|
|
151
|
+
for task in tasks:
|
|
152
|
+
if not task.done():
|
|
153
|
+
task.cancel()
|
|
154
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
155
|
+
return results
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""``kicad_connections``: read-only connectivity questions about the design.
|
|
2
|
+
|
|
3
|
+
* ``pin`` (``U1.3`` or ``U1.VCC``): the pin's net and every other pin on it, with
|
|
4
|
+
pin names and electrical types.
|
|
5
|
+
* ``part`` (``U1``): each pin of the part and its net.
|
|
6
|
+
* ``net``: every pin on a net.
|
|
7
|
+
* ``from`` + ``to`` (pins or parts): shortest paths between them, walking from net
|
|
8
|
+
to net through two-terminal passives (R, C, L, ferrite, fuse, diode, jumper) but
|
|
9
|
+
not through ICs, connectors, ground or supply rails (which would connect everything).
|
|
10
|
+
|
|
11
|
+
Connectivity comes from the schematic netlist (kicad-cli) when available, else from
|
|
12
|
+
the board's pad nets; pin names from the schematic symbols, else the pads' functions.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections import deque
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from traceforge.tools.base import Tool, ToolContext, ToolError
|
|
21
|
+
|
|
22
|
+
_PASSIVE = {"R", "C", "L", "FB", "F", "D", "JP", "SJ", "RN"}
|
|
23
|
+
_MAX_NODES = 60
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _model(args: dict[str, Any], ctx: ToolContext):
|
|
27
|
+
from traceforge.kicad.cli import KicadCli, KicadCliError
|
|
28
|
+
from traceforge.kicad.pcb import read_board
|
|
29
|
+
from traceforge.kicad.project import discover
|
|
30
|
+
from traceforge.kicad.schematic import read_schematic
|
|
31
|
+
from traceforge.validate.netmodel import build_model
|
|
32
|
+
|
|
33
|
+
ws = discover(ctx.workspace)
|
|
34
|
+
sch_path = ctx.resolve(args["schematic"], must_exist=True) if args.get("schematic") else None
|
|
35
|
+
pcb_path = ctx.resolve(args["pcb"], must_exist=True) if args.get("pcb") else None
|
|
36
|
+
sch_path, pcb_path = sch_path or ws.schematic, pcb_path or ws.pcb
|
|
37
|
+
if sch_path is None and pcb_path is None:
|
|
38
|
+
if ws.others.get("altium"): # Altium project: use the converted model
|
|
39
|
+
from traceforge.altium import AltiumError, load_project
|
|
40
|
+
from traceforge.altium.model import to_design_model
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return to_design_model(load_project(ctx.workspace))
|
|
44
|
+
except AltiumError as exc:
|
|
45
|
+
raise ToolError(str(exc)) from exc
|
|
46
|
+
raise ToolError("no .kicad_sch or .kicad_pcb in the workspace")
|
|
47
|
+
source = args.get("source") or "auto"
|
|
48
|
+
schematic = read_schematic(sch_path) if sch_path else None
|
|
49
|
+
board = read_board(pcb_path) if pcb_path else None
|
|
50
|
+
netlist = None
|
|
51
|
+
if schematic is not None and source in {"auto", "schematic"}:
|
|
52
|
+
cli = KicadCli()
|
|
53
|
+
if cli.available:
|
|
54
|
+
try:
|
|
55
|
+
netlist = cli.netlist(sch_path, ctx.workspace / ".traceforge" / "reports")
|
|
56
|
+
except KicadCliError:
|
|
57
|
+
netlist = None
|
|
58
|
+
if netlist is None and (source == "schematic" or board is None):
|
|
59
|
+
raise ToolError("schematic connectivity needs kicad-cli; pass source=board")
|
|
60
|
+
if source == "board" and board is None:
|
|
61
|
+
raise ToolError("no board found")
|
|
62
|
+
return build_model(schematic, board, netlist)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _pin_info(model, ref: str, pin: str) -> dict[str, Any]:
|
|
66
|
+
part = model.parts.get(ref)
|
|
67
|
+
info: dict[str, Any] = {"pin": f"{ref}.{pin}"}
|
|
68
|
+
definition = part.pin_defs.get(pin) if part else None
|
|
69
|
+
name = definition.name if definition else ""
|
|
70
|
+
if not name:
|
|
71
|
+
name = next((p.function for p in model.pads(ref, pin) if p.function), "")
|
|
72
|
+
if name and name not in {pin, "~"}:
|
|
73
|
+
info["name"] = name.replace("~{", "").replace("}", "")
|
|
74
|
+
if definition and definition.type:
|
|
75
|
+
info["type"] = definition.type
|
|
76
|
+
if part is not None and part.value:
|
|
77
|
+
info["value"] = part.value
|
|
78
|
+
return info
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_pin(model, text: str) -> tuple[str, str]:
|
|
82
|
+
ref, sep, pin = text.strip().replace(":", ".").partition(".")
|
|
83
|
+
if not sep or not pin:
|
|
84
|
+
raise ToolError(f"{text!r}: give a pin as REF.PIN, e.g. U1.3 or U1.VCC")
|
|
85
|
+
if ref not in model.parts and not model.part_nets(ref):
|
|
86
|
+
raise ToolError(f"no part {ref}")
|
|
87
|
+
nets = model.part_nets(ref)
|
|
88
|
+
part = model.parts.get(ref)
|
|
89
|
+
if pin in nets or (part and pin in part.pin_defs):
|
|
90
|
+
return ref, pin
|
|
91
|
+
wanted = pin.upper()
|
|
92
|
+
matches = []
|
|
93
|
+
for number in {*nets, *(part.pin_defs if part else {})}:
|
|
94
|
+
name = _pin_info(model, ref, number).get("name", "")
|
|
95
|
+
if name.upper() == wanted or wanted in name.upper().split("/"):
|
|
96
|
+
matches.append(number)
|
|
97
|
+
if len(matches) == 1:
|
|
98
|
+
return ref, matches[0]
|
|
99
|
+
if matches:
|
|
100
|
+
raise ToolError(f"{text}: several pins match ({', '.join(sorted(matches))}); use a number")
|
|
101
|
+
raise ToolError(f"{ref} has no pin {pin}; pins: {', '.join(sorted(nets)) or 'none connected'}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_rail(net: str) -> bool:
|
|
105
|
+
from traceforge.validate.circuits import is_ground, rail_voltage
|
|
106
|
+
|
|
107
|
+
return is_ground(net) or rail_voltage(net) is not None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _net_nodes(model, net: str, exclude: tuple[str, str] | None = None) -> list[dict]:
|
|
111
|
+
nodes = [_pin_info(model, r, p) for r, p in model.nodes(net) if (r, p) != exclude]
|
|
112
|
+
return nodes[:_MAX_NODES]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _find_net(model, name: str) -> str:
|
|
116
|
+
wanted = name.strip().lstrip("/").upper()
|
|
117
|
+
for net in model.nets:
|
|
118
|
+
if net.lstrip("/").upper() == wanted:
|
|
119
|
+
return net
|
|
120
|
+
close = [n for n in model.nets if wanted in n.upper()][:10]
|
|
121
|
+
raise ToolError(f"no net {name}" + (f"; similar: {', '.join(close)}" if close else ""))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _endpoints(model, text: str) -> tuple[set[str], set[str]]:
|
|
125
|
+
"""(start nets, owning refs) for a pin, a whole part or a net."""
|
|
126
|
+
if "." in text or ":" in text:
|
|
127
|
+
ref, pin = _resolve_pin(model, text)
|
|
128
|
+
net = model.net_of(ref, pin)
|
|
129
|
+
return ({net} if net else set()), {ref}
|
|
130
|
+
nets = set(model.part_nets(text).values())
|
|
131
|
+
if not nets:
|
|
132
|
+
return {_find_net(model, text)}, set()
|
|
133
|
+
signal = {n for n in nets if not _is_rail(n)}
|
|
134
|
+
return (signal or nets), {text} # a shared GND/supply says nothing about a path
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _paths(model, src: str, dst: str, max_hops: int) -> list[list[str]]:
|
|
138
|
+
starts, src_refs = _endpoints(model, src)
|
|
139
|
+
targets, dst_refs = _endpoints(model, dst)
|
|
140
|
+
if starts & targets:
|
|
141
|
+
net = sorted(starts & targets)[0]
|
|
142
|
+
return [[src, f"net {net}", dst]]
|
|
143
|
+
# BFS over nets; a step crosses one two-terminal passive to its other net.
|
|
144
|
+
queue = deque((net, [f"net {net}"]) for net in sorted(starts))
|
|
145
|
+
seen = set(starts)
|
|
146
|
+
found: list[list[str]] = []
|
|
147
|
+
while queue and len(found) < 3:
|
|
148
|
+
net, path = queue.popleft()
|
|
149
|
+
if len(path) // 2 >= max_hops:
|
|
150
|
+
continue
|
|
151
|
+
for ref, pin in model.nodes(net):
|
|
152
|
+
part = model.parts.get(ref)
|
|
153
|
+
if ref in src_refs or ref in dst_refs or part is None or part.prefix not in _PASSIVE:
|
|
154
|
+
continue
|
|
155
|
+
pins = model.part_nets(ref)
|
|
156
|
+
if len(pins) != 2:
|
|
157
|
+
continue
|
|
158
|
+
for other_pin, other in pins.items():
|
|
159
|
+
if other_pin == pin or other in seen:
|
|
160
|
+
continue
|
|
161
|
+
step = [*path, f"{ref} ({part.value}) pin {pin}->{other_pin}", f"net {other}"]
|
|
162
|
+
if other in targets:
|
|
163
|
+
found.append([src, *step, dst])
|
|
164
|
+
continue
|
|
165
|
+
if _is_rail(other):
|
|
166
|
+
continue # do not wander through GND/supply rails
|
|
167
|
+
seen.add(other)
|
|
168
|
+
queue.append((other, step))
|
|
169
|
+
return found
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def kicad_connections(args: dict[str, Any], ctx: ToolContext) -> dict:
|
|
173
|
+
model = _model(args, ctx)
|
|
174
|
+
out: dict[str, Any] = {"source": model.source}
|
|
175
|
+
asked = False
|
|
176
|
+
if args.get("pin"):
|
|
177
|
+
asked = True
|
|
178
|
+
ref, pin = _resolve_pin(model, str(args["pin"]))
|
|
179
|
+
net = model.net_of(ref, pin)
|
|
180
|
+
out["pin"] = _pin_info(model, ref, pin)
|
|
181
|
+
out["net"] = net or "(unconnected)"
|
|
182
|
+
if net:
|
|
183
|
+
out["connected_to"] = _net_nodes(model, net, (ref, pin))
|
|
184
|
+
if args.get("part"):
|
|
185
|
+
asked = True
|
|
186
|
+
ref = str(args["part"]).strip()
|
|
187
|
+
nets = model.part_nets(ref)
|
|
188
|
+
if ref not in model.parts and not nets:
|
|
189
|
+
raise ToolError(f"no part {ref}")
|
|
190
|
+
part = model.parts.get(ref)
|
|
191
|
+
out["part"] = {"ref": ref, "value": part.value if part else "", "pins": []}
|
|
192
|
+
numbers = sorted({*nets, *(part.pin_defs if part else {})}, key=_pin_key)
|
|
193
|
+
for number in numbers:
|
|
194
|
+
row = _pin_info(model, ref, number)
|
|
195
|
+
net = nets.get(number)
|
|
196
|
+
row["net"] = net or "(unconnected)"
|
|
197
|
+
if net:
|
|
198
|
+
row["others"] = len(model.nodes(net)) - 1
|
|
199
|
+
out["part"]["pins"].append(row)
|
|
200
|
+
if args.get("net"):
|
|
201
|
+
asked = True
|
|
202
|
+
net = _find_net(model, str(args["net"]))
|
|
203
|
+
out["net_pins"] = {"net": net, "pins": _net_nodes(model, net)}
|
|
204
|
+
if args.get("from") or args.get("to"):
|
|
205
|
+
asked = True
|
|
206
|
+
if not (args.get("from") and args.get("to")):
|
|
207
|
+
raise ToolError("a path needs both 'from' and 'to'")
|
|
208
|
+
paths = _paths(model, str(args["from"]), str(args["to"]), int(args.get("max_hops") or 4))
|
|
209
|
+
out["paths"] = [" -> ".join(p) for p in paths]
|
|
210
|
+
if not paths:
|
|
211
|
+
out["note"] = (
|
|
212
|
+
"no path through passives within max_hops (ICs, connectors, GND and supply "
|
|
213
|
+
"rails are not traversed)"
|
|
214
|
+
)
|
|
215
|
+
if not asked:
|
|
216
|
+
raise ToolError("ask about a 'pin' (U1.3), a 'part' (U1), a 'net', or 'from' + 'to'")
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _pin_key(number: str) -> tuple[int, str]:
|
|
221
|
+
return (int(number), "") if number.isdigit() else (10**6, number)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
CONNECTIONS_TOOL = Tool(
|
|
225
|
+
"kicad_connections",
|
|
226
|
+
"Connectivity questions, read-only: pin='U1.3' or 'U1.VCC' -> its net and every other "
|
|
227
|
+
"pin on it (names, types); part='U1' -> each pin and its net; net='+3V3' -> its pins; "
|
|
228
|
+
"from='U2.PB3' to='D1' -> paths through passives (R/C/L/D/fuse; not ICs or rails). "
|
|
229
|
+
"Uses the schematic netlist (kicad-cli) when available, else the board.",
|
|
230
|
+
{
|
|
231
|
+
"type": "object",
|
|
232
|
+
"properties": {
|
|
233
|
+
"pin": {"type": "string"},
|
|
234
|
+
"part": {"type": "string"},
|
|
235
|
+
"net": {"type": "string"},
|
|
236
|
+
"from": {"type": "string"},
|
|
237
|
+
"to": {"type": "string"},
|
|
238
|
+
"max_hops": {"type": "integer", "description": "passives to cross (default 4)"},
|
|
239
|
+
"source": {"type": "string", "enum": ["auto", "schematic", "board"]},
|
|
240
|
+
"schematic": {"type": "string"},
|
|
241
|
+
"pcb": {"type": "string"},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
kicad_connections,
|
|
245
|
+
)
|