@huaqiu/dsh-kicad 0.4.2

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.
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@huaqiu/dsh-kicad",
3
+ "version": "0.4.2",
4
+ "type": "module",
5
+ "main": "./lib/index.mjs",
6
+ "types": "./lib/index.d.mts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.mts",
10
+ "default": "./lib/index.mjs"
11
+ },
12
+ "./cordis.patch.yml": "./cordis.patch.yml",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "dsh": {
16
+ "bundle": {
17
+ "patch": "./cordis.patch.yml"
18
+ }
19
+ },
20
+ "peerDependencies": {
21
+ "@deepseek-ai/cordis": "^4.0.1",
22
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.0"
23
+ },
24
+ "dependencies": {
25
+ "@huaqiu/dsh-plugin-log": "0.4.2"
26
+ },
27
+ "files": [
28
+ "lib",
29
+ "src",
30
+ "skills",
31
+ "cordis.patch.yml"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "typecheck": "tsc --noEmit",
38
+ "build": "tsdown",
39
+ "test": "vitest run"
40
+ }
41
+ }
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: kicad-ipc
3
+ description: "通过 KiCad IPC API 和官方 kicad-python 在打开的 PCB 中检查、新建、修改或删除对象;适用于 PCB 自动化、外部布线结果导入和插件开发,不用于直接编辑 .kicad_pcb 文件。"
4
+ ---
5
+
6
+ # KiCad IPC PCB
7
+
8
+ 使用 KiCad 的 IPC API 与 `kicad-python` 作为 PCB 读写的默认且优先接口。目标是让 KiCad 自己维护对象、UUID、连通性、撤销历史和磁盘保存;不要把 `.kicad_pcb` 当作可直接改写的中间格式。
9
+
10
+ ## 本技能与工具的配合
11
+
12
+ 安装 `@huaqiu/dsh-kicad` 后,本技能与下面这些工具一起可用,无需单独安装技能。
13
+
14
+ 工具是**可执行接口**:它们负责连接 KiCad、传参、提交事务并把 KiCad 的返回原样带回来。本技能是**操作准则**:什么时候该用、先读什么、如何验证、什么时候必须停下来问用户。
15
+
16
+ | 工具 | 作用 |
17
+ | --- | --- |
18
+ | `kicad_ipc_diagnose` | 只读:检查连接、API 版本和当前 PCB。任何 KiCad 操作前先跑它。 |
19
+ | `kicad_ipc_verify_live` | 在一个被 drop 的事务里跑完整 CRUD 冒烟测试,不落盘。 |
20
+ | `kicad_pcb_create_track` / `create_via` / `create_copper_zone` | 在**已存在的网络**上新建走线、过孔、未填充铜区。 |
21
+ | `kicad_pcb_add_footprint_from_template` | 克隆板上已有封装作为模板,新增一个封装。 |
22
+ | `kicad_pcb_move_rotate_footprint` | 按 reference 移动/旋转一个封装。 |
23
+ | `kicad_pcb_update_selected_track_width` | 改当前选中走线的线宽。 |
24
+ | `kicad_pcb_refill_zones` | 等待铺铜填充完成。 |
25
+ | `kicad_pcb_remove_selected_items` | 删除当前选中对象(必须显式 `confirm`)。 |
26
+
27
+ 工具的返回信封统一为 `{ ok, script, effect, output }` 或 `{ ok, error: { kind, message } }`。`effect` 说明该操作对板子做了什么:`read` 只读、`probe` 改了但事务被丢弃、`mutate` 已提交。
28
+
29
+ **工具返回 `ok: true` 不等于改动已达成用户意图。** 每次重要改动后仍要重新读取受影响的状态确认(见下文的“读取 → 校验 → 变更 → 验证 → 保存”)。
30
+
31
+ 需要工具没覆盖的操作时,读取并复制 [scripts/](scripts/) 内的模板改参数运行;本目录下的脚本就是这些工具的实现。
32
+
33
+ ## 基本边界
34
+
35
+ - 不要直接解析后改写、拼接或格式化 `.kicad_pcb`(包括把 `get_as_string()` 的结果改后写回)。它只可用于只读诊断或用户明确要求的导出。
36
+ - 不要用 `pcbnew` / SWIG API(如 `import pcbnew`、`pcbnew.GetBoard()`、`BOARD.Add()`、`SaveBoard()`)来新建或修改 PCB。它是与 KiCad 内部实现紧耦合的旧接口,KiCad 11 已移除。
37
+ - 不要绕过 SDK 直接构造或发送 Protobuf 请求;使用 `kipy` 的公开对象、属性及 `Board` 方法。不要依赖内部 `proto` 字段作为业务接口。
38
+ - 不要假定 IPC API 覆盖所有 PCB 功能。仅当缺少所需公开接口时退回 `pcbnew` 作为默认替代方案。
39
+ - 任何会删除、覆写、移动大量对象、改变层叠或保存到磁盘的操作,先确定用户给出的精确作用域。禁止用“全部走线”“所有对象”一类模糊范围作默认选择。
40
+
41
+ ## 连接与版本路由
42
+
43
+ 1. 确认目标是 KiCad 9.0+,并使用官方包 `kicad-python`(Python 模块为 `kipy`)。先以 `KiCad()` 连接、调用 `check_version()`,再通过 `get_board()` 取得当前 PCB。
44
+ 2. KiCad 9/10 只能连接到启用 API 服务的运行中 GUI;由 KiCad 启动的 IPC 插件会提供 `KICAD_API_SOCKET` 和 `KICAD_API_TOKEN`。不要猜测、硬编码或扫描其他 KiCad 实例的连接端点。
45
+ 3. KiCad 11 可使用 `KiCad(headless=True, file_path=...)` / `kicad-cli api-server` 处理无头任务。此前版本不可把 IPC SDK 当作脱离 GUI 的板文件库。
46
+ 4. 若 KiCad 已打开但连接不到服务器,先判定不是 GUI 正忙:提示用户在 **偏好设置 → 插件** 中启用 KiCad API 服务,并在变更后重启 PCB Editor,再重新运行脚本。不要扫描命名管道、猜测 token 或以文件编辑替代连接。
47
+ 5. 在 DSH 中运行外部 KiCad 交互前,确认当前沙盒权限是 **Full Access**。非 Full Access 的沙盒可能禁止访问 KiCad 的 Windows 命名管道,即使 GUI 已打开也无法连接;停止操作并提示用户以 Full Access 重新运行或提升当前任务权限。权限不足不是可重试的连接故障。
48
+ 6. 在一个任务中只维护一个连接。GUI 正忙时 API 会返回忙或超时:有限次数地重试短暂、幂等的读取;对写入操作,重读对象状态后再决定是否重试,绝不盲目重复。
49
+
50
+ 详细版本差异、连接方式、公开 CRUD 调用和最小代码模式见 [references/ipc-pcb-workflows.md](references/ipc-pcb-workflows.md)。需要可直接改参数运行的常用操作时,读取并复制 [scripts/](scripts/) 内的对应模板;先执行 `diagnose_ipc_connection.py` 排除 API 与 DSH 权限问题。
51
+
52
+ ## PCB 对象工作流
53
+
54
+ 对每一次实际修改,遵守“读取 → 校验 → 变更 → 验证 → 保存”的流程:
55
+
56
+ 1. **读取并定位。** 使用 `get_nets()`、`get_footprints()`、`get_pads()`、`get_tracks()`、`get_vias()`、`get_zones()` 或版本支持时的 `get_items_by_id()`。按网络名、封装 reference、焊盘号、层和位置等稳定语义定位;在写入前重新获取目标对象,保留其 KiCad UUID。
57
+ 2. **预校验。** 解析并检查单位、坐标、层、线宽/孔径、网络存在性、目标数量及锁定状态。所有长度换算为 SDK 所用的纳米整数(优先 `from_mm()`);铜对象只能落在启用的铜层上。不要凭空建立网络来掩盖网名不匹配,应先从原理图/网表把网络同步到板中。
58
+ 3. **成组变更。** 多对象操作以 `begin_commit()` 包裹;成功后 `push_commit(commit, message)`,异常或验证失败则 `drop_commit(commit)`。这样用户在 KiCad 中只看到一个可撤销步骤。
59
+ 4. **写入。** 用 `create_items()` 新建,用从板上新鲜读取的对象配合 `update_items()` 修改,用 `remove_items()` / `remove_items_by_id()` 删除。`update_items()` 会以传入对象的全部属性更新同 UUID 对象,所以不可用手工重建的简化对象覆盖既有对象。
60
+ 5. **验证与持久化。** 检索 SDK 返回的创建/更新对象以确认 UUID、数目和被 KiCad 约束后的参数;必要时调用 `refill_zones()` 并运行目标版本有公开接口支持的检查。只有任务要求将改动落盘且用户授权时,调用 `board.save()`;禁止自己写文件。
61
+
62
+ ## 新建和修改时的选择
63
+
64
+ - 新建走线、圆弧走线和过孔时,实例化相应 `kipy.board_types` 对象,设置公开的 `start` / `end` / `mid` / `position`、`width`、`layer`、`net` 和过孔公开属性,然后将完整对象交给 `create_items()`。使用 `create_items()` 的返回值继续后续操作,不要自行生成 UUID。
65
+ - 新建图形、文本、尺寸、区域或封装时,同样使用 SDK 的对应 wrapper 和 `create_items()`;区域创建后按需要在同一事务完成 `refill_zones()`。对于 API 尚不能创建的复杂对象,明确报告能力缺口。
66
+ - 修改已有对象时,先按 UUID 或稳定语义重新读取,再只调整所需公开属性,最后批量 `update_items()`。移动或旋转封装使用 `position` 与 `orientation` 的公开属性;不要通过编辑封装/板的 S 表达式来间接修改。
67
+ - 当目标版本提供 `Item.clone()` 时,新增与现有封装相同的封装应从用户指定的板上模板封装 `clone()`,设置新的 reference 和位置后交给 `create_items()`。不要复制私有 `proto` 或 UUID。若用户要求“从库直接放置”而 SDK 没有对应公开 API,应报告该能力缺口,而不是读写板文件。
68
+ - 新建铜区先校验外形闭合、网络、层和作用域,通过 `create_items()` 创建。铺铜填充使用 `refill_zones()`,并在填充后重新读取区域;填充和保存应由用户明确请求,不能把填充失败用文件编辑掩盖。
69
+ - 删除前报告将被删除的精确对象数和选择条件。批量删除自动布线时,只删除用户明确界定的网络、区域或由工具拥有的对象;默认保留用户手工走线。
70
+
71
+ ## 外部 SES 布线结果导入
72
+
73
+ SES 是外部工具的输入,不是可直接合并进 `.kicad_pcb` 的补丁。始终采用“解析 → 映射 → 校验 → IPC 写入”的路径:
74
+
75
+ 1. 只读解析 SES,转换为内部、单位明确的布线计划:每段包含网络名、起止坐标、层、线宽;每个过孔包含网络名、位置、尺寸、钻孔和层对。保留源记录编号,便于诊断;解析阶段不触碰 PCB。
76
+ 2. 从当前板通过 IPC 取得网络、焊盘、启用层和既有走线。将 SES 网名映射到现有 `Net`,将层映射到当前板的有效铜层;任何未知网络、无效层、尺寸缺失或单位不确定均停止写入并报告。
77
+ 3. 先构建所有 `Track` / `ArcTrack` / `Via` wrapper 并进行数量、端点、层与网络完整性校验。必要时先预览计划和替换范围。
78
+ 4. 只在用户指定的替换范围内,事务性地删除旧的同范围自动布线,再以 `create_items()` 批量创建导入对象。不得调用 `pcbnew.ImportSpecctraSES()`,不得修改板文件文本,也不得把 SES 片段复制进文件。
79
+ 5. 推送事务后,重新读取相关网络的走线和过孔,比较计划数与实际数;按需填充区域、执行可用的检查,并在需要持久化时调用 `board.save()`。保存失败或验证不通过时保留诊断,不以文件编辑补救。
80
+
81
+ 若任务只是审阅、转换或预览 SES,停在计划阶段,不修改 PCB。
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "KiCad IPC PCB"
3
+ short_description: "通过官方 IPC SDK 安全地编辑 PCB"
@@ -0,0 +1,145 @@
1
+ # KiCad IPC PCB 工作流参考
2
+
3
+ 仅在需要实施、调试或审查具体 IPC 脚本时阅读本文件。以目标环境实际安装的 `kicad-python` 文档和版本为准;不要从本参考推断未列出的 API 一定可用。
4
+
5
+ ## 版本与连接
6
+
7
+ - IPC API 从 KiCad 9.0 起提供。KiCad 9/10 通过运行中 PCB Editor 的 IPC 服务工作;KiCad 11 增加 `kicad-cli` 无头 API 服务。
8
+ - `kicad-python` 是官方 Python 绑定,入口是 `from kipy import KiCad`。由 KiCad 启动的插件应让 `KiCad()` 使用它提供的 `KICAD_API_SOCKET` 和 `KICAD_API_TOKEN`;不要自行伪造凭据。
9
+ - 首次连接后调用 `kicad.check_version()`。如果失败,停止并报告本机 KiCad 与 Python 包版本不匹配,而不是继续尝试写入。
10
+ - 若连接报错而 KiCad GUI 已打开,首先请用户在 **偏好设置 → 插件** 中启用 API 服务,重启 PCB Editor 后再试。KiCad 未启用 API 时,不存在可供脚本连接的服务端;`KICAD_API_SOCKET`、扫描命名管道或重试不能修复此配置。
11
+ - 在 DSH 运行时,IPC 客户端需要 **Full Access** 才能访问 KiCad 的 Windows 命名管道。若当前权限不是 Full Access,先请求用户在 DSH 中提升权限或重新在 Full Access 任务中执行;不应把这类权限拒绝描述为 KiCad 未运行或不断重试。
12
+
13
+ ```python
14
+ from kipy import KiCad
15
+
16
+ kicad = KiCad(timeout_ms=5000)
17
+ if not kicad.check_version():
18
+ raise RuntimeError("kicad-python 与已连接的 KiCad API 版本不匹配")
19
+ board = kicad.get_board()
20
+ ```
21
+
22
+ 运行任何修改脚本之前,可先执行 [../scripts/diagnose_ipc_connection.py](../scripts/diagnose_ipc_connection.py)。它只诊断 Python 包、连接、版本和打开的 PCB,不会修改或保存板文件。
23
+
24
+ ## 公开 CRUD 速查
25
+
26
+ | 目标 | 首选调用 | 要点 |
27
+ | --- | --- | --- |
28
+ | 查询 | `board.get_nets()`、`get_footprints()`、`get_pads()`、`get_tracks()`、`get_vias()`、`get_zones()`、`get_items_by_id()` | 写入前重读目标;按 UUID 或稳定语义定位。 |
29
+ | 新建 | `board.create_items(item_or_items)` | 返回由 KiCad 创建的实际对象及其 UUID。 |
30
+ | 更新 | `board.update_items(item_or_items)` | 目标必须已存在,按 UUID 匹配;传入的是完整对象状态。 |
31
+ | 删除 | `board.remove_items(items)` 或 `remove_items_by_id(ids)` | 只传入已审核的精确范围。 |
32
+ | 原子撤销 | `begin_commit()` → 写操作 → `push_commit()`;失败则 `drop_commit()` | 一项用户意图应对应一个 KiCad 撤销步骤。 |
33
+ | 保存 | `board.save()` | 仅在任务允许持久化时调用,绝不手写 PCB 文件。 |
34
+
35
+ `create_items()`、`update_items()` 与 `remove_items()` 来自所有文档编辑器共有的公开命令接口;不要替换为底层 Protobuf 请求。
36
+
37
+ ## 创建一条走线的最小模式
38
+
39
+ 下面示例强调调用形状,坐标和线宽均使用纳米整数。实际任务应先验证目标网络、层、空间、规则与作用域。
40
+
41
+ ```python
42
+ from kipy.board_types import BoardLayer, Track
43
+ from kipy.geometry import Vector2
44
+ from kipy.util import from_mm
45
+
46
+ nets = {net.name: net for net in board.get_nets()}
47
+ net = nets["GND"] # 缺失时应报错,不要新建替代网络
48
+
49
+ track = Track()
50
+ track.start = Vector2.from_xy(from_mm(10), from_mm(20))
51
+ track.end = Vector2.from_xy(from_mm(20), from_mm(20))
52
+ track.width = from_mm(0.25)
53
+ track.layer = BoardLayer.BL_F_Cu
54
+ track.net = net
55
+
56
+ commit = board.begin_commit()
57
+ try:
58
+ created_track, = board.create_items(track)
59
+ # 在此读取 created_track 并验证返回的实际属性
60
+ board.push_commit(commit, "Add GND track")
61
+ except Exception:
62
+ board.drop_commit(commit)
63
+ raise
64
+ ```
65
+
66
+ 创建圆弧走线、过孔、图形、文本、区域和封装遵循同一结构:使用相应的 `kipy.board_types` wrapper,设置公开属性,并通过 `create_items()` 发送。不要手动设置 `id` 或 `parent`。
67
+
68
+ ## 修改和删除的最小模式
69
+
70
+ 对已存在对象,读取最新对象后再改属性,避免用一个缺失未知字段的临时 wrapper 覆盖当前板状态。
71
+
72
+ ```python
73
+ target = board.get_items_by_id(existing_id)[0]
74
+ target.width = from_mm(0.30)
75
+
76
+ commit = board.begin_commit()
77
+ try:
78
+ updated_target, = board.update_items(target)
79
+ if updated_target.width != from_mm(0.30):
80
+ raise RuntimeError("KiCad 未接受预期线宽")
81
+ board.push_commit(commit, "Widen selected track")
82
+ except Exception:
83
+ board.drop_commit(commit)
84
+ raise
85
+ ```
86
+
87
+ 删除时首先从同样的、可审计的条件得到 `targets`,展示数量与网络/层范围,再在事务中调用 `board.remove_items(targets)`。如果某个目标没有稳定身份或范围不清,停止并请求澄清。
88
+
89
+ ## SES 导入的实现检查表
90
+
91
+ - 解析器仅将 SES 转成中性记录;它不能写 `.kicad_pcb`,也不能调用 `pcbnew`。
92
+ - 每条记录必须有网络名、明确单位的坐标、铜层和几何尺寸;过孔还要有孔径与层对。拒绝静默默认。
93
+ - 对每个网名在 `board.get_nets()` 返回结果中精确匹配;对每层确认它是当前板启用的铜层。目标版本支持时可用 `get_layer_by_name()`;否则使用已验证的层枚举映射。
94
+ - 将计划记录生成 `Track` / `ArcTrack` / `Via` 对象列表,在任何写入前检查列表非空、数量、端点、层和网络。
95
+ - "替换" 不是默认行为:只有用户指定网络、区域或其他可精确表达的范围,才删除旧自动布线。对混合有人工作业的网络,默认只导入新增路线或要求用户选择。
96
+ - 写入使用一个 commit;成功后重新调用 `get_tracks()` / `get_vias()` 或按 UUID 取回对象进行核验。区域受影响时,调用 `refill_zones()` 并等待完成。
97
+
98
+ ## 常见失败处理
99
+
100
+ - KiCad 已打开但连接失败:先确认当前打开的是 PCB Editor,而非仅项目管理器;接着让用户在 **偏好设置 → 插件** 启用 API 服务并重启 PCB Editor。若运行在 DSH,还要确认任务为 **Full Access**。只有这两项都成立后,才把错误作为普通连接问题诊断。
101
+ - `AS_BUSY`、超时或 GUI 交互阻塞:不要并发开新连接。对读取进行有上限的退避重试;对写入先重新获取相关对象,确认未出现部分成功,再谨慎处理。
102
+ - API 未实现或目标 KiCad 版本不支持:说明所需能力、版本和不执行的原因。可建议升级 KiCad 或改变用户工作流,但不要直接改板文件作为偷偷的后备方案。
103
+ - SDK 返回的值被 KiCad 约束或钳制:以返回对象为准;若不满足要求,撤销事务或在明确许可下重新规划,不要假定请求值已生效。
104
+
105
+ ## 可复用脚本模板
106
+
107
+ 这些脚本位于 [../scripts](../scripts),均只使用 `kipy` 公共接口,且所有会修改板的模板都使用 KiCad commit。复制整个 `scripts` 目录,或将其放在同一目录下运行,以便脚本导入 `kipy_common.py`。
108
+
109
+ | 脚本 | 用途 | 写入范围 |
110
+ | --- | --- | --- |
111
+ | `diagnose_ipc_connection.py` | 检查 `kipy`、连接、版本与当前 PCB;输出 API 设置和 DSH Full Access 提示 | 无 |
112
+ | `create_track.py` | 在指定已有网络和层上创建一条直线走线 | 新建一条 track |
113
+ | `create_via.py` | 在指定已有网络上创建一个通孔 via | 新建一个 via |
114
+ | `update_selected_track_width.py` | 修改 KiCad 当前选中直线/圆弧走线的线宽 | 当前选择中的 track/arc track |
115
+ | `remove_selected_items.py` | 删除 KiCad 当前选中对象,须显式 `--yes` | 当前选择 |
116
+ | `move_rotate_footprint.py` | 按 reference 移动和旋转一个封装 | 一个既有封装 |
117
+ | `add_footprint_from_board_template.py` | 用板上指定封装的公开 `clone()` 创建新封装 | 新建一个封装 |
118
+ | `create_copper_zone.py` | 在已有网络的指定铜层建立封闭铜区 | 新建一个未填充区域 |
119
+ | `refill_zones.py` | 等待当前 PCB 的已有铜区填充完成 | 已有区域的填充结果 |
120
+ | `verify_live_ipc.py` | 在未推送 commit 中进行真实 IPC 烟测并丢弃改动 | 无持久化改动 |
121
+
122
+ 模板只提供结构,不会替用户决定网络、层、尺寸或删除范围。执行它们仍须获得与实际 PCB 修改相符的授权。
123
+
124
+ ### 移动、旋转与新增封装
125
+
126
+ - 通过 `get_footprints()` 按精确 reference 找到唯一封装,修改 `position` 和 `orientation`,再调用 `update_items()`;不要修改 footprint 子对象的绝对坐标来模拟整体变换。
127
+ - `Item.clone()`(`kicad-python` 0.8.0 起)会清除顶层对象 ID,适合用完整的板上封装作为模板,再由 `create_items()` 生成新 UUID。脚本仍要求新 reference 在板内唯一。
128
+ - 当前 SDK 版本未提供“按 footprint library nickname/名称取出定义并直接放置”的通用公开 API。`add_footprint_from_board_template.py` 因而有意只支持板上模板克隆;不要用私有 `proto`、`pcbnew` 或 S 表达式来伪造库放置。
129
+
130
+ ### 新建与填充铜区
131
+
132
+ - `create_copper_zone.py` 只建立区域,默认不填充,便于用户先审阅外形和网络;随后按用户意图运行 `refill_zones.py`。
133
+ - KiCad 10.0.6 / `kicad-python` 0.8.0 的公开 `refill_zones()` 接口作用于当前板全部区域,不接受区域 ID 参数。不要把只在更新版本中出现的参数传给这一版本。
134
+ - 填充可能耗时;使用 `block=True` 和有上限的等待时间。填充完成不等于持久化,仍仅在用户要求后调用 `board.save()`。
135
+
136
+ ### 真实环境验证
137
+
138
+ `verify_live_ipc.py` 适用于已启用 IPC 的测试 PCB:它创建和更新临时对象、测试移动旋转、克隆封装、创建铜区并删除临时走线,但所有变化都在同一个未推送的 KiCad commit 内以 `drop_commit()` 丢弃,且从不调用 `save()`。它不应用于含有用户未保存 GUI 改动的板,因为测试脚本无法判定或保护那些改动。
139
+
140
+ ## 官方资料
141
+
142
+ - KiCad IPC API for add-on developers: <https://dev-docs.kicad.org/en/apis-and-binding/ipc-api/for-addon-developers/>
143
+ - `kicad-python` API documentation: <https://docs.kicad.org/kicad-python-main/>
144
+ - Official `kicad-python` source and examples: <https://gitlab.com/kicad/code/kicad-python>
145
+ - Shared editor CRUD implementation: <https://gitlab.com/kicad/code/kicad-python/-/raw/main/kipy/editor.py>
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """Create a new footprint by cloning a named on-board footprint via KiCad IPC.
3
+
4
+ This preserves the complete footprint definition without editing a .kicad_pcb
5
+ file. It deliberately uses the public clone() API so KiCad generates a new
6
+ top-level UUID. It is not a replacement for a future direct library-placement
7
+ API.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+
14
+ from kipy.geometry import Vector2
15
+
16
+ from kipy_common import close_kicad, commit_or_drop, connect_board
17
+
18
+
19
+ def get_unique_footprint(board, reference: str):
20
+ matches = [
21
+ footprint
22
+ for footprint in board.get_footprints()
23
+ if footprint.reference_field.text.value == reference
24
+ ]
25
+ if len(matches) != 1:
26
+ raise RuntimeError(f"source reference {reference!r} 匹配到 {len(matches)} 个封装。")
27
+ return matches[0]
28
+
29
+
30
+ def main() -> None:
31
+ parser = argparse.ArgumentParser(description=__doc__)
32
+ parser.add_argument("--source-reference", required=True, help="作为完整定义模板的现有 reference")
33
+ parser.add_argument("--new-reference", required=True, help="新封装的唯一 reference")
34
+ parser.add_argument("--dx-mm", type=float, required=True, help="相对模板的 X 位移,单位 mm")
35
+ parser.add_argument("--dy-mm", type=float, required=True, help="相对模板的 Y 位移,单位 mm")
36
+ parser.add_argument("--save", action="store_true", help="验证成功后通过 IPC 保存 PCB")
37
+ args = parser.parse_args()
38
+ if args.source_reference == args.new_reference:
39
+ parser.error("--new-reference 必须不同于 --source-reference")
40
+
41
+ kicad, board = connect_board()
42
+ try:
43
+ if any(fp.reference_field.text.value == args.new_reference for fp in board.get_footprints()):
44
+ raise RuntimeError(f"reference {args.new_reference!r} 已存在;未创建封装。")
45
+
46
+ source = get_unique_footprint(board, args.source_reference)
47
+ new_footprint = source.clone()
48
+ new_footprint.position += Vector2.from_xy_mm(args.dx_mm, args.dy_mm)
49
+ new_footprint.reference_field.text.value = args.new_reference
50
+
51
+ def validate(created):
52
+ if len(created) != 1:
53
+ raise RuntimeError("KiCad 未创建恰好一个封装。")
54
+ if created[0].reference_field.text.value != args.new_reference:
55
+ raise RuntimeError("KiCad 返回的封装 reference 与请求不一致。")
56
+
57
+ created, = commit_or_drop(
58
+ board,
59
+ f"Create footprint {args.new_reference} from {args.source_reference}",
60
+ lambda: board.create_items(new_footprint),
61
+ validate,
62
+ )
63
+ print(f"已创建 {created.reference_field.text.value}: {created.id}")
64
+ if args.save:
65
+ board.save()
66
+ print("已通过 IPC 保存 PCB。")
67
+ finally:
68
+ close_kicad(kicad)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """Create one copper zone from a closed polygon via KiCad IPC.
3
+
4
+ Pass --points as x,y pairs in millimetres separated by semicolons. This
5
+ creates the zone only; use refill_zones.py separately after reviewing it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+
12
+ from kipy.board_types import Zone
13
+ from kipy.common_types import PolygonWithHoles
14
+ from kipy.geometry import PolyLine, PolyLineNode
15
+ from kipy.util import from_mm
16
+
17
+ from kipy_common import (
18
+ close_kicad,
19
+ commit_or_drop,
20
+ connect_board,
21
+ get_required_net,
22
+ resolve_copper_layer,
23
+ )
24
+
25
+
26
+ def polygon(value: str) -> PolygonWithHoles:
27
+ points = []
28
+ try:
29
+ for token in value.split(";"):
30
+ x_mm, y_mm = (float(part.strip()) for part in token.split(",", 1))
31
+ points.append((x_mm, y_mm))
32
+ except ValueError as exc:
33
+ raise argparse.ArgumentTypeError("--points 格式应为 x,y;x,y;x,y,单位 mm") from exc
34
+
35
+ if len(points) < 3:
36
+ raise argparse.ArgumentTypeError("铺铜外形至少需要三个不同顶点")
37
+ if points[0] != points[-1]:
38
+ points.append(points[0])
39
+
40
+ outline = PolyLine()
41
+ for x_mm, y_mm in points:
42
+ outline.append(PolyLineNode.from_xy(from_mm(x_mm), from_mm(y_mm)))
43
+ result = PolygonWithHoles()
44
+ result.outline = outline
45
+ return result
46
+
47
+
48
+ def main() -> None:
49
+ parser = argparse.ArgumentParser(description=__doc__)
50
+ parser.add_argument("--net", required=True, help="已存在的 PCB 网络名")
51
+ parser.add_argument("--layer", default="F.Cu", help="目标铜层,默认 F.Cu")
52
+ parser.add_argument("--points", type=polygon, required=True, help="外形顶点 x,y;x,y;...,单位 mm")
53
+ parser.add_argument("--save", action="store_true", help="验证成功后通过 IPC 保存 PCB")
54
+ args = parser.parse_args()
55
+
56
+ kicad, board = connect_board()
57
+ try:
58
+ zone = Zone()
59
+ zone.net = get_required_net(board, args.net)
60
+ zone.layers = [resolve_copper_layer(board, args.layer)]
61
+ zone.outline = args.points
62
+
63
+ def validate(created):
64
+ if len(created) != 1:
65
+ raise RuntimeError("KiCad 未创建恰好一个区域。")
66
+ if created[0].net is None or created[0].net.name != args.net:
67
+ raise RuntimeError("KiCad 返回的区域网络与请求不一致。")
68
+
69
+ created_zone, = commit_or_drop(
70
+ board,
71
+ f"Create {args.net} copper zone",
72
+ lambda: board.create_items(zone),
73
+ validate,
74
+ )
75
+ print(f"已创建未填充的铺铜区域: {created_zone.id}")
76
+ print("审阅外形后,再运行 refill_zones.py 填充区域。")
77
+ if args.save:
78
+ board.save()
79
+ print("已通过 IPC 保存 PCB。")
80
+ finally:
81
+ close_kicad(kicad)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """Create one straight track on an existing KiCad PCB net via IPC."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+
8
+ from kipy.board_types import Track
9
+ from kipy.geometry import Vector2
10
+ from kipy.util import from_mm
11
+
12
+ from kipy_common import (
13
+ close_kicad,
14
+ commit_or_drop,
15
+ connect_board,
16
+ get_required_net,
17
+ resolve_copper_layer,
18
+ )
19
+
20
+
21
+ def point(value: str) -> Vector2:
22
+ try:
23
+ x_mm, y_mm = (float(part.strip()) for part in value.split(",", 1))
24
+ except ValueError as exc:
25
+ raise argparse.ArgumentTypeError("坐标必须是 x,y(单位 mm),例如 10,20") from exc
26
+ return Vector2.from_xy(from_mm(x_mm), from_mm(y_mm))
27
+
28
+
29
+ def main() -> None:
30
+ parser = argparse.ArgumentParser(description=__doc__)
31
+ parser.add_argument("--net", required=True, help="已存在的 PCB 网络名")
32
+ parser.add_argument("--start", type=point, required=True, help="起点 x,y,单位 mm")
33
+ parser.add_argument("--end", type=point, required=True, help="终点 x,y,单位 mm")
34
+ parser.add_argument("--width-mm", type=float, required=True, help="线宽,单位 mm")
35
+ parser.add_argument("--layer", default="F.Cu", help="目标铜层,默认 F.Cu")
36
+ parser.add_argument("--save", action="store_true", help="验证成功后通过 IPC 保存 PCB")
37
+ args = parser.parse_args()
38
+
39
+ if args.width_mm <= 0:
40
+ parser.error("--width-mm 必须大于 0")
41
+
42
+ kicad, board = connect_board()
43
+ try:
44
+ track = Track()
45
+ track.start = args.start
46
+ track.end = args.end
47
+ track.width = from_mm(args.width_mm)
48
+ track.layer = resolve_copper_layer(board, args.layer)
49
+ track.net = get_required_net(board, args.net)
50
+
51
+ def validate(created):
52
+ if len(created) != 1:
53
+ raise RuntimeError("KiCad 未创建恰好一条走线。")
54
+ if created[0].width != track.width or created[0].net.name != args.net:
55
+ raise RuntimeError("KiCad 返回的创建结果与请求不一致。")
56
+
57
+ created_track, = commit_or_drop(
58
+ board,
59
+ f"Create track on {args.net}",
60
+ lambda: board.create_items(track),
61
+ validate,
62
+ )
63
+
64
+ print(f"已创建 track: {created_track.id}")
65
+ if args.save:
66
+ board.save()
67
+ print("已通过 IPC 保存 PCB。")
68
+ finally:
69
+ close_kicad(kicad)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ """Create one through via on an existing KiCad PCB net via IPC."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+
8
+ from kipy.board_types import Via
9
+ from kipy.geometry import Vector2
10
+ from kipy.util import from_mm
11
+
12
+ from kipy_common import close_kicad, commit_or_drop, connect_board, get_required_net
13
+
14
+
15
+ def main() -> None:
16
+ parser = argparse.ArgumentParser(description=__doc__)
17
+ parser.add_argument("--net", required=True, help="已存在的 PCB 网络名")
18
+ parser.add_argument("--x-mm", type=float, required=True, help="X 坐标,单位 mm")
19
+ parser.add_argument("--y-mm", type=float, required=True, help="Y 坐标,单位 mm")
20
+ parser.add_argument("--diameter-mm", type=float, required=True, help="外径,单位 mm")
21
+ parser.add_argument("--drill-mm", type=float, required=True, help="钻孔直径,单位 mm")
22
+ parser.add_argument("--save", action="store_true", help="验证成功后通过 IPC 保存 PCB")
23
+ args = parser.parse_args()
24
+
25
+ if args.drill_mm <= 0 or args.diameter_mm <= args.drill_mm:
26
+ parser.error("必须满足 0 < --drill-mm < --diameter-mm")
27
+
28
+ kicad, board = connect_board()
29
+ try:
30
+ via = Via()
31
+ via.position = Vector2.from_xy(from_mm(args.x_mm), from_mm(args.y_mm))
32
+ via.diameter = from_mm(args.diameter_mm)
33
+ via.drill_diameter = from_mm(args.drill_mm)
34
+ via.net = get_required_net(board, args.net)
35
+
36
+ def validate(created):
37
+ if len(created) != 1:
38
+ raise RuntimeError("KiCad 未创建恰好一个过孔。")
39
+ if created[0].net.name != args.net:
40
+ raise RuntimeError("KiCad 返回的创建结果与请求不一致。")
41
+
42
+ created_via, = commit_or_drop(
43
+ board,
44
+ f"Create via on {args.net}",
45
+ lambda: board.create_items(via),
46
+ validate,
47
+ )
48
+
49
+ print(f"已创建 via: {created_via.id}")
50
+ if args.save:
51
+ board.save()
52
+ print("已通过 IPC 保存 PCB。")
53
+ finally:
54
+ close_kicad(kicad)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only diagnostic for a KiCad IPC connection.
3
+
4
+ Use before a write script. It does not edit or save the current PCB.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+
12
+ def main() -> int:
13
+ try:
14
+ from kipy import KiCad
15
+ except ModuleNotFoundError:
16
+ print("未找到 kicad-python/kipy。请使用 KiCad IPC 插件环境或安装匹配的官方包。")
17
+ return 2
18
+
19
+ kicad = None
20
+ try:
21
+ kicad = KiCad(timeout_ms=5000)
22
+ if not kicad.check_version():
23
+ print("已连接,但 kicad-python 与 KiCad API 版本不匹配。")
24
+ return 3
25
+
26
+ board = kicad.get_board()
27
+ if board is None:
28
+ print("已连接 KiCad API,但 PCB Editor 中没有打开 .kicad_pcb。")
29
+ return 4
30
+
31
+ print(f"已连接 KiCad {kicad.get_version()}。")
32
+ print(f"当前 PCB: {board.name}")
33
+ return 0
34
+ except Exception as exc:
35
+ print(f"无法连接 KiCad IPC API: {exc}")
36
+ print("请确认:")
37
+ print(" 1. 已打开 PCB Editor(仅打开项目管理器还不够)。")
38
+ print(" 2. 在 KiCad 的 偏好设置 → 插件 中启用了 API 服务,并已重启 PCB Editor。")
39
+ print(" 3. 若在 DSH 中执行,当前任务拥有 Full Access;非 Full Access 无法访问 KiCad 命名管道。")
40
+ return 1
41
+ finally:
42
+ if kicad is not None:
43
+ close = getattr(kicad, "close", None)
44
+ if callable(close):
45
+ close()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ sys.exit(main())