diff --git a/README.md b/README.md
index 3763425..546d8da 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
-
+
@@ -55,13 +62,13 @@ Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's
| 'LCSC Part #' | 'LCSC Part' | 'JLCPCB Part #' | 'JLCPCB Part' |
| --- | --- | --- | --- |
-_The fields will be query in the order denoted above._
+_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'LCSC' | 'JLC' | 'MPN' | 'Mpn' | 'mpn' |
| --- | --- | --- | --- | --- |
-_The fields will be query in the order denoted above._
+_The fields will be queried in the order denoted above._
---
@@ -102,7 +109,7 @@ _The fields will be queried in the order denoted above._
---
### ④ Offset Component Position
-The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPCB used different variation of the same standard. To the exception cases: add an 'FT Position Offset'* field with an comma separated x,y position offset to correct it.
+The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPCB used different variation of the same standard. To the exception cases: add an 'FT Position Offset'* field with an comma separated x,y position offset to correct it.
Use following table to quickly find out to which coordinate enter the correction based on JLC arrows clicks - depending on footprint rotation in KiCad PCB Editor status bar:
|KiCad footprint deg | x | y|
@@ -134,7 +141,7 @@ _The fields will be queried in the order denoted above._
_The fields will be queried in the order denoted above._
### ⑤ Override Component Origin
-The Fabrication Toolkit reports the position of each component based on an automatically selected point of reference. This default behavior can be overridden by adding an 'FT Origin'* field to the component.
+The Fabrication Toolkit reports the position of each component based on an automatically selected point of reference. This default behavior can be overridden by adding an 'FT Origin'* field to the component.
#### Primary Fields*:
| 'FT Origin' |
diff --git a/assets/options.png b/assets/options.png
index 0fa55eb..7eb078e 100644
Binary files a/assets/options.png and b/assets/options.png differ
diff --git a/plugins/cli.py b/plugins/cli.py
index 3b3dd17..d052818 100644
--- a/plugins/cli.py
+++ b/plugins/cli.py
@@ -2,7 +2,7 @@
from .thread import ProcessThread
from .options import *
-
+
if __name__ == '__main__':
parser = ap.ArgumentParser(prog="Fabrication Toolkit",
@@ -17,6 +17,7 @@
parser.add_argument("--excludeDNP", "-e", action="store_true", help="Exclude DNP components from BOM")
parser.add_argument("--allActiveLayers", "-aaL",action="store_true", help="Export all active layers instead of only commonly used ones")
parser.add_argument("--openBrowser", "-b", action="store_true", help="Open webbrowser with directory file overview after generation")
+ parser.add_argument("--pnColumn", "-c", type=str, help="bom.csv column name for Manufacturer Part Numbers")
args = parser.parse_args()
options = dict()
@@ -27,6 +28,7 @@
options[ALTERNATIVE_EDGE_CUT_OPT] = args.user2AltVCut
options[ALL_ACTIVE_LAYERS_OPT] = args.allActiveLayers
options[EXTRA_LAYERS] = args.additionalLayers
+ options[PN_COLUMN] = args.pnColumn
openBrowser = args.openBrowser
diff --git a/plugins/options.py b/plugins/options.py
index 27ae033..9b78b2e 100644
--- a/plugins/options.py
+++ b/plugins/options.py
@@ -5,4 +5,5 @@
EXTEND_EDGE_CUT_OPT = "EXTEND_EDGE_CUT"
ALTERNATIVE_EDGE_CUT_OPT = "ALTERNATIVE_EDGE_CUT"
ALL_ACTIVE_LAYERS_OPT = "ALL_ACTIVE_LAYERS"
-EXTRA_LAYERS = "EXTRA_LAYERS"
\ No newline at end of file
+EXTRA_LAYERS = "EXTRA_LAYERS"
+PN_COLUMN = "PN_COLUMN"
\ No newline at end of file
diff --git a/plugins/plugin.py b/plugins/plugin.py
index d94d94f..4330304 100644
--- a/plugins/plugin.py
+++ b/plugins/plugin.py
@@ -4,7 +4,9 @@
from .thread import ProcessThread
from .events import StatusEvent
-from .options import AUTO_FILL_OPT, AUTO_TRANSLATE_OPT, EXCLUDE_DNP_OPT, EXTEND_EDGE_CUT_OPT, ALTERNATIVE_EDGE_CUT_OPT, EXTRA_LAYERS, ALL_ACTIVE_LAYERS_OPT
+from .options import AUTO_FILL_OPT, AUTO_TRANSLATE_OPT, EXCLUDE_DNP_OPT, \
+ EXTEND_EDGE_CUT_OPT, ALTERNATIVE_EDGE_CUT_OPT, EXTRA_LAYERS, \
+ ALL_ACTIVE_LAYERS_OPT, PN_COLUMN
from .utils import load_user_options, save_user_options, get_layer_names
@@ -19,7 +21,7 @@ def __init__(self):
pos=wx.DefaultPosition,
size=wx.DefaultSize,
style=wx.DEFAULT_DIALOG_STYLE)
-
+
# self.app = wx.PySimpleApp()
icon = wx.Icon(os.path.join(os.path.dirname(__file__), 'icon.png'))
self.SetIcon(icon)
@@ -34,7 +36,8 @@ def __init__(self):
ALTERNATIVE_EDGE_CUT_OPT: False,
AUTO_TRANSLATE_OPT: True,
AUTO_FILL_OPT: True,
- EXCLUDE_DNP_OPT: False
+ EXCLUDE_DNP_OPT: False,
+ PN_COLUMN: "LCSC Part #"
})
self.mOptionsLabel = wx.StaticText(self, label='Options:')
@@ -58,6 +61,10 @@ def __init__(self):
self.mAutomaticFillCheckbox.SetValue(userOptions[AUTO_FILL_OPT])
self.mExcludeDnpCheckbox = wx.CheckBox(self, label='Exclude DNP components from BOM')
self.mExcludeDnpCheckbox.SetValue(userOptions[EXCLUDE_DNP_OPT])
+ self.mPNColStaticText = wx.StaticText(self, label="MPN column in bom.csv:")
+ self.mPNColumnListbox = wx.ListBox(self, style=wx.LB_SINGLE, choices=["LCSC Part #", "Comment"])
+ index = self.mPNColumnListbox.FindString(userOptions[PN_COLUMN])
+ self.mPNColumnListbox.SetSelection(index)
self.mGaugeStatus = wx.Gauge(
self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size(600, 20), wx.GA_HORIZONTAL)
@@ -78,6 +85,8 @@ def __init__(self):
boxSizer.Add(self.mAutomaticTranslationCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mAutomaticFillCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mExcludeDnpCheckbox, 0, wx.ALL, 5)
+ boxSizer.Add(self.mPNColStaticText, 0, wx.ALL | wx.ALIGN_LEFT, 5)
+ boxSizer.Add(self.mPNColumnListbox, 0, wx.ALL | wx.ALIGN_LEFT | wx.EXPAND, 5)
boxSizer.Add(self.mGaugeStatus, 0, wx.ALL, 5)
boxSizer.Add(self.mGenerateButton, 0, wx.ALL, 5)
@@ -107,7 +116,8 @@ def onGenerateButtonClick(self, event):
options[AUTO_TRANSLATE_OPT] = self.mAutomaticTranslationCheckbox.GetValue()
options[AUTO_FILL_OPT] = self.mAutomaticFillCheckbox.GetValue()
options[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
-
+ index = self.mPNColumnListbox.GetSelection()
+ options[PN_COLUMN] = self.mPNColumnListbox.GetString(index)
save_user_options(options)
self.mOptionsLabel.Hide()
@@ -118,6 +128,8 @@ def onGenerateButtonClick(self, event):
self.mAutomaticTranslationCheckbox.Hide()
self.mAutomaticFillCheckbox.Hide()
self.mExcludeDnpCheckbox.Hide()
+ self.mPNColStaticText.Hide();
+ self.mPNColumnListbox.Hide()
self.mGenerateButton.Hide()
self.mGaugeStatus.Show()
diff --git a/plugins/process.py b/plugins/process.py
index 91dfb7d..25546d5 100644
--- a/plugins/process.py
+++ b/plugins/process.py
@@ -67,7 +67,7 @@ def generate_gerber(self, temp_dir, extra_layers, extend_edge_cuts, alternative_
plot_options.SetSubtractMaskFromSilk(True)
plot_options.SetUseGerberX2format(False)
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
-
+
if hasattr(plot_options, "SetExcludeEdgeLayer"):
plot_options.SetExcludeEdgeLayer(True)
@@ -117,7 +117,7 @@ def generate_netlist(self, temp_dir):
netlist_writer = pcbnew.IPC356D_WRITER(self.board)
netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
- def _get_footprint_position(self, footprint):
+ def _get_footprint_position(self, footprint):
"""Calculate position based on center of pads / bounding box."""
origin_type = self._get_origin_from_footprint(footprint)
@@ -133,10 +133,10 @@ def _get_footprint_position(self, footprint):
position = bbox.GetCenter()
else:
position = footprint.GetPosition() # if we have no pads we fallback to anchor
-
+
return position
- def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
+ def generate_tables(self, temp_dir, auto_translate, exclude_dnp, pnColumn):
'''Generate the data tables.'''
if hasattr(self.board, 'GetModules'):
footprints = list(self.board.GetModules())
@@ -158,6 +158,8 @@ def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
for key, value in footprint_designators.items():
f.write('%s:%s\n' % (key, value))
+ part_column_name = pnColumn
+
for i, footprint in enumerate(footprints):
try:
footprint_name = str(footprint.GetFPID().GetFootprintName())
@@ -172,8 +174,8 @@ def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
# 2: 'unspecified'
# }.get(footprint.GetAttributes())
- is_dnp = (footprint_has_field(footprint, 'dnp')
- or (footprint.GetValue().upper() == 'DNP')
+ is_dnp = (footprint_has_field(footprint, 'dnp')
+ or (footprint.GetValue().upper() == 'DNP')
or getattr(footprint, 'IsDNP', bool)())
skip_dnp = exclude_dnp and is_dnp
@@ -236,7 +238,7 @@ def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
for component in self.bom:
same_footprint = component['Footprint'] == self._normalize_footprint_name(footprint_name)
same_value = component['Value'].upper() == footprint.GetValue().upper()
- same_lcsc = component['LCSC Part #'] == self._get_mpn_from_footprint(footprint)
+ same_lcsc = component[part_column_name] == self._get_mpn_from_footprint(footprint)
under_limit = component['Quantity'] < bomRowLimit
if same_footprint and same_value and same_lcsc and under_limit:
@@ -247,14 +249,15 @@ def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
# add component to BOM
if insert:
- self.bom.append({
+ fields = {
'Designator': "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id),
'Footprint': self._normalize_footprint_name(footprint_name),
'Quantity': 1,
'Value': footprint.GetValue(),
# 'Mount': mount_type,
- 'LCSC Part #': self._get_mpn_from_footprint(footprint),
- })
+ part_column_name: self._get_mpn_from_footprint(footprint),
+ }
+ self.bom.append(fields)
def generate_positions(self, temp_dir):
'''Generate the position file.'''
@@ -294,7 +297,7 @@ def generate_archive(self, temp_dir, temp_file):
os.remove(os.path.join(temp_dir, item))
return temp_file
-
+
""" Private """
def __read_rotation_db(self, filename: str = os.path.join(os.path.dirname(__file__), 'transformations.csv')) -> dict[str, float]:
@@ -352,7 +355,7 @@ def __read_rotation_db(self, filename: str = os.path.join(os.path.dirname(__file
db[rowNum]['name'] = row['footprint']
db[rowNum]['rotation'] = rotation
db[rowNum]['x'] = delta_x
- db[rowNum]['y'] = delta_y
+ db[rowNum]['y'] = delta_y
return db
@@ -405,7 +408,7 @@ def _get_position_offset_from_db(self, footprint: str) -> Tuple[float, float]:
return (0.0, 0.0)
def _get_mpn_from_footprint(self, footprint) -> str:
- ''''Get the MPN/LCSC stock code from standard symbol fields.'''
+ '''Get the MPN/LCSC stock code from standard symbol fields.'''
keys = ['LCSC Part #', 'LCSC Part', 'JLCPCB Part #', 'JLCPCB Part']
fallback_keys = ['LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
@@ -479,7 +482,7 @@ def _get_position_offset_from_footprint(self, footprint) -> Tuple[float, float]:
return (float(offset[0]), float(offset[1]))
except Exception as e:
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
-
+
def _get_origin_from_footprint(self, footprint) -> float:
'''Get the origin from standard symbol fields.'''
keys = ['FT Origin']
@@ -490,7 +493,7 @@ def _get_origin_from_footprint(self, footprint) -> float:
# determine origin type by package type
if attributes & pcbnew.FP_SMD:
origin_type = 'Anchor'
- else:
+ else:
origin_type = 'Center'
for key in keys + fallback_keys:
diff --git a/plugins/thread.py b/plugins/thread.py
index 14769d1..1b3d6e4 100644
--- a/plugins/thread.py
+++ b/plugins/thread.py
@@ -18,11 +18,11 @@ class ProcessThread(Thread):
def __init__(self, wx, options, cli = None, openBrowser = True):
Thread.__init__(self)
- # prevent use of cli and grapgical mode at the same time
+ # prevent use of cli and graphical mode at the same time
if (wx is None and cli is None) or (wx is not None and cli is not None):
logging.error("Specify either graphical or cli use!")
return
-
+
if cli is not None:
try:
self.board = pcbnew.LoadBoard(cli)
@@ -31,7 +31,7 @@ def __init__(self, wx, options, cli = None, openBrowser = True):
return
else:
self.board = None
-
+
self.process_manager = ProcessManager(self.board)
self.wx = wx
self.cli = cli
@@ -71,7 +71,9 @@ def run(self):
# generate data tables
self.progress(50)
- self.process_manager.generate_tables(temp_dir, self.options[AUTO_TRANSLATE_OPT], self.options[EXCLUDE_DNP_OPT])
+ self.process_manager.generate_tables(
+ temp_dir, self.options[AUTO_TRANSLATE_OPT], self.options[EXCLUDE_DNP_OPT],
+ self.options[PN_COLUMN])
# generate pick and place file
self.progress(60)
@@ -126,7 +128,7 @@ def run(self):
output_path = os.path.join(project_directory, outputFolder)
if not os.path.exists(output_path):
os.makedirs(output_path)
-
+
# rename gerber archive
gerberArchiveName = ProcessManager.normalize_filename("_".join(("{} {}".format(title or filename, revision or '').strip() + '.zip').split()))
os.rename(temp_file, os.path.join(temp_dir, gerberArchiveName))
@@ -146,7 +148,7 @@ def run(self):
if self.openBrowser:
webbrowser.open("file://%s" % (temp_dir))
- if self.wx is None:
+ if self.wx is None:
self.progress(100)
else:
self.progress(-1)