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
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/logo.svg?raw=true"
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/logo.svg?raw=true"
style="display:block margin-left: auto; margin-right: auto;" alt="JLC PCB Plug-in for KiCad">

<div align="center">

| **JLC PCB Plug-in for KiCad** |
Expand Down Expand Up @@ -37,31 +37,38 @@ Click on the Fabrication Toolkit <img src="https://github.com/bennymeg/JLC-Plugi

Options can be set in the dialog that appears when the plugin is invoked. They are saved in a file called `fabrication-toolkit-options.json` in the project directory so that they are remembered between invocations of the plugin.

<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/options.png?raw=true" height=275>
![Option Dialog](assets/options.png)

☑ __Additional layers__: Comma-separated list of additional layers to include in the gerber archive.</br>
☑ __Set User.1 as V-Cut layer__: Merge User.1 layer with the Edge-Cut layer in production.</br>
☑ __Use User.2 for an alternative Edge-Cut layer__: Use the User.2 instead of the Edge-Cut layer for the board outline in production. This is useful if you need process edges or panelization during production but still want to keep the individual outline for prototyping, 3D model exports, or similar purposes.</br>
☑ __Apply automatic translations__: Apply known translation fixes for common components.</br>
☑ __Apply automatic fill for all zones__: Refill all zones before generation production files.</br>
☑ __Exclude DNP components from BOM__: Exclude components the had been set a DNP from th BOM.</br>
☑ __MPN Column in bom.csv__: Select bom.csv column to use for part numbers.</br>

### ① Include Component Part Number in Production Files
Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's fields property.
Use one of the fields from "Primary Fields" or "Fallback Fields" shown below
for Manufacturer Part Numbers.

JLCPCB uses the Comment and Footprint columns in the bom.csv to match parts.
Older version of Fabrication Toolkit generated a 'LCSC Part #' column instead.
Use the column name that suits your process to contain the Manufacturer Part
Number or LCSC/JLCPCB component number.

<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/mpn.png?raw=true" height=420>

#### Primary Fields*:
| '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._

---

Expand Down Expand Up @@ -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|
Expand Down Expand Up @@ -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' |
Expand Down
Binary file modified assets/options.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion plugins/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from .thread import ProcessThread
from .options import *


if __name__ == '__main__':
parser = ap.ArgumentParser(prog="Fabrication Toolkit",
Expand All @@ -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()
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion plugins/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
EXTRA_LAYERS = "EXTRA_LAYERS"
PN_COLUMN = "PN_COLUMN"
20 changes: 16 additions & 4 deletions plugins/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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)
Expand All @@ -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:')
Expand All @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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()
Expand All @@ -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()

Expand Down
33 changes: 18 additions & 15 deletions plugins/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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())
Expand All @@ -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())
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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.'''
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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']

Expand Down Expand Up @@ -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']
Expand All @@ -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:
Expand Down
14 changes: 8 additions & 6 deletions plugins/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down