├── README.md ├── .gitattributes ├── .gitignore ├── bom_single_line_r31.py ├── bom_grouped_v33.py ├── bom_single_line_r31_ky.py └── bom_grouped_v33_ky.py /README.md: -------------------------------------------------------------------------------- 1 | @package 2 | Generate a comma delimited list (csv file type). 3 | 4 | Components are sorted by ref and grouped by: 5 | Value Footprint Voltage Tolerance Temp Current 6 | Fields are (if exist) 7 | 'Ref', 'Qnty', 'Value', 'Footprint', 8 | 'Manf#', 'Voltage', 'Tolerance', 'Power', 'Temp', 'Current', 'Description' 9 | version 3.3 - 06.2016 [bom_grouped_v33_ky.py] 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /bom_single_line_r31.py: -------------------------------------------------------------------------------- 1 | # 2 | # Example python script to generate a BOM from a KiCad generic netlist 3 | # 4 | # Example: Sorted and Grouped CSV BOM 5 | # grouped Part Value, Footprint, Voltage, Toll 6 | # 7 | 8 | # Import the KiCad python helper module and the csv formatter 9 | 10 | 11 | """ 12 | @package 13 | Generate a comma delimited list (csv file type). 14 | Components are sorted by ref in signle line 15 | Fields are (if exist) 16 | 'Ref', 'Qnty', 'Value', 'Footprint', 17 | 'Description', 'Voltage', 'Tolerance', 'Power', 'Temp', 'Current' 18 | version 31 - 05.2015 [bom_single_line_r31_ky.py] 19 | """ 20 | 21 | #maui 22 | import logging 23 | import textwrap 24 | #maui 25 | import bom_single_line_r31_ky 26 | import shutil 27 | import os 28 | import ntpath 29 | 30 | import csv 31 | import sys 32 | 33 | # logging.basicConfig(filename='example.log',level=logging.DEBUG) 34 | 35 | # Generate an instance of a generic netlist, and load the netlist tree from 36 | # the command line option. If the file doesn't exist, execution will stop 37 | #maui 38 | net = bom_single_line_r31_ky.netlist(sys.argv[1]) 39 | 40 | # Open a file to write to, if the file cannot be opened output to stdout 41 | # instead 42 | try: 43 | f = open(sys.argv[2], 'wb') 44 | except IOError: 45 | print >> sys.stderr, __file__, ":", e 46 | f = stdout 47 | 48 | # Create a new csv writer object to use as the output formatter 49 | #out = csv.writer(f, delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) 50 | #maui 51 | out = csv.writer(f, delimiter=',', quotechar='\"', escapechar='', quoting=csv.QUOTE_NONE) 52 | 53 | # Output a set of rows for a header providing general information 54 | #maui 55 | out.writerow(['In:', sys.argv[1]]) 56 | 57 | out.writerow(['Source:', net.getSource()]) 58 | out.writerow(['Date:', net.getDate()]) 59 | out.writerow(['Tool:', net.getTool()]) 60 | out.writerow(['Component Count:', len(net.components)]) 61 | out.writerow(['Ref', 'Qnty', 'Value', 'Footprint', 'Description', 'Voltage', 'Tolerance', 'Power', 'Temp', 'Current']) 62 | 63 | 64 | #logging.debug('This message should go to the log file') 65 | #logging.info('So should this') 66 | #logging.warning('And this, too') 67 | 68 | # Get all of the components in groups of matching parts + values (see ky.py) 69 | grouped = net.groupComponents() 70 | 71 | # Output all of the component information 72 | for group in grouped: 73 | refs = "" 74 | 75 | # Add the reference of every component in the group and keep a reference 76 | # to the component so that the other data can be filled in once per group 77 | for component in group: 78 | #refs += component.getRef() + ", " 79 | refs += component.getRef() 80 | c = component 81 | 82 | #maui 83 | #logging.warning(c.getFootprint()) 84 | 85 | # Fill in the component groups common data 86 | # out.writerow([refs, len(group), c.getValue(), c.getLib() + "/" + c.getPart(), c.getDatasheet(), 87 | # c.getDescription(), c.getField("Vendor")]) 88 | #out.writerow([refs, len(group), c.getValue(), c.getLib() + "/" + c.getFootprint(), 89 | # out.writerow([refs, c.getValue(), c.getLib() + "/" + c.getFootprint(), 90 | # c.getDescription(), c.getField("Voltage"), c.getField("Toll")]) 91 | 92 | # out.writerow([refs, c.getValue(), c.getFootprint(), 93 | # c.getDescription(), c.getField("Voltage"), c.getField("Tolerance"), c.getField("Temp")]) 94 | 95 | out.writerow([refs, len(group), c.getValue(), c.getFootprint(), 96 | c.getDescription(), c.getField('Voltage'), c.getField('Tolerance'), c.getField('Power'), c.getField('Temp'), c.getField('Current')]) 97 | 98 | #maui 99 | #os.path.splitext(sys.argv[2])[0] 100 | f.close() 101 | #csv_file=ntpath.basename(sys.argv[2]).split('.')[0]+'.csv' 102 | csv_file=os.path.splitext(sys.argv[2])[0]+'.csv' 103 | 104 | shutil.copy (sys.argv[2], csv_file) 105 | #print sys.argv[2], "=>", ntpath.basename(sys.argv[2]).split('.')[0]+".csv 106 | 107 | res = [] 108 | max_len_element = [0,0,0,0,0,0,0,0,0,0,0] 109 | 110 | with open(csv_file) as csvfile: 111 | file1 = csv.reader(csvfile, delimiter=',') 112 | line='' 113 | p=0 114 | for row in file1: 115 | k=0 116 | for element in row: 117 | # print (len(element)) 118 | len_element=len(element) 119 | k=k+1 120 | # print k 121 | if p>4: 122 | if len_element <50: 123 | if len_element > max_len_element[k]: 124 | max_len_element[k]=len_element 125 | else: 126 | max_len_element[k]=50 127 | p=p+1 128 | p=0 129 | 130 | with open(csv_file) as csvfile: 131 | file2 = csv.reader(csvfile, delimiter=',') 132 | line='' 133 | p=0 134 | for row in file2: 135 | k=0 136 | for element in row: 137 | # print (len(element)) 138 | len_element=len(element) 139 | k=k+1 140 | # print k 141 | #dif = 30- len_element 142 | dif=0 143 | if p>4: 144 | if len_element >= 50: 145 | wrapped = textwrap.wrap(element, 50) 146 | # for index, item in enumerate(wrapped): 147 | # print index, item 148 | # print item[0] 149 | for line1 in wrapped: 150 | #print line1 151 | line +=line1+'\r\n' 152 | line=line.rstrip('\n'); 153 | line=line.rstrip('\r'); 154 | index = line.find(line1) 155 | line_space='' 156 | for index2 in range(k-1): 157 | line_space+=(max_len_element[k-1]+3)*' ' 158 | line_space=line_space[:len(line_space)-3] 159 | line = line[:index] + line_space + line[index:] 160 | dif = 50-len(line1)+3 161 | line+=','.ljust(dif,' ') 162 | #dif = max_len_element[k]-50+3 163 | #line += element+ ','.ljust(dif,' ')+'\r\n' 164 | else: 165 | dif = max_len_element[k]-len_element+3 166 | line += element+ ','.ljust(dif,' ') 167 | else: 168 | line += element 169 | line += '\r\n' 170 | p=p+1 171 | res.append(line) 172 | 173 | txt_file=os.path.splitext(sys.argv[2])[0]+'.txt' 174 | # now, outside the loop, we can do this: 175 | with open(txt_file, 'wb') as f: 176 | f.writelines(res) 177 | 178 | -------------------------------------------------------------------------------- /bom_grouped_v33.py: -------------------------------------------------------------------------------- 1 | # 2 | # Example python script to generate a BOM from a KiCad generic netlist 3 | # 4 | # Example: Sorted and Grouped CSV BOM 5 | # grouped Part Value, Footprint, Voltage, Toll 6 | # 7 | 8 | # Import the KiCad python helper module and the csv formatter 9 | 10 | """ 11 | @package 12 | Generate a comma delimited list (csv file type). 13 | Components are sorted by ref and grouped by: 14 | Value Footprint Voltage Tolerance Temp Current 15 | Fields are (if exist) 16 | 'Ref', 'Qnty', 'Value', 'Footprint', 17 | 'Manf#', 'Voltage', 'Tolerance', 'Power', 'Temp', 'Current', 'Description' 18 | version 3.3 - 06.2016 [bom_grouped_v33_ky.py] 19 | """ 20 | 21 | #maui 22 | import logging 23 | import textwrap 24 | #maui 25 | import bom_grouped_v33_ky 26 | import shutil 27 | import os 28 | import ntpath 29 | 30 | import csv 31 | import sys 32 | 33 | 34 | # logging.basicConfig(filename='example.log',level=logging.DEBUG) 35 | 36 | # Generate an instance of a generic netlist, and load the netlist tree from 37 | # the command line option. If the file doesn't exist, execution will stop 38 | #maui 39 | net = bom_grouped_v33_ky.netlist(sys.argv[1]) 40 | 41 | # Open a file to write to, if the file cannot be opened output to stdout 42 | # instead 43 | try: 44 | f = open(sys.argv[2], 'wb') 45 | except IOError: 46 | print >> sys.stderr, __file__, ":", e 47 | f = stdout 48 | 49 | # Create a new csv writer object to use as the output formatter 50 | #out = csv.writer(f, delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL) 51 | #maui 52 | out = csv.writer(f, delimiter=',', quotechar='\"') 53 | #out = csv.writer(f, delimiter=',', quotechar='\"', escapechar='', quoting=csv.QUOTE_ALL) 54 | #out = csv.writer(f, delimiter='\t', quotechar='\"', escapechar='', quoting=csv.QUOTE_NONE) 55 | 56 | # Output a set of rows for a header providing general information 57 | #maui 58 | out.writerow(['In:', sys.argv[1]]) 59 | 60 | out.writerow(['Source:', net.getSource()]) 61 | out.writerow(['Date:', net.getDate()]) 62 | out.writerow(['Tool:', net.getTool()]) 63 | out.writerow(['Component Count:', len(net.components)]) 64 | out.writerow(['Ref', 'Qnty', 'Value', 'Footprint', 'Manf#', 'Voltage', 'Tolerance', 'Power', 'Temp', 'Current', 'Description']) 65 | 66 | #logging.debug('This message should go to the log file') 67 | #logging.info('So should this') 68 | #logging.warning('And this, too') 69 | 70 | # Get all of the components in groups of matching parts + values (see ky.py) 71 | grouped = net.groupComponents() 72 | 73 | # Output all of the component information 74 | for group in grouped: 75 | refs = "" 76 | 77 | # Add the reference of every component in the group and keep a reference 78 | # to the component so that the other data can be filled in once per group 79 | for component in group: 80 | #refs += component.getRef() + ", " 81 | if len(group)==1: 82 | refs += component.getRef() 83 | else : 84 | refs += component.getRef()+ " " 85 | c = component 86 | #maui 87 | refs=refs.rstrip(' '); 88 | #logging.warning(c.getFootprint()) 89 | 90 | # Fill in the component groups common data 91 | # out.writerow([refs, len(group), c.getValue(), c.getLib() + "/" + c.getPart(), c.getDatasheet(), 92 | # c.getDescription(), c.getField("Vendor")]) 93 | #out.writerow([refs, len(group), c.getValue(), c.getLib() + "/" + c.getFootprint(), 94 | # c.getDescription(), c.getField("Voltage"), c.getField("Toll")]) 95 | 96 | out.writerow([refs, len(group), c.getValue(), c.getFootprint(), 97 | c.getField('Manf#'), c.getField('Voltage'), c.getField('Tolerance'), c.getField('Power'), c.getField('Temp'), c.getField('Current'), c.getDescription()]) 98 | 99 | # out.writerow([("{0:30s}".format(c.getField("Value")), "{0:30s}".format(c.getField("Footprint")) )] ) 100 | 101 | #maui 102 | #os.path.splitext(sys.argv[2])[0] 103 | f.close() 104 | #csv_file=ntpath.basename(sys.argv[2]).split('.')[0]+'.csv' 105 | csv_file=os.path.splitext(sys.argv[2])[0]+'.csv' 106 | 107 | shutil.copy (sys.argv[2], csv_file) 108 | #print sys.argv[2], "=>", ntpath.basename(sys.argv[2]).split('.')[0]+".csv 109 | 110 | res = [] 111 | max_len_element = [0,0,0,0,0,0,0,0,0,0,0,0] 112 | 113 | with open(csv_file) as csvfile: 114 | file1 = csv.reader(csvfile, delimiter=',') 115 | line='' 116 | p=0 117 | for row in file1: 118 | k=0 119 | for element in row: 120 | # print (len(element)) 121 | len_element=len(element) 122 | k=k+1 123 | # print k 124 | if p>4: 125 | if len_element <50: 126 | if len_element > max_len_element[k]: 127 | max_len_element[k]=len_element 128 | else: 129 | max_len_element[k]=50 130 | p=p+1 131 | p=0 132 | 133 | with open(csv_file) as csvfile: 134 | file2 = csv.reader(csvfile, delimiter=',') 135 | line='' 136 | p=0 137 | for row in file2: 138 | k=0 139 | for element in row: 140 | # print (len(element)) 141 | len_element=len(element) 142 | k=k+1 143 | # print k 144 | #dif = 30- len_element 145 | dif=0 146 | if p>4: 147 | if len_element >= 50: 148 | wrapped = textwrap.wrap(element, 50) 149 | # for index, item in enumerate(wrapped): 150 | # print index, item 151 | # print item[0] 152 | for line1 in wrapped: 153 | #print line1 154 | line +=line1+'\r\n' 155 | line=line.rstrip('\n'); 156 | line=line.rstrip('\r'); 157 | index = line.find(line1) 158 | line_space='' 159 | for index2 in range(k-1): 160 | line_space+=(max_len_element[k-1]+3)*' ' 161 | line_space=line_space[:len(line_space)-3] 162 | line = line[:index] + line_space + line[index:] 163 | dif = 50-len(line1)+3 164 | line+=','.ljust(dif,' ') 165 | #dif = max_len_element[k]-50+3 166 | #line += element+ ','.ljust(dif,' ')+'\r\n' 167 | else: 168 | dif = max_len_element[k]-len_element+3 169 | line += element+ ','.ljust(dif,' ') 170 | else: 171 | line += element 172 | line += '\r\n' 173 | p=p+1 174 | res.append(line) 175 | 176 | txt_file=os.path.splitext(sys.argv[2])[0]+'.txt' 177 | # now, outside the loop, we can do this: 178 | with open(txt_file, 'wb') as f: 179 | f.writelines(res) 180 | 181 | -------------------------------------------------------------------------------- /bom_single_line_r31_ky.py: -------------------------------------------------------------------------------- 1 | # 2 | # KiCad python module for interpreting generic netlists which can be used 3 | # to generate Bills of materials, etc. 4 | # 5 | # No string formatting is used on purpose as the only string formatting that 6 | # is current compatible with python 2.4+ to 3.0+ is the '%' method, and that 7 | # is due to be deprecated in 3.0+ soon 8 | # 9 | 10 | """ 11 | @package 12 | children of 'bom_single_line_r31.py' 13 | version 31 - 05.2015 [bom_single_line_r31_ky.py] 14 | """ 15 | 16 | import sys 17 | import xml.sax as sax 18 | #maui 19 | import logging 20 | 21 | class component(): 22 | """Class for a set of component information""" 23 | def __init__(self, element): 24 | self.element = element 25 | self.libpart = None 26 | 27 | # Set to true when this component is included in a component group 28 | self.grouped = False 29 | 30 | def __eq__(self, other): 31 | """Equlivalency operator, remember this can be easily overloaded""" 32 | result = False 33 | #if self.getValue() == other.getValue(): 34 | # if self.getLib() == other.getLib(): 35 | # if self.getPart() == other.getPart(): 36 | # if self.getFootprint() == other.getFootprint(): 37 | # #logging.warning('%s 1', self.getFootprint()); 38 | # #logging.warning('%s 2', other.getFootprint()); 39 | # #logging.warning('%s 2', self.getField('Voltage')); 40 | # if self.getField('Voltage') == other.getField('Voltage'): #maui test 41 | # if self.getField('Toll') == other.getField('Toll'): #maui test 42 | # result = False 43 | return result 44 | 45 | def setPart(self, part): 46 | self.libpart = part 47 | 48 | def setValue(self, value): 49 | """Set the value of this component""" 50 | v = self.element.getChild("value") 51 | if v: 52 | v.setChars(value) 53 | 54 | def getValue(self): 55 | return self.element.get("value") 56 | 57 | def getRef(self): 58 | return self.element.get("comp", "ref") 59 | 60 | def getFootprint(self): 61 | return self.element.get("footprint") 62 | 63 | def getDatasheet(self): 64 | return self.element.get("datasheet") 65 | 66 | def getLib(self): 67 | return self.element.get("libsource", "lib") 68 | 69 | def getPart(self): 70 | return self.element.get("libsource", "part") 71 | 72 | def getTimestamp(self): 73 | return self.element.get("tstamp") 74 | 75 | def getDescription(self): 76 | # When attempting to access the part, we must take care in case the part 77 | # cannot be found in the netlist 78 | try: 79 | d = self.libpart.getDescription() 80 | except AttributeError: 81 | d = "" 82 | return d 83 | 84 | def getDatasheet(self): 85 | # When attempting to access the part, we must take care in case the part 86 | # cannot be found in the netlist 87 | try: 88 | d = self.libpart.getDatasheet() 89 | except AttributeError: 90 | d = "" 91 | return d 92 | 93 | def getField(self, name): 94 | """Return the value of a field named name. The component is first 95 | checked for the field, and then the components library part is checked 96 | for the field. If the field doesn't exist in either, an empty string is 97 | returned 98 | 99 | Keywords: 100 | name -- The name of the field to return the value for 101 | 102 | """ 103 | field = self.element.get("field", "name", name).encode('utf-8') 104 | if field == "": 105 | try: 106 | field = self.libpart.getField(name) 107 | except AttributeError: 108 | field = "" 109 | return field 110 | 111 | 112 | class netlistElement(): 113 | """Generic netlist element. All elements for a netlist tree which can be 114 | used to easily generate various output formats by propogating format 115 | requests to all children 116 | """ 117 | def __init__(self, name, parent=None): 118 | self.name = name 119 | self.attributes = {} 120 | self.parent = parent 121 | self.chars = "" 122 | self.children = [] 123 | self.indent = "" 124 | 125 | def __str__(self): 126 | """String representation of this netlist element 127 | 128 | """ 129 | return (self.name + "[" + self.chars + "]" + " attr:" + 130 | str(len(self.attributes[a]))) 131 | 132 | def formatXML(self, amChild=False): 133 | """Return this element formatted as XML 134 | 135 | Keywords: 136 | amChild -- If set to True, the start of document is not returned 137 | 138 | """ 139 | s = "" 140 | 141 | if not amChild: 142 | s = "\n" 143 | 144 | s += self.indent + "<" + self.name 145 | for a in self.attributes: 146 | s += " " + a + "=\"" + self.attributes[a] + "\"" 147 | 148 | if (len(self.chars) == 0) and (len(self.children) == 0): 149 | s += "/>" 150 | else: 151 | s += ">" + self.chars 152 | 153 | for c in self.children: 154 | c.indent += self.indent + " " 155 | s += "\n" 156 | s += c.formatXML(True) 157 | 158 | if (len(self.children) > 0): 159 | s += "\n" + self.indent 160 | 161 | if (len(self.children) > 0) or (len(self.chars) > 0): 162 | s += "" 163 | 164 | return s 165 | 166 | def formatHTML(self, amChild=False): 167 | """Return this element formatted as HTML 168 | 169 | Keywords: 170 | amChild -- If set to True, the start of document is not returned 171 | 172 | """ 173 | s = "" 174 | 175 | if not amChild: 176 | s = """ 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | """ 186 | 187 | s += "\n" 192 | 193 | for c in self.children: 194 | s += c.formatHTML(True) 195 | 196 | if not amChild: 197 | s += """
" + self.name + "
" + self.chars + "
    " 188 | for a in self.attributes: 189 | s += "
  • " + a + " = " + self.attributes[a] + "
  • " 190 | 191 | s += "
198 | 199 | """ 200 | 201 | return s 202 | 203 | def addAttribute(self, attr, value): 204 | """Add an attribute to this element""" 205 | self.attributes[attr] = value 206 | 207 | def setChars(self, chars): 208 | """Set the characters for this element""" 209 | self.chars = chars 210 | 211 | def addChars(self, chars): 212 | """Add characters (textual value) to this element""" 213 | self.chars += chars 214 | 215 | def addChild(self, child): 216 | """Add a child element to this element""" 217 | self.children.append(child) 218 | return self.children[len(self.children) - 1] 219 | 220 | def getParent(self): 221 | """Get the parent of this element (Could be None)""" 222 | return self.parent 223 | 224 | def setAttribute(self, attr, value): 225 | """Set an attributes value - in fact does the same thing as add 226 | attribute 227 | 228 | """ 229 | self.attributes[attr] = value 230 | 231 | def getChild(self, name): 232 | """Returns a child element of name 233 | 234 | Keywords: 235 | name -- The name of the child element to return""" 236 | for child in self.children: 237 | if child.name == name: 238 | return child 239 | 240 | return None 241 | 242 | def get(self, element, attribute="", attrmatch=""): 243 | """Return the data for either an attribute, or else an element""" 244 | if (self.name == element): 245 | if attribute != "": 246 | if attrmatch != "": 247 | if self.attributes[attribute] == attrmatch: 248 | return self.chars 249 | else: 250 | return self.attributes[attribute] 251 | else: 252 | return self.chars 253 | 254 | for child in self.children: 255 | if child.get(element, attribute, attrmatch) != "": 256 | return child.get(element, attribute, attrmatch) 257 | 258 | return "" 259 | 260 | 261 | class netlist(): 262 | """ Kicad generic netlist class. Generally loaded from a kicad generic 263 | netlist file. Includes several helper functions to ease BOM creating 264 | scripts 265 | 266 | """ 267 | def __init__(self, fname=""): 268 | """Initialiser for the genericNetlist class 269 | 270 | Keywords: 271 | fname -- The name of the generic netlist file to open (Optional) 272 | 273 | """ 274 | self.design = None 275 | self.components = [] 276 | self.libparts = [] 277 | self.libraries = [] 278 | self.nets = [] 279 | 280 | # The entire tree is loaded into self.tree 281 | self.tree = [] 282 | 283 | self._curr_element = None 284 | 285 | if fname != "": 286 | self.load(fname) 287 | 288 | def addChars(self, content): 289 | """Add characters to the current element""" 290 | self._curr_element.addChars(content) 291 | 292 | def addElement(self, name): 293 | """Add a new kicad generic element to the list""" 294 | if self._curr_element == None: 295 | self.tree = netlistElement(name) 296 | self._curr_element = self.tree 297 | else: 298 | self._curr_element = self._curr_element.addChild( 299 | netlistElement(name, self._curr_element)) 300 | 301 | # If this element is a component, add it to the components list 302 | if self._curr_element.name == "comp": 303 | self.components.append(component(self._curr_element)) 304 | 305 | # Assign the design element 306 | if self._curr_element.name == "design": 307 | self.design = self._curr_element 308 | 309 | # If this element is a library part, add it to the parts list 310 | if self._curr_element.name == "libpart": 311 | self.libparts.append(part(self._curr_element)) 312 | 313 | # If this element is a net, add it to the nets list 314 | if self._curr_element.name == "net": 315 | self.nets.append(self._curr_element) 316 | 317 | # If this element is a library, add it to the libraries list 318 | if self._curr_element.name == "library": 319 | self.libraries.append(self._curr_element) 320 | 321 | return self._curr_element 322 | 323 | def endDocument(self): 324 | """Called when the netlist document has been fully parsed""" 325 | # When the document is complete, the library parts must be linked to 326 | # the components as they are seperate in the tree so as not to 327 | # duplicate library part information for every component 328 | for c in self.components: 329 | for p in self.libparts: 330 | if p.getPart() == c.getPart() and p.getLib() == c.getLib(): 331 | c.setPart(p) 332 | 333 | def endElement(self): 334 | """End the current element and switch to its parent""" 335 | self._curr_element = self._curr_element.getParent() 336 | 337 | def getDate(self): 338 | """Return the date + time string generated by the tree creation tool""" 339 | return self.design.get("date") 340 | 341 | def getSource(self): 342 | """Return the source string for the design""" 343 | return self.design.get("source") 344 | 345 | def getTool(self): 346 | """Return the tool string which was used to create the netlist tree""" 347 | return self.design.get("tool") 348 | 349 | def groupComponents(self): 350 | """Return a list of component lists. Components are grouped together 351 | when the value, library and part identifiers match 352 | 353 | """ 354 | groups = [] 355 | 356 | # Make sure to start off will all components ungrouped to begin with 357 | for c in self.components: 358 | c.grouped = False 359 | 360 | # Group components based on the value, library and part identifiers 361 | for c in self.components: 362 | if c.grouped == False: 363 | c.grouped = True 364 | newgroup = [] 365 | newgroup.append(c) 366 | 367 | # Check every other ungrouped component against this component 368 | # and add to the group as necessary 369 | for ci in self.components: 370 | if ci.grouped == False and ci == c: 371 | newgroup.append(ci) 372 | ci.grouped = True 373 | 374 | # Add the new component group to the groups list 375 | groups.append(newgroup) 376 | 377 | # Each group is a list of components, we need to sort each list first 378 | # to get them in order as this makes for easier to read BOM's 379 | for g in groups: 380 | g = sorted(g, key=lambda g: g.getRef()) 381 | 382 | # Finally, sort the groups to order the references alphabetically 383 | groups = sorted(groups, key=lambda group: group[0].getRef()) 384 | 385 | return groups 386 | 387 | def formatXML(self): 388 | """Return the whole netlist formatted in XML""" 389 | return self.tree.formatXML() 390 | 391 | def formatHTML(self): 392 | """Return the whole netlist formatted in HTML""" 393 | return self.tree.formatHTML() 394 | 395 | def load(self, fname): 396 | """Load a kicad generic netlist 397 | 398 | Keywords: 399 | fname -- The name of the generic netlist file to open 400 | 401 | """ 402 | try: 403 | self._reader = sax.make_parser() 404 | self._reader.setContentHandler(_gNetReader(self)) 405 | self._reader.parse(fname) 406 | except IOError as e: 407 | print >> sys.stderr, __file__, ":", e 408 | sys.exit(-1) 409 | 410 | 411 | class part(): 412 | """Class for a library part""" 413 | def __init__(self, part): 414 | # The part is a reference to a libpart generic netlist element 415 | self.element = part 416 | 417 | def __str__(self): 418 | # simply print the generic netlist element associated with this part 419 | return str(self.element) 420 | 421 | def getDatasheet(self): 422 | return self.element.get("docs") 423 | 424 | def getLib(self): 425 | return self.element.get("libpart", "lib") 426 | 427 | def getPart(self): 428 | return self.element.get("libpart", "part") 429 | 430 | def getDescription(self): 431 | return self.element.get("description") 432 | 433 | def getField(self, name): 434 | return self.element.get("field", "name", name) 435 | 436 | 437 | class _gNetReader(sax.handler.ContentHandler): 438 | """SAX kicad generic netlist content handler - passes most of the work back 439 | to the gNetlist class which builds a complete tree in RAM for the design 440 | 441 | """ 442 | def __init__(self, aParent): 443 | self.parent = aParent 444 | 445 | def startElement(self, name, attrs): 446 | """Start of a new XML element event""" 447 | element = self.parent.addElement(name) 448 | 449 | for name in attrs.getNames(): 450 | element.addAttribute(name, attrs.getValue(name)) 451 | 452 | def endElement(self, name): 453 | self.parent.endElement() 454 | 455 | def characters(self, content): 456 | # Ignore erroneous white space - ignoreableWhitespace does not get rid 457 | # of the need for this! 458 | if not content.isspace(): 459 | self.parent.addChars(content) 460 | 461 | def endDocument(self): 462 | """End of the XML document event""" 463 | self.parent.endDocument() 464 | -------------------------------------------------------------------------------- /bom_grouped_v33_ky.py: -------------------------------------------------------------------------------- 1 | # 2 | # KiCad python module for interpreting generic netlists which can be used 3 | # to generate Bills of materials, etc. 4 | # 5 | # No string formatting is used on purpose as the only string formatting that 6 | # is current compatible with python 2.4+ to 3.0+ is the '%' method, and that 7 | # is due to be deprecated in 3.0+ soon 8 | # 9 | 10 | 11 | """ 12 | @package 13 | children of 'bom_grouped_v33.py' 14 | version r33 - 06.2016 [bom_grouped_v33_ky.py] 15 | """ 16 | 17 | import sys 18 | import xml.sax as sax 19 | #maui 20 | import logging 21 | 22 | class component(): 23 | """Class for a set of component information""" 24 | def __init__(self, element): 25 | self.element = element 26 | self.libpart = None 27 | 28 | # Set to true when this component is included in a component group 29 | self.grouped = False 30 | 31 | def __eq__(self, other): 32 | """Equlivalency operator, remember this can be easily overloaded""" 33 | result = False 34 | if self.getValue() == other.getValue(): 35 | if self.getLib() == other.getLib(): 36 | if self.getPart() == other.getPart(): 37 | if self.getFootprint() == other.getFootprint(): 38 | #logging.warning('%s 1', self.getFootprint()); 39 | #logging.warning('%s 2', other.getFootprint()); 40 | #logging.warning('%s 2', self.getField('Voltage')); 41 | if self.getField('Manf#') == other.getField('Manf#'): #maui test 42 | if self.getField('Voltage') == other.getField('Voltage'): #maui test 43 | if self.getField('Tolerance') == other.getField('Tolerance'): #maui test 44 | if self.getField('Power') == other.getField('Power'): #maui test 45 | if self.getField('Temp') == other.getField('Temp'): #maui test 46 | if self.getField('Current') == other.getField('Current'): #maui test 47 | result = True 48 | return result 49 | 50 | def setPart(self, part): 51 | self.libpart = part 52 | 53 | def setValue(self, value): 54 | """Set the value of this component""" 55 | v = self.element.getChild("value") 56 | if v: 57 | v.setChars(value) 58 | 59 | def getValue(self): 60 | return self.element.get("value") 61 | 62 | def getRef(self): 63 | return self.element.get("comp", "ref") 64 | 65 | def getFootprint(self): 66 | return self.element.get("footprint") 67 | 68 | def getDatasheet(self): 69 | return self.element.get("datasheet") 70 | 71 | def getLib(self): 72 | return self.element.get("libsource", "lib") 73 | 74 | def getPart(self): 75 | return self.element.get("libsource", "part") 76 | 77 | def getTimestamp(self): 78 | return self.element.get("tstamp") 79 | 80 | def getDescription(self): 81 | # When attempting to access the part, we must take care in case the part 82 | # cannot be found in the netlist 83 | try: 84 | d = self.libpart.getDescription() 85 | except AttributeError: 86 | d = "" 87 | return d 88 | 89 | def getDatasheet(self): 90 | # When attempting to access the part, we must take care in case the part 91 | # cannot be found in the netlist 92 | try: 93 | d = self.libpart.getDatasheet() 94 | except AttributeError: 95 | d = "" 96 | return d 97 | 98 | def getField(self, name): 99 | """Return the value of a field named name. The component is first 100 | checked for the field, and then the components library part is checked 101 | for the field. If the field doesn't exist in either, an empty string is 102 | returned 103 | 104 | Keywords: 105 | name -- The name of the field to return the value for 106 | 107 | """ 108 | # field = self.element.get("field", "name", name).encode('ascii', 'ignore').decode('ascii') 109 | field = self.element.get("field", "name", name).encode('utf-8') 110 | if field == "": 111 | try: 112 | field = self.libpart.getField(name) 113 | except AttributeError: 114 | field = "" 115 | return field 116 | 117 | 118 | class netlistElement(): 119 | """Generic netlist element. All elements for a netlist tree which can be 120 | used to easily generate various output formats by propogating format 121 | requests to all children 122 | """ 123 | def __init__(self, name, parent=None): 124 | self.name = name 125 | self.attributes = {} 126 | self.parent = parent 127 | self.chars = "" 128 | self.children = [] 129 | self.indent = "" 130 | 131 | def __str__(self): 132 | """String representation of this netlist element 133 | 134 | """ 135 | return (self.name + "[" + self.chars + "]" + " attr:" + 136 | str(len(self.attributes[a]))) 137 | 138 | def formatXML(self, amChild=False): 139 | """Return this element formatted as XML 140 | 141 | Keywords: 142 | amChild -- If set to True, the start of document is not returned 143 | 144 | """ 145 | s = "" 146 | 147 | if not amChild: 148 | s = "\n" 149 | 150 | s += self.indent + "<" + self.name 151 | for a in self.attributes: 152 | s += " " + a + "=\"" + self.attributes[a] + "\"" 153 | 154 | if (len(self.chars) == 0) and (len(self.children) == 0): 155 | s += "/>" 156 | else: 157 | s += ">" + self.chars 158 | 159 | for c in self.children: 160 | c.indent += self.indent + " " 161 | s += "\n" 162 | s += c.formatXML(True) 163 | 164 | if (len(self.children) > 0): 165 | s += "\n" + self.indent 166 | 167 | if (len(self.children) > 0) or (len(self.chars) > 0): 168 | s += "" 169 | 170 | return s 171 | 172 | def formatHTML(self, amChild=False): 173 | """Return this element formatted as HTML 174 | 175 | Keywords: 176 | amChild -- If set to True, the start of document is not returned 177 | 178 | """ 179 | s = "" 180 | 181 | if not amChild: 182 | s = """ 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | """ 192 | 193 | s += "\n" 198 | 199 | for c in self.children: 200 | s += c.formatHTML(True) 201 | 202 | if not amChild: 203 | s += """
" + self.name + "
" + self.chars + "
    " 194 | for a in self.attributes: 195 | s += "
  • " + a + " = " + self.attributes[a] + "
  • " 196 | 197 | s += "
204 | 205 | """ 206 | 207 | return s 208 | 209 | def addAttribute(self, attr, value): 210 | """Add an attribute to this element""" 211 | self.attributes[attr] = value 212 | 213 | def setChars(self, chars): 214 | """Set the characters for this element""" 215 | self.chars = chars 216 | 217 | def addChars(self, chars): 218 | """Add characters (textual value) to this element""" 219 | self.chars += chars 220 | 221 | def addChild(self, child): 222 | """Add a child element to this element""" 223 | self.children.append(child) 224 | return self.children[len(self.children) - 1] 225 | 226 | def getParent(self): 227 | """Get the parent of this element (Could be None)""" 228 | return self.parent 229 | 230 | def setAttribute(self, attr, value): 231 | """Set an attributes value - in fact does the same thing as add 232 | attribute 233 | 234 | """ 235 | self.attributes[attr] = value 236 | 237 | def getChild(self, name): 238 | """Returns a child element of name 239 | 240 | Keywords: 241 | name -- The name of the child element to return""" 242 | for child in self.children: 243 | if child.name == name: 244 | return child 245 | 246 | return None 247 | 248 | def get(self, element, attribute="", attrmatch=""): 249 | """Return the data for either an attribute, or else an element""" 250 | if (self.name == element): 251 | if attribute != "": 252 | if attrmatch != "": 253 | if self.attributes[attribute] == attrmatch: 254 | return self.chars 255 | else: 256 | return self.attributes[attribute] 257 | else: 258 | return self.chars 259 | 260 | for child in self.children: 261 | if child.get(element, attribute, attrmatch) != "": 262 | return child.get(element, attribute, attrmatch) 263 | 264 | return "" 265 | 266 | 267 | class netlist(): 268 | """ Kicad generic netlist class. Generally loaded from a kicad generic 269 | netlist file. Includes several helper functions to ease BOM creating 270 | scripts 271 | 272 | """ 273 | def __init__(self, fname=""): 274 | """Initialiser for the genericNetlist class 275 | 276 | Keywords: 277 | fname -- The name of the generic netlist file to open (Optional) 278 | 279 | """ 280 | self.design = None 281 | self.components = [] 282 | self.libparts = [] 283 | self.libraries = [] 284 | self.nets = [] 285 | 286 | # The entire tree is loaded into self.tree 287 | self.tree = [] 288 | 289 | self._curr_element = None 290 | 291 | if fname != "": 292 | self.load(fname) 293 | 294 | def addChars(self, content): 295 | """Add characters to the current element""" 296 | self._curr_element.addChars(content) 297 | 298 | def addElement(self, name): 299 | """Add a new kicad generic element to the list""" 300 | if self._curr_element == None: 301 | self.tree = netlistElement(name) 302 | self._curr_element = self.tree 303 | else: 304 | self._curr_element = self._curr_element.addChild( 305 | netlistElement(name, self._curr_element)) 306 | 307 | # If this element is a component, add it to the components list 308 | if self._curr_element.name == "comp": 309 | self.components.append(component(self._curr_element)) 310 | 311 | # Assign the design element 312 | if self._curr_element.name == "design": 313 | self.design = self._curr_element 314 | 315 | # If this element is a library part, add it to the parts list 316 | if self._curr_element.name == "libpart": 317 | self.libparts.append(part(self._curr_element)) 318 | 319 | # If this element is a net, add it to the nets list 320 | if self._curr_element.name == "net": 321 | self.nets.append(self._curr_element) 322 | 323 | # If this element is a library, add it to the libraries list 324 | if self._curr_element.name == "library": 325 | self.libraries.append(self._curr_element) 326 | 327 | return self._curr_element 328 | 329 | def endDocument(self): 330 | """Called when the netlist document has been fully parsed""" 331 | # When the document is complete, the library parts must be linked to 332 | # the components as they are seperate in the tree so as not to 333 | # duplicate library part information for every component 334 | for c in self.components: 335 | for p in self.libparts: 336 | if p.getPart() == c.getPart() and p.getLib() == c.getLib(): 337 | c.setPart(p) 338 | 339 | def endElement(self): 340 | """End the current element and switch to its parent""" 341 | self._curr_element = self._curr_element.getParent() 342 | 343 | def getDate(self): 344 | """Return the date + time string generated by the tree creation tool""" 345 | return self.design.get("date") 346 | 347 | def getSource(self): 348 | """Return the source string for the design""" 349 | return self.design.get("source") 350 | 351 | def getTool(self): 352 | """Return the tool string which was used to create the netlist tree""" 353 | return self.design.get("tool") 354 | 355 | def groupComponents(self): 356 | """Return a list of component lists. Components are grouped together 357 | when the value, library and part identifiers match 358 | 359 | """ 360 | groups = [] 361 | 362 | # Make sure to start off will all components ungrouped to begin with 363 | for c in self.components: 364 | c.grouped = False 365 | 366 | # Group components based on the value, library and part identifiers 367 | for c in self.components: 368 | if c.grouped == False: 369 | c.grouped = True 370 | newgroup = [] 371 | newgroup.append(c) 372 | 373 | # Check every other ungrouped component against this component 374 | # and add to the group as necessary 375 | for ci in self.components: 376 | if ci.grouped == False and ci == c: 377 | newgroup.append(ci) 378 | ci.grouped = True 379 | 380 | # Add the new component group to the groups list 381 | groups.append(newgroup) 382 | 383 | # Each group is a list of components, we need to sort each list first 384 | # to get them in order as this makes for easier to read BOM's 385 | for g in groups: 386 | g = sorted(g, key=lambda g: g.getRef()) 387 | 388 | # Finally, sort the groups to order the references alphabetically 389 | groups = sorted(groups, key=lambda group: group[0].getRef()) #maui 390 | #groups = sorted(groups, key=lambda group: int(group[0].getRef()[1:])) #maui to complete 391 | return groups 392 | 393 | def formatXML(self): 394 | """Return the whole netlist formatted in XML""" 395 | return self.tree.formatXML() 396 | 397 | def formatHTML(self): 398 | """Return the whole netlist formatted in HTML""" 399 | return self.tree.formatHTML() 400 | 401 | def load(self, fname): 402 | """Load a kicad generic netlist 403 | 404 | Keywords: 405 | fname -- The name of the generic netlist file to open 406 | 407 | """ 408 | try: 409 | self._reader = sax.make_parser() 410 | self._reader.setContentHandler(_gNetReader(self)) 411 | self._reader.parse(fname) 412 | except IOError as e: 413 | print >> sys.stderr, __file__, ":", e 414 | sys.exit(-1) 415 | 416 | 417 | class part(): 418 | """Class for a library part""" 419 | def __init__(self, part): 420 | # The part is a reference to a libpart generic netlist element 421 | self.element = part 422 | 423 | def __str__(self): 424 | # simply print the generic netlist element associated with this part 425 | return str(self.element) 426 | 427 | def getDatasheet(self): 428 | return self.element.get("docs") 429 | 430 | def getLib(self): 431 | return self.element.get("libpart", "lib") 432 | 433 | def getPart(self): 434 | return self.element.get("libpart", "part") 435 | 436 | def getDescription(self): 437 | return self.element.get("description") 438 | 439 | def getField(self, name): 440 | return self.element.get("field", "name", name) 441 | 442 | 443 | class _gNetReader(sax.handler.ContentHandler): 444 | """SAX kicad generic netlist content handler - passes most of the work back 445 | to the gNetlist class which builds a complete tree in RAM for the design 446 | 447 | """ 448 | def __init__(self, aParent): 449 | self.parent = aParent 450 | 451 | def startElement(self, name, attrs): 452 | """Start of a new XML element event""" 453 | element = self.parent.addElement(name) 454 | 455 | for name in attrs.getNames(): 456 | element.addAttribute(name, attrs.getValue(name)) 457 | 458 | def endElement(self, name): 459 | self.parent.endElement() 460 | 461 | def characters(self, content): 462 | # Ignore erroneous white space - ignoreableWhitespace does not get rid 463 | # of the need for this! 464 | if not content.isspace(): 465 | self.parent.addChars(content) 466 | 467 | def endDocument(self): 468 | """End of the XML document event""" 469 | self.parent.endDocument() 470 | --------------------------------------------------------------------------------