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/kicad_index.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Index a local KiCad library directory (*.kicad_sym, *.pretty/*.kicad_mod) for BOM matching."""
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from . import sexpr
|
|
7
|
+
|
|
8
|
+
MPN_KEYS = {"mpn", "manufacturer part number", "mfr part number", "mfr_pn", "mpn1", "part number", "manufacturer_part_number"}
|
|
9
|
+
LCSC_KEYS = {"lcsc", "lcsc part", "lcsc part #", "lcsc_part", "jlcpcb part #", "jlc"}
|
|
10
|
+
SI = {"p": 1e-12, "n": 1e-9, "u": 1e-6, "µ": 1e-6, "μ": 1e-6, "m": 1e-3, "": 1.0, "r": 1.0,
|
|
11
|
+
"k": 1e3, "K": 1e3, "M": 1e6, "G": 1e9}
|
|
12
|
+
PKG_RE = re.compile(r"(?<![0-9])(01005|0201|0402|0603|0805|1206|1210|1812|2010|2512)(?![0-9])|"
|
|
13
|
+
r"(?<![A-Z0-9])(SOT-?23(?:-\d)?|SOT-?223|SOIC-?\d+|SOP-?\d+|TSSOP-?\d+|QFN-?\d+|VQFN-?\d+|LQFP-?\d+|SMA|SMB|SOD-?\d+)(?![A-Z0-9])", re.I)
|
|
14
|
+
BODY_RE = re.compile(r"(?<![0-9.])(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)(?:x\d+(?:\.\d+)?)?mm", re.I)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def norm_mpn(s):
|
|
18
|
+
return re.sub(r"[^A-Z0-9]", "", str(s or "").upper())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_UNIT = r"((?i:ohms?|[fhω]))?(?![A-Za-z0-9])" # optional unit, then the number must end (74HC595 is not 74)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_value(s, with_unit=False):
|
|
25
|
+
"""'10k' / '10kΩ' / '4k7' / '100nF' / '0.1uF' / '10K 1%' / '0R' -> float, else None.
|
|
26
|
+
with_unit=True -> (value, unit) with unit in 'F', 'H', 'R' (ohm) or '' when not stated."""
|
|
27
|
+
s = re.sub(r"(?i)(?<=\d)meg", "M", str(s or "").strip())
|
|
28
|
+
m = re.match(r"^(\d+)([pnuµμmkKMGR])(\d+)" + _UNIT, s) # 4k7 / 4R7 notation
|
|
29
|
+
if m:
|
|
30
|
+
v, pre, u = float(f"{m.group(1)}.{m.group(3)}"), m.group(2), m.group(4)
|
|
31
|
+
else:
|
|
32
|
+
m = re.match(r"^(\d+(?:[.,]\d+)?)\s*([pnuµμmkKMGR]?)" + _UNIT, s) # '4,7k' / '2,2µF': European decimals
|
|
33
|
+
if not m:
|
|
34
|
+
return (None, "") if with_unit else None
|
|
35
|
+
v, pre, u = float(m.group(1).replace(",", ".")), m.group(2), m.group(3)
|
|
36
|
+
v *= SI["r" if pre == "R" else pre]
|
|
37
|
+
u = "R" if pre == "R" or (u or "").lower()[:1] in ("o", "ω") else (u or "").upper()
|
|
38
|
+
return (v, u) if with_unit else v
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
IPC_CHIP_RE = re.compile(r"(?:RES|CAP|IND|LED|DIO)C(\d{4})X\d+", re.I) # IPC-7351 chip names: RESC1608X55N
|
|
42
|
+
METRIC_TO_IMPERIAL = {"0402": "01005", "0603": "0201", "1005": "0402", "1608": "0603", "2012": "0805",
|
|
43
|
+
"3216": "1206", "3225": "1210", "4532": "1812", "5025": "2010", "6332": "2512"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def package_of(*texts):
|
|
47
|
+
for t in texts:
|
|
48
|
+
m = IPC_CHIP_RE.search(str(t or ""))
|
|
49
|
+
if m and m.group(1) in METRIC_TO_IMPERIAL:
|
|
50
|
+
return METRIC_TO_IMPERIAL[m.group(1)]
|
|
51
|
+
m = PKG_RE.search(str(t or ""))
|
|
52
|
+
if m:
|
|
53
|
+
return (m.group(1) or m.group(2)).upper().replace("-", "")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _body(text):
|
|
58
|
+
m = BODY_RE.search(str(text or ""))
|
|
59
|
+
return tuple(sorted((float(m.group(1)), float(m.group(2))))) if m else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Symbol:
|
|
64
|
+
lib: str
|
|
65
|
+
name: str
|
|
66
|
+
props: dict = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def mpn(self):
|
|
70
|
+
return next((v for k, v in self.props.items() if k.lower() in MPN_KEYS), "")
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def lcsc(self):
|
|
74
|
+
return next((v for k, v in self.props.items() if k.lower() in LCSC_KEYS), "")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class KiCadIndex:
|
|
78
|
+
def __init__(self, roots):
|
|
79
|
+
self.symbols, self.footprints = [], {} # footprints: "lib:name" -> {"path", "has_3d"}
|
|
80
|
+
for root in roots:
|
|
81
|
+
root = Path(root)
|
|
82
|
+
for f in root.rglob("*.kicad_sym"):
|
|
83
|
+
self._load_sym(f)
|
|
84
|
+
for d in root.rglob("*.pretty"):
|
|
85
|
+
for m in d.glob("*.kicad_mod"):
|
|
86
|
+
txt = m.read_text(encoding="utf-8", errors="replace")
|
|
87
|
+
self.footprints[f"{d.stem}:{m.stem}"] = {"path": str(m), "has_3d": "(model " in txt}
|
|
88
|
+
|
|
89
|
+
def _load_sym(self, f):
|
|
90
|
+
try:
|
|
91
|
+
tree = sexpr.parse(f.read_text(encoding="utf-8", errors="replace"))
|
|
92
|
+
except ValueError:
|
|
93
|
+
return
|
|
94
|
+
for s in sexpr.find(tree, "symbol"):
|
|
95
|
+
props = {p[1]: p[2] for p in sexpr.find(s, "property") if len(p) > 2}
|
|
96
|
+
self.symbols.append(Symbol(f.stem, s[1], props))
|
|
97
|
+
|
|
98
|
+
def footprint(self, ref):
|
|
99
|
+
"""Resolve 'Lib:Name' (or bare name) to an indexed footprint."""
|
|
100
|
+
if not ref:
|
|
101
|
+
return None, None
|
|
102
|
+
if ref in self.footprints:
|
|
103
|
+
return ref, self.footprints[ref]
|
|
104
|
+
name = ref.split(":")[-1]
|
|
105
|
+
for k, v in self.footprints.items():
|
|
106
|
+
if k.split(":")[-1] == name:
|
|
107
|
+
return k, v
|
|
108
|
+
return None, None
|
|
109
|
+
|
|
110
|
+
def match(self, mpn="", lcsc="", value="", package="", footprint="", ref_class=""):
|
|
111
|
+
"""Return (symbol, how) - by MPN, LCSC number, or (for passives) value + package.
|
|
112
|
+
ref_class (R, C, L...) keeps a C 10u 0805 line off an L 10u 0805 symbol."""
|
|
113
|
+
nm = norm_mpn(mpn)
|
|
114
|
+
for s in self.symbols:
|
|
115
|
+
if nm and nm in (norm_mpn(s.name), norm_mpn(s.mpn)):
|
|
116
|
+
return s, "mpn"
|
|
117
|
+
for s in self.symbols:
|
|
118
|
+
if lcsc and s.lcsc.upper() == lcsc.upper():
|
|
119
|
+
return s, "lcsc"
|
|
120
|
+
v, u = parse_value(value, True)
|
|
121
|
+
if v is not None and package:
|
|
122
|
+
for s in self.symbols:
|
|
123
|
+
sv, su = parse_value(s.props.get("Value"), True)
|
|
124
|
+
if (sv is not None and abs(sv - v) <= 1e-6 * v and not (u and su and u != su) # 0R == 0; 10uF != 10uH
|
|
125
|
+
and package_of(s.props.get("Footprint"), s.name) == package and self._same_class(s, ref_class)):
|
|
126
|
+
return s, "value+package"
|
|
127
|
+
return None, None
|
|
128
|
+
|
|
129
|
+
CLASS_PREFIX = {"R": ("R_",), "C": ("C_",), "L": ("L_",), "FB": ("L_", "FB_"), "D": ("D_", "LED_"), "LED": ("LED_",)}
|
|
130
|
+
|
|
131
|
+
def _same_class(self, s, ref_class):
|
|
132
|
+
if not ref_class:
|
|
133
|
+
return True
|
|
134
|
+
sref = re.match(r"[A-Z]+", str(s.props.get("Reference", "")).upper())
|
|
135
|
+
if sref:
|
|
136
|
+
return sref.group(0) == ref_class or (ref_class, sref.group(0)) in (("FB", "L"), ("LED", "D"), ("D", "LED"))
|
|
137
|
+
pref = self.CLASS_PREFIX.get(ref_class)
|
|
138
|
+
fp = str(s.props.get("Footprint", "")).split(":")[-1]
|
|
139
|
+
return pref is None or not fp or fp.startswith(pref)
|
|
140
|
+
|
|
141
|
+
def find_footprint_for_package(self, package, ref_class="", hint=""):
|
|
142
|
+
"""Best-effort: an indexed footprint whose name mentions the package (e.g. 0603, SOIC8).
|
|
143
|
+
For 2-terminal passives the footprint family must match the designator class
|
|
144
|
+
(an LED 0603 must not land on R_0603)."""
|
|
145
|
+
if not package:
|
|
146
|
+
return None
|
|
147
|
+
pref, hits = self.CLASS_PREFIX.get(ref_class), []
|
|
148
|
+
for k in self.footprints:
|
|
149
|
+
name = k.split(":")[-1]
|
|
150
|
+
if package_of(name) == package and (pref is None or name.startswith(pref)):
|
|
151
|
+
if pref is None and re.match(r"(R|C|L|D|LED)_", name):
|
|
152
|
+
continue # don't hand a passive footprint to an IC/connector
|
|
153
|
+
hits.append(k)
|
|
154
|
+
bodies = {k: _body(k) for k in hits}
|
|
155
|
+
want = _body(hint)
|
|
156
|
+
if want: # body size known: it must agree (QFN-32 4x4 is not 5x5)
|
|
157
|
+
hits = [k for k in hits if bodies[k] in (want, None)]
|
|
158
|
+
elif len({b for b in bodies.values() if b}) > 1:
|
|
159
|
+
return None # several body sizes and nothing to choose by: ambiguous
|
|
160
|
+
return hits[0] if hits else None
|
ecadlib/kicad_menu.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""The ecadlib window inside KiCad: what the PCB editor plugin runs (add part, check the project BOM, fill the library,
|
|
2
|
+
review queue), the same flow as the ecadlib menu in Altium (altium_menu), with a KiCad library as the target.
|
|
3
|
+
|
|
4
|
+
ecadlib kicad-menu run-job JOB --action add-part --project BOARD.kicad_pcb --mpn LM358DR [--manufacturer TI]
|
|
5
|
+
ecadlib kicad-menu run-job JOB --action check-bom|fill --project BOARD.kicad_pcb|SCHEMATIC.kicad_sch
|
|
6
|
+
ecadlib kicad-menu run-job JOB --action queue|open --project BOARD.kicad_pcb [--path REPORT]
|
|
7
|
+
ecadlib kicad-menu check-bom "%I" # the schematic editor's Tools > Generate BOM hook (a netlist or .kicad_sch)
|
|
8
|
+
|
|
9
|
+
The plugin (ecadlib/assets/kicad_action_plugin.py) runs in KiCad's own Python, starts `run-job` in the background and
|
|
10
|
+
reads JOB/progress.ini (live steps) and JOB/result.ini (the answer: big, row1..rowN "label|value|ok", message, fix,
|
|
11
|
+
footprint to place) - the same files, written the same way, as the Altium dialog reads.
|
|
12
|
+
|
|
13
|
+
Per project, next to the board: ecadlib-work/ is the ecadlib workspace (one parts list, uploads/altium-menu/parts.csv,
|
|
14
|
+
for every part added or filled, so one plan makes the whole library) and ecadlib/ the KiCad library (ecadlib.kicad_sym,
|
|
15
|
+
ecadlib.pretty, ecadlib.3dshapes, kicad_export.json). The plugin adds ${KIPRJMOD}/ecadlib/... rows to the project's
|
|
16
|
+
library tables and places the footprint; nothing here needs KiCad."""
|
|
17
|
+
import argparse, csv, json, struct, sys, zlib
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from . import altium_menu as AM
|
|
21
|
+
|
|
22
|
+
WORK, LIB = "ecadlib-work", "ecadlib"
|
|
23
|
+
ACTIONS = ("add-part", "check-bom", "fill", "queue", "open")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def project_dir(project):
|
|
27
|
+
p = Path(project)
|
|
28
|
+
return p if p.is_dir() else p.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def config(project, port=8765, offline=False):
|
|
32
|
+
"""The menu settings for a KiCad project: workspace ecadlib-work/, library ecadlib/ next to the board."""
|
|
33
|
+
d = project_dir(project).resolve()
|
|
34
|
+
return {"workspace": d / WORK, "library": d / LIB, "name": LIB, "port": port, "offline": offline, "tool": "kicad"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def schematic_of(project):
|
|
38
|
+
"""The root schematic of the board's project: <stem>.kicad_sch next to the .kicad_pcb / .kicad_pro, or the only
|
|
39
|
+
.kicad_pro's. A .kicad_sch or a netlist is itself."""
|
|
40
|
+
p = Path(project)
|
|
41
|
+
if p.is_file() and p.suffix.lower() in (".kicad_sch", ".xml", ".net"):
|
|
42
|
+
return p
|
|
43
|
+
d = project_dir(p)
|
|
44
|
+
if p.suffix.lower() in (".kicad_pcb", ".kicad_pro") and p.with_suffix(".kicad_sch").is_file():
|
|
45
|
+
return p.with_suffix(".kicad_sch")
|
|
46
|
+
pros = sorted(d.glob("*.kicad_pro"))
|
|
47
|
+
if len(pros) == 1 and pros[0].with_suffix(".kicad_sch").is_file():
|
|
48
|
+
return pros[0].with_suffix(".kicad_sch")
|
|
49
|
+
raise ValueError(f"no schematic for {p.name}: save the schematic next to the board (same name, .kicad_sch)")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def adopt_old_list(cfg):
|
|
53
|
+
"""Parts the first plugin (ecadlib-work/parts.csv) added go into the menu's list once."""
|
|
54
|
+
old = Path(cfg["workspace"]) / "parts.csv"
|
|
55
|
+
if not old.is_file() or (Path(cfg["workspace"]) / AM.MENU_BOM).is_file():
|
|
56
|
+
return 0
|
|
57
|
+
n = 0
|
|
58
|
+
for r in csv.DictReader(old.open(encoding="utf-8", newline="")):
|
|
59
|
+
if (r.get("Mfr. Part #") or "").strip():
|
|
60
|
+
AM.add_to_parts(cfg["workspace"], r["Mfr. Part #"], r.get("Manufacturer", ""))
|
|
61
|
+
n += 1
|
|
62
|
+
return n
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _exported_footprint(cfg, names):
|
|
66
|
+
rep = Path(cfg["library"]) / "kicad_export.json"
|
|
67
|
+
try:
|
|
68
|
+
parts = json.loads(rep.read_text(encoding="utf-8")).get("parts", [])
|
|
69
|
+
except (OSError, ValueError):
|
|
70
|
+
return ""
|
|
71
|
+
return next((p.get("footprint") or "" for p in parts if p.get("part") in names), "")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def add_part(cfg, mpn, manufacturer="", progress=None):
|
|
75
|
+
adopt_old_list(cfg)
|
|
76
|
+
res = AM.add_part(cfg, mpn, manufacturer, progress=progress)
|
|
77
|
+
if not res.get("footprint") and res.get("status") == "duplicate": # already in the library: place that one
|
|
78
|
+
res["footprint"] = _exported_footprint(cfg, {res.get("part"), *res.get("message", "").rstrip(".").split()[-1:]})
|
|
79
|
+
return res
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def fill(cfg, project, progress=None):
|
|
83
|
+
"""The schematic's fitted lines with a part number go into the menu's parts list; the librarian builds the list
|
|
84
|
+
and the whole list is exported (one plan = the whole KiCad library)."""
|
|
85
|
+
from .bom_triage import read_bom
|
|
86
|
+
adopt_old_list(cfg)
|
|
87
|
+
lines, _ = read_bom(schematic_of(project))
|
|
88
|
+
skipped = 0
|
|
89
|
+
for ln in lines:
|
|
90
|
+
if ln.get("dnp"):
|
|
91
|
+
continue
|
|
92
|
+
if not str(ln.get("mpn") or "").strip() or not AM.MPN_RE.match(str(ln["mpn"]).strip()):
|
|
93
|
+
skipped += 1
|
|
94
|
+
continue
|
|
95
|
+
AM.add_to_parts(cfg["workspace"], ln["mpn"], ln.get("manufacturer", ""), value=ln.get("value", ""),
|
|
96
|
+
footprint=ln.get("footprint", ""), designator=ln.get("designator", ""), lcsc=ln.get("lcsc", ""),
|
|
97
|
+
datasheet=ln.get("datasheet", ""))
|
|
98
|
+
res = AM.fill(cfg, Path(cfg["workspace"]) / AM.MENU_BOM, progress=progress)
|
|
99
|
+
if skipped:
|
|
100
|
+
from . import messages as M
|
|
101
|
+
res["message"] += f" {M.plural(skipped, 'schematic line')} without a part number left out."
|
|
102
|
+
return res
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_action(cfg, action, project, mpn="", manufacturer="", path="", opener=None, progress=None):
|
|
106
|
+
if action == "add-part":
|
|
107
|
+
return add_part(cfg, mpn, manufacturer, progress=progress)
|
|
108
|
+
if action == "check-bom":
|
|
109
|
+
return AM.check_bom(cfg, schematic_of(project), opener=opener, progress=progress)
|
|
110
|
+
if action == "fill":
|
|
111
|
+
return fill(cfg, project, progress=progress)
|
|
112
|
+
if action == "queue":
|
|
113
|
+
return AM.open_queue(cfg, opener=opener)
|
|
114
|
+
if action == "open":
|
|
115
|
+
return AM.open_file(cfg, path, opener=opener)
|
|
116
|
+
raise ValueError(f"unknown action {action!r} (one of {', '.join(ACTIONS)})")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_job(job, action, project, mpn="", manufacturer="", path="", opener=None, offline=False):
|
|
120
|
+
"""What the plugin runs: JOB/progress.ini while working, JOB/result.ini at the end. Returns the result dict."""
|
|
121
|
+
job = Path(job)
|
|
122
|
+
job.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
try:
|
|
124
|
+
cfg = config(project, offline=offline)
|
|
125
|
+
res = run_action(cfg, action, project, mpn, manufacturer, path,
|
|
126
|
+
opener=False if action == "check-bom" else opener, progress=AM.Progress(job))
|
|
127
|
+
except Exception as e: # every failure becomes one dialog line with a fix
|
|
128
|
+
res = AM._error(e)
|
|
129
|
+
AM.write_result(job / "result.ini", res)
|
|
130
|
+
AM.prune_jobs(job.parent, keep=job)
|
|
131
|
+
(job / "exit.txt").write_text("0\n", encoding="utf-8")
|
|
132
|
+
return res
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------ the toolbar icon
|
|
136
|
+
def icon_png(size=24):
|
|
137
|
+
"""The ecadlib toolbar icon (a blue tile with three white shelves, the one on top shorter) as PNG bytes."""
|
|
138
|
+
blue, white = (0x2F, 0x77, 0xD9, 255), (255, 255, 255, 255)
|
|
139
|
+
rows = []
|
|
140
|
+
r = size // 6
|
|
141
|
+
for y in range(size):
|
|
142
|
+
line = bytearray([0])
|
|
143
|
+
for x in range(size):
|
|
144
|
+
cx, cy = min(x, size - 1 - x), min(y, size - 1 - y) # rounded corners
|
|
145
|
+
if cx < r and cy < r and (r - cx) ** 2 + (r - cy) ** 2 > r * r:
|
|
146
|
+
line += bytes(4)
|
|
147
|
+
continue
|
|
148
|
+
px = blue
|
|
149
|
+
for i, (y0, x1) in enumerate(((size * 5 // 24, size * 14 // 24), (size * 10 // 24, size * 18 // 24),
|
|
150
|
+
(size * 15 // 24, size * 18 // 24))):
|
|
151
|
+
if y0 <= y < y0 + max(2, size // 10) and size * 6 // 24 <= x < x1:
|
|
152
|
+
px = white
|
|
153
|
+
line += bytes(px)
|
|
154
|
+
rows.append(bytes(line))
|
|
155
|
+
def chunk(t, d):
|
|
156
|
+
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
|
|
157
|
+
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0))
|
|
158
|
+
+ chunk(b"IDAT", zlib.compress(b"".join(rows), 9)) + chunk(b"IEND", b""))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main(argv=None):
|
|
162
|
+
ap = argparse.ArgumentParser(prog="ecadlib kicad-menu",
|
|
163
|
+
description="what the ecadlib window in KiCad's PCB editor runs (add part, check BOM, "
|
|
164
|
+
"fill the library, review queue)")
|
|
165
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
166
|
+
r = sub.add_parser("run-job", help="what the KiCad plugin runs: JOB/progress.ini while working, JOB/result.ini")
|
|
167
|
+
r.add_argument("job")
|
|
168
|
+
r.add_argument("--action", required=True, choices=ACTIONS)
|
|
169
|
+
r.add_argument("--project", required=True, help="the board (.kicad_pcb), its project or schematic")
|
|
170
|
+
r.add_argument("--mpn", default="")
|
|
171
|
+
r.add_argument("--manufacturer", default="")
|
|
172
|
+
r.add_argument("--path", default="", help="open: the report to open")
|
|
173
|
+
r.add_argument("--offline", action="store_true", help="cached downloads and standard parts only")
|
|
174
|
+
c = sub.add_parser("check-bom", help='check a schematic against the library and open the report (Generate BOM: "%%I")')
|
|
175
|
+
c.add_argument("project", help="netlist (.xml) KiCad's Generate BOM passes as %%I, a .kicad_sch or a board")
|
|
176
|
+
c.add_argument("output", nargs="?", help="KiCad's %%O (ignored)")
|
|
177
|
+
c.add_argument("--no-open", action="store_true")
|
|
178
|
+
c.add_argument("--json", action="store_true")
|
|
179
|
+
a = ap.parse_args(argv)
|
|
180
|
+
if a.cmd == "run-job":
|
|
181
|
+
res = run_job(a.job, a.action, a.project, a.mpn, a.manufacturer, a.path, offline=a.offline)
|
|
182
|
+
return 0 if res.get("status") != "error" else 1
|
|
183
|
+
try:
|
|
184
|
+
res = AM.check_bom(config(a.project), schematic_of(a.project), opener=False if a.no_open else None)
|
|
185
|
+
except (OSError, ValueError) as e:
|
|
186
|
+
print(f"ecadlib kicad-menu: {e}", file=sys.stderr)
|
|
187
|
+
return 2
|
|
188
|
+
print(json.dumps(res, indent=2) if a.json else res["message"])
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
sys.exit(main())
|