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/stdparts.py ADDED
@@ -0,0 +1,755 @@
1
+ """Standard parts without Claude: when the package is fully set by standard data, build the footprint (and, for the
2
+ part kinds whose pinout is standard, the symbol) offline and deterministically. The librarian's `standard` source.
3
+
4
+ ecadlib stdparts resolve "SOIC-8 3.9x4.9mm" | "0603" --kind R | --mpn GRM188R71H104KA93D --kind C what would be built, and why
5
+ ecadlib stdparts list [PATTERN] the packages the table knows
6
+ ecadlib stdparts bench [--kicad DIR] [--fixture F] every table entry against KiCad's official footprint
7
+ ecadlib stdparts coverage BOM [BOM ...] how many BOM lines this source resolves (no network)
8
+
9
+ Footprints come only from package_specs/standard.json.gz: 615 packages, each with its KiCad name and library. Their
10
+ dimensions are the KiCad footprint generator's size definitions (JEDEC / IPC-SM-782 / manufacturer package drawings,
11
+ built here with footprint_gen's IPC-7351B equations) or, for the packages KiCad draws by hand (SOD, SMA/SMB/SMC, MELF,
12
+ SOT-223, SOT-89, DPAK/D2PAK, TO-220, TO-92), the published recommended land as a pad table. Every entry was built and
13
+ compared with kicad-footprints (pads and courtyard within footprint_bench tolerances) before it went into the table;
14
+ `bench` repeats that check.
15
+
16
+ What resolves (first match wins; footprint text = the BOM footprint, the KiCad footprint name, a KiCad symbol's
17
+ Footprint property; then the description and value; then the MPN):
18
+ exact KiCad name "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", "SOT-23-5", "R_0603_1608Metric" (library prefix optional)
19
+ chip passives R / C / L / LED / D / fuse by the part's kind and a size code: imperial 01005..2512 ("0603",
20
+ "C0805", "0603 (1608)"), metric with a marker ("1608Metric", "1608M"), IPC names (RESC1608X55N),
21
+ or the MPN (Yageo RC/CC, Murata GRM/GCM/GJM/LQG/LQM/LQW/BLM, Samsung CL, KEMET C....C, TDK C<metric>,
22
+ Panasonic ERJ-, Vishay CRCW/TNPW, Stackpole RMCF, Bourns CR, KOA RK73, Uni-Royal). Two different
23
+ sizes from two places (footprint 0603, MPN GRM21 = 0805) is a conflict: nothing is built.
24
+ named packages SOT-23/-5/-6, SOT-323/353/363, SOT-523, SOT-89, SOD-123(F)/323(F)/523/923/128, SMA/SMB/SMC/SMF
25
+ (DO-214), MiniMELF/MELF, DPAK/D2PAK (TO-252/263), TO-220 (vertical), TO-92 (inline), and
26
+ SOIC/SO/TSSOP/MSOP/SSOP/QSOP/QFN/DFN/SON/QFP families from the name (pins, body, pitch, exposed pad,
27
+ KiCad or LCSC style "QFN-32_L5.0-W5.0-P0.50-EP3.5"). Plain SOIC-8/14/16 means JEDEC MS-012
28
+ (3.9 mm body), SOIC-nW MS-013 (7.5 mm), TSSOP-8..28 MO-153 (4.4 mm, 0.65 mm pitch), MSOP-8 3x3
29
+ 0.65 mm, QFP without "EP" no exposed pad. Anything else must be in the name.
30
+ ambiguous -> nothing: a name that fits two different footprints (QFN-16 without a body size, LQFP-64 without body and
31
+ pitch, "SOT-23-3", which KiCad uses for the 1.6 mm MO-178 body and distributors for TO-236,
32
+ bare "SOT-223" / TSOT-23 / SC-70 / SC-75 / SOT-26 (tab numbering or body differ by vendor),
33
+ power packages (PowerPAK, LFPAK, SO-8FL ...), TO-220 marked horizontal, a diode in a TO/DPAK
34
+ package, "SMA" on a part that is not a diode); the next source tries. Uniqueness is judged
35
+ against every generic name in KiCad's library, not only this table. A description is weaker
36
+ evidence than a footprint field: there no JEDEC defaults apply, IC families need the body
37
+ size, and a bare family (SOT-23, TO-220 ...) only counts for transistors and diodes.
38
+
39
+ Symbols (only when the pinout is standard for the part): R, C, C_Polarized, L, LED and diodes (2-pin packages: pin 1
40
+ cathode, as KiCad's footprints), and these MPN families: SOT-23 BJTs (MMBT39xx/2222/2907/4401/4403/5401/5551/A42/A92,
41
+ BC807/817/846-860, S8050/S8550: 1=B 2=E 3=C), SOT-23 MOSFETs (2N7002, BSS138/123/84, AO34xx, SI230x, DMG230x,
42
+ IRLML: 1=G 2=S 3=D), TO-92 2N39xx/2N2222A/2N440x/2N5401/5551 (E B C), BC327/337/546-558 (C B E), 2N7000 (S G D),
43
+ BS170 (D G S), 1117-type LDOs (AMS1117, LM1117, LD1117S ...) in SOT-223-3_TabPin2 (1=GND/ADJ 2=VOUT 3=VIN,
44
+ tab = 2), 78xx/79xx and LM317 in TO-220, 78Lxx in TO-92/SOT-89. Other parts get the footprint only and the symbol comes from the next source.
45
+ The librarian records `source: standard:<Lib>:<Name>` (footprint) and `standard:<symbol kind>`; it tries this
46
+ source after kicad and easyeda and before datasheet, and never replaces a symbol or footprint an earlier source or
47
+ the user's own library already filled."""
48
+ import argparse, functools, gzip, json, math, re, sys
49
+ from pathlib import Path
50
+
51
+ from . import footprint_gen as F
52
+
53
+ TABLE = Path(__file__).with_name("package_specs") / "standard.json.gz"
54
+ IMPERIAL = ("01005", "0201", "0402", "0603", "0805", "1008", "1206", "1210", "1806", "1812", "1825", "2010", "2220",
55
+ "2225", "2512", "2816", "2920", "3640", "4020")
56
+ METRIC = {"0402": "01005", "0603": "0201", "1005": "0402", "1608": "0603", "2012": "0805", "2520": "1008", "3216": "1206",
57
+ "3225": "1210", "4516": "1806", "4532": "1812", "4564": "1825", "5025": "2010", "5750": "2220", "5763": "2225",
58
+ "6332": "2512", "7142": "2816", "7451": "2920", "9116": "3640", "10251": "4020"}
59
+ IMP2MET = {v: k for k, v in METRIC.items()}
60
+ CHIP_PREFIX = {"R": "R", "C": "C", "L": "L", "LED": "LED", "D": "D", "F": "Fuse"}
61
+ # named packages: normalised text (upper case, no spaces / dashes / underscores) -> table name; diode-only ones need a diode
62
+ ALIASES = {
63
+ "SOT23": "SOT-23", "TO236": "SOT-23", "TO236AB": "SOT-23",
64
+ "SOT235": "SOT-23-5", "SOT25": "SOT-23-5", "SOT753": "SOT-23-5",
65
+ "SOT236": "SOT-23-6", "SOT26": None, # SOT-26 is also sold as SC-74 (1.5 mm body)
66
+ "TSOT235": "TSOT-23-5", "TSOT236": "TSOT-23-6", "TSOT23": None,
67
+ "SOT323": "SOT-323_SC-70", "SC70": None, "SC703": "SOT-323_SC-70",
68
+ "SOT353": "SOT-353_SC-70-5", "SC705": "SOT-353_SC-70-5", "SC88A": "SOT-353_SC-70-5",
69
+ "SOT363": "SOT-363_SC-70-6", "SC706": "SOT-363_SC-70-6", "SC88": "SOT-363_SC-70-6",
70
+ "SOT523": "SOT-523", "SC75": None, "SOT883": "SOT-883", # SC-75 is SOT-416 or SOT-523
71
+ "SOT223": None, "SOT2233": None, "TO261": None, "TO261AA": None, # tab = pin 2 or pin 4: the part decides
72
+ "SOT89": "SOT-89-3", "SOT893": "SOT-89-3", "TO243": "SOT-89-3", "TO243AA": "SOT-89-3",
73
+ "DPAK": "TO-252-2", "TO252": "TO-252-2", "TO252AA": "TO-252-2", "TO2522": "TO-252-2", "TO2525": None,
74
+ "D2PAK": "TO-263-2", "TO263": "TO-263-2", "TO263AB": "TO-263-2", "TO2632": "TO-263-2", "TO2635": None, # 5-lead: KiCad counts 4 or 5
75
+ "TO220": "TO-220-3_Vertical", "TO2203": "TO-220-3_Vertical", "TO220AB": "TO-220-3_Vertical",
76
+ "TO92": "TO-92_Inline", "TO923": "TO-92_Inline", "TO92A": None,
77
+ }
78
+ DIODE_ALIASES = {
79
+ "SOD123": "D_SOD-123", "SOD123F": "D_SOD-123F", "SOD323": "D_SOD-323", "SC76": "D_SOD-323", "SC90": "D_SOD-323",
80
+ "SOD323F": "D_SOD-323F", "SOD523": "D_SOD-523", "SC79": "D_SOD-523", "SOD923": "D_SOD-923", "SOD128": "D_SOD-128",
81
+ "SMA": "D_SMA", "DO214AC": "D_SMA", "SMB": "D_SMB", "DO214AA": "D_SMB", "SMC": "D_SMC", "DO214AB": "D_SMC",
82
+ "SMF": "D_SMF", "DO219AB": "D_SMF", "MINIMELF": "D_MiniMELF", "SOD80": "D_MiniMELF", "LL34": "D_MiniMELF",
83
+ "MELF": "D_MELF", "DO213AB": "D_MELF", "LL41": "D_MELF",
84
+ }
85
+ AMBIGUOUS = {"SOT233": "KiCad's SOT-23-3 is the 1.6 mm MO-178 body; distributors use SOT-23-3 for TO-236 SOT-23",
86
+ "SOT223": "SOT-223: tab numbered 2 (SOT-223-3_TabPin2, regulators) or 4 (KiCad's SOT-223)",
87
+ "SOT2233": "SOT-223: tab numbered 2 or 4", "TO261": "SOT-223: tab numbered 2 or 4", "TO261AA": "SOT-223: tab numbered 2 or 4",
88
+ "TSOT23": "TSOT-23 without a pin count", "TO92A": "TO-92 variant", "SOT23W": "wide SOT-23",
89
+ **{k: "KiCad has TO-252-2 and TO-252-2_TabPin1 (tab numbered 2 or 1): the part decides"
90
+ for k in ("TO2522", "TO252", "TO252AA", "DPAK")},
91
+ **{k: "KiCad has TO-263-2 and TO-263-2_TabPin1 (tab numbered 2 or 1): the part decides"
92
+ for k in ("TO2632", "TO263", "TO263AB", "D2PAK")}}
93
+ # IC families from the name: generic table entries are '<FAM>-<n>[W][-1EP]_<a>x<b>mm_P<p>mm[_EP<x>x<y>mm]'
94
+ NAME_RE = re.compile(r"^(?P<fam>[A-Z]+)-(?P<pins>\d+)(?P<w>W)?(?:-(?P<ep>\d)EP)?_(?P<a>[\d.]+)x(?P<b>[\d.]+)mm"
95
+ r"_P(?P<p>[\d.]+)mm(?:_EP(?P<ea>[\d.]*\d)x(?P<eb>[\d.]*\d)mm)?(?P<rest>_.+)?$")
96
+ FAMILIES = ("SOIC", "SO", "SOP", "TSSOP", "HTSSOP", "MSOP", "VSSOP", "SSOP", "QSOP", "TSOP", "QFN", "VQFN", "WQFN", "UQFN",
97
+ "TQFN", "HVQFN", "LQFN", "DFN", "TDFN", "WDFN", "UDFN", "SON", "VSON", "WSON", "USON", "LQFP", "TQFP", "QFP",
98
+ "PQFP", "LFCSP")
99
+ NO_LEAD = {"QFN", "VQFN", "WQFN", "UQFN", "TQFN", "HVQFN", "LQFN", "DFN", "TDFN", "WDFN", "UDFN", "SON", "VSON", "WSON", "USON", "LFCSP"}
100
+ QUAD_LEADED = {"LQFP", "TQFP", "QFP", "PQFP"}
101
+ FREE_RE = re.compile(r"(?<![A-Z0-9])(?P<fam>" + "|".join(sorted(FAMILIES, key=len, reverse=True)) +
102
+ r")[-_ ]?(?P<pins>\d{1,3})(?P<w>W)?(?:-(?P<ep>\d)EP)?(?![0-9A-Z])", re.I)
103
+ POWER_PKG = re.compile(r"Power ?PAK|PowerDI|PowerSO|PowerFLAT|LFPAK|DirectFET|PQFN|\bSO-?8FL|\bWDFN\d|TSDSON|SuperSO|TOLL|\bPDFN", re.I)
104
+ MIL_BODY = {"150": 3.9, "208": 5.3, "209": 5.3, "300": 7.5}
105
+ NO_GO = re.compile(r"\b(horizontal|lying|right[- ]angle|bent)\b", re.I)
106
+
107
+
108
+ # ---------------------------------------------------------------- the table
109
+ @functools.lru_cache(maxsize=1)
110
+ def table():
111
+ with gzip.open(TABLE, "rt", encoding="utf-8") as f:
112
+ return json.load(f)
113
+
114
+
115
+ def packages():
116
+ return table()["packages"]
117
+
118
+
119
+ def spec(name):
120
+ """Table entry -> footprint_gen spec (a fresh copy with its name)."""
121
+ e = packages()[name]
122
+ s = {k: v for k, v in json.loads(json.dumps(e)).items() if k != "lib"}
123
+ s["name"] = name
124
+ return s
125
+
126
+
127
+ def kicad_ref(name):
128
+ return f"{packages()[name]['lib']}:{name}"
129
+
130
+
131
+ @functools.lru_cache(maxsize=4096)
132
+ def build(name, density=None):
133
+ """(kicad_mod text, report) for a table entry."""
134
+ return F.build(spec(name), density)
135
+
136
+
137
+ @functools.lru_cache(maxsize=1)
138
+ def _generic():
139
+ """Every generic IC footprint name in KiCad's library (the table's kicad_generic_names, built and checked or
140
+ not), parsed: [(attrs, name)]. Ambiguity is judged against all of them, so a name that fits two KiCad packages
141
+ is ambiguous even when only one of them is in the checked table."""
142
+ out = []
143
+ for n in sorted(set(table().get("kicad_generic_names") or []) | set(packages())):
144
+ m = NAME_RE.match(n)
145
+ if m and m.group("fam") in FAMILIES:
146
+ g = m.groupdict()
147
+ out.append(({"fam": g["fam"], "pins": int(g["pins"]), "w": bool(g["w"]), "ep": bool(g["ep"]),
148
+ "body": tuple(sorted((float(g["a"]), float(g["b"])))), "pitch": float(g["p"]),
149
+ "epsize": tuple(sorted((float(g["ea"]), float(g["eb"])))) if g["ea"] else None,
150
+ "variant": bool(g["rest"])}, n))
151
+ return out
152
+
153
+
154
+ # ---------------------------------------------------------------- name parsing
155
+ def _norm(s):
156
+ return re.sub(r"[^A-Z0-9]", "", str(s or "").upper())
157
+
158
+
159
+ def _strip_lib(s):
160
+ s = str(s or "").strip()
161
+ return s.split(":", 1)[1] if re.match(r"^[A-Za-z0-9_.\-]+:[^:]", s) and not re.match(r"^[A-Z]:[\\/]", s) else s
162
+
163
+
164
+ def chip_size(text):
165
+ """Imperial size code from a footprint / description text, or None. Bare 4-digit codes are imperial (the
166
+ industry default); metric needs a marker (1608Metric, 1608M, '0603 (1608)' gives both and they must agree)."""
167
+ t = str(text or "")
168
+ found = set()
169
+ m = re.search(r"(?:RES|CAP|IND|LED|DIO|FUS)C(\d{4})X\d+", t, re.I) # IPC-7351 names: RESC1608X55N
170
+ if m and m.group(1) in METRIC:
171
+ found.add(METRIC[m.group(1)])
172
+ for m in re.finditer(r"(?<![0-9])(\d{4,5})\s*(?:_?Metric|M)(?![A-Za-z0-9])", t, re.I):
173
+ if m.group(1) in METRIC:
174
+ found.add(METRIC[m.group(1)])
175
+ for m in re.finditer(r"(?<![0-9.])(\d{4,5})(?![0-9.])(?!\s*(?:_?Metric|M\b|mm|mil|V|W|K|R|nF|pF|uF|µF|Ω|ohm|%))", t, re.I):
176
+ code = m.group(1)
177
+ if code in IMPERIAL and not re.search(r"(?:RES|CAP|IND|LED|DIO|FUS)C$", t[:m.start()], re.I):
178
+ found.add(code)
179
+ return found.pop() if len(found) == 1 else ("conflict:" + "/".join(sorted(found)) if found else None)
180
+
181
+
182
+ # MPN -> (kind, imperial size); kind "" = any passive kind (decided by the part)
183
+ def _mpn_rules():
184
+ m3 = {"03": "0201", "15": "0402", "18": "0603", "21": "0805", "31": "1206", "32": "1210", "43": "1812", "55": "2220"}
185
+ cl = {"03": "0201", "05": "0402", "10": "0603", "21": "0805", "31": "1206", "32": "1210", "43": "1812", "55": "2220"}
186
+ erj = {"XG": "01005", "1G": "0201", "2": "0402", "3": "0603", "6": "0805", "8": "1206", "14": "1210", "12Z": "2010",
187
+ "12": "1812", "1T": "2512", "P03": "0603", "P06": "0805", "P08": "1206", "PA2": "0402", "PA3": "0603", "PA6": "0805",
188
+ "PA8": "1206"}
189
+ koa = {"1H": "0201", "1E": "0402", "1J": "0603", "2A": "0805", "2B": "1206", "2E": "1210", "2H": "2010", "3A": "2512"}
190
+ imp = "(01005|0201|0402|0603|0805|1206|1210|1812|2010|2512)"
191
+ return [
192
+ (re.compile(r"^(?:RC|RT|RL|AC|AF|AT|PE|PT|RV)" + imp + r"[A-Z]"), "R", None), # Yageo resistors
193
+ (re.compile(r"^CC" + imp + r"[A-Z]{2}"), "C", None), # Yageo MLCC
194
+ (re.compile(r"^(?:GRM|GCM|GJM|GCJ|GRT|GCQ|GQM|GMA)(\d\d)"), "C", m3), # Murata MLCC
195
+ (re.compile(r"^(?:LQG|LQM|LQW|LQP)(\d\d)"), "L", m3), (re.compile(r"^BLM(\d\d)"), "L", m3), # Murata L, beads
196
+ (re.compile(r"^CL(\d\d)[A-Z]\d"), "C", cl), # Samsung MLCC
197
+ (re.compile(r"^C(0402|0603|1005|1608|2012|3216|3225|4532|5750)(?:C0G|NP0|X[5-8][A-Z]|JB|CH)"), "C", "metric"), # TDK
198
+ # KEMET MLCC: C<imperial><C standard | X flexible termination | F ...><capacitance>; C0603X563K1RACTU is an
199
+ # imperial 0603, not TDK's metric 0603 (X followed by a digit is KEMET's capacitance code, not a dielectric)
200
+ (re.compile(r"^C" + imp + r"[CXF]\d{3}[A-Z]"), "C", None),
201
+ (re.compile(r"^ERJ-?(XG|1G|1T|12Z|12|14|PA[2368]|P0[368]|[2368])"), "R", erj), # Panasonic
202
+ (re.compile(r"^(?:CRCW|TNPW|RCG|RCS)" + imp), "R", None), # Vishay
203
+ (re.compile(r"^RMCF" + imp), "R", None), (re.compile(r"^CR" + imp + r"-"), "R", None), # Stackpole, Bourns
204
+ (re.compile(r"^RK73[HB](1H|1E|1J|2A|2B|2E|2H|3A)"), "R", koa), # KOA
205
+ (re.compile(r"^" + imp + r"W[A-Z]{2}[A-Z0-9]"), "R", None), # Uni-Royal 0603WAF1002T5E
206
+ ]
207
+
208
+
209
+ MPN_RULES = _mpn_rules()
210
+
211
+
212
+ def mpn_size(mpn):
213
+ """-> (kind, imperial size) or (None, None)."""
214
+ s = re.sub(r"\s+", "", str(mpn or "").upper())
215
+ for rx, kind, conv in MPN_RULES:
216
+ m = rx.match(s)
217
+ if m:
218
+ code = m.group(1)
219
+ size = METRIC.get(code) if conv == "metric" else conv.get(code) if isinstance(conv, dict) else code
220
+ if size:
221
+ return kind, size
222
+ return None, None
223
+
224
+
225
+ def _free_attrs(text):
226
+ """IC family attributes from free text (BOM footprint, LCSC package, description)."""
227
+ t = str(text or "")
228
+ m = FREE_RE.search(t)
229
+ if not m:
230
+ return None
231
+ a = {"fam": m.group("fam").upper(), "pins": int(m.group("pins")), "w": bool(m.group("w")), "ep": True if m.group("ep") else None,
232
+ "body": None, "pitch": None, "epsize": None}
233
+ rest = t[m.end():] + " " + t[:m.start()]
234
+ v = re.match(r"-(N?\d+)(?![\d.]|\s*(?:[xX×*]|mm|mil|EP))", t[m.end():], re.I) # SOIC-8-N7, SOIC-14-16: pins removed
235
+ if v:
236
+ a["variant"] = v.group(0)
237
+ b = (re.search(r"(?<![\d.])(\d+(?:\.\d+)?)\s*[xX×*]\s*(\d+(?:\.\d+)?)\s*(?:x\s*[\d.]+\s*)?(?:mm)?", rest)
238
+ or re.search(r"(?<![A-Z])L(\d+(?:\.\d+)?)-W(\d+(?:\.\d+)?)", rest)) # LCSC L5.0-W5.0
239
+ if b:
240
+ a["body"] = tuple(sorted((float(b.group(1)), float(b.group(2)))))
241
+ p = re.search(r"(?<![A-Z])P\s?(\d+(?:\.\d+)?)\s*(?:mm)?(?![\d.])", rest) or re.search(r"(\d\.\d+)\s*mm\s*pitch|pitch\s*(\d\.\d+)", rest, re.I)
242
+ if p:
243
+ a["pitch"] = float(next(g for g in p.groups() if g))
244
+ e = re.search(r"(?<![A-Z])EP\s?(\d+(?:\.\d+)?)(?:\s*[xX×]\s*(\d+(?:\.\d+)?))?", rest)
245
+ if e:
246
+ a["ep"] = True
247
+ a["epsize"] = tuple(sorted((float(e.group(1)), float(e.group(2) or e.group(1)))))
248
+ elif re.search(r"exposed pad|thermal pad|\bEP\b|-EP\b|_EP\b|PowerPAD", rest, re.I):
249
+ a["ep"] = True
250
+ mil = re.search(r"(150|208|209|300)\s*mil", t, re.I)
251
+ if mil and not a["body"]:
252
+ a["body_w"] = MIL_BODY[mil.group(1)]
253
+ return a
254
+
255
+
256
+ def _defaults(a):
257
+ """JEDEC defaults where the bare family name means one package."""
258
+ fam, n = a["fam"], a["pins"]
259
+ if fam == "SO" or (fam == "SOP" and n in (8, 14, 16) and (a.get("body_w") == 3.9 or (a["body"] or (0,))[0] == 3.9)):
260
+ a["fam"] = fam = "SOIC" # SO-8, and SOP-8 150 mil (LCSC), are JEDEC SOIC
261
+ if fam == "SOIC" and a["pitch"] is None:
262
+ a["pitch"] = 1.27
263
+ if fam == "SOIC" and not a["body"] and not a.get("body_w"):
264
+ a["body_w"] = 7.5 if a["w"] else 3.9 if n in (8, 14, 16) else None
265
+ if fam == "TSSOP" and not a["body"] and not a.get("body_w") and n in (8, 14, 16, 20, 24, 28) and a["pitch"] in (None, 0.65):
266
+ a["body_w"], a["pitch"] = 4.4, 0.65
267
+ if fam == "MSOP" and not a["body"] and n == 8 and a["pitch"] in (None, 0.65):
268
+ a["body"], a["pitch"] = (3.0, 3.0), 0.65
269
+ if fam not in NO_LEAD and a["ep"] is None: # leaded packages with a pad name it (HTSSOP, -1EP, PowerPAD)
270
+ a["ep"] = False
271
+ return a
272
+
273
+
274
+ def _close(x, y, tol=0.051):
275
+ return abs(x - y) <= tol
276
+
277
+
278
+ def _candidates(a):
279
+ out = []
280
+ for g, n in _generic():
281
+ if g["fam"] != a["fam"] or g["pins"] != a["pins"]:
282
+ continue
283
+ if a["fam"] == "SOIC" and g["w"] != a["w"] and not a["body"]:
284
+ continue
285
+ if a["body"] and not all(_close(x, y) for x, y in zip(g["body"], a["body"])):
286
+ continue
287
+ if a.get("body_w") and not _close(g["body"][0], a["body_w"]):
288
+ continue
289
+ if a["pitch"] is not None and not _close(g["pitch"], a["pitch"], 0.011):
290
+ continue
291
+ if a["ep"] is not None and g["ep"] != a["ep"]:
292
+ continue
293
+ if a["epsize"] and not (g["epsize"] and all(_close(x, y) for x, y in zip(g["epsize"], a["epsize"]))):
294
+ continue
295
+ if g["variant"] and n not in packages(): # _Mask / _PullBack ... variants: count only when we can build them
296
+ continue
297
+ out.append(n)
298
+ return out
299
+
300
+
301
+ def _same_land(names):
302
+ """True when every candidate gives the same pads (then the name is not really ambiguous)."""
303
+ from . import footprint_bench as B
304
+ ref = None
305
+ for n in names:
306
+ pads = sorted((p[0], p[1], p[2], p[3], p[4]) for p in B.numbers(build(n)[0])["pads"])
307
+ if ref is None:
308
+ ref = pads
309
+ elif len(pads) != len(ref) or any(p[0] != q[0] or max(abs(x - y) for x, y in zip(p[1:], q[1:])) > 0.01 for p, q in zip(pads, ref)):
310
+ return False
311
+ return True
312
+
313
+
314
+ def ic_package(text, defaults=True):
315
+ """IC package name from free text -> (table name, how) / (None, why) / (None, None) when no family is named.
316
+ defaults=False (descriptions): no JEDEC default body / pitch; the text alone must pick one package."""
317
+ if POWER_PKG.search(str(text or "")):
318
+ return None, f"power package ({POWER_PKG.search(str(text)).group(0)}): not a standard JEDEC outline"
319
+ a = _free_attrs(text)
320
+ if not a:
321
+ return None, None
322
+ if a.get("variant"):
323
+ return None, f"{a['fam']}-{a['pins']}{'W' if a['w'] else ''}{a['variant']}: a variant with pins removed, not the standard package"
324
+ if not defaults and not a["body"]:
325
+ return None, f"{a['fam']}-{a['pins']} in the description without a body size"
326
+ label = f"{a['fam']}-{a['pins']}" + ("W" if a["w"] else "")
327
+ # Never a guess: a wrong footprint skips the datasheet reader. The name must give the body (SO-14 is 3.9 or 5.3 mm,
328
+ # SOIC-16W 10.3 or 12.8 mm long, TDFN-12 3x3 at 0.4 or 0.5 mm) and, with an exposed pad, its size. MSOP-8 is the
329
+ # one family name JEDEC (MO-187-AA) gives a single body.
330
+ if not a["body"] and not a.get("body_w") and not (a["fam"] == "MSOP" and a["pins"] == 8 and not a["ep"]):
331
+ return None, f"{label}: no body size in the name (several bodies share it)"
332
+ if (a["ep"] or a["fam"] in NO_LEAD) and not a["epsize"]:
333
+ return None, f"{label}: no exposed-pad size in the name; give e.g. _EP2.1x2.1mm"
334
+ a = _defaults(a) if defaults else dict(a, fam="SOIC" if a["fam"] == "SO" else a["fam"])
335
+ c = _candidates(a)
336
+ if not c and a["fam"] in NO_LEAD: # VQFN/WQFN/...: same land as QFN, height differs
337
+ for fam in ("QFN", "DFN") if a["fam"] not in ("QFN", "DFN") else ():
338
+ c = _candidates(dict(a, fam=fam))
339
+ if c:
340
+ break
341
+ if not c:
342
+ return None, f"{label}: no standard package with those dimensions in the table"
343
+ if len(c) > 1 and not (all(n in packages() for n in c) and _same_land(c)):
344
+ miss = [k for k in ("body", "pitch", "epsize") if not a.get(k) and not (k == "body" and a.get("body_w"))]
345
+ return None, f"{label}: ambiguous ({len(c)} KiCad packages fit, e.g. {', '.join(sorted(c)[:3])}; give the {'/'.join(miss) or 'full name'})"
346
+ best = sorted(c, key=lambda n: (n not in packages(), len(n)))[0]
347
+ if best not in packages():
348
+ return None, f"{label} -> {best}: not in the checked table (ecadlib's IPC build differs from KiCad's)"
349
+ return best, f"{label} -> {best}"
350
+
351
+
352
+ BARE_FAMILY = {"SOT23", "TO220", "TO220AB", "TO263", "TO252", "DPAK", "D2PAK", "TO92"} # in a description: often the family
353
+
354
+
355
+ def named_package(text, diode=False, weak=False, kind=None):
356
+ """SOT/SOD/DO-214/TO names -> (table name, how) / (None, why) / (None, None). weak (a description): a bare family
357
+ name (SOT-23, TO-220 ...) counts only for a transistor or diode, where it means the 3-lead package."""
358
+ if POWER_PKG.search(str(text or "")):
359
+ return None, f"power package ({POWER_PKG.search(str(text)).group(0)}): not a standard JEDEC outline"
360
+ t = str(text or "").upper()
361
+ for m in re.finditer(r"(?<![A-Z0-9])(SOT|TSOT|SOD|SC|DO|TO|SM[ABCF]|D2PAK|DPAK|MINIMELF|MELF|LL)[-_ ]?(\d{0,3}[A-Z]{0,2})(?:[-_ ]?(\d)L?)?(?![A-Z0-9]|[-_ ]?\d)", t):
362
+ key = _norm(m.group(0))
363
+ key = re.sub(r"L$", "", key) if re.fullmatch(r"(SOT|TSOT)\d+\dL", key) else key
364
+ if key in AMBIGUOUS:
365
+ return None, f"{m.group(0).strip()}: ambiguous ({AMBIGUOUS[key]})"
366
+ if diode and re.match(r"(TO2[256]|D2?PAK)", key):
367
+ return None, f"{m.group(0).strip()}: a diode in a TO / DPAK package has 2 or 3 leads"
368
+ if weak and key in BARE_FAMILY and kind not in ("Q", "D", "LED"):
369
+ return None, f"{m.group(0).strip()} in the description: the package family, pin count not given"
370
+ name = ALIASES.get(key, "") if key in ALIASES else DIODE_ALIASES.get(key) if diode else None
371
+ if key in ALIASES and name is None:
372
+ return None, f"{m.group(0).strip()}: ambiguous"
373
+ if key in DIODE_ALIASES and not diode and key not in ALIASES:
374
+ return None, f"{m.group(0).strip()}: a diode package name on a part that is not a diode"
375
+ if name and name.startswith(("TO-220", "TO-92")) and NO_GO.search(str(text)):
376
+ return None, f"{m.group(0).strip()}: horizontal mounting is not the standard footprint"
377
+ if name and name in packages():
378
+ return name, f"{m.group(0).strip()} -> {name}"
379
+ return None, None
380
+
381
+
382
+ def footprint_for(texts, kind=None, mpn="", weak=()):
383
+ """texts: footprint-like strings in priority order (BOM footprint, KiCad footprint names); weak: descriptions and
384
+ values, trusted less (no JEDEC default sizes for IC families). kind: R C L LED D F or None.
385
+ -> (table name, how) or (None, why)."""
386
+ pk = packages()
387
+ texts = [str(t) for t in texts if str(t or "").strip()]
388
+ weak = [str(t) for t in weak if str(t or "").strip()]
389
+ low = {n.lower(): n for n in pk}
390
+ for t in texts: # exact KiCad name, library prefix optional
391
+ n = low.get(_strip_lib(t).strip().lower())
392
+ if n and _norm(n) in AMBIGUOUS and _strip_lib(t) == t.strip():
393
+ return None, f"{n}: ambiguous without a library prefix ({AMBIGUOUS[_norm(n)]})"
394
+ if n:
395
+ if NAME_RE.match(n) or not n.startswith(("R_", "C_", "L_", "LED_", "D_", "Fuse_")) or _chip_kind_ok(n, kind):
396
+ return n, f"KiCad name {n}"
397
+ if kind in CHIP_PREFIX: # chip passives: size from the texts, then the MPN
398
+ sizes = {}
399
+ for t in texts + weak:
400
+ s = chip_size(t)
401
+ if s:
402
+ sizes.setdefault(s, t)
403
+ mk, ms = mpn_size(mpn)
404
+ if ms and (mk == kind or (mk == "C" and kind == "C") or (mk == "L" and kind == "L")):
405
+ sizes.setdefault(ms, f"MPN {mpn}")
406
+ elif ms and mk != kind:
407
+ return None, f"MPN {mpn} looks like a {mk} ({ms}) but the part is a {kind}"
408
+ real = {s for s in sizes if not s.startswith("conflict")}
409
+ if any(s.startswith("conflict") for s in sizes) or len(real) > 1:
410
+ return None, "size codes disagree: " + ", ".join(f"{s} ({w})" for s, w in sizes.items())
411
+ if real:
412
+ s = real.pop()
413
+ n = f"{CHIP_PREFIX[kind]}_{s}_{IMP2MET[s]}Metric"
414
+ if n in pk:
415
+ return n, f"{kind} chip {s} from {sizes[s]}"
416
+ return None, f"{kind} chip {s}: not a standard KiCad size"
417
+ why = None
418
+ for t in texts + weak:
419
+ n, how = named_package(t, diode=kind in ("D", "LED"), weak=t not in texts, kind=kind)
420
+ if n or how:
421
+ return n, how
422
+ n, how = ic_package(t, defaults=t in texts)
423
+ if n or (how and t in texts):
424
+ return n, how + ("" if t in texts else " (from the description)") if how else how
425
+ why = why or how
426
+ return None, why or "no standard package named"
427
+
428
+
429
+ def _chip_kind_ok(name, kind):
430
+ pre = name.split("_")[0]
431
+ return kind is None or CHIP_PREFIX.get(kind) == pre or (kind == "D" and pre == "D")
432
+
433
+
434
+ # ---------------------------------------------------------------- symbols (KiCad .kicad_sym text)
435
+ def _pin(num, name, typ, x, y, ang, ln=2.54, hide_name=True):
436
+ return (f'(pin {typ} line (at {x:g} {y:g} {ang}) (length {ln:g}) (name "{name}" (effects (font (size 1.27 1.27))))'
437
+ f' (number "{num}" (effects (font (size 1.27 1.27)))))')
438
+
439
+
440
+ def _poly(pts, fill="none", w=0.254):
441
+ return (f"(polyline (pts {' '.join(f'(xy {x:g} {y:g})' for x, y in pts)}) (stroke (width {w:g}) (type default))"
442
+ f" (fill (type {fill})))")
443
+
444
+
445
+ def _rect(x1, y1, x2, y2, fill="none", w=0.254):
446
+ return f"(rectangle (start {x1:g} {y1:g}) (end {x2:g} {y2:g}) (stroke (width {w:g}) (type default)) (fill (type {fill})))"
447
+
448
+
449
+ def _circle(x, y, r, w=0.254):
450
+ return f"(circle (center {x:g} {y:g}) (radius {r:g}) (stroke (width {w:g}) (type default)) (fill (type none)))"
451
+
452
+
453
+ def _arc(s, m, e, w=0.254):
454
+ return f"(arc (start {s[0]:g} {s[1]:g}) (mid {m[0]:g} {m[1]:g}) (end {e[0]:g} {e[1]:g}) (stroke (width {w:g}) (type default)) (fill (type none)))"
455
+
456
+
457
+ def _diode_body(kind):
458
+ g = [_poly([(1.27, 1.27), (1.27, -1.27), (-1.27, 0), (1.27, 1.27)], "none"), _poly([(-1.27, 0), (1.27, 0)])]
459
+ bar = {"D": [(-1.27, 1.27), (-1.27, -1.27)],
460
+ "D_Schottky": [(-0.635, 0.762), (-0.635, 1.27), (-1.27, 1.27), (-1.27, -1.27), (-1.905, -1.27), (-1.905, -0.762)],
461
+ "D_Zener": [(-0.762, 1.27), (-1.27, 1.27), (-1.27, -1.27), (-1.778, -1.27)],
462
+ "D_TVS": [(-0.762, 1.27), (-1.27, 1.27), (-1.27, -1.27), (-1.778, -1.27)]}
463
+ g.append(_poly(bar.get(kind, bar["D"])))
464
+ if kind == "LED":
465
+ g += [_poly([(-0.508, -1.524), (0.762, -2.794)]), _poly([(0.762, -2.794), (0.0, -2.794)]), _poly([(0.762, -2.794), (0.762, -2.032)]),
466
+ _poly([(0.762, -1.524), (2.032, -2.794)]), _poly([(2.032, -2.794), (1.27, -2.794)]), _poly([(2.032, -2.794), (2.032, -2.032)])]
467
+ return g
468
+
469
+
470
+ SYMBOLS = { # kind -> (KiCad-style name, reference, description, graphics, pins [(num, name, type, x, y, angle, length)])
471
+ "R": ("R", "R", "Resistor", [_rect(-1.016, -2.54, 1.016, 2.54)],
472
+ [("1", "~", "passive", 0, 3.81, 270, 1.27), ("2", "~", "passive", 0, -3.81, 90, 1.27)]),
473
+ "C": ("C", "C", "Unpolarized capacitor", [_poly([(-2.032, 0.762), (2.032, 0.762)], w=0.508), _poly([(-2.032, -0.762), (2.032, -0.762)], w=0.508)],
474
+ [("1", "~", "passive", 0, 3.81, 270, 2.794), ("2", "~", "passive", 0, -3.81, 90, 2.794)]),
475
+ "CP": ("C_Polarized", "C", "Polarized capacitor", [_rect(-2.286, 0.508, 2.286, 1.016), _rect(-2.286, -0.508, 2.286, -1.016, "outline"),
476
+ _poly([(-1.778, 2.286), (-0.762, 2.286)]), _poly([(-1.27, 2.794), (-1.27, 1.778)])],
477
+ [("1", "~", "passive", 0, 3.81, 270, 2.794), ("2", "~", "passive", 0, -3.81, 90, 2.794)]),
478
+ "L": ("L", "L", "Inductor", [_arc((0, 2.54), (0.635, 1.905), (0, 1.27)), _arc((0, 1.27), (0.635, 0.635), (0, 0)),
479
+ _arc((0, 0), (0.635, -0.635), (0, -1.27)), _arc((0, -1.27), (0.635, -1.905), (0, -2.54))],
480
+ [("1", "1", "passive", 0, 3.81, 270, 1.27), ("2", "2", "passive", 0, -3.81, 90, 1.27)]),
481
+ "LED": ("LED", "D", "Light emitting diode (pin 1 = cathode)", _diode_body("LED"),
482
+ [("1", "K", "passive", 3.81, 0, 180, 2.54), ("2", "A", "passive", -3.81, 0, 0, 2.54)]),
483
+ }
484
+ for _k, _d in (("D", "Diode"), ("D_Schottky", "Schottky diode"), ("D_Zener", "Zener diode"), ("D_TVS", "TVS diode (unidirectional)")):
485
+ SYMBOLS[_k] = (_k, "D", f"{_d} (pin 1 = cathode)", _diode_body(_k), [("1", "K", "passive", 3.81, 0, 180, 2.54), ("2", "A", "passive", -3.81, 0, 0, 2.54)])
486
+
487
+ BJT = lambda pnp: [_circle(1.27, 0, 2.8194), _poly([(0.635, 1.905), (0.635, -1.905)], w=0.508), _poly([(0.635, 0.635), (2.54, 2.54)]),
488
+ _poly([(0.635, -0.635), (2.54, -2.54)]), _poly([(-2.54, 0), (0.635, 0)])] + (
489
+ [_poly([(2.286, -1.778), (1.778, -2.286), (1.27, -1.27), (2.286, -1.778)], "outline")] if pnp else
490
+ [_poly([(1.524, -2.286), (2.286, -1.524), (2.54, -2.54), (1.524, -2.286)], "outline")])
491
+ FET = lambda pch: [_circle(1.27, 0, 2.8194), _poly([(0.254, 1.905), (0.254, -1.905)]), _poly([(0.762, 2.286), (0.762, 1.27)], w=0.508),
492
+ _poly([(0.762, 0.508), (0.762, -0.508)], w=0.508), _poly([(0.762, -1.27), (0.762, -2.286)], w=0.508),
493
+ _poly([(0.762, 1.778), (2.54, 1.778), (2.54, 2.54)]), _poly([(0.762, -1.778), (2.54, -1.778), (2.54, -2.54)]),
494
+ _poly([(0.762, 0), (2.54, 0), (2.54, -1.778)]), _poly([(-2.54, 0), (0.254, 0)])] + (
495
+ [_poly([(2.286, 0), (1.27, 0.381), (1.27, -0.381), (2.286, 0)], "outline")] if pch else
496
+ [_poly([(1.016, 0), (2.032, 0.381), (2.032, -0.381), (1.016, 0)], "outline")])
497
+
498
+
499
+ def _three(kind, order):
500
+ """3-pin discrete: order = pin names for pins 1, 2, 3 (B/E/C or G/S/D)."""
501
+ pos = {"B": (-5.08, 0, 0), "G": (-5.08, 0, 0), "C": (2.54, 5.08, 270), "D": (2.54, 5.08, 270), "E": (2.54, -5.08, 90),
502
+ "S": (2.54, -5.08, 90)}
503
+ pins = [(str(i + 1), nm, "input" if nm in "BG" else "passive", *pos[nm], 2.54) for i, nm in enumerate(order)]
504
+ g = BJT(kind == "PNP") if kind in ("NPN", "PNP") else FET(kind == "PMOS")
505
+ desc = {"NPN": "NPN transistor", "PNP": "PNP transistor", "NMOS": "N-channel MOSFET", "PMOS": "P-channel MOSFET"}[kind]
506
+ ref = "Q"
507
+ return (f"Q_{kind}_{''.join(order)}", ref, f"{desc}, pins {', '.join(f'{i + 1}={n}' for i, n in enumerate(order))}", g, pins)
508
+
509
+
510
+ def _regulator(order, adj_names):
511
+ """3-pin linear regulator box; order = names for pins 1..3 (VI, VO, GND or ADJ); tab = the VO pin's number."""
512
+ side = {"VI": (-7.62, 0, 0), "VO": (7.62, 0, 180), "GND": (0, -7.62, 90), "ADJ": (0, -7.62, 90)}
513
+ typ = {"VI": "power_in", "VO": "power_out", "GND": "power_in", "ADJ": "input"}
514
+ pins = [(str(i + 1), nm, typ[nm], *side[nm], 2.54) for i, nm in enumerate(order)]
515
+ return (f"Regulator_Linear_{'_'.join(order)}", "U", f"Linear regulator, pins {', '.join(f'{i + 1}={n}' for i, n in enumerate(order))}",
516
+ [_rect(-5.08, 2.54, 5.08, -5.08, "background")], pins)
517
+
518
+
519
+ # MPN family -> (allowed packages (table names), symbol builder args)
520
+ FAMILY = [
521
+ (r"^(MMBT(3904|2222A?|4401|5551|A42|A06|A05|2369)|BC8(17|18|46|47|48|49|50)|BCW3[123]|S8050|MMBT8050)", ("SOT-23",), ("NPN", "BEC")),
522
+ (r"^(MMBT(3906|2907A?|4403|5401|A92|A56|A55)|BC8(07|08|56|57|58|59|60)|S8550|MMBT8550)", ("SOT-23",), ("PNP", "BEC")),
523
+ (r"^(2N7002|BSS138|BSS123|AO340[0246]|AO341[68]|AO3422|SI230[24]|SI2312|DMG230[2]|IRLML(2502|6344|6346|0030|0040|0060|2030|2060|6244))", ("SOT-23",), ("NMOS", "GSD")),
524
+ (r"^(BSS84|AO340[17]|AO341[359]|AO3423|SI230[15]|SI2307|DMG230[57]|IRLML(6402|6401|2244|2246|5103|5203|9301|9303))", ("SOT-23",), ("PMOS", "GSD")),
525
+ (r"^(2N3904|2N3906|2N2222A?|2N2907A?|2N440[13]|2N5401|2N5551)", ("TO-92_Inline",), ("NPN|PNP", "EBC")),
526
+ (r"^(BC3[23]7|BC54[6-9]|BC55[6-9]|BC550|BC560)", ("TO-92_Inline",), ("NPN|PNP", "CBE")),
527
+ (r"^2N7000", ("TO-92_Inline",), ("NMOS", "SGD")),
528
+ (r"^BS170", ("TO-92_Inline",), ("NMOS", "DGS")),
529
+ (r"^(AMS1117|LM1117|LD1117S|NCP1117ST|TLV1117|SPX1117M3|HT1117|CJT1117)", ("SOT-223-3_TabPin2",), ("REG", "1117")),
530
+ (r"^(L|LM|MC|UA|KA)?78\d\d", ("TO-220-3_Vertical",), ("REG", "78")),
531
+ (r"^(L|LM|MC|UA|KA)?79\d\d", ("TO-220-3_Vertical",), ("REG", "79")),
532
+ (r"^LM317T", ("TO-220-3_Vertical",), ("REG", "317")),
533
+ (r"^(L|LM|MC|UA|KA)?78L\d\d", ("TO-92_Inline", "SOT-89-3"), ("REG", "78L")),
534
+ ]
535
+ FAMILY_PKG = {"SOT-23": r"\bSOT-?23(?![-\d])", "TO-92_Inline": r"\bTO-?92(?![-\dLS])", "SOT-223-3_TabPin2": r"\bSOT-?223\b|\bTO-?261",
536
+ "TO-220-3_Vertical": r"\bTO-?220(?:AB|-3)?(?![-\dF])", "SOT-89-3": r"\bSOT-?89(?:-3)?(?![-\d])"}
537
+ PNP_RE = re.compile(r"^(2N3906|2N2907|2N4403|2N5401|BC3[2]7|BC55[6-9]|BC560)")
538
+
539
+
540
+ def symbol_kind(kind, mpn, fp_name, description=""):
541
+ """-> (symbol key, why) for a standard symbol, or (None, why)."""
542
+ if kind in ("R", "C", "CP", "L", "LED"):
543
+ return kind, "generic symbol"
544
+ two_pin = fp_name and (fp_name.startswith(("D_", "LED_")) or (spec_pins(fp_name) == 2))
545
+ if kind == "D":
546
+ if not two_pin:
547
+ return None, "diode in a package with more than 2 pins: pinout not standard"
548
+ d = str(description or "") + " " + str(mpn or "")
549
+ if re.match(r"^D_\d{4}", fp_name) and not re.search(r"diode|rectifier|zener|schottky|\bTVS\b|\bESD\b", d, re.I):
550
+ return None, "chip-size D part: LED or diode is not stated (footprint is the same)"
551
+ sub = "D_Zener" if re.search(r"zener|BZX|BZT|MMSZ|\bZD", d, re.I) else "D_TVS" if re.search(r"\bTVS\b|transient|SMAJ|SMBJ|SMCJ|P6KE|ESD", d, re.I) \
552
+ else "D_Schottky" if re.search(r"schottky|\bBAT\d|\bSS\d|\bB5\d|\bMBR|\bSK\d|\bSB\d|1N58[1-9]", d, re.I) else "D"
553
+ if sub == "D_TVS": # KiCad's D_TVS is bidirectional (A1/A2); the MPN does not reliably say which kind it is
554
+ return None, "TVS diode: uni- or bidirectional is not certain from the MPN; symbol left to the other sources"
555
+ return sub, "2-pin diode, pin 1 = cathode"
556
+ m = re.sub(r"[^A-Z0-9]", "", str(mpn or "").upper())
557
+ for rx, pk, (what, pins) in FAMILY:
558
+ if re.match(rx, m):
559
+ if fp_name not in pk:
560
+ return None, f"{mpn}: standard pinout known for {'/'.join(pk)}, not {fp_name}"
561
+ return f"{what}:{pins}", f"{mpn}: {what} family pinout {pins}"
562
+ return None, "no standard pinout for this part"
563
+
564
+
565
+ def spec_pins(name):
566
+ try:
567
+ return spec(name).get("pin_count")
568
+ except KeyError:
569
+ return None
570
+
571
+
572
+ def symbol_text(key, mpn=""):
573
+ """KiCad .kicad_sym text for a symbol key from symbol_kind; returns (text, symbol name)."""
574
+ if ":" in key:
575
+ what, order = key.split(":")
576
+ if what == "REG":
577
+ fixed = order != "1117" or bool(re.search(r"-?(\d\.\d|\d{2}|\d-\d)(?![\d.])", re.sub(r"^.*?1117", "", str(mpn).upper())))
578
+ gnd = "GND" if fixed else "ADJ"
579
+ names = {"1117": [gnd, "VO", "VI"], "78": ["VI", "GND", "VO"], "79": ["GND", "VI", "VO"], "317": ["ADJ", "VO", "VI"],
580
+ "78L": ["VO", "GND", "VI"]}[order]
581
+ name, ref, desc, g, pins = _regulator(names, ())
582
+ else:
583
+ kind = what if "|" not in what else ("PNP" if PNP_RE.match(re.sub(r"[^A-Z0-9]", "", str(mpn).upper())) else "NPN")
584
+ name, ref, desc, g, pins = _three(kind, list(order))
585
+ else:
586
+ name, ref, desc, g, pins = SYMBOLS[key]
587
+ body = " ".join(g)
588
+ pin_txt = " ".join(_pin(*p) for p in pins)
589
+ hide_names = "(pin_names (offset 0) hide)" if all(p[1] in ("~", "1", "2", "K", "A") for p in pins) else "(pin_names (offset 0.254))"
590
+ text = (f'(kicad_symbol_lib (version 20220914) (generator "ecadlib_stdparts")\n'
591
+ f' (symbol "{name}" {hide_names} (in_bom yes) (on_board yes)\n'
592
+ f' (property "Reference" "{ref}" (at 2.54 0 0) (effects (font (size 1.27 1.27))))\n'
593
+ f' (property "Value" "{name}" (at -2.54 0 0) (effects (font (size 1.27 1.27))))\n'
594
+ f' (property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
595
+ f' (property "Datasheet" "~" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
596
+ f' (property "Description" "{desc}" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
597
+ f' (symbol "{name}_0_1" {body})\n'
598
+ f' (symbol "{name}_1_1" {pin_txt})))\n')
599
+ return text, name
600
+
601
+
602
+ # ---------------------------------------------------------------- one part
603
+ def part_kind(category, footprint="", description="", designator=""):
604
+ """R C CP L LED D F Q U or None from the plan folder's last segment, the footprint and the description."""
605
+ cat = str(category or "").split("/")[-1]
606
+ fp, d = str(footprint or ""), str(description or "")
607
+ if cat == "Resistors":
608
+ return "R"
609
+ if cat == "Capacitors":
610
+ return "CP" if fp.startswith("CP_") or re.search(r"tantal|electrolytic|polarized|polymer", d, re.I) else "C"
611
+ if cat == "Inductors":
612
+ return "L"
613
+ if cat == "Diodes":
614
+ return "LED" if fp.startswith("LED_") or re.search(r"\bLED\b", d, re.I) else "D"
615
+ if re.search(r"\bfuse\b|polyfuse|PTC", d, re.I) and re.match(r"^F\d", str(designator or "")):
616
+ return "F"
617
+ return {"Transistors": "Q", "ICs": "U"}.get(cat)
618
+
619
+
620
+ # polarized capacitors (tantalum, polymer, aluminium) by MPN: their case is not a chip-capacitor land and pin 1 is +
621
+ POLAR_CAP = re.compile(r"^(T49[1-9]|T5[2-9]\d|TAJ|TPS[A-Z]|TPM|TRJ|TCJ|TCM|TCN|TCQ|TLJ|TPC|NOJ|NOS|F9[1-8]|293D|593D|298D|"
622
+ r"T8[36]|TR3|TP3|EEE|EEV|EEH|EEF|EEU|ECA|\d+TP[A-Z]|\d+SVP|\d+SEP|UWT|UWX|UUD|UUX|UCD|PCJ|PCF|PLV)")
623
+
624
+
625
+ def resolve(texts, kind=None, mpn="", description=""):
626
+ """What the standard source builds for one part:
627
+ {"footprint": table name | None, "fp_how": str, "kicad_ref": "Lib:Name", "symbol": key | None, "sym_how": str}.
628
+ A polarized capacitor (kind CP, or a C whose MPN is a tantalum / polymer / electrolytic series) gets nothing: the
629
+ table has no polarized case, and a chip-capacitor land would lose the + mark."""
630
+ if kind == "C" and POLAR_CAP.match(_norm(mpn)):
631
+ kind = "CP"
632
+ if kind == "CP":
633
+ return {"footprint": None, "fp_how": "polarized capacitor: its case (tantalum / polymer / electrolytic) is not in "
634
+ "the standard table", "kicad_ref": None, "symbol": None, "sym_how": "polarized capacitor: pin 1 = + only "
635
+ "with a footprint that marks it"}
636
+ fk = {"Q": None, "U": None}.get(kind, kind)
637
+ name, how = footprint_for(list(texts), fk, mpn, weak=[description])
638
+ if not name: # a known MPN family settles a bare family name (SOT-223, TO-220)
639
+ fam = next((pk for rx, pk, _ in FAMILY if re.match(rx, _norm(mpn))), ())
640
+ said = " ".join(map(str, texts)) + " " + str(description)
641
+ hit = [p for p in fam if re.search(FAMILY_PKG[p], said, re.I)]
642
+ if len(hit) == 1 and not POWER_PKG.search(said) and not NO_GO.search(said):
643
+ name, how = hit[0], f"{mpn}: its family comes in {hit[0]} (named as {re.search(FAMILY_PKG[hit[0]], said, re.I).group(0)})"
644
+ sym, sym_how = symbol_kind(kind, mpn, name, description) if (name or kind in ("R", "C", "CP", "L", "LED")) else (None, "no standard package")
645
+ return {"footprint": name, "fp_how": how, "kicad_ref": kicad_ref(name) if name else None, "symbol": sym, "sym_how": sym_how}
646
+
647
+
648
+ # ---------------------------------------------------------------- checks and CLI
649
+ def bench(reference, names=None):
650
+ """Compare table entries with KiCad. reference(name, lib) -> numbers dict or None.
651
+ -> (rows, summary) like footprint_bench.run."""
652
+ from . import footprint_bench as B
653
+ rows = []
654
+ for n in names or sorted(packages()):
655
+ ref = reference(n, packages()[n]["lib"])
656
+ if ref is None:
657
+ rows.append({"name": n, "error": "no reference"})
658
+ continue
659
+ r = B.compare(B.numbers(build(n)[0]), ref)
660
+ rows.append({"name": n, **{k: r[k] for k in ("pads", "pads_ok", "all_pads_ok", "courtyard_ok", "courtyard_diff", "worst_pos", "worst_size")}})
661
+ done = [r for r in rows if "error" not in r]
662
+ return rows, {"footprints": len(done), "unavailable": len(rows) - len(done), "all_pads_ok": sum(r["all_pads_ok"] for r in done),
663
+ "courtyards_ok": sum(r["courtyard_ok"] for r in done),
664
+ "both_ok": sum(r["all_pads_ok"] and r["courtyard_ok"] for r in done)}
665
+
666
+
667
+ def coverage(boms, rules=None):
668
+ """How many BOM lines the standard source resolves offline (footprint; symbol too). Reads the BOMs like triage."""
669
+ from . import bom_triage as T, rules as RU
670
+ out = []
671
+ for b in boms:
672
+ lines, _ = T.read_bom(b, RU.bom_columns(RU.resolve(rules, Path(b).parent)))
673
+ for ln in lines:
674
+ if str(ln.get("dnp") or "").strip() or not any(str(ln.get(k) or "").strip() for k in ("mpn", "value", "footprint")):
675
+ continue
676
+ cat = RU.REF_CLASS.get(re.match(r"[A-Za-z]*", str(ln.get("designator") or "")).group(0).upper())
677
+ kind = part_kind(cat, ln.get("footprint"), ln.get("description"), ln.get("designator"))
678
+ r = resolve([ln.get("footprint"), ln.get("value")], kind, ln.get("mpn") or "", ln.get("description") or "")
679
+ out.append({"bom": str(b), "designator": str(ln.get("designator") or "")[:40], "mpn": ln.get("mpn") or "",
680
+ "footprint_text": ln.get("footprint") or "", "kind": kind or "", **r})
681
+ return out
682
+
683
+
684
+ def main(argv=None):
685
+ ap = argparse.ArgumentParser(prog="ecadlib stdparts", description=__doc__.split("\n")[0])
686
+ sub = ap.add_subparsers(dest="cmd", required=True)
687
+ r = sub.add_parser("resolve", help="what the standard source would build for one part")
688
+ r.add_argument("text", nargs="*", help="footprint / package text(s), in priority order")
689
+ r.add_argument("--kind", choices=["R", "C", "CP", "L", "LED", "D", "F", "Q", "U"])
690
+ r.add_argument("--mpn", default="")
691
+ r.add_argument("--description", default="")
692
+ r.add_argument("--kicad-mod", help="write the footprint here")
693
+ l_ = sub.add_parser("list", help="the packages in the table")
694
+ l_.add_argument("pattern", nargs="?", default="")
695
+ b = sub.add_parser("bench", help="every table entry against KiCad's official footprint")
696
+ b.add_argument("--kicad", help="a kicad-footprints checkout (default: fetch from GitLab through the HTTP cache)")
697
+ b.add_argument("--fixture", help="numbers-only reference file (.json or .json.gz)")
698
+ b.add_argument("--cache", default="out/http_cache")
699
+ b.add_argument("--offline", action="store_true")
700
+ c = sub.add_parser("coverage", help="BOM lines the standard source resolves")
701
+ c.add_argument("bom", nargs="+")
702
+ c.add_argument("--rules")
703
+ for p in (r, l_, b, c):
704
+ p.add_argument("--json", action="store_true", help="print JSON")
705
+ a = ap.parse_args(argv)
706
+ if a.cmd == "resolve":
707
+ res = resolve(a.text, a.kind, a.mpn, a.description)
708
+ if a.kicad_mod and res["footprint"]:
709
+ Path(a.kicad_mod).write_text(build(res["footprint"])[0], encoding="utf-8", newline="\n")
710
+ print(json.dumps(res, indent=2) if a.json else
711
+ f"footprint: {res['kicad_ref'] or '-'} ({res['fp_how']})\nsymbol: {res['symbol'] or '-'} ({res['sym_how']})")
712
+ return 0 if res["footprint"] or res["symbol"] else 1
713
+ if a.cmd == "list":
714
+ names = [n for n in sorted(packages()) if a.pattern.lower() in n.lower()]
715
+ print(json.dumps({n: packages()[n]["lib"] for n in names}, indent=2) if a.json else "\n".join(kicad_ref(n) for n in names))
716
+ return 0
717
+ if a.cmd == "bench":
718
+ from . import footprint_bench as B
719
+ if a.fixture:
720
+ op = gzip.open if a.fixture.endswith(".gz") else open
721
+ with op(a.fixture, "rt", encoding="utf-8") as f:
722
+ fx = json.load(f)
723
+ fx = fx.get("footprints", fx)
724
+ ref = lambda n, lib: fx.get(n)
725
+ elif a.kicad:
726
+ ref = lambda n, lib: (B.numbers((Path(a.kicad) / f"{lib}.pretty" / f"{n}.kicad_mod").read_text(encoding="utf-8"))
727
+ if (Path(a.kicad) / f"{lib}.pretty" / f"{n}.kicad_mod").exists() else None)
728
+ else:
729
+ from .httpcache import CachedHTTP
730
+ http = CachedHTTP(a.cache, min_interval=0.3, offline=a.offline, ttl_s=30 * 86400)
731
+ ref = lambda n, lib: B.reference(n, lib, http)
732
+ rows, s = bench(ref)
733
+ if a.json:
734
+ print(json.dumps({"summary": s, "rows": rows}, indent=2))
735
+ else:
736
+ for row in rows:
737
+ if "error" in row or not (row["all_pads_ok"] and row["courtyard_ok"]):
738
+ print(f" {row['name']}: " + (row.get("error") or f"pads {row['pads_ok']}/{row['pads']}, courtyard diff {row['courtyard_diff']}"))
739
+ print(f"{s['both_ok']}/{s['footprints']} match KiCad (pads and courtyard); {s['unavailable']} without a reference")
740
+ return 0 if s["both_ok"] == s["footprints"] and s["footprints"] else 1
741
+ rows = coverage(a.bom, a.rules)
742
+ if a.json:
743
+ print(json.dumps(rows, indent=2))
744
+ else:
745
+ for row in rows:
746
+ print(f" {row['designator']:<16} {row['mpn'][:24]:<24} {row['footprint_text'][:28]:<28} -> {row['kicad_ref'] or '-'}"
747
+ f" sym {row['symbol'] or '-'} ({row['fp_how']})")
748
+ fp = sum(bool(r_["footprint"]) for r_ in rows)
749
+ both = sum(bool(r_["footprint"] and r_["symbol"]) for r_ in rows)
750
+ print(f"{len(rows)} lines: footprint for {fp}, footprint and symbol for {both}")
751
+ return 0
752
+
753
+
754
+ if __name__ == "__main__":
755
+ sys.exit(main())