update plugins
This commit is contained in:
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/__init__.py
vendored
Normal file
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/__init__.py
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
try:
|
||||
from .plugin import Plugin
|
||||
plugin = Plugin()
|
||||
plugin.register()
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger()
|
||||
logger.debug(repr(e))
|
36
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/cli.py
vendored
Normal file
36
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/cli.py
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
import argparse as ap
|
||||
|
||||
from .thread import ProcessThread
|
||||
from .options import *
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ap.ArgumentParser(prog="Fabrication Toolkit",
|
||||
description="Generates JLCPCB production files from a KiCAD board file")
|
||||
|
||||
parser.add_argument("--path", "-p", type=str, help="Path to KiCAD board file", required=True)
|
||||
parser.add_argument("--additionalLayers", "-aL", type=str, help="Additional layers(comma-separated)")
|
||||
parser.add_argument("--user1VCut", "-u1", action="store_true", help="Set User.1 as V-Cut layer")
|
||||
parser.add_argument("--user2AltVCut", "-u2", action="store_true", help="Use User.2 for alternative Edge-Cut layer")
|
||||
parser.add_argument("--autoTranslate", "-t", action="store_true", help="Apply automatic position/rotation translations")
|
||||
parser.add_argument("--autoFill", "-f", action="store_true", help="Apply automatic fill for all zones")
|
||||
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")
|
||||
args = parser.parse_args()
|
||||
|
||||
options = dict()
|
||||
options[AUTO_TRANSLATE_OPT] = args.autoTranslate
|
||||
options[AUTO_FILL_OPT] = args.autoFill
|
||||
options[EXCLUDE_DNP_OPT] = args.excludeDNP
|
||||
options[EXTEND_EDGE_CUT_OPT] = args.user1VCut
|
||||
options[ALTERNATIVE_EDGE_CUT_OPT] = args.user2AltVCut
|
||||
options[ALL_ACTIVE_LAYERS_OPT] = args.allActiveLayers
|
||||
options[EXTRA_LAYERS] = args.additionalLayers
|
||||
|
||||
openBrowser = args.openBrowser
|
||||
|
||||
|
||||
path = args.path
|
||||
|
||||
ProcessThread(wx=None, cli=path, options=options, openBrowser=openBrowser)
|
26
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/config.py
vendored
Normal file
26
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/config.py
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
import pcbnew # type: ignore
|
||||
|
||||
baseUrl = 'https://www.jlcpcb.com'
|
||||
|
||||
netlistFileName = 'netlist.ipc'
|
||||
designatorsFileName = 'designators.csv'
|
||||
placementFileName = 'positions.csv'
|
||||
bomFileName = 'bom.csv'
|
||||
gerberArchiveName = 'gerbers.zip'
|
||||
outputFolder = 'production'
|
||||
bomRowLimit = 200
|
||||
|
||||
optionsFileName = 'fabrication-toolkit-options.json'
|
||||
|
||||
standardLayers = [ pcbnew.F_Cu, pcbnew.B_Cu,
|
||||
pcbnew.In1_Cu, pcbnew.In2_Cu, pcbnew.In3_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu,
|
||||
pcbnew.In6_Cu, pcbnew.In7_Cu, pcbnew.In8_Cu, pcbnew.In9_Cu, pcbnew.In10_Cu,
|
||||
pcbnew.In11_Cu, pcbnew.In12_Cu, pcbnew.In13_Cu, pcbnew.In14_Cu, pcbnew.In15_Cu,
|
||||
pcbnew.In16_Cu, pcbnew.In17_Cu, pcbnew.In18_Cu, pcbnew.In19_Cu, pcbnew.In20_Cu,
|
||||
pcbnew.In21_Cu, pcbnew.In22_Cu, pcbnew.In23_Cu, pcbnew.In24_Cu, pcbnew.In25_Cu,
|
||||
pcbnew.In26_Cu, pcbnew.In27_Cu, pcbnew.In28_Cu, pcbnew.In29_Cu, pcbnew.In30_Cu,
|
||||
pcbnew.F_SilkS, pcbnew.B_SilkS,
|
||||
pcbnew.F_Mask, pcbnew.B_Mask,
|
||||
pcbnew.F_Paste, pcbnew.B_Paste,
|
||||
pcbnew.Edge_Cuts
|
||||
]
|
14
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/events.py
vendored
Normal file
14
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/events.py
vendored
Normal 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)
|
BIN
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/icon.png
vendored
Normal file
BIN
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/options.py
vendored
Normal file
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/options.py
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
AUTO_TRANSLATE_OPT = "AUTO TRANSLATE"
|
||||
AUTO_FILL_OPT = "AUTO FILL"
|
||||
EXCLUDE_DNP_OPT = "EXCLUDE DNP"
|
||||
OUTPUT_NAME_OPT = "OUTPUT NAME"
|
||||
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"
|
151
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/plugin.py
vendored
Normal file
151
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/plugin.py
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew # type: ignore
|
||||
|
||||
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 .utils import load_user_options, save_user_options, get_layer_names
|
||||
|
||||
|
||||
# WX GUI form that show the plugin progress
|
||||
class KiCadToJLCForm(wx.Frame):
|
||||
def __init__(self):
|
||||
wx.Dialog.__init__(
|
||||
self,
|
||||
None,
|
||||
id=wx.ID_ANY,
|
||||
title=u"Fabrication Toolkit",
|
||||
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)
|
||||
self.SetBackgroundColour(wx.LIGHT_GREY)
|
||||
|
||||
self.SetSizeHints(wx.Size(600, 100), wx.DefaultSize)
|
||||
|
||||
userOptions = load_user_options({
|
||||
EXTRA_LAYERS: "",
|
||||
ALL_ACTIVE_LAYERS_OPT: False,
|
||||
EXTEND_EDGE_CUT_OPT: False,
|
||||
ALTERNATIVE_EDGE_CUT_OPT: False,
|
||||
AUTO_TRANSLATE_OPT: True,
|
||||
AUTO_FILL_OPT: True,
|
||||
EXCLUDE_DNP_OPT: False
|
||||
})
|
||||
|
||||
self.mOptionsLabel = wx.StaticText(self, label='Options:')
|
||||
# self.mOptionsSeparator = wx.StaticLine(self)
|
||||
|
||||
layers = get_layer_names(pcbnew.GetBoard())
|
||||
self.mAdditionalLayersControl = wx.TextCtrl(self, size=wx.Size(600, 50))
|
||||
self.mAdditionalLayersControl.Hint = "Additional layers (comma-separated)"
|
||||
self.mAdditionalLayersControl.AutoComplete(layers)
|
||||
self.mAdditionalLayersControl.Enable()
|
||||
self.mAdditionalLayersControl.SetValue(userOptions[EXTRA_LAYERS])
|
||||
self.mAllActiveLayersCheckbox = wx.CheckBox(self, label='Plot all active layers')
|
||||
self.mAllActiveLayersCheckbox.SetValue(userOptions[ALL_ACTIVE_LAYERS_OPT])
|
||||
self.mExtendEdgeCutsCheckbox = wx.CheckBox(self, label='Set User.1 as V-Cut layer')
|
||||
self.mExtendEdgeCutsCheckbox.SetValue(userOptions[EXTEND_EDGE_CUT_OPT])
|
||||
self.mAlternativeEdgeCutsCheckbox = wx.CheckBox(self, label='Use User.2 for alternative Edge-Cut layer')
|
||||
self.mAlternativeEdgeCutsCheckbox.SetValue(userOptions[ALTERNATIVE_EDGE_CUT_OPT])
|
||||
self.mAutomaticTranslationCheckbox = wx.CheckBox(self, label='Apply automatic translations')
|
||||
self.mAutomaticTranslationCheckbox.SetValue(userOptions[AUTO_TRANSLATE_OPT])
|
||||
self.mAutomaticFillCheckbox = wx.CheckBox(self, label='Apply automatic fill for all zones')
|
||||
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.mGaugeStatus = wx.Gauge(
|
||||
self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size(600, 20), wx.GA_HORIZONTAL)
|
||||
self.mGaugeStatus.SetValue(0)
|
||||
self.mGaugeStatus.Hide()
|
||||
|
||||
self.mGenerateButton = wx.Button(self, label='Generate', size=wx.Size(600, 60))
|
||||
self.mGenerateButton.Bind(wx.EVT_BUTTON, self.onGenerateButtonClick)
|
||||
|
||||
boxSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
boxSizer.Add(self.mOptionsLabel, 0, wx.ALL, 5)
|
||||
# boxSizer.Add(self.mOptionsSeparator, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mAdditionalLayersControl, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mAllActiveLayersCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mExtendEdgeCutsCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mAlternativeEdgeCutsCheckbox, 0, wx.ALL, 5)
|
||||
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.mGaugeStatus, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mGenerateButton, 0, wx.ALL, 5)
|
||||
|
||||
self.SetSizer(boxSizer)
|
||||
self.Layout()
|
||||
boxSizer.Fit(self)
|
||||
|
||||
self.Centre(wx.BOTH)
|
||||
|
||||
# Bind the ESC key event to a handler
|
||||
self.Bind(wx.EVT_CHAR_HOOK, self.onKey)
|
||||
|
||||
# Close the dialog when pressing the ESC key
|
||||
def onKey(self, event):
|
||||
if event.GetKeyCode() == wx.WXK_ESCAPE:
|
||||
self.Close(True)
|
||||
else:
|
||||
event.Skip()
|
||||
|
||||
|
||||
def onGenerateButtonClick(self, event):
|
||||
options = dict()
|
||||
options[EXTRA_LAYERS] = self.mAdditionalLayersControl.GetValue()
|
||||
options[ALL_ACTIVE_LAYERS_OPT] = self.mAllActiveLayersCheckbox.GetValue()
|
||||
options[EXTEND_EDGE_CUT_OPT] = self.mExtendEdgeCutsCheckbox.GetValue()
|
||||
options[ALTERNATIVE_EDGE_CUT_OPT] = self.mAlternativeEdgeCutsCheckbox.GetValue()
|
||||
options[AUTO_TRANSLATE_OPT] = self.mAutomaticTranslationCheckbox.GetValue()
|
||||
options[AUTO_FILL_OPT] = self.mAutomaticFillCheckbox.GetValue()
|
||||
options[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
|
||||
|
||||
save_user_options(options)
|
||||
|
||||
self.mOptionsLabel.Hide()
|
||||
self.mAdditionalLayersControl.Hide()
|
||||
self.mAllActiveLayersCheckbox.Hide()
|
||||
self.mExtendEdgeCutsCheckbox.Hide()
|
||||
self.mAlternativeEdgeCutsCheckbox.Hide()
|
||||
self.mAutomaticTranslationCheckbox.Hide()
|
||||
self.mAutomaticFillCheckbox.Hide()
|
||||
self.mExcludeDnpCheckbox.Hide()
|
||||
self.mGenerateButton.Hide()
|
||||
self.mGaugeStatus.Show()
|
||||
|
||||
self.Fit()
|
||||
self.SetTitle('Fabrication Toolkit (Processing...)')
|
||||
|
||||
StatusEvent.invoke(self, self.updateDisplay)
|
||||
ProcessThread(self, options)
|
||||
|
||||
def updateDisplay(self, status):
|
||||
if status.data == -1:
|
||||
self.SetTitle('Fabrication Toolkit (Done!)')
|
||||
pcbnew.Refresh()
|
||||
self.Destroy()
|
||||
else:
|
||||
self.mGaugeStatus.SetValue(int(status.data))
|
||||
|
||||
|
||||
# Plugin definition
|
||||
class Plugin(pcbnew.ActionPlugin):
|
||||
def __init__(self):
|
||||
self.name = "Fabrication Toolkit"
|
||||
self.category = "Manufacturing"
|
||||
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')
|
||||
|
||||
def Run(self):
|
||||
KiCadToJLCForm().Show()
|
510
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/process.py
vendored
Normal file
510
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/process.py
vendored
Normal file
@ -0,0 +1,510 @@
|
||||
# For better annotation.
|
||||
from __future__ import annotations
|
||||
|
||||
# System base libraries
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import math
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from typing import Tuple
|
||||
|
||||
# Interaction with KiCad.
|
||||
import pcbnew # type: ignore
|
||||
from .utils import footprint_has_field, footprint_get_field, get_plot_plan
|
||||
|
||||
# Application definitions.
|
||||
from .config import *
|
||||
|
||||
class ProcessManager:
|
||||
def __init__(self, board = None):
|
||||
# if no board is already loaded by cli mode getBoard from kicad environment
|
||||
if board is None:
|
||||
self.board = pcbnew.GetBoard()
|
||||
else:
|
||||
self.board = board
|
||||
self.bom = []
|
||||
self.components = []
|
||||
self.__rotation_db = self.__read_rotation_db()
|
||||
|
||||
@staticmethod
|
||||
def normalize_filename(filename):
|
||||
return re.sub(r'[^\w\s\.\-]', '', filename)
|
||||
|
||||
def update_zone_fills(self):
|
||||
'''Verify all zones have up-to-date fills.'''
|
||||
filler = pcbnew.ZONE_FILLER(self.board)
|
||||
zones = self.board.Zones()
|
||||
|
||||
# Fill returns true/false if a refill was made
|
||||
# We cant use aCheck = True as that would require a rollback on the commit object if
|
||||
# user decided to not perform the zone fill and the commit object is not exposed to python API
|
||||
filler.Fill(zones, False)
|
||||
|
||||
# Finally rebuild the connectivity db
|
||||
self.board.BuildConnectivity()
|
||||
|
||||
def generate_gerber(self, temp_dir, extra_layers, extend_edge_cuts, alternative_edge_cuts, all_active_layers):
|
||||
'''Generate the Gerber files.'''
|
||||
settings = self.board.GetDesignSettings()
|
||||
settings.m_SolderMaskMargin = 50000
|
||||
settings.m_SolderMaskToCopperClearance = 5000
|
||||
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.SetUseGerberProtelExtensions(True)
|
||||
plot_options.SetUseAuxOrigin(True)
|
||||
plot_options.SetSubtractMaskFromSilk(True)
|
||||
plot_options.SetUseGerberX2format(False)
|
||||
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
|
||||
|
||||
if hasattr(plot_options, "SetExcludeEdgeLayer"):
|
||||
plot_options.SetExcludeEdgeLayer(True)
|
||||
|
||||
if extra_layers is not None:
|
||||
extra_layers = [element.strip() for element in extra_layers.strip().split(',') if element.strip()]
|
||||
else:
|
||||
extra_layers = []
|
||||
|
||||
for layer_info in get_plot_plan(self.board):
|
||||
if (self.board.IsLayerEnabled(layer_info[1]) and (all_active_layers or layer_info[1] in standardLayers)) or layer_info[0] in extra_layers:
|
||||
plot_controller.SetLayer(layer_info[1])
|
||||
plot_controller.OpenPlotfile(layer_info[2], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
|
||||
|
||||
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'PlotLayers') and (extend_edge_cuts or alternative_edge_cuts):
|
||||
seq = pcbnew.LSEQ()
|
||||
# uses User_2 layer for alternative Edge_Cuts layer
|
||||
if alternative_edge_cuts:
|
||||
seq.push_back(pcbnew.User_2)
|
||||
else:
|
||||
seq.push_back(layer_info[1])
|
||||
# includes User_1 layer with Edge_Cuts layer to allow V Cuts to be defined as User_1 layer
|
||||
# available for KiCad 7.0.1+
|
||||
if extend_edge_cuts:
|
||||
seq.push_back(layer_info[1])
|
||||
seq.push_back(pcbnew.User_1)
|
||||
plot_controller.PlotLayers(seq)
|
||||
else:
|
||||
plot_controller.PlotLayer()
|
||||
|
||||
plot_controller.ClosePlot()
|
||||
|
||||
def generate_drills(self, temp_dir):
|
||||
'''Generate the drill file.'''
|
||||
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
|
||||
|
||||
drill_writer.SetOptions(
|
||||
False,
|
||||
False,
|
||||
self.board.GetDesignSettings().GetAuxOrigin(),
|
||||
False)
|
||||
drill_writer.SetFormat(True)
|
||||
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, True)
|
||||
|
||||
def generate_netlist(self, temp_dir):
|
||||
'''Generate the connection netlist.'''
|
||||
netlist_writer = pcbnew.IPC356D_WRITER(self.board)
|
||||
netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
|
||||
|
||||
def _get_footprint_position(self, footprint):
|
||||
"""Calculate position based on center of pads / bounding box."""
|
||||
origin_type = self._get_origin_from_footprint(footprint)
|
||||
|
||||
if origin_type == 'Anchor':
|
||||
position = footprint.GetPosition()
|
||||
else: # if type_origin == 'Center' or anything else
|
||||
pads = footprint.Pads()
|
||||
if len(pads) > 0:
|
||||
# get bounding box based on pads only to ignore non-copper layers, e.g. silkscreen
|
||||
bbox = pads[0].GetBoundingBox() # start with small bounding box
|
||||
for pad in pads:
|
||||
bbox.Merge(pad.GetBoundingBox()) # expand bounding box
|
||||
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):
|
||||
'''Generate the data tables.'''
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
footprints = list(self.board.GetModules())
|
||||
else:
|
||||
footprints = list(self.board.GetFootprints())
|
||||
|
||||
# sort footprint after designator
|
||||
footprints.sort(key=lambda x: x.GetReference().upper())
|
||||
|
||||
# unique designator dictionary
|
||||
footprint_designators = defaultdict(int)
|
||||
for i, footprint in enumerate(footprints):
|
||||
# count unique designators
|
||||
footprint_designators[footprint.GetReference().upper()] += 1
|
||||
bom_designators = footprint_designators.copy()
|
||||
|
||||
if len(footprint_designators.items()) > 0:
|
||||
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8-sig') 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 = self._get_layer_override_from_footprint(footprint)
|
||||
|
||||
# mount_type = {
|
||||
# 0: 'smt',
|
||||
# 1: 'tht',
|
||||
# 2: 'unspecified'
|
||||
# }.get(footprint.GetAttributes())
|
||||
|
||||
is_dnp = (footprint_has_field(footprint, 'dnp')
|
||||
or (footprint.GetValue().upper() == 'DNP')
|
||||
or getattr(footprint, 'IsDNP', bool)())
|
||||
skip_dnp = exclude_dnp and is_dnp
|
||||
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES) and not is_dnp:
|
||||
# append unique ID if duplicate footprint designator
|
||||
unique_id = ""
|
||||
if footprint_designators[footprint.GetReference().upper()] > 1:
|
||||
unique_id = str(footprint_designators[footprint.GetReference().upper()])
|
||||
footprint_designators[footprint.GetReference().upper()] -= 1
|
||||
|
||||
designator = "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id)
|
||||
position = self._get_footprint_position(footprint)
|
||||
mid_x = (position[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
|
||||
mid_y = (position[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0
|
||||
rotation = footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
|
||||
rotation_offset_db = self._get_rotation_from_db(footprint_name) # internal database offset
|
||||
rotation_offset_manual = self._get_rotation_offset_from_footprint(footprint) # explicated offset by the designer
|
||||
|
||||
# position offset needs to take rotation into account
|
||||
pos_offset = self._get_position_offset_from_footprint(footprint)
|
||||
if auto_translate:
|
||||
pos_offset_db = self._get_position_offset_from_db(footprint_name)
|
||||
pos_offset = (pos_offset[0] + pos_offset_db[0], pos_offset[1] + pos_offset_db[1])
|
||||
|
||||
rsin = math.sin(rotation / 180 * math.pi)
|
||||
rcos = math.cos(rotation / 180 * math.pi)
|
||||
|
||||
if layer == 'bottom':
|
||||
pos_offset = ( pos_offset[0] * rcos + pos_offset[1] * rsin, pos_offset[0] * rsin - pos_offset[1] * rcos )
|
||||
else:
|
||||
pos_offset = ( pos_offset[0] * rcos - pos_offset[1] * rsin, pos_offset[0] * rsin + pos_offset[1] * rcos )
|
||||
mid_x, mid_y = tuple(map(sum,zip((mid_x, mid_y), pos_offset)))
|
||||
|
||||
# JLC expect 'Rotation' to be 'as viewed from above component', so bottom needs inverting, and ends up 180 degrees out as well
|
||||
if layer == 'bottom':
|
||||
rotation = (180.0 - rotation)
|
||||
|
||||
if auto_translate:
|
||||
rotation += rotation_offset_db
|
||||
|
||||
rotation = (rotation + rotation_offset_manual) % 360.0
|
||||
|
||||
self.components.append({
|
||||
'Designator': designator,
|
||||
'Mid X': mid_x,
|
||||
'Mid Y': mid_y,
|
||||
'Rotation': rotation,
|
||||
'Layer': layer,
|
||||
})
|
||||
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM) and not skip_dnp:
|
||||
# append unique ID if we are dealing with duplicate bom designator
|
||||
unique_id = ""
|
||||
if bom_designators[footprint.GetReference().upper()] > 1:
|
||||
unique_id = str(bom_designators[footprint.GetReference().upper()])
|
||||
bom_designators[footprint.GetReference().upper()] -= 1
|
||||
|
||||
# merge similar parts into single entry
|
||||
insert = True
|
||||
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)
|
||||
under_limit = component['Quantity'] < bomRowLimit
|
||||
|
||||
if same_footprint and same_value and same_lcsc and under_limit:
|
||||
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id)
|
||||
component['Quantity'] += 1
|
||||
insert = False
|
||||
break
|
||||
|
||||
# add component to BOM
|
||||
if insert:
|
||||
self.bom.append({
|
||||
'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),
|
||||
})
|
||||
|
||||
def generate_positions(self, temp_dir):
|
||||
'''Generate the position file.'''
|
||||
if len(self.components) > 0:
|
||||
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8-sig') as outfile:
|
||||
csv_writer = csv.writer(outfile)
|
||||
# writing headers of CSV file
|
||||
csv_writer.writerow(self.components[0].keys())
|
||||
|
||||
for component in self.components:
|
||||
# writing data of CSV file
|
||||
if ('**' not in component['Designator']):
|
||||
csv_writer.writerow(component.values())
|
||||
|
||||
def generate_bom(self, temp_dir):
|
||||
'''Generate the bom file.'''
|
||||
if len(self.bom) > 0:
|
||||
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8-sig') as outfile:
|
||||
csv_writer = csv.writer(outfile)
|
||||
# writing headers of CSV file
|
||||
csv_writer.writerow(self.bom[0].keys())
|
||||
|
||||
# Output all of the component information
|
||||
for component in self.bom:
|
||||
# writing data of CSV file
|
||||
if ('**' not in component['Designator']):
|
||||
csv_writer.writerow(component.values())
|
||||
|
||||
def generate_archive(self, temp_dir, temp_file):
|
||||
'''Generate the archive 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
|
||||
|
||||
""" Private """
|
||||
|
||||
def __read_rotation_db(self, filename: str = os.path.join(os.path.dirname(__file__), 'transformations.csv')) -> dict[str, float]:
|
||||
'''Read the rotations.cf config file so we know what rotations
|
||||
to apply later.
|
||||
'''
|
||||
db = {}
|
||||
|
||||
with open(filename, newline='') as csvfile:
|
||||
csvDialect = csv.Sniffer().sniff(csvfile.read(1024))
|
||||
csvfile.seek(0)
|
||||
csvData = csv.DictReader(csvfile, fieldnames=["footprint", "rotation", "x", "y"],
|
||||
restkey="extra", restval="0", dialect=csvDialect)
|
||||
|
||||
rowNum = 0
|
||||
for row in csvData:
|
||||
rowNum = rowNum + 1
|
||||
# First row is header row, skip.
|
||||
if rowNum == 1:
|
||||
skipFirst = False
|
||||
continue
|
||||
|
||||
# If there was too many fields, throw an exception.
|
||||
if len(row) > 4:
|
||||
raise RuntimeError("{}: Too many fields found in row {}: {}".format(filename, rowNum, row))
|
||||
|
||||
# See if the values we expect to be floating point numbers
|
||||
# can be converted to floating point, if not throw an exception.
|
||||
if row['rotation'] == "":
|
||||
rotation = 0.0
|
||||
else:
|
||||
try:
|
||||
rotation = float(row['rotation'])
|
||||
except ValueError:
|
||||
raise RuntimeError("{}: Non-numeric rotation value found in row {}".format(filename, rowNum))
|
||||
|
||||
if row['x'] == "":
|
||||
delta_x = 0.0
|
||||
else:
|
||||
try:
|
||||
delta_x = float(row['x'])
|
||||
except ValueError:
|
||||
raise RuntimeError("{}: Non-numeric translation value found in row {}".format(filename, rowNum))
|
||||
|
||||
if row['y'] == "":
|
||||
delta_y = 0.0
|
||||
else:
|
||||
try:
|
||||
delta_y = float(row['y'])
|
||||
except ValueError:
|
||||
raise RuntimeError("{}: Non-numeric translation value found in row {}".format(filename, rowNum))
|
||||
|
||||
# Add the entry to the database in the format we expect.
|
||||
db[rowNum] = {}
|
||||
db[rowNum]['name'] = row['footprint']
|
||||
db[rowNum]['rotation'] = rotation
|
||||
db[rowNum]['x'] = delta_x
|
||||
db[rowNum]['y'] = delta_y
|
||||
|
||||
return db
|
||||
|
||||
def _get_rotation_from_db(self, footprint: str) -> float:
|
||||
'''Get the rotation to be added from the database file.'''
|
||||
# Look for regular expression math of the footprint name and not its root library.
|
||||
|
||||
for entry in self.__rotation_db.items():
|
||||
# If the expression in the DB contains a :, search for it literally.
|
||||
if (re.search(':', entry[1]['name'])):
|
||||
if (re.search(entry[1]['name'], footprint)):
|
||||
return float(entry[1]['rotation'])
|
||||
# There is no : in the expression, so only search the right side of the :
|
||||
else:
|
||||
footprint_segments = footprint.split(':')
|
||||
# Only one means there was no :, just check the short.
|
||||
if (len(footprint_segments) == 1):
|
||||
check = footprint_segments[0]
|
||||
# More means there was a :, check the right side.
|
||||
else:
|
||||
check = footprint_segments[1]
|
||||
if (re.search(entry[1]['name'], check)):
|
||||
return float(entry[1]['rotation'])
|
||||
|
||||
# Not found, no rotation.
|
||||
return 0.0
|
||||
|
||||
def _get_position_offset_from_db(self, footprint: str) -> Tuple[float, float]:
|
||||
'''Get the rotation to be added from the database file.'''
|
||||
# Look for regular expression math of the footprint name and not its root library.
|
||||
|
||||
for entry in self.__rotation_db.items():
|
||||
# If the expression in the DB contains a :, search for it literally.
|
||||
if (re.search(':', entry[1]['name'])):
|
||||
if (re.search(entry[1]['name'], footprint)):
|
||||
return ( float(entry[1]['x']), float(entry[1]['y']) )
|
||||
# There is no : in the expression, so only search the right side of the :
|
||||
else:
|
||||
footprint_segments = footprint.split(':')
|
||||
# Only one means there was no :, just check the short.
|
||||
if (len(footprint_segments) == 1):
|
||||
check = footprint_segments[0]
|
||||
# More means there was a :, check the right side.
|
||||
else:
|
||||
check = footprint_segments[1]
|
||||
if (re.search(entry[1]['name'], check)):
|
||||
return ( float(entry[1]['x']), float(entry[1]['y']) )
|
||||
|
||||
# Not found, no delta.
|
||||
return (0.0, 0.0)
|
||||
|
||||
def _get_mpn_from_footprint(self, footprint) -> str:
|
||||
''''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']
|
||||
|
||||
if footprint_has_field(footprint, 'dnp'):
|
||||
return 'DNP'
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
return footprint_get_field(footprint, key)
|
||||
|
||||
def _get_layer_override_from_footprint(self, footprint) -> str:
|
||||
'''Get the layer override from standard symbol fields.'''
|
||||
keys = ['FT Layer Override']
|
||||
fallback_keys = ['Layer Override', 'LayerOverride']
|
||||
|
||||
layer = {
|
||||
pcbnew.F_Cu: 'top',
|
||||
pcbnew.B_Cu: 'bottom',
|
||||
}.get(footprint.GetLayer())
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
temp_layer = footprint_get_field(footprint, key)
|
||||
if len(temp_layer) > 0:
|
||||
if (temp_layer[0] == 'b' or temp_layer[0] == 'B'):
|
||||
layer = "bottom"
|
||||
break
|
||||
elif (temp_layer[0] == 't' or temp_layer[0] == 'T'):
|
||||
layer = "top"
|
||||
break
|
||||
|
||||
return layer
|
||||
|
||||
def _get_rotation_offset_from_footprint(self, footprint) -> float:
|
||||
'''Get the rotation offset from standard symbol fields.'''
|
||||
keys = ['FT Rotation Offset']
|
||||
fallback_keys = ['Rotation Offset', 'RotOffset']
|
||||
|
||||
offset = ""
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
offset = footprint_get_field(footprint, key)
|
||||
break
|
||||
|
||||
if offset is None or offset == "":
|
||||
return 0.0
|
||||
else:
|
||||
try:
|
||||
return float(offset)
|
||||
except ValueError:
|
||||
raise RuntimeError("Rotation offset of {} is not a valid number".format(footprint.GetReference()))
|
||||
|
||||
def _get_position_offset_from_footprint(self, footprint) -> Tuple[float, float]:
|
||||
'''Get the position offset from standard symbol fields.'''
|
||||
keys = ['FT Position Offset']
|
||||
fallback_keys = ['Position Offset', 'PosOffset']
|
||||
|
||||
offset = ""
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
offset = footprint_get_field(footprint, key)
|
||||
break
|
||||
|
||||
if offset == "":
|
||||
return (0.0, 0.0)
|
||||
else:
|
||||
try:
|
||||
offset = offset.split(",")
|
||||
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']
|
||||
fallback_keys = ['Origin']
|
||||
|
||||
attributes = footprint.GetAttributes()
|
||||
|
||||
# determine origin type by package type
|
||||
if attributes & pcbnew.FP_SMD:
|
||||
origin_type = 'Anchor'
|
||||
else:
|
||||
origin_type = 'Center'
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint_has_field(footprint, key):
|
||||
origin_type_override = str(footprint_get_field(footprint, key)).strip().capitalize()
|
||||
|
||||
if origin_type_override in ['Anchor', 'Center']:
|
||||
origin_type = origin_type_override
|
||||
break
|
||||
|
||||
return origin_type
|
||||
|
||||
def _normalize_footprint_name(self, footprint) -> str:
|
||||
# replace footprint names of resistors, capacitors, inductors, diodes, LEDs, fuses etc, with the footprint size only
|
||||
pattern = re.compile(r'^(\w*_SMD:)?\w{1,4}_(\d+)_\d+Metric.*$')
|
||||
|
||||
return pattern.sub(r'\2', footprint)
|
158
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/thread.py
vendored
Normal file
158
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/thread.py
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew # type: ignore
|
||||
import shutil
|
||||
import tempfile
|
||||
import webbrowser
|
||||
import datetime
|
||||
import logging
|
||||
from threading import Thread
|
||||
from .events import StatusEvent
|
||||
from .process import ProcessManager
|
||||
from .config import *
|
||||
from .options import *
|
||||
from .utils import print_cli_progress_bar
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
except Exception as e:
|
||||
logging.error("Fabrication Toolkit - Error" + str(e))
|
||||
return
|
||||
else:
|
||||
self.board = None
|
||||
|
||||
self.process_manager = ProcessManager(self.board)
|
||||
self.wx = wx
|
||||
self.cli = cli
|
||||
self.options = options
|
||||
self.openBrowser = openBrowser
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
# initializing
|
||||
self.progress(0)
|
||||
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
temp_dir_gerber = temp_dir + "_g"
|
||||
os.makedirs(temp_dir_gerber)
|
||||
|
||||
_, temp_file = tempfile.mkstemp()
|
||||
project_directory = os.path.dirname(self.process_manager.board.GetFileName())
|
||||
|
||||
try:
|
||||
# Verify all zones are up-to-date
|
||||
self.progress(10)
|
||||
if (self.options[AUTO_FILL_OPT]):
|
||||
self.process_manager.update_zone_fills()
|
||||
|
||||
# generate gerber
|
||||
self.progress(20)
|
||||
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS], self.options[EXTEND_EDGE_CUT_OPT],
|
||||
self.options[ALTERNATIVE_EDGE_CUT_OPT], self.options[ALL_ACTIVE_LAYERS_OPT])
|
||||
|
||||
# generate drill file
|
||||
self.progress(30)
|
||||
self.process_manager.generate_drills(temp_dir_gerber)
|
||||
|
||||
# generate netlist
|
||||
self.progress(40)
|
||||
self.process_manager.generate_netlist(temp_dir)
|
||||
|
||||
# generate data tables
|
||||
self.progress(50)
|
||||
self.process_manager.generate_tables(temp_dir, self.options[AUTO_TRANSLATE_OPT], self.options[EXCLUDE_DNP_OPT])
|
||||
|
||||
# generate pick and place file
|
||||
self.progress(60)
|
||||
self.process_manager.generate_positions(temp_dir)
|
||||
|
||||
# generate BOM file
|
||||
self.progress(70)
|
||||
self.process_manager.generate_bom(temp_dir)
|
||||
|
||||
# generate production archive
|
||||
self.progress(85)
|
||||
temp_file = self.process_manager.generate_archive(temp_dir_gerber, temp_file)
|
||||
shutil.move(temp_file, temp_dir)
|
||||
shutil.rmtree(temp_dir_gerber)
|
||||
temp_file = os.path.join(temp_dir, os.path.basename(temp_file))
|
||||
except Exception as e:
|
||||
if self.wx is None:
|
||||
logging.error("Fabrication Toolkit - Error" + str(e))
|
||||
else:
|
||||
wx.MessageBox(str(e), "Fabrication Toolkit - Error", wx.OK | wx.ICON_ERROR)
|
||||
self.progress(-1)
|
||||
return
|
||||
|
||||
# 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
|
||||
read_so_far += len(data)
|
||||
percent = read_so_far * 1e2 / total_size
|
||||
self.progress(85 + percent / 8)
|
||||
|
||||
# generate gerber name
|
||||
title_block = self.process_manager.board.GetTitleBlock()
|
||||
title = title_block.GetTitle()
|
||||
revision = title_block.GetRevision()
|
||||
company = title_block.GetCompany()
|
||||
file_date = title_block.GetDate()
|
||||
|
||||
if (hasattr(self.process_manager.board, "GetProject") and hasattr(pcbnew, "ExpandTextVars")):
|
||||
project = self.process_manager.board.GetProject()
|
||||
title = pcbnew.ExpandTextVars(title, project)
|
||||
revision = pcbnew.ExpandTextVars(revision, project)
|
||||
company = pcbnew.ExpandTextVars(company, project)
|
||||
file_date = pcbnew.ExpandTextVars(file_date, project)
|
||||
|
||||
# make output dir
|
||||
filename = os.path.splitext(os.path.basename(self.process_manager.board.GetFileName()))[0]
|
||||
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))
|
||||
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
|
||||
backup_name = ProcessManager.normalize_filename("_".join(("{} {} {}".format(title or filename, revision or '', timestamp).strip()).split()))
|
||||
shutil.make_archive(os.path.join(output_path, 'backups', backup_name), 'zip', temp_dir)
|
||||
|
||||
|
||||
# copy to & open output dir
|
||||
try:
|
||||
shutil.copytree(temp_dir, output_path, dirs_exist_ok=True)
|
||||
if self.openBrowser:
|
||||
webbrowser.open("file://%s" % (output_path))
|
||||
shutil.rmtree(temp_dir)
|
||||
except Exception as e:
|
||||
if self.openBrowser:
|
||||
webbrowser.open("file://%s" % (temp_dir))
|
||||
|
||||
if self.wx is None:
|
||||
self.progress(100)
|
||||
else:
|
||||
self.progress(-1)
|
||||
|
||||
def progress(self, percent):
|
||||
if self.wx is None:
|
||||
print_cli_progress_bar(percent, prefix = 'Progress:', suffix = 'Complete', length = 50)
|
||||
else:
|
||||
wx.PostEvent(self.wx, StatusEvent(percent))
|
52
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/transformations.csv
vendored
Normal file
52
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/transformations.csv
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
"Regex To Match","Rotation","Delta X","Delta Y"
|
||||
"^Bosch_LGA-",90,0,0
|
||||
"^CP_EIA-",180,0,0
|
||||
"^CP_Elec_",180,0,0
|
||||
"^C_Elec_",180,0,0
|
||||
"^DFN-",270,0,0
|
||||
"^DFN-",270,0,0
|
||||
"^D_SOT-23",180,0,0
|
||||
"^HTSSOP-",270,0,0
|
||||
"^HTSSOP-",270,0,0
|
||||
"^HTSSOP-",270,0,0
|
||||
"^JST_GH_SM",180,0,0
|
||||
"^JST_PH_S",180,0,0
|
||||
"^LQFP-",270,0,0
|
||||
"^MSOP-",270,0,0
|
||||
"^PowerPAK_SO-8_Single",270,0,0
|
||||
"^QFN-",90,0,0
|
||||
"^R_Array_Concave_",90,0,0
|
||||
"^R_Array_Convex_",90,0,0
|
||||
"^SC-74-6",180,0,0
|
||||
"^SOIC-",270,0,0
|
||||
"^SOIC-16_",270,0,0
|
||||
"^SOIC-8_",270,0,0
|
||||
"^SOIC127P798X216-8N",-90,0,0
|
||||
"^SOP-(?!18_)",270,0,0
|
||||
"^SOP-(?!18_)",270,0,0
|
||||
"^SOP-18_",0,0,0
|
||||
"^SOP-18_",0,0,0
|
||||
"^SOP-4_",0,0,0
|
||||
"^SOP-4_",0,0,0
|
||||
"^SOT-143",180,0,0
|
||||
"^SOT-223",180,0,0
|
||||
"^SOT-23",180,0,0
|
||||
"^SOT-353",180,0,0
|
||||
"^SOT-363",180,0,0
|
||||
"^SOT-89",180,0,0
|
||||
"^SSOP-",270,0,0
|
||||
"^SW_SPST_B3",90,0,0
|
||||
"^TDSON-8-1",270,0,0
|
||||
"^TO-277",90,0,0
|
||||
"^TQFP-",270,0,0
|
||||
"^TSOT-23",180,0,0
|
||||
"^TSSOP-",270,0,0
|
||||
"^UDFN-10",270,0,0
|
||||
"^USON-10",270,0,0
|
||||
"^VSON-8_",270,0,0
|
||||
"^VSSOP-10_-",270,0,0
|
||||
"^VSSOP-10_-",270,0,0
|
||||
"^VSSOP-8_",180,0,0
|
||||
"^VSSOP-8_",270,0,0
|
||||
"^VSSOP-8_3.0x3.0mm_P0.65mm",270,0,0
|
||||
"^qfn-",90,0,0
|
|
101
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/utils.py
vendored
Normal file
101
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/utils.py
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
import pcbnew # type: ignore
|
||||
import os
|
||||
import json
|
||||
from .config import optionsFileName
|
||||
import wx
|
||||
|
||||
def get_version():
|
||||
return float('.'.join(pcbnew.GetBuildVersion().split(".")[0:2])) # e.g GetBuildVersion(): e.g. '7.99.0-3969-gc5ac2337e4'
|
||||
|
||||
def is_v9(version = get_version()):
|
||||
return version >= 8.99 and version < 9.99
|
||||
|
||||
def is_v8(version = get_version()):
|
||||
return version >= 7.99 and version < 8.99
|
||||
|
||||
def is_v7(version = get_version()):
|
||||
return version >= 6.99 and version < 7.99
|
||||
|
||||
def is_v6(version = get_version()):
|
||||
return version >= 5.99 and version < 6.99
|
||||
|
||||
def footprint_has_field(footprint, field_name):
|
||||
version = get_version()
|
||||
|
||||
if is_v8(version) or is_v9(version):
|
||||
return footprint.HasFieldByName(field_name)
|
||||
else:
|
||||
return footprint.HasProperty(field_name)
|
||||
|
||||
def footprint_get_field(footprint, field_name):
|
||||
version = get_version()
|
||||
|
||||
if is_v8(version) or is_v9(version):
|
||||
return footprint.GetFieldByName(field_name).GetText()
|
||||
else:
|
||||
return footprint.GetProperty(field_name)
|
||||
|
||||
def get_user_options_file_path():
|
||||
boardFilePath = pcbnew.GetBoard().GetFileName()
|
||||
return os.path.join(os.path.dirname(boardFilePath), optionsFileName)
|
||||
|
||||
def load_user_options(default_options):
|
||||
try:
|
||||
with open(get_user_options_file_path(), 'r') as f:
|
||||
user_options = json.load(f)
|
||||
except:
|
||||
user_options = default_options
|
||||
|
||||
# merge the user options with the default options
|
||||
options = default_options.copy()
|
||||
options.update(user_options)
|
||||
return options
|
||||
|
||||
def save_user_options(options):
|
||||
try:
|
||||
with open(get_user_options_file_path(), 'w') as f:
|
||||
json.dump(options, f)
|
||||
except:
|
||||
wx.MessageBox("Error saving user options", "Error", wx.OK | wx.ICON_ERROR)
|
||||
|
||||
def get_plot_plan(board, active_only=True):
|
||||
"""Returns `(KiCad standard name, layer id, custom user name)` of all (active) layers of the given board."""
|
||||
layers = []
|
||||
i = pcbnew.PCBNEW_LAYER_ID_START - 1
|
||||
while i < pcbnew.PCBNEW_LAYER_ID_START + pcbnew.PCB_LAYER_ID_COUNT - 1:
|
||||
i += 1
|
||||
if active_only and not board.IsLayerEnabled(i):
|
||||
continue
|
||||
|
||||
layer_std_name = pcbnew.BOARD.GetStandardLayerName(i)
|
||||
layer_name = pcbnew.BOARD.GetLayerName(board, i)
|
||||
|
||||
layers.append((layer_std_name, i, layer_name))
|
||||
return layers
|
||||
|
||||
def get_layer_names(board, active_only=True):
|
||||
"""Returns a list of (active) layer names of the current board"""
|
||||
plotPlan = get_plot_plan(board, active_only)
|
||||
return [layer_info[0] for layer_info in plotPlan]
|
||||
|
||||
def print_cli_progress_bar(percent, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', printEnd = "\r"):
|
||||
"""
|
||||
Call in a loop to create terminal progress bar string
|
||||
@params:
|
||||
percent - Required : current percentage (Int)
|
||||
prefix - Optional : prefix string (Str)
|
||||
suffix - Optional : suffix string (Str)
|
||||
decimals - Optional : positive number of decimals in percent complete (Int)
|
||||
length - Optional : character length of bar (Int)
|
||||
fill - Optional : bar fill character (Str)
|
||||
printEnd - Optional : end character (e.g. "\r", "\r\n") (Str)
|
||||
"""
|
||||
if percent == -1:
|
||||
percent = 0
|
||||
filledLength = int(length * (percent / 100 ))
|
||||
bar = fill * filledLength + '-' * (length - filledLength)
|
||||
percent2dec = "%.2f" % percent
|
||||
print(f'\r{prefix} |{bar}| {percent2dec}% {suffix}', end = printEnd)
|
||||
# Print New Line on Complete
|
||||
if percent == 100:
|
||||
print()
|
Reference in New Issue
Block a user