kicad-mcp/src/mckicad/patterns/crystal_oscillator.py
Ryan Malloy ce65035a17 Add batch operations, power symbols, pattern templates, and schematic editing
New modules:
- patterns/ library: decoupling bank, pull resistor, crystal oscillator
  placement with power symbol attachment and grid math helpers
- tools/batch.py: atomic file-based batch operations with dry_run
- tools/power_symbols.py: add_power_symbol with auto #PWR refs
- tools/schematic_patterns.py: MCP wrappers for pattern library
- tools/schematic_edit.py: modify/remove components, title blocks, annotations
- resources/schematic.py: schematic data resources

43 new tests (99 total), lint clean.
2026-03-04 16:55:09 -07:00

171 lines
5.3 KiB
Python

"""
Crystal oscillator with load capacitors pattern.
Places a crystal at a specified position with load capacitors on each
side, ground symbols on the caps, and optional wires to IC pins.
"""
import logging
from typing import Any
from mckicad.patterns._geometry import add_power_symbol_to_pin, snap_to_grid
logger = logging.getLogger(__name__)
def place_crystal_with_caps(
sch: Any,
xtal_value: str,
cap_value: str,
x: float,
y: float,
ic_ref: str | None = None,
ic_xin_pin: str | None = None,
ic_xout_pin: str | None = None,
ground_net: str = "GND",
xtal_lib_id: str = "Device:Crystal",
cap_lib_id: str = "Device:C",
) -> dict[str, Any]:
"""Place a crystal oscillator with load capacitors.
Creates the classic crystal + 2x load cap circuit:
- Crystal at center
- Load cap on each side (XIN and XOUT)
- GND on each cap's second pin
- Optional wires to IC oscillator pins
Does NOT call ``sch.save()`` — caller manages persistence.
Args:
sch: A kicad-sch-api SchematicDocument.
xtal_value: Crystal frequency string (e.g. "16MHz", "32.768kHz").
cap_value: Load capacitor value (e.g. "22pF", "15pF").
x: Center X position for the crystal.
y: Center Y position for the crystal.
ic_ref: Optional IC reference for wiring (e.g. "U1").
ic_xin_pin: XIN pin number on the IC.
ic_xout_pin: XOUT pin number on the IC.
ground_net: Ground rail name.
xtal_lib_id: Crystal symbol library ID.
cap_lib_id: Capacitor symbol library ID.
Returns:
Dictionary with crystal and cap references, wire IDs, and
power symbol details.
"""
cap_offset = 10.16 # Horizontal offset for load caps from crystal
# Auto-generate references
max_y_num = 0
max_c_num = 0
for comp in sch.components:
ref = getattr(comp, "reference", "")
if ref.startswith("Y") and ref[1:].isdigit():
max_y_num = max(max_y_num, int(ref[1:]))
elif ref.startswith("C") and ref[1:].isdigit():
max_c_num = max(max_c_num, int(ref[1:]))
xtal_ref = f"Y{max_y_num + 1}"
cap_xin_ref = f"C{max_c_num + 1}"
cap_xout_ref = f"C{max_c_num + 2}"
# Place crystal at center
xtal_x = snap_to_grid(x)
xtal_y = snap_to_grid(y)
sch.components.add(
lib_id=xtal_lib_id,
reference=xtal_ref,
value=xtal_value,
position=(xtal_x, xtal_y),
)
# Place load caps on each side
cap_xin_x = snap_to_grid(x - cap_offset)
cap_xout_x = snap_to_grid(x + cap_offset)
cap_y = snap_to_grid(y + 7.62) # Below the crystal
sch.components.add(
lib_id=cap_lib_id,
reference=cap_xin_ref,
value=cap_value,
position=(cap_xin_x, cap_y),
)
sch.components.add(
lib_id=cap_lib_id,
reference=cap_xout_ref,
value=cap_value,
position=(cap_xout_x, cap_y),
)
# GND on each cap's pin 2
gnd_symbols: list[dict[str, Any]] = []
for cap_ref in (cap_xin_ref, cap_xout_ref):
pin2_pos = sch.get_component_pin_position(cap_ref, "2")
if pin2_pos:
gnd_result = add_power_symbol_to_pin(
sch=sch,
pin_position=(pin2_pos.x, pin2_pos.y),
net=ground_net,
)
gnd_symbols.append(gnd_result)
# Wire crystal pins to cap pins
internal_wires: list[str] = []
# Crystal pin 1 -> XIN cap pin 1
xtal_pin1 = sch.get_component_pin_position(xtal_ref, "1")
cap_xin_pin1 = sch.get_component_pin_position(cap_xin_ref, "1")
if xtal_pin1 and cap_xin_pin1:
wid = sch.add_wire(
start=(xtal_pin1.x, xtal_pin1.y),
end=(cap_xin_pin1.x, cap_xin_pin1.y),
)
internal_wires.append(str(wid))
# Crystal pin 2 -> XOUT cap pin 1
xtal_pin2 = sch.get_component_pin_position(xtal_ref, "2")
cap_xout_pin1 = sch.get_component_pin_position(cap_xout_ref, "1")
if xtal_pin2 and cap_xout_pin1:
wid = sch.add_wire(
start=(xtal_pin2.x, xtal_pin2.y),
end=(cap_xout_pin1.x, cap_xout_pin1.y),
)
internal_wires.append(str(wid))
# Optional wires to IC pins
ic_wires: list[str] = []
if ic_ref and ic_xin_pin:
ic_xin_pos = sch.get_component_pin_position(ic_ref, ic_xin_pin)
if ic_xin_pos and xtal_pin1:
wid = sch.add_wire(
start=(xtal_pin1.x, xtal_pin1.y),
end=(ic_xin_pos.x, ic_xin_pos.y),
)
ic_wires.append(str(wid))
if ic_ref and ic_xout_pin:
ic_xout_pos = sch.get_component_pin_position(ic_ref, ic_xout_pin)
if ic_xout_pos and xtal_pin2:
wid = sch.add_wire(
start=(xtal_pin2.x, xtal_pin2.y),
end=(ic_xout_pos.x, ic_xout_pos.y),
)
ic_wires.append(str(wid))
logger.info(
"Placed crystal %s (%s) with caps %s, %s (%s) at (%.1f, %.1f)",
xtal_ref, xtal_value, cap_xin_ref, cap_xout_ref, cap_value, x, y,
)
return {
"crystal_ref": xtal_ref,
"xtal_value": xtal_value,
"cap_xin_ref": cap_xin_ref,
"cap_xout_ref": cap_xout_ref,
"cap_value": cap_value,
"gnd_symbols": gnd_symbols,
"internal_wires": internal_wires,
"ic_wires": ic_wires,
"ground_net": ground_net,
}