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/symbol_layout.py
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
"""Tidy symbols for big chips: pins grouped by function and laid out like a person would draw them.
|
|
2
|
+
|
|
3
|
+
ecadlib symbol PINS.csv|PINS.json|PART.SchLib|LIB.kicad_sym:NAME|kicad:Lib:Name -o out.SchLib [--kicad out.kicad_sym]
|
|
4
|
+
[--preview out.svg] [--split N] [--stack stack|hide|none] [--rules ecadlib.toml]
|
|
5
|
+
|
|
6
|
+
The house rule: inputs left, outputs right, power on top, ground on the bottom ([symbols] power_side = "top"), or
|
|
7
|
+
the top and bottom edges left empty with the power pins at the top of the left side and the ground pins at its
|
|
8
|
+
bottom (power_side = "left"). Each pin is put in a functional group from its name (and type when the name says nothing):
|
|
9
|
+
power, analog (VDDA, VREF+) -> top (power_side "left": top of the left side)
|
|
10
|
+
ground (GND, VSS, AGND, VREF-), ep (exposed pad) -> bottom (power_side "left": bottom of the left side)
|
|
11
|
+
reset (NRST, BOOT0, EN), clock (OSC_IN, XTAL), debug (SWDIO, TCK, U0TXD), usb, adc, inputs (IN+, TRIG, FB, type
|
|
12
|
+
input) -> left
|
|
13
|
+
power_out (VOUT, VCAP), uart, i2c, spi, can, eth, ports (PA0..PE15, GPIO12, P0.1), outputs (OUT, Q, PG, SW, type
|
|
14
|
+
output), nc -> right
|
|
15
|
+
other (no name rule and no direction: passive, bidirectional) -> left, moved right when that keeps the symbol square
|
|
16
|
+
Alternate-function names ("PA9/USART1_TX", "PH0-OSC_IN") are grouped by their first (primary) name; overbars
|
|
17
|
+
('~{RESET}', 'R\\E\\S\\E\\T\\', '/RESET', 'RESET#') are ignored for grouping. Port pins are sorted by port and
|
|
18
|
+
number and each port is its own block; blocks are separated by one empty grid row. Ports (and unclassified pins)
|
|
19
|
+
and 'other' pins move between the left and right side to keep the symbol about square, whole ports first; inputs and
|
|
20
|
+
outputs never move.
|
|
21
|
+
|
|
22
|
+
Power pins sharing a name (VDD x5) are stacked (rules [symbols] stack_power): 'stack' puts them on one spot, one
|
|
23
|
+
pin showing its name and number and the rest drawn on top of it with name and number hidden (Altium joins pins at
|
|
24
|
+
one point; the KiCad export writes the rest as hidden passive pins, KLC S4.3); 'hide' makes the rest hidden pins
|
|
25
|
+
tied to the net by name (Altium HiddenNetName, KiCad hidden power_in); 'none' draws every pin.
|
|
26
|
+
|
|
27
|
+
Above [symbols] split_pins pins the part becomes a multi-part symbol: part A holds the power and ground pins, the
|
|
28
|
+
other parts the signal blocks in order, at most unit_pins pins each (a port is never split across parts unless it
|
|
29
|
+
alone is bigger). Everything is on the [symbols] grid (100 mil): body edges, pin ends, pin pitch.
|
|
30
|
+
|
|
31
|
+
altium_lib.ic_symbol uses this for parts with at least [symbols] layout_pins pins (default 1: every IC; above that the
|
|
32
|
+
smaller parts keep the plain 'side per pin' layout). librarian re-lays out EasyEDA IC symbols the same way, and a KiCad
|
|
33
|
+
library symbol whose pin text would overlap in Altium (text_problems). datasheet.py can call arrange_pins() to show the
|
|
34
|
+
grouping in its review."""
|
|
35
|
+
import argparse, csv, json, re, sys
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from . import altium_lib as AL
|
|
39
|
+
|
|
40
|
+
GROUPS = ("power", "analog", "power_out", "ground", "ep", "reset", "clock", "debug", "usb", "adc", "other_in",
|
|
41
|
+
"other", "uart", "i2c", "spi", "can", "eth", "ports", "other_out", "nc")
|
|
42
|
+
ORDER = ["power", "analog", "reset", "clock", "debug", "usb", "adc", "other_in", "other", "power_out", "uart", "i2c",
|
|
43
|
+
"spi", "can", "eth", "ports", "other_out", "nc", "ground", "ep"]
|
|
44
|
+
SIDES = {"power": "top", "analog": "top", "power_out": "right", "ground": "bottom", "ep": "bottom",
|
|
45
|
+
"reset": "left", "clock": "left", "debug": "left", "usb": "left", "adc": "left", "other_in": "left",
|
|
46
|
+
"other": "left", "uart": "right", "i2c": "right", "spi": "right", "can": "right", "eth": "right",
|
|
47
|
+
"ports": "right", "other_out": "right", "nc": "right"}
|
|
48
|
+
POWER_GROUPS = {"power", "analog", "power_out", "ground", "ep"}
|
|
49
|
+
FLEX = ("ports", "other") # may move between left and right to balance the symbol
|
|
50
|
+
STACKS = ("stack", "hide", "none")
|
|
51
|
+
POWER_SIDES = ("top", "left")
|
|
52
|
+
DIRECTED = {"input": "other_in", "output": "other_out", "open_collector": "other_out", "open_emitter": "other_out",
|
|
53
|
+
"tristate": "other_out", "hiz": "other_out"} # [symbols] power_side: the house rule's two variants
|
|
54
|
+
SPLIT_PENALTY = 6 # rows a port split across sides must save before it is used
|
|
55
|
+
CHAR = 7 # name width per character, DXP units (as altium_lib.ic_symbol)
|
|
56
|
+
|
|
57
|
+
_R = lambda s: re.compile(r"^(?:" + s + r")$")
|
|
58
|
+
RX = [ # (group, regex on the primary name, upper case, overbar / active-low markers removed); first match wins
|
|
59
|
+
("nc", _R(r"N\.?C\.?\d*|DNC|DNU|NU|RESERVED\d*|RSVD\d*|NOT ?CONNECTED")),
|
|
60
|
+
("ep", _R(r"EP|E?PAD|EXPOSED[ _]?PAD|THERMAL[ _]?PAD|TAB|SLUG")),
|
|
61
|
+
("ground", _R(r"[ADPSE]?GND[A-Z0-9_]*|GND[ADPS]?|VSS[A-Z0-9_]*|V?REF-|VREFN|VREF_?N|VEE|V-|0V|AVSS|DVSS|PVSS")),
|
|
62
|
+
("analog", _R(r"VDDA[A-Z0-9_]*|AVDD[A-Z0-9_]*|AVCC|VCCA|V?REF\+?|VREFP|VREF_?P|VREF_?OUT|VREFBUF")),
|
|
63
|
+
("power_out", _R(r"V?OUT|VO|VREG[A-Z0-9_]*|VCAP[A-Z0-9_]*|VDDCORE_?OUT|\d+V\d*_?OUT|V\d+V?\d*_?OUT|LDO_?OUT|VCORE|DECOUPLE")),
|
|
64
|
+
("usb", _R(r"USB[A-Z0-9_+\-]*|D[PM]\d?|D[+\-]|UD[+\-PM]|VBUS|USB_?ID|OTG[A-Z0-9_]*|CC[12]")),
|
|
65
|
+
("power", _R(r"VDD[A-Z0-9_]*|VCC[A-Z0-9_]*|VBAT[A-Z0-9_]*|VIN\d*|VS|V\+|VPP|VIO|VM|VSYS|DVDD|IOVDD|DVCC|PVDD|PVIN|"
|
|
66
|
+
r"V\d+V\d*|\d+V\d*|VBACKUP|VDDIO\d*|VBATT|BAT|VLOGIC|VL")),
|
|
67
|
+
("reset", _R(r"N?RST[A-Z0-9_]*|N?RESET[A-Z0-9_]*|MCLR|N?MCLR|EN|CHIP_?EN|CHIP_?PU|N?CE|BOOT\d*|BOOT_?SEL\d*|PDR_?ON|"
|
|
68
|
+
r"MODE\d*|TEST\d*|TST|N?WAKE[A-Z0-9_]*|PWR_?EN|N?SLEEP|N?SHDN|SHUTDOWN|N?STBY|N?PD|PWRDN|N?PWRDWN|N?SUSPEND|NMI")),
|
|
69
|
+
("clock", _R(r"OSC[A-Z0-9_]*|XTAL[A-Z0-9_]*|XI|XO|XIN|XOUT|X[12]|CLK_?IN|CLK_?OUT|CLKO?|MCO\d*|EXT_?CLK|XT[IO]|"
|
|
70
|
+
r"RTC_?X[A-Z0-9_]*|32K_?X[PN]|OSC32[A-Z0-9_]*|LSE[A-Z_]*|HSE[A-Z_]*")),
|
|
71
|
+
("debug", _R(r"SWDIO|SWCLK|SWO|SWD[A-Z0-9_]*|TMS|TCK|TDI|TDO|N?TRST|JT[A-Z0-9_]*|DBG[A-Z0-9_]*|TRACE[A-Z0-9_]*|"
|
|
72
|
+
r"U0TXD|U0RXD|UART0_?TXD?|UART0_?RXD?|TXD0|RXD0|SWIM|PDI|UPDI|ICSP[A-Z]*|PGE?[CD]\d?")),
|
|
73
|
+
("uart", _R(r"L?P?US?ART[A-Z0-9_]*|TXD?\d*|RXD?\d*|N?RTS\d*|N?CTS\d*|N?DTR|N?DSR|N?DCD|N?RI|TXEN|TXDEN|RS485[A-Z_]*|"
|
|
74
|
+
r"TXLED|RXLED|N?TXLED|N?RXLED")),
|
|
75
|
+
("i2c", _R(r"I2C[A-Z0-9_]*|SDA\d*|SCL\d*|TWI[A-Z0-9_]*|SMB[A-Z0-9_]*|IIC[A-Z0-9_]*")),
|
|
76
|
+
("spi", _R(r"Q?SPI[A-Z0-9_]*|MOSI\d*|MISO\d*|SCK\d*|SCLK\d*|N?CS\d*|N?SS\d*|N?SEL|SDI|SDO|SIO\d|SD[0-3]|CMD|"
|
|
77
|
+
r"FLASH[A-Z0-9_]*|N?HOLD|N?WP")),
|
|
78
|
+
("can", _R(r"F?D?CAN[A-Z0-9_]*|TXCAN|RXCAN")),
|
|
79
|
+
("eth", _R(r"ETH[A-Z0-9_]*|R?G?MII[A-Z0-9_]*|MDIO|MDC|T[XD][PN+\-]|R[XD][PN+\-]|TXP|TXN|RXP|RXN|PHY[A-Z0-9_]*")),
|
|
80
|
+
("adc", _R(r"ADC[A-Z0-9_]*|AIN\d*|AN\d+|DAC[A-Z0-9_]*|AOUT\d*|SENSOR_?V[PN]|SENSOR[A-Z0-9_]*|VP|VN|AIN[PN]\d*")),
|
|
81
|
+
# direction from the name, for the small analog / power / logic parts whose pins have no better group
|
|
82
|
+
("other_out", _R(r"OUT[A-Z0-9_+\-]*|[A-Z0-9]+_?OUT[A-Z0-9]*|N?Q\d*|Q_?[NB]|DIS|DISCH[A-Z]*|N?PG|N?PGOOD|P_?GOOD|PWR_?(GD|GOOD|OK)|"
|
|
83
|
+
r"N?FLT|N?FAULT|N?FLAG|N?ALERT|N?INT\d*|N?IRQ\d*|RDY|N?READY|BUSY|DONE|STAT\d*|LED[A-Z0-9_]*|SW\d*|LX\d*|"
|
|
84
|
+
r"BST|BOOST|PH|DRV[A-Z0-9_]*|GATE[A-Z0-9_]*|[HL]O|[HL]G|ALARM|TRIP")),
|
|
85
|
+
("other_in", _R(r"IN\d*|IN[+\-PNAB]\d*|[+\-]IN[A-Z0-9]*|IN_?[PN]\d*|[A-Z0-9]+_IN|TRIG[A-Z]*|TRG|TR|THR[A-Z]*|CV|CONT[A-Z]*|"
|
|
86
|
+
r"CTRL[A-Z0-9_]*|CTL|FB\d*|VFB|ADJ|SET|SENSE[A-Z0-9_+\-]*|SNS[A-Z0-9_+\-]*|CS[+\-]|SS|SS_?TR|ILIM|ISET|RT|"
|
|
87
|
+
r"RT_?CLK|COMP|DIR|STEP|PWM[A-Z0-9_]*|[HL]I|D|J|K|S|R|C?LR|PRE|IREF|RSET|RLIM|SEL\d*|A\d?|B\d?")),
|
|
88
|
+
]
|
|
89
|
+
PORT_RX = [re.compile(r"^P([A-Z])(\d{1,2})$"), re.compile(r"^P(\d{1,2})[._](\d{1,2})$"), re.compile(r"^PIO(\d)_(\d{1,2})$"),
|
|
90
|
+
re.compile(r"^(GPIO|IO|GP)(\d{1,3})$"), re.compile(r"^R(\d)(\d)$")]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------ names
|
|
94
|
+
def plain(name):
|
|
95
|
+
"""Pin name without overbar markup: '~{RESET}' / 'R\\E\\S\\E\\T\\' -> 'RESET'."""
|
|
96
|
+
s = re.sub(r"~\{([^}]*)\}", r"\1", str(name or ""))
|
|
97
|
+
return s.replace("\\", "").replace("~", "")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def primary(name):
|
|
101
|
+
"""Primary function of an alternate-function name: 'PA9/USART1_TX' -> 'PA9', 'PH0-OSC_IN' -> 'PH0'."""
|
|
102
|
+
s = plain(name).strip()
|
|
103
|
+
if re.fullmatch(r"(?i)n/?c|not connected", s):
|
|
104
|
+
return "NC"
|
|
105
|
+
if s.startswith("/") and len(s) > 1: # '/RESET' active low
|
|
106
|
+
s = s[1:]
|
|
107
|
+
tok = re.split(r"[/,;(\s]", s, maxsplit=1)[0] or s
|
|
108
|
+
m = re.match(r"^(P[A-Z]\d{1,2}|GPIO\d+|IO\d+)[-_](?=[A-Z])", tok, re.I)
|
|
109
|
+
return m.group(1) if m else tok
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _key(name):
|
|
113
|
+
"""Primary name for matching: upper case, active-low suffixes removed ('RESET#', 'RST_N', 'RESET_B')."""
|
|
114
|
+
s = primary(name).upper()
|
|
115
|
+
if len(s) > 2 and not re.fullmatch(r".*(OUT|IN)\d*_[AB]", s): # OUT_B / IN_B: channel B of a dual part, not active low
|
|
116
|
+
s = re.sub(r"(#|\*|_N|_B|_L)$", "", s)
|
|
117
|
+
return s
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def port_of(name):
|
|
121
|
+
"""(port, number) for a port pin name ('PA9' -> ('A', 9), 'GPIO12' -> ('IO', 12), 'P0.3' -> ('0', 3)); else None."""
|
|
122
|
+
s = _key(name)
|
|
123
|
+
for rx in PORT_RX:
|
|
124
|
+
m = rx.match(s)
|
|
125
|
+
if m:
|
|
126
|
+
port, num = m.groups()
|
|
127
|
+
return ("IO" if port in ("GPIO", "IO", "GP") else port), int(num)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def classify(pin):
|
|
132
|
+
"""-> functional group (see GROUPS). pin: {'name', 'type', 'num', optional 'group'} (an explicit 'group' wins)."""
|
|
133
|
+
if pin.get("group") in GROUPS:
|
|
134
|
+
return pin["group"]
|
|
135
|
+
k, t = _key(pin.get("name", "")), str(pin.get("type") or "").lower()
|
|
136
|
+
num = str(pin.get("num", "")).upper()
|
|
137
|
+
if t == "nc":
|
|
138
|
+
return "nc"
|
|
139
|
+
if port_of(pin.get("name", "")):
|
|
140
|
+
return "ports"
|
|
141
|
+
for g, rx in RX:
|
|
142
|
+
if rx.match(k):
|
|
143
|
+
if g == "usb" and k == "VBUS" and t in ("power", "power_in"):
|
|
144
|
+
return "power"
|
|
145
|
+
if g in ("other_in", "other_out") and t in DIRECTED: # a name only hints the direction: the type says it
|
|
146
|
+
return DIRECTED[t]
|
|
147
|
+
return g
|
|
148
|
+
if num in ("EP", "PAD", "EPAD", "TAB"):
|
|
149
|
+
return "ep"
|
|
150
|
+
if t == "ground":
|
|
151
|
+
return "ground"
|
|
152
|
+
if t in ("power", "power_in"):
|
|
153
|
+
return "power"
|
|
154
|
+
if t == "power_out":
|
|
155
|
+
return "power_out"
|
|
156
|
+
return DIRECTED.get(t, "other")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _natural(s):
|
|
160
|
+
return [int(x) if x.isdigit() else x for x in re.split(r"(\d+)", str(s))]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def stack_key(name):
|
|
164
|
+
"""Pins whose names give the same key may be one net: 'VDD_1', 'VDD_2', 'VDD' -> 'VDD'. Only an index after a
|
|
165
|
+
separator is dropped: 'VDD1' / 'VDD2' (an isolator's two sides) and 'VDD18' / 'VDD33' are different supplies."""
|
|
166
|
+
return re.sub(r"[_\-]\d+$", "", _key(name)) or _key(name)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def stack_keys(names):
|
|
170
|
+
"""name -> stacking key for the power pins of one group. 'VDD_1'..'VDD_5' (instance numbers 1..n or 0..n-1) share
|
|
171
|
+
the key 'VDD'; 'VDD_18' + 'VDD_33' are not instance numbers and keep their own names (never stacked together)."""
|
|
172
|
+
idx = {}
|
|
173
|
+
for n in names:
|
|
174
|
+
m = re.fullmatch(r"(.+?)[_\-](\d+)", _key(n))
|
|
175
|
+
if m:
|
|
176
|
+
idx.setdefault(m.group(1), set()).add(int(m.group(2)))
|
|
177
|
+
ok = {st for st, ix in idx.items() if ix in (set(range(1, len(ix) + 1)), set(range(len(ix))))}
|
|
178
|
+
out = {}
|
|
179
|
+
for n in names:
|
|
180
|
+
m = re.fullmatch(r"(.+?)[_\-](\d+)", _key(n))
|
|
181
|
+
out[n] = m.group(1) if m and m.group(1) in ok else _key(n)
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------ settings
|
|
186
|
+
def settings(rules=None):
|
|
187
|
+
"""[symbols] settings: rules may be a loaded rules dict, a dict of [symbols] overrides, or None (defaults)."""
|
|
188
|
+
from . import rules as RU
|
|
189
|
+
s = dict(RU.DEFAULTS["symbols"])
|
|
190
|
+
if isinstance(rules, dict):
|
|
191
|
+
over = rules.get("symbols") if isinstance(rules.get("symbols"), dict) else {k: v for k, v in rules.items() if k in s}
|
|
192
|
+
s.update(over or {})
|
|
193
|
+
return s
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def wants_layout(pins, rules=None):
|
|
197
|
+
"""True when a part with these pins (or this many) gets the grouped layout."""
|
|
198
|
+
n = pins if isinstance(pins, int) else len(pins)
|
|
199
|
+
t = settings(rules)["layout_pins"]
|
|
200
|
+
return bool(t) and n >= t
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------ arrangement
|
|
204
|
+
def _by_channel(items):
|
|
205
|
+
"""A dual / quad part's pins one channel at a time: 1IN-, 1IN+, 2IN-, 2IN+ (not 1IN-, 2IN-, 1IN+, 2IN+). Only
|
|
206
|
+
when two or more names differ just by a channel number that is not at the end of the name (D0..D7 / A0..A7 are
|
|
207
|
+
buses and keep their order) and every name has the same channels. items: [(_, index, pin)] sorted by stem."""
|
|
208
|
+
split = [re.fullmatch(r"(\D*)(\d+)(\D+)", _key(t[2].get("name", ""))) for t in items]
|
|
209
|
+
if len(items) < 4 or not all(split):
|
|
210
|
+
return items
|
|
211
|
+
stems = {}
|
|
212
|
+
for m in split:
|
|
213
|
+
stems.setdefault(m.group(1) + "#" + m.group(3), set()).add(int(m.group(2)))
|
|
214
|
+
chans = list(stems.values())
|
|
215
|
+
if len(stems) < 2 or len(chans[0]) < 2 or any(c != chans[0] for c in chans):
|
|
216
|
+
return items
|
|
217
|
+
order = {st: i for i, st in enumerate(stems)}
|
|
218
|
+
key = {id(t): (int(m.group(2)), order[m.group(1) + "#" + m.group(3)]) for t, m in zip(items, split)}
|
|
219
|
+
return sorted(items, key=lambda t: key[id(t)])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _blocks(pins, s):
|
|
223
|
+
"""pins -> ordered blocks {group, label, side, slots: [[lead pin, stacked...], ...]}."""
|
|
224
|
+
order = {g: i for i, g in enumerate(list(s["order"]) + [g for g in ORDER if g not in s["order"]])}
|
|
225
|
+
sides = dict(SIDES, **(s.get("sides") or {}))
|
|
226
|
+
by = {}
|
|
227
|
+
for i, p in enumerate(pins):
|
|
228
|
+
g = classify(p)
|
|
229
|
+
port = port_of(p.get("name", "")) if g == "ports" else None
|
|
230
|
+
sub = port[0] if port else ""
|
|
231
|
+
by.setdefault((g, sub), []).append((port[1] if port else i, i, p))
|
|
232
|
+
blocks = []
|
|
233
|
+
for (g, sub), items in sorted(by.items(), key=lambda kv: (order[kv[0][0]], _natural(kv[0][1]))):
|
|
234
|
+
if g == "ports":
|
|
235
|
+
items.sort(key=lambda t: (t[0], _natural(t[2].get("num", ""))))
|
|
236
|
+
else: # numbered names in number order (CBUS0..4, SD0..3, VSS_1..5),
|
|
237
|
+
stem = (lambda n: stack_key(n)) if g in POWER_GROUPS else (lambda n: re.sub(r"\d+", "#", _key(n)))
|
|
238
|
+
stems = {} # otherwise the order given (TXD, RXD, RTS, CTS ...)
|
|
239
|
+
for t in items:
|
|
240
|
+
stems.setdefault(stem(t[2].get("name", "")), t[1])
|
|
241
|
+
items.sort(key=lambda t: (stems[stem(t[2].get("name", ""))], _natural(_key(t[2].get("name", ""))), t[1]))
|
|
242
|
+
if g not in POWER_GROUPS:
|
|
243
|
+
items = _by_channel(items)
|
|
244
|
+
slots, at, keys = [], {}, []
|
|
245
|
+
sk = stack_keys([t[2].get("name", "") for t in items])
|
|
246
|
+
for _, _, p in items:
|
|
247
|
+
k = sk[p.get("name", "")]
|
|
248
|
+
if g in POWER_GROUPS and s["stack_power"] != "none" and k in at:
|
|
249
|
+
slots[at[k]].append(p)
|
|
250
|
+
else:
|
|
251
|
+
at[k] = len(slots)
|
|
252
|
+
slots.append([p])
|
|
253
|
+
keys.append(k)
|
|
254
|
+
blocks.append({"group": g, "label": f"P{sub}" if sub and sub.isalpha() and len(sub) == 1 else sub or g,
|
|
255
|
+
"side": sides[g], "slots": slots, "nets": {id(sl): k for sl, k in zip(slots, keys)}})
|
|
256
|
+
return blocks
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _npins(blocks):
|
|
260
|
+
return sum(len(sl) for b in blocks for sl in b["slots"])
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _height(blocks):
|
|
264
|
+
"""Rows a side takes: slots plus one gap between blocks (pieces of one block that follow each other: no gap)."""
|
|
265
|
+
rows = sum(len(b["slots"]) for b in blocks)
|
|
266
|
+
return rows + sum(1 for a, b in zip(blocks, blocks[1:]) if not (b.get("piece") and b["piece"][0] == id(a.get("src", a))))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _pieces(b, step=8):
|
|
270
|
+
"""A flexible block cut where it may be split between the sides (every `step` slots, blocks of 16+ slots)."""
|
|
271
|
+
n = len(b["slots"])
|
|
272
|
+
if n < 16:
|
|
273
|
+
return [b]
|
|
274
|
+
src = b.get("src", b)
|
|
275
|
+
return [dict(b, slots=b["slots"][i:i + step], piece=(id(src), i), src=src) for i in range(0, n, step)]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _balance(blocks, power_side="top"):
|
|
279
|
+
"""Assign flexible blocks to left/right: left = fixed left + flex[:k], right = flex[k:] + fixed right. Ties keep
|
|
280
|
+
'other' pins (no direction known) on the left with the inputs. power_side "left": the top blocks go first and the
|
|
281
|
+
bottom blocks last on the left side, and the top and bottom edges stay empty."""
|
|
282
|
+
top = [b for b in blocks if b["side"] == "top"]
|
|
283
|
+
bot = [b for b in blocks if b["side"] == "bottom"]
|
|
284
|
+
fixed_l = [b for b in blocks if b["side"] == "left" and b["group"] not in FLEX]
|
|
285
|
+
fixed_r = [b for b in blocks if b["side"] == "right" and b["group"] not in FLEX]
|
|
286
|
+
flex = [b for b in blocks if b["group"] in FLEX and b["side"] in ("left", "right")]
|
|
287
|
+
head, tail = (top, bot) if power_side == "left" else ([], [])
|
|
288
|
+
pieces = [pc for b in flex for pc in _pieces(b)]
|
|
289
|
+
best = None
|
|
290
|
+
for k in range(len(pieces) + 1):
|
|
291
|
+
left, right = head + fixed_l + pieces[:k] + tail, pieces[k:] + fixed_r
|
|
292
|
+
split = 0 < k < len(pieces) and pieces[k].get("piece") and pieces[k]["piece"][1] > 0
|
|
293
|
+
hl, hr = _height(left), _height(right)
|
|
294
|
+
moved = sum(1 for pc in pieces[k:] if pc["group"] == "other")
|
|
295
|
+
cost = (max(hl, hr) + (SPLIT_PENALTY if split else 0), abs(hl - hr), moved, k)
|
|
296
|
+
if best is None or cost < best[0]:
|
|
297
|
+
best = (cost, left, right)
|
|
298
|
+
_, left, right = best
|
|
299
|
+
if power_side == "left":
|
|
300
|
+
return {"left": left, "right": right, "top": [], "bottom": []}
|
|
301
|
+
return {"left": left, "right": right, "top": top, "bottom": bot}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _chunk(block, size):
|
|
305
|
+
return [dict(block, slots=block["slots"][i:i + size]) for i in range(0, len(block["slots"]), size)] if size else [block]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def arrange(pins, rules=None, split=None):
|
|
309
|
+
"""pins -> [unit {'unit': n, 'sides': {side: [block, ...]}}]. split: True/False forces multi-part on/off."""
|
|
310
|
+
s = settings(rules)
|
|
311
|
+
blocks = _blocks(pins, s)
|
|
312
|
+
big = s["split_pins"] and len(pins) > s["split_pins"]
|
|
313
|
+
ps = s.get("power_side", "top")
|
|
314
|
+
if not (big if split is None else split):
|
|
315
|
+
return [{"unit": 1, "sides": _balance(blocks, ps)}]
|
|
316
|
+
pw = [b for b in blocks if b["side"] in ("top", "bottom") or b["group"] in POWER_GROUPS or b["group"] == "nc"] # part A
|
|
317
|
+
sig = [b for b in blocks if b not in pw]
|
|
318
|
+
cap = max(8, s["unit_pins"])
|
|
319
|
+
units, cur = [], []
|
|
320
|
+
for b in sig:
|
|
321
|
+
for c in (_chunk(b, cap) if _npins([b]) > cap else [b]):
|
|
322
|
+
if cur and _npins(cur) + _npins([c]) > cap:
|
|
323
|
+
units.append(cur); cur = []
|
|
324
|
+
cur.append(c)
|
|
325
|
+
if cur:
|
|
326
|
+
units.append(cur)
|
|
327
|
+
out = [{"unit": 1, "sides": _balance(pw, ps)}] if pw else []
|
|
328
|
+
return out + [{"unit": len(out) + i + 1, "sides": _balance(u, ps)} for i, u in enumerate(units)]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def arrange_pins(pins, rules=None, split=None):
|
|
332
|
+
"""For datasheet.py and reviews: the pins in layout order, each a copy with 'group', 'side', 'unit' (1 = part A)
|
|
333
|
+
and 'stacked_on' (the pin number it is stacked on, for stacked power pins)."""
|
|
334
|
+
out = []
|
|
335
|
+
for u in arrange(pins, rules, split):
|
|
336
|
+
for side in ("left", "right", "top", "bottom"):
|
|
337
|
+
for b in u["sides"][side]:
|
|
338
|
+
for sl in b["slots"]:
|
|
339
|
+
for j, p in enumerate(sl):
|
|
340
|
+
out.append(dict(p, group=b["group"], side=side, unit=u["unit"], **({"stacked_on": sl[0].get("num")} if j else {})))
|
|
341
|
+
return out
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ------------------------------------------------------------------ geometry
|
|
345
|
+
def _up(v, g):
|
|
346
|
+
return g * -(-int(v) // g)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _rows(blocks):
|
|
350
|
+
"""[slot or None (gap)] down one side."""
|
|
351
|
+
out = []
|
|
352
|
+
for i, b in enumerate(blocks):
|
|
353
|
+
if i and not (b.get("piece") and b["piece"][0] == id(blocks[i - 1].get("src", blocks[i - 1]))):
|
|
354
|
+
out.append(None)
|
|
355
|
+
out += b["slots"]
|
|
356
|
+
return out
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _nlen(p):
|
|
360
|
+
return len(plain(p.get("name", "")))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def build(name, pins, description="", designator="U?", footprint=None, comment="*", rules=None, split=None):
|
|
364
|
+
"""Grouped multi-block (and optionally multi-part) IC symbol, as an altium_lib symbol dict."""
|
|
365
|
+
bad = [p.get("num") for p in pins if not str(p.get("num", "")).strip()]
|
|
366
|
+
if bad:
|
|
367
|
+
raise ValueError(f"{name}: {len(bad)} pins have no number")
|
|
368
|
+
s = settings(rules)
|
|
369
|
+
g, L = s["grid"] // 10, s["pin_length"] // 10
|
|
370
|
+
units = arrange(pins, rules, split)
|
|
371
|
+
prims, parts = [], []
|
|
372
|
+
tallest = (0, 0)
|
|
373
|
+
for u in units:
|
|
374
|
+
sd = u["sides"]
|
|
375
|
+
nets = {k: v for bl in sd.values() for b in bl for k, v in b.get("nets", {}).items()}
|
|
376
|
+
left, right, top, bot = _rows(sd["left"]), _rows(sd["right"]), _rows(sd["top"]), _rows(sd["bottom"])
|
|
377
|
+
names = lambda rows: max([_nlen(sl[0]) for sl in rows if sl] or [0])
|
|
378
|
+
lch, rch = names(left), names(right)
|
|
379
|
+
t_need = _up(names(top) * CHAR + 10, g) if top else 0 # vertical names of top / bottom pins
|
|
380
|
+
b_need = _up(names(bot) * CHAR + 10, g) if bot else 0
|
|
381
|
+
nrows = max(len(left), len(right), 1)
|
|
382
|
+
cols = max(len(top), len(bot))
|
|
383
|
+
def shape(w): # -> (w + h, w, h, top room, bottom room, x0s)
|
|
384
|
+
W = w // g
|
|
385
|
+
fits = lambda rows, x0: (x0 - g // 2 > lch * CHAR + g // 2 # clear of the side names
|
|
386
|
+
and x0 + g * (len(rows) - 1) + g // 2 < w - rch * CHAR - g // 2)
|
|
387
|
+
x0s = {}
|
|
388
|
+
for rows in (top, bot): # centred, else just right of the left names
|
|
389
|
+
c = [g * max(1, (W - len(rows) + 1) // 2), _up(lch * CHAR + g + 1, g)]
|
|
390
|
+
x0s[id(rows)] = next((x for x in c if fits(rows, x)), c[0])
|
|
391
|
+
clear = lambda rows: not rows or fits(rows, x0s[id(rows)])
|
|
392
|
+
tr, br = (0 if clear(top) else t_need), (0 if clear(bot) else b_need) # side pins start below them only if they meet
|
|
393
|
+
h = max(tr + g * (nrows + 1) + br, t_need + b_need + 2 * g)
|
|
394
|
+
return w + h, w, h, tr, br, x0s
|
|
395
|
+
w0 = max(4 * g, _up(lch * CHAR + rch * CHAR + 2 * g, g), g * (cols + 1))
|
|
396
|
+
wide = _up(lch * CHAR + rch * CHAR + 2 * g, g) + g * (cols + 2) # wide enough for the top/bottom pins between names
|
|
397
|
+
_, w, h, t_room, b_room, x0s = min(shape(w0), shape(max(w0, wide)), key=lambda t: t[:3])
|
|
398
|
+
prims.append(("rect", 0, 0, w, -h)); parts.append(u["unit"])
|
|
399
|
+
tallest = max(tallest, (h, w))
|
|
400
|
+
|
|
401
|
+
def put(sl, side, x, y):
|
|
402
|
+
for j, p in enumerate(sl):
|
|
403
|
+
q = dict(p)
|
|
404
|
+
if j:
|
|
405
|
+
if s["stack_power"] == "hide":
|
|
406
|
+
q.update(hidden=True, net=q.get("net") or nets.get(id(sl)) or stack_key(p.get("name", "")))
|
|
407
|
+
else:
|
|
408
|
+
q.update(show_name=False, show_num=False)
|
|
409
|
+
prims.append(("pin", q, side, x, y, L)); parts.append(u["unit"])
|
|
410
|
+
for i, sl in enumerate(left):
|
|
411
|
+
if sl:
|
|
412
|
+
put(sl, "left", 0, -t_room - g * (i + 1))
|
|
413
|
+
for i, sl in enumerate(right):
|
|
414
|
+
if sl:
|
|
415
|
+
put(sl, "right", w, -t_room - g * (i + 1))
|
|
416
|
+
for rows, side, y in ((top, "top", 0), (bot, "bottom", -h)):
|
|
417
|
+
x0 = x0s[id(rows)]
|
|
418
|
+
for j, sl in enumerate(rows):
|
|
419
|
+
if sl:
|
|
420
|
+
put(sl, side, x0 + g * j, y)
|
|
421
|
+
h, _ = tallest
|
|
422
|
+
top_pins = any(u["sides"]["top"] for u in units)
|
|
423
|
+
bot_pins = any(u["sides"]["bottom"] for u in units)
|
|
424
|
+
sym = {"name": name, "description": description, "designator": designator, "comment": comment, "footprint": footprint,
|
|
425
|
+
"prims": prims, "label_at": ((0, (L + 2) if top_pins else 2), (0, -h - (L + 8 if bot_pins else 8)))}
|
|
426
|
+
if len(units) > 1:
|
|
427
|
+
sym.update(parts=len(units), prim_parts=parts)
|
|
428
|
+
return sym
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def relayout(sym, rules=None, split=None):
|
|
432
|
+
"""An altium_lib symbol dict (e.g. converted from EasyEDA) -> the grouped layout, keeping its name, labels,
|
|
433
|
+
footprint and parameters. Pins keep number, name, type, shape. Only single-part symbols are re-laid out."""
|
|
434
|
+
if sym.get("parts", 1) > 1 or any(sym.get("prim_modes") or []):
|
|
435
|
+
return sym
|
|
436
|
+
pins, seen = [], set()
|
|
437
|
+
for pr in sym["prims"]:
|
|
438
|
+
if pr[0] == "pin" and pr[1]["num"] not in seen:
|
|
439
|
+
seen.add(pr[1]["num"])
|
|
440
|
+
pins.append({k: v for k, v in pr[1].items() if k in ("num", "name", "type", "shape", "description")})
|
|
441
|
+
new = build(sym["name"], pins, sym.get("description", ""), sym.get("designator", "U?"), sym.get("footprint"),
|
|
442
|
+
sym.get("comment", "*"), rules, split)
|
|
443
|
+
if sym.get("params"):
|
|
444
|
+
new["params"] = sym["params"]
|
|
445
|
+
return new
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ------------------------------------------------------------------ checks
|
|
449
|
+
def check(sym, grid=100):
|
|
450
|
+
"""Problems in a laid-out symbol: pins off the grid, two different pins at one spot (other than stacked power
|
|
451
|
+
pins with the same name), a pin whose end or line lies on another pin's line, pin lines crossing, pin text that
|
|
452
|
+
overlaps (text_problems). -> [text]."""
|
|
453
|
+
g = grid // 10
|
|
454
|
+
probs, at, segs = [], {}, []
|
|
455
|
+
parts = sym.get("prim_parts") or [1] * len(sym["prims"])
|
|
456
|
+
for pr, u in zip(sym["prims"], parts):
|
|
457
|
+
if pr[0] != "pin":
|
|
458
|
+
continue
|
|
459
|
+
_, p, side, x, y, L, *_ = pr
|
|
460
|
+
dx, dy = {"left": (-1, 0), "right": (1, 0), "top": (0, 1), "bottom": (0, -1)}[side]
|
|
461
|
+
ex, ey = x + dx * L, y + dy * L
|
|
462
|
+
if any(v % g for v in (x, y, ex, ey)):
|
|
463
|
+
probs.append(f"pin {p['num']} off the {grid} mil grid at ({x}, {y})")
|
|
464
|
+
key = (u, x, y)
|
|
465
|
+
if key in at and stack_key(at[key]["name"]) != stack_key(p["name"]):
|
|
466
|
+
probs.append(f"pins {at[key]['num']} and {p['num']} overlap at ({x}, {y}) in part {u}")
|
|
467
|
+
at.setdefault(key, p)
|
|
468
|
+
segs.append((u, p, (x, y), (ex, ey)))
|
|
469
|
+
# a pin whose end or body lies on another pin's line, or two pin lines crossing (stacked pins share one line: fine)
|
|
470
|
+
on = lambda q, a, b: min(a[0], b[0]) <= q[0] <= max(a[0], b[0]) and min(a[1], b[1]) <= q[1] <= max(a[1], b[1]) and \
|
|
471
|
+
(b[0] - a[0]) * (q[1] - a[1]) == (b[1] - a[1]) * (q[0] - a[0])
|
|
472
|
+
for i, (u, p, a, b) in enumerate(segs):
|
|
473
|
+
for v, q, c, d in segs[i + 1:]:
|
|
474
|
+
if u != v or (a, b) == (c, d):
|
|
475
|
+
continue
|
|
476
|
+
if on(c, a, b) or on(d, a, b) or on(a, c, d) or on(b, c, d) or _cross(a, b, c, d):
|
|
477
|
+
probs.append(f"pin {q['num']} lies on or crosses pin {p['num']}'s line in part {u}")
|
|
478
|
+
return probs + text_problems(sym)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# pin text as Altium draws it (default 10 pt pin font), DXP units (10 mil): a little wide, so a symbol that passes looks
|
|
482
|
+
# clear in Altium and KiCad. The layout itself sizes bodies with CHAR (wider still).
|
|
483
|
+
TXT_W, TXT_H, TXT_GAP = 6, 8, 3
|
|
484
|
+
CONNECTOR_PREFIX = ("J", "P", "CN", "CON", "X", "XS", "XP", "JP", "SW", "S", "K", "T") # pin order is the drawing
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _boxes(sym):
|
|
488
|
+
"""[(part, what, (x0, y0, x1, y1))] for every shown pin name and number, and {part: body rect}."""
|
|
489
|
+
parts = sym.get("prim_parts") or [1] * len(sym["prims"])
|
|
490
|
+
body, out = {}, []
|
|
491
|
+
for pr, u in zip(sym["prims"], parts):
|
|
492
|
+
if pr[0] == "rect":
|
|
493
|
+
r = (min(pr[1], pr[3]), min(pr[2], pr[4]), max(pr[1], pr[3]), max(pr[2], pr[4]))
|
|
494
|
+
if u not in body or (r[2] - r[0]) * (r[3] - r[1]) > (body[u][2] - body[u][0]) * (body[u][3] - body[u][1]):
|
|
495
|
+
body[u] = r
|
|
496
|
+
pos = {}
|
|
497
|
+
for pr, u in zip(sym["prims"], parts):
|
|
498
|
+
if pr[0] == "pin":
|
|
499
|
+
pos.setdefault((u, pr[2], pr[3] if pr[2] in ("left", "right") else pr[4]), set()).add(pr[4] if pr[2] in ("left", "right") else pr[3])
|
|
500
|
+
gaps = [b - a for v in pos.values() for a, b in zip(sorted(v), sorted(v)[1:])]
|
|
501
|
+
f = min(1.0, min(gaps or [10]) / 10) # a 50 mil pitch symbol is drawn with a font half as big
|
|
502
|
+
tw, th, tg = TXT_W * f, TXT_H * f, TXT_GAP * f
|
|
503
|
+
for pr, u in zip(sym["prims"], parts):
|
|
504
|
+
if pr[0] != "pin":
|
|
505
|
+
continue
|
|
506
|
+
_, p, side, x, y, L, *show = pr
|
|
507
|
+
if p.get("hidden"):
|
|
508
|
+
continue
|
|
509
|
+
sn = p.get("show_name", show[0] if show else True) and plain(p.get("name", "")) not in ("", "~")
|
|
510
|
+
sd = p.get("show_num", show[0] if show else True) and str(p.get("num", ""))
|
|
511
|
+
n, m, h = len(plain(p.get("name", ""))) * tw, len(str(p.get("num", ""))) * tw, th / 2
|
|
512
|
+
if side in ("left", "right"):
|
|
513
|
+
s = 1 if side == "left" else -1 # +1: the body is to the right of the pin's end
|
|
514
|
+
if sn:
|
|
515
|
+
a = x + s * tg
|
|
516
|
+
out.append((u, f"pin {p['num']} name", (min(a, a + s * n), y - h, max(a, a + s * n), y + h)))
|
|
517
|
+
if sd:
|
|
518
|
+
mx = x - s * L / 2
|
|
519
|
+
out.append((u, f"pin {p['num']} number", (mx - m / 2, y + f, mx + m / 2, y + f + th)))
|
|
520
|
+
else:
|
|
521
|
+
s = -1 if side == "top" else 1 # names run down from a top pin, up from a bottom one
|
|
522
|
+
if sn:
|
|
523
|
+
a = y + s * tg
|
|
524
|
+
out.append((u, f"pin {p['num']} name", (x - h, min(a, a + s * n), x + h, max(a, a + s * n))))
|
|
525
|
+
if sd:
|
|
526
|
+
my = y - s * L / 2
|
|
527
|
+
out.append((u, f"pin {p['num']} number", (x - f - th, my - m / 2, x - f, my + m / 2)))
|
|
528
|
+
return out, body
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def text_problems(sym):
|
|
532
|
+
"""Pin text that would overlap in Altium: two pin names or numbers on top of each other, or a pin name running
|
|
533
|
+
out of the body (the estimate: TXT_W per character, TXT_H high). -> [text]."""
|
|
534
|
+
out, body = _boxes(sym)
|
|
535
|
+
probs = []
|
|
536
|
+
hit = lambda a, b: a[0] < b[2] - 0.5 and b[0] < a[2] - 0.5 and a[1] < b[3] - 0.5 and b[1] < a[3] - 0.5
|
|
537
|
+
for i, (u, w, a) in enumerate(out):
|
|
538
|
+
r = body.get(u)
|
|
539
|
+
if r and w.endswith("name") and (a[0] < r[0] - 0.5 or a[2] > r[2] + 0.5 or a[1] < r[1] - 0.5 or a[3] > r[3] + 0.5):
|
|
540
|
+
probs.append(f"{w} runs out of the body" + (f" in part {u}" if len(body) > 1 else ""))
|
|
541
|
+
for v, w2, b in out[i + 1:]:
|
|
542
|
+
if u == v and hit(a, b):
|
|
543
|
+
probs.append(f"{w} overlaps {w2}" + (f" in part {u}" if len(body) > 1 else ""))
|
|
544
|
+
return probs
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def is_box_ic(sym):
|
|
548
|
+
"""True for a plain IC symbol: designator U / IC (or 8+ pins and not a connector), one part, a rectangle body with every pin on its edge, and nothing
|
|
549
|
+
else drawn but small marks (a pin 1 dot, text). Those can be re-laid out by the house rule without losing a drawing;
|
|
550
|
+
connectors, transistors, op-amp triangles and logic gates cannot."""
|
|
551
|
+
if sym.get("parts", 1) > 1 or any(sym.get("prim_modes") or []):
|
|
552
|
+
return False
|
|
553
|
+
pre = re.sub(r"[^A-Z]", "", str(sym.get("designator", "")).upper())
|
|
554
|
+
rects = [pr for pr in sym["prims"] if pr[0] == "rect"]
|
|
555
|
+
pins = [pr for pr in sym["prims"] if pr[0] == "pin"]
|
|
556
|
+
if len(rects) != 1 or len(pins) < 3 or pre in CONNECTOR_PREFIX or (pre not in ("U", "IC") and len(pins) < 8):
|
|
557
|
+
return False
|
|
558
|
+
x0, x1 = sorted((rects[0][1], rects[0][3]))
|
|
559
|
+
y0, y1 = sorted((rects[0][2], rects[0][4]))
|
|
560
|
+
if any(pr[0] in ("line", "polyline", "polygon", "arc", "bezier") or pr[0] == "circle" and pr[3] > 10
|
|
561
|
+
for pr in sym["prims"]):
|
|
562
|
+
return False
|
|
563
|
+
edge = {"left": lambda x, y: abs(x - x0) < 1 and y0 <= y <= y1, "right": lambda x, y: abs(x - x1) < 1 and y0 <= y <= y1,
|
|
564
|
+
"top": lambda x, y: abs(y - y1) < 1 and x0 <= x <= x1, "bottom": lambda x, y: abs(y - y0) < 1 and x0 <= x <= x1}
|
|
565
|
+
return all(edge[pr[2]](pr[3], pr[4]) for pr in pins)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _cross(a, b, c, d):
|
|
569
|
+
"""Axis-aligned segments a-b and c-d (one horizontal, one vertical) cross in their interiors."""
|
|
570
|
+
if (a[0] == b[0]) == (c[0] == d[0]):
|
|
571
|
+
return False
|
|
572
|
+
(h1, h2), (v1, v2) = ((a, b), (c, d)) if a[1] == b[1] else ((c, d), (a, b))
|
|
573
|
+
return min(h1[0], h2[0]) < v1[0] < max(h1[0], h2[0]) and min(v1[1], v2[1]) < h1[1] < max(v1[1], v2[1])
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# ------------------------------------------------------------------ SVG preview
|
|
577
|
+
def svg(sym, scale=2.0):
|
|
578
|
+
"""SVG drawing of an altium_lib symbol dict (every part side by side): body, pins, names, numbers."""
|
|
579
|
+
parts = sym.get("prim_parts") or [1] * len(sym["prims"])
|
|
580
|
+
units = sorted(set(parts))
|
|
581
|
+
S, out, xoff = scale, [], 0.0
|
|
582
|
+
fs = 7 * S
|
|
583
|
+
boxes = []
|
|
584
|
+
for u in units:
|
|
585
|
+
prs = [pr for pr, pu in zip(sym["prims"], parts) if pu == u]
|
|
586
|
+
xs, ys = [], []
|
|
587
|
+
for pr in prs:
|
|
588
|
+
if pr[0] == "rect":
|
|
589
|
+
xs += [pr[1], pr[3]]; ys += [pr[2], pr[4]]
|
|
590
|
+
elif pr[0] == "pin":
|
|
591
|
+
_, p, side, x, y, L, *_ = pr
|
|
592
|
+
dx, dy = {"left": (-1, 0), "right": (1, 0), "top": (0, 1), "bottom": (0, -1)}[side]
|
|
593
|
+
xs += [x, x + dx * L]; ys += [y, y + dy * L]
|
|
594
|
+
x0, x1, y0, y1 = min(xs or [0]) - 10, max(xs or [0]) + 10, min(ys or [0]) - 10, max(ys or [0]) + 20
|
|
595
|
+
boxes.append((u, prs, x0, x1, y0, y1, xoff))
|
|
596
|
+
xoff += (x1 - x0) + 30
|
|
597
|
+
W, H = xoff * S, max(b[5] - b[4] for b in boxes) * S + 20
|
|
598
|
+
esc = lambda t: str(t).replace("&", "&").replace("<", "<").replace(">", ">")
|
|
599
|
+
out.append(f'<svg xmlns="http://www.w3.org/2000/svg" width="{W:.0f}" height="{H:.0f}" viewBox="0 0 {W:.0f} {H:.0f}" '
|
|
600
|
+
f'font-family="Arial, sans-serif" font-size="{fs:.0f}">')
|
|
601
|
+
out.append(f'<rect width="{W:.0f}" height="{H:.0f}" fill="#ffffff"/>')
|
|
602
|
+
for u, prs, x0, x1, y0, y1, xo in boxes:
|
|
603
|
+
X = lambda x: (x - x0 + xo) * S
|
|
604
|
+
Y = lambda y: (y1 - y) * S + 10
|
|
605
|
+
if len(units) > 1:
|
|
606
|
+
out.append(f'<text x="{X(x0 + 2):.1f}" y="{Y(y1 - 8):.1f}" fill="#555">{esc(sym["name"])} part {chr(64 + u)}</text>')
|
|
607
|
+
for pr in prs:
|
|
608
|
+
if pr[0] == "rect":
|
|
609
|
+
out.append(f'<rect x="{X(min(pr[1], pr[3])):.1f}" y="{Y(max(pr[2], pr[4])):.1f}" width="{abs(pr[3] - pr[1]) * S:.1f}" '
|
|
610
|
+
f'height="{abs(pr[4] - pr[2]) * S:.1f}" fill="#ffffb0" stroke="#800000" stroke-width="1.5"/>')
|
|
611
|
+
elif pr[0] == "line":
|
|
612
|
+
out.append(f'<line x1="{X(pr[1]):.1f}" y1="{Y(pr[2]):.1f}" x2="{X(pr[3]):.1f}" y2="{Y(pr[4]):.1f}" stroke="#000080"/>')
|
|
613
|
+
elif pr[0] in ("polyline", "polygon"):
|
|
614
|
+
pts = " ".join(f"{X(a):.1f},{Y(b):.1f}" for a, b in pr[1])
|
|
615
|
+
out.append(f'<{pr[0]} points="{pts}" fill="none" stroke="#000080"/>')
|
|
616
|
+
elif pr[0] == "circle":
|
|
617
|
+
out.append(f'<circle cx="{X(pr[1]):.1f}" cy="{Y(pr[2]):.1f}" r="{pr[3] * S:.1f}" fill="none" stroke="#000080"/>')
|
|
618
|
+
elif pr[0] == "text":
|
|
619
|
+
out.append(f'<text x="{X(pr[1]):.1f}" y="{Y(pr[2]):.1f}">{esc(pr[3])}</text>')
|
|
620
|
+
for pr in prs:
|
|
621
|
+
if pr[0] != "pin":
|
|
622
|
+
continue
|
|
623
|
+
_, p, side, x, y, L, *show = pr
|
|
624
|
+
if p.get("hidden"):
|
|
625
|
+
continue
|
|
626
|
+
dx, dy = {"left": (-1, 0), "right": (1, 0), "top": (0, 1), "bottom": (0, -1)}[side]
|
|
627
|
+
ex, ey = x + dx * L, y + dy * L
|
|
628
|
+
out.append(f'<line x1="{X(x):.1f}" y1="{Y(y):.1f}" x2="{X(ex):.1f}" y2="{Y(ey):.1f}" stroke="#000080" stroke-width="1.2"/>')
|
|
629
|
+
out.append(f'<circle cx="{X(ex):.1f}" cy="{Y(ey):.1f}" r="1.2" fill="#000080"/>')
|
|
630
|
+
sn = p.get("show_name", show[0] if show else True) and p.get("name") not in ("", "~")
|
|
631
|
+
sd = p.get("show_num", show[0] if show else True)
|
|
632
|
+
nm, num = esc(plain(p.get("name", ""))), esc(p.get("num", ""))
|
|
633
|
+
over = ' text-decoration="overline"' if re.search(r"~\{|\\", str(p.get("name", ""))) else ""
|
|
634
|
+
if dy == 0:
|
|
635
|
+
tx = X(x) + (6 if dx < 0 else -6)
|
|
636
|
+
anchor = "start" if dx < 0 else "end"
|
|
637
|
+
if sn:
|
|
638
|
+
out.append(f'<text x="{tx:.1f}" y="{Y(y) + fs / 3:.1f}" text-anchor="{anchor}"{over}>{nm}</text>')
|
|
639
|
+
if sd:
|
|
640
|
+
mx = (X(x) + X(ex)) / 2
|
|
641
|
+
out.append(f'<text x="{mx:.1f}" y="{Y(y) - 2:.1f}" text-anchor="middle" fill="#800000" font-size="{fs * 0.85:.0f}">{num}</text>')
|
|
642
|
+
else:
|
|
643
|
+
tx, ty = X(x) + fs / 3, Y(y) + (6 if dy > 0 else -6)
|
|
644
|
+
anchor = "end" if dy > 0 else "start"
|
|
645
|
+
if sn:
|
|
646
|
+
out.append(f'<text x="{tx:.1f}" y="{ty:.1f}" text-anchor="{anchor}" transform="rotate(-90 {tx:.1f} {ty:.1f})"'
|
|
647
|
+
f'{over}>{nm}</text>')
|
|
648
|
+
if sd:
|
|
649
|
+
my = (Y(y) + Y(ey)) / 2
|
|
650
|
+
out.append(f'<text x="{X(x) - 2:.1f}" y="{my:.1f}" text-anchor="middle" fill="#800000" font-size="{fs * 0.85:.0f}" '
|
|
651
|
+
f'transform="rotate(-90 {X(x) - 2:.1f} {my:.1f})">{num}</text>')
|
|
652
|
+
out.append("</svg>")
|
|
653
|
+
return "\n".join(out) + "\n"
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ------------------------------------------------------------------ inputs
|
|
657
|
+
ETYPE_NAME = {0: "input", 1: "io", 2: "output", 3: "open_collector", 4: "passive", 5: "tristate", 6: "open_emitter", 7: "power"}
|
|
658
|
+
TYPE_ALIASES = {"i": "input", "in": "input", "o": "output", "out": "output", "bidirectional": "io", "i/o": "io", "inout": "io",
|
|
659
|
+
"pwr": "power", "power_input": "power_in", "power_output": "power_out", "gnd": "ground", "tri_state": "tristate",
|
|
660
|
+
"unspecified": "passive", "free": "passive", "no_connect": "nc", "": "passive", "p": "power", "analog": "passive"}
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _ptype(t):
|
|
664
|
+
t = str(t or "").strip().lower().replace(" ", "_")
|
|
665
|
+
t = TYPE_ALIASES.get(t, t)
|
|
666
|
+
return t if t in AL.ETYPE else "passive"
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def pins_from_csv(path):
|
|
670
|
+
"""CSV/TSV with a header: number (num/pin/#), name, optional type, group. -> pins."""
|
|
671
|
+
text = Path(path).read_text(encoding="utf-8-sig")
|
|
672
|
+
dialect = "excel-tab" if "\t" in text.splitlines()[0] else "excel"
|
|
673
|
+
rows = list(csv.DictReader(text.splitlines(), dialect=dialect))
|
|
674
|
+
pick = lambda r, *ks: next((str(r[k]).strip() for k in r if k and k.strip().lower() in ks and r[k] is not None), "")
|
|
675
|
+
out = []
|
|
676
|
+
for r in rows:
|
|
677
|
+
num, name = pick(r, "num", "number", "pin", "#", "pin number", "pin_number"), pick(r, "name", "pin name", "signal", "pin_name")
|
|
678
|
+
if not num:
|
|
679
|
+
continue
|
|
680
|
+
p = {"num": num, "name": name or num, "type": _ptype(pick(r, "type", "electrical", "electrical type", "etype", "dir"))}
|
|
681
|
+
grp = pick(r, "group")
|
|
682
|
+
if grp:
|
|
683
|
+
p["group"] = grp
|
|
684
|
+
out.append(p)
|
|
685
|
+
return out
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def pins_from_schlib(path, index=0):
|
|
689
|
+
_, syms = AL.read_schlib(path)
|
|
690
|
+
sy = syms[index]
|
|
691
|
+
slot_names = {v: k for k, v in AL.PIN_SYMBOL.items()}
|
|
692
|
+
pins, seen = [], set()
|
|
693
|
+
for r in sy["records"]:
|
|
694
|
+
if r.get("RECORD") == "2" and r["num"] not in seen and not r["mode"]:
|
|
695
|
+
seen.add(r["num"])
|
|
696
|
+
d = {sl: slot_names.get(v, v) for sl, v in zip(AL.PIN_SLOTS, r["symbols"]) if v}
|
|
697
|
+
shape = next((k for k, v in AL.PIN_SHAPE.items() if v == d), None)
|
|
698
|
+
p = {"num": r["num"], "name": _kicad_name(r["name"]), "type": ETYPE_NAME.get(r["etype"], "passive")}
|
|
699
|
+
if shape and shape != "line":
|
|
700
|
+
p["shape"] = shape
|
|
701
|
+
pins.append(p)
|
|
702
|
+
return sy["name"], pins
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _kicad_name(s):
|
|
706
|
+
"""Altium 'R\\E\\S\\E\\T\\' -> '~{RESET}' so altium_lib.overline writes it back the same."""
|
|
707
|
+
from .kicad_export import _kicad_pin_name
|
|
708
|
+
return _kicad_pin_name(s)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def load_pins(src, http=None):
|
|
712
|
+
"""CSV / JSON (pin list, datasheet extract or symbol JSON) / .SchLib / LIB.kicad_sym:NAME / kicad:Lib:Name ->
|
|
713
|
+
(name, pins, extra symbol fields)."""
|
|
714
|
+
s = str(src)
|
|
715
|
+
low = s.lower()
|
|
716
|
+
if low.endswith((".csv", ".tsv", ".txt")):
|
|
717
|
+
return Path(s).stem, pins_from_csv(s), {}
|
|
718
|
+
if low.endswith(".json"):
|
|
719
|
+
d = json.loads(Path(s).read_text(encoding="utf-8"))
|
|
720
|
+
if isinstance(d, list):
|
|
721
|
+
return Path(s).stem, d, {}
|
|
722
|
+
d = d.get("symbol", d) if isinstance(d.get("symbol"), dict) else d
|
|
723
|
+
extra = {k: d[k] for k in ("description", "designator", "footprint") if d.get(k)}
|
|
724
|
+
return d.get("name") or d.get("mpn") or Path(s).stem, d["pins"], extra
|
|
725
|
+
if low.endswith(".schlib"):
|
|
726
|
+
name, pins = pins_from_schlib(s)
|
|
727
|
+
return name, pins, {}
|
|
728
|
+
from . import kicad_import as K
|
|
729
|
+
ref = s[6:] if low.startswith("kicad:") else s
|
|
730
|
+
chain, _ = K.load_symbol(ref, http if low.startswith("kicad:") else None)
|
|
731
|
+
sym = K.convert_symbol(chain, None, None, [], None, [])
|
|
732
|
+
pins, seen = [], set()
|
|
733
|
+
for pr in sym["prims"]:
|
|
734
|
+
if pr[0] == "pin" and pr[1]["num"] not in seen:
|
|
735
|
+
seen.add(pr[1]["num"])
|
|
736
|
+
pins.append({k: v for k, v in pr[1].items() if k in ("num", "name", "type", "shape")})
|
|
737
|
+
return str(sym["name"]).strip('"'), pins, {k: sym[k] for k in ("description", "designator") if sym.get(k)}
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def main(argv=None):
|
|
741
|
+
ap = argparse.ArgumentParser(prog="ecadlib symbol", description="group a pin list by function and write a tidy symbol "
|
|
742
|
+
"(Altium .SchLib, optional KiCad .kicad_sym and SVG preview)")
|
|
743
|
+
ap.add_argument("source", help="pins: .csv/.tsv (num,name[,type][,group]), .json (pin list / datasheet extract / symbol JSON), "
|
|
744
|
+
".SchLib (re-layout its first symbol), LIB.kicad_sym:NAME or kicad:Lib:Name")
|
|
745
|
+
ap.add_argument("-o", "--out", help="output .SchLib (default: <name>.SchLib)")
|
|
746
|
+
ap.add_argument("--name", help="symbol name (default: from the source)")
|
|
747
|
+
ap.add_argument("--kicad", help="also write this .kicad_sym")
|
|
748
|
+
ap.add_argument("--preview", help="write an SVG preview here")
|
|
749
|
+
ap.add_argument("--split", type=int, help="multi-part above this many pins (0 = never; default: rules [symbols] split_pins)")
|
|
750
|
+
ap.add_argument("--stack", choices=STACKS, help="same-name power pins (default: rules [symbols] stack_power)")
|
|
751
|
+
ap.add_argument("--footprint", help="footprint name to link")
|
|
752
|
+
ap.add_argument("--rules", help="library rules file (default: ecadlib.toml found from here)")
|
|
753
|
+
ap.add_argument("--list", action="store_true", help="print the grouping")
|
|
754
|
+
a = ap.parse_args(argv)
|
|
755
|
+
from . import rules as RU
|
|
756
|
+
try:
|
|
757
|
+
rules = RU.load(a.rules, start=Path(a.out).parent if a.out else ".")
|
|
758
|
+
except RU.RulesError as e:
|
|
759
|
+
print(f"error: {e}", file=sys.stderr)
|
|
760
|
+
return 2
|
|
761
|
+
sec = dict(rules["symbols"])
|
|
762
|
+
if a.split is not None:
|
|
763
|
+
sec["split_pins"] = a.split
|
|
764
|
+
if a.stack:
|
|
765
|
+
sec["stack_power"] = a.stack
|
|
766
|
+
name, pins, extra = load_pins(a.source)
|
|
767
|
+
name = a.name or name
|
|
768
|
+
sym = build(name, pins, extra.get("description", ""), extra.get("designator", "U?"), a.footprint or extra.get("footprint"),
|
|
769
|
+
rules={"symbols": sec})
|
|
770
|
+
out = Path(a.out or f"{AL.file_stem(name)}.SchLib")
|
|
771
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
772
|
+
AL.write_schlib(out, [sym])
|
|
773
|
+
probs = check(sym, sec["grid"])
|
|
774
|
+
nparts = sym.get("parts", 1)
|
|
775
|
+
groups = {}
|
|
776
|
+
for p in arrange_pins(pins, {"symbols": sec}):
|
|
777
|
+
groups.setdefault((p["unit"], p["side"], p["group"]), []).append(p)
|
|
778
|
+
print(f"{name}: {len(pins)} pins in {len({k[2] for k in groups})} groups, {nparts} part{'s' if nparts > 1 else ''} -> {out}")
|
|
779
|
+
if a.list:
|
|
780
|
+
for (u, side, g), ps in groups.items():
|
|
781
|
+
print(f" part {chr(64 + u)} {side:<6} {g:<9} " + ", ".join(f"{p['num']}:{plain(p['name'])}" + ("*" if p.get("stacked_on") else "") for p in ps))
|
|
782
|
+
if a.kicad:
|
|
783
|
+
from . import kicad_export as X, sexpr
|
|
784
|
+
notes = []
|
|
785
|
+
ks = X.symbol_from_schlib(out, X.kname(name), notes)
|
|
786
|
+
tree = ["kicad_symbol_lib", ["version", X.SYM_VERSION], ["generator", sexpr.Q("ecadlib")], ["generator_version", sexpr.Q("8.0")], ks]
|
|
787
|
+
Path(a.kicad).write_text(sexpr.dumps(tree), encoding="utf-8", newline="\n")
|
|
788
|
+
print(f" KiCad symbol -> {a.kicad}")
|
|
789
|
+
if a.preview:
|
|
790
|
+
Path(a.preview).write_text(svg(sym), encoding="utf-8")
|
|
791
|
+
print(f" preview -> {a.preview}")
|
|
792
|
+
for pr in probs:
|
|
793
|
+
print(f" problem: {pr}")
|
|
794
|
+
return 1 if probs else 0
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
if __name__ == "__main__":
|
|
798
|
+
sys.exit(main())
|