app
Adom Desktop - KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
"""Handler for BOM (Bill of Materials) generation from schematics."""
import sys
from pathlib import Path
def handle_generate_bom(kicad_info: dict, args: dict) -> dict:
"""Generate a BOM from a schematic file.
Groups components by value + footprint, counts quantities,
and returns a structured BOM.
"""
sys.path.insert(0, str(Path(__file__).parent.parent))
from parsers.schematic import parse_schematic
file_path = args.get("filePath", "")
if not file_path:
return {"success": False, "error": "No filePath specified"}
sch_path = Path(file_path)
if not sch_path.exists():
return {
"success": False,
"error": f"Schematic not found: {file_path}",
"_hint": "The path may exist on Docker but not on the Windows host. Use push_file to copy from Docker first, or verify the host-side path.",
}
try:
data = parse_schematic(sch_path)
except Exception as e:
return {"success": False, "error": f"Failed to parse schematic: {e}"}
# Filter: only in_bom, non-power components
power_uuids = {ps["uuid"] for ps in data["power_symbols"]}
components = [
c for c in data["components"]
if c["in_bom"] and c["uuid"] not in power_uuids and not c["dnp"]
]
# Group by value + footprint
groups = {}
for comp in components:
key = (comp["value"], comp["footprint"])
if key not in groups:
groups[key] = {
"value": comp["value"],
"footprint": comp["footprint"],
"references": [],
"lib_id": comp["lib_id"],
"description": comp["properties"].get("Description", ""),
"manufacturer": comp["properties"].get("Manufacturer", ""),
"mpn": comp["properties"].get("MPN", comp["properties"].get("Manufacturer_Part_Number", "")),
"datasheet": comp["properties"].get("Datasheet", ""),
}
groups[key]["references"].append(comp["reference"])
# Sort references naturally within each group
bom = []
for group in groups.values():
group["references"].sort(key=_natural_sort_key)
group["quantity"] = len(group["references"])
group["reference_list"] = ", ".join(group["references"])
bom.append(group)
# Sort BOM by reference prefix then number
bom.sort(key=lambda g: _natural_sort_key(g["references"][0] if g["references"] else ""))
# DNP items (reported separately)
dnp_items = [
{"reference": c["reference"], "value": c["value"], "footprint": c["footprint"]}
for c in data["components"]
if c["dnp"] and c["uuid"] not in power_uuids
]
total_unique = len(bom)
total_parts = sum(g["quantity"] for g in bom)
return {
"success": True,
"output": f"BOM: {total_unique} unique part(s), {total_parts} total",
"data": {
"bom": bom,
"summary": {
"unique_parts": total_unique,
"total_parts": total_parts,
"dnp_count": len(dnp_items),
},
"dnp_items": dnp_items,
},
}
def _natural_sort_key(s: str):
"""Sort key that handles mixed alpha-numeric strings naturally.
E.g., R1, R2, R10 sort correctly instead of R1, R10, R2.
"""
import re
parts = re.split(r'(\d+)', s)
return [int(p) if p.isdigit() else p.lower() for p in parts]