diff --git a/README.md b/README.md index ad805d1..f29f497 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/pattern_guide.md b/docs/pattern_guide.md new file mode 100644 index 0000000..b457f72 --- /dev/null +++ b/docs/pattern_guide.md @@ -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! diff --git a/kicad_mcp/resources/pattern_resources.py b/kicad_mcp/resources/pattern_resources.py new file mode 100644 index 0000000..6385e2a --- /dev/null +++ b/kicad_mcp/resources/pattern_resources.py @@ -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)}" diff --git a/kicad_mcp/server.py b/kicad_mcp/server.py index a750ab3..b730b01 100644 --- a/kicad_mcp/server.py +++ b/kicad_mcp/server.py @@ -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 diff --git a/kicad_mcp/tools/pattern_tools.py b/kicad_mcp/tools/pattern_tools.py new file mode 100644 index 0000000..65734d8 --- /dev/null +++ b/kicad_mcp/tools/pattern_tools.py @@ -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)} diff --git a/kicad_mcp/utils/component_utils.py b/kicad_mcp/utils/component_utils.py new file mode 100644 index 0000000..0ed134b --- /dev/null +++ b/kicad_mcp/utils/component_utils.py @@ -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}kΩ" if resistance.is_integer() else f"{resistance}kΩ" + elif unit == "M": + return f"{resistance:.0f}MΩ" if resistance.is_integer() else f"{resistance}MΩ" + 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 diff --git a/kicad_mcp/utils/pattern_recognition.py b/kicad_mcp/utils/pattern_recognition.py new file mode 100644 index 0000000..958f1c6 --- /dev/null +++ b/kicad_mcp/utils/pattern_recognition.py @@ -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