Replaces the bpj2ini.py prototype with a packaged tool. The A2F codec is now reimplemented in Python, so neither Boxsim.exe nor a CPU emulator is needed to decode a container; the emulator survives as a development-only oracle that the codec is differentially tested against. Verified: - 134 containers decode byte-identically to Boxsim's own decoder, 270 pre-2.0 files rejected by both implementations (tools/difftest.py --self-test) - Default.BPJ: 22 components, 14 nets, no dangling terminal - 22 of 22 parts resolve to library symbols; one series inferred from a sibling, two DC resistance deviations reported - ERC clean and the netlist KiCad extracts from the generated sheet is an identical partition to the one derived from the Boxsim grid - container build byte-identical to the host build - 32 tests passing, ruff clean Decode time went 69 s -> 14.5 s (binding emulator hooks to trap addresses) -> 1.6 s (pure Python). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
"""Write a complete KiCad project directory.
|
|
|
|
Emits the schematic, a `.kicad_pro`, and project-local library tables pointing
|
|
at the Visaton library so the project opens with every symbol and footprint
|
|
resolved. No board is generated: KiCad's own *Update PCB from Schematic* does
|
|
that, and every Visaton symbol already carries a `Footprint` property.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from . import symbols as symbols_mod
|
|
from .catalog import Catalog
|
|
from .model import Project
|
|
from .schematic import DEFAULT_PITCH_MM, SchematicBuilder, Sheet
|
|
from .symbols import Resolution, Resolver, SymbolLibrary
|
|
|
|
LIB_NICKNAME = "Visaton"
|
|
|
|
|
|
@dataclass
|
|
class BuildResult:
|
|
directory: Path
|
|
schematic: Path
|
|
project_file: Path
|
|
sheet: Sheet
|
|
resolutions: list[Resolution]
|
|
|
|
@property
|
|
def notes(self) -> list[str]:
|
|
out = []
|
|
for r in self.resolutions:
|
|
for note in r.notes:
|
|
out.append(f"Part{r.part.slot}: {note}")
|
|
return out
|
|
|
|
|
|
def _pro_contents(name: str) -> dict:
|
|
"""Minimal but complete project file. KiCad fills in the rest on open."""
|
|
return {
|
|
"board": {"design_settings": {}, "layer_presets": [], "viewports": []},
|
|
"boards": [],
|
|
"cvpcb": {"equivalence_files": []},
|
|
"libraries": {"pinned_footprint_libs": [], "pinned_symbol_libs": []},
|
|
"meta": {"filename": f"{name}.kicad_pro", "version": 3},
|
|
"net_settings": {
|
|
"classes": [
|
|
{
|
|
"bus_width": 12,
|
|
"clearance": 0.2,
|
|
"diff_pair_gap": 0.25,
|
|
"diff_pair_width": 0.2,
|
|
"line_style": 0,
|
|
"microvia_diameter": 0.3,
|
|
"microvia_drill": 0.1,
|
|
"name": "Default",
|
|
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
|
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
|
"track_width": 0.8,
|
|
"via_diameter": 1.2,
|
|
"via_drill": 0.6,
|
|
"wire_width": 6,
|
|
}
|
|
],
|
|
"meta": {"version": 4},
|
|
},
|
|
"pcbnew": {"page_layout_descr_file": ""},
|
|
"schematic": {
|
|
"legacy_lib_dir": "",
|
|
"legacy_lib_list": [],
|
|
"meta": {"version": 1},
|
|
"page_layout_descr_file": "",
|
|
},
|
|
"sheets": [],
|
|
"text_variables": {},
|
|
}
|
|
|
|
|
|
def _lib_table(kind: str, entries: list[tuple[str, str]]) -> str:
|
|
"""Render a sym-lib-table or fp-lib-table."""
|
|
lines = [f"({kind}", "\t(version 7)"]
|
|
for nickname, uri in entries:
|
|
lines.append(
|
|
f'\t(lib (name "{nickname}")(type "KiCad")(uri "{uri}")'
|
|
f'(options "")(descr "Visaton crossover parts"))'
|
|
)
|
|
lines.append(")")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def resolve_library_root(library: SymbolLibrary) -> Path:
|
|
"""The checkout root, derived from the symbol file location."""
|
|
path = library.path.resolve()
|
|
if path.parent.name == "symbols":
|
|
return path.parent.parent
|
|
return path.parent
|
|
|
|
|
|
def build(
|
|
project: Project,
|
|
raw_name: str,
|
|
outdir: Path,
|
|
catalog: Catalog,
|
|
library: SymbolLibrary,
|
|
*,
|
|
pitch: float = DEFAULT_PITCH_MM,
|
|
crossover_index: int = 0,
|
|
) -> BuildResult:
|
|
if not project.crossovers:
|
|
raise ValueError("project contains no crossover")
|
|
if project.amp_count > 1:
|
|
raise NotImplementedError(
|
|
f"project has {project.amp_count} amplifier channels; "
|
|
"this version handles single-channel projects only"
|
|
)
|
|
|
|
crossover = project.crossovers[crossover_index]
|
|
resolver = Resolver(catalog, library)
|
|
resolutions = resolver.resolve_all(crossover.components)
|
|
|
|
builder = SchematicBuilder(library, pitch=pitch)
|
|
sheet = builder.build(project, crossover, resolutions, title=raw_name)
|
|
text = builder.render(sheet, raw_name)
|
|
|
|
outdir.mkdir(parents=True, exist_ok=True)
|
|
sch_path = outdir / f"{raw_name}.kicad_sch"
|
|
sch_path.write_text(text, encoding="utf-8")
|
|
|
|
pro_path = outdir / f"{raw_name}.kicad_pro"
|
|
pro_path.write_text(json.dumps(_pro_contents(raw_name), indent=2) + "\n", encoding="utf-8")
|
|
|
|
root = resolve_library_root(library)
|
|
(outdir / "sym-lib-table").write_text(
|
|
_lib_table("sym_lib_table", [(LIB_NICKNAME, str(root / "symbols" / "Visaton.kicad_sym"))]),
|
|
encoding="utf-8",
|
|
)
|
|
(outdir / "fp-lib-table").write_text(
|
|
_lib_table("fp_lib_table", [(LIB_NICKNAME, str(root / "footprints" / "Visaton.pretty"))]),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
return BuildResult(
|
|
directory=outdir,
|
|
schematic=sch_path,
|
|
project_file=pro_path,
|
|
sheet=sheet,
|
|
resolutions=resolutions,
|
|
)
|
|
|
|
|
|
def load_parts(
|
|
catalog_path: str | None = None, library_path: str | None = None
|
|
) -> tuple[Catalog, SymbolLibrary]:
|
|
from . import catalog as catalog_mod
|
|
|
|
cat = catalog_mod.load(catalog_path)
|
|
lib = symbols_mod.load(library_path)
|
|
return cat, lib
|