343 lines
10 KiB
Python
343 lines
10 KiB
Python
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
import wx
|
|
import wx.aui
|
|
from wx.lib import buttons
|
|
import pcbnew
|
|
import dataclasses
|
|
|
|
path_ = Path(__file__).parent.absolute()
|
|
sys.path.append(str(path_))
|
|
|
|
from kicad_testpoints_ import (
|
|
get_pads_by_property,
|
|
build_test_point_report,
|
|
write_csv,
|
|
Settings,
|
|
)
|
|
|
|
from _version import __version__
|
|
|
|
_log = logging.getLogger("kicad_testpoints-pcm")
|
|
_log.setLevel(logging.DEBUG)
|
|
|
|
_board = None
|
|
_frame_size = (800, 600)
|
|
_frame_size_min = (300, 400)
|
|
|
|
def set_board(board):
|
|
"""
|
|
Sets the board global.
|
|
"""
|
|
global _board
|
|
_board = board
|
|
|
|
def get_board():
|
|
"""
|
|
Use instead of pcbnew.GetBoard to allow
|
|
command line use.
|
|
"""
|
|
return _board
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Meta:
|
|
"""
|
|
Information about package
|
|
"""
|
|
toolname : str = "kicadtestpoints"
|
|
title : str = "Testpoint Report Setup"
|
|
body : str = ("Set testpoints by setting the desired pads 'Fabrication Property' to "
|
|
"'Test Point Pad'. The output default is in the JigsApp test point report style. ")
|
|
about_text : str = "This plugin generates TheJigsApp styletest point reports. Test more, worry less."
|
|
short_description : str = "Fabrication Testpoint Report Generator"
|
|
website : str = "https://www.thejigsapp.com"
|
|
gitlink : str = "https://github.com/snhobbs/kicad-testpoints-pcm"
|
|
version : str = __version__
|
|
category : str = "Read PCB"
|
|
icon_dir : Path = Path(__file__).parent
|
|
icon_file_path : Path = icon_dir / "icon-24x24.png"
|
|
# assert icon_file_path.exists()
|
|
about_body = ("Bed-of-nails test jigs greatly improve electronics development and production. "
|
|
"TheJigsApp autogenerated test jigs make them cost-effective, flexible, and fast. "
|
|
"Checkout our ordering portal and getting started guides to build yours now.")
|
|
|
|
|
|
def setattr_keywords(obj, name, value):
|
|
return setattr(obj, name, value)
|
|
|
|
|
|
class MyPanel(wx.Panel):
|
|
def __init__(self, parent):
|
|
_log.debug("MyPanel.__init__")
|
|
super().__init__(parent)
|
|
self.settings = Settings()
|
|
|
|
# Get current working directory
|
|
dir_ = Path(os.getcwd())
|
|
if pcbnew.GetBoard():
|
|
set_board(pcbnew.GetBoard())
|
|
|
|
dir_path = Path(os.path.curdir)
|
|
stem = Meta.toolname
|
|
if get_board():
|
|
wd = Path(get_board().GetFileName()).absolute()
|
|
if wd.exists():
|
|
dir_path = wd.parent
|
|
stem = wd.stem
|
|
|
|
default_file_path = dir_path / f"{stem}-testpoints.csv"
|
|
|
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
bold = wx.Font(
|
|
12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD
|
|
)
|
|
|
|
body_title_text = wx.StaticText(self, label="Usage")
|
|
body_title_text.SetFont(bold)
|
|
sizer.Add(body_title_text, 0, wx.EXPAND | wx.ALL, 5)
|
|
|
|
body_text = wx.StaticText(self, label=Meta.body)
|
|
sizer.Add(body_text, 1, wx.EXPAND | wx.ALL, 5)
|
|
|
|
# Origin selection
|
|
choices = (
|
|
"Reference to file/drill origin",
|
|
"Reference to absolute origin"
|
|
)
|
|
self.coordinate_selection = wx.RadioBox(self, label='Coordinate Positions', choices=choices, majorDimension=2, style=wx.RA_SPECIFY_ROWS)
|
|
sizer.Add(self.coordinate_selection, 0, wx.ALL, 10)
|
|
|
|
body_text = wx.StaticText(self, label=(
|
|
"Coordinates are Cartesian with x increasing to the right and y increasing upwards. "
|
|
"Note that the origin should be consistent between gerbers and the testpoints."))
|
|
|
|
sizer.Add(body_text, 1, wx.EXPAND | wx.ALL, 5)
|
|
|
|
|
|
# File output selector
|
|
file_output_label = wx.StaticText(self, label="File Output:")
|
|
sizer.Add(file_output_label, 0, wx.ALL, 5)
|
|
|
|
self.file_output_selector = wx.FilePickerCtrl(
|
|
self,
|
|
style=wx.FLP_SAVE | wx.FLP_USE_TEXTCTRL,
|
|
wildcard="CSV files (*.csv)|*.csv",
|
|
path=default_file_path.as_posix(),
|
|
)
|
|
self.file_output_selector.SetPath(default_file_path.as_posix())
|
|
sizer.Add(self.file_output_selector, 0, wx.EXPAND | wx.ALL, 5)
|
|
|
|
# Buttons
|
|
self.submit_button = buttons.GenButton(self, label="Submit")
|
|
self.cancel_button = buttons.GenButton(self, label="Cancel")
|
|
self.submit_button.SetBackgroundColour(wx.Colour(50, 225, 50))
|
|
self.cancel_button.SetBackgroundColour(wx.Colour(225, 50, 50))
|
|
self.submit_button.Bind(wx.EVT_BUTTON, self.on_submit)
|
|
self.cancel_button.Bind(wx.EVT_BUTTON, self.on_cancel)
|
|
|
|
# Horizontal box sizer for buttons
|
|
button_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
button_sizer.Add(self.cancel_button, 0, wx.ALL, 5)
|
|
button_sizer.Add(self.submit_button, 0, wx.ALL | wx.EXPAND, 5)
|
|
sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5)
|
|
|
|
# Sizer for layout
|
|
self.SetSizer(sizer)
|
|
|
|
def on_submit(self, _):
|
|
file_path = Path(self.file_output_selector.GetPath())
|
|
if not file_path:
|
|
wx.MessageBox(
|
|
"Please select a file output path.", "Error", wx.OK | wx.ICON_ERROR
|
|
)
|
|
return
|
|
|
|
self.settings.use_aux_origin = self.coordinate_selection.GetSelection() == 0
|
|
|
|
_log.debug("Submitting.\n%s\nAux origin %s", file_path, str(self.settings.use_aux_origin))
|
|
|
|
board = get_board()
|
|
pads = get_pads_by_property(board)
|
|
data = build_test_point_report(board, pads=pads, settings=self.settings)
|
|
if not data:
|
|
wx.MessageBox(
|
|
"No test point pads found, have you set any?",
|
|
"Error",
|
|
wx.OK | wx.ICON_ERROR,
|
|
)
|
|
return
|
|
|
|
nets = set(board.GetNetsByName())
|
|
tp_nets = set([pt["net"] for pt in data])
|
|
|
|
write_csv(data, filename=file_path)
|
|
|
|
_log.info("Coverage: %d / %d nets\n\nSaved to: %s", len(tp_nets), len(nets), file_path)
|
|
|
|
wx.MessageBox(
|
|
"Coverage: %d / %d nets\n\nSaved to: %s"%(len(tp_nets), len(nets), file_path),
|
|
"Success",
|
|
wx.OK,
|
|
)
|
|
|
|
self.GetTopLevelParent().EndModal(wx.ID_OK)
|
|
return
|
|
|
|
def on_cancel(self, _):
|
|
_log.debug("Canceling")
|
|
self.GetTopLevelParent().EndModal(wx.ID_CANCEL)
|
|
|
|
|
|
class AboutPanel(wx.Panel):
|
|
def __init__(self, parent):
|
|
super().__init__(parent)
|
|
font = wx.Font(
|
|
12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
|
|
)
|
|
bold = wx.Font(
|
|
12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD
|
|
)
|
|
|
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
# Static text for about information
|
|
version_text = wx.StaticText(self, label=f"Version: {Meta.version}")
|
|
version_text.SetFont(bold)
|
|
sizer.Add(version_text, 1, wx.EXPAND | wx.ALL, 5)
|
|
|
|
message_text = wx.StaticText(self, label=Meta.about_text)
|
|
message_text.SetFont(font)
|
|
sizer.Add(message_text, 1, wx.EXPAND | wx.ALL, 5)
|
|
|
|
body_text = wx.StaticText(self, label=Meta.about_body)
|
|
body_text.SetFont(font)
|
|
sizer.Add(body_text, 5, wx.EXPAND | wx.ALL, 5)
|
|
|
|
from wx.lib.agw.hyperlink import HyperLinkCtrl
|
|
link_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
pre_link_text = wx.StaticText(self, label="Brought to you by TheJigsApp: ")
|
|
pre_link_text.SetFont(font)
|
|
link_sizer.Add(pre_link_text, 0, wx.EXPAND, 0)
|
|
|
|
link = HyperLinkCtrl(self, wx.ID_ANY, f"{Meta.website}", URL=Meta.website)
|
|
link.SetFont(font)
|
|
link.SetColours(wx.BLUE, wx.BLUE, wx.BLUE)
|
|
link_sizer.Add(link, 0, wx.EXPAND, 0)
|
|
|
|
sizer.Add(link_sizer, 1, wx.EXPAND | wx.ALL, 5)
|
|
|
|
gh_link_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
gh_pre_link_text = wx.StaticText(self, label="Git Repo: ")
|
|
gh_pre_link_text.SetFont(font)
|
|
gh_link_sizer.Add(gh_pre_link_text, 0, wx.EXPAND, 0)
|
|
|
|
gh_link = HyperLinkCtrl(self, wx.ID_ANY, f"{Meta.gitlink}", URL=Meta.gitlink)
|
|
gh_link.SetFont(font)
|
|
gh_link.SetColours(wx.BLUE, wx.BLUE, wx.BLUE)
|
|
gh_link_sizer.Add(gh_link, 0, wx.EXPAND, 0)
|
|
|
|
sizer.Add(gh_link_sizer, 1, wx.EXPAND | wx.ALL, 5)
|
|
self.SetSizer(sizer)
|
|
|
|
|
|
class MyDialog(wx.Dialog):
|
|
"""
|
|
Top level GUI view
|
|
"""
|
|
def __init__(self, parent, title):
|
|
super().__init__(
|
|
parent, title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
|
|
)
|
|
|
|
# Sizer for layout
|
|
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
# Create a notebook with two tabs
|
|
notebook = wx.Notebook(self)
|
|
tab_panel = MyPanel(notebook)
|
|
about_panel = AboutPanel(notebook)
|
|
|
|
notebook.AddPage(tab_panel, "Main")
|
|
notebook.AddPage(about_panel, "About")
|
|
|
|
sizer.Add(notebook, 1, wx.EXPAND | wx.ALL, 10)
|
|
|
|
self.SetSizer(sizer)
|
|
self.SetMinSize(_frame_size_min)
|
|
self.SetSize(_frame_size)
|
|
|
|
def on_close(self, event):
|
|
self.EndModal(wx.ID_CANCEL)
|
|
event.Skip()
|
|
|
|
def on_maximize(self, _):
|
|
self.fit_to_screen()
|
|
|
|
def on_size(self, _):
|
|
if self.IsMaximized():
|
|
self.fit_to_screen()
|
|
|
|
def fit_to_screen(self):
|
|
screen_width, screen_height = wx.DisplaySize()
|
|
self.SetSize(wx.Size(screen_width, screen_height))
|
|
|
|
|
|
def get_gui_frame(name: str = "PcbFrame"):
|
|
pcb_frame = None
|
|
|
|
try:
|
|
pcb_frame = [
|
|
x for x in wx.GetTopLevelWindows() if x.GetName() == name
|
|
][0]
|
|
except IndexError:
|
|
pass
|
|
return pcb_frame
|
|
|
|
|
|
class Plugin(pcbnew.ActionPlugin):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
_log.debug("Loading kicad_testpoints")
|
|
|
|
self.logger = _log
|
|
self.config_file = None
|
|
|
|
self.name = Meta.title
|
|
self.category = Meta.category
|
|
self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
|
|
self.show_toolbar_button = True
|
|
self.description = Meta.body
|
|
self.icon_file_name = str(Meta.icon_file_path)
|
|
|
|
def defaults(self):
|
|
pass
|
|
|
|
def Run(self):
|
|
dlg = MyDialog(get_gui_frame(name="PcbFrame"), title=Meta.title)
|
|
try:
|
|
dlg.ShowModal()
|
|
|
|
except Exception as e:
|
|
_log.error(e)
|
|
raise
|
|
finally:
|
|
_log.debug("Destroy Dialog")
|
|
dlg.Destroy()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig()
|
|
_log.setLevel(logging.DEBUG)
|
|
if len(sys.argv) > 1:
|
|
set_board(pcbnew.LoadBoard(sys.argv[1]))
|
|
app = wx.App()
|
|
p = Plugin()
|
|
p.Run()
|