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:
parent
f8bafe8beb
commit
750dd260c4
@ -143,6 +143,7 @@ The KiCad MCP Server provides several key features:
|
|||||||
|
|
||||||
- **Project Management**: List, examine, and open KiCad projects
|
- **Project Management**: List, examine, and open KiCad projects
|
||||||
- **PCB Design Analysis**: Get insights about your PCB designs and schematics
|
- **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
|
- **BOM Management**: Analyze and export Bills of Materials
|
||||||
- **Design Rule Checking**: Run DRC checks and track your progress over time
|
- **Design Rule Checking**: Run DRC checks and track your progress over time
|
||||||
- **PCB Visualization**: Generate visual representations of your PCB layouts
|
- **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)
|
- [Project Management](docs/project_guide.md)
|
||||||
- [PCB Design Analysis](docs/analysis_guide.md)
|
- [PCB Design Analysis](docs/analysis_guide.md)
|
||||||
|
- [Netlist Extraction](docs/netlist_guide.md)
|
||||||
- [Bill of Materials (BOM)](docs/bom_guide.md)
|
- [Bill of Materials (BOM)](docs/bom_guide.md)
|
||||||
- [Design Rule Checking (DRC)](docs/drc_guide.md)
|
- [Design Rule Checking (DRC)](docs/drc_guide.md)
|
||||||
- [PCB Visualization](docs/thumbnail_guide.md)
|
- [PCB Visualization](docs/thumbnail_guide.md)
|
||||||
|
183
docs/netlist_guide.md
Normal file
183
docs/netlist_guide.md
Normal 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
|
253
kicad_mcp/resources/netlist_resources.py
Normal file
253
kicad_mcp/resources/netlist_resources.py
Normal 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)}"
|
@ -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.files import register_file_resources
|
||||||
from kicad_mcp.resources.drc_resources import register_drc_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.bom_resources import register_bom_resources
|
||||||
|
from kicad_mcp.resources.netlist_resources import register_netlist_resources
|
||||||
|
|
||||||
# Import tool handlers
|
# Import tool handlers
|
||||||
from kicad_mcp.tools.project_tools import register_project_tools
|
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.export_tools import register_export_tools
|
||||||
from kicad_mcp.tools.drc_tools import register_drc_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.bom_tools import register_bom_tools
|
||||||
|
from kicad_mcp.tools.netlist_tools import register_netlist_tools
|
||||||
|
|
||||||
# Import prompt handlers
|
# Import prompt handlers
|
||||||
from kicad_mcp.prompts.templates import register_prompts
|
from kicad_mcp.prompts.templates import register_prompts
|
||||||
@ -53,6 +55,7 @@ def create_server() -> FastMCP:
|
|||||||
register_file_resources(mcp)
|
register_file_resources(mcp)
|
||||||
register_drc_resources(mcp)
|
register_drc_resources(mcp)
|
||||||
register_bom_resources(mcp)
|
register_bom_resources(mcp)
|
||||||
|
register_netlist_resources(mcp)
|
||||||
|
|
||||||
# Register tools
|
# Register tools
|
||||||
logger.debug("Registering tools...")
|
logger.debug("Registering tools...")
|
||||||
@ -61,6 +64,7 @@ def create_server() -> FastMCP:
|
|||||||
register_export_tools(mcp)
|
register_export_tools(mcp)
|
||||||
register_drc_tools(mcp)
|
register_drc_tools(mcp)
|
||||||
register_bom_tools(mcp)
|
register_bom_tools(mcp)
|
||||||
|
register_netlist_tools(mcp)
|
||||||
|
|
||||||
# Register prompts
|
# Register prompts
|
||||||
logger.debug("Registering prompts...")
|
logger.debug("Registering prompts...")
|
||||||
|
371
kicad_mcp/tools/netlist_tools.py
Normal file
371
kicad_mcp/tools/netlist_tools.py
Normal 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(
|
453
kicad_mcp/utils/netlist_parser.py
Normal file
453
kicad_mcp/utils/netlist_parser.py
Normal 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
|
@ -1,3 +1,4 @@
|
|||||||
mcp[cli]
|
mcp[cli]
|
||||||
httpx
|
httpx
|
||||||
pytest
|
pytest
|
||||||
|
pandas
|
||||||
|
Loading…
x
Reference in New Issue
Block a user