Commit Graph

9 Commits

Author SHA1 Message Date
e61404478e tools/epro2/kicad: Phase-1 .kicad_pcb exporter — 6/6 boards open in KiCad 8
Phase-1 scope: produce a .kicad_pcb that kicad-cli loads cleanly and
that has the right geometry (nets, footprints, tracks, vias, board
outline) — not a 1:1 EDA round-trip. Skipped on purpose for Phase 2:
copper pours (POUR/POURED), manual FILL, teardrops, board-level
strings/images, ARC circle-center recovery.

What lands:
  - pcb_writer.write_pcb(): header/general, data-driven layer table
    (F.Cu = ord 0; B.Cu = ord 31; SIGNAL inner ids 15+ allocated to
    In1.Cu/In2.Cu/... in EPRO2-id sorted order so used inner layers
    stay contiguous), net-name → integer id map (id 0 reserved for the
    empty net per KiCad convention), LINE→segment / LINE→gr_line on
    Edge.Cuts, layer-11 POLY paths walked into Edge.Cuts gr_line chains
    (the actual board outline lives on POLY here, not LINE — without
    this stats showed edge=0), VIA→via.
  - footprint_writer.write_footprint_placement(): inline (footprint ...)
    blocks per PCB COMPONENT. EPRO2 RECT/ELLIPSE/OVAL/POLYGON pad
    shapes mapped to KiCad rect/circle/oval/custom; SMD vs THT detected
    by PAD.hole presence; SLOT holes use (drill oval w h). Pad nets
    resolved cross-doc via the existing PCB.PAD_NET → footprint.pad
    chain in ProjectRelations. layerId=2 component → (layer B.Cu) +
    text on B.SilkS so bottom-side parts render correctly.

Smoke test on ESP-VoCat (6 PCBs): all 6 pass `kicad-cli pcb export svg`
and render. DRC on smallest (MicBoard) reports 145 violations + 75
unconnected — most of the unconnected are GND nets that the EPRO2
source resolves through POUR copper, which Phase 2 will export.

CLI: `python -m tools.epro2.kicad <project> --all-pcb --out <dir>`
emits one .kicad_pcb per PCB doc.

52 → 65 unit tests pass. Float comparisons in tests use math.isclose
because the s-expr 6-decimal trim doesn't preserve strict equality
through `value * MIL_TO_MM` round-trips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:18:32 +08:00
ff5553fb06 tools/epro2/kicad: hierarchical export + global_label + 5-Voltage power ports
Three coupled changes so kicad-cli sch erc runs at the project level
(across all sheets of one schematic) instead of single-sheet:

1. (label) → (global_label (shape passive)). EPRO2 nets are
   project-global by construction (named rails span every page in the
   SCH and physically wire across PCBs); KiCad's local label is sheet-
   scoped and triggers `label_dangling` for any name not duplicated on
   the same page.

2. New root_sch_writer that groups SCH_PAGE docs by their parent SCH
   (META.schematic), emits one root .kicad_sch per group with one
   (sheet ...) entry per child, and threads the root-assigned uuid back
   into each child's (sheet_instances) so KiCad can bind them.
   --all-sch now defaults to this; --flat falls back to one-file-per-page.

3. EPRO2's "5-Voltage" placeholder COMPONENT (partId
   pid8a0e77bacb214e, 365 instances on ESP-VoCat) is the editor's power
   port. The rail name lives in the placement's `Global Net Name` ATTR,
   not in the PART. We now emit a (global_label "<rail>") at the
   placement coords whenever that attr is set (101/365 of them on
   ESP-VoCat — the rest are unconfigured drafts).

ESP-VoCat 5 hierarchical roots: 2325 → 2265 violations. Modest because
5 of 6 SCHs are single-page (no cross-sheet nets to resolve), and the
one 4-page schematic (CoreBoard) shares only a handful of names across
sheets — most net names are de-facto sheet-local. The remaining ~190
pin_not_connected are dominated by 0402-style passives whose pin tip
lies on a wire's interior, not at an endpoint; KiCad needs an explicit
(junction) at those points and we don't yet emit one. Marked as the
next follow-up in log.md.

47 → 52 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:05:47 +08:00
54f0173947 tools/epro2/kicad: fix two structural ERC bugs — wire_dangling -88%, pin_not_connected -52%
Bisect found two semantics mismatches between EPRO2 and KiCad that cause
the 850 real-connectivity ERC violations on the ESP-VoCat ref project:

1. sym_writer was emitting lib coords without negating Y, but KiCad lib
   uses Y-up and re-flips Y on placement (Y-down schematic). So vertically
   arranged pins ended up at Y-mirrored absolute positions and wires that
   reach the geometric pin tip in EPRO2 missed the rendered pin tip in
   KiCad. Fix: lib_y = -epro2_y, lib_rot = (360 - rot) % 360 for pin/text.

2. sch_writer was treating each LINE as an isolated wire — but EPRO2
   binds segments into nets by NAME (WIRE.NET attr), not just geometry.
   Multi-segment nets like GND/VBUS show up as N disconnected stubs to
   KiCad. Fix: per-LINE, look up lineGroup → WIRE → NET attr and emit a
   `(label "<NET>")` at the LINE's start. Same-named labels on distinct
   physical wires is how KiCad's ERC recognizes a multi-segment net.

ESP-VoCat 9 sheets:
  wire_dangling           444 →  52  (-88%)
  pin_not_connected       406 → 196  (-52%)
  real connectivity total 850 → 248  (-71%)

Why we did NOT round to grid (the obvious-looking fix): EPRO2 places
some pins on a 10-mil pitch (e.g. magnetic socket); rounding to KiCad's
default 50-mil ERC grid would collapse those pins. The 248 residual is
fundamentally cross-sheet — single-sheet ERC can't see a net's other
endpoints on sibling sheets — and is a Phase-3 (hierarchical sheet)
problem, not a per-sheet one.

41 → 46 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:43:11 +08:00
fb577cc89f tools/epro2/kicad: fix two KiCad 8 parse blockers (newline + pin_numbers)
装 kicad 8.0.9 (apt PPA) 后跑 kicad-cli sch erc 校验我们 emit 的
.kicad_sch 文件,发现 9/9 sheets 一开始全部报 "Failed to load schematic
file" — 父节点解析就挂掉。Bisect 找到两个语法 bug:

1. **(pin_numbers (hide no)) 不被 KiCad 8 接受**
   KiCad 8 lib_symbols 里 `pin_numbers` 是 token-form,不接受 (hide
   yes/no) 子块。要么省略整个 block 默认 visible,要么 `(pin_numbers
   hide)` 表示隐藏。原来的 `(hide no)` 风格是 KiCad 7 旧语法。

   Fix: tools/epro2/kicad/sym_writer.py 删掉 (pin_numbers (hide no))
        行;KiCad 默认 visible 行为正是我们想要的。

2. **String 里的字面 \n / \r / \t 让 KiCad 解析器中止**
   ESP-VoCat 的 Overview sheet 有 TEXT "Battary\n3.7V 700mAH"(多行
   电池标签),EPRO2 里以**字面 0x0a 字符**存储。我们把它原样 emit
   成 "..." 包住的字符串 → KiCad reader 在 quoted string 内遇到 \n
   就报 parse error 不给 message。

   Fix: tools/epro2/kicad/sexpr.py 在 str escape 路径加 \n / \r / \t
        转义;reader 加 \r 解码(roundtrip 用)。

修完后:

  9/9 sheets parse OK in KiCad 8.0.9
  ERC 跑通,9 个 sheet 共 2793 violations,分布:
     1372 endpoint_off_grid        (49%, cosmetic — 30-mil EPRO2 grid 不
                                    snap KiCad 默认 50-mil grid)
      571 lib_symbol_issues        (20%, cosmetic — facere 库未注册到
                                    user library table;库已 embed 在
                                    .kicad_sch 内联可用)
      444 wire_dangling            (16%, real — wire 端点没精确对齐 pin)
      406 pin_not_connected        (15%, 同上的另一面)

  Cosmetic 占 70%,real connectivity 30%,下个 phase 处理:
    - grid 校准(把 coord 精确 round 到统一 grid 上)
    - pin tip 端点匹配(KiCad 需要 wire 端点 == pin (at) 字段对应的
      绝对坐标,浮点必须精确相等)
    - 生成 sym-lib-table 注册 facere 库(消 lib_symbol_issues)

测试:
  + test_string_escapes_newlines_and_tabs
  + test_lib_symbol_omits_pin_numbers_block
  reader 加 \r 解码

41/41 通过(39 旧 + 2 新)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:04:58 +08:00
8a91ce43f4 tools/epro2/kicad: Phase-2 lib_symbols — render symbol bodies from SYMBOL docs
Phase 1 emit的 .kicad_sch 里组件位置 + 属性都对,但 lib_symbols 是空
stub —— KiCad 渲染时每个组件显示成红色 "?"。Phase 2 把 SYMBOL 文档
里的 PART + RECT/POLY/CIRCLE/TEXT/PIN primitives 翻成 KiCad lib symbol
块,填到 lib_symbols 里,让 KiCad 显示真正的原理图符号。

新增 tools/epro2/kicad/sym_writer.py:
  write_lib_symbol(symbol_doc) → S-expr list 形如:
    (symbol "facere:<partId>"
      (pin_numbers (hide no))
      (pin_names (offset 1.016))
      (in_bom yes) (on_board yes)
      (property "Reference" "U" ...)
      (property "Value" "<title>" ...)
      (property "Footprint" "" hide)
      (property "Datasheet" "" hide)
      (symbol "<partId>_1_1"
        (rectangle ...)        ← from RECT.dotX1/Y1/dotX2/Y2
        (polyline (pts ...))   ← from POLY.points + closed → fill
        (circle ...)            ← from CIRCLE.center/radius
        (text "..." ...)        ← from TEXT.value/x/y/rotation
        (pin <type> line (at ...) (length ...) (name ...) (number ...))
                                ← from PIN + sibling ATTR ops
      ))

PIN 名字/编号/电气类型解析(这是关键数据探测点):
  EPRO2 PIN 不直接带 number/name/type 字段;这些信息存为独立 ATTR 操作
  (parentId=<pin_id>, key="Pin Name"/"Pin Number"/"Pin Type")
  Pin Type 取值映射:IN→input, OUT→output, BIDIR→bidirectional,
  POWER_IN→power_in, POWER_OUT→power_out, NC→no_connect, ...
  默认 passive(保守)

sch_writer 集成(lib_symbols 自动填):
  write_sch_page(doc, project_relations=pr) — 增 pr 可选参数
  内部 _build_lib_symbols(): 收集本 sheet 用到的 partIds → 通过
  ProjectRelations.parts_by_id 解析到 SYMBOL 文档 → write_lib_symbol →
  组装 (lib_symbols ...) 块;同 partId 多 SYMBOL 候选取第一个,去重
  WriteStats 增 lib_symbols_embedded / lib_symbols_missing

CLI 加 --no-lib-symbols 用于回到 Phase-1 行为(占位符调试用)。

ESP-VoCat 重导出验证:9/9 SCH_PAGE 全部 0 lib_miss
  P1_45092758.kicad_sch    wires=187 symbols=138 lib_emb=29
  codec_0b0163fa.kicad_sch wires=190 symbols=112 lib_emb=20
  Interface_b336a7c7.kicad_sch        symbols=95  lib_emb=13
  ...
  P1_408c9f4f.kicad_sch    wires=  6 symbols= 10 lib_emb= 3

测试:6 个新单测覆盖 outer wrapper / pin ATTR pull / 多形状 primitives /
sch_writer 集成路径 / 缺失 lib 计数 / no-pr 回退到 Phase 1。
合计 **39/39 通过**(parser 6 + relations 9 + project_relations 6 +
sexpr 6 + sch_writer 6 + sym_writer 6)。

下一步 Phase 3:footprint library + .kicad_pcb 导出。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:45:19 +08:00
9213429a57 tools/epro2/kicad: Phase-1 EPRO2 → KiCad schematic exporter
写第一版 EPRO2 → .kicad_sch 转换:把 SCH_PAGE Document 的 wires +
COMPONENT placements + TEXT 输出到一个可被 KiCad 7+ 打开的 sch 文件。
不含 symbol 主体(lib_symbols 留空 stub),所以 KiCad 里组件会渲染
成红色 "?" 占位,但布线 + 位置 + Designator/Value 属性都正确。完整
symbol 库导出留 Phase 2。

模块结构:
  tools/epro2/kicad/sexpr.py        手写 S-expr emitter,Sym 标记裸符号,
                                    str 自动加引号 + 转义;float 去尾零;
                                    bool→yes/no;NaN/Inf 主动报错
  tools/epro2/kicad/_sexpr_reader.py  极简 S-expr parser,仅给 round-trip
                                    测试用(非完整 KiCad reader)
  tools/epro2/kicad/sch_writer.py   write_sch_page(doc) → str;处理:
                                      LINE  → (wire (pts ...) ...)
                                      COMPONENT → (symbol (lib_id facere:<partId>)
                                                  (at x y rot) (property Reference ...) ...)
                                      TEXT  → (text "..." (at ...))
                                    单位 mil → mm × 0.0254;零长 wire 跳过
  tools/epro2/kicad/__main__.py     CLI: --doc <uuid> | --all-sch

ESP-VoCat 验证(python -m tools.epro2.kicad <project> --all-sch):
  9 SCH_PAGE 全部转换成功
  P1_408c9f4f.kicad_sch    wires=  6  symbols= 10  text=  0  skipped= 2  (370 lines)
  P1_ee409917.kicad_sch    wires= 20  symbols= 14  text=  0  skipped= 3
  P1_54743d77.kicad_sch    wires= 42  symbols= 30  text=  3
  Overview_dc13d6d2.kicad_sch wires=  0  symbols=  1  text= 34   (说明页)
  MCU_510cff33.kicad_sch   wires= 91  symbols= 86  text=  9
  Interface_b336a7c7.kicad_sch wires= 99  symbols= 95  text=  6
  P1_5c38f45b.kicad_sch    wires=179  symbols= 86  text=  9
  P1_45092758.kicad_sch    wires=187  symbols=138  text= 10  (主图)
  codec_0b0163fa.kicad_sch wires=190  symbols=112  text= 10

输出落在 data/processed/kicad_sch/<filename>.kicad_sch(gitignore 内,
可重新生成;不入库)。

测试:6 个 sexpr 测 + 6 个 sch_writer 测,含 round-trip parse 验证。
parser/relations/project_relations 的旧 21 个不动,合计 **33/33 通过**。

下一步:
1. Phase 2 — symbol library 导出 (.kicad_sym),把 SYMBOL doc 的 PIN/RECT/
   TEXT primitives 转 KiCad symbol 主体;填 lib_symbols 块让组件渲染
   出真正的 schematic 符号
2. footprint library + .kicad_pcb 导出
3. 用 KiCad CLI (kicad-cli sch erc) 跑 ERC 校验

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:29:15 +08:00
3052e42991 tools/epro2: add ProjectRelations for cross-document resolution
per-doc Relations 在大量 cross-doc 引用前是不够的:PCB 的 PAD_NET 复合
id [PAD_NET, comp, pin, pad] 里的 pad 实际是 FOOTPRINT 文档里的 pad
实例;SCH_PAGE 的 COMPONENT.partId 指向某个 SYMBOL 文档的 PART.id。

ProjectRelations 在 per-doc Relations 之上做项目级聚合,把这些跨文档
引用拼起来。

Probe 阶段(ESP-VoCat)发现的映射规则(已写入 docstring):

1. SCH_PAGE COMPONENT.partId  ===  PART.id in some SYMBOL doc
   - 命名两种风格:'pid<hex>' (anonymous/系统 part) + '<name>.<n>' (具
     名 SKU),但都直接相等 PART.id,**不**是不同 namespace
   - 同一 PART.id 可能出现在多个 SYMBOL 文档里(库快照),
     parts_by_id 保留全部,consumer 通常取第一个

2. PCB COMPONENT.id  →  FOOTPRINT 文档 UUID  via 单独 ATTR op:
       ATTR(parentId=<comp>, key="Footprint", value=<fp_doc_uuid>)
   COMPONENT.attrs 子 dict 只有内务字段(Unique ID / Channel ID / ...),
   **不**含 footprint 引用。这跟 schematic 的 partId 在 COMPONENT 上的
   做法不一样,是 EPRO2 流的一处不对称

3. PCB PAD_NET[comp,pin,pad] 里的 pad 是 FOOTPRINT 文档内部的 pad id;
   解析链: comp → ATTR Footprint → FOOTPRINT relations.pads[pad]

API:
  ProjectRelations.build(project) — 单遍构建
  resolve_symbol_docs(sch_uuid, comp_id) → [SYMBOL doc uuids]
  resolve_footprint_doc(pcb_uuid, comp_id) → FOOTPRINT doc uuid | None
  pad_in_footprint(fp_uuid, pad_id) → PAD payload | None
  resolve_pcb_pad_net(pcb_uuid, comp, pin, pad) → {footprint, pad} | None
  attrs_for_pcb_component(pcb_uuid, comp_id) → {key: value} 折叠

CLI 加 --project-relations,跑 ESP-VoCat:
  documents                                 278
  distinct_parts                             87
  duplicated_parts                            9
  pcb_components_with_footprint             206
  pcb_components_unresolved_footprint         0
  sch_components_with_partid                572
  sch_components_unresolved_part              0

PCB 样本验证:comp=e0 → fp=1069352d81c6 Designator='U8',
PAD_NET pin=1 pad=e7 net=GND 跨文档解到坐标 (-37.4,-45.24)。

测试:6 个新单测覆盖 partId→symbol、comp→footprint、PAD_NET 跨文档、
attrs 折叠、unresolved 计数。parser + relations + project_relations
共 21/21 通过。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:22:39 +08:00
7f9e2fad73 tools/epro2: add Relations layer for cross-object navigation
在 replay 的扁平 objects[id] -> payload 之上盖一层 Relations,建索引和
反向引用,把孤立对象拼成可遍历的图,是后续 EPRO2 → KiCad 转换器的
中间表示前置。

Relations.build(doc) 单遍扫所有对象,得到:

主集合(按类型分桶):
  parts / components / pins / pads / wires / nets / layers / rules

复合 ID 解析(关键):
  '["LAYER",1]'                          → layers[1]
  '["NET","GND"]'                        → nets["GND"]
  '["PAD_NET","e0","1","e7"]'            → pad_nets_by_pad/by_net
  '["RULE","SAFE","copperThickness1oz"]' → rules[("RULE","SAFE",...)]

反向引用:
  obj_ids_by_part         partId            → 引用对象 ids(lib 内 RECT/TEXT/PIN 都带 partId)
  components_by_part      partId            → component ids
  attrs_by_parent         parentId          → ATTR ids
  lines_by_wire           WIRE.id           → LINE ids(wire 由若干 LINE 段组成)
  pad_nets_by_pad         PAD.id            → PAD_NET 记录
  pad_nets_by_net         net name          → PAD_NET 记录
  objects_on_layer / objects_in_net  字段反查

便捷 accessor:
  attrs_dict(parent_id)   折叠所有 ATTR ops 到 {key: value} dict(last
                          write wins),KiCad 转换时按 component 拿
                          Designator/Value/Footprint 的常用入口

ATTR.parentId 解析(实测发现的两种坑):
1. 不仅指向 COMPONENT/PART —— 也大量指向 WIRE(schematic 上的网络
   标签 / 网络属性)。原查重函数漏算,636 个 false positive
   unresolved;改为"任意 doc.objects[parentId] 命中即算 resolved"
2. 复合形式 `<comp_id>-<pin_id>` 用于把 ATTR 挂在某 component 的某个
   pin 上(如 PinName)。`_resolve_parent()` 用 split("-",1) 兜底

CLI 加 --relations,按 docType 聚合 stats:
  uv run python -m tools.epro2 data/raw/oshwhub/<uuid> --relations

ESP-VoCat 验证:
  SCH_PAGE 9 docs : 572 components, 563 wires, 934 lines_grouped,
                    4111 attrs_attached, 0 unresolved_parents
  PCB      6 docs : 206 components, 807 pad_nets, 173 nets, 544 layers
  SYMBOL 105 docs : 106 parts, 560 pins, 1680 attrs_attached
  FOOTPRINT 55 docs: 496 pads, 9 nets, 1771 layers, 140 rules

注:PCB 内 pads=6 vs pad_nets=807 不矛盾 —— PAD 实例存在 FOOTPRINT
文档里,PCB stream 用 ["PAD_NET",comp,pin,pad] 复合 id 跨文档引用;
解析"comp 的某 pin 通过哪个 footprint 的哪个 pad"需要 project-级
Relations 聚合(下个 task)。

测试:tools/epro2/tests/test_relations.py 9 个单测覆盖复合 id 解析、
lineGroup 链接、parentId 直/复合解析、partId 反查、attrs 折叠。
parser + relations 共 15/15 通过。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:17:28 +08:00
3c57e75d51 Add tools/epro2 — EPRO2 parser + replay prototype
为 Pro 3.x .epro2 工程源数据写解析骨架,下游做 EPRO2→KiCad 转换器
前的基础设施。在 ESP-VoCat (278 docs / 7.5 MB) + 220V 桌面电源
(771 docs / 26 MB) 端到端跑通,0 parse errors。

模块结构:
  tools/epro2/parser.py    单行 → Op:rstrip("|") + split("||") + json.loads
  tools/epro2/replay.py    state-machine:DOCHEAD 设头;其它 op 按 id 做
                           upsert(payload=None 当 delete);EDIT_HEAD/
                           META/CANVAS/PREFERENCE/PANELIZE 当 doc 级单
                           例存
  tools/epro2/__main__.py  CLI:传项目目录走 manifest.json 重放每个 doc,
                           按 docType 聚合输出 + 可选 --dump-doc 看单文
                           档详情
  tools/epro2/tests/       6 个单测 pin 死 trailing-pipe / 三段消息 /
                           id-only-no-payload / 嵌入管道符等坑

ESP-VoCat 输出示例:
  Documents: 278  (parse_errors=0)
   count  docType         objects        ops  deletes  untyped_ops
     105  SYMBOL             4124       4439        0            0
      88  DEVICE               88        264        0            0
      55  FOOTPRINT          4641       4855        0            0
       9  SCH_PAGE           7982       8167       42            0
       6  PCB                8428       8547       38            0
       6  BOARD                 9         18        0            0
       6  SCH                   9         26        0            0
       1  BLOB                  4          8        0            0
       1  FONT                 16         28        0            0
       1  CONFIG                2          3        0            0
  Top ops: ATTR 7035 / ELE_PLACEHOLDER 4225 / LINE 3005 / LAYER 2318 ...

PCB 文档单 dump 验证语义正确:META 含 title (PCB-EchoEar-CoreBoard-V1_0)
+ board 引用;CANVAS 含 origin/grid/unit (mm);LAYER 1/2/3 = TOP/BOTTOM/
TOP_SILK 配色齐全。

跑法:
  uv run python -m tools.epro2 data/raw/oshwhub/<project_uuid>
  uv run python -m tools.epro2 data/raw/oshwhub/<uuid> --dump-doc <doc_uuid>

下一步(不在本 commit):
1. 把对象间关系建起来(COMPONENT.partId → PART;LINE.lineGroup → WIRE;
   PAD_NET id → PAD + NET 三方关联)—— 当前 replay 只做扁平 dict
2. EPRO2 → KiCad 序列化层(Forge 投影硬门槛)
3. 在 Pro 3.x 三个项目做整体回归(X86 主板 7374 docs 可作压力测试)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:10:27 +08:00