Skip to content

Commit cd2e70b

Browse files
committed
Implement custom output name format with substitutions
Issue #67
1 parent e5264b2 commit cd2e70b

File tree

6 files changed

+597
-107
lines changed

6 files changed

+597
-107
lines changed

InteractiveHtmlBom/config.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Config:
3737

3838
# General section
3939
bom_dest_dir = 'bom/' # This is relative to pcb file directory
40+
bom_name_format = 'ibom'
4041
component_sort_order = default_sort_order
4142
component_blacklist = []
4243
blacklist_virtual = True
@@ -76,6 +77,7 @@ def __init__(self):
7677

7778
f.SetPath('/general')
7879
self.bom_dest_dir = f.Read('bom_dest_dir', self.bom_dest_dir)
80+
self.bom_name_format = f.Read('bom_name_format', self.bom_name_format)
7981
self.component_sort_order = self._split(f.Read(
8082
'component_sort_order',
8183
','.join(self.component_sort_order)))
@@ -121,6 +123,7 @@ def save(self):
121123
bom_dest_dir = os.path.relpath(
122124
bom_dest_dir, self.netlist_initial_directory)
123125
f.Write('bom_dest_dir', bom_dest_dir)
126+
f.Write('bom_name_format', self.bom_name_format)
124127
f.Write('component_sort_order',
125128
','.join(self.component_sort_order))
126129
f.Write('component_blacklist',
@@ -154,6 +157,7 @@ def set_from_dialog(self, dlg):
154157

155158
# General
156159
self.bom_dest_dir = dlg.general.bomDirPicker.Path
160+
self.bom_name_format = dlg.general.fileNameFormatTextControl.Value
157161
self.component_sort_order = dlg.general.componentSortOrderBox.GetItems()
158162
self.component_blacklist = dlg.general.blacklistBox.GetItems()
159163
self.blacklist_virtual = \
@@ -197,6 +201,7 @@ def transfer_to_dialog(self, dlg):
197201
else:
198202
dlg.general.bomDirPicker.Path = os.path.join(
199203
self.netlist_initial_directory, self.bom_dest_dir)
204+
dlg.general.fileNameFormatTextControl.Value = self.bom_name_format
200205
dlg.general.componentSortOrderBox.SetItems(self.component_sort_order)
201206
dlg.general.blacklistBox.SetItems(self.component_blacklist)
202207
dlg.general.blacklistVirtualCheckbox.Value = self.blacklist_virtual
@@ -222,8 +227,8 @@ def safe_set_checked_strings(clb, strings):
222227
dlg.finish_init()
223228

224229
# noinspection PyTypeChecker
225-
def add_options(self, parser):
226-
# type: (argparse.ArgumentParser) -> None
230+
def add_options(self, parser, file_name_format_hint):
231+
# type: (argparse.ArgumentParser, str) -> None
227232
parser.add_argument('--show-dialog', action='store_true',
228233
help='Shows config dialog. All other flags '
229234
'will be ignored.')
@@ -259,6 +264,8 @@ def add_options(self, parser):
259264
parser.add_argument('--dest-dir', default=self.bom_dest_dir,
260265
help='Destination directory for bom file '
261266
'relative to pcb file directory.')
267+
parser.add_argument('--name-format', default=self.bom_name_format,
268+
help=file_name_format_hint.replace('%', '%%'))
262269
parser.add_argument('--sort-order',
263270
help='Default sort order for components. '
264271
'Must contain "~" once.',
@@ -310,6 +317,7 @@ def set_from_args(self, args):
310317

311318
# General
312319
self.bom_dest_dir = args.dest_dir
320+
self.bom_name_format = args.name_format
313321
self.component_sort_order = self._split(args.sort_order)
314322
self.component_blacklist = self._split(args.blacklist)
315323
self.blacklist_virtual = not args.no_blacklist_virtual
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .settings_dialog import SettingsDialog
1+
from .settings_dialog import SettingsDialog, GeneralSettingsPanel

InteractiveHtmlBom/dialog/dialog_base.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,39 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
190190

191191
bSizer32 = wx.BoxSizer( wx.VERTICAL )
192192

193-
sbSizer6 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Bom destination directory" ), wx.VERTICAL )
193+
sbSizer6 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Bom destination" ), wx.VERTICAL )
194+
195+
fgSizer1 = wx.FlexGridSizer( 0, 2, 0, 0 )
196+
fgSizer1.AddGrowableCol( 1 )
197+
fgSizer1.SetFlexibleDirection( wx.BOTH )
198+
fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )
199+
200+
self.m_staticText8 = wx.StaticText( sbSizer6.GetStaticBox(), wx.ID_ANY, u"Directory", wx.DefaultPosition, wx.DefaultSize, 0 )
201+
self.m_staticText8.Wrap( -1 )
202+
203+
fgSizer1.Add( self.m_staticText8, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
194204

195205
self.bomDirPicker = wx.DirPickerCtrl( sbSizer6.GetStaticBox(), wx.ID_ANY, wx.EmptyString, u"Select bom folder", wx.DefaultPosition, wx.DefaultSize, wx.DIRP_SMALL|wx.DIRP_USE_TEXTCTRL|wx.BORDER_SIMPLE )
196-
sbSizer6.Add( self.bomDirPicker, 0, wx.EXPAND|wx.BOTTOM|wx.RIGHT|wx.LEFT, 5 )
206+
fgSizer1.Add( self.bomDirPicker, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND, 5 )
207+
208+
self.m_staticText9 = wx.StaticText( sbSizer6.GetStaticBox(), wx.ID_ANY, u"Name format", wx.DefaultPosition, wx.DefaultSize, 0 )
209+
self.m_staticText9.Wrap( -1 )
210+
211+
fgSizer1.Add( self.m_staticText9, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
212+
213+
bSizer20 = wx.BoxSizer( wx.HORIZONTAL )
214+
215+
self.fileNameFormatTextControl = wx.TextCtrl( sbSizer6.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
216+
bSizer20.Add( self.fileNameFormatTextControl, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND, 5 )
217+
218+
self.m_button12 = wx.Button( sbSizer6.GetStaticBox(), wx.ID_ANY, u"?", wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
219+
bSizer20.Add( self.m_button12, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 4 )
220+
221+
222+
fgSizer1.Add( bSizer20, 1, wx.EXPAND, 5 )
223+
224+
225+
sbSizer6.Add( fgSizer1, 1, wx.EXPAND, 5 )
197226

198227

199228
bSizer32.Add( sbSizer6, 0, wx.ALL|wx.EXPAND, 5 )
@@ -282,6 +311,7 @@ def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.
282311
bSizer32.Fit( self )
283312

284313
# Connect Events
314+
self.m_button12.Bind( wx.EVT_BUTTON, self.OnNameFormatHintClick )
285315
self.m_button1.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderUp )
286316
self.m_button2.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderDown )
287317
self.m_button3.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderAdd )
@@ -294,6 +324,9 @@ def __del__( self ):
294324

295325

296326
# Virtual event handlers, overide them in your derived class
327+
def OnNameFormatHintClick( self, event ):
328+
event.Skip()
329+
297330
def OnComponentSortOrderUp( self, event ):
298331
event.Skip()
299332

InteractiveHtmlBom/dialog/settings_dialog.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ def OnBoardRotationSlider(self, event):
7474

7575
# Implementing GeneralSettingsPanelBase
7676
class GeneralSettingsPanel(dialog_base.GeneralSettingsPanelBase):
77+
FILE_NAME_FORMAT_HINT = (
78+
'Output file name format supports substitutions:\n' +
79+
'\n' +
80+
' %f : original pcb file name without extension.\n' +
81+
' %p : pcb/project title from pcb metadata.\n' +
82+
' %c : company from pcb metadata.\n' +
83+
' %r : revision from pcb metadata.\n' +
84+
' %d : pcb date from metadata if available, ' +
85+
'file modification date otherwise.\n' +
86+
' %D : bom generation date.\n' +
87+
' %T : bom generation time.\n' +
88+
'\n' +
89+
'Extension .html will be added automatically.'
90+
) # type: str
91+
7792
def __init__(self, parent):
7893
dialog_base.GeneralSettingsPanelBase.__init__(self, parent)
7994

@@ -143,6 +158,10 @@ def OnComponentBlacklistRemove(self, event):
143158
if self.blacklistBox.Count > 0:
144159
self.blacklistBox.SetSelection(max(selection - 1, 0))
145160

161+
def OnNameFormatHintClick(self, event):
162+
wx.MessageBox(self.FILE_NAME_FORMAT_HINT, 'File name format help',
163+
style=wx.ICON_NONE|wx.OK)
164+
146165

147166
# Implementing ExtraFieldsPanelBase
148167
class ExtraFieldsPanel(dialog_base.ExtraFieldsPanelBase):

InteractiveHtmlBom/generate_interactive_bom.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,22 @@ def open_file(filename):
511511
logwarn('Failed to open browser: {}'.format(oe.message))
512512

513513

514-
def generate_file(pcb_file_dir, pcbdata, config):
514+
def process_substitutions(bom_name_format, pcb_file_name, metadata):
515+
# type: (str, str, dict)->str
516+
name = bom_name_format.replace('%f', os.path.splitext(pcb_file_name)[0])
517+
name = name.replace('%p', metadata['title'])
518+
name = name.replace('%c', metadata['company'])
519+
name = name.replace('%r', metadata['revision'])
520+
name = name.replace('%d', metadata['date'].replace(':', '-'))
521+
now = datetime.now()
522+
name = name.replace('%D', now.strftime('%Y-%m-%d'))
523+
name = name.replace('%T', now.strftime('%H-%M-%S'))
524+
# sanitize the name to avoid characters illegal in file systems
525+
name = re.sub(r'[/\\?%*:|"<>]', '_', name)
526+
return name + '.html'
527+
528+
529+
def generate_file(pcb_file_dir, pcb_file_name, pcbdata, config):
515530
def get_file_content(file_name):
516531
path = os.path.join(os.path.dirname(__file__), "web", file_name)
517532
with open(path, "r") as f:
@@ -520,12 +535,14 @@ def get_file_content(file_name):
520535
loginfo("Dumping pcb json data")
521536

522537
if os.path.isabs(config.bom_dest_dir):
523-
bom_file_name = config.bom_dest_dir
538+
bom_file_dir = config.bom_dest_dir
524539
else:
525-
bom_file_name = os.path.join(pcb_file_dir, config.bom_dest_dir)
526-
if not os.path.isdir(bom_file_name):
527-
os.makedirs(bom_file_name)
528-
bom_file_name = os.path.join(bom_file_name, "ibom.html")
540+
bom_file_dir = os.path.join(pcb_file_dir, config.bom_dest_dir)
541+
if not os.path.isdir(bom_file_dir):
542+
os.makedirs(bom_file_dir)
543+
bom_file_name = process_substitutions(
544+
config.bom_name_format, pcb_file_name, pcbdata['metadata'])
545+
bom_file_name = os.path.join(bom_file_dir, bom_file_name)
529546
pcbdata_js = "var pcbdata = " + json.dumps(pcbdata)
530547
config_js = "var config = " + config.get_html_config()
531548
html = get_file_content("ibom.html")
@@ -579,10 +596,10 @@ def main(pcb, config):
579596
file_date = datetime.fromtimestamp(file_mtime).strftime(
580597
'%Y-%m-%d %H:%M:%S')
581598
title = title_block.GetTitle()
599+
pcb_file_name = os.path.basename(pcb_file_name)
582600
if not title:
583-
title = os.path.basename(pcb_file_name)
584601
# remove .kicad_pcb extension
585-
title = os.path.splitext(title)[0]
602+
title = os.path.splitext(pcb_file_name)[0]
586603
edges, bbox = parse_edges(pcb)
587604
if bbox is None:
588605
logerror('Please draw pcb outline on the edges '
@@ -619,7 +636,7 @@ def main(pcb, config):
619636
pcbdata["bom"]["F" if layer == pcbnew.F_Cu else "B"] = bom_table
620637

621638
pcbdata["font_data"] = font_parser.get_parsed_font()
622-
bom_file = generate_file(pcb_file_dir, pcbdata, config)
639+
bom_file = generate_file(pcb_file_dir, pcb_file_name, pcbdata, config)
623640

624641
if config.open_browser:
625642
loginfo("Opening file in browser")
@@ -689,7 +706,8 @@ def Run(self):
689706
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
690707
parser.add_argument('file', type=str, help="KiCad PCB file")
691708
config = Config()
692-
config.add_options(parser)
709+
config.add_options(
710+
parser, dialog.GeneralSettingsPanel.FILE_NAME_FORMAT_HINT)
693711
args = parser.parse_args()
694712
if not os.path.isfile(args.file.decode('utf8')):
695713
print("File %s does not exist." % args.file)

0 commit comments

Comments
 (0)