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.
Files changed (75) hide show
  1. enginehall-2.0.0.dist-info/METADATA +511 -0
  2. enginehall-2.0.0.dist-info/RECORD +75 -0
  3. enginehall-2.0.0.dist-info/WHEEL +4 -0
  4. enginehall-2.0.0.dist-info/entry_points.txt +4 -0
  5. enginehall-2.0.0.dist-info/licenses/LICENSE +51 -0
  6. pcba/THIRD_PARTY_NOTICES.md +72 -0
  7. pcba/__init__.py +6 -0
  8. pcba/adapters/__init__.py +10 -0
  9. pcba/adapters/altium_bridge/MANIFEST.json +5 -0
  10. pcba/adapters/altium_bridge/__init__.py +1 -0
  11. pcba/adapters/altium_bridge/bridge.pas +3342 -0
  12. pcba/adapters/altium_drive.py +1160 -0
  13. pcba/adapters/altium_house.py +591 -0
  14. pcba/adapters/altium_ops.py +858 -0
  15. pcba/adapters/altium_outjob.py +247 -0
  16. pcba/adapters/altium_uia.py +304 -0
  17. pcba/adapters/altium_vcs.py +197 -0
  18. pcba/adapters/altium_win32.py +457 -0
  19. pcba/adapters/cubemx_drive.py +248 -0
  20. pcba/baseline.py +151 -0
  21. pcba/changes.py +81 -0
  22. pcba/cli.py +891 -0
  23. pcba/components.py +323 -0
  24. pcba/config.py +180 -0
  25. pcba/data/components/generic.json +61 -0
  26. pcba/discover.py +350 -0
  27. pcba/doctor.py +131 -0
  28. pcba/house.py +123 -0
  29. pcba/mcp/__init__.py +5 -0
  30. pcba/mcp/altium_server.py +1358 -0
  31. pcba/mcp/protocol.py +224 -0
  32. pcba/mcp/server.py +332 -0
  33. pcba/model/__init__.py +38 -0
  34. pcba/model/design.py +232 -0
  35. pcba/model/finding.py +148 -0
  36. pcba/model/pinmap.py +200 -0
  37. pcba/parsers/__init__.py +0 -0
  38. pcba/parsers/altium/__init__.py +37 -0
  39. pcba/parsers/altium/_sheet.py +293 -0
  40. pcba/parsers/altium/geometry.py +103 -0
  41. pcba/parsers/altium/netlist.py +302 -0
  42. pcba/parsers/altium/pinmap.py +336 -0
  43. pcba/parsers/altium/project.py +249 -0
  44. pcba/parsers/altium/records.py +227 -0
  45. pcba/parsers/firmware/__init__.py +5 -0
  46. pcba/parsers/firmware/index.py +250 -0
  47. pcba/parsers/ioc/__init__.py +49 -0
  48. pcba/parsers/ioc/mcu_db.py +180 -0
  49. pcba/parsers/ioc/reader.py +182 -0
  50. pcba/parsers/ioc/validate.py +192 -0
  51. pcba/parsers/kicad/__init__.py +103 -0
  52. pcba/parsers/kicad/board.py +269 -0
  53. pcba/parsers/kicad/kicad_cli.py +190 -0
  54. pcba/parsers/kicad/pinmap.py +90 -0
  55. pcba/parsers/kicad/schematic.py +517 -0
  56. pcba/parsers/kicad/sexpr.py +136 -0
  57. pcba/publish.py +269 -0
  58. pcba/report/__init__.py +7 -0
  59. pcba/report/json_report.py +39 -0
  60. pcba/report/sarif.py +128 -0
  61. pcba/report/text.py +304 -0
  62. pcba/requirements.py +320 -0
  63. pcba/review.py +177 -0
  64. pcba/rules/__init__.py +312 -0
  65. pcba/rules/cross_domain.py +496 -0
  66. pcba/rules/kicad_board.py +775 -0
  67. pcba/rules/kicad_common.py +167 -0
  68. pcba/rules/kicad_components.py +603 -0
  69. pcba/rules/kicad_schematic.py +621 -0
  70. pcba/rules/locate.py +22 -0
  71. pcba/rules/schematic.py +123 -0
  72. pcba/rules/signals.py +149 -0
  73. pcba/rules/spice.py +59 -0
  74. pcba/rules/stm32.py +155 -0
  75. 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 "")