lc-step: LCEDA 3D model (STEP/OBJ) downloader by LCSC code
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
# lc-step
|
||||||
|
|
||||||
|
通过**立创商城料号(Cxxxxx)**或**组件 UUID** 批量下载立创EDA(LCEDA / EasyEDA Pro)3D 模型(STEP / OBJ)的命令行工具。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 输入立创商城料号(如 `C25744`),自动解析 3D 模型并下载 STEP 文件
|
||||||
|
- 可同时下载源 OBJ 网格模型
|
||||||
|
- 支持批量料号,一次请求批量解析
|
||||||
|
- 兼容直接传 32 位十六进制组件/模型 UUID
|
||||||
|
- 私有器件可携带登录 Cookie
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
Python 3.6+,仅标准库,无第三方依赖。
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 下载 STEP(保存到当前目录,文件名取器件名)
|
||||||
|
python3 lceda_step.py C25744
|
||||||
|
|
||||||
|
# 批量下载多个料号
|
||||||
|
python3 lceda_step.py C25744 C2590 C14663 C48642328
|
||||||
|
|
||||||
|
# 同时下载源 OBJ 模型
|
||||||
|
python3 lceda_step.py --obj C25744
|
||||||
|
|
||||||
|
# 指定输出目录
|
||||||
|
python3 lceda_step.py --outdir ./models C2590
|
||||||
|
|
||||||
|
# 私有器件:携带 pro.lceda.cn 登录 Cookie
|
||||||
|
python3 lceda_step.py --cookie "lc_auth=xxx" C2590
|
||||||
|
|
||||||
|
# 直接传组件/模型 UUID(与料号混用亦可)
|
||||||
|
python3 lceda_step.py 98c88bc2b9094f218f3a3e30a7012738
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `items` | 立创商城料号(`Cxxxxx`)或 32 位十六进制 UUID,可多个 |
|
||||||
|
| `--obj` | 额外下载源 OBJ 网格模型 |
|
||||||
|
| `--outdir` | 输出目录,默认当前目录 |
|
||||||
|
| `--cookie` | pro.lceda.cn 登录 Cookie(私有器件需要) |
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
```
|
||||||
|
LCSC 料号 (Cxxxxx)
|
||||||
|
→ POST /api/v2/eda/product/search 批量查询
|
||||||
|
→ device_info.footprint_info.model_3d.uri
|
||||||
|
→ GET /api/v2/components/{uri} 解析 3D 模型 UUID(失败则直接用 uri)
|
||||||
|
→ 3d_model_uuid + path(owner)
|
||||||
|
→ GET modules.lceda.cn/qAxj6KHrDKw4blvCG8QJPs7Y/{3d_model_uuid}?path={owner}
|
||||||
|
→ STEP(AP214,服务端 SolidWorks 预生成)
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- 器件级 STEP 由立创服务端用 SolidWorks 预生成并存在公开 CDN 上,本工具直接下载,不依赖浏览器;
|
||||||
|
- 部分器件的 `model_3d.uri` 本身就是 CDN 模型 UUID(如 RK3506B),`/api/v2/components` 查不到时脚本会自动回退直接使用;
|
||||||
|
- CDN 路径中的固定 token 取自前端引擎 JS,如未来失效需更新脚本中的 `STEP_TOKEN` 常量;
|
||||||
|
- 仅公开器件可匿名下载;私有器件需 `--cookie`。
|
||||||
|
|
||||||
|
## 示例输出
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python3 lceda_step.py C25744 C2590
|
||||||
|
C25744 | 0402WGF1002TCE | model_doc=98c88bc2b9094f218f3a3e30a7012738
|
||||||
|
-> C25744 STEP 1068243 bytes -> 0402WGF1002TCE.step
|
||||||
|
C2590 | IRLML2803TRPBF | model_doc=c62791f0466c4ae2a892c056b845b0ca
|
||||||
|
-> C2590 STEP 2773949 bytes -> IRLML2803TRPBF.step
|
||||||
|
```
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Download 3D models (STEP/OBJ) from LCEDA by LCSC part code (Cxxxxx) or component UUID.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 lceda_step.py C25744 C2590
|
||||||
|
python3 lceda_step.py --obj --outdir ./models C25744 C2590
|
||||||
|
python3 lceda_step.py 98c88bc2b9094f218f3a3e30a7012738
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
PRODUCT_SEARCH = "https://pro.lceda.cn/api/v2/eda/product/search"
|
||||||
|
COMPONENT_API = "https://pro.lceda.cn/api/v2/components/{uuid}"
|
||||||
|
CDN = "https://modules.lceda.cn"
|
||||||
|
STEP_TOKEN = "qAxj6KHrDKw4blvCG8QJPs7Y"
|
||||||
|
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||||
|
UUID_RE = re.compile(r"^[0-9a-f]{32}$")
|
||||||
|
|
||||||
|
|
||||||
|
def http_get(url, cookie=None, timeout=60):
|
||||||
|
headers = {"User-Agent": UA}
|
||||||
|
if cookie:
|
||||||
|
headers["Cookie"] = cookie
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return resp.read()
|
||||||
|
|
||||||
|
|
||||||
|
def http_post(url, payload, cookie=None, timeout=60):
|
||||||
|
headers = {"User-Agent": UA, "Content-Type": "application/json"}
|
||||||
|
if cookie:
|
||||||
|
headers["Cookie"] = cookie
|
||||||
|
req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers=headers)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
|
||||||
|
|
||||||
|
def search_products(codes, cookie=None):
|
||||||
|
data = http_post(PRODUCT_SEARCH, {"codes": ",".join(codes)}, cookie=cookie)
|
||||||
|
return data.get("result", {}).get("productList", [])
|
||||||
|
|
||||||
|
|
||||||
|
def get_component(uuid, cookie=None):
|
||||||
|
data = json.loads(http_get(COMPONENT_API.format(uuid=uuid), cookie=cookie))
|
||||||
|
if not data.get("success"):
|
||||||
|
raise RuntimeError("{}".format(data.get("message", "failed")))
|
||||||
|
return data["result"]
|
||||||
|
|
||||||
|
|
||||||
|
def safe_name(name):
|
||||||
|
return re.sub(r'[\\/:*?"<>|\s]+', "_", name).strip("_") or "model"
|
||||||
|
|
||||||
|
|
||||||
|
def download(url, dest, cookie=None):
|
||||||
|
body = http_get(url, cookie=cookie)
|
||||||
|
with open(dest, "wb") as f:
|
||||||
|
f.write(body)
|
||||||
|
return len(body)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_step(code, name, model_doc_uuid, owner, outdir, want_obj, cookie):
|
||||||
|
try:
|
||||||
|
comp = get_component(model_doc_uuid, cookie)
|
||||||
|
model = comp.get("3d_model_uuid") or model_doc_uuid
|
||||||
|
if not owner:
|
||||||
|
owner = comp.get("path", "")
|
||||||
|
except Exception:
|
||||||
|
model = model_doc_uuid
|
||||||
|
if not model:
|
||||||
|
print(" {}: no 3D model uuid for {}".format(code, model_doc_uuid))
|
||||||
|
return
|
||||||
|
fname = safe_name(name)
|
||||||
|
step_url = "{}/{}/{}?path={}".format(CDN, STEP_TOKEN, model, owner)
|
||||||
|
try:
|
||||||
|
n = download(step_url, "{}/{}.step".format(outdir, fname), cookie)
|
||||||
|
print(" -> {} STEP {} bytes -> {}.step".format(code, n, fname))
|
||||||
|
except Exception as e:
|
||||||
|
print(" -> {} STEP download failed: {}".format(code, e))
|
||||||
|
if want_obj:
|
||||||
|
obj_url = "{}/3dmodel/{}?path={}".format(CDN, model, owner)
|
||||||
|
try:
|
||||||
|
n = download(obj_url, "{}/{}.obj".format(outdir, fname), cookie)
|
||||||
|
print(" -> {} OBJ {} bytes -> {}.obj".format(code, n, fname))
|
||||||
|
except Exception as e:
|
||||||
|
print(" -> {} OBJ download failed: {}".format(code, e))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Download LCEDA 3D models by LCSC code / UUID")
|
||||||
|
ap.add_argument("items", nargs="+", help="LCSC part codes (Cxxxxx) or 32-hex component UUIDs")
|
||||||
|
ap.add_argument("--obj", action="store_true", help="also download the source OBJ model")
|
||||||
|
ap.add_argument("--outdir", default=".", help="output directory (default: current)")
|
||||||
|
ap.add_argument("--cookie", default="", help="optional Cookie header for private components")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
codes = [i for i in args.items if not UUID_RE.match(i)]
|
||||||
|
uuids = [i for i in args.items if UUID_RE.match(i)]
|
||||||
|
|
||||||
|
if codes:
|
||||||
|
try:
|
||||||
|
products = search_products(codes, args.cookie)
|
||||||
|
except Exception as e:
|
||||||
|
print("product search failed: {}".format(e))
|
||||||
|
products = []
|
||||||
|
found = {}
|
||||||
|
for p in products:
|
||||||
|
di = p.get("device_info") or {}
|
||||||
|
fi = di.get("footprint_info") or {}
|
||||||
|
m3 = fi.get("model_3d") or {}
|
||||||
|
uri = m3.get("uri") or (di.get("attributes") or {}).get("3D Model")
|
||||||
|
owner = (di.get("owner") or {}).get("uuid", "")
|
||||||
|
found[p["code"]] = {
|
||||||
|
"name": p.get("model") or p.get("code"),
|
||||||
|
"uri": uri,
|
||||||
|
"owner": owner,
|
||||||
|
}
|
||||||
|
for code in codes:
|
||||||
|
info = found.get(code)
|
||||||
|
if not info or not info["uri"]:
|
||||||
|
print("{}: no device/3D model info".format(code))
|
||||||
|
continue
|
||||||
|
print("{} | {} | model_doc={}".format(code, info["name"], info["uri"]))
|
||||||
|
try:
|
||||||
|
fetch_step(code, info["name"], info["uri"], info["owner"], args.outdir, args.obj, args.cookie)
|
||||||
|
except Exception as e:
|
||||||
|
print(" {}: {}".format(code, e))
|
||||||
|
|
||||||
|
for uuid in uuids:
|
||||||
|
try:
|
||||||
|
comp = get_component(uuid, args.cookie)
|
||||||
|
except Exception as e:
|
||||||
|
print("{}: {}".format(uuid, e))
|
||||||
|
continue
|
||||||
|
name = comp.get("display_title") or comp.get("title") or uuid
|
||||||
|
model = comp.get("3d_model_uuid")
|
||||||
|
owner = comp.get("path", "")
|
||||||
|
print("{} | {} | model={} | owner={}".format(uuid, name, model, owner))
|
||||||
|
if not model:
|
||||||
|
print(" no 3D model available")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
fetch_step(uuid, name, uuid, owner, args.outdir, args.obj, args.cookie)
|
||||||
|
except Exception as e:
|
||||||
|
print(" {}: {}".format(uuid, e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user