Add circuit pattern recognition feature for KiCad schematics

This commit introduces a new circuit pattern recognition system that can
automatically identify common circuit patterns in KiCad schematics, including:

- Power supply circuits (linear regulators, switching converters)
- Amplifier circuits (op-amps, transistor amplifiers)
- Filter circuits (passive and active)
- Oscillator circuits (crystal, RC, IC-based)
- Digital interfaces (I2C, SPI, UART, USB)
- Microcontroller circuits
- Sensor interfaces

The implementation includes:
- Pattern recognition algorithms for common components
- Component value extraction and normalization utilities
- MCP tools for running pattern analysis
- MCP resources for displaying formatted results
- Comprehensive documentation

Users can easily extend the pattern recognition by adding new component
patterns or circuit recognition functions.
This commit is contained in:
Lama 2025-03-21 10:43:34 -04:00
parent 750dd260c4
commit 7343e032c1
7 changed files with 2070 additions and 2 deletions

View File

@ -12,6 +12,7 @@ This guide will help you set up a Model Context Protocol (MCP) server for KiCad.
- [Installation Steps](#installation-steps)
- [Understanding MCP Components](#understanding-mcp-components)
- [Feature Highlights](#feature-highlights)
- [Natural Language Interaction](#natural-language-interaction)
- [Documentation](#documentation)
- [Configuration](#configuration)
- [Development Guide](#development-guide)
@ -139,16 +140,54 @@ The Model Context Protocol (MCP) defines three primary ways to provide capabilit
## Feature Highlights
The KiCad MCP Server provides several key features:
The KiCad MCP Server provides several key features, each with detailed documentation:
- **Project Management**: List, examine, and open KiCad projects
- *Example:* "Show me all my recent KiCad projects" → Lists all projects sorted by modification date
- **PCB Design Analysis**: Get insights about your PCB designs and schematics
- *Example:* "Analyze the component density of my temperature sensor board" → Provides component spacing analysis
- **Netlist Extraction**: Extract and analyze component connections from schematics
- *Example:* "What components are connected to the MCU in my Arduino shield?" → Shows all connections to the microcontroller
- **BOM Management**: Analyze and export Bills of Materials
- *Example:* "Generate a BOM for my smart watch project" → Creates a detailed bill of materials
- **Design Rule Checking**: Run DRC checks and track your progress over time
- *Example:* "Run DRC on my power supply board and compare to last week" → Shows progress in fixing violations
- **PCB Visualization**: Generate visual representations of your PCB layouts
- *Example:* "Show me a thumbnail of my audio amplifier PCB" → Displays a visual render of the board
- **Circuit Pattern Recognition**: Automatically identify common circuit patterns in your schematics
- *Example:* "What power supply topologies am I using in my IoT device?" → Identifies buck, boost, or linear regulators
For detailed usage guides, see the [documentation](#documentation).
For more examples and details on each feature, see the dedicated guides in the documentation.
## Natural Language Interaction
While our documentation often shows examples like:
```
Show me the DRC report for /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
```
You don't need to type the full path to your files! The LLM can understand more natural language requests.
For example, instead of the formal command above, you can simply ask:
```
Can you check if there are any design rule violations in my Arduino shield project?
```
Or:
```
I'm working on the temperature sensor circuit. Can you identify what patterns it uses?
```
The LLM will understand your intent and request the relevant information from the KiCad MCP Server. If it needs clarification about which project you're referring to, it will ask.
## Documentation
@ -160,6 +199,7 @@ Detailed documentation for each feature is available in the `docs/` directory:
- [Bill of Materials (BOM)](docs/bom_guide.md)
- [Design Rule Checking (DRC)](docs/drc_guide.md)
- [PCB Visualization](docs/thumbnail_guide.md)
- [Circuit Pattern Recognition](docs/pattern_guide.md)
- [Prompt Templates](docs/prompt_guide.md)
## Configuration
@ -243,6 +283,12 @@ Want to contribute to the KiCad MCP Server? Here's how you can help improve this
3. Add your changes
4. Submit a pull request
Key areas for contribution:
- Adding support for more component patterns in the Circuit Pattern Recognition system
- Improving documentation and examples
- Adding new features or enhancing existing ones
- Fixing bugs and improving error handling
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
## Future Development Ideas
@ -258,6 +304,7 @@ Interested in contributing? Here are some ideas for future development:
7. **Web UI** - Create a simple web interface for configuration and monitoring
8. **Circuit Analysis** - Add automated circuit analysis features
9. **Test Coverage** - Improve test coverage across the codebase
10. **Circuit Pattern Recognition** - Expand the pattern database with more component types and circuit topologies
## License

251
docs/pattern_guide.md Normal file
View File

@ -0,0 +1,251 @@
# KiCad Circuit Pattern Recognition Guide
This guide explains how to use the circuit pattern recognition features in the KiCad MCP Server to identify common circuit blocks in your schematics.
## Overview
The circuit pattern recognition functionality allows you to:
1. Automatically identify common circuit patterns in your KiCad schematics
2. Get detailed information about each identified circuit
3. Understand the structure and function of different parts of your design
4. Quickly locate specific circuit types (power supplies, amplifiers, etc.)
## Quick Reference
| Task | Example Prompt |
|------|---------------|
| Identify patterns in a schematic | `Identify circuit patterns in my schematic at /path/to/schematic.kicad_sch` |
| Identify patterns in a project | `Analyze circuit patterns in my KiCad project at /path/to/project.kicad_pro` |
| Get a report of identified patterns | `Show me the circuit patterns in my KiCad project at /path/to/project.kicad_pro` |
| Find specific patterns | `Find all power supply circuits in my schematic at /path/to/schematic.kicad_sch` |
## Using Pattern Recognition Features
### Identifying Circuit Patterns
To identify circuit patterns in a schematic:
```
Identify circuit patterns in my schematic at /path/to/schematic.kicad_sch
```
This will:
- Parse your schematic to extract component and connection information
- Apply pattern recognition algorithms to identify common circuit blocks
- Generate a comprehensive report of all identified patterns
- Provide details about each pattern's components and characteristics
### Project-Based Pattern Recognition
To analyze circuit patterns in a KiCad project:
```
Analyze circuit patterns in my KiCad project at /path/to/project.kicad_pro
```
This will find the schematic associated with your project and perform pattern recognition on it.
### Viewing Pattern Reports
For a formatted report of identified patterns:
```
Show me the circuit patterns in my KiCad project at /path/to/project.kicad_pro
```
This will load the `kicad://patterns/project/path/to/project.kicad_pro` resource, showing:
- A summary of all identified patterns
- Detailed information for each pattern type
- Component references and values
- Additional characteristics specific to each pattern type
### Searching for Specific Pattern Types
You can also ask about specific types of patterns:
```
Find all power supply circuits in my schematic at /path/to/schematic.kicad_sch
```
```
Show me the microcontroller circuits in my KiCad project at /path/to/project.kicad_pro
```
## Supported Pattern Types
The pattern recognition system currently identifies the following types of circuits:
### Power Supply Circuits
- Linear voltage regulators (78xx/79xx series, LDOs, etc.)
- Switching regulators (buck, boost, buck-boost)
### Amplifier Circuits
- Operational amplifiers (general-purpose, audio, instrumentation)
- Transistor amplifiers (BJT, FET)
- Audio amplifier ICs
### Filter Circuits
- Passive filters (RC low-pass/high-pass)
- Active filters (op-amp based)
- Crystal and ceramic filters
### Oscillator Circuits
- Crystal oscillators
- Oscillator ICs
- RC oscillators (555 timer, etc.)
### Digital Interface Circuits
- I2C interfaces
- SPI interfaces
- UART/Serial interfaces
- USB interfaces
- Ethernet interfaces
### Microcontroller Circuits
- Various microcontroller families (AVR, STM32, PIC, ESP, etc.)
- Development boards (Arduino, ESP32, Raspberry Pi Pico, etc.)
### Sensor Interface Circuits
- Temperature sensors
- Humidity sensors
- Pressure sensors
- Motion sensors (accelerometers, gyroscopes)
- Light sensors
- Many other sensor types
## Extending the Pattern Recognition System
The pattern recognition system is designed to be extensible. If you find that certain components or circuit patterns you use frequently aren't being recognized, you can contribute to the system.
### Adding New Component Patterns
The pattern recognition is primarily based on regular expression matching of component values and library IDs. The patterns are defined in the `kicad_mcp/utils/pattern_recognition.py` file.
For example, to add support for a new microcontroller family, you could update the `mcu_patterns` dictionary in the `identify_microcontrollers()` function:
```python
mcu_patterns = {
# Existing patterns...
"AVR": r"ATMEGA\d+|ATTINY\d+|AT90\w+",
"STM32": r"STM32\w+",
# Add your new pattern here
"Renesas": r"R[A-Z]\d+|RL78|RX\d+",
}
```
Similarly, you can add patterns for new sensors, power supply ICs, or other components in their respective functions.
### Adding New Circuit Recognition Functions
For entirely new types of circuits, you can add new recognition functions in the `kicad_mcp/utils/pattern_recognition.py` file, following the pattern of existing functions.
For example, you might add:
```python
def identify_motor_drivers(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify motor driver circuits in the schematic."""
# Your implementation here
...
```
Then, update the `identify_circuit_patterns()` function in `kicad_mcp/tools/pattern_tools.py` to call your new function and include its results.
### Contributing Your Extensions
We strongly encourage you to contribute your pattern recognition extensions back to the project so that everyone can benefit from improved recognition capabilities!
To contribute:
1. Fork the repository on GitHub
2. Make your changes to add new patterns or recognition functions
3. Test your changes with your own schematics
4. Submit a pull request with:
- A description of the new patterns you've added
- Examples of components/circuits that can now be recognized
- Any test cases you used to verify the recognition
Your contributions will help build a more comprehensive pattern recognition system that works for a wider variety of designs.
## Troubleshooting
### Patterns Not Being Recognized
If your circuits aren't being recognized:
1. **Check component naming**: The pattern recognition often relies on standard reference designators (R for resistors, C for capacitors, etc.)
2. **Check component values**: Make sure your component values are in standard formats
3. **Check library IDs**: The system also looks at library IDs, so using standard libraries can help
4. **Look at existing patterns**: Check the pattern_recognition.py file to see if your components match the existing patterns
### Pattern Recognition Fails
If the pattern recognition process fails entirely:
1. **Check file paths**: Ensure the schematic file exists and has the correct extension
2. **Verify schematic format**: Make sure it's a valid KiCad 6+ .kicad_sch file
3. **Check file permissions**: Ensure you have read access to the file
4. **Try a simpler schematic**: Start with a small test case to verify functionality
## Advanced Usage
### Integration with Other Features
Combine pattern recognition with other KiCad MCP Server features:
1. **DRC + Pattern Recognition**: Find design issues in specific circuit blocks
```
Find DRC issues affecting the power supply circuits in my schematic
```
2. **BOM + Pattern Recognition**: Analyze component usage by circuit type
```
Show me the BOM breakdown for the digital interface circuits in my design
```
3. **Netlist + Pattern Recognition**: Understand connectivity in specific patterns
```
Analyze the connections between the microcontroller and sensor interfaces in my design
```
### Batch Pattern Recognition
For analyzing multiple projects:
```
Find all projects in my KiCad directory that contain switching regulator circuits
```
```
Compare the digital interfaces used across all my KiCad projects
```
## Future Improvements
We plan to enhance the pattern recognition system with:
1. **More pattern types**: Support for additional circuit patterns
2. **Better connection analysis**: More accurate tracing of connections between components
3. **Hierarchical pattern recognition**: Identifying patterns across hierarchical sheets
4. **Pattern verification**: Validating that recognized patterns follow design best practices
5. **Component suggestions**: Recommending alternative components for recognized patterns
## Contribute to Pattern Recognition
The pattern recognition system relies on a community-driven database of component patterns. The more patterns we have, the better the recognition works for everyone!
If you work with components that aren't being recognized:
1. Check the current patterns in `kicad_mcp/utils/pattern_recognition.py`
2. Add your own patterns for components you use
3. Submit a pull request to share with the community
Common areas where contributions are valuable:
- Modern microcontroller families and variants
- Specialized sensor types
- Power management ICs
- Interface and communication chips
- Industry-specific components
Your expertise in specific component types can help make the pattern recognition more useful for everyone!

View File

@ -0,0 +1,294 @@
"""
Circuit pattern recognition 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
from kicad_mcp.utils.pattern_recognition import (
identify_power_supplies,
identify_amplifiers,
identify_filters,
identify_oscillators,
identify_digital_interfaces,
identify_microcontrollers,
identify_sensor_interfaces
)
def register_pattern_resources(mcp: FastMCP) -> None:
"""Register circuit pattern recognition resources with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.resource("kicad://patterns/{schematic_path}")
def get_circuit_patterns_resource(schematic_path: str) -> str:
"""Get a formatted report of identified circuit patterns in a KiCad schematic.
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
Returns:
Markdown-formatted circuit pattern report
"""
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"# Circuit Pattern Analysis Error\n\nError: {netlist_data['error']}"
components = netlist_data.get("components", {})
nets = netlist_data.get("nets", {})
# Identify circuit patterns
power_supplies = identify_power_supplies(components, nets)
amplifiers = identify_amplifiers(components, nets)
filters = identify_filters(components, nets)
oscillators = identify_oscillators(components, nets)
digital_interfaces = identify_digital_interfaces(components, nets)
microcontrollers = identify_microcontrollers(components)
sensor_interfaces = identify_sensor_interfaces(components, nets)
# Format as Markdown report
schematic_name = os.path.basename(schematic_path)
report = f"# Circuit Pattern Analysis for {schematic_name}\n\n"
# Add summary
total_patterns = (
len(power_supplies) +
len(amplifiers) +
len(filters) +
len(oscillators) +
len(digital_interfaces) +
len(microcontrollers) +
len(sensor_interfaces)
)
report += f"## Summary\n\n"
report += f"- **Total Components**: {netlist_data['component_count']}\n"
report += f"- **Total Circuit Patterns Identified**: {total_patterns}\n\n"
report += "### Pattern Types\n\n"
report += f"- **Power Supply Circuits**: {len(power_supplies)}\n"
report += f"- **Amplifier Circuits**: {len(amplifiers)}\n"
report += f"- **Filter Circuits**: {len(filters)}\n"
report += f"- **Oscillator Circuits**: {len(oscillators)}\n"
report += f"- **Digital Interface Circuits**: {len(digital_interfaces)}\n"
report += f"- **Microcontroller Circuits**: {len(microcontrollers)}\n"
report += f"- **Sensor Interface Circuits**: {len(sensor_interfaces)}\n\n"
# Add detailed sections
if power_supplies:
report += "## Power Supply Circuits\n\n"
for i, ps in enumerate(power_supplies, 1):
ps_type = ps.get("type", "Unknown")
ps_subtype = ps.get("subtype", "")
report += f"### Power Supply {i}: {ps_subtype.upper() if ps_subtype else ps_type.title()}\n\n"
if ps_type == "linear_regulator":
report += f"- **Type**: Linear Voltage Regulator\n"
report += f"- **Subtype**: {ps_subtype}\n"
report += f"- **Main Component**: {ps.get('main_component', 'Unknown')}\n"
report += f"- **Value**: {ps.get('value', 'Unknown')}\n"
report += f"- **Output Voltage**: {ps.get('output_voltage', 'Unknown')}\n"
elif ps_type == "switching_regulator":
report += f"- **Type**: Switching Voltage Regulator\n"
report += f"- **Topology**: {ps_subtype.title() if ps_subtype else 'Unknown'}\n"
report += f"- **Main Component**: {ps.get('main_component', 'Unknown')}\n"
report += f"- **Inductor**: {ps.get('inductor', 'Unknown')}\n"
report += f"- **Value**: {ps.get('value', 'Unknown')}\n"
report += "\n"
if amplifiers:
report += "## Amplifier Circuits\n\n"
for i, amp in enumerate(amplifiers, 1):
amp_type = amp.get("type", "Unknown")
amp_subtype = amp.get("subtype", "")
report += f"### Amplifier {i}: {amp_subtype.upper() if amp_subtype else amp_type.title()}\n\n"
if amp_type == "operational_amplifier":
report += f"- **Type**: Operational Amplifier\n"
report += f"- **Subtype**: {amp_subtype.replace('_', ' ').title() if amp_subtype else 'General Purpose'}\n"
report += f"- **Component**: {amp.get('component', 'Unknown')}\n"
report += f"- **Value**: {amp.get('value', 'Unknown')}\n"
elif amp_type == "transistor_amplifier":
report += f"- **Type**: Transistor Amplifier\n"
report += f"- **Transistor Type**: {amp_subtype}\n"
report += f"- **Component**: {amp.get('component', 'Unknown')}\n"
report += f"- **Value**: {amp.get('value', 'Unknown')}\n"
elif amp_type == "audio_amplifier_ic":
report += f"- **Type**: Audio Amplifier IC\n"
report += f"- **Component**: {amp.get('component', 'Unknown')}\n"
report += f"- **Value**: {amp.get('value', 'Unknown')}\n"
report += "\n"
if filters:
report += "## Filter Circuits\n\n"
for i, filt in enumerate(filters, 1):
filt_type = filt.get("type", "Unknown")
filt_subtype = filt.get("subtype", "")
report += f"### Filter {i}: {filt_subtype.upper() if filt_subtype else filt_type.title()}\n\n"
if filt_type == "passive_filter":
report += f"- **Type**: Passive Filter\n"
report += f"- **Topology**: {filt_subtype.replace('_', ' ').upper() if filt_subtype else 'Unknown'}\n"
report += f"- **Components**: {', '.join(filt.get('components', []))}\n"
elif filt_type == "active_filter":
report += f"- **Type**: Active Filter\n"
report += f"- **Main Component**: {filt.get('main_component', 'Unknown')}\n"
report += f"- **Value**: {filt.get('value', 'Unknown')}\n"
elif filt_type == "crystal_filter":
report += f"- **Type**: Crystal Filter\n"
report += f"- **Component**: {filt.get('component', 'Unknown')}\n"
report += f"- **Value**: {filt.get('value', 'Unknown')}\n"
elif filt_type == "ceramic_filter":
report += f"- **Type**: Ceramic Filter\n"
report += f"- **Component**: {filt.get('component', 'Unknown')}\n"
report += f"- **Value**: {filt.get('value', 'Unknown')}\n"
report += "\n"
if oscillators:
report += "## Oscillator Circuits\n\n"
for i, osc in enumerate(oscillators, 1):
osc_type = osc.get("type", "Unknown")
osc_subtype = osc.get("subtype", "")
report += f"### Oscillator {i}: {osc_subtype.upper() if osc_subtype else osc_type.title()}\n\n"
if osc_type == "crystal_oscillator":
report += f"- **Type**: Crystal Oscillator\n"
report += f"- **Component**: {osc.get('component', 'Unknown')}\n"
report += f"- **Value**: {osc.get('value', 'Unknown')}\n"
report += f"- **Frequency**: {osc.get('frequency', 'Unknown')}\n"
report += f"- **Has Load Capacitors**: {'Yes' if osc.get('has_load_capacitors', False) else 'No'}\n"
elif osc_type == "oscillator_ic":
report += f"- **Type**: Oscillator IC\n"
report += f"- **Component**: {osc.get('component', 'Unknown')}\n"
report += f"- **Value**: {osc.get('value', 'Unknown')}\n"
report += f"- **Frequency**: {osc.get('frequency', 'Unknown')}\n"
elif osc_type == "rc_oscillator":
report += f"- **Type**: RC Oscillator\n"
report += f"- **Subtype**: {osc_subtype.replace('_', ' ').title() if osc_subtype else 'Unknown'}\n"
report += f"- **Component**: {osc.get('component', 'Unknown')}\n"
report += f"- **Value**: {osc.get('value', 'Unknown')}\n"
report += "\n"
if digital_interfaces:
report += "## Digital Interface Circuits\n\n"
for i, iface in enumerate(digital_interfaces, 1):
iface_type = iface.get("type", "Unknown")
report += f"### Interface {i}: {iface_type.replace('_', ' ').upper()}\n\n"
report += f"- **Type**: {iface_type.replace('_', ' ').title()}\n"
signals = iface.get("signals_found", [])
if signals:
report += f"- **Signals Found**: {', '.join(signals)}\n"
report += "\n"
if microcontrollers:
report += "## Microcontroller Circuits\n\n"
for i, mcu in enumerate(microcontrollers, 1):
mcu_type = mcu.get("type", "Unknown")
if mcu_type == "microcontroller":
report += f"### Microcontroller {i}: {mcu.get('model', mcu.get('family', 'Unknown'))}\n\n"
report += f"- **Type**: Microcontroller\n"
report += f"- **Family**: {mcu.get('family', 'Unknown')}\n"
if "model" in mcu:
report += f"- **Model**: {mcu['model']}\n"
report += f"- **Component**: {mcu.get('component', 'Unknown')}\n"
if "common_usage" in mcu:
report += f"- **Common Usage**: {mcu['common_usage']}\n"
if "features" in mcu:
report += f"- **Features**: {mcu['features']}\n"
elif mcu_type == "development_board":
report += f"### Development Board {i}: {mcu.get('board_type', 'Unknown')}\n\n"
report += f"- **Type**: Development Board\n"
report += f"- **Board Type**: {mcu.get('board_type', 'Unknown')}\n"
report += f"- **Component**: {mcu.get('component', 'Unknown')}\n"
report += f"- **Value**: {mcu.get('value', 'Unknown')}\n"
report += "\n"
if sensor_interfaces:
report += "## Sensor Interface Circuits\n\n"
for i, sensor in enumerate(sensor_interfaces, 1):
sensor_type = sensor.get("type", "Unknown")
sensor_subtype = sensor.get("subtype", "")
report += f"### Sensor {i}: {sensor_subtype.title() + ' ' if sensor_subtype else ''}{sensor_type.replace('_', ' ').title()}\n\n"
report += f"- **Type**: {sensor_type.replace('_', ' ').title()}\n"
if sensor_subtype:
report += f"- **Subtype**: {sensor_subtype}\n"
report += f"- **Component**: {sensor.get('component', 'Unknown')}\n"
if "model" in sensor:
report += f"- **Model**: {sensor['model']}\n"
report += f"- **Value**: {sensor.get('value', 'Unknown')}\n"
if "interface" in sensor:
report += f"- **Interface**: {sensor['interface']}\n"
if "measures" in sensor:
if isinstance(sensor["measures"], list):
report += f"- **Measures**: {', '.join(sensor['measures'])}\n"
else:
report += f"- **Measures**: {sensor['measures']}\n"
if "range" in sensor:
report += f"- **Range**: {sensor['range']}\n"
report += "\n"
return report
except Exception as e:
return f"# Circuit Pattern Analysis Error\n\nError: {str(e)}"
@mcp.resource("kicad://patterns/project/{project_path}")
def get_project_patterns_resource(project_path: str) -> str:
"""Get a formatted report of identified circuit patterns in a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Markdown-formatted circuit pattern report
"""
if not os.path.exists(project_path):
return f"Project not found: {project_path}"
try:
# Get the schematic file from the project
files = get_project_files(project_path)
if "schematic" not in files:
return "Schematic file not found in project"
schematic_path = files["schematic"]
# Use the existing resource handler to generate the report
return get_circuit_patterns_resource(schematic_path)
except Exception as e:
return f"# Circuit Pattern Analysis Error\n\nError: {str(e)}"

View File

@ -9,6 +9,8 @@ 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
from kicad_mcp.resources.pattern_resources import register_pattern_resources
# Import tool handlers
from kicad_mcp.tools.project_tools import register_project_tools
@ -17,11 +19,13 @@ 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
from kicad_mcp.tools.pattern_tools import register_pattern_tools
# Import prompt handlers
from kicad_mcp.prompts.templates import register_prompts
from kicad_mcp.prompts.drc_prompt import register_drc_prompts
from kicad_mcp.prompts.bom_prompts import register_bom_prompts
from kicad_mcp.prompts.pattern_prompts import register_pattern_prompts
# Import utils
from kicad_mcp.utils.logger import Logger
@ -56,6 +60,7 @@ def create_server() -> FastMCP:
register_drc_resources(mcp)
register_bom_resources(mcp)
register_netlist_resources(mcp)
register_pattern_resources(mcp)
# Register tools
logger.debug("Registering tools...")
@ -65,12 +70,14 @@ def create_server() -> FastMCP:
register_drc_tools(mcp)
register_bom_tools(mcp)
register_netlist_tools(mcp)
register_pattern_tools(mcp)
# Register prompts
logger.debug("Registering prompts...")
register_prompts(mcp)
register_drc_prompts(mcp)
register_bom_prompts(mcp)
register_pattern_prompts(mcp)
logger.info("Server initialization complete")
return mcp

View File

@ -0,0 +1,177 @@
"""
Circuit pattern recognition 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.netlist_parser import extract_netlist, analyze_netlist
from kicad_mcp.utils.pattern_recognition import (
identify_power_supplies,
identify_amplifiers,
identify_filters,
identify_oscillators,
identify_digital_interfaces,
identify_microcontrollers,
identify_sensor_interfaces
)
def register_pattern_tools(mcp: FastMCP) -> None:
"""Register circuit pattern recognition tools with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.tool()
async def identify_circuit_patterns(schematic_path: str, ctx: Context) -> Dict[str, Any]:
"""Identify common circuit patterns in a KiCad schematic.
This tool analyzes a schematic to recognize common circuit blocks such as:
- Power supply circuits (linear regulators, switching converters)
- Amplifier circuits (op-amps, transistor amplifiers)
- Filter circuits (RC, LC, active filters)
- Digital interfaces (I2C, SPI, UART)
- Microcontroller circuits
- And more
Args:
schematic_path: Path to the KiCad schematic file (.kicad_sch)
ctx: MCP context for progress reporting
Returns:
Dictionary with identified circuit patterns
"""
if not os.path.exists(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)}")
try:
# Extract netlist information
await ctx.report_progress(20, 100)
ctx.info("Parsing schematic structure...")
netlist_data = extract_netlist(schematic_path)
if "error" in netlist_data:
ctx.info(f"Error extracting netlist: {netlist_data['error']}")
return {"success": False, "error": netlist_data['error']}
# Analyze components and nets
await ctx.report_progress(30, 100)
ctx.info("Analyzing components and connections...")
components = netlist_data.get("components", {})
nets = netlist_data.get("nets", {})
# Start pattern recognition
await ctx.report_progress(50, 100)
ctx.info("Identifying circuit patterns...")
identified_patterns = {
"power_supply_circuits": [],
"amplifier_circuits": [],
"filter_circuits": [],
"oscillator_circuits": [],
"digital_interface_circuits": [],
"microcontroller_circuits": [],
"sensor_interface_circuits": [],
"other_patterns": []
}
# Identify power supply circuits
await ctx.report_progress(60, 100)
identified_patterns["power_supply_circuits"] = identify_power_supplies(components, nets)
# Identify amplifier circuits
await ctx.report_progress(70, 100)
identified_patterns["amplifier_circuits"] = identify_amplifiers(components, nets)
# Identify filter circuits
await ctx.report_progress(75, 100)
identified_patterns["filter_circuits"] = identify_filters(components, nets)
# Identify oscillator circuits
await ctx.report_progress(80, 100)
identified_patterns["oscillator_circuits"] = identify_oscillators(components, nets)
# Identify digital interface circuits
await ctx.report_progress(85, 100)
identified_patterns["digital_interface_circuits"] = identify_digital_interfaces(components, nets)
# Identify microcontroller circuits
await ctx.report_progress(90, 100)
identified_patterns["microcontroller_circuits"] = identify_microcontrollers(components)
# Identify sensor interface circuits
await ctx.report_progress(95, 100)
identified_patterns["sensor_interface_circuits"] = identify_sensor_interfaces(components, nets)
# Build result
result = {
"success": True,
"schematic_path": schematic_path,
"component_count": netlist_data["component_count"],
"identified_patterns": identified_patterns
}
# Count total patterns
total_patterns = sum(len(patterns) for patterns in identified_patterns.values())
result["total_patterns_found"] = total_patterns
# Complete progress
await ctx.report_progress(100, 100)
ctx.info(f"Pattern recognition complete. Found {total_patterns} circuit patterns.")
return result
except Exception as e:
ctx.info(f"Error identifying circuit patterns: {str(e)}")
return {"success": False, "error": str(e)}
@mcp.tool()
async def analyze_project_circuit_patterns(project_path: str, ctx: Context) -> Dict[str, Any]:
"""Identify circuit patterns in a KiCad project's schematic.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
ctx: MCP context for progress reporting
Returns:
Dictionary with identified circuit patterns
"""
if not os.path.exists(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:
ctx.info("Schematic file not found in project")
return {"success": False, "error": "Schematic file not found in project"}
schematic_path = files["schematic"]
ctx.info(f"Found schematic file: {os.path.basename(schematic_path)}")
# Identify patterns in the schematic
result = await identify_circuit_patterns(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:
ctx.info(f"Error analyzing project circuit patterns: {str(e)}")
return {"success": False, "error": str(e)}

View File

@ -0,0 +1,433 @@
"""
Utility functions for working with KiCad component values and properties.
"""
import re
from typing import Optional, Tuple, Union, Dict
def extract_voltage_from_regulator(value: str) -> str:
"""Extract output voltage from a voltage regulator part number or description.
Args:
value: Regulator part number or description
Returns:
Extracted voltage as a string or "unknown" if not found
"""
# Common patterns:
# 78xx/79xx series: 7805 = 5V, 7812 = 12V
# LDOs often have voltage in the part number, like LM1117-3.3
# 78xx/79xx series
match = re.search(r'78(\d\d)|79(\d\d)', value, re.IGNORECASE)
if match:
group = match.group(1) or match.group(2)
# Convert code to voltage (e.g., 05 -> 5V, 12 -> 12V)
try:
voltage = int(group)
# For 78xx series, voltage code is directly in volts
if voltage < 50: # Sanity check to prevent weird values
return f"{voltage}V"
except ValueError:
pass
# Look for common voltage indicators in the string
voltage_patterns = [
r'(\d+\.?\d*)V', # 3.3V, 5V, etc.
r'-(\d+\.?\d*)V', # -5V, -12V, etc. (for negative regulators)
r'(\d+\.?\d*)[_-]?V', # 3.3_V, 5-V, etc.
r'[_-](\d+\.?\d*)', # LM1117-3.3, LD1117-3.3, etc.
]
for pattern in voltage_patterns:
match = re.search(pattern, value, re.IGNORECASE)
if match:
try:
voltage = float(match.group(1))
if 0 < voltage < 50: # Sanity check
# Format as integer if it's a whole number
if voltage.is_integer():
return f"{int(voltage)}V"
else:
return f"{voltage}V"
except ValueError:
pass
# Check for common fixed voltage regulators
regulators = {
"LM7805": "5V",
"LM7809": "9V",
"LM7812": "12V",
"LM7905": "-5V",
"LM7912": "-12V",
"LM1117-3.3": "3.3V",
"LM1117-5": "5V",
"LM317": "Adjustable",
"LM337": "Adjustable (Negative)",
"AP1117-3.3": "3.3V",
"AMS1117-3.3": "3.3V",
"L7805": "5V",
"L7812": "12V",
"MCP1700-3.3": "3.3V",
"MCP1700-5.0": "5V"
}
for reg, volt in regulators.items():
if re.search(re.escape(reg), value, re.IGNORECASE):
return volt
return "unknown"
def extract_frequency_from_value(value: str) -> str:
"""Extract frequency information from a component value or description.
Args:
value: Component value or description (e.g., "16MHz", "Crystal 8MHz")
Returns:
Frequency as a string or "unknown" if not found
"""
# Common frequency patterns with various units
frequency_patterns = [
r'(\d+\.?\d*)[\s-]*([kKmMgG]?)[hH][zZ]', # 16MHz, 32.768 kHz, etc.
r'(\d+\.?\d*)[\s-]*([kKmMgG])', # 16M, 32.768k, etc.
]
for pattern in frequency_patterns:
match = re.search(pattern, value, re.IGNORECASE)
if match:
try:
freq = float(match.group(1))
unit = match.group(2).upper() if match.group(2) else ""
# Make sure the frequency is in a reasonable range
if freq > 0:
# Format the output
if unit == "K":
if freq >= 1000:
return f"{freq/1000:.3f}MHz"
else:
return f"{freq:.3f}kHz"
elif unit == "M":
if freq >= 1000:
return f"{freq/1000:.3f}GHz"
else:
return f"{freq:.3f}MHz"
elif unit == "G":
return f"{freq:.3f}GHz"
else: # No unit, need to determine based on value
if freq < 1000:
return f"{freq:.3f}Hz"
elif freq < 1000000:
return f"{freq/1000:.3f}kHz"
elif freq < 1000000000:
return f"{freq/1000000:.3f}MHz"
else:
return f"{freq/1000000000:.3f}GHz"
except ValueError:
pass
# Check for common crystal frequencies
if "32.768" in value or "32768" in value:
return "32.768kHz" # Common RTC crystal
elif "16M" in value or "16MHZ" in value.upper():
return "16MHz" # Common MCU crystal
elif "8M" in value or "8MHZ" in value.upper():
return "8MHz"
elif "20M" in value or "20MHZ" in value.upper():
return "20MHz"
elif "27M" in value or "27MHZ" in value.upper():
return "27MHz"
elif "25M" in value or "25MHZ" in value.upper():
return "25MHz"
return "unknown"
def extract_resistance_value(value: str) -> Tuple[Optional[float], Optional[str]]:
"""Extract resistance value and unit from component value.
Args:
value: Resistance value (e.g., "10k", "4.7k", "100")
Returns:
Tuple of (numeric value, unit) or (None, None) if parsing fails
"""
# Common resistance patterns
# 10k, 4.7k, 100R, 1M, 10, etc.
match = re.search(r'(\d+\.?\d*)([kKmMrRΩ]?)', value)
if match:
try:
resistance = float(match.group(1))
unit = match.group(2).upper() if match.group(2) else "Ω"
# Normalize unit
if unit == "R" or unit == "":
unit = "Ω"
return resistance, unit
except ValueError:
pass
# Handle special case like "4k7" (means 4.7k)
match = re.search(r'(\d+)[kKmM](\d+)', value)
if match:
try:
value1 = int(match.group(1))
value2 = int(match.group(2))
resistance = float(f"{value1}.{value2}")
unit = "k" if "k" in value.lower() else "M" if "m" in value.lower() else "Ω"
return resistance, unit
except ValueError:
pass
return None, None
def extract_capacitance_value(value: str) -> Tuple[Optional[float], Optional[str]]:
"""Extract capacitance value and unit from component value.
Args:
value: Capacitance value (e.g., "10uF", "4.7nF", "100pF")
Returns:
Tuple of (numeric value, unit) or (None, None) if parsing fails
"""
# Common capacitance patterns
# 10uF, 4.7nF, 100pF, etc.
match = re.search(r'(\d+\.?\d*)([pPnNuUμF]+)', value)
if match:
try:
capacitance = float(match.group(1))
unit = match.group(2).lower()
# Normalize unit
if "p" in unit or "pf" in unit:
unit = "pF"
elif "n" in unit or "nf" in unit:
unit = "nF"
elif "u" in unit or "μ" in unit or "uf" in unit or "μf" in unit:
unit = "μF"
else:
unit = "F"
return capacitance, unit
except ValueError:
pass
# Handle special case like "4n7" (means 4.7nF)
match = re.search(r'(\d+)[pPnNuUμ](\d+)', value)
if match:
try:
value1 = int(match.group(1))
value2 = int(match.group(2))
capacitance = float(f"{value1}.{value2}")
if "p" in value.lower():
unit = "pF"
elif "n" in value.lower():
unit = "nF"
elif "u" in value.lower() or "μ" in value:
unit = "μF"
else:
unit = "F"
return capacitance, unit
except ValueError:
pass
return None, None
def extract_inductance_value(value: str) -> Tuple[Optional[float], Optional[str]]:
"""Extract inductance value and unit from component value.
Args:
value: Inductance value (e.g., "10uH", "4.7nH", "100mH")
Returns:
Tuple of (numeric value, unit) or (None, None) if parsing fails
"""
# Common inductance patterns
# 10uH, 4.7nH, 100mH, etc.
match = re.search(r'(\d+\.?\d*)([pPnNuUμmM][hH])', value)
if match:
try:
inductance = float(match.group(1))
unit = match.group(2).lower()
# Normalize unit
if "p" in unit:
unit = "pH"
elif "n" in unit:
unit = "nH"
elif "u" in unit or "μ" in unit:
unit = "μH"
elif "m" in unit:
unit = "mH"
else:
unit = "H"
return inductance, unit
except ValueError:
pass
# Handle special case like "4u7" (means 4.7uH)
match = re.search(r'(\d+)[pPnNuUμmM](\d+)[hH]', value)
if match:
try:
value1 = int(match.group(1))
value2 = int(match.group(2))
inductance = float(f"{value1}.{value2}")
if "p" in value.lower():
unit = "pH"
elif "n" in value.lower():
unit = "nH"
elif "u" in value.lower() or "μ" in value:
unit = "μH"
elif "m" in value.lower():
unit = "mH"
else:
unit = "H"
return inductance, unit
except ValueError:
pass
return None, None
def format_resistance(resistance: float, unit: str) -> str:
"""Format resistance value with appropriate unit.
Args:
resistance: Resistance value
unit: Unit string (Ω, k, M)
Returns:
Formatted resistance string
"""
if unit == "Ω":
return f"{resistance:.0f}Ω" if resistance.is_integer() else f"{resistance}Ω"
elif unit == "k":
return f"{resistance:.0f}" if resistance.is_integer() else f"{resistance}"
elif unit == "M":
return f"{resistance:.0f}" if resistance.is_integer() else f"{resistance}"
else:
return f"{resistance}{unit}"
def format_capacitance(capacitance: float, unit: str) -> str:
"""Format capacitance value with appropriate unit.
Args:
capacitance: Capacitance value
unit: Unit string (pF, nF, μF, F)
Returns:
Formatted capacitance string
"""
if capacitance.is_integer():
return f"{int(capacitance)}{unit}"
else:
return f"{capacitance}{unit}"
def format_inductance(inductance: float, unit: str) -> str:
"""Format inductance value with appropriate unit.
Args:
inductance: Inductance value
unit: Unit string (pH, nH, μH, mH, H)
Returns:
Formatted inductance string
"""
if inductance.is_integer():
return f"{int(inductance)}{unit}"
else:
return f"{inductance}{unit}"
def normalize_component_value(value: str, component_type: str) -> str:
"""Normalize a component value string based on component type.
Args:
value: Raw component value string
component_type: Type of component (R, C, L, etc.)
Returns:
Normalized value string
"""
if component_type == "R":
resistance, unit = extract_resistance_value(value)
if resistance is not None and unit is not None:
return format_resistance(resistance, unit)
elif component_type == "C":
capacitance, unit = extract_capacitance_value(value)
if capacitance is not None and unit is not None:
return format_capacitance(capacitance, unit)
elif component_type == "L":
inductance, unit = extract_inductance_value(value)
if inductance is not None and unit is not None:
return format_inductance(inductance, unit)
# For other component types or if parsing fails, return the original value
return value
def get_component_type_from_reference(reference: str) -> str:
"""Determine component type from reference designator.
Args:
reference: Component reference (e.g., R1, C2, U3)
Returns:
Component type letter (R, C, L, Q, etc.)
"""
# Extract the alphabetic prefix (component type)
match = re.match(r'^([A-Za-z_]+)', reference)
if match:
return match.group(1)
return ""
def is_power_component(component: Dict[str, Any]) -> bool:
"""Check if a component is likely a power-related component.
Args:
component: Component information dictionary
Returns:
True if the component is power-related, False otherwise
"""
ref = component.get("reference", "")
value = component.get("value", "").upper()
lib_id = component.get("lib_id", "").upper()
# Check reference designator
if ref.startswith(("VR", "PS", "REG")):
return True
# Check for power-related terms in value or library ID
power_terms = ["VCC", "VDD", "GND", "POWER", "PWR", "SUPPLY", "REGULATOR", "LDO"]
if any(term in value or term in lib_id for term in power_terms):
return True
# Check for regulator part numbers
regulator_patterns = [
r"78\d\d", # 7805, 7812, etc.
r"79\d\d", # 7905, 7912, etc.
r"LM\d{3}", # LM317, LM337, etc.
r"LM\d{4}", # LM1117, etc.
r"AMS\d{4}", # AMS1117, etc.
r"MCP\d{4}", # MCP1700, etc.
]
if any(re.search(pattern, value, re.IGNORECASE) for pattern in regulator_patterns):
return True
# Not identified as a power component
return False

View File

@ -0,0 +1,859 @@
"""
Circuit pattern recognition functions for KiCad schematics.
"""
import re
from typing import Dict, List, Any
from kicad_mcp.utils.component_utils import extract_voltage_from_regulator, extract_frequency_from_value
def identify_power_supplies(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify power supply circuits in the schematic.
Args:
components: Dictionary of components from netlist
nets: Dictionary of nets from netlist
Returns:
List of identified power supply circuits
"""
power_supplies = []
# Look for voltage regulators (Linear)
regulator_patterns = {
"78xx": r"78\d\d|LM78\d\d|MC78\d\d", # 7805, 7812, etc.
"79xx": r"79\d\d|LM79\d\d|MC79\d\d", # 7905, 7912, etc.
"LDO": r"LM\d{3}|LD\d{3}|AMS\d{4}|LT\d{4}|TLV\d{3}|AP\d{4}|MIC\d{4}|NCP\d{3}|LP\d{4}|L\d{2}|TPS\d{5}"
}
for ref, component in components.items():
# Check for voltage regulators by part value or lib_id
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
for reg_type, pattern in regulator_patterns.items():
if re.search(pattern, component_value, re.IGNORECASE) or re.search(pattern, component_lib, re.IGNORECASE):
# Found a regulator, look for associated components
power_supplies.append({
"type": "linear_regulator",
"subtype": reg_type,
"main_component": ref,
"value": component_value,
"input_voltage": "unknown", # Would need more analysis to determine
"output_voltage": extract_voltage_from_regulator(component_value),
"associated_components": [] # Would need connection analysis to find these
})
# Look for switching regulators
switching_patterns = {
"buck": r"LM\d{4}|TPS\d{4}|MP\d{4}|RT\d{4}|LT\d{4}|MC\d{4}|NCP\d{4}|TL\d{4}|LTC\d{4}",
"boost": r"MC\d{4}|LT\d{4}|TPS\d{4}|MAX\d{4}|NCP\d{4}|LTC\d{4}",
"buck_boost": r"LTC\d{4}|LM\d{4}|TPS\d{4}|MAX\d{4}"
}
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
# Check for inductor (key component in switching supplies)
if ref.startswith('L') or 'Inductor' in component_lib:
# Look for nearby ICs that might be switching controllers
for ic_ref, ic_component in components.items():
if ic_ref.startswith('U') or ic_ref.startswith('IC'):
ic_value = ic_component.get('value', '').upper()
ic_lib = ic_component.get('lib_id', '').upper()
for converter_type, pattern in switching_patterns.items():
if re.search(pattern, ic_value, re.IGNORECASE) or re.search(pattern, ic_lib, re.IGNORECASE):
power_supplies.append({
"type": "switching_regulator",
"subtype": converter_type,
"main_component": ic_ref,
"inductor": ref,
"value": ic_value
})
return power_supplies
def identify_amplifiers(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify amplifier circuits in the schematic.
Args:
components: Dictionary of components from netlist
nets: Dictionary of nets from netlist
Returns:
List of identified amplifier circuits
"""
amplifiers = []
# Look for op-amps
opamp_patterns = [
r"LM\d{3}|TL\d{3}|NE\d{3}|LF\d{3}|OP\d{2}|MCP\d{3}|AD\d{3}|LT\d{4}|OPA\d{3}",
r"Opamp|Op-Amp|OpAmp|Operational Amplifier"
]
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
# Check for op-amps
for pattern in opamp_patterns:
if re.search(pattern, component_value, re.IGNORECASE) or re.search(pattern, component_lib, re.IGNORECASE):
# Common op-amps
if re.search(r"LM358|LM324|TL072|TL082|NE5532|LF353|MCP6002|AD8620|OPA2134", component_value, re.IGNORECASE):
amplifiers.append({
"type": "operational_amplifier",
"subtype": "general_purpose",
"component": ref,
"value": component_value
})
# Audio op-amps
elif re.search(r"NE5534|OPA134|OPA1612|OPA1652|LM4562|LME49720|LME49860|TL071|TL072", component_value, re.IGNORECASE):
amplifiers.append({
"type": "operational_amplifier",
"subtype": "audio",
"component": ref,
"value": component_value
})
# Instrumentation amplifiers
elif re.search(r"INA\d{3}|AD620|AD8221|AD8429|LT1167", component_value, re.IGNORECASE):
amplifiers.append({
"type": "operational_amplifier",
"subtype": "instrumentation",
"component": ref,
"value": component_value
})
else:
amplifiers.append({
"type": "operational_amplifier",
"subtype": "unknown",
"component": ref,
"value": component_value
})
# Look for transistor amplifiers
transistor_refs = [ref for ref in components.keys() if ref.startswith('Q')]
for ref in transistor_refs:
component = components[ref]
component_lib = component.get('lib_id', '').upper()
# Check if it's a BJT or FET
if 'BJT' in component_lib or 'NPN' in component_lib or 'PNP' in component_lib:
# Look for resistors connected to transistor (biasing network)
has_biasing = False
for net_name, pins in nets.items():
# Check if this net connects to our transistor
if any(pin.get('component') == ref for pin in pins):
# Check if the net also connects to resistors
if any(pin.get('component', '').startswith('R') for pin in pins):
has_biasing = True
break
if has_biasing:
amplifiers.append({
"type": "transistor_amplifier",
"subtype": "BJT",
"component": ref,
"value": component.get('value', '')
})
elif 'FET' in component_lib or 'MOSFET' in component_lib or 'JFET' in component_lib:
# Similar check for FET amplifiers
has_biasing = False
for net_name, pins in nets.items():
if any(pin.get('component') == ref for pin in pins):
if any(pin.get('component', '').startswith('R') for pin in pins):
has_biasing = True
break
if has_biasing:
amplifiers.append({
"type": "transistor_amplifier",
"subtype": "FET",
"component": ref,
"value": component.get('value', '')
})
# Look for audio amplifier ICs
audio_amp_patterns = [
r"LM386|LM383|LM380|LM1875|LM3886|TDA\d{4}|TPA\d{4}|SSM\d{4}|PAM\d{4}|TAS\d{4}"
]
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
for pattern in audio_amp_patterns:
if re.search(pattern, component_value, re.IGNORECASE) or re.search(pattern, component_lib, re.IGNORECASE):
amplifiers.append({
"type": "audio_amplifier_ic",
"component": ref,
"value": component_value
})
return amplifiers
def identify_filters(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify filter circuits in the schematic.
Args:
components: Dictionary of components from netlist
nets: Dictionary of nets from netlist
Returns:
List of identified filter circuits
"""
filters = []
# Look for RC low-pass filters
# These typically have a resistor followed by a capacitor to ground
resistor_refs = [ref for ref in components.keys() if ref.startswith('R')]
capacitor_refs = [ref for ref in components.keys() if ref.startswith('C')]
for r_ref in resistor_refs:
r_nets = []
# Find which nets this resistor is connected to
for net_name, pins in nets.items():
if any(pin.get('component') == r_ref for pin in pins):
r_nets.append(net_name)
# For each net, check if there's a capacitor connected to it
for net_name in r_nets:
# Find capacitors connected to this net
connected_caps = []
for pin in nets.get(net_name, []):
comp = pin.get('component')
if comp and comp.startswith('C'):
connected_caps.append(comp)
if connected_caps:
# Check if the other side of the capacitor goes to ground
for c_ref in connected_caps:
c_is_to_ground = False
for gnd_name in ['GND', 'AGND', 'DGND', 'VSS']:
for pin in nets.get(gnd_name, []):
if pin.get('component') == c_ref:
c_is_to_ground = True
break
if c_is_to_ground:
break
if c_is_to_ground:
filters.append({
"type": "passive_filter",
"subtype": "rc_low_pass",
"components": [r_ref, c_ref]
})
# Look for active filters (op-amp with feedback RC components)
opamp_refs = []
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
if re.search(r"LM\d{3}|TL\d{3}|NE\d{3}|LF\d{3}|OP\d{2}|MCP\d{3}|AD\d{3}|LT\d{4}|OPA\d{3}",
component_value, re.IGNORECASE) or "OP_AMP" in component_lib:
opamp_refs.append(ref)
for op_ref in opamp_refs:
# Find op-amp output
# In a full implementation, we'd know which pin is the output
# For simplicity, we'll look for feedback components
has_feedback_r = False
has_feedback_c = False
for net_name, pins in nets.items():
# If this net connects to our op-amp
if any(pin.get('component') == op_ref for pin in pins):
# Check if it also connects to resistors and capacitors
connects_to_r = any(pin.get('component', '').startswith('R') for pin in pins)
connects_to_c = any(pin.get('component', '').startswith('C') for pin in pins)
if connects_to_r:
has_feedback_r = True
if connects_to_c:
has_feedback_c = True
if has_feedback_r and has_feedback_c:
filters.append({
"type": "active_filter",
"main_component": op_ref,
"value": components[op_ref].get('value', '')
})
# Look for crystal filters or ceramic filters
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
if ref.startswith('Y') or ref.startswith('X') or "CRYSTAL" in component_lib or "XTAL" in component_lib:
filters.append({
"type": "crystal_filter",
"component": ref,
"value": component_value
})
if "FILTER" in component_lib or "MURATA" in component_lib or "CERAMIC_FILTER" in component_lib:
filters.append({
"type": "ceramic_filter",
"component": ref,
"value": component_value
})
return filters
def identify_oscillators(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify oscillator circuits in the schematic.
Args:
components: Dictionary of components from netlist
nets: Dictionary of nets from netlist
Returns:
List of identified oscillator circuits
"""
oscillators = []
# Look for crystal oscillators
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
# Crystals
if ref.startswith('Y') or ref.startswith('X') or "CRYSTAL" in component_lib or "XTAL" in component_lib:
# Check if the crystal has load capacitors
has_load_caps = False
crystal_nets = []
for net_name, pins in nets.items():
if any(pin.get('component') == ref for pin in pins):
crystal_nets.append(net_name)
# Look for capacitors connected to the crystal nets
for net_name in crystal_nets:
for pin in nets.get(net_name, []):
comp = pin.get('component')
if comp and comp.startswith('C'):
has_load_caps = True
break
if has_load_caps:
break
oscillators.append({
"type": "crystal_oscillator",
"component": ref,
"value": component_value,
"frequency": extract_frequency_from_value(component_value),
"has_load_capacitors": has_load_caps
})
# Oscillator ICs
if "OSC" in component_lib or "OSCILLATOR" in component_lib or re.search(r"OSC|OSCILLATOR", component_value, re.IGNORECASE):
oscillators.append({
"type": "oscillator_ic",
"component": ref,
"value": component_value,
"frequency": extract_frequency_from_value(component_value)
})
# RC oscillators (555 timer, etc)
if re.search(r"NE555|LM555|ICM7555|TLC555", component_value, re.IGNORECASE) or "555" in component_lib:
oscillators.append({
"type": "rc_oscillator",
"subtype": "555_timer",
"component": ref,
"value": component_value
})
return oscillators
def identify_digital_interfaces(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify digital interface circuits in the schematic.
Args:
components: Dictionary of components from netlist
nets: Dictionary of nets from netlist
Returns:
List of identified digital interface circuits
"""
interfaces = []
# I2C interface detection
i2c_signals = {"SCL", "SDA", "I2C_SCL", "I2C_SDA"}
has_i2c = False
for net_name in nets.keys():
if any(signal in net_name.upper() for signal in i2c_signals):
has_i2c = True
break
if has_i2c:
interfaces.append({
"type": "i2c_interface",
"signals_found": [net for net in nets.keys() if any(signal in net.upper() for signal in i2c_signals)]
})
# SPI interface detection
spi_signals = {"MOSI", "MISO", "SCK", "SS", "SPI_MOSI", "SPI_MISO", "SPI_SCK", "SPI_CS"}
has_spi = False
for net_name in nets.keys():
if any(signal in net_name.upper() for signal in spi_signals):
has_spi = True
break
if has_spi:
interfaces.append({
"type": "spi_interface",
"signals_found": [net for net in nets.keys() if any(signal in net.upper() for signal in spi_signals)]
})
# UART interface detection
uart_signals = {"TX", "RX", "TXD", "RXD", "UART_TX", "UART_RX"}
has_uart = False
for net_name in nets.keys():
if any(signal in net_name.upper() for signal in uart_signals):
has_uart = True
break
if has_uart:
interfaces.append({
"type": "uart_interface",
"signals_found": [net for net in nets.keys() if any(signal in net.upper() for signal in uart_signals)]
})
# USB interface detection
usb_signals = {"USB_D+", "USB_D-", "USB_DP", "USB_DM", "D+", "D-", "DP", "DM", "VBUS"}
has_usb = False
for net_name in nets.keys():
if any(signal in net_name.upper() for signal in usb_signals):
has_usb = True
break
# Also check for USB interface ICs
for ref, component in components.items():
component_value = component.get('value', '').upper()
if re.search(r"FT232|CH340|CP210|MCP2200|TUSB|FT231|FT201", component_value, re.IGNORECASE):
has_usb = True
break
if has_usb:
interfaces.append({
"type": "usb_interface",
"signals_found": [net for net in nets.keys() if any(signal in net.upper() for signal in usb_signals)]
})
# Ethernet interface detection
ethernet_signals = {"TX+", "TX-", "RX+", "RX-", "MDI", "MDIO", "ETH"}
has_ethernet = False
for net_name in nets.keys():
if any(signal in net_name.upper() for signal in ethernet_signals):
has_ethernet = True
break
# Also check for Ethernet PHY ICs
for ref, component in components.items():
component_value = component.get('value', '').upper()
if re.search(r"W5500|ENC28J60|LAN87|KSZ80|DP83|RTL8|AX88", component_value, re.IGNORECASE):
has_ethernet = True
break
if has_ethernet:
interfaces.append({
"type": "ethernet_interface",
"signals_found": [net for net in nets.keys() if any(signal in net.upper() for signal in ethernet_signals)]
})
return interfaces
def identify_sensor_interfaces(components: Dict[str, Any], nets: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify sensor interface circuits in the schematic.
Args:
components: Dictionary of components from netlist
nets: Dictionary of nets from netlist
Returns:
List of identified sensor interface circuits
"""
sensor_interfaces = []
# Common sensor IC patterns
sensor_patterns = {
"temperature": r"LM35|DS18B20|DHT11|DHT22|BME280|BMP280|TMP\d+|MCP9808|MAX31855|MAX6675|SI7021|HTU21|SHT[0123]\d|PCT2075",
"humidity": r"DHT11|DHT22|BME280|SI7021|HTU21|SHT[0123]\d|HDC1080",
"pressure": r"BMP\d+|BME280|LPS\d+|MS5611|DPS310|MPL3115|SPL06",
"accelerometer": r"ADXL\d+|LIS3DH|MMA\d+|MPU\d+|LSM\d+|BMI\d+|BMA\d+|KX\d+",
"gyroscope": r"L3G\d+|MPU\d+|BMI\d+|LSM\d+|ICM\d+",
"magnetometer": r"HMC\d+|QMC\d+|LSM\d+|MMC\d+|RM\d+",
"proximity": r"APDS9960|VL53L0X|VL6180|GP2Y|VCNL4040|VCNL4010",
"light": r"BH1750|TSL\d+|MAX4\d+|VEML\d+|APDS9960|LTR329|OPT\d+",
"air_quality": r"CCS811|BME680|SGP\d+|SEN\d+|MQ\d+|MiCS",
"current": r"ACS\d+|INA\d+|MAX\d+|ZXCT\d+",
"voltage": r"INA\d+|MCP\d+|ADS\d+",
"ADC": r"ADS\d+|MCP33\d+|MCP32\d+|LTC\d+|NAU7802|HX711",
"GPS": r"NEO-[67]M|L80|MTK\d+|SIM\d+|SAM-M8Q|MAX-M8"
}
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
for sensor_type, pattern in sensor_patterns.items():
if re.search(pattern, component_value, re.IGNORECASE) or re.search(pattern, component_lib, re.IGNORECASE):
# Identify specific sensors
# Temperature sensors
if sensor_type == "temperature":
if re.search(r"DS18B20", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "temperature_sensor",
"model": "DS18B20",
"component": ref,
"interface": "1-Wire",
"range": "-55°C to +125°C"
})
elif re.search(r"BME280|BMP280", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "multi_sensor",
"model": component_value,
"component": ref,
"measures": ["temperature", "pressure", "humidity" if "BME" in component_value else "pressure"],
"interface": "I2C/SPI"
})
elif re.search(r"LM35", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "temperature_sensor",
"model": "LM35",
"component": ref,
"interface": "Analog",
"range": "0°C to +100°C"
})
else:
sensor_interfaces.append({
"type": "temperature_sensor",
"model": component_value,
"component": ref
})
# Motion sensors (accelerometer, gyroscope, etc.)
elif sensor_type in ["accelerometer", "gyroscope"]:
if re.search(r"MPU6050", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "motion_sensor",
"model": "MPU6050",
"component": ref,
"measures": ["accelerometer", "gyroscope"],
"interface": "I2C"
})
elif re.search(r"MPU9250", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "motion_sensor",
"model": "MPU9250",
"component": ref,
"measures": ["accelerometer", "gyroscope", "magnetometer"],
"interface": "I2C/SPI"
})
elif re.search(r"LSM6DS3", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "motion_sensor",
"model": "LSM6DS3",
"component": ref,
"measures": ["accelerometer", "gyroscope"],
"interface": "I2C/SPI"
})
else:
sensor_interfaces.append({
"type": "motion_sensor",
"model": component_value,
"component": ref,
"measures": [sensor_type]
})
# Light and proximity sensors
elif sensor_type in ["light", "proximity"]:
if re.search(r"APDS9960", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "optical_sensor",
"model": "APDS9960",
"component": ref,
"measures": ["proximity", "light", "gesture", "color"],
"interface": "I2C"
})
elif re.search(r"VL53L0X", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "optical_sensor",
"model": "VL53L0X",
"component": ref,
"measures": ["time-of-flight distance"],
"interface": "I2C",
"range": "Up to 2m"
})
elif re.search(r"BH1750", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "optical_sensor",
"model": "BH1750",
"component": ref,
"measures": ["ambient light"],
"interface": "I2C"
})
else:
sensor_interfaces.append({
"type": "optical_sensor",
"model": component_value,
"component": ref,
"measures": [sensor_type]
})
# ADCs (often used for sensor interfaces)
elif sensor_type == "ADC":
if re.search(r"ADS1115", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "analog_interface",
"model": "ADS1115",
"component": ref,
"resolution": "16-bit",
"channels": 4,
"interface": "I2C"
})
elif re.search(r"HX711", component_value, re.IGNORECASE):
sensor_interfaces.append({
"type": "analog_interface",
"model": "HX711",
"component": ref,
"resolution": "24-bit",
"common_usage": "Load cell/strain gauge",
"interface": "Digital"
})
else:
sensor_interfaces.append({
"type": "analog_interface",
"model": component_value,
"component": ref
})
# Other types of sensors
else:
sensor_interfaces.append({
"type": f"{sensor_type}_sensor",
"model": component_value,
"component": ref
})
# Once identified a component as a specific sensor, no need to check other types
break
# Look for common analog sensors
# These often don't have specific ICs but have designators like "RT" for thermistors
thermistor_refs = [ref for ref in components.keys() if ref.startswith('RT') or ref.startswith('TH')]
for ref in thermistor_refs:
component = components[ref]
sensor_interfaces.append({
"type": "temperature_sensor",
"subtype": "thermistor",
"component": ref,
"value": component.get('value', ''),
"interface": "Analog"
})
# Look for photodiodes, photoresistors (LDRs)
photosensor_refs = [ref for ref in components.keys() if ref.startswith('PD') or ref.startswith('LDR')]
for ref in photosensor_refs:
component = components[ref]
sensor_interfaces.append({
"type": "optical_sensor",
"subtype": "photosensor",
"component": ref,
"value": component.get('value', ''),
"interface": "Analog"
})
# Look for potentiometers (often used for manual sensing/control)
pot_refs = [ref for ref in components.keys() if ref.startswith('RV') or ref.startswith('POT')]
for ref in pot_refs:
component = components[ref]
sensor_interfaces.append({
"type": "position_sensor",
"subtype": "potentiometer",
"component": ref,
"value": component.get('value', ''),
"interface": "Analog"
})
return sensor_interfaces
def identify_microcontrollers(components: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Identify microcontroller circuits in the schematic.
Args:
components: Dictionary of components from netlist
Returns:
List of identified microcontroller circuits
"""
microcontrollers = []
# Common microcontroller families
mcu_patterns = {
"AVR": r"ATMEGA\d+|ATTINY\d+|AT90\w+",
"STM32": r"STM32\w+",
"PIC": r"PIC\d+\w+",
"ESP": r"ESP32|ESP8266",
"Arduino": r"ARDUINO",
"MSP430": r"MSP430\w+",
"RP2040": r"RP2040|PICO",
"NXP": r"LPC\d+|IMXRT\d+|MK\d+",
"SAM": r"SAMD\d+|SAM\w+",
"ARM Cortex": r"CORTEX|ARM",
"8051": r"8051|AT89"
}
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
for family, pattern in mcu_patterns.items():
if re.search(pattern, component_value, re.IGNORECASE) or re.search(pattern, component_lib, re.IGNORECASE):
# Identify specific models
identified = False
# ATmega328P (Arduino Uno/Nano)
if re.search(r"ATMEGA328P|ATMEGA328", component_value, re.IGNORECASE):
microcontrollers.append({
"type": "microcontroller",
"family": "AVR",
"model": "ATmega328P",
"component": ref,
"common_usage": "Arduino Uno/Nano compatible"
})
identified = True
# ATmega32U4 (Arduino Leonardo/Micro)
elif re.search(r"ATMEGA32U4", component_value, re.IGNORECASE):
microcontrollers.append({
"type": "microcontroller",
"family": "AVR",
"model": "ATmega32U4",
"component": ref,
"common_usage": "Arduino Leonardo/Micro compatible"
})
identified = True
# ESP32
elif re.search(r"ESP32", component_value, re.IGNORECASE):
microcontrollers.append({
"type": "microcontroller",
"family": "ESP",
"model": "ESP32",
"component": ref,
"features": "Wi-Fi & Bluetooth"
})
identified = True
# ESP8266
elif re.search(r"ESP8266", component_value, re.IGNORECASE):
microcontrollers.append({
"type": "microcontroller",
"family": "ESP",
"model": "ESP8266",
"component": ref,
"features": "Wi-Fi"
})
identified = True
# STM32 series
elif re.search(r"STM32F\d+", component_value, re.IGNORECASE):
model = re.search(r"(STM32F\d+)", component_value, re.IGNORECASE).group(1)
microcontrollers.append({
"type": "microcontroller",
"family": "STM32",
"model": model.upper(),
"component": ref,
"features": "ARM Cortex-M"
})
identified = True
# Raspberry Pi Pico (RP2040)
elif re.search(r"RP2040|PICO", component_value, re.IGNORECASE):
microcontrollers.append({
"type": "microcontroller",
"family": "RP2040",
"model": "RP2040",
"component": ref,
"common_usage": "Raspberry Pi Pico"
})
identified = True
# PIC microcontrollers
elif re.search(r"PIC\d+", component_value, re.IGNORECASE):
model = re.search(r"(PIC\d+\w+)", component_value, re.IGNORECASE)
if model:
microcontrollers.append({
"type": "microcontroller",
"family": "PIC",
"model": model.group(1).upper(),
"component": ref
})
identified = True
# MSP430 series
elif re.search(r"MSP430\w+", component_value, re.IGNORECASE):
model = re.search(r"(MSP430\w+)", component_value, re.IGNORECASE)
if model:
microcontrollers.append({
"type": "microcontroller",
"family": "MSP430",
"model": model.group(1).upper(),
"component": ref,
"features": "Ultra-low power"
})
identified = True
# If not identified specifically but matches a family
if not identified:
microcontrollers.append({
"type": "microcontroller",
"family": family,
"component": ref,
"value": component_value
})
# Once identified a component as a microcontroller, no need to check other families
break
# Look for microcontroller development boards
dev_board_patterns = {
"Arduino": r"ARDUINO|UNO|NANO|MEGA|LEONARDO|DUE",
"ESP32 Dev Board": r"ESP32-DEVKIT|NODEMCU-32S|ESP-WROOM-32",
"ESP8266 Dev Board": r"NODEMCU|WEMOS|D1_MINI|ESP-01",
"STM32 Dev Board": r"NUCLEO|DISCOVERY|BLUEPILL",
"Raspberry Pi": r"RASPBERRY|RPI|RPICO|PICO"
}
for ref, component in components.items():
component_value = component.get('value', '').upper()
component_lib = component.get('lib_id', '').upper()
for board_type, pattern in dev_board_patterns.items():
if re.search(pattern, component_value, re.IGNORECASE) or re.search(pattern, component_lib, re.IGNORECASE):
microcontrollers.append({
"type": "development_board",
"board_type": board_type,
"component": ref,
"value": component_value
})
break
return microcontrollers