diff --git a/python/commands/pin_locator.py b/python/commands/pin_locator.py index 857c76e..040cbbd 100644 --- a/python/commands/pin_locator.py +++ b/python/commands/pin_locator.py @@ -220,14 +220,20 @@ class PinLocator: symbol_at = target_symbol.at.value symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0 - lib_id = target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None + lib_id = ( + target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None + ) if not lib_id: return None pins = self.get_symbol_pins(schematic_path, lib_id) if pin_number not in pins: matched_num = next( - (num for num, data in pins.items() if data.get("name") == pin_number), + ( + num + for num, data in pins.items() + if data.get("name") == pin_number + ), None, ) if matched_num: @@ -305,11 +311,17 @@ class PinLocator: if pin_number not in pins: # Try matching by pin name (e.g. "VCC1", "SDA", "GND") matched_num = next( - (num for num, data in pins.items() if data.get("name") == pin_number), + ( + num + for num, data in pins.items() + if data.get("name") == pin_number + ), None, ) if matched_num: - logger.debug(f"Resolved pin name '{pin_number}' to pin number '{matched_num}' on {symbol_reference}") + logger.debug( + f"Resolved pin name '{pin_number}' to pin number '{matched_num}' on {symbol_reference}" + ) pin_number = matched_num else: logger.error( @@ -418,21 +430,23 @@ if __name__ == "__main__": # Test pin location discovery import sys - sys.path.insert(0, "/home/chris/MCP/KiCAD-MCP-Server/python") - from pathlib import Path from commands.component_schematic import ComponentManager from commands.schematic import SchematicManager import shutil + sys.path.insert(0, str(Path(__file__).parent.parent)) + print("=" * 80) print("PIN LOCATOR TEST") print("=" * 80) # Create test schematic with components (cross-platform temp directory) test_path = Path(tempfile.gettempdir()) / "test_pin_locator.kicad_sch" - template_path = Path( - "/home/chris/MCP/KiCAD-MCP-Server/python/templates/template_with_symbols_expanded.kicad_sch" + template_path = ( + Path(__file__).parent.parent + / "templates" + / "template_with_symbols_expanded.kicad_sch" ) shutil.copy(template_path, test_path) diff --git a/python/commands/wire_manager.py b/python/commands/wire_manager.py index 5e30cf8..1fba738 100644 --- a/python/commands/wire_manager.py +++ b/python/commands/wire_manager.py @@ -15,25 +15,32 @@ from typing import List, Tuple, Optional import sexpdata from sexpdata import Symbol -logger = logging.getLogger('kicad_interface') +logger = logging.getLogger("kicad_interface") # Module-level Symbol constants — avoids repeated allocation on every call -_SYM_WIRE = Symbol('wire') -_SYM_PTS = Symbol('pts') -_SYM_XY = Symbol('xy') -_SYM_STROKE = Symbol('stroke') -_SYM_WIDTH = Symbol('width') -_SYM_TYPE = Symbol('type') -_SYM_UUID = Symbol('uuid') -_SYM_SHEET_INSTANCES = Symbol('sheet_instances') +_SYM_WIRE = Symbol("wire") +_SYM_PTS = Symbol("pts") +_SYM_XY = Symbol("xy") +_SYM_AT = Symbol("at") +_SYM_LABEL = Symbol("label") +_SYM_STROKE = Symbol("stroke") +_SYM_WIDTH = Symbol("width") +_SYM_TYPE = Symbol("type") +_SYM_UUID = Symbol("uuid") +_SYM_SHEET_INSTANCES = Symbol("sheet_instances") class WireManager: """Manage wires in KiCad schematics using S-expression manipulation""" @staticmethod - def add_wire(schematic_path: Path, start_point: List[float], end_point: List[float], - stroke_width: float = 0, stroke_type: str = 'default') -> bool: + def add_wire( + schematic_path: Path, + start_point: List[float], + end_point: List[float], + stroke_width: float = 0, + stroke_type: str = "default", + ) -> bool: """ Add a wire to the schematic using S-expression manipulation @@ -49,7 +56,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -62,12 +69,18 @@ class WireManager: # Create wire S-expression # Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...)) - wire_sexp = WireManager._make_wire_sexp(start_point, end_point, stroke_width, stroke_type) + wire_sexp = WireManager._make_wire_sexp( + start_point, end_point, stroke_width, stroke_type + ) # Find insertion point (before sheet_instances) sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -80,7 +93,7 @@ class WireManager: logger.info(f"Injected wire from {start_point} to {end_point}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -90,12 +103,17 @@ class WireManager: except Exception as e: logger.error(f"Error adding wire: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def add_polyline_wire(schematic_path: Path, points: List[List[float]], - stroke_width: float = 0, stroke_type: str = 'default') -> bool: + def add_polyline_wire( + schematic_path: Path, + points: List[List[float]], + stroke_width: float = 0, + stroke_type: str = "default", + ) -> bool: """ Add a multi-segment wire (polyline) to the schematic @@ -114,7 +132,7 @@ class WireManager: return False # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -128,14 +146,20 @@ class WireManager: # KiCAD wire elements only support exactly 2 pts each. # Split N waypoints into N-1 individual wire segments. wire_sexps = [ - WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type) + WireManager._make_wire_sexp( + points[i], points[i + 1], stroke_width, stroke_type + ) for i in range(len(points) - 1) ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -146,10 +170,12 @@ class WireManager: # Insert all segments (in reverse so order is preserved after inserts) for wire_sexp in reversed(wire_sexps): sch_data.insert(sheet_instances_index, wire_sexp) - logger.info(f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline") + logger.info( + f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline" + ) # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -159,12 +185,18 @@ class WireManager: except Exception as e: logger.error(f"Error adding polyline wire: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def add_label(schematic_path: Path, text: str, position: List[float], - label_type: str = 'label', orientation: int = 0) -> bool: + def add_label( + schematic_path: Path, + text: str, + position: List[float], + label_type: str = "label", + orientation: int = 0, + ) -> bool: """ Add a net label to the schematic @@ -180,7 +212,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -190,19 +222,24 @@ class WireManager: label_sexp = [ Symbol(label_type), text, - [Symbol('at'), position[0], position[1], orientation], - [Symbol('fields_autoplaced'), Symbol('yes')], - [Symbol('effects'), - [Symbol('font'), [Symbol('size'), 1.27, 1.27]], - [Symbol('justify'), Symbol('left'), Symbol('bottom')] + [Symbol("at"), position[0], position[1], orientation], + [Symbol("fields_autoplaced"), Symbol("yes")], + [ + Symbol("effects"), + [Symbol("font"), [Symbol("size"), 1.27, 1.27]], + [Symbol("justify"), Symbol("left"), Symbol("bottom")], ], - [Symbol('uuid'), str(uuid.uuid4())] + [Symbol("uuid"), str(uuid.uuid4())], ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -215,7 +252,7 @@ class WireManager: logger.info(f"Injected label '{text}' at {position}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -225,21 +262,27 @@ class WireManager: except Exception as e: logger.error(f"Error adding label: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def _parse_wire(wire_item) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]: + def _parse_wire( + wire_item, + ) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]: """ Parse a wire S-expression item in a single pass. Returns ((x1,y1), (x2,y2), stroke_width, stroke_type), or None if not a valid wire. """ - if not (isinstance(wire_item, list) and len(wire_item) >= 2 - and wire_item[0] == _SYM_WIRE): + if not ( + isinstance(wire_item, list) + and len(wire_item) >= 2 + and wire_item[0] == _SYM_WIRE + ): return None start = end = None stroke_width: float = 0 - stroke_type: str = 'default' + stroke_type: str = "default" for part in wire_item[1:]: if not isinstance(part, list) or not part: continue @@ -265,10 +308,15 @@ class WireManager: return None @staticmethod - def _point_strictly_on_wire(px: float, py: float, - x1: float, y1: float, - x2: float, y2: float, - eps: float = 1e-6) -> bool: + def _point_strictly_on_wire( + px: float, + py: float, + x1: float, + y1: float, + x2: float, + y2: float, + eps: float = 1e-6, + ) -> bool: """ Return True if (px, py) lies strictly between (x1,y1) and (x2,y2) on a horizontal or vertical wire segment (not at either endpoint). @@ -286,17 +334,17 @@ class WireManager: return False @staticmethod - def _make_wire_sexp(start: List[float], end: List[float], - stroke_width: float = 0, stroke_type: str = 'default') -> list: + def _make_wire_sexp( + start: List[float], + end: List[float], + stroke_width: float = 0, + stroke_type: str = "default", + ) -> list: return [ _SYM_WIRE, - [_SYM_PTS, - [_SYM_XY, start[0], start[1]], - [_SYM_XY, end[0], end[1]]], - [_SYM_STROKE, - [_SYM_WIDTH, stroke_width], - [_SYM_TYPE, Symbol(stroke_type)]], - [_SYM_UUID, str(uuid.uuid4())] + [_SYM_PTS, [_SYM_XY, start[0], start[1]], [_SYM_XY, end[0], end[1]]], + [_SYM_STROKE, [_SYM_WIDTH, stroke_width], [_SYM_TYPE, Symbol(stroke_type)]], + [_SYM_UUID, str(uuid.uuid4())], ] @staticmethod @@ -316,9 +364,13 @@ class WireManager: if parsed is not None: (x1, y1), (x2, y2), stroke_width, stroke_type = parsed if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2): - seg_a = WireManager._make_wire_sexp([x1, y1], [px, py], stroke_width, stroke_type) - seg_b = WireManager._make_wire_sexp([px, py], [x2, y2], stroke_width, stroke_type) - sch_data[i:i + 1] = [seg_a, seg_b] + seg_a = WireManager._make_wire_sexp( + [x1, y1], [px, py], stroke_width, stroke_type + ) + seg_b = WireManager._make_wire_sexp( + [px, py], [x2, y2], stroke_width, stroke_type + ) + sch_data[i : i + 1] = [seg_a, seg_b] logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})") splits += 1 i += 2 # skip the two new segments @@ -327,7 +379,9 @@ class WireManager: return splits @staticmethod - def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool: + def add_junction( + schematic_path: Path, position: List[float], diameter: float = 0 + ) -> bool: """ Add a junction (connection dot) to the schematic. @@ -345,7 +399,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -359,17 +413,21 @@ class WireManager: # Create junction S-expression # Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...)) junction_sexp = [ - Symbol('junction'), - [Symbol('at'), position[0], position[1]], - [Symbol('diameter'), diameter], - [Symbol('color'), 0, 0, 0, 0], - [Symbol('uuid'), str(uuid.uuid4())] + Symbol("junction"), + [Symbol("at"), position[0], position[1]], + [Symbol("diameter"), diameter], + [Symbol("color"), 0, 0, 0, 0], + [Symbol("uuid"), str(uuid.uuid4())], ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -382,7 +440,7 @@ class WireManager: logger.info(f"Injected junction at {position}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -392,6 +450,7 @@ class WireManager: except Exception as e: logger.error(f"Error adding junction: {e}") import traceback + logger.error(traceback.format_exc()) return False @@ -409,7 +468,7 @@ class WireManager: """ try: # Read schematic - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -417,15 +476,19 @@ class WireManager: # Create no_connect S-expression # Format: (no_connect (at x y) (uuid ...)) no_connect_sexp = [ - Symbol('no_connect'), - [Symbol('at'), position[0], position[1]], - [Symbol('uuid'), str(uuid.uuid4())] + Symbol("no_connect"), + [Symbol("at"), position[0], position[1]], + [Symbol("uuid"), str(uuid.uuid4())], ] # Find insertion point sheet_instances_index = None for i, item in enumerate(sch_data): - if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: + if ( + isinstance(item, list) + and len(item) > 0 + and item[0] == _SYM_SHEET_INSTANCES + ): sheet_instances_index = i break @@ -438,7 +501,7 @@ class WireManager: logger.info(f"Injected no-connect at {position}") # Write back - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) f.write(output) @@ -448,12 +511,17 @@ class WireManager: except Exception as e: logger.error(f"Error adding no-connect: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def delete_wire(schematic_path: Path, start_point: List[float], end_point: List[float], - tolerance: float = 0.5) -> bool: + def delete_wire( + schematic_path: Path, + start_point: List[float], + end_point: List[float], + tolerance: float = 0.5, + ) -> bool: """ Delete a wire from the schematic matching given start/end coordinates. @@ -467,7 +535,7 @@ class WireManager: True if a wire was found and removed, False otherwise """ try: - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) @@ -476,34 +544,48 @@ class WireManager: ex, ey = end_point for i, item in enumerate(sch_data): - if not (isinstance(item, list) and len(item) > 0 and item[0] == Symbol('wire')): + if not ( + isinstance(item, list) and len(item) > 0 and item[0] == _SYM_WIRE + ): continue # Extract pts from the wire s-expression pts_list = None for part in item[1:]: - if isinstance(part, list) and len(part) > 0 and part[0] == Symbol('pts'): + if isinstance(part, list) and len(part) > 0 and part[0] == _SYM_PTS: pts_list = part break if pts_list is None: continue - xy_points = [p for p in pts_list[1:] if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol('xy')] + xy_points = [ + p + for p in pts_list[1:] + if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY + ] if len(xy_points) < 2: continue x1, y1 = float(xy_points[0][1]), float(xy_points[0][2]) x2, y2 = float(xy_points[-1][1]), float(xy_points[-1][2]) - match_fwd = (abs(x1 - sx) < tolerance and abs(y1 - sy) < tolerance and - abs(x2 - ex) < tolerance and abs(y2 - ey) < tolerance) - match_rev = (abs(x1 - ex) < tolerance and abs(y1 - ey) < tolerance and - abs(x2 - sx) < tolerance and abs(y2 - sy) < tolerance) + match_fwd = ( + abs(x1 - sx) < tolerance + and abs(y1 - sy) < tolerance + and abs(x2 - ex) < tolerance + and abs(y2 - ey) < tolerance + ) + match_rev = ( + abs(x1 - ex) < tolerance + and abs(y1 - ey) < tolerance + and abs(x2 - sx) < tolerance + and abs(y2 - sy) < tolerance + ) if match_fwd or match_rev: del sch_data[i] - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: f.write(sexpdata.dumps(sch_data)) logger.info(f"Deleted wire from {start_point} to {end_point}") return True @@ -514,13 +596,17 @@ class WireManager: except Exception as e: logger.error(f"Error deleting wire: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def delete_label(schematic_path: Path, net_name: str, - position: Optional[List[float]] = None, - tolerance: float = 0.5) -> bool: + def delete_label( + schematic_path: Path, + net_name: str, + position: Optional[List[float]] = None, + tolerance: float = 0.5, + ) -> bool: """ Delete a net label from the schematic by name (and optionally position). @@ -534,14 +620,15 @@ class WireManager: True if a label was found and removed, False otherwise """ try: - with open(schematic_path, 'r', encoding='utf-8') as f: + with open(schematic_path, "r", encoding="utf-8") as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) for i, item in enumerate(sch_data): - if not (isinstance(item, list) and len(item) > 0 - and item[0] == Symbol('label')): + if not ( + isinstance(item, list) and len(item) > 0 and item[0] == _SYM_LABEL + ): continue # Second element is the label text @@ -551,19 +638,24 @@ class WireManager: if position is not None: # Find (at x y ...) sub-expression and check coordinates at_entry = next( - (p for p in item[1:] if isinstance(p, list) - and len(p) >= 3 and p[0] == Symbol('at')), + ( + p + for p in item[1:] + if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_AT + ), None, ) if at_entry is None: continue lx, ly = float(at_entry[1]), float(at_entry[2]) - if not (abs(lx - position[0]) < tolerance - and abs(ly - position[1]) < tolerance): + if not ( + abs(lx - position[0]) < tolerance + and abs(ly - position[1]) < tolerance + ): continue del sch_data[i] - with open(schematic_path, 'w', encoding='utf-8') as f: + with open(schematic_path, "w", encoding="utf-8") as f: f.write(sexpdata.dumps(sch_data)) logger.info(f"Deleted label '{net_name}'") return True @@ -574,12 +666,14 @@ class WireManager: except Exception as e: logger.error(f"Error deleting label: {e}") import traceback + logger.error(traceback.format_exc()) return False @staticmethod - def create_orthogonal_path(start: List[float], end: List[float], - prefer_horizontal_first: bool = True) -> List[List[float]]: + def create_orthogonal_path( + start: List[float], end: List[float], prefer_horizontal_first: bool = True + ) -> List[List[float]]: """ Create an orthogonal (right-angle) path between two points @@ -608,21 +702,22 @@ class WireManager: return [start, corner, end] -if __name__ == '__main__': +if __name__ == "__main__": # Test wire creation import sys - sys.path.insert(0, '/home/chris/MCP/KiCAD-MCP-Server/python') from pathlib import Path import shutil + sys.path.insert(0, str(Path(__file__).parent.parent)) + print("=" * 80) print("WIRE MANAGER TEST") print("=" * 80) # Create test schematic (cross-platform temp directory) - test_path = Path(tempfile.gettempdir()) / 'test_wire_manager.kicad_sch' - template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch') + test_path = Path(tempfile.gettempdir()) / "test_wire_manager.kicad_sch" + template_path = Path(__file__).parent.parent / "templates" / "empty.kicad_sch" shutil.copy(template_path, test_path) print(f"\nāœ“ Created test schematic: {test_path}") @@ -658,8 +753,9 @@ if __name__ == '__main__': print("\n[Verification] Loading with kicad-skip...") try: from skip import Schematic + sch = Schematic(str(test_path)) - wire_count = len(list(sch.wire)) if hasattr(sch, 'wire') else 0 + wire_count = len(list(sch.wire)) if hasattr(sch, "wire") else 0 print(f" āœ“ Loaded successfully") print(f" āœ“ Wire count: {wire_count}") except Exception as e: