# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import copy
import enum
import itertools
__all__ = [
"PinType", "ConnectDirection",
"Net", "Part", "Pin"
]
class Plugin(object):
def __new__(cls, instance):
self = super(Plugin,cls).__new__(cls)
self.instance = instance
return self
@staticmethod
def register(plugin_targets):
if not isinstance(plugin_targets, collections.abc.Iterable):
plugin_targets = (plugin_targets,)
def wrapper(plugin):
for target_cls in plugin_targets:
try:
target_cls.plugins
except AttributeError:
target_cls.plugins = set()
target_cls.plugins.add(plugin)
return plugin
return wrapper
@staticmethod
def init(instance):
"""Init plugins associated with this instance"""
try:
factories = instance.plugins
except AttributeError:
return
assert type(instance.plugins) is not dict
instance.plugins = {plugin: plugin(instance) for plugin in factories}
class ConnectDirection(enum.Enum):
UNKNOWN = 0
IN = 1
OUT = 2
class PinType(enum.Enum):
UNKNOWN = 0
PRIMARY = 1
SECONDARY = 2
POWER_INPUT = 3
POWER_OUTPUT = 4
GROUND = 5
INPUT = 6
OUTPUT = 7
def _maybe_single(o):
if isinstance(o, collections.abc.Iterable):
yield from o
else:
yield o
class _PinList(collections.OrderedDict):
def __getitem__(self, pin_name):
if isinstance(pin_name, int):
return tuple(self.values())[pin_name]
pin_name = pin_name.upper()
try:
return super().__getitem__(pin_name)
except KeyError:
# try looking slowly through the other names
for pin in self.values():
if pin_name.upper() in pin.names:
return pin
else:
raise
def __iter__(self):
yield from self.values()
def __repr__(self):
return repr(tuple(self.values()))
[docs]class Net(object):
_name = None
has_name = False
[docs] def __init__(self, name=None):
if name is not None:
self.name = name.upper()
self._connections = []
Plugin.init(self)
def connect(self, others, direction=ConnectDirection.UNKNOWN, pin_type=PinType.PRIMARY):
try:
connection_group = self.group
except AttributeError:
connection_group = collections.OrderedDict()
self._connections.append(connection_group)
for other in _maybe_single(others):
pin = None
if isinstance(other, Part):
pin = other.get_pin_to_connect(pin_type, self)
if isinstance(other, PartInstancePin):
pin = other
if isinstance(other, Net):
raise NotImplementedError("Can't connect nets together yet.")
if pin is None:
raise TypeError("Don't know how to get %s pin from %r." % (pin_type.name, other))
connection_group[pin] = direction
pin.net = self
self._last_connection_group = connection_group
def _shift(self, direction, others):
self.connect(others, direction, PinType.PRIMARY)
if hasattr(self, "group"):
return self
# Return a copy that acts just like us, but already knows the group
grouped_net = copy.copy(self)
grouped_net.parent = self
grouped_net.group = self._last_connection_group
return grouped_net
[docs] def __lshift__(self, others):
return self._shift(ConnectDirection.IN, others)
[docs] def __rshift__(self, others):
return self._shift(ConnectDirection.OUT, others)
_MAX_REPR_CONNECTIONS = 10
def __repr__(self):
connected = self.connections
if len(connected) >= self._MAX_REPR_CONNECTIONS:
inside_str = "%d connections" % (len(connected))
elif len(connected) == 0:
inside_str = "unconnected"
elif len(connected) == 1:
inside_str = "connected to " + repr(connected[0])
else:
inside_str = "connected to " + repr(connected)[1:-1]
return "%s(%s)" % (self, inside_str)
def __str__(self):
return self.name
@property
def name(self):
if hasattr(self, "parent"):
return self.parent.name
if not self.has_name:
# This path should be rare, only if the user really wants trouble
return "ANON_NET?m%05x" % (id(self) // 32 & 0xfffff)
return self._name
@name.setter
def name(self, new_name):
self._name = new_name.upper()
self.has_name = True
@property
def connections(self):
"""
A :class:`tuple` of pins connected to this net.
Useful in the interpreter and/or when you want to inspect your schematic::
>>> gnd.connections
(U1.GND, VREG1.GND, U2.GND, VREG2.GND)
"""
return sum(self.grouped_connections, ())
@property
def grouped_connections(self):
"""
Similar to :attr:`connections`, but this time pins that were connected together stay in groups::
>>> pp1800.grouped_connections
((U1.GND, VREG1.GND), (U2.GND, VREG2.GND))
"""
return tuple(tuple(group.keys()) for group in self._connections)
def is_net_of_class(self, keywords):
for keyword in keywords:
if keyword in self.name:
return True
@property
def is_power(self):
return self.is_net_of_class(("VCC", "PP", "VBUS"))
@property
def is_gnd(self):
return self.is_net_of_class(("GND",))
[docs]class PinFragment(object):
"""
This is the fully featured (as opposed to just a tuple of parameters)
element of :attr:`PINS<pcbdl.Part.PINS>` at the time of writing
a :class:`Part<pcbdl.Part>`. Saves all parameters it's given,
merges later once the Part is fully defined.
.. warning:: Just like the name implies this is just a fragment of the
information we need for the pin. It's possible the Part needs to be
overlayed on top of its parents before we can have a complete picture.
Ex: this could be the pin labeled "PA2" of a microcontroller, but until
the part is told what package it is, we don't really know the pin
number.
"""
def __init__(self, names_or_numbers=(), names_if_numbers=None, *args, **kwargs):
# Check if short form for the positional arguments
if names_if_numbers is None:
names, numbers = names_or_numbers, ()
else:
names, numbers = names_if_numbers, names_or_numbers
if isinstance(names, str):
names = (names,)
names += kwargs.pop("names", ())
try:
names += (kwargs.pop("name"),)
except KeyError:
pass
names = tuple(name.upper() for name in names)
self.names = names
if isinstance(numbers, (str, int)):
numbers = (numbers,)
numbers += kwargs.pop("numbers", ())
try:
numbers += (kwargs.pop("number"),)
except KeyError:
pass
numbers = tuple(str(maybe_int) for maybe_int in numbers)
self.numbers = numbers
self.args = args
self.kwargs = kwargs
Plugin.init(self)
def __repr__(self):
def arguments():
yield repr(self.names)
if self.numbers:
yield "numbers=" + repr(self.numbers)
for arg in self.args:
yield repr(arg)
for name, value in self.kwargs.items():
yield "%s=%r" % (name, value)
return "PinFragment(%s)" % (", ".join(arguments()))
def __eq__(self, other):
"""If any names match between two fragments, we're talking about the same pin. This is associative, so it chains through other fragments."""
for my_name in self.names:
if my_name in other.names:
return True
return False
@staticmethod
def part_superclasses(part):
for cls in type(part).__mro__:
if cls is Part:
return
yield cls
@staticmethod
def gather_fragments(cls_list):
all_fragments = [pin for cls in cls_list for pin in cls.PINS]
while len(all_fragments) > 0:
same_pin_fragments = []
same_pin_fragments.append(all_fragments.pop(0))
pin_index = 0
while True:
try:
i = all_fragments.index(same_pin_fragments[pin_index])
same_pin_fragments.append(all_fragments.pop(i))
except ValueError:
pin_index += 1 # try following the chain of names, maybe there's another one we need to search by
except IndexError:
break # probably no more fragments for this pin
yield same_pin_fragments
@staticmethod
def resolve(fragments):
# union the names, keep order
name_generator = (n for f in fragments for n in f.names)
seen_names = set()
deduplicated_names = [n for n in name_generator if not (n in seen_names or seen_names.add(n))]
pin_numbers = [number for fragment in fragments for number in fragment.numbers]
# union the args and kwargs, stuff near the front has priority to override
args = []
kwargs = {}
for fragment in reversed(fragments):
args[:len(fragment.args)] = fragment.args
kwargs.update(fragment.kwargs)
return PartClassPin(deduplicated_names, pin_numbers, *args, **kwargs)
[docs] @staticmethod
def second_name_important(pin):
"""
Swap the order of the pin names so the functional (second) name is first.
Used as a :func:`Part._postprocess_pin filter<pcbdl.Part._postprocess_pin>`.
"""
pin.names = pin.names[1:] + (pin.names[0],)
Pin = PinFragment
[docs]class PartClassPin(object):
"""
Pin of a Part, but no particular Part instance.
Contains general information about the pin (but it could be for any
part of that type), nothing related to a specific part instance.
"""
well_name = None
def __init__(self, names, numbers, type=PinType.UNKNOWN, well=None):
self.names = names
self.numbers = numbers
self.type = type
self.well_name = well
Plugin.init(self)
@property
def name(self):
return self.names[0]
@property
def number(self):
return self.numbers[0]
def __str__(self):
return "Pin %s" % (self.name)
__repr__ = __str__
[docs]class PartInstancePin(PartClassPin):
"""Particular pin of a particular part instance. Can connect to nets. Knows the refdes of its part."""
_net = None
def __init__(self, part_instance, part_class_pin, inject_number=None):
# copy state of the Pin to be inherited, then continue as if the parent class always existed that way
self.__dict__.update(part_class_pin.__dict__.copy())
# no need to call PartClassPin.__init__
self._part_class_pin = part_class_pin
# save arguments
self.part = part_instance
if inject_number is not None:
self.numbers = (inject_number,)
assert self.numbers is not None, "this Pin really should have had real pin numbers assigned by now"
well_name = self.well_name
if well_name is not None:
try:
self.well = self.part.pins[well_name]
except KeyError:
raise KeyError("Couldn't find voltage well pin %s on part %r" % (well_name, part_instance))
if self.well.type not in (PinType.POWER_INPUT, PinType.POWER_OUTPUT):
raise ValueError("The chosen well pin %s is not a power pin (but is %s)" % (self.well, self.well.type))
Plugin.init(self)
@property
def net(self):
"""
The :class:`Net<pcbdl.Net>` that this pin is connected to.
If it's not connected to anything yet, we'll get a fresh net.
"""
if self._net is None:
fresh_net = Net() #defined_at: not here
return fresh_net << self
#fresh_net.connect(self, direction=ConnectDirection.UNKNOWN) # This indirectly sets self.netf
return self._net
@net.setter
def net(self, new_net):
if self._net is not None:
# TODO: Maybe just unify the existing net and the new
# net and allow this.
raise ValueError("%s pin is already connected to a net (%s). Can't connect to %s too." %
(self, self._net, new_net))
self._net = new_net
def connect(self, *args, **kwargs):
self.net.connect(*args, **kwargs)
[docs] def __lshift__(self, others):
net = self._net
if net is None:
# don't let the net property create a new one,
# we want to dictate the direction to that Net
net = Net() #defined_at: not here
net >>= self
return net << others
[docs] def __rshift__(self, others):
net = self._net
if net is None:
# don't let the net property create a new one,
# we want to dictate the direction to that Net
net = Net() #defined_at: not here
net <<= self
return net >> others
def __str__(self):
return "%r.%s" % (self.part, self.name)
__repr__ = __str__
class PinFragmentList(list):
"""Used as a marker that we have visited Part.PINS and converted all the elements to PinFragment."""
def __init__(self, part_cls):
self.part_cls = part_cls
list.__init__(self, part_cls.PINS)
for i, maybenames in enumerate(self):
# syntactic sugar, .PIN list might have only names instead of the long form Pin instances
if not isinstance(maybenames, Pin):
self[i] = PinFragment(maybenames)
if part_cls._postprocess_pin.__code__ == Part._postprocess_pin.__code__:
# Let's not waste our time with a noop
return
for i, _ in enumerate(self):
# do user's postprocessing
part_cls._postprocess_pin(self[i])
[docs]class Part(object):
"""
This is the :ref:`base class<python:tut-inheritance>` for any new Part the writer of a schematic or a part librarian has to make. ::
class Transistor(Part):
REFDES_PREFIX = "Q"
PINS = ["B", "C", "E"]
"""
PINS = []
"""
This is how the pins of a part are defined, as a :class:`list` of pins.
Each pin entry can be one of:
* :class:`Pin`
* :class:`tuple` of names which will automatically be turned into a :class:`Pin`
* just one :class:`string<str>`, representing a pin name, if one cares about nothing else.
So these are all valid ways to define a pin (in decreasing order of detail), and mean about the same thing::
PINS = [
Pin("1", ("GND", "GROUND"), type=PinType.POWER_INPUT),
("GND", "GROUND"),
"GND",
]
See the :class:`Pins Section<Pin>` for the types of properties that can be
defined on each Pin entry.
"""
pins = _PinList()
"""
Once the Part is instanced (aka populated on the schematic), our pins become real too (they turn into :class:`PartInstancePins<pcbdl.base.PartInstancePin>`).
This is a :class:`dict` like object where the pins are stored. One can look up pins by any of its names::
somechip.pins["VCC"]
Though most pins are also directly populated as a attributes to the part, so this is equivalent::
somechip.VCC
The pins list can still be used to view all of the pins at once, like on the console:
>>> diode.pins
(D1.VCC, D1.NC, D1.P1, D1.GND, D1.P2)
"""
REFDES_PREFIX = "UNK"
"""
The prefix that every reference designator of this part will have.
Example: :attr:`"R"<pcbdl.small_parts.R.REFDES_PREFIX>` for resistors,
:attr:`"C"<pcbdl.small_parts.C.REFDES_PREFIX>` for capacitors.
The auto namer system will eventually put numbers after the prefix to get the complete :attr:`refdes`.
"""
pin_names_match_nets = False
"""
Sometimes when connecting nets to a part, the pin names become very redundant::
Net("GND") >> somepart.GND
Net("VCC") >> somepart.VCC
Net("RESET") >> somepart.RESET
We can use this variable tells the part to pick the right pin depending on
the variable name, at that point the part itself can be used in lieu of
the pin::
Net("GND") >> somepart
Net("VCC") >> somepart
Net("RESET") >> somepart
"""
pin_names_match_nets_prefix = ""
"""
When :attr:`pin_names_match_nets` is active, it strips a
little bit of the net name in case it's part of a bigger net group::
class SPIFlash(Part):
pin_names_match_nets = True
pin_names_match_nets_prefix = "SPI1"
PINS = ["MOSI", "MISO", "SCK", "CS", ...]
...
Net("SPI1_MOSI") >> spi_flash # autoconnects to the pin called only "MOSI"
Net("SPI1_MISO") << spi_flash # "MISO"
Net("SPI1_SCK") >> spi_flash # "SCK"
Net("SPI1_CS") >> spi_flash # "CS"
"""
def __init__(self, value=None, refdes=None, package=None, part_number=None, populated=True):
if part_number is not None:
self.part_number = part_number
if value is not None:
self.value = value
# if we don't have a value xor a package, use one of them for both
if not hasattr(self, "value") and hasattr(self, "part_number"):
self.value = self.part_number
if not hasattr(self, "part_number") and hasattr(self, "value"):
self.part_number = self.value
# if we don't have either, then there's not much we can do
if not hasattr(self, "value") and not hasattr(self, "part_number"):
self.value = ""
self.part_number = ""
self._refdes = refdes
if package is not None:
self.package = package
self.populated = populated
self._generate_pin_instances()
Plugin.init(self)
def _generate_pin_instances(self):
cls_list = list(PinFragment.part_superclasses(self))
# process the pin lists a little bit
for cls in cls_list:
# but only if we didn't already do it
if isinstance(cls.PINS, PinFragmentList):
continue
cls.PINS = PinFragmentList(cls)
self.__class__.pins = [PinFragment.resolve(f) for f in PinFragment.gather_fragments(cls_list)]
self.pins = _PinList()
for i, part_class_pin in enumerate(self.__class__.pins):
# if we don't have an assigned pin number, generate one
inject_pin_number = str(i + 1) if not part_class_pin.numbers else None
pin = PartInstancePin(self, part_class_pin, inject_pin_number)
self.pins[pin.name] = pin
# save the pin as an attr for this part too
for name in pin.names:
self.__dict__[name] = pin
@property
def _refdes_from_memory_address(self):
return "%s?m%05x" % (self.REFDES_PREFIX, id(self) // 32 & 0xfffff)
@property
def refdes(self):
"""
Reference designator of the part. Example: R1, R2.
It's essentially the unique id for the part that will be used to
refer to it in most output methods.
"""
if self._refdes is not None:
return self._refdes
# make up a refdes based on memory address
return self._refdes_from_memory_address
@refdes.setter
def refdes(self, new_value):
self._refdes = new_value.upper()
def __repr__(self):
return self.refdes
def __str__(self):
return "%s - %s%s" % (self.refdes, self.value, " DNS" if not self.populated else "")
def get_pin_to_connect(self, pin_type, net=None): # pragma: no cover
assert isinstance(pin_type, PinType)
if self.pin_names_match_nets and net is not None:
prefix = self.pin_names_match_nets_prefix
net_name = net.name
for pin in self.pins:
for pin_name in pin.names:
if pin_name == net_name:
return pin
if prefix + pin_name == net_name:
return pin
raise ValueError("Couldn't find a matching named pin on %r to connect the net %s" % (self, net_name))
raise NotImplementedError("Don't know how to get %s pin from %r" % (pin_type.name, self))
[docs] @classmethod
def _postprocess_pin(cls, pin):
"""
It's sometimes useful to process the pins from the source code before the part gets placed down.
This method will be called for each pin by each subclass of a Part.
Good uses for this:
* :func:`Raise the importance of the second name<pcbdl.base.PinFragment.second_name_important>` in a connector, so the more semantic name is the primary name, not the pin number::
PINS = [
("P1", "Nicer name"),
]
_postprocess_pin = Pin.second_name_important
* Populate alternate functions of a pin if they follow an easy pattern.
* A simple programmatic alias on pin names without subclassing the part itself.
"""
raise TypeError("This particular implementation of _postprocess_pin should be skipped by PinFragmentList()")