From eb1ab773316106b60e85debf4e12f9473fe7456f Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Thu, 25 Jul 2024 16:37:34 +0200 Subject: [PATCH 01/11] Added tmc debug openloop test mode --- CHANGELOG.md | 3 + main.py | 2 +- res/tmcdebug.ui | 181 +++++++++++++++++++++++++++++++++++++++++++++++- tmcdebug_ui.py | 83 +++++++++++++++++++++- 4 files changed, 265 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d16c9f1..a60ffb28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ ### Changes this version: +- Added TMC debug openloop test mode + +### Changes in 1.15.x: - Added permanent inertia and friction effect sliders - Added position save toggle for ODrive \ No newline at end of file diff --git a/main.py b/main.py index f9c7ae52..8ede0dd8 100644 --- a/main.py +++ b/main.py @@ -56,7 +56,7 @@ import activetasks # This GUIs version -VERSION = "1.15.0" +VERSION = "1.15.1" # Minimal supported firmware version. # Major version of firmware must match firmware. Minor versions must be higher or equal diff --git a/res/tmcdebug.ui b/res/tmcdebug.ui index 00634a18..2c520de8 100644 --- a/res/tmcdebug.ui +++ b/res/tmcdebug.ui @@ -30,8 +30,187 @@ + + + + Test mode + + + + + + <html><head/><body><p>Warning: Openloop mode does NOT use current sensors. You are manually applying a PWM value. Carefully check the currents.<br/>High currents may DAMAGE the motor, driver, connected devices or cause INJURY.<br/><br/>Set up motor before enabling this.</p></body></html> + + + + + + + false + + + Yes i know what i am doing. Enable openloop mode. + + + + + + + false + + + Openloop test + + + + + + 28000 + + + 0 + + + false + + + + + + + PWM + + + + + + + Speed + + + + + + + Qt::Horizontal + + + QSlider::NoTicks + + + + + + + 20000 + + + Qt::Horizontal + + + + + + + false + + + true + + + QAbstractSpinBox::NoButtons + + + false + + + % + + + 99 + + + + + + + Current + + + + + + + false + + + true + + + QAbstractSpinBox::NoButtons + + + false + + + rpm + + + + + + + false + + + true + + + QAbstractSpinBox::NoButtons + + + false + + + /32787 + + + 40000 + + + + + + + Reverse + + + + + + + + + - + + + horizontalSlider_speed + valueChanged(int) + spinBox_speed + setValue(int) + + + 316 + 391 + + + 611 + 391 + + + + diff --git a/tmcdebug_ui.py b/tmcdebug_ui.py index 93be827e..5131cdcc 100644 --- a/tmcdebug_ui.py +++ b/tmcdebug_ui.py @@ -7,16 +7,95 @@ from PyQt6.QtCore import QTimer import main from base_ui import WidgetUI +from base_ui import CommunicationHandler -class TMCDebugUI(WidgetUI): +class TMCDebugUI(WidgetUI,CommunicationHandler): def __init__(self, main=None, unique=1): WidgetUI.__init__(self, main,'tmcdebug.ui') self.main = main #type: main.MainUi + self.timer = QTimer(self) + self.timer_slow = QTimer(self) + self.pwm = 0 + self.speed = 0 + self.axis = 0 + self.openloopenabled = False + self.adc_to_amps = 0 + + self.timer.timeout.connect(self.updateTimer) + self.timer_slow.timeout.connect(self.updateTimerSlow) + + self.register_callback("tmc","acttrq",self.updateCurrent,self.axis,str) + self.register_callback("tmc","state",self.stateCb,self.axis,str,typechar='?') + self.register_callback("tmc","iScale",self.setCurrentScaler,self.axis,float) + + self.horizontalSlider_speed.valueChanged.connect(self.speedchanged) + self.horizontalSlider_pwm.valueChanged.connect(self.pwmchanged) + self.pushButton_openloop.clicked.connect(lambda : self.set_openloop(True)) + self.checkBox_reverse.stateChanged.connect(lambda : self.speedchanged(self.speed)) self.init_ui() def init_ui(self): - pass + self.send_commands("tmc",["state","iScale"],self.axis) + + def updateTimer(self): + self.send_command("tmc","acttrq",self.axis) + + def updateTimerSlow(self): + self.send_command("tmc","state",self.axis) + + def stateCb(self,state): + intstate = int(state) + if(intstate == 3 or intstate == 2): + self.set_ready(True) + + def hideEvent(self,event): + self.timer.stop() + self.timer_slow.stop() + + def showEvent(self,event): + self.init_ui() + self.timer.start(100) + self.timer_slow.start(1000) + + def set_ready(self,ready): + self.pushButton_openloop.setEnabled(ready) + + def set_openloop(self,enable): + self.openloopenabled = enable + self.groupBox_openlooptest.setEnabled(enable) + self.send_value("main","openloopspeed",instance=self.axis,val=0,adr=0) + self.horizontalSlider_speed.setValue(0) + self.horizontalSlider_pwm.setValue(0) + def setCurrentScaler(self,x): + if x != self.adc_to_amps: + self.adc_to_amps = x + if x != 0: + self.spinBox_current.setSuffix("mA") + else: + self.spinBox_current.setSuffix("/32787") + + + def updateCurrent(self,torqueflux): + tflist = [(int(v)) for v in torqueflux.split(":")] + torque = abs(tflist[0]) + flux = tflist[1] + currents = complex(torque,flux) + self.progressBar_power.setValue(int(abs(currents))) + if self.adc_to_amps != 0: + self.spinBox_current.setValue(int(abs(currents*self.adc_to_amps*1000))) + else: + self.spinBox_current.setValue(int(abs(currents))) + + def speedchanged(self,speed): + self.speed = speed + newspeed = -self.speed if self.checkBox_reverse.isChecked() else self.speed + self.send_value("main","openloopspeed",instance=self.axis,val=newspeed,adr=self.pwm) + + def pwmchanged(self,pwm): + self.pwm = pwm + self.spinBox_pwm.setValue(int(pwm/200)) + self.send_value("main","openloopspeed",instance=self.axis,val=self.speed,adr=self.pwm) \ No newline at end of file From 9fa9ef2d7fb7329dac88cc120b82ba4aa2728c43 Mon Sep 17 00:00:00 2001 From: adilrepas Date: Wed, 31 Jul 2024 19:21:03 +0800 Subject: [PATCH 02/11] fix spinbox fix spinbox on the Effect statsistic. Effect graphics and advanced ffb tuning --- res/effects_graph.ui | 2 +- res/effects_stats.ui | 2 +- res/effects_tuning.ui | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/effects_graph.ui b/res/effects_graph.ui index 0329918e..722ae9e6 100644 --- a/res/effects_graph.ui +++ b/res/effects_graph.ui @@ -422,7 +422,7 @@ - 30 + 100 0 diff --git a/res/effects_stats.ui b/res/effects_stats.ui index 6f0da89b..e6180aba 100644 --- a/res/effects_stats.ui +++ b/res/effects_stats.ui @@ -466,7 +466,7 @@ - 30 + 100 0 diff --git a/res/effects_tuning.ui b/res/effects_tuning.ui index 7e08d874..7d3c0177 100644 --- a/res/effects_tuning.ui +++ b/res/effects_tuning.ui @@ -424,7 +424,7 @@ - 40 + 100 0 From 2f5aaa6515eaa87269cabe7465ce7bddfe0960a7 Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Tue, 24 Sep 2024 10:45:27 +0200 Subject: [PATCH 03/11] Added biss-c dir checkbox. Fixed abn index checkbox --- encoderconf_ui.py | 8 +++++--- main.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/encoderconf_ui.py b/encoderconf_ui.py index 1d408dd7..252f72b5 100644 --- a/encoderconf_ui.py +++ b/encoderconf_ui.py @@ -104,7 +104,7 @@ def onshown(self): def apply(self): val = self.spinBox_cpr.value() self.send_value("localenc","cpr",val=val) - self.send_value("localenc","index",val = 1 if self.checkBox_index.checkState() else 0) + self.send_value("localenc","index",val = 1 if self.checkBox_index.isChecked() else 0) class MtEncoderConf(EncoderOption,CommunicationHandler): @@ -144,20 +144,22 @@ def initUI(self): layout = QFormLayout() layout.setContentsMargins(0,0,0,0) - self.spinBox_cs = QSpinBox() - self.spinBox_cs.setRange(1,3) + self.checkBox_direction = QCheckBox("Reverse direction (default)") self.spinBox_bits = QSpinBox() self.spinBox_bits.setRange(1,32) layout.addWidget(QLabel("SPI3 extension port")) layout.addRow(QLabel("Bits"),self.spinBox_bits) + layout.addWidget(self.checkBox_direction) layout.addWidget(QLabel("Port is used exclusively!")) self.setLayout(layout) def onshown(self): self.get_value_async("bissenc","bits",self.spinBox_bits.setValue,0,int) + self.get_value_async("bissenc","dir",self.checkBox_direction.setChecked,0,int) def apply(self): self.send_value("bissenc","bits",val=self.spinBox_bits.value()) + self.send_value("bissenc","dir",val= 1 if self.checkBox_direction.isChecked() else 0) class SsiEncoderConf(EncoderOption,CommunicationHandler): def __init__(self,parent,main): diff --git a/main.py b/main.py index 8ede0dd8..56e81951 100644 --- a/main.py +++ b/main.py @@ -56,11 +56,11 @@ import activetasks # This GUIs version -VERSION = "1.15.1" +VERSION = "1.15.2" # Minimal supported firmware version. # Major version of firmware must match firmware. Minor versions must be higher or equal -MIN_FW = "1.15.0" +MIN_FW = "1.15.1" DEFAULTLANG = "en_US" From efd796b0c1700f591f372ce330de56513a605b81 Mon Sep 17 00:00:00 2001 From: tinyboxxx Date: Sun, 20 Oct 2024 18:06:42 +0800 Subject: [PATCH 04/11] Fix language initialization in MainUI Implemented a new profile initialization without UI and adjusted the initialization order within MainUI. Known issue: After booting, changing the language from any translation to en_US requires a manual restart of the application to take effect. --- base_ui.py | 1 + main.py | 35 +++++++++++++++++++++++++---------- profile_ui.py | 40 +++++++++++++++++++++++++--------------- updater.py | 2 +- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/base_ui.py b/base_ui.py index 2306050f..e4e96290 100644 --- a/base_ui.py +++ b/base_ui.py @@ -47,6 +47,7 @@ def __init__(self, parent: PyQt6.QtWidgets.QWidget = None, ui_form: str = ""): self.tech_log = logging.getLogger(ui_form) if ui_form: PyQt6.uic.loadUi(helper.res_path(ui_form), self) + # print(f"UI file loaded : {ui_form}") def init_ui(self): """Prototype of init_ui to manage this status in subclass.""" diff --git a/main.py b/main.py index 56e81951..2e879ed3 100644 --- a/main.py +++ b/main.py @@ -73,6 +73,11 @@ def __init__(self): """Init the mainUI : init the UI, all the dlg element, and the main timer.""" PyQt6.QtWidgets.QMainWindow.__init__(self) base_ui.CommunicationHandler.__init__(self) + + self.profile_ui = profile_ui.ProfileUI(main=self) # load profile without UI + self.translator = PyQt6.QtCore.QTranslator(self) # Languages must be created before UI loaded + self.load_language() # load manually + base_ui.WidgetUI.__init__(self, None, "MainWindow.ui") self.restart_app_flag = False @@ -97,9 +102,8 @@ def __init__(self): # Systray self.systray = SystrayWrapper(self) # Profile - self.profile_ui = profile_ui.ProfileUI(main=self) - - self.make_lang_selector() # Languages must be created as early as possible + self.profile_ui.initialize_ui() # Profile UI + self.make_lang_selector() self.timer = PyQt6.QtCore.QTimer(self) self.timer.timeout.connect(self.update_timer) # pylint: disable=no-value-for-parameter @@ -197,13 +201,23 @@ def setup(self): nb_device_compat = self.serialchooser.get_ports() self.serialchooser.auto_connect(nb_device_compat) - # def refresh_widgets(self): - # for w in app.allWidgets(): # Does not actually update the translation - # w.update() - # w.repaint() - # # print(w) + def load_language(self): + """load language file in profile befor creating UI""" + app.removeTranslator(self.translator) + + langid = self.profile_ui.get_global_setting("language",DEFAULTLANG) + # print(f"Loading langid: {langid}") + if langid == DEFAULTLANG: + langfile = '' + else: + langfile = helper.res_path(f"{langid}.qm","translations") + # print(f"Loading language file: {langfile}") + if self.translator.load(langfile): + app.installTranslator(self.translator) + # print(f"Language file loaded: {langfile}") def change_language(self,enabled): + """Change language of the UI, this will run too when initializing the UI""" if(not enabled): return langfile,user_language = self.language_action_group.checkedAction().data() @@ -218,6 +232,7 @@ def change_language(self,enabled): app.removeTranslator(self.translator) if self.translator.load(langfile): app.installTranslator(self.translator) + # print(f"Language file loaded: {langfile}") self.profile_ui.set_global_setting("language",user_language) #store language #self.refresh_widgets() @@ -914,8 +929,8 @@ def process_events(): QueryValueEx as getSubkeyValue, OpenKey as getKey, ) - - if windows_theme_is_light() == 0: + # Check if is not using windows 11 style(windows 11 style is dark mode compatible) + if windows_theme_is_light() == 0 and app.style().objectName() != "windows11": app.setStyle("Fusion") app.setPalette(dark_palette.PALETTE_DARK) window.menubar.setStyleSheet("QMenu::item {color: white; }") # Menu item text ignores palette setting and stays black. Force to white. diff --git a/profile_ui.py b/profile_ui.py index f16cd4f7..a405af48 100644 --- a/profile_ui.py +++ b/profile_ui.py @@ -35,10 +35,30 @@ class ProfileUI(base_ui.WidgetUI, base_ui.CommunicationHandler): profile_selected_event = PyQt6.QtCore.pyqtSignal(str) def __init__(self, main=None): + """Initialize profile without UI.""" + self.main = main + self.profiles_dlg = None # ProfilesDialog is not initialized yet + self.profile_setup = {} + self.profiles = {} + + self._current_class = -1 + self._current_command = -1 + self._current_instance = -1 + self._map_class_running = [] + self._running_profile = [] + self._profilename_tosave: str = None + + self.ui_initialized = False + + # 加载配置文件和资料 + self.load_profile_settings() + self.load_profiles() + + def initialize_ui(self): """Init the UI and link the event.""" - base_ui.WidgetUI.__init__(self, main, "profile.ui") + base_ui.WidgetUI.__init__(self, self.main, "profile.ui") base_ui.CommunicationHandler.__init__(self) - self.main = main + self.profiles_dlg = ProfilesDialog(self) self.profiles_dlg.closeSignal.connect(self.close_profile_manager) @@ -63,19 +83,7 @@ def __init__(self, main=None): self.main.systray.refresh_profile_action_status ) - self.profile_setup = {} - self.profiles = {} - - self._current_class = -1 - self._current_command = -1 - self._current_instance = -1 - self._map_class_running = [] - self._running_profile = [] - self._profilename_tosave: str = None - self.setEnabled(False) - self.load_profile_settings() - self.load_profiles() def save_clicked(self): """Save current seeting in Flash and replace the 'Flash profile' settings by the new one.""" @@ -141,7 +149,9 @@ def load_profiles_from_file(self): self.log("Profile: profiles are not compatible, need to redo them") else: self.log("Profile: profiles loaded") - self.refresh_combox_list() + + if self.ui_initialized: + self.refresh_combox_list() def create_or_update_profile_file(self, create: bool = False): """Create a profile file if not exist, else update the existing one.""" diff --git a/updater.py b/updater.py index 35d4c89c..a8aa8923 100644 --- a/updater.py +++ b/updater.py @@ -48,7 +48,7 @@ def get_latest_release(repo : str): """Returns the latest release for repo or empty dict on error""" url = f"https://api.github.com/repos/{repo}/releases/latest" try: - response = requests.get(url) + response = requests.get(url,timeout=3) # timeout to avoid blocking when board connecting except requests.ConnectionError: return {} if not response: From c8a5d4871a96e7a1806c5600f5fce878076e4230 Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Thu, 24 Oct 2024 12:30:43 +0200 Subject: [PATCH 05/11] Reverted language change order for now --- main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 2e879ed3..004d2f2c 100644 --- a/main.py +++ b/main.py @@ -75,8 +75,8 @@ def __init__(self): base_ui.CommunicationHandler.__init__(self) self.profile_ui = profile_ui.ProfileUI(main=self) # load profile without UI - self.translator = PyQt6.QtCore.QTranslator(self) # Languages must be created before UI loaded - self.load_language() # load manually + #self.translator = PyQt6.QtCore.QTranslator(self) # Languages must be created before UI loaded + #self.load_language() # load manually base_ui.WidgetUI.__init__(self, None, "MainWindow.ui") @@ -95,7 +95,7 @@ def __init__(self): self.lang_actions = {} self.translator = PyQt6.QtCore.QTranslator(self) self.language_action_group = QActionGroup(self) - self.language_action_group.setExclusive(True) + self.language_action_group.setExclusive(True) self.tab_connections = [] # Signals to disconnect on reset @@ -103,7 +103,7 @@ def __init__(self): self.systray = SystrayWrapper(self) # Profile self.profile_ui.initialize_ui() # Profile UI - self.make_lang_selector() + self.make_lang_selector() self.timer = PyQt6.QtCore.QTimer(self) self.timer.timeout.connect(self.update_timer) # pylint: disable=no-value-for-parameter @@ -201,6 +201,7 @@ def setup(self): nb_device_compat = self.serialchooser.get_ports() self.serialchooser.auto_connect(nb_device_compat) + # TODO this function is likely not required. Remove if language changing is fixed def load_language(self): """load language file in profile befor creating UI""" app.removeTranslator(self.translator) @@ -235,7 +236,6 @@ def change_language(self,enabled): # print(f"Language file loaded: {langfile}") self.profile_ui.set_global_setting("language",user_language) #store language - #self.refresh_widgets() self.languagechanged.emit() def restart_app(self): From 45ef9721b99f76649c1d45d9695a0c9ec80b0106 Mon Sep 17 00:00:00 2001 From: tinyboxxx Date: Mon, 28 Oct 2024 01:01:34 +0800 Subject: [PATCH 06/11] Refactor: Optimize Language Loading and Simplify Selection Process --- main.py | 71 +++++++++++++++------------------------------------ profile_ui.py | 1 - 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/main.py b/main.py index 004d2f2c..430940cd 100644 --- a/main.py +++ b/main.py @@ -75,8 +75,7 @@ def __init__(self): base_ui.CommunicationHandler.__init__(self) self.profile_ui = profile_ui.ProfileUI(main=self) # load profile without UI - #self.translator = PyQt6.QtCore.QTranslator(self) # Languages must be created before UI loaded - #self.load_language() # load manually + self.load_language_id(self.profile_ui.get_global_setting("language",DEFAULTLANG)) # load language file base_ui.WidgetUI.__init__(self, None, "MainWindow.ui") @@ -201,42 +200,24 @@ def setup(self): nb_device_compat = self.serialchooser.get_ports() self.serialchooser.auto_connect(nb_device_compat) - # TODO this function is likely not required. Remove if language changing is fixed - def load_language(self): - """load language file in profile befor creating UI""" - app.removeTranslator(self.translator) - - langid = self.profile_ui.get_global_setting("language",DEFAULTLANG) - # print(f"Loading langid: {langid}") - if langid == DEFAULTLANG: - langfile = '' - else: + def load_language_id(self, langid:str): + """load language file""" + if langid != DEFAULTLANG: langfile = helper.res_path(f"{langid}.qm","translations") - # print(f"Loading language file: {langfile}") - if self.translator.load(langfile): - app.installTranslator(self.translator) - # print(f"Language file loaded: {langfile}") + if translator.load(langfile): + app.installTranslator(translator) - def change_language(self,enabled): + def change_lang_callback(self, enabled:bool): """Change language of the UI, this will run too when initializing the UI""" - if(not enabled): - return - langfile,user_language = self.language_action_group.checkedAction().data() - - # try to load the user's language file - curlang = self.translator.language() - if self.translator.isEmpty(): - curlang = DEFAULTLANG - if(curlang == user_language): + if(not enabled): # Language not selected return - - app.removeTranslator(self.translator) - if self.translator.load(langfile): - app.installTranslator(self.translator) - # print(f"Language file loaded: {langfile}") - self.profile_ui.set_global_setting("language",user_language) #store language + + app.removeTranslator(translator) + + user_lang_id = self.language_action_group.checkedAction().data() + self.profile_ui.set_global_setting("language",user_lang_id) # store language setting - self.languagechanged.emit() + self.languagechanged.emit() # loading in next start def restart_app(self): self.restart_app_flag = True @@ -245,30 +226,19 @@ def restart_app(self): app.quit() def make_lang_selector(self): + '''Create the language selector menu, and connect the callback to change language.''' + languages = [DEFAULTLANG] + languages.extend([os.path.splitext(os.path.basename(f))[0] for f in glob.glob(helper.res_path("*.qm","translations"))]) - languages = [("",DEFAULTLANG)] - languages.extend([(f,os.path.splitext(os.path.basename(f))[0]) for f in glob.glob(helper.res_path("*.qm","translations"))]) - - for langfile,langid in languages: + for langid in languages: action = QAction(langid) - action.setData([langfile,langid]) + action.setData(langid) action.setCheckable(True) self.language_action_group.addAction(action) self.lang_actions[langid] = action self.menuLanguage.addAction(action) - action.toggled.connect(self.change_language) - - user_language = self.profile_ui.get_global_setting("language",None) - - if not user_language: - user_language = PyQt6.QtCore.QLocale.system().name() + action.toggled.connect(self.change_lang_callback) - if user_language in self.lang_actions: - self.lang_actions.get(user_language).setChecked(True) - else: - self.lang_actions.get(DEFAULTLANG).setChecked(True) - - def reboot(self): """Send the reboot message to the board.""" @@ -919,6 +889,7 @@ def process_events(): app = PyQt6.QtWidgets.QApplication(sys.argv) while(restart): restart = False + translator = PyQt6.QtCore.QTranslator() # Languages must be created before UI loaded window = MainUi() if (sys.platform == "win32" or "Windows" in sys.platform): # only on windows, for macOS and linux use system palette. diff --git a/profile_ui.py b/profile_ui.py index a405af48..50287ac6 100644 --- a/profile_ui.py +++ b/profile_ui.py @@ -50,7 +50,6 @@ def __init__(self, main=None): self.ui_initialized = False - # 加载配置文件和资料 self.load_profile_settings() self.load_profiles() From e75e573092a104c3404f05f079082e71e54261d1 Mon Sep 17 00:00:00 2001 From: Yannick Richter Date: Mon, 28 Oct 2024 11:34:37 +0100 Subject: [PATCH 07/11] Removing duplicate translator and keeping one translator instance --- main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.py b/main.py index 430940cd..b1b3c18b 100644 --- a/main.py +++ b/main.py @@ -92,7 +92,6 @@ def __init__(self): self.systray : SystrayWrapper = None self.lang_actions = {} - self.translator = PyQt6.QtCore.QTranslator(self) self.language_action_group = QActionGroup(self) self.language_action_group.setExclusive(True) @@ -887,9 +886,9 @@ def process_events(): restart = True exit_code = -1 app = PyQt6.QtWidgets.QApplication(sys.argv) + translator = PyQt6.QtCore.QTranslator(app) # Translator must be created before UI loaded while(restart): restart = False - translator = PyQt6.QtCore.QTranslator() # Languages must be created before UI loaded window = MainUi() if (sys.platform == "win32" or "Windows" in sys.platform): # only on windows, for macOS and linux use system palette. From a123614da20ed31327a67463e052671a325f3073 Mon Sep 17 00:00:00 2001 From: Vincent Manoukian <10980775+manoukianv@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:40:30 +0100 Subject: [PATCH 08/11] Fix the autoconnection error, and strange deco during connection process. --- main.py | 23 ++++++++++++++--------- serial_comms.py | 7 ++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index b1b3c18b..ed012263 100644 --- a/main.py +++ b/main.py @@ -116,13 +116,16 @@ def __init__(self): self.active_classes = {} self.fw_version_str = None - self.setup() self.process_events_timer = PyQt6.QtCore.QTimer() self.process_events_timer.timeout.connect(process_events) # Kick eventloop when timeouting self.axes = 0 + self.setup() self.languagechanged.connect(self.restart_app) + + # start the auto disconnect timer (call the board) + self.timer.start(5000) def setup(self): """Init the systray, the serial, the toolbar, the status bar and the connection status.""" @@ -149,9 +152,6 @@ def setup(self): self.actionDebug_mode.triggered.connect(self.toggle_debug) - self.timer.start(5000) - - #self.serialchooser.connected.connect(self.effects_monitor_dlg.setEnabled) # Gets enabled in class management self.effects_monitor_dlg.setEnabled(False) @@ -194,7 +194,9 @@ def setup(self): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.profile_ui) self.groupBox_main.setLayout(layout) - + + + def autoconnect(self) : # after UI load get serial port and if only one : autoconnect nb_device_compat = self.serialchooser.get_ports() self.serialchooser.auto_connect(nb_device_compat) @@ -612,8 +614,7 @@ def version_check(self, ver): def serial_connected(self, connected): """Check the release when a board is connected.""" - self.serial_timer = PyQt6.QtCore.QTimer() - + def timer_cb(): if not self.connected: self.log("Can't detect board") @@ -621,16 +622,19 @@ def timer_cb(): def id_cb(identifier): if identifier: - self.connected = True self.serial_timer.stop() + self.connected = True if connected: - self.serial_timer.singleShot(500, timer_cb) self.get_value_async("main", "id", id_cb, 0) self.errors_dlg.registerCallbacks() self.get_value_async("sys", "swver", self.version_check) self.get_value_async("sys", "hwtype", self.wrapper_status_bar.set_board_text) self.get_value_async("sys", "debug", self.actionDebug_mode.setChecked,0,int) + + if (self.serial_timer is None) : + self.serial_timer = PyQt6.QtCore.QTimer(singleShot=True, timeout=timer_cb) + self.serial_timer.start(500) else: self.connected = False @@ -909,6 +913,7 @@ def process_events(): window.setWindowIcon(PyQt6.QtGui.QIcon(helper.res_path('app.ico'))) window.show() window.check_configurator_update() # Check for updates after window is shown + window.autoconnect() exit_code = app.exec() # Check if we need to restart diff --git a/serial_comms.py b/serial_comms.py index 39185ba1..f2958c25 100644 --- a/serial_comms.py +++ b/serial_comms.py @@ -214,7 +214,12 @@ def processMatchedReply(self,match): if callbackObject["delete"]: # delete if flag is set #print("Deleting",callbackObject) - SerialComms.callbackDict[cls].remove(callbackObject) + if (SerialComms.callbackDict[cls] is not None) \ + and (callbackObject in SerialComms.callbackDict[cls]) : + SerialComms.callbackDict[cls].remove(callbackObject) + else : + #self.logger.error(f"Not found callback {callbackObject} for {cls}") + pass deleted = True return deleted From e9fc90ca16d26f0f566145433d5b46bb277b0434 Mon Sep 17 00:00:00 2001 From: Vincent Manoukian <10980775+manoukianv@users.noreply.github.com> Date: Tue, 29 Oct 2024 20:32:40 +0100 Subject: [PATCH 09/11] Fix the loading process, :stop sending the receiving data on sliders.Clean code to reuse helper. --- ffb_ui.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ffb_ui.py b/ffb_ui.py index 7b3c1dab..ed5f449c 100644 --- a/ffb_ui.py +++ b/ffb_ui.py @@ -3,7 +3,7 @@ from PyQt6.QtWidgets import QWidget,QToolButton from PyQt6.QtWidgets import QMessageBox,QVBoxLayout,QCheckBox,QButtonGroup,QGridLayout,QSpinBox from PyQt6 import uic -from helper import res_path,classlistToIds,splitListReply,throttle +from helper import res_path,classlistToIds,splitListReply,throttle,qtBlockAndCall from PyQt6.QtCore import QTimer,QEvent, pyqtSignal import main import buttonconf_ui @@ -177,9 +177,7 @@ def updateTimer(self): def sliderChangedUpdateSpinbox(self,val,spinbox,factor,command=None): newVal = val * factor if(spinbox.value != newVal): - spinbox.blockSignals(True) - spinbox.setValue(newVal) - spinbox.blockSignals(False) + qtBlockAndCall(spinbox, spinbox.setValue,newVal) if(command): self.send_value("fx",command,val) @@ -204,11 +202,10 @@ def display_accel_cutoff_inertia(self, gain): max_accel = 32767 / inertia_accel self.label_accel.setText(f"{max_accel:.0f}") - def updateSpinboxAndSlider(self,val,spinbox : QSlider,slider,factor): - slider.setValue(val) + def updateSpinboxAndSlider(self,val,spinbox,slider : QSlider,factor): + qtBlockAndCall(slider, slider.setValue, val) self.sliderChangedUpdateSpinbox(val,spinbox,factor) - def hidreportrate_cb(self,modes): self.comboBox_reportrate.blockSignals(True) self.comboBox_reportrate.clear() From b092849b428d111cc8c4f58c5cfdd221eb813175 Mon Sep 17 00:00:00 2001 From: Vincent Manoukian <10980775+manoukianv@users.noreply.github.com> Date: Tue, 29 Oct 2024 20:32:40 +0100 Subject: [PATCH 10/11] Adding wheel page if the mainclass ID is 1. Add menu item to enabled summary --- ffb_ui.py | 29 +- main.py | 77 ++++- profile_ui.py | 15 +- res/MainWindow.ui | 17 +- res/wheel.jpg | Bin 0 -> 49335 bytes res/wheel.ui | 855 ++++++++++++++++++++++++++++++++++++++++++++++ updater.py | 8 +- wheel.py | 274 +++++++++++++++ 8 files changed, 1231 insertions(+), 44 deletions(-) create mode 100644 res/wheel.jpg create mode 100644 res/wheel.ui create mode 100644 wheel.py diff --git a/ffb_ui.py b/ffb_ui.py index ed5f449c..de2e8ca1 100644 --- a/ffb_ui.py +++ b/ffb_ui.py @@ -130,9 +130,7 @@ def init_ui(self): self.send_command("main","lsain",0,'?') # get analog types self.send_command("main","aintypes",0,'?') # get active analog - - - self.updateSliders() + self.send_command("main","hidsendspd",0,'!') # get speed except: @@ -141,12 +139,12 @@ def init_ui(self): return True # Tab is currently shown - # def showEvent(self,event): - # self.timer.start(500) + def showEvent(self,event): + self.updateSliders() - # # Tab is hidden - # def hideEvent(self,event): - # self.timer.stop() + # Tab is hidden + def hideEvent(self,event): + pass def startTimer(self): self.timer.start(500) @@ -180,6 +178,14 @@ def sliderChangedUpdateSpinbox(self,val,spinbox,factor,command=None): qtBlockAndCall(spinbox, spinbox.setValue,newVal) if(command): self.send_value("fx",command,val) + + def refresh_limit(self, slider : QSlider) : + if slider == self.horizontalSlider_damper : + self.display_speed_cutoff_damper(slider.value()) + elif slider == self.horizontalSlider_friction : + self.display_speed_cutoff_friction(slider.value()) + elif slider == self.horizontalSlider_inertia : + self.display_accel_cutoff_inertia(slider.value()) def display_speed_cutoff_damper(self, gain): """Update the max rpm speed cutoff""" @@ -205,6 +211,7 @@ def display_accel_cutoff_inertia(self, gain): def updateSpinboxAndSlider(self,val,spinbox,slider : QSlider,factor): qtBlockAndCall(slider, slider.setValue, val) self.sliderChangedUpdateSpinbox(val,spinbox,factor) + self.refresh_limit(slider) def hidreportrate_cb(self,modes): self.comboBox_reportrate.blockSignals(True) @@ -355,12 +362,6 @@ def cffilter_changed(self,v,send=True): self.doubleSpinBox_CFq.setEnabled(qOn) self.label_cffilter.setText(lbl) - def extract_scaler(self, gain_default, repl) : - infos = {key:value for (key,value) in [entry.split(":") for entry in repl.split(",")]} - if "scale" in infos: - gain_default = float(infos["scale"]) if float(infos["scale"]) > 0 else gain_default - return gain_default - def updateGainScaler(self,slider : QSlider,spinbox : QSpinBox, gain): spinbox.setMaximum(gain) self.sliderChangedUpdateSpinbox(slider.value(),spinbox,gain) diff --git a/main.py b/main.py index ed012263..42437cf0 100644 --- a/main.py +++ b/main.py @@ -54,6 +54,7 @@ import updater import simplemotion_ui import activetasks +import wheel # This GUIs version VERSION = "1.15.2" @@ -75,10 +76,9 @@ def __init__(self): base_ui.CommunicationHandler.__init__(self) self.profile_ui = profile_ui.ProfileUI(main=self) # load profile without UI - self.load_language_id(self.profile_ui.get_global_setting("language",DEFAULTLANG)) # load language file + self.load_language_id(self.profile_ui.get_global_setting(profile_ui.ConfigKey.language,DEFAULTLANG)) # load language file base_ui.WidgetUI.__init__(self, None, "MainWindow.ui") - self.restart_app_flag = False self.serial = PyQt6.QtSerialPort.QSerialPort() @@ -88,6 +88,7 @@ def __init__(self): self.connected = False self.not_minimize_and_close = True self.serial_timer = None + self.summary_tab = True self.systray : SystrayWrapper = None @@ -99,9 +100,12 @@ def __init__(self): # Systray self.systray = SystrayWrapper(self) + # Profile + self.summary_tab = self.profile_ui.get_global_setting(profile_ui.ConfigKey.display_summary_tabs, True) self.profile_ui.initialize_ui() # Profile UI self.make_lang_selector() + self.setup_summary_tab() self.timer = PyQt6.QtCore.QTimer(self) self.timer.timeout.connect(self.update_timer) # pylint: disable=no-value-for-parameter @@ -149,8 +153,8 @@ def setup(self): self.serialchooser.connected.connect(self.serial_connected) self.actionUpdates.triggered.connect(self.open_updater) - self.actionDebug_mode.triggered.connect(self.toggle_debug) + self.actionDisplayed_summary.triggered.connect(self.toggle_summary) #self.serialchooser.connected.connect(self.effects_monitor_dlg.setEnabled) # Gets enabled in class management self.effects_monitor_dlg.setEnabled(False) @@ -168,6 +172,7 @@ def setup(self): ) # Open active classes list self.serialchooser.connected.connect(self.actionActive_features.setEnabled) self.serialchooser.connected.connect(self.actionDebug_mode.setEnabled) + self.serialchooser.connected.connect(self.actionDisplayed_summary.setEnabled) self.actionActive_threads.triggered.connect(self.active_threads_dlg.show) @@ -216,16 +221,10 @@ def change_lang_callback(self, enabled:bool): app.removeTranslator(translator) user_lang_id = self.language_action_group.checkedAction().data() - self.profile_ui.set_global_setting("language",user_lang_id) # store language setting + self.profile_ui.set_global_setting(profile_ui.ConfigKey.language,user_lang_id) # store language setting self.languagechanged.emit() # loading in next start - def restart_app(self): - self.restart_app_flag = True - self.reset_port() - base_ui.CommunicationHandler.comms.removeAllCallbacks() - app.quit() - def make_lang_selector(self): '''Create the language selector menu, and connect the callback to change language.''' languages = [DEFAULTLANG] @@ -239,6 +238,13 @@ def make_lang_selector(self): self.lang_actions[langid] = action self.menuLanguage.addAction(action) action.toggled.connect(self.change_lang_callback) + + def restart_app(self): + self.restart_app_flag = True + self.reset_port() + base_ui.CommunicationHandler.comms.removeAllCallbacks() + app.quit() + def reboot(self): @@ -248,7 +254,7 @@ def reboot(self): def check_configurator_update(self): """Checks if there is an update for the configurator only""" - donotnotify = self.profile_ui.get_global_setting("donotnotify_updates",False) + donotnotify = self.profile_ui.get_global_setting(profile_ui.ConfigKey.donotnotify_updates, False) if donotnotify: return @@ -259,7 +265,7 @@ def check_configurator_update(self): if updater.UpdateChecker.compare_versions(VERSION,releaseversion): # New release available for firmware msg = "New configurator update available.
Warning: Check if compatible with firmware.
Install firmware from main repo" - notification = updater.UpdateNotification(release,self,msg,VERSION,donotnotifysetting="donotnotify_updates") + notification = updater.UpdateNotification(release,self,msg,VERSION,donotnotifysetting=profile_ui.ConfigKey.donotnotify_updates) notification.exec() def open_dfu_dialog(self): @@ -285,6 +291,9 @@ def moveEvent(self, event: PyQt6.QtGui.QMoveEvent): #pylint: disable=invalid-nam if dialog and dialog.isVisible(): dialog.move(dialog.pos() + diff) dialog.update() + + def setup_summary_tab(self) : + self.actionDisplayed_summary.setChecked(self.summary_tab) def open_logs_errors_dialog(self): """Display the log file on the right if it fill in the screen.""" @@ -313,6 +322,10 @@ def toggle_debug(self,enabled): # Reload mainclasses self.serialchooser.get_main_classes() # TODO better move somewhere else + def toggle_summary(self,enabled): + self.summary_tab = enabled + self.profile_ui.set_global_setting(profile_ui.ConfigKey.display_summary_tabs,enabled) # store language setting + self.serialchooser.get_main_classes() # TODO better move somewhere else def save_flashdump_to_file(self): """Send a async message to get the flashdump from board.""" @@ -418,6 +431,27 @@ def reset_tabs(self): def update_tabs(self): """Get the active classes from the board, and add tab when not exist.""" + def update_classes_with_plugin(actual_new_active_classes : dict) -> dict: + #if plugin/summary class are allowed in the help menu, we add new ui class + if (self.summary_tab) : + summary_class = {} + for name, classe_active in actual_new_active_classes.items() : + if (classe_active["id"] == 1) and self.summary_tab : + wheel_summary_class_key = "wheelsummary:0" + wheel_summary_class_value = { + "name": "Wheel Summary", + "clsname": "ui_wheelsummary", + "id": 0xFFFF01, + "unique": 0, + "ui": None, + "cmdaddr": [4], + } + summary_class[wheel_summary_class_key] = wheel_summary_class_value + for elt, class_elt in summary_class.items() : + actual_new_active_classes[elt] = class_elt + return actual_new_active_classes + + def update_tabs_cb(active): """Process the name received by callback : split string and add tabs.""" lines = [l.split(":") for l in active.split("\n") if l] @@ -434,6 +468,8 @@ def update_tabs_cb(active): } for i in lines } + new_active_classes = update_classes_with_plugin(new_active_classes) + delete_classes = [ (classe, name) for name, classe in self.active_classes.items() @@ -444,11 +480,12 @@ def update_tabs_cb(active): for classe, name in delete_classes: self.del_tab(classe) del self.active_classes[name] + for name, classe_active in new_active_classes.items(): if name in self.active_classes: continue - classname = classe_active["name"] - if classe_active["id"] == 1 or classe_active["id"] == 2 or classe_active["id"] == 3: + if classe_active["id"] == 1 or classe_active["id"] == 2 or classe_active["id"] == 3: + classname = classe_active["name"] self.main_class_ui = ffb_ui.FfbUI(main=self, title=classname) self.active_classes[name] = self.main_class_ui self.profile_ui.set_save_btn(True) @@ -513,8 +550,14 @@ def update_tabs_cb(active): self.effects_graph_dlg.setEnabled(True) self.actionEffectsMonitor.setEnabled(True) self.actionEffects_forces.setEnabled(True) - - + + #Logical tab, for summary for exemple + elif classe_active["id"] == 0xFFFF01: + """If FFB is select, there is 1 FFB Class, and one Axis, display the wheel tab only in the case where id=1 (1 axis)""" + classe = wheel.WheelUI (main=self) + self.active_classes[name] = classe + position = self.tabWidget_main.insertTab(1, classe, "SimWheel Summary") + self.select_tab(position) self.tabsinitialized.emit(True) @@ -603,7 +646,7 @@ def version_check(self, ver): mainreporelease = updater.GithubRelease.get_latest_release(updater.MAINREPO) releaseversion,_ = updater.GithubRelease.get_version(mainreporelease) if updater.UpdateChecker.compare_versions(self.fw_version_str,releaseversion): - donotnotify = self.profile_ui.get_global_setting("donotnotify_updates",False) + donotnotify = self.profile_ui.get_global_setting(profile_ui.ConfigKey.donotnotify_updates, False) if not donotnotify: # New release available for firmware msg = self.tr( "New firmware available") diff --git a/profile_ui.py b/profile_ui.py index 50287ac6..f01afc53 100644 --- a/profile_ui.py +++ b/profile_ui.py @@ -9,6 +9,7 @@ import os import json import copy +from enum import Enum import PyQt6.QtCore import PyQt6.QtWidgets @@ -16,6 +17,10 @@ import base_ui import helper +class ConfigKey(Enum): + donotnotify_updates = "donotnotify_updates" + language = "language" + display_summary_tabs = "display_summary" class ProfileUI(base_ui.WidgetUI, base_ui.CommunicationHandler): """Manage the Profile selector and the board communication about them.""" @@ -168,17 +173,17 @@ def create_or_update_profile_file(self, create: bool = False): return True - def get_global_setting(self,key : str, default = None): + def get_global_setting(self,key : ConfigKey, default = None): """Returns an entry of the global section or saves a default if set and not found""" if "global" in self.profiles: - if (key not in self.profiles['global']) and default != None: + if (key.name not in self.profiles['global']) and default != None: self.set_global_setting(key,default) - return self.profiles['global'].get(key,None) + return self.profiles['global'].get(key.name,None) return None - def set_global_setting(self,key : str, entry, save : bool = True): + def set_global_setting(self,key : ConfigKey, entry, save : bool = True): """Adds an item to the global section of the profile file. Set save true to write file immediately""" - self.profiles['global'][key] = entry + self.profiles['global'][key.name] = entry if save: self.create_or_update_profile_file() diff --git a/res/MainWindow.ui b/res/MainWindow.ui index 71a55a52..173adeb4 100644 --- a/res/MainWindow.ui +++ b/res/MainWindow.ui @@ -6,8 +6,8 @@ 0 0 - 644 - 489 + 932 + 742 @@ -63,7 +63,7 @@ 0 0 - 644 + 932 21 @@ -77,6 +77,7 @@
+ @@ -99,7 +100,7 @@ - Monitoring + Effects @@ -239,6 +240,14 @@ Active threads + + + true + + + Displayed Summary tabs + + diff --git a/res/wheel.jpg b/res/wheel.jpg new file mode 100644 index 0000000000000000000000000000000000000000..14367022cfa29994ef21424015890651eb1a0187 GIT binary patch literal 49335 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 He*64Czmv8& literal 0 HcmV?d00001 diff --git a/res/wheel.ui b/res/wheel.ui new file mode 100644 index 00000000..cc433938 --- /dev/null +++ b/res/wheel.ui @@ -0,0 +1,855 @@ + + + Form + + + + 0 + 0 + 996 + 1013 + + + + Form + + + + + + + 0 + 1 + + + + Gamepad + + + + + + + 0 + 0 + + + + + 120 + 120 + + + + QFrame::Shape::NoFrame + + + + + + wheel.jpg + + + true + + + Qt::AlignmentFlag::AlignCenter + + + 5 + + + + + + + 0 + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + Pos + + + + + + + Center wheel + + + + + + + true + + + + 0 + 0 + + + + + 90 + 16777215 + + + + false + + + true + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + false + + + + + + ° + + + -9223372036854775808.000000000000000 + + + 9223372036854775808.000000000000000 + + + 0.000000000000000 + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + FFB Settings + + + + + + <html><head/><body><p>Intensity of inertia effect</p></body></html> + + + 255 + + + 4 + + + 0 + + + 0 + + + Qt::Orientation::Horizontal + + + + + + + <html><head/><body><p>Intensity of friction effect</p></body></html> + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + Friction Gain + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + <html><head/><body><p>Intensity of spring effects</p></body></html> + + + 255 + + + 4 + + + 0 + + + 0 + + + Qt::Orientation::Horizontal + + + + + + + rpm + + + + + + + <html><head/><body><p>Intensity of inertia effects</p></body></html> + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + Inertia Gain + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + Max effect at + + + + + + + <html><head/><body><p>Centering force when FFB is inactive</p></body></html> + + + 255 + + + 4 + + + 0 + + + 0 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + false + + + 2 + + + 4.000000000000000 + + + 0.010000000000000 + + + + + + + + 100 + 0 + + + + false + + + 2.000000000000000 + + + 0.010000000000000 + + + + + + + 0 + + + + + + + <html><head/><body><p>Intensity of spring effects</p></body></html> + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + Spring Gain + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 100 + 0 + + + + false + + + 2.000000000000000 + + + 0.010000000000000 + + + + + + + Qt::Orientation::Vertical + + + + + + + rpm + + + + + + + <html><head/><body><p>Intensity of damper effect to smooth out acceleration spikes.</p></body></html> + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + Damper Gain + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + <html><head/><body><p>Lowpass filter for constant force effect.</p><p>Applies only to the CF effect (usually covers all physics based forces for most games) and interpolated between updates.</p><p>Use this filter to tune between sharp and smooth forces and to reduce noise. <br/>Frequency should be lower than games FFB update speed.</p></body></html> + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + 1 + + + 1 + + + <html><head/><body><p>Constant Force<br/>smoothing<br/><span style=" font-size:7pt; font-style:italic;">(main effect in simu)</span></p></body></html> + + + true + + + Qt::AlignmentFlag::AlignCenter + + + + + + + 0 + + + + + + + Advanced Tuning + + + + + + + <html><head/><body><p>Intensity of friction effects</p></body></html> + + + 0 + + + 255 + + + 4 + + + 0 + + + 0 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + false + + + 2.000000000000000 + + + 0.010000000000000 + + + + + + + °/s2 + + + + + + + 0 + + + + + + + 0 + + + + + + + <html><head/><body><p>Lowpass filter frequency for constant force effect.</p><p>Applies only to the CF effect (usually covers all physics based forces for most games) and interpolated between updates.</p><p>Use this filter to tune between sharp and smooth forces and to reduce noise. <br/>Should be lower than games FFB update speed.</p></body></html> + + + 10 + + + 500 + + + 4 + + + 10 + + + 10 + + + Qt::Orientation::Horizontal + + + + + + + <html><head/><body><p>Lowpass filter frequency for constant force effect.</p><p>Applies only to the CF effect (usually covers all physics based forces for most games) and interpolated between updates.</p><p>Use this filter to tune between sharp and smooth forces and to reduce noise. <br/>Should be lower than games FFB update speed.</p></body></html> + + + Freq. + + + + + + + + + + Mechanical settings (Game independent) + + + + + + <html><head/><body><p>Centering force when FFB is inactive.</p><p>Turned off when game effects takes place</p></body></html> + + + Desktop spring (No FFB) + + + + + + + <html><head/><body><p>Centering force when FFB is inactive</p></body></html> + + + 255 + + + 0 + + + 0 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + false + + + + + + + + + 255 + + + + + + + <html><head/><body><p>Independent damper is an always on damper effect to smooth out acceleration spikes.</p></body></html> + + + Permanent damper + + + + + + + <html><head/><body><p>Independent damper is an always on damper effect to smooth out acceleration spikes</p></body></html> + + + 255 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + false + + + 255 + + + + + + + Permanent inertia + + + + + + + 255 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + 255 + + + + + + + Permanent friction + + + + + + + 255 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + 255 + + + + + + + + + + + 0 + 0 + + + + General FFB + + + + + + <html><head/><body><p>Degrees of rotation</p></body></html> + + + Range (deg) + + + + + + + 10 + + + 1440 + + + 10 + + + 10 + + + Qt::Orientation::Horizontal + + + + + + + + 0 + 0 + + + + 0 + + + + + + + <html><head/><body><p>Intensity of the effects used by the force feedback effects from the game.</p><p>It defines the peak torque of the motor.</p></body></html> + + + Effects intensity + + + + + + + <html><head/><body><p>High = Effects and endstop have the same peak torque. Low = effect peak is reduced to make endstop more prominent</p></body></html> + + + 102 + + + 255 + + + 102 + + + 102 + + + Qt::Orientation::Horizontal + + + + + + + + 100 + 0 + + + + false + + + ° + + + 10 + + + 32767 + + + 10 + + + + + + + + + + + diff --git a/updater.py b/updater.py index a8aa8923..999acfc7 100644 --- a/updater.py +++ b/updater.py @@ -16,7 +16,7 @@ from PyQt6.QtCore import Qt from datetime import datetime from optionsdialog import OptionsDialogGroupBox -from profile_ui import ProfileUI +from profile_ui import ProfileUI, ConfigKey MAINREPO = "Ultrawipf/OpenFFBoard" GUIREPO = "Ultrawipf/OpenFFBoard-Configurator" @@ -124,13 +124,13 @@ def __init__(self,parentWidget,settingsmanager : ProfileUI = None): self.settingsmanager = settingsmanager if settingsmanager: - self.checkBox_notify.setChecked(not self.settingsmanager.get_global_setting("donotnotify_updates",False)) + self.checkBox_notify.setChecked(not self.settingsmanager.get_global_setting(ConfigKey.donotnotify_updates,False)) self.checkBox_notify.toggled.connect(self.notify_checkbox_toggled) else: self.checkBox_notify.setVisible(False) def notify_checkbox_toggled(self,status): - self.settingsmanager.set_global_setting("donotnotify_updates",not status) + self.settingsmanager.set_global_setting(ConfigKey.donotnotify_updates,not status) def fill_releases(self,releases : dict): @@ -199,7 +199,7 @@ def file_changed(self,current : QListWidgetItem,old : QListWidgetItem): class UpdateNotification(QDialog): """Shows a dialog with release information""" - def __init__(self,release,main,desc,curver,donotnotifysetting = None): + def __init__(self,release,main,desc,curver,donotnotifysetting : ConfigKey = None): self.main = main QDialog.__init__(self, main) self.setWindowTitle("Update available") diff --git a/wheel.py b/wheel.py new file mode 100644 index 00000000..6291454f --- /dev/null +++ b/wheel.py @@ -0,0 +1,274 @@ +from PyQt6.QtWidgets import QMainWindow +from PyQt6.QtWidgets import QDialog +from PyQt6.QtWidgets import QWidget,QToolButton +from PyQt6.QtWidgets import QMessageBox,QVBoxLayout +from PyQt6.QtWidgets import QCheckBox,QButtonGroup,QGridLayout, QSpinBox, QSlider, QLabel +from PyQt6 import uic +from helper import res_path,classlistToIds,updateClassComboBox,qtBlockAndCall,throttle, map_infostring +from PyQt6.QtCore import QTimer,QEvent +from base_ui import WidgetUI,CommunicationHandler +import main +import effects_tuning_ui + +class WheelUI(WidgetUI,CommunicationHandler): + + def __init__(self, main: 'main.MainUi'=None): + WidgetUI.__init__(self, main, 'wheel.ui') + CommunicationHandler.__init__(self) + + self.main = main + + self.cpr = -1 + self.springgain = 4 + self.dampergain = 2 + self.inertiagain = 2 + self.frictiongain = 2 + self.damper_internal_scale = 1 + self.inertia_internal_scale = 1 + self.friction_internal_scale = 1 + self.damper_internal_factor = 1 + self.inertia_internal_factor = 1 + self.friction_internal_factor = 1 + self.friction_pct_speed_rampup = 25 + + self.timer = QTimer(self) + self.timer.timeout.connect(self.timer_cb) + + self.effect_tuning_dlg = effects_tuning_ui.AdvancedFFBTuneDialog(self) + self.main.maxaxischanged.connect(self.effect_tuning_dlg.set_max_axes) + + ### Event management with board message + # General FFB Section + self.horizontalSlider_fxratio.valueChanged.connect(lambda val : self.sliderChanged_UpdateLabel(val,self.label_fxratio,"{:2.2f}%",1/255,"axis","fxratio")) + + self.horizontalSlider_degrees.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val,self.spinBox_range,1,"axis","degrees")) + self.spinBox_range.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val,self.spinBox_range,1)) + + # Mechanical section + self.horizontalSlider_idle.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val, self.spinBox_idlespring,1,"axis","idlespring")) + self.spinBox_idlespring.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val,self.horizontalSlider_idle,1,)) + + self.horizontalSlider_perma_damper.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val, self.spinBox_perma_damper,1,"axis","axisdamper")) + self.spinBox_perma_damper.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val,self.horizontalSlider_perma_damper,1)) + + self.horizontalSlider_perma_inertia.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val, self.spinBox_perma_inertia,1,"axis","axisinertia")) + self.spinBox_perma_inertia.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val,self.horizontalSlider_perma_inertia,1)) + + self.horizontalSlider_perma_friction.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val, self.spinBox_perma_friction,1,"axis","axisfriction")) + self.spinBox_perma_friction.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val,self.horizontalSlider_perma_friction,1)) + + # FFB Settings + self.horizontalSlider_cffilter.valueChanged.connect(lambda val : self.cffilter_changed(val, send=True)) + + self.horizontalSlider_spring.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val,self.doubleSpinBox_spring,self.springgain/256,"fx", "spring")) + self.doubleSpinBox_spring.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val,self.horizontalSlider_spring, 256/self.springgain)) + + self.horizontalSlider_damper.valueChanged.connect(self.display_speed_cutoff_damper) + self.horizontalSlider_damper.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val,self.doubleSpinBox_damper,self.dampergain/256,"fx", "damper")) + self.doubleSpinBox_damper.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val, self.horizontalSlider_damper, 256/self.dampergain)) + + self.horizontalSlider_friction.valueChanged.connect(self.display_speed_cutoff_friction) + self.horizontalSlider_friction.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val,self.doubleSpinBox_friction,self.frictiongain/256,"fx", "friction")) + self.doubleSpinBox_friction.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val, self.horizontalSlider_friction, 256/self.frictiongain)) + + self.horizontalSlider_inertia.valueChanged.connect(self.display_accel_cutoff_inertia) + self.horizontalSlider_inertia.valueChanged.connect(lambda val : self.sliderChanged_UpdateSpinbox(val,self.doubleSpinBox_inertia,self.inertiagain/256,"fx", "inertia")) + self.doubleSpinBox_inertia.valueChanged.connect(lambda val : self.spinboxChanged_UpdateSlider(val, self.horizontalSlider_inertia, 256/self.inertiagain)) + + ### Event manager for UI button + self.pushButton_advanced_tuning.clicked.connect(self.effect_tuning_dlg.display) + self.pushButton_pushcenter.clicked.connect(lambda : self.send_command("axis","zeroenc",0)) + + + + ### Register event to received data from board + # Callback used for axis incoming data + self.register_callback("axis","fxratio",lambda val : self.dataChanged_UpdateSliderAndLabel(val,self.horizontalSlider_fxratio, self.label_fxratio, "{:2.2%}", 1/255),0,int) + self.register_callback("axis","degrees",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val,self.horizontalSlider_degrees,self.spinBox_range,1), 0, int) + + self.register_callback("axis","idlespring",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_idle, self.spinBox_idlespring, 1), 0, int) + self.register_callback("axis","axisdamper",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_perma_damper, self.spinBox_perma_damper, 1), 0, int) + self.register_callback("axis","axisinertia",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_perma_inertia, self.spinBox_perma_inertia, 1), 0, int) + self.register_callback("axis","axisfriction",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_perma_friction, self.spinBox_perma_friction, 1), 0, int) + + # Callback used for FFB incoming data + # This callback are used to adapt the min/max/scale value for the UI (depending on firmware internal setup) + self.register_callback("fx","frictionPctSpeedToRampup", self.set_friction_pct_speed_rampup_cb,0,int) + self.register_callback("fx","spring", self.set_spring_scaler_cb, 0, str, typechar="!") + self.register_callback("fx","damper", self.set_damper_scaler_cb, 0, str, typechar="!") + self.register_callback("fx","inertia", self.set_inertia_scaler_cb, 0, str, typechar="!") + self.register_callback("fx","friction", self.set_friction_scaler_cb, 0, str, typechar="!") + + # This callback are used to get the current setting + self.register_callback("fx","filterCfFreq",lambda val : self.cffilter_changed(val),0,int) + self.register_callback("fx","spring",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_spring, self.doubleSpinBox_spring, self.springgain/256), 0, int) + self.register_callback("fx","damper",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_damper, self.doubleSpinBox_damper, self.dampergain/256), 0, int) + self.register_callback("fx","friction",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_friction, self.doubleSpinBox_friction, self.frictiongain/256), 0, int) + self.register_callback("fx","inertia",lambda val : self.dataChanged_UpdateSliderAndSpinbox(val, self.horizontalSlider_inertia, self.doubleSpinBox_inertia, self.inertiagain/256), 0, int) + + # This callback are used to display the wheel position + self.register_callback("axis","pos",self.enc_pos_cb,0,int) + self.register_callback("axis","cpr",self.cpr_cb,0,int) + + def init_ui(self): + try: + self.send_commands("fx",["spring","damper","friction","inertia"],0,typechar="!") + self.send_commands("axis",["cpr","pos","degrees", "fxratio", "idlespring", "axisdamper", "axisinertia", "axisfriction"]) + self.send_commands("fx",["filterCfFreq","spring","damper","friction","inertia"],0) + except: + self.main.log("Error initializing Wheel tab") + return False + return True + + # Tab is currently shown + def showEvent(self,event): + self.init_ui() # Refresh data in UI + self.timer.start(500) + + # Tab is hidden + def hideEvent(self,event): + self.timer.stop() + + # Timer interval reached + def timer_cb(self): + if self.cpr > 0: + self.send_command("axis","pos",0, typechar='?') + elif self.cpr == -1: + # cpr invalid. Request cpr + self.send_command("axis","cpr",0, typechar='?') + + ####################################################################################################### + # Windows Event + ####################################################################################################### + + @throttle(50) + def sliderChanged_UpdateSpinbox(self, val : int, spinbox : QSpinBox, factor :float, cls : str=None, command : str=None): + """when a slider move, and it provide a command, the slider update de spinbox + and send to the board the Value + """ + newVal = val * factor + if(spinbox.value() != newVal): + qtBlockAndCall(spinbox, spinbox.setValue,newVal) + if(command): + self.send_value(cls,command,val) + + def spinboxChanged_UpdateSlider(self, val : float, slider : QSlider, factor : float): + newVal = int(round(val * factor)) + if (slider.value() != newVal) : + slider.setValue(newVal) + + + @throttle(50) + def sliderChanged_UpdateLabel(self, val : int, label : QLabel, pattern :str, factor: float, cls : str=None, command : str=None): + """when a slider move, and it provide a command, the slider update de label using the pattern + and send to the board the Value + """ + newVal = val * factor + chaine = pattern.format(newVal) + if(label.text != chaine): + qtBlockAndCall(label, label.setText,chaine) + if(command): + self.send_value(cls,command,val) + + @throttle(50) + def cffilter_changed(self,v,send=False): + self.tech_log.debug("Freq %s send %d", v, send) + freq = max(min(v,500),0) + + if self.horizontalSlider_cffilter.value != freq : + self.horizontalSlider_cffilter.setValue(freq) + + if send: + self.send_value("fx","filterCfFreq",(freq)) + + lbl = str(freq)+"Hz" + if(freq == 500): + lbl = "Off" + qOn = False + + self.label_cffilter.setText(lbl) + + def display_speed_cutoff_damper(self, gain): + """Update the max rpm speed cutoff""" + damper_fw_internal_scaler = self.damper_internal_factor * self.damper_internal_scale + damper_speed = self.dampergain * damper_fw_internal_scaler * ((gain + 1) / 256) + max_speed = (32767 * 60 / 360) / damper_speed + self.label_damper_rpm.setText(f"{max_speed:.1f}") + + # TODO actually use the gain + def display_speed_cutoff_friction(self, gain): + """Update the max rpm speed cutoff""" + friction_fw_internal_scaler = self.friction_internal_factor * self.friction_internal_scale + max_speed = (32767 * self.friction_pct_speed_rampup / 100.0) * (60 / 360) / friction_fw_internal_scaler + self.label_friction_rpm.setText(f"{max_speed:.1f}") + + def display_accel_cutoff_inertia(self, gain): + """Update the max accel cutoff for inertia""" + inertia_fw_internal_scaler = self.inertia_internal_factor * self.inertia_internal_scale + inertia_accel = self.inertiagain * inertia_fw_internal_scaler * ((gain + 1) / 256) + max_accel = 32767 / inertia_accel + self.label_accel.setText(f"{max_accel:.0f}") + + def refresh_limit_ui(self, slider : QSlider) : + if slider == self.horizontalSlider_damper : + self.display_speed_cutoff_damper(slider.value()) + elif slider == self.horizontalSlider_friction : + self.display_speed_cutoff_friction(slider.value()) + elif slider == self.horizontalSlider_inertia : + self.display_accel_cutoff_inertia(slider.value()) + + ####################################################################################################### + # Board CallBack + ####################################################################################################### + + def cpr_cb(self,val : int): + if val > 0: + self.cpr = val + + def enc_pos_cb(self,val : int): + if self.cpr > 0: + rots = val / self.cpr + degs = rots * 360 + self.doubleSpinBox_curdeg.setValue(degs) + + def dataChanged_UpdateSliderAndSpinbox(self,val : float,slider : QSlider,spinbox : QSpinBox,factor : float): + newval = int(round(val,1)) + qtBlockAndCall(slider, slider.setValue, newval) + qtBlockAndCall(spinbox, spinbox.setValue,newval * factor) + self.refresh_limit_ui(slider) + + def dataChanged_UpdateSliderAndLabel(self,val : float,slider : QSlider, label : QLabel, pattern : str, factor : float): + newval = int(round(val)) + qtBlockAndCall(slider, slider.setValue, newval) + self.sliderChanged_UpdateLabel(newval,label,pattern, factor) + + def update_gain_scaler(self,slider : QSlider,spinbox : QSpinBox, gain): + spinbox.setMaximum(gain) + self.sliderChanged_UpdateSpinbox(slider.value(), spinbox, gain) + + def set_spring_scaler_cb(self,repl): + dat = map_infostring(repl) + self.springgain = dat.get("scale",self.springgain) + self.update_gain_scaler(self.horizontalSlider_spring, self.doubleSpinBox_spring, self.springgain) + + def set_damper_scaler_cb(self,repl): + dat = map_infostring(repl) + self.dampergain = dat.get("scale",self.dampergain) + self.damper_internal_factor = dat.get("factor",self.damper_internal_factor) + self.update_gain_scaler(self.horizontalSlider_damper, self.doubleSpinBox_damper, self.dampergain) + + def set_friction_scaler_cb(self,repl): + dat = map_infostring(repl) + self.frictiongain = dat.get("scale",self.frictiongain) + self.friction_internal_factor = dat.get("factor",self.friction_internal_factor) + self.update_gain_scaler(self.horizontalSlider_friction, self.doubleSpinBox_friction, self.frictiongain) + + def set_inertia_scaler_cb(self,repl): + dat = map_infostring(repl) + self.inertiagain = dat.get("scale",self.inertiagain) + self.inertia_internal_factor = dat.get("factor",self.inertia_internal_factor) + self.update_gain_scaler(self.horizontalSlider_inertia, self.doubleSpinBox_inertia, self.inertiagain) + + def set_friction_pct_speed_rampup_cb(self,value): + self.friction_pct_speed_rampup = value + \ No newline at end of file From 64ea133d4b1b38efc5d11182b90080b118050b77 Mon Sep 17 00:00:00 2001 From: Vincent Manoukian <10980775+manoukianv@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:18:21 +0100 Subject: [PATCH 11/11] Fix message lable in menu, and duplicate control. --- main.py | 2 +- res/MainWindow.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 42437cf0..7079e331 100644 --- a/main.py +++ b/main.py @@ -436,7 +436,7 @@ def update_classes_with_plugin(actual_new_active_classes : dict) -> dict: if (self.summary_tab) : summary_class = {} for name, classe_active in actual_new_active_classes.items() : - if (classe_active["id"] == 1) and self.summary_tab : + if (classe_active["id"] == 1) : wheel_summary_class_key = "wheelsummary:0" wheel_summary_class_value = { "name": "Wheel Summary", diff --git a/res/MainWindow.ui b/res/MainWindow.ui index 173adeb4..738c58fb 100644 --- a/res/MainWindow.ui +++ b/res/MainWindow.ui @@ -245,7 +245,7 @@ true - Displayed Summary tabs + Display Summary tabs