Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bce98f72c3 | |||
| 84e933b566 | |||
| 3714af0afb | |||
| b80a14dfbb | |||
| bfa87db797 | |||
| c1322b705f | |||
| 371611e6f4 | |||
| 038cd1ddbe | |||
| e2e10578cf | |||
| eeca1704e2 | |||
| 6729b564db | |||
| 9e4e497899 | |||
| f92c64e7dc | |||
| 13fc67bbd7 | |||
| 8dc0e8a440 | |||
| e9f4b5b29b | |||
| 3cdcc31e63 | |||
| 8db1a4d446 | |||
| 2b8c898d22 | |||
| bad1d2d1a3 |
Vendored
+8
@@ -1,13 +1,21 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Colour",
|
||||
"csvfile",
|
||||
"Drilland",
|
||||
"EXCELLON",
|
||||
"FPID",
|
||||
"JLCPCB",
|
||||
"LCSC",
|
||||
"LSEQ",
|
||||
"netlist",
|
||||
"pcbnew",
|
||||
"Plotfile",
|
||||
"Protel",
|
||||
"rcos",
|
||||
"restkey",
|
||||
"restval",
|
||||
"rsin",
|
||||
"Soldermask"
|
||||
]
|
||||
}
|
||||
@@ -29,9 +29,9 @@ Download the [latest release](https://github.com/bennymeg/JLC-Plugin-for-KiCad/r
|
||||
## Usage
|
||||
Click on the Fabrication Toolkit <img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/resources/icon.png?raw=true" style="magin-bottom: 8px;" alt="Logo" height=24> button on the top tool box inside KiCad pcb editor (pcbnew).
|
||||
|
||||
⊛ Ensure your board is syncronized before invoking this feature.
|
||||
**⊛** Ensure your board is syncronized before invoking this addon [**F8**].
|
||||
|
||||
⊛ The `User_1` layer in internally defined as a **V-Cuts** layer, please avoid using it for anything else. <span style="text-color: light-grey !important;">_(since v3.0.0)_.</span>
|
||||
**⊛** The `User_1` layer in internally defined as a **V-Cuts** layer, please avoid using it for anything else. <span style="text-color: light-grey !important;">_(since v3.0.0)_.</span>
|
||||
|
||||
## Options
|
||||
|
||||
@@ -66,7 +66,7 @@ Select 'Exclude from position files' or 'Exclude from BOM' in the footprint's fa
|
||||
---
|
||||
|
||||
### ③ Offset Component Rotation
|
||||
The rotation of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLC PCB used different variation of the same standard. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'JLCPCB Rotation Offset' field with an counter-clockwise orientation offset in degrees to correct this.
|
||||
The rotation of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLC PCB used different variation of the same standard. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'JLCPCB Rotation Offset' field - with positive values indicating counter-clockwise orientation offset in degrees.
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation-jlc.png?raw=true" height=164>
|
||||
|
||||
@@ -91,7 +91,20 @@ _The fields will be queried in the order denoted above._
|
||||
---
|
||||
|
||||
### ④ Offset Component Position
|
||||
The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPB used different variation of the same standard. To the exception cases: add an 'JLCPCB Position Offset' field with an comma seperated x,y position offset to correct it.
|
||||
The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPB used different variation of the same standard. To the exception cases: add an 'JLCPCB Position Offset' field with an comma separated x,y position offset to correct it.
|
||||
|
||||
Use following table to quickly find out to which coordinate enter the correction based on JLC arrows clicks - depending on footprint rotation in Kicad PCB Editor status bar:
|
||||
|Kicad footprint deg | x | y|
|
||||
|----|----|----|
|
||||
|0deg, Front | right arrow | up arrow |
|
||||
|0deg, Back | left arrow | down arrow |
|
||||
|180deg, Front | left arrow | down arrow |
|
||||
|180deg, Back | right arrow | up arrow |
|
||||
|90deg, Front or Back | up arrow | left arrow |
|
||||
|-90deg, Front or Back | down arrow | right arrow |
|
||||
|
||||
For custom angles it's best to place also a temporary straight symbol to perform alignment.
|
||||
Single arrow press in JLC is about 0.07mm shift (8 clicks is about 0.5mm).
|
||||
|
||||
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/position.png?raw=true" height=420>
|
||||
|
||||
@@ -109,6 +122,25 @@ _The fields will be queried in the order denoted above._
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
### ⑤ Override Component Layer
|
||||
Some footprints may have their components defined on the opposite layer to there actual footprints. In these instances you can override mount side by using this field.
|
||||
|
||||
Values can be `top`, `bottom`, `t` or `b`.
|
||||
|
||||
#### Primary Fields*:
|
||||
| 'JLCPCB Layer Override' |
|
||||
| --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
#### Fallback Fields*:
|
||||
| 'JlcLayerOverride' | 'JLCLayerOverride' |
|
||||
| --- | --- |
|
||||
|
||||
_The fields will be queried in the order denoted above._
|
||||
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
Benny Megidish
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.1.0",
|
||||
"version": "4.1.0",
|
||||
"status": "stable",
|
||||
"kicad_version": "6.00"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ placementFileName = 'positions.csv'
|
||||
bomFileName = 'bom.csv'
|
||||
gerberArchiveName = 'gerbers.zip'
|
||||
outputFolder = 'production'
|
||||
bomRowLimit = 200
|
||||
|
||||
plotPlan = [
|
||||
("F.Cu", pcbnew.F_Cu, "Top Layer"),
|
||||
@@ -51,3 +52,5 @@ plotPlan = [
|
||||
("Edge.Cuts", pcbnew.Edge_Cuts, "Board Outline"),
|
||||
("User.Comments", pcbnew.Cmts_User, "User Comments")
|
||||
]
|
||||
|
||||
layers = [layer[0] for layer in plotPlan]
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
EXCLUDE_DNP_OPT = "EXCLUDE DNP"
|
||||
OUTPUT_NAME_OPT = "OUTPUT NAME"
|
||||
OUTPUT_NAME_OPT = "OUTPUT NAME"
|
||||
EXTRA_LAYERS = "EXTRA_LAYERS"
|
||||
+11
-2
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew # type: ignore
|
||||
import pcbnew # type: ignore
|
||||
|
||||
from .thread import ProcessThread
|
||||
from .events import StatusEvent
|
||||
from .options import EXCLUDE_DNP_OPT
|
||||
from .options import EXCLUDE_DNP_OPT, EXTRA_LAYERS
|
||||
from .config import layers
|
||||
|
||||
|
||||
# WX GUI form that show the plugin progress
|
||||
@@ -31,6 +32,11 @@ class KiCadToJLCForm(wx.Frame):
|
||||
self.mExcludeDnpCheckbox = wx.CheckBox(self, label='Exclude DNP components')
|
||||
self.mExcludeDnpCheckbox.SetValue(False)
|
||||
|
||||
self.mAdditionalLayersControl = wx.TextCtrl(self, size=wx.Size(600, 50))
|
||||
self.mAdditionalLayersControl.Hint = "Additional layers"
|
||||
self.mAdditionalLayersControl.AutoComplete(layers)
|
||||
self.mAdditionalLayersControl.Enable()
|
||||
|
||||
self.mGenerateButton = wx.Button(self, label='Generate', size=wx.Size(600, 60))
|
||||
|
||||
self.mGaugeStatus = wx.Gauge(
|
||||
@@ -44,6 +50,7 @@ class KiCadToJLCForm(wx.Frame):
|
||||
|
||||
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.mExcludeDnpCheckbox, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mGaugeStatus, 0, wx.ALL, 5)
|
||||
boxSizer.Add(self.mGenerateButton, 0, wx.ALL, 5)
|
||||
@@ -58,7 +65,9 @@ class KiCadToJLCForm(wx.Frame):
|
||||
def onGenerateButtonClick(self, event):
|
||||
options = dict()
|
||||
options[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
|
||||
options[EXTRA_LAYERS] = self.mAdditionalLayersControl.GetValue()
|
||||
|
||||
self.mAdditionalLayersControl.Hide()
|
||||
self.mExcludeDnpCheckbox.Hide()
|
||||
self.mOptionsLabel.Hide()
|
||||
self.mGenerateButton.Hide()
|
||||
|
||||
+192
-77
@@ -3,14 +3,16 @@ from __future__ import annotations
|
||||
|
||||
# System base libraries
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import math
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
# Interaction with KiCad.
|
||||
import pcbnew # type: ignore
|
||||
from .utils import footprint_has_field, footprint_get_field
|
||||
|
||||
# Application definitions.
|
||||
from .config import *
|
||||
@@ -27,45 +29,6 @@ class ProcessManager:
|
||||
def normalize_filename(filename):
|
||||
return re.sub(r'[^\w\s\.\-]', '', filename)
|
||||
|
||||
@staticmethod
|
||||
def __read_rotation_db(filename: str = os.path.join(os.path.dirname(__file__), 'rotations.cf')) -> dict[str, float]:
|
||||
'''Read the rotations.cf config file so we know what rotations
|
||||
to apply later.
|
||||
'''
|
||||
db = {}
|
||||
|
||||
with open(filename, 'r') as fh:
|
||||
for line in fh:
|
||||
line = line.rstrip()
|
||||
line = re.sub('#.*$', '', line) # remove anything after a comment
|
||||
line = re.sub('\s*$', '', line) # remove all trailing space
|
||||
|
||||
if (line == ""):
|
||||
continue
|
||||
|
||||
match = re.match('^([^\s]+)\s+(\d+)$', line)
|
||||
|
||||
if match:
|
||||
db.update({ match.group(1): int(match.group(2)) })
|
||||
|
||||
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.
|
||||
fpshort = footprint.split(':')[-1]
|
||||
|
||||
for expression, delta in self.db.items():
|
||||
fp = fpshort
|
||||
|
||||
if (re.search(':', expression)):
|
||||
fp = footprint
|
||||
|
||||
if(re.search(expression, fp)):
|
||||
return delta
|
||||
|
||||
return 0.0
|
||||
|
||||
def update_zone_fills(self):
|
||||
'''Verify all zones have up-to-date fills.'''
|
||||
filler = pcbnew.ZONE_FILLER(self.board)
|
||||
@@ -79,7 +42,7 @@ class ProcessManager:
|
||||
# Finally rebuild the connectivity db
|
||||
self.board.BuildConnectivity()
|
||||
|
||||
def generate_gerber(self, temp_dir):
|
||||
def generate_gerber(self, temp_dir, extra_layers):
|
||||
'''Generate the Gerber files.'''
|
||||
settings = self.board.GetDesignSettings()
|
||||
settings.m_SolderMaskMargin = 50000
|
||||
@@ -104,12 +67,17 @@ class ProcessManager:
|
||||
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 plotPlan:
|
||||
if self.board.IsLayerEnabled(layer_info[1]):
|
||||
if self.board.IsLayerEnabled(layer_info[1]) or layer_info[0] in extra_layers:
|
||||
plot_controller.SetLayer(layer_info[1])
|
||||
plot_controller.OpenPlotfile(layer_info[0], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
|
||||
|
||||
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'plotLayers'):
|
||||
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'PlotLayers'):
|
||||
# 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+
|
||||
seq = pcbnew.LSEQ()
|
||||
@@ -138,6 +106,19 @@ class ProcessManager:
|
||||
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 bounding box."""
|
||||
if footprint.GetAttributes() & pcbnew.FP_SMD:
|
||||
return footprint.GetPosition()
|
||||
if footprint.GetAttributes() & pcbnew.FP_THROUGH_HOLE:
|
||||
return footprint.GetBoundingBox(False, False).GetCenter()
|
||||
pads = footprint.Pads()
|
||||
#get bounding box based on pads only to ignore non-copper layers, e.g. silkscreen
|
||||
bbox = pads[0].GetBoundingBox()
|
||||
for pad in pads:
|
||||
bbox.Merge(pad.GetBoundingBox())
|
||||
return bbox.GetCenter()
|
||||
|
||||
def generate_tables(self, temp_dir, exclude_dnp):
|
||||
'''Generate the data tables.'''
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
@@ -166,18 +147,17 @@ class ProcessManager:
|
||||
except AttributeError:
|
||||
footprint_name = str(footprint.GetFPID().GetLibItemName())
|
||||
|
||||
layer = {
|
||||
pcbnew.F_Cu: 'top',
|
||||
pcbnew.B_Cu: 'bottom',
|
||||
}.get(footprint.GetLayer())
|
||||
layer = self._get_top_or_bottom_side_override_from_footprint(footprint)
|
||||
|
||||
# mount_type = {
|
||||
# 0: 'smt',
|
||||
# 1: 'tht',
|
||||
# 2: 'smt'
|
||||
# 2: 'unspecified'
|
||||
# }.get(footprint.GetAttributes())
|
||||
|
||||
skip_footprint = exclude_dnp and (footprint.HasProperty('dnp') or footprint.GetValue().upper() == 'DNP')
|
||||
skip_footprint = exclude_dnp and (footprint_has_field(footprint, 'dnp')
|
||||
or footprint.GetValue().upper() == 'DNP'
|
||||
or getattr(footprint, 'IsDNP', bool)())
|
||||
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES) and not skip_footprint:
|
||||
# append unique ID if duplicate footprint designator
|
||||
@@ -187,25 +167,28 @@ class ProcessManager:
|
||||
footprint_designators[footprint.GetReference()] -= 1
|
||||
|
||||
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
|
||||
mid_x = (self._get_footprint_position(footprint)[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
|
||||
mid_y = (self._get_footprint_position(footprint)[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0
|
||||
rotation = footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
|
||||
# Get the rotation offset to be added to the actual rotation prioritizing the explicated by the
|
||||
# designer at the standards symbol fields. If not specified use the internal database.
|
||||
rotation_offset = self._get_rotation_offset_from_footprint(footprint) #or self._get_rotation_from_db(footprint)
|
||||
rotation = (rotation + rotation_offset) % 360.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)
|
||||
rsin = math.sin(rotation / 180 * math.pi)
|
||||
rcos = math.cos(rotation / 180 * math.pi)
|
||||
pos_offset = ( pos_offset[0] * rcos - pos_offset[1] * rsin, pos_offset[0] * rsin + pos_offset[1] * rcos )
|
||||
|
||||
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 = (540.0 - rotation) % 360.0
|
||||
|
||||
rotation = (180.0 - rotation)
|
||||
rotation = (rotation + rotation_offset_db + rotation_offset_manual) % 360.0
|
||||
|
||||
self.components.append({
|
||||
'Designator': designator,
|
||||
'Mid X': mid_x,
|
||||
@@ -227,12 +210,13 @@ class ProcessManager:
|
||||
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:
|
||||
if same_footprint and same_value and same_lcsc and under_limit:
|
||||
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
|
||||
component['Quantity'] += 1
|
||||
insert = False
|
||||
# break ?
|
||||
break
|
||||
|
||||
# add component to BOM
|
||||
if insert:
|
||||
@@ -283,59 +267,190 @@ class ProcessManager:
|
||||
os.remove(os.path.join(temp_dir, item))
|
||||
|
||||
return temp_file
|
||||
|
||||
""" Private """
|
||||
|
||||
def _get_mpn_from_footprint(self, footprint):
|
||||
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 #', 'JLCPCB Part #']
|
||||
fallback_keys = ['LCSC Part', 'JLC Part', 'LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
|
||||
|
||||
if footprint.HasProperty('dnp'):
|
||||
if footprint_has_field(footprint, 'dnp'):
|
||||
return 'DNP'
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint.HasProperty(key):
|
||||
return footprint.GetProperty(key)
|
||||
if footprint_has_field(footprint, key):
|
||||
return footprint_get_field(footprint, key)
|
||||
|
||||
def _get_top_or_bottom_side_override_from_footprint(self, footprint) -> str:
|
||||
keys = ['JLCPCB Layer Override']
|
||||
fallback_keys = ['JlcLayerOverride', 'JLCLayerOverride']
|
||||
|
||||
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 (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 from standard symbol fields.'''
|
||||
keys = ['JLCPCB Rotation Offset']
|
||||
fallback_keys = ['JlcRotOffset', 'JLCRotOffset']
|
||||
|
||||
offset = None
|
||||
offset = ""
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint.HasProperty(key):
|
||||
offset = footprint.GetProperty(key)
|
||||
if footprint_has_field(footprint, key):
|
||||
offset = footprint_get_field(footprint, key)
|
||||
break
|
||||
|
||||
if offset is None or offset == "":
|
||||
return 0
|
||||
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):
|
||||
def _get_position_offset_from_footprint(self, footprint) -> Tuple[float, float]:
|
||||
keys = ['JLCPCB Position Offset']
|
||||
fallback_keys = ['JlcPosOffset', 'JLCPosOffset']
|
||||
|
||||
offset = None
|
||||
offset = ""
|
||||
|
||||
for key in keys + fallback_keys:
|
||||
if footprint.HasProperty(key):
|
||||
offset = footprint.GetProperty(key)
|
||||
if footprint_has_field(footprint, key):
|
||||
offset = footprint_get_field(footprint, key)
|
||||
break
|
||||
|
||||
if offset is None or offset == "":
|
||||
return (0, 0)
|
||||
if offset == "":
|
||||
return (0.0, 0.0)
|
||||
else:
|
||||
try:
|
||||
return ( float(offset.split(",")[0]), float(offset.split(",")[1]) )
|
||||
except ValueError:
|
||||
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 _normalize_footprint_name(self, footprint):
|
||||
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.*$')
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#
|
||||
# Rotations for jlcpcba
|
||||
#
|
||||
# This defines additional rotations we need to apply to ensure the
|
||||
# footprints are correctly orientated to work with the JLCPCB PBCA
|
||||
# service
|
||||
#
|
||||
# These are regular expressions that are matched by default against
|
||||
# the short footprint name (without the library name), however if there
|
||||
# is a colon in the regex then the full name (with the library name) is
|
||||
# used.
|
||||
#
|
||||
# The matches are checked against one by one, and the first match
|
||||
# is used
|
||||
#
|
||||
# The regex and the rotation value is separated by any amount of
|
||||
# whitespace, blank lines and comments are ignored
|
||||
#
|
||||
|
||||
#
|
||||
# Normal short-name matches
|
||||
#
|
||||
^SOT-223 180
|
||||
^SOT-23 180
|
||||
^D_SOT-23 180
|
||||
^TSOT-23 180
|
||||
^SOT-353 180
|
||||
^QFN- 90
|
||||
^qfn- 90
|
||||
^LQFP- 270
|
||||
^TQFP- 270
|
||||
^MSOP- 270
|
||||
^TSSOP- 270
|
||||
^DFN- 270
|
||||
^SOIC-8_ 270
|
||||
^SOIC-16_ 270
|
||||
^VSSOP-10_- 270
|
||||
|
||||
#
|
||||
# Polarised caps are 180 out
|
||||
#
|
||||
^CP_Elec_ 180
|
||||
^C_Elec_ 180
|
||||
^CP_EIA- 180
|
||||
|
||||
#
|
||||
# Long name matches
|
||||
#
|
||||
#^Lees_Footprints: 270
|
||||
^LED_WS2812B_PLCC4 180
|
||||
|
||||
# More rotation from https://github.com/matthewlai/JLCKicadTools/tree/master/jlc_kicad_tools
|
||||
^R_Array_Convex_ 90
|
||||
^R_Array_Concave_ 90
|
||||
^SOP-4_ 0
|
||||
^SOP-(?!18_) 270
|
||||
^SOP-18_ 0
|
||||
^VSSOP-8_ 270
|
||||
^Bosch_LGA- 90
|
||||
^PowerPAK_SO-8_Single 270
|
||||
^HTSSOP- 270
|
||||
+18
-18
@@ -25,11 +25,9 @@ class ProcessThread(Thread):
|
||||
# initializing
|
||||
self.progress(0)
|
||||
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
|
||||
|
||||
temp_dir = tempfile.mkdtemp() + timestamp
|
||||
os.makedirs(temp_dir)
|
||||
os.makedirs(temp_dir + "_g")
|
||||
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())
|
||||
@@ -41,11 +39,11 @@ class ProcessThread(Thread):
|
||||
|
||||
# generate gerber
|
||||
self.progress(20)
|
||||
self.process_manager.generate_gerber(temp_dir + "_g")
|
||||
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS])
|
||||
|
||||
# generate drill file
|
||||
self.progress(30)
|
||||
self.process_manager.generate_drills(temp_dir + "_g")
|
||||
self.process_manager.generate_drills(temp_dir_gerber)
|
||||
|
||||
# generate netlist
|
||||
self.progress(40)
|
||||
@@ -65,12 +63,12 @@ class ProcessThread(Thread):
|
||||
|
||||
# generate production archive
|
||||
self.progress(85)
|
||||
temp_file = self.process_manager.generate_archive(temp_dir + "_g", temp_file)
|
||||
temp_file = self.process_manager.generate_archive(temp_dir_gerber, temp_file)
|
||||
shutil.move(temp_file, temp_dir)
|
||||
shutil.rmtree(temp_dir + "_g")
|
||||
shutil.rmtree(temp_dir_gerber)
|
||||
temp_file = os.path.join(temp_dir, os.path.basename(temp_file))
|
||||
except Exception as e:
|
||||
wx.MessageBox(str(e), "Error", wx.OK | wx.ICON_ERROR)
|
||||
wx.MessageBox(str(e), "Fabrication Toolkit - Error", wx.OK | wx.ICON_ERROR)
|
||||
self.progress(-1)
|
||||
return
|
||||
|
||||
@@ -102,20 +100,22 @@ class ProcessThread(Thread):
|
||||
|
||||
# make output dir
|
||||
filename = os.path.splitext(os.path.basename(self.process_manager.board.GetFileName()))[0]
|
||||
name = ProcessManager.normalize_filename("_".join(("{} {} {}".format(title or filename, revision or '', timestamp).strip()).split()))
|
||||
output_path = os.path.join(project_directory, outputFolder, name)
|
||||
os.makedirs(output_path)
|
||||
|
||||
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:
|
||||
if os.path.exists(output_path):
|
||||
shutil.rmtree(output_path)
|
||||
|
||||
shutil.copytree(temp_dir, output_path)
|
||||
shutil.copytree(temp_dir, output_path, dirs_exist_ok=True)
|
||||
webbrowser.open("file://%s" % (output_path))
|
||||
shutil.rmtree(temp_dir)
|
||||
except Exception as e:
|
||||
|
||||
@@ -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
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import pcbnew # type: ignore
|
||||
|
||||
def get_version():
|
||||
return float('.'.join(pcbnew.GetBuildVersion().split(".")[0:2])) #e.g GetBuildVersion(): e.g. '7.99.0-3969-gc5ac2337e4'
|
||||
|
||||
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):
|
||||
if is_v8():
|
||||
return footprint.HasFieldByName(field_name)
|
||||
else:
|
||||
return footprint.HasProperty(field_name)
|
||||
|
||||
def footprint_get_field(footprint, field_name):
|
||||
if is_v8():
|
||||
return footprint.GetFieldByName(field_name).GetText()
|
||||
else:
|
||||
return footprint.GetProperty(field_name)
|
||||
Reference in New Issue
Block a user