Add comprehensive netlist extraction functionality

- Implement schematic netlist parser with S-expression parsing
- Create netlist tools for extraction and connection analysis
- Add resources for netlist and component connection reporting
- Include documentation with usage guide and troubleshooting
- Register new tools and resources in server configuration

This enables extracting component connections from KiCad schematics
and analyzing connectivity between components.
This commit is contained in:
Lama 2025-03-21 09:31:15 -04:00
parent f8bafe8beb
commit 750dd260c4
7 changed files with 1267 additions and 0 deletions

View File

@ -143,6 +143,7 @@ The KiCad MCP Server provides several key features:
- **Project Management**: List, examine, and open KiCad projects
- **PCB Design Analysis**: Get insights about your PCB designs and schematics
- **Netlist Extraction**: Extract and analyze component connections from schematics
- **BOM Management**: Analyze and export Bills of Materials
- **Design Rule Checking**: Run DRC checks and track your progress over time
- **PCB Visualization**: Generate visual representations of your PCB layouts
@ -155,6 +156,7 @@ Detailed documentation for each feature is available in the `docs/` directory:
- [Project Management](docs/project_guide.md)
- [PCB Design Analysis](docs/analysis_guide.md)
- [Netlist Extraction](docs/netlist_guide.md)
- [Bill of Materials (BOM)](docs/bom_guide.md)
- [Design Rule Checking (DRC)](docs/drc_guide.md)
- [PCB Visualization](docs/thumbnail_guide.md)

183
docs/netlist_guide.md Normal file
View File

@ -0,0 +1,183 @@
# KiCad Netlist Extraction Guide
This guide explains how to use the schematic netlist extraction features in the KiCad MCP Server.
## Overview
The netlist extraction functionality allows you to:
1. Extract comprehensive netlist information from KiCad schematics
2. Analyze component connections and relationships
3. Identify power and signal nets
4. Find specific component connections
5. Visualize the connectivity of your design
## Quick Reference
| Task | Example Prompt |
|------|---------------|
| Extract netlist | `Extract the netlist from my schematic at /path/to/project.kicad_sch` |
| Analyze project netlist | `Analyze the netlist in my KiCad project at /path/to/project.kicad_pro` |
| Check component connections | `Show me the connections for R5 in my schematic at /path/to/project.kicad_sch` |
| View formatted netlist | `Show me the netlist report for /path/to/project.kicad_sch` |
## Using Netlist Features
### Extracting a Netlist
To extract a netlist from a schematic:
```
Extract the netlist from my schematic at /path/to/project.kicad_sch
```
This will:
- Parse the schematic file
- Extract all components and their properties
- Identify connections between components
- Analyze power and signal nets
- Return comprehensive netlist information
### Project-Based Netlist Extraction
To extract a netlist from a KiCad project:
```
Extract the netlist for my KiCad project at /path/to/project.kicad_pro
```
This will find the schematic associated with your project and extract its netlist.
### Analyzing Component Connections
To find all connections for a specific component:
```
Show me the connections for U1 in my schematic at /path/to/project.kicad_sch
```
This will provide:
- Detailed component information
- All pins and their connections
- Components connected to each pin
- Net names for each connection
### Viewing Netlist Reports
For a formatted netlist report:
```
Show me the netlist report for /path/to/project.kicad_sch
```
This will load the `kicad://netlist/project_path` resource, showing:
- Component summary
- Net summary
- Connection details
- Power nets
- Potential issues
## Understanding Netlist Data
### Components
Components in a netlist include:
| Field | Description | Example |
|-------|-------------|---------|
| Reference | Component reference designator | R1, C2, U3 |
| Type (lib_id) | Component type from library | Device:R, Device:C |
| Value | Component value | 10k, 100n, ATmega328P |
| Footprint | PCB footprint | Resistor_SMD:R_0805 |
| Pins | List of pin numbers and names | 1 (VCC), 2 (GND) |
### Nets
Nets in a netlist include:
| Field | Description | Example |
|-------|-------------|---------|
| Name | Net name | VCC, GND, NET1 |
| Pins | List of connected pins | R1.1, C1.1, U1.5 |
| Type | Power or signal | Power, Signal |
## Advanced Usage
### Integration with BOM Analysis
You can combine netlist extraction with BOM analysis:
```
Compare the netlist and BOM for my project at /path/to/project.kicad_pro
```
This helps identify:
- Components in the schematic but missing from the BOM
- Components in the BOM but missing from the schematic
- Value or footprint inconsistencies
### Design Validation
Use netlist extraction for design validation:
```
Check for floating inputs in my schematic at /path/to/project.kicad_sch
```
```
Verify power connections for all ICs in my project at /path/to/project.kicad_pro
```
### Power Analysis
Analyze your design's power distribution:
```
Show me all power nets in my schematic at /path/to/project.kicad_sch
```
```
List all components connected to the VCC net in my project at /path/to/project.kicad_pro
```
## Tips for Better Netlist Analysis
### Schematic Organization
For more meaningful netlist analysis:
1. **Use descriptive net names** instead of auto-generated ones
2. **Add power flags** to explicitly mark power inputs
3. **Organize hierarchical sheets** by function
4. **Use global labels** consistently for important signals
5. **Add metadata as properties** to components for better analysis
### Working with Complex Designs
For large schematics:
1. Focus on **specific sections** using hierarchical labels
2. Analyze **one component type at a time**
3. Examine **critical nets** individually
4. Use **reference designators systematically** (e.g., U1-U10 for microcontrollers)
## Troubleshooting
### Netlist Extraction Fails
If netlist extraction fails:
1. **Check file paths**: Ensure the schematic file exists and has the correct extension
2. **Verify file format**: Make sure the schematic is a valid KiCad 6+ .kicad_sch file
3. **Check file permissions**: Ensure you have read access to the file
4. **Look for syntax errors**: Recent edits might have corrupted the schematic file
5. **Try a simpler schematic**: Start with a small test case to verify functionality
### Missing Connections
If connections are missing from the netlist:
1. **Check for disconnected wires**: Wires that appear connected in KiCad might not actually be connected
2. **Verify junction points**: Make sure junction dots are present where needed
3. **Check hierarchical connections**: Ensure labels match across hierarchical sheets
4. **Verify net labels**: Net labels must be correctly placed to establish connections

View File

@ -0,0 +1,253 @@
"""
Netlist resources for KiCad schematics.
"""
import os
from mcp.server.fastmcp import FastMCP
from kicad_mcp.utils.file_utils import get_project_files
from kicad_mcp.utils.netlist_parser import extract_netlist, analyze_netlist
def register_netlist_resources(mcp: FastMCP) -> None:
"""Register netlist-related resources with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.resource("kicad://netlist/{schematic_path}")
def get_netlist_resource(schematic_path: str) -> str:
"""Get a formatted netlist report for a KiCad schematic.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
Returns:
Markdown-formatted netlist report
"""
print(f"Generating netlist report for schematic: {schematic_path}")
if not os.path.exists(schematic_path):
return f"Schematic file not found: {schematic_path}"
try:
# Extract netlist information
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
return f"# Netlist Extraction Error\n\nError: {netlist_data['error']}"
# Analyze the netlist
analysis_results = analyze_netlist(netlist_data)
# Format as Markdown report
schematic_name = os.path.basename(schematic_path)
report = f"# Netlist Analysis for {schematic_name}\n\n"
# Overview section
report += "## Overview\n\n"
report += f"- **Components**: {netlist_data['component_count']}\n"
report += f"- **Nets**: {netlist_data['net_count']}\n"
if "total_pin_connections" in analysis_results:
report += f"- **Pin Connections**: {analysis_results['total_pin_connections']}\n"
report += "\n"
# Component Types section
if "component_types" in analysis_results and analysis_results["component_types"]:
report += "## Component Types\n\n"
for comp_type, count in analysis_results["component_types"].items():
report += f"- **{comp_type}**: {count}\n"
report += "\n"
# Power Nets section
if "power_nets" in analysis_results and analysis_results["power_nets"]:
report += "## Power Nets\n\n"
for net_name in analysis_results["power_nets"]:
report += f"- **{net_name}**\n"
report += "\n"
# Components section
components = netlist_data.get("components", {})
if components:
report += "## Component List\n\n"
report += "| Reference | Type | Value | Footprint |\n"
report += "|-----------|------|-------|----------|\n"
# Sort components by reference
for ref in sorted(components.keys()):
component = components[ref]
lib_id = component.get('lib_id', 'Unknown')
value = component.get('value', '')
footprint = component.get('footprint', '')
report += f"| {ref} | {lib_id} | {value} | {footprint} |\n"
report += "\n"
# Nets section (limit to showing first 20 for readability)
nets = netlist_data.get("nets", {})
if nets:
report += "## Net List\n\n"
# Filter to show only the first 20 nets
net_items = list(nets.items())[:20]
for net_name, pins in net_items:
report += f"### Net: {net_name}\n\n"
if pins:
report += "**Connected Pins:**\n\n"
for pin in pins:
component = pin.get('component', 'Unknown')
pin_num = pin.get('pin', 'Unknown')
report += f"- {component}.{pin_num}\n"
else:
report += "*No connections found*\n"
report += "\n"
if len(nets) > 20:
report += f"*...and {len(nets) - 20} more nets*\n\n"
return report
except Exception as e:
return f"# Netlist Extraction Error\n\nError: {str(e)}"
@mcp.resource("kicad://project_netlist/{project_path}")
def get_project_netlist_resource(project_path: str) -> str:
"""Get a formatted netlist report for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Markdown-formatted netlist report
"""
print(f"Generating netlist report for project: {project_path}")
if not os.path.exists(project_path):
return f"Project not found: {project_path}"
# Get the schematic file
try:
files = get_project_files(project_path)
if "schematic" not in files:
return "Schematic file not found in project"
schematic_path = files["schematic"]
print(f"Found schematic file: {schematic_path}")
# Get the netlist resource for this schematic
return get_netlist_resource(schematic_path)
except Exception as e:
return f"# Netlist Extraction Error\n\nError: {str(e)}"
@mcp.resource("kicad://component/{schematic_path}/{component_ref}")
def get_component_resource(schematic_path: str, component_ref: str) -> str:
"""Get detailed information about a specific component and its connections.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
component_ref: Component reference designator (e.g., R1)
Returns:
Markdown-formatted component report
"""
print(f"Generating component report for {component_ref} in schematic: {schematic_path}")
if not os.path.exists(schematic_path):
return f"Schematic file not found: {schematic_path}"
try:
# Extract netlist information
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
return f"# Component Analysis Error\n\nError: {netlist_data['error']}"
# Check if the component exists
components = netlist_data.get("components", {})
if component_ref not in components:
return f"# Component Not Found\n\nComponent {component_ref} was not found in the schematic.\n\n**Available Components**:\n\n" + "\n".join([f"- {ref}" for ref in sorted(components.keys())])
component_info = components[component_ref]
# Format as Markdown report
report = f"# Component Analysis: {component_ref}\n\n"
# Component Details section
report += "## Component Details\n\n"
report += f"- **Reference**: {component_ref}\n"
if "lib_id" in component_info:
report += f"- **Type**: {component_info['lib_id']}\n"
if "value" in component_info:
report += f"- **Value**: {component_info['value']}\n"
if "footprint" in component_info:
report += f"- **Footprint**: {component_info['footprint']}\n"
# Add other properties
if "properties" in component_info:
for prop_name, prop_value in component_info["properties"].items():
report += f"- **{prop_name}**: {prop_value}\n"
report += "\n"
# Pins section
if "pins" in component_info:
report += "## Pins\n\n"
for pin in component_info["pins"]:
report += f"- **Pin {pin['num']}**: {pin['name']}\n"
report += "\n"
# Connections section
report += "## Connections\n\n"
nets = netlist_data.get("nets", {})
connected_nets = []
for net_name, pins in nets.items():
# Check if any pin belongs to our component
for pin in pins:
if pin.get('component') == component_ref:
connected_nets.append({
"net_name": net_name,
"pin": pin.get('pin', 'Unknown'),
"connections": [p for p in pins if p.get('component') != component_ref]
})
if connected_nets:
for net in connected_nets:
report += f"### Pin {net['pin']} - Net: {net['net_name']}\n\n"
if net["connections"]:
report += "**Connected To:**\n\n"
for conn in net["connections"]:
comp = conn.get('component', 'Unknown')
pin = conn.get('pin', 'Unknown')
report += f"- {comp}.{pin}\n"
else:
report += "*No connections*\n"
report += "\n"
else:
report += "*No connections found for this component*\n\n"
return report
except Exception as e:
return f"# Component Analysis Error\n\nError: {str(e)}"

View File

@ -8,6 +8,7 @@ from kicad_mcp.resources.projects import register_project_resources
from kicad_mcp.resources.files import register_file_resources
from kicad_mcp.resources.drc_resources import register_drc_resources
from kicad_mcp.resources.bom_resources import register_bom_resources
from kicad_mcp.resources.netlist_resources import register_netlist_resources
# Import tool handlers
from kicad_mcp.tools.project_tools import register_project_tools
@ -15,6 +16,7 @@ from kicad_mcp.tools.analysis_tools import register_analysis_tools
from kicad_mcp.tools.export_tools import register_export_tools
from kicad_mcp.tools.drc_tools import register_drc_tools
from kicad_mcp.tools.bom_tools import register_bom_tools
from kicad_mcp.tools.netlist_tools import register_netlist_tools
# Import prompt handlers
from kicad_mcp.prompts.templates import register_prompts
@ -53,6 +55,7 @@ def create_server() -> FastMCP:
register_file_resources(mcp)
register_drc_resources(mcp)
register_bom_resources(mcp)
register_netlist_resources(mcp)
# Register tools
logger.debug("Registering tools...")
@ -61,6 +64,7 @@ def create_server() -> FastMCP:
register_export_tools(mcp)
register_drc_tools(mcp)
register_bom_tools(mcp)
register_netlist_tools(mcp)
# Register prompts
logger.debug("Registering prompts...")

View File

@ -0,0 +1,371 @@
"""
Netlist extraction and analysis tools for KiCad schematics.
"""
import os
from typing import Dict, List, Any, Optional
from mcp.server.fastmcp import FastMCP, Context
from kicad_mcp.utils.file_utils import get_project_files
from kicad_mcp.utils.logger import Logger
from kicad_mcp.utils.netlist_parser import extract_netlist, analyze_netlist
# Create logger for this module
logger = Logger()
def register_netlist_tools(mcp: FastMCP) -> None:
"""Register netlist-related tools with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.tool()
async def extract_schematic_netlist(schematic_path: str, ctx: Context) -> Dict[str, Any]:
"""Extract netlist information from a KiCad schematic.
This tool parses a KiCad schematic file and extracts comprehensive
netlist information including components, connections, and labels.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
ctx: MCP context for progress reporting
Returns:
Dictionary with netlist information
"""
logger.info(f"Extracting netlist from schematic: {schematic_path}")
if not os.path.exists(schematic_path):
logger.error(f"Schematic file not found: {schematic_path}")
ctx.info(f"Schematic file not found: {schematic_path}")
return {"success": False, "error": f"Schematic file not found: {schematic_path}"}
# Report progress
await ctx.report_progress(10, 100)
ctx.info(f"Loading schematic file: {os.path.basename(schematic_path)}")
# Extract netlist information
try:
await ctx.report_progress(20, 100)
ctx.info("Parsing schematic structure...")
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
logger.error(f"Error extracting netlist: {netlist_data['error']}")
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
await ctx.report_progress(50, 100)
# Check if the component exists
components = netlist_data.get("components", {})
if component_ref not in components:
logger.error(f"Component {component_ref} not found in schematic")
ctx.info(f"Component {component_ref} not found in schematic")
return {
"success": False,
"error": f"Component {component_ref} not found in schematic",
"available_components": list(components.keys())
}
component_info = components[component_ref]
ctx.info(f"Found component: {component_ref} ({component_info.get('lib_id', 'unknown type')})")
# Find all nets connected to this component
await ctx.report_progress(70, 100)
ctx.info("Analyzing component connections...")
# Build connection information
connections = []
connected_components = set()
nets = netlist_data.get("nets", {})
for net_name, pins in nets.items():
# Check if any pin belongs to our component
component_pins = [pin for pin in pins if pin.get('component') == component_ref]
if component_pins:
# This net connects to our component
net_info = {
"net_name": net_name,
"pins": component_pins,
"connected_to": []
}
# Find other components connected to this net
for pin in pins:
other_component = pin.get('component')
if other_component and other_component != component_ref:
connected_components.add(other_component)
net_info["connected_to"].append({
"component": other_component,
"pin": pin.get('pin', 'unknown')
})
connections.append(net_info)
await ctx.report_progress(90, 100)
# Build result
result = {
"success": True,
"component_ref": component_ref,
"component_info": component_info,
"connections": connections,
"connected_component_count": len(connected_components),
"connected_components": list(connected_components)
}
# Complete progress
await ctx.report_progress(100, 100)
ctx.info(f"Found {len(connections)} connections to component {component_ref}")
return resultf"Error extracting netlist: {netlist_data['error']}")
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
await ctx.report_progress(60, 100)
ctx.info(f"Extracted {netlist_data['component_count']} components and {netlist_data['net_count']} nets")
# Analyze the netlist
await ctx.report_progress(70, 100)
ctx.info("Analyzing netlist data...")
analysis_results = analyze_netlist(netlist_data)
await ctx.report_progress(90, 100)
# Build result
result = {
"success": True,
"schematic_path": schematic_path,
"component_count": netlist_data["component_count"],
"net_count": netlist_data["net_count"],
"components": netlist_data["components"],
"nets": netlist_data["nets"],
"analysis": analysis_results
}
# Complete progress
await ctx.report_progress(100, 100)
ctx.info("Netlist extraction complete")
return result
except Exception as e:
logger.error(f"Error extracting netlist: {str(e)}")
ctx.info(f"Error extracting netlist: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def extract_project_netlist(project_path: str, ctx: Context) -> Dict[str, Any]:
"""Extract netlist from a KiCad project's schematic.
This tool finds the schematic associated with a KiCad project
and extracts its netlist information.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
ctx: MCP context for progress reporting
Returns:
Dictionary with netlist information
"""
logger.info(f"Extracting netlist for project: {project_path}")
if not os.path.exists(project_path):
logger.error(f"Project not found: {project_path}")
ctx.info(f"Project not found: {project_path}")
return {"success": False, "error": f"Project not found: {project_path}"}
# Report progress
await ctx.report_progress(10, 100)
# Get the schematic file
try:
files = get_project_files(project_path)
if "schematic" not in files:
logger.error("Schematic file not found in project")
ctx.info("Schematic file not found in project")
return {"success": False, "error": "Schematic file not found in project"}
schematic_path = files["schematic"]
logger.info(f"Found schematic file: {schematic_path}")
ctx.info(f"Found schematic file: {os.path.basename(schematic_path)}")
# Extract netlist
await ctx.report_progress(20, 100)
# Call the schematic netlist extraction
result = await extract_schematic_netlist(schematic_path, ctx)
# Add project path to result
if "success" in result and result["success"]:
result["project_path"] = project_path
return result
except Exception as e:
logger.error(f"Error extracting project netlist: {str(e)}")
ctx.info(f"Error extracting project netlist: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def analyze_schematic_connections(schematic_path: str, ctx: Context) -> Dict[str, Any]:
"""Analyze connections in a KiCad schematic.
This tool provides detailed analysis of component connections,
including power nets, signal paths, and potential issues.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
ctx: MCP context for progress reporting
Returns:
Dictionary with connection analysis
"""
logger.info(f"Analyzing connections in schematic: {schematic_path}")
if not os.path.exists(schematic_path):
logger.error(f"Schematic file not found: {schematic_path}")
ctx.info(f"Schematic file not found: {schematic_path}")
return {"success": False, "error": f"Schematic file not found: {schematic_path}"}
# Report progress
await ctx.report_progress(10, 100)
ctx.info(f"Extracting netlist from: {os.path.basename(schematic_path)}")
# Extract netlist information
try:
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
logger.error(f"Error extracting netlist: {netlist_data['error']}")
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
await ctx.report_progress(40, 100)
# Advanced connection analysis
ctx.info("Performing connection analysis...")
analysis = {
"component_count": netlist_data["component_count"],
"net_count": netlist_data["net_count"],
"component_types": {},
"power_nets": [],
"signal_nets": [],
"potential_issues": []
}
# Analyze component types
components = netlist_data.get("components", {})
for ref, component in components.items():
# Extract component type from reference (e.g., R1 -> R)
import re
comp_type_match = re.match(r'^([A-Za-z_]+)', ref)
if comp_type_match:
comp_type = comp_type_match.group(1)
if comp_type not in analysis["component_types"]:
analysis["component_types"][comp_type] = 0
analysis["component_types"][comp_type] += 1
await ctx.report_progress(60, 100)
# Identify power nets
nets = netlist_data.get("nets", {})
for net_name, pins in nets.items():
if any(net_name.startswith(prefix) for prefix in ["VCC", "VDD", "GND", "+5V", "+3V3", "+12V"]):
analysis["power_nets"].append({
"name": net_name,
"pin_count": len(pins)
})
else:
analysis["signal_nets"].append({
"name": net_name,
"pin_count": len(pins)
})
await ctx.report_progress(80, 100)
# Check for potential issues
# 1. Nets with only one connection (floating)
for net_name, pins in nets.items():
if len(pins) <= 1 and not any(net_name.startswith(prefix) for prefix in ["VCC", "VDD", "GND", "+5V", "+3V3", "+12V"]):
analysis["potential_issues"].append({
"type": "floating_net",
"net": net_name,
"description": f"Net '{net_name}' appears to be floating (only has {len(pins)} connection)"
})
# 2. Power pins without connections
# This would require more detailed parsing of the schematic
await ctx.report_progress(90, 100)
# Build result
result = {
"success": True,
"schematic_path": schematic_path,
"analysis": analysis
}
# Complete progress
await ctx.report_progress(100, 100)
ctx.info("Connection analysis complete")
return result
except Exception as e:
logger.error(f"Error analyzing connections: {str(e)}")
ctx.info(f"Error analyzing connections: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def find_component_connections(project_path: str, component_ref: str, ctx: Context) -> Dict[str, Any]:
"""Find all connections for a specific component in a KiCad project.
This tool extracts information about how a specific component
is connected to other components in the schematic.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
component_ref: Component reference (e.g., "R1", "U3")
ctx: MCP context for progress reporting
Returns:
Dictionary with component connection information
"""
logger.info(f"Finding connections for component {component_ref} in project: {project_path}")
if not os.path.exists(project_path):
logger.error(f"Project not found: {project_path}")
ctx.info(f"Project not found: {project_path}")
return {"success": False, "error": f"Project not found: {project_path}"}
# Report progress
await ctx.report_progress(10, 100)
# Get the schematic file
try:
files = get_project_files(project_path)
if "schematic" not in files:
logger.error("Schematic file not found in project")
ctx.info("Schematic file not found in project")
return {"success": False, "error": "Schematic file not found in project"}
schematic_path = files["schematic"]
logger.info(f"Found schematic file: {schematic_path}")
ctx.info(f"Found schematic file: {os.path.basename(schematic_path)}")
# Extract netlist
await ctx.report_progress(30, 100)
ctx.info(f"Extracting netlist to find connections for {component_ref}...")
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
logger.error(

View File

@ -0,0 +1,453 @@
"""
KiCad schematic netlist extraction utilities.
"""
import os
import re
from typing import Dict, List, Set, Tuple, Any, Optional
from collections import defaultdict
from kicad_mcp.utils.logger import Logger
# Create logger for this module
logger = Logger()
class SchematicParser:
"""Parser for KiCad schematic files to extract netlist information."""
def __init__(self, schematic_path: str):
"""Initialize the schematic parser.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
"""
self.schematic_path = schematic_path
self.content = ""
self.components = []
self.labels = []
self.wires = []
self.junctions = []
self.no_connects = []
self.power_symbols = []
self.hierarchical_labels = []
self.global_labels = []
# Netlist information
self.nets = defaultdict(list) # Net name -> connected pins
self.component_pins = {} # (component_ref, pin_num) -> net_name
# Component information
self.component_info = {} # component_ref -> component details
# Load the file
self._load_schematic()
def _load_schematic(self) -> None:
"""Load the schematic file content."""
if not os.path.exists(self.schematic_path):
logger.error(f"Schematic file not found: {self.schematic_path}")
raise FileNotFoundError(f"Schematic file not found: {self.schematic_path}")
try:
with open(self.schematic_path, 'r') as f:
self.content = f.read()
logger.info(f"Successfully loaded schematic: {self.schematic_path}")
except Exception as e:
logger.error(f"Error reading schematic file: {str(e)}")
raise
def parse(self) -> Dict[str, Any]:
"""Parse the schematic to extract netlist information.
Returns:
Dictionary with parsed netlist information
"""
logger.info("Starting schematic parsing")
# Extract symbols (components)
self._extract_components()
# Extract wires
self._extract_wires()
# Extract junctions
self._extract_junctions()
# Extract labels
self._extract_labels()
# Extract power symbols
self._extract_power_symbols()
# Extract no-connects
self._extract_no_connects()
# Build netlist
self._build_netlist()
# Create result
result = {
"components": self.component_info,
"nets": dict(self.nets),
"labels": self.labels,
"wires": self.wires,
"junctions": self.junctions,
"power_symbols": self.power_symbols,
"component_count": len(self.component_info),
"net_count": len(self.nets)
}
logger.info(f"Schematic parsing complete: found {len(self.component_info)} components and {len(self.nets)} nets")
return result
def _extract_s_expressions(self, pattern: str) -> List[str]:
"""Extract all matching S-expressions from the schematic content.
Args:
pattern: Regex pattern to match the start of S-expressions
Returns:
List of matching S-expressions
"""
matches = []
positions = []
# Find all starting positions of matches
for match in re.finditer(pattern, self.content):
positions.append(match.start())
# Extract full S-expressions for each match
for pos in positions:
# Start from the matching position
current_pos = pos
depth = 0
s_exp = ""
# Extract the full S-expression by tracking parentheses
while current_pos < len(self.content):
char = self.content[current_pos]
s_exp += char
if char == '(':
depth += 1
elif char == ')':
depth -= 1
if depth == 0:
# Found the end of the S-expression
break
current_pos += 1
matches.append(s_exp)
return matches
def _extract_components(self) -> None:
"""Extract component information from schematic."""
logger.info("Extracting components")
# Extract all symbol expressions (components)
symbols = self._extract_s_expressions(r'\(symbol\s+')
for symbol in symbols:
component = self._parse_component(symbol)
if component:
self.components.append(component)
# Add to component info dictionary
ref = component.get('reference', 'Unknown')
self.component_info[ref] = component
logger.info(f"Extracted {len(self.components)} components")
def _parse_component(self, symbol_expr: str) -> Dict[str, Any]:
"""Parse a component from a symbol S-expression.
Args:
symbol_expr: Symbol S-expression
Returns:
Component information dictionary
"""
component = {}
# Extract library component ID
lib_id_match = re.search(r'\(lib_id\s+"([^"]+)"\)', symbol_expr)
if lib_id_match:
component['lib_id'] = lib_id_match.group(1)
# Extract reference (e.g., R1, C2)
property_matches = re.finditer(r'\(property\s+"([^"]+)"\s+"([^"]+)"', symbol_expr)
for match in property_matches:
prop_name = match.group(1)
prop_value = match.group(2)
if prop_name == "Reference":
component['reference'] = prop_value
elif prop_name == "Value":
component['value'] = prop_value
elif prop_name == "Footprint":
component['footprint'] = prop_value
else:
# Store other properties
if 'properties' not in component:
component['properties'] = {}
component['properties'][prop_name] = prop_value
# Extract position
pos_match = re.search(r'\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)', symbol_expr)
if pos_match:
component['position'] = {
'x': float(pos_match.group(1)),
'y': float(pos_match.group(2)),
'angle': float(pos_match.group(3).strip() if pos_match.group(3) else 0)
}
# Extract pins
pins = []
pin_matches = re.finditer(r'\(pin\s+\(num\s+"([^"]+)"\)\s+\(name\s+"([^"]+)"\)', symbol_expr)
for match in pin_matches:
pin_num = match.group(1)
pin_name = match.group(2)
pins.append({
'num': pin_num,
'name': pin_name
})
if pins:
component['pins'] = pins
return component
def _extract_wires(self) -> None:
"""Extract wire information from schematic."""
logger.info("Extracting wires")
# Extract all wire expressions
wires = self._extract_s_expressions(r'\(wire\s+')
for wire in wires:
# Extract the wire coordinates
pts_match = re.search(r'\(pts\s+\(xy\s+([\d\.-]+)\s+([\d\.-]+)\)\s+\(xy\s+([\d\.-]+)\s+([\d\.-]+)\)\)', wire)
if pts_match:
self.wires.append({
'start': {
'x': float(pts_match.group(1)),
'y': float(pts_match.group(2))
},
'end': {
'x': float(pts_match.group(3)),
'y': float(pts_match.group(4))
}
})
logger.info(f"Extracted {len(self.wires)} wires")
def _extract_junctions(self) -> None:
"""Extract junction information from schematic."""
logger.info("Extracting junctions")
# Extract all junction expressions
junctions = self._extract_s_expressions(r'\(junction\s+')
for junction in junctions:
# Extract the junction coordinates
xy_match = re.search(r'\(junction\s+\(xy\s+([\d\.-]+)\s+([\d\.-]+)\)\)', junction)
if xy_match:
self.junctions.append({
'x': float(xy_match.group(1)),
'y': float(xy_match.group(2))
})
logger.info(f"Extracted {len(self.junctions)} junctions")
def _extract_labels(self) -> None:
"""Extract label information from schematic."""
logger.info("Extracting labels")
# Extract local labels
local_labels = self._extract_s_expressions(r'\(label\s+')
for label in local_labels:
# Extract label text and position
label_match = re.search(r'\(label\s+"([^"]+)"\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)', label)
if label_match:
self.labels.append({
'type': 'local',
'text': label_match.group(1),
'position': {
'x': float(label_match.group(2)),
'y': float(label_match.group(3)),
'angle': float(label_match.group(4).strip() if label_match.group(4) else 0)
}
})
# Extract global labels
global_labels = self._extract_s_expressions(r'\(global_label\s+')
for label in global_labels:
# Extract global label text and position
label_match = re.search(r'\(global_label\s+"([^"]+)"\s+\(shape\s+([^\s\)]+)\)\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)', label)
if label_match:
self.global_labels.append({
'type': 'global',
'text': label_match.group(1),
'shape': label_match.group(2),
'position': {
'x': float(label_match.group(3)),
'y': float(label_match.group(4)),
'angle': float(label_match.group(5).strip() if label_match.group(5) else 0)
}
})
# Extract hierarchical labels
hierarchical_labels = self._extract_s_expressions(r'\(hierarchical_label\s+')
for label in hierarchical_labels:
# Extract hierarchical label text and position
label_match = re.search(r'\(hierarchical_label\s+"([^"]+)"\s+\(shape\s+([^\s\)]+)\)\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)', label)
if label_match:
self.hierarchical_labels.append({
'type': 'hierarchical',
'text': label_match.group(1),
'shape': label_match.group(2),
'position': {
'x': float(label_match.group(3)),
'y': float(label_match.group(4)),
'angle': float(label_match.group(5).strip() if label_match.group(5) else 0)
}
})
logger.info(f"Extracted {len(self.labels)} local labels, {len(self.global_labels)} global labels, and {len(self.hierarchical_labels)} hierarchical labels")
def _extract_power_symbols(self) -> None:
"""Extract power symbol information from schematic."""
logger.info("Extracting power symbols")
# Extract all power symbol expressions
power_symbols = self._extract_s_expressions(r'\(symbol\s+\(lib_id\s+"power:')
for symbol in power_symbols:
# Extract power symbol type and position
type_match = re.search(r'\(lib_id\s+"power:([^"]+)"\)', symbol)
pos_match = re.search(r'\(at\s+([\d\.-]+)\s+([\d\.-]+)(\s+[\d\.-]+)?\)', symbol)
if type_match and pos_match:
self.power_symbols.append({
'type': type_match.group(1),
'position': {
'x': float(pos_match.group(1)),
'y': float(pos_match.group(2)),
'angle': float(pos_match.group(3).strip() if pos_match.group(3) else 0)
}
})
logger.info(f"Extracted {len(self.power_symbols)} power symbols")
def _extract_no_connects(self) -> None:
"""Extract no-connect information from schematic."""
logger.info("Extracting no-connects")
# Extract all no-connect expressions
no_connects = self._extract_s_expressions(r'\(no_connect\s+')
for no_connect in no_connects:
# Extract the no-connect coordinates
xy_match = re.search(r'\(no_connect\s+\(at\s+([\d\.-]+)\s+([\d\.-]+)\)', no_connect)
if xy_match:
self.no_connects.append({
'x': float(xy_match.group(1)),
'y': float(xy_match.group(2))
})
logger.info(f"Extracted {len(self.no_connects)} no-connects")
def _build_netlist(self) -> None:
"""Build the netlist from extracted components and connections."""
logger.info("Building netlist from schematic data")
# TODO: Implement netlist building algorithm
# This is a complex task that involves:
# 1. Tracking connections between components via wires
# 2. Handling labels (local, global, hierarchical)
# 3. Processing power symbols
# 4. Resolving junctions
# For now, we'll implement a basic version that creates a list of nets
# based on component references and pin numbers
# Process global labels as nets
for label in self.global_labels:
net_name = label['text']
self.nets[net_name] = [] # Initialize empty list for this net
# Process power symbols as nets
for power in self.power_symbols:
net_name = power['type']
if net_name not in self.nets:
self.nets[net_name] = []
# In a full implementation, we would now trace connections between
# components, but that requires a more complex algorithm to follow wires
# and detect connected pins
# For demonstration, we'll add a placeholder note
logger.info("Note: Full netlist building requires complex connectivity tracing")
logger.info(f"Found {len(self.nets)} potential nets from labels and power symbols")
def extract_netlist(schematic_path: str) -> Dict[str, Any]:
"""Extract netlist information from a KiCad schematic file.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
Returns:
Dictionary with netlist information
"""
try:
parser = SchematicParser(schematic_path)
return parser.parse()
except Exception as e:
logger.error(f"Error extracting netlist: {str(e)}")
return {
"error": str(e),
"components": {},
"nets": {},
"component_count": 0,
"net_count": 0
}
def analyze_netlist(netlist_data: Dict[str, Any]) -> Dict[str, Any]:
"""Analyze netlist data to provide insights.
Args:
netlist_data: Dictionary with netlist information
Returns:
Dictionary with analysis results
"""
results = {
"component_count": netlist_data.get("component_count", 0),
"net_count": netlist_data.get("net_count", 0),
"component_types": defaultdict(int),
"power_nets": []
}
# Analyze component types
for ref, component in netlist_data.get("components", {}).items():
# Extract component type from reference (e.g., R1 -> R)
comp_type = re.match(r'^([A-Za-z_]+)', ref)
if comp_type:
results["component_types"][comp_type.group(1)] += 1
# Identify power nets
for net_name in netlist_data.get("nets", {}):
if any(net_name.startswith(prefix) for prefix in ["VCC", "VDD", "GND", "+5V", "+3V3", "+12V"]):
results["power_nets"].append(net_name)
# Count pin connections
total_pins = sum(len(pins) for pins in netlist_data.get("nets", {}).values())
results["total_pin_connections"] = total_pins
return results

View File

@ -1,3 +1,4 @@
mcp[cli]
httpx
pytest
pandas