diff --git a/python/commands/board/outline.py b/python/commands/board/outline.py index e826000..d003213 100644 --- a/python/commands/board/outline.py +++ b/python/commands/board/outline.py @@ -7,7 +7,8 @@ import logging import math from typing import Dict, Any, Optional -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") + class BoardOutlineCommands: """Handles board outline operations""" @@ -23,7 +24,7 @@ class BoardOutlineCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } shape = params.get("shape", "rectangle") @@ -40,46 +41,54 @@ class BoardOutlineCommands: return { "success": False, "message": "Invalid shape", - "errorDetails": f"Shape '{shape}' not supported" + "errorDetails": f"Shape '{shape}' not supported", } # Convert to internal units (nanometers) scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm - + # Create drawing for edge cuts edge_layer = self.board.GetLayerID("Edge.Cuts") - + if shape == "rectangle": if width is None or height is None: return { "success": False, "message": "Missing dimensions", - "errorDetails": "Both width and height are required for rectangle" + "errorDetails": "Both width and height are required for rectangle", } width_nm = int(width * scale) height_nm = int(height * scale) center_x_nm = int(center_x * scale) center_y_nm = int(center_y * scale) - + # Create rectangle - top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2) - top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2) - bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2) - bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2) - + top_left = pcbnew.VECTOR2I( + center_x_nm - width_nm // 2, center_y_nm - height_nm // 2 + ) + top_right = pcbnew.VECTOR2I( + center_x_nm + width_nm // 2, center_y_nm - height_nm // 2 + ) + bottom_right = pcbnew.VECTOR2I( + center_x_nm + width_nm // 2, center_y_nm + height_nm // 2 + ) + bottom_left = pcbnew.VECTOR2I( + center_x_nm - width_nm // 2, center_y_nm + height_nm // 2 + ) + # Add lines for rectangle self._add_edge_line(top_left, top_right, edge_layer) self._add_edge_line(top_right, bottom_right, edge_layer) self._add_edge_line(bottom_right, bottom_left, edge_layer) self._add_edge_line(bottom_left, top_left, edge_layer) - + elif shape == "rounded_rectangle": if width is None or height is None: return { "success": False, "message": "Missing dimensions", - "errorDetails": "Both width and height are required for rounded rectangle" + "errorDetails": "Both width and height are required for rounded rectangle", } width_nm = int(width * scale) @@ -87,26 +96,29 @@ class BoardOutlineCommands: center_x_nm = int(center_x * scale) center_y_nm = int(center_y * scale) corner_radius_nm = int(corner_radius * scale) - + # Create rounded rectangle self._add_rounded_rect( - center_x_nm, center_y_nm, - width_nm, height_nm, - corner_radius_nm, edge_layer + center_x_nm, + center_y_nm, + width_nm, + height_nm, + corner_radius_nm, + edge_layer, ) - + elif shape == "circle": if radius is None: return { "success": False, "message": "Missing radius", - "errorDetails": "Radius is required for circle" + "errorDetails": "Radius is required for circle", } center_x_nm = int(center_x * scale) center_y_nm = int(center_y * scale) radius_nm = int(radius * scale) - + # Create circle circle = pcbnew.PCB_SHAPE(self.board) circle.SetShape(pcbnew.SHAPE_T_CIRCLE) @@ -121,7 +133,7 @@ class BoardOutlineCommands: return { "success": False, "message": "Missing points", - "errorDetails": "At least 3 points are required for polygon" + "errorDetails": "At least 3 points are required for polygon", } # Convert points to nm @@ -130,13 +142,13 @@ class BoardOutlineCommands: x_nm = int(point["x"] * scale) y_nm = int(point["y"] * scale) polygon_points.append(pcbnew.VECTOR2I(x_nm, y_nm)) - + # Add lines for polygon for i in range(len(polygon_points)): self._add_edge_line( - polygon_points[i], - polygon_points[(i + 1) % len(polygon_points)], - edge_layer + polygon_points[i], + polygon_points[(i + 1) % len(polygon_points)], + edge_layer, ) return { @@ -149,8 +161,8 @@ class BoardOutlineCommands: "center": {"x": center_x, "y": center_y, "unit": unit}, "radius": radius, "cornerRadius": corner_radius, - "points": points - } + "points": points, + }, } except Exception as e: @@ -158,7 +170,7 @@ class BoardOutlineCommands: return { "success": False, "message": "Failed to add board outline", - "errorDetails": str(e) + "errorDetails": str(e), } def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -168,7 +180,7 @@ class BoardOutlineCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } position = params.get("position") @@ -180,19 +192,24 @@ class BoardOutlineCommands: return { "success": False, "message": "Missing parameters", - "errorDetails": "position and diameter are required" + "errorDetails": "position and diameter are required", } # Convert to internal units (nanometers) - scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm + scale = ( + 1000000 if position.get("unit", "mm") == "mm" else 25400000 + ) # mm or inch to nm x_nm = int(position["x"] * scale) y_nm = int(position["y"] * scale) diameter_nm = int(diameter * scale) - pad_diameter_nm = int(pad_diameter * scale) if pad_diameter else diameter_nm + scale # 1mm larger by default + pad_diameter_nm = ( + int(pad_diameter * scale) if pad_diameter else diameter_nm + scale + ) # 1mm larger by default # Create footprint for mounting hole with unique reference existing_mh = [ - fp.GetReference() for fp in self.board.GetFootprints() + fp.GetReference() + for fp in self.board.GetFootprints() if fp.GetReference().startswith("MH") ] next_num = 1 @@ -202,20 +219,22 @@ class BoardOutlineCommands: module = pcbnew.FOOTPRINT(self.board) module.SetReference(f"MH{next_num}") module.SetValue(f"MountingHole_{diameter}mm") - + # Create the pad for the hole pad = pcbnew.PAD(module) pad.SetNumber(1) pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE) - pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH) + pad.SetAttribute( + pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH + ) pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm)) pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm)) pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module module.Add(pad) - + # Position the mounting hole module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm)) - + # Add to board self.board.Add(module) @@ -226,8 +245,8 @@ class BoardOutlineCommands: "position": position, "diameter": diameter, "padDiameter": pad_diameter or diameter + 1, - "plated": plated - } + "plated": plated, + }, } except Exception as e: @@ -235,7 +254,7 @@ class BoardOutlineCommands: return { "success": False, "message": "Failed to add mounting hole", - "errorDetails": str(e) + "errorDetails": str(e), } def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -245,7 +264,7 @@ class BoardOutlineCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } text = params.get("text") @@ -260,11 +279,13 @@ class BoardOutlineCommands: return { "success": False, "message": "Missing parameters", - "errorDetails": "text and position are required" + "errorDetails": "text and position are required", } # Convert to internal units (nanometers) - scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm + scale = ( + 1000000 if position.get("unit", "mm") == "mm" else 25400000 + ) # mm or inch to nm x_nm = int(position["x"] * scale) y_nm = int(position["y"] * scale) size_nm = int(size * scale) @@ -276,7 +297,7 @@ class BoardOutlineCommands: return { "success": False, "message": "Invalid layer", - "errorDetails": f"Layer '{layer}' does not exist" + "errorDetails": f"Layer '{layer}' does not exist", } # Create text @@ -297,7 +318,7 @@ class BoardOutlineCommands: pcb_text.SetTextAngle(int(rotation * 10)) pcb_text.SetMirrored(mirror) - + # Add to board self.board.Add(pcb_text) @@ -311,8 +332,8 @@ class BoardOutlineCommands: "size": size, "thickness": thickness, "rotation": rotation, - "mirror": mirror - } + "mirror": mirror, + }, } except Exception as e: @@ -320,10 +341,12 @@ class BoardOutlineCommands: return { "success": False, "message": "Failed to add text", - "errorDetails": str(e) + "errorDetails": str(e), } - def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None: + def _add_edge_line( + self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int + ) -> None: """Add a line to the edge cuts layer""" line = pcbnew.PCB_SHAPE(self.board) line.SetShape(pcbnew.SHAPE_T_SEGMENT) @@ -333,96 +356,112 @@ class BoardOutlineCommands: line.SetWidth(0) # Zero width for edge cuts self.board.Add(line) - def _add_rounded_rect(self, center_x_nm: int, center_y_nm: int, - width_nm: int, height_nm: int, - radius_nm: int, layer: int) -> None: + def _add_rounded_rect( + self, + center_x_nm: int, + center_y_nm: int, + width_nm: int, + height_nm: int, + radius_nm: int, + layer: int, + ) -> None: """Add a rounded rectangle to the edge cuts layer""" if radius_nm <= 0: # If no radius, create regular rectangle - top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2) - top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2) - bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2) - bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2) - + top_left = pcbnew.VECTOR2I( + center_x_nm - width_nm // 2, center_y_nm - height_nm // 2 + ) + top_right = pcbnew.VECTOR2I( + center_x_nm + width_nm // 2, center_y_nm - height_nm // 2 + ) + bottom_right = pcbnew.VECTOR2I( + center_x_nm + width_nm // 2, center_y_nm + height_nm // 2 + ) + bottom_left = pcbnew.VECTOR2I( + center_x_nm - width_nm // 2, center_y_nm + height_nm // 2 + ) + self._add_edge_line(top_left, top_right, layer) self._add_edge_line(top_right, bottom_right, layer) self._add_edge_line(bottom_right, bottom_left, layer) self._add_edge_line(bottom_left, top_left, layer) return - + # Calculate corner centers half_width = width_nm // 2 half_height = height_nm // 2 - + # Ensure radius is not larger than half the smallest dimension max_radius = min(half_width, half_height) if radius_nm > max_radius: radius_nm = max_radius - + # Calculate corner centers top_left_center = pcbnew.VECTOR2I( - center_x_nm - half_width + radius_nm, - center_y_nm - half_height + radius_nm + center_x_nm - half_width + radius_nm, center_y_nm - half_height + radius_nm ) top_right_center = pcbnew.VECTOR2I( - center_x_nm + half_width - radius_nm, - center_y_nm - half_height + radius_nm + center_x_nm + half_width - radius_nm, center_y_nm - half_height + radius_nm ) bottom_right_center = pcbnew.VECTOR2I( - center_x_nm + half_width - radius_nm, - center_y_nm + half_height - radius_nm + center_x_nm + half_width - radius_nm, center_y_nm + half_height - radius_nm ) bottom_left_center = pcbnew.VECTOR2I( - center_x_nm - half_width + radius_nm, - center_y_nm + half_height - radius_nm + center_x_nm - half_width + radius_nm, center_y_nm + half_height - radius_nm ) - + # Add arcs for corners self._add_corner_arc(top_left_center, radius_nm, 180, 270, layer) self._add_corner_arc(top_right_center, radius_nm, 270, 0, layer) self._add_corner_arc(bottom_right_center, radius_nm, 0, 90, layer) self._add_corner_arc(bottom_left_center, radius_nm, 90, 180, layer) - + # Add lines for straight edges # Top edge self._add_edge_line( pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm), pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm), - layer + layer, ) # Right edge self._add_edge_line( pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y), pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y), - layer + layer, ) # Bottom edge self._add_edge_line( pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm), pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm), - layer + layer, ) # Left edge self._add_edge_line( pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y), pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y), - layer + layer, ) - def _add_corner_arc(self, center: pcbnew.VECTOR2I, radius: int, - start_angle: float, end_angle: float, layer: int) -> None: + def _add_corner_arc( + self, + center: pcbnew.VECTOR2I, + radius: int, + start_angle: float, + end_angle: float, + layer: int, + ) -> None: """Add an arc for a rounded corner""" # Create arc for corner arc = pcbnew.PCB_SHAPE(self.board) arc.SetShape(pcbnew.SHAPE_T_ARC) arc.SetCenter(center) - + # Calculate start and end points start_x = center.x + int(radius * math.cos(math.radians(start_angle))) start_y = center.y + int(radius * math.sin(math.radians(start_angle))) end_x = center.x + int(radius * math.cos(math.radians(end_angle))) end_y = center.y + int(radius * math.sin(math.radians(end_angle))) - + arc.SetStart(pcbnew.VECTOR2I(start_x, start_y)) arc.SetEnd(pcbnew.VECTOR2I(end_x, end_y)) arc.SetLayer(layer) diff --git a/python/commands/export.py b/python/commands/export.py index d773431..5c74a66 100644 --- a/python/commands/export.py +++ b/python/commands/export.py @@ -8,7 +8,8 @@ import logging from typing import Dict, Any, Optional, List, Tuple import base64 -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") + class ExportCommands: """Handles export-related KiCAD operations""" @@ -24,7 +25,7 @@ class ExportCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } output_dir = params.get("outputDir") @@ -38,7 +39,7 @@ class ExportCommands: return { "success": False, "message": "Missing output directory", - "errorDetails": "outputDir parameter is required" + "errorDetails": "outputDir parameter is required", } # Create output directory if it doesn't exist @@ -47,7 +48,7 @@ class ExportCommands: # Create plot controller plotter = pcbnew.PLOT_CONTROLLER(self.board) - + # Set up plot options plot_opts = plotter.GetPlotOptions() plot_opts.SetOutputDirectory(output_dir) @@ -84,28 +85,40 @@ class ExportCommands: if kicad_cli and board_file and os.path.exists(board_file): import subprocess + # Generate drill files using kicad-cli cmd = [ kicad_cli, - 'pcb', 'export', 'drill', - '--output', output_dir, - '--format', 'excellon', - '--drill-origin', 'absolute', - '--excellon-separate-th', # Separate plated/non-plated - board_file + "pcb", + "export", + "drill", + "--output", + output_dir, + "--format", + "excellon", + "--drill-origin", + "absolute", + "--excellon-separate-th", # Separate plated/non-plated + board_file, ] try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=60 + ) if result.returncode == 0: # Get list of generated drill files for file in os.listdir(output_dir): if file.endswith((".drl", ".cnc")): drill_files.append(file) else: - logger.warning(f"Drill file generation failed: {result.stderr}") + logger.warning( + f"Drill file generation failed: {result.stderr}" + ) except Exception as drill_error: - logger.warning(f"Could not generate drill files: {str(drill_error)}") + logger.warning( + f"Could not generate drill files: {str(drill_error)}" + ) else: logger.warning("kicad-cli not available for drill file generation") @@ -115,9 +128,9 @@ class ExportCommands: "files": { "gerber": plotted_layers, "drill": drill_files, - "map": ["job.gbrjob"] if generate_map_file else [] + "map": ["job.gbrjob"] if generate_map_file else [], }, - "outputDir": output_dir + "outputDir": output_dir, } except Exception as e: @@ -125,7 +138,7 @@ class ExportCommands: return { "success": False, "message": "Failed to export Gerber files", - "errorDetails": str(e) + "errorDetails": str(e), } def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -135,7 +148,7 @@ class ExportCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } output_path = params.get("outputPath") @@ -148,7 +161,7 @@ class ExportCommands: return { "success": False, "message": "Missing output path", - "errorDetails": "outputPath parameter is required" + "errorDetails": "outputPath parameter is required", } # Create output directory if it doesn't exist @@ -180,13 +193,15 @@ class ExportCommands: 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.") + 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, '') + 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 = [] @@ -212,7 +227,9 @@ class ExportCommands: # 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) + actual_output_path = os.path.join( + os.path.dirname(output_path), actual_filename + ) return { "success": True, @@ -221,8 +238,8 @@ class ExportCommands: "path": actual_output_path, "requestedPath": output_path, "layers": plotted_layers, - "pageSize": page_size if page_size == "A4" else "auto-scaled" - } + "pageSize": page_size if page_size == "A4" else "auto-scaled", + }, } except Exception as e: @@ -230,7 +247,7 @@ class ExportCommands: return { "success": False, "message": "Failed to export PDF file", - "errorDetails": str(e) + "errorDetails": str(e), } def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -240,7 +257,7 @@ class ExportCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } output_path = params.get("outputPath") @@ -252,7 +269,7 @@ class ExportCommands: return { "success": False, "message": "Missing output path", - "errorDetails": "outputPath parameter is required" + "errorDetails": "outputPath parameter is required", } # Create output directory if it doesn't exist @@ -261,7 +278,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)) @@ -290,10 +307,7 @@ class ExportCommands: return { "success": True, "message": "Exported SVG file", - "file": { - "path": output_path, - "layers": plotted_layers - } + "file": {"path": output_path, "layers": plotted_layers}, } except Exception as e: @@ -301,7 +315,7 @@ class ExportCommands: return { "success": False, "message": "Failed to export SVG file", - "errorDetails": str(e) + "errorDetails": str(e), } def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -315,7 +329,7 @@ class ExportCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } output_path = params.get("outputPath") @@ -329,7 +343,7 @@ class ExportCommands: return { "success": False, "message": "Missing output path", - "errorDetails": "outputPath parameter is required" + "errorDetails": "outputPath parameter is required", } # Get board file path @@ -338,7 +352,7 @@ class ExportCommands: return { "success": False, "message": "Board file not found", - "errorDetails": "Board must be saved before exporting 3D models" + "errorDetails": "Board must be saved before exporting 3D models", } # Create output directory if it doesn't exist @@ -351,7 +365,7 @@ class ExportCommands: return { "success": False, "message": "kicad-cli not found", - "errorDetails": "KiCAD CLI tool not found. Install KiCAD 8.0+ or set PATH." + "errorDetails": "KiCAD CLI tool not found. Install KiCAD 8.0+ or set PATH.", } # Build command based on format @@ -360,30 +374,39 @@ class ExportCommands: if format_upper == "STEP": cmd = [ kicad_cli, - 'pcb', 'export', 'step', - '--output', output_path, - '--force' # Overwrite existing file + "pcb", + "export", + "step", + "--output", + output_path, + "--force", # Overwrite existing file ] # Add options based on parameters if not include_components: - cmd.append('--no-components') + cmd.append("--no-components") if include_copper: - cmd.extend(['--include-tracks', '--include-pads', '--include-zones']) + cmd.extend( + ["--include-tracks", "--include-pads", "--include-zones"] + ) if include_silkscreen: - cmd.append('--include-silkscreen') + cmd.append("--include-silkscreen") if include_solder_mask: - cmd.append('--include-soldermask') + cmd.append("--include-soldermask") cmd.append(board_file) elif format_upper == "VRML": cmd = [ kicad_cli, - 'pcb', 'export', 'vrml', - '--output', output_path, - '--units', 'mm', # Use mm for consistency - '--force' + "pcb", + "export", + "vrml", + "--output", + output_path, + "--units", + "mm", # Use mm for consistency + "--force", ] if not include_components: @@ -397,7 +420,7 @@ class ExportCommands: return { "success": False, "message": "Unsupported format", - "errorDetails": f"Format {format} is not supported. Use 'STEP' or 'VRML'." + "errorDetails": f"Format {format} is not supported. Use 'STEP' or 'VRML'.", } # Execute kicad-cli command @@ -407,7 +430,7 @@ class ExportCommands: cmd, capture_output=True, text=True, - timeout=300 # 5 minute timeout for 3D export + timeout=300, # 5 minute timeout for 3D export ) if result.returncode != 0: @@ -415,16 +438,13 @@ class ExportCommands: return { "success": False, "message": "3D export command failed", - "errorDetails": result.stderr + "errorDetails": result.stderr, } return { "success": True, "message": f"Exported {format_upper} file", - "file": { - "path": output_path, - "format": format_upper - } + "file": {"path": output_path, "format": format_upper}, } except subprocess.TimeoutExpired: @@ -432,14 +452,14 @@ class ExportCommands: return { "success": False, "message": "3D export timed out", - "errorDetails": "Export took longer than 5 minutes" + "errorDetails": "Export took longer than 5 minutes", } except Exception as e: logger.error(f"Error exporting 3D model: {str(e)}") return { "success": False, "message": "Failed to export 3D model", - "errorDetails": str(e) + "errorDetails": str(e), } def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]: @@ -449,7 +469,7 @@ class ExportCommands: return { "success": False, "message": "No board is loaded", - "errorDetails": "Load or create a board first" + "errorDetails": "Load or create a board first", } output_path = params.get("outputPath") @@ -461,7 +481,7 @@ class ExportCommands: return { "success": False, "message": "Missing output path", - "errorDetails": "outputPath parameter is required" + "errorDetails": "outputPath parameter is required", } # Create output directory if it doesn't exist @@ -475,7 +495,7 @@ class ExportCommands: "reference": module.GetReference(), "value": module.GetValue(), "footprint": module.GetFPID().GetUniStringLibId(), - "layer": self.board.GetLayerName(module.GetLayer()) + "layer": self.board.GetLayerName(module.GetLayer()), } # Add requested attributes @@ -495,7 +515,7 @@ class ExportCommands: "value": comp["value"], "footprint": comp["footprint"], "quantity": 1, - "references": [comp["reference"]] + "references": [comp["reference"]], } else: grouped[key]["quantity"] += 1 @@ -515,7 +535,7 @@ class ExportCommands: return { "success": False, "message": "Unsupported format", - "errorDetails": f"Format {format} is not supported" + "errorDetails": f"Format {format} is not supported", } return { @@ -524,8 +544,8 @@ class ExportCommands: "file": { "path": output_path, "format": format, - "componentCount": len(components) - } + "componentCount": len(components), + }, } except Exception as e: @@ -533,13 +553,14 @@ class ExportCommands: return { "success": False, "message": "Failed to export BOM", - "errorDetails": str(e) + "errorDetails": str(e), } def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None: """Export BOM to CSV format""" import csv - with open(path, 'w', newline='') as f: + + with open(path, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=components[0].keys()) writer.writeheader() writer.writerows(components) @@ -547,6 +568,7 @@ class ExportCommands: def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None: """Export BOM to XML format""" import xml.etree.ElementTree as ET + root = ET.Element("bom") for comp in components: comp_elem = ET.SubElement(root, "component") @@ -554,7 +576,7 @@ class ExportCommands: elem = ET.SubElement(comp_elem, key) elem.text = str(value) tree = ET.ElementTree(root) - tree.write(path, encoding='utf-8', xml_declaration=True) + tree.write(path, encoding="utf-8", xml_declaration=True) def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None: """Export BOM to HTML format""" @@ -571,13 +593,14 @@ class ExportCommands: html.append(f"{value}") html.append("") html.append("") - with open(path, 'w') as f: + with open(path, "w") as f: f.write("\n".join(html)) def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None: """Export BOM to JSON format""" import json - with open(path, 'w') as f: + + with open(path, "w") as f: json.dump({"components": components}, f, indent=2) def _find_kicad_cli(self) -> Optional[str]: diff --git a/python/commands/routing.py b/python/commands/routing.py index 2aa366a..b84f4c1 100644 --- a/python/commands/routing.py +++ b/python/commands/routing.py @@ -145,18 +145,40 @@ class RoutingCommands: net = start_pad.GetNetname() or end_pad.GetNetname() or "" # Delegate to route_trace - result = self.route_trace({ - "start": {"x": start_pos.x / scale, "y": start_pos.y / scale, "unit": "mm"}, - "end": {"x": end_pos.x / scale, "y": end_pos.y / scale, "unit": "mm"}, - "layer": layer, - "width": width, - "net": net, - }) + result = self.route_trace( + { + "start": { + "x": start_pos.x / scale, + "y": start_pos.y / scale, + "unit": "mm", + }, + "end": { + "x": end_pos.x / scale, + "y": end_pos.y / scale, + "unit": "mm", + }, + "layer": layer, + "width": width, + "net": net, + } + ) if result.get("success"): - result["message"] = f"Routed {from_ref}.{from_pad} → {to_ref}.{to_pad} (net: {net or 'none'})" - result["fromPad"] = {"ref": from_ref, "pad": from_pad, "x": start_pos.x / scale, "y": start_pos.y / scale} - result["toPad"] = {"ref": to_ref, "pad": to_pad, "x": end_pos.x / scale, "y": end_pos.y / scale} + result["message"] = ( + f"Routed {from_ref}.{from_pad} → {to_ref}.{to_pad} (net: {net or 'none'})" + ) + result["fromPad"] = { + "ref": from_ref, + "pad": from_pad, + "x": start_pos.x / scale, + "y": start_pos.y / scale, + } + result["toPad"] = { + "ref": to_ref, + "pad": to_pad, + "x": end_pos.x / scale, + "y": end_pos.y / scale, + } return result @@ -886,7 +908,9 @@ class RoutingCommands: else: traces_to_copy.append(track) - filter_method = "net-based" if use_net_filter else "geometric (pads have no nets)" + filter_method = ( + "net-based" if use_net_filter else "geometric (pads have no nets)" + ) logger.info( f"copy_routing_pattern: {len(traces_to_copy)} traces, " f"{len(vias_to_copy)} vias selected via {filter_method}" diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 94b63af..891a6d1 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -592,7 +592,9 @@ class KiCADInterface: self._current_project_path = project_path local_lib = FootprintLibraryManager(project_path=project_path) self.component_commands = ComponentCommands(self.board, local_lib) - logger.info(f"Reloaded FootprintLibraryManager with project_path={project_path}") + logger.info( + f"Reloaded FootprintLibraryManager with project_path={project_path}" + ) return self.component_commands.place_component(params) @@ -665,7 +667,10 @@ class KiCADInterface: sch_file = Path(schematic_path) if not sch_file.exists(): - return {"success": False, "message": f"Schematic not found: {schematic_path}"} + return { + "success": False, + "message": f"Schematic not found: {schematic_path}", + } with open(sch_file, "r", encoding="utf-8") as f: lines = f.read().split("\n") @@ -676,9 +681,13 @@ class KiCADInterface: for i, line in enumerate(lines): if "(lib_symbols" in line and lib_sym_start is None: lib_sym_start = i - depth = sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")") + depth = sum(1 for c in line if c == "(") - sum( + 1 for c in line if c == ")" + ) elif lib_sym_start is not None and lib_sym_end is None: - depth += sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")") + depth += sum(1 for c in line if c == "(") - sum( + 1 for c in line if c == ")" + ) if depth == 0: lib_sym_end = i break @@ -695,15 +704,22 @@ class KiCADInterface: if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", lines[i]): b_start = i - b_depth = sum(1 for c in lines[i] if c == "(") - sum(1 for c in lines[i] if c == ")") + b_depth = sum(1 for c in lines[i] if c == "(") - sum( + 1 for c in lines[i] if c == ")" + ) j = i + 1 while j < len(lines) and b_depth > 0: - b_depth += sum(1 for c in lines[j] if c == "(") - sum(1 for c in lines[j] if c == ")") + b_depth += sum(1 for c in lines[j] if c == "(") - sum( + 1 for c in lines[j] if c == ")" + ) j += 1 b_end = j - 1 - block_text = "\n".join(lines[b_start:b_end + 1]) - if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text): + block_text = "\n".join(lines[b_start : b_end + 1]) + if re.search( + r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', + block_text, + ): blocks_to_delete.append((b_start, b_end)) i = b_end + 1 @@ -719,7 +735,7 @@ class KiCADInterface: # Delete from back to front to preserve line indices for b_start, b_end in sorted(blocks_to_delete, reverse=True): - del lines[b_start:b_end + 1] + del lines[b_start : b_end + 1] if b_start < len(lines) and lines[b_start].strip() == "": del lines[b_start] @@ -727,18 +743,27 @@ class KiCADInterface: f.write("\n".join(lines)) deleted_count = len(blocks_to_delete) - logger.info(f"Deleted {deleted_count} instance(s) of {reference} from {sch_file.name}") - return {"success": True, "reference": reference, "deleted_count": deleted_count, "schematic": str(sch_file)} + logger.info( + f"Deleted {deleted_count} instance(s) of {reference} from {sch_file.name}" + ) + return { + "success": True, + "reference": reference, + "deleted_count": deleted_count, + "schematic": str(sch_file), + } except Exception as e: logger.error(f"Error deleting schematic component: {e}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} def _handle_edit_schematic_component(self, params): """Update properties of a placed symbol in a schematic (footprint, value, reference). - Uses text-based in-place editing – preserves position, UUID and all other fields.""" + Uses text-based in-place editing – preserves position, UUID and all other fields. + """ logger.info("Editing schematic component") try: from pathlib import Path @@ -754,12 +779,24 @@ class KiCADInterface: return {"success": False, "message": "schematicPath is required"} if not reference: return {"success": False, "message": "reference is required"} - if not any([new_footprint is not None, new_value is not None, new_reference is not None]): - return {"success": False, "message": "At least one of footprint, value, or newReference must be provided"} + if not any( + [ + new_footprint is not None, + new_value is not None, + new_reference is not None, + ] + ): + return { + "success": False, + "message": "At least one of footprint, value, or newReference must be provided", + } sch_file = Path(schematic_path) if not sch_file.exists(): - return {"success": False, "message": f"Schematic not found: {schematic_path}"} + return { + "success": False, + "message": f"Schematic not found: {schematic_path}", + } with open(sch_file, "r", encoding="utf-8") as f: content = f.read() @@ -769,9 +806,9 @@ class KiCADInterface: depth = 0 i = start while i < len(s): - if s[i] == '(': + if s[i] == "(": depth += 1 - elif s[i] == ')': + elif s[i] == ")": depth -= 1 if depth == 0: return i @@ -780,7 +817,9 @@ class KiCADInterface: # Skip lib_symbols section lib_sym_pos = content.find("(lib_symbols") - lib_sym_end = find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1 + lib_sym_end = ( + find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1 + ) # Find placed symbol blocks that match the reference # Search for (symbol (lib_id "...") ... (property "Reference" "" ...) ...) @@ -800,36 +839,61 @@ class KiCADInterface: if end < 0: search_start = pos + 1 continue - block_text = content[pos:end + 1] - if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text): + block_text = content[pos : end + 1] + if re.search( + r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', + block_text, + ): block_start, block_end = pos, end break search_start = end + 1 if block_start is None: - return {"success": False, "message": f"Component '{reference}' not found in schematic"} + return { + "success": False, + "message": f"Component '{reference}' not found in schematic", + } # Apply property replacements within the found block - block_text = content[block_start:block_end + 1] + block_text = content[block_start : block_end + 1] if new_footprint is not None: - block_text = re.sub(r'(\(property\s+"Footprint"\s+)"[^"]*"', rf'\1"{new_footprint}"', block_text) + block_text = re.sub( + r'(\(property\s+"Footprint"\s+)"[^"]*"', + rf'\1"{new_footprint}"', + block_text, + ) if new_value is not None: - block_text = re.sub(r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', block_text) + block_text = re.sub( + r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', block_text + ) if new_reference is not None: - block_text = re.sub(r'(\(property\s+"Reference"\s+)"[^"]*"', rf'\1"{new_reference}"', block_text) + block_text = re.sub( + r'(\(property\s+"Reference"\s+)"[^"]*"', + rf'\1"{new_reference}"', + block_text, + ) - content = content[:block_start] + block_text + content[block_end + 1:] + content = content[:block_start] + block_text + content[block_end + 1 :] with open(sch_file, "w", encoding="utf-8") as f: f.write(content) - changes = {k: v for k, v in {"footprint": new_footprint, "value": new_value, "reference": new_reference}.items() if v is not None} + changes = { + k: v + for k, v in { + "footprint": new_footprint, + "value": new_value, + "reference": new_reference, + }.items() + if v is not None + } logger.info(f"Edited schematic component {reference}: {changes}") return {"success": True, "reference": reference, "updated": changes} except Exception as e: logger.error(f"Error editing schematic component: {e}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} @@ -899,7 +963,9 @@ class KiCADInterface: def _handle_create_footprint(self, params): """Create a new .kicad_mod footprint file in a .pretty library.""" - logger.info(f"create_footprint: {params.get('name')} in {params.get('libraryPath')}") + logger.info( + f"create_footprint: {params.get('name')} in {params.get('libraryPath')}" + ) try: creator = FootprintCreator() return creator.create_footprint( @@ -921,7 +987,9 @@ class KiCADInterface: def _handle_edit_footprint_pad(self, params): """Edit an existing pad in a .kicad_mod file.""" - logger.info(f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}") + logger.info( + f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}" + ) try: creator = FootprintCreator() return creator.edit_footprint_pad( @@ -970,7 +1038,9 @@ class KiCADInterface: def _handle_create_symbol(self, params): """Create a new symbol in a .kicad_sym library.""" - logger.info(f"create_symbol: {params.get('name')} in {params.get('libraryPath')}") + logger.info( + f"create_symbol: {params.get('name')} in {params.get('libraryPath')}" + ) try: creator = SymbolCreator() return creator.create_symbol( @@ -994,7 +1064,9 @@ class KiCADInterface: def _handle_delete_symbol(self, params): """Delete a symbol from a .kicad_sym library.""" - logger.info(f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}") + logger.info( + f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}" + ) try: creator = SymbolCreator() return creator.delete_symbol( @@ -2210,7 +2282,10 @@ class KiCADInterface: return manager.enrich_schematic(Path(schematic_path), dry_run=dry_run) except Exception as e: logger.error(f"Error enriching datasheets: {e}", exc_info=True) - return {"success": False, "message": f"Failed to enrich datasheets: {str(e)}"} + return { + "success": False, + "message": f"Failed to enrich datasheets: {str(e)}", + } def _handle_get_datasheet_url(self, params): """Return LCSC datasheet and product URLs for a part number""" @@ -2232,7 +2307,10 @@ class KiCADInterface: } except Exception as e: logger.error(f"Error getting datasheet URL: {e}", exc_info=True) - return {"success": False, "message": f"Failed to get datasheet URL: {str(e)}"} + return { + "success": False, + "message": f"Failed to get datasheet URL: {str(e)}", + } def main():