diff --git a/KiBOM/component.py b/KiBOM/component.py new file mode 100644 index 00000000..827f252a --- /dev/null +++ b/KiBOM/component.py @@ -0,0 +1,302 @@ +from columns import Columns + +import units + +from sort import natural_sort + +class Component(): + """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 + + #compare the value of this part, to the value of another part (see if they match) + def compareValue(self, other): + #simple string comparison + if self.getValue().lower() == other.getValue().lower(): return True + + #otherwise, perform a more complicate value comparison + if units.compareValues(self.getValue(), other.getValue()): return True + + #no match, return False + return False + + #compare footprint with another component + def compareFootprint(self, other): + return self.getFootprint().lower() == other.getFootprint().lower() + + #compare the component library of this part to another part + def compareLibName(self, other): + return self.getLibName().lower() == other.getLibName().lower() + + #determine if two parts have the same name + def comparePartName(self, other): + pn1 = self.getPartName().lower() + pn2 = other.getPartName().lower() + + #simple direct match + if pn1 == pn2: return True + + #compare part aliases e.g. "c" to "c_small" + for alias in ALIASES: + if pn1 in alias and pn2 in alias: + return True + + return False + + #Equivalency operator is used to determine if two parts are 'equal' + def __eq__(self, other): + """Equlivalency operator, remember this can be easily overloaded""" + + #special case for connectors, as the "Value" is the description of the connector (and is somewhat meaningless) + if "connector" in self.getDescription().lower(): + #ignore "value" + valueResult = True + else: + valueResult = self.compareValue(other) + + return valueResult and self.compareFootprint(other) and self.compareLibName(other) and self.comparePartName(other) and self.isFitted() == other.isFitted() + + def setLibPart(self, part): + self.libpart = part + + def getPrefix(self): #return the reference prefix + #e.g. if this component has a reference U12, will return "U" + prefix = "" + + for c in self.getRef(): + if c.isalpha(): prefix += c + else: break + + return prefix + + 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: + 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") + + #determine if a component is FITTED or not + def isFitted(self): + + check = [self.getValue().lower(), self.getField("Notes").lower()] + + for item in check: + if any([dnf in item for dnf in DNF]): return False + + return True + + def getFootprint(self, libraryToo=True): + ret = self.element.get("footprint") + if ret =="" and libraryToo: + ret = self.libpart.getFootprint() + return ret + + def getDatasheet(self, libraryToo=True): + return self.libpart.getDatasheet() + + def getTimestamp(self): + return self.element.get("tstamp") + + def getDescription(self): + return self.libpart.getDescription() + +class ComponentGroup(): + + """ + Initialize the group with no components, and default fields + """ + def __init__(self): + self.components = [] + self.fields = dict.fromkeys(Columns._COLUMNS_GROUPED) #columns loaded from KiCAD + self.csvFields = dict.fromkeys(Columns._COLUMNS_GROUPED) #columns loaded from .csv file + + def getField(self, field): + if not field in self.fields.keys(): return "" + if not self.fields[field]: return "" + return str(self.fields[field]) + + def getCSVField(self, field): + + #ignore protected fields + if field in CSV_PROTECTED: return "" + + if not field in self.csvFields.keys(): return "" + if not self.csvFields[field]: return "" + return str(self.csvFields[field]) + + def getHarmonizedField(self,field): + + #for protected fields, source from KiCAD + if field in CSV_PROTECTED: + return self.getField(field) + + #if there is kicad data, that takes preference + if not self.getField(field) == "": + return self.getField(field) + + elif not self.getCSVField(field) == "": + return self.getCSVField(field) + else: + return "" + + + def compareCSVLine(self, line): + """ + Compare a line (dict) and see if it matches this component group + """ + for field in CSV_MATCH: + if not field in line.keys(): return False + if not field in self.fields.keys(): return False + if not line[field] == self.fields[field]: return False + + return True + + def getCount(self): + for c in self.components: + if not c.isFitted(): return "0" + return len(self.components) + + #Test if a given component fits in this group + def matchComponent(self, c): + if len(self.components) == 0: return True + if c == self.components[0]: return True + + #test if a given component is already contained in this grop + def containsComponent(self, c): + if self.matchComponent(c) == False: return False + + for comp in self.components: + if comp.getRef() == c.getRef(): return True + + return False + + #add a component to the group + def addComponent(self, c): + + if len(self.components) == 0: + self.components.append(c) + elif self.containsComponent(c): + return + elif self.matchComponent(c): + self.components.append(c) + + def isFitted(self): + return any([c.isFitted() for c in self.components]) + + #return a list of the components + def getRefs(self): + #print([c.getRef() for c in self.components])) + #return " ".join([c.getRef() for c in self.components]) + return " ".join([c.getRef() for c in self.components]) + + #sort the components in correct order + def sortComponents(self): + self.components = sorted(self.components, key=lambda c: natural_sort(c.getRef())) + + #update a given field, based on some rules and such + def updateField(self, field, fieldData): + + if field in CSV_PROTECTED: return + + if (field == None or field == ""): return + elif fieldData == "" or fieldData == None: + return + elif (not field in self.fields.keys()) or (self.fields[field] == None) or (self.fields[field] == ""): + self.fields[field] = fieldData + elif fieldData.lower() in self.fields[field].lower(): + return + else: + print("Conflict:",self.fields[field],",",fieldData) + self.fields[field] += " " + fieldData + + def updateFields(self): + + for f in CSV_DEFAULT: + + #get info from each field + for c in self.components: + + self.updateField(f, c.getField(f)) + + #update 'global' fields + self.fields["Reference"] = self.getRefs() + + self.fields["Quantity"] = self.getCount() + + self.fields["Value"] = self.components[0].getValue() + + self.fields["Part"] = self.components[0].getPartName() + + self.fields["Description"] = self.components[0].getDescription() + + self.fields["Datasheet"] = self.components[0].getDatasheet() + + self.fields["Footprint"] = self.components[0].getFootprint().split(":")[-1] + + #return a dict of the CSV data based on the supplied columns + def getCSVRow(self, columns): + row = [self.getCSVField(key) for key in columns] + return row + + #return a dict of the KiCAD data based on the supplied columns + def getKicadRow(self, columns): + row = [self.getField(key) for key in columns] + #print(row) + return row + + #return a dict of harmonized data based on the supplied columns + def getHarmonizedRow(self,columns): + return [self.getHarmonizedField(key) for key in columns] \ No newline at end of file diff --git a/KiBOM/netlist_reader.py b/KiBOM/netlist_reader.py new file mode 100644 index 00000000..3a73f93e --- /dev/null +++ b/KiBOM/netlist_reader.py @@ -0,0 +1,592 @@ +# +# KiCad python module for interpreting generic netlists which can be used +# to generate Bills of materials, etc. +# +# 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 + Generate a HTML BOM list. + Components are sorted and grouped by value + Any existing fields are read +""" + + +from __future__ import print_function +import sys +import xml.sax as sax +import re +import pdb + +from component import Component, ComponentGroup +from sort import natural_sort + +#--------------------------------------------------------------------- + +# 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' + ] + +# When comparing part names, components will match if they are both elements of the +# same set defined here +ALIASES = [ + ["c", "c_small", "cap", "capacitor"], + ["r", "r_small", "res", "resistor"], + ["sw", "switch"], + ["l, l_small", "inductor"] + ] + +DNF = ["dnf", "do not fit", "nofit", "no stuff", "nostuff", "noload", "do not load"] + +#-------------------------------------------------------------------- + + + +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 += "" + + 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 += "\n" + + for c in self.children: + s += c.formatHTML(True) + + if not amChild: + s += """
" + self.name + "
" + self.chars + "
    " + for a in self.attributes: + s += "
  • " + a + " = " + self.attributes[a] + "
  • " + + s += "
+ + """ + + return s + + def addAttribute(self, attr, value): + """Add an attribute to this element""" + 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: + return self.chars + else: + return self.attributes[attribute] + except AttributeError: + return "" + else: + return self.chars + + for child in self.children: + ret = child.get(elemName, attribute, attrmatch) + if ret != "": + return ret + + return "" + +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 getDocs(self): + return self.element.get("docs") + + 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): + + datasheet = self.getField("Datasheet") + + if not datasheet or datasheet == "": + docs = self.getDocs() + + if "http" in docs or ".pdf" in docs: + datasheet = docs + + return 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 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(Component(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(): + 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 getSheet(self): + return self.design.getChild("sheet") + + def getVersion(self): + """Return the verison of the sheet info""" + sheet = self.getSheet() + if sheet == None: return "" + return sheet.get("rev") + + 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) + + # Sort first by ref as this makes for easier to read BOM's + ret.sort(key=lambda g: 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. + + ALSO THE FOOTPRINTS MUST MATCH YOU DINGBAT + + 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 = [] + + """ + Iterate through each component, and test whether a group for these already exists + """ + for c in components: + found = False + + for g in groups: + if g.matchComponent(c): + g.addComponent(c) + found = True + break + + if not found: + g = ComponentGroup() + g.addComponent(c) + groups.append(g) + + #sort the references within each group + for g in groups: + g.sortComponents() + g.updateFields() + + #sort the groups + #first priority is the Type of component (e.g. R, U, + groups = sorted(groups, key=lambda g: [g.components[0].getPrefix(), g.components[0].getValue()]) + + return groups + + 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() diff --git a/KiBOM/sort.py b/KiBOM/sort.py new file mode 100644 index 00000000..7cd6718f --- /dev/null +++ b/KiBOM/sort.py @@ -0,0 +1,3 @@ +#'better' sorting function which sorts by NUMERICAL value not ASCII +def natural_sort(string): + return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)',string)] \ No newline at end of file diff --git a/KiBOM/units.py b/KiBOM/units.py new file mode 100644 index 00000000..b8f63c45 --- /dev/null +++ b/KiBOM/units.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +""" + +This file contains a set of functions for matching values which may be written in different formats +e.g. +0.1uF = 100n (different suffix specified, one has missing unit) +0R1 = 0.1Ohm (Unit replaces decimal, different units) + +""" + +import re + +PREFIX_MICRO = [u"μ","u","micro"] +PREFIX_MILLI = ["milli","m"] +PREFIX_NANO = ["nano","n"] +PREFIX_PICO = ["pico","p"] +PREFIX_KILO = ["kilo","k"] +PREFIX_MEGA = ["mega","meg"] +PREFIX_GIGA = ["giga","g"] + +#All prefices +PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA + +#Common methods of expressing component units +UNIT_R = ["r","ohms","ohm",u"Ω"] +UNIT_C = ["farad","f"] +UNIT_L = ["henry","h"] + +UNIT_ALL = UNIT_R + UNIT_C + UNIT_L + +""" +Return a simplified version of a units string, for comparison purposes +""" +def getUnit(unit): + + if not unit: return None + + unit = unit.lower() + + if unit in UNIT_R: return "R" + if unit in UNIT_C: return "F" + if unit in UNIT_L: return "H" + + return None + +""" +Return the (numerical) value of a given prefix +""" +def getPrefix(prefix): + + if not prefix: return 1 + + prefix = prefix.lower() + + if prefix in PREFIX_PICO: return 1.0e-12 + if prefix in PREFIX_NANO: return 1.0e-9 + if prefix in PREFIX_MICRO: return 1.0e-6 + if prefix in PREFIX_MILLI: return 1.0e-3 + if prefix in PREFIX_KILO: return 1.0e3 + if prefix in PREFIX_MEGA: return 1.0e6 + if prefix in PREFIX_GIGA: return 1.0e9 + + return 1 + +def groupString(group): #return a reg-ex string for a list of values + return "|".join(group) + +def matchString(): + return "^([0-9\.]+)(" + groupString(PREFIX_ALL) + ")*(" + groupString(UNIT_ALL) + ")*(\d*)$" + + +""" +Return a normalized value and units for a given component value string +e.g. compMatch("10R2") returns (10, R) +e.g. compMatch("3.3mOhm") returns (0.0033, R) +""" +def compMatch(component): + + #remove any commas + component = component.strip().replace(",","").lower() + + match = matchString() + + result = re.search(match, component) + + if not result: return None + + if not len(result.groups()) == 4: return None + + value,prefix,units,post = result.groups() + + #special case where units is in the middle of the string + #e.g. "0R05" for 0.05Ohm + #in this case, we will NOT have a decimal + #we will also have a trailing number + + if post and "." not in value: + try: + value = float(int(value)) + postValue = float(int(post)) / (10 ** len(post)) + value = value * 1.0 + postValue + except: + return None + + try: + val = float(value) + except: + return None + + val = "{0:.15f}".format(val * 1.0 * getPrefix(prefix)) + + return (val, getUnit(units)) + +def componentValue(valString): + + result = compMatch(valString) + + if not result: + return valString #return the same string back + + if not len(result) == 2: #result length is incorrect + return valString + + (val, unit) = result + + return val + +#compare two values +def compareValues(c1, c2): + r1 = compMatch(c1) + r2 = compMatch(c2) + + if not r1 or not r2: return False + + (v1, u1) = r1 + (v2, u2) = r2 + + if v1 == v2: + #values match + if u1 == u2: return True #units match + if not u1: return True #no units for component 1 + if not u2: return True #no units for component 2 + + return False + diff --git a/KiBOM_GUI.py b/KiBOM_GUI.py new file mode 100644 index 00000000..1b596fc3 --- /dev/null +++ b/KiBOM_GUI.py @@ -0,0 +1,87 @@ +import sys +import os + +import wx + +import wx.grid + +def Debug(*arg): + pass + +sys.path.append(os.path.dirname(sys.argv[0])) + +from KiBOM.columns import Columns + +#import bomfunk_netlist_reader + +class KiBOMTable(wx.grid.Grid): + def __init__(self, parent): + wx.grid.Grid.__init__(self,parent) + + #Setup default columns + self.SetupColumns(Columns._COLUMNS_DEFAULT) + + #configure column headings + def SetupColumns(self, columns): + + self.CreateGrid(0, len(columns)) + + for i,h in enumerate(columns): + self.SetColLabelValue(i,h) + +class KiBOMFrame(wx.Frame): + + def __init__(self, parent, title): + wx.Frame.__init__(self, parent,title=title) + + self.panel = wx.Panel(self) + + self.table = KiBOMTable(self.panel) + + #Vertical sizer that separates the "export options" (lower) from the main table and selectors + self.hSizer = wx.BoxSizer(wx.HORIZONTAL) + + #vertical sizer for srstoring the component selection options + self.optSizer = wx.BoxSizer(wx.VERTICAL) + + #options + self.showHideSizer = wx.BoxSizer(wx.VERTICAL) + + #add grouping option + self.groupOption = wx.CheckBox(self.panel, label="Group Components") + self.showHideSizer.Add(self.groupOption) + + self.optSizer.Add(self.showHideSizer) + + self.hSizer.Add(self.optSizer) + self.hSizer.Add(self.table, 1, wx.EXPAND) + + self.panel.SetSizer(self.hSizer) + + self.AddMenuBar() + + self.Show(True) + + def AddMenuBar(self): + #add a menu + filemenu = wx.Menu() + + menuExit = filemenu.Append(wx.ID_EXIT, "E&xit"," Exit the BoM Manager") + + menuBar = wx.MenuBar() + + menuBar.Append(filemenu,"&File") + self.SetMenuBar(menuBar) + + self.Bind(wx.EVT_MENU, self.OnExit, menuExit) + + def OnExit(self, e): + self.Close(True) + +Debug("starting") + +app = wx.App(False) + +frame = KiBOMFrame(None,"KiBoM") + +app.MainLoop() \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..90b1d514 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ + +import sys +sys.path.append(".") \ No newline at end of file