From 55feb4c41f7e372aacca7b4e2a4188e5ab76d086 Mon Sep 17 00:00:00 2001 From: 7iwind <7wind@7iwind.cn> Date: Tue, 11 Aug 2026 18:28:21 +0800 Subject: [PATCH] lc-step: LCEDA 3D model (STEP/OBJ) downloader by LCSC code --- README.md | 75 +++++++++++++++++++++++++ lceda_step.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 README.md create mode 100644 lceda_step.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e58081e --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/lceda_step.py b/lceda_step.py new file mode 100644 index 0000000..ddc0678 --- /dev/null +++ b/lceda_step.py @@ -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()