add assembly outputs to script
This commit is contained in:
parent
db604e8abb
commit
52c826e373
115
plugins/bom_csv_combined.py
Normal file
115
plugins/bom_csv_combined.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
#
|
||||||
|
# Example python script to generate a BOM from a KiCad generic netlist
|
||||||
|
#
|
||||||
|
# Example: Sorted and Grouped CSV BOM
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
@package
|
||||||
|
Output: CSV (comma-separated)
|
||||||
|
Grouped By: Value, Footprint, DNP, specified extra fields
|
||||||
|
Sorted By: Reference
|
||||||
|
Fields: #, Reference, Qty, Value, Footprint, DNP, specified extra fields
|
||||||
|
|
||||||
|
Outputs components grouped by Value, Footprint, and specified extra fields.
|
||||||
|
Extra fields can be passed as command line arguments at the end, one field per argument.
|
||||||
|
|
||||||
|
Command line:
|
||||||
|
python "pathToFile/bom_csv_grouped_extra.py" "%I" "%O.csv" "Extra_Field1" "Extra_Field2"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Import the KiCad python helper module and the csv formatter
|
||||||
|
import kicad_netlist_reader
|
||||||
|
import kicad_utils
|
||||||
|
|
||||||
|
# Get extra fields from the command line
|
||||||
|
extra_fields = sys.argv[3:]
|
||||||
|
|
||||||
|
|
||||||
|
def myEqu(self, other):
|
||||||
|
"""myEqu is a more advanced equivalence function for components which is
|
||||||
|
used by component grouping. Normal operation is to group components based
|
||||||
|
on their Value and Footprint.
|
||||||
|
|
||||||
|
In this example of a more advanced equivalency operator we also compare the
|
||||||
|
Footprint, Value and all extra fields passed from the command line. If
|
||||||
|
these fields are not used in some parts they will simply be ignored (they
|
||||||
|
will match as both will be empty strings).
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = True
|
||||||
|
if self.getValue() != other.getValue():
|
||||||
|
result = False
|
||||||
|
elif self.getFootprint() != other.getFootprint():
|
||||||
|
result = False
|
||||||
|
elif self.getDNP() != other.getDNP():
|
||||||
|
result = False
|
||||||
|
else:
|
||||||
|
for field_name in extra_fields:
|
||||||
|
if self.getField(field_name) != other.getField(field_name):
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Override the component equivalence operator - it is important to do this
|
||||||
|
# before loading the netlist, otherwise all components will have the original
|
||||||
|
# equivalency operator.
|
||||||
|
kicad_netlist_reader.comp.__eq__ = myEqu
|
||||||
|
|
||||||
|
# Generate an instance of a generic netlist, and load the netlist tree from
|
||||||
|
# the command line option. If the file doesn't exist, execution will stop
|
||||||
|
net = kicad_netlist_reader.netlist(sys.argv[1])
|
||||||
|
|
||||||
|
# Open a file to write to, if the file cannot be opened output to stdout
|
||||||
|
# instead
|
||||||
|
try:
|
||||||
|
f = kicad_utils.open_file_writeUTF8(sys.argv[2], "w")
|
||||||
|
except IOError:
|
||||||
|
e = "Can't open output file for writing: " + sys.argv[2]
|
||||||
|
print(__file__, ":", e, sys.stderr)
|
||||||
|
f = sys.stdout
|
||||||
|
|
||||||
|
# Create a new csv writer object to use as the output formatter
|
||||||
|
out = csv.writer(f, lineterminator="\n", delimiter=",", quotechar='"', quoting=csv.QUOTE_ALL)
|
||||||
|
|
||||||
|
# Output a CSV header
|
||||||
|
out.writerow(["#", "Reference", "Qty", "Value", "Footprint", "DNP"] + extra_fields)
|
||||||
|
|
||||||
|
# Get all of the components in groups of matching parts + values
|
||||||
|
# (see kicad_netlist_reader.py)
|
||||||
|
grouped = net.groupComponents()
|
||||||
|
|
||||||
|
# Output all of the component information
|
||||||
|
index = 1
|
||||||
|
for group in grouped:
|
||||||
|
refs = ""
|
||||||
|
|
||||||
|
# Add the reference of every component in the group and keep a reference
|
||||||
|
# to the component so that the other data can be filled in once per group
|
||||||
|
for component in group:
|
||||||
|
refs += component.getRef() + ", "
|
||||||
|
c = component
|
||||||
|
|
||||||
|
# Remove trailing comma
|
||||||
|
refs = refs[:-2]
|
||||||
|
|
||||||
|
# Fill in the component groups common data
|
||||||
|
row = []
|
||||||
|
row.append(index)
|
||||||
|
row.append(refs)
|
||||||
|
row.append(len(group))
|
||||||
|
row.append(c.getValue())
|
||||||
|
row.append(c.getFootprint())
|
||||||
|
row.append(c.getDNPString())
|
||||||
|
|
||||||
|
# Add the values of extra fields
|
||||||
|
for field_name in extra_fields:
|
||||||
|
row.append(c.getField(field_name))
|
||||||
|
|
||||||
|
out.writerow(row)
|
||||||
|
|
||||||
|
index += 1
|
115
plugins/bom_csv_grouped_extra.py
Normal file
115
plugins/bom_csv_grouped_extra.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
#
|
||||||
|
# Example python script to generate a BOM from a KiCad generic netlist
|
||||||
|
#
|
||||||
|
# Example: Sorted and Grouped CSV BOM
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
@package
|
||||||
|
Output: CSV (comma-separated)
|
||||||
|
Grouped By: Value, Footprint, DNP, specified extra fields
|
||||||
|
Sorted By: Reference
|
||||||
|
Fields: #, Reference, Qty, Value, Footprint, DNP, specified extra fields
|
||||||
|
|
||||||
|
Outputs components grouped by Value, Footprint, and specified extra fields.
|
||||||
|
Extra fields can be passed as command line arguments at the end, one field per argument.
|
||||||
|
|
||||||
|
Command line:
|
||||||
|
python "pathToFile/bom_csv_grouped_extra.py" "%I" "%O.csv" "Extra_Field1" "Extra_Field2"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Import the KiCad python helper module and the csv formatter
|
||||||
|
import kicad_netlist_reader
|
||||||
|
import kicad_utils
|
||||||
|
|
||||||
|
# Get extra fields from the command line
|
||||||
|
extra_fields = sys.argv[3:]
|
||||||
|
|
||||||
|
|
||||||
|
def myEqu(self, other):
|
||||||
|
"""myEqu is a more advanced equivalence function for components which is
|
||||||
|
used by component grouping. Normal operation is to group components based
|
||||||
|
on their Value and Footprint.
|
||||||
|
|
||||||
|
In this example of a more advanced equivalency operator we also compare the
|
||||||
|
Footprint, Value and all extra fields passed from the command line. If
|
||||||
|
these fields are not used in some parts they will simply be ignored (they
|
||||||
|
will match as both will be empty strings).
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = True
|
||||||
|
if self.getValue() != other.getValue():
|
||||||
|
result = False
|
||||||
|
elif self.getFootprint() != other.getFootprint():
|
||||||
|
result = False
|
||||||
|
elif self.getDNP() != other.getDNP():
|
||||||
|
result = False
|
||||||
|
else:
|
||||||
|
for field_name in extra_fields:
|
||||||
|
if self.getField(field_name) != other.getField(field_name):
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Override the component equivalence operator - it is important to do this
|
||||||
|
# before loading the netlist, otherwise all components will have the original
|
||||||
|
# equivalency operator.
|
||||||
|
kicad_netlist_reader.comp.__eq__ = myEqu
|
||||||
|
|
||||||
|
# Generate an instance of a generic netlist, and load the netlist tree from
|
||||||
|
# the command line option. If the file doesn't exist, execution will stop
|
||||||
|
net = kicad_netlist_reader.netlist(sys.argv[1])
|
||||||
|
|
||||||
|
# Open a file to write to, if the file cannot be opened output to stdout
|
||||||
|
# instead
|
||||||
|
try:
|
||||||
|
f = kicad_utils.open_file_writeUTF8(sys.argv[2], "w")
|
||||||
|
except IOError:
|
||||||
|
e = "Can't open output file for writing: " + sys.argv[2]
|
||||||
|
print(__file__, ":", e, sys.stderr)
|
||||||
|
f = sys.stdout
|
||||||
|
|
||||||
|
# Create a new csv writer object to use as the output formatter
|
||||||
|
out = csv.writer(f, lineterminator="\n", delimiter=",", quotechar='"', quoting=csv.QUOTE_ALL)
|
||||||
|
|
||||||
|
# Output a CSV header
|
||||||
|
out.writerow(["#", "Reference", "Qty", "Value", "Footprint", "DNP"] + extra_fields)
|
||||||
|
|
||||||
|
# Get all of the components in groups of matching parts + values
|
||||||
|
# (see kicad_netlist_reader.py)
|
||||||
|
grouped = net.groupComponents()
|
||||||
|
|
||||||
|
# Output all of the component information
|
||||||
|
index = 1
|
||||||
|
for group in grouped:
|
||||||
|
refs = ""
|
||||||
|
|
||||||
|
# Add the reference of every component in the group and keep a reference
|
||||||
|
# to the component so that the other data can be filled in once per group
|
||||||
|
for component in group:
|
||||||
|
refs += component.getRef() + ", "
|
||||||
|
c = component
|
||||||
|
|
||||||
|
# Remove trailing comma
|
||||||
|
refs = refs[:-2]
|
||||||
|
|
||||||
|
# Fill in the component groups common data
|
||||||
|
row = []
|
||||||
|
row.append(index)
|
||||||
|
row.append(refs)
|
||||||
|
row.append(len(group))
|
||||||
|
row.append(c.getValue())
|
||||||
|
row.append(c.getFootprint())
|
||||||
|
row.append(c.getDNPString())
|
||||||
|
|
||||||
|
# Add the values of extra fields
|
||||||
|
for field_name in extra_fields:
|
||||||
|
row.append(c.getField(field_name))
|
||||||
|
|
||||||
|
out.writerow(row)
|
||||||
|
|
||||||
|
index += 1
|
904
plugins/kicad_netlist_reader.py
Normal file
904
plugins/kicad_netlist_reader.py
Normal file
|
@ -0,0 +1,904 @@
|
||||||
|
#
|
||||||
|
# KiCad python module for interpreting generic netlists which can be used
|
||||||
|
# to generate Bills of materials, etc.
|
||||||
|
#
|
||||||
|
# Remember these files use UTF8 encoding
|
||||||
|
#
|
||||||
|
# No string formatting is used on purpose as the only string formatting that
|
||||||
|
# is current compatible with python 2.4+ to 3.0+ is the '%' method, and that
|
||||||
|
# is due to be deprecated in 3.0+ soon
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
@package
|
||||||
|
Helper module for interpreting generic netlist and build custom
|
||||||
|
bom generators or netlists in foreign format
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import pdb
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import xml.sax as sax
|
||||||
|
|
||||||
|
# -----<Configure>----------------------------------------------------------------
|
||||||
|
|
||||||
|
# excluded_fields is a list of regular expressions. If any one matches a field
|
||||||
|
# from either a component or a libpart, then that will not be included as a
|
||||||
|
# column in the BOM. Otherwise all columns from all used libparts and components
|
||||||
|
# will be unionized and will appear. Some fields are impossible to blacklist, such
|
||||||
|
# as Ref, Value, Footprint, and Datasheet. Additionally Qty and Item are supplied
|
||||||
|
# unconditionally as columns, and may not be removed.
|
||||||
|
excluded_fields = [
|
||||||
|
#'Price@1000'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# You may exclude components from the BOM by either:
|
||||||
|
#
|
||||||
|
# 1) adding a custom field named "Installed" to your components and filling it
|
||||||
|
# with a value of "NU" (Normally Uninstalled).
|
||||||
|
# See netlist.getInterestingComponents(), or
|
||||||
|
#
|
||||||
|
# 2) blacklisting it in any of the three following lists:
|
||||||
|
|
||||||
|
|
||||||
|
# regular expressions which match component 'Reference' fields of components that
|
||||||
|
# are to be excluded from the BOM.
|
||||||
|
excluded_references = [
|
||||||
|
# 'TP[0-9]+' # all test points
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# regular expressions which match component 'Value' fields of components that
|
||||||
|
# are to be excluded from the BOM.
|
||||||
|
excluded_values = [
|
||||||
|
# 'MOUNTHOLE',
|
||||||
|
# 'SCOPETEST',
|
||||||
|
# 'MOUNT_HOLE',
|
||||||
|
# 'SOLDER_BRIDGE.*'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# regular expressions which match component 'Footprint' fields of components that
|
||||||
|
# are to be excluded from the BOM.
|
||||||
|
excluded_footprints = [
|
||||||
|
#'MOUNTHOLE'
|
||||||
|
]
|
||||||
|
|
||||||
|
# -----</Configure>---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class xmlElement:
|
||||||
|
"""xml element which can represent all nodes of the netlist tree. It can be
|
||||||
|
used to easily generate various output formats by propagating format
|
||||||
|
requests to children recursively.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, parent=None):
|
||||||
|
self.name = name
|
||||||
|
self.attributes = {}
|
||||||
|
self.parent = parent
|
||||||
|
self.chars = ""
|
||||||
|
self.children = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""String representation of this netlist element"""
|
||||||
|
return self.name + "[" + self.chars + "]" + " attr_count:" + str(len(self.attributes))
|
||||||
|
|
||||||
|
def formatXML(self, nestLevel=0, amChild=False):
|
||||||
|
"""Return this element formatted as XML
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
nestLevel -- increases by one for each level of nesting.
|
||||||
|
amChild -- If set to True, the start of document is not returned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
s = ""
|
||||||
|
|
||||||
|
indent = ""
|
||||||
|
for i in range(nestLevel):
|
||||||
|
indent += " "
|
||||||
|
|
||||||
|
if not amChild:
|
||||||
|
s = '<?xml version="1.0" encoding="utf-8"?>\n'
|
||||||
|
|
||||||
|
s += indent + "<" + self.name
|
||||||
|
for a in self.attributes:
|
||||||
|
s += " " + a + '="' + self.attributes[a] + '"'
|
||||||
|
|
||||||
|
if (len(self.chars) == 0) and (len(self.children) == 0):
|
||||||
|
s += "/>"
|
||||||
|
else:
|
||||||
|
s += ">" + self.chars
|
||||||
|
|
||||||
|
for c in self.children:
|
||||||
|
s += "\n"
|
||||||
|
s += c.formatXML(nestLevel + 1, True)
|
||||||
|
|
||||||
|
if len(self.children) > 0:
|
||||||
|
s += "\n" + indent
|
||||||
|
|
||||||
|
if (len(self.children) > 0) or (len(self.chars) > 0):
|
||||||
|
s += "</" + self.name + ">"
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
def formatHTML(self, amChild=False):
|
||||||
|
"""Return this element formatted as HTML
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
amChild -- If set to True, the start of document is not returned
|
||||||
|
|
||||||
|
"""
|
||||||
|
s = ""
|
||||||
|
|
||||||
|
if not amChild:
|
||||||
|
s = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
s += "<tr><td><b>" + self.name + "</b><br>" + self.chars + "</td><td><ul>"
|
||||||
|
for a in self.attributes:
|
||||||
|
s += "<li>" + a + " = " + self.attributes[a] + "</li>"
|
||||||
|
|
||||||
|
s += "</ul></td></tr>\n"
|
||||||
|
|
||||||
|
for c in self.children:
|
||||||
|
s += c.formatHTML(True)
|
||||||
|
|
||||||
|
if not amChild:
|
||||||
|
s += """</table>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
def addAttribute(self, attr, value):
|
||||||
|
"""Add an attribute to this element"""
|
||||||
|
if type(value) != str:
|
||||||
|
value = value.encode("utf-8")
|
||||||
|
self.attributes[attr] = value
|
||||||
|
|
||||||
|
def setAttribute(self, attr, value):
|
||||||
|
"""Set an attributes value - in fact does the same thing as add
|
||||||
|
attribute
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.attributes[attr] = value
|
||||||
|
|
||||||
|
def setChars(self, chars):
|
||||||
|
"""Set the characters for this element"""
|
||||||
|
self.chars = chars
|
||||||
|
|
||||||
|
def addChars(self, chars):
|
||||||
|
"""Add characters (textual value) to this element"""
|
||||||
|
self.chars += chars
|
||||||
|
|
||||||
|
def addChild(self, child):
|
||||||
|
"""Add a child element to this element"""
|
||||||
|
self.children.append(child)
|
||||||
|
return self.children[len(self.children) - 1]
|
||||||
|
|
||||||
|
def getParent(self):
|
||||||
|
"""Get the parent of this element (Could be None)"""
|
||||||
|
return self.parent
|
||||||
|
|
||||||
|
def getChild(self, name):
|
||||||
|
"""Returns the first child element named 'name'
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
name -- The name of the child element to return"""
|
||||||
|
for child in self.children:
|
||||||
|
if child.name == name:
|
||||||
|
return child
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getChildren(self, name=None):
|
||||||
|
if name:
|
||||||
|
# return _all_ children named "name"
|
||||||
|
ret = []
|
||||||
|
for child in self.children:
|
||||||
|
if child.name == name:
|
||||||
|
ret.append(child)
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
return self.children
|
||||||
|
|
||||||
|
def get(self, elemName, attribute="", attrmatch=""):
|
||||||
|
"""Return the text data for either an attribute or an xmlElement"""
|
||||||
|
if self.name == elemName:
|
||||||
|
if attribute != "":
|
||||||
|
try:
|
||||||
|
if attrmatch != "":
|
||||||
|
if self.attributes[attribute] == attrmatch:
|
||||||
|
ret = self.chars
|
||||||
|
if type(ret) != str:
|
||||||
|
ret = ret.encode("utf-8")
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
ret = self.attributes[attribute]
|
||||||
|
if type(ret) != str:
|
||||||
|
ret = ret.encode("utf-8")
|
||||||
|
return ret
|
||||||
|
except AttributeError:
|
||||||
|
ret = ""
|
||||||
|
if type(ret) != str:
|
||||||
|
ret = ret.encode("utf-8")
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
ret = self.chars
|
||||||
|
if type(ret) != str:
|
||||||
|
ret = ret.encode("utf-8")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
for child in self.children:
|
||||||
|
ret = child.get(elemName, attribute, attrmatch)
|
||||||
|
if ret != "":
|
||||||
|
if type(ret) != str:
|
||||||
|
ret = ret.encode("utf-8")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
ret = ""
|
||||||
|
if type(ret) != str:
|
||||||
|
ret = ret.encode("utf-8")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class libpart:
|
||||||
|
"""Class for a library part, aka 'libpart' in the xml netlist file.
|
||||||
|
(Components in eeschema are instantiated from library parts.)
|
||||||
|
This part class is implemented by wrapping an xmlElement with accessors.
|
||||||
|
This xmlElement instance is held in field 'element'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, xml_element):
|
||||||
|
#
|
||||||
|
self.element = xml_element
|
||||||
|
|
||||||
|
# def __str__(self):
|
||||||
|
# simply print the xmlElement associated with this part
|
||||||
|
# return str(self.element)
|
||||||
|
|
||||||
|
def getLibName(self):
|
||||||
|
return self.element.get("libpart", "lib")
|
||||||
|
|
||||||
|
def getPartName(self):
|
||||||
|
return self.element.get("libpart", "part")
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return self.element.get("description")
|
||||||
|
|
||||||
|
def getField(self, name):
|
||||||
|
return self.element.get("field", "name", name)
|
||||||
|
|
||||||
|
def getFieldNames(self):
|
||||||
|
"""Return a list of field names in play for this libpart."""
|
||||||
|
fieldNames = []
|
||||||
|
fields = self.element.getChild("fields")
|
||||||
|
if fields:
|
||||||
|
for f in fields.getChildren():
|
||||||
|
fieldNames.append(f.get("field", "name"))
|
||||||
|
return fieldNames
|
||||||
|
|
||||||
|
def getPinList(self):
|
||||||
|
"""Return a list of pins in play for this libpart."""
|
||||||
|
pinList = []
|
||||||
|
pins = self.element.getChild("pins")
|
||||||
|
if pins:
|
||||||
|
for f in pins.getChildren():
|
||||||
|
pinList.append(f)
|
||||||
|
return pinList
|
||||||
|
|
||||||
|
def getDatasheet(self):
|
||||||
|
return self.getField("Datasheet")
|
||||||
|
|
||||||
|
def getFootprint(self):
|
||||||
|
return self.getField("Footprint")
|
||||||
|
|
||||||
|
def getAliases(self):
|
||||||
|
"""Return a list of aliases or None"""
|
||||||
|
aliases = self.element.getChild("aliases")
|
||||||
|
if aliases:
|
||||||
|
ret = []
|
||||||
|
children = aliases.getChildren()
|
||||||
|
# grab the text out of each child:
|
||||||
|
for child in children:
|
||||||
|
ret.append(child.get("alias"))
|
||||||
|
return ret
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class comp:
|
||||||
|
"""Class for a component, aka 'comp' in the xml netlist file.
|
||||||
|
This component class is implemented by wrapping an xmlElement instance
|
||||||
|
with accessors. The xmlElement is held in field 'element'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, xml_element):
|
||||||
|
self.element = xml_element
|
||||||
|
self.libpart = None
|
||||||
|
|
||||||
|
# Set to true when this component is included in a component group
|
||||||
|
self.grouped = False
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""Equivalency operator, remember this can be easily overloaded
|
||||||
|
2 components are equivalent ( i.e. can be grouped
|
||||||
|
if they have same value and same footprint and are both set to be populated
|
||||||
|
|
||||||
|
Override the component equivalence operator must be done before
|
||||||
|
loading the netlist, otherwise all components will have the original
|
||||||
|
equivalency operator.
|
||||||
|
|
||||||
|
You have to define a comparison module (for instance named myEqu)
|
||||||
|
and add the line;
|
||||||
|
kicad_netlist_reader.comp.__eq__ = myEqu
|
||||||
|
in your bom generator script before calling the netliste reader by something like:
|
||||||
|
net = kicad_netlist_reader.netlist(sys.argv[1])
|
||||||
|
"""
|
||||||
|
result = False
|
||||||
|
if self.getValue() == other.getValue():
|
||||||
|
if self.getFootprint() == other.getFootprint():
|
||||||
|
if self.getRef().rstrip(string.digits) == other.getRef().rstrip(string.digits):
|
||||||
|
if self.getDNP() == other.getDNP():
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
def setLibPart(self, part):
|
||||||
|
self.libpart = part
|
||||||
|
|
||||||
|
def getLibPart(self):
|
||||||
|
return self.libpart
|
||||||
|
|
||||||
|
def getPartName(self):
|
||||||
|
return self.element.get("libsource", "part")
|
||||||
|
|
||||||
|
def getLibName(self):
|
||||||
|
return self.element.get("libsource", "lib")
|
||||||
|
|
||||||
|
def setValue(self, value):
|
||||||
|
"""Set the value of this component"""
|
||||||
|
v = self.element.getChild("value")
|
||||||
|
if v:
|
||||||
|
v.setChars(value)
|
||||||
|
|
||||||
|
def getValue(self):
|
||||||
|
return self.element.get("value")
|
||||||
|
|
||||||
|
def getField(self, name, aLibraryToo=True):
|
||||||
|
"""
|
||||||
|
Return the value of a field named name. The component is first
|
||||||
|
checked for the field, and then the components library part is checked
|
||||||
|
for the field. If the field doesn't exist in either, an empty string is
|
||||||
|
returned
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
name -- The name of the field to return the value for
|
||||||
|
aLibraryToo -- look in the libpart's fields for the same name if not found
|
||||||
|
in component itself
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = self.element.get("field", "name", name)
|
||||||
|
if field == "" and aLibraryToo and self.libpart:
|
||||||
|
field = self.libpart.getField(name)
|
||||||
|
return field
|
||||||
|
|
||||||
|
def getFieldNames(self):
|
||||||
|
"""Return a list of field names in play for this component. Mandatory
|
||||||
|
fields are not included, and they are: Value, Footprint, Datasheet, Ref.
|
||||||
|
The netlist format only includes fields with non-empty values. So if a field
|
||||||
|
is empty, it will not be present in the returned list.
|
||||||
|
"""
|
||||||
|
fieldNames = []
|
||||||
|
fields = self.element.getChild("fields")
|
||||||
|
if fields:
|
||||||
|
for f in fields.getChildren():
|
||||||
|
fieldNames.append(f.get("field", "name"))
|
||||||
|
return fieldNames
|
||||||
|
|
||||||
|
def getRef(self):
|
||||||
|
return self.element.get("comp", "ref")
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return true if the component has the DNP property set
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getDNP(self):
|
||||||
|
for child in self.element.getChildren("property"):
|
||||||
|
try:
|
||||||
|
if child.attributes["name"] == "dnp":
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return 'DNP' if the component has the DNP property set
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getDNPString(self):
|
||||||
|
if self.getDNP():
|
||||||
|
return "DNP"
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return true if the component has the exclude from BOM property set
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getExcludeFromBOM(self):
|
||||||
|
for child in self.element.getChildren("property"):
|
||||||
|
try:
|
||||||
|
if child.attributes["name"] == "exclude_from_bom":
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return true if the component has the exclude from Board property set
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getExcludeFromBoard(self):
|
||||||
|
for child in self.element.getChildren("property"):
|
||||||
|
try:
|
||||||
|
if child.attributes["name"] == "exclude_from_board":
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
return the footprint name. if empty and aLibraryToo = True, return the
|
||||||
|
footprint name from libary
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getFootprint(self, aLibraryToo=True):
|
||||||
|
ret = self.element.get("footprint")
|
||||||
|
|
||||||
|
if ret == "" and aLibraryToo and self.libpart:
|
||||||
|
ret = self.libpart.getFootprint()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
"""
|
||||||
|
return the datasheet name. if empty and aLibraryToo = True, return the
|
||||||
|
datasheet name from libary
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getDatasheet(self, aLibraryToo=True):
|
||||||
|
ret = self.element.get("datasheet")
|
||||||
|
if ret == "" and aLibraryToo and self.libpart:
|
||||||
|
ret = self.libpart.getDatasheet()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def getTimestamp(self):
|
||||||
|
"""
|
||||||
|
Kicad 5 uses tstamp keyword for time stamp (8 digits) as UUID
|
||||||
|
Kicad 6 uses tstamps keyword for UUID and a multi unit symbol has more than one UUID
|
||||||
|
(UUIDs are separated by spaces)
|
||||||
|
"""
|
||||||
|
ret = self.element.get("tstamp")
|
||||||
|
if ret == "":
|
||||||
|
ret = self.element.get("tstamps")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def getDescription(self):
|
||||||
|
return self.element.get("libsource", "description")
|
||||||
|
|
||||||
|
"""
|
||||||
|
return the netname of the pin aPinNum in netlist aNetlist
|
||||||
|
if aSkipEmptyNet = True, net having only one pin will return a empty name
|
||||||
|
"""
|
||||||
|
|
||||||
|
def getPinNetname(self, aPinNum, aNetlist, aSkipEmptyNet):
|
||||||
|
ref = self.getRef()
|
||||||
|
|
||||||
|
for net in aNetlist.nets:
|
||||||
|
net_name = net.get("net", "name")
|
||||||
|
|
||||||
|
item_cnt = 1
|
||||||
|
netitems = net.children
|
||||||
|
|
||||||
|
for node in netitems:
|
||||||
|
curr_item_ref = node.get("node", "ref")
|
||||||
|
|
||||||
|
if curr_item_ref == ref:
|
||||||
|
curr_pin = node.get("node", "pin")
|
||||||
|
|
||||||
|
if aPinNum == curr_pin:
|
||||||
|
if aSkipEmptyNet: # ensure at least 2 pins are in net
|
||||||
|
pin_count = 0
|
||||||
|
|
||||||
|
for curr_node in netitems:
|
||||||
|
pin_count += 1
|
||||||
|
|
||||||
|
if pin_count > 1:
|
||||||
|
return net_name
|
||||||
|
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return net_name
|
||||||
|
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
|
||||||
|
class netlist:
|
||||||
|
"""Kicad generic netlist class. Generally loaded from a kicad generic
|
||||||
|
netlist file. Includes several helper functions to ease BOM creating
|
||||||
|
scripts
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fname=""):
|
||||||
|
"""Initialiser for the genericNetlist class
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
fname -- The name of the generic netlist file to open (Optional)
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.design = None
|
||||||
|
self.components = []
|
||||||
|
self.libparts = []
|
||||||
|
self.libraries = []
|
||||||
|
self.nets = []
|
||||||
|
|
||||||
|
# The entire tree is loaded into self.tree
|
||||||
|
self.tree = []
|
||||||
|
|
||||||
|
self._curr_element = None
|
||||||
|
|
||||||
|
# component blacklist regexs, made from excluded_* above.
|
||||||
|
self.excluded_references = []
|
||||||
|
self.excluded_values = []
|
||||||
|
self.excluded_footprints = []
|
||||||
|
|
||||||
|
if fname != "":
|
||||||
|
self.load(fname)
|
||||||
|
|
||||||
|
def addChars(self, content):
|
||||||
|
"""Add characters to the current element"""
|
||||||
|
self._curr_element.addChars(content)
|
||||||
|
|
||||||
|
def addElement(self, name):
|
||||||
|
"""Add a new kicad generic element to the list"""
|
||||||
|
if self._curr_element is None:
|
||||||
|
self.tree = xmlElement(name)
|
||||||
|
self._curr_element = self.tree
|
||||||
|
else:
|
||||||
|
self._curr_element = self._curr_element.addChild(xmlElement(name, self._curr_element))
|
||||||
|
|
||||||
|
# If this element is a component, add it to the components list
|
||||||
|
if self._curr_element.name == "comp":
|
||||||
|
self.components.append(comp(self._curr_element))
|
||||||
|
|
||||||
|
# Assign the design element
|
||||||
|
if self._curr_element.name == "design":
|
||||||
|
self.design = self._curr_element
|
||||||
|
|
||||||
|
# If this element is a library part, add it to the parts list
|
||||||
|
if self._curr_element.name == "libpart":
|
||||||
|
self.libparts.append(libpart(self._curr_element))
|
||||||
|
|
||||||
|
# If this element is a net, add it to the nets list
|
||||||
|
if self._curr_element.name == "net":
|
||||||
|
self.nets.append(self._curr_element)
|
||||||
|
|
||||||
|
# If this element is a library, add it to the libraries list
|
||||||
|
if self._curr_element.name == "library":
|
||||||
|
self.libraries.append(self._curr_element)
|
||||||
|
|
||||||
|
return self._curr_element
|
||||||
|
|
||||||
|
def endDocument(self):
|
||||||
|
"""Called when the netlist document has been fully parsed"""
|
||||||
|
# When the document is complete, the library parts must be linked to
|
||||||
|
# the components as they are separate in the tree so as not to
|
||||||
|
# duplicate library part information for every component
|
||||||
|
for c in self.components:
|
||||||
|
for p in self.libparts:
|
||||||
|
if p.getLibName() == c.getLibName():
|
||||||
|
if p.getPartName() == c.getPartName():
|
||||||
|
c.setLibPart(p)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
aliases = p.getAliases()
|
||||||
|
if aliases and self.aliasMatch(c.getPartName(), aliases):
|
||||||
|
c.setLibPart(p)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not c.getLibPart():
|
||||||
|
print("missing libpart for ref:", c.getRef(), c.getPartName(), c.getLibName())
|
||||||
|
|
||||||
|
def aliasMatch(self, partName, aliasList):
|
||||||
|
for alias in aliasList:
|
||||||
|
if partName == alias:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def endElement(self):
|
||||||
|
"""End the current element and switch to its parent"""
|
||||||
|
self._curr_element = self._curr_element.getParent()
|
||||||
|
|
||||||
|
def getDate(self):
|
||||||
|
"""Return the date + time string generated by the tree creation tool"""
|
||||||
|
return self.design.get("date")
|
||||||
|
|
||||||
|
def getSource(self):
|
||||||
|
"""Return the source string for the design"""
|
||||||
|
return self.design.get("source")
|
||||||
|
|
||||||
|
def getTool(self):
|
||||||
|
"""Return the tool string which was used to create the netlist tree"""
|
||||||
|
return self.design.get("tool")
|
||||||
|
|
||||||
|
def getNets(self):
|
||||||
|
"""Return the nets"""
|
||||||
|
return self.nets
|
||||||
|
|
||||||
|
def gatherComponentFieldUnion(self, components=None):
|
||||||
|
"""Gather the complete 'set' of unique component fields, fields found in any component."""
|
||||||
|
if not components:
|
||||||
|
components = self.components
|
||||||
|
|
||||||
|
s = set()
|
||||||
|
for c in components:
|
||||||
|
s.update(c.getFieldNames())
|
||||||
|
|
||||||
|
# omit anything matching any regex in excluded_fields
|
||||||
|
ret = set()
|
||||||
|
for field in s:
|
||||||
|
exclude = False
|
||||||
|
for rex in excluded_fields:
|
||||||
|
if re.match(rex, field):
|
||||||
|
exclude = True
|
||||||
|
break
|
||||||
|
if not exclude:
|
||||||
|
ret.add(field)
|
||||||
|
|
||||||
|
return ret # this is a python 'set'
|
||||||
|
|
||||||
|
def gatherLibPartFieldUnion(self):
|
||||||
|
"""Gather the complete 'set' of part fields, fields found in any part."""
|
||||||
|
s = set()
|
||||||
|
for p in self.libparts:
|
||||||
|
s.update(p.getFieldNames())
|
||||||
|
|
||||||
|
# omit anything matching any regex in excluded_fields
|
||||||
|
ret = set()
|
||||||
|
for field in s:
|
||||||
|
exclude = False
|
||||||
|
for rex in excluded_fields:
|
||||||
|
if re.match(rex, field):
|
||||||
|
exclude = True
|
||||||
|
break
|
||||||
|
if not exclude:
|
||||||
|
ret.add(field)
|
||||||
|
|
||||||
|
return ret # this is a python 'set'
|
||||||
|
|
||||||
|
def getInterestingComponents(self, excludeBOM=False, excludeBoard=False, DNP=False):
|
||||||
|
"""Return a subset of all components, those that should show up in the BOM.
|
||||||
|
Omit those that should not, by consulting the blacklists:
|
||||||
|
excluded_values, excluded_refs, and excluded_footprints, which hold one
|
||||||
|
or more regular expressions. If any of the regular expressions match
|
||||||
|
the corresponding field's value in a component, then the component is excluded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pre-compile all the regex expressions:
|
||||||
|
del self.excluded_references[:]
|
||||||
|
del self.excluded_values[:]
|
||||||
|
del self.excluded_footprints[:]
|
||||||
|
|
||||||
|
for rex in excluded_references:
|
||||||
|
self.excluded_references.append(re.compile(rex))
|
||||||
|
|
||||||
|
for rex in excluded_values:
|
||||||
|
self.excluded_values.append(re.compile(rex))
|
||||||
|
|
||||||
|
for rex in excluded_footprints:
|
||||||
|
self.excluded_footprints.append(re.compile(rex))
|
||||||
|
|
||||||
|
# the subset of components to return, considered as "interesting".
|
||||||
|
ret = []
|
||||||
|
|
||||||
|
# run each component thru a series of tests, if it passes all, then add it
|
||||||
|
# to the interesting list 'ret'.
|
||||||
|
for c in self.components:
|
||||||
|
exclude = False
|
||||||
|
if not exclude:
|
||||||
|
for refs in self.excluded_references:
|
||||||
|
if refs.match(c.getRef()):
|
||||||
|
exclude = True
|
||||||
|
break
|
||||||
|
if not exclude:
|
||||||
|
for vals in self.excluded_values:
|
||||||
|
if vals.match(c.getValue()):
|
||||||
|
exclude = True
|
||||||
|
break
|
||||||
|
if not exclude:
|
||||||
|
for mods in self.excluded_footprints:
|
||||||
|
if mods.match(c.getFootprint()):
|
||||||
|
exclude = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if excludeBOM and c.getExcludeFromBOM():
|
||||||
|
exclude = True
|
||||||
|
|
||||||
|
if excludeBoard and c.getExcludeFromBoard():
|
||||||
|
exclude = True
|
||||||
|
|
||||||
|
if DNP and c.getDNP():
|
||||||
|
exclude = True
|
||||||
|
|
||||||
|
if not exclude:
|
||||||
|
ret.append(c)
|
||||||
|
|
||||||
|
# The key to sort the components in the BOM
|
||||||
|
# This sorts using a natural sorting order (e.g. 100 after 99), and if it wasn't used
|
||||||
|
# the normal sort would place 100 before 99 since it only would look at the first digit.
|
||||||
|
def sortKey(str):
|
||||||
|
return [int(t) if t.isdigit() else t.lower() for t in re.split("(\d+)", str)]
|
||||||
|
|
||||||
|
ret.sort(key=lambda g: sortKey(g.getRef()))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def groupComponents(self, components=None):
|
||||||
|
"""Return a list of component lists. Components are grouped together
|
||||||
|
when the value, library and part identifiers match.
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
components -- is a list of components, typically an interesting subset
|
||||||
|
of all components, or None. If None, then all components are looked at.
|
||||||
|
"""
|
||||||
|
if not components:
|
||||||
|
components = self.components
|
||||||
|
|
||||||
|
groups = []
|
||||||
|
|
||||||
|
# Make sure to start off will all components ungrouped to begin with
|
||||||
|
for c in components:
|
||||||
|
c.grouped = False
|
||||||
|
|
||||||
|
# Group components based on the value, library and part identifiers
|
||||||
|
for c in components:
|
||||||
|
if c.grouped == False:
|
||||||
|
c.grouped = True
|
||||||
|
newgroup = []
|
||||||
|
newgroup.append(c)
|
||||||
|
|
||||||
|
# Check every other ungrouped component against this component
|
||||||
|
# and add to the group as necessary
|
||||||
|
for ci in components:
|
||||||
|
if ci.grouped == False and ci == c:
|
||||||
|
newgroup.append(ci)
|
||||||
|
ci.grouped = True
|
||||||
|
|
||||||
|
# Add the new component group to the groups list
|
||||||
|
groups.append(newgroup)
|
||||||
|
|
||||||
|
# The key to sort the components in the BOM
|
||||||
|
# This sorts using a natural sorting order (e.g. 100 after 99), and if it wasn't used
|
||||||
|
# the normal sort would place 100 before 99 since it only would look at the first digit.
|
||||||
|
def sortKey(str):
|
||||||
|
return [int(t) if t.isdigit() else t.lower() for t in re.split("(\d+)", str)]
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
# g = sorted(g, key=lambda g: sortKey(g.getRef()))
|
||||||
|
g.sort(key=lambda g: sortKey(g.getRef()))
|
||||||
|
|
||||||
|
# Finally, sort the groups to order the references alphabetically
|
||||||
|
groups.sort(key=lambda group: sortKey(group[0].getRef()))
|
||||||
|
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def getGroupField(self, group, field):
|
||||||
|
"""Return the whatever is known about the given field by consulting each
|
||||||
|
component in the group. If any of them know something about the property/field,
|
||||||
|
then return that first non-blank value.
|
||||||
|
"""
|
||||||
|
for c in group:
|
||||||
|
ret = c.getField(field, False)
|
||||||
|
if ret != "":
|
||||||
|
return ret
|
||||||
|
|
||||||
|
libpart = group[0].getLibPart()
|
||||||
|
if not libpart:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return libpart.getField(field)
|
||||||
|
|
||||||
|
def getGroupFootprint(self, group):
|
||||||
|
"""Return the whatever is known about the Footprint by consulting each
|
||||||
|
component in the group. If any of them know something about the Footprint,
|
||||||
|
then return that first non-blank value.
|
||||||
|
"""
|
||||||
|
for c in group:
|
||||||
|
ret = c.getFootprint()
|
||||||
|
if ret != "":
|
||||||
|
return ret
|
||||||
|
return group[0].getLibPart().getFootprint()
|
||||||
|
|
||||||
|
def getGroupDatasheet(self, group):
|
||||||
|
"""Return the whatever is known about the Datasheet by consulting each
|
||||||
|
component in the group. If any of them know something about the Datasheet,
|
||||||
|
then return that first non-blank value.
|
||||||
|
"""
|
||||||
|
for c in group:
|
||||||
|
ret = c.getDatasheet()
|
||||||
|
if ret != "":
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if len(group) > 0:
|
||||||
|
return group[0].getLibPart().getDatasheet()
|
||||||
|
else:
|
||||||
|
print("NULL!")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def formatXML(self):
|
||||||
|
"""Return the whole netlist formatted in XML"""
|
||||||
|
return self.tree.formatXML()
|
||||||
|
|
||||||
|
def formatHTML(self):
|
||||||
|
"""Return the whole netlist formatted in HTML"""
|
||||||
|
return self.tree.formatHTML()
|
||||||
|
|
||||||
|
def load(self, fname):
|
||||||
|
"""Load a kicad generic netlist
|
||||||
|
|
||||||
|
Keywords:
|
||||||
|
fname -- The name of the generic netlist file to open
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._reader = sax.make_parser()
|
||||||
|
self._reader.setContentHandler(_gNetReader(self))
|
||||||
|
self._reader.parse(fname)
|
||||||
|
except IOError as e:
|
||||||
|
print(__file__, ":", e, file=sys.stderr)
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
|
||||||
|
class _gNetReader(sax.handler.ContentHandler):
|
||||||
|
"""SAX kicad generic netlist content handler - passes most of the work back
|
||||||
|
to the 'netlist' class which builds a complete tree in RAM for the design
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, aParent):
|
||||||
|
self.parent = aParent
|
||||||
|
|
||||||
|
def startElement(self, name, attrs):
|
||||||
|
"""Start of a new XML element event"""
|
||||||
|
element = self.parent.addElement(name)
|
||||||
|
|
||||||
|
for name in attrs.getNames():
|
||||||
|
element.addAttribute(name, attrs.getValue(name))
|
||||||
|
|
||||||
|
def endElement(self, name):
|
||||||
|
self.parent.endElement()
|
||||||
|
|
||||||
|
def characters(self, content):
|
||||||
|
# Ignore erroneous white space - ignoreableWhitespace does not get rid
|
||||||
|
# of the need for this!
|
||||||
|
if not content.isspace():
|
||||||
|
self.parent.addChars(content)
|
||||||
|
|
||||||
|
def endDocument(self):
|
||||||
|
"""End of the XML document event"""
|
||||||
|
self.parent.endDocument()
|
28
plugins/kicad_utils.py
Normal file
28
plugins/kicad_utils.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#
|
||||||
|
# KiCad python module for some helper functions
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def open_file_write(path, mode):
|
||||||
|
"""Open "path" for writing, creating any parent directories as needed."""
|
||||||
|
dir_path = os.path.dirname(path)
|
||||||
|
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
os.makedirs(dir_path)
|
||||||
|
|
||||||
|
return open(path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
def open_file_writeUTF8(path, mode):
|
||||||
|
"""
|
||||||
|
Open "path" for writing, creating any parent directories as needed.
|
||||||
|
Use it only for text files. Force text encoding in UTF-8.
|
||||||
|
"""
|
||||||
|
dir_path = os.path.dirname(path)
|
||||||
|
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
os.makedirs(dir_path)
|
||||||
|
|
||||||
|
return open(path, mode, encoding="utf-8")
|
|
@ -115,6 +115,8 @@ with zipfile.ZipFile(dir_gerber.with_suffix(".zip"), "w") as z:
|
||||||
z.write(f, f.name)
|
z.write(f, f.name)
|
||||||
|
|
||||||
# %% Assembly files
|
# %% Assembly files
|
||||||
|
|
||||||
|
# Assembly Drawing
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
"kicad-cli",
|
"kicad-cli",
|
||||||
|
@ -131,6 +133,26 @@ subprocess.run(
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Position Files
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"kicad-cli",
|
||||||
|
"pcb",
|
||||||
|
"export",
|
||||||
|
"pos",
|
||||||
|
str(root_project.with_suffix(".kicad_pcb")),
|
||||||
|
"-o",
|
||||||
|
str(dir_out / f"{project_name}.pos"),
|
||||||
|
"--format",
|
||||||
|
"ascii",
|
||||||
|
"--side",
|
||||||
|
"both",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# BOM
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[
|
[
|
||||||
"kicad-cli",
|
"kicad-cli",
|
||||||
|
@ -143,8 +165,17 @@ subprocess.run(
|
||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
)
|
)
|
||||||
# TODO: post-process XML BOM into something more useful or skip XML altogether and just make an interactive HTML BOM
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"python3",
|
||||||
|
"/home/brendan/Documents/projects/bhht/common_libraries/plugins/bom_csv_grouped_extra.py", # TODO: move this
|
||||||
|
str(dir_out / f"{project_name}_bom.xml"),
|
||||||
|
str(dir_out / f"{project_name}_bom.csv"),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Interactive BOM
|
||||||
# subprocess.run(
|
# subprocess.run(
|
||||||
# [
|
# [
|
||||||
# "python3",
|
# "python3",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user