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