Compare commits

..

1 Commits

Author SHA1 Message Date
Benny Megidish 4c08e18ef9 feat: upload 2022-07-26 14:21:57 +03:00
13 changed files with 93 additions and 343 deletions
-13
View File
@@ -1,13 +0,0 @@
# These are supported funding model platforms
github: bennymeg
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-1
View File
@@ -1 +0,0 @@
- Hildo Guillardi Júnior
+7 -64
View File
@@ -6,8 +6,6 @@
| **JLC PCB Plug-in for KiCad** |
|:-----:|
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/bennymeg)
</div>
## Features
@@ -19,82 +17,27 @@
## Installation
### Official Installation
Fabrication Toolkit is distributed with the official releases of KiCad 6+. Open the "Plugin and Content Manager" from the KiCad main menu and install the "<ins>Fabrication Toolkit</ins>" plugin from the selection list.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/manager.png?raw=true" height=275>
Open the "Plugin and Content Manager" from the KiCad main menu an install the "<ins>Fabrication Toolkit</ins>" plugin from the selection list.
### Manual installation
Download the [latest release](https://github.com/bennymeg/JLC-Plugin-for-KiCad/releases) ZIP file. Open the "Plugin and Content Manager" from the KiCads main window and install the ZIP file via "Install from File".
## 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).
Download the latest ZIP file from https://github.com/bennymeg/JLC-Plugin-for-KiCad. Open the "Plugin and Content Manager" from the KiCads main window and install the ZIP file via "Install from File".
## Options
### Include Component Part Number in Production Files
Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's fields property.
Add an 'MPN'* field with the LCSC component part number to the symbol's fields property.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/mpn.png?raw=true" height=420>
#### Primary Fields*:
| 'LCSC Part #' | 'JLCPCB Part #' |
| --- | --- |
_The fields will be query in the order denoted above._
#### Fallback Fields*:
| 'lcsc#' | 'LCSC' | 'JLC' | 'MPN' | 'Mpn' | 'mpn' |
| --- | --- | --- | --- | --- | --- |
_The fields will be query in the order denoted above._
#### Similar Fields*:
|'mpn' | 'Mpn' | 'MPN' | 'JLC_MPN' | 'LCSC_MPN' | 'LCSC Part #' | 'JLC' | 'LCSC'|
| --- | --- | --- | --- | --- | --- | --- | --- |
### Ignore Footprint in Production Files
Select 'Exclude from board' or 'Exclude from BOM' in the symbol's attributes property in order to ignore the footprint from the relevant file.
Select 'Exclude from position files' or 'Exclude from BOM' in the symbol's attributes property in order to ignore the footprint from the relevant file.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/attributes.png?raw=true" height=420>
Select 'Exclude from position files' or 'Exclude from BOM' in the footprint's fabrication attributes property in order to ignore the footprint from the relevant file.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/fabrication.png?raw=true" height=505>
### Offset Component Rotation
The rotation 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. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'JLC Rotation Offset' field with an counter-clockwise orientation offset in degrees to correct this.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation-jlc.png?raw=true" height=164>
If the JLC preview shows a footprint like this, enter a rotation offset of -90 to rotate pin 1 to the lower right corner.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation.png?raw=true" height=420>
#### Primary Fields*:
| 'JLCPCB Rotation Offset' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'JlcRotOffset' | 'JLCRotOffset' |
| --- | --- |
_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 'JLC Position Offset' field with an comma seperated x,y position offset to correct it.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/position.png?raw=true" height=420>
#### Primary Fields*:
| 'JLCPCB Position Offset' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'JlcPosOffset' | 'JLCPosOffset' |
| --- | --- |
_The fields will be queried in the order denoted above._
## Author
Benny Megidish
Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

+4 -4
View File
@@ -2,7 +2,7 @@
"$schema": "https://go.kicad.org/pcm/schemas/v1",
"name": "Fabrication Toolkit",
"description": "JLC PCB fabrication toolkit",
"description_full": "Toolkit for automating PCB fabrication process with KiCad and JLC PCB. \n\n OPTIONS: \n\u2022 Include Component Part Number in Production Files \n Add an 'MPN' field with the LCSC component part number to the footprint component options. \n\n\u2022 Ignore Footprint in Production Files \n Select 'Exclude from position files' or 'Exclude from BOM' in the footprint properties fabrication attributes in order to ignore the footprint from the relevant file. \n\n\u2022 Rotate components \n Add an 'JLCPCB Rotation Offset' field with the additional counter-clockwise rotation in degrees to correct a mismatch between KiCad and JLC footprint orientation.",
"description_full": "Toolkit for automating PCB fabrication process with KiCad and JLC PCB. \n\n OPTIONS: \n\u2022 Include Component Part Number in Production Files \n Add an 'MPN' field with the LCSC component part number to the footprint component options. \n\n\u2022 Ignore Footprint in Production Files \n Select 'Exclude from position files' or 'Exclude from BOM' in the footprint component options to in order ignore the footprint from the relevant file.",
"identifier": "com.github.bennymeg.JLC-Plugin-for-KiCad",
"type": "plugin",
"author": {
@@ -29,9 +29,9 @@
],
"versions": [
{
"version": "2.1.0",
"status": "stable",
"version": "0.4.2",
"status": "testing",
"kicad_version": "6.00"
}
]
}
}
+1 -1
View File
@@ -43,7 +43,7 @@ class KiCadToJLCForm(wx.Frame):
pcbnew.Refresh()
self.Destroy()
else:
self.m_gaugeStatus.SetValue(int(status.data))
self.m_gaugeStatus.SetValue(status.data)
# Plugin definition
+59 -176
View File
@@ -1,18 +1,10 @@
# For better annotation.
from __future__ import annotations
# System base libraries
import json
import requests
import os
import csv
import math
import shutil
from collections import defaultdict
import re
# Interaction with KiCad.
import pcbnew
# Application definitions.
from collections import defaultdict
from .config import *
@@ -21,49 +13,8 @@ class ProcessManager:
self.board = pcbnew.GetBoard()
self.bom = []
self.components = []
self.__rotation_db = self.__read_rotation_db()
@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 generate_gerber(self, temp_dir):
'''Generate the Gerber files.'''
settings = self.board.GetDesignSettings()
settings.m_SolderMaskMargin = 0
settings.m_SolderMaskMinWidth = 0
@@ -78,13 +29,11 @@ class ProcessManager:
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(True)
plot_options.SetSubtractMaskFromSilk(False)
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
if hasattr(plot_options, "SetExcludeEdgeLayer"):
plot_options.SetExcludeEdgeLayer(True)
for layer_info in plotPlan:
if self.board.IsLayerEnabled(layer_info[1]):
@@ -98,7 +47,6 @@ class ProcessManager:
plot_controller.ClosePlot()
def generate_drills(self, temp_dir):
'''Generate the drill file.'''
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
drill_writer.SetOptions(
@@ -110,20 +58,15 @@ class ProcessManager:
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, False)
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 generate_positions(self, temp_dir):
'''Generate the position files.'''
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())
# unique designator dictionary
footprint_designators = defaultdict(int)
for i, footprint in enumerate(footprints):
@@ -131,10 +74,9 @@ class ProcessManager:
footprint_designators[footprint.GetReference()] += 1
bom_designators = footprint_designators.copy()
if len(footprint_designators.items()) > 0:
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8') as f:
for key, value in footprint_designators.items():
f.write('%s:%s\n' % (key, value))
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8') as f:
for key, value in footprint_designators.items():
f.write('%s:%s\n' % (key, value))
for i, footprint in enumerate(footprints):
try:
@@ -160,27 +102,11 @@ class ProcessManager:
unique_id = str(footprint_designators[footprint.GetReference()])
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
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
# 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 )
mid_x, mid_y = tuple(map(sum,zip((mid_x, mid_y), pos_offset)))
self.components.append({
'Designator': designator,
'Mid X': mid_x,
'Mid Y': mid_y,
'Rotation': rotation,
'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,
})
@@ -191,51 +117,48 @@ class ProcessManager:
unique_id = str(bom_designators[footprint.GetReference()])
bom_designators[footprint.GetReference()] -= 1
# merge similar parts into single entry
insert = True
for component in self.bom:
if component['Footprint'] == self._normalize_footprint_name(footprint_name) and component['Value'].upper() == footprint.GetValue().upper():
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
component['Quantity'] += 1
insert = False
# todo: merge similar parts into single entry
# add component to BOM
if insert:
self.bom.append({
'Designator': "{}{}{}".format(footprint.GetReference(), "" 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),
})
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),
})
if len(self.components) > 0:
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8') as outfile:
csv_writer = csv.writer(outfile)
# writing headers of CSV file
csv_writer.writerow(self.components[0].keys())
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8') as outfile:
header = True
csv_writer = csv.writer(outfile)
for component in self.components:
# writing data of CSV file
if ('**' not in component['Designator']):
csv_writer.writerow(component.values())
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):
if len(self.bom) > 0:
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8') 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())
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8') 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):
'''Generate the files.'''
temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
temp_file = shutil.move(temp_file, temp_dir)
@@ -246,60 +169,20 @@ class ProcessManager:
return temp_file
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', 'JLC', 'MPN', 'Mpn', 'mpn']
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') }
data = { 'boardWidth': boardWidth, 'boardHeight': boardHeight, 'boardLayer': boardLayer }
upload_url = baseUrl + '/Common/KiCadUpFile/'
response = requests.post(upload_url, files=files, data=data)
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)
for key in fallback_keys:
if footprint.HasProperty(key):
return footprint.GetProperty(key)
def _get_rotation_offset_from_footprint(self, footprint: str) -> float:
'''Get the rotation from standard symbol fields.'''
keys = ['JLCPCB Rotation Offset']
fallback_keys = ['JlcRotOffset', 'JLCRotOffset']
offset = None
for key in keys + fallback_keys:
if footprint.HasProperty(key):
offset = footprint.GetProperty(key)
break
if offset is None or offset == "":
return 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):
keys = ['JLCPCB Position Offset']
fallback_keys = ['JlcPosOffset', 'JLCPosOffset']
offset = None
for key in keys + fallback_keys:
if footprint.HasProperty(key):
offset = footprint.GetProperty(key)
break
if offset is None or offset == "":
return (0, 0)
else:
try:
return ( float(offset.split(",")[0]), float(offset.split(",")[1]) )
except ValueError:
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
def _normalize_footprint_name(self, footprint):
# 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)
-61
View File
@@ -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
+22 -23
View File
@@ -26,34 +26,33 @@ class ProcessThread(Thread):
project_directory = os.path.dirname(self.process_manager.board.GetFileName())
output_path = os.path.join(project_directory, outputFolder)
try:
# configure and generate gerber
self.report(5)
self.process_manager.generate_gerber(temp_dir)
# configure and generate gerber
self.report(5)
self.process_manager.generate_gerber(temp_dir)
# generate drill file
self.report(15)
self.process_manager.generate_drills(temp_dir)
# generate drill file
self.report(15)
self.process_manager.generate_drills(temp_dir)
# generate netlist
self.report(25)
self.process_manager.generate_netlist(temp_dir)
# generate netlist
self.report(25)
self.process_manager.generate_netlist(temp_dir)
# generate pick and place file
self.report(40)
self.process_manager.generate_positions(temp_dir)
# generate pick and place file
self.report(40)
self.process_manager.generate_positions(temp_dir)
# generate BOM file
self.report(60)
self.process_manager.generate_bom(temp_dir)
# generate BOM file
self.report(60)
self.process_manager.generate_bom(temp_dir)
# generate production archive
self.report(75)
temp_file = self.process_manager.generate_archive(temp_dir, temp_file)
# generate production archive
self.report(75)
temp_file = self.process_manager.generate_archive(temp_dir, temp_file)
except Exception as e:
wx.MessageBox(str(e), "Error", wx.OK | wx.ICON_ERROR)
self.report(-1)
return
# upload files
#self.report(87.5)
#self.process_manager.upload_archive(temp_file)
# progress bar done animation
read_so_far = 0