tools/epro2: add std/ writer — EPRO2 → EasyEDA Std-format JSON for downstream
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>
This commit is contained in:
106
tools/epro2/std/__main__.py
Normal file
106
tools/epro2/std/__main__.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user