From 8c04038371f1c9c5c7efb61b570d632490951d76 Mon Sep 17 00:00:00 2001 From: ByteBard Date: Fri, 14 Nov 2025 16:38:16 -0500 Subject: [PATCH] various fixes for kicad 9 --- python/commands/board/layers.py | 11 +- python/commands/board/view.py | 27 ++- python/commands/component.py | 2 +- python/commands/design_rules.py | 343 +++++++++++++++++++++++++------- python/commands/export.py | 72 ++++--- src/server.ts | 65 +++--- 6 files changed, 377 insertions(+), 143 deletions(-) diff --git a/python/commands/board/layers.py b/python/commands/board/layers.py index 9d4d6c0..1e407ec 100644 --- a/python/commands/board/layers.py +++ b/python/commands/board/layers.py @@ -151,8 +151,9 @@ class BoardLayerCommands: layers.append({ "name": self.board.GetLayerName(layer_id), "type": self._get_layer_type_name(self.board.GetLayerType(layer_id)), - "id": layer_id, - "isActive": layer_id == self.board.GetActiveLayer() + "id": layer_id + # Note: isActive removed - GetActiveLayer() doesn't exist in KiCAD 9.0 + # Active layer is a UI concept not applicable to headless scripting }) return { @@ -173,7 +174,7 @@ class BoardLayerCommands: type_map = { "copper": pcbnew.LT_SIGNAL, "technical": pcbnew.LT_SIGNAL, - "user": pcbnew.LT_USER, + "user": pcbnew.LT_SIGNAL, # LT_USER removed in KiCAD 9.0, use LT_SIGNAL instead "signal": pcbnew.LT_SIGNAL } return type_map.get(type_name.lower(), pcbnew.LT_SIGNAL) @@ -184,7 +185,7 @@ class BoardLayerCommands: pcbnew.LT_SIGNAL: "signal", pcbnew.LT_POWER: "power", pcbnew.LT_MIXED: "mixed", - pcbnew.LT_JUMPER: "jumper", - pcbnew.LT_USER: "user" + pcbnew.LT_JUMPER: "jumper" } + # Note: LT_USER was removed in KiCAD 9.0 return type_map.get(type_id, "unknown") diff --git a/python/commands/board/view.py b/python/commands/board/view.py index 66f823f..8760880 100644 --- a/python/commands/board/view.py +++ b/python/commands/board/view.py @@ -58,8 +58,9 @@ class BoardViewCommands: "unit": "mm" }, "layers": layers, - "title": self.board.GetTitleBlock().GetTitle(), - "activeLayer": self.board.GetActiveLayer() + "title": self.board.GetTitleBlock().GetTitle() + # Note: activeLayer removed - GetActiveLayer() doesn't exist in KiCAD 9.0 + # Active layer is a UI concept not applicable to headless scripting } } @@ -95,26 +96,32 @@ class BoardViewCommands: plot_opts.SetOutputDirectory(os.path.dirname(self.board.GetFileName())) plot_opts.SetScale(1) plot_opts.SetMirror(False) - plot_opts.SetExcludeEdgeLayer(False) + # Note: SetExcludeEdgeLayer() removed in KiCAD 9.0 - default behavior includes all layers plot_opts.SetPlotFrameRef(False) plot_opts.SetPlotValue(True) plot_opts.SetPlotReference(True) # Plot to SVG first (for vector output) - temp_svg = os.path.join(os.path.dirname(self.board.GetFileName()), "temp_view.svg") + # Note: KiCAD 9.0 prepends the project name to the filename, so we use GetPlotFileName() to get the actual path plotter.OpenPlotfile("temp_view", pcbnew.PLOT_FORMAT_SVG, "Temporary View") - + # Plot specified layers or all enabled layers + # Note: In KiCAD 9.0, SetLayer() must be called before PlotLayer() if layers: for layer_name in layers: layer_id = self.board.GetLayerID(layer_name) if layer_id >= 0 and self.board.IsLayerEnabled(layer_id): - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() else: for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT): if self.board.IsLayerEnabled(layer_id): - plotter.PlotLayer(layer_id) - + plotter.SetLayer(layer_id) + plotter.PlotLayer() + + # Get the actual filename that was created (includes project name prefix) + temp_svg = plotter.GetPlotFileName() + plotter.ClosePlot() # Convert SVG to requested format @@ -165,7 +172,7 @@ class BoardViewCommands: pcbnew.LT_SIGNAL: "signal", pcbnew.LT_POWER: "power", pcbnew.LT_MIXED: "mixed", - pcbnew.LT_JUMPER: "jumper", - pcbnew.LT_USER: "user" + pcbnew.LT_JUMPER: "jumper" } + # Note: LT_USER was removed in KiCAD 9.0 return type_map.get(type_id, "unknown") diff --git a/python/commands/component.py b/python/commands/component.py index a9da4e5..d395145 100644 --- a/python/commands/component.py +++ b/python/commands/component.py @@ -405,7 +405,7 @@ class ComponentCommands: "attributes": { "smd": module.GetAttributes() & pcbnew.FP_SMD, "through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE, - "virtual": module.GetAttributes() & pcbnew.FP_VIRTUAL + "board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY } } } diff --git a/python/commands/design_rules.py b/python/commands/design_rules.py index 6054353..9ef158a 100644 --- a/python/commands/design_rules.py +++ b/python/commands/design_rules.py @@ -33,65 +33,82 @@ class DesignRuleCommands: # Set clearance if "clearance" in params: - design_settings.SetMinClearance(int(params["clearance"] * scale)) + design_settings.m_MinClearance = int(params["clearance"] * scale) + + # KiCAD 9.0: Use SetCustom* methods instead of SetCurrent* (which were removed) + # Track if we set any custom track/via values + custom_values_set = False - # Set track width if "trackWidth" in params: - design_settings.SetCurrentTrackWidth(int(params["trackWidth"] * scale)) + design_settings.SetCustomTrackWidth(int(params["trackWidth"] * scale)) + custom_values_set = True - # Set via settings + # Via settings if "viaDiameter" in params: - design_settings.SetCurrentViaSize(int(params["viaDiameter"] * scale)) + design_settings.SetCustomViaSize(int(params["viaDiameter"] * scale)) + custom_values_set = True if "viaDrill" in params: - design_settings.SetCurrentViaDrill(int(params["viaDrill"] * scale)) + design_settings.SetCustomViaDrill(int(params["viaDrill"] * scale)) + custom_values_set = True - # Set micro via settings + # KiCAD 9.0: Activate custom track/via values so they become the current values + if custom_values_set: + design_settings.UseCustomTrackViaSize(True) + + # Set micro via settings (use properties - methods removed in KiCAD 9.0) if "microViaDiameter" in params: - design_settings.SetCurrentMicroViaSize(int(params["microViaDiameter"] * scale)) + design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale) if "microViaDrill" in params: - design_settings.SetCurrentMicroViaDrill(int(params["microViaDrill"] * scale)) + design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale) # Set minimum values if "minTrackWidth" in params: design_settings.m_TrackMinWidth = int(params["minTrackWidth"] * scale) if "minViaDiameter" in params: design_settings.m_ViasMinSize = int(params["minViaDiameter"] * scale) + + # KiCAD 9.0: m_ViasMinDrill removed - use m_MinThroughDrill instead if "minViaDrill" in params: - design_settings.m_ViasMinDrill = int(params["minViaDrill"] * scale) + design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale) + if "minMicroViaDiameter" in params: design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale) if "minMicroViaDrill" in params: design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale) - # Set hole diameter + # KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill if "minHoleDiameter" in params: - design_settings.m_MinHoleDiameter = int(params["minHoleDiameter"] * scale) + design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale) - # Set courtyard settings - if "requireCourtyard" in params: - design_settings.m_RequireCourtyards = params["requireCourtyard"] - if "courtyardClearance" in params: - design_settings.m_CourtyardMinClearance = int(params["courtyardClearance"] * scale) + # KiCAD 9.0: Added hole clearance settings + if "holeClearance" in params: + design_settings.m_HoleClearance = int(params["holeClearance"] * scale) + if "holeToHoleMin" in params: + design_settings.m_HoleToHoleMin = int(params["holeToHoleMin"] * scale) + + # Build response with KiCAD 9.0 compatible properties + # After UseCustomTrackViaSize(True), GetCurrent* returns the custom values + response_rules = { + "clearance": design_settings.m_MinClearance / scale, + "trackWidth": design_settings.GetCurrentTrackWidth() / scale, + "viaDiameter": design_settings.GetCurrentViaSize() / scale, + "viaDrill": design_settings.GetCurrentViaDrill() / scale, + "microViaDiameter": design_settings.m_MicroViasMinSize / scale, + "microViaDrill": design_settings.m_MicroViasMinDrill / scale, + "minTrackWidth": design_settings.m_TrackMinWidth / scale, + "minViaDiameter": design_settings.m_ViasMinSize / scale, + "minThroughDrill": design_settings.m_MinThroughDrill / scale, + "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale, + "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale, + "holeClearance": design_settings.m_HoleClearance / scale, + "holeToHoleMin": design_settings.m_HoleToHoleMin / scale, + "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale + } return { "success": True, "message": "Updated design rules", - "rules": { - "clearance": design_settings.GetMinClearance() / scale, - "trackWidth": design_settings.GetCurrentTrackWidth() / scale, - "viaDiameter": design_settings.GetCurrentViaSize() / scale, - "viaDrill": design_settings.GetCurrentViaDrill() / scale, - "microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale, - "microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale, - "minTrackWidth": design_settings.m_TrackMinWidth / scale, - "minViaDiameter": design_settings.m_ViasMinSize / scale, - "minViaDrill": design_settings.m_ViasMinDrill / scale, - "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale, - "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale, - "minHoleDiameter": design_settings.m_MinHoleDiameter / scale, - "requireCourtyard": design_settings.m_RequireCourtyards, - "courtyardClearance": design_settings.m_CourtyardMinClearance / scale - } + "rules": response_rules } except Exception as e: @@ -103,7 +120,7 @@ class DesignRuleCommands: } def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]: - """Get current design rules""" + """Get current design rules - KiCAD 9.0 compatible""" try: if not self.board: return { @@ -115,24 +132,40 @@ class DesignRuleCommands: design_settings = self.board.GetDesignSettings() scale = 1000000 # nm to mm + # Build rules dict with KiCAD 9.0 compatible properties + rules = { + # Core clearance and track settings + "clearance": design_settings.m_MinClearance / scale, + "trackWidth": design_settings.GetCurrentTrackWidth() / scale, + "minTrackWidth": design_settings.m_TrackMinWidth / scale, + + # Via settings (current values from methods) + "viaDiameter": design_settings.GetCurrentViaSize() / scale, + "viaDrill": design_settings.GetCurrentViaDrill() / scale, + + # Via minimum values + "minViaDiameter": design_settings.m_ViasMinSize / scale, + "viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale, + + # Micro via settings + "microViaDiameter": design_settings.m_MicroViasMinSize / scale, + "microViaDrill": design_settings.m_MicroViasMinDrill / scale, + "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale, + "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale, + + # KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter) + "minThroughDrill": design_settings.m_MinThroughDrill / scale, + "holeClearance": design_settings.m_HoleClearance / scale, + "holeToHoleMin": design_settings.m_HoleToHoleMin / scale, + + # Other constraints + "copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale, + "silkClearance": design_settings.m_SilkClearance / scale, + } + return { "success": True, - "rules": { - "clearance": design_settings.GetMinClearance() / scale, - "trackWidth": design_settings.GetCurrentTrackWidth() / scale, - "viaDiameter": design_settings.GetCurrentViaSize() / scale, - "viaDrill": design_settings.GetCurrentViaDrill() / scale, - "microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale, - "microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale, - "minTrackWidth": design_settings.m_TrackMinWidth / scale, - "minViaDiameter": design_settings.m_ViasMinSize / scale, - "minViaDrill": design_settings.m_ViasMinDrill / scale, - "minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale, - "minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale, - "minHoleDiameter": design_settings.m_MinHoleDiameter / scale, - "requireCourtyard": design_settings.m_RequireCourtyards, - "courtyardClearance": design_settings.m_CourtyardMinClearance / scale - } + "rules": rules } except Exception as e: @@ -144,7 +177,13 @@ class DesignRuleCommands: } def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]: - """Run Design Rule Check""" + """Run Design Rule Check using kicad-cli""" + import subprocess + import json + import tempfile + import platform + import shutil + try: if not self.board: return { @@ -155,38 +194,144 @@ class DesignRuleCommands: report_path = params.get("reportPath") - # Create DRC runner - drc = pcbnew.DRC(self.board) - - # Run DRC - drc.Run() + # Get the board file path + board_file = self.board.GetFileName() + if not board_file or not os.path.exists(board_file): + return { + "success": False, + "message": "Board file not found", + "errorDetails": "Cannot run DRC without a saved board file" + } - # Get violations - violations = [] - for marker in drc.GetMarkers(): - violations.append({ - "type": marker.GetErrorCode(), - "severity": "error", - "message": marker.GetDescription(), - "location": { - "x": marker.GetPos().x / 1000000, - "y": marker.GetPos().y / 1000000, - "unit": "mm" + # Find kicad-cli executable + kicad_cli = self._find_kicad_cli() + if not kicad_cli: + return { + "success": False, + "message": "kicad-cli not found", + "errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH." + } + + # Create temporary JSON output file + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp: + json_output = tmp.name + + try: + # Build command + cmd = [ + kicad_cli, + 'pcb', + 'drc', + '--format', 'json', + '--output', json_output, + '--units', 'mm', + board_file + ] + + logger.info(f"Running DRC command: {' '.join(cmd)}") + + # Run DRC + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=600 # 10 minute timeout for large boards (21MB PCB needs time) + ) + + if result.returncode != 0: + logger.error(f"DRC command failed: {result.stderr}") + return { + "success": False, + "message": "DRC command failed", + "errorDetails": result.stderr } - }) - # Save report if path provided - if report_path: - report_path = os.path.abspath(os.path.expanduser(report_path)) - drc.WriteReport(report_path) + # Read JSON output + with open(json_output, 'r', encoding='utf-8') as f: + drc_data = json.load(f) + # Parse violations from kicad-cli output + violations = [] + violation_counts = {} + severity_counts = {"error": 0, "warning": 0, "info": 0} + + for violation in drc_data.get('violations', []): + vtype = violation.get("type", "unknown") + vseverity = violation.get("severity", "error") + + violations.append({ + "type": vtype, + "severity": vseverity, + "message": violation.get("description", ""), + "location": { + "x": violation.get("x", 0), + "y": violation.get("y", 0), + "unit": "mm" + } + }) + + # Count violations by type + violation_counts[vtype] = violation_counts.get(vtype, 0) + 1 + + # Count by severity + if vseverity in severity_counts: + severity_counts[vseverity] += 1 + + # Determine where to save the violations file + board_dir = os.path.dirname(board_file) + board_name = os.path.splitext(os.path.basename(board_file))[0] + violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json") + + # Always save violations to JSON file (for large result sets) + with open(violations_file, 'w', encoding='utf-8') as f: + json.dump({ + "board": board_file, + "timestamp": drc_data.get("date", "unknown"), + "total_violations": len(violations), + "violation_counts": violation_counts, + "severity_counts": severity_counts, + "violations": violations + }, f, indent=2) + + # Save text report if requested + if report_path: + report_path = os.path.abspath(os.path.expanduser(report_path)) + cmd_report = [ + kicad_cli, + 'pcb', + 'drc', + '--format', 'report', + '--output', report_path, + '--units', 'mm', + board_file + ] + subprocess.run(cmd_report, capture_output=True, timeout=600) + + # Return summary only (not full violations list) + return { + "success": True, + "message": f"Found {len(violations)} DRC violations", + "summary": { + "total": len(violations), + "by_severity": severity_counts, + "by_type": violation_counts + }, + "violationsFile": violations_file, + "reportPath": report_path if report_path else None + } + + finally: + # Clean up temp JSON file + if os.path.exists(json_output): + os.unlink(json_output) + + except subprocess.TimeoutExpired: + logger.error("DRC command timed out") return { - "success": True, - "message": f"Found {len(violations)} DRC violations", - "violations": violations, - "reportPath": report_path if report_path else None + "success": False, + "message": "DRC command timed out", + "errorDetails": "Command took longer than 600 seconds (10 minutes)" } - except Exception as e: logger.error(f"Error running DRC: {str(e)}") return { @@ -195,6 +340,50 @@ class DesignRuleCommands: "errorDetails": str(e) } + def _find_kicad_cli(self) -> Optional[str]: + """Find kicad-cli executable""" + import platform + import shutil + + # Try system PATH first + cli_name = "kicad-cli.exe" if platform.system() == "Windows" else "kicad-cli" + cli_path = shutil.which(cli_name) + if cli_path: + return cli_path + + # Try common installation paths (version-specific) + if platform.system() == "Windows": + common_paths = [ + r"C:\Program Files\KiCad\10.0\bin\kicad-cli.exe", + r"C:\Program Files\KiCad\9.0\bin\kicad-cli.exe", + r"C:\Program Files\KiCad\8.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\10.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\9.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\8.0\bin\kicad-cli.exe", + r"C:\Program Files\KiCad\bin\kicad-cli.exe", + ] + for path in common_paths: + if os.path.exists(path): + return path + elif platform.system() == "Darwin": # macOS + common_paths = [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "/usr/local/bin/kicad-cli", + ] + for path in common_paths: + if os.path.exists(path): + return path + else: # Linux + common_paths = [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli", + ] + for path in common_paths: + if os.path.exists(path): + return path + + return None + def get_drc_violations(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get list of DRC violations""" try: diff --git a/python/commands/export.py b/python/commands/export.py index 255333d..059f32b 100644 --- a/python/commands/export.py +++ b/python/commands/export.py @@ -63,13 +63,15 @@ class ExportCommands: for layer_name in layers: layer_id = self.board.GetLayerID(layer_name) if layer_id >= 0: - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() plotted_layers.append(layer_name) else: for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT): if self.board.IsLayerEnabled(layer_id): layer_name = self.board.GetLayerName(layer_id) - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() plotted_layers.append(layer_name) # Generate drill files if requested @@ -137,7 +139,7 @@ class ExportCommands: # Create plot controller plotter = pcbnew.PLOT_CONTROLLER(self.board) - + # Set up plot options plot_opts = plotter.GetPlotOptions() plot_opts.SetOutputDirectory(os.path.dirname(output_path)) @@ -145,22 +147,28 @@ class ExportCommands: plot_opts.SetPlotFrameRef(frame_reference) plot_opts.SetPlotValue(True) plot_opts.SetPlotReference(True) - plot_opts.SetMonochrome(black_and_white) + plot_opts.SetBlackAndWhite(black_and_white) - # Set page size - page_sizes = { - "A4": (297, 210), - "A3": (420, 297), - "A2": (594, 420), - "A1": (841, 594), - "A0": (1189, 841), - "Letter": (279.4, 215.9), - "Legal": (355.6, 215.9), - "Tabloid": (431.8, 279.4) - } - if page_size in page_sizes: - height, width = page_sizes[page_size] - plot_opts.SetPageSettings((width, height)) + # KiCAD 9.0 page size handling: + # - SetPageSettings() was removed in KiCAD 9.0 + # - SetA4Output(bool) forces A4 page size when True + # - For other sizes, KiCAD auto-scales to fit the board + # - SetAutoScale(True) enables automatic scaling to fit page + if page_size == "A4": + plot_opts.SetA4Output(True) + else: + # For non-A4 sizes, disable A4 forcing and use auto-scale + plot_opts.SetA4Output(False) + plot_opts.SetAutoScale(True) + # Note: KiCAD 9.0 doesn't support explicit page size selection + # for formats other than A4. The PDF will auto-scale to fit. + logger.warning(f"Page size '{page_size}' requested, but KiCAD 9.0 only supports A4 explicitly. Using auto-scale instead.") + + # Open plot for writing + # Note: For PDF, all layers are combined into a single file + # KiCAD prepends the board filename to the plot file name + base_name = os.path.basename(output_path).replace('.pdf', '') + plotter.OpenPlotfile(base_name, pcbnew.PLOT_FORMAT_PDF, '') # Plot specified layers or all enabled layers plotted_layers = [] @@ -168,22 +176,34 @@ class ExportCommands: for layer_name in layers: layer_id = self.board.GetLayerID(layer_name) if layer_id >= 0: - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() plotted_layers.append(layer_name) else: for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT): if self.board.IsLayerEnabled(layer_id): layer_name = self.board.GetLayerName(layer_id) - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() plotted_layers.append(layer_name) + # Close the plot file to finalize the PDF + plotter.ClosePlot() + + # KiCAD automatically prepends the board name to the output file + # Get the actual output filename that was created + board_name = os.path.splitext(os.path.basename(self.board.GetFileName()))[0] + actual_filename = f"{board_name}-{base_name}.pdf" + actual_output_path = os.path.join(os.path.dirname(output_path), actual_filename) + return { "success": True, "message": "Exported PDF file", "file": { - "path": output_path, + "path": actual_output_path, + "requestedPath": output_path, "layers": plotted_layers, - "pageSize": page_size + "pageSize": page_size if page_size == "A4" else "auto-scaled" } } @@ -230,7 +250,7 @@ class ExportCommands: plot_opts.SetFormat(pcbnew.PLOT_FORMAT_SVG) plot_opts.SetPlotValue(include_components) plot_opts.SetPlotReference(include_components) - plot_opts.SetMonochrome(black_and_white) + plot_opts.SetBlackAndWhite(black_and_white) # Plot specified layers or all enabled layers plotted_layers = [] @@ -238,13 +258,15 @@ class ExportCommands: for layer_name in layers: layer_id = self.board.GetLayerID(layer_name) if layer_id >= 0: - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() plotted_layers.append(layer_name) else: for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT): if self.board.IsLayerEnabled(layer_id): layer_name = self.board.GetLayerName(layer_id) - plotter.PlotLayer(layer_id) + plotter.SetLayer(layer_id) + plotter.PlotLayer() plotted_layers.append(layer_name) return { diff --git a/src/server.ts b/src/server.ts index 3e8fd72..f2dec85 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,7 +5,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import express from 'express'; -import { spawn, ChildProcess } from 'child_process'; +import { spawn, exec, ChildProcess } from 'child_process'; import { existsSync } from 'fs'; import { join, dirname } from 'path'; import { logger } from './logger.js'; @@ -176,7 +176,6 @@ export class KiCADMcpServer { logger.info('Validating pcbnew module access...'); const testCommand = `"${pythonExe}" -c "import pcbnew; print('OK')"`; - const { exec } = require('child_process'); try { const { stdout, stderr } = await new Promise<{stdout: string, stderr: string}>((resolve, reject) => { @@ -325,7 +324,7 @@ export class KiCADMcpServer { /** * Call the KiCAD scripting interface to execute commands - * + * * @param command The command to execute * @param params The parameters for the command * @returns The result of the command execution @@ -338,14 +337,23 @@ export class KiCADMcpServer { reject(new Error("Python process for KiCAD scripting is not running")); return; } - - // Add request to queue + + // Determine timeout based on command type + // DRC and export operations need longer timeouts for large boards + let commandTimeout = 30000; // Default 30 seconds + const longRunningCommands = ['run_drc', 'export_gerber', 'export_pdf', 'export_3d']; + if (longRunningCommands.includes(command)) { + commandTimeout = 600000; // 10 minutes for long operations + logger.info(`Using extended timeout (${commandTimeout/1000}s) for command: ${command}`); + } + + // Add request to queue with timeout info this.requestQueue.push({ - request: { command, params }, + request: { command, params, timeout: commandTimeout }, resolve, reject }); - + // Process the queue if not already processing if (!this.processingRequest) { this.processNextRequest(); @@ -376,40 +384,46 @@ export class KiCADMcpServer { // Set up response handling let responseData = ''; - + let timeoutHandle: NodeJS.Timeout | null = null; + // Clear any previous listeners if (this.pythonProcess?.stdout) { this.pythonProcess.stdout.removeAllListeners('data'); this.pythonProcess.stdout.removeAllListeners('end'); } - + // Set up new listeners if (this.pythonProcess?.stdout) { this.pythonProcess.stdout.on('data', (data: Buffer) => { const chunk = data.toString(); logger.debug(`Received data chunk: ${chunk.length} bytes`); responseData += chunk; - + // Check if we have a complete response try { // Try to parse the response as JSON const result = JSON.parse(responseData); - + // If we get here, we have a valid JSON response logger.debug(`Completed KiCAD command: ${request.command} with result: ${result.success ? 'success' : 'failure'}`); - + + // Clear the timeout since we got a response + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + // Reset processing flag this.processingRequest = false; - + // Process next request if any setTimeout(() => this.processNextRequest(), 0); - + // Clear listeners if (this.pythonProcess?.stdout) { this.pythonProcess.stdout.removeAllListeners('data'); this.pythonProcess.stdout.removeAllListeners('end'); } - + // Resolve the promise with the result resolve(result); } catch (e) { @@ -417,26 +431,27 @@ export class KiCADMcpServer { } }); } - - // Set a timeout - const timeout = setTimeout(() => { - logger.error(`Command timeout: ${request.command}`); - + + // Set a timeout (use command-specific timeout or default) + const timeoutDuration = request.timeout || 30000; + timeoutHandle = setTimeout(() => { + logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`); + // Clear listeners if (this.pythonProcess?.stdout) { this.pythonProcess.stdout.removeAllListeners('data'); this.pythonProcess.stdout.removeAllListeners('end'); } - + // Reset processing flag this.processingRequest = false; - + // Process next request setTimeout(() => this.processNextRequest(), 0); - + // Reject the promise - reject(new Error(`Command timeout: ${request.command}`)); - }, 30000); // 30 seconds timeout + reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`)); + }, timeoutDuration); // Write the request to the Python process logger.debug(`Sending request: ${requestStr}`);