diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/.idea/KiCadJLCplugin.iml b/.idea/KiCadJLCplugin.iml
new file mode 100644
index 0000000..2c80e12
--- /dev/null
+++ b/.idea/KiCadJLCplugin.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..ba5cbb8
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..2750b21
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..64924e0
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1658165450946
+
+
+ 1658165450946
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..853e837
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,13 @@
+{
+ "cSpell.words": [
+ "Drilland",
+ "EXCELLON",
+ "FPID",
+ "LCSC",
+ "netlist",
+ "pcbnew",
+ "Plotfile",
+ "Protel",
+ "Soldermask"
+ ]
+}
\ No newline at end of file
diff --git a/metadata.json b/metadata.json
index 53828cc..fe85c9c 100644
--- a/metadata.json
+++ b/metadata.json
@@ -29,7 +29,7 @@
],
"versions": [
{
- "version": "0.3.2",
+ "version": "0.4.0",
"status": "testing",
"kicad_version": "6.00"
}
diff --git a/plugins/__init__.py b/plugins/__init__.py
index 822037f..5fa3685 100644
--- a/plugins/__init__.py
+++ b/plugins/__init__.py
@@ -4,5 +4,5 @@ try:
plugin.register()
except Exception as e:
import logging
- root = logging.getLogger()
- root.debug(repr(e))
+ logger = logging.getLogger()
+ logger.debug(repr(e))
diff --git a/plugins/config.py b/plugins/config.py
index 30e8274..acda1c0 100644
--- a/plugins/config.py
+++ b/plugins/config.py
@@ -14,6 +14,8 @@ plotPlan = [
("B.Cu", pcbnew.B_Cu, "Bottom Layer"),
("In1.Cu", pcbnew.In1_Cu, "Internal plane 1"),
("In2.Cu", pcbnew.In2_Cu, "Internal plane 2"),
+ ("In3.Cu", pcbnew.In3_Cu, "Internal plane 3"),
+ ("In4.Cu", pcbnew.In4_Cu, "Internal plane 4"),
("F.SilkS", pcbnew.F_SilkS, "Top Silkscreen"),
("B.SilkS", pcbnew.B_SilkS, "Bottom Silkscreen"),
("F.Mask", pcbnew.F_Mask, "Top Soldermask"),
diff --git a/plugins/events.py b/plugins/events.py
new file mode 100644
index 0000000..0282195
--- /dev/null
+++ b/plugins/events.py
@@ -0,0 +1,14 @@
+import wx
+
+STATUS_EVENT_ID = wx.NewId()
+
+
+class StatusEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self)
+ self.SetEventType(STATUS_EVENT_ID)
+ self.data = data
+
+ @staticmethod
+ def invoke(window, function):
+ window.Connect(-1, -1, STATUS_EVENT_ID, function)
diff --git a/plugins/plugin.py b/plugins/plugin.py
index 6a61e26..7517c9e 100644
--- a/plugins/plugin.py
+++ b/plugins/plugin.py
@@ -1,10 +1,12 @@
+import os
import wx
import pcbnew
-from .thread import *
-from .result_event import *
+from .thread import ProcessThread
+from .events import StatusEvent
+# WX GUI form that show the plugin progress
class KiCadToJLCForm(wx.Frame):
def __init__(self):
wx.Dialog.__init__(
@@ -32,9 +34,10 @@ class KiCadToJLCForm(wx.Frame):
self.Centre(wx.BOTH)
- EVT_RESULT(self, self.updateDisplay)
+ StatusEvent.invoke(self, self.updateDisplay)
ProcessThread(self)
+
def updateDisplay(self, status):
if status.data == -1:
pcbnew.Refresh()
@@ -43,6 +46,7 @@ class KiCadToJLCForm(wx.Frame):
self.m_gaugeStatus.SetValue(status.data)
+# Plugin definition
class Plugin(pcbnew.ActionPlugin):
def __init__(self):
self.name = "Fabrication Toolkit"
@@ -50,10 +54,8 @@ class Plugin(pcbnew.ActionPlugin):
self.description = "Toolkit for automating PCB fabrication process with KiCad and JLC PCB"
self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
self.show_toolbar_button = True
- self.icon_file_name = os.path.join(
- os.path.dirname(__file__), 'icon.png')
- self.dark_icon_file_name = os.path.join(
- os.path.dirname(__file__), 'icon.png')
+ self.icon_file_name = os.path.join(os.path.dirname(__file__), 'icon.png')
+ self.dark_icon_file_name = os.path.join(os.path.dirname(__file__), 'icon.png')
def Run(self):
KiCadToJLCForm().Show()
diff --git a/plugins/process.py b/plugins/process.py
new file mode 100644
index 0000000..f5190c5
--- /dev/null
+++ b/plugins/process.py
@@ -0,0 +1,189 @@
+# from urllib import response
+# import json
+# import requests
+import os
+import csv
+import shutil
+import pcbnew
+from collections import defaultdict
+from .config import *
+
+
+class ProcessManager:
+ def __init__(self):
+ self.board = pcbnew.GetBoard()
+ self.bom = []
+ self.components = []
+
+ def generate_gerber(self, temp_dir):
+ settings = self.board.GetDesignSettings()
+ settings.m_SolderMaskMargin = 0
+ settings.m_SolderMaskMinWidth = 0
+
+ plot_controller = pcbnew.PLOT_CONTROLLER(self.board)
+
+ plot_options = plot_controller.GetPlotOptions()
+ plot_options.SetOutputDirectory(temp_dir)
+ plot_options.SetPlotFrameRef(False)
+ plot_options.SetSketchPadLineWidth(pcbnew.FromMM(0.1))
+ plot_options.SetAutoScale(False)
+ plot_options.SetScale(1)
+ plot_options.SetMirror(False)
+ plot_options.SetUseGerberAttributes(True)
+ plot_options.SetExcludeEdgeLayer(True)
+ plot_options.SetUseGerberProtelExtensions(False)
+ plot_options.SetUseAuxOrigin(True)
+ plot_options.SetSubtractMaskFromSilk(False)
+ plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
+
+ for layer_info in plotPlan:
+ if self.board.IsLayerEnabled(layer_info[1]):
+ plot_controller.SetLayer(layer_info[1])
+ plot_controller.OpenPlotfile(
+ layer_info[0],
+ pcbnew.PLOT_FORMAT_GERBER,
+ layer_info[2])
+ plot_controller.PlotLayer()
+
+ plot_controller.ClosePlot()
+
+ def generate_drills(self, temp_dir):
+ drill_writer = pcbnew.EXCELLON_WRITER(self.board)
+
+ drill_writer.SetOptions(
+ False,
+ True,
+ self.board.GetDesignSettings().GetAuxOrigin(),
+ False)
+ drill_writer.SetFormat(False)
+ drill_writer.CreateDrillandMapFilesSet(temp_dir, True, False)
+
+ def generate_netlist(self, temp_dir):
+ netlist_writer = pcbnew.IPC356D_WRITER(self.board)
+ netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
+
+ def generate_positions(self, temp_dir):
+ if hasattr(self.board, 'GetModules'):
+ footprints = list(self.board.GetModules())
+ else:
+ footprints = list(self.board.GetFootprints())
+
+ # unique designator dictionary
+ footprint_designators = defaultdict(int)
+ for i, footprint in enumerate(footprints):
+ # count unique designators
+ footprint_designators[footprint.GetReference()] += 1
+ bom_designators = footprint_designators.copy()
+
+ with open((os.path.join(temp_dir, designatorsFileName)), 'w') as f:
+ for key, value in footprint_designators.items():
+ f.write('%s:%s\n' % (key, value))
+
+ for i, footprint in enumerate(footprints):
+ try:
+ footprint_name = str(footprint.GetFPID().GetFootprintName())
+ except AttributeError:
+ footprint_name = str(footprint.GetFPID().GetLibItemName())
+
+ layer = {
+ pcbnew.F_Cu: 'top',
+ pcbnew.B_Cu: 'bottom',
+ }.get(footprint.GetLayer())
+
+ # mount_type = {
+ # 0: 'smt',
+ # 1: 'tht',
+ # 2: 'smt'
+ # }.get(footprint.GetAttributes())
+
+ if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES:
+ # append unique ID if duplicate footprint designator
+ unique_id = ""
+ if footprint_designators[footprint.GetReference()] > 1:
+ unique_id = str(footprint_designators[footprint.GetReference()])
+ footprint_designators[footprint.GetReference()] -= 1
+
+ self.components.append({
+ 'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
+ 'Mid X': (footprint.GetPosition()[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0,
+ 'Mid Y': (footprint.GetPosition()[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0,
+ 'Rotation': footprint.GetOrientation() / 10.0,
+ 'Layer': layer,
+ })
+
+ if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
+ # append unique ID if we are dealing with duplicate bom designator
+ unique_id = ""
+ if bom_designators[footprint.GetReference()] > 1:
+ unique_id = str(bom_designators[footprint.GetReference()])
+ bom_designators[footprint.GetReference()] -= 1
+
+ # todo: merge similar parts into single entry
+
+ self.bom.append({
+ 'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
+ 'Footprint': footprint_name,
+ 'Quantity': 1,
+ 'Value': footprint.GetValue(),
+ # 'Mount': mount_type,
+ 'LCSC Part #': self._getMpnFromFootprint(footprint),
+ })
+
+ with open((os.path.join(temp_dir, placementFileName)), 'w', newline='') as outfile:
+ header = True
+ csv_writer = csv.writer(outfile)
+
+ for component in self.components:
+ if header:
+ # writing headers of CSV file
+ csv_writer.writerow(component.keys())
+ header = False
+
+ # writing data of CSV file
+ if ('**' not in component['Designator']):
+ csv_writer.writerow(component.values())
+
+ def generate_bom(self, temp_dir):
+
+ with open((os.path.join(temp_dir, bomFileName)), 'w', newline='') as outfile:
+ header = True
+ csv_writer = csv.writer(outfile)
+
+ for component in self.bom:
+ if header:
+ # writing headers of CSV file
+ csv_writer.writerow(component.keys())
+ header = False
+
+ # writing data of CSV file
+ if ('**' not in component['Designator']):
+ csv_writer.writerow(component.values())
+
+ def generate_archive(self, temp_dir, temp_file):
+ temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
+ temp_file = shutil.move(temp_file, temp_dir)
+
+ # remove non essential files
+ for item in os.listdir(temp_dir):
+ if not item.endswith(".zip") and not item.endswith(".csv") and not item.endswith(".ipc"):
+ os.remove(os.path.join(temp_dir, item))
+
+ return temp_file
+
+ def upload_archive(self, temp_file):
+ boardWidth = pcbnew.Iu2Millimeter(self.board.GetBoardEdgesBoundingBox().GetWidth())
+ boardHeight = pcbnew.Iu2Millimeter(self.board.GetBoardEdgesBoundingBox().GetHeight())
+ boardLayer = self.board.GetCopperLayerCount()
+
+ files = { 'upload[file]': open(temp_file, 'rb') }
+ upload_url = baseUrl + '/Common/KiCadUpFile/'
+
+ # response = requests.post(
+ # upload_url, files=files, data={ 'boardWidth': boardWidth, 'boardHeight': boardHeight, 'boardLayer': boardLayer })
+ # urls = json.loads(response.content)
+
+ def _getMpnFromFootprint(self, footprint):
+ keys = ['mpn', 'Mpn', 'MPN', 'JLC_MPN', 'LCSC_MPN', 'LCSC Part #', 'JLC', 'LCSC']
+ for key in keys:
+ if footprint.HasProperty(key):
+ return footprint.GetProperty(key)
diff --git a/plugins/result_event.py b/plugins/result_event.py
deleted file mode 100644
index fc5f3b0..0000000
--- a/plugins/result_event.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import wx
-
-EVT_RESULT_ID = wx.NewId()
-
-
-def EVT_RESULT(win, func):
- win.Connect(-1, -1, EVT_RESULT_ID, func)
-
-
-class ResultEvent(wx.PyEvent):
- def __init__(self, data):
- wx.PyEvent.__init__(self)
- self.SetEventType(EVT_RESULT_ID)
- self.data = data
diff --git a/plugins/thread.py b/plugins/thread.py
index 3643000..2d2db15 100644
--- a/plugins/thread.py
+++ b/plugins/thread.py
@@ -1,233 +1,83 @@
-# original copyright Aisler and licensed under the MIT license.
-# https://opensource.org/licenses/MIT
-
-# from urllib import response
-# import json
-# import requests
import os
import wx
-import csv
import shutil
import tempfile
import webbrowser
-from collections import defaultdict
from threading import Thread
-from .result_event import *
+from .events import StatusEvent
+from .process import ProcessManager
from .config import *
class ProcessThread(Thread):
- def __init__(self, wxObject):
+ def __init__(self, wx):
Thread.__init__(self)
- self.wxObject = wxObject
+
+ self.process_manager = ProcessManager()
+ self.wx = wx
self.start()
def run(self):
+ # initializing
+ self.report(0)
temp_dir = tempfile.mkdtemp()
_, temp_file = tempfile.mkstemp()
- board = pcbnew.GetBoard()
- title_block = board.GetTitleBlock()
+ project_directory = os.path.dirname(self.process_manager.board.GetFileName())
+ output_path = os.path.join(project_directory, outputFolder)
- # configure gerber
+ # configure and generate gerber
self.report(5)
-
- settings = board.GetDesignSettings()
- settings.m_SolderMaskMargin = 0
- settings.m_SolderMaskMinWidth = 0
-
- pctl = pcbnew.PLOT_CONTROLLER(board)
-
- popt = pctl.GetPlotOptions()
- popt.SetOutputDirectory(temp_dir)
- popt.SetPlotFrameRef(False)
- popt.SetSketchPadLineWidth(pcbnew.FromMM(0.1))
- popt.SetAutoScale(False)
- popt.SetScale(1)
- popt.SetMirror(False)
- popt.SetUseGerberAttributes(True)
- popt.SetExcludeEdgeLayer(True)
- popt.SetUseGerberProtelExtensions(False)
- popt.SetUseAuxOrigin(True)
- popt.SetSubtractMaskFromSilk(False)
- popt.SetDrillMarksType(0) # NO_DRILL_SHAPE
-
- # generate gerber
- self.report(10)
- for layer_info in plotPlan:
- if board.IsLayerEnabled(layer_info[1]):
- pctl.SetLayer(layer_info[1])
- pctl.OpenPlotfile(
- layer_info[0],
- pcbnew.PLOT_FORMAT_GERBER,
- layer_info[2])
- pctl.PlotLayer()
-
- pctl.ClosePlot()
+ self.process_manager.generate_gerber(temp_dir)
# generate drill file
self.report(15)
- drlwriter = pcbnew.EXCELLON_WRITER(board)
-
- drlwriter.SetOptions(
- False,
- True,
- board.GetDesignSettings().GetAuxOrigin(),
- False)
- drlwriter.SetFormat(False)
- drlwriter.CreateDrillandMapFilesSet(pctl.GetPlotDirName(), True, False)
-
+ self.process_manager.generate_drills(temp_dir)
+
# generate netlist
self.report(25)
- netlist_writer = pcbnew.IPC356D_WRITER(board)
- netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
+ self.process_manager.generate_netlist(temp_dir)
# generate pick and place file
self.report(40)
- components = []
- bom = []
- if hasattr(board, 'GetModules'):
- footprints = list(board.GetModules())
- else:
- footprints = list(board.GetFootprints())
-
- # unique designator dictionary
- footprint_designators = defaultdict(int)
- for i, footprint in enumerate(footprints):
- # count unique designators
- footprint_designators[footprint.GetReference()] += 1
- bom_designators = footprint_designators.copy()
-
- with open((os.path.join(temp_dir, designatorsFileName)), 'w') as f:
- for key, value in footprint_designators.items():
- f.write('%s:%s\n' % (key, value))
-
- for i, footprint in enumerate(footprints):
- try:
- footprint_name = str(footprint.GetFPID().GetFootprintName())
- except AttributeError:
- footprint_name = str(footprint.GetFPID().GetLibItemName())
-
- layer = {
- pcbnew.F_Cu: 'top',
- pcbnew.B_Cu: 'bottom',
- }.get(footprint.GetLayer())
-
- # mount_type = {
- # 0: 'smt',
- # 1: 'tht',
- # 2: 'smt'
- # }.get(footprint.GetAttributes())
-
- if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES:
- # append unique ID if duplicate footprint designator
- unique_id = ""
- if footprint_designators[footprint.GetReference()] > 1:
- unique_id = str(footprint_designators[footprint.GetReference()])
- footprint_designators[footprint.GetReference()] -= 1
-
- components.append({
- 'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
- 'Mid X': (footprint.GetPosition()[0] - board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0,
- 'Mid Y': (footprint.GetPosition()[1] - board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0,
- 'Rotation': footprint.GetOrientation() / 10.0,
- 'Layer': layer,
- })
-
- if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
- # append unique ID if we are dealing with duplicate bom designator
- unique_id = ""
- if bom_designators[footprint.GetReference()] > 1:
- unique_id = str(bom_designators[footprint.GetReference()])
- bom_designators[footprint.GetReference()] -= 1
-
- # todo: merge similar parts into single entry
-
- bom.append({
- 'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
- 'Footprint': footprint_name,
- 'Quantity': 1,
- 'Value': footprint.GetValue(),
- # 'Mount': mount_type,
- 'LCSC Part #': self.getMpnFromFootprint(footprint),
- })
-
- with open((os.path.join(temp_dir, placementFileName)), 'w', newline='') as outfile:
- header = True
- csv_writer = csv.writer(outfile)
-
- for component in components:
- if header:
- # writing headers of CSV file
- csv_writer.writerow(component.keys())
- header = False
-
- # writing data of CSV file
- if ('**' not in component['Designator']):
- csv_writer.writerow(component.values())
+ self.process_manager.generate_positions(temp_dir)
# generate BOM file
self.report(60)
- with open((os.path.join(temp_dir, bomFileName)), 'w', newline='') as outfile:
- header = True
- csv_writer = csv.writer(outfile)
-
- for component in bom:
- if header:
- # writing headers of CSV file
- csv_writer.writerow(component.keys())
- header = False
-
- # writing data of CSV file
- if ('**' not in component['Designator']):
- csv_writer.writerow(component.values())
+ self.process_manager.generate_bom(temp_dir)
# generate production archive
self.report(75)
- temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
- temp_file = shutil.move(temp_file, temp_dir)
-
- # remove non essential files
- for item in os.listdir(temp_dir):
- if not item.endswith(".zip") and not item.endswith(".csv") and not item.endswith(".ipc"):
- os.remove(os.path.join(temp_dir, item))
+ temp_file = self.process_manager.generate_archive(temp_dir, temp_file)
# upload files
- self.report(87.5)
- boardWidth = pcbnew.Iu2Millimeter(board.GetBoardEdgesBoundingBox().GetWidth())
- boardHeight = pcbnew.Iu2Millimeter(board.GetBoardEdgesBoundingBox().GetHeight())
- boardLayer = board.GetCopperLayerCount()
+ #self.report(87.5)
+ #self.process_manager.upload_archive(temp_file)
- # files = {'upload[file]': open(temp_file, 'rb')}
- # upload_url = baseUrl + '/Common/KiCadUpFile/'
-
- # response = requests.post(
- # upload_url, files=files, data={'boardWidth':boardWidth,'boardHeight':boardHeight,'boardLayer':boardLayer})
-
- # urls = json.loads(response.content)
-
- readsofar = 0
- totalsize = os.path.getsize(temp_file)
+ # progress bar done animation
+ read_so_far = 0
+ total_size = os.path.getsize(temp_file)
with open(temp_file, 'rb') as file:
while True:
data = file.read(10)
if not data:
break
- readsofar += len(data)
- percent = readsofar * 1e2 / totalsize
+ read_so_far += len(data)
+ percent = read_so_far * 1e2 / total_size
self.report(75 + percent / 8)
os.rename(temp_file, os.path.join(temp_dir, gerberArchiveName))
- webbrowser.open(temp_dir)
- # webbrowser.open(urls['redirect'])
+ try:
+ if os.path.exists(output_path):
+ shutil.rmtree(output_path)
+
+ shutil.copytree(temp_dir, output_path)
+ webbrowser.open(output_path)
+ except Exception as e:
+ webbrowser.open(temp_dir)
+
self.report(-1)
def report(self, status):
- wx.PostEvent(self.wxObject, ResultEvent(status))
-
- def getMpnFromFootprint(self, footprint):
- keys = ['mpn', 'Mpn', 'MPN', 'JLC_MPN', 'LCSC_MPN', 'LCSC Part #', 'JLC', 'LCSC']
- for key in keys:
- if footprint.HasProperty(key):
- return footprint.GetProperty(key)
+ wx.PostEvent(self.wx, StatusEvent(status))