491 lines
18 KiB
Python
491 lines
18 KiB
Python
import pcbnew
|
|
import os.path
|
|
from pathlib import Path
|
|
import wx
|
|
from time import sleep
|
|
from threading import Thread
|
|
import sys
|
|
import traceback
|
|
import subprocess
|
|
import os
|
|
import venv
|
|
|
|
try:
|
|
if __name__ == "__main__":
|
|
from impart_gui import impartGUI
|
|
from KiCadImport import import_lib
|
|
from impart_helper_func import filehandler, config_handler, KiCad_Settings
|
|
from impart_migration import find_old_lib_files, convert_lib_list
|
|
else:
|
|
# relative import is required in kicad
|
|
from .impart_gui import impartGUI
|
|
from .KiCadImport import import_lib
|
|
from .impart_helper_func import filehandler, config_handler, KiCad_Settings
|
|
from .impart_migration import find_old_lib_files, convert_lib_list
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
|
|
|
|
def activate_virtualenv(venv_dir):
|
|
"""Activates a virtual environment, but creates it first if it does not exist."""
|
|
venv_dir = os.path.abspath(venv_dir)
|
|
|
|
if os.name == "nt": # Windows
|
|
if not os.path.exists(venv_dir):
|
|
# venv.create(venv_dir, with_pip=True) # dont work
|
|
kicad_executable = sys.executable
|
|
kicad_bin_dir = os.path.dirname(kicad_executable)
|
|
python_executable = os.path.join(kicad_bin_dir, "python.exe")
|
|
subprocess.run(
|
|
[python_executable, "-m", "venv", venv_dir],
|
|
check=True,
|
|
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
)
|
|
print(f"Virtual environment not found. Create new in {venv_dir} ...")
|
|
|
|
python_executable = os.path.join(venv_dir, "Scripts", "python.exe")
|
|
site_packages = os.path.join(venv_dir, "Lib", "site-packages")
|
|
else: # Linux / macOS
|
|
if not os.path.exists(venv_dir):
|
|
venv.create(venv_dir, with_pip=True)
|
|
print(f"Virtual environment not found. Create new in {venv_dir} ...")
|
|
python_executable = os.path.join(venv_dir, "bin", "python")
|
|
site_packages = os.path.join(
|
|
venv_dir,
|
|
"lib",
|
|
f"python{sys.version_info.major}.{sys.version_info.minor}",
|
|
"site-packages",
|
|
)
|
|
|
|
sys.path.insert(0, site_packages)
|
|
return python_executable
|
|
|
|
|
|
def ensure_package(package_name, python_executable="python"):
|
|
try:
|
|
__import__(package_name)
|
|
return True
|
|
except ModuleNotFoundError:
|
|
try:
|
|
cmd = [python_executable, "-m", "pip", "install", package_name]
|
|
print(" ".join(cmd))
|
|
subprocess.check_call(cmd)
|
|
__import__(package_name)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
EVT_UPDATE_ID = wx.NewIdRef()
|
|
|
|
|
|
def EVT_UPDATE(win, func):
|
|
win.Connect(-1, -1, EVT_UPDATE_ID, func)
|
|
|
|
|
|
class ResultEvent(wx.PyEvent):
|
|
def __init__(self, data):
|
|
wx.PyEvent.__init__(self)
|
|
self.SetEventType(EVT_UPDATE_ID)
|
|
self.data = data
|
|
|
|
|
|
class PluginThread(Thread):
|
|
def __init__(self, wxObject):
|
|
Thread.__init__(self)
|
|
self.wxObject = wxObject
|
|
self.stopThread = False
|
|
self.start()
|
|
|
|
def run(self):
|
|
lenStr = 0
|
|
global backend_h
|
|
while not self.stopThread:
|
|
if lenStr != len(backend_h.print_buffer):
|
|
self.report(backend_h.print_buffer)
|
|
lenStr = len(backend_h.print_buffer)
|
|
sleep(0.5)
|
|
|
|
def report(self, status):
|
|
wx.PostEvent(self.wxObject, ResultEvent(status))
|
|
|
|
|
|
class impart_backend:
|
|
|
|
def __init__(self):
|
|
path2config = os.path.join(os.path.dirname(__file__), "config.ini")
|
|
self.config = config_handler(path2config)
|
|
path_seting = pcbnew.SETTINGS_MANAGER().GetUserSettingsPath()
|
|
self.KiCad_Settings = KiCad_Settings(path_seting)
|
|
self.runThread = False
|
|
self.autoImport = False
|
|
self.overwriteImport = False
|
|
self.import_old_format = False
|
|
self.autoLib = False
|
|
self.folderhandler = filehandler(".")
|
|
self.print_buffer = ""
|
|
self.importer = import_lib()
|
|
self.importer.print = self.print2buffer
|
|
|
|
def version_to_tuple(version_str):
|
|
try:
|
|
return tuple(map(int, version_str.split("-")[0].split(".")))
|
|
except (ValueError, AttributeError) as e:
|
|
print(f"Version extractions error '{version_str}': {e}")
|
|
return None
|
|
|
|
minVersion = "8.0.4"
|
|
KiCadVers = version_to_tuple(pcbnew.Version())
|
|
if not KiCadVers or KiCadVers < version_to_tuple(minVersion):
|
|
self.print2buffer("KiCad Version: " + str(pcbnew.FullVersion()))
|
|
self.print2buffer("Minimum required KiCad version is " + minVersion)
|
|
self.print2buffer("This can limit the functionality of the plugin.")
|
|
|
|
if not self.config.config_is_set:
|
|
self.print2buffer(
|
|
"Warning: The path where the libraries should be saved has not been adjusted yet."
|
|
+ " Maybe you use the plugin in this version for the first time.\n"
|
|
)
|
|
|
|
additional_information = (
|
|
"If this plugin is being used for the first time, settings in KiCad are required. "
|
|
+ "The settings are checked at the end of the import process. For easy setup, "
|
|
+ "auto setting can be activated."
|
|
)
|
|
self.print2buffer(additional_information)
|
|
self.print2buffer("\n##############################\n")
|
|
|
|
def print2buffer(self, *args):
|
|
for text in args:
|
|
self.print_buffer = self.print_buffer + str(text) + "\n"
|
|
|
|
def __find_new_file__(self):
|
|
path = self.config.get_SRC_PATH()
|
|
|
|
if not os.path.isdir(path):
|
|
return 0
|
|
|
|
while True:
|
|
newfilelist = self.folderhandler.GetNewFiles(path)
|
|
for lib in newfilelist:
|
|
try:
|
|
(res,) = self.importer.import_all(
|
|
lib,
|
|
overwrite_if_exists=self.overwriteImport,
|
|
import_old_format=self.import_old_format,
|
|
)
|
|
self.print2buffer(res)
|
|
except AssertionError as e:
|
|
self.print2buffer(e)
|
|
except Exception as e:
|
|
self.print2buffer(e)
|
|
backend_h.print2buffer(f"Error: {e}")
|
|
backend_h.print2buffer("Python version " + sys.version)
|
|
print(traceback.format_exc())
|
|
self.print2buffer("")
|
|
|
|
if not self.runThread:
|
|
break
|
|
if not pcbnew.GetBoard():
|
|
# print("pcbnew close")
|
|
break
|
|
sleep(1)
|
|
|
|
|
|
backend_h = impart_backend()
|
|
|
|
|
|
def checkImport(add_if_possible=True):
|
|
libnames = ["Octopart", "Samacsys", "UltraLibrarian", "Snapeda", "EasyEDA"]
|
|
setting = backend_h.KiCad_Settings
|
|
DEST_PATH = backend_h.config.get_DEST_PATH()
|
|
|
|
msg = ""
|
|
msg += setting.check_GlobalVar(DEST_PATH, add_if_possible)
|
|
|
|
for name in libnames:
|
|
# The lines work but old libraries should not be added automatically
|
|
# libname = os.path.join(DEST_PATH, name + ".lib")
|
|
# if os.path.isfile(libname):
|
|
# msg += setting.check_symbollib(name + ".lib", add_if_possible)
|
|
|
|
libdir = os.path.join(DEST_PATH, name + ".kicad_sym")
|
|
libdir_old = os.path.join(DEST_PATH, name + "_kicad_sym.kicad_sym")
|
|
libdir_convert_lib = os.path.join(DEST_PATH, name + "_old_lib.kicad_sym")
|
|
if os.path.isfile(libdir):
|
|
libname = name + ".kicad_sym"
|
|
msg += setting.check_symbollib(libname, add_if_possible)
|
|
elif os.path.isfile(libdir_old):
|
|
libname = name + "_kicad_sym.kicad_sym"
|
|
msg += setting.check_symbollib(libname, add_if_possible)
|
|
|
|
if os.path.isfile(libdir_convert_lib):
|
|
libname = name + "_old_lib.kicad_sym"
|
|
msg += setting.check_symbollib(libname, add_if_possible)
|
|
|
|
libdir = os.path.join(DEST_PATH, name + ".pretty")
|
|
if os.path.isdir(libdir):
|
|
msg += setting.check_footprintlib(name, add_if_possible)
|
|
return msg
|
|
|
|
|
|
class impart_frontend(impartGUI):
|
|
global backend_h
|
|
|
|
def __init__(self, board, action):
|
|
super(impart_frontend, self).__init__(None)
|
|
self.board = board
|
|
self.action = action
|
|
|
|
self.m_dirPicker_sourcepath.SetPath(backend_h.config.get_SRC_PATH())
|
|
self.m_dirPicker_librarypath.SetPath(backend_h.config.get_DEST_PATH())
|
|
|
|
self.m_autoImport.SetValue(backend_h.autoImport)
|
|
self.m_overwrite.SetValue(backend_h.overwriteImport)
|
|
self.m_check_autoLib.SetValue(backend_h.autoLib)
|
|
self.m_check_import_all.SetValue(backend_h.import_old_format)
|
|
|
|
if backend_h.runThread:
|
|
self.m_button.Label = "automatic import / press to stop"
|
|
else:
|
|
self.m_button.Label = "Start"
|
|
|
|
EVT_UPDATE(self, self.updateDisplay)
|
|
self.Thread = PluginThread(self) # only for text output
|
|
|
|
self.test_migrate_possible()
|
|
|
|
def updateDisplay(self, status):
|
|
self.m_text.SetValue(status.data)
|
|
self.m_text.SetInsertionPointEnd()
|
|
|
|
# def print(self, text):
|
|
# self.m_text.AppendText(str(text)+"\n")
|
|
|
|
def on_close(self, event):
|
|
if backend_h.runThread:
|
|
dlg = wx.MessageDialog(
|
|
None,
|
|
"The automatic import process continues in the background. "
|
|
+ "If this is not desired, it must be stopped.\n"
|
|
+ "As soon as the PCB Editor window is closed, the import process also ends.",
|
|
"WARNING: impart background process",
|
|
wx.KILL_OK | wx.ICON_WARNING,
|
|
)
|
|
if dlg.ShowModal() != wx.ID_OK:
|
|
return
|
|
|
|
backend_h.autoImport = self.m_autoImport.IsChecked()
|
|
backend_h.overwriteImport = self.m_overwrite.IsChecked()
|
|
backend_h.autoLib = self.m_check_autoLib.IsChecked()
|
|
backend_h.import_old_format = self.m_check_import_all.IsChecked()
|
|
# backend_h.runThread = False
|
|
self.Thread.stopThread = True # only for text output
|
|
event.Skip()
|
|
|
|
def BottonClick(self, event):
|
|
backend_h.importer.set_DEST_PATH(backend_h.config.get_DEST_PATH())
|
|
|
|
backend_h.autoImport = self.m_autoImport.IsChecked()
|
|
|
|
tmp = self.m_overwrite.IsChecked()
|
|
if tmp and not tmp == backend_h.overwriteImport:
|
|
backend_h.folderhandler.filelist = []
|
|
backend_h.overwriteImport = self.m_overwrite.IsChecked()
|
|
|
|
backend_h.autoLib = self.m_check_autoLib.IsChecked()
|
|
backend_h.import_old_format = self.m_check_import_all.IsChecked()
|
|
|
|
if backend_h.runThread:
|
|
backend_h.runThread = False
|
|
self.m_button.Label = "Start"
|
|
return
|
|
|
|
backend_h.runThread = False
|
|
backend_h.__find_new_file__()
|
|
self.m_button.Label = "Start"
|
|
|
|
if backend_h.autoImport:
|
|
backend_h.runThread = True
|
|
self.m_button.Label = "automatic import / press to stop"
|
|
x = Thread(target=backend_h.__find_new_file__, args=[])
|
|
x.start()
|
|
|
|
add_if_possible = self.m_check_autoLib.IsChecked()
|
|
msg = checkImport(add_if_possible)
|
|
if msg:
|
|
msg += "\n\nMore information can be found in the README for the integration into KiCad.\n"
|
|
msg += "github.com/Steffen-W/Import-LIB-KiCad-Plugin"
|
|
msg += "\nSome configurations require a KiCad restart to be detected correctly."
|
|
|
|
dlg = wx.MessageDialog(None, msg, "WARNING", wx.KILL_OK | wx.ICON_WARNING)
|
|
|
|
if dlg.ShowModal() != wx.ID_OK:
|
|
return
|
|
|
|
backend_h.print2buffer("\n##############################\n")
|
|
backend_h.print2buffer(msg)
|
|
backend_h.print2buffer("\n##############################\n")
|
|
event.Skip()
|
|
|
|
def DirChange(self, event):
|
|
backend_h.config.set_SRC_PATH(self.m_dirPicker_sourcepath.GetPath())
|
|
backend_h.config.set_DEST_PATH(self.m_dirPicker_librarypath.GetPath())
|
|
backend_h.folderhandler.filelist = []
|
|
self.test_migrate_possible()
|
|
event.Skip()
|
|
|
|
def ButtomManualImport(self, event):
|
|
try:
|
|
from .impart_easyeda import easyeda2kicad_wrapper
|
|
|
|
component_id = self.m_textCtrl2.GetValue().strip() # example: "C2040"
|
|
overwrite = self.m_overwrite.IsChecked()
|
|
backend_h.print2buffer("")
|
|
backend_h.print2buffer(
|
|
"Try to import EeasyEDA / LCSC Part# : " + component_id
|
|
)
|
|
base_folder = backend_h.config.get_DEST_PATH()
|
|
easyeda_import = easyeda2kicad_wrapper()
|
|
easyeda_import.print = backend_h.print2buffer
|
|
easyeda_import.full_import(component_id, base_folder, overwrite)
|
|
event.Skip()
|
|
except Exception as e:
|
|
backend_h.print2buffer(f"Error: {e}")
|
|
backend_h.print2buffer("Python version " + sys.version)
|
|
print(traceback.format_exc())
|
|
|
|
def get_old_libfiles(self):
|
|
libpath = self.m_dirPicker_librarypath.GetPath()
|
|
libs = ["Octopart", "Samacsys", "UltraLibrarian", "Snapeda", "EasyEDA"]
|
|
return find_old_lib_files(folder_path=libpath, libs=libs)
|
|
|
|
def test_migrate_possible(self):
|
|
libs2migrate = self.get_old_libfiles()
|
|
conv = convert_lib_list(libs2migrate, drymode=True)
|
|
|
|
if len(conv):
|
|
self.m_button_migrate.Show()
|
|
else:
|
|
self.m_button_migrate.Hide()
|
|
|
|
def migrate_libs(self, event):
|
|
libs2migrate = self.get_old_libfiles()
|
|
|
|
conv = convert_lib_list(libs2migrate, drymode=True)
|
|
|
|
def print2GUI(text):
|
|
backend_h.print2buffer(text)
|
|
|
|
if len(conv) <= 0:
|
|
print2GUI("Error in migrate_libs()")
|
|
return
|
|
|
|
SymbolTable = backend_h.KiCad_Settings.get_sym_table()
|
|
SymbolLibsUri = {lib["uri"]: lib for lib in SymbolTable}
|
|
libRename = []
|
|
|
|
def lib_entry(lib):
|
|
return "${KICAD_3RD_PARTY}/" + lib
|
|
|
|
msg = ""
|
|
for line in conv:
|
|
if line[1].endswith(".blk"):
|
|
msg += "\n" + line[0] + " rename to " + line[1]
|
|
else:
|
|
msg += "\n" + line[0] + " convert to " + line[1]
|
|
if lib_entry(line[0]) in SymbolLibsUri:
|
|
entry = SymbolLibsUri[lib_entry(line[0])]
|
|
tmp = {
|
|
"oldURI": entry["uri"],
|
|
"newURI": lib_entry(line[1]),
|
|
"name": entry["name"],
|
|
}
|
|
libRename.append(tmp)
|
|
|
|
msg_lib = ""
|
|
if len(libRename):
|
|
msg_lib += "The following changes must be made to the list of imported Symbol libs:\n"
|
|
|
|
for tmp in libRename:
|
|
msg_lib += f"\n{tmp['name']} : {tmp['oldURI']} \n-> {tmp['newURI']}"
|
|
|
|
msg_lib += "\n\n"
|
|
msg_lib += "It is necessary to adjust the settings of the imported symbol libraries in KiCad."
|
|
msg += "\n\n" + msg_lib
|
|
|
|
msg += "\n\nBackup files are also created automatically. "
|
|
msg += "These are named '*.blk'.\nShould the changes be applied?"
|
|
|
|
dlg = wx.MessageDialog(
|
|
None, msg, "WARNING", wx.KILL_OK | wx.ICON_WARNING | wx.CANCEL
|
|
)
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
print2GUI("Converted libraries:")
|
|
conv = convert_lib_list(libs2migrate, drymode=False)
|
|
for line in conv:
|
|
if line[1].endswith(".blk"):
|
|
print2GUI(line[0] + " rename to " + line[1])
|
|
else:
|
|
print2GUI(line[0] + " convert to " + line[1])
|
|
else:
|
|
return
|
|
|
|
if not len(msg_lib):
|
|
return
|
|
|
|
msg_dlg = "\nShould the change be made automatically? A restart of KiCad is then necessary to apply all changes."
|
|
dlg2 = wx.MessageDialog(
|
|
None, msg_lib + msg_dlg, "WARNING", wx.KILL_OK | wx.ICON_WARNING | wx.CANCEL
|
|
)
|
|
if dlg2.ShowModal() == wx.ID_OK:
|
|
for tmp in libRename:
|
|
print2GUI(f"\n{tmp['name']} : {tmp['oldURI']} \n-> {tmp['newURI']}")
|
|
backend_h.KiCad_Settings.sym_table_change_entry(
|
|
tmp["oldURI"], tmp["newURI"]
|
|
)
|
|
print2GUI("\nA restart of KiCad is then necessary to apply all changes.")
|
|
else:
|
|
print2GUI(msg_lib)
|
|
|
|
self.test_migrate_possible() # When everything has worked, the button disappears
|
|
event.Skip()
|
|
|
|
|
|
class ActionImpartPlugin(pcbnew.ActionPlugin):
|
|
def defaults(self):
|
|
plugin_dir = Path(__file__).resolve().parent
|
|
self.resources_dir = plugin_dir.parent.parent / "resources" / plugin_dir.name
|
|
self.plugin_dir = plugin_dir
|
|
|
|
self.name = "impartGUI"
|
|
self.category = "Import library files"
|
|
self.description = "Import library files from Octopart, Samacsys, Ultralibrarian, Snapeda and EasyEDA"
|
|
self.show_toolbar_button = True
|
|
|
|
self.icon_file_name = str(self.resources_dir / "icon.png")
|
|
self.dark_icon_file_name = self.icon_file_name
|
|
|
|
def Run(self):
|
|
# Use virtual env
|
|
# TODO: Does not work completely reliably
|
|
python_executable = activate_virtualenv(venv_dir=self.plugin_dir / "venv")
|
|
if not ensure_package("pydantic", python_executable):
|
|
print("Problems with loading", "pydantic")
|
|
if not ensure_package("easyeda2kicad", python_executable):
|
|
print("Problems with loading", "easyeda2kicad")
|
|
|
|
# Start GUI
|
|
board = pcbnew.GetBoard()
|
|
Impart_h = impart_frontend(board, self)
|
|
Impart_h.ShowModal()
|
|
Impart_h.Destroy()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = wx.App()
|
|
frame = wx.Frame(None, title="KiCad Plugin")
|
|
Impart_t = impart_frontend(None, None)
|
|
Impart_t.ShowModal()
|
|
Impart_t.Destroy()
|