chore: scafolding + project dir output #3

This commit is contained in:
Benny Megidish
2022-07-21 12:03:02 +03:00
parent b801028d63
commit c7cc47718d
16 changed files with 354 additions and 209 deletions
View File
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+19
View File
@@ -0,0 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N801" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="bpy" />
</list>
</option>
</inspection_tool>
</profile>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/KiCadJLCplugin.iml" filepath="$PROJECT_DIR$/.idea/KiCadJLCplugin.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="81624dc4-41ca-48ac-93b8-92da00936c57" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/plugins/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/plugins/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/config.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/plugins/plugin.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/plugin.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/plugins/result_event.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/plugins/thread.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/thread.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectId" id="2C7tSUh2zYdb8tYB6kgwqDOifTc" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="81624dc4-41ca-48ac-93b8-92da00936c57" name="Changes" comment="" />
<created>1658165450946</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1658165450946</updated>
</task>
<servers />
</component>
</project>
+13
View File
@@ -0,0 +1,13 @@
{
"cSpell.words": [
"Drilland",
"EXCELLON",
"FPID",
"LCSC",
"netlist",
"pcbnew",
"Plotfile",
"Protel",
"Soldermask"
]
}
+1 -1
View File
@@ -29,7 +29,7 @@
],
"versions": [
{
"version": "0.3.2",
"version": "0.4.0",
"status": "testing",
"kicad_version": "6.00"
}
+2 -2
View File
@@ -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))
+2
View File
@@ -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"),
+14
View File
@@ -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)
+9 -7
View File
@@ -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()
+189
View File
@@ -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)
-14
View File
@@ -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
+33 -183
View File
@@ -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))
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)
# webbrowser.open(urls['redirect'])
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))