The downstream colleague consumes oshwhub Std (lceda) dict-format JSON,
not KiCad. The EPRO2 decryption part (per-doc plaintext .epro2 streams
in data/raw/<uuid>/source/) is what we already provide; the missing
piece is converting EPRO2 op-streams into the same `dataStr.shape`
tilde-delimited format their parser already speaks.
New tools/epro2/std/ module, peer of tools/epro2/kicad/, kept
deliberately separate so the KiCad path stays untouched:
- pcb_writer.write_pcb_std() — high-fidelity, validated against a Std
PCB sample at data/raw/oshwhub/3e2f893d.../25931ddab8.json. Maps
LINE→TRACK, VIA→VIA, POUR→COPPERAREA (with SVG `M..L..Z` path),
POLY→CIRCLE/SOLIDREGION, COMPONENT+FOOTPRINT→LIB nested with
#@$-separated PADs (placement rotation + translate applied so pad
coords land at PCB-absolute positions). Layer-id mapping (EPRO2 5↔7
flipped vs Std solder/paste, 11→10 outline, 12→11 multi, SIGNAL
inner 15+ → Std 21+) noted inline.
- sch_writer.write_sch_std() — best-effort. Our corpus has zero Std
schematic samples (docType=1) so verb field orders follow the
EasyEDA Std public spec, not direct observation. Emits W (wire),
N (net flag, including the 5-Voltage Global Net Name power-port
pattern), T (text), LIB (placement with #@$-nested PIN/T). If
downstream's parser bails the fix is almost certainly a positional
field tweak, not a re-architecture.
- __main__.py — flat output `<doc_uuid>.json` per doc directly under
--out (mirrors Std's own data layout); --all-pcb / --all-sch / --all.
Smoke test on ESP-VoCat: 6 PCB + 9 SCH = 15 JSON files, libs_unresolved=0
across the board. Compact JSON (separators=(",",":")) matches Std's
single-line format. Numbers use _num() — integers without trailing .0,
floats trimmed.
71 → 82 unit tests pass.
Open questions for downstream: (1) confirm SCH verb field orders, (2)
do they want any of the upstream metadata fields we drop (master,
owner, created_at, etc — those live on the crawler side, not the
schematic itself)?
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""CLI: convert EPRO2 docs to EasyEDA Std-format JSON files.
|
|
|
|
Mirrors the layout of Std project sources: one ``<doc_uuid>.json`` per
|
|
document, flat in ``--out``. Use this for downstream consumers that
|
|
already speak Std (Wokwi-based pipelines, dataStr parsers, etc.) — the
|
|
KiCad writer at ``tools.epro2.kicad`` is the alternate target for
|
|
downstream that wants kicad_sch / kicad_pcb instead.
|
|
|
|
Usage:
|
|
uv run python -m tools.epro2.std <project_dir> --all-pcb --out <dir>
|
|
uv run python -m tools.epro2.std <project_dir> --all-sch --out <dir>
|
|
uv run python -m tools.epro2.std <project_dir> --all --out <dir>
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from ..project_relations import ProjectRelations
|
|
from ..replay import Project, replay_project
|
|
from .pcb_writer import write_pcb_std
|
|
from .sch_writer import write_sch_std
|
|
|
|
|
|
def _convert_pcbs(proj: Project, out_dir: Path, pr: ProjectRelations) -> int:
|
|
pcb_uuids = [u for u, d in proj.documents.items() if d.doc_type == "PCB"]
|
|
if not pcb_uuids:
|
|
return 0
|
|
print(f"PCB: converting {len(pcb_uuids)} doc(s) → {out_dir}")
|
|
for u in pcb_uuids:
|
|
try:
|
|
payload = write_pcb_std(proj.documents[u], project_relations=pr)
|
|
except Exception as e: # noqa: BLE001
|
|
print(f" FAIL {u[:12]}: {e}", file=sys.stderr)
|
|
continue
|
|
# Stamp puuid so downstream can wire docs back to a project
|
|
payload["result"]["puuid"] = proj.project_uuid or ""
|
|
(out_dir / f"{u}.json").write_text(
|
|
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
|
encoding="utf-8",
|
|
)
|
|
s = getattr(write_pcb_std, "last_stats", None)
|
|
if s:
|
|
print(
|
|
f" {u[:12]}.json: tracks={s.tracks} vias={s.vias} "
|
|
f"copperareas={s.copperareas} libs={s.libs} pads={s.pads} "
|
|
f"libs_unresolved={s.libs_unresolved}"
|
|
)
|
|
return len(pcb_uuids)
|
|
|
|
|
|
def _convert_schs(proj: Project, out_dir: Path, pr: ProjectRelations) -> int:
|
|
sch_uuids = [u for u, d in proj.documents.items() if d.doc_type == "SCH_PAGE"]
|
|
if not sch_uuids:
|
|
return 0
|
|
print(f"SCH: converting {len(sch_uuids)} doc(s) → {out_dir}")
|
|
for u in sch_uuids:
|
|
try:
|
|
payload = write_sch_std(proj.documents[u], project_relations=pr)
|
|
except Exception as e: # noqa: BLE001
|
|
print(f" FAIL {u[:12]}: {e}", file=sys.stderr)
|
|
continue
|
|
payload["result"]["puuid"] = proj.project_uuid or ""
|
|
(out_dir / f"{u}.json").write_text(
|
|
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
|
|
encoding="utf-8",
|
|
)
|
|
s = getattr(write_sch_std, "last_stats", None)
|
|
if s:
|
|
print(
|
|
f" {u[:12]}.json: wires={s.wires} libs={s.libs} "
|
|
f"netflags={s.netflags} texts={s.texts} libs_unresolved={s.libs_unresolved}"
|
|
)
|
|
return len(sch_uuids)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(description="EPRO2 → EasyEDA Std JSON exporter")
|
|
ap.add_argument("project_dir", type=Path)
|
|
g = ap.add_mutually_exclusive_group(required=True)
|
|
g.add_argument("--all-pcb", action="store_true", help="convert every PCB doc to Std JSON")
|
|
g.add_argument("--all-sch", action="store_true", help="convert every SCH_PAGE doc to Std JSON")
|
|
g.add_argument("--all", action="store_true", help="convert both PCB and SCH_PAGE docs")
|
|
ap.add_argument("--out", type=Path, default=Path("data/processed/std_json"))
|
|
args = ap.parse_args(argv)
|
|
|
|
proj = replay_project(args.project_dir)
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
pr = ProjectRelations.build(proj)
|
|
|
|
n = 0
|
|
if args.all_pcb or args.all:
|
|
n += _convert_pcbs(proj, args.out, pr)
|
|
if args.all_sch or args.all:
|
|
n += _convert_schs(proj, args.out, pr)
|
|
if n == 0:
|
|
print("nothing to convert (no PCB / SCH_PAGE docs found)", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|