R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+227
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 KiCad 符号库 kicad/lib/PhoneRail.kicad_sym。
|
||||
|
||||
来源:EasyEDA 工程包里的 .esym(页面源码里没有符号图形,必须用工程包)。
|
||||
|
||||
坐标:EasyEDA 符号和 KiCad .kicad_sym **都是 Y 向上**,所以图形不翻 Y,只做单位换算
|
||||
(1 单位 = 0.01 inch = 0.254 mm)。引脚 (x,y) 两边都是「连接点」,rotation 两边都是
|
||||
「从连接点指向本体」的方向,可以直接映射 —— 已用 LMR51430YDDCR 实测核对。
|
||||
|
||||
产出两类符号:
|
||||
· 68 个元件符号(part 引用的)
|
||||
· 4 个电源符号(netflag 的 GND / +12V / +3V3 / +3V3_HUB),带 (power) 标记,
|
||||
KiCad 靠它做电源网络的全局连接
|
||||
"""
|
||||
import json, os, re, sys, collections
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import k_esym
|
||||
from k_esym import MM, PIN_TYPE
|
||||
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC = os.path.join(ROOT, "eda_src")
|
||||
SYMDIR = os.path.join(SRC, "epro", "SYMBOL")
|
||||
OUTDIR = os.path.join(os.path.dirname(ROOT), "kicad", "lib")
|
||||
LIBNAME = "PhoneRail"
|
||||
|
||||
FONT = "(effects (font (size 1.27 1.27)))"
|
||||
FONT_HIDE = "(effects (font (size 1.27 1.27)) (hide yes))"
|
||||
|
||||
|
||||
def q(s):
|
||||
"""S-expression 字符串转义"""
|
||||
return '"' + str(s if s is not None else "").replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def n(v):
|
||||
"""坐标数字:EasyEDA 单位 -> mm,去掉浮点毛刺"""
|
||||
x = round(float(v) * MM, 4)
|
||||
return f"{x:.4f}".rstrip("0").rstrip(".") or "0"
|
||||
|
||||
|
||||
def safe(name):
|
||||
"""KiCad 符号名不能带 : / 换行"""
|
||||
return re.sub(r'[:/\r\n\t]', "_", (name or "UNNAMED")).strip()
|
||||
|
||||
|
||||
def shapes_sexp(sym, ind):
|
||||
out = []
|
||||
for s in sym["shapes"]:
|
||||
if s["k"] == "rect":
|
||||
out.append(f'{ind}(rectangle (start {n(s["x1"])} {n(s["y1"])}) (end {n(s["x2"])} {n(s["y2"])})\n'
|
||||
f'{ind}\t(stroke (width 0) (type default)) (fill (type background))\n{ind})')
|
||||
elif s["k"] == "poly":
|
||||
pts = " ".join(f"(xy {n(x)} {n(y)})" for x, y in s["pts"])
|
||||
fill = "background" if s["closed"] else "none"
|
||||
out.append(f'{ind}(polyline (pts {pts})\n'
|
||||
f'{ind}\t(stroke (width 0) (type default)) (fill (type {fill}))\n{ind})')
|
||||
elif s["k"] == "circle":
|
||||
out.append(f'{ind}(circle (center {n(s["cx"])} {n(s["cy"])}) (radius {n(s["r"])})\n'
|
||||
f'{ind}\t(stroke (width 0) (type default)) (fill (type none))\n{ind})')
|
||||
elif s["k"] == "arc":
|
||||
out.append(f'{ind}(arc (start {n(s["sx"])} {n(s["sy"])}) (mid {n(s["mx"])} {n(s["my"])}) '
|
||||
f'(end {n(s["ex"])} {n(s["ey"])})\n'
|
||||
f'{ind}\t(stroke (width 0) (type default)) (fill (type none))\n{ind})')
|
||||
for t in sym["texts"]:
|
||||
out.append(f'{ind}(text {q(t["s"])} (at {n(t["x"])} {n(t["y"])} {int(t.get("rot") or 0)}) {FONT})')
|
||||
return out
|
||||
|
||||
|
||||
def pins_sexp(sym, ind, hide_names=False):
|
||||
out = []
|
||||
for p in sym["pins"]:
|
||||
et = PIN_TYPE.get(p["type"], "unspecified")
|
||||
nm = p["name"] if p["name"] is not None else "~"
|
||||
nf = FONT_HIDE if (hide_names or not p["nameVisible"]) else FONT
|
||||
numf = FONT_HIDE if not p["numberVisible"] else FONT
|
||||
out.append(
|
||||
f'{ind}(pin {et} line (at {n(p["x"])} {n(p["y"])} {int(p["rot"]) % 360}) (length {n(p["len"])})\n'
|
||||
f'{ind}\t(name {q(nm)} {nf})\n'
|
||||
f'{ind}\t(number {q(p["number"] if p["number"] is not None else "")} {numf})\n{ind})')
|
||||
return out
|
||||
|
||||
|
||||
def symbol_sexp(kname, sym, ref, value, power=False, descr=""):
|
||||
I = "\t"
|
||||
L = [f'{I}(symbol {q(kname)}']
|
||||
if power:
|
||||
L.append(f'{I*2}(power)')
|
||||
L.append(f'{I*2}(pin_names (offset 0) hide)')
|
||||
else:
|
||||
L.append(f'{I*2}(pin_names (offset 0.254))')
|
||||
L += [f'{I*2}(exclude_from_sim no)', f'{I*2}(in_bom {"no" if power else "yes"})',
|
||||
f'{I*2}(on_board {"no" if power else "yes"})']
|
||||
bb = sym.get("bbox") or [-10, -10, 10, 10]
|
||||
top = n(max(bb[1], bb[3]) + 8)
|
||||
bot = n(min(bb[1], bb[3]) - 8)
|
||||
L.append(f'{I*2}(property "Reference" {q(ref)} (at 0 {top} 0) {FONT})')
|
||||
L.append(f'{I*2}(property "Value" {q(value)} (at 0 {bot} 0) {FONT})')
|
||||
L.append(f'{I*2}(property "Footprint" "" (at 0 0 0) {FONT_HIDE})')
|
||||
L.append(f'{I*2}(property "Datasheet" "" (at 0 0 0) {FONT_HIDE})')
|
||||
L.append(f'{I*2}(property "Description" {q(descr)} (at 0 0 0) {FONT_HIDE})')
|
||||
g = shapes_sexp(sym, I * 3)
|
||||
L.append(f'{I*2}(symbol {q(kname + "_0_1")}')
|
||||
L += g
|
||||
L.append(f'{I*2})')
|
||||
L.append(f'{I*2}(symbol {q(kname + "_1_1")}')
|
||||
L += pins_sexp(sym, I * 3, hide_names=power)
|
||||
L.append(f'{I*2})')
|
||||
L.append(f'{I})')
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def mirror_sym(sym):
|
||||
"""沿 Y 轴镜像一个符号(X 取反)。
|
||||
|
||||
为什么不用 KiCad 的 (mirror y):EasyEDA 的镜像是「先旋转再翻 X」,而 KiCad 的
|
||||
mirror 与旋转的复合次序不同,2026-09-07 实测直接用 (mirror y) 会让 U24~U28
|
||||
的引脚落到镜像位置(对账报 U24.3 变成 U24.6)。生成显式镜像符号后,配合
|
||||
角度取 (360-θ) 就能精确复现,且不依赖对 KiCad 内部次序的猜测。
|
||||
"""
|
||||
out = {"name": sym["name"], "bbox": None, "shapes": [], "texts": [], "pins": [], "skipped": []}
|
||||
if sym.get("bbox"):
|
||||
b = sym["bbox"]; out["bbox"] = [-b[2], b[1], -b[0], b[3]]
|
||||
for sh in sym["shapes"]:
|
||||
k = sh["k"]
|
||||
if k == "rect":
|
||||
out["shapes"].append({"k": "rect", "x1": -sh["x1"], "y1": sh["y1"],
|
||||
"x2": -sh["x2"], "y2": sh["y2"]})
|
||||
elif k == "poly":
|
||||
out["shapes"].append({"k": "poly", "closed": sh["closed"],
|
||||
"pts": [(-x, y) for x, y in sh["pts"]]})
|
||||
elif k == "circle":
|
||||
out["shapes"].append({"k": "circle", "cx": -sh["cx"], "cy": sh["cy"], "r": sh["r"]})
|
||||
elif k == "arc":
|
||||
out["shapes"].append({"k": "arc", "sx": -sh["sx"], "sy": sh["sy"],
|
||||
"mx": -sh["mx"], "my": sh["my"],
|
||||
"ex": -sh["ex"], "ey": sh["ey"]})
|
||||
out["texts"] = [dict(t, x=-t["x"]) for t in sym["texts"]]
|
||||
for q in sym["pins"]:
|
||||
out["pins"].append(dict(q, x=-q["x"], rot=(180 - int(q["rot"])) % 360))
|
||||
return out
|
||||
|
||||
|
||||
def mirrored_uuids():
|
||||
"""哪些符号被 mirror=True 的实例用到 —— 只给这些生成镜像变体"""
|
||||
tree = json.load(open(os.path.join(SRC, "_tree.json"), encoding="utf-8"))
|
||||
out = set()
|
||||
for b in tree["boards"]:
|
||||
for p in b["pages"]:
|
||||
g = json.load(open(os.path.join(SRC, b["board"], p["name"] + ".geom.json"), encoding="utf-8"))
|
||||
for c in (g["prims"]["Component"] or []):
|
||||
if c["ComponentType"] == "part" and c.get("Mirror"):
|
||||
out.add((c.get("Symbol") or {}).get("uuid"))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
used = json.load(open(os.path.join(SRC, "_symbols_used.json"), encoding="utf-8"))
|
||||
# netflag / netport 的符号 uuid + 网络,从几何数据里再扫一遍
|
||||
tree = json.load(open(os.path.join(SRC, "_tree.json"), encoding="utf-8"))
|
||||
flagsym = {} # net -> symbol uuid
|
||||
for b in tree["boards"]:
|
||||
for p in b["pages"]:
|
||||
g = json.load(open(os.path.join(SRC, b["board"], p["name"] + ".geom.json"), encoding="utf-8"))
|
||||
for c in (g["prims"]["Component"] or []):
|
||||
if c["ComponentType"] == "netflag" and c.get("Net"):
|
||||
flagsym.setdefault(c["Net"], (c.get("Component") or {}).get("uuid"))
|
||||
|
||||
os.makedirs(OUTDIR, exist_ok=True)
|
||||
need_mirror = mirrored_uuids()
|
||||
body, names, mirrors, missing = [], {}, {}, []
|
||||
seen = collections.Counter()
|
||||
|
||||
for uuid, meta in sorted(used.items(), key=lambda kv: (kv[1]["name"] or "")):
|
||||
path = os.path.join(SYMDIR, uuid + ".esym")
|
||||
if not os.path.exists(path):
|
||||
missing.append((uuid, meta["name"])); continue
|
||||
sym = k_esym.parse(path)
|
||||
base = safe((sym["name"] or meta["name"] or "").rsplit(".", 1)[0])
|
||||
seen[base] += 1
|
||||
kname = base if seen[base] == 1 else f"{base}_{seen[base]}"
|
||||
names[uuid] = kname
|
||||
body.append(symbol_sexp(kname, sym, meta["refs"][0], base))
|
||||
if uuid in need_mirror:
|
||||
mk = kname + "_MIRRORED"
|
||||
mirrors[uuid] = mk
|
||||
body.append(symbol_sexp(mk, mirror_sym(sym), meta["refs"][0], base))
|
||||
|
||||
# 电源符号:netflag 用的是 EasyEDA 系统符号,不在工程包里,所以按 KiCad 官方
|
||||
# power 库的画法自建(引脚在原点、length 0、GND 朝下 270 / +V 朝上 90)。
|
||||
# 旋转语义两边一致,实测见 easyeda-sch-geometry skill §2:
|
||||
# Ground rot=0 -> 符号在下、引脚在上;KiCad GND 图形也在引脚下方,rot 0 对 rot 0。
|
||||
# Power rot=0 -> 符号在上、引脚在下;KiCad +V 图形在引脚上方,同样 1:1。
|
||||
powers = {}
|
||||
for net in sorted(flagsym):
|
||||
kname = safe(net)
|
||||
powers[net] = kname
|
||||
if net.upper().startswith("GND") or net.upper() in ("AGND", "PGND"):
|
||||
gfx = [{"k": "poly", "closed": False,
|
||||
"pts": [(0, 0), (0, -5), (5, -5), (0, -10), (-5, -5), (0, -5)]}]
|
||||
ang, vy = 270, -15
|
||||
else:
|
||||
gfx = [{"k": "poly", "closed": False, "pts": [(-3, 5), (0, 10)]},
|
||||
{"k": "poly", "closed": False, "pts": [(0, 10), (3, 5)]},
|
||||
{"k": "poly", "closed": False, "pts": [(0, 0), (0, 10)]}]
|
||||
ang, vy = 90, 14
|
||||
sym = {"name": net, "bbox": [-5, -10, 5, 10], "shapes": gfx, "texts": [],
|
||||
"pins": [{"x": 0, "y": 0, "len": 0, "rot": ang, "number": "1", "name": "",
|
||||
"type": "POWER", "nameVisible": False, "numberVisible": False}]}
|
||||
body.append(symbol_sexp(kname, sym, "#PWR", net, power=True,
|
||||
descr=f"Power symbol creates a global label with name {net!r}"))
|
||||
|
||||
out = ["(kicad_symbol_lib", "\t(version 20251024)", f"\t(generator {q('phonerail-eda2kicad')})",
|
||||
"\t(generator_version \"10.0\")"] + body + [")"]
|
||||
p = os.path.join(OUTDIR, LIBNAME + ".kicad_sym")
|
||||
open(p, "w", encoding="utf-8", newline="\n").write("\n".join(out) + "\n")
|
||||
|
||||
json.dump({"symbols": names, "powers": powers, "mirrors": mirrors},
|
||||
open(os.path.join(SRC, "_kicad_symnames.json"), "w", encoding="utf-8"),
|
||||
ensure_ascii=False, indent=1)
|
||||
print(f"元件符号 {len(names)} 镜像变体 {len(mirrors)} 电源符号 {len(powers)} -> {p} ({os.path.getsize(p):,d} B)")
|
||||
if missing:
|
||||
print(f" ⚠ 找不到 .esym 的元件符号 {len(missing)}: {missing[:5]}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户