"""
Wire Manager for KiCad Schematics
Handles wire creation using S-expression manipulation, similar to dynamic symbol loading.
kicad-skip's wire API doesn't support creating wires with standard parameters, so we
manipulate the .kicad_sch file directly.
"""
import uuid
import logging
import math
from pathlib import Path
from typing import List, Tuple, Optional, Dict
import sexpdata
from sexpdata import Symbol
logger = logging.getLogger('kicad_interface')
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:
"""
Add a wire to the schematic using S-expression manipulation
Args:
schematic_path: Path to .kicad_sch file
start_point: [x, y] coordinates for wire start
end_point: [x, y] coordinates for wire end
stroke_width: Wire width (default 0 for standard)
stroke_type: Stroke type (default, solid, dashed, etc.)
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Create wire S-expression
# Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...))
wire_sexp = [
Symbol('wire'),
[Symbol('pts'),
[Symbol('xy'), start_point[0], start_point[1]],
[Symbol('xy'), end_point[0], end_point[1]]
],
[Symbol('stroke'),
[Symbol('width'), stroke_width],
[Symbol('type'), Symbol(stroke_type)]
],
[Symbol('uuid'), str(uuid.uuid4())]
]
# 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] == Symbol('sheet_instances'):
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert wire before sheet_instances
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected wire from {start_point} to {end_point}")
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added wire to {schematic_path.name}")
return True
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:
"""
Add a multi-segment wire (polyline) to the schematic
Args:
schematic_path: Path to .kicad_sch file
points: List of [x, y] coordinates for each point in the path
stroke_width: Wire width
stroke_type: Stroke type
Returns:
True if successful, False otherwise
"""
try:
if len(points) < 2:
logger.error("Polyline requires at least 2 points")
return False
# Read schematic
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Create pts list
pts_list = [Symbol('pts')]
for point in points:
pts_list.append([Symbol('xy'), point[0], point[1]])
# Create wire S-expression with multiple points
wire_sexp = [
Symbol('wire'),
pts_list,
[Symbol('stroke'),
[Symbol('width'), stroke_width],
[Symbol('type'), Symbol(stroke_type)]
],
[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] == Symbol('sheet_instances'):
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert wire
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected polyline wire with {len(points)} points")
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added polyline wire to {schematic_path.name}")
return True
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:
"""
Add a net label to the schematic
Args:
schematic_path: Path to .kicad_sch file
text: Label text (net name)
position: [x, y] coordinates for label
label_type: Type of label ('label', 'global_label', 'hierarchical_label')
orientation: Rotation angle (0, 90, 180, 270)
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Create label S-expression
# Format: (label "TEXT" (at x y angle) (effects (font (size 1.27 1.27))))
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('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] == Symbol('sheet_instances'):
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert label
sch_data.insert(sheet_instances_index, label_sexp)
logger.info(f"Injected label '{text}' at {position}")
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added label to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding label: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
"""
Add a junction (connection dot) to the schematic
Args:
schematic_path: Path to .kicad_sch file
position: [x, y] coordinates for junction
diameter: Junction diameter (0 for default)
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# 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())]
]
# 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] == Symbol('sheet_instances'):
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert junction
sch_data.insert(sheet_instances_index, junction_sexp)
logger.info(f"Injected junction at {position}")
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added junction to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding junction: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_no_connect(schematic_path: Path, position: List[float]) -> bool:
"""
Add a no-connect flag to the schematic
Args:
schematic_path: Path to .kicad_sch file
position: [x, y] coordinates for no-connect flag
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# 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())]
]
# 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] == Symbol('sheet_instances'):
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert no_connect
sch_data.insert(sheet_instances_index, no_connect_sexp)
logger.info(f"Injected no-connect at {position}")
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added no-connect to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding no-connect: {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]]:
"""
Create an orthogonal (right-angle) path between two points
Args:
start: [x, y] start coordinates
end: [x, y] end coordinates
prefer_horizontal_first: If True, route horizontally first, else vertically first
Returns:
List of points defining the path: [start, corner, end]
"""
x1, y1 = start
x2, y2 = end
if prefer_horizontal_first:
# Route: start → (x2, y1) → end
corner = [x2, y1]
else:
# Route: start → (x1, y2) → end
corner = [x1, y2]
# If start and end are already aligned, return direct path
if x1 == x2 or y1 == y2:
return [start, end]
return [start, corner, end]
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
print("=" * 80)
print("WIRE MANAGER TEST")
print("=" * 80)
# Create test schematic
test_path = Path('/tmp/test_wire_manager.kicad_sch')
template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch')
shutil.copy(template_path, test_path)
print(f"\n✓ Created test schematic: {test_path}")
# Test 1: Add simple wire
print("\n[1/5] Testing simple wire creation...")
success = WireManager.add_wire(test_path, [50.8, 50.8], [101.6, 50.8])
print(f" {'✓' if success else '✗'} Simple wire: {success}")
# Test 2: Add orthogonal wire
print("\n[2/5] Testing orthogonal wire...")
path = WireManager.create_orthogonal_path([50.8, 60.96], [101.6, 88.9])
print(f" Orthogonal path: {path}")
success = WireManager.add_polyline_wire(test_path, path)
print(f" {'✓' if success else '✗'} Polyline wire: {success}")
# Test 3: Add label
print("\n[3/5] Testing label creation...")
success = WireManager.add_label(test_path, "VCC", [76.2, 50.8])
print(f" {'✓' if success else '✗'} Label: {success}")
# Test 4: Add junction
print("\n[4/5] Testing junction creation...")
success = WireManager.add_junction(test_path, [76.2, 50.8])
print(f" {'✓' if success else '✗'} Junction: {success}")
# Test 5: Add no-connect
print("\n[5/5] Testing no-connect creation...")
success = WireManager.add_no_connect(test_path, [127, 50.8])
print(f" {'✓' if success else '✗'} No-connect: {success}")
# Verify with kicad-skip
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
print(f" ✓ Loaded successfully")
print(f" ✓ Wire count: {wire_count}")
except Exception as e:
print(f" ✗ Failed: {e}")
print("\n" + "=" * 80)
print(f"Test schematic saved: {test_path}")
print("Open in KiCad to verify visual appearance!")
print("=" * 80)