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.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- 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())
|