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 + + + + \ 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))