Source code for skidl.tools.kicad9.gen_netlist

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

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

"""
Generate KiCad 8 netlist.
"""

import os.path
import time
import uuid
from simp_sexp import Sexp

from skidl.design_class import NetClass
from skidl.pckg_info import __version__
from skidl.scriptinfo import scriptinfo, get_script_dir
from skidl.utilities import export_to_all

# This UUID was generated using uuidgen for passing as the namespace argument to uuid.uuid5().
namespace_uuid = uuid.UUID("7026fcc6-e1a0-409e-aaf4-6a17ea82654f")


def gen_sheetpath(hierarchy):
    """
    Generate a sheetpath string from a hierarchical path tuple.

    A sheetpath is a string representation of the hierarchical path
    in a KiCad project. This function converts the given hierarchy
    tuple into a valid sheetpath format by joining the elements of the
    hierarchy tuple with '/' and ensuring it starts and ends with '/'.

    Args:
        hierarchy (tuple): A tuple of strings with the name of each level
            of the hierarchy.

    Returns:
        str: The generated sheetpath string. If the input hierarchy
             is empty or None, the function returns "/".
    """

    assert hierarchy[0] == "", "Top level of hierarchy must be an empty string."
    return f"{'/'.join(hierarchy)}/"


def gen_part_tstamp(part):
    """
    Generate a unique timestamp for a given part based on its hierarchical name.

    This function uses a UUID version 5 (SHA-1 hash) to create a deterministic
    and unique identifier for the part. The UUID is generated using a namespace
    UUID and the hierarchical name of the part.

    Args:
        part: An object representing the part. It is expected to have a
              'hiername' attribute that uniquely identifies the part
              within its hierarchy.

    Returns:
        str: A string representation of the generated UUID.
    """

    part_tstamp = str(uuid.uuid5(namespace_uuid, part.hiername))
    return part_tstamp


def gen_sheetpath_tstamp(hierarchy):
    """
    Generate a timestamp from a hierarchical path tuple.

    This function creates a unique timestamp for a hierarchical path
    in a KiCad project. If the hierarchy is empty, the timestamp
    will be "/". Otherwise, it generates a UUID for each
    entry of the tuple and combines them into a single timestamp.

    Args:
        hierarchy (tuple): A tuple of strings with the name of each level
            of the hierarchy.

    Returns:
        str: A timestamp for the sheetpath. For the root path, it returns "/".
             For other paths, it returns a UUID-based timestamp in the format
             "/<UUID>/<UUID>/.../<UUID>/", where each UUID corresponds to a
             segment of the sheetpath.
    """

    assert hierarchy[0] == "", "Top level of hierarchy must be an empty string."
    if len(hierarchy) == 1:
        tstamp = "/"
    else:
        tstamp = "/".join(
            [str(uuid.uuid5(namespace_uuid, level)) for level in hierarchy[1:]]
        )
        tstamp = "/" + tstamp + "/"
    return tstamp


def gen_netlist_sheet(hierarchy, number, src_file, **kwargs):
    """
    Generate a netlist sheet representation for a KiCad project.

    This function creates a hierarchical representation of a sheet in a KiCad
    netlist, including its path, timestamp, and title block information.

    Args:
        hierarchy (list): A list representing the hierarchical structure of the sheet.
        number (int): The sheet number in the hierarchy.
        src_file (str): The source file associated with the sheet.

    Returns:
        list: A nested list structure representing the netlist sheet, including
              its metadata and title block information.
    """
    sheetpath = gen_sheetpath(hierarchy)
    sheetpath_tstamp = gen_sheetpath_tstamp(hierarchy)

    if kwargs.get("track_abs_path", True):
        # If track_abs_path is True, use the absolute path of the source filename.
        src_file = os.path.abspath(src_file)
    else:
        # If track_abs_path is False, use the path of the source filename relative to the script.
        src_file = os.path.relpath(src_file, get_script_dir())

    sheet = Sexp([
        "sheet",
        ["number", number],
        ["name", sheetpath],
        ["tstamps", sheetpath_tstamp],
        [
            "title_block",
            ["title"],
            ["company"],
            ["rev"],
            ["date"],
            ["source", src_file],
            ["comment", ["number", "1"], ["value", ""]],
            ["comment", ["number", "2"], ["value", ""]],
            ["comment", ["number", "3"], ["value", ""]],
            ["comment", ["number", "4"], ["value", ""]],
            ["comment", ["number", "5"], ["value", ""]],
            ["comment", ["number", "6"], ["value", ""]],
            ["comment", ["number", "7"], ["value", ""]],
            ["comment", ["number", "8"], ["value", ""]],
            ["comment", ["number", "9"], ["value", ""]],
        ],
    ])

    return sheet


def gen_netlist_comp(part, **kwargs):
    """
    Generate a netlist component representation for a given part.

    This function takes a part object and generates a hierarchical representation
    of the part's attributes and metadata in a format suitable for inclusion in
    a KiCad netlist. The generated structure includes information such as the
    part's reference, value, footprint, description, datasheet, and additional
    fields.

    Args:
        part (Part): The part object containing attributes such as name, reference,
                     value, footprint, description, datasheet, and other metadata.

    Returns:
        list: A nested list structure representing the netlist component,
              including fields like reference, value, description, library source,
              sheetpath, timestamps, and custom fields.
    """

    part_name = part.name
    ref = part.ref
    value = part.value_to_str()
    footprint = getattr(part, "footprint", "")
    description = getattr(part, "description", "")
    datasheet = getattr(part, "datasheet", "")
    lib_filename = getattr(getattr(part, "lib", ""), "filename", "NO_LIB")

    # Embed the part hierarchy as a set of UUIDs into the sheetpath for each component.
    # This enables hierarchical selection in pcbnew.
    sheetpath = gen_sheetpath(part.hiertuple)
    sheetpath_tstamp = gen_sheetpath_tstamp(part.hiertuple)
    part_tstamp = gen_part_tstamp(part)

    part_classes = part.partclasses.by_priority()
    component_classes = Sexp(["component_classes"])
    for cls in reversed(part_classes):
        component_classes.append(Sexp(["class", cls.name]))

    fields = Sexp(["fields"])
    part_fields = list(part.fields.items())
    part_fields += list(
        {
            "Description": description,
            "Footprint": footprint,
            "Datasheet": datasheet,
        }.items()
    )
    part_fields.append(["SKiDL Tag", part.tag])
    if kwargs["track_src"]:
        part_fields.append(["SKiDL Line", part.src_line(kwargs["track_abs_path"])])
    for fld_name, fld_value in part_fields:
        if fld_value:
            field = Sexp(["field", ["name", fld_name], fld_value])
        else:
            field = Sexp(["field", ["name", fld_name]])
        fields.append(field)

    comp = Sexp([
        "comp",
        ["ref", ref],
        ["value", value],
        ["description", description],
        ["footprint", footprint],
        # ["datasheet", datasheet],
        fields,
        ["libsource", ["lib", lib_filename], ["part", part_name]],
        ["sheetpath", ["names", sheetpath], ["tstamps", sheetpath_tstamp]],
        component_classes,
        ["tstamps", part_tstamp],
    ])

    # If part has a 'dnp' attribute set to True, add the dnp property
    if getattr(part, "dnp", False):
        comp.append(Sexp(["property", ["name", "dnp"]]))
    
    # If part has an 'exclude_from_bom' attribute set to True, add the exclude_from_bom property
    if getattr(part, "exclude_from_bom", False):
        comp.append(Sexp(["property", ["name", "exclude_from_bom"]]))

    return comp


def gen_netlist_net(net, **kwargs):
    """
    Generate a netlist representation for a given net.

    This function creates a hierarchical list structure representing a net
    in a KiCad netlist. The netlist includes the net's code, name, and the
    associated pins sorted by their string representation.

    Args:
        net (Net): The net object containing information about the net's
                   code, name, and associated pins.

    Returns:
        list: A nested list structure representing the net, including
              its code, name, and associated pins with their part references
              and pin numbers.
    """
    net_classes = net.netclasses.by_priority()
    net_class_str = ",".join(reversed(net_classes))
    nt_lst = Sexp(["net", ["code", net.code], ["name", net.name], ["class", net_class_str]])
    for p in sorted(net.pins, key=str):
        _, _, pin_type = p.get_pin_info()
        nt_lst.append(["node", ["ref", p.part.ref], ["pin", p.num], ["pintype", pin_type]])

    return nt_lst


[docs] @export_to_all def gen_netlist(circuit, **kwargs): """ Generate a netlist for a given circuit. This function creates a netlist representation of the circuit, which includes information about components, nets, and design metadata. The netlist is formatted as a nested list structure and converted to S-expression format using the Sexp class. Args: circuit (Circuit): The circuit object containing parts, nets, and other design information. Returns: str: The netlist in S-expression format. Notes: - The function performs checks for empty footprints and randomly-assigned part tags to ensure the netlist is stable and usable for PCB design. - The netlist includes metadata such as the source file, date, and tool version. - Components and nets are sorted for consistent output. - The Sexp class is used to create a properly formatted S-expression. """ # If track_src, track_abs_path is not specified in kwargs, use values from the circuit attributes. kwargs["track_src"] = kwargs.get("track_src", circuit.track_src) kwargs["track_abs_path"] = kwargs.get("track_abs_path", circuit.track_abs_path) # Check for some things that can cause problems if the netlist is # used to create a PCB. # Check for parts with no physical footprint to place on the PCB. circuit.check_for_empty_footprints() # Check for any missing tags since those will lead to # unstable associations between parts and PCB footprints. circuit.check_tags() # Add a Default netclass to the circuit if none exists. if "Default" not in circuit.netclasses: NetClass("Default", circuit=circuit, priority=0) # Add the Default netclass to all the nets. for net in circuit.get_nets(): net.netclasses = "Default" scr_dict = scriptinfo() src_file = os.path.join(scr_dict["dir"], scr_dict["source"]) if kwargs.get("track_abs_path", True): # If track_abs_path is True, use the absolute path of the source filename. src_file = os.path.abspath(src_file) else: # If track_abs_path is False, use the path of the source filename relative to the script. src_file = os.path.relpath(src_file, get_script_dir()) date = time.strftime("%m/%d/%Y %I:%M %p") tool = f"SKiDL ({__version__})" sheets = Sexp() for num, node_name in enumerate(circuit.get_node_names(), 1): sheets.append(gen_netlist_sheet(node_name, num, src_file, **kwargs)) components = Sexp() for p in sorted(circuit.parts, key=lambda p: str(p.ref)): components.append(gen_netlist_comp(p, **kwargs)) nets = Sexp() sorted_nets = sorted(circuit.get_nets(), key=lambda n: str(n.name)) for code, net in enumerate(sorted_nets, 1): net.code = code nets.append(gen_netlist_net(net, **kwargs)) netlist = Sexp([ "export", ["version", "D"], [ "design", ["source", src_file], ["date", date], ["tool", tool], *sheets, ], ["components", *components], ["nets", *nets], ]) # Add quotes to all strings following the initial keyword in each S-expression of the netlist. netlist.add_quotes(lambda s: True) # For some reason, KiCad's PCBNEW expects a space after the beginning export keyword # or else it rejects the netlist file. return netlist.to_str().replace("(export\n", "(export \n", 1)