Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: IPC2581 Support #345

Open
sjgallagher2 opened this issue Nov 22, 2024 · 1 comment
Open

Enhancement: IPC2581 Support #345

sjgallagher2 opened this issue Nov 22, 2024 · 1 comment

Comments

@sjgallagher2
Copy link
Contributor

This may be out of scope for this project, but I think pygerber has enough infrastructure to add support for IPC2581. That standard is supported by an increasing number of board houses and CAD packages, and parsing the files is fairly straightforward.

A CAD packages produces a .cvg file, which is an XML file following the IPC2581 standard. Unfortunately, the standard is not freely available (unless you become a member, which is free). Even still, being a plain text XML file it's easy to understand the structure:

  • IPC-2581 (root)
    • Content - Layer names, color dictionary, standard shapes with units
    • LogisticHeader - Owner and enterprise information, if applicable
    • Bom - Collection of BomItems, each having attributes relating to a component in the BOM
    • Ecad - Layer stackup and materials, layer geometry

Most interesting is the Ecad node. It is divided into CadHeader and CadData. The CadHeader has layers in Spec nodes which have optional MATERIAL properties, e.g. Solder Resist. The CadData node contains layer information (each layer has a name, function, side, and polarity), stackup information under a Stackup node, and actual geometry (both traces and component geometry) under a Step node.

Here's a sort of tree view of the nodes:

# IPC2581
# . Content
# . . FunctionMode
# . . StepRef
# . . LayerRef
# . . BomRef
# . . DictionaryStandard
# . . . EntryStandard
# . . . . RectCenter
# . . . . Circle
# . . DictionaryColor
# . . . EntryColor
# . . . . Color
# . LogisticHeader
# . . Role
# . . Enterprise
# . . Person
# . Bom
# . . BomHeader
# . . . StepRef
# . . BomItem
# . . . RefDes
# . . . Characteristics
# . Ecad
# . . CadHeader
# . . . Spec
# . . . . General
# . . . . . Property
# . . CadData
# . . . Layer
# . . . Stackup
# . . . . StackupGroup
# . . . . . StackupLayer
# . . . . . . SpecRef
# . . . Step
# . . . . Datum
# . . . . Profile
# . . . . . Polygon
# . . . . . . PolyBegin
# . . . . . . PolyStepCurve
# . . . . . . PolyStepSegment
# . . . . . Cutout
# . . . . . . PolyBegin
# . . . . . . PolyStepCurve
# . . . . . . PolyStepSegment
# . . . . PadStack
# . . . . . LayerPad
# . . . . . . Xform
# . . . . . . Location
# . . . . . . StandardPrimitiveRef
# . . . . . . PinRef
# . . . . . LayerHole
# . . . . . . Span
# . . . . Package
# . . . . . Outline
# . . . . . . Polygon
# . . . . . . . PolyBegin
# . . . . . . . PolyStepCurve
# . . . . . . . PolyStepSegment
# . . . . . . LineDesc
# . . . . . Pin
# . . . . . . Location
# . . . . . . StandardPrimitiveRef
# . . . . Component
# . . . . . Xform
# . . . . . Location
# . . . . LayerFeature
# . . . . . Set
# . . . . . . ColorRef
# . . . . . . Hole
# . . . . . . Features
# . . . . . . . Contour
# . . . . . . . . Polygon
# . . . . . . . . . PolyBegin
# . . . . . . . . . PolyStepSegment
# . . . . . . . UserSpecial
# . . . . . . . . Arc
# . . . . . . . . . LineDesc
# . . . . . . . . Line
# . . . . . . . . . LineDesc

This is not comprehensive, it's built up based on a random example file I had.

Examples

Parsing Layers and Nets

def get_layer_list(root: ET.Element):
    """
    Return a list of layers in dictionary format for this document

    Parameters
    ----------
    root : ET.Element

    Returns
    -------
    List of layer dictionaries with elements:
        name            Layer name
        layerFunction   Layer function as a string, one of {'DRILL', 'DOCUMENT', 'PASTEMASK', 'LEGEND', 'SOLDERMASK', 'SIGNAL', 'DIELCORE'}
        side            PCB side, one of {'TOP', 'BOTTOM', 'INTERNAL'}
        polarity        Layer drawing polarity, one of {'POSITIVE','NEGATIVE'}
        thickness       Layer thickness as float
        sequence        Layer sequence position as int
        z               Layer z position as float (bottom is z=0)

    """
    rpf = '{http://webstds.ipc.org/2581}'  # Root prefix
    # Get layer names and functions
    layers = root.findall(f'{rpf}Ecad/{rpf}CadData/{rpf}Layer')
    layers = [l.attrib for l in layers]

    # Get layer thicknesses
    for layer in layers:
        name = layer['name']
        stackuplayer = root.find(
            f'{rpf}Ecad/{rpf}CadData/{rpf}Stackup/{rpf}StackupGroup/{rpf}StackupLayer[@layerOrGroupRef="{name}"]')
        layer['thickness'] = float(stackuplayer.attrib['thickness'])
        layer['sequence'] = int(stackuplayer.attrib['sequence'])

    thicknesses = [layer['thickness'] for layer in layers]
    zpos = np.cumsum(-np.array(thicknesses)) + np.sum(thicknesses)
    for i, layer in enumerate(layers):
        layer['z'] = np.around(zpos[i], decimals=6)

    return layers


def get_layer_net_list(root: ET.Element, layername: str):
    """
    Return list of layer nets (as strings) given root and layer name

    Parameters
    ----------
    root : ET.Element
    layername : str

    Returns
    -------
    None.

    """
    rpf = '{http://webstds.ipc.org/2581}'  # Root prefix
    LayerFeatureRoot = root.find(f'{rpf}Ecad/{rpf}CadData/{rpf}Step/{rpf}LayerFeature[@layerRef="{layername}"]')
    LayerSets = LayerFeatureRoot.findall(f'{rpf}Set')
    netnames = []
    for lset in LayerSets:
        if 'net' in lset.attrib.keys():
            #if lset.attrib['net'] != 'No Net':
            netnames.append(lset.attrib['net'])
    return netnames

Parsing Line and Arc Elements

def parse_Line_element(line_elem: ET.Element, z: float = 0.):
    """
    Given the XML element for a Line, parse into an OCCT Geom_TrimmedCurve

    :param line_elem:
    :param z: z-coordinate for layer
    """
    prec = 10  # number of decimal places to keep, in case of precision issues
    startX = np.around(float(line_elem.attrib['startX']), prec)
    startY = np.around(float(line_elem.attrib['startY']), prec)
    endX = np.around(float(line_elem.attrib['endX']), prec)
    endY = np.around(float(line_elem.attrib['endY']), prec)
    start_pt = (startX, startY, z)
    end_pt = (endX, endY, z)

    LineDesc_elem = line_elem.find(f'{rpf}LineDesc')
    tracewidth = np.around(float(LineDesc_elem.attrib['lineWidth']), 3)
    endstyle = LineDesc_elem.attrib['lineEnd']


def parse_Arc_element(arc_elem: ET.Element, z: float = 0.):
    """
    Given the XML element for an Arc, parse into an OCCT Geom_TrimmedCurve

    :param arc_elem:
    :param z:
    """
    prec = 10
    startX = np.around(float(arc_elem.attrib['startX']), prec)
    startY = np.around(float(arc_elem.attrib['startY']), prec)
    endX = np.around(float(arc_elem.attrib['endX']), prec)
    endY = np.around(float(arc_elem.attrib['endY']), prec)
    centerX = np.around(float(arc_elem.attrib['centerX']), prec)
    centerY = np.around(float(arc_elem.attrib['centerY']), prec)
    is_cw = bool(arc_elem.attrib['clockwise'] == 'true')

    start_pt = (startX, startY, z)
    end_pt = (endX, endY, z)
    center_pt = (centerX, centerY, z)
    radius = distance(center_pt,start_pt)

    LineDesc_elem = arc_elem.find(f'{rpf}LineDesc')
    tracewidth = np.around(float(LineDesc_elem.attrib['lineWidth']), 3)
    endstyle = LineDesc_elem.attrib['lineEnd']

IPC2581 has information on geometry, stackup, BOM, components, vias, and pads. This means generating 3D geometry is possible without any additional information, and everything is in a single file which is convenient. I wrote myself a simple proof-of-concept program that uses Open CASCADE Technology (OCCT) as the geometry kernel, so you can generate .IGES and .STEP files. The only tools available for generating CAD data from Gerbers or IPC2581 .cvg files seem to be paid tools (e.g. Zofsz, NETEX-G) so including basic export functionality for STEP or IGES could be a good addition. But that's just one reason why IPC2581 support would be beneficial.

@Argmaster
Copy link
Owner

Hi, thanks for the suggestion.

In general PyGerber was designed with support for other PCB-realted file formats in mind and hence addiotion of IPC-2851 related modules is welcome. I have reached out to IPC-2581 consortium via contact form, we will see how they respond. I am not so keen on reverse enginieering the format as I am not interested in facing legal action for violation of intelectual property. Additionally I would like the implementation to be high quality rather than limited by what we could have guessed on our own.

Assuming positive response on their side, we could look into implementation of IPC-2851. I must admit that currently I don't have enough processing capacity to tackle it myself, so help would be welcome.

Since implementation will likely require additional dependencies I would prefer to have this feature as dedicated extras set to avoid limiting poratability of PyGerber and keep minimal set of dependencies required to use core functionalities of PyGerber (similarily to how language server and SVG rendering is done). Implementation details are yet to be discussed, but I would like to mention tools like CadQuery and lxml/BeautifulSoup4 which could be helpful in implementing IPC-2851.

I hope that Ucamco wont feel ofended by addition IPC-2851 but if they happen to be, we will probably resort to creating separate library for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants