def __init__(self, refdef, component): """ """ self._refdef = refdef self._layer = component.get('layer') or 'top' self._rotate = component.get('rotate') or 0 if self._layer == 'bottom': self._rotate *= -1 self._rotate_point = utils.toPoint( component.get('rotate-point') or [0, 0]) self._scale = component.get('scale') or 1 self._location = component.get('location') or [0, 0] # Get footprint definition and shapes try: self._footprint_name = component['footprint'] except: msg.error("Cannot find a 'footprint' name for refdef %s." % refdef) filename = self._footprint_name + '.json' paths = [ os.path.join(config.cfg['base-dir'], config.cfg['locations']['shapes'], filename), os.path.join(config.cfg['base-dir'], config.cfg['locations']['components'], filename) ] footprint_dict = None for path in paths: if os.path.isfile(path): footprint_dict = utils.dictFromJsonFile(path) break if footprint_dict == None: fname_list = "" for path in paths: fname_list += " %s" % path msg.error("Couldn't find shape file. Looked for it here:\n%s" % (fname_list)) footprint = Footprint(footprint_dict) footprint_shapes = footprint.getShapes() #------------------------------------------------ # Apply component-specific modifiers to footprint #------------------------------------------------ for sheet in [ 'conductor', 'soldermask', 'solderpaste', 'pours', 'silkscreen', 'assembly', 'drills' ]: for layer in config.stk['layer-names']: for shape in footprint_shapes[sheet].get(layer) or []: # In order to apply the rotation we need to adust the location shape.rotateLocation(self._rotate, self._rotate_point) shape.transformPath(scale=self._scale, rotate=self._rotate, rotate_point=self._rotate_point, mirror=shape.getMirrorPlacement(), add=True) #-------------------------------------------------------------- # Remove silkscreen and assembly shapes if instructed #-------------------------------------------------------------- # If the 'show' flag is 'false then remove these items from the # shapes dictionary #-------------------------------------------------------------- for sheet in ['silkscreen', 'assembly']: try: shapes_dict = component[sheet].get('shapes') or {} except: shapes_dict = {} # If the setting is to not show silkscreen shapes for the # component, delete the shapes from the shapes' dictionary if shapes_dict.get('show') == False: for pcb_layer in utils.getSurfaceLayers(): footprint_shapes[sheet][pcb_layer] = [] #---------------------------------------------------------- # Add silkscreen and assembly reference designator (refdef) #---------------------------------------------------------- for sheet in ['silkscreen', 'assembly']: try: refdef_dict = component[sheet].get('refdef') or {} except: refdef_dict = {} if refdef_dict.get('show') != False: layer = refdef_dict.get('layer') or 'top' # Rotate the refdef; if unspecified the rotation is the same as # the rotation of the component refdef_dict['rotate'] = refdef_dict.get('rotate') or 0 # Sometimes you'd want to keep all refdefs at the same angle # and not rotated with the component if refdef_dict.get('rotate-with-component') != False: refdef_dict['rotate'] += self._rotate refdef_dict['rotate-point'] = utils.toPoint( refdef_dict.get('rotate-point')) or self._rotate_point refdef_dict['location'] = refdef_dict.get('location') or [0, 0] refdef_dict['type'] = 'text' refdef_dict['value'] = refdef_dict.get('value') or refdef refdef_dict['font-family'] = ( refdef_dict.get('font-family') or config.stl['layout'][sheet]['refdef'].get('font-family') or config.stl['defaults']['font-family']) refdef_dict['font-size'] = ( refdef_dict.get('font-size') or config.stl['layout'][sheet]['refdef'].get('font-size') or "2mm") refdef_shape = Shape(refdef_dict) refdef_shape.is_refdef = True refdef_shape.rotateLocation(self._rotate, self._rotate_point) style = Style(refdef_dict, sheet, 'refdef') refdef_shape.setStyle(style) # Add the refdef to the silkscreen/assembly list. It's # important that this is added at the very end since the # placement process assumes the refdef is last try: footprint_shapes[sheet][layer] except: footprint_shapes[sheet][layer] = [] footprint_shapes[sheet][layer].append(refdef_shape) #------------------------------------------------------ # Invert layers #------------------------------------------------------ # If the placement is on the bottom of the baord then we need # to invert the placement of all components. This affects the # surface laters but also internal layers if self._layer == 'bottom': layers = config.stk['layer-names'] for sheet in [ 'conductor', 'pours', 'soldermask', 'solderpaste', 'silkscreen', 'assembly' ]: sheet_dict = footprint_shapes[sheet] sheet_dict_new = {} for i, pcb_layer in enumerate(layers): try: sheet_dict_new[layers[len(layers) - i - 1]] = copy.copy( sheet_dict[pcb_layer]) except: continue footprint_shapes[sheet] = copy.copy(sheet_dict_new) self._footprint_shapes = footprint_shapes
def make_bom(quantity=None): """ """ def natural_key(string_): """See http://www.codinghorror.com/blog/archives/001018.html""" return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)] dnp_text = 'Do not populate' uncateg_text = 'Uncategorised' components_dict = config.brd['components'] bom_dict = {} for refdef in components_dict: description = '' try: place = components_dict[refdef]['place'] except: place = True try: ignore = components_dict[refdef]['bom']['ignore'] except: ignore = False # If component isn't placed, ignore it if place == True and ignore == False: # Get footprint definition and shapes try: footprint_name = components_dict[refdef]['footprint'] except: msg.error("Cannot find a 'footprint' name for refdef %s." % refdef) # Open footprint file fname = os.path.join(config.cfg['base-dir'], config.cfg['locations']['components'], footprint_name + '.json') footprint_dict = utils.dictFromJsonFile(fname) info_dict = footprint_dict.get('info') or {} try: comp_bom_dict = components_dict[refdef]['bom'] except: comp_bom_dict = {} try: fp_bom_dict = footprint_dict['info'] except: fp_bom_dict = {} # Override component BoM info on top of footprint info for key in comp_bom_dict: fp_bom_dict[key] = comp_bom_dict[key] description = fp_bom_dict.get('description') or uncateg_text try: dnp = components_dict[refdef]['bom']['dnp'] except: dnp = False if dnp == True: description = dnp_text if description not in bom_dict: bom_dict[description] = fp_bom_dict bom_dict[description]['refdefs'] = [] bom_dict[description]['refdefs'].append(refdef) bom_dict[description]['placement'] = components_dict[refdef]['layer'] try: bom_content = config.brd['bom'] except: bom_content = [ { "field": "line-item", "text": "#" }, { "field": "quantity", "text": "Qty" }, { "field": "designators", "text": "Designators" }, { "field": "description", "text": "Description" }, { "field": "package", "text": "Package" }, { "field": "manufacturer", "text": "Manufacturer" }, { "field": "part-number", "text": "Part #" }, { "field": "suppliers", "text": "Suppliers", "suppliers": [ { "field": "farnell", "text": "Farnell #", "search-url": "http://uk.farnell.com/catalog/Search?st=" }, { "field": "mouser", "text": "Mouser #", "search-url": "http://uk.mouser.com/Search/Refine.aspx?Keyword=" }, { "field": "octopart", "text": "Octopart", "search-url": "https://octopart.com/search?q=" } ] }, { "field": "placement", "text": "Layer" }, { "field": "notes", "text": "Notes" } ] # Set up the BoM file name bom_path = os.path.join(config.cfg['base-dir'], config.cfg['locations']['build'], 'bom') # Create path if it doesn't exist already utils.create_dir(bom_path) board_name = config.cfg['name'] board_revision = config.brd['config'].get('rev') base_name = "%s_rev_%s" % (board_name, board_revision) bom_html = os.path.join(bom_path, base_name + '_%s.html'% 'bom') bom_csv = os.path.join(bom_path, base_name + '_%s.csv'% 'bom') html = [] csv = [] html.append('<html>') html.append('<style type="text/css">') try: css = config.stl['layout']['bom']['css'] except: css = [] for line in css: html.append(line) html.append('</style>') html.append('<table class="tg">') header = [] for item in bom_content: if item['field'] == 'suppliers': for supplier in item['suppliers']: header.append("%s" % supplier['text']) else: header.append("%s" % item['text']) if item['field'] == 'quantity' and quantity != None: header.append("@%s" % quantity) html.append(' <tr>') html.append(' <th class="tg-title" colspan="%s">Bill of materials -- %s rev %s</th>' % (len(header), board_name, board_revision)) html.append(' </tr>') html.append(' <tr>') for item in header: html.append(' <th class="tg-header">%s</th>' % item) html.append(' </tr>') uncateg_content = [] dnp_content = [] index = 1 for desc in bom_dict: content = [] for item in bom_content: if item['field'] == 'line-item': content.append("<strong>%s</strong>" % str(index)) elif item['field'] == 'suppliers': for supplier in item['suppliers']: try: number = bom_dict[desc][item['field']][supplier['field']] except: number = "" search_url = supplier.get('search-url') if search_url != None: content.append('<a href="%s%s">%s</a>' % (search_url, number, number)) else: content.append(number) elif item['field'] == 'quantity': units = len(bom_dict[desc]['refdefs']) content.append("%s" % (str(units))) if quantity != None: content.append("%s" % (str(units*int(quantity)))) elif item['field'] == 'designators': # Natural/human sort the list of designators sorted_list = sorted(bom_dict[desc]['refdefs'], key=natural_key) refdefs = '' for refdef in sorted_list[:-1]: refdefs += "%s " % refdef refdefs += "%s" % sorted_list[-1] content.append("%s " % refdefs) elif item['field'] == 'description': content.append("%s " % desc) else: try: content.append(bom_dict[desc][item['field']]) except: content.append("") if desc == uncateg_text: uncateg_content = content elif desc == dnp_text: dnp_content = content else: html.append(' <tr>') for item in content: html.append(' <td class="tg-item-%s">%s</td>' % (('odd','even')[index%2==0], item)) html.append(' </tr>') index += 1 for content in (dnp_content, uncateg_content): html.append(' <tr class="tg-skip">') html.append(' </tr>') html.append(' <tr>') if len(content) > 0: content[0] = index for item in content: html.append(' <td class="tg-item-%s">%s</td>' % (('odd','even')[index%2==0], item)) html.append(' </tr>') index += 1 html.append('</table>') html.append('<p>Generated by <a href="http://pcbmode.com">PCBmodE</a>, an open source PCB design software. PCBmodE was written and is maintained by <a href="http://boldport.com">Boldport</a>, creators of beautifully functional circuits.') html.append('</html>') with open(bom_html, "wb") as f: for line in html: f.write(line+'\n')
def extractRouting(svg_in): """ Extracts routing from the the 'routing' SVG layers of each PCB layer. Inkscape SVG layers for each PCB ('top', 'bottom', etc.) layer. """ # Open the routing file if it exists. The existing data is used # for stats displayed as PCBmodE is run. The file is then # overwritten. output_file = os.path.join(config.cfg['base-dir'], config.cfg['name'] + '_routing.json') try: routing_dict_old = utils.dictFromJsonFile(output_file, False) except: routing_dict_old = {'routes': {}} #--------------- # Extract routes #--------------- # Store extracted data here routing_dict = {} # The XPATH expression for extracting routes, but not vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='routing']//svg:path[(@d) and not (@pcbmode:type='via')]" routes_dict = {'top': {}, 'bottom': {}} for pcb_layer in utils.getSurfaceLayers(): routes = svg_in.xpath(xpath_expr % pcb_layer, namespaces={'pcbmode':config.cfg['ns']['pcbmode'], 'svg':config.cfg['ns']['svg']}) for route in routes: route_dict = {} route_id = route.get('{'+config.cfg['ns']['pcbmode']+'}id') path = route.get('d') style_text = route.get('style') or '' # This hash digest provides a unique identifier for # the route based on its path, location, and style digest = utils.digest(path+ #str(location.x)+ #str(location.y)+ style_text) routes_dict[pcb_layer][digest] = {} routes_dict[pcb_layer][digest]['type'] = 'path' routes_dict[pcb_layer][digest]['value'] = path stroke_width = utils.getStyleAttrib(style_text, 'stroke-width') if stroke_width != None: routes_dict[pcb_layer][digest]['style'] = 'stroke' routes_dict[pcb_layer][digest]['stroke-width'] = round(float(stroke_width), 4) custom_buffer = route.get('{'+config.cfg['ns']['pcbmode']+'}buffer-to-pour') if custom_buffer != None: routes_dict[pcb_layer][digest]['buffer-to-pour'] = float(custom_buffer) gerber_lp = route.get('{'+config.cfg['ns']['pcbmode']+'}gerber-lp') if gerber_lp != None: routes_dict[pcb_layer][digest]['gerber-lp'] = gerber_lp routing_dict['routes'] = routes_dict # Create simple stats and display them total = 0 total_old = 0 new = 0 existing = 0 for pcb_layer in utils.getSurfaceLayers(): try: total += len(routing_dict['routes'][pcb_layer]) except: pass try: new_dict = routing_dict['routes'][pcb_layer] except: new_dict = {} try: old_dict = routing_dict_old['routes'][pcb_layer] except: old_dict = {} for key in new_dict: if key not in old_dict: new += 1 else: existing += 1 for pcb_layer in utils.getSurfaceLayers(): total_old += len(old_dict) message = "Extracted %s routes; %s new (or modified), %s existing" % (total, new, existing) if total_old > total: message += ", %s removed" % (total_old - total) msg.subInfo(message) #------------- # Extract vias #------------- # XPATH expression for extracting vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='routing']//svg:path[@pcbmode:type='via']" # Get new vias; only search the top layer new_vias = svg_in.xpath(xpath_expr % 'top', namespaces={'pcbmode':config.cfg['ns']['pcbmode'], 'svg':config.cfg['ns']['svg']}) # XPATH expression for extracting vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='pads']//svg:g[@pcbmode:type='via']" # Get new vias; only search the top layer vias = svg_in.xpath(xpath_expr % 'top', namespaces={'pcbmode':config.cfg['ns']['pcbmode'], 'svg':config.cfg['ns']['svg']}) vias_dict = {} for via in vias: transform = via.get('transform') if transform != None: transform_data = utils.parseTransform(transform) location = transform_data['location'] else: location = Point() # Invery 'y' axis if needed location.y *= config.cfg['invert-y'] digest = utils.digest("%s%s" % (location.x, location.y)) # Define a via, just like any other component, but disable # placement of refdef vias_dict[digest] = {} vias_dict[digest]['footprint'] = via.get('{'+config.cfg['ns']['pcbmode']+'}via') vias_dict[digest]['location'] = [location.x, location.y] vias_dict[digest]['silkscreen'] = {'refdef': {'show': False }} vias_dict[digest]['assembly'] = {'refdef': {'show': False }} vias_dict[digest]['layer'] = 'top' for via in new_vias: # A newly-defined via will have a location set through the # 'sodipodi' namespace and possible also through a transform try: sodipodi_loc = Point(via.get('{'+config.cfg['ns']['sodipodi']+'}cx'), via.get('{'+config.cfg['ns']['sodipodi']+'}cy')) except: sodipodi_loc = Pound() transform = via.get('transform') if transform != None: transform_data = utils.parseTransform(transform) location = transform_data['location'] else: location = Point() location += sodipodi_loc # Invery 'y' axis if needed location.y *= config.cfg['invert-y'] digest = utils.digest("%s%s" % (location.x, location.y)) # Define a via, just like any other component, but disable # placement of refdef vias_dict[digest] = {} vias_dict[digest]['footprint'] = via.get('{'+config.cfg['ns']['pcbmode']+'}via') vias_dict[digest]['location'] = [location.x, location.y] vias_dict[digest]['silkscreen'] = {'refdef': {'show': False }} vias_dict[digest]['assembly'] = {'refdef': {'show': False }} vias_dict[digest]['layer'] = 'top' routing_dict['vias'] = vias_dict # Display stats if len(vias_dict) == 0: msg.subInfo("No vias found") elif len(vias_dict) == 1: msg.subInfo("Extracted 1 via") else: msg.subInfo("Extracted %s vias" % (len(vias_dict))) # Save extracted routing into routing file try: with open(output_file, 'wb') as f: f.write(json.dumps(routing_dict, sort_keys=True, indent=2)) except: msg.error("Cannot save file %s" % output_file) return
def extractRouting(svg_in): """ Extracts routing from the the 'routing' SVG layers of each PCB layer. Inkscape SVG layers for each PCB ('top', 'bottom', etc.) layer. """ # Open the routing file if it exists. The existing data is used # for stats displayed as PCBmodE is run. The file is then # overwritten. output_file = os.path.join(config.cfg['base-dir'], config.cfg['name'] + '_routing.json') try: routing_dict_old = utils.dictFromJsonFile(output_file, False) except: routing_dict_old = {'routes': {}} #--------------- # Extract routes #--------------- # Store extracted data here routing_dict = {} # The XPATH expression for extracting routes, but not vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='routing']//svg:path[(@d) and not (@pcbmode:type='via')]" routes_dict = {} for pcb_layer in config.stk['layer-names']: routes = svg_in.xpath(xpath_expr % pcb_layer, namespaces={'pcbmode':config.cfg['ns']['pcbmode'], 'svg':config.cfg['ns']['svg']}) for route in routes: route_dict = {} route_id = route.get('{'+config.cfg['ns']['pcbmode']+'}id') path = route.get('d') style_text = route.get('style') or '' # This hash digest provides a unique identifier for # the route based on its path, location, and style digest = utils.digest(path+ #str(location.x)+ #str(location.y)+ style_text) try: routes_dict[pcb_layer][digest] = {} except: routes_dict[pcb_layer] = {} routes_dict[pcb_layer][digest] = {} routes_dict[pcb_layer][digest]['type'] = 'path' routes_dict[pcb_layer][digest]['value'] = path stroke_width = utils.getStyleAttrib(style_text, 'stroke-width') if stroke_width != None: # Sometimes Inkscape will add a 'px' suffix to the stroke-width #property pf a path; this removes it stroke_width = stroke_width.rstrip('px') routes_dict[pcb_layer][digest]['style'] = 'stroke' routes_dict[pcb_layer][digest]['stroke-width'] = round(float(stroke_width), 4) custom_buffer = route.get('{'+config.cfg['ns']['pcbmode']+'}buffer-to-pour') if custom_buffer != None: routes_dict[pcb_layer][digest]['buffer-to-pour'] = float(custom_buffer) gerber_lp = route.get('{'+config.cfg['ns']['pcbmode']+'}gerber-lp') if gerber_lp != None: routes_dict[pcb_layer][digest]['gerber-lp'] = gerber_lp routing_dict['routes'] = routes_dict # Create simple stats and display them total = 0 total_old = 0 new = 0 existing = 0 for pcb_layer in config.stk['layer-names']: try: total += len(routing_dict['routes'][pcb_layer]) except: pass try: new_dict = routing_dict['routes'][pcb_layer] except: new_dict = {} try: old_dict = routing_dict_old['routes'][pcb_layer] except: old_dict = {} for key in new_dict: if key not in old_dict: new += 1 else: existing += 1 for pcb_layer in config.stk['layer-names']: total_old += len(old_dict) message = "Extracted %s routes; %s new (or modified), %s existing" % (total, new, existing) if total_old > total: message += ", %s removed" % (total_old - total) msg.subInfo(message) #------------------------------- # Extract vias #------------------------------- xpath_expr_place = '//svg:g[@pcbmode:pcb-layer="%s"]//svg:g[@pcbmode:sheet="placement"]//svg:g[@pcbmode:type="via"]' vias_dict = {} for pcb_layer in config.stk['surface-layer-names']: # Find all markers markers = svg_in.findall(xpath_expr_place % pcb_layer, namespaces={'pcbmode':config.cfg['ns']['pcbmode'], 'svg':config.cfg['ns']['svg']}) for marker in markers: transform_data = utils.parseTransform(marker.get('transform')) location = transform_data['location'] # Invert 'y' coordinate location.y *= config.cfg['invert-y'] # Change component rotation if needed if transform_data['type'] == 'matrix': rotate = transform_data['rotate'] rotate = utils.niceFloat((rotate) % 360) digest = utils.digest("%s%s" % (location.x, location.y)) # Define a via, just like any other component, but disable # placement of refdef vias_dict[digest] = {} vias_dict[digest]['footprint'] = marker.get('{'+config.cfg['ns']['pcbmode']+'}footprint') vias_dict[digest]['location'] = [utils.niceFloat(location.x), utils.niceFloat(location.y)] vias_dict[digest]['layer'] = 'top' routing_dict['vias'] = vias_dict # Display stats if len(vias_dict) == 0: msg.subInfo("No vias found") elif len(vias_dict) == 1: msg.subInfo("Extracted 1 via") else: msg.subInfo("Extracted %s vias" % (len(vias_dict))) # Save extracted routing into routing file try: with open(output_file, 'wb') as f: f.write(json.dumps(routing_dict, sort_keys=True, indent=2)) except: msg.error("Cannot save file %s" % output_file) return
def extractRouting(svg_in): """ Extracts routing from the the 'routing' SVG layers of each PCB layer. Inkscape SVG layers for each PCB ('top', 'bottom', etc.) layer. """ # Open the routing file if it exists. The existing data is used # for stats displayed as PCBmodE is run. The file is then # overwritten. output_file = os.path.join(config.cfg['base-dir'], config.cfg['name'] + '_routing.json') try: routing_dict_old = utils.dictFromJsonFile(output_file, False) except: routing_dict_old = {'routes': {}} #--------------- # Extract routes #--------------- # Store extracted data here routing_dict = {} # The XPATH expression for extracting routes, but not vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='routing']//svg:path[(@d) and not (@pcbmode:type='via')]" routes_dict = {'top': {}, 'bottom': {}} for pcb_layer in utils.getSurfaceLayers(): routes = svg_in.xpath(xpath_expr % pcb_layer, namespaces={ 'pcbmode': config.cfg['ns']['pcbmode'], 'svg': config.cfg['ns']['svg'] }) for route in routes: route_dict = {} route_id = route.get('{' + config.cfg['ns']['pcbmode'] + '}id') path = route.get('d') style_text = route.get('style') or '' # This hash digest provides a unique identifier for # the route based on its path, location, and style digest = utils.digest(path + #str(location.x)+ #str(location.y)+ style_text) routes_dict[pcb_layer][digest] = {} routes_dict[pcb_layer][digest]['type'] = 'path' routes_dict[pcb_layer][digest]['value'] = path stroke_width = utils.getStyleAttrib(style_text, 'stroke-width') if stroke_width != None: # Sometimes Inkscape will add a 'px' suffix to the stroke-width #property pf a path; this removes it stroke_width = stroke_width.rstrip('px') routes_dict[pcb_layer][digest]['style'] = 'stroke' routes_dict[pcb_layer][digest]['stroke-width'] = round( float(stroke_width), 4) custom_buffer = route.get('{' + config.cfg['ns']['pcbmode'] + '}buffer-to-pour') if custom_buffer != None: routes_dict[pcb_layer][digest]['buffer-to-pour'] = float( custom_buffer) gerber_lp = route.get('{' + config.cfg['ns']['pcbmode'] + '}gerber-lp') if gerber_lp != None: routes_dict[pcb_layer][digest]['gerber-lp'] = gerber_lp routing_dict['routes'] = routes_dict # Create simple stats and display them total = 0 total_old = 0 new = 0 existing = 0 for pcb_layer in utils.getSurfaceLayers(): try: total += len(routing_dict['routes'][pcb_layer]) except: pass try: new_dict = routing_dict['routes'][pcb_layer] except: new_dict = {} try: old_dict = routing_dict_old['routes'][pcb_layer] except: old_dict = {} for key in new_dict: if key not in old_dict: new += 1 else: existing += 1 for pcb_layer in utils.getSurfaceLayers(): total_old += len(old_dict) message = "Extracted %s routes; %s new (or modified), %s existing" % ( total, new, existing) if total_old > total: message += ", %s removed" % (total_old - total) msg.subInfo(message) #------------- # Extract vias #------------- # XPATH expression for extracting vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='routing']//svg:*[@pcbmode:type='via']" # Get new vias; only search the top layer new_vias = svg_in.xpath(xpath_expr % 'top', namespaces={ 'pcbmode': config.cfg['ns']['pcbmode'], 'svg': config.cfg['ns']['svg'] }) # XPATH expression for extracting vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='pads']//svg:g[@pcbmode:type='via']" # Get nexisting vias; only search the top layer vias = svg_in.xpath(xpath_expr % 'top', namespaces={ 'pcbmode': config.cfg['ns']['pcbmode'], 'svg': config.cfg['ns']['svg'] }) vias_dict = {} for via in vias: transform = via.get('transform') if transform != None: transform_data = utils.parseTransform(transform) location = transform_data['location'] else: location = Point() # Invery 'y' axis if needed location.y *= config.cfg['invert-y'] digest = utils.digest("%s%s" % (location.x, location.y)) # Define a via, just like any other component, but disable # placement of refdef vias_dict[digest] = {} vias_dict[digest]['footprint'] = via.get('{' + config.cfg['ns']['pcbmode'] + '}via') vias_dict[digest]['location'] = [location.x, location.y] vias_dict[digest]['silkscreen'] = {'refdef': {'show': False}} vias_dict[digest]['assembly'] = {'refdef': {'show': False}} vias_dict[digest]['layer'] = 'top' for via in new_vias: # A newly-defined via will have a location set through the # 'sodipodi' namespace and possible also through a transform try: # The commented lines below wored fro Inkscape prior to 0.91 #sodipodi_loc = Point(via.get('{'+config.cfg['ns']['sodipodi']+'}cx'), # via.get('{'+config.cfg['ns']['sodipodi']+'}cy')) sodipodi_loc = Point(via.get('cx'), via.get('cy')) except: sodipodi_loc = Point() print sodipodi_loc transform = via.get('transform') if transform != None: transform_data = utils.parseTransform(transform) location = transform_data['location'] else: location = Point() location += sodipodi_loc # Invery 'y' axis if needed location.y *= config.cfg['invert-y'] digest = utils.digest("%s%s" % (location.x, location.y)) # Define a via, just like any other component, but disable # placement of refdef vias_dict[digest] = {} vias_dict[digest]['footprint'] = via.get('{' + config.cfg['ns']['pcbmode'] + '}via') vias_dict[digest]['location'] = [location.x, location.y] vias_dict[digest]['silkscreen'] = {'refdef': {'show': False}} vias_dict[digest]['assembly'] = {'refdef': {'show': False}} vias_dict[digest]['layer'] = 'top' routing_dict['vias'] = vias_dict # Display stats if len(vias_dict) == 0: msg.subInfo("No vias found") elif len(vias_dict) == 1: msg.subInfo("Extracted 1 via") else: msg.subInfo("Extracted %s vias" % (len(vias_dict))) # Save extracted routing into routing file try: with open(output_file, 'wb') as f: f.write(json.dumps(routing_dict, sort_keys=True, indent=2)) except: msg.error("Cannot save file %s" % output_file) return
def __init__(self, refdef, component): """ """ self._refdef = refdef self._layer = component.get('layer') or 'top' self._rotate = component.get('rotate') or 0 self._rotate_point = utils.toPoint(component.get('rotate-point') or [0, 0]) self._scale = component.get('scale') or 1 self._location = component.get('location') or [0, 0] # Get footprint definition and shapes try: self._footprint_name = component['footprint'] except: msg.error("Cannot find a 'footprint' name for refdef %s." % refdef) fname = os.path.join(config.cfg['base-dir'], config.cfg['locations']['components'], self._footprint_name + '.json') footprint_dict = utils.dictFromJsonFile(fname) footprint = Footprint(footprint_dict) footprint_shapes = footprint.getShapes() #------------------------------------------------ # Apply component-specific modifiers to footprint #------------------------------------------------ for sheet in ['copper', 'soldermask', 'solderpaste', 'silkscreen', 'assembly', 'drills']: for layer in utils.getSurfaceLayers() + utils.getInternalLayers(): for shape in footprint_shapes[sheet][layer]: # In order to apply the rotation we need to adust the location # of each element shape.rotateLocation(self._rotate, self._rotate_point) shape.transformPath(self._scale, self._rotate, self._rotate_point, False, True) #-------------------------------------- # Remove silkscreen and assembly shapes #-------------------------------------- for sheet in ['silkscreen','assembly']: try: shapes_dict = component[sheet].get('shapes') or {} except: shapes_dict = {} # If the setting is to not show silkscreen shapes for the # component, delete the shapes from the shapes' dictionary if shapes_dict.get('show') == False: for pcb_layer in utils.getSurfaceLayers(): footprint_shapes[sheet][pcb_layer] = [] #---------------------------------------------------------- # Add silkscreen and assembly reference designator (refdef) #---------------------------------------------------------- for sheet in ['silkscreen','assembly']: try: refdef_dict = component[sheet].get('refdef') or {} except: refdef_dict = {} if refdef_dict.get('show') != False: layer = refdef_dict.get('layer') or 'top' # Rotate the refdef; if unspecified the rotation is the same as # the rotation of the component refdef_dict['rotate'] = refdef_dict.get('rotate') or 0 # Sometimes you'd want to keep all refdefs at the same angle # and not rotated with the component if refdef_dict.get('rotate-with-component') != False: refdef_dict['rotate'] += self._rotate refdef_dict['rotate-point'] = utils.toPoint(refdef_dict.get('rotate-point')) or self._rotate_point refdef_dict['location'] = refdef_dict.get('location') or [0, 0] refdef_dict['type'] = 'text' refdef_dict['value'] = refdef refdef_dict['font-family'] = (config.stl['layout'][sheet]['refdef'].get('font-family') or config.stl['defaults']['font-family']) refdef_dict['font-size'] = (config.stl['layout'][sheet]['refdef'].get('font-size') or "2mm") refdef_shape = Shape(refdef_dict) refdef_shape.is_refdef = True refdef_shape.rotateLocation(self._rotate, self._rotate_point) style = Style(refdef_dict, sheet, 'refdef') refdef_shape.setStyle(style) # Add the refdef to the silkscreen/assembly list. It's # important that this is added at the very end since the # plcament process assumes the refdef is last footprint_shapes[sheet][layer].append(refdef_shape) #------------------------------------------------------ # Invert 'top' and 'bottom' if layer is on the 'bottom' #------------------------------------------------------ if self._layer == 'bottom': layers = utils.getSurfaceLayers() layers_reversed = reversed(utils.getSurfaceLayers()) for sheet in ['copper', 'soldermask', 'solderpaste', 'silkscreen', 'assembly']: sheet_dict = footprint_shapes[sheet] # TODO: this nasty hack won't work for more than two # layers, so when 2+ are supported this needs to be # revisited for i in range(0,len(layers)-1): sheet_dict['temp'] = sheet_dict.pop(layers[i]) sheet_dict[layers[i]] = sheet_dict.pop(layers[len(layers)-1-i]) sheet_dict[layers[len(layers)-1-i]] = sheet_dict.pop('temp') self._footprint_shapes = footprint_shapes
def __init__(self, refdef, component): """ """ self._refdef = refdef self._layer = component.get('layer') or 'top' self._rotate = component.get('rotate') or 0 self._rotate_point = utils.toPoint(component.get('rotate-point') or [0, 0]) self._scale = component.get('scale') or 1 self._location = component.get('location') or [0, 0] # Get footprint definition and shapes try: self._footprint_name = component['footprint'] except: msg.error("Cannot find a 'footprint' name for refdef %s." % refdef) fname = os.path.join(config.cfg['base-dir'], config.cfg['locations']['components'], self._footprint_name + '.json') footprint_dict = utils.dictFromJsonFile(fname) footprint = Footprint(footprint_dict) footprint_shapes = footprint.getShapes() #------------------------------------------------ # Apply component-specific modifiers to footprint #------------------------------------------------ for sheet in ['copper', 'soldermask', 'solderpaste', 'silkscreen', 'assembly', 'drills']: for layer in utils.getSurfaceLayers() + utils.getInternalLayers(): for shape in footprint_shapes[sheet][layer]: # In order to apply the rotation we need to adust the location # of each element shape.rotateLocation(self._rotate, self._rotate_point) shape.transformPath(self._scale, self._rotate, self._rotate_point, False, True) #-------------------------------------- # Remove silkscreen and assembly shapes #-------------------------------------- for sheet in ['silkscreen','assembly']: try: shapes_dict = component[sheet].get('shapes') or {} except: shapes_dict = {} # If the setting is to not show silkscreen shapes for the # component, delete the shapes from the shapes' dictionary if shapes_dict.get('show') == False: for pcb_layer in utils.getSurfaceLayers(): footprint_shapes[sheet][pcb_layer] = [] #---------------------------------------------------------- # Add silkscreen and assembly reference designator (refdef) #---------------------------------------------------------- for sheet in ['silkscreen','assembly']: try: refdef_dict = component[sheet].get('refdef') or {} except: refdef_dict = {} if refdef_dict.get('show') != False: layer = refdef_dict.get('layer') or 'top' # Rotate the refdef; if unspecified the rotation is the same as # the rotation of the component refdef_dict['rotate'] = refdef_dict.get('rotate') or 0 # Sometimes you'd want to keep all refdefs at the same angle # and not rotated with the component if refdef_dict.get('rotate-with-component') != False: refdef_dict['rotate'] += self._rotate refdef_dict['rotate-point'] = utils.toPoint(refdef_dict.get('rotate-point')) or self._rotate_point refdef_dict['location'] = refdef_dict.get('location') or [0, 0] refdef_dict['type'] = 'text' refdef_dict['value'] = refdef_dict.get('value') or refdef refdef_dict['font-family'] = (refdef_dict.get('font-family') or config.stl['layout'][sheet]['refdef'].get('font-family') or config.stl['defaults']['font-family']) refdef_dict['font-size'] = (refdef_dict.get('font-size') or config.stl['layout'][sheet]['refdef'].get('font-size') or "2mm") refdef_shape = Shape(refdef_dict) refdef_shape.is_refdef = True refdef_shape.rotateLocation(self._rotate, self._rotate_point) style = Style(refdef_dict, sheet, 'refdef') refdef_shape.setStyle(style) # Add the refdef to the silkscreen/assembly list. It's # important that this is added at the very end since the # placement process assumes the refdef is last footprint_shapes[sheet][layer].append(refdef_shape) #------------------------------------------------------ # Invert 'top' and 'bottom' if layer is on the 'bottom' #------------------------------------------------------ if self._layer == 'bottom': layers = utils.getSurfaceLayers() layers_reversed = reversed(utils.getSurfaceLayers()) for sheet in ['copper', 'soldermask', 'solderpaste', 'silkscreen', 'assembly']: sheet_dict = footprint_shapes[sheet] # TODO: this nasty hack won't work for more than two # layers, so when 2+ are supported this needs to be # revisited for i in range(0,len(layers)-1): sheet_dict['temp'] = sheet_dict.pop(layers[i]) sheet_dict[layers[i]] = sheet_dict.pop(layers[len(layers)-1-i]) sheet_dict[layers[len(layers)-1-i]] = sheet_dict.pop('temp') self._footprint_shapes = footprint_shapes
def __init__(self, refdef, component): """ """ self._refdef = refdef self._layer = component.get('layer') or 'top' self._rotate = component.get('rotate') or 0 if self._layer=='bottom': self._rotate *= -1 self._rotate_point = utils.toPoint(component.get('rotate-point') or [0, 0]) self._scale = component.get('scale') or 1 self._location = component.get('location') or [0, 0] # Get footprint definition and shapes try: self._footprint_name = component['footprint'] except: msg.error("Cannot find a 'footprint' name for refdef %s." % refdef) filename = self._footprint_name + '.json' paths = [os.path.join(config.cfg['base-dir'], config.cfg['locations']['shapes'], filename), os.path.join(config.cfg['base-dir'], config.cfg['locations']['components'], filename)] footprint_dict = None for path in paths: if os.path.isfile(path): footprint_dict = utils.dictFromJsonFile(path) break if footprint_dict == None: fname_list = "" for path in paths: fname_list += " %s" % path msg.error("Couldn't find shape file. Looked for it here:\n%s" % (fname_list)) footprint = Footprint(footprint_dict) footprint_shapes = footprint.getShapes() #------------------------------------------------ # Apply component-specific modifiers to footprint #------------------------------------------------ for sheet in ['conductor', 'soldermask', 'solderpaste', 'pours', 'silkscreen', 'assembly', 'drills']: for layer in config.stk['layer-names']: for shape in footprint_shapes[sheet].get(layer) or []: # In order to apply the rotation we need to adust the location shape.rotateLocation(self._rotate, self._rotate_point) shape.transformPath(scale=self._scale, rotate=self._rotate, rotate_point=self._rotate_point, mirror=shape.getMirrorPlacement(), add=True) #-------------------------------------------------------------- # Remove silkscreen and assembly shapes if instructed #-------------------------------------------------------------- # If the 'show' flag is 'false then remove these items from the # shapes dictionary #-------------------------------------------------------------- for sheet in ['silkscreen','assembly']: try: shapes_dict = component[sheet].get('shapes') or {} except: shapes_dict = {} # If the setting is to not show silkscreen shapes for the # component, delete the shapes from the shapes' dictionary if shapes_dict.get('show') == False: for pcb_layer in utils.getSurfaceLayers(): footprint_shapes[sheet][pcb_layer] = [] #---------------------------------------------------------- # Add silkscreen and assembly reference designator (refdef) #---------------------------------------------------------- for sheet in ['silkscreen','assembly']: try: refdef_dict = component[sheet].get('refdef') or {} except: refdef_dict = {} if refdef_dict.get('show') != False: layer = refdef_dict.get('layer') or 'top' # Rotate the refdef; if unspecified the rotation is the same as # the rotation of the component refdef_dict['rotate'] = refdef_dict.get('rotate') or 0 # Sometimes you'd want to keep all refdefs at the same angle # and not rotated with the component if refdef_dict.get('rotate-with-component') != False: refdef_dict['rotate'] += self._rotate refdef_dict['rotate-point'] = utils.toPoint(refdef_dict.get('rotate-point')) or self._rotate_point refdef_dict['location'] = refdef_dict.get('location') or [0, 0] refdef_dict['type'] = 'text' refdef_dict['value'] = refdef_dict.get('value') or refdef refdef_dict['font-family'] = (refdef_dict.get('font-family') or config.stl['layout'][sheet]['refdef'].get('font-family') or config.stl['defaults']['font-family']) refdef_dict['font-size'] = (refdef_dict.get('font-size') or config.stl['layout'][sheet]['refdef'].get('font-size') or "2mm") refdef_shape = Shape(refdef_dict) refdef_shape.is_refdef = True refdef_shape.rotateLocation(self._rotate, self._rotate_point) style = Style(refdef_dict, sheet, 'refdef') refdef_shape.setStyle(style) # Add the refdef to the silkscreen/assembly list. It's # important that this is added at the very end since the # placement process assumes the refdef is last try: footprint_shapes[sheet][layer] except: footprint_shapes[sheet][layer] = [] footprint_shapes[sheet][layer].append(refdef_shape) #------------------------------------------------------ # Invert layers #------------------------------------------------------ # If the placement is on the bottom of the baord then we need # to invert the placement of all components. This affects the # surface laters but also internal layers if self._layer == 'bottom': layers = config.stk['layer-names'] for sheet in ['conductor', 'pours', 'soldermask', 'solderpaste', 'silkscreen', 'assembly']: sheet_dict = footprint_shapes[sheet] sheet_dict_new = {} for i, pcb_layer in enumerate(layers): try: sheet_dict_new[layers[len(layers)-i-1]] = copy.copy(sheet_dict[pcb_layer]) except: continue footprint_shapes[sheet] = copy.copy(sheet_dict_new) self._footprint_shapes = footprint_shapes
def extractRouting(svg_in): """ Extracts routing from the the 'routing' SVG layers of each PCB layer. Inkscape SVG layers for each PCB ('top', 'bottom', etc.) layer. """ # Open the routing file if it exists. The existing data is used # for stats displayed as PCBmodE is run. The file is then # overwritten. output_file = os.path.join(config.cfg['base-dir'], config.cfg['name'] + '_routing.json') try: routing_dict_old = utils.dictFromJsonFile(output_file, False) except: routing_dict_old = {'routes': {}} #--------------- # Extract routes #--------------- # Store extracted data here routing_dict = {} # The XPATH expression for extracting routes, but not vias xpath_expr = "//svg:g[@pcbmode:pcb-layer='%s']//svg:g[@pcbmode:sheet='routing']//svg:path[(@d) and not (@pcbmode:type='via')]" routes_dict = {} for pcb_layer in config.stk['layer-names']: routes = svg_in.xpath(xpath_expr % pcb_layer, namespaces={ 'pcbmode': config.cfg['ns']['pcbmode'], 'svg': config.cfg['ns']['svg'] }) for route in routes: route_dict = {} route_id = route.get('{' + config.cfg['ns']['pcbmode'] + '}id') path = route.get('d') style_text = route.get('style') or '' # This hash digest provides a unique identifier for # the route based on its path, location, and style digest = utils.digest(path + #str(location.x)+ #str(location.y)+ style_text) try: routes_dict[pcb_layer][digest] = {} except: routes_dict[pcb_layer] = {} routes_dict[pcb_layer][digest] = {} routes_dict[pcb_layer][digest]['type'] = 'path' routes_dict[pcb_layer][digest]['value'] = path stroke_width = utils.getStyleAttrib(style_text, 'stroke-width') if stroke_width != None: # Sometimes Inkscape will add a 'px' suffix to the stroke-width #property pf a path; this removes it stroke_width = stroke_width.rstrip('px') routes_dict[pcb_layer][digest]['style'] = 'stroke' routes_dict[pcb_layer][digest]['stroke-width'] = round( float(stroke_width), 4) custom_buffer = route.get('{' + config.cfg['ns']['pcbmode'] + '}buffer-to-pour') if custom_buffer != None: routes_dict[pcb_layer][digest]['buffer-to-pour'] = float( custom_buffer) gerber_lp = route.get('{' + config.cfg['ns']['pcbmode'] + '}gerber-lp') if gerber_lp != None: routes_dict[pcb_layer][digest]['gerber-lp'] = gerber_lp routing_dict['routes'] = routes_dict # Create simple stats and display them total = 0 total_old = 0 new = 0 existing = 0 for pcb_layer in config.stk['layer-names']: try: total += len(routing_dict['routes'][pcb_layer]) except: pass try: new_dict = routing_dict['routes'][pcb_layer] except: new_dict = {} try: old_dict = routing_dict_old['routes'][pcb_layer] except: old_dict = {} for key in new_dict: if key not in old_dict: new += 1 else: existing += 1 for pcb_layer in config.stk['layer-names']: total_old += len(old_dict) message = "Extracted %s routes; %s new (or modified), %s existing" % ( total, new, existing) if total_old > total: message += ", %s removed" % (total_old - total) msg.subInfo(message) #------------------------------- # Extract vias #------------------------------- xpath_expr_place = '//svg:g[@pcbmode:pcb-layer="%s"]//svg:g[@pcbmode:sheet="placement"]//svg:g[@pcbmode:type="via"]' vias_dict = {} for pcb_layer in config.stk['surface-layer-names']: # Find all markers markers = svg_in.findall(xpath_expr_place % pcb_layer, namespaces={ 'pcbmode': config.cfg['ns']['pcbmode'], 'svg': config.cfg['ns']['svg'] }) for marker in markers: transform_data = utils.parseTransform(marker.get('transform')) location = transform_data['location'] # Invert 'y' coordinate location.y *= config.cfg['invert-y'] # Change component rotation if needed if transform_data['type'] == 'matrix': rotate = transform_data['rotate'] rotate = utils.niceFloat((rotate) % 360) digest = utils.digest("%s%s" % (location.x, location.y)) # Define a via, just like any other component, but disable # placement of refdef vias_dict[digest] = {} vias_dict[digest]['footprint'] = marker.get( '{' + config.cfg['ns']['pcbmode'] + '}footprint') vias_dict[digest]['location'] = [ utils.niceFloat(location.x), utils.niceFloat(location.y) ] vias_dict[digest]['layer'] = 'top' routing_dict['vias'] = vias_dict # Display stats if len(vias_dict) == 0: msg.subInfo("No vias found") elif len(vias_dict) == 1: msg.subInfo("Extracted 1 via") else: msg.subInfo("Extracted %s vias" % (len(vias_dict))) # Save extracted routing into routing file try: with open(output_file, 'wb') as f: f.write(json.dumps(routing_dict, sort_keys=True, indent=2)) except: msg.error("Cannot save file %s" % output_file) return
def make_bom(quantity=None): """ """ def natural_key(string_): """See http://www.codinghorror.com/blog/archives/001018.html""" return [ int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_) ] dnp_text = 'Do not populate' uncateg_text = 'Uncategorised' components_dict = config.brd['components'] bom_dict = {} for refdef in components_dict: description = '' try: place = components_dict[refdef]['place'] except: place = True try: ignore = components_dict[refdef]['bom']['ignore'] except: ignore = False # If component isn't placed, ignore it if place == True and ignore == False: # Get footprint definition and shapes try: footprint_name = components_dict[refdef]['footprint'] except: msg.error("Cannot find a 'footprint' name for refdef %s." % refdef) # Open footprint file fname = os.path.join(config.cfg['base-dir'], config.cfg['locations']['components'], footprint_name + '.json') footprint_dict = utils.dictFromJsonFile(fname) info_dict = footprint_dict.get('info') or {} try: comp_bom_dict = components_dict[refdef]['bom'] except: comp_bom_dict = {} try: fp_bom_dict = footprint_dict['info'] except: fp_bom_dict = {} # Override component BoM info on top of footprint info for key in comp_bom_dict: fp_bom_dict[key] = comp_bom_dict[key] description = fp_bom_dict.get('description') or uncateg_text try: dnp = components_dict[refdef]['bom']['dnp'] except: dnp = False if dnp == True: description = dnp_text if description not in bom_dict: bom_dict[description] = fp_bom_dict bom_dict[description]['refdefs'] = [] bom_dict[description]['refdefs'].append(refdef) bom_dict[description]['placement'] = components_dict[refdef][ 'layer'] try: bom_content = config.brd['bom'] except: bom_content = [{ "field": "line-item", "text": "#" }, { "field": "quantity", "text": "Qty" }, { "field": "designators", "text": "Designators" }, { "field": "description", "text": "Description" }, { "field": "package", "text": "Package" }, { "field": "manufacturer", "text": "Manufacturer" }, { "field": "part-number", "text": "Part #" }, { "field": "suppliers", "text": "Suppliers", "suppliers": [{ "field": "farnell", "text": "Farnell #", "search-url": "http://uk.farnell.com/catalog/Search?st=" }, { "field": "mouser", "text": "Mouser #", "search-url": "http://uk.mouser.com/Search/Refine.aspx?Keyword=" }, { "field": "octopart", "text": "Octopart", "search-url": "https://octopart.com/search?q=" }] }, { "field": "placement", "text": "Layer" }, { "field": "notes", "text": "Notes" }] # Set up the BoM file name bom_path = os.path.join(config.cfg['base-dir'], config.cfg['locations']['build'], 'bom') # Create path if it doesn't exist already utils.create_dir(bom_path) board_name = config.cfg['name'] board_revision = config.brd['config'].get('rev') base_name = "%s_rev_%s" % (board_name, board_revision) bom_html = os.path.join(bom_path, base_name + '_%s.html' % 'bom') bom_csv = os.path.join(bom_path, base_name + '_%s.csv' % 'bom') html = [] csv = [] html.append('<html>') html.append('<style type="text/css">') try: css = config.stl['layout']['bom']['css'] except: css = [] for line in css: html.append(line) html.append('</style>') html.append('<table class="tg">') header = [] for item in bom_content: if item['field'] == 'suppliers': for supplier in item['suppliers']: header.append("%s" % supplier['text']) else: header.append("%s" % item['text']) if item['field'] == 'quantity' and quantity != None: header.append("@%s" % quantity) html.append(' <tr>') html.append( ' <th class="tg-title" colspan="%s">Bill of materials -- %s rev %s</th>' % (len(header), board_name, board_revision)) html.append(' </tr>') html.append(' <tr>') for item in header: html.append(' <th class="tg-header">%s</th>' % item) html.append(' </tr>') uncateg_content = [] dnp_content = [] index = 1 for desc in bom_dict: content = [] for item in bom_content: if item['field'] == 'line-item': content.append("<strong>%s</strong>" % str(index)) elif item['field'] == 'suppliers': for supplier in item['suppliers']: try: number = bom_dict[desc][item['field']][ supplier['field']] except: number = "" search_url = supplier.get('search-url') if search_url != None: content.append('<a href="%s%s">%s</a>' % (search_url, number, number)) else: content.append(number) elif item['field'] == 'quantity': units = len(bom_dict[desc]['refdefs']) content.append("%s" % (str(units))) if quantity != None: content.append("%s" % (str(units * int(quantity)))) elif item['field'] == 'designators': # Natural/human sort the list of designators sorted_list = sorted(bom_dict[desc]['refdefs'], key=natural_key) refdefs = '' for refdef in sorted_list[:-1]: refdefs += "%s " % refdef refdefs += "%s" % sorted_list[-1] content.append("%s " % refdefs) elif item['field'] == 'description': content.append("%s " % desc) else: try: content.append(bom_dict[desc][item['field']]) except: content.append("") if desc == uncateg_text: uncateg_content = content elif desc == dnp_text: dnp_content = content else: html.append(' <tr>') for item in content: html.append(' <td class="tg-item-%s">%s</td>' % (('odd', 'even')[index % 2 == 0], item)) html.append(' </tr>') index += 1 for content in (dnp_content, uncateg_content): html.append(' <tr class="tg-skip">') html.append(' </tr>') html.append(' <tr>') if len(content) > 0: content[0] = index for item in content: html.append(' <td class="tg-item-%s">%s</td>' % (('odd', 'even')[index % 2 == 0], item)) html.append(' </tr>') index += 1 html.append('</table>') html.append( '<p>Generated by <a href="http://pcbmode.com">PCBmodE</a>, an open source PCB design software. PCBmodE was written and is maintained by <a href="http://boldport.com">Boldport</a>, creators of beautifully functional circuits.' ) html.append('</html>') with open(bom_html, "wb") as f: for line in html: f.write(line + '\n')