From 3550d1554e90043b2eb607d057f6b1f2fa87146a Mon Sep 17 00:00:00 2001 From: VaradhaCodes Date: Tue, 2 Jun 2026 11:44:16 +0530 Subject: [PATCH 1/2] PyQt6 migration and bug fixes across frontend, kicad, nghdl, and related modules --- nghdl/src/createKicadLibrary.py | 12 +-- nghdl/src/model_generation.py | 3 +- nghdl/src/ngspice_ghdl.py | 79 ++++++++++++-------- src/configuration/Appconfig.py | 6 +- src/converter/browseSchematic.py | 2 +- src/frontEnd/Application.py | 2 +- src/frontEnd/ProjectExplorer.py | 19 ++--- src/frontEnd/TerminalUi.py | 4 +- src/frontEnd/TimeExplorer.py | 6 +- src/kicadtoNgspice/Analysis.py | 105 +++++++++++++++------------ src/kicadtoNgspice/Convert.py | 2 + src/kicadtoNgspice/KicadtoNgspice.py | 9 +-- src/kicadtoNgspice/Processing.py | 2 + src/kicadtoNgspice/TrackWidget.py | 25 +++++++ src/maker/Maker.py | 82 +++++++++++++-------- src/maker/ModelGeneration.py | 2 +- src/maker/NgVeri.py | 4 +- src/modelEditor/ModelEditor.py | 8 +- src/projManagement/newProject.py | 1 + 19 files changed, 229 insertions(+), 144 deletions(-) mode change 100755 => 100644 nghdl/src/ngspice_ghdl.py mode change 100755 => 100644 src/frontEnd/ProjectExplorer.py mode change 100755 => 100644 src/kicadtoNgspice/Convert.py mode change 100755 => 100644 src/maker/Maker.py mode change 100755 => 100644 src/maker/ModelGeneration.py mode change 100755 => 100644 src/maker/NgVeri.py diff --git a/nghdl/src/createKicadLibrary.py b/nghdl/src/createKicadLibrary.py index 9eb5ab239..73f6d9b46 100644 --- a/nghdl/src/createKicadLibrary.py +++ b/nghdl/src/createKicadLibrary.py @@ -2,7 +2,7 @@ import re import os import xml.etree.cElementTree as ET -from PyQt5 import QtWidgets +from PyQt6 import QtWidgets class AutoSchematic(QtWidgets.QWidget): @@ -20,7 +20,7 @@ def __init__(self, parent, modelname): inst_dir + '/KiCad/share/kicad/symbols/eSim_Nghdl.kicad_sym' else: self.kicad_nghdl_sym = \ - '/Users/thethtarshwesin/Documents/KiCad/6.0/symbols/eSim_Nghdl.kicad_sym' + '/usr/share/kicad/symbols/eSim_Nghdl.kicad_sym' self.parser = Appconfig.parser_nghdl def createKicadSymbol(self): @@ -40,9 +40,9 @@ def createKicadSymbol(self): ''' already exist. Do you want to overwrite it?
If yes press ok, else cancel it and ''' + '''change the name of your vhdl file.''', - QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel + QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel ) - if ret == QtWidgets.QMessageBox.Ok: + if ret == QtWidgets.QMessageBox.StandardButton.Ok: print("Overwriting existing libraries") self.getPortInformation() self.createXML() @@ -57,7 +57,7 @@ def createKicadSymbol(self): self.parent, "Error", '''A standard library already ''' + '''exists with this name.
Please change the ''' + '''name of your vhdl file and upload it again.''', - QtWidgets.QMessageBox.Ok + QtWidgets.QMessageBox.StandardButton.Ok ) # quit() @@ -433,7 +433,7 @@ def createSym(self): "Symbol details for this model were added to " "eSim_Nghdl.kicad_sym successfully." ), - QtWidgets.QMessageBox.Ok + QtWidgets.QMessageBox.StandardButton.Ok ) diff --git a/nghdl/src/model_generation.py b/nghdl/src/model_generation.py index 58f1aac9d..fb180ff95 100644 --- a/nghdl/src/model_generation.py +++ b/nghdl/src/model_generation.py @@ -15,8 +15,7 @@ def __init__(self, file): if os.name == 'nt': self.home = os.path.join('library', 'config') else: - # self.home = os.expanduser('~') - self.home = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) # direcotory: Resources/ + self.home = os.path.expanduser('~') self.parser = ConfigParser() self.parser.read(os.path.join( diff --git a/nghdl/src/ngspice_ghdl.py b/nghdl/src/ngspice_ghdl.py old mode 100755 new mode 100644 index 1f7975f4e..15aa42376 --- a/nghdl/src/ngspice_ghdl.py +++ b/nghdl/src/ngspice_ghdl.py @@ -1,4 +1,4 @@ -#! /Users/thethtarshwesin/Desktop/eSim-2.5/esim-venv/bin/python3 +#!/usr/bin/env python3 # This file create the GUI to install code model in the Ngspice. @@ -6,7 +6,7 @@ import sys import shutil import subprocess -from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt6 import QtGui, QtCore, QtWidgets from configparser import ConfigParser from Appconfig import Appconfig from createKicadLibrary import AutoSchematic @@ -39,6 +39,7 @@ def __init__(self): print(fileopen.read()) fileopen.close() self.file_list = [] # to keep the supporting files + self.filename = '' self.errorFlag = False # to keep the check of "make install" errors self.initUI() @@ -60,7 +61,7 @@ def initUI(self): self.termedit.setReadOnly(1) pal = QtGui.QPalette() bgc = QtGui.QColor(0, 0, 0) - pal.setColor(QtGui.QPalette.Base, bgc) + pal.setColor(QtGui.QPalette.ColorRole.Base, bgc) self.termedit.setPalette(pal) self.termedit.setStyleSheet("QTextEdit {color:white}") @@ -139,9 +140,9 @@ def createModelDirectory(self): "This model already exist. Do you want to " + "overwrite it?
If yes press ok, else cancel it and " + "change the name of your vhdl file.", - QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Cancel + QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel ) - if ret == QtWidgets.QMessageBox.Ok: + if ret == QtWidgets.QMessageBox.StandardButton.Ok: print("Overwriting existing model " + self.modelname) if os.name == 'nt': cmd = "rmdir " + self.modelname + "/s /q" @@ -234,7 +235,7 @@ def createModelFiles(self): self.msys_home = self.parser.get('COMPILER', 'MSYS_HOME') subprocess.call(self.msys_home + "/usr/bin/bash.exe " + path + "/DUTghdl/compile.sh", shell=True) - subprocess.call(self.msys_hoscme + "/usr/bin/bash.exe -c " + + subprocess.call(self.msys_home + "/usr/bin/bash.exe -c " + "'chmod a+x start_server.sh'", shell=True) subprocess.call(self.msys_home + "/usr/bin/bash.exe -c " + "'chmod a+x sock_pkg_create.sh'", shell=True) @@ -249,11 +250,14 @@ def createModelFiles(self): # Slot to redirect stdout and stderr to window console @QtCore.pyqtSlot() def readAllStandard(self): + proc = self.sender() + if not isinstance(proc, QtCore.QProcess): + return self.termedit.append( - str(self.process.readAllStandardOutput().data(), encoding='utf-8') + str(proc.readAllStandardOutput().data(), encoding='utf-8') ) - stderror = self.process.readAllStandardError() - if stderror.toUpper().contains(b"ERROR"): + stderror = proc.readAllStandardError() + if stderror.toUpper().contains(QtCore.QByteArray(b"ERROR")): self.errorFlag = True self.termedit.append(str(stderror.data(), encoding='utf-8')) @@ -265,22 +269,21 @@ def runMake(self): try: if os.name == 'nt': - # path to msys bin directory where make is located self.msys_home = self.parser.get('COMPILER', 'MSYS_HOME') cmd = self.msys_home + "/mingw64/bin/mingw32-make.exe" else: - cmd = " make" + cmd = "make" print("Running Make command in " + path_icm) - path = os.getcwd() # noqa self.process = QtCore.QProcess(self) - self.process.start(cmd) - print("make command process pid ---------- >", self.process.pid()) - + self.process.readyReadStandardOutput.connect(self.readAllStandard) + self.process.readyReadStandardError.connect(self.readAllStandard) if os.name == "nt": self.process.finished.connect(self.createSchematicLib) - self.process \ - .readyReadStandardOutput.connect(self.readAllStandard) + else: + self.process.finished.connect(self.runMakeInstall) + self.process.start(cmd) + print("make command process pid ---------- >", self.process.processId()) except BaseException: print("There is error in 'make' ") @@ -291,20 +294,18 @@ def runMakeInstall(self): try: if os.name == 'nt': self.msys_home = self.parser.get('COMPILER', 'MSYS_HOME') - cmd = self.msys_home + "/mingw64/bin/mingw32-make.exe install" + prog = self.msys_home + "/mingw64/bin/mingw32-make.exe" + args = ["install"] else: - cmd = " make install" + prog = "make" + args = ["install"] print("Running Make Install") - path = os.getcwd() # noqa - try: - self.process.close() - except BaseException: - pass self.process = QtCore.QProcess(self) - self.process.start(cmd) - self.process.finished.connect(self.createSchematicLib) self.process.readyReadStandardOutput.connect(self.readAllStandard) + self.process.readyReadStandardError.connect(self.readAllStandard) + self.process.finished.connect(self.createSchematicLib) + self.process.start(prog, args) os.chdir(self.cur_dir) except BaseException: @@ -312,6 +313,19 @@ def runMakeInstall(self): sys.exit() def createSchematicLib(self): + try: + self._createSchematicLib() + except Exception as e: + print("createSchematicLib exception:", e) + import traceback + traceback.print_exc() + QtWidgets.QMessageBox.critical( + self, 'Error', 'Library creation failed: ' + str(e) + ) + self.uploadbtn.setEnabled(True) + self.exitbtn.setEnabled(True) + + def _createSchematicLib(self): if os.name == "nt": shutil.copy("ghdl/ghdl.cm", "../../../../lib/ngspice/") @@ -333,6 +347,8 @@ def createSchematicLib(self): '''To create Schematic Library of your model, ''' + '''use NGHDL through eSim ''' ) + self.uploadbtn.setEnabled(True) + self.exitbtn.setEnabled(True) def uploadModel(self): print("Upload button clicked") @@ -340,6 +356,10 @@ def uploadModel(self): self.process.close() except BaseException: pass + if not self.filename: + QtWidgets.QMessageBox.warning( + self, 'No File', 'Use Browse to select a .vhdl file first.') + return try: self.file_extension = os.path.splitext(str(self.filename))[1] print("Uploaded File extension :" + self.file_extension) @@ -348,12 +368,13 @@ def uploadModel(self): self.checkSupportFiles() if self.file_extension == ".vhdl": self.errorFlag = False + self.uploadbtn.setEnabled(False) + self.exitbtn.setEnabled(False) + self.termedit.append('Processing... do not close until Symbol Added dialog appears.') self.createModelDirectory() self.addingModelInModpath() self.createModelFiles() self.runMake() - if os.name != 'nt': - self.runMakeInstall() else: QtWidgets.QMessageBox.information( self, 'Message', '''Important Message.
''' + @@ -439,7 +460,7 @@ def main(): # Mainwindow() object must be assigned to a variable. # Otherwise, it is destroyed as soon as it gets created. w = Mainwindow() # noqa - sys.exit(app.exec_()) + sys.exit(app.exec()) if __name__ == '__main__': diff --git a/src/configuration/Appconfig.py b/src/configuration/Appconfig.py index 3a4ffb4ad..31920a53f 100644 --- a/src/configuration/Appconfig.py +++ b/src/configuration/Appconfig.py @@ -46,7 +46,11 @@ class Appconfig(QtWidgets.QWidget): ) workspace_check, home = file.readline().split(' ', 1) file.close() - except IOError: + except (IOError, ValueError): + # ValueError: workspace.txt was truncated/empty (e.g. an interrupted + # write left it blank), so " ".split(' ', 1) cannot unpack + # into two names. Fall back to the default workspace instead of letting + # the exception escape the class body and abort startup. home = os.path.join(os.path.expanduser("~"), "eSim-Workspace") workspace_check = 0 diff --git a/src/converter/browseSchematic.py b/src/converter/browseSchematic.py index 436c11c6a..f5e9abdc4 100644 --- a/src/converter/browseSchematic.py +++ b/src/converter/browseSchematic.py @@ -2,7 +2,7 @@ def browse_path(self, text_box): file_dialog = QFileDialog() # a dialog that allows the user to select files or directories - file_dialog.setFileMode(QFileDialog.AnyFile) + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) file_dialog.setNameFilter("Supported Files (*.sch *.asc *.slb *.asy);;ASY Files (*.asy)") # Include all supported extensions file_dialog.exec() # Execute the dialog diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 953b8806b..7f8418731 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -447,7 +447,7 @@ def dev_docs(self): webbrowser.open("https://esim.readthedocs.io/en/latest/index.html") @QtCore.pyqtSlot(QtCore.QProcess.ExitStatus, int) - def plotSimulationData(self, exitCode, exitStatus): + def plotSimulationData(self, exitStatus, exitCode): """Enables interaction for new simulation and displays the plotter dock where graphs can be plotted. """ diff --git a/src/frontEnd/ProjectExplorer.py b/src/frontEnd/ProjectExplorer.py old mode 100755 new mode 100644 index ededeb8aa..75f7e0a71 --- a/src/frontEnd/ProjectExplorer.py +++ b/src/frontEnd/ProjectExplorer.py @@ -118,12 +118,14 @@ def addTreeNode(self, parents, children): def openMenu(self, position): indexes = self.treewidget.selectedIndexes() - if len(indexes) > 0: - level = 0 - index = indexes[0] - while index.parent().isValid(): - index = index.parent() - level += 1 + if not indexes: + return + + level = 0 + index = indexes[0] + while index.parent().isValid(): + index = index.parent() + level += 1 menu = QtWidgets.QMenu() if level == 0: @@ -139,7 +141,7 @@ def openMenu(self, position): snapshot = menu.addAction(self.tr("Snapshot")) snapshot.triggered.connect(self.takeSnapshot) - menu.exec_(self.treewidget.viewport().mapToGlobal(position)) + menu.exec(self.treewidget.viewport().mapToGlobal(position)) def openProject(self): self.indexItem = self.treewidget.currentIndex() @@ -292,7 +294,7 @@ def renameProject(self): newBaseFileName, ok = QtWidgets.QInputDialog.getText( self, 'Rename Project', 'Project Name:', - QtWidgets.QLineEdit.Normal, self.baseFileName + QtWidgets.QLineEdit.EchoMode.Normal, self.baseFileName ) if ok and newBaseFileName: @@ -474,4 +476,3 @@ def takeSnapshot(self): self.time_explorer.add_snapshot(file_name, formatted_time) else: print(f"Snapshot taken: {snapshot_path}") - msg.exec() diff --git a/src/frontEnd/TerminalUi.py b/src/frontEnd/TerminalUi.py index 2384991e3..20861d9c0 100644 --- a/src/frontEnd/TerminalUi.py +++ b/src/frontEnd/TerminalUi.py @@ -24,7 +24,7 @@ def __init__(self, qProcess, args): self.iconDir = "../../images" # Load the ui file - uic.loadUi("TerminalUi.ui", self) + uic.loadUi(os.path.join(os.path.dirname(__file__), "TerminalUi.ui"), self) # Define Our Widgets self.progressBar = self.findChild( @@ -94,7 +94,7 @@ def cancelSimulation(self): def redoSimulation(self): """This function reruns the ngspice simulation """ - self.Flag = "Flase" + self.Flag = False self.cancelSimulationButton.setEnabled(True) self.redoSimulationButton.setEnabled(False) diff --git a/src/frontEnd/TimeExplorer.py b/src/frontEnd/TimeExplorer.py index 1c38d8023..b028c9740 100644 --- a/src/frontEnd/TimeExplorer.py +++ b/src/frontEnd/TimeExplorer.py @@ -2,7 +2,7 @@ import re import shutil import json -from PyQt5 import QtWidgets +from PyQt6 import QtWidgets class TimeExplorer(QtWidgets.QWidget): @@ -81,6 +81,8 @@ def load_last_snapshots(self): def clear_snapshots(self): selected = self.treewidget.selectedItems() project_name = self.current_project["ProjectName"] + if not project_name: + return snapshot_dir = os.path.join(self.user_home, ".esim", "history", project_name) if selected: @@ -125,6 +127,8 @@ def restore_snapshots(self): selected_items = self.treewidget.selectedItems() project_name = self.current_project["ProjectName"] + if not project_name: + return snapshot_dir = os.path.join(self.user_home, ".esim", "history", project_name) if not os.path.exists(snapshot_dir): diff --git a/src/kicadtoNgspice/Analysis.py b/src/kicadtoNgspice/Analysis.py index 0b169efd9..c474228e0 100644 --- a/src/kicadtoNgspice/Analysis.py +++ b/src/kicadtoNgspice/Analysis.py @@ -282,7 +282,7 @@ def createACgroup(self): self.ac_parameter[self.parameter_cnt] = "Hz" # Event listener for combo action - self.start_fre_combo.activated[str].connect(self.start_combovalue) + self.start_fre_combo.currentTextChanged.connect(self.start_combovalue) self.parameter_cnt = self.parameter_cnt + 1 self.stop_fre_combo = QtWidgets.QComboBox() @@ -300,7 +300,7 @@ def createACgroup(self): except BaseException: self.ac_parameter[self.parameter_cnt] = "Hz" - self.stop_fre_combo.activated[str].connect(self.stop_combovalue) + self.stop_fre_combo.currentTextChanged.connect(self.stop_combovalue) self.track_obj.AC_entry_var["ITEMS"] = self.ac_entry_var self.track_obj.AC_Parameter["ITEMS"] = self.ac_parameter @@ -327,13 +327,15 @@ def createACgroup(self): self.Dec.setChecked(False) self.Oct.setChecked(True) - self.ac_entry_var[0].setText(root[0][3].text) - self.ac_entry_var[1].setText(root[0][4].text) - self.ac_entry_var[2].setText(root[0][5].text) - index = self.start_fre_combo.findText(root[0][6].text) - self.start_fre_combo.setCurrentIndex(index) - index = self.stop_fre_combo.findText(root[0][7].text) - self.stop_fre_combo.setCurrentIndex(index) + self.ac_entry_var[0].setText(root[0][3].text or "") + self.ac_entry_var[1].setText(root[0][4].text or "") + self.ac_entry_var[2].setText(root[0][5].text or "") + index = self.start_fre_combo.findText(root[0][6].text or "") + if index >= 0: + self.start_fre_combo.setCurrentIndex(index) + index = self.stop_fre_combo.findText(root[0][7].text or "") + if index >= 0: + self.stop_fre_combo.setCurrentIndex(index) except BaseException: print("AC Analysis XML Parse Error") @@ -498,7 +500,7 @@ def createDCgroup(self): except BaseException: self.dc_parameter[self.parameter_cnt] = "Volts or Amperes" - self.start_combo.activated[str].connect(self.start_changecombo) + self.start_combo.currentTextChanged.connect(self.start_changecombo) self.parameter_cnt += 1 self.increment_combo = QtWidgets.QComboBox(self) @@ -515,7 +517,7 @@ def createDCgroup(self): except BaseException: self.dc_parameter[self.parameter_cnt] = "Volts or Amperes" - self.increment_combo.activated[str].connect(self.increment_changecombo) + self.increment_combo.currentTextChanged.connect(self.increment_changecombo) self.parameter_cnt += 1 self.stop_combo = QtWidgets.QComboBox(self) @@ -532,7 +534,7 @@ def createDCgroup(self): except BaseException: self.dc_parameter[self.parameter_cnt] = "Volts or Amperes" - self.stop_combo.activated[str].connect(self.stop_changecombo) + self.stop_combo.currentTextChanged.connect(self.stop_changecombo) self.parameter_cnt += 1 self.start_combo2 = QtWidgets.QComboBox(self) @@ -549,7 +551,7 @@ def createDCgroup(self): except BaseException: self.dc_parameter[self.parameter_cnt] = "Volts or Amperes" - self.start_combo2.activated[str].connect(self.start_changecombo2) + self.start_combo2.currentTextChanged.connect(self.start_changecombo2) self.parameter_cnt += 1 self.increment_combo2 = QtWidgets.QComboBox(self) @@ -566,7 +568,7 @@ def createDCgroup(self): except BaseException: self.dc_parameter[self.parameter_cnt] = "Volts or Amperes" - self.increment_combo2.activated[str].connect( + self.increment_combo2.currentTextChanged.connect( self.increment_changecombo2) self.parameter_cnt += 1 @@ -584,7 +586,7 @@ def createDCgroup(self): except BaseException: self.dc_parameter[self.parameter_cnt] = "Volts or Amperes" - self.stop_combo2.activated[str].connect(self.stop_changecombo2) + self.stop_combo2.currentTextChanged.connect(self.stop_changecombo2) self.parameter_cnt += 1 self.check = QtWidgets.QCheckBox('Operating Point Analysis', self) @@ -609,26 +611,32 @@ def createDCgroup(self): ") if check: try: - self.dc_entry_var[0].setText(root[1][0].text) - self.dc_entry_var[1].setText(root[1][1].text) - self.dc_entry_var[2].setText(root[1][2].text) - self.dc_entry_var[3].setText(root[1][3].text) - index = self.start_combo.findText(root[1][5].text) - self.start_combo.setCurrentIndex(index) - index = self.increment_combo.findText(root[1][6].text) - self.increment_combo.setCurrentIndex(index) - index = self.stop_combo.findText(root[1][7].text) - self.stop_combo.setCurrentIndex(index) - self.dc_entry_var[4].setText(root[1][8].text) - self.dc_entry_var[5].setText(root[1][9].text) - self.dc_entry_var[6].setText(root[1][10].text) - self.dc_entry_var[7].setText(root[1][11].text) - index = self.start_combo2.findText(root[1][12].text) - self.start_combo2.setCurrentIndex(index) - index = self.increment_combo2.findText(root[1][13].text) - self.increment_combo2.setCurrentIndex(index) - index = self.stop_combo2.findText(root[1][14].text) - self.stop_combo2.setCurrentIndex(index) + self.dc_entry_var[0].setText(root[1][0].text or "") + self.dc_entry_var[1].setText(root[1][1].text or "") + self.dc_entry_var[2].setText(root[1][2].text or "") + self.dc_entry_var[3].setText(root[1][3].text or "") + index = self.start_combo.findText(root[1][5].text or "") + if index >= 0: + self.start_combo.setCurrentIndex(index) + index = self.increment_combo.findText(root[1][6].text or "") + if index >= 0: + self.increment_combo.setCurrentIndex(index) + index = self.stop_combo.findText(root[1][7].text or "") + if index >= 0: + self.stop_combo.setCurrentIndex(index) + self.dc_entry_var[4].setText(root[1][8].text or "") + self.dc_entry_var[5].setText(root[1][9].text or "") + self.dc_entry_var[6].setText(root[1][10].text or "") + self.dc_entry_var[7].setText(root[1][11].text or "") + index = self.start_combo2.findText(root[1][12].text or "") + if index >= 0: + self.start_combo2.setCurrentIndex(index) + index = self.increment_combo2.findText(root[1][13].text or "") + if index >= 0: + self.increment_combo2.setCurrentIndex(index) + index = self.stop_combo2.findText(root[1][14].text or "") + if index >= 0: + self.stop_combo2.setCurrentIndex(index) if root[1][4].text == 1: self.check.setChecked(True) @@ -746,7 +754,7 @@ def createTRANgroup(self): except BaseException: self.tran_parameter[self.parameter_cnt] = "sec" - self.start_combobox.activated[str].connect(self.start_combo_change) + self.start_combobox.currentTextChanged.connect(self.start_combo_change) self.parameter_cnt += 1 self.step_combobox = QtWidgets.QComboBox() @@ -761,7 +769,7 @@ def createTRANgroup(self): except BaseException: self.tran_parameter[self.parameter_cnt] = "sec" - self.step_combobox.activated[str].connect(self.step_combo_change) + self.step_combobox.currentTextChanged.connect(self.step_combo_change) self.parameter_cnt += 1 self.stop_combobox = QtWidgets.QComboBox() @@ -776,7 +784,7 @@ def createTRANgroup(self): except BaseException: self.tran_parameter[self.parameter_cnt] = "sec" - self.stop_combobox.activated[str].connect(self.stop_combo_change) + self.stop_combobox.currentTextChanged.connect(self.stop_combo_change) self.parameter_cnt += 1 self.track_obj.TRAN_entry_var["ITEMS"] = self.tran_entry_var @@ -791,15 +799,18 @@ def createTRANgroup(self): ") if check: try: - self.tran_entry_var[0].setText(root[2][0].text) - self.tran_entry_var[1].setText(root[2][1].text) - self.tran_entry_var[2].setText(root[2][2].text) - index = self.start_combobox.findText(root[2][3].text) - self.start_combobox.setCurrentIndex(index) - index = self.step_combobox.findText(root[2][4].text) - self.step_combobox.setCurrentIndex(index) - index = self.stop_combobox.findText(root[2][5].text) - self.stop_combobox.setCurrentIndex(index) + self.tran_entry_var[0].setText(root[2][0].text or "") + self.tran_entry_var[1].setText(root[2][1].text or "") + self.tran_entry_var[2].setText(root[2][2].text or "") + index = self.start_combobox.findText(root[2][3].text or "") + if index >= 0: + self.start_combobox.setCurrentIndex(index) + index = self.step_combobox.findText(root[2][4].text or "") + if index >= 0: + self.step_combobox.setCurrentIndex(index) + index = self.stop_combobox.findText(root[2][5].text or "") + if index >= 0: + self.stop_combobox.setCurrentIndex(index) except BaseException: print("Transient Analysis XML Parse Error") diff --git a/src/kicadtoNgspice/Convert.py b/src/kicadtoNgspice/Convert.py old mode 100755 new mode 100644 index 665a30130..4e5fcf227 --- a/src/kicadtoNgspice/Convert.py +++ b/src/kicadtoNgspice/Convert.py @@ -331,6 +331,8 @@ def converttosciform(self, string_obj): This function is used for scientific conversion. """ self.string_obj = string_obj + if not self.string_obj: + return "e-00" if self.string_obj[0] == 'm': return "e-03" elif self.string_obj[0] == 'u': diff --git a/src/kicadtoNgspice/KicadtoNgspice.py b/src/kicadtoNgspice/KicadtoNgspice.py index 50131609a..1a4f00a12 100644 --- a/src/kicadtoNgspice/KicadtoNgspice.py +++ b/src/kicadtoNgspice/KicadtoNgspice.py @@ -59,13 +59,8 @@ def __init__(self, clarg1, clarg2=None): # Track the dynamically created widget of KicadtoNgspice Window self.obj_track = TrackWidget.TrackWidget() - # Clear Dictionary/List item of sub circuit and Ngspice model - # Dictionary - self.obj_track.subcircuitList.clear() - self.obj_track.subcircuitTrack.clear() - self.obj_track.model_entry_var.clear() - # List - self.obj_track.modelTrack[:] = [] + # Reset all shared class-level state for this conversion run + TrackWidget.TrackWidget.reset() # Object of Processing obj_proc = PrcocessNetlist() diff --git a/src/kicadtoNgspice/Processing.py b/src/kicadtoNgspice/Processing.py index 94de9440c..dbc8f4625 100644 --- a/src/kicadtoNgspice/Processing.py +++ b/src/kicadtoNgspice/Processing.py @@ -142,6 +142,8 @@ def insertSpecialSourceParam(self, schematicInfo, sourcelist): if (compName[0] == 'v' or compName[0] == 'i') and not compName.startswith('ihp'): # Find the index component from circuit index = schematicInfo.index(compline) + if len(words) <= 3: + continue if words[3] == "pulse": Title = "Add parameters for pulse source " + compName v1 = ' Enter initial value (Volts/Amps): ' diff --git a/src/kicadtoNgspice/TrackWidget.py b/src/kicadtoNgspice/TrackWidget.py index 9f1d07c2d..2aafc2251 100644 --- a/src/kicadtoNgspice/TrackWidget.py +++ b/src/kicadtoNgspice/TrackWidget.py @@ -7,6 +7,9 @@ class TrackWidget: - References - Model Details - ... etc + - All attributes are class-level (shared across instances by design). + - Call TrackWidget.reset() at the start of each conversion to clear + accumulated state from previous runs. """ # Track widget list for Source details sourcelisttrack = {"ITEMS": "None"} @@ -35,3 +38,25 @@ class TrackWidget: subcircuitTrack = {} # Track subcircuits which are specified in .cir file subcircuitList = {} + + @classmethod + def reset(cls): + """Reset all shared class-level state for a fresh conversion run.""" + cls.sourcelisttrack = {"ITEMS": "None"} + cls.source_entry_var = {"ITEMS": "None"} + cls.AC_entry_var = {"ITEMS": "None"} + cls.AC_Parameter = {"ITEMS": "None"} + cls.DC_entry_var = {"ITEMS": "None"} + cls.DC_Parameter = {"ITEMS": "None"} + cls.TRAN_entry_var = {"ITEMS": "None"} + cls.TRAN_Parameter = {"ITEMS": "None"} + cls.set_CheckBox = {"ITEMS": "None"} + cls.AC_type = {"ITEMS": "None"} + cls.op_check = [] + cls.modelTrack = [] + cls.microcontrollerTrack = [] + cls.model_entry_var = {} + cls.microcontroller_var = {} + cls.deviceModelTrack = {} + cls.subcircuitTrack = {} + cls.subcircuitList = {} diff --git a/src/maker/Maker.py b/src/maker/Maker.py old mode 100755 new mode 100644 index 9c8727e56..21bdc4bb0 --- a/src/maker/Maker.py +++ b/src/maker/Maker.py @@ -29,7 +29,7 @@ # importing the files and libraries import hdlparse.verilog_parser as vlog from PyQt6 import QtCore, QtWidgets -from PyQt6.QtCore import QThread +from PyQt6.QtCore import QThread, pyqtSignal from configuration.Appconfig import Appconfig import os import watchdog.events @@ -95,6 +95,23 @@ def createMakerWidget(self): # self.grid.addWidget(self.creategroup(), 1, 0, 5, 0) self.show() + def _stop_current_toggle(self): + global toggle_flag + if self.refreshoption in toggle_flag: + toggle_flag.remove(self.refreshoption) + if hasattr(self, 'event_handler') and self.event_handler.toggle.isRunning(): + self.event_handler.toggle.requestInterruption() + self.event_handler.toggle.wait(2000) + + def closeEvent(self, event): + self._stop_current_toggle() + if hasattr(self, '_rc_toggle') and self._rc_toggle.isRunning(): + self._rc_toggle.requestInterruption() + self._rc_toggle.wait(2000) + if hasattr(self, 'observer') and self.observer.is_alive(): + self.observer.stop() + super().closeEvent(event) + # This function is to Add new verilog file def addverilog(self): @@ -136,8 +153,7 @@ def addverilog(self): global verilogFile verilogFile[self.filecount] = self.verilogfile - if self.refreshoption in toggle_flag: - toggle_flag.remove(self.refreshoption) + self._stop_current_toggle() self.observer = watchdog.observers.Observer() self.event_handler = Handler( @@ -159,8 +175,10 @@ def addverilog(self): # (as the original one gets destroyed) def refresh_change(self): if self.refreshoption in toggle_flag: - self.toggle = toggle(self.refreshoption) - self.toggle.start() + if hasattr(self, '_rc_toggle') and self._rc_toggle.isRunning(): + return + self._rc_toggle = toggle(self.refreshoption) + self._rc_toggle.start() # It is used to refresh the file in eSim if its edited anywhere else def refresh(self): @@ -171,6 +189,7 @@ def refresh(self): print("NgVeri File: " + self.verilogfile + " Refreshed") self.obj_Appconfig.print_info( "NgVeri File: " + self.verilogfile + " Refreshed") + self._stop_current_toggle() self.observer = watchdog.observers.Observer() self.event_handler = Handler( self.verilogfile, @@ -182,10 +201,6 @@ def refresh(self): path=self.verilogfile, recursive=True) self.observer.start() - # self.notify.start() - global toggle_flag - if self.refreshoption in toggle_flag: - toggle_flag.remove(self.refreshoption) # This function is used to save the edited file in eSim def save(self): @@ -321,7 +336,7 @@ def runmakerchip(self): self.process.start(cmd) print( "Makerchip IDE command process pid ---------->", - self.process.pid()) + self.process.processId()) except BaseException as e: print(e) self.msg = QtWidgets.QErrorMessage(self) @@ -444,6 +459,11 @@ def creategroup(self): return self.trbox +class _FileModifiedNotifier(QtCore.QObject): + """Bridges watchdog background thread to main thread for GUI notifications.""" + modified = pyqtSignal(str) + + # The Handler class is used to create a watch on the files using WatchDog class Handler(watchdog.events.PatternMatchingEventHandler): # this function initialisses the variable and the objects of watchdog @@ -456,27 +476,28 @@ def __init__(self, verilogfile, refreshoption, observer): self.obj_Appconfig = Appconfig() self.observer = observer self.toggle = toggle(self.refreshoption) + self._notifier = _FileModifiedNotifier() + self._notifier.modified.connect(self._show_modified_dialog) - # if a file is modified, toggle starts to toggle the refresh button - def on_modified(self, event): - print("Watchdog received modified event - % s." % event.src_path) + def _show_modified_dialog(self, verilogfile): msg = QtWidgets.QErrorMessage() msg.setWindowTitle("eSim Message") msg.showMessage( - "NgVeri File: " + - self.verilogfile + - " modified. Please click on Refresh") + "NgVeri File: " + verilogfile + " modified. Please click on Refresh") msg.exec() + + # if a file is modified, toggle starts to toggle the refresh button + def on_modified(self, event): + print("Watchdog received modified event - % s." % event.src_path) print("NgVeri File: " + self.verilogfile + " modified. Please click on Refresh") - # self.obj_Appconfig.print_info("NgVeri File:\ - # "+self.verilogfile+" modified. Please click on Refresh") global toggle_flag if self.refreshoption not in toggle_flag: toggle_flag.append(self.refreshoption) - # i.rm_watch() self.observer.stop() - self.toggle.start() + if not self.toggle.isRunning(): + self.toggle.start() + self._notifier.modified.emit(self.verilogfile) # class notify(QThread): @@ -522,24 +543,23 @@ def on_modified(self, event): # This class is used to toggle a button(change colour by toggling) class toggle(QThread): + changeStyle = pyqtSignal(str) + # initialising the threads def __init__(self, option): QThread.__init__(self) self.option = option - - def __del__(self): - self.wait() + self.changeStyle.connect(option.setStyleSheet) # running the thread to toggle def run(self): - - while True: - self.option.setStyleSheet("background-color: red") - self.sleep(1) - self.option.setStyleSheet("background-color: none") - self.sleep(1) - print(toggle_flag) - if not self.option.isVisible(): + while not self.isInterruptionRequested(): + self.changeStyle.emit("background-color: red") + self.msleep(1000) + if self.isInterruptionRequested(): break + self.changeStyle.emit("background-color: none") + self.msleep(1000) if self.option not in toggle_flag: break + self.changeStyle.emit("") diff --git a/src/maker/ModelGeneration.py b/src/maker/ModelGeneration.py old mode 100755 new mode 100644 index a58d4a432..3950eb20f --- a/src/maker/ModelGeneration.py +++ b/src/maker/ModelGeneration.py @@ -1000,7 +1000,7 @@ def runMake(self): print("Running Make command in " + path_icm) self.process = QtCore.QProcess(self) self.process.start('sh', ['-c', self.cmd]) - print("make command process pid ---------- >", self.process.pid()) + print("make command process pid ---------- >", self.process.processId()) self.termtitle("MAKE COMMAND") self.termtext("Current Directory: " + path_icm) diff --git a/src/maker/NgVeri.py b/src/maker/NgVeri.py old mode 100755 new mode 100644 index 3b0f4c2da..3f4efc7ed --- a/src/maker/NgVeri.py +++ b/src/maker/NgVeri.py @@ -402,7 +402,7 @@ def creategroup(self): for item in self.data: if item != "\n": self.entry_var[self.count].addItem(item.strip()) - self.entry_var[self.count].activated[str].connect(self.edit_modlst) + self.entry_var[self.count].currentTextChanged.connect(self.edit_modlst) self.trgrid.addWidget(self.entry_var[self.count], 1, 4, 1, 2) self.count += 1 self.entry_var[self.count] = QtWidgets.QComboBox() @@ -418,7 +418,7 @@ def creategroup(self): for item in self.data: if item != "\n": self.entry_var[self.count].addItem(item.strip()) - self.entry_var[self.count].activated[str].connect(self.lint_off_edit) + self.entry_var[self.count].currentTextChanged.connect(self.lint_off_edit) self.trgrid.addWidget(self.entry_var[self.count], 2, 4, 1, 2) self.count += 1 self.entry_var[self.count] = QtWidgets.QLineEdit(self) diff --git a/src/modelEditor/ModelEditor.py b/src/modelEditor/ModelEditor.py index 51136ead2..c069e4b57 100644 --- a/src/modelEditor/ModelEditor.py +++ b/src/modelEditor/ModelEditor.py @@ -185,7 +185,7 @@ def bjt_click(self): filetype = str(self.types.currentText()) self.openfiletype(filetype) # When element selected from combo box, call setfiletype - self.types.activated[str].connect(self.setfiletype) + self.types.currentTextChanged.connect(self.setfiletype) def mos_click(self): ''' @@ -207,7 +207,7 @@ def mos_click(self): self.types.addItem('PMOS(Level-8 180um)') filetype = str(self.types.currentText()) self.openfiletype(filetype) - self.types.activated[str].connect(self.setfiletype) + self.types.currentTextChanged.connect(self.setfiletype) def jfet_click(self): ''' @@ -224,7 +224,7 @@ def jfet_click(self): self.types.addItem('P-JFET') filetype = str(self.types.currentText()) self.openfiletype(filetype) - self.types.activated[str].connect(self.setfiletype) + self.types.currentTextChanged.connect(self.setfiletype) def igbt_click(self): ''' @@ -241,7 +241,7 @@ def igbt_click(self): self.types.addItem('P-IGBT') filetype = str(self.types.currentText()) self.openfiletype(filetype) - self.types.activated[str].connect(self.setfiletype) + self.types.currentTextChanged.connect(self.setfiletype) def magnetic_click(self): ''' diff --git a/src/projManagement/newProject.py b/src/projManagement/newProject.py index 18ef1b4ce..9163e7715 100644 --- a/src/projManagement/newProject.py +++ b/src/projManagement/newProject.py @@ -95,6 +95,7 @@ def createProject(self, projName): 'write permission on ' + self.workspace ) self.msg.exec() + return None, None # New KiCad v6 file extension f.write("schematicFile " + self.projName + ".kicad_sch\n") From 314bfe7a780849f24eef13de515bf167d657de4c Mon Sep 17 00:00:00 2001 From: VaradhaCodes Date: Tue, 2 Jun 2026 12:03:34 +0530 Subject: [PATCH 2/2] fix(devicemodel): guard all eSim_general_libs branches against short netlist lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Malformed SPICE component lines with fewer tokens than expected caused IndexError crashes when opening KiCad→Ngspice converter. Added len(words) guards on all five device branches (q, d, j, s, m) — short lines are silently skipped, consistent with existing try/except BaseException patterns. Triggered by SN55451B netlist where diode lines had only 2 tokens (D2 __D2). --- src/kicadtoNgspice/DeviceModel.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/kicadtoNgspice/DeviceModel.py b/src/kicadtoNgspice/DeviceModel.py index 2088894d0..ead12b3ca 100755 --- a/src/kicadtoNgspice/DeviceModel.py +++ b/src/kicadtoNgspice/DeviceModel.py @@ -572,7 +572,7 @@ def eSim_general_libs(self, schematicInfo): print("=========================================") print(eachline) words = eachline.split() - if eachline[0] == 'q': + if eachline[0] == 'q' and len(words) > 4: # print("Device Model Transistor: ", words[0]) self.devicemodel_dict_beg[words[0]] = self.count transbox = QtWidgets.QGroupBox() @@ -637,7 +637,7 @@ def eSim_general_libs(self, schematicInfo): self.devicemodel_dict_end[words[0]] = self.count self.count = self.count + 1 - elif eachline[0] == 'd': + elif eachline[0] == 'd' and len(words) > 3: # print("Device Model Diode:", words[0]) self.devicemodel_dict_beg[words[0]] = self.count diodebox = QtWidgets.QGroupBox() @@ -701,7 +701,7 @@ def eSim_general_libs(self, schematicInfo): self.devicemodel_dict_end[words[0]] = self.count self.count = self.count + 1 - elif eachline[0] == 'j': + elif eachline[0] == 'j' and len(words) > 4: # print("Device Model JFET:", words[0]) self.devicemodel_dict_beg[words[0]] = self.count jfetbox = QtWidgets.QGroupBox() @@ -764,7 +764,7 @@ def eSim_general_libs(self, schematicInfo): self.devicemodel_dict_end[words[0]] = self.count self.count = self.count + 1 - elif eachline[0] == 's': + elif eachline[0] == 's' and len(words) > 5: # print("Device Model Switch:", words[0]) self.devicemodel_dict_beg[words[0]] = self.count switchbox = QtWidgets.QGroupBox() @@ -890,7 +890,7 @@ def eSim_general_libs(self, schematicInfo): self.devicemodel_dict_end[words[0]] = self.count self.count = self.count + 1 - elif eachline[0] == 'm': + elif eachline[0] == 'm' and len(words) > 4: self.devicemodel_dict_beg[words[0]] = self.count mosfetbox = QtWidgets.QGroupBox()