From f50a2ce1affc56b6cc47928b81fb2000fa4254fe Mon Sep 17 00:00:00 2001 From: Lama Date: Thu, 20 Mar 2025 03:16:14 -0400 Subject: [PATCH] Enhance PCB thumbnail generation with robust fallback methods Implements a more reliable PCB thumbnail generation feature using two methods: - Primary: pcbnew Python module for high-quality rendering - Fallback: pcbnew_cli for environments without Python modules Adds detailed progress reporting and comprehensive error handling. Includes documentation in docs/thumbnail_guide.md. --- README.md | 1 + docs/thumbnail_guide.md | 73 ++++++++++ kicad_mcp/server.py | 2 +- kicad_mcp/tools/__init__.py | 6 + kicad_mcp/tools/export_tools.py | 251 ++++++++++++++++++++++++++++++++ 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 docs/thumbnail_guide.md diff --git a/README.md b/README.md index f8e8871..ed00184 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ The KiCad MCP Server provides several capabilities: - Analysis tools (validate projects, generate thumbnails) - Export tools (extract bill of materials) - Design Rule Check tools (run DRC checks, get detailed violation reports, track DRC history and improvements over time) +- PCB Visualization - Generate thumbnails of PCB layouts for easy project identification ([see guide](docs/thumbnail_guide.md)) ### Prompts - Create new component guide diff --git a/docs/thumbnail_guide.md b/docs/thumbnail_guide.md new file mode 100644 index 0000000..1db65ca --- /dev/null +++ b/docs/thumbnail_guide.md @@ -0,0 +1,73 @@ +# PCB Thumbnail Feature Guide + +The KiCad MCP Server now includes a powerful PCB thumbnail generation feature, making it easier to visually browse and identify your KiCad projects. This guide explains how to use the feature and how it works behind the scenes. + +## Using the PCB Thumbnail Feature + +You can generate thumbnails for your KiCad PCB designs directly through Claude: + +``` +Please generate a thumbnail for my KiCad project at /path/to/my_project/my_project.kicad_pro +``` + +The tool will: +1. Find the PCB file (.kicad_pcb) associated with your project +2. Generate a visual representation of your PCB layout +3. Return an image that you can view directly in Claude + +## How It Works + +The thumbnail generator uses multiple methods to create PCB thumbnails, automatically falling back to alternative approaches if the primary method fails: + +1. **pcbnew Python Module (Primary Method)** + - Uses KiCad's official Python API for the most accurate representation + - Renders the PCB with proper layers, components, and traces + - Requires the KiCad Python modules to be installed and accessible + +2. **Command Line Interface (Fallback Method)** + - Uses KiCad's command-line tools (pcbnew_cli) when Python modules aren't available + - Creates high-quality renders similar to what you'd see in KiCad's PCB editor + - Works across different operating systems (macOS, Windows, Linux) + +## Thumbnail Examples + +When viewing a PCB thumbnail, you'll typically see: +- The PCB board outline (Edge.Cuts layer) +- Copper layers (F.Cu and B.Cu) +- Silkscreen layers (F.SilkS and B.SilkS) +- Mask layers (F.Mask and B.Mask) +- Component outlines and reference designators + +## Tips for Best Results + +For optimal thumbnail quality: + +1. **Ensure KiCad is properly installed** - The thumbnail generator relies on KiCad's libraries and tools +2. **Use the full absolute path** to your project file to avoid path resolution issues +3. **Make sure your PCB has a defined board outline** (Edge.Cuts layer) for proper visualization +4. **Update to the latest KiCad version** for best compatibility with the thumbnail generator + +## Troubleshooting + +If you encounter issues: + +- **No thumbnail generated**: Check that your project exists and contains a valid PCB file +- **Low-quality thumbnail**: Ensure your PCB has a properly defined board outline +- **"pcbnew module not found"**: This is expected if KiCad's Python modules aren't in your Python path + +## Integration Ideas + +The PCB thumbnail feature can be used in various ways: + +1. **Project browsing**: Generate thumbnails for all your projects to visually identify them +2. **Documentation**: Include PCB thumbnails in your project documentation +3. **Design review**: Use thumbnails to quickly check PCB layouts during discussions + +## Future Enhancements + +The thumbnail generation feature will be expanded in future releases with: + +- Higher quality rendering options +- Layer selection capabilities +- 3D rendering of PCB assemblies +- Annotation and markup support for design review diff --git a/kicad_mcp/server.py b/kicad_mcp/server.py index bd59488..16f3d50 100644 --- a/kicad_mcp/server.py +++ b/kicad_mcp/server.py @@ -51,7 +51,7 @@ def create_server() -> FastMCP: logger.debug("Registering tools...") register_project_tools(mcp) register_analysis_tools(mcp) - register_export_tools(mcp) + register_export_tools(mcp, kicad_modules_available) register_drc_tools(mcp, kicad_modules_available) # Register prompts diff --git a/kicad_mcp/tools/__init__.py b/kicad_mcp/tools/__init__.py index 16917aa..32cfe3d 100644 --- a/kicad_mcp/tools/__init__.py +++ b/kicad_mcp/tools/__init__.py @@ -1,3 +1,9 @@ """ Tool handlers for KiCad MCP Server. """ + +This package includes: +- Project management tools +- Analysis tools +- Export tools (BOM extraction, PCB thumbnail generation) +- DRC tools diff --git a/kicad_mcp/tools/export_tools.py b/kicad_mcp/tools/export_tools.py index be50508..2262ab2 100644 --- a/kicad_mcp/tools/export_tools.py +++ b/kicad_mcp/tools/export_tools.py @@ -64,6 +64,64 @@ def register_analysis_tools(mcp: FastMCP, kicad_modules_available: bool = False) logger.info(f"Validation result: {'valid' if result['valid'] else 'invalid'}") return result + @mcp.tool() + async def generate_pcb_thumbnail(project_path: str, ctx: Context) -> Optional[Image]: + """Generate a thumbnail image of a KiCad PCB layout. + + Args: + project_path: Path to the KiCad project file (.kicad_pro) + + Returns: + Thumbnail image of the PCB or None if generation failed + """ + logger.info(f"Generating thumbnail 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 None + + # Get PCB file from project + files = get_project_files(project_path) + if "pcb" not in files: + logger.error("PCB file not found in project") + ctx.info("PCB file not found in project") + return None + + pcb_file = files["pcb"] + logger.info(f"Found PCB file: {pcb_file}") + + await ctx.report_progress(10, 100) + ctx.info(f"Generating thumbnail for {os.path.basename(pcb_file)}") + + # Method 1: Try to use pcbnew Python module if available + if kicad_modules_available: + try: + thumbnail = await generate_thumbnail_with_pcbnew(pcb_file, ctx) + if thumbnail: + return thumbnail + + # If pcbnew method failed, log it but continue to try alternative method + logger.warning("Failed to generate thumbnail with pcbnew, trying CLI method") + except Exception as e: + logger.error(f"Error using pcbnew for thumbnail: {str(e)}", exc_info=True) + ctx.info(f"Error with pcbnew method, trying alternative approach") + else: + logger.info("KiCad Python modules not available, trying CLI method") + + # Method 2: Try to use command-line tools + try: + thumbnail = await generate_thumbnail_with_cli(pcb_file, ctx) + if thumbnail: + return thumbnail + except Exception as e: + logger.error(f"Error using CLI for thumbnail: {str(e)}", exc_info=True) + ctx.info(f"Error generating thumbnail with CLI method") + + # If all methods fail, inform the user + ctx.info("Could not generate thumbnail for PCB - all methods failed") + return None + @mcp.tool() async def generate_project_thumbnail(project_path: str, ctx: Context) -> Optional[Image]: """Generate a thumbnail of a KiCad project's PCB layout.""" @@ -173,3 +231,196 @@ def register_analysis_tools(mcp: FastMCP, kicad_modules_available: bool = False) logger.error(f"Error generating thumbnail: {str(e)}", exc_info=True) ctx.info(f"Error generating thumbnail: {str(e)}") return None + +# Helper functions for thumbnail generation +async def generate_thumbnail_with_pcbnew(pcb_file: str, ctx: Context) -> Optional[Image]: + """Generate PCB thumbnail using the pcbnew Python module. + + Args: + pcb_file: Path to the PCB file (.kicad_pcb) + ctx: MCP context for progress reporting + + Returns: + Image object containing the PCB thumbnail or None if generation failed + """ + try: + import pcbnew + logger.info("Successfully imported pcbnew module") + await ctx.report_progress(20, 100) + + # Load the PCB file + logger.debug(f"Loading PCB file with pcbnew: {pcb_file}") + board = pcbnew.LoadBoard(pcb_file) + if not board: + logger.error("Failed to load PCB file with pcbnew") + return None + + # Report progress + await ctx.report_progress(30, 100) + ctx.info("PCB file loaded, generating image...") + + # Get board dimensions + board_box = board.GetBoardEdgesBoundingBox() + width_mm = board_box.GetWidth() / 1000000.0 # Convert to mm + height_mm = board_box.GetHeight() / 1000000.0 + + logger.info(f"PCB dimensions: {width_mm:.2f}mm x {height_mm:.2f}mm") + + # Create temporary directory for output + with tempfile.TemporaryDirectory() as temp_dir: + logger.debug(f"Created temporary directory: {temp_dir}") + + # Create PLOT_CONTROLLER for plotting + pctl = pcbnew.PLOT_CONTROLLER(board) + popt = pctl.GetPlotOptions() + + # Set plot options for PNG output + popt.SetOutputDirectory(temp_dir) + popt.SetPlotFrameRef(False) + popt.SetPlotValue(True) + popt.SetPlotReference(True) + popt.SetPlotInvisibleText(False) + popt.SetPlotViaOnMaskLayer(False) + + # Set color mode (if available in this version) + if hasattr(popt, "SetColorMode"): + popt.SetColorMode(True) # Color mode + + # Set color theme (if available in this version) + if hasattr(popt, "SetColorTheme"): + popt.SetColorTheme("default") + + # Calculate a reasonable scale to fit in a thumbnail + max_pixels = 800 # Max pixel dimension + scale = min(max_pixels / width_mm, max_pixels / height_mm) * 0.8 # 80% to leave margin + + # Set plot scale if the function exists + if hasattr(popt, "SetScale"): + popt.SetScale(scale) + + # Determine output filename + plot_basename = "thumbnail" + + logger.debug(f"Plotting PCB to PNG") + await ctx.report_progress(50, 100) + + # Plot PNG + pctl.OpenPlotfile(plot_basename, pcbnew.PLOT_FORMAT_PNG, "Thumbnail") + pctl.PlotLayer() + pctl.ClosePlot() + + await ctx.report_progress(70, 100) + + # The plot controller creates files with predictable names + plot_file = os.path.join(temp_dir, f"{plot_basename}.png") + + if not os.path.exists(plot_file): + logger.error(f"Expected plot file not found: {plot_file}") + return None + + # Read the image file + with open(plot_file, 'rb') as f: + img_data = f.read() + + logger.info(f"Successfully generated thumbnail, size: {len(img_data)} bytes") + await ctx.report_progress(90, 100) + return Image(data=img_data, format="png") + + except ImportError as e: + logger.error(f"Failed to import pcbnew module: {str(e)}") + return None + except Exception as e: + logger.error(f"Error generating thumbnail with pcbnew: {str(e)}", exc_info=True) + return None + +async def generate_thumbnail_with_cli(pcb_file: str, ctx: Context) -> Optional[Image]: + """Generate PCB thumbnail using command line tools. + This is a fallback method when pcbnew Python module is not available. + + Args: + pcb_file: Path to the PCB file (.kicad_pcb) + ctx: MCP context for progress reporting + + Returns: + Image object containing the PCB thumbnail or None if generation failed + """ + import subprocess + + logger.info("Attempting to generate thumbnail using command line tools") + await ctx.report_progress(20, 100) + + # Check for required command-line tools based on OS + if system == "Darwin": # macOS + pcbnew_cli = os.path.join(KICAD_APP_PATH, "Contents/MacOS/pcbnew_cli") + if not os.path.exists(pcbnew_cli) and shutil.which("pcbnew_cli") is not None: + pcbnew_cli = "pcbnew_cli" # Try to use from PATH + elif not os.path.exists(pcbnew_cli): + logger.error(f"pcbnew_cli not found at {pcbnew_cli} or in PATH") + return None + elif system == "Windows": + pcbnew_cli = os.path.join(KICAD_APP_PATH, "bin", "pcbnew_cli.exe") + if not os.path.exists(pcbnew_cli) and shutil.which("pcbnew_cli") is not None: + pcbnew_cli = "pcbnew_cli" # Try to use from PATH + elif not os.path.exists(pcbnew_cli): + logger.error(f"pcbnew_cli not found at {pcbnew_cli} or in PATH") + return None + elif system == "Linux": + pcbnew_cli = shutil.which("pcbnew_cli") + if not pcbnew_cli: + logger.error("pcbnew_cli not found in PATH") + return None + else: + logger.error(f"Unsupported operating system: {system}") + return None + + await ctx.report_progress(30, 100) + ctx.info("Using KiCad command line tools for thumbnail generation") + + # Create temporary directory for output + with tempfile.TemporaryDirectory() as temp_dir: + # Output PNG file + output_file = os.path.join(temp_dir, "thumbnail.png") + + # Build command for generating PNG from PCB + cmd = [ + pcbnew_cli, + "--export-png", + output_file, + "--page-size-inches", "8x6", # Set a reasonable page size + "--layers", "F.Cu,B.Cu,F.SilkS,B.SilkS,F.Mask,B.Mask,Edge.Cuts", # Important layers + pcb_file + ] + + logger.debug(f"Running command: {' '.join(cmd)}") + await ctx.report_progress(50, 100) + + # Run the command + try: + process = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + if process.returncode != 0: + logger.error(f"Command failed with code {process.returncode}") + logger.error(f"Error: {process.stderr}") + return None + + await ctx.report_progress(70, 100) + + # Check if the output file was created + if not os.path.exists(output_file): + logger.error(f"Output file not created: {output_file}") + return None + + # Read the image file + with open(output_file, 'rb') as f: + img_data = f.read() + + logger.info(f"Successfully generated thumbnail with CLI, size: {len(img_data)} bytes") + await ctx.report_progress(90, 100) + return Image(data=img_data, format="png") + + except subprocess.TimeoutExpired: + logger.error("Command timed out after 30 seconds") + return None + except Exception as e: + logger.error(f"Error running CLI command: {str(e)}", exc_info=True) + return None