my-kicad-user-folder/3rdparty/plugins/com_github_Steffen-W_impartGUI/impart_action.py
2025-03-21 13:28:36 +08:00

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()