enginehall 2.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.
- enginehall-2.0.0.dist-info/METADATA +511 -0
- enginehall-2.0.0.dist-info/RECORD +75 -0
- enginehall-2.0.0.dist-info/WHEEL +4 -0
- enginehall-2.0.0.dist-info/entry_points.txt +4 -0
- enginehall-2.0.0.dist-info/licenses/LICENSE +51 -0
- pcba/THIRD_PARTY_NOTICES.md +72 -0
- pcba/__init__.py +6 -0
- pcba/adapters/__init__.py +10 -0
- pcba/adapters/altium_bridge/MANIFEST.json +5 -0
- pcba/adapters/altium_bridge/__init__.py +1 -0
- pcba/adapters/altium_bridge/bridge.pas +3342 -0
- pcba/adapters/altium_drive.py +1160 -0
- pcba/adapters/altium_house.py +591 -0
- pcba/adapters/altium_ops.py +858 -0
- pcba/adapters/altium_outjob.py +247 -0
- pcba/adapters/altium_uia.py +304 -0
- pcba/adapters/altium_vcs.py +197 -0
- pcba/adapters/altium_win32.py +457 -0
- pcba/adapters/cubemx_drive.py +248 -0
- pcba/baseline.py +151 -0
- pcba/changes.py +81 -0
- pcba/cli.py +891 -0
- pcba/components.py +323 -0
- pcba/config.py +180 -0
- pcba/data/components/generic.json +61 -0
- pcba/discover.py +350 -0
- pcba/doctor.py +131 -0
- pcba/house.py +123 -0
- pcba/mcp/__init__.py +5 -0
- pcba/mcp/altium_server.py +1358 -0
- pcba/mcp/protocol.py +224 -0
- pcba/mcp/server.py +332 -0
- pcba/model/__init__.py +38 -0
- pcba/model/design.py +232 -0
- pcba/model/finding.py +148 -0
- pcba/model/pinmap.py +200 -0
- pcba/parsers/__init__.py +0 -0
- pcba/parsers/altium/__init__.py +37 -0
- pcba/parsers/altium/_sheet.py +293 -0
- pcba/parsers/altium/geometry.py +103 -0
- pcba/parsers/altium/netlist.py +302 -0
- pcba/parsers/altium/pinmap.py +336 -0
- pcba/parsers/altium/project.py +249 -0
- pcba/parsers/altium/records.py +227 -0
- pcba/parsers/firmware/__init__.py +5 -0
- pcba/parsers/firmware/index.py +250 -0
- pcba/parsers/ioc/__init__.py +49 -0
- pcba/parsers/ioc/mcu_db.py +180 -0
- pcba/parsers/ioc/reader.py +182 -0
- pcba/parsers/ioc/validate.py +192 -0
- pcba/parsers/kicad/__init__.py +103 -0
- pcba/parsers/kicad/board.py +269 -0
- pcba/parsers/kicad/kicad_cli.py +190 -0
- pcba/parsers/kicad/pinmap.py +90 -0
- pcba/parsers/kicad/schematic.py +517 -0
- pcba/parsers/kicad/sexpr.py +136 -0
- pcba/publish.py +269 -0
- pcba/report/__init__.py +7 -0
- pcba/report/json_report.py +39 -0
- pcba/report/sarif.py +128 -0
- pcba/report/text.py +304 -0
- pcba/requirements.py +320 -0
- pcba/review.py +177 -0
- pcba/rules/__init__.py +312 -0
- pcba/rules/cross_domain.py +496 -0
- pcba/rules/kicad_board.py +775 -0
- pcba/rules/kicad_common.py +167 -0
- pcba/rules/kicad_components.py +603 -0
- pcba/rules/kicad_schematic.py +621 -0
- pcba/rules/locate.py +22 -0
- pcba/rules/schematic.py +123 -0
- pcba/rules/signals.py +149 -0
- pcba/rules/spice.py +59 -0
- pcba/rules/stm32.py +155 -0
- pcba/simulation.py +143 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Shared reading of a KiCad design for the KiCad rules: part classes, rails, locations.
|
|
2
|
+
|
|
3
|
+
Classification is by reference prefix first (R, C, L, FB, D, Q, U, J, TP ...), then by library
|
|
4
|
+
symbol: the prefix is what a designer controls and checks, the symbol name varies by library.
|
|
5
|
+
Rail voltages come from `pcba-requirements.toml` when declared, otherwise from the net name
|
|
6
|
+
(`+3V3`); a finding that rests on a name-derived voltage says so and is marked heuristic.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from pcba.model.design import Component, Design, Net, Schematic, SchLabel, SchPin
|
|
14
|
+
from pcba.model.finding import Detector, Evidence, Location
|
|
15
|
+
from pcba.model.pinmap import Project
|
|
16
|
+
from pcba.requirements import Rail, Requirements, is_ground
|
|
17
|
+
|
|
18
|
+
_PREFIX = re.compile(r"^#?([A-Za-z]+)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def design(project: Project) -> Design | None:
|
|
22
|
+
return project.design
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def schematic(project: Project) -> Schematic | None:
|
|
26
|
+
return project.design.schematic if project.design else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def requirements(project: Project) -> Requirements:
|
|
30
|
+
return project.design.requirements if project.design and project.design.requirements else Requirements()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prefix(ref: str) -> str:
|
|
34
|
+
match = _PREFIX.match(ref or "")
|
|
35
|
+
return match.group(1).upper() if match else ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pin_numbers(component: Component) -> set[str]:
|
|
39
|
+
return {p.number for p in component.pins}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def classify(component: Component) -> str:
|
|
43
|
+
"""power | net_tie | test_point | capacitor | resistor | inductor | ferrite | fuse | jumper |
|
|
44
|
+
diode | connector | transistor | crystal | mechanical | ic | other."""
|
|
45
|
+
if component.power or component.ref.startswith("#"):
|
|
46
|
+
return "power"
|
|
47
|
+
lib = component.lib_id.upper()
|
|
48
|
+
name = lib.split(":")[-1]
|
|
49
|
+
first = prefix(component.ref)
|
|
50
|
+
if "NETTIE" in lib or first == "NT":
|
|
51
|
+
return "net_tie"
|
|
52
|
+
if "TESTPOINT" in lib or first == "TP":
|
|
53
|
+
return "test_point"
|
|
54
|
+
if first == "FB" or "FERRITE" in lib:
|
|
55
|
+
return "ferrite"
|
|
56
|
+
if first in ("JP", "SJ") or "JUMPER" in lib or "SOLDERJUMPER" in lib:
|
|
57
|
+
return "jumper"
|
|
58
|
+
by_prefix = {"C": "capacitor", "R": "resistor", "L": "inductor", "F": "fuse", "D": "diode", "LED": "diode",
|
|
59
|
+
"J": "connector", "P": "connector", "CN": "connector", "X": "connector", "CON": "connector",
|
|
60
|
+
"Q": "transistor", "T": "transistor", "Y": "crystal", "MH": "mechanical", "H": "mechanical",
|
|
61
|
+
"U": "ic", "IC": "ic"}
|
|
62
|
+
if first in by_prefix:
|
|
63
|
+
return by_prefix[first]
|
|
64
|
+
if re.match(r"^(C|CP|C_.*|CP_.*|C_POLARIZED.*)$", name):
|
|
65
|
+
return "capacitor"
|
|
66
|
+
if re.match(r"^(R|R_.*)$", name):
|
|
67
|
+
return "resistor"
|
|
68
|
+
if lib.startswith("CONNECTOR"):
|
|
69
|
+
return "connector"
|
|
70
|
+
if sum(p.type == "power_in" for p in component.pins) and len(pin_numbers(component)) > 2:
|
|
71
|
+
return "ic"
|
|
72
|
+
return "other"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parts(project: Project, *kinds: str) -> list[Component]:
|
|
76
|
+
sch = schematic(project)
|
|
77
|
+
if sch is None:
|
|
78
|
+
return []
|
|
79
|
+
return [c for c in sch.components.values() if not c.power and (not kinds or classify(c) in kinds)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_ground_net(net: Net | None) -> bool:
|
|
83
|
+
return bool(net and (is_ground(net.label) or is_ground(net.name)))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def rail(project: Project, net: Net | None) -> Rail | None:
|
|
87
|
+
if net is None:
|
|
88
|
+
return None
|
|
89
|
+
return requirements(project).rail(net.name, net.label)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_supply_rail(project: Project, net: Net | None) -> bool:
|
|
93
|
+
"""A named, non-ground supply: a power-symbol net, a declared rail, a voltage name, or a net
|
|
94
|
+
driven by a power output pin."""
|
|
95
|
+
if net is None or is_ground_net(net):
|
|
96
|
+
return False
|
|
97
|
+
known = rail(project, net)
|
|
98
|
+
if known is not None and known.voltage != 0:
|
|
99
|
+
return True
|
|
100
|
+
return net.power or any(p.type == "power_out" for p in net.pins if not p.ref.startswith("#"))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def other_nets(sch: Schematic, component: Component, number: str) -> list[Net]:
|
|
104
|
+
"""The nets on the other pins of a part (for a two-pin part: the far side)."""
|
|
105
|
+
out = []
|
|
106
|
+
for pin in component.pins:
|
|
107
|
+
if pin.number != number:
|
|
108
|
+
net = sch.net_of(component.ref, pin.number)
|
|
109
|
+
if net is not None and net not in out:
|
|
110
|
+
out.append(net)
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def net_display(net: Net) -> str:
|
|
115
|
+
return net.label or net.name
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def sheet_of(sheet_path: str) -> str | None:
|
|
119
|
+
return None if sheet_path in ("/", "") else sheet_path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def at_component(project: Project, component: Component, pin: str | None = None, key: str | None = None) -> Location:
|
|
123
|
+
return Location(artifact=project.relative(component.sheet_file), sheet=sheet_of(component.sheet_path),
|
|
124
|
+
component=component.ref, pin=pin, line=component.line or None, key=key)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def at_pin(project: Project, pin: SchPin, key: str | None = None) -> Location:
|
|
128
|
+
return Location(artifact=project.relative(pin.sheet_file), sheet=sheet_of(pin.sheet_path),
|
|
129
|
+
component=pin.ref, pin=pin.number, line=pin.line or None, key=key)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def at_label(project: Project, label: SchLabel, key: str | None = None) -> Location:
|
|
133
|
+
return Location(artifact=project.relative(label.sheet_file), sheet=sheet_of(label.sheet_path),
|
|
134
|
+
line=label.line or None, key=key)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def at_net(project: Project, net: Net, key: str | None = None) -> Location:
|
|
138
|
+
"""A net is shown where its first part pin is, or its first label."""
|
|
139
|
+
if net.parts:
|
|
140
|
+
return at_pin(project, net.parts[0], key=key or net_display(net))
|
|
141
|
+
if net.labels:
|
|
142
|
+
return at_label(project, net.labels[0], key=key or net_display(net))
|
|
143
|
+
sch = schematic(project)
|
|
144
|
+
return Location(artifact=project.relative(sch.root_file) if sch else ".", key=key or net_display(net))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def rail_evidence(project: Project, net: Net, known: Rail) -> Evidence:
|
|
148
|
+
sch = schematic(project)
|
|
149
|
+
if known.source == "requirements":
|
|
150
|
+
req = requirements(project)
|
|
151
|
+
source = project.relative(req.path) if req.path else "pcba-requirements.toml"
|
|
152
|
+
span = f" ({known.low:g} to {known.high:g} V)" if known.low != known.high else ""
|
|
153
|
+
return Evidence(kind="rail_voltage", value=f"rail {known.name} = {known.voltage:g} V{span}", source=source)
|
|
154
|
+
return Evidence(kind="rail_voltage",
|
|
155
|
+
value=f"net {net_display(net)!r} read as {known.voltage:g} V from its name",
|
|
156
|
+
source=project.relative(sch.root_file) if sch else None)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def detector_for(*rails: Rail | None) -> tuple[Detector, float]:
|
|
160
|
+
"""Deterministic when every voltage was declared; heuristic when one came from a net name."""
|
|
161
|
+
if any(r is not None and r.source != "requirements" for r in rails):
|
|
162
|
+
return Detector.HEURISTIC, 0.9
|
|
163
|
+
return Detector.DETERMINISTIC, 1.0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def pin_label(pin: SchPin) -> str:
|
|
167
|
+
return f"{pin.ref}.{pin.number}" + (f" ({pin.name})" if pin.name else "")
|