ecadlib 0.3.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 (199) hide show
  1. ecadlib/__init__.py +3 -0
  2. ecadlib/__main__.py +5 -0
  3. ecadlib/a365_sync.py +637 -0
  4. ecadlib/agent.py +1407 -0
  5. ecadlib/agent_queue.py +780 -0
  6. ecadlib/agent_watch.py +732 -0
  7. ecadlib/altium365.py +385 -0
  8. ecadlib/altium_dblib_sync.py +530 -0
  9. ecadlib/altium_export.py +209 -0
  10. ecadlib/altium_forms.py +768 -0
  11. ecadlib/altium_lib.py +1089 -0
  12. ecadlib/altium_menu.py +1751 -0
  13. ecadlib/altium_pkg.py +396 -0
  14. ecadlib/altium_project.py +1095 -0
  15. ecadlib/app.py +1458 -0
  16. ecadlib/approvals.py +395 -0
  17. ecadlib/assets/demo_bom.csv +8 -0
  18. ecadlib/assets/kicad_action_plugin.py +607 -0
  19. ecadlib/assets/kicad_reread.py +60 -0
  20. ecadlib/assets/pcblib_template.json.gz +0 -0
  21. ecadlib/bom_inputs.py +339 -0
  22. ecadlib/bom_triage.py +519 -0
  23. ecadlib/cad_model.py +528 -0
  24. ecadlib/cfb.py +171 -0
  25. ecadlib/cli.py +185 -0
  26. ecadlib/clihelp.py +149 -0
  27. ecadlib/cliutil.py +119 -0
  28. ecadlib/completion.py +173 -0
  29. ecadlib/costs.py +276 -0
  30. ecadlib/crosscheck.py +412 -0
  31. ecadlib/datasheet.py +2168 -0
  32. ecadlib/dedupe.py +572 -0
  33. ecadlib/detect.py +170 -0
  34. ecadlib/doctor.py +450 -0
  35. ecadlib/ds_route.py +541 -0
  36. ecadlib/eagle_export.py +1409 -0
  37. ecadlib/easy_setup.py +338 -0
  38. ecadlib/easyeda_convert.py +386 -0
  39. ecadlib/easyeda_pro_export.py +972 -0
  40. ecadlib/eval/__init__.py +0 -0
  41. ecadlib/eval/baselines/kicad-v1.json +404 -0
  42. ecadlib/eval/build_set.py +299 -0
  43. ecadlib/eval/cli.py +119 -0
  44. ecadlib/eval/gate.py +358 -0
  45. ecadlib/eval/golden/demo.txt +379 -0
  46. ecadlib/eval/golden/specs.txt +3120 -0
  47. ecadlib/eval/golden/testpack.txt +463 -0
  48. ecadlib/eval/golden.py +446 -0
  49. ecadlib/eval/replay/kicad-v1.json.gz +0 -0
  50. ecadlib/eval/runner.py +456 -0
  51. ecadlib/eval/score.py +179 -0
  52. ecadlib/eval/sets/kicad-v1.json +630 -0
  53. ecadlib/firstrun.py +400 -0
  54. ecadlib/footprint_adv.py +1006 -0
  55. ecadlib/footprint_bench.py +218 -0
  56. ecadlib/footprint_check.py +562 -0
  57. ecadlib/footprint_gen.py +582 -0
  58. ecadlib/footprint_rules.py +1092 -0
  59. ecadlib/forge.py +267 -0
  60. ecadlib/fp_share.py +95 -0
  61. ecadlib/fp_tidy.py +389 -0
  62. ecadlib/fsutil.py +62 -0
  63. ecadlib/house_style.py +1051 -0
  64. ecadlib/httpcache.py +124 -0
  65. ecadlib/hub.py +1490 -0
  66. ecadlib/hub_copy.py +161 -0
  67. ecadlib/hub_tools.py +79 -0
  68. ecadlib/kicad_dblib.py +257 -0
  69. ecadlib/kicad_export.py +837 -0
  70. ecadlib/kicad_import.py +913 -0
  71. ecadlib/kicad_index.py +160 -0
  72. ecadlib/kicad_menu.py +193 -0
  73. ecadlib/kicad_pcm.py +475 -0
  74. ecadlib/launcher.py +156 -0
  75. ecadlib/librarian.py +963 -0
  76. ecadlib/lifecycle.py +691 -0
  77. ecadlib/llm/__init__.py +276 -0
  78. ecadlib/llm/cli.py +70 -0
  79. ecadlib/llm/openai_compat.py +214 -0
  80. ecadlib/llm/profiles.py +245 -0
  81. ecadlib/llm/rails.py +570 -0
  82. ecadlib/llm/run.py +126 -0
  83. ecadlib/llm/sampling.py +49 -0
  84. ecadlib/mcp_server.py +723 -0
  85. ecadlib/messages.py +419 -0
  86. ecadlib/migrate.py +407 -0
  87. ecadlib/notify.py +329 -0
  88. ecadlib/package_schema.json +1238 -0
  89. ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
  90. ecadlib/package_specs/C_0402_1005Metric.json +22 -0
  91. ecadlib/package_specs/C_0805_2012Metric.json +22 -0
  92. ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
  93. ecadlib/package_specs/D_SMA.json +27 -0
  94. ecadlib/package_specs/D_SMB.json +27 -0
  95. ecadlib/package_specs/D_SOD-123.json +43 -0
  96. ecadlib/package_specs/D_SOD-323.json +43 -0
  97. ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
  98. ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
  99. ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
  100. ecadlib/package_specs/L_0805_2012Metric.json +22 -0
  101. ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
  102. ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
  103. ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
  104. ecadlib/package_specs/R_0402_1005Metric.json +22 -0
  105. ecadlib/package_specs/R_0603_1608Metric.json +22 -0
  106. ecadlib/package_specs/R_0805_2012Metric.json +22 -0
  107. ecadlib/package_specs/R_1206_3216Metric.json +22 -0
  108. ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
  109. ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
  110. ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
  111. ecadlib/package_specs/SOT-23-5.json +57 -0
  112. ecadlib/package_specs/SOT-23-6.json +62 -0
  113. ecadlib/package_specs/SOT-23.json +48 -0
  114. ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
  115. ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
  116. ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  117. ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
  118. ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
  119. ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
  120. ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
  121. ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  122. ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  123. ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
  124. ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
  125. ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
  126. ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
  127. ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
  128. ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
  129. ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
  130. ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
  131. ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
  132. ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
  133. ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
  134. ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
  135. ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
  136. ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
  137. ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
  138. ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
  139. ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
  140. ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
  141. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
  142. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
  143. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
  144. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
  145. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
  146. ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
  147. ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
  148. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
  149. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
  150. ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
  151. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
  152. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
  153. ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
  154. ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
  155. ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
  156. ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
  157. ecadlib/package_specs/standard.json.gz +0 -0
  158. ecadlib/parity.py +1204 -0
  159. ecadlib/parts_export.py +154 -0
  160. ecadlib/partview.py +215 -0
  161. ecadlib/plugins.py +231 -0
  162. ecadlib/providers.py +226 -0
  163. ecadlib/rebuild.py +471 -0
  164. ecadlib/release_check.py +437 -0
  165. ecadlib/reports.py +274 -0
  166. ecadlib/resolve.py +556 -0
  167. ecadlib/review_cli.py +292 -0
  168. ecadlib/rules.py +979 -0
  169. ecadlib/rules_infer.py +1205 -0
  170. ecadlib/sexpr.py +149 -0
  171. ecadlib/sheetsafe.py +65 -0
  172. ecadlib/sim_models.py +1087 -0
  173. ecadlib/sourcing.py +1161 -0
  174. ecadlib/stdparts.py +755 -0
  175. ecadlib/step_gen.py +929 -0
  176. ecadlib/symbol_layout.py +798 -0
  177. ecadlib/team.py +558 -0
  178. ecadlib/team_setup.py +288 -0
  179. ecadlib/term.py +420 -0
  180. ecadlib/term_views.py +64 -0
  181. ecadlib/testpack.py +334 -0
  182. ecadlib/ui/__init__.py +1 -0
  183. ecadlib/ui/__main__.py +3 -0
  184. ecadlib/ui/server.py +1029 -0
  185. ecadlib/ui/static/hairline.js +2 -0
  186. ecadlib/ui/static/index.html +2098 -0
  187. ecadlib/ui/static/setup.html +294 -0
  188. ecadlib/upkeep.py +717 -0
  189. ecadlib/upstream.py +573 -0
  190. ecadlib/userconfig.py +257 -0
  191. ecadlib/visual_check.py +1603 -0
  192. ecadlib/ws_status.py +203 -0
  193. ecadlib-0.3.0.dist-info/METADATA +2245 -0
  194. ecadlib-0.3.0.dist-info/RECORD +199 -0
  195. ecadlib-0.3.0.dist-info/WHEEL +5 -0
  196. ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
  197. ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  198. ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
  199. ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/kicad_index.py ADDED
@@ -0,0 +1,160 @@
1
+ """Index a local KiCad library directory (*.kicad_sym, *.pretty/*.kicad_mod) for BOM matching."""
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ from . import sexpr
7
+
8
+ MPN_KEYS = {"mpn", "manufacturer part number", "mfr part number", "mfr_pn", "mpn1", "part number", "manufacturer_part_number"}
9
+ LCSC_KEYS = {"lcsc", "lcsc part", "lcsc part #", "lcsc_part", "jlcpcb part #", "jlc"}
10
+ SI = {"p": 1e-12, "n": 1e-9, "u": 1e-6, "µ": 1e-6, "μ": 1e-6, "m": 1e-3, "": 1.0, "r": 1.0,
11
+ "k": 1e3, "K": 1e3, "M": 1e6, "G": 1e9}
12
+ PKG_RE = re.compile(r"(?<![0-9])(01005|0201|0402|0603|0805|1206|1210|1812|2010|2512)(?![0-9])|"
13
+ r"(?<![A-Z0-9])(SOT-?23(?:-\d)?|SOT-?223|SOIC-?\d+|SOP-?\d+|TSSOP-?\d+|QFN-?\d+|VQFN-?\d+|LQFP-?\d+|SMA|SMB|SOD-?\d+)(?![A-Z0-9])", re.I)
14
+ BODY_RE = re.compile(r"(?<![0-9.])(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)(?:x\d+(?:\.\d+)?)?mm", re.I)
15
+
16
+
17
+ def norm_mpn(s):
18
+ return re.sub(r"[^A-Z0-9]", "", str(s or "").upper())
19
+
20
+
21
+ _UNIT = r"((?i:ohms?|[fhω]))?(?![A-Za-z0-9])" # optional unit, then the number must end (74HC595 is not 74)
22
+
23
+
24
+ def parse_value(s, with_unit=False):
25
+ """'10k' / '10kΩ' / '4k7' / '100nF' / '0.1uF' / '10K 1%' / '0R' -> float, else None.
26
+ with_unit=True -> (value, unit) with unit in 'F', 'H', 'R' (ohm) or '' when not stated."""
27
+ s = re.sub(r"(?i)(?<=\d)meg", "M", str(s or "").strip())
28
+ m = re.match(r"^(\d+)([pnuµμmkKMGR])(\d+)" + _UNIT, s) # 4k7 / 4R7 notation
29
+ if m:
30
+ v, pre, u = float(f"{m.group(1)}.{m.group(3)}"), m.group(2), m.group(4)
31
+ else:
32
+ m = re.match(r"^(\d+(?:[.,]\d+)?)\s*([pnuµμmkKMGR]?)" + _UNIT, s) # '4,7k' / '2,2µF': European decimals
33
+ if not m:
34
+ return (None, "") if with_unit else None
35
+ v, pre, u = float(m.group(1).replace(",", ".")), m.group(2), m.group(3)
36
+ v *= SI["r" if pre == "R" else pre]
37
+ u = "R" if pre == "R" or (u or "").lower()[:1] in ("o", "ω") else (u or "").upper()
38
+ return (v, u) if with_unit else v
39
+
40
+
41
+ IPC_CHIP_RE = re.compile(r"(?:RES|CAP|IND|LED|DIO)C(\d{4})X\d+", re.I) # IPC-7351 chip names: RESC1608X55N
42
+ METRIC_TO_IMPERIAL = {"0402": "01005", "0603": "0201", "1005": "0402", "1608": "0603", "2012": "0805",
43
+ "3216": "1206", "3225": "1210", "4532": "1812", "5025": "2010", "6332": "2512"}
44
+
45
+
46
+ def package_of(*texts):
47
+ for t in texts:
48
+ m = IPC_CHIP_RE.search(str(t or ""))
49
+ if m and m.group(1) in METRIC_TO_IMPERIAL:
50
+ return METRIC_TO_IMPERIAL[m.group(1)]
51
+ m = PKG_RE.search(str(t or ""))
52
+ if m:
53
+ return (m.group(1) or m.group(2)).upper().replace("-", "")
54
+ return None
55
+
56
+
57
+ def _body(text):
58
+ m = BODY_RE.search(str(text or ""))
59
+ return tuple(sorted((float(m.group(1)), float(m.group(2))))) if m else None
60
+
61
+
62
+ @dataclass
63
+ class Symbol:
64
+ lib: str
65
+ name: str
66
+ props: dict = field(default_factory=dict)
67
+
68
+ @property
69
+ def mpn(self):
70
+ return next((v for k, v in self.props.items() if k.lower() in MPN_KEYS), "")
71
+
72
+ @property
73
+ def lcsc(self):
74
+ return next((v for k, v in self.props.items() if k.lower() in LCSC_KEYS), "")
75
+
76
+
77
+ class KiCadIndex:
78
+ def __init__(self, roots):
79
+ self.symbols, self.footprints = [], {} # footprints: "lib:name" -> {"path", "has_3d"}
80
+ for root in roots:
81
+ root = Path(root)
82
+ for f in root.rglob("*.kicad_sym"):
83
+ self._load_sym(f)
84
+ for d in root.rglob("*.pretty"):
85
+ for m in d.glob("*.kicad_mod"):
86
+ txt = m.read_text(encoding="utf-8", errors="replace")
87
+ self.footprints[f"{d.stem}:{m.stem}"] = {"path": str(m), "has_3d": "(model " in txt}
88
+
89
+ def _load_sym(self, f):
90
+ try:
91
+ tree = sexpr.parse(f.read_text(encoding="utf-8", errors="replace"))
92
+ except ValueError:
93
+ return
94
+ for s in sexpr.find(tree, "symbol"):
95
+ props = {p[1]: p[2] for p in sexpr.find(s, "property") if len(p) > 2}
96
+ self.symbols.append(Symbol(f.stem, s[1], props))
97
+
98
+ def footprint(self, ref):
99
+ """Resolve 'Lib:Name' (or bare name) to an indexed footprint."""
100
+ if not ref:
101
+ return None, None
102
+ if ref in self.footprints:
103
+ return ref, self.footprints[ref]
104
+ name = ref.split(":")[-1]
105
+ for k, v in self.footprints.items():
106
+ if k.split(":")[-1] == name:
107
+ return k, v
108
+ return None, None
109
+
110
+ def match(self, mpn="", lcsc="", value="", package="", footprint="", ref_class=""):
111
+ """Return (symbol, how) - by MPN, LCSC number, or (for passives) value + package.
112
+ ref_class (R, C, L...) keeps a C 10u 0805 line off an L 10u 0805 symbol."""
113
+ nm = norm_mpn(mpn)
114
+ for s in self.symbols:
115
+ if nm and nm in (norm_mpn(s.name), norm_mpn(s.mpn)):
116
+ return s, "mpn"
117
+ for s in self.symbols:
118
+ if lcsc and s.lcsc.upper() == lcsc.upper():
119
+ return s, "lcsc"
120
+ v, u = parse_value(value, True)
121
+ if v is not None and package:
122
+ for s in self.symbols:
123
+ sv, su = parse_value(s.props.get("Value"), True)
124
+ if (sv is not None and abs(sv - v) <= 1e-6 * v and not (u and su and u != su) # 0R == 0; 10uF != 10uH
125
+ and package_of(s.props.get("Footprint"), s.name) == package and self._same_class(s, ref_class)):
126
+ return s, "value+package"
127
+ return None, None
128
+
129
+ CLASS_PREFIX = {"R": ("R_",), "C": ("C_",), "L": ("L_",), "FB": ("L_", "FB_"), "D": ("D_", "LED_"), "LED": ("LED_",)}
130
+
131
+ def _same_class(self, s, ref_class):
132
+ if not ref_class:
133
+ return True
134
+ sref = re.match(r"[A-Z]+", str(s.props.get("Reference", "")).upper())
135
+ if sref:
136
+ return sref.group(0) == ref_class or (ref_class, sref.group(0)) in (("FB", "L"), ("LED", "D"), ("D", "LED"))
137
+ pref = self.CLASS_PREFIX.get(ref_class)
138
+ fp = str(s.props.get("Footprint", "")).split(":")[-1]
139
+ return pref is None or not fp or fp.startswith(pref)
140
+
141
+ def find_footprint_for_package(self, package, ref_class="", hint=""):
142
+ """Best-effort: an indexed footprint whose name mentions the package (e.g. 0603, SOIC8).
143
+ For 2-terminal passives the footprint family must match the designator class
144
+ (an LED 0603 must not land on R_0603)."""
145
+ if not package:
146
+ return None
147
+ pref, hits = self.CLASS_PREFIX.get(ref_class), []
148
+ for k in self.footprints:
149
+ name = k.split(":")[-1]
150
+ if package_of(name) == package and (pref is None or name.startswith(pref)):
151
+ if pref is None and re.match(r"(R|C|L|D|LED)_", name):
152
+ continue # don't hand a passive footprint to an IC/connector
153
+ hits.append(k)
154
+ bodies = {k: _body(k) for k in hits}
155
+ want = _body(hint)
156
+ if want: # body size known: it must agree (QFN-32 4x4 is not 5x5)
157
+ hits = [k for k in hits if bodies[k] in (want, None)]
158
+ elif len({b for b in bodies.values() if b}) > 1:
159
+ return None # several body sizes and nothing to choose by: ambiguous
160
+ return hits[0] if hits else None
ecadlib/kicad_menu.py ADDED
@@ -0,0 +1,193 @@
1
+ """The ecadlib window inside KiCad: what the PCB editor plugin runs (add part, check the project BOM, fill the library,
2
+ review queue), the same flow as the ecadlib menu in Altium (altium_menu), with a KiCad library as the target.
3
+
4
+ ecadlib kicad-menu run-job JOB --action add-part --project BOARD.kicad_pcb --mpn LM358DR [--manufacturer TI]
5
+ ecadlib kicad-menu run-job JOB --action check-bom|fill --project BOARD.kicad_pcb|SCHEMATIC.kicad_sch
6
+ ecadlib kicad-menu run-job JOB --action queue|open --project BOARD.kicad_pcb [--path REPORT]
7
+ ecadlib kicad-menu check-bom "%I" # the schematic editor's Tools > Generate BOM hook (a netlist or .kicad_sch)
8
+
9
+ The plugin (ecadlib/assets/kicad_action_plugin.py) runs in KiCad's own Python, starts `run-job` in the background and
10
+ reads JOB/progress.ini (live steps) and JOB/result.ini (the answer: big, row1..rowN "label|value|ok", message, fix,
11
+ footprint to place) - the same files, written the same way, as the Altium dialog reads.
12
+
13
+ Per project, next to the board: ecadlib-work/ is the ecadlib workspace (one parts list, uploads/altium-menu/parts.csv,
14
+ for every part added or filled, so one plan makes the whole library) and ecadlib/ the KiCad library (ecadlib.kicad_sym,
15
+ ecadlib.pretty, ecadlib.3dshapes, kicad_export.json). The plugin adds ${KIPRJMOD}/ecadlib/... rows to the project's
16
+ library tables and places the footprint; nothing here needs KiCad."""
17
+ import argparse, csv, json, struct, sys, zlib
18
+ from pathlib import Path
19
+
20
+ from . import altium_menu as AM
21
+
22
+ WORK, LIB = "ecadlib-work", "ecadlib"
23
+ ACTIONS = ("add-part", "check-bom", "fill", "queue", "open")
24
+
25
+
26
+ def project_dir(project):
27
+ p = Path(project)
28
+ return p if p.is_dir() else p.parent
29
+
30
+
31
+ def config(project, port=8765, offline=False):
32
+ """The menu settings for a KiCad project: workspace ecadlib-work/, library ecadlib/ next to the board."""
33
+ d = project_dir(project).resolve()
34
+ return {"workspace": d / WORK, "library": d / LIB, "name": LIB, "port": port, "offline": offline, "tool": "kicad"}
35
+
36
+
37
+ def schematic_of(project):
38
+ """The root schematic of the board's project: <stem>.kicad_sch next to the .kicad_pcb / .kicad_pro, or the only
39
+ .kicad_pro's. A .kicad_sch or a netlist is itself."""
40
+ p = Path(project)
41
+ if p.is_file() and p.suffix.lower() in (".kicad_sch", ".xml", ".net"):
42
+ return p
43
+ d = project_dir(p)
44
+ if p.suffix.lower() in (".kicad_pcb", ".kicad_pro") and p.with_suffix(".kicad_sch").is_file():
45
+ return p.with_suffix(".kicad_sch")
46
+ pros = sorted(d.glob("*.kicad_pro"))
47
+ if len(pros) == 1 and pros[0].with_suffix(".kicad_sch").is_file():
48
+ return pros[0].with_suffix(".kicad_sch")
49
+ raise ValueError(f"no schematic for {p.name}: save the schematic next to the board (same name, .kicad_sch)")
50
+
51
+
52
+ def adopt_old_list(cfg):
53
+ """Parts the first plugin (ecadlib-work/parts.csv) added go into the menu's list once."""
54
+ old = Path(cfg["workspace"]) / "parts.csv"
55
+ if not old.is_file() or (Path(cfg["workspace"]) / AM.MENU_BOM).is_file():
56
+ return 0
57
+ n = 0
58
+ for r in csv.DictReader(old.open(encoding="utf-8", newline="")):
59
+ if (r.get("Mfr. Part #") or "").strip():
60
+ AM.add_to_parts(cfg["workspace"], r["Mfr. Part #"], r.get("Manufacturer", ""))
61
+ n += 1
62
+ return n
63
+
64
+
65
+ def _exported_footprint(cfg, names):
66
+ rep = Path(cfg["library"]) / "kicad_export.json"
67
+ try:
68
+ parts = json.loads(rep.read_text(encoding="utf-8")).get("parts", [])
69
+ except (OSError, ValueError):
70
+ return ""
71
+ return next((p.get("footprint") or "" for p in parts if p.get("part") in names), "")
72
+
73
+
74
+ def add_part(cfg, mpn, manufacturer="", progress=None):
75
+ adopt_old_list(cfg)
76
+ res = AM.add_part(cfg, mpn, manufacturer, progress=progress)
77
+ if not res.get("footprint") and res.get("status") == "duplicate": # already in the library: place that one
78
+ res["footprint"] = _exported_footprint(cfg, {res.get("part"), *res.get("message", "").rstrip(".").split()[-1:]})
79
+ return res
80
+
81
+
82
+ def fill(cfg, project, progress=None):
83
+ """The schematic's fitted lines with a part number go into the menu's parts list; the librarian builds the list
84
+ and the whole list is exported (one plan = the whole KiCad library)."""
85
+ from .bom_triage import read_bom
86
+ adopt_old_list(cfg)
87
+ lines, _ = read_bom(schematic_of(project))
88
+ skipped = 0
89
+ for ln in lines:
90
+ if ln.get("dnp"):
91
+ continue
92
+ if not str(ln.get("mpn") or "").strip() or not AM.MPN_RE.match(str(ln["mpn"]).strip()):
93
+ skipped += 1
94
+ continue
95
+ AM.add_to_parts(cfg["workspace"], ln["mpn"], ln.get("manufacturer", ""), value=ln.get("value", ""),
96
+ footprint=ln.get("footprint", ""), designator=ln.get("designator", ""), lcsc=ln.get("lcsc", ""),
97
+ datasheet=ln.get("datasheet", ""))
98
+ res = AM.fill(cfg, Path(cfg["workspace"]) / AM.MENU_BOM, progress=progress)
99
+ if skipped:
100
+ from . import messages as M
101
+ res["message"] += f" {M.plural(skipped, 'schematic line')} without a part number left out."
102
+ return res
103
+
104
+
105
+ def run_action(cfg, action, project, mpn="", manufacturer="", path="", opener=None, progress=None):
106
+ if action == "add-part":
107
+ return add_part(cfg, mpn, manufacturer, progress=progress)
108
+ if action == "check-bom":
109
+ return AM.check_bom(cfg, schematic_of(project), opener=opener, progress=progress)
110
+ if action == "fill":
111
+ return fill(cfg, project, progress=progress)
112
+ if action == "queue":
113
+ return AM.open_queue(cfg, opener=opener)
114
+ if action == "open":
115
+ return AM.open_file(cfg, path, opener=opener)
116
+ raise ValueError(f"unknown action {action!r} (one of {', '.join(ACTIONS)})")
117
+
118
+
119
+ def run_job(job, action, project, mpn="", manufacturer="", path="", opener=None, offline=False):
120
+ """What the plugin runs: JOB/progress.ini while working, JOB/result.ini at the end. Returns the result dict."""
121
+ job = Path(job)
122
+ job.mkdir(parents=True, exist_ok=True)
123
+ try:
124
+ cfg = config(project, offline=offline)
125
+ res = run_action(cfg, action, project, mpn, manufacturer, path,
126
+ opener=False if action == "check-bom" else opener, progress=AM.Progress(job))
127
+ except Exception as e: # every failure becomes one dialog line with a fix
128
+ res = AM._error(e)
129
+ AM.write_result(job / "result.ini", res)
130
+ AM.prune_jobs(job.parent, keep=job)
131
+ (job / "exit.txt").write_text("0\n", encoding="utf-8")
132
+ return res
133
+
134
+
135
+ # ------------------------------------------------------------------ the toolbar icon
136
+ def icon_png(size=24):
137
+ """The ecadlib toolbar icon (a blue tile with three white shelves, the one on top shorter) as PNG bytes."""
138
+ blue, white = (0x2F, 0x77, 0xD9, 255), (255, 255, 255, 255)
139
+ rows = []
140
+ r = size // 6
141
+ for y in range(size):
142
+ line = bytearray([0])
143
+ for x in range(size):
144
+ cx, cy = min(x, size - 1 - x), min(y, size - 1 - y) # rounded corners
145
+ if cx < r and cy < r and (r - cx) ** 2 + (r - cy) ** 2 > r * r:
146
+ line += bytes(4)
147
+ continue
148
+ px = blue
149
+ for i, (y0, x1) in enumerate(((size * 5 // 24, size * 14 // 24), (size * 10 // 24, size * 18 // 24),
150
+ (size * 15 // 24, size * 18 // 24))):
151
+ if y0 <= y < y0 + max(2, size // 10) and size * 6 // 24 <= x < x1:
152
+ px = white
153
+ line += bytes(px)
154
+ rows.append(bytes(line))
155
+ def chunk(t, d):
156
+ return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
157
+ return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0))
158
+ + chunk(b"IDAT", zlib.compress(b"".join(rows), 9)) + chunk(b"IEND", b""))
159
+
160
+
161
+ def main(argv=None):
162
+ ap = argparse.ArgumentParser(prog="ecadlib kicad-menu",
163
+ description="what the ecadlib window in KiCad's PCB editor runs (add part, check BOM, "
164
+ "fill the library, review queue)")
165
+ sub = ap.add_subparsers(dest="cmd", required=True)
166
+ r = sub.add_parser("run-job", help="what the KiCad plugin runs: JOB/progress.ini while working, JOB/result.ini")
167
+ r.add_argument("job")
168
+ r.add_argument("--action", required=True, choices=ACTIONS)
169
+ r.add_argument("--project", required=True, help="the board (.kicad_pcb), its project or schematic")
170
+ r.add_argument("--mpn", default="")
171
+ r.add_argument("--manufacturer", default="")
172
+ r.add_argument("--path", default="", help="open: the report to open")
173
+ r.add_argument("--offline", action="store_true", help="cached downloads and standard parts only")
174
+ c = sub.add_parser("check-bom", help='check a schematic against the library and open the report (Generate BOM: "%%I")')
175
+ c.add_argument("project", help="netlist (.xml) KiCad's Generate BOM passes as %%I, a .kicad_sch or a board")
176
+ c.add_argument("output", nargs="?", help="KiCad's %%O (ignored)")
177
+ c.add_argument("--no-open", action="store_true")
178
+ c.add_argument("--json", action="store_true")
179
+ a = ap.parse_args(argv)
180
+ if a.cmd == "run-job":
181
+ res = run_job(a.job, a.action, a.project, a.mpn, a.manufacturer, a.path, offline=a.offline)
182
+ return 0 if res.get("status") != "error" else 1
183
+ try:
184
+ res = AM.check_bom(config(a.project), schematic_of(a.project), opener=False if a.no_open else None)
185
+ except (OSError, ValueError) as e:
186
+ print(f"ecadlib kicad-menu: {e}", file=sys.stderr)
187
+ return 2
188
+ print(json.dumps(res, indent=2) if a.json else res["message"])
189
+ return 0
190
+
191
+
192
+ if __name__ == "__main__":
193
+ sys.exit(main())