akcli-kicad 0.15.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.
- akcli/__init__.py +46 -0
- akcli/__main__.py +7 -0
- akcli/_vendor/__init__.py +0 -0
- akcli/_vendor/jlc2kicadlib/LICENSE +9 -0
- akcli/_vendor/jlc2kicadlib/PROVENANCE.md +23 -0
- akcli/_vendor/jlc2kicadlib/__init__.py +7 -0
- akcli/_vendor/jlc2kicadlib/_http.py +49 -0
- akcli/_vendor/jlc2kicadlib/_kmt.py +339 -0
- akcli/_vendor/jlc2kicadlib/footprint/__init__.py +0 -0
- akcli/_vendor/jlc2kicadlib/footprint/footprint.py +181 -0
- akcli/_vendor/jlc2kicadlib/footprint/footprint_handlers.py +617 -0
- akcli/_vendor/jlc2kicadlib/footprint/model3d.py +255 -0
- akcli/_vendor/jlc2kicadlib/helper.py +29 -0
- akcli/_vendor/jlc2kicadlib/symbol/__init__.py +0 -0
- akcli/_vendor/jlc2kicadlib/symbol/symbol.py +234 -0
- akcli/_vendor/jlc2kicadlib/symbol/symbol_handlers.py +469 -0
- akcli/adapters/__init__.py +1 -0
- akcli/adapters/dts.py +241 -0
- akcli/adapters/pinout_md.py +162 -0
- akcli/arrange.py +372 -0
- akcli/bom_policy.py +129 -0
- akcli/calc/__init__.py +28 -0
- akcli/calc/analog.py +148 -0
- akcli/calc/codes.py +132 -0
- akcli/calc/convert.py +86 -0
- akcli/calc/electrical.py +177 -0
- akcli/calc/eseries.py +236 -0
- akcli/calc/ic.py +169 -0
- akcli/calc/interface.py +68 -0
- akcli/calc/opsmap.py +141 -0
- akcli/calc/pcb.py +254 -0
- akcli/calc/power.py +209 -0
- akcli/calc/protection.py +107 -0
- akcli/calc/registry.py +113 -0
- akcli/calc/regulator.py +110 -0
- akcli/calc/rf.py +296 -0
- akcli/calc/sensor.py +151 -0
- akcli/calc/si.py +74 -0
- akcli/checks/__init__.py +1 -0
- akcli/checks/_rails.py +57 -0
- akcli/checks/bom.py +421 -0
- akcli/checks/contract.py +275 -0
- akcli/checks/diff.py +517 -0
- akcli/checks/erc.py +541 -0
- akcli/checks/geom.py +157 -0
- akcli/checks/intent.py +452 -0
- akcli/checks/layout.py +508 -0
- akcli/checks/libsync.py +193 -0
- akcli/checks/nets.py +101 -0
- akcli/checks/pairs.py +158 -0
- akcli/checks/pinmap.py +288 -0
- akcli/checks/power.py +322 -0
- akcli/checks/schpcb.py +153 -0
- akcli/cli.py +235 -0
- akcli/commands/__init__.py +7 -0
- akcli/commands/_shared.py +440 -0
- akcli/commands/calc.py +207 -0
- akcli/commands/capabilities.py +270 -0
- akcli/commands/checks.py +546 -0
- akcli/commands/doc.py +219 -0
- akcli/commands/doctor.py +225 -0
- akcli/commands/drawing.py +1388 -0
- akcli/commands/fab.py +116 -0
- akcli/commands/jlc.py +838 -0
- akcli/commands/library.py +322 -0
- akcli/commands/log.py +75 -0
- akcli/commands/query.py +729 -0
- akcli/commands/release.py +302 -0
- akcli/commands/render.py +79 -0
- akcli/commands/review.py +687 -0
- akcli/commands/sim.py +758 -0
- akcli/commands/view.py +96 -0
- akcli/config.py +281 -0
- akcli/drivers/__init__.py +1 -0
- akcli/drivers/altium_live/__init__.py +1 -0
- akcli/drivers/altium_live/bridge.py +289 -0
- akcli/drivers/jlc2kicad.py +170 -0
- akcli/drivers/kicad_cli.py +185 -0
- akcli/drivers/pdftotext.py +63 -0
- akcli/errors.py +313 -0
- akcli/exporters.py +142 -0
- akcli/fab.py +379 -0
- akcli/groupframe.py +140 -0
- akcli/journal.py +157 -0
- akcli/kicad_escape.py +99 -0
- akcli/libtable.py +578 -0
- akcli/model.py +325 -0
- akcli/netbuild.py +584 -0
- akcli/netdiff.py +265 -0
- akcli/ops.py +1301 -0
- akcli/parts/__init__.py +18 -0
- akcli/parts/bom_jlc.py +932 -0
- akcli/parts/datasheet.py +241 -0
- akcli/parts/easyeda.py +433 -0
- akcli/parts/search.py +548 -0
- akcli/parts/value_parse.py +196 -0
- akcli/readers/__init__.py +1 -0
- akcli/readers/_cfbf.py +314 -0
- akcli/readers/altium_pcb.py +150 -0
- akcli/readers/altium_pcb_bin.py +230 -0
- akcli/readers/altium_prj.py +93 -0
- akcli/readers/altium_records.py +192 -0
- akcli/readers/altium_sch.py +503 -0
- akcli/readers/altium_schlib.py +134 -0
- akcli/readers/footprint_lib.py +311 -0
- akcli/readers/gerber.py +254 -0
- akcli/readers/kicad.py +810 -0
- akcli/readers/kicad_lib.py +400 -0
- akcli/readers/sexpr.py +339 -0
- akcli/relink.py +310 -0
- akcli/render_art.py +312 -0
- akcli/render_svg.py +337 -0
- akcli/report.py +488 -0
- akcli/review/__init__.py +83 -0
- akcli/review/detectors/__init__.py +8 -0
- akcli/review/detectors/domain/__init__.py +8 -0
- akcli/review/detectors/domain/usb.py +104 -0
- akcli/review/detectors/emc/__init__.py +10 -0
- akcli/review/detectors/emc/diffpair.py +95 -0
- akcli/review/detectors/emc/edge.py +112 -0
- akcli/review/detectors/emc/planes.py +122 -0
- akcli/review/detectors/emc/protection.py +97 -0
- akcli/review/detectors/emc/stitching.py +88 -0
- akcli/review/detectors/gerber.py +198 -0
- akcli/review/detectors/pcb/__init__.py +3 -0
- akcli/review/detectors/pcb/decap.py +98 -0
- akcli/review/detectors/pcb/routing.py +67 -0
- akcli/review/detectors/pcb/thermal.py +170 -0
- akcli/review/detectors/pcb/trace_width.py +78 -0
- akcli/review/detectors/signal/__init__.py +6 -0
- akcli/review/detectors/signal/crystal.py +166 -0
- akcli/review/detectors/signal/divider.py +250 -0
- akcli/review/detectors/signal/opamp.py +198 -0
- akcli/review/detectors/signal/power_protect.py +279 -0
- akcli/review/detectors/signal/protection.py +79 -0
- akcli/review/detectors/signal/rc_filter.py +72 -0
- akcli/review/detectors/validation/__init__.py +3 -0
- akcli/review/detectors/validation/enable_pin.py +71 -0
- akcli/review/detectors/validation/i2c_pullup.py +169 -0
- akcli/review/detectors/validation/vdomain.py +131 -0
- akcli/review/diff.py +73 -0
- akcli/review/engine.py +111 -0
- akcli/review/facts.py +340 -0
- akcli/review/geometry.py +258 -0
- akcli/review/propose.py +237 -0
- akcli/review/tables.py +146 -0
- akcli/review/testbench.py +246 -0
- akcli/review/topo.py +314 -0
- akcli/review/tree.py +75 -0
- akcli/review/validate.py +169 -0
- akcli/safety.py +127 -0
- akcli/schemas/__init__.py +8 -0
- akcli/schemas/datasheet-facts.schema.json +54 -0
- akcli/schemas/diff.schema.json +98 -0
- akcli/schemas/doc.schema.json +79 -0
- akcli/schemas/draw-result.schema.json +62 -0
- akcli/schemas/findings.schema.json +100 -0
- akcli/schemas/netlist.schema.json +50 -0
- akcli/schemas/ops.capabilities.json +122 -0
- akcli/schemas/ops.schema.json +484 -0
- akcli/schemas/pinmap.schema.json +40 -0
- akcli/schemas/proposals.schema.json +62 -0
- akcli/schemas/schematic.schema.json +82 -0
- akcli/schemas/sim.schema.json +119 -0
- akcli/sim/__init__.py +24 -0
- akcli/sim/assertions.py +448 -0
- akcli/sim/builtin.lib +119 -0
- akcli/sim/deck.py +574 -0
- akcli/sim/engine.py +359 -0
- akcli/sim/models.py +614 -0
- akcli/sim/wave.py +92 -0
- akcli/units.py +68 -0
- akcli/webui/__init__.py +17 -0
- akcli/webui/calc.html +1593 -0
- akcli/webui/hub.html +226 -0
- akcli/webui/live.html +961 -0
- akcli/webui/server.py +807 -0
- akcli/writers/__init__.py +1 -0
- akcli/writers/connectivity.py +581 -0
- akcli/writers/footprint_mod.py +86 -0
- akcli/writers/geometry.py +221 -0
- akcli/writers/instances.py +289 -0
- akcli/writers/kicad.py +2089 -0
- akcli/writers/lib_cache.py +536 -0
- akcli/writers/sexpr_writer.py +200 -0
- akcli_kicad-0.15.0.dist-info/METADATA +219 -0
- akcli_kicad-0.15.0.dist-info/RECORD +191 -0
- akcli_kicad-0.15.0.dist-info/WHEEL +5 -0
- akcli_kicad-0.15.0.dist-info/entry_points.txt +2 -0
- akcli_kicad-0.15.0.dist-info/licenses/LICENSE +21 -0
- akcli_kicad-0.15.0.dist-info/top_level.txt +1 -0
akcli/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""akcli — read Altium .SchDoc and KiCad .kicad_sch, run checks, draw KiCad.
|
|
2
|
+
|
|
3
|
+
Zero-runtime-dependency Python package (stdlib only). This top-level module exposes
|
|
4
|
+
the package version and the analysis/protocol version constants used across the CLI.
|
|
5
|
+
|
|
6
|
+
Name cascade (LOCKED): PyPI dist = ``akcli-kicad`` (the ``akcli`` name is
|
|
7
|
+
taken on PyPI by an unrelated project); import package = ``akcli``; CLI =
|
|
8
|
+
``akcli``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve_version() -> str:
|
|
17
|
+
"""Report the version of the code actually running, dev checkout or install.
|
|
18
|
+
|
|
19
|
+
A co-located ``pyproject.toml`` (``project.name == "akcli-kicad"``) means we are
|
|
20
|
+
executing FROM the source tree; that file is the freshest truth and WINS —
|
|
21
|
+
an editable or older install otherwise pins stale metadata, so bumping the
|
|
22
|
+
working tree would not move ``akcli --version`` and a real update reads as
|
|
23
|
+
"unchanged" (the exact confusion the 0.7.0/design-integrity overlap caused).
|
|
24
|
+
An installed wheel has no such sibling pyproject, so it falls through to the
|
|
25
|
+
dist metadata. Last-resort ``"0.0.0"`` only if neither source is readable.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
import tomllib
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
32
|
+
with pyproject.open("rb") as fh:
|
|
33
|
+
project = tomllib.load(fh).get("project", {})
|
|
34
|
+
if project.get("name") == "akcli-kicad" and project.get("version"):
|
|
35
|
+
return project["version"]
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
try:
|
|
39
|
+
return _pkg_version("akcli-kicad")
|
|
40
|
+
except PackageNotFoundError:
|
|
41
|
+
return "0.0.0"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__version__ = _resolve_version()
|
|
45
|
+
|
|
46
|
+
__all__ = ["__version__"]
|
akcli/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2021 TousstNicolas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Provenance — vendored JLC2KiCadLib
|
|
2
|
+
|
|
3
|
+
- Upstream: https://github.com/TousstNicolas/JLC2KiCad_lib
|
|
4
|
+
- License: MIT (see `LICENSE` in this directory, preserved verbatim)
|
|
5
|
+
- Vendored commit: `48d36032108d64b0f59755234681f1ce8bc98d46`
|
|
6
|
+
- Files taken: `helper.py`, `footprint/{footprint,footprint_handlers,model3d}.py`,
|
|
7
|
+
`symbol/{symbol,symbol_handlers}.py`. The upstream CLI entry
|
|
8
|
+
(`JLC2KiCadLib.py`) is NOT vendored — akcli drives `create_footprint` /
|
|
9
|
+
`create_symbol` directly from `drivers/jlc2kicad.py`.
|
|
10
|
+
|
|
11
|
+
## Local modifications (kept minimal, all import-level)
|
|
12
|
+
|
|
13
|
+
1. `import requests` → `from .. import _http as requests` (stdlib urllib shim;
|
|
14
|
+
keeps akcli zero-dependency).
|
|
15
|
+
2. `from KicadModTree import ...` → `from .._kmt import ...`. **KicadModTree is
|
|
16
|
+
GPLv3+ and is deliberately not vendored or imported**; `_kmt.py` is an
|
|
17
|
+
original, clean-room implementation of just the API surface these files use,
|
|
18
|
+
written against the KiCad footprint *file format* (it emits the modern
|
|
19
|
+
`(footprint ...)` s-expression dialect rather than the legacy `(module ...)`
|
|
20
|
+
output of KicadModTree 1.x).
|
|
21
|
+
|
|
22
|
+
`_http.py` and `_kmt.py` are original akcli code (project MIT license), not
|
|
23
|
+
upstream files.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Vendored JLC2KiCadLib (MIT, (c) 2021 TousstNicolas) — see LICENSE + PROVENANCE.md.
|
|
2
|
+
|
|
3
|
+
Only the conversion core is vendored (footprint/ + symbol/ + helper.py). Its two
|
|
4
|
+
upstream dependencies are NOT vendored: ``requests`` is replaced by the stdlib
|
|
5
|
+
shim ``_http`` and the GPLv3 ``KicadModTree`` by the clean-room ``_kmt`` writer
|
|
6
|
+
(implemented from the KiCad footprint file format, not from KicadModTree code).
|
|
7
|
+
"""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Stdlib drop-in for the tiny slice of ``requests`` the vendored code uses.
|
|
2
|
+
|
|
3
|
+
Original akcli code (project MIT license) — NOT an upstream JLC2KiCadLib file.
|
|
4
|
+
Surface: ``get(url, headers=...)`` returning an object with ``.status_code`` and
|
|
5
|
+
``.content``, plus ``codes.ok``. A module-level ``opener`` is injectable so
|
|
6
|
+
tests run fully offline (same pattern as :mod:`akcli.parts.search`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
# EasyEDA payloads are small JSON / STEP files; cap reads defensively.
|
|
17
|
+
_MAX_BYTES = 64 * 1024 * 1024
|
|
18
|
+
_TIMEOUT_S = 30.0
|
|
19
|
+
|
|
20
|
+
#: Injectable transport: any object with ``open(request, *, timeout=...)``.
|
|
21
|
+
opener = None
|
|
22
|
+
|
|
23
|
+
codes = SimpleNamespace(ok=200)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Response:
|
|
28
|
+
status_code: int
|
|
29
|
+
content: bytes
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def text(self) -> str:
|
|
33
|
+
return self.content.decode("utf-8", "replace")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get(url: str, headers: dict | None = None, timeout: float = _TIMEOUT_S) -> Response:
|
|
37
|
+
req = urllib.request.Request(url, headers=dict(headers or {}))
|
|
38
|
+
op = opener or urllib.request.build_opener()
|
|
39
|
+
try:
|
|
40
|
+
with op.open(req, timeout=timeout) as resp:
|
|
41
|
+
body = resp.read(_MAX_BYTES + 1)
|
|
42
|
+
if len(body) > _MAX_BYTES:
|
|
43
|
+
return Response(status_code=502, content=b"")
|
|
44
|
+
status = getattr(resp, "status", None) or resp.getcode() or 200
|
|
45
|
+
return Response(status_code=int(status), content=body)
|
|
46
|
+
except urllib.error.HTTPError as exc: # non-2xx: report the code, never raise
|
|
47
|
+
return Response(status_code=int(exc.code), content=b"")
|
|
48
|
+
except (urllib.error.URLError, OSError, TimeoutError):
|
|
49
|
+
return Response(status_code=599, content=b"")
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Clean-room mini footprint writer replacing the GPLv3 ``KicadModTree``.
|
|
2
|
+
|
|
3
|
+
Original akcli code (project MIT license) — NOT an upstream JLC2KiCadLib file,
|
|
4
|
+
and NOT derived from KicadModTree's source: it implements, from the public
|
|
5
|
+
KiCad footprint *file format*, exactly the API surface the vendored
|
|
6
|
+
JLC2KiCadLib modules use (class names/kwargs are the interface those modules
|
|
7
|
+
call; the serialization below is the modern ``(footprint ...)`` dialect KiCad
|
|
8
|
+
7-10 read natively, not KicadModTree 1.x's legacy ``(module ...)`` output).
|
|
9
|
+
|
|
10
|
+
Coordinate/unit contract: geometry values arrive in **mm** (the vendored
|
|
11
|
+
handlers convert EasyEDA mils to mm). ``Model.at`` arrives in the legacy
|
|
12
|
+
KicadModTree convention of **inches** and is serialized as an
|
|
13
|
+
``(offset (xyz ...))`` in mm (x25.4).
|
|
14
|
+
|
|
15
|
+
Dialect: the serializer emits the **KiCad 6** footprint dialect
|
|
16
|
+
(``(layer ...) (width ...)`` on graphic items, version ``20211014``). Every
|
|
17
|
+
KiCad from 6 through 10 reads it natively — and, decisively, so does **Altium
|
|
18
|
+
Designer's Import Wizard** (File » Import Wizard » KiCad Design Files), whose
|
|
19
|
+
KiCad support is pinned to 6.0x: the same converted library becomes an Altium
|
|
20
|
+
``SchLib``/``PcbLib`` without any extra tool.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Arc", "Circle", "Footprint", "KicadFileHandler", "Line", "Model", "Pad",
|
|
29
|
+
"Polygon", "RectFill", "RectLine", "Text", "Translation", "Vector2D",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --------------------------------------------------------------------------- #
|
|
34
|
+
# small geometry helper (used by the vendored SVG-arc math)
|
|
35
|
+
# --------------------------------------------------------------------------- #
|
|
36
|
+
class Vector2D:
|
|
37
|
+
def __init__(self, x, y=None):
|
|
38
|
+
if y is None: # Vector2D((x, y)) / Vector2D([x, y]) / Vector2D(other)
|
|
39
|
+
x, y = x[0], x[1]
|
|
40
|
+
self.x = float(x)
|
|
41
|
+
self.y = float(y)
|
|
42
|
+
|
|
43
|
+
def __getitem__(self, i):
|
|
44
|
+
return (self.x, self.y)[i]
|
|
45
|
+
|
|
46
|
+
def __add__(self, other):
|
|
47
|
+
return Vector2D(self.x + other[0], self.y + other[1])
|
|
48
|
+
|
|
49
|
+
def __mul__(self, k):
|
|
50
|
+
return Vector2D(self.x * k, self.y * k)
|
|
51
|
+
|
|
52
|
+
__rmul__ = __mul__
|
|
53
|
+
|
|
54
|
+
def rotate(self, degrees):
|
|
55
|
+
r = math.radians(degrees)
|
|
56
|
+
c, s = math.cos(r), math.sin(r)
|
|
57
|
+
return Vector2D(self.x * c - self.y * s, self.x * s + self.y * c)
|
|
58
|
+
|
|
59
|
+
def distance_to(self, other):
|
|
60
|
+
return math.hypot(self.x - other[0], self.y - other[1])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _xy(pt) -> tuple[float, float]:
|
|
64
|
+
return (float(pt[0]), float(pt[1]))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _fmt(v: float) -> str:
|
|
68
|
+
s = f"{float(v):.6f}".rstrip("0").rstrip(".")
|
|
69
|
+
return s if s not in ("-0", "") else "0"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _q(s: str) -> str:
|
|
73
|
+
return '"' + str(s).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------- #
|
|
77
|
+
# nodes (constructor kwargs == the surface the vendored handlers call)
|
|
78
|
+
# --------------------------------------------------------------------------- #
|
|
79
|
+
class Line:
|
|
80
|
+
def __init__(self, start, end, width, layer):
|
|
81
|
+
self.start, self.end = _xy(start), _xy(end)
|
|
82
|
+
self.width, self.layer = float(width), layer
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Arc:
|
|
86
|
+
def __init__(self, start, end, center, width, layer):
|
|
87
|
+
self.start, self.end, self.center = _xy(start), _xy(end), _xy(center)
|
|
88
|
+
self.width, self.layer = float(width), layer
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Circle:
|
|
92
|
+
def __init__(self, center, radius, width, layer):
|
|
93
|
+
self.center, self.radius = _xy(center), float(radius)
|
|
94
|
+
self.width, self.layer = float(width), layer
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Polygon:
|
|
98
|
+
def __init__(self, nodes, layer=None, width=0.0):
|
|
99
|
+
self.nodes = [_xy(p) for p in nodes] # materialize (callers pass zip())
|
|
100
|
+
self.layer, self.width = layer, float(width)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RectLine:
|
|
104
|
+
def __init__(self, start, end, width, layer):
|
|
105
|
+
self.start, self.end = _xy(start), _xy(end)
|
|
106
|
+
self.width, self.layer = float(width), layer
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RectFill:
|
|
110
|
+
def __init__(self, start, end, layer):
|
|
111
|
+
self.start, self.end, self.layer = _xy(start), _xy(end), layer
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Text:
|
|
115
|
+
def __init__(self, type, text, at, layer): # noqa: A002 - upstream kwarg name
|
|
116
|
+
self.type, self.text = type, str(text)
|
|
117
|
+
self.at, self.layer = _xy(at), layer
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Pad:
|
|
121
|
+
TYPE_THT = "thru_hole"
|
|
122
|
+
TYPE_SMT = "smd"
|
|
123
|
+
TYPE_NPTH = "np_thru_hole"
|
|
124
|
+
SHAPE_CIRCLE = "circle"
|
|
125
|
+
SHAPE_OVAL = "oval"
|
|
126
|
+
SHAPE_RECT = "rect"
|
|
127
|
+
SHAPE_CUSTOM = "custom"
|
|
128
|
+
LAYERS_THT = ["*.Cu", "*.Mask"]
|
|
129
|
+
LAYERS_SMT = ["F.Cu", "F.Paste", "F.Mask"]
|
|
130
|
+
LAYERS_NPTH = ["*.Cu", "*.Mask"]
|
|
131
|
+
|
|
132
|
+
def __init__(self, number, type, shape, at, size, layers, # noqa: A002
|
|
133
|
+
rotation=0.0, drill=None, primitives=""):
|
|
134
|
+
self.number = str(number)
|
|
135
|
+
self.type = type
|
|
136
|
+
self.shape = shape
|
|
137
|
+
self.at = _xy(at)
|
|
138
|
+
self.size = _xy(size) if isinstance(size, (list, tuple)) else (float(size), float(size))
|
|
139
|
+
self.rotation = float(rotation or 0.0)
|
|
140
|
+
self.drill = drill
|
|
141
|
+
self.layers = list(layers)
|
|
142
|
+
self.primitives = list(primitives) if primitives else []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Model:
|
|
146
|
+
def __init__(self, filename, at, rotate, scale=(1.0, 1.0, 1.0)):
|
|
147
|
+
# upstream sometimes pre-quotes path-variable filenames — strip that.
|
|
148
|
+
self.filename = str(filename).strip('"')
|
|
149
|
+
self.at = tuple(float(v) for v in at) # legacy inches
|
|
150
|
+
self.rotate = tuple(float(v) for v in rotate)
|
|
151
|
+
self.scale = tuple(float(v) for v in scale)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Translation:
|
|
155
|
+
"""Container that offsets everything inside it (the only transform used)."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, x, y):
|
|
158
|
+
self.offset = (float(x), float(y))
|
|
159
|
+
self.children: list = []
|
|
160
|
+
|
|
161
|
+
def append(self, node) -> None:
|
|
162
|
+
self.children.append(node)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Footprint:
|
|
166
|
+
def __init__(self, name):
|
|
167
|
+
self.name = str(name).strip('"') # upstream passes a pre-quoted name
|
|
168
|
+
self.description = ""
|
|
169
|
+
self.tags = ""
|
|
170
|
+
self.attribute = "smd"
|
|
171
|
+
self.children: list = []
|
|
172
|
+
|
|
173
|
+
def setDescription(self, s): # noqa: N802 - upstream API name
|
|
174
|
+
self.description = str(s)
|
|
175
|
+
|
|
176
|
+
def setTags(self, s): # noqa: N802
|
|
177
|
+
self.tags = str(s)
|
|
178
|
+
|
|
179
|
+
def setAttribute(self, s): # noqa: N802
|
|
180
|
+
self.attribute = str(s)
|
|
181
|
+
|
|
182
|
+
def append(self, node) -> None:
|
|
183
|
+
self.children.append(node)
|
|
184
|
+
|
|
185
|
+
def insert(self, node) -> None:
|
|
186
|
+
"""Re-parent all current children under ``node`` (Translation wrap)."""
|
|
187
|
+
if isinstance(node, Translation):
|
|
188
|
+
node.children = self.children
|
|
189
|
+
self.children = [node]
|
|
190
|
+
else:
|
|
191
|
+
self.children.append(node)
|
|
192
|
+
|
|
193
|
+
def getAllChilds(self): # noqa: N802
|
|
194
|
+
out = []
|
|
195
|
+
stack = list(self.children)
|
|
196
|
+
while stack:
|
|
197
|
+
n = stack.pop()
|
|
198
|
+
out.append(n)
|
|
199
|
+
stack.extend(getattr(n, "children", []) or [])
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
# serialization — modern (footprint ...) s-expression
|
|
205
|
+
# --------------------------------------------------------------------------- #
|
|
206
|
+
def _shift(pt, off):
|
|
207
|
+
return (pt[0] + off[0], pt[1] + off[1])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _arc_mid(start, end, center):
|
|
211
|
+
"""Point on the arc halfway from start to end (clockwise, +Y-down plane).
|
|
212
|
+
|
|
213
|
+
The vendored handler normalizes SVG sweep so the arc always runs clockwise
|
|
214
|
+
(screen sense, +Y down) from ``start`` to ``end`` around ``center``.
|
|
215
|
+
"""
|
|
216
|
+
a0 = math.atan2(start[1] - center[1], start[0] - center[0])
|
|
217
|
+
a1 = math.atan2(end[1] - center[1], end[0] - center[0])
|
|
218
|
+
r = math.hypot(start[0] - center[0], start[1] - center[1])
|
|
219
|
+
sweep = (a1 - a0) % (2 * math.pi) # CW in y-down == increasing atan2 angle
|
|
220
|
+
am = a0 + sweep / 2.0
|
|
221
|
+
return (center[0] + r * math.cos(am), center[1] + r * math.sin(am))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _gtail(layer, width):
|
|
225
|
+
"""KiCad-6 graphic-item tail: ``(layer "X") (width W)`` (no stroke node)."""
|
|
226
|
+
return f"(layer {_q(layer)}) (width {_fmt(width)})"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _serialize_node(node, off, out: list[str]) -> None:
|
|
230
|
+
ind = "\t"
|
|
231
|
+
if isinstance(node, Translation):
|
|
232
|
+
noff = (off[0] + node.offset[0], off[1] + node.offset[1])
|
|
233
|
+
for child in node.children:
|
|
234
|
+
_serialize_node(child, noff, out)
|
|
235
|
+
return
|
|
236
|
+
if isinstance(node, Line):
|
|
237
|
+
s, e = _shift(node.start, off), _shift(node.end, off)
|
|
238
|
+
out.append(f"{ind}(fp_line (start {_fmt(s[0])} {_fmt(s[1])}) "
|
|
239
|
+
f"(end {_fmt(e[0])} {_fmt(e[1])}) {_gtail(node.layer, node.width)})")
|
|
240
|
+
elif isinstance(node, RectLine) or isinstance(node, RectFill):
|
|
241
|
+
s, e = _shift(node.start, off), _shift(node.end, off)
|
|
242
|
+
width = getattr(node, "width", 0.0)
|
|
243
|
+
fill = "none" if isinstance(node, RectLine) else "solid"
|
|
244
|
+
out.append(f"{ind}(fp_rect (start {_fmt(s[0])} {_fmt(s[1])}) "
|
|
245
|
+
f"(end {_fmt(e[0])} {_fmt(e[1])}) {_gtail(node.layer, width)} "
|
|
246
|
+
f"(fill {fill}))")
|
|
247
|
+
elif isinstance(node, Arc):
|
|
248
|
+
s, e = _shift(node.start, off), _shift(node.end, off)
|
|
249
|
+
c = _shift(node.center, off)
|
|
250
|
+
m = _arc_mid(s, e, c)
|
|
251
|
+
out.append(f"{ind}(fp_arc (start {_fmt(s[0])} {_fmt(s[1])}) "
|
|
252
|
+
f"(mid {_fmt(m[0])} {_fmt(m[1])}) (end {_fmt(e[0])} {_fmt(e[1])}) "
|
|
253
|
+
f"{_gtail(node.layer, node.width)})")
|
|
254
|
+
elif isinstance(node, Circle):
|
|
255
|
+
c = _shift(node.center, off)
|
|
256
|
+
out.append(f"{ind}(fp_circle (center {_fmt(c[0])} {_fmt(c[1])}) "
|
|
257
|
+
f"(end {_fmt(c[0] + node.radius)} {_fmt(c[1])}) "
|
|
258
|
+
f"{_gtail(node.layer, node.width)} (fill none))")
|
|
259
|
+
elif isinstance(node, Polygon):
|
|
260
|
+
pts = " ".join(f"(xy {_fmt(p[0] + off[0])} {_fmt(p[1] + off[1])})"
|
|
261
|
+
for p in node.nodes)
|
|
262
|
+
layer = node.layer or "F.Fab"
|
|
263
|
+
out.append(f"{ind}(fp_poly (pts {pts}) {_gtail(layer, node.width)} "
|
|
264
|
+
f"(fill solid))")
|
|
265
|
+
elif isinstance(node, Text):
|
|
266
|
+
a = _shift(node.at, off)
|
|
267
|
+
out.append(f"{ind}(fp_text {node.type} {_q(node.text)} "
|
|
268
|
+
f"(at {_fmt(a[0])} {_fmt(a[1])}) (layer {_q(node.layer)}) "
|
|
269
|
+
f"(effects (font (size 1 1) (thickness 0.15))))")
|
|
270
|
+
elif isinstance(node, Pad):
|
|
271
|
+
a = _shift(node.at, off)
|
|
272
|
+
at = (f"(at {_fmt(a[0])} {_fmt(a[1])}"
|
|
273
|
+
+ (f" {_fmt(node.rotation)}" if node.rotation else "") + ")")
|
|
274
|
+
parts = [f"(pad {_q(node.number)} {node.type} {node.shape} {at} "
|
|
275
|
+
f"(size {_fmt(node.size[0])} {_fmt(node.size[1])})"]
|
|
276
|
+
drill = node.drill
|
|
277
|
+
if drill and node.type in (Pad.TYPE_THT, Pad.TYPE_NPTH):
|
|
278
|
+
if isinstance(drill, (list, tuple)):
|
|
279
|
+
parts.append(f"(drill oval {_fmt(drill[0])} {_fmt(drill[1])})")
|
|
280
|
+
elif float(drill) > 0:
|
|
281
|
+
parts.append(f"(drill {_fmt(drill)})")
|
|
282
|
+
parts.append("(layers " + " ".join(_q(layer) for layer in node.layers) + ")")
|
|
283
|
+
if node.shape == Pad.SHAPE_CUSTOM and node.primitives:
|
|
284
|
+
prims = []
|
|
285
|
+
for prim in node.primitives:
|
|
286
|
+
if isinstance(prim, Polygon):
|
|
287
|
+
pts = " ".join(f"(xy {_fmt(p[0])} {_fmt(p[1])})" for p in prim.nodes)
|
|
288
|
+
prims.append(f"(gr_poly (pts {pts}) (width 0) (fill yes))")
|
|
289
|
+
parts.append("(options (clearance outline) (anchor circle)) "
|
|
290
|
+
"(primitives " + " ".join(prims) + ")")
|
|
291
|
+
out.append(ind + " ".join(parts) + ")")
|
|
292
|
+
elif isinstance(node, Model):
|
|
293
|
+
o = tuple(v * 25.4 for v in node.at) # legacy inches -> mm offset
|
|
294
|
+
out.append(
|
|
295
|
+
f"{ind}(model {_q(node.filename)}\n"
|
|
296
|
+
f"{ind}\t(offset (xyz {_fmt(o[0])} {_fmt(o[1])} {_fmt(o[2])}))\n"
|
|
297
|
+
f"{ind}\t(scale (xyz {_fmt(node.scale[0])} {_fmt(node.scale[1])} "
|
|
298
|
+
f"{_fmt(node.scale[2])}))\n"
|
|
299
|
+
f"{ind}\t(rotate (xyz {_fmt(node.rotate[0])} {_fmt(node.rotate[1])} "
|
|
300
|
+
f"{_fmt(node.rotate[2])}))\n"
|
|
301
|
+
f"{ind})"
|
|
302
|
+
)
|
|
303
|
+
# unknown nodes are ignored (none exist in the vendored call surface)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class KicadFileHandler:
|
|
307
|
+
def __init__(self, footprint: Footprint):
|
|
308
|
+
self.footprint = footprint
|
|
309
|
+
|
|
310
|
+
def serialize(self) -> str:
|
|
311
|
+
fp = self.footprint
|
|
312
|
+
lines = [
|
|
313
|
+
f"(footprint {_q(fp.name)}",
|
|
314
|
+
"\t(version 20211014)",
|
|
315
|
+
"\t(generator akcli)",
|
|
316
|
+
'\t(layer "F.Cu")',
|
|
317
|
+
f"\t(descr {_q(fp.description)})",
|
|
318
|
+
f"\t(tags {_q(fp.tags)})",
|
|
319
|
+
f"\t(attr {fp.attribute})",
|
|
320
|
+
]
|
|
321
|
+
body: list[str] = []
|
|
322
|
+
for child in fp.children:
|
|
323
|
+
_serialize_node(child, (0.0, 0.0), body)
|
|
324
|
+
# KiCad expects graphics/text before pads before models; keep a stable
|
|
325
|
+
# grouped order for diffability.
|
|
326
|
+
def _rank(line: str) -> int:
|
|
327
|
+
if line.lstrip("\t").startswith("(pad"):
|
|
328
|
+
return 1
|
|
329
|
+
if line.lstrip("\t").startswith("(model"):
|
|
330
|
+
return 2
|
|
331
|
+
return 0
|
|
332
|
+
body.sort(key=_rank)
|
|
333
|
+
lines.extend(body)
|
|
334
|
+
lines.append(")")
|
|
335
|
+
return "\n".join(lines) + "\n"
|
|
336
|
+
|
|
337
|
+
def writeFile(self, path) -> None: # noqa: N802 - upstream API name
|
|
338
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
339
|
+
fh.write(self.serialize())
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .. import _http as requests
|
|
7
|
+
from .._kmt import Footprint, KicadFileHandler, Pad, Text, Translation
|
|
8
|
+
|
|
9
|
+
from .. import helper
|
|
10
|
+
from .footprint_handlers import handlers, mil2mm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FootprintInfo:
|
|
15
|
+
max_X: float = -10000
|
|
16
|
+
max_Y: float = -10000
|
|
17
|
+
min_X: float = 10000
|
|
18
|
+
min_Y: float = 10000
|
|
19
|
+
footprint_name: str = ""
|
|
20
|
+
output_dir: str = ""
|
|
21
|
+
footprint_lib: str = ""
|
|
22
|
+
model_base_variable: str = ""
|
|
23
|
+
model_dir: str = ""
|
|
24
|
+
origin: tuple = (0, 0)
|
|
25
|
+
models: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_footprint(
|
|
29
|
+
footprint_component_uuid,
|
|
30
|
+
component_id,
|
|
31
|
+
footprint_lib,
|
|
32
|
+
output_dir,
|
|
33
|
+
model_base_variable,
|
|
34
|
+
model_dir,
|
|
35
|
+
skip_existing,
|
|
36
|
+
models,
|
|
37
|
+
):
|
|
38
|
+
logging.info("Creating footprint ...")
|
|
39
|
+
|
|
40
|
+
(
|
|
41
|
+
footprint_name,
|
|
42
|
+
datasheet_link,
|
|
43
|
+
footprint_shape,
|
|
44
|
+
translation,
|
|
45
|
+
) = get_footprint_info(footprint_component_uuid)
|
|
46
|
+
|
|
47
|
+
if skip_existing and os.path.isfile(
|
|
48
|
+
os.path.join(output_dir, footprint_lib, footprint_name + ".kicad_mod")
|
|
49
|
+
):
|
|
50
|
+
logging.info(f"Footprint {footprint_name} already exists, skipping.")
|
|
51
|
+
return f"{footprint_lib}:{footprint_name}", datasheet_link
|
|
52
|
+
|
|
53
|
+
# init kicad footprint
|
|
54
|
+
kicad_mod = Footprint(f'"{footprint_name}"')
|
|
55
|
+
kicad_mod.setDescription(f"{footprint_name} footprint") # TODO Set real description
|
|
56
|
+
kicad_mod.setTags(f"{footprint_name} footprint {component_id}")
|
|
57
|
+
|
|
58
|
+
footprint_info = FootprintInfo(
|
|
59
|
+
footprint_name=footprint_name,
|
|
60
|
+
output_dir=output_dir,
|
|
61
|
+
footprint_lib=footprint_lib,
|
|
62
|
+
model_base_variable=model_base_variable,
|
|
63
|
+
model_dir=model_dir,
|
|
64
|
+
origin=translation,
|
|
65
|
+
models=models,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# for each line in data : use the appropriate handler
|
|
69
|
+
for line in footprint_shape:
|
|
70
|
+
args = [i for i in line.split("~")] # split and remove empty string in list
|
|
71
|
+
model = args[0]
|
|
72
|
+
logging.debug(args)
|
|
73
|
+
if model not in handlers:
|
|
74
|
+
logging.warning(f"footprint : model not in handler : {model}")
|
|
75
|
+
else:
|
|
76
|
+
handlers.get(model)(args[1:], kicad_mod, footprint_info)
|
|
77
|
+
|
|
78
|
+
if any(
|
|
79
|
+
isinstance(child, Pad) and child.type == Pad.TYPE_THT
|
|
80
|
+
for child in kicad_mod.getAllChilds()
|
|
81
|
+
):
|
|
82
|
+
kicad_mod.setAttribute("through_hole")
|
|
83
|
+
else:
|
|
84
|
+
kicad_mod.setAttribute("smd")
|
|
85
|
+
|
|
86
|
+
kicad_mod.insert(Translation(-mil2mm(translation[0]), -mil2mm(translation[1])))
|
|
87
|
+
|
|
88
|
+
# Translate the footprint max and min values to the origin
|
|
89
|
+
footprint_info.max_X -= mil2mm(translation[0])
|
|
90
|
+
footprint_info.max_Y -= mil2mm(translation[1])
|
|
91
|
+
footprint_info.min_X -= mil2mm(translation[0])
|
|
92
|
+
footprint_info.min_Y -= mil2mm(translation[1])
|
|
93
|
+
|
|
94
|
+
# set general values
|
|
95
|
+
kicad_mod.append(
|
|
96
|
+
Text(
|
|
97
|
+
type="reference",
|
|
98
|
+
text="REF**",
|
|
99
|
+
at=[
|
|
100
|
+
(footprint_info.min_X + footprint_info.max_X) / 2,
|
|
101
|
+
footprint_info.min_Y - 2,
|
|
102
|
+
],
|
|
103
|
+
layer="F.SilkS",
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
kicad_mod.append(
|
|
107
|
+
Text(
|
|
108
|
+
type="user",
|
|
109
|
+
text="${REFERENCE}",
|
|
110
|
+
at=[
|
|
111
|
+
(footprint_info.min_X + footprint_info.max_X) / 2,
|
|
112
|
+
(footprint_info.min_Y + footprint_info.max_Y) / 2,
|
|
113
|
+
],
|
|
114
|
+
layer="F.Fab",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
kicad_mod.append(
|
|
118
|
+
Text(
|
|
119
|
+
type="value",
|
|
120
|
+
text=footprint_name,
|
|
121
|
+
at=[
|
|
122
|
+
(footprint_info.min_X + footprint_info.max_X) / 2,
|
|
123
|
+
footprint_info.max_Y + 2,
|
|
124
|
+
],
|
|
125
|
+
layer="F.Fab",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if not os.path.exists(f"{output_dir}/{footprint_lib}"):
|
|
130
|
+
os.makedirs(f"{output_dir}/{footprint_lib}")
|
|
131
|
+
|
|
132
|
+
# output kicad model
|
|
133
|
+
file_handler = KicadFileHandler(kicad_mod)
|
|
134
|
+
file_handler.writeFile(f"{output_dir}/{footprint_lib}/{footprint_name}.kicad_mod")
|
|
135
|
+
logging.info(f"Created '{output_dir}/{footprint_lib}/{footprint_name}.kicad_mod'")
|
|
136
|
+
|
|
137
|
+
# return the datasheet link and footprint name to be linked with the symbol
|
|
138
|
+
return (f"{footprint_lib}:{footprint_name}", datasheet_link)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_footprint_info(footprint_component_uuid):
|
|
142
|
+
# fetch the component data from easyeda library
|
|
143
|
+
response = requests.get(
|
|
144
|
+
f"https://easyeda.com/api/components/{footprint_component_uuid}",
|
|
145
|
+
headers={"User-Agent": helper.get_user_agent()},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if response.status_code == requests.codes.ok:
|
|
149
|
+
data = json.loads(response.content.decode())
|
|
150
|
+
else:
|
|
151
|
+
logging.error(
|
|
152
|
+
"create_footprint error. Requests returned with error code "
|
|
153
|
+
f"{response.status_code}"
|
|
154
|
+
)
|
|
155
|
+
return ("", None, "", (0, 0))
|
|
156
|
+
|
|
157
|
+
footprint_shape = data["result"]["dataStr"]["shape"]
|
|
158
|
+
x = data["result"]["dataStr"]["head"]["x"]
|
|
159
|
+
y = data["result"]["dataStr"]["head"]["y"]
|
|
160
|
+
try:
|
|
161
|
+
datasheet_link = data["result"]["dataStr"]["head"]["c_para"]["link"]
|
|
162
|
+
except KeyError:
|
|
163
|
+
datasheet_link = ""
|
|
164
|
+
logging.warning("Could not retrieve datasheet link from EASYEDA")
|
|
165
|
+
|
|
166
|
+
footprint_name = (
|
|
167
|
+
data["result"]["title"]
|
|
168
|
+
.replace(" ", "_")
|
|
169
|
+
.replace("/", "_")
|
|
170
|
+
.replace("(", "_")
|
|
171
|
+
.replace(")", "_")
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not footprint_name:
|
|
175
|
+
footprint_name = "NoName"
|
|
176
|
+
logging.warning(
|
|
177
|
+
"Could not retrieve components information from EASYEDA, default name "
|
|
178
|
+
"'NoName'."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return (footprint_name, datasheet_link, footprint_shape, (x, y))
|