Add comprehensive BOM management functionality with analysis, export, and viewing features
This commit is contained in:
parent
c69bd66f71
commit
6953e5dc9a
218
docs/bom_guide.md
Normal file
218
docs/bom_guide.md
Normal file
@ -0,0 +1,218 @@
|
||||
# KiCad BOM Management Guide
|
||||
|
||||
This guide explains how to use the Bill of Materials (BOM) features in the KiCad MCP Server.
|
||||
|
||||
## Overview
|
||||
|
||||
The BOM functionality allows you to:
|
||||
|
||||
1. Analyze component usage in your KiCad projects
|
||||
2. Export BOMs from your schematics
|
||||
3. Estimate project costs
|
||||
4. View BOM data in various formats (Markdown, CSV, JSON)
|
||||
5. Get assistance with component sourcing and optimization
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Example Prompt |
|
||||
|------|---------------|
|
||||
| Analyze components | `Analyze the BOM for my KiCad project at /path/to/project.kicad_pro` |
|
||||
| Export a BOM | `Export a BOM for my KiCad project at /path/to/project.kicad_pro` |
|
||||
| View formatted report | `Show me the BOM report for /path/to/project.kicad_pro` |
|
||||
| Get raw CSV data | `Show me the CSV BOM data for /path/to/project.kicad_pro` |
|
||||
| Get JSON data | `Show me the JSON BOM data for /path/to/project.kicad_pro` |
|
||||
|
||||
## Using BOM Features
|
||||
|
||||
### Analyzing an Existing BOM
|
||||
|
||||
To analyze a BOM that already exists in your project:
|
||||
|
||||
1. In your MCP client, request analysis of your project's BOM:
|
||||
|
||||
```
|
||||
Please analyze the BOM for my KiCad project at /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
|
||||
```
|
||||
|
||||
The `analyze_bom` tool will:
|
||||
- Search for BOM files in your project directory
|
||||
- Parse and analyze the component data
|
||||
- Generate a comprehensive report with component counts, categories, and cost estimates (if available)
|
||||
- Provide insights into your component usage
|
||||
|
||||
### Exporting a New BOM
|
||||
|
||||
If you don't have a BOM yet, you can export one directly:
|
||||
|
||||
```
|
||||
Can you export a BOM for my KiCad project at /Users/username/Documents/KiCad/my_project/my_project.kicad_pro?
|
||||
```
|
||||
|
||||
The `export_bom_csv` tool will:
|
||||
- Find the schematic file in your project
|
||||
- Use KiCad's command-line tools to generate a new BOM
|
||||
- Save the BOM in your project directory
|
||||
- Provide a path to the generated file
|
||||
|
||||
### Viewing BOM Information
|
||||
|
||||
There are several ways to view your BOM data:
|
||||
|
||||
#### Formatted BOM Report
|
||||
|
||||
For a well-formatted report with component analysis:
|
||||
|
||||
```
|
||||
Show me the BOM report for /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
|
||||
```
|
||||
|
||||
This will load the `kicad://bom/project_path` resource, showing:
|
||||
- Total component count
|
||||
- Breakdown by component type
|
||||
- Cost estimates (if available)
|
||||
- A table of components
|
||||
- Suggestions for optimization
|
||||
|
||||
#### Raw BOM Data
|
||||
|
||||
To get the raw BOM data in CSV format:
|
||||
|
||||
```
|
||||
Can I see the raw CSV BOM data for /Users/username/Documents/KiCad/my_project/my_project.kicad_pro?
|
||||
```
|
||||
|
||||
This will load the `kicad://bom/project_path/csv` resource, providing:
|
||||
- The raw CSV data from your BOM file
|
||||
- Ideal for importing into spreadsheets or other tools
|
||||
|
||||
#### JSON Data
|
||||
|
||||
For structured data access:
|
||||
|
||||
```
|
||||
Show me the JSON BOM data for /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
|
||||
```
|
||||
|
||||
This will load the `kicad://bom/project_path/json` resource, providing:
|
||||
- Structured JSON representation of all BOM data
|
||||
- Component analysis in machine-readable format
|
||||
- Useful for programmatic processing
|
||||
|
||||
### Getting Help with BOM Tasks
|
||||
|
||||
The KiCad MCP Server provides several prompt templates for BOM-related tasks:
|
||||
|
||||
1. **Analyze Components** - Help with analyzing your component usage
|
||||
2. **Cost Estimation** - Assistance with estimating project costs
|
||||
3. **BOM Export Help** - Guidance on exporting BOMs from KiCad
|
||||
4. **Component Sourcing** - Help with finding and sourcing components
|
||||
5. **BOM Comparison** - Compare BOMs between different project versions
|
||||
|
||||
To use these prompts, click on the prompt templates button in your MCP client, then select the desired BOM template.
|
||||
|
||||
## Understanding BOM Results
|
||||
|
||||
### Component Categories
|
||||
|
||||
The BOM analysis provides a breakdown of component categories, such as:
|
||||
|
||||
| Category | Description | Example Components |
|
||||
|----------|-------------|-------------------|
|
||||
| Resistors | Current-limiting or voltage-dividing components | R1, R2, R3 |
|
||||
| Capacitors | Energy storage and filtering components | C1, C2, C3 |
|
||||
| ICs | Integrated circuits | U1, U2, U3 |
|
||||
| Connectors | Board-to-board or board-to-wire connectors | J1, J2, J3 |
|
||||
| Transistors | Switching or amplifying components | Q1, Q2, Q3 |
|
||||
| Diodes | Unidirectional current flow components | D1, D2, D3 |
|
||||
|
||||
### Cost Information
|
||||
|
||||
The BOM analysis will attempt to extract cost information if it's available in your BOM file. This includes:
|
||||
|
||||
- Individual component costs
|
||||
- Total project cost
|
||||
- Breakdown of cost by component type
|
||||
|
||||
To include cost information in your BOM, add a "Cost" or "Price" column to your KiCad component fields, or include this information when exporting your BOM.
|
||||
|
||||
## Tips for Better BOM Management
|
||||
|
||||
### Structure Your BOM Export
|
||||
|
||||
When exporting a BOM from KiCad:
|
||||
|
||||
1. Use descriptive component values
|
||||
2. Add meaningful component descriptions
|
||||
3. Group components logically
|
||||
4. Include footprint information
|
||||
5. Add supplier part numbers where possible
|
||||
6. Include cost information for better estimates
|
||||
|
||||
### Optimize Your Component Selection
|
||||
|
||||
Based on BOM analysis, consider:
|
||||
|
||||
- Standardizing component values (e.g., using the same resistor values across the design)
|
||||
- Reducing the variety of footprints
|
||||
- Selecting commonly available components
|
||||
- Using components from the same supplier where possible
|
||||
- Finding alternatives for expensive or hard-to-find components
|
||||
|
||||
### Keep BOM Files Updated
|
||||
|
||||
When making changes to your schematic:
|
||||
|
||||
1. Re-export your BOM after significant changes
|
||||
2. Compare with previous versions to identify changes
|
||||
3. Verify that all components are still available
|
||||
4. Update cost estimates regularly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### BOM Analysis Fails
|
||||
|
||||
If BOM analysis fails:
|
||||
|
||||
1. Ensure your BOM file is in a supported format (CSV, XML, or JSON)
|
||||
2. Check that the file is not corrupted or empty
|
||||
3. Verify that the BOM file is in your project directory
|
||||
4. Try exporting a new BOM from KiCad
|
||||
5. Check for unusual characters or formatting in your BOM
|
||||
|
||||
### BOM Export Fails
|
||||
|
||||
If BOM export fails:
|
||||
|
||||
1. Make sure KiCad is properly installed on your system
|
||||
2. Verify that your schematic file exists and is valid
|
||||
3. Check that you have write permissions in your project directory
|
||||
4. Look for KiCad command-line tools in your KiCad installation
|
||||
5. Try exporting manually from KiCad to see if there are specific errors
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom BOM Analysis
|
||||
|
||||
For deeper analysis of your BOM, you can ask specific questions about:
|
||||
|
||||
1. Component distribution
|
||||
2. Cost optimization
|
||||
3. Footprint standardization
|
||||
4. Supply chain considerations
|
||||
5. Design for manufacturing improvements
|
||||
|
||||
Simply ask with your specific query, referencing your BOM:
|
||||
|
||||
```
|
||||
Looking at the BOM for my project at /path/to/project.kicad_pro, can you suggest ways to reduce the variety of resistor values while maintaining the same functionality?
|
||||
```
|
||||
|
||||
### Comparing Multiple Projects
|
||||
|
||||
To compare BOMs across different projects or revisions:
|
||||
|
||||
```
|
||||
Can you compare the BOMs between my projects at /path/to/project_v1.kicad_pro and /path/to/project_v2.kicad_pro?
|
||||
```
|
||||
|
||||
This allows you to see how component selection evolves across design iterations.
|
117
kicad_mcp/prompts/bom_prompts.py
Normal file
117
kicad_mcp/prompts/bom_prompts.py
Normal file
@ -0,0 +1,117 @@
|
||||
"""
|
||||
BOM-related prompt templates for KiCad.
|
||||
"""
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
def register_bom_prompts(mcp: FastMCP) -> None:
|
||||
"""Register BOM-related prompt templates with the MCP server.
|
||||
|
||||
Args:
|
||||
mcp: The FastMCP server instance
|
||||
"""
|
||||
|
||||
@mcp.prompt()
|
||||
def analyze_components() -> str:
|
||||
"""Prompt for analyzing a KiCad project's components."""
|
||||
prompt = """
|
||||
I'd like to analyze the components used in my KiCad PCB design. Can you help me with:
|
||||
|
||||
1. Identifying all the components in my design
|
||||
2. Analyzing the distribution of component types
|
||||
3. Checking for any potential issues or opportunities for optimization
|
||||
4. Suggesting any alternatives for hard-to-find or expensive components
|
||||
|
||||
My KiCad project is located at:
|
||||
[Enter the full path to your .kicad_pro file here]
|
||||
|
||||
Please use the BOM analysis tools to help me understand my component usage.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
@mcp.prompt()
|
||||
def cost_estimation() -> str:
|
||||
"""Prompt for estimating project costs based on BOM."""
|
||||
prompt = """
|
||||
I need to estimate the cost of my KiCad PCB project for:
|
||||
|
||||
1. A prototype run (1-5 boards)
|
||||
2. A small production run (10-100 boards)
|
||||
3. Larger scale production (500+ boards)
|
||||
|
||||
My KiCad project is located at:
|
||||
[Enter the full path to your .kicad_pro file here]
|
||||
|
||||
Please analyze my BOM to help estimate component costs, and provide guidance on:
|
||||
|
||||
- Which components contribute most to the overall cost
|
||||
- Where I might find cost savings
|
||||
- Potential volume discounts for larger runs
|
||||
- Suggestions for alternative components that could reduce costs
|
||||
- Estimated PCB fabrication costs based on board size and complexity
|
||||
|
||||
If my BOM doesn't include cost data, please suggest how I might find pricing information for my components.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
@mcp.prompt()
|
||||
def bom_export_help() -> str:
|
||||
"""Prompt for assistance with exporting BOMs from KiCad."""
|
||||
prompt = """
|
||||
I need help exporting a Bill of Materials (BOM) from my KiCad project. I'm interested in:
|
||||
|
||||
1. Understanding the different BOM export options in KiCad
|
||||
2. Exporting a BOM with specific fields (reference, value, footprint, etc.)
|
||||
3. Generating a BOM in a format compatible with my preferred supplier
|
||||
4. Adding custom fields to my components that will appear in the BOM
|
||||
|
||||
My KiCad project is located at:
|
||||
[Enter the full path to your .kicad_pro file here]
|
||||
|
||||
Please guide me through the process of creating a well-structured BOM for my project.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
@mcp.prompt()
|
||||
def component_sourcing() -> str:
|
||||
"""Prompt for help with component sourcing."""
|
||||
prompt = """
|
||||
I need help sourcing components for my KiCad PCB project. Specifically, I need assistance with:
|
||||
|
||||
1. Identifying reliable suppliers for my components
|
||||
2. Finding alternatives for any hard-to-find or obsolete parts
|
||||
3. Understanding lead times and availability constraints
|
||||
4. Balancing cost versus quality considerations
|
||||
|
||||
My KiCad project is located at:
|
||||
[Enter the full path to your .kicad_pro file here]
|
||||
|
||||
Please analyze my BOM and provide guidance on sourcing these components efficiently.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
@mcp.prompt()
|
||||
def bom_comparison() -> str:
|
||||
"""Prompt for comparing BOMs between two design revisions."""
|
||||
prompt = """
|
||||
I have two versions of a KiCad project and I'd like to compare the changes between their Bills of Materials. I need to understand:
|
||||
|
||||
1. Which components were added or removed
|
||||
2. Which component values or footprints changed
|
||||
3. The impact of these changes on the overall design
|
||||
4. Any potential issues introduced by these changes
|
||||
|
||||
My original KiCad project is located at:
|
||||
[Enter the full path to your first .kicad_pro file here]
|
||||
|
||||
My revised KiCad project is located at:
|
||||
[Enter the full path to your second .kicad_pro file here]
|
||||
|
||||
Please analyze the BOMs from both projects and help me understand the differences between them.
|
||||
"""
|
||||
|
||||
return prompt
|
281
kicad_mcp/resources/bom_resources.py
Normal file
281
kicad_mcp/resources/bom_resources.py
Normal file
@ -0,0 +1,281 @@
|
||||
"""
|
||||
Bill of Materials (BOM) resources for KiCad projects.
|
||||
"""
|
||||
import os
|
||||
import csv
|
||||
import json
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Any, Optional
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from kicad_mcp.utils.file_utils import get_project_files
|
||||
|
||||
# Import the helper functions from bom_tools.py to avoid code duplication
|
||||
from kicad_mcp.tools.bom_tools import parse_bom_file, analyze_bom_data
|
||||
|
||||
def register_bom_resources(mcp: FastMCP) -> None:
|
||||
"""Register BOM-related resources with the MCP server.
|
||||
|
||||
Args:
|
||||
mcp: The FastMCP server instance
|
||||
"""
|
||||
|
||||
@mcp.resource("kicad://bom/{project_path}")
|
||||
def get_bom_resource(project_path: str) -> str:
|
||||
"""Get a formatted BOM report for a KiCad project.
|
||||
|
||||
Args:
|
||||
project_path: Path to the KiCad project file (.kicad_pro)
|
||||
|
||||
Returns:
|
||||
Markdown-formatted BOM report
|
||||
"""
|
||||
print(f"Generating BOM report for project: {project_path}")
|
||||
|
||||
if not os.path.exists(project_path):
|
||||
return f"Project not found: {project_path}"
|
||||
|
||||
# Get all project files
|
||||
files = get_project_files(project_path)
|
||||
|
||||
# Look for BOM files
|
||||
bom_files = {}
|
||||
for file_type, file_path in files.items():
|
||||
if "bom" in file_type.lower() or file_path.lower().endswith(".csv"):
|
||||
bom_files[file_type] = file_path
|
||||
print(f"Found potential BOM file: {file_path}")
|
||||
|
||||
if not bom_files:
|
||||
print("No BOM files found for project")
|
||||
return f"# BOM Report\n\nNo BOM files found for project: {os.path.basename(project_path)}.\n\nExport a BOM from KiCad first, or use the `export_bom_csv` tool to generate one."
|
||||
|
||||
# Format as Markdown report
|
||||
project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro
|
||||
|
||||
report = f"# Bill of Materials for {project_name}\n\n"
|
||||
|
||||
# Process each BOM file
|
||||
for file_type, file_path in bom_files.items():
|
||||
try:
|
||||
# Parse and analyze the BOM
|
||||
bom_data, format_info = parse_bom_file(file_path)
|
||||
|
||||
if not bom_data:
|
||||
report += f"## {file_type}\n\nFailed to parse BOM file: {os.path.basename(file_path)}\n\n"
|
||||
continue
|
||||
|
||||
analysis = analyze_bom_data(bom_data, format_info)
|
||||
|
||||
# Add file section
|
||||
report += f"## {file_type.capitalize()}\n\n"
|
||||
report += f"**File**: {os.path.basename(file_path)}\n\n"
|
||||
report += f"**Format**: {format_info.get('detected_format', 'Unknown')}\n\n"
|
||||
|
||||
# Add summary
|
||||
report += "### Summary\n\n"
|
||||
report += f"- **Total Components**: {analysis.get('total_component_count', 0)}\n"
|
||||
report += f"- **Unique Components**: {analysis.get('unique_component_count', 0)}\n"
|
||||
|
||||
# Add cost if available
|
||||
if analysis.get('has_cost_data', False) and 'total_cost' in analysis:
|
||||
currency = analysis.get('currency', 'USD')
|
||||
currency_symbols = {'USD': '$', 'EUR': '€', 'GBP': '£'}
|
||||
symbol = currency_symbols.get(currency, '')
|
||||
|
||||
report += f"- **Estimated Cost**: {symbol}{analysis['total_cost']} {currency}\n"
|
||||
|
||||
report += "\n"
|
||||
|
||||
# Add categories breakdown
|
||||
if 'categories' in analysis and analysis['categories']:
|
||||
report += "### Component Categories\n\n"
|
||||
|
||||
for category, count in analysis['categories'].items():
|
||||
report += f"- **{category}**: {count}\n"
|
||||
|
||||
report += "\n"
|
||||
|
||||
# Add most common components if available
|
||||
if 'most_common_values' in analysis and analysis['most_common_values']:
|
||||
report += "### Most Common Components\n\n"
|
||||
|
||||
for value, count in analysis['most_common_values'].items():
|
||||
report += f"- **{value}**: {count}\n"
|
||||
|
||||
report += "\n"
|
||||
|
||||
# Add component table (first 20 items)
|
||||
if bom_data:
|
||||
report += "### Component List\n\n"
|
||||
|
||||
# Try to identify key columns
|
||||
columns = []
|
||||
if format_info.get('header_fields'):
|
||||
# Use a subset of columns for readability
|
||||
preferred_cols = ['Reference', 'Value', 'Footprint', 'Quantity', 'Description']
|
||||
|
||||
# Find matching columns (case-insensitive)
|
||||
header_lower = [h.lower() for h in format_info['header_fields']]
|
||||
for col in preferred_cols:
|
||||
col_lower = col.lower()
|
||||
if col_lower in header_lower:
|
||||
idx = header_lower.index(col_lower)
|
||||
columns.append(format_info['header_fields'][idx])
|
||||
|
||||
# If we didn't find any preferred columns, use the first 4
|
||||
if not columns and len(format_info['header_fields']) > 0:
|
||||
columns = format_info['header_fields'][:min(4, len(format_info['header_fields']))]
|
||||
|
||||
# Generate the table header
|
||||
if columns:
|
||||
report += "| " + " | ".join(columns) + " |\n"
|
||||
report += "| " + " | ".join(["---"] * len(columns)) + " |\n"
|
||||
|
||||
# Add rows (limit to first 20 for readability)
|
||||
for i, component in enumerate(bom_data[:20]):
|
||||
row = []
|
||||
for col in columns:
|
||||
value = component.get(col, "")
|
||||
# Clean up cell content for Markdown table
|
||||
value = str(value).replace("|", "\\|").replace("\n", " ")
|
||||
row.append(value)
|
||||
|
||||
report += "| " + " | ".join(row) + " |\n"
|
||||
|
||||
# Add note if there are more components
|
||||
if len(bom_data) > 20:
|
||||
report += f"\n*...and {len(bom_data) - 20} more components*\n"
|
||||
else:
|
||||
report += "*Component table could not be generated - column headers not recognized*\n"
|
||||
|
||||
report += "\n---\n\n"
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing BOM file {file_path}: {str(e)}")
|
||||
report += f"## {file_type}\n\nError processing BOM file: {str(e)}\n\n"
|
||||
|
||||
# Add export instructions
|
||||
report += "## How to Export a BOM\n\n"
|
||||
report += "To generate a new BOM from your KiCad project:\n\n"
|
||||
report += "1. Open your schematic in KiCad\n"
|
||||
report += "2. Go to **Tools → Generate BOM**\n"
|
||||
report += "3. Choose a BOM plugin and click **Generate**\n"
|
||||
report += "4. Save the BOM file in your project directory\n\n"
|
||||
report += "Alternatively, use the `export_bom_csv` tool in this MCP server to generate a BOM file.\n"
|
||||
|
||||
return report
|
||||
|
||||
@mcp.resource("kicad://bom/{project_path}/csv")
|
||||
def get_bom_csv_resource(project_path: str) -> str:
|
||||
"""Get a CSV representation of the BOM for a KiCad project.
|
||||
|
||||
Args:
|
||||
project_path: Path to the KiCad project file (.kicad_pro)
|
||||
|
||||
Returns:
|
||||
CSV-formatted BOM data
|
||||
"""
|
||||
print(f"Generating CSV BOM for project: {project_path}")
|
||||
|
||||
if not os.path.exists(project_path):
|
||||
return f"Project not found: {project_path}"
|
||||
|
||||
# Get all project files
|
||||
files = get_project_files(project_path)
|
||||
|
||||
# Look for BOM files
|
||||
bom_files = {}
|
||||
for file_type, file_path in files.items():
|
||||
if "bom" in file_type.lower() or file_path.lower().endswith(".csv"):
|
||||
bom_files[file_type] = file_path
|
||||
print(f"Found potential BOM file: {file_path}")
|
||||
|
||||
if not bom_files:
|
||||
print("No BOM files found for project")
|
||||
return "No BOM files found for project. Export a BOM from KiCad first."
|
||||
|
||||
# Use the first BOM file found
|
||||
file_type = next(iter(bom_files))
|
||||
file_path = bom_files[file_type]
|
||||
|
||||
try:
|
||||
# If it's already a CSV, just return its contents
|
||||
if file_path.lower().endswith('.csv'):
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||
return f.read()
|
||||
|
||||
# Otherwise, try to parse and convert to CSV
|
||||
bom_data, format_info = parse_bom_file(file_path)
|
||||
|
||||
if not bom_data:
|
||||
return f"Failed to parse BOM file: {file_path}"
|
||||
|
||||
# Convert to DataFrame and then to CSV
|
||||
df = pd.DataFrame(bom_data)
|
||||
return df.to_csv(index=False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating CSV from BOM file: {str(e)}")
|
||||
return f"Error generating CSV from BOM file: {str(e)}"
|
||||
|
||||
@mcp.resource("kicad://bom/{project_path}/json")
|
||||
def get_bom_json_resource(project_path: str) -> str:
|
||||
"""Get a JSON representation of the BOM for a KiCad project.
|
||||
|
||||
Args:
|
||||
project_path: Path to the KiCad project file (.kicad_pro)
|
||||
|
||||
Returns:
|
||||
JSON-formatted BOM data
|
||||
"""
|
||||
print(f"Generating JSON BOM for project: {project_path}")
|
||||
|
||||
if not os.path.exists(project_path):
|
||||
return f"Project not found: {project_path}"
|
||||
|
||||
# Get all project files
|
||||
files = get_project_files(project_path)
|
||||
|
||||
# Look for BOM files
|
||||
bom_files = {}
|
||||
for file_type, file_path in files.items():
|
||||
if "bom" in file_type.lower() or file_path.lower().endswith((".csv", ".json")):
|
||||
bom_files[file_type] = file_path
|
||||
print(f"Found potential BOM file: {file_path}")
|
||||
|
||||
if not bom_files:
|
||||
print("No BOM files found for project")
|
||||
return json.dumps({"error": "No BOM files found for project"}, indent=2)
|
||||
|
||||
try:
|
||||
# Collect data from all BOM files
|
||||
result = {"project": os.path.basename(project_path)[:-10], "bom_files": {}}
|
||||
|
||||
for file_type, file_path in bom_files.items():
|
||||
# If it's already JSON, parse it directly
|
||||
if file_path.lower().endswith('.json'):
|
||||
with open(file_path, 'r') as f:
|
||||
try:
|
||||
result["bom_files"][file_type] = json.load(f)
|
||||
continue
|
||||
except:
|
||||
# If JSON parsing fails, fall back to regular parsing
|
||||
pass
|
||||
|
||||
# Otherwise parse with our utility
|
||||
bom_data, format_info = parse_bom_file(file_path)
|
||||
|
||||
if bom_data:
|
||||
analysis = analyze_bom_data(bom_data, format_info)
|
||||
result["bom_files"][file_type] = {
|
||||
"file": os.path.basename(file_path),
|
||||
"format": format_info,
|
||||
"analysis": analysis,
|
||||
"components": bom_data
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2, default=str)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating JSON from BOM file: {str(e)}")
|
||||
return json.dumps({"error": str(e)}, indent=2)
|
@ -7,16 +7,19 @@ from mcp.server.fastmcp import FastMCP
|
||||
from kicad_mcp.resources.projects import register_project_resources
|
||||
from kicad_mcp.resources.files import register_file_resources
|
||||
from kicad_mcp.resources.drc_resources import register_drc_resources
|
||||
from kicad_mcp.resources.bom_resources import register_bom_resources
|
||||
|
||||
# Import tool handlers
|
||||
from kicad_mcp.tools.project_tools import register_project_tools
|
||||
from kicad_mcp.tools.analysis_tools import register_analysis_tools
|
||||
from kicad_mcp.tools.export_tools import register_export_tools
|
||||
from kicad_mcp.tools.drc_tools import register_drc_tools
|
||||
from kicad_mcp.tools.bom_tools import register_bom_tools
|
||||
|
||||
# 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
|
||||
|
||||
# Import utils
|
||||
from kicad_mcp.utils.logger import Logger
|
||||
@ -49,6 +52,7 @@ def create_server() -> FastMCP:
|
||||
register_project_resources(mcp)
|
||||
register_file_resources(mcp)
|
||||
register_drc_resources(mcp)
|
||||
register_bom_resources(mcp)
|
||||
|
||||
# Register tools
|
||||
logger.debug("Registering tools...")
|
||||
@ -56,11 +60,13 @@ def create_server() -> FastMCP:
|
||||
register_analysis_tools(mcp)
|
||||
register_export_tools(mcp)
|
||||
register_drc_tools(mcp)
|
||||
register_bom_tools(mcp)
|
||||
|
||||
# Register prompts
|
||||
logger.debug("Registering prompts...")
|
||||
register_prompts(mcp)
|
||||
register_drc_prompts(mcp)
|
||||
register_bom_prompts(mcp)
|
||||
|
||||
logger.info("Server initialization complete")
|
||||
return mcp
|
||||
|
762
kicad_mcp/tools/bom_tools.py
Normal file
762
kicad_mcp/tools/bom_tools.py
Normal file
@ -0,0 +1,762 @@
|
||||
"""
|
||||
Bill of Materials (BOM) processing tools for KiCad projects.
|
||||
"""
|
||||
import os
|
||||
import csv
|
||||
import json
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from mcp.server.fastmcp import FastMCP, Context, Image
|
||||
|
||||
from kicad_mcp.utils.file_utils import get_project_files
|
||||
|
||||
def register_bom_tools(mcp: FastMCP) -> None:
|
||||
"""Register BOM-related tools with the MCP server.
|
||||
|
||||
Args:
|
||||
mcp: The FastMCP server instance
|
||||
"""
|
||||
|
||||
@mcp.tool()
|
||||
async def analyze_bom(project_path: str, ctx: Context) -> Dict[str, Any]:
|
||||
"""Analyze a KiCad project's Bill of Materials.
|
||||
|
||||
This tool will look for BOM files related to a KiCad project and provide
|
||||
analysis including component counts, categories, and cost estimates if available.
|
||||
|
||||
Args:
|
||||
project_path: Path to the KiCad project file (.kicad_pro)
|
||||
ctx: MCP context for progress reporting
|
||||
|
||||
Returns:
|
||||
Dictionary with BOM analysis results
|
||||
"""
|
||||
logger.info(f"Analyzing BOM for project: {project_path}")
|
||||
|
||||
if not os.path.exists(project_path):
|
||||
logger.error(f"Project not found: {project_path}")
|
||||
ctx.info(f"Project not found: {project_path}")
|
||||
return {"success": False, "error": f"Project not found: {project_path}"}
|
||||
|
||||
# Report progress
|
||||
await ctx.report_progress(10, 100)
|
||||
ctx.info(f"Looking for BOM files related to {os.path.basename(project_path)}")
|
||||
|
||||
# Get all project files
|
||||
files = get_project_files(project_path)
|
||||
|
||||
# Look for BOM files
|
||||
bom_files = {}
|
||||
for file_type, file_path in files.items():
|
||||
if "bom" in file_type.lower() or file_path.lower().endswith(".csv"):
|
||||
bom_files[file_type] = file_path
|
||||
logger.info(f"Found potential BOM file: {file_path}")
|
||||
|
||||
if not bom_files:
|
||||
logger.warning("No BOM files found for project")
|
||||
ctx.info("No BOM files found for project")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "No BOM files found. Export a BOM from KiCad first.",
|
||||
"project_path": project_path
|
||||
}
|
||||
|
||||
await ctx.report_progress(30, 100)
|
||||
|
||||
# Analyze each BOM file
|
||||
results = {
|
||||
"success": True,
|
||||
"project_path": project_path,
|
||||
"bom_files": {},
|
||||
"component_summary": {}
|
||||
}
|
||||
|
||||
total_unique_components = 0
|
||||
total_components = 0
|
||||
|
||||
for file_type, file_path in bom_files.items():
|
||||
try:
|
||||
ctx.info(f"Analyzing {os.path.basename(file_path)}")
|
||||
|
||||
# Parse the BOM file
|
||||
bom_data, format_info = parse_bom_file(file_path)
|
||||
|
||||
if not bom_data or len(bom_data) == 0:
|
||||
logger.warning(f"Failed to parse BOM file: {file_path}")
|
||||
continue
|
||||
|
||||
# Analyze the BOM data
|
||||
analysis = analyze_bom_data(bom_data, format_info)
|
||||
|
||||
# Add to results
|
||||
results["bom_files"][file_type] = {
|
||||
"path": file_path,
|
||||
"format": format_info,
|
||||
"analysis": analysis
|
||||
}
|
||||
|
||||
# Update totals
|
||||
total_unique_components += analysis["unique_component_count"]
|
||||
total_components += analysis["total_component_count"]
|
||||
|
||||
logger.info(f"Successfully analyzed BOM file: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing BOM file {file_path}: {str(e)}", exc_info=True)
|
||||
results["bom_files"][file_type] = {
|
||||
"path": file_path,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
await ctx.report_progress(70, 100)
|
||||
|
||||
# Generate overall component summary
|
||||
if total_components > 0:
|
||||
results["component_summary"] = {
|
||||
"total_unique_components": total_unique_components,
|
||||
"total_components": total_components
|
||||
}
|
||||
|
||||
# Calculate component categories across all BOMs
|
||||
all_categories = {}
|
||||
for file_type, file_info in results["bom_files"].items():
|
||||
if "analysis" in file_info and "categories" in file_info["analysis"]:
|
||||
for category, count in file_info["analysis"]["categories"].items():
|
||||
if category not in all_categories:
|
||||
all_categories[category] = 0
|
||||
all_categories[category] += count
|
||||
|
||||
results["component_summary"]["categories"] = all_categories
|
||||
|
||||
# Calculate total cost if available
|
||||
total_cost = 0.0
|
||||
cost_available = False
|
||||
for file_type, file_info in results["bom_files"].items():
|
||||
if "analysis" in file_info and "total_cost" in file_info["analysis"]:
|
||||
if file_info["analysis"]["total_cost"] > 0:
|
||||
total_cost += file_info["analysis"]["total_cost"]
|
||||
cost_available = True
|
||||
|
||||
if cost_available:
|
||||
results["component_summary"]["total_cost"] = round(total_cost, 2)
|
||||
currency = next((
|
||||
file_info["analysis"].get("currency", "USD")
|
||||
for file_type, file_info in results["bom_files"].items()
|
||||
if "analysis" in file_info and "currency" in file_info["analysis"]
|
||||
), "USD")
|
||||
results["component_summary"]["currency"] = currency
|
||||
|
||||
await ctx.report_progress(100, 100)
|
||||
ctx.info(f"BOM analysis complete: found {total_components} components")
|
||||
|
||||
return results
|
||||
|
||||
@mcp.tool()
|
||||
async def export_bom_csv(project_path: str, ctx: Context) -> Dict[str, Any]:
|
||||
"""Export a Bill of Materials for a KiCad project.
|
||||
|
||||
This tool attempts to generate a CSV BOM file for a KiCad project.
|
||||
It requires KiCad to be installed with the appropriate command-line tools.
|
||||
|
||||
Args:
|
||||
project_path: Path to the KiCad project file (.kicad_pro)
|
||||
ctx: MCP context for progress reporting
|
||||
|
||||
Returns:
|
||||
Dictionary with export results
|
||||
"""
|
||||
logger.info(f"Exporting BOM for project: {project_path}")
|
||||
|
||||
if not os.path.exists(project_path):
|
||||
logger.error(f"Project not found: {project_path}")
|
||||
ctx.info(f"Project not found: {project_path}")
|
||||
return {"success": False, "error": f"Project not found: {project_path}"}
|
||||
|
||||
# Get access to the app context
|
||||
app_context = ctx.request_context.lifespan_context
|
||||
kicad_modules_available = app_context.kicad_modules_available
|
||||
|
||||
# Report progress
|
||||
await ctx.report_progress(10, 100)
|
||||
|
||||
# Get all project files
|
||||
files = get_project_files(project_path)
|
||||
|
||||
# We need the schematic file to generate a BOM
|
||||
if "schematic" not in files:
|
||||
logger.error("Schematic file not found in project")
|
||||
ctx.info("Schematic file not found in project")
|
||||
return {"success": False, "error": "Schematic file not found"}
|
||||
|
||||
schematic_file = files["schematic"]
|
||||
project_dir = os.path.dirname(project_path)
|
||||
project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro extension
|
||||
|
||||
await ctx.report_progress(20, 100)
|
||||
ctx.info(f"Found schematic file: {os.path.basename(schematic_file)}")
|
||||
|
||||
# Try to export BOM
|
||||
# This will depend on KiCad's command-line tools or Python modules
|
||||
export_result = {"success": False}
|
||||
|
||||
if kicad_modules_available:
|
||||
try:
|
||||
# Try to use KiCad Python modules
|
||||
ctx.info("Attempting to export BOM using KiCad Python modules...")
|
||||
export_result = await export_bom_with_python(schematic_file, project_dir, project_name, ctx)
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting BOM with Python modules: {str(e)}", exc_info=True)
|
||||
ctx.info(f"Error using Python modules: {str(e)}")
|
||||
export_result = {"success": False, "error": str(e)}
|
||||
|
||||
# If Python method failed, try command-line method
|
||||
if not export_result.get("success", False):
|
||||
try:
|
||||
ctx.info("Attempting to export BOM using command-line tools...")
|
||||
export_result = await export_bom_with_cli(schematic_file, project_dir, project_name, ctx)
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting BOM with CLI: {str(e)}", exc_info=True)
|
||||
ctx.info(f"Error using command-line tools: {str(e)}")
|
||||
export_result = {"success": False, "error": str(e)}
|
||||
|
||||
await ctx.report_progress(100, 100)
|
||||
|
||||
if export_result.get("success", False):
|
||||
ctx.info(f"BOM exported successfully to {export_result.get('output_file', 'unknown location')}")
|
||||
else:
|
||||
ctx.info(f"Failed to export BOM: {export_result.get('error', 'Unknown error')}")
|
||||
|
||||
return export_result
|
||||
|
||||
|
||||
# Helper functions for BOM processing
|
||||
|
||||
def parse_bom_file(file_path: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
||||
"""Parse a BOM file and detect its format.
|
||||
|
||||
Args:
|
||||
file_path: Path to the BOM file
|
||||
|
||||
Returns:
|
||||
Tuple containing:
|
||||
- List of component dictionaries
|
||||
- Dictionary with format information
|
||||
"""
|
||||
logger.info(f"Parsing BOM file: {file_path}")
|
||||
|
||||
# Check file extension
|
||||
_, ext = os.path.splitext(file_path)
|
||||
ext = ext.lower()
|
||||
|
||||
# Dictionary to store format detection info
|
||||
format_info = {
|
||||
"file_type": ext,
|
||||
"detected_format": "unknown",
|
||||
"header_fields": []
|
||||
}
|
||||
|
||||
# Empty list to store component data
|
||||
components = []
|
||||
|
||||
try:
|
||||
if ext == '.csv':
|
||||
# Try to parse as CSV
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||
# Read a few lines to analyze the format
|
||||
sample = ''.join([f.readline() for _ in range(10)])
|
||||
f.seek(0) # Reset file pointer
|
||||
|
||||
# Try to detect the delimiter
|
||||
if ',' in sample:
|
||||
delimiter = ','
|
||||
elif ';' in sample:
|
||||
delimiter = ';'
|
||||
elif '\t' in sample:
|
||||
delimiter = '\t'
|
||||
else:
|
||||
delimiter = ',' # Default
|
||||
|
||||
format_info["delimiter"] = delimiter
|
||||
|
||||
# Read CSV
|
||||
reader = csv.DictReader(f, delimiter=delimiter)
|
||||
format_info["header_fields"] = reader.fieldnames if reader.fieldnames else []
|
||||
|
||||
# Detect BOM format based on header fields
|
||||
header_str = ','.join(format_info["header_fields"]).lower()
|
||||
|
||||
if 'reference' in header_str and 'value' in header_str:
|
||||
format_info["detected_format"] = "kicad"
|
||||
elif 'designator' in header_str:
|
||||
format_info["detected_format"] = "altium"
|
||||
elif 'part number' in header_str or 'manufacturer part' in header_str:
|
||||
format_info["detected_format"] = "generic"
|
||||
|
||||
# Read components
|
||||
for row in reader:
|
||||
components.append(dict(row))
|
||||
|
||||
elif ext == '.xml':
|
||||
# Basic XML parsing
|
||||
import xml.etree.ElementTree as ET
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
|
||||
format_info["detected_format"] = "xml"
|
||||
|
||||
# Try to extract components based on common XML BOM formats
|
||||
component_elements = root.findall('.//component') or root.findall('.//Component')
|
||||
|
||||
if component_elements:
|
||||
for elem in component_elements:
|
||||
component = {}
|
||||
for attr in elem.attrib:
|
||||
component[attr] = elem.attrib[attr]
|
||||
for child in elem:
|
||||
component[child.tag] = child.text
|
||||
components.append(component)
|
||||
|
||||
elif ext == '.json':
|
||||
# Parse JSON
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
format_info["detected_format"] = "json"
|
||||
|
||||
# Try to find components array in common JSON formats
|
||||
if isinstance(data, list):
|
||||
components = data
|
||||
elif 'components' in data:
|
||||
components = data['components']
|
||||
elif 'parts' in data:
|
||||
components = data['parts']
|
||||
|
||||
else:
|
||||
# Unknown format, try generic CSV parsing as fallback
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||
reader = csv.DictReader(f)
|
||||
format_info["header_fields"] = reader.fieldnames if reader.fieldnames else []
|
||||
format_info["detected_format"] = "unknown_csv"
|
||||
|
||||
for row in reader:
|
||||
components.append(dict(row))
|
||||
except:
|
||||
logger.error(f"Failed to parse unknown file format: {file_path}")
|
||||
return [], {"detected_format": "unsupported"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing BOM file: {str(e)}", exc_info=True)
|
||||
return [], {"error": str(e)}
|
||||
|
||||
# Check if we actually got components
|
||||
if not components:
|
||||
logger.warning(f"No components found in BOM file: {file_path}")
|
||||
else:
|
||||
logger.info(f"Successfully parsed {len(components)} components from {file_path}")
|
||||
|
||||
# Add a sample of the fields found
|
||||
if components:
|
||||
format_info["sample_fields"] = list(components[0].keys())
|
||||
|
||||
return components, format_info
|
||||
|
||||
|
||||
def analyze_bom_data(components: List[Dict[str, Any]], format_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Analyze component data from a BOM file.
|
||||
|
||||
Args:
|
||||
components: List of component dictionaries
|
||||
format_info: Dictionary with format information
|
||||
|
||||
Returns:
|
||||
Dictionary with analysis results
|
||||
"""
|
||||
logger.info(f"Analyzing {len(components)} components")
|
||||
|
||||
# Initialize results
|
||||
results = {
|
||||
"unique_component_count": 0,
|
||||
"total_component_count": 0,
|
||||
"categories": {},
|
||||
"has_cost_data": False
|
||||
}
|
||||
|
||||
if not components:
|
||||
return results
|
||||
|
||||
# Try to convert to pandas DataFrame for easier analysis
|
||||
try:
|
||||
df = pd.DataFrame(components)
|
||||
|
||||
# Clean up column names
|
||||
df.columns = [str(col).strip().lower() for col in df.columns]
|
||||
|
||||
# Try to identify key columns based on format
|
||||
ref_col = None
|
||||
value_col = None
|
||||
quantity_col = None
|
||||
footprint_col = None
|
||||
cost_col = None
|
||||
category_col = None
|
||||
|
||||
# Check for reference designator column
|
||||
for possible_col in ['reference', 'designator', 'references', 'designators', 'refdes', 'ref']:
|
||||
if possible_col in df.columns:
|
||||
ref_col = possible_col
|
||||
break
|
||||
|
||||
# Check for value column
|
||||
for possible_col in ['value', 'component', 'comp', 'part', 'component value', 'comp value']:
|
||||
if possible_col in df.columns:
|
||||
value_col = possible_col
|
||||
break
|
||||
|
||||
# Check for quantity column
|
||||
for possible_col in ['quantity', 'qty', 'count', 'amount']:
|
||||
if possible_col in df.columns:
|
||||
quantity_col = possible_col
|
||||
break
|
||||
|
||||
# Check for footprint column
|
||||
for possible_col in ['footprint', 'package', 'pattern', 'pcb footprint']:
|
||||
if possible_col in df.columns:
|
||||
footprint_col = possible_col
|
||||
break
|
||||
|
||||
# Check for cost column
|
||||
for possible_col in ['cost', 'price', 'unit price', 'unit cost', 'cost each']:
|
||||
if possible_col in df.columns:
|
||||
cost_col = possible_col
|
||||
break
|
||||
|
||||
# Check for category column
|
||||
for possible_col in ['category', 'type', 'group', 'component type', 'lib']:
|
||||
if possible_col in df.columns:
|
||||
category_col = possible_col
|
||||
break
|
||||
|
||||
# Count total components
|
||||
if quantity_col:
|
||||
# Try to convert quantity to numeric
|
||||
df[quantity_col] = pd.to_numeric(df[quantity_col], errors='coerce').fillna(1)
|
||||
results["total_component_count"] = int(df[quantity_col].sum())
|
||||
else:
|
||||
# If no quantity column, assume each row is one component
|
||||
results["total_component_count"] = len(df)
|
||||
|
||||
# Count unique components
|
||||
results["unique_component_count"] = len(df)
|
||||
|
||||
# Calculate categories
|
||||
if category_col:
|
||||
# Use provided category column
|
||||
categories = df[category_col].value_counts().to_dict()
|
||||
results["categories"] = {str(k): int(v) for k, v in categories.items()}
|
||||
elif footprint_col:
|
||||
# Use footprint as category
|
||||
categories = df[footprint_col].value_counts().to_dict()
|
||||
results["categories"] = {str(k): int(v) for k, v in categories.items()}
|
||||
elif ref_col:
|
||||
# Try to extract categories from reference designators (R=resistor, C=capacitor, etc.)
|
||||
def extract_prefix(ref):
|
||||
if isinstance(ref, str):
|
||||
import re
|
||||
match = re.match(r'^([A-Za-z]+)', ref)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "Other"
|
||||
|
||||
if isinstance(df[ref_col].iloc[0], str) and ',' in df[ref_col].iloc[0]:
|
||||
# Multiple references in one cell
|
||||
all_refs = []
|
||||
for refs in df[ref_col]:
|
||||
all_refs.extend([r.strip() for r in refs.split(',')])
|
||||
|
||||
categories = {}
|
||||
for ref in all_refs:
|
||||
prefix = extract_prefix(ref)
|
||||
categories[prefix] = categories.get(prefix, 0) + 1
|
||||
|
||||
results["categories"] = categories
|
||||
else:
|
||||
# Single reference per row
|
||||
categories = df[ref_col].apply(extract_prefix).value_counts().to_dict()
|
||||
results["categories"] = {str(k): int(v) for k, v in categories.items()}
|
||||
|
||||
# Map common reference prefixes to component types
|
||||
category_mapping = {
|
||||
'R': 'Resistors',
|
||||
'C': 'Capacitors',
|
||||
'L': 'Inductors',
|
||||
'D': 'Diodes',
|
||||
'Q': 'Transistors',
|
||||
'U': 'ICs',
|
||||
'SW': 'Switches',
|
||||
'J': 'Connectors',
|
||||
'K': 'Relays',
|
||||
'Y': 'Crystals/Oscillators',
|
||||
'F': 'Fuses',
|
||||
'T': 'Transformers'
|
||||
}
|
||||
|
||||
mapped_categories = {}
|
||||
for cat, count in results["categories"].items():
|
||||
if cat in category_mapping:
|
||||
mapped_name = category_mapping[cat]
|
||||
mapped_categories[mapped_name] = mapped_categories.get(mapped_name, 0) + count
|
||||
else:
|
||||
mapped_categories[cat] = count
|
||||
|
||||
results["categories"] = mapped_categories
|
||||
|
||||
# Calculate cost if available
|
||||
if cost_col:
|
||||
try:
|
||||
# Try to extract numeric values from cost field
|
||||
df[cost_col] = df[cost_col].astype(str).str.replace('$', '').str.replace(',', '')
|
||||
df[cost_col] = pd.to_numeric(df[cost_col], errors='coerce')
|
||||
|
||||
# Remove NaN values
|
||||
df_with_cost = df.dropna(subset=[cost_col])
|
||||
|
||||
if not df_with_cost.empty:
|
||||
results["has_cost_data"] = True
|
||||
|
||||
if quantity_col:
|
||||
total_cost = (df_with_cost[cost_col] * df_with_cost[quantity_col]).sum()
|
||||
else:
|
||||
total_cost = df_with_cost[cost_col].sum()
|
||||
|
||||
results["total_cost"] = round(float(total_cost), 2)
|
||||
|
||||
# Try to determine currency
|
||||
# Check first row that has cost for currency symbols
|
||||
for _, row in df.iterrows():
|
||||
cost_str = str(row.get(cost_col, ''))
|
||||
if '$' in cost_str:
|
||||
results["currency"] = "USD"
|
||||
break
|
||||
elif '€' in cost_str:
|
||||
results["currency"] = "EUR"
|
||||
break
|
||||
elif '£' in cost_str:
|
||||
results["currency"] = "GBP"
|
||||
break
|
||||
|
||||
if "currency" not in results:
|
||||
results["currency"] = "USD" # Default
|
||||
except:
|
||||
logger.warning("Failed to parse cost data")
|
||||
|
||||
# Add extra insights
|
||||
if ref_col and value_col:
|
||||
# Check for common components by value
|
||||
value_counts = df[value_col].value_counts()
|
||||
most_common = value_counts.head(5).to_dict()
|
||||
results["most_common_values"] = {str(k): int(v) for k, v in most_common.items()}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing BOM data: {str(e)}", exc_info=True)
|
||||
# Fallback to basic analysis
|
||||
results["unique_component_count"] = len(components)
|
||||
results["total_component_count"] = len(components)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def export_bom_with_python(schematic_file: str, output_dir: str, project_name: str, ctx: Context) -> Dict[str, Any]:
|
||||
"""Export a BOM using KiCad Python modules.
|
||||
|
||||
Args:
|
||||
schematic_file: Path to the schematic file
|
||||
output_dir: Directory to save the BOM
|
||||
project_name: Name of the project
|
||||
ctx: MCP context for progress reporting
|
||||
|
||||
Returns:
|
||||
Dictionary with export results
|
||||
"""
|
||||
logger.info(f"Exporting BOM for schematic: {schematic_file}")
|
||||
await ctx.report_progress(30, 100)
|
||||
|
||||
try:
|
||||
# Try to import KiCad Python modules
|
||||
# This is a placeholder since exporting BOMs from schematic files
|
||||
# is complex and KiCad's API for this is not well-documented
|
||||
import pcbnew
|
||||
|
||||
# For now, return a message indicating this method is not implemented yet
|
||||
logger.warning("BOM export with Python modules not fully implemented")
|
||||
ctx.info("BOM export with Python modules not fully implemented yet")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": "BOM export using Python modules is not fully implemented yet. Try using the command-line method.",
|
||||
"schematic_file": schematic_file
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
logger.error("Failed to import KiCad Python modules")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Failed to import KiCad Python modules",
|
||||
"schematic_file": schematic_file
|
||||
}
|
||||
|
||||
|
||||
async def export_bom_with_cli(schematic_file: str, output_dir: str, project_name: str, ctx: Context) -> Dict[str, Any]:
|
||||
"""Export a BOM using KiCad command-line tools.
|
||||
|
||||
Args:
|
||||
schematic_file: Path to the schematic file
|
||||
output_dir: Directory to save the BOM
|
||||
project_name: Name of the project
|
||||
ctx: MCP context for progress reporting
|
||||
|
||||
Returns:
|
||||
Dictionary with export results
|
||||
"""
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
system = platform.system()
|
||||
logger.info(f"Exporting BOM using CLI tools on {system}")
|
||||
await ctx.report_progress(40, 100)
|
||||
|
||||
# Output file path
|
||||
output_file = os.path.join(output_dir, f"{project_name}_bom.csv")
|
||||
|
||||
# Define the command based on operating system
|
||||
if system == "Darwin": # macOS
|
||||
from kicad_mcp.config import KICAD_APP_PATH
|
||||
|
||||
# Path to KiCad command-line tools on macOS
|
||||
kicad_cli = os.path.join(KICAD_APP_PATH, "Contents/MacOS/kicad-cli")
|
||||
|
||||
if not os.path.exists(kicad_cli):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"KiCad CLI tool not found at {kicad_cli}",
|
||||
"schematic_file": schematic_file
|
||||
}
|
||||
|
||||
# Command to generate BOM
|
||||
cmd = [
|
||||
kicad_cli,
|
||||
"sch",
|
||||
"export",
|
||||
"bom",
|
||||
"--output", output_file,
|
||||
schematic_file
|
||||
]
|
||||
|
||||
elif system == "Windows":
|
||||
from kicad_mcp.config import KICAD_APP_PATH
|
||||
|
||||
# Path to KiCad command-line tools on Windows
|
||||
kicad_cli = os.path.join(KICAD_APP_PATH, "bin", "kicad-cli.exe")
|
||||
|
||||
if not os.path.exists(kicad_cli):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"KiCad CLI tool not found at {kicad_cli}",
|
||||
"schematic_file": schematic_file
|
||||
}
|
||||
|
||||
# Command to generate BOM
|
||||
cmd = [
|
||||
kicad_cli,
|
||||
"sch",
|
||||
"export",
|
||||
"bom",
|
||||
"--output", output_file,
|
||||
schematic_file
|
||||
]
|
||||
|
||||
elif system == "Linux":
|
||||
# Assume kicad-cli is in the PATH
|
||||
kicad_cli = "kicad-cli"
|
||||
|
||||
# Command to generate BOM
|
||||
cmd = [
|
||||
kicad_cli,
|
||||
"sch",
|
||||
"export",
|
||||
"bom",
|
||||
"--output", output_file,
|
||||
schematic_file
|
||||
]
|
||||
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unsupported operating system: {system}",
|
||||
"schematic_file": schematic_file
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"Running command: {' '.join(cmd)}")
|
||||
await ctx.report_progress(60, 100)
|
||||
|
||||
# Run the command
|
||||
process = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
# Check if the command was successful
|
||||
if process.returncode != 0:
|
||||
logger.error(f"BOM export command failed with code {process.returncode}")
|
||||
logger.error(f"Error output: {process.stderr}")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"BOM export command failed: {process.stderr}",
|
||||
"schematic_file": schematic_file,
|
||||
"command": ' '.join(cmd)
|
||||
}
|
||||
|
||||
# Check if the output file was created
|
||||
if not os.path.exists(output_file):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "BOM file was not created",
|
||||
"schematic_file": schematic_file,
|
||||
"output_file": output_file
|
||||
}
|
||||
|
||||
await ctx.report_progress(80, 100)
|
||||
|
||||
# Read the first few lines of the BOM to verify it's valid
|
||||
with open(output_file, 'r') as f:
|
||||
bom_content = f.read(1024) # Read first 1KB
|
||||
|
||||
if len(bom_content.strip()) == 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Generated BOM file is empty",
|
||||
"schematic_file": schematic_file,
|
||||
"output_file": output_file
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"schematic_file": schematic_file,
|
||||
"output_file": output_file,
|
||||
"file_size": os.path.getsize(output_file),
|
||||
"message": "BOM exported successfully"
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("BOM export command timed out after 30 seconds")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "BOM export command timed out after 30 seconds",
|
||||
"schematic_file": schematic_file
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting BOM: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Error exporting BOM: {str(e)}",
|
||||
"schematic_file": schematic_file
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user