#!/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()