Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DATAFORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pcbdata = {
"revision": "rev",
"company": "Horns and Hoofs",
"date": "2019-04-18",
"variant": "var" // optional
},
// Contains full bom table as well as filtered by front/back.
// See bom row description below.
Expand Down Expand Up @@ -258,7 +259,10 @@ attribute.
// 0: justify center
// 1: justify right/bot
"justify": [horizontal, vertical],
// Either the thickness or the fillrule must be used
"thickness": thickness,
// fillrule is only supported for svgpath
"fillrule": "nonzero" | "evenodd",
"attr": [
// may include none, one or both
"italic", "mirrored"
Expand Down
4 changes: 2 additions & 2 deletions InteractiveHtmlBom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import threading
import time

import wx
import wx.aui
from .compat import get_wx


def check_for_bom_button():
wx = get_wx()
# From Miles McCoo's blog
# https://kicad.mmccoo.com/2017/03/05/adding-your-own-command-buttons-to-the-pcbnew-gui/
def find_pcbnew_window():
Expand Down
23 changes: 23 additions & 0 deletions InteractiveHtmlBom/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Compatibility utilities for optional wx dependency."""

import os

_wx = None


def should_create_wx_app():
"""Check if we should create a wx app based on environment."""
return 'INTERACTIVE_HTML_BOM_NO_DISPLAY' not in os.environ


def get_wx():
"""Get the wx module, or None if not available."""
global _wx
if _wx is not None:
return _wx
try:
import wx
_wx = wx
return _wx
except ImportError:
return None
187 changes: 178 additions & 9 deletions InteractiveHtmlBom/core/config.py

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions InteractiveHtmlBom/core/ibom.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
import sys
from datetime import datetime

import wx

from . import units
from .config import Config
from ..dialog import SettingsDialog
from ..ecad.common import EcadParser, Component
from ..errors import ParsingException
from ..compat import get_wx


class Logger(object):
Expand All @@ -38,13 +36,13 @@ def error(self, msg):
if self.cli:
self.logger.error(msg)
else:
wx.MessageBox(msg)
get_wx().MessageBox(msg)

def warn(self, msg):
if self.cli:
self.logger.warning(msg)
else:
wx.LogWarning(msg)
get_wx().LogWarning(msg)


log = None
Expand Down Expand Up @@ -234,6 +232,8 @@ def process_substitutions(bom_name_format, pcb_file_name, metadata):
name = name.replace('%c', metadata['company'])
name = name.replace('%r', metadata['revision'])
name = name.replace('%d', metadata['date'].replace(':', '-'))
name = name.replace('%v', metadata.get('variant', ''))
name = name.replace('%V', metadata.get('variant', '') or 'default')
now = datetime.now()
name = name.replace('%D', now.strftime('%Y-%m-%d'))
name = name.replace('%T', now.strftime('%H-%M-%S'))
Expand Down Expand Up @@ -341,6 +341,8 @@ def main(parser, config, logger):

def run_with_dialog(parser, config, logger):
# type: (EcadParser, Config, Logger) -> None
from ..dialog import SettingsDialog

def save_config(dialog_panel, locally=False):
config.set_from_dialog(dialog_panel)
config.save(locally)
Expand All @@ -358,7 +360,7 @@ def save_config(dialog_panel, locally=False):
if extra_data_file is not None:
dlg.set_extra_data_path(extra_data_file)
config.transfer_to_dialog(dlg.panel)
if dlg.ShowModal() == wx.ID_OK:
if dlg.ShowModal() == get_wx().ID_OK:
config.set_from_dialog(dlg.panel)
main(parser, config, logger)
finally:
Expand Down
9 changes: 8 additions & 1 deletion InteractiveHtmlBom/dialog/dialog_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-

###########################################################################
## Python code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3)
## Python code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
## http://www.wxformbuilder.org/
##
## PLEASE DO *NOT* EDIT THIS FILE!
Expand Down Expand Up @@ -413,6 +413,13 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
self.extraDataFilePicker = wx.FilePickerCtrl( sbSizer7.GetStaticBox(), wx.ID_ANY, wx.EmptyString, u"Select a file", u"Netlist and xml files (*.net; *.xml)|*.net;*.xml", wx.DefaultPosition, wx.DefaultSize, wx.FLP_DEFAULT_STYLE|wx.FLP_FILE_MUST_EXIST|wx.FLP_OPEN|wx.FLP_SMALL|wx.FLP_USE_TEXTCTRL|wx.BORDER_SIMPLE )
sbSizer7.Add( self.extraDataFilePicker, 0, wx.EXPAND|wx.BOTTOM|wx.RIGHT|wx.LEFT, 5 )

self.variantLabel = wx.StaticText( sbSizer7.GetStaticBox(), wx.ID_ANY, u"Current variant:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.variantLabel.Wrap( -1 )

self.variantLabel.Hide()

sbSizer7.Add( self.variantLabel, 0, wx.ALL, 5 )


bSizer42.Add( sbSizer7, 0, wx.ALL|wx.EXPAND, 5 )

Expand Down
5 changes: 4 additions & 1 deletion InteractiveHtmlBom/dialog/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,11 @@ def OnExtraDataFileChanged(self, event):
self.extra_field_data = self.extra_data_func(
extra_data_file, self.normalizeCaseCheckbox.Value)
except Exception as e:
import traceback
pop_error(
"Failed to parse file %s\n\n%s" % (extra_data_file, e))
"Failed to parse file %s\n\n%s" % (
extra_data_file,
str(e) + '\n' + traceback.format_exc()))
self.extraDataFilePicker.Path = ''

if self.extra_field_data is not None:
Expand Down
72 changes: 53 additions & 19 deletions InteractiveHtmlBom/ecad/kicad.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ def __init__(self, file_name, config, logger, board=None):
self.board = board
if self.board is None:
self.board = pcbnew.LoadBoard(self.file_name) # type: pcbnew.BOARD
if not self.board:
raise Exception('Failed to load board file')
if hasattr(self.board, "SetCurrentVariant"):
self.board.SetCurrentVariant(config.kicad_variant)
if hasattr(self.board, 'GetModules'):
self.footprints = list(self.board.GetModules()) # type: list[pcbnew.MODULE]
# type: list[pcbnew.MODULE]
self.footprints = list(self.board.GetModules())
else:
self.footprints = list(self.board.GetFootprints()) # type: list[pcbnew.FOOTPRINT]
# type: list[pcbnew.FOOTPRINT]
self.footprints = list(self.board.GetFootprints())
self.font_parser = FontParser()

def get_extra_field_data(self, file_name):
Expand All @@ -46,8 +52,7 @@ def get_extra_field_data(self, file_name):

return ExtraFieldData(data[0], data[1])

@staticmethod
def get_footprint_fields(f):
def get_footprint_fields(self, f):
# type: (pcbnew.FOOTPRINT) -> dict
props = {}
if hasattr(f, "GetProperties"):
Expand All @@ -60,6 +65,14 @@ def get_footprint_fields(f):
if hasattr(f, "IsDNP"):
if f.IsDNP():
props["kicad_dnp"] = "DNP"
if hasattr(f, 'GetVariant'):
variant = f.GetVariant(self.config.kicad_variant)
if variant:
var_fields = variant.GetFields()
for k in var_fields.keys():
props[str(k)] = str(f.GetFieldShownText(str(k)))
props["kicad_dnp"] = "DNP" if variant.GetDNP() else ""

return props

def parse_extra_data_from_pcb(self):
Expand Down Expand Up @@ -209,7 +222,8 @@ def parse_shape(self, d):
"angle": angle,
"polygons": polygons
}
if hasattr(d, "IsFilled") and not d.IsFilled():
if ((hasattr(d, "IsFilled") and not d.IsFilled()) or
(hasattr(d, "IsSolidFill") and not d.IsSolidFill())):
shape_dict["filled"] = 0
shape_dict["width"] = d.GetWidth() * 1e-6
return shape_dict
Expand Down Expand Up @@ -273,7 +287,8 @@ def parse_text(self, d):
"svgpath": create_path(lines)
}
elif hasattr(d, 'GetEffectiveTextShape'):
shape = d.GetEffectiveTextShape(False) # type: pcbnew.SHAPE_COMPOUND
# type: pcbnew.SHAPE_COMPOUND
shape = d.GetEffectiveTextShape(False)
segments = []
polygons = []
for s in shape.GetSubshapes():
Expand Down Expand Up @@ -426,7 +441,7 @@ def get_all_drawings(self):
for d in f.GraphicalItems():
drawings.append((d.GetClass(), d))
if hasattr(f, "GetFields"):
fields = f.GetFields() # type: list[pcbnew.PCB_FIELD]
fields = f.GetFields() # type: list[pcbnew.PCB_FIELD]
for field in fields:
if field.IsReference() or field.IsValue():
continue
Expand All @@ -450,11 +465,15 @@ def parse_pad(self, pad):
custom_padstack = False
outer_layers = [(pcbnew.F_Cu, "F"), (pcbnew.B_Cu, "B")]
if hasattr(pad, 'Padstack'):
padstack = pad.Padstack() # type: pcbnew.PADSTACK
padstack = pad.Padstack() # type: pcbnew.PADSTACK
layers_set = list(padstack.LayerSet().Seq())
if hasattr(pcbnew, "UNCONNECTED_LAYER_MODE_REMOVE_ALL"):
ULMRA = pcbnew.UNCONNECTED_LAYER_MODE_REMOVE_ALL
else:
ULMRA = padstack.UNCONNECTED_LAYER_MODE_REMOVE_ALL
custom_padstack = (
padstack.Mode() != padstack.MODE_NORMAL or \
padstack.UnconnectedLayerMode() == padstack.UNCONNECTED_LAYER_MODE_REMOVE_ALL
padstack.Mode() != padstack.MODE_NORMAL or
padstack.UnconnectedLayerMode() == ULMRA
)
else:
layers_set = list(pad.GetLayerSet().Seq())
Expand All @@ -474,7 +493,8 @@ def parse_pad(self, pad):
pads.append(pad_dict)
return pads
else:
pad_dict = self.parse_pad_layer(pad, layers_set[0])
pad_layer = layers_set[0] if layers_set else pcbnew.F_Cu
pad_dict = self.parse_pad_layer(pad, pad_layer)
pad_dict["layers"] = layers
return [pad_dict]

Expand Down Expand Up @@ -715,6 +735,8 @@ def parse_zones(self, zones):
if (hasattr(zone, 'GetFilledPolysUseThickness') and
not zone.GetFilledPolysUseThickness()):
width = 0
if KICAD_VERSION[0] >= 7:
width = 0
zone_dict = {
"polygons": self.parse_poly_set(poly_set),
"width": width,
Expand All @@ -735,16 +757,24 @@ def parse_netlist(net_info):
nets = sorted([str(s) for s in nets])
return nets

@staticmethod
def footprint_to_component(footprint, extra_fields):
def footprint_to_component(self, footprint, extra_fields):
# type: (pcbnew.FOOTPRINT, list) -> Component
try:
footprint_name = str(footprint.GetFPID().GetFootprintName())
except AttributeError:
footprint_name = str(footprint.GetFPID().GetLibItemName())

value = footprint.GetValue()
if hasattr(footprint, 'GetFieldValueForVariant'):
value = footprint.GetFieldValueForVariant(
self.config.kicad_variant, 'Value')
attr = 'Normal'
if hasattr(pcbnew, 'FP_EXCLUDE_FROM_BOM'):
if footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
if hasattr(footprint, 'GetExcludedFromBOMForVariant'):
if footprint.GetExcludedFromBOMForVariant(
self.config.kicad_variant):
attr = 'Virtual'
elif footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
attr = 'Virtual'
elif hasattr(pcbnew, 'MOD_VIRTUAL'):
if footprint.GetAttributes() == pcbnew.MOD_VIRTUAL:
Expand All @@ -755,7 +785,7 @@ def footprint_to_component(footprint, extra_fields):
}.get(footprint.GetLayer())

return Component(footprint.GetReference(),
footprint.GetValue(),
value,
footprint_name,
layer,
attr,
Expand All @@ -774,9 +804,9 @@ def parse(self):
self.config.dnp_field)

if not self.config.extra_data_file and need_extra_fields:
self.logger.warn('Ignoring extra fields related config parameters '
'since no netlist/xml file was specified.')
need_extra_fields = False
self.config.extra_data_file = self.file_name
self.logger.warn('Assuming extra data file to be the pcb file '
'since --extra-data-file was not specified.')

extra_field_data = None
if (self.config.extra_data_file and
Expand Down Expand Up @@ -839,6 +869,7 @@ def parse(self):
"revision": revision,
"company": company,
"date": file_date,
"variant": self.config.kicad_variant,
},
"bom": {},
"font_data": self.font_parser.get_parsed_font()
Expand Down Expand Up @@ -905,14 +936,17 @@ def Run(self):
from ..errors import ParsingException

logger = ibom.Logger()
board = pcbnew.GetBoard()
board = pcbnew.GetBoard() # type: pcbnew.BOARD
pcb_file_name = board.GetFileName()

if not pcb_file_name:
logger.error('Please save the board file before generating BOM.')
return

config = Config(version, os.path.dirname(pcb_file_name))
if hasattr(board, 'GetCurrentVariant'):
config.kicad_variant = board.GetCurrentVariant()

parser = PcbnewParser(pcb_file_name, config, logger, board)

try:
Expand Down
17 changes: 14 additions & 3 deletions InteractiveHtmlBom/ecad/schema/genericjsonpcbdata_v1.schema
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@
},
"date": {
"type": "string"
},
"variant": {
"type": "string"
}
},
"required": ["title", "revision", "company", "date"],
Expand Down Expand Up @@ -443,12 +446,20 @@
"properties": {
"svgpath": { "type": "string" },
"thickness": { "type": "number" },
"fillrule": {
"type": "string",
"enum": [
"nonzero",
"evenodd"
]
},
"ref": { "type": "integer" , "const": 1 },
"val": { "type": "integer" , "const": 1 }
},
"required": [
"svgpath",
"thickness"
"required": ["svgpath"],
"oneOf": [
{ "required": ["thickness"] },
{ "required": ["fillrule"] }
],
"title": "DrawingText"
},
Expand Down
Loading