Skip to content

Commit b33239c

Browse files
committed
Initial draft
0 parents  commit b33239c

27 files changed

+1682
-0
lines changed

.github/workflows/build_asset.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Build Asset
2+
on:
3+
workflow_dispatch:
4+
branches:
5+
6+
jobs:
7+
build:
8+
9+
name: Build
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@master
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.10'
20+
21+
- name: Update zip
22+
run: |
23+
cd pcm
24+
python build.py
25+
cd build
26+
echo "ZIP_NAME=$(ls SparkFunKiCadCAMmer*.zip)" >> $GITHUB_ENV
27+
echo "PCM_NAME=$(ls SparkFunKiCadCAMmer*.zip | rev | cut -c 5- | rev)" >> $GITHUB_ENV
28+
29+
- name: Upload pcm build to action - avoid double-zip
30+
uses: actions/upload-artifact@v3
31+
with:
32+
name: ${{ env.PCM_NAME }}
33+
path: ./pcm/build/${{ env.PCM_NAME }}
34+
retention-days: 7
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Build Asset for Release
2+
on:
3+
release:
4+
types: [published]
5+
6+
jobs:
7+
build:
8+
9+
name: Build
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@master
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.10'
20+
21+
- name: Update zip
22+
run: |
23+
cd pcm
24+
python build.py
25+
cd build
26+
echo "ZIP_NAME=$(ls SparkFunKiCadCAMmer*.zip)" >> $GITHUB_ENV
27+
echo "PCM_NAME=$(ls SparkFunKiCadCAMmer*.zip | rev | cut -c 5- | rev)" >> $GITHUB_ENV
28+
29+
- name: Publish release
30+
uses: softprops/action-gh-release@v1
31+
with:
32+
files: |
33+
./pcm/build/${{ env.ZIP_NAME }}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__pycache__/
2+
*.py[co]
3+
cammer_config.json
4+
cammer.log
5+
pcm/build/

LICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
**SparkFun code, firmware, and software is released under the MIT License(http://opensource.org/licenses/MIT).**
3+
4+
The MIT License (MIT)
5+
6+
Copyright (c) 2023 SparkFun Electronics
7+
8+
Permission is hereby granted, free of charge, to any person obtaining a copy
9+
of this software and associated documentation files (the "Software"), to deal
10+
in the Software without restriction, including without limitation the rights
11+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
copies of the Software, and to permit persons to whom the Software is
13+
furnished to do so, subject to the following conditions:
14+
15+
The above copyright notice and this permission notice shall be included in all
16+
copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SparkFun PCB CAMmer plugin for KiCad 7
2+
3+
This plugin generates the Gerber and Drill files for a KiCad PCB.
4+
5+
![CAMmer](./img/cammer.png)
6+
7+
We've tried to keep this CAMmer simple and easy-to-use, while also including all of the features of the original [SparkFun CAMmer for Eagle](https://github.com/sparkfun/SparkFun_Eagle_Settings/blob/main/ulp/SparkFun-CAMmer.ulp). This plugin:
8+
9+
* Generates and zips the gerber and drill files
10+
* Adds `ordering_instructions.txt` to the zip file, if found
11+
* Will merge multiple layers (E.g. `Edge.Cuts` and `User.Comments`) to generate the board outline
12+
* The SparkFun KiCad Panelizer places the V-SCORE information in `User.Comments` by default
13+
14+
You can of course use KiCad's built-in **Fabrication Output** tools to do most of the above manually.
15+
16+
## Installation and Usage
17+
18+
Open the KiCad Plugin and Content Manager (PCM) from the main window and filter for `SparkFun CAMmer`.
19+
20+
To install manually, open the [GitHub Repo Releases page](https://github.com/sparkfun/SparkFun_KiCad_CAMmer/releases) and download the `SparkFunKiCadCAMmer-pcm.zip` file attached to the latest release. Then use the PCM _**Install from File...**_ option and select the .zip file to install it. For best results, **Uninstall** the previous version first, **Apply Pending Changes**, and then **Install from File...**.
21+
22+
![Install manually](./img/install_from_file.png)
23+
24+
The CAMmer plugin runs inside the KiCad PCB Editor window. (Although you can run the CAMmer in a Command Prompt too. See [below](#how-it-works) for details.)
25+
26+
Click the CAMmer icon to open the GUI:
27+
28+
![Open CAMmer](./img/run_cammer.png)
29+
30+
We have deliberately kept the GUI options as simple as possible:
31+
32+
* In column 1: select your copper, silkscreen and solder mask layers
33+
* In column 2: select which layer(s) will be used to generate the board outline
34+
35+
Click **Run CAMmer** to run the CAMmer.
36+
37+
![Run CAMmer](./img/run_cammer_2.png)
38+
39+
The CAMmer settings are saved in a file called `cammer_config.json` so they can be reused.
40+
41+
`cammer.log` contains useful diagnostic information.
42+
43+
## License and Credits
44+
45+
The code for this plugin is licensed under the MIT license. Please see `LICENSE` for more info.
46+
47+
`cammer.py` is based heavily on the [KiCad code example](https://gitlab.com/kicad/code/kicad/-/blob/master/demos/python_scripts_examples/plot_board.py).
48+
49+
The [wxFormBuilder](https://github.com/wxFormBuilder/wxFormBuilder/releases) `text_dialog.fbp` and associated code is based on [Greg Davill (@gregdavill)](https://github.com/gregdavill)'s [KiBuzzard](https://github.com/gregdavill/KiBuzzard).
50+
51+
## How It Works
52+
53+
The plugin GUI itself is designed with [wxFormBuilder](https://github.com/wxFormBuilder/wxFormBuilder/releases) and stored in `text_dialog.fbp`.
54+
Copy and paste the wx Python code from wxFormBuilder into `./SparkFunKiCadCAMmer/dialog/dialog_text_base.py`.
55+
56+
`.github/workflows/build_asset_release.yml` generates the .zip file containing the plugin Python code (`./plugins`), icon (`./resources`) and the Plugin and Content Manager (PCM) `metadata.json`. The workflow automatically attaches the zip file to each release as an asset. Edit `./SparkFunKiCadCAMmer/resource/_version.py` first and update the version number. `build.py` is called by the workflow, copies `metadata_template.json` into `metadata.json` and then updates it with the correct version and download URL. The version number is also added to the .zip filename. The PCM should automatically download and install new versions of the CAMmer for you.
57+
58+
You can run the CAMmer stand-alone if you want to. Open a **KiCad 7.0 Command Prompt**. On Windows, you will find this in `Start Menu / All Apps / KiCad 7.0`. cd to the `SparkFun_KiCad_CAMmer\SparkFunKiCadCAMmer\cammer` directory. `python cammer.py` will show the help for the arguments. When you run the CAMmer plugin in KiCad, it will generate files for whichever PCB is currently open. When running the CAMmer stand-alone, you need to provide the path `-p` to the PCB.
59+
60+
- Your friends at SparkFun
61+

SparkFunKiCadCAMmer/__init__.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import os
2+
import sys
3+
import subprocess
4+
import threading
5+
import time
6+
7+
import wx
8+
import wx.aui
9+
from wx import FileConfig
10+
11+
import os
12+
from .util import add_paths
13+
dir_path = os.path.dirname(os.path.realpath(__file__))
14+
paths = dir_path
15+
#paths = [
16+
# os.path.join(dir_path, 'deps'),
17+
#]
18+
19+
def check_for_panelizer_button():
20+
# From Miles McCoo's blog
21+
# https://kicad.mmccoo.com/2017/03/05/adding-your-own-command-buttons-to-the-pcbnew-gui/
22+
def find_pcbnew_window():
23+
windows = wx.GetTopLevelWindows()
24+
pcbneww = [w for w in windows if "pcbnew" in w.GetTitle().lower()]
25+
if len(pcbneww) != 1:
26+
return None
27+
return pcbneww[0]
28+
29+
def callback(_):
30+
plugin.Run()
31+
32+
while not wx.GetApp():
33+
time.sleep(1)
34+
bm = wx.Bitmap(os.path.join(os.path.dirname(__file__),'icon.png'), wx.BITMAP_TYPE_PNG)
35+
button_wx_item_id = 0
36+
37+
from pcbnew import ID_H_TOOLBAR
38+
while True:
39+
time.sleep(1)
40+
pcbnew_window = find_pcbnew_window()
41+
if not pcbnew_window:
42+
continue
43+
44+
top_tb = pcbnew_window.FindWindowById(ID_H_TOOLBAR)
45+
if button_wx_item_id == 0 or not top_tb.FindTool(button_wx_item_id):
46+
top_tb.AddSeparator()
47+
button_wx_item_id = wx.NewId()
48+
top_tb.AddTool(button_wx_item_id, "SparkFunKiCadPanelizer", bm,
49+
"SparkFun KiCad Panelizer", wx.ITEM_NORMAL)
50+
top_tb.Bind(wx.EVT_TOOL, callback, id=button_wx_item_id)
51+
top_tb.Realize()
52+
53+
54+
try:
55+
with add_paths(paths):
56+
from .plugin import PanelizerPlugin
57+
plugin = PanelizerPlugin()
58+
plugin.register()
59+
except Exception as e:
60+
print(e)
61+
import logging
62+
root = logging.getLogger()
63+
root.debug(repr(e))
64+
65+
# Add a button the hacky way if plugin button is not supported
66+
# in pcbnew, unless this is linux.
67+
if not plugin.pcbnew_icon_support and not sys.platform.startswith('linux'):
68+
t = threading.Thread(target=check_for_panelizer_button)
69+
t.daemon = True
70+
t.start()
71+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .cammer import CAMmer
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import os
2+
import sys
3+
from argparse import ArgumentParser
4+
import pcbnew
5+
import logging
6+
from datetime import datetime
7+
import wx
8+
9+
class CAMmer():
10+
def __init__(self):
11+
pass
12+
13+
def args_parse(self, args):
14+
# set up command-line arguments parser
15+
parser = ArgumentParser(description="A script to generate and zip PCB Gerber and drill files.")
16+
parser.add_argument(
17+
"-p", "--path", help="Path to the *.kicad_pcb file"
18+
)
19+
parser.add_argument(
20+
"-l", "--layers", help="CSV list of copper, silk and resist layers"
21+
)
22+
parser.add_argument(
23+
"-e", "--edges", help="CSV list of edge (outline / keep out) layers"
24+
)
25+
return parser.parse_args(args)
26+
27+
def startCAMmer(self, args, board=None, logger=None):
28+
"""The main method
29+
30+
Args:
31+
args - the command line args [1:] - parsed with args_parse
32+
board - the KiCad BOARD when running in a plugin
33+
logger - logging logger from parent
34+
35+
Returns:
36+
sysExit - the value for sys.exit (if called from __main__)
37+
report - a helpful text report
38+
"""
39+
40+
sysExit = -1 # -1 indicates sysExit has not (yet) been set. The code below will set this to 0, 1, 2.
41+
report = "\nSTART: " + datetime.now().isoformat() + "\n"
42+
43+
if logger is None:
44+
logger = logging.getLogger()
45+
logger.setLevel([ logging.WARNING, logging.DEBUG ][args.verbose])
46+
47+
logger.info('CAMMER START: ' + datetime.now().isoformat())
48+
49+
# Read the args
50+
sourceBoardFile = args.path
51+
layers = args.layers
52+
edges = args.edge
53+
54+
# Check if this is running in a plugin
55+
if board is None:
56+
if sourceBoardFile is None:
57+
report += "No path to kicad_pcb file. Quitting.\n"
58+
sysExit = 2
59+
return sysExit, report
60+
else:
61+
# Check that input board is a *.kicad_pcb file
62+
sourceFileExtension = os.path.splitext(sourceBoardFile)[1]
63+
if not sourceFileExtension == ".kicad_pcb":
64+
report += sourceBoardFile + " is not a *.kicad_pcb file. Quitting.\n"
65+
sysExit = 2
66+
return sysExit, report
67+
68+
# Load source board from file
69+
board = pcbnew.LoadBoard(sourceBoardFile)
70+
outputPath = os.path.split(sourceBoardFile)[0] # Get the file path head
71+
zipFile = os.path.split(sourceBoardFile)[1] # Get the file path tail
72+
zipFile = os.path.join(outputPath, os.path.splitext(zipFile)[0] + ".zip")
73+
else: # Running in a plugin
74+
outputPath = os.path.split(board.GetFileName())[0] # Get the file path head
75+
zipFile = os.path.split(board.GetFileName())[1] # Get the file path tail
76+
zipFile = os.path.join(outputPath, os.path.splitext(zipFile)[0] + ".zip")
77+
78+
if board is None:
79+
report += "Could not load board. Quitting.\n"
80+
sysExit = 2
81+
return sysExit, report
82+
83+
if (not layers) and (not edges):
84+
report += "No layers and edges defined. Quitting.\n"
85+
sysExit = 2
86+
return sysExit, report
87+
88+
# Check if about to overwrite a zip file
89+
if os.path.isfile(zipFile):
90+
if wx.GetApp() is not None:
91+
resp = wx.MessageBox("You are about to overwrite existing files.\nAre you sure?",
92+
'Warning', wx.OK | wx.CANCEL | wx.ICON_WARNING)
93+
if resp != wx.OK:
94+
report += "User does not want to overwrite the files. Quitting.\n"
95+
sysExit = 1
96+
return sysExit, report
97+
98+
99+
# ADD MUCH STUFF HERE
100+
101+
102+
if sysExit < 0:
103+
sysExit = 0
104+
105+
return sysExit, report
106+
107+
def startCAMmerCommand(self, command, board=None, logger=None):
108+
109+
parser = self.args_parse(command)
110+
111+
sysExit, report = self.startCAMmer(parser, board, logger)
112+
113+
return sysExit, report
114+
115+
if __name__ == '__main__':
116+
117+
cammer = CAMmer()
118+
119+
if len(sys.argv) < 2:
120+
parser = cammer.args_parse(['-h']) # Test args: e.g. ['-p','<path to board>']
121+
else:
122+
parser = cammer.args_parse(sys.argv[1:]) #Parse the args
123+
124+
sysExit, report = cammer.startCAMmer(parser)
125+
126+
print(report)
127+
128+
sys.exit(sysExit)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .dialog import Dialog
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
import wx
3+
4+
class DialogShim(wx.Dialog):
5+
def __init__(self, parent, **kwargs):
6+
try:
7+
wx.StockGDI._initStockObjects()
8+
except:
9+
pass
10+
11+
wx.Dialog.__init__(self, parent, **kwargs)
12+
13+
def SetSizeHints(self, a, b, c=None):
14+
if c is not None:
15+
super(wx.Dialog, self).SetSizeHints(a,b,c)
16+
else:
17+
super(wx.Dialog, self).SetSizeHints(a,b) # Was super(wx.Dialog, self).SetSizeHintsSz(a,b)
18+

0 commit comments

Comments
 (0)