# src/circuit_synth/core/pin.py
from enum import Enum
from typing import TYPE_CHECKING, Optional, Union
from ._logger import context_logger
from .net import Net
if TYPE_CHECKING:
from .component import Component
[docs]
class PinType(Enum):
"""Enumeration of valid pin types."""
INPUT = "input"
OUTPUT = "output"
BIDIRECTIONAL = "bidirectional"
POWER_IN = "power_in"
POWER_OUT = "power_out"
PASSIVE = "passive"
TRI_STATE = "tri_state" # Added for components like 74HC595
NO_CONNECT = "no_connect" # Added for unconnected pins
UNSPECIFIED = "unspecified" # Added for KiCad unspecified pin types
OPEN_COLLECTOR = "open_collector" # Added for open collector outputs
OPEN_EMITTER = "open_emitter" # Added for open emitter outputs
[docs]
def __eq__(self, other) -> bool:
"""
Override equality comparison to ensure type safety.
Args:
other: The value to compare with
Returns:
bool: True if equal, False if not equal and same type
Raises:
TypeError: If comparing with non-PinType value
"""
if not isinstance(other, PinType):
raise TypeError(f"Cannot compare PinType with {type(other)}")
return super().__eq__(other)
[docs]
def __str__(self) -> str:
"""Return the string value of the pin type."""
return self.value
[docs]
@classmethod
def from_string(cls, value: str) -> "PinType":
"""
Create a PinType from a string value.
Args:
value: String representation of the pin type
Returns:
PinType: The corresponding PinType enum value
Raises:
ValueError: If the string doesn't match a valid pin type
"""
try:
return next(
pin_type for pin_type in cls if pin_type.value.lower() == value.lower()
)
except StopIteration:
raise ValueError(f"Invalid pin type: {value}")
[docs]
def can_connect_to(self, other: "PinType") -> bool:
"""
Check if this pin type can connect to another pin type.
Args:
other: The PinType to check compatibility with
Returns:
bool: True if the pins can be connected, False otherwise
Note:
All pin types can connect to each other except NO_CONNECT pins.
Design Rule Checks (DRC) should be used to flag potentially incorrect connections.
"""
# No-connect pins cannot connect to anything
if self == PinType.NO_CONNECT or other == PinType.NO_CONNECT:
return False
# All other pin types can connect to each other
return True
[docs]
class Pin:
"""
Minimal Pin class for net connectivity.
- No dedicated geometry fields in the constructor
(x, y, length, orientation).
- We do accept **kwargs so geometry can be passed (and ignored by core),
but stored for schematic export.
"""
[docs]
def __init__(
self, name: str, num: str, func: Union[str, PinType], unit: int = 1, **kwargs
):
self.name = name
self.num = num # e.g. "1", "2", ...
# Convert string pin type to enum if needed
self.func = PinType.from_string(func) if isinstance(func, str) else func
self.unit = unit
# The Net currently connected (None if unconnected)
self.net: Optional[Net] = None
# Assigned by the parent Component
self._component: "Component" = None
self._component_pin_id: int = None
# Store geometry or other unexpected fields in a private dict:
self._geometry = {}
# If geometry keys were provided, keep them for schematic generation
for key in ("x", "y", "length", "orientation"):
# default to 0 if not present
self._geometry[key] = kwargs.get(key, 0)
@property
def x(self) -> float:
"""Geometry helper, returns 0 if not set."""
return self._geometry.get("x", 0)
@property
def y(self) -> float:
return self._geometry.get("y", 0)
@property
def length(self) -> float:
return self._geometry.get("length", 0)
@property
def orientation(self) -> float:
return self._geometry.get("orientation", 0)
@property
def connected(self) -> bool:
"""Return True if this pin is connected to a net."""
return self.net is not None
[docs]
def connect_to_net(self, net: Net):
"""
If already on another net, remove from old net. Then join the new net.
Args:
net: The Net to connect this pin to
Raises:
ValueError: If connecting would create an invalid pin type combination
"""
# Check pin type compatibility if there are other pins on the net
if net._pins:
other_pin = next(iter(net._pins))
# Check compatibility in both directions
if not (
self.func.can_connect_to(other_pin.func)
or other_pin.func.can_connect_to(self.func)
):
raise ValueError(
f"Cannot connect {self.func} pin to {other_pin.func} pin"
)
if self.net is not net:
context_logger.debug(
"Pin connecting to net",
component="PIN",
pin_name=self.name,
pin_number=self.num,
old_net=self.net.name if self.net else None,
new_net=net.name,
)
# Remove from old net
if self.net is not None:
self.net._pins.discard(self)
# Attach the new net
self.net = net
net._pins.add(self)
[docs]
def __iadd__(self, other):
"""
Support pin += net, or pin += pin.
Args:
other: Either a Net or another Pin to connect to
Returns:
Pin: Self, for chaining
Raises:
TypeError: If other is not a Net or Pin
ValueError: If pin types are incompatible
"""
from .pin import Pin as PinClass
if isinstance(other, Net):
self.connect_to_net(other)
elif isinstance(other, PinClass):
# Validate pin type compatibility in both directions
if not (
self.func.can_connect_to(other.func)
or other.func.can_connect_to(self.func)
):
raise ValueError(f"Cannot connect {self.func} pin to {other.func} pin")
# Pin-to-pin logic
if self.net is None and other.net is None:
new_net = Net()
self.connect_to_net(new_net)
other.connect_to_net(new_net)
elif self.net is None and other.net is not None:
self.connect_to_net(other.net)
elif self.net is not None and other.net is None:
other.connect_to_net(self.net)
else:
# Both pins have nets; unify them
if self.net != other.net:
# Validate all pins in both nets are compatible
all_pins = list(self.net._pins) + list(other.net._pins)
for p1 in all_pins:
for p2 in all_pins:
if p1 != p2 and not (
p1.func.can_connect_to(p2.func)
or p2.func.can_connect_to(p1.func)
):
raise ValueError(
f"Cannot merge nets: incompatible pin types "
f"{p1.func} and {p2.func}"
)
# All pins are compatible, proceed with merge
for p in list(other.net._pins):
p.connect_to_net(self.net)
else:
raise TypeError(f"Cannot add {type(other)} to Pin.")
return self
def __repr__(self):
comp_ref = self._component.ref if self._component else "?"
net_name = self.net.name if self.net else "None"
return f"Pin({self.name} of {comp_ref}, net={net_name})"
[docs]
def to_dict(self) -> dict:
"""Convert pin to dictionary for JSON serialization."""
return {
"name": self.name,
"num": self.num,
"func": self.func.value, # Convert PinType to string
"unit": self.unit,
"net": self.net.name if self.net else None,
"component": self._component.ref if self._component else None,
"geometry": self._geometry,
}
[docs]
def __json__(self) -> dict:
"""Alternative JSON serialization method."""
return self.to_dict()