Source code for skidl.tools.kicad.kicad

# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) 2016-2021 Dave Vandenbout.

"""
Handler for reading Kicad libraries and generating netlists.
"""

from __future__ import (  # isort:skip
    absolute_import,
    division,
    print_function,
    unicode_literals,
)

import os.path
import re
import time
from builtins import dict, int, range, str, zip
from collections import namedtuple

from future import standard_library

from ...coord import *
from ...logger import active_logger
from ...part import LIBRARY, TEMPLATE
from ...pckg_info import __version__
from ...scriptinfo import get_script_name, scriptinfo
from ...utilities import *

standard_library.install_aliases()


# These aren't used here, but they are used in modules
# that include this module.
tool_name = "kicad"
# lib_suffix = [".kicad_sym", ".lib"]
lib_suffix = [".lib", ".kicad_sym"]


[docs]def load_sch_lib(self, filename=None, lib_search_paths_=None, lib_section=None): """ Load the parts from a KiCad schematic library file. Args: filename: The name of the KiCad schematic library file. """ from ...skidl import lib_suffixes from .. import KICAD # Try to open the file using allowable suffixes for the versions of KiCAD. suffixes = lib_suffixes[KICAD] base, suffix = os.path.splitext(filename) if suffix: # If an explicit file extension was given, use it instead of tool lib default extensions. suffixes = [suffix] for suffix in suffixes: # Allow file open failure so multiple suffixes can be tried without error messages. f, _ = find_and_open_file( filename, lib_search_paths_, suffix, allow_failure=True ) if f: # Break from the loop once a library file is successfully opened. break if not f: raise FileNotFoundError( "Unable to open KiCad Schematic Library File {}".format(filename) ) if suffix == ".kicad_sym": _load_sch_lib_kicad_v6(self, f, filename, lib_search_paths_) else: _load_sch_lib_kicad(self, f, filename, lib_search_paths_)
def _load_sch_lib_kicad(self, f, filename, lib_search_paths_): """ Load the parts from a KiCad schematic library file. Args: filename: The name of the KiCad schematic library file. """ from ...part import Part from .. import KICAD # Check the file header to make sure it's a KiCad library. header = [] header = [f.readline()] if header and "EESchema-LIBRARY" not in header[0]: raise RuntimeError( "The file {} is not a KiCad Schematic Library File.\n".format(filename) ) # Read the definition of each part line-by-line and then create # a Part object that gets stored in the part list. part_defn = [] for line in f: # Skip over comments. if line.startswith("#"): pass # Look for the start of a part definition. elif line.startswith("DEF"): # Initialize the part definition with the first line. # This will also signal that succeeding lines should be added. part_defn = [line] part_name = line.split()[1] # Get the part name. part_aliases = [] # If gathering the part definition has begun, then continue adding lines. elif part_defn: part_defn.append(line) # Get aliases to add to search text. if line.startswith("ALIAS"): part_aliases = line.split()[1:] # If the current line ends this part definition, then create # the Part object and add it to the part list. Be sure to # indicate that the Part object is being added to a library # and not to a schematic netlist. # Also, add null attributes in case a DCM file is not # available for this part. if line.startswith("ENDDEF"): self.add_parts( Part( part_defn=part_defn, tool=KICAD, dest=LIBRARY, filename=filename, name=part_name, aliases=part_aliases, keywords="", datasheet="", description="", search_text="", tool_version="kicad", ) ) # Clear the part definition in preparation for the next one. part_defn = [] # Now add information from any associated DCM file. base_fn = os.path.splitext(filename)[0] # Strip any extension. f, _ = find_and_open_file(base_fn, lib_search_paths_, ".dcm", allow_failure=True) if f: part_desc = {} for line in f: # Skip over comments. if line.startswith("#"): pass # Look for the start of a part description. elif line.startswith("$CMP"): part_desc["name"] = line.split()[-1] # If gathering the part definition has begun, then continue adding lines. elif part_desc: if line.startswith("D"): part_desc["description"] = " ".join(line.split()[1:]) elif line.startswith("K"): part_desc["keywords"] = " ".join(line.split()[1:]) elif line.startswith("F"): part_desc["datasheet"] = " ".join(line.split()[1:]) elif line.startswith("$ENDCMP"): try: part = self.get_part_by_name( re.escape(part_desc["name"]), silent=True, get_name_only=True, ) except Exception as e: pass else: part.description = part_desc.get("description", "") part.keywords = part_desc.get("keywords", "") part.datasheet = part_desc.get("datasheet", "") part_desc = {} else: pass # Create text string to be used when searching for parts. for part in self.parts: search_text_pieces = [part.filename, part.name, part.description, part.keywords] search_text_pieces.extend(part.aliases) # Join the various text pieces by newlines so the ^ and $ special characters # can be used to detect the start and end of a piece of text during RE searches. part.search_text = "\n".join(search_text_pieces) def _split_into_symbols(libstr): """Split a KiCad V6 library and return a list of symbol strings.""" # Split using "(symbol" as delimiter and discard any preamble. libstr = libstr.replace("( ", "(") delimiter = "(symbol " pieces = libstr.split(delimiter)[1:] symbol_name = "_" # Name of current symbol being assembled. symbols = {} # Symbols indexed by their names. # Go through the pieces and assemble each symbol. for piece in pieces: # Get the symbol name immediately following the delimiter. name = piece.split(None, 1)[0] name = name.replace('"', "") # Remove quotes around name. name1 = "_".join(name.split("_")[:-2]) # Remove '_#_#' from subsymbols. if name1 == symbol_name: # if name.startswith(symbol_name): # If the name starts with the same string as the # current symbol, then this is a unit of the symbol. # Therefore, just append the unit to the symbol. symbols[symbol_name] += delimiter + piece else: # Otherwise, this is the start of a new symbol. # Remove the library name preceding the symbol name. symbol_name = name.split(":", 1)[-1] symbols[symbol_name] = delimiter + piece return symbols def _load_sch_lib_kicad_v6(self, f, filename, lib_search_paths_): """ Load the parts from a KiCad schematic library file. Args: filename: The name of the KiCad schematic library file. """ from ...part import Part # Parse the library and return a nested list of library parts. lib_sexp = "".join(f.readlines()) parts = _split_into_symbols(lib_sexp) def extract_quoted_string(part, property_type): """Extract quoted string from a property in a part symbol definition.""" try: # Quoted string follows the property type id. value = part.split(property_type)[1] except IndexError: # Property didn't exist, so return empty string. return "" # Remove quotes and return the string. return re.findall(r'"(.*?)(?<!\\)"', value)[0] # Create Part objects for each part in library. for part_name, part_defn in parts.items(): # Get part properties. keywords = extract_quoted_string(part_defn, "ki_keywords") datasheet = extract_quoted_string(part_defn, "Datasheet") description = extract_quoted_string(part_defn, "ki_description") # Join the various text pieces by newlines so the ^ and $ special characters # can be used to detect the start and end of a piece of text during RE searches. search_text = "\n".join([filename, part_name, description, keywords]) # Create a Part object and add it to the library object. self.add_parts( Part( part_defn=part_defn, tool=tool_name, dest=LIBRARY, filename=filename, name=part_name, aliases=list(), # No aliases in KiCad V6? keywords=keywords, datasheet=datasheet, description=description, search_text=search_text, tool_version="kicad_v6", ) )
[docs]def parse_lib_part(self, get_name_only=False): """ Create a Part using a part definition from a KiCad schematic library. Args: get_name_only: If true, scan the part definition until the name and aliases are found. The rest of the definition will be parsed if the part is actually used. """ if self.tool_version == "kicad_v6": _parse_lib_part_kicad_v6(self, get_name_only) else: _parse_lib_part_kicad(self, get_name_only)
# Named tuples for part DRAW primitives. DrawDef = namedtuple( "DrawDef", "name ref zero name_offset show_nums show_names num_units lock_units power_symbol", ) DrawF0 = namedtuple("DrawF0", "ref x y size orientation visibility halign valign") DrawF1 = namedtuple( "DrawF1", "name x y size orientation visibility halign valign fieldname" ) DrawArc = namedtuple( "DrawArc", "cx cy radius start_angle end_angle unit dmg thickness fill startx starty endx endy", ) DrawCircle = namedtuple("DrawCircle", "cx cy radius unit dmg thickness fill") DrawPoly = namedtuple("DrawPoly", "point_count unit dmg thickness points fill") DrawRect = namedtuple("DrawRect", "x1 y1 x2 y2 unit dmg thickness fill") DrawText = namedtuple( "DrawText", "angle x y size hidden unit dmg text italic bold halign valign" ) DrawPin = namedtuple( "DrawPin", "name num x y length orientation num_size name_size unit dmg electrical_type shape", ) def _parse_lib_part_kicad(self, get_name_only): """ Create a Part using a part definition from a KiCad schematic library. This method was written based on the code from https://github.com/KiCad/kicad-library-utils/tree/master/schlib. It's covered by GPL3. Args: get_name_only: If true, scan the part definition until the name and aliases are found. The rest of the definition will be parsed if the part is actually used. """ from ...pin import Pin _DEF_KEYS = [ "name", "reference", "unused", "text_offset", "draw_pinnumber", "draw_pinname", "unit_count", "units_locked", "option_flag", ] _F0_KEYS = [ "reference", "posx", "posy", "text_size", "text_orient", "visibility", "htext_justify", "vtext_justify", ] _FN_KEYS = [ "name", "posx", "posy", "text_size", "text_orient", "visibility", "htext_justify", "vtext_justify", "fieldname", ] _ARC_KEYS = [ "posx", "posy", "radius", "start_angle", "end_angle", "unit", "convert", "thickness", "fill", "startx", "starty", "endx", "endy", ] _CIRCLE_KEYS = ["posx", "posy", "radius", "unit", "convert", "thickness", "fill"] _POLY_KEYS = ["point_count", "unit", "convert", "thickness", "points", "fill"] _RECT_KEYS = [ "startx", "starty", "endx", "endy", "unit", "convert", "thickness", "fill", ] _TEXT_KEYS = [ "direction", "posx", "posy", "text_size", "text_type", "unit", "convert", "text", "italic", "bold", "hjustify", "vjustify", ] _PIN_KEYS = [ "name", "num", "posx", "posy", "length", "direction", "name_text_size", "num_text_size", "unit", "convert", "electrical_type", "pin_type", ] _DRAW_KEYS = { "arcs": _ARC_KEYS, "circles": _CIRCLE_KEYS, "polylines": _POLY_KEYS, "rectangles": _RECT_KEYS, "texts": _TEXT_KEYS, "pins": _PIN_KEYS, } _DRAW_ELEMS = { "arcs": "A", "circles": "C", "polylines": "P", "rectangles": "S", "texts": "T", "pins": "X", } _KEYS = { "DEF": _DEF_KEYS, "F0": _F0_KEYS, "F": _FN_KEYS, "A": _ARC_KEYS, "C": _CIRCLE_KEYS, "P": _POLY_KEYS, "S": _RECT_KEYS, "T": _TEXT_KEYS, "X": _PIN_KEYS, } def numberize(v): """If possible, convert a string into a number.""" try: return int(v) except ValueError: try: return float(v) except ValueError: pass return v # Unable to convert to number. Return string. # Return if there's nothing to do (i.e., part has already been parsed). if not self.part_defn: return self.aliases = [] # Part aliases. self.fplist = [] # Footprint list. self.draw = [] # Drawing commands for symbol, including pins. building_fplist = False # True when working on footprint list in defn. building_draw = False # True when gathering part drawing from defn. pins = {} # Dict of symbol pins to check for duplicates. # Regular expression for non-quoted and quoted text pieces. unqu = r'[^\s"]+' # Word without spaces or double-quotes. qu = r'(?<!\\)".*?(?<!\\)"' # Quoted string, possibly with escaped quotes. srch = "|".join([unqu + qu, qu, unqu]) srch = re.compile(srch) # Go through the part definition line-by-line. for line in self.part_defn: # Split the line into words. line = line.replace("\n", "") # Extract all the non-quoted and quoted text pieces, accounting for escaped quotes. line = re.findall(srch, line) # Replace line with list of pieces of line. # The first word indicates the type of part definition data that will follow. if line[0] in _KEYS: # Get the keywords for the current part definition data. key_list = _KEYS[line[0]] # Make a list of the values in the part data associated with each key. # Use an empty string for any missing values so every key will be # associated with something. values = line[1:] + ["" for _ in range(len(key_list) - len(line[1:]))] values = [rmv_quotes(v) for v in values] # Remove any quotes from values. # Create a dictionary of part definition keywords and values. if line[0] == "DEF": self.definition = dict(list(zip(_DEF_KEYS, values))) self.name = self.definition["name"] # To handle libraries quickly, just get the name and # aliases and parse the rest of the part definition later. if get_name_only: if self.aliases: # Name found, aliases already found so we're done. return # Name found so scan defn to see if aliases are present. # (The majority of parts don't have aliases.) for ln in self.part_defn: if re.match(r"^\s*ALIAS\s", ln): # Found aliases, so store them. self.aliases = re.findall(srch, ln)[1:] return return # Add DEF field to list of things to draw. values = [numberize(v) for v in values] self.draw.append(DrawDef(*values)) # End the parsing of the part definition. elif line[0] == "ENDDEF": break # Create a dictionary of F0 part field keywords and values. elif line[0] == "F0": field_dict = dict(list(zip(_F0_KEYS, values))) # Add the field name and its value as an attribute to the part. self.fields["F0"] = field_dict["reference"] # Add F0 field to list of things to draw. values = [numberize(v) for v in values] self.draw.append(DrawF0(*values)) # Create a dictionary of the other part field keywords and values. elif line[0][0] == "F": # Make a list of field values with empty strings for missing fields. values = line[1:] + ["" for _ in range(len(_FN_KEYS) - len(line[1:]))] values = [rmv_quotes(v) for v in values] # Remove any quotes from values. field_dict = dict(list(zip(_FN_KEYS, values))) # If no field name at end of line, use the field identifier F1, F2, ... field_dict["fieldname"] = field_dict["fieldname"] or line[0] # Add the field name and its value as an attribute to the part. self.fields[field_dict["fieldname"]] = field_dict["name"] # Add F1 field to list of things to draw. if line[0] == "F1": values = [numberize(v) for v in values] self.draw.append(DrawF1(*values)) # Create a list of part aliases. elif line[0] == "ALIAS": self.aliases = line[1:] # Start the list of part footprints. elif line[0] == "$FPLIST": building_fplist = True # End the list of part footprints. elif line[0] == "$ENDFPLIST": building_fplist = False # Start gathering the drawing primitives for the part symbol. elif line[0] == "DRAW": building_draw = True # End the gathering of drawing primitives. elif line[0] == "ENDDRAW": building_draw = False # Every other line is either a footprint or a drawing primitive. else: # If the footprint list is being built, then add this line to it. if building_fplist: self.fplist.append( line[0].strip().rstrip() ) # Remove begin & end whitespace. # Else if the drawing primitives are being gathered, process the # current line to see what type of primitive is in play. elif building_draw: values = [numberize(v) for v in values] # Gather arcs. if line[0] == "A": self.draw.append(DrawArc(*values)) # Gather circles. elif line[0] == "C": self.draw.append(DrawCircle(*values)) # Gather polygons. elif line[0] == "P": n_points = values[0] points = values[4 : 4 + (2 * n_points)] values = values[0:4] + [points] if len(line) > (5 + len(points)): values += [line[-1]] else: values += [""] self.draw.append(DrawPoly(*values)) # Gather rectangles. elif line[0] == "S": self.draw.append(DrawRect(*values)) # Gather text. elif line[0] == "T": self.draw.append(DrawText(*values)) # Gather the pin symbols. This is what we really want since # this defines the names, numbers and attributes of the # pins associated with the part. elif line[0] == "X": # Get the information for this pin. values[0:2] = line[ 1:3 ] # Restore pin num & name in case they were made into integers. pin = DrawPin(*values) try: # See if the pin number already exists for this part. rpt_pin = pins[pin.num] except KeyError: # No, this pin number is unique (so far), so store it # using the pin number as the dict key. self.draw.append(pin) pins[pin.num] = pin else: # Uh, oh: Repeated pin number! Check to see if the # duplicated pins have the same I/O type and unit num. if ( pin.electrical_type != rpt_pin.electrical_type or pin.unit != rpt_pin.unit ): active_logger.warning( "Non-identical pins with the same number ({}) in symbol drawing {}".format( pin.num, self.name ) ) # Found something unknown in the drawing section. else: msg = "Found something strange in {} symbol drawing: {}.".format( self.name, line ) active_logger.warning(msg) # Found something unknown outside the footprint list or drawing section. else: msg = "Found something strange in {} symbol definition: {}.".format( self.name, line ) active_logger.warning(msg) # Define some shortcuts to part information. self.num_units = int(self.definition["unit_count"]) # # of units within the part. self.name = self.definition["name"] # Part name (e.g., 'LM324'). self.ref_prefix = self.definition["reference"] # Part ref prefix (e.g., 'R'). # Clear the part reference field directly. Don't use the setter function # since it will try to generate and assign a unique part reference if # passed a value of None. self._ref = None # Make a Pin object from the information in the KiCad pin data fields. def kicad_pin_to_pin(kicad_pin): p = Pin() # Create a blank pin. # Replicate the KiCad pin fields as attributes in the Pin object. # Note that this update will not give the pins valid references # to the current part, but we'll fix that soon. p.__dict__.update(kicad_pin._asdict()) pin_type_translation = { "I": Pin.types.INPUT, "O": Pin.types.OUTPUT, "B": Pin.types.BIDIR, "T": Pin.types.TRISTATE, "P": Pin.types.PASSIVE, "U": Pin.types.UNSPEC, "W": Pin.types.PWRIN, "w": Pin.types.PWROUT, "C": Pin.types.OPENCOLL, "E": Pin.types.OPENEMIT, "N": Pin.types.NOCONNECT, } p.func = pin_type_translation[p.electrical_type] return p self.pins = [kicad_pin_to_pin(p) for p in pins.values()] # Make sure all the pins have a valid reference to this part. self.associate_pins() # Create part units if there are more than 1. if self.num_units > 1: for i in range(1, self.num_units + 1): self.make_unit("u" + num_to_chars(i), **{"unit": i}) # Part definition has been parsed, so clear it out. This prevents a # part from being parsed more than once. self.part_defn = None def _parse_lib_part_kicad_v6(self, get_name_only): """ Create a Part using a part definition from a KiCad V6 schematic library. Args: get_name_only: If true, scan the part definition until the name and aliases are found. The rest of the definition will be parsed if the part is actually used. """ # For info on part library format, look at: # https://docs.google.com/document/d/1lyL_8FWZRouMkwqLiIt84rd2Htg4v1vz8_2MzRKHRkc/edit # https://gitlab.com/kicad/code/kicad/-/blob/master/eeschema/sch_plugins/kicad/sch_sexpr_parser.cpp from ...pin import Pin # Return if there's nothing to do (i.e., part has already been parsed). if not self.part_defn: return # If a part def already exists, the name has already been set, so exit. if get_name_only: return self.aliases = [] # Part aliases. self.fplist = [] # Footprint list. self.draw = [] # Drawing commands for symbol, including pins. part_defn = parse_sexp(self.part_defn, allow_underflow=True) for item in part_defn: if to_list(item)[0] == "extends": # Populate this part (child) from another part (parent) it is extended from. # Make a copy of the parent part from the library. parent_part_name = item[1] parent_part = self.lib[parent_part_name].copy(dest=TEMPLATE) # Remove parent attributes that we don't want to overwrite in the child. parent_part_dict = parent_part.__dict__ for key in ( "part_defn", "name", "aliases", "description", "datasheet", "keywords", "search_text", ): try: del parent_part_dict[key] except KeyError: pass # Overwrite child with the parent part. self.__dict__.update(parent_part_dict) # Make sure all the pins have a valid reference to the child. self.associate_pins() # Copy part units so all the pin and part references stay valid. self.copy_units(parent_part) # Perform some operations on the child part. for item in part_defn: cmd = to_list(item)[0] if cmd == "del": self.rmv_pins(item[1]) elif cmd == "swap": self.swap_pins(item[1], item[2]) elif cmd == "renum": self.renumber_pin(item[1], item[2]) elif cmd == "rename": self.rename_pin(item[1], item[2]) elif cmd == "property_del": del self.fields[item[1]] elif cmd == "alternate": pass break # Populate part fields from symbol properties. properties = { item[1]: item[2:] for item in part_defn if to_list(item)[0] == "property" } for name, data in properties.items(): value = data[0] for item in data[1:]: if to_list(item)[0] == "id": self.fields["F" + str(item[1])] = value break self.fields[name] = value self.ref_prefix = self.fields["F0"] # Part ref prefix (e.g., 'R'). # Association between KiCad and SKiDL pin types. pin_io_type_translation = { "input": Pin.types.INPUT, "output": Pin.types.OUTPUT, "bidirectional": Pin.types.BIDIR, "tri_state": Pin.types.TRISTATE, "passive": Pin.types.PASSIVE, "unspecified": Pin.types.UNSPEC, "power_in": Pin.types.PWRIN, "power_out": Pin.types.PWROUT, "open_collector": Pin.types.OPENCOLL, "open_emitter": Pin.types.OPENEMIT, "no_connect": Pin.types.NOCONNECT, } # Find all the units within a symbol. Skip the first item which is the # 'symbol' marking the start of the entire part definition. units = [item for item in part_defn[1:] if to_list(item)[0] == "symbol"] self.num_units = len(units) # Get pins and assign them to each unit as well as the entire part. unit_nums = [] # Stores unit numbers for units with pins. for unit in units: # Extract the part name, unit number, and conversion flag. unit_name_pieces = unit[1].split("_") # unit name follows 'symbol' symbol_name = "_".join(unit_name_pieces[:-2]) assert symbol_name == self.name unit_num = int(unit_name_pieces[-2]) conversion_flag = int(unit_name_pieces[-1]) # Don't add this unit to the part if the conversion flag is 0. if not conversion_flag: continue # Get the pins for this unit. unit_pins = [item for item in unit if to_list(item)[0] == "pin"] # Save unit number if the unit has pins. Use this to create units # after the entire part is created. if unit_pins: unit_nums.append(unit_num) # Process the pins for the current unit. for pin in unit_pins: # Pin electrical type immediately follows the "pin" tag. pin_func = pin_io_type_translation[pin[1]] # Find the pin name and number starting somewhere after the pin function and shape. pin_name = "" pin_number = None for item in pin[3:]: item = to_list(item) if item[0] == "name": pin_name = item[1] elif item[0] == "number": pin_number = item[1] # Add the pins that were found to the total part. Include the unit identifier # in the pin so we can find it later when the part unit is created. self.add_pins( Pin(name=pin_name, num=pin_number, func=pin_func, unit=unit_num) ) # Clear the part reference field directly. Don't use the setter function # since it will try to generate and assign a unique part reference if # passed a value of None. self._ref = None # Make sure all the pins have a valid reference to this part. self.associate_pins() # Create the units now that all the part pins have been added. if len(unit_nums) > 1: for unit_num in unit_nums: unit_label = "u" + num_to_chars(unit_num) self.make_unit(unit_label, unit=unit_num) # Part definition has been parsed, so clear it out. This prevents a # part from being parsed more than once. self.part_defn = None
[docs]def gen_netlist(self): from .. import KICAD scr_dict = scriptinfo() src_file = os.path.join(scr_dict["dir"], scr_dict["source"]) date = time.strftime("%m/%d/%Y %I:%M %p") tool = "SKiDL (" + __version__ + ")" template = ( "(export (version D)\n" + " (design\n" + ' (source "{src_file}")\n' + ' (date "{date}")\n' + ' (tool "{tool}"))\n' ) netlist = template.format(**locals()) netlist += " (components" for p in sorted(self.parts, key=lambda p: str(p.ref)): netlist += "\n" + p.generate_netlist_component(KICAD) netlist += ")\n" netlist += " (nets" sorted_nets = sorted(self.get_nets(), key=lambda n: str(n.name)) for code, n in enumerate(sorted_nets, 1): n.code = code netlist += "\n" + n.generate_netlist_net(KICAD) netlist += ")\n)\n" return netlist
[docs]def gen_netlist_comp(self): ref = add_quotes(self.ref) value = add_quotes(self.value_str) try: footprint = self.footprint except AttributeError: active_logger.error( "No footprint for {part}/{ref}.".format(part=self.name, ref=ref) ) footprint = "No Footprint" footprint = add_quotes(footprint) lib_filename = getattr(getattr(self, "lib", ""), "filename", "NO_LIB") part_name = add_quotes(self.name) # Embed the hierarchy along with a random integer into the sheetpath for each component. # This enables hierarchical selection in pcbnew. hierarchy = add_quotes("/" + self.hierarchical_name.replace(".", "/")) tstamps = hierarchy fields = "" for fld_name, fld_value in self.fields.items(): fld_value = add_quotes(fld_value) if fld_value: fld_name = add_quotes(fld_name) fields += "\n (field (name {fld_name}) {fld_value})".format( **locals() ) if fields: fields = " (fields" + fields fields += ")\n" template = ( " (comp (ref {ref})\n" + " (value {value})\n" + " (footprint {footprint})\n" + "{fields}" + " (libsource (lib {lib_filename}) (part {part_name}))\n" + " (sheetpath (names {hierarchy}) (tstamps {tstamps})))" ) txt = template.format(**locals()) return txt
[docs]def gen_netlist_net(self): code = add_quotes(self.code) name = add_quotes(self.name) txt = " (net (code {code}) (name {name})".format(**locals()) for p in sorted(self.get_pins(), key=str): part_ref = add_quotes(p.part.ref) pin_num = add_quotes(p.num) txt += "\n (node (ref {part_ref}) (pin {pin_num}))".format(**locals()) txt += ")" return txt
[docs]def gen_pcb(self, file_): """Create a KiCad PCB file directly from a Circuit object.""" # Keep the import in here so it doesn't get triggered unless this is used # so it eases some problems with tox testing. # It requires pcbnew module which may not be present or may be for the # wrong Python version (2 vs. 3). try: import kinet2pcb # For creating KiCad PCB directly from Circuit object. except ImportError: active_logger.warning( "kinet2pcb module is missing. Can't generate a KiCad PCB without it." ) else: file_ = file_ or (get_script_name() + ".kicad_pcb") kinet2pcb.kinet2pcb(self, file_)
[docs]def gen_xml(self): from .. import KICAD scr_dict = scriptinfo() src_file = os.path.join(scr_dict["dir"], scr_dict["source"]) date = time.strftime("%m/%d/%Y %I:%M %p") tool = "SKiDL (" + __version__ + ")" template = ( '<?xml version="1.0" encoding="UTF-8"?>\n' + '<export version="D">\n' + " <design>\n" + " <source>{src_file}</source>\n" + " <date>{date}</date>\n" + " <tool>{tool}</tool>\n" + " </design>\n" ) netlist = template.format(**locals()) netlist += " <components>" for p in self.parts: netlist += "\n" + p.generate_xml_component(KICAD) netlist += "\n </components>\n" netlist += " <nets>" for code, n in enumerate(self.get_nets()): n.code = code netlist += "\n" + n.generate_xml_net(KICAD) netlist += "\n </nets>\n" netlist += "</export>\n" return netlist
[docs]def gen_xml_comp(self): ref = self.ref value = self.value_str try: footprint = self.footprint except AttributeError: active_logger.error( "No footprint for {part}/{ref}.".format(part=self.name, ref=ref) ) footprint = "No Footprint" lib_filename = getattr(getattr(self, "lib", ""), "filename", "NO_LIB") part_name = add_quotes(self.name) fields = "" for fld_name, fld_value in self.fields.items(): fld_value = add_quotes(fld_value) if fld_value: fld_name = add_quotes(fld_name) fields += "\n (field (name {fld_name}) {fld_value})".format( **locals() ) if fields: fields = " <fields>" + fields fields += "\n </fields>\n" template = ( ' <comp ref="{ref}">\n' + " <value>{value}</value>\n" + " <footprint>{footprint}</footprint>\n" + "{fields}" + ' <libsource lib="{lib_filename}" part="{part_name}"/>\n' + " </comp>" ) txt = template.format(**locals()) return txt
[docs]def gen_xml_net(self): code = self.code name = self.name txt = ' <net code="{code}" name="{name}">'.format(**locals()) for p in self.get_pins(): part_ref = p.part.ref pin_num = p.num txt += '\n <node ref="{part_ref}" pin="{pin_num}"/>'.format(**locals()) txt += "\n </net>" return txt
[docs]def gen_svg_comp(self, symtx, net_stubs=None): """ Generate SVG for this component. Args: self: Part object for which an SVG symbol will be created. net_stubs: List of Net objects whose names will be connected to part symbol pins as connection stubs. symtx: String such as "HR" that indicates symbol mirroring/rotation. Returns: SVG for the part symbol.""" def tx(obj, ops): """Transform Point, number, or direction according to the list of opcodes.""" def H(obj): # Flip horizontally. if isinstance(obj, Point): return Point(-obj.x, obj.y) if isinstance(obj, (float, int)): return 180.0 - obj else: return {"U": "U", "D": "D", "L": "R", "R": "L"}[obj] def V(obj): # Flip vertically. if isinstance(obj, Point): return Point(obj.x, -obj.y) if isinstance(obj, (float, int)): return -obj else: return {"U": "D", "D": "U", "L": "L", "R": "R"}[obj] def R(obj): # Rotate right. if isinstance(obj, Point): return Point(-obj.y, obj.x) if isinstance(obj, (float, int)): return obj + 90.0 else: return {"U": "R", "D": "L", "L": "U", "R": "D"}[obj] def L(obj): # Rotate left. if isinstance(obj, Point): return Point(obj.y, -obj.x) if isinstance(obj, (float, int)): return obj - 90.0 else: return {"U": "L", "D": "R", "L": "D", "R": "U"}[obj] # Each character in ops applies a geometrical transformation. for op in ops: obj = locals()[op.upper()](obj) # op selects the H, V, L, or R subroutine. return obj def draw_text(text, size, justify, origin, rotation, offset, class_, extra=""): return " ".join( [ "<text", "class='{class_}'", "text-anchor='{justify}'", "x='{origin.x}' y='{origin.y}'", "transform='rotate({rotation} {origin.x} {origin.y}) translate({offset.x} {offset.y})'", "style='font-size:{size}px'", "{extra}", ">", "{text}", "</text>", ] ).format(**locals()) def make_pin_dir_tbl(abs_xoff=20): # abs_xoff is the absolute distance of name/num from the end of the pin. rel_yoff_num = -0.15 # Relative distance of number above pin line. rel_yoff_name = ( 0.2 # Relative distance that places name midline even with pin line. ) # Tuple for storing information about pins in each of four directions: # direction: The direction the pin line is drawn from start to end. # side: The side of the symbol the pin is on. (Opposite of the direction.) # angle: The angle of the name/number text for the pin (usually 0, -90.). # num_justify: Text justification of the pin number. # name_justify: Text justification of the pin name. # num_offset: (x,y) offset of the pin number w.r.t. the end of the pin. # name_offset: (x,y) offset of the pin name w.r.t. the end of the pin. PinDir = namedtuple( "PinDir", "direction side angle num_justify name_justify num_offset name_offset net_offset", ) return { "U": PinDir( Point(0, -1), "bottom", -90, "end", "start", Point(-abs_xoff, rel_yoff_num), Point(abs_xoff, rel_yoff_name), Point(abs_xoff, rel_yoff_num), ), "D": PinDir( Point(0, 1), "top", -90, "start", "end", Point(abs_xoff, rel_yoff_num), Point(-abs_xoff, rel_yoff_name), Point(-abs_xoff, rel_yoff_num), ), "L": PinDir( Point(-1, 0), "right", 0, "start", "end", Point(abs_xoff, rel_yoff_num), Point(-abs_xoff, rel_yoff_name), Point(-abs_xoff, rel_yoff_num), ), "R": PinDir( Point(1, 0), "left", 0, "end", "start", Point(-abs_xoff, rel_yoff_num), Point(abs_xoff, rel_yoff_name), Point(abs_xoff, rel_yoff_num), ), } fill_tbl = {"f": "background_fill", "F": "pen_fill", "N": ""} scale = 0.30 # Scale of KiCad units to SVG units. default_thickness = 1 / scale # Default line thickness = 1. default_pin_name_offset = 20 # Named tuple for storing component pin information. PinInfo = namedtuple("PinInfo", "x y side pid") # Get maximum length of net stub name if any are needed for this part symbol. net_stubs = net_stubs or [] # Empty list of stub nets if argument is None. max_stub_len = 0 # If no net stubs are needed, this stays at zero. for pin in self.get_pins(): for net in pin.get_nets(): # Don't let names for no-connect nets affect maximum stub length. if net in [NC, None]: continue if net in net_stubs: max_stub_len = max(len(net.name), max_stub_len) # Go through each graphic object that makes up the component symbol. for obj in self.draw: obj_pin_info = ( [] ) # Component pin info so they can be generated once bbox is known. obj_svg = [] # Component graphic objects. obj_filled_svg = [] # Filled component graphic objects. obj_txt_svg = [] # Component text (because it has to be drawn last). obj_bbox = BBox() # Bounding box of all the component objects. if isinstance(obj, DrawDef): def_ = obj show_name = def_.name[0] != "~" show_nums = def_.show_nums == "Y" show_names = def_.show_names == "Y" # Make pin direction table with symbol-specific name offset. pin_dir_tbl = make_pin_dir_tbl(def_.name_offset or default_pin_name_offset) # Make structures for holding info on each part unit. num_units = def_.num_units unit_pin_info = [[] for _ in range(num_units + 1)] unit_svg = [[] for _ in range(num_units + 1)] unit_filled_svg = [[] for _ in range(num_units + 1)] unit_txt_svg = [[] for _ in range(num_units + 1)] unit_bbox = [BBox() for _ in range(num_units + 1)] elif isinstance(obj, DrawF0): f0 = obj if f0.visibility != "I": # F0 field is not invisible. origin = tx(Point(f0.x, -f0.y), symtx) * scale orientation = f0.orientation + f0.halign dir = { "HL": "L", "HC": "L", "HR": "R", "VL": "D", "VC": "D", "VR": "U", }[orientation] dir = tx(dir, symtx) angle = pin_dir_tbl[dir].angle size = f0.size * scale justify = "middle" if f0.halign == "C" else pin_dir_tbl[dir].num_justify offset = ( tx( {"T": Point(0, 1), "B": Point(0, 0), "C": Point(0, 0.5)}[ f0.valign[0] ], symtx, ) * size ) class_ = "part_ref_text" extra = 's:attribute="ref"' obj_txt_svg.append( draw_text("X", size, justify, origin, angle, offset, class_, extra) ) elif isinstance(obj, DrawF1): f1 = obj if f1.visibility != "I" and show_name: # F1 field is not invisible. origin = tx(Point(f1.x, -f1.y), symtx) * scale orientation = f1.orientation + f1.halign dir = { "HL": "L", "HC": "L", "HR": "R", "VL": "D", "VC": "D", "VR": "U", }[orientation] dir = tx(dir, symtx) angle = pin_dir_tbl[dir].angle size = f1.size * scale justify = "middle" if f1.halign == "C" else pin_dir_tbl[dir].num_justify offset = ( tx( {"T": Point(0, 1), "B": Point(0, 0), "C": Point(0, 0.5)}[ f1.valign[0] ], symtx, ) * size ) class_ = "part_name_text" extra = 's:attribute="value"' obj_txt_svg.append( draw_text("X", size, justify, origin, angle, offset, class_, extra) ) elif isinstance(obj, DrawArc): arc = obj center = tx(Point(arc.cx, -arc.cy), symtx) * scale radius = arc.radius * scale start = tx(Point(arc.startx, -arc.starty), symtx) * scale end = tx(Point(arc.endx, -arc.endy), symtx) * scale start_angle = tx(arc.start_angle / 10, symtx) end_angle = tx(arc.end_angle / 10, symtx) clock_wise = int(end_angle < start_angle) large_arc = int(abs(end_angle - start_angle) > 180) thickness = (arc.thickness or default_thickness) * scale fill = fill_tbl.get(arc.fill, "") radius_pt = Point(radius, radius) obj_bbox.add(center - radius_pt) obj_bbox.add(center + radius_pt) svg = obj_filled_svg if fill else obj_svg svg.append( " ".join( [ "<path", 'd="M {start.x} {start.y} A {radius} {radius} 0 {large_arc} {clock_wise} {end.x} {end.y}"', 'style="stroke-width:{thickness}"', 'class="$cell_id symbol {fill}"', "/>", ] ).format(**locals()) ) elif isinstance(obj, DrawCircle): circle = obj center = tx(Point(circle.cx, -circle.cy), symtx) * scale radius = circle.radius * scale thickness = (circle.thickness or default_thickness) * scale fill = fill_tbl.get(circle.fill, "") radius_pt = Point(radius, radius) obj_bbox.add(center - radius_pt) obj_bbox.add(center + radius_pt) svg = obj_filled_svg if fill else obj_svg svg.append( " ".join( [ "<circle", 'cx="{center.x}" cy="{center.y}" r="{radius}"', 'style="stroke-width:{thickness}"', 'class="$cell_id symbol {fill}"', "/>", ] ).format(**locals()) ) elif isinstance(obj, DrawPoly): poly = obj pts = [ tx(Point(x, -y), symtx) * scale for x, y in zip(poly.points[0::2], poly.points[1::2]) ] path = [] path_op = "M" for pt in pts: obj_bbox.add(pt) path.append("{path_op} {pt.x} {pt.y}".format(**locals())) path_op = "L" path = " ".join(path) thickness = (poly.thickness or default_thickness) * scale fill = fill_tbl.get(poly.fill, "") svg = obj_filled_svg if fill else obj_svg svg.append( " ".join( [ "<path", 'd="{path}"', 'style="stroke-width:{thickness}"', 'class="$cell_id symbol {fill}"', "/>", ] ).format(**locals()) ) elif isinstance(obj, DrawRect): rect = obj start = tx(Point(rect.x1, -rect.y1), symtx) * scale end = tx(Point(rect.x2, -rect.y2), symtx) * scale obj_bbox.add(start) obj_bbox.add(end) rect_bbox = BBox(start, end) thickness = (rect.thickness or default_thickness) * scale fill = fill_tbl.get(rect.fill, "") svg = obj_filled_svg if fill else obj_svg svg.append( " ".join( [ "<rect", 'x="{rect_bbox.min.x}" y="{rect_bbox.min.y}"', 'width="{rect_bbox.w}" height="{rect_bbox.h}"', 'style="stroke-width:{thickness}"', 'class="$cell_id symbol {fill}"', "/>", ] ).format(**locals()) ) elif isinstance(obj, DrawText): text = obj origin = tx(Point(text.x, -text.y), symtx) * scale angle = tx(text.angle, symtx) size = text.size * scale justify = {"L": "start", "C": "middle", "R": "end"}[text.halign] offset = ( tx( {"T": Point(0, 1), "B": Point(0, 0), "C": Point(0, 0.5)}[ text.valign ], symtx, ) * size ) obj_txt_svg.append( draw_text( text.text, size, justify, origin, angle, offset, class_="part_text" ) ) elif isinstance(obj, DrawPin): pin = obj part_pin = self[ pin.num ] # Get Pin object associated with this pin drawing object. try: visible = pin.shape[0] != "N" except IndexError: visible = True # No pin shape given, so it is visible by default. # Start pin group. orientation = tx(pin.orientation, symtx) dir = pin_dir_tbl[orientation].direction if part_pin.net in [None, NC]: # Unconnected pins remain at the length of the default symbol pin. extension = Point(0, 0) else: # Extend the pin if it's connected to a net. extension = ( dir * ( pin.name_size * 0.5 * max_stub_len + 2 * abs(pin_dir_tbl[orientation].net_offset.x) ) * scale ) start = tx(Point(pin.x, -pin.y), symtx) * scale - extension side = pin_dir_tbl[orientation].side obj_pin_info.append(PinInfo(x=start.x, y=start.y, side=side, pid=pin.num)) if visible: # Draw pin if it's not invisible. # Create line for pin lead. l = dir * pin.length * scale end = start + l + extension thickness = default_thickness * scale obj_bbox.add(start) obj_bbox.add(end) obj_svg.append( " ".join( [ "<path", 'd="M {start.x} {start.y} L {end.x} {end.y}"', 'style="stroke-width:{thickness}"', 'class="$cell_id symbol"' "/>", ] ).format(**locals()) ) # Create pin number. if show_nums: angle = pin_dir_tbl[orientation].angle num_justify = pin_dir_tbl[orientation].num_justify num_size = pin.num_size * scale num_offset = pin_dir_tbl[orientation].num_offset * scale num_offset.y = num_offset.y * pin.num_size # Pin nums are text, but they go into graphical SVG because they are part of a pin object. obj_svg.append( draw_text( str(pin.num), num_size, num_justify, end, angle, num_offset, "pin_num_text", ) ) # Create pin name. if pin.name != "~" and show_names: name_justify = pin_dir_tbl[orientation].name_justify name_size = pin.name_size * scale name_offset = pin_dir_tbl[orientation].name_offset * scale name_offset.y = name_offset.y * pin.name_size # Pin names are text, but they go into graphical SVG because they are part of a pin object. obj_svg.append( draw_text( str(pin.name), name_size, name_justify, end, angle, name_offset, "pin_name_text", ) ) # Create net stub name. if max_stub_len: # Only do this if stub length > 0; otherwise, no stubs are needed. for net in part_pin.get_nets(): # Don't create stubs for no-connect nets. if net in [NC, None]: continue if net in net_stubs: net_justify = pin_dir_tbl[orientation].name_justify net_size = ( pin.name_size * scale ) # Net name font size same as pin name font size. net_offset = pin_dir_tbl[orientation].net_offset * scale net_offset.y = net_offset.y * pin.name_size obj_svg.append( draw_text( net.name, net_size, net_justify, start, angle, net_offset, "net_name_text", ) ) break # Only one label is needed per stub. else: active_logger.error( "Unknown graphical object {} in part symbol {}.".format( type(obj), self.name ) ) # Enter the current object into the SVG for this part. unit = getattr(obj, "unit", 0) if unit == 0: # Anything in unit #0 gets added to all units. for pin_info in unit_pin_info: pin_info.extend(obj_pin_info) for svg in unit_svg: svg.extend(obj_svg) for svg in unit_filled_svg: svg.extend(obj_filled_svg) for txt_svg in unit_txt_svg: txt_svg.extend(obj_txt_svg) for bbox in unit_bbox: bbox.add(obj_bbox) else: unit_pin_info[unit].extend(obj_pin_info) unit_svg[unit].extend(obj_svg) unit_filled_svg[unit].extend(obj_filled_svg) unit_txt_svg[unit].extend(obj_txt_svg) unit_bbox[unit].add(obj_bbox) # End of loop through all the component objects. # Assemble and name the SVGs for all the part units. svg = [] for unit in range(1, num_units + 1): bbox = unit_bbox[unit] # Assign part unit name. if max_stub_len: # If net stubs are attached to symbol, then it's only to be used # for a specific part. Therefore, tag the symbol name with the unique # part reference so it will only be used by this part. symbol_name = "{self.name}_{self.ref}_{unit}_{symtx}".format(**locals()) else: # No net stubs means this symbol can be used for any part that # also has no net stubs, so don't tag it with a specific part reference. symbol_name = "{self.name}_{unit}_{symtx}".format(**locals()) # Begin SVG for part unit. Translate it so the bbox.min is at (0,0). translate = bbox.min * -1 svg.append( " ".join( [ "<g", 's:type="{symbol_name}"', 's:width="{bbox.w}"', 's:height="{bbox.h}"', 'transform="translate({translate.x},{translate.y})"', ">", ] ).format(**locals()) ) # Add part alias. svg.append('<s:alias val="{symbol_name}"/>'.format(**locals())) # Add part unit text and graphics. svg.extend(unit_filled_svg[unit]) # Filled items go on the bottom. svg.extend(unit_svg[unit]) # Then unfilled items. svg.extend(unit_txt_svg[unit]) # Text comes last. # Place a visible bounding-box around symbol for trouble-shooting. show_bbox = False if show_bbox: svg.append( " ".join( [ "<rect", 'x="{bbox.min.x}" y="{bbox.min.y}"', 'width="{bbox.w}" height="{bbox.h}"', 'style="stroke-width:3; stroke:#f00"', 'class="$cell_id symbol"', "/>", ] ).format(**locals()) ) # Keep the pins out of the grouped text & graphics but adjust their coords # to account for moving the bbox. for pin_info in unit_pin_info[unit]: pin_pt = Point(pin_info.x, pin_info.y) side = pin_info.side pid = pin_info.pid pin_svg = '<g s:x="{pin_pt.x}" s:y="{pin_pt.y}" s:pid="{pid}" s:position="{side}"/>'.format( **locals() ) svg.append(pin_svg) # Finish SVG for part unit. svg.append("</g>") return "\n".join(svg)
[docs]def gen_pinboxes(self): """Generate bounding box and I/O pin positions for each unit in a part.""" pass
[docs]def gen_schematic(self, route): pass