Add boxsim CLI: pure Python codec, netlist extraction, KiCad project generation
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>
This commit is contained in:
1 parent
eb81ca61cb
commit
4737a5bae8
28 files changed
+4459
-122
No files matched your search
@@ -0,0 +1,161 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user