diff --git a/README.md b/README.md
index df352c7..848a692 100644
--- a/README.md
+++ b/README.md
@@ -150,3 +150,74 @@ This plugin makes use of a lot of icons from the excellent [Material Design Icon
Make sure you make use of pre-commit hooks in order to format everything nicely with `black`
In the near future I'll add `ruff` / `pylint` and possibly other pre-commit-hooks that enforce nice and clean code style.
+
+## Standalone mode
+
+Allows the plugin UI to be started without KiCAD, enabling debugging with an IDE like pycharm / vscode.
+
+Standalone mode is under development.
+
+### Limitations
+
+- All board / footprint / value data are hardcoded stubs, see standalone_impl.py
+
+### How to use
+
+To use the plugin in standlone mode you'll need to identify three pieces of information specific to your Kicad version, plugin path, and OS.
+
+#### Python
+
+The {KiCad python} should be used, this can be found at different locations depending on your system:
+
+| OS | Kicad python |
+|---|---|
+|Mac| /Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3|
+|Linux| TBD|
+|Windows | TBD|
+
+#### Working directory
+
+The {working directory} should be your plugins directory, ie:
+
+|OS | Working dir|
+|Mac| ~/Documents/KiCad/8.0/scripting/plugins/|
+|Linux|TBD|
+|Windows|TBD|
+
+#### Plugin folder name
+
+The {kicad-jlcpcb-tools folder name} should be the name of the kicad-jlcpcb-tools folder.
+
+- For Kicad managed plugins this may be like
+
+> com_github_bouni_kicad-jlcpcb-tools
+
+- If you are developing kicad-jlcpcb-tools this is the folder you cloned the kicad-jlcpcb-tools as.
+
+#### Command line
+
+- Change to the working directory as noted above
+- Run the python interpreter with the {kicad-jlcpcb-tools folder name} folder as a module.
+
+For example:
+
+```sh
+cd {working directory}
+{kicad_python} -m {kicad-jlcpcb-tools folder name}
+```
+
+For example on Mac:
+
+```sh
+/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3 -m kicad-jlcpcb-tools
+```
+
+#### IDE
+
+- Configure the command line to be '{kicad_python} -m {kicad-jlcpcb-tools folder name}'
+- Set the working directory to {working directory}
+
+If using PyCharm or Jetbrains IDEs, set the interpreter to Kicad's python, {Kicad python} and under 'run configuration' select Python.
+
+Click on 'script path' and change instead to 'module name',
+entering the name of the kicad-jlcpcb-tools folder, {kicad-jlcpcb-tools folder name}.
diff --git a/__init__.py b/__init__.py
index 1bb9f3d..b572809 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,4 +1,5 @@
"""Init file for pligin."""
from .plugin import JLCPCBPlugin
-JLCPCBPlugin().register()
+if __name__ != "__main__":
+ JLCPCBPlugin().register()
diff --git a/__main__.py b/__main__.py
new file mode 100644
index 0000000..f8bbed9
--- /dev/null
+++ b/__main__.py
@@ -0,0 +1,19 @@
+"""Entry point for running the plugin in standalone mode."""
+
+import wx
+
+from . import standalone_impl
+from .mainwindow import JLCPCBTools
+
+if __name__ == "__main__":
+ print("starting jlcpcbtools standalone mode...") # noqa: T201
+
+ # See README.md for how to use this
+
+ app = wx.App(None)
+
+ dialog = JLCPCBTools(None, kicad_provider=standalone_impl.KicadStub())
+ dialog.Center()
+ dialog.Show()
+
+ app.MainLoop()
diff --git a/fabrication.py b/fabrication.py
index 2502d95..66c152d 100644
--- a/fabrication.py
+++ b/fabrication.py
@@ -23,7 +23,6 @@
F_Mask,
F_Paste,
F_SilkS,
- GetBoard,
Refresh,
ToMM,
)
@@ -42,10 +41,10 @@
class Fabrication:
"""Contains all functionality to generate the JLCPCB production files."""
- def __init__(self, parent):
+ def __init__(self, parent, board):
self.parent = parent
self.logger = logging.getLogger(__name__)
- self.board = GetBoard()
+ self.board = board
self.corrections = []
self.path, self.filename = os.path.split(self.board.GetFileName())
self.create_folders()
diff --git a/mainwindow.py b/mainwindow.py
index af8de81..f6a56a1 100644
--- a/mainwindow.py
+++ b/mainwindow.py
@@ -7,7 +7,7 @@
import sys
import time
-import pcbnew
+import pcbnew as kicad_pcbnew
import wx # pylint: disable=import-error
from wx import adv # pylint: disable=import-error
import wx.dataview # pylint: disable=import-error
@@ -65,11 +65,17 @@
ID_CONTEXT_MENU_ADD_ROT_BY_PACKAGE = wx.NewIdRef()
ID_CONTEXT_MENU_ADD_ROT_BY_NAME = wx.NewIdRef()
+class KicadProvider:
+ """KiCad implementation of the provider, see standalone_impl.py for the stub version."""
+
+ def get_pcbnew(self):
+ """Get the pcbnew instance."""
+ return kicad_pcbnew
class JLCPCBTools(wx.Dialog):
- """Main Windows class for this plugin."""
+ """JLCPCBTools main dialog."""
- def __init__(self, parent):
+ def __init__(self, parent, kicad_provider=KicadProvider()):
while not wx.GetApp():
time.sleep(1)
wx.Dialog.__init__(
@@ -81,12 +87,13 @@ def __init__(self, parent):
size=wx.Size(1300, 800),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX,
)
- self.KicadBuildVersion = pcbnew.GetBuildVersion()
+ self.pcbnew = kicad_provider.get_pcbnew()
+ self.KicadBuildVersion = self.pcbnew.GetBuildVersion()
self.window = wx.GetTopLevelParent(self)
self.SetSize(HighResWxSize(self.window, wx.Size(1300, 800)))
self.scale_factor = GetScaleFactor(self.window)
- self.project_path = os.path.split(pcbnew.GetBoard().GetFileName())[0]
- self.board_name = os.path.split(pcbnew.GetBoard().GetFileName())[1]
+ self.project_path = os.path.split(self.pcbnew.GetBoard().GetFileName())[0]
+ self.board_name = os.path.split(self.pcbnew.GetBoard().GetFileName())[1]
self.schematic_name = f"{self.board_name.split('.')[0]}.kicad_sch"
self.hide_bom_parts = False
self.hide_pos_parts = False
@@ -517,13 +524,13 @@ def init_library(self):
def init_store(self):
"""Initialize the store of part assignments."""
- self.store = Store(self, self.project_path)
+ self.store = Store(self, self.project_path, self.pcbnew.GetBoard())
if self.library.state == LibraryState.INITIALIZED:
self.populate_footprint_list()
def init_fabrication(self):
"""Initialize the fabrication."""
- self.fabrication = Fabrication(self)
+ self.fabrication = Fabrication(self, self.pcbnew.GetBoard())
def reset_gauge(self, *_):
"""Initialize the gauge."""
@@ -574,8 +581,7 @@ def populate_footprint_list(self, *_):
numbers = []
parts = []
for part in self.store.read_all():
- board = pcbnew.GetBoard()
- fp = board.FindFootprintByReference(part[0])
+ fp = self.pcbnew.GetBoard().FindFootprintByReference(part[0])
if part[3] and part[3] not in numbers:
numbers.append(part[3])
part.insert(4, "")
@@ -699,7 +705,7 @@ def OnFootprintSelected(self, *_):
)
# clear the present selections
- selection = pcbnew.GetCurrentSelection()
+ selection = self.pcbnew.GetCurrentSelection()
for selected in selection:
selected.ClearSelected()
@@ -708,12 +714,12 @@ def OnFootprintSelected(self, *_):
for item in self.footprint_list.GetSelections():
row = self.footprint_list.ItemToRow(item)
ref = self.footprint_list.GetTextValue(row, 0)
- fp = pcbnew.GetBoard().FindFootprintByReference(ref)
+ fp = self.pcbnew.GetBoard().FindFootprintByReference(ref)
fp.SetSelected()
# cause pcbnew to refresh the board with the changes to the selected footprint(s)
- pcbnew.Refresh()
+ self.pcbnew.Refresh()
def enable_all_buttons(self, state):
"""Control state of all the buttons."""
@@ -747,7 +753,7 @@ def toggle_bom_pos(self, *_):
row = self.footprint_list.ItemToRow(item)
selected_rows.append(row)
ref = self.footprint_list.GetTextValue(row, 0)
- board = pcbnew.GetBoard()
+ board = self.pcbnew.GetBoard()
fp = board.FindFootprintByReference(ref)
bom = toggle_exclude_from_bom(fp)
pos = toggle_exclude_from_pos(fp)
@@ -767,7 +773,7 @@ def toggle_bom(self, *_):
row = self.footprint_list.ItemToRow(item)
selected_rows.append(row)
ref = self.footprint_list.GetTextValue(row, 0)
- board = pcbnew.GetBoard()
+ board = self.pcbnew.GetBoard()
fp = board.FindFootprintByReference(ref)
bom = toggle_exclude_from_bom(fp)
self.store.set_bom(ref, bom)
@@ -782,7 +788,7 @@ def toggle_pos(self, *_):
row = self.footprint_list.ItemToRow(item)
selected_rows.append(row)
ref = self.footprint_list.GetTextValue(row, 0)
- board = pcbnew.GetBoard()
+ board = self.pcbnew.GetBoard()
fp = board.FindFootprintByReference(ref)
pos = toggle_exclude_from_pos(fp)
self.store.set_pos(ref, pos)
diff --git a/standalone_impl.py b/standalone_impl.py
new file mode 100644
index 0000000..56e1bbf
--- /dev/null
+++ b/standalone_impl.py
@@ -0,0 +1,99 @@
+"""Stubs for standalone usage of the plugin."""
+
+class LIB_ID_Stub:
+ """Implementation of pcbnew.LIB_ID."""
+
+ def __init__(self, item_name):
+ self.item_name = item_name
+
+ def GetLibItemName(self) -> str:
+ """Item name."""
+ return self.item_name
+
+
+class Footprint_Stub:
+ """Implementation of pcbnew.Footprint."""
+
+ def __init__(self, reference, value, fpid):
+ self.reference = reference
+ self.value = value
+ self.fpid = fpid
+
+ def GetReference(self) -> str:
+ """Retrieve the reference designator string."""
+ return self.reference
+
+ def GetValue(self) -> str:
+ """Value string."""
+ return self.value
+
+ def GetFPID(self) -> LIB_ID_Stub:
+ """Footprint LIB_ID."""
+ return self.fpid
+
+ def GetProperties(self) -> dict:
+ """Properties."""
+ return {}
+
+ def GetAttributes(self) -> int:
+ """Attributes."""
+ return 0
+
+ def GetLayer(self) -> int:
+ """Layer number."""
+ # TODO: maybe this is defined in a python module we can import and reuse here?
+ return 3 # F_Cu, see https://docs.kicad.org/doxygen/layer__ids_8h.html#ae0ad6e574332a997f501d1b091c3f53f
+
+ def SetSelected(self):
+ """Select this item."""
+
+
+class BoardStub:
+ """Implementation of pcbnew.Board."""
+
+ def __init__(self):
+ self.footprints = []
+ self.footprints.append(Footprint_Stub("R1", "100", LIB_ID_Stub("resistors")))
+
+ def GetFileName(self):
+ """Board filename."""
+ return "fake_test_board.kicad_pcb"
+
+ def GetFootprints(self):
+ """Footprint list."""
+ return self.footprints
+
+ def FindFootprintByReference(self, reference):
+ """Get a list of footprints that match a reference."""
+ return Footprint_Stub(reference, "stub", 100)
+
+class PcbnewStub:
+ """Stub implementation of pcbnew."""
+
+ def __init__(self):
+ self.board = BoardStub()
+
+ def GetBoard(self):
+ """Get the board."""
+ return self.board
+
+ def GetBuildVersion(self):
+ """Get the kicad build version."""
+ return "8.0.1"
+
+ def GetCurrentSelection(self):
+ """Get the currently selected board items."""
+ return []
+
+ def Refresh(self):
+ """Redraw the screen."""
+
+class KicadStub:
+ """Stub implementation of Kicad."""
+
+ def __init__(self):
+ self.pcbnew = PcbnewStub()
+
+ def get_pcbnew(self):
+ """Get the pcbnew stub."""
+ return self.pcbnew
diff --git a/store.py b/store.py
index 7bbb68e..5a9d946 100644
--- a/store.py
+++ b/store.py
@@ -7,8 +7,6 @@
from pathlib import Path
import sqlite3
-from pcbnew import GetBoard # pylint: disable=import-error
-
from .helpers import (
get_exclude_from_bom,
get_exclude_from_pos,
@@ -21,10 +19,11 @@
class Store:
"""A storage class to get data from a sqlite database and write it back."""
- def __init__(self, parent, project_path):
+ def __init__(self, parent, project_path, board):
self.logger = logging.getLogger(__name__)
self.parent = parent
self.project_path = project_path
+ self.board = board
self.datadir = os.path.join(self.project_path, "jlcpcb")
self.dbfile = os.path.join(self.datadir, "project.db")
self.order_by = "reference"
@@ -184,8 +183,7 @@ def set_lcsc(self, ref, value):
def update_from_board(self):
"""Read all footprints from the board and insert them into the database if they do not exist."""
- board = GetBoard()
- for fp in get_valid_footprints(board):
+ for fp in get_valid_footprints(self.board):
part = [
fp.GetReference(),
fp.GetValue(),
@@ -241,7 +239,7 @@ def update_from_board(self):
def clean_database(self):
"""Delete all parts from the database that are no longer present on the board."""
- refs = [f"'{fp.GetReference()}'" for fp in get_valid_footprints(GetBoard())]
+ refs = [f"'{fp.GetReference()}'" for fp in get_valid_footprints(self.board)]
with contextlib.closing(sqlite3.connect(self.dbfile)) as con, con as cur:
cur.execute(
f"DELETE FROM part_info WHERE reference NOT IN ({','.join(refs)})"