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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
"""Parser for KiCad schematic files (.kicad_sch).
Extracts components, nets, wires, labels, and hierarchy information
from KiCad schematics using the S-expression parser.
"""
from __future__ import annotations
from pathlib import Path
from .sexpr import parse_file, find_node, find_nodes, find_nodes_recursive, node_value
def parse_schematic(filepath: str | Path) -> dict:
"""Parse a .kicad_sch file and return structured data.
Returns a dict with keys: version, generator, uuid, paper, title_block,
lib_symbols, components, wires, junctions, labels, no_connects,
sheets, sheet_instances.
"""
tree = parse_file(filepath)
return _extract_schematic(tree, filepath)
def _extract_schematic(tree: list, filepath: str | Path) -> dict:
"""Extract all relevant data from a parsed schematic tree."""
return {
"file": str(filepath),
"version": node_value(find_node(tree, "version")),
"generator": node_value(find_node(tree, "generator")),
"uuid": node_value(find_node(tree, "uuid")),
"paper": node_value(find_node(tree, "paper")),
"title_block": _extract_title_block(find_node(tree, "title_block")),
"components": _extract_components(tree),
"wires": _extract_wires(tree),
"labels": _extract_labels(tree),
"global_labels": _extract_global_labels(tree),
"hierarchical_labels": _extract_hierarchical_labels(tree),
"junctions": _extract_junctions(tree),
"no_connects": _extract_no_connects(tree),
"sheets": _extract_sheets(tree),
"power_symbols": _extract_power_symbols(tree),
}
def _extract_title_block(node: list | None) -> dict:
"""Extract title block metadata."""
if not node:
return {}
result = {}
for field in ("title", "date", "rev", "company"):
val = node_value(find_node(node, field))
if val:
result[field] = val
# Comments are (comment N "text")
for child in node:
if isinstance(child, list) and child and child[0] == "comment" and len(child) >= 3:
result[f"comment_{child[1]}"] = child[2]
return result
def _extract_property(node: list) -> dict | None:
"""Extract a property node: (property "Name" "Value" (at x y rot) ...)."""
if not node or node[0] != "property" or len(node) < 3:
return None
name = node[1]
value = node[2]
hidden = False
# Check for (effects ... (hide yes))
effects = find_node(node, "effects")
if effects:
hide = find_node(effects, "hide")
if hide and node_value(hide) == "yes":
hidden = True
return {"name": name, "value": value, "hidden": hidden}
def _extract_position(node: list) -> dict | None:
"""Extract position from an (at x y [rot]) node."""
at_node = find_node(node, "at")
if not at_node or len(at_node) < 3:
return None
try:
result = {"x": float(at_node[1]), "y": float(at_node[2])}
if len(at_node) >= 4:
result["rotation"] = float(at_node[3])
return result
except (ValueError, TypeError):
return None
def _extract_components(tree: list) -> list[dict]:
"""Extract component instances from the schematic.
Components are (symbol ...) nodes at the sheet level that have a
(lib_id "...") child — distinct from symbol definitions in lib_symbols
which don't have lib_id.
"""
components = []
for node in tree:
if not isinstance(node, list) or not node or node[0] != "symbol":
continue
lib_id_node = find_node(node, "lib_id")
if not lib_id_node:
continue # This is a lib_symbols definition, not an instance
comp = {
"lib_id": node_value(lib_id_node),
"uuid": node_value(find_node(node, "uuid")),
"position": _extract_position(node),
"unit": node_value(find_node(node, "unit")),
"dnp": node_value(find_node(node, "dnp")) == "yes",
"exclude_from_sim": node_value(find_node(node, "exclude_from_sim")) == "yes",
"in_bom": node_value(find_node(node, "in_bom")) != "no",
"on_board": node_value(find_node(node, "on_board")) != "no",
}
# Extract properties (Reference, Value, Footprint, etc.)
props = {}
for child in node:
p = _extract_property(child) if isinstance(child, list) else None
if p:
props[p["name"]] = p["value"]
comp["properties"] = props
comp["reference"] = props.get("Reference", "")
comp["value"] = props.get("Value", "")
comp["footprint"] = props.get("Footprint", "")
# Extract instance references (for hierarchical designs)
instances_node = find_node(node, "instances")
if instances_node:
instances = []
for proj in find_nodes(instances_node, "project"):
proj_name = proj[1] if len(proj) > 1 and isinstance(proj[1], str) else ""
for path_node in find_nodes(proj, "path"):
path_str = path_node[1] if len(path_node) > 1 and isinstance(path_node[1], str) else ""
ref_node = find_node(path_node, "reference")
unit_node = find_node(path_node, "unit")
instances.append({
"project": proj_name,
"path": path_str,
"reference": node_value(ref_node) or "",
"unit": node_value(unit_node) or "",
})
comp["instances"] = instances
components.append(comp)
return components
def _extract_wires(tree: list) -> list[dict]:
"""Extract wire segments."""
wires = []
for node in find_nodes(tree, "wire"):
pts_node = find_node(node, "pts")
if not pts_node:
continue
points = []
for xy in find_nodes(pts_node, "xy"):
if len(xy) >= 3:
try:
points.append({"x": float(xy[1]), "y": float(xy[2])})
except (ValueError, TypeError):
pass
wires.append({
"points": points,
"uuid": node_value(find_node(node, "uuid")),
})
return wires
def _extract_labels(tree: list) -> list[dict]:
"""Extract net labels."""
labels = []
for node in find_nodes(tree, "label"):
if len(node) < 2:
continue
labels.append({
"name": node[1] if isinstance(node[1], str) else "",
"position": _extract_position(node),
"uuid": node_value(find_node(node, "uuid")),
})
return labels
def _extract_global_labels(tree: list) -> list[dict]:
"""Extract global labels."""
labels = []
for node in find_nodes(tree, "global_label"):
if len(node) < 2:
continue
labels.append({
"name": node[1] if isinstance(node[1], str) else "",
"shape": node_value(find_node(node, "shape")),
"position": _extract_position(node),
"uuid": node_value(find_node(node, "uuid")),
})
return labels
def _extract_hierarchical_labels(tree: list) -> list[dict]:
"""Extract hierarchical labels."""
labels = []
for node in find_nodes(tree, "hierarchical_label"):
if len(node) < 2:
continue
labels.append({
"name": node[1] if isinstance(node[1], str) else "",
"shape": node_value(find_node(node, "shape")),
"position": _extract_position(node),
"uuid": node_value(find_node(node, "uuid")),
})
return labels
def _extract_junctions(tree: list) -> list[dict]:
"""Extract junction points."""
junctions = []
for node in find_nodes(tree, "junction"):
junctions.append({
"position": _extract_position(node),
"uuid": node_value(find_node(node, "uuid")),
})
return junctions
def _extract_no_connects(tree: list) -> list[dict]:
"""Extract no-connect markers."""
ncs = []
for node in find_nodes(tree, "no_connect"):
ncs.append({
"position": _extract_position(node),
"uuid": node_value(find_node(node, "uuid")),
})
return ncs
def _extract_sheets(tree: list) -> list[dict]:
"""Extract sub-sheet references."""
sheets = []
for node in find_nodes(tree, "sheet"):
sheet = {
"uuid": node_value(find_node(node, "uuid")),
"position": _extract_position(node),
}
# Size
size_node = find_node(node, "size")
if size_node and len(size_node) >= 3:
try:
sheet["width"] = float(size_node[1])
sheet["height"] = float(size_node[2])
except (ValueError, TypeError):
pass
# Properties (sheet name, sheet file)
props = {}
for child in node:
p = _extract_property(child) if isinstance(child, list) else None
if p:
props[p["name"]] = p["value"]
sheet["properties"] = props
sheet["name"] = props.get("Sheetname", props.get("Sheet name", ""))
sheet["file"] = props.get("Sheetfile", props.get("Sheet file", ""))
# Pins on the sheet symbol
pins = []
for pin_node in find_nodes(node, "pin"):
if len(pin_node) >= 2:
pin = {"name": pin_node[1] if isinstance(pin_node[1], str) else ""}
direction = node_value(find_node(pin_node, "direction"))
if direction:
pin["direction"] = direction
pins.append(pin)
sheet["pins"] = pins
sheets.append(sheet)
return sheets
def _extract_power_symbols(tree: list) -> list[dict]:
"""Identify power symbols among the components.
Power symbols have lib_id containing a power flag or their lib_symbols
entry has the (power) tag.
"""
# First collect all lib_symbols that are marked as power
power_lib_ids = set()
lib_symbols = find_node(tree, "lib_symbols")
if lib_symbols:
for sym in find_nodes(lib_symbols, "symbol"):
if len(sym) < 2:
continue
# Check if the symbol has a bare (power) child
for child in sym:
if isinstance(child, list) and child == ["power"]:
power_lib_ids.add(sym[1] if isinstance(sym[1], str) else "")
break
if child == "power":
power_lib_ids.add(sym[1] if isinstance(sym[1], str) else "")
break
# Filter components to only power symbols
power_syms = []
for node in tree:
if not isinstance(node, list) or not node or node[0] != "symbol":
continue
lib_id_node = find_node(node, "lib_id")
if not lib_id_node:
continue
lib_id = node_value(lib_id_node)
if lib_id in power_lib_ids:
props = {}
for child in node:
p = _extract_property(child) if isinstance(child, list) else None
if p:
props[p["name"]] = p["value"]
power_syms.append({
"lib_id": lib_id,
"value": props.get("Value", ""),
"reference": props.get("Reference", ""),
"position": _extract_position(node),
"uuid": node_value(find_node(node, "uuid")),
})
return power_syms
# ── High-level query functions ──────────────────────────────────────
def list_schematic_components(filepath: str | Path) -> list[dict]:
"""List all component instances in a schematic.
Returns a list of dicts with: reference, value, footprint, lib_id,
position, dnp, in_bom, on_board.
"""
data = parse_schematic(filepath)
result = []
for comp in data["components"]:
result.append({
"reference": comp["reference"],
"value": comp["value"],
"footprint": comp["footprint"],
"lib_id": comp["lib_id"],
"position": comp["position"],
"dnp": comp["dnp"],
"in_bom": comp["in_bom"],
"on_board": comp["on_board"],
})
return result
def get_component_details(filepath: str | Path, reference: str) -> dict | None:
"""Get detailed info for a specific component by reference designator."""
data = parse_schematic(filepath)
for comp in data["components"]:
if comp["reference"] == reference:
return comp
# Also check instance references for hierarchical designs
for inst in comp.get("instances", []):
if inst.get("reference") == reference:
result = dict(comp)
result["reference"] = reference
result["instance_path"] = inst.get("path", "")
return result
return None
def list_schematic_nets(filepath: str | Path) -> list[dict]:
"""List all net names found in the schematic via labels.
Returns nets from labels, global_labels, and power symbols.
"""
data = parse_schematic(filepath)
nets = {}
for label in data["labels"]:
name = label["name"]
if name not in nets:
nets[name] = {"name": name, "type": "local", "count": 0}
nets[name]["count"] += 1
for label in data["global_labels"]:
name = label["name"]
if name not in nets:
nets[name] = {"name": name, "type": "global", "count": 0}
nets[name]["count"] += 1
for ps in data["power_symbols"]:
name = ps["value"]
if name not in nets:
nets[name] = {"name": name, "type": "power", "count": 0}
nets[name]["count"] += 1
return list(nets.values())
def get_schematic_stats(filepath: str | Path) -> dict:
"""Get summary statistics for a schematic."""
data = parse_schematic(filepath)
# Count non-power components
components = [c for c in data["components"]
if not any(ps["uuid"] == c["uuid"] for ps in data["power_symbols"])]
# Unique footprints
footprints = set(c["footprint"] for c in components if c["footprint"])
return {
"file": str(filepath),
"title": data["title_block"].get("title", ""),
"component_count": len(components),
"power_symbol_count": len(data["power_symbols"]),
"wire_count": len(data["wires"]),
"junction_count": len(data["junctions"]),
"label_count": len(data["labels"]),
"global_label_count": len(data["global_labels"]),
"hierarchical_label_count": len(data["hierarchical_labels"]),
"no_connect_count": len(data["no_connects"]),
"sheet_count": len(data["sheets"]),
"unique_footprints": len(footprints),
"dnp_count": sum(1 for c in components if c["dnp"]),
}
def get_schematic_hierarchy(filepath: str | Path) -> dict:
"""Get the sheet hierarchy starting from this schematic.
Returns a tree structure of sheets with their sub-sheets.
"""
data = parse_schematic(filepath)
base_dir = Path(filepath).parent
result = {
"file": str(filepath),
"title": data["title_block"].get("title", ""),
"sheets": [],
}
for sheet in data["sheets"]:
sheet_file = sheet.get("file", "")
sub = {
"name": sheet.get("name", ""),
"file": sheet_file,
}
# Recursively parse sub-sheets if the file exists
sub_path = base_dir / sheet_file
if sub_path.exists():
try:
sub_data = parse_schematic(sub_path)
sub["component_count"] = len(sub_data["components"])
sub["sheets"] = [
{"name": s.get("name", ""), "file": s.get("file", "")}
for s in sub_data["sheets"]
]
except Exception:
sub["error"] = "Failed to parse sub-sheet"
result["sheets"].append(sub)
return result