diff --git a/Fabrication/jlc_kicad_tools/KC-POS_to_JLC.py b/Fabrication/jlc_kicad_tools/KC-POS_to_JLC.py
new file mode 100644
index 0000000..594f297
--- /dev/null
+++ b/Fabrication/jlc_kicad_tools/KC-POS_to_JLC.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+# Copyright (C) 2019 Matthew Lai
+#
+# This file is part of JLC Kicad Tools.
+#
+# JLC Kicad Tools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# JLC Kicad Tools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with JLC Kicad Tools. If not, see .
+
+import os
+import re
+import sys
+import argparse
+import logging
+import errno
+
+import csv
+import re
+import sys
+import logging
+
+# JLC requires columns to be named a certain way.
+HEADER_REPLACEMENT_TABLE={
+ "Ref": "Designator",
+ "Val": "Val",
+ "Package": "PackageReference",
+ "PosX": "Mid X",
+ "PosY": "Mid Y",
+ "Rot": "Rotation",
+ "Side": "Layer",
+}
+
+ROW_REPLACEMENT_TABLE={
+ "TopLayer": "Top",
+ "BottomLayer": "Bottom",
+}
+
+def ReadDB(filename):
+ db = {}
+ with open(filename) as csvfile:
+ reader = csv.reader(csvfile, delimiter=',')
+ for row in reader:
+ if row[0] == "Footprint pattern":
+ continue
+ else:
+ db[re.compile(row[0])] = int(row[1])
+ logging.info("Read {} rules from {}".format(len(db), filename))
+ return db
+
+def FixRotations(input_filename, output_filename, db):
+ with open(input_filename) as csvfile:
+ reader = csv.reader(csvfile, delimiter=',')
+ writer = csv.writer(open(output_filename, 'w', newline=''), delimiter=',')
+ package_index = None
+ rotation_index = None
+ for row in reader:
+ if not package_index:
+ # This is the first row. Find "Package" and "Rot" column indices.
+ for i in range(len(row)):
+ if row[i] == "Package":
+ package_index = i
+ elif row[i] == "Rot":
+ rotation_index = i
+ if package_index is None:
+ logging.warning("Failed to find 'Package' column in the csv file")
+ return False
+ if rotation_index is None:
+ logging.warning("Failed to find 'Rot' column in the csv file")
+ return False
+ # Replace column names with labels JLC wants.
+ for i in range(len(row)):
+ if row[i] in HEADER_REPLACEMENT_TABLE:
+ row[i] = HEADER_REPLACEMENT_TABLE[row[i]]
+ else:
+ for pattern, correction in db.items():
+ if pattern.match(row[package_index]):
+ logging.info("Footprint {} matched {}. Applying {} deg correction"
+ .format(row[package_index], pattern.pattern, correction))
+ row[rotation_index] = "{0:.0f}".format((float(row[rotation_index]) + correction) % 360)
+ break
+ for i in range(len(row)):
+ if row[i] in ROW_REPLACEMENT_TABLE:
+ row[i] = ROW_REPLACEMENT_TABLE[row[i]]
+ del row[package_index]
+ writer.writerow(row)
+ return True
+
+DEFAULT_DB_PATH="cpl_KC-to-JLC-rotations_db.csv"
+
+def main():
+ parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description='Generates BOM and CPL in CSV fashion to be used in JLCPCB Assembly Service', prog='generate_jlc_files')
+ parser.add_argument('project_dir', metavar='INPUT_DIRECTORY', type=os.path.abspath, help='Directory of KiCad project. Name should match KiCad project name.')
+ parser.add_argument('-d', '--database', metavar='DATABASE', type=str, help='Filename of database', default=os.path.join(os.path.dirname(__file__), DEFAULT_DB_PATH))
+ verbosity = parser.add_argument_group('verbosity arguments')
+ verbosity.add_argument('-v', '--verbose', help='Increases log verbosity for each occurrence', dest='verbose_count', action="count", default=0)
+ verbosity.add_argument('--warn-no-lcsc-partnumber', help='Enable warning output if lcsc part number is not found', dest='warn_no_partnumber', action='store_true')
+ parser.add_argument('--assume-same-lcsc-partnumber', help='Assume same lcsc partnumber for all components of a group', action='store_true', dest='assume_same_lcsc_partnumber')
+ parser.add_argument('-o', '--output', metavar='OUTPUT_DIRECTORY', dest='output_dir', type=os.path.abspath, help='Output directory. Default: INPUT_DIRECTORY')
+
+ if (len(sys.argv) == 1):
+ parser.print_help()
+ sys.exit()
+
+ # Parse arguments
+ opts = parser.parse_args(sys.argv[1:])
+
+ # Default log level is WARNING
+ logging.basicConfig(format="%(message)s", level=max(logging.WARNING - opts.verbose_count * 10, logging.NOTSET))
+
+ if not os.path.isdir(opts.project_dir):
+ logging.error("Failed to open project directory: {}".format(opts.project_dir))
+ return errno.ENOENT
+
+ # Set default output directory
+ if opts.output_dir == None:
+ opts.output_dir = opts.project_dir
+
+ if not os.path.isdir(opts.output_dir):
+ logging.info("Creating output directory {}".format(opts.output_dir))
+ os.mkdir(opts.output_dir)
+
+ project_name = os.path.basename(opts.project_dir)
+ logging.debug("Project name is '%s'.", project_name)
+ cpl_filename = project_name + "-top-pos.csv"
+ cpl_path = None
+
+ for dir_name, subdir_list, file_list in os.walk(opts.project_dir):
+ for file_name in file_list:
+ if file_name == cpl_filename:
+ cpl_path = os.path.join(dir_name, file_name)
+
+ if cpl_path is None:
+ logging.error((
+ "Failed to find CPL file: {} in {} (and sub-directories). "
+ "Run 'File -> Fabrication Outputs -> Footprint Position (.pos) File' in Pcbnew. "
+ "Settings: 'CSV', 'mm', 'single file for board'.").format(cpl_filename, opts.project_dir))
+ return errno.ENOENT
+
+ logging.info("CPL file found at: {}".format(cpl_path))
+
+ cpl_output_path = os.path.join(opts.output_dir, project_name + "_JLC_CPL.csv")
+
+ db = ReadDB(opts.database)
+ if FixRotations(cpl_path, cpl_output_path, db):
+ logging.info("JLC CPL file written to: {}".format(cpl_output_path))
+ else:
+ return errno.EINVAL
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main())
+
\ No newline at end of file
diff --git a/Fabrication/jlc_kicad_tools/__init__.py b/Fabrication/jlc_kicad_tools/__init__.py
new file mode 100644
index 0000000..1f356cc
--- /dev/null
+++ b/Fabrication/jlc_kicad_tools/__init__.py
@@ -0,0 +1 @@
+__version__ = '1.0.0'
diff --git a/Fabrication/jlc_kicad_tools/cpl_KC-to-JLC-rotations_db.csv b/Fabrication/jlc_kicad_tools/cpl_KC-to-JLC-rotations_db.csv
new file mode 100644
index 0000000..1eb1760
--- /dev/null
+++ b/Fabrication/jlc_kicad_tools/cpl_KC-to-JLC-rotations_db.csv
@@ -0,0 +1,9 @@
+"Footprint pattern","Correction"
+"CP_Elec_8x10.5",180
+"SOT-23",180
+"D_SMC",180
+"R_Array_Convex_4x0603",270
+"SOT-223-3_TabPin2",180
+"SO-8_5.3x6.2mm_P1.27mm-150+208",90
+"TSSOP-14_4.4x5mm_P0.65mm",180
+"SOIC-8_3.9x4.9mm_P1.27mm",90
diff --git a/Fabrication/jlc_kicad_tools/jlc_lib/__init__.py b/Fabrication/jlc_kicad_tools/jlc_lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Fabrication/jlc_kicad_tools/jlc_lib/cpl_fix_rotations.py b/Fabrication/jlc_kicad_tools/jlc_lib/cpl_fix_rotations.py
new file mode 100644
index 0000000..21834d1
--- /dev/null
+++ b/Fabrication/jlc_kicad_tools/jlc_lib/cpl_fix_rotations.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2019 Matthew Lai
+#
+# This file is part of JLC Kicad Tools.
+#
+# JLC Kicad Tools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# JLC Kicad Tools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with JLC Kicad Tools. If not, see .
+
+import csv
+import re
+import sys
+import logging
+
+# JLC requires columns to be named a certain way.
+HEADER_REPLACEMENT_TABLE={
+ "Ref": "Designator",
+ "PosX": "Mid X",
+ "PosY": "Mid Y",
+ "Rot": "Rotation",
+ "Side": "Layer"
+}
+
+def ReadDB(filename):
+ db = {}
+ with open(filename) as csvfile:
+ reader = csv.reader(csvfile, delimiter=',')
+ for row in reader:
+ if row[0] == "Footprint pattern":
+ continue
+ else:
+ db[re.compile(row[0])] = int(row[1])
+ logging.info("Read {} rules from {}".format(len(db), filename))
+ return db
+
+def FixRotations(input_filename, output_filename, db):
+ with open(input_filename) as csvfile:
+ reader = csv.reader(csvfile, delimiter=',')
+ writer = csv.writer(open(output_filename, 'w', newline=''), delimiter=',')
+ package_index = None
+ rotation_index = None
+ for row in reader:
+ if not package_index:
+ # This is the first row. Find "Package" and "Rot" column indices.
+ for i in range(len(row)):
+ if row[i] == "Package":
+ package_index = i
+ elif row[i] == "Rot":
+ rotation_index = i
+ if package_index is None:
+ logging.warning("Failed to find 'Package' column in the csv file")
+ return False
+ if rotation_index is None:
+ logging.warning("Failed to find 'Rot' column in the csv file")
+ return False
+ # Replace column names with labels JLC wants.
+ for i in range(len(row)):
+ if row[i] in HEADER_REPLACEMENT_TABLE:
+ row[i] = HEADER_REPLACEMENT_TABLE[row[i]]
+ else:
+ for pattern, correction in db.items():
+ if pattern.match(row[package_index]):
+ logging.info("Footprint {} matched {}. Applying {} deg correction"
+ .format(row[package_index], pattern.pattern, correction))
+ row[rotation_index] = "{0:.6f}".format((float(row[rotation_index]) + correction) % 360)
+ break
+ writer.writerow(row)
+ return True
diff --git a/Fabrication/jlc_kicad_tools/jlc_lib/generate_bom.py b/Fabrication/jlc_kicad_tools/jlc_lib/generate_bom.py
new file mode 100644
index 0000000..f0c1153
--- /dev/null
+++ b/Fabrication/jlc_kicad_tools/jlc_lib/generate_bom.py
@@ -0,0 +1,107 @@
+# Copyright (C) 2019 Matthew Lai
+# Copyright (C) 1992-2019 Kicad Developers Team
+#
+# This file is part of JLC Kicad Tools.
+#
+# JLC Kicad Tools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# JLC Kicad Tools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with JLC Kicad Tools. If not, see .
+
+from jlc_kicad_tools.jlc_lib import kicad_netlist_reader
+import csv
+import re
+import logging
+
+LCSC_PART_NUMBER_MATCHER=re.compile('^C[0-9]+$')
+
+def GenerateBOM(input_filename, output_filename, opts):
+ net = kicad_netlist_reader.netlist(input_filename)
+
+ try:
+ f = open(output_filename, mode='w', encoding='utf-8')
+ except IOError:
+ logging.error("Failed to open file for writing: {}".format(output_filename))
+ return False
+
+ out = csv.writer(f, lineterminator='\n', delimiter=',', quotechar='\"',
+ quoting=csv.QUOTE_ALL)
+
+ out.writerow(['Comment', 'Designator', 'Footprint', 'LCSC Part #'])
+
+ grouped = net.groupComponents()
+
+ num_groups_found = 0
+ for group in grouped:
+ refs = []
+ lcsc_part_numbers = set()
+ lcsc_part_numbers_none_found = False
+ footprints = set()
+
+ for component in group:
+ refs.append(component.getRef())
+ c = component
+ lcsc_part_number = None
+
+ # Get the field name for the LCSC part number.
+ for field_name in c.getFieldNames():
+ field_value = c.getField(field_name)
+
+ if LCSC_PART_NUMBER_MATCHER.match(field_value):
+ lcsc_part_number = field_value
+
+ if lcsc_part_number:
+ lcsc_part_numbers.add(lcsc_part_number)
+ else:
+ lcsc_part_numbers_none_found = True
+
+ if c.getFootprint() != '':
+ footprints.add(c.getFootprint())
+
+ # Check part numbers for uniqueness
+ if len(lcsc_part_numbers) == 0:
+ if opts.warn_no_partnumber:
+ logging.warning("No LCSC part number found for components {}".format(",".join(refs)))
+ continue
+ elif len(lcsc_part_numbers) != 1:
+ logging.error("Components {components} from same group have different LCSC part numbers: {partnumbers}".format(
+ components = ", ".join(refs),
+ partnumbers = ", ".join(lcsc_part_numbers)))
+ return False
+ lcsc_part_number = list(lcsc_part_numbers)[0]
+
+ if (not opts.assume_same_lcsc_partnumber) and (lcsc_part_numbers_none_found):
+ logging.error("Components {components} from same group do not all have LCSC part number {partnumber} set. Use --assume-same-lcsc-partnumber to ignore.".format(
+ components = ", ".join(refs),
+ partnumber = lcsc_part_number))
+ return False
+
+ # Check footprints for uniqueness
+ if (len(footprints) == 0):
+ logging.error("No footprint found for components {}".format(",".join(refs)))
+ return False
+ if len(footprints) != 1:
+ logging.error("Components {components} from same group have different foot prints: {footprints}".format(
+ components = ", ".join(refs),
+ footprints = ", ".join(footprints)))
+ return False
+ footprint = list(footprints)[0]
+
+ # They don't seem to like ':' in footprint names.
+ footprint = footprint[(footprint.find(':') + 1):]
+
+ # Fill in the component groups common data
+ out.writerow([c.getValue(), ",".join(refs), footprint, lcsc_part_number])
+ num_groups_found += 1
+
+ logging.info("{} component groups found from BOM file.".format(num_groups_found))
+
+ return True
diff --git a/Fabrication/jlc_kicad_tools/jlc_lib/kicad_netlist_reader.py b/Fabrication/jlc_kicad_tools/jlc_lib/kicad_netlist_reader.py
new file mode 100644
index 0000000..6cdf529
--- /dev/null
+++ b/Fabrication/jlc_kicad_tools/jlc_lib/kicad_netlist_reader.py
@@ -0,0 +1,794 @@
+# Copyright (C) 1992-2019 Kicad Developers Team
+#
+# This file is part of JLC Kicad Tools.
+#
+# JLC Kicad Tools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# JLC Kicad Tools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with JLC Kicad Tools. If not, see .
+
+#
+# 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 sys
+import xml.sax as sax
+import re
+import pdb
+import logging
+
+#---------------------------------------------------------------------
+
+# 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 exlude 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'
+ ]
+
+#--------------------------------------------------------------------
+
+
+class xmlElement():
+ """xml element which can represent all nodes of the netlist tree. It can be
+ used to easily generate various output formats by propogating 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 = "\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 = """
+
+
+
+
+
+
+
+ """
+
+ s += "" + self.name + " " + self.chars + " | "
+ for a in self.attributes:
+ s += "- " + a + " = " + self.attributes[a] + "
"
+
+ s += " |
\n"
+
+ for c in self.children:
+ s += c.formatHTML(True)
+
+ if not amChild:
+ s += """
+
+ """
+
+ 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 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
+
+ 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():
+ 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, libraryToo=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
+ libraryToo -- 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 libraryToo 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")
+
+ def getFootprint(self, libraryToo=True):
+ ret = self.element.get("footprint")
+ if ret == "" and libraryToo and self.libpart:
+ ret = self.libpart.getFootprint()
+ return ret
+
+ def getDatasheet(self, libraryToo=True):
+ ret = self.element.get("datasheet")
+ if ret == "" and libraryToo and self.libpart:
+ ret = self.libpart.getDatasheet()
+ return ret
+
+ def getTimestamp(self):
+ return self.element.get("tstamp")
+
+ def getDescription(self):
+ return self.element.get("libsource", "description")
+
+
+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 exluded_* 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 == 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 seperate 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():
+ logging.error('Missing libpart for ref {}: {}:{}'.format(c.getRef(), c.getLibName(), c.getPartName() ))
+
+
+ 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 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):
+ """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 the regular expressions match
+ the corresponding field's value in a component, then the component is exluded.
+ """
+
+ # 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 not exclude:
+ # This is a fairly personal way to flag DNS (Do Not Stuff). NU for
+ # me means Normally Uninstalled. You can 'or in' another expression here.
+ if c.getField( "Installed" ) == 'NU':
+ 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()))
+
+ # Finally, sort the groups to order the references alphabetically
+ groups = sorted(groups, 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:
+ logging.error("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:
+ logging.error("{}: {}".format(__file__, e))
+ 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()