diff --git a/axis_ui.py b/axis_ui.py
index 1ed08985..a188153f 100644
--- a/axis_ui.py
+++ b/axis_ui.py
@@ -11,6 +11,88 @@
import encoder_tuning_ui
import expo_ui
+from PyQt6.QtCharts import QChart, QChartView, QLineSeries, QScatterSeries, QValueAxis
+from PyQt6.QtCore import Qt, QPointF, QMargins
+from PyQt6.QtGui import QColor, QPainter, QPen
+from PyQt6.QtWidgets import QApplication
+import math
+from biquad import Biquad
+
+def calc_freq_response(b, a, freqs, fs):
+ resp = []
+ for f in freqs:
+ w = 2 * math.pi * f / fs
+ cos_w = math.cos(w)
+ sin_w = -math.sin(w)
+ cos_2w = math.cos(2*w)
+ sin_2w = -math.sin(2*w)
+
+ num_real = b[0] + b[1]*cos_w + b[2]*cos_2w
+ num_imag = b[1]*sin_w + b[2]*sin_2w
+
+ den_real = 1 + a[1]*cos_w + a[2]*cos_2w
+ den_imag = a[1]*sin_w + a[2]*sin_2w
+
+ mag_num = math.sqrt(num_real**2 + num_imag**2)
+ mag_den = math.sqrt(den_real**2 + den_imag**2)
+
+ mag = mag_num / (mag_den + 1e-10)
+ resp.append(20 * math.log10(mag + 1e-10))
+ return resp
+
+class EqChartView(QChartView):
+ def __init__(self, chart, parent=None):
+ super().__init__(chart, parent)
+ self.setRenderHint(QPainter.RenderHint.Antialiasing)
+ self.dragged_index = -1
+ self.scatter_series = None
+ self.frequencies = []
+ self.on_gain_changed = None
+
+ def set_data_series(self, scatter_series, frequencies):
+ self.scatter_series = scatter_series
+ self.frequencies = frequencies
+
+ def mousePressEvent(self, event):
+ if self.scatter_series:
+ for i, p in enumerate(self.scatter_series.points()):
+ p_pixel = self.chart().mapToPosition(p)
+ dist = math.hypot(p_pixel.x() - event.position().x(), p_pixel.y() - event.position().y())
+ if dist < 15:
+ if event.button() == Qt.MouseButton.LeftButton:
+ self.dragged_index = i
+ event.accept()
+ return
+ elif event.button() == Qt.MouseButton.RightButton:
+ self.scatter_series.replace(i, self.frequencies[i], 0.0)
+ if self.on_gain_changed:
+ self.on_gain_changed(i, 0)
+ event.accept()
+ return
+ super().mousePressEvent(event)
+
+ def mouseMoveEvent(self, event):
+ if self.dragged_index >= 0 and self.scatter_series:
+ val_point = self.chart().mapToValue(event.position())
+ y = max(-120.0, min(120.0, val_point.y()))
+ self.scatter_series.replace(self.dragged_index, self.frequencies[self.dragged_index], y)
+ if self.on_gain_changed:
+ self.on_gain_changed(self.dragged_index, int(y))
+ event.accept()
+ else:
+ super().mouseMoveEvent(event)
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == Qt.MouseButton.LeftButton and self.dragged_index >= 0:
+ val_point = self.chart().mapToValue(event.position())
+ y = max(-120.0, min(120.0, val_point.y()))
+ self.scatter_series.replace(self.dragged_index, self.frequencies[self.dragged_index], y)
+ if self.on_gain_changed:
+ self.on_gain_changed(self.dragged_index, int(y))
+ self.dragged_index = -1
+ event.accept()
+ else:
+ super().mouseReleaseEvent(event)
class AxisUI(WidgetUI,CommunicationHandler):
@@ -54,12 +136,9 @@ def __init__(self, main: 'main.MainUi'=None, unique=0):
self.horizontalSlider_inertia.valueChanged.connect(lambda val : self.send_value("axis","axisinertia",val,instance=self.axis))
self.pushButton_center.clicked.connect(lambda : self.send_command("axis","zeroenc",instance=self.axis))
- #self.checkBox_invert.stateChanged.connect(lambda val : self.send_value("axis","invert",(0 if val == 0 else 1),instance=self.axis))
self.checkBox_speedlimit.stateChanged.connect(self.setSpeedLimitEnabled)
self.spinBox_speedlimit.valueChanged.connect(lambda val : self.send_value("axis","maxspeed",val,instance=self.axis))
- self.spinBox_reduction_numerator.valueChanged.connect(self.updateReductionText)
- self.spinBox_reduction_denominator.valueChanged.connect(self.updateReductionText)
self.pushButton_apply_options.clicked.connect(self.applyOptions)
self.pushButton_submit_hw.clicked.connect(self.submitHw)
@@ -70,6 +149,7 @@ def __init__(self, main: 'main.MainUi'=None, unique=0):
self.register_callback("axis","power",self.updatePowerSlider,self.axis,int)
self.register_callback("axis","degrees",lambda val : self.updateRange(val),self.axis,int)
+ self.register_callback("axis","maxspeed",self.speedLimitCb,self.axis,int)
self.register_callback("axis","invert",lambda val : qtBlockAndCall(self.checkBox_invert,self.checkBox_invert.setChecked,val),self.axis,int)
self.register_callback("axis","fxratio",lambda val : self.updateFxratio(val),self.axis,int)
@@ -81,9 +161,9 @@ def __init__(self, main: 'main.MainUi'=None, unique=0):
self.register_callback("axis","reduction",lambda val : self.updateReduction(val),self.axis,lambda x : tuple(map(int,x.split(":"))))
- self.register_callback("axis","cmdinfo",self.reductionAvailable,self.axis,int,adr = 17)
+ # Check if reduction command is available
+ self.register_callback("axis","cmdinfo",self.reductionAvailable,self.axis,int,adr = 19)
- self.register_callback("axis","maxspeed",self.speedLimitCb,self.axis,int)
self.register_callback("axis","pos",self.enc_pos_cb,self.axis,int)
self.register_callback("axis","cpr",self.cpr_cb,self.axis,int)
@@ -93,10 +173,137 @@ def __init__(self, main: 'main.MainUi'=None, unique=0):
# Check if expo is available
self.register_callback("axis","cmdinfo",self.expoAvailable,self.axis,int,adr = 24)
+
+ # manage display
+ self.groupBox_enableAxisBlock.toggled.connect(self.toggleAxisBlock)
self.pushButton_encoderTuning.clicked.connect(self.encoder_tuning_dlg.display)
self.pushButton_expo.clicked.connect(self.expo_dlg.display)
+
+
+
+
+ # --- Equalizer Controls ---
+
+ self.eq_freqs = [10, 15, 25, 40, 60, 100]
+ self.eq_gains = [0] * len(self.eq_freqs)
+ self.eq_q = 2.14
+ self.eq_fs = 1000.0
+
+ self.chart_eq = QChart()
+ self.chart_eq.legend().hide()
+ self.chart_eq.setBackgroundRoundness(5)
+ self.chart_eq.setBackgroundBrush(QApplication.instance().palette().window())
+
+ self.axis_x_eq = QValueAxis(self.chart_eq)
+ self.axis_x_eq.setRange(0, 120)
+ self.axis_x_eq.setTitleText("Frequency (Hz)")
+ grid_color = QColor(QApplication.instance().palette().text().color())
+ grid_color.setAlpha(40)
+ self.axis_x_eq.setGridLineColor(grid_color)
+ self.chart_eq.addAxis(self.axis_x_eq, Qt.AlignmentFlag.AlignBottom)
+
+ self.axis_y_eq = QValueAxis(self.chart_eq)
+ self.axis_y_eq.setRange(-120, 120)
+ self.axis_y_eq.setTitleText("Gain (%)")
+ self.axis_y_eq.setTickCount(7)
+ self.axis_y_eq.setGridLineColor(grid_color)
+ self.chart_eq.addAxis(self.axis_y_eq, Qt.AlignmentFlag.AlignLeft)
+
+ self.line_series_eq = QLineSeries()
+ pen = QPen(QColor("cornflowerblue"))
+ pen.setWidth(3)
+ self.line_series_eq.setPen(pen)
+ self.line_series_eq.setUseOpenGL(True)
+ self.chart_eq.addSeries(self.line_series_eq)
+ self.line_series_eq.attachAxis(self.axis_x_eq)
+ self.line_series_eq.attachAxis(self.axis_y_eq)
+
+ self.axis_y_eq_right = QValueAxis(self.chart_eq)
+ self.axis_y_eq_right.setRange(-120, 120)
+ self.axis_y_eq_right.setTickCount(7)
+ self.axis_y_eq_right.setGridLineVisible(False)
+ self.chart_eq.addAxis(self.axis_y_eq_right, Qt.AlignmentFlag.AlignRight)
+ self.line_series_eq.attachAxis(self.axis_y_eq_right)
+
+ self.zero_line_eq = QLineSeries()
+ pen_zero = QPen(QApplication.instance().palette().text().color())
+ pen_zero.setWidth(1)
+ pen_zero.setDashPattern([4, 4])
+ self.zero_line_eq.setPen(pen_zero)
+ self.zero_line_eq.setUseOpenGL(True)
+ self.zero_line_eq.append(0, 0)
+ self.zero_line_eq.append(120, 0)
+ self.chart_eq.addSeries(self.zero_line_eq)
+ self.zero_line_eq.attachAxis(self.axis_x_eq)
+ self.zero_line_eq.attachAxis(self.axis_y_eq)
+ self.zero_line_eq.attachAxis(self.axis_y_eq_right)
+
+ self.scatter_series_eq = QScatterSeries()
+ self.scatter_series_eq.setMarkerShape(QScatterSeries.MarkerShape.MarkerShapeCircle)
+ self.scatter_series_eq.setMarkerSize(12.0)
+ self.scatter_series_eq.setColor(QColor("white"))
+ self.scatter_series_eq.setBorderColor(QColor("cornflowerblue"))
+
+ for f in self.eq_freqs:
+ self.scatter_series_eq.append(f, 0)
+
+ self.chart_eq.addSeries(self.scatter_series_eq)
+ self.scatter_series_eq.attachAxis(self.axis_x_eq)
+ self.scatter_series_eq.attachAxis(self.axis_y_eq)
+ self.scatter_series_eq.attachAxis(self.axis_y_eq_right)
+
+ idx = self.gridLayout_9.indexOf(self.graphWidget_eq)
+ row, col, rowSpan, colSpan = self.gridLayout_9.getItemPosition(idx)
+ self.graphWidget_eq.deleteLater()
+
+ self.graphWidget_eq = EqChartView(self.chart_eq)
+ self.graphWidget_eq.set_data_series(self.scatter_series_eq, self.eq_freqs)
+ self.graphWidget_eq.on_gain_changed = self.on_eq_point_moved
+ self.gridLayout_9.addWidget(self.graphWidget_eq, row, col, rowSpan, colSpan)
+
+ for ax in self.chart_eq.axes():
+ ax.setLabelsBrush(QApplication.instance().palette().text())
+ ax.setTitleBrush(QApplication.instance().palette().text())
+
+ self.update_eq_curve()
+
+ self.checkBox_eq.stateChanged.connect(self.send_eq_enabled)
+
+ self.pushButton_resetEq.clicked.connect(self.reset_eq)
+
+ self.register_callback("axis","equalizer",self.update_eq_enabled,self.axis,int)
+ for i in range(6):
+ self.register_callback("axis",f"eqb{i+1}",lambda val, index=i: self.update_eq_band(val, index),self.axis,int)
+
+ # Set initial state of the collapsible groupbox
+ self.toggleAxisBlock(self.groupBox_enableAxisBlock.isChecked())
+ def toggleAxisBlock(self, checked):
+ # This function hides/shows the content of the groupbox and adjusts its height
+ # to create a collapsible effect.
+ self.groupBox_hardware.setVisible(checked)
+ self.groupBox_axisOption.setVisible(checked)
+ self.groupBox_encoder.setVisible(checked)
+
+ if checked:
+ # When checked (expanded), remove the maximum height constraint.
+ self.groupBox_enableAxisBlock.setMaximumHeight(16777215) # QWIDGETSIZE_MAX
+ else:
+ # When unchecked (collapsed), set a fixed height for the title bar.
+ # You might need to adjust this value (e.g., 30) to fit your UI style.
+ self.groupBox_enableAxisBlock.setMaximumHeight(40)
+ self.groupBox_enableAxisBlock.setMaximumWidth(800)
+
+ # Notify this widget (the tab page) that its size hint has changed.
+ self.updateGeometry()
+ # Also notify the parent tab widget, forcing it to recalculate its own size hint.
+ self.main.tabWidget_main.updateGeometry()
+
+ # Use a single shot timer to allow the event to process, then update the main window layout.
+ # This will now get the correct, updated size hint from the tab widget.
+ QTimer.singleShot(0, self.main.adjustSize)
+
def setSpeedLimit(self,val):
if self.checkBox_speedlimit.isChecked():
self.send_value("axis","maxspeed",self.spinBox_speedlimit.value(),instance=self.axis)
@@ -113,33 +320,28 @@ def speedLimitCb(self,val):
self.checkBox_speedlimit.setChecked(True)
self.spinBox_speedlimit.setEnabled(True)
-
- def setSpeedLimitEnabled(self,val):
- self.spinBox_speedlimit.setEnabled(val)
- if self.checkBox_speedlimit.isChecked():
- self.send_value("axis","maxspeed",self.spinBox_speedlimit.value(),instance=self.axis)
- else:
- self.send_value("axis","maxspeed",0,instance=self.axis)
-
-
def updateReduction(self,val):
numerator,denominator = val
self.spinBox_reduction_numerator.setValue(numerator)
self.spinBox_reduction_denominator.setValue(denominator)
- self.updateReductionText()
def reductionAvailable(self,available):
- self.frame_reduction.setVisible(available>0)
+ self.groupBox_reduction.setVisible(available>0)
if available > 0:
self.send_command("axis","reduction",self.axis)
- def updateReductionText(self):
- self.label_gear_reduction_value.setText(f"Prescaler: {round(self.spinBox_reduction_numerator.value()/self.spinBox_reduction_denominator.value(),5)}")
-
def applyOptions(self):
self.send_value("axis","invert",(0 if self.checkBox_invert.isChecked() == 0 else 1),instance=self.axis)
- if(self.frame_reduction.isVisible()):
- self.send_value("axis","reduction",self.spinBox_reduction_numerator.value(),self.spinBox_reduction_denominator.value(),self.axis)
+ self.send_value("axis","reduction",self.spinBox_reduction_numerator.value(),self.spinBox_reduction_denominator.value(),self.axis)
+
+ # check if speed is required
+ if self.checkBox_speedlimit.isChecked() :
+ self.send_value("axis","maxspeed",self.spinBox_speedlimit.value(),instance=self.axis)
+ else:
+ self.send_value("axis","maxspeed",0,instance=self.axis)
+
+ def setSpeedLimitEnabled(self,val):
+ self.spinBox_speedlimit.setEnabled(val)
def updateEsgain(self,val):
qtBlockAndCall(self.spinBox_esgain,self.spinBox_esgain.setValue,val)
@@ -168,8 +370,72 @@ def updateInertia(self,val):
def expoAvailable(self,available):
self.pushButton_expo.setEnabled(available>0)
self.expo_dlg.setEnabled(available>0)
- # if available > 0:
- # self.send_commands("axis",["expo","exposcale"],self.axis)
+ if available > 0:
+ self.send_commands("axis",["expo","exposcale"],self.axis)
+
+
+
+
+ # --- Equalizer Methods ---
+
+ # Called when the 'Effect equalizer' checkbox is toggled
+ def send_eq_enabled(self, state):
+ """Sends the command to enable or disable the equalizer on the firmware."""
+ self.send_value("axis", "equalizer", 1 if state else 0, instance=self.axis)
+
+ def on_eq_point_moved(self, band_index, value):
+ self.eq_gains[band_index] = value
+ self.update_eq_curve()
+ self.send_eq_band_value(value, band_index + 1)
+
+ def send_eq_band_value(self, value, band):
+ """Sends the gain value for a specific equalizer band to the firmware."""
+ self.send_value("axis", f"eqb{band}", value, instance=self.axis)
+
+ def reset_eq(self):
+ """Asks for user confirmation and then resets all equalizer bands to 0."""
+ reply = QMessageBox.question(self, 'Reset Equalizer', "Are you sure you want to reset all equalizer bands to 0?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
+ if reply == QMessageBox.StandardButton.Yes:
+ for band in range(len(self.eq_freqs)):
+ self.update_eq_band(0, band)
+ self.send_eq_band_value(0, band + 1)
+
+ def update_eq_enabled(self, value):
+ """Updates the 'Effect equalizer' checkbox state based on data from the firmware."""
+ qtBlockAndCall(self.checkBox_eq, self.checkBox_eq.setChecked, value)
+
+ def update_eq_band(self, value, band):
+ """Updates an equalizer gain value based on data from the firmware."""
+ self.eq_gains[band] = value
+ self.scatter_series_eq.replace(band, self.eq_freqs[band], value)
+ self.update_eq_curve()
+
+ def update_eq_curve(self):
+ """Calculates and draws the frequency response curve of the equalizer."""
+ freqs = [f for f in range(5, 121)]
+ resp_total = [0] * len(freqs)
+
+ for i, f0 in enumerate(self.eq_freqs):
+ gain = self.eq_gains[i] / 10.0
+ if abs(gain) > 0.01:
+ if i == 0:
+ b_type = 5 # lowshelf
+ elif i == len(self.eq_freqs) - 1:
+ b_type = 6 # highshelf
+ else:
+ b_type = 4 # peak
+
+ bq = Biquad(b_type, f0 / self.eq_fs, self.eq_q, gain)
+ b = [bq.a0, bq.a1, bq.a2]
+ a = [1.0, bq.b1, bq.b2]
+
+ resp = calc_freq_response(b, a, freqs, self.eq_fs)
+ for j in range(len(freqs)):
+ resp_total[j] += resp[j]
+
+ points = [QPointF(f, r * 10.0) for f, r in zip(freqs, resp_total)]
+ self.line_series_eq.replace(points)
def init_ui(self):
try:
@@ -177,8 +443,10 @@ def init_ui(self):
self.getEncoder()
#self.updateSliders()
self.send_commands("axis",["invert","cpr"],self.axis)
- self.send_command("axis","cmdinfo",self.axis,adr=17)
+ self.send_command("axis","cmdinfo",self.axis,adr=19) # reduction
self.send_command("axis","cmdinfo",self.axis,adr=24) # Expo
+ # Request initial equalizer status and all band gains from the firmware
+ self.send_commands("axis",["equalizer","eqb1","eqb2","eqb3","eqb4","eqb5","eqb6"],self.axis)
except:
self.main.log("Error initializing Axis tab")
@@ -351,10 +619,18 @@ def f(dat):
def encid_f(id):
if(id == 255):
- self.groupBox_encoder.setVisible(False)
+ #self.groupBox_encoder.setVisible(False)
+ self.label_encoderSource.setVisible(False)
+ self.comboBox_encoder.setVisible(False)
+ self.pushButton_submit_enc.setVisible(False)
+ self.stackedWidget_encoder.setVisible(False)
return
else:
- self.groupBox_encoder.setVisible(True)
+ #self.groupBox_encoder.setVisible(True)
+ self.label_encoderSource.setVisible(True)
+ self.comboBox_encoder.setVisible(True)
+ self.pushButton_submit_enc.setVisible(True)
+ self.stackedWidget_encoder.setVisible(True)
if(id == None):
self.main.log("Error getting encoder")
return
diff --git a/encoderconf_ui.py b/encoderconf_ui.py
index c0757076..ef119dde 100644
--- a/encoderconf_ui.py
+++ b/encoderconf_ui.py
@@ -29,20 +29,23 @@ def setType(self,id):
found = False
elif(id == 4): # MT SPI
self.widget = (MtEncoderConf(self,self.main))
- self.setTitle("SPI Settings")
+ self.setTitle("SPI Settings (port SPI3)")
elif(id == 5): # BISS-C
self.widget = (BissEncoderConf(self,self.main))
- self.setTitle("BISS Settings")
+ self.setTitle("BISS Settings (port SPI3 - exclusif usage)")
elif(id == 6): # SSI
self.widget = (SsiEncoderConf(self,self.main))
- self.setTitle("SSI Settings")
+ self.setTitle("SSI Settings (port SPI3 - exclusif usage)")
else:
layout.addWidget(QLabel("No settings"))
-
- layout.setContentsMargins(0,0,0,0)
+ layout.setContentsMargins(5,0,5,0)
+ layout.setStretch(0,0)
if self.widget:
layout.addWidget(self.widget)
+ self.setLayout(layout)
+
+ if self.widget:
self.applyBtn = QPushButton("Apply")
self.applyBtn.clicked.connect(self.widget.apply)
footer = QHBoxLayout()
@@ -52,7 +55,6 @@ def setType(self,id):
footer.addStretch(5)
layout.addLayout(footer)
- self.setLayout(layout)
class EncoderOption(QWidget):
def __init__(self,parent):
@@ -89,7 +91,7 @@ def initUI(self):
self.spinBox_cpr = QSpinBox()
self.spinBox_cpr.setRange(0,0xffff)
- layout.addWidget(QLabel("CPR = 4x PPR"))
+ layout.addRow(QLabel("CPR = 4x PPR"))
layout.addRow(QLabel("CPR:"),self.spinBox_cpr)
self.checkBox_index = QCheckBox("Use index homing")
@@ -121,7 +123,6 @@ def initUI(self):
self.spinBox_cs = QSpinBox()
self.spinBox_cs.setRange(1,3)
- layout.addWidget(QLabel("SPI3 extension port"))
layout.addRow(QLabel("CS pin"),self.spinBox_cs)
self.comboBox_mode = QComboBox()
@@ -169,10 +170,8 @@ def initUI(self):
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):
@@ -202,11 +201,9 @@ def initUI(self):
self.comboBox_mode = QComboBox()
self.comboBox_speed = QComboBox()
- layout.addWidget(QLabel("SPI3 extension port"))
layout.addRow(QLabel("Bits"),self.spinBox_bits)
layout.addRow(QLabel("Mode"),self.comboBox_mode)
layout.addRow(QLabel("SPI speed"),self.comboBox_speed)
- layout.addWidget(QLabel("Port is used exclusively!"))
self.setLayout(layout)
def updateSpeeds(self,reply):
diff --git a/ffb_ui.py b/ffb_ui.py
index 2ff871cf..76abf66a 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
@@ -111,6 +111,13 @@ def __init__(self, main : 'main.MainUi'=None, title = "FFB main"):
self.register_callback("fx", "frictionPctSpeedToRampup", self.set_friction_pct_speed_rampup,0,int)
+ # --- Smoothing Controls ---
+ self.radioButton_reconfilter_0.toggled.connect(lambda checked: self.send_recon_filter(0) if checked else None)
+ self.radioButton_reconfilter_1.toggled.connect(lambda checked: self.send_recon_filter(1) if checked else None)
+ self.radioButton_reconfilter_2.toggled.connect(lambda checked: self.send_recon_filter(2) if checked else None)
+ self.radioButton_reconfilter_3.toggled.connect(lambda checked: self.send_recon_filter(3) if checked else None)
+ self.register_callback("fx", "reconFilterMode", self.update_recon_filter_ui, 0, int)
+
if(self.init_ui()):
tabId = self.main.add_tab(self,title)
self.main.select_tab(tabId)
@@ -131,6 +138,7 @@ def init_ui(self):
self.send_command("main","lsain",0,'?') # get analog types
self.send_command("main","aintypes",0,'?') # get active analog
+ self.send_commands("fx", ["reconFilterMode"])
self.updateSliders()
self.send_command("main","hidsendspd",0,'!') # get speed
@@ -337,6 +345,28 @@ def updateAnalogSources(self,types):
self.groupBox_analogaxes.setLayout(layout)
+ # Called when the reconstruction filter slider is moved
+ def send_recon_filter(self, value):
+ """Sends the selected reconstruction filter mode to the firmware."""
+ self.send_value("fx", "reconFilterMode", value)
+
+ # Callback to update the reconstruction filter UI from firmware data
+ def update_recon_filter_ui(self, value):
+ """Updates the reconstruction filter slider and label."""
+ button_radio = None
+ if (value < 0 or value >= 4):
+ self.main.log(f"Warning: Received unknown reconstruction filter mode value: {value}")
+ elif value == 0:
+ button_radio = self.radioButton_reconfilter_0
+ elif value == 1:
+ button_radio = self.radioButton_reconfilter_1
+ elif value == 2:
+ button_radio = self.radioButton_reconfilter_2
+ elif value == 3:
+ button_radio = self.radioButton_reconfilter_3
+ qtBlockAndCall(button_radio, button_radio.setChecked, True)
+ button_radio.setChecked(True)
+
@throttle(50)
def cffilter_changed(self,v,send=True):
self.tech_log.debug("Freq %s send %d", v, send)
diff --git a/main.py b/main.py
index c221af34..b4a432b9 100644
--- a/main.py
+++ b/main.py
@@ -112,6 +112,8 @@ def __init__(self):
self.timer = PyQt6.QtCore.QTimer(self)
self.timer.timeout.connect(self.update_timer) # pylint: disable=no-value-for-parameter
self.tabWidget_main.currentChanged.connect(self.tab_changed)
+ # Force the main window to resize to the content of the newly selected tab
+ self.tabWidget_main.currentChanged.connect(lambda: PyQt6.QtCore.QTimer.singleShot(0, self.adjustSize))
self.errors_dlg = errors.ErrorsDialog(self)
self.effects_monitor_dlg = effects_monitor.EffectsMonitorDialog(self)
self.maxaxischanged.connect(self.effects_monitor_dlg.set_max_axes)
@@ -287,6 +289,15 @@ def open_dfu_dialog(self):
dfu.deleteLater()
+ def changeEvent(self, event: PyQt6.QtCore.QEvent):
+ """Minimize to systray when window is minimized."""
+ if event.type() == PyQt6.QtCore.QEvent.Type.WindowStateChange:
+ if self.windowState() & PyQt6.QtCore.Qt.WindowState.WindowMinimized:
+ self.hide()
+ event.ignore()
+ return
+ super().changeEvent(event)
+
def moveEvent(self, event: PyQt6.QtGui.QMoveEvent): #pylint: disable=invalid-name
"""Move all modal dialog when moving main ui."""
super().moveEvent(event)
@@ -758,7 +769,7 @@ def __init__(self, main: MainUi):
self._submenu_profiles: PyQt6.QtWidgets.QMenu = None
# Adding an icon
- icon = PyQt6.QtGui.QIcon("app.png")
+ icon = PyQt6.QtGui.QIcon(helper.res_path('app.ico'))
# Adding item on the menu bar
tray = PyQt6.QtWidgets.QSystemTrayIcon(main)
@@ -928,6 +939,14 @@ def __init__(self, parent : MainUi = None ):
"""Display the about box with the release number updated."""
PyQt6.QtWidgets.QDialog.__init__(self, parent)
PyQt6.uic.loadUi(helper.res_path("about.ui"), self)
+ self.parent = parent
+ self.init_ui()
+
+ self.sig_dialog.signature_result.connect(parent.update_signature)
+ self.sig_dialog.signature_result.connect(lambda : self.init_ui())
+
+ def init_ui(self):
+ parent = self.parent
verstr = "Version: " + VERSION
self.hwuid = parent.hwuid
self.sig_dialog = SignatureInputDialog(self,parent.hwuid)
@@ -943,15 +962,13 @@ def __init__(self, parent : MainUi = None ):
self.pushButton_signature.setEnabled(False)
signaturestr = self.tr("Signature: ")
- if not parent.hwsignature:
+ if not parent.hwsignature or parent.hwsignature == bytes([0xff])*64:
signaturestr += self.tr("Missing")
self.pushButton_signature.setEnabled(True)
else:
signaturestr += self.tr("Valid ✅" if parent.signature_valid else "Invalid ❌")
self.label_signature.setText(signaturestr)
- self.sig_dialog.signature_result.connect(parent.update_signature)
-
def signature_dialog(self):
self.sig_dialog.open()
@@ -1045,6 +1062,12 @@ def process_events():
if __name__ == "__main__":
logging.config.fileConfig(helper.res_path('logger.conf'))
+ if sys.platform == "win32" or "Windows" in sys.platform:
+ import ctypes
+ try:
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("openffboard.configurator.app")
+ except Exception:
+ pass
restart = True
exit_code = -1
app = PyQt6.QtWidgets.QApplication(sys.argv)
diff --git a/profile_ui.py b/profile_ui.py
index c69f50b3..a57e5f99 100644
--- a/profile_ui.py
+++ b/profile_ui.py
@@ -10,6 +10,7 @@
import json
import copy
import sys
+import shutil
import PyQt6.QtCore
import PyQt6.QtWidgets
@@ -17,20 +18,16 @@
import base_ui
import helper
-def get_config_dir_path(profiles_filename):
- if sys.platform == "linux" and not os.path.exists(profiles_filename):
- defaultConfigDir = os.path.expandvars("$HOME/.config")
- return os.path.join(os.getenv("XDG_CONFIG_HOME", defaultConfigDir), "openffboard")
- return os.getcwd()
class ProfileUI(base_ui.WidgetUI, base_ui.CommunicationHandler):
"""Manage the Profile selector and the board communication about them."""
- __RELEASE = 2
+ __RELEASE = 3
__PROFILES_FILENAME = "profiles.json"
- __PROFILES_FILEPATH = os.path.join(get_config_dir_path(__PROFILES_FILENAME), __PROFILES_FILENAME)
+ __APP_NAME = "openffboard"
+ __PROFILES_FILEPATH = None
__PROFILESSETUP_FILENAME = helper.res_path("profile.cfg")
__PROFILES_TEMPLATE = {
"release": __RELEASE,
@@ -56,8 +53,11 @@ def __init__(self, main=None):
self._map_class_running = []
self._running_profile = []
self._profilename_tosave: str = None
+ self._is_communicating = False
self.ui_initialized = False
+
+ self.__PROFILES_FILEPATH = self.setup_and_migrate_config(self.__PROFILES_FILENAME, self.__APP_NAME)
self.load_profile_settings()
self.load_profiles()
@@ -92,9 +92,76 @@ def initialize_ui(self):
)
self.setEnabled(False)
+
+ def get_system_config_dir(self, app_name):
+ """
+ Returns the system-specific configuration directory.
+ """
+ home = os.path.expanduser("~")
+
+ if sys.platform == "win32":
+ # Windows: User requested specific path: C:\Users\Name\.appname
+ # We add a dot prefix for Windows to match the request
+ return os.path.join(home, f".{app_name}")
+
+ elif sys.platform == "darwin":
+ # macOS Standard: ~/Library/Application Support/appname
+ return os.path.join(home, "Library", "Application Support", app_name)
+
+ else:
+ # Linux Standard: ~/.config/appname (XDG standard)
+ base = os.getenv("XDG_CONFIG_HOME", os.path.join(home, ".config"))
+ return os.path.join(base, app_name)
+
+ def setup_and_migrate_config(self, filename, app_name):
+ """
+ Prepares the configuration directory and migrates the old file if it exists locally.
+ Returns the final path of the file to use.
+ """
+ system_dir = self.get_system_config_dir(app_name)
+ system_file_path = os.path.join(system_dir, filename)
+
+ # Old location (current working directory)
+ local_file_path = os.path.join(os.getcwd(), filename)
+
+ # Create system directory if it doesn't exist
+ if not os.path.exists(system_dir):
+ try:
+ os.makedirs(system_dir)
+ self.log(f"[Info] Config directory created: {system_dir}")
+ except OSError as e:
+ self.log(f"[Error] Could not create directory: {e}")
+ return local_file_path
+
+ # Migration Logic, ff a file exists in the current directory (old method)...
+ if os.path.exists(local_file_path):
+
+ # and NO file exists in the new system folder yet:
+ if not os.path.exists(system_file_path):
+ self.log("[Info] Migration in progress: Moving local config to system folder...")
+ try:
+ shutil.move(local_file_path, system_file_path)
+ self.log("[Success] Migration complete.")
+ except OSError as e:
+ self.log(f"[Error] Migration failed: {e}")
+ return local_file_path # Fallback to local on failure
+
+ # if a file exists in BOTH locations (Conflict):
+ else:
+ self.log("[Warning] Config file exists in both locations.")
+ self.log("[Info] Renaming the local (old) file to .bak to avoid confusion.")
+ try:
+ shutil.move(local_file_path, local_file_path + ".bak")
+ except Exception as e:
+ self.log(f"[Error] Could not rename local file: {e}")
+
+ # 4. Return the final path (System path)
+ return system_file_path
def save_clicked(self):
"""Save current seeting in Flash and replace the 'Flash profile' settings by the new one."""
+ if self._is_communicating:
+ return
def log_save_cb(res):
"""Display the confirmation in log."""
@@ -109,11 +176,11 @@ def onclose(self):
"""Remove the serial call backs on close."""
self.remove_callbacks()
- def setEnabled(self, a0: bool) -> None: # pylint: disable=invalid-name
+ def setEnabled(self, enabled: bool) -> None:
"""Refresh the combo box content with profile when connection is up."""
- if a0 and self.comboBox_profiles.count() == 0:
+ if enabled and self.comboBox_profiles.count() == 0:
self.refresh_combox_list()
- return super().setEnabled(a0)
+ return super().setEnabled(enabled)
def set_save_btn(self, status):
"""Enable the save button based on status."""
@@ -126,9 +193,9 @@ def open_profile_manager(self):
def close_profile_manager(self, profile_name:str = ''):
"""Close the profile list manager."""
- self.comboBox_profiles.currentIndexChanged.disconnect(self.apply_config)
+ self.comboBox_profiles.blockSignals(True)
self.refresh_combox_list()
- self.comboBox_profiles.currentIndexChanged.connect(self.apply_config)
+ self.comboBox_profiles.blockSignals(False)
if (profile_name):
self.select_profile(profile_name)
self.create_or_update_profile_file()
@@ -154,7 +221,13 @@ def load_profiles_from_file(self):
with open(self.__PROFILES_FILEPATH, "r", encoding="utf_8") as profile_file:
self.profiles = json.load(profile_file)
if self.profiles['release'] < self.__RELEASE :
- os.rename(self.__PROFILES_FILEPATH, self.__PROFILES_FILEPATH + '.' + str(self.profiles['release']) + '.old')
+ backup_path = self.__PROFILES_FILEPATH + '.' + str(self.profiles['release']) + '.old'
+ if os.path.exists(backup_path):
+ try:
+ os.remove(backup_path)
+ except OSError:
+ pass
+ os.rename(self.__PROFILES_FILEPATH, backup_path)
self.create_or_update_profile_file(create=True)
self.log("Profile: profiles are not compatible, need to redo them")
else:
@@ -188,7 +261,7 @@ def create_or_update_profile_file(self, create: bool = False) -> bool:
self.log("Profile: profile file created")
# Ensure the parent directory exists
- os.makedirs(get_config_dir_path(self.__PROFILES_FILENAME), exist_ok=True)
+ os.makedirs(self.get_system_config_dir(self.__APP_NAME), exist_ok=True)
try:
with open(self.__PROFILES_FILEPATH, "w", encoding="utf_8") as f:
@@ -200,7 +273,7 @@ def create_or_update_profile_file(self, create: bool = False) -> bool:
def get_global_setting(self,key : str, 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 not in self.profiles['global']) and default is not None:
self.set_global_setting(key,default)
return self.profiles['global'].get(key,None)
return None
@@ -220,11 +293,8 @@ def refresh_combox_list(self):
for profilename in data:
listprofile.append(profilename["name"])
- try:
- listprofile.index(ProfileUI.FLASH_PROFILE_NAME)
+ if ProfileUI.FLASH_PROFILE_NAME in listprofile and ProfileUI.NONE_PROFILE_NAME in listprofile:
listprofile.remove(ProfileUI.NONE_PROFILE_NAME)
- except ValueError:
- pass
self.comboBox_profiles.addItems(listprofile)
self.profiles_updated_event.emit(listprofile)
@@ -251,7 +321,7 @@ def apply_config(self):
Send the paramter for active class to the board.
"""
# if not enabled, don't select profile
- if not self.isEnabled():
+ if not self.isEnabled() or self._is_communicating:
return
# read the selected profile name, if profile is None, remove the last message
@@ -275,6 +345,9 @@ def save_config_in_profile(self, profile_name: str = ""):
When the profile_name is pass, we don't check if "None or Flash Profile", else we have recursivity with method
'save_clicked'.
"""
+ if self._is_communicating:
+ return
+
if not(profile_name) or profile_name == "":
self._profilename_tosave = str(self.comboBox_profiles.currentText())
@@ -296,6 +369,9 @@ def save_config_in_profile(self, profile_name: str = ""):
def _read_running_class_and_go_cb(self, call_back):
"""Get the running class from board, and process the call_back when board respond."""
# refresh the global var when starting to read value from board
+ self._is_communicating = True
+ self.comboBox_profiles.setEnabled(False)
+ self.set_save_btn(False)
self._current_class = -1
self._current_command = -1
self._current_instance = -1
@@ -308,28 +384,27 @@ def _read_running_class_and_go_cb(self, call_back):
def _build_running_map(self, buffer: str):
self._map_class_running = []
- # Split the string buffer into array
- splitted_running_class = [x.split(":") for x in buffer.split("\n")]
- # Format the arrray in array of object {classname, fullname, instance}
- formated_iterator = list(
- map(
- lambda tab: {
- "classname": tab[1],
- "fullname": tab[0],
- "instance": int(tab[2]),
- },
- splitted_running_class,
- )
- )
+ formated_iterator = []
+ for line in buffer.splitlines():
+ tab = line.split(":")
+ if len(tab) >= 3:
+ try:
+ formated_iterator.append({
+ "classname": tab[1],
+ "fullname": tab[0],
+ "instance": int(tab[2])
+ })
+ except ValueError:
+ pass
+
# For each class to saved declared in the cfg file,
# we filter the running instance to keep only those
for call_order in self.profile_setup["callOrder"]:
- filtered_iterator = filter(
- lambda x, call=call_order: x["classname"] == call["classname"]
- and x["fullname"] == call["fullname"],
- formated_iterator,
- )
- self._map_class_running.extend(list(filtered_iterator))
+ filtered_iterator = [
+ x for x in formated_iterator
+ if x["classname"] == call_order["classname"] and x["fullname"] == call_order["fullname"]
+ ]
+ self._map_class_running.extend(filtered_iterator)
def _get_instance_running(self, indexclass: int, indexinstance: int):
if indexclass > len(self.profile_setup["callOrder"]):
@@ -485,74 +560,110 @@ def _read_profile_cb(self, buffer: str):
self._read_value(
self._current_class, self._current_command, self._current_instance
)
- elif self._profilename_tosave is not False:
+ elif self._profilename_tosave is not None:
self._save_profile_in_file(self._running_profile, self._profilename_tosave)
+ self._is_communicating = False
+ self.comboBox_profiles.setEnabled(True)
+ self.set_save_btn(True)
else:
self.log("Profiles: profile read from board")
+ self._is_communicating = False
+ self.comboBox_profiles.setEnabled(True)
+ self.set_save_btn(True)
def _write_profile_cb(self, buffer: str):
- # process the incoming buffer
- if self._current_class == -1:
- # first call is sys.lsactive to get all active class
- # that running and we extract a map of class/instances
- self._build_running_map(buffer)
- else:
- return
+ try:
+ # process the incoming buffer
+ if self._current_class == -1:
+ # first call is sys.lsactive to get all active class
+ # that running and we extract a map of class/instances
+ self._build_running_map(buffer)
+ else:
+ return
- # read the selected profile name
- profilename = str(self.comboBox_profiles.currentText())
- if profilename == "":
- return
+ # read the selected profile name
+ profilename = str(self.comboBox_profiles.currentText())
+ if profilename == "":
+ return
- # From the profile, filter parameters that are running
- parameters_running = []
- profile_json_entry = next(
- filter(lambda x: x["name"] == profilename, self.profiles["profiles"]), None
- )
- for running_class in self._map_class_running:
- parameters_running.extend(
- list(
- filter(
- lambda x, running=running_class: x["fullname"]
- == running["fullname"]
- and x["cls"] == running["classname"]
- and x["instance"] == running["instance"],
- profile_json_entry["data"],
- )
- )
+ # From the profile, filter parameters that are running
+ parameters_running = []
+ profile_json_entry = next(
+ filter(lambda x: x["name"] == profilename, self.profiles["profiles"]), None
)
- # sent the filter running parameter to the board
- # and after, read a new time values to refresh UI
- if len(parameters_running) > 0:
- for pararmeter in parameters_running:
- self.send_value(
- cls=pararmeter["cls"],
- cmd=pararmeter["cmd"],
- val=pararmeter["value"],
- instance=pararmeter["instance"],
+ if not profile_json_entry:
+ self.log(f"Profile: '{profilename}' not found in configuration data.")
+ return
+
+ for running_class in self._map_class_running:
+ parameters_running.extend(
+ list(
+ filter(
+ lambda x, running=running_class: x["fullname"]
+ == running["fullname"]
+ and x["cls"] == running["classname"]
+ and x["instance"] == running["instance"],
+ profile_json_entry["data"],
+ )
+ )
)
- # axis.0.degrees?|900
- replytext = (
- "["
- + pararmeter["cls"]
- + "."
- + str(pararmeter["instance"])
- + "."
- + pararmeter["cmd"]
- + "?|"
- + str(pararmeter["value"])
- + "]"
- )
- self.process_virtual_comms_buffer(replytext)
+ # sent the filter running parameter to the board
+ # and after, read a new time values to refresh UI
+ if len(parameters_running) > 0:
+ for pararmeter in parameters_running:
+ self.send_value(
+ cls=pararmeter["cls"],
+ cmd=pararmeter["cmd"],
+ val=pararmeter["value"],
+ instance=pararmeter["instance"],
+ )
- # self.sendCommand(cls=pararmeter["cls"],
- # cmd=pararmeter["cmd"], instance=pararmeter["instance"])
+ # axis.0.degrees?|900
+ replytext = f"[{pararmeter['cls']}.{pararmeter['instance']}.{pararmeter['cmd']}?|{pararmeter['value']}]"
+ self.process_virtual_comms_buffer(replytext)
- # send message that announce new profile is selected
- self.profile_selected_event.emit(profilename)
- self.log("Profile: '" + profilename + "' is active")
+ # self.sendCommand(cls=pararmeter["cls"],
+ # cmd=pararmeter["cmd"], instance=pararmeter["instance"])
+
+ # send message that announce new profile is selected
+ self.profile_selected_event.emit(profilename)
+ self.log("Profile: '" + profilename + "' is active")
+ finally:
+ self._is_communicating = False
+ self.comboBox_profiles.setEnabled(True)
+ self.set_save_btn(True)
+
+ def delete_profile(self, item_name: str):
+ """Remove the specified profile from the list."""
+ for i in range(len(self.profiles["profiles"])):
+ if self.profiles["profiles"][i]["name"] == item_name:
+ self.profiles["profiles"].pop(i)
+ break
+ self.create_or_update_profile_file()
+
+ def copy_profile(self, item_name: str, new_name: str):
+ """Copy the specified profile under a new name."""
+ profile_json_entry = next(
+ filter(lambda x: x["name"] == item_name, self.profiles["profiles"]),
+ None,
+ )
+ if profile_json_entry is not None:
+ new_profile = copy.deepcopy(profile_json_entry)
+ new_profile["name"] = new_name
+ self.profiles["profiles"].append(new_profile)
+ self.create_or_update_profile_file()
+
+ def rename_profile(self, item_name: str, new_name: str):
+ """Rename the specified profile."""
+ profile_json_entry = next(
+ filter(lambda x: x["name"] == item_name, self.profiles["profiles"]),
+ None,
+ )
+ if profile_json_entry is not None:
+ profile_json_entry["name"] = new_name
+ self.create_or_update_profile_file()
class ProfilesDialog(PyQt6.QtWidgets.QDialog):
@@ -640,10 +751,7 @@ def delete(self):
if len(self.selection_model.selection().indexes()) <= 0:
return
item_name = self.selection_model.selection().indexes()[0].data()
- for i in range(len(self.profile_dlg.profiles["profiles"])):
- if self.profile_dlg.profiles["profiles"][i]["name"] == item_name:
- self.profile_dlg.profiles["profiles"].pop(i)
- break
+ self.profile_dlg.parent().delete_profile(item_name)
self.read_profiles()
def copy_as(self):
@@ -653,17 +761,7 @@ def copy_as(self):
item_name = self.selection_model.selection().indexes()[0].data()
name, status = PyQt6.QtWidgets.QInputDialog.getText(self, "Copy as", "new name")
if status and (name != "") and (name not in self.get_profiles_name()):
- profile_json_entry = next(
- filter(
- lambda x: x["name"] == item_name,
- self.profile_dlg.profiles["profiles"],
- ),
- None,
- )
- new_profile = copy.deepcopy(profile_json_entry)
- if profile_json_entry is not None:
- new_profile["name"] = name
- self.profile_dlg.profiles["profiles"].append(new_profile)
+ self.profile_dlg.parent().copy_profile(item_name, name)
self.read_profiles()
def rename(self):
@@ -675,15 +773,7 @@ def rename(self):
self, "Copy as", "new name", text=item_name
)
if status and (name != "") and (name not in self.get_profiles_name()):
- profile_json_entry = next(
- filter(
- lambda x: x["name"] == item_name,
- self.profile_dlg.profiles["profiles"],
- ),
- None,
- )
- if profile_json_entry is not None:
- profile_json_entry["name"] = name
+ self.profile_dlg.parent().rename_profile(item_name, name)
self.read_profiles()
def read_profiles(self):
diff --git a/res/MainWindow.ui b/res/MainWindow.ui
index 71a55a52..cc46dc7f 100644
--- a/res/MainWindow.ui
+++ b/res/MainWindow.ui
@@ -15,7 +15,7 @@
-
+
0
0
@@ -24,15 +24,15 @@
-
-
+
0
- 20
+ 0
0
- 200
+ 0
@@ -43,7 +43,7 @@
-
-
+
0
0
diff --git a/res/axis_ui.ui b/res/axis_ui.ui
index 3de7ad74..a8888406 100644
--- a/res/axis_ui.ui
+++ b/res/axis_ui.ui
@@ -6,578 +6,181 @@
0
0
- 941
- 907
+ 948
+ 873
Form
-
-
-
-
+
+
-
+
- Axis settings
+ Position
-
-
- 5
-
-
- 5
-
-
-
-
-
- Qt::Orientation::Vertical
+
+
-
+
+
+ Pos
-
- QSizePolicy::Policy::Preferred
+
+
+ -
+
+
+ <html><head/><body><p>Zero the axis (Center)</p></body></html>
-
-
- 20
- 5
-
+
+ Set center position
-
+
- -
-
-
- Qt::Orientation::Vertical
-
-
-
- 20
- 1
-
+
-
+
+
+ Raw
-
+
- -
-
-
- Qt::Orientation::Horizontal
+
-
+
+
+ true
-
- QSizePolicy::Policy::Expanding
+
+ false
-
-
- 10
- 10
-
+
+ true
-
-
- -
-
-
- Qt::Orientation::Vertical
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
-
- QSizePolicy::Policy::Minimum
+
+ false
-
-
- 20
- 0
-
+
+
-
-
- -
-
-
- Qt::Orientation::Vertical
+
+ °
-
-
- 20
- 0
-
+
+ -9223372036854775808.000000000000000
-
-
- -
-
-
-
- 0
- 0
-
+
+ 9223372036854775808.000000000000000
-
- Motor driver
+
+ 0.000000000000000
-
-
-
-
-
- Qt::Orientation::Horizontal
-
-
- QSizePolicy::Policy::Minimum
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Change driver
-
-
-
- -
-
-
-
- 2
- 0
-
-
-
-
- -
-
-
- Motor driver
-
-
-
-
- -
-
-
-
- 1
- 0
-
+
-
+
+
+ false
-
- Encoder selection
+
+ true
+
+
+ QAbstractSpinBox::ButtonSymbols::NoButtons
+
+
+ false
+
+
+ -1999999999
+
+
+ 1999999999
+
+
+ 0
-
-
- 5
-
-
- 5
-
-
- 5
-
-
-
-
-
-
- 1
- 1
-
-
-
- 0
-
-
- 1
-
-
-
-
-
- -
-
-
- Encoder source
-
-
-
- -
-
-
-
- 1
- 0
-
-
-
- QComboBox::SizeAdjustPolicy::AdjustToContents
-
-
-
- -
-
-
- Change enc
-
-
-
-
- -
-
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ FFB Power
+
+
+
+ 10
+
+
+ 5
+
+
-
+
-
+
0
0
-
- Actions
+
+ <html><head/><body><p>How stiff the endstop should be (Tune to taste while keeping noise low)</p></body></html>
+
+
+ 255
+
+
+ Qt::Orientation::Horizontal
-
-
-
-
-
- <html><head/><body><p>Zero the axis (Center)</p></body></html>
-
-
- Set center position
-
-
-
- -
-
-
- Encoder tuning...
-
-
-
- -
-
-
- false
-
-
- Torque curve...
-
-
-
-
- -
-
-
- Options
+
-
+
+
+ <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>
-
-
- 5
-
-
- 5
-
-
-
-
-
-
- 0
- 0
-
-
-
- QFrame::Shape::StyledPanel
-
-
- QFrame::Shadow::Sunken
-
-
- 1
-
-
-
- 4
-
-
- 4
-
-
- 4
-
-
- 4
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 100
- 0
-
-
-
- 1
-
-
- 256
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- :
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 100
- 0
-
-
-
- 1
-
-
- 256
-
-
-
- -
-
-
- 0.0
-
-
-
- -
-
-
- <html><head/><body><p>Encoder reduction <br/>prescaler for <br/>range selection.</p></body></html>
-
-
-
-
-
-
- -
-
-
- Apply
-
-
-
- -
-
-
- <html><head/><body><p>Reverses the axis.<br/>Also reverses the forces so they keep pointing in the correct direction.</p></body></html>
-
-
- Invert axis
-
-
-
-
-
-
- -
-
-
- Limits
-
-
-
- 9
-
-
-
-
-
- false
-
-
- false
-
-
- °/s
-
-
- 0
-
-
- 65535
-
-
- 10
-
-
- 0
-
-
-
- -
-
-
- Speed limit
-
-
-
-
-
-
- -
-
-
- Info
-
-
-
-
-
-
- true
-
-
- false
-
-
- true
-
-
- QAbstractSpinBox::ButtonSymbols::NoButtons
-
-
- false
-
-
-
-
-
- °
-
-
- -9223372036854775808.000000000000000
-
-
- 9223372036854775808.000000000000000
-
-
- 0.000000000000000
-
-
-
- -
-
-
- Pos
-
-
-
- -
-
-
- false
-
-
- true
-
-
- QAbstractSpinBox::ButtonSymbols::NoButtons
-
-
- false
-
-
- -1999999999
-
-
- 1999999999
-
-
- 0
-
-
-
- -
-
-
- Raw
-
-
-
-
-
-
-
-
-
- -
-
-
- Mechanical settings (Game independent)
-
-
-
- 10
-
-
- 5
-
-
-
-
-
- <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>Independent damper is an always on damper effect to smooth out acceleration spikes</p></body></html>
+
+ 102
255
+
+ 102
+
+
+ 102
+
Qt::Orientation::Horizontal
- -
-
-
- <html><head/><body><p>Independent damper is an always on damper effect to smooth out acceleration spikes.</p></body></html>
+
-
+
+
+
+ 30
+ 0
+
- Permanent damper
+ 0
+(0%)
- -
-
+
-
+
100
@@ -590,194 +193,201 @@
-
-
-
255
-
-
+
- <html><head/><body><p>Degrees of rotation</p></body></html>
+ <html><head/><body><p>How much torque going to be used from the motor. <br/><br/>More power means more current being used from the motor as well as stronger ffb.</p></body></html>
- Range (deg)
+ Overall Power
- -
-
-
- <html><head/><body><p>Centering force when FFB is inactive</p></body></html>
-
+
-
+
- 255
-
-
- 0
+ 32767
-
- 0
+
+ 100
Qt::Orientation::Horizontal
+
+ 100
+
- -
-
-
-
- 100
- 0
-
+
-
+
+
+ <html><head/><body><p>How much torque will be reserved to stop the motor for endstop effect.</p></body></html>
-
- false
+
+ Endstop intensity
-
- °
+
+
+ -
+
+
+ 0
-
- 10
+
+
+ -
+
+
+ <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
+
+
+
+
+
+
+ -
+
+
+ Mechanical settings (Game independent)
+
+
+
+ 10
+
+
+ 5
+
+
-
+
+
+ Permanent friction
+
+
+ -
+
- 32767
+ 255
-
- 10
+
+ Qt::Orientation::Horizontal
- -
-
+
-
+
100
0
-
- false
-
255
- -
-
-
- 10
-
+
-
+
- 1440
-
-
- 10
-
-
- 10
+ 255
Qt::Orientation::Horizontal
- -
-
+
-
+
+
+ <html><head/><body><p>Independent damper is an always on damper effect to smooth out acceleration spikes.</p></body></html>
+
- Permanent inertia
+ Permanent damper
- -
-
+
-
+
+
+ <html><head/><body><p>Centering force when FFB is inactive.</p><p>Turned off when game effects takes place</p></body></html>
+
- Permanent friction
+ Desktop spring (No FFB)
- -
-
+
-
+
+
+
+ 100
+ 0
+
+
255
-
- Qt::Orientation::Horizontal
-
- -
-
-
- 255
+
-
+
+
+ <html><head/><body><p>Degrees of rotation</p></body></html>
-
- Qt::Orientation::Horizontal
+
+ Range (deg)
- -
-
-
-
- 100
- 0
-
+
-
+
+
+ 10
- 255
+ 1440
+
+
+ 10
+
+
+ 10
+
+
+ Qt::Orientation::Horizontal
- -
-
+
-
+
100
0
+
+ false
+
255
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- FFB Power
-
-
-
- 10
-
-
- 5
-
-
-
-
-
- 0
- 0
-
-
+
- <html><head/><body><p>How stiff the endstop should be (Tune to taste while keeping noise low)</p></body></html>
+ <html><head/><body><p>Independent damper is an always on damper effect to smooth out acceleration spikes</p></body></html>
255
@@ -788,43 +398,55 @@
-
-
+
- <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
+ <html><head/><body><p>Centering force when FFB is inactive</p></body></html>
255
- 102
+ 0
- 102
+ 0
Qt::Orientation::Horizontal
- -
-
+
-
+
- 30
+ 100
0
+
+ false
+
+
+
+
+
+
+
+
+ 255
+
+
+
+ -
+
- 0
-(0%)
+ Permanent inertia
- -
-
+
-
+
100
@@ -835,84 +457,502 @@
false
-
+ °
+
+
+ 10
- 255
+ 32767
-
-
- -
-
-
- <html><head/><body><p>How much torque going to be used from the motor. <br/><br/>More power means more current being used from the motor as well as stronger ffb.</p></body></html>
+
+ 10
+
+
+
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+ Smoothing
+
+
+
+ 10
+
+
-
+
- Power
+ Effect equalizer
+
+
+ true
- -
-
-
- 32767
-
-
- 100
+
-
+
+
+
+ 0
+ 2
+
-
- Qt::Orientation::Horizontal
+
+
+ 0
+ 0
+
-
- 100
+
+
+ -
+
+
+ Reset gain
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Show Axis settings
+
+
+ false
+
+
+ true
+
+
+
+ 5
+
+
+ 5
+
-
-
-
- <html><head/><body><p>How much torque will be reserved to stop the motor for endstop effect.</p></body></html>
+
+
+ Axis Options
-
- Endstop gain
+
+
+ 5
+
+
+ 5
+
+
-
+
+
+ false
+
+
+ false
+
+
+ °/s
+
+
+ 0
+
+
+ 65535
+
+
+ 10
+
+
+ 0
+
+
+
+ -
+
+
+ Speed limit
+
+
+
+ -
+
+
+ false
+
+
+ Torque curve...
+
+
+
+ -
+
+
+ 1.0
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ <html><head/><body><p>Reverses the axis.<br/>Also reverses the forces so they keep pointing in the correct direction.</p></body></html>
+
+
+ Invert axis
+
+
+
+ -
+
+
+ false
+
+
+ Auto Detect
+
+
+
+ -
+
+
+ Expo torque
+
+
+
+ -
+
+
+ Apply
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+ Encoder
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Encoder reduction (prescaler)
+
+
+ false
+
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ :
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 0
+
+
+
+ 1
+
+
+ 256
+
+
+
+ -
+
+
+ Apply
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 0
+
+
+
+ 1
+
+
+ 256
+
+
+
+
+
+
+ -
+
+
+ Change enc
+
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+ QComboBox::SizeAdjustPolicy::AdjustToContents
+
+
+
+ -
+
+
+ Encoder tuning...
+
+
+
+ -
+
+
+ Encoder source
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+ 1
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
- -
-
-
- 0
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Motor driver
+
+
-
+
+
+ Change driver
+
+
+
+ -
+
+
+
+ 2
+ 0
+
+
+
+
+ -
+
+
+ Motor driver
+
+
+
+
-
-
-
- <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>
+
+
+ Qt::Orientation::Vertical
-
- Effects intensity
+
+ QSizePolicy::Policy::Expanding
-
+
+
+ 20
+ 40
+
+
+
- -
+
-
Qt::Orientation::Vertical
+
+ QSizePolicy::Policy::MinimumExpanding
+
20
- 10
+ 0
+
+
+ QChartView
+ QWidget
+
+ 1
+
+
@@ -922,12 +962,12 @@
setValue(int)
- 512
- 505
+ 673
+ 397
- 539
- 504
+ 930
+ 402
@@ -938,12 +978,12 @@
setValue(int)
- 551
- 510
+ 930
+ 402
- 520
- 509
+ 681
+ 397
@@ -954,12 +994,12 @@
setValue(int)
- 309
- 291
+ 470
+ 587
- 553
- 291
+ 930
+ 592
@@ -970,12 +1010,12 @@
setValue(int)
- 553
- 291
+ 930
+ 592
- 309
- 291
+ 470
+ 587
@@ -986,12 +1026,12 @@
setValue(int)
- 309
- 319
+ 470
+ 617
- 553
- 319
+ 930
+ 622
@@ -1002,12 +1042,12 @@
setValue(int)
- 553
- 319
+ 930
+ 622
- 309
- 319
+ 470
+ 617
@@ -1018,12 +1058,12 @@
setValue(int)
- 469
- 573
+ 630
+ 677
- 840
- 573
+ 930
+ 682
@@ -1034,12 +1074,12 @@
setValue(int)
- 469
- 546
+ 630
+ 647
- 840
- 546
+ 930
+ 652
@@ -1050,12 +1090,12 @@
setValue(int)
- 840
- 573
+ 930
+ 682
- 469
- 573
+ 630
+ 677
@@ -1066,12 +1106,12 @@
setValue(int)
- 840
- 546
+ 930
+ 652
- 469
- 546
+ 630
+ 647
diff --git a/res/ffbclass.ui b/res/ffbclass.ui
index 8d3fa786..0fb9744d 100644
--- a/res/ffbclass.ui
+++ b/res/ffbclass.ui
@@ -7,7 +7,7 @@
0
0
543
- 469
+ 542
@@ -32,10 +32,10 @@
-
- Qt::Vertical
+ Qt::Orientation::Vertical
- QSizePolicy::MinimumExpanding
+ QSizePolicy::Policy::MinimumExpanding
@@ -158,7 +158,7 @@
0
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -168,16 +168,16 @@
<html><head/><body><p>Intensity of friction effect</p></body></html>
- QFrame::NoFrame
+ QFrame::Shape::NoFrame
- QFrame::Raised
+ QFrame::Shadow::Raised
Friction Gain
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -209,7 +209,7 @@
0
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -250,14 +250,14 @@
0
- Qt::Horizontal
+ Qt::Orientation::Horizontal
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -267,16 +267,16 @@
<html><head/><body><p>Intensity of spring effects</p></body></html>
- QFrame::NoFrame
+ QFrame::Shape::NoFrame
- QFrame::Raised
+ QFrame::Shadow::Raised
Spring Gain
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -348,16 +348,16 @@
<html><head/><body><p>Intensity of damper effect to smooth out acceleration spikes.</p></body></html>
- QFrame::NoFrame
+ QFrame::Shape::NoFrame
- QFrame::Raised
+ QFrame::Shadow::Raised
Damper Gain
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -379,7 +379,7 @@
0
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -389,16 +389,16 @@
<html><head/><body><p>Intensity of inertia effects</p></body></html>
- QFrame::NoFrame
+ QFrame::Shape::NoFrame
- QFrame::Raised
+ QFrame::Shadow::Raised
Inertia Gain
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -454,7 +454,7 @@
- Static effect settings (Game effects)
+ Static && Periodic Filter (Game effects)
false
@@ -466,31 +466,6 @@
5
- -
-
-
- <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::Horizontal
-
-
-
-
@@ -498,6 +473,13 @@
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
-
@@ -520,16 +502,28 @@
- -
-
+
-
+
<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.
+
+ 10
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ 500
+
+
+ 4
+
+
+ 10
+
+
+ 10
+
+
+ Qt::Orientation::Horizontal
@@ -539,10 +533,10 @@
<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::NoFrame
+ QFrame::Shape::NoFrame
- QFrame::Raised
+ QFrame::Shadow::Raised
1
@@ -557,14 +551,74 @@
true
- Qt::AlignCenter
+ Qt::AlignmentFlag::AlignCenter
- -
-
-
- Qt::Vertical
+
-
+
+
+ Reconstruction Filter
+
+
+
+ -
+
+
-
+
+
+ <html><head/><body><p>Disable the reconstruction filter.</p></body></html>
+
+
+ None
+
+
+ true
+
+
+
+ -
+
+
+ <html><head/><body><p>The linear reconstruction filter takes the last two points sent by the game and calculates the linear line between the two points. </p><p>The FFB engine calculates subsamples every time on this straight line. </p><p>The game signal is slowed by 1 sample before being sent to the driver.</p></body></html>
+
+
+ Linear
+
+
+
+ -
+
+
+ <html><head/><body><p>The "High Fidelity" reconstruction filter takes the last four points sent by the game and calculates a spline between the four points. </p><p>The FFB engine calculates subsamples between the last two points by following this spline most faithfully. </p><p>The signal coming from the game is slowed down by 1.5 samples before being sent to the driver.</p></body></html>
+
+
+ High Fidelity
+
+
+
+ -
+
+
+ <html><head/><body><p>The "Mixed" reconstruction filter takes the last three points sent by the game and calculates a spline between these points, less faithfully than "HiFi" mode, but more finely than "Linear" mode. </p><p>The FFB engine calculates the subsamples between the last three points following this curve. The signal from the game is slowed down by 1 sample, like linear mode, before being sent to the driver.</p><p>This solution combines the advantage of spline precision and the responsiveness of linear mode, it is recommended by default.</p></body></html>
+
+
+ Mixed (optimum)
+
+
+
+
+
+ -
+
+
+ <html><head/><body><p>Q Factor for constant force low pass filter.<br/>Controls how sharp the response drops at the frequency limit.</p><p>Be careful to prevent oscillations by not limiting the bandwidth too much.</p></body></html>
+
+
+ Q
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
@@ -589,30 +643,30 @@
0
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- -
-
+
-
+
- <html><head/><body><p>Q Factor for constant force low pass filter.<br/>Controls how sharp the response drops at the frequency limit.</p><p>Be careful to prevent oscillations by not limiting the bandwidth too much.</p></body></html>
+ <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>
- Q
+ Freq.
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
- -
+
-
- Qt::Vertical
+ Qt::Orientation::Vertical
@@ -624,6 +678,25 @@
+
+ comboBox_reportrate
+ horizontalSlider_cffilter
+ horizontalSlider_CFq
+ doubleSpinBox_CFq
+ radioButton_reconfilter_0
+ radioButton_reconfilter_1
+ radioButton_reconfilter_2
+ radioButton_reconfilter_3
+ horizontalSlider_spring
+ doubleSpinBox_spring
+ horizontalSlider_damper
+ doubleSpinBox_damper
+ horizontalSlider_friction
+ doubleSpinBox_friction
+ horizontalSlider_inertia
+ doubleSpinBox_inertia
+ pushButton_advanced_tuning
+
diff --git a/res/profile.cfg b/res/profile.cfg
index 596a7884..cd04e7b5 100644
--- a/res/profile.cfg
+++ b/res/profile.cfg
@@ -1,8 +1,8 @@
{
- "release":1,
+ "release":2,
"callOrder": [
{"fullname":"FFB Wheel" ,"classname":"main", "key":["hidsendspd"]},
- {"fullname":"Effects", "classname":"fx", "key":["filterCfFreq","filterCfQ","spring","friction","damper","inertia"]},
- {"fullname":"Axis", "classname":"axis", "key":["power","degrees","fxratio","esgain","idlespring","axisdamper","axisfriction","axisinertia","maxspeed"]}
+ {"fullname":"Effects", "classname":"fx", "key":["filterCfFreq","filterCfQ","spring","friction","damper","inertia","reconFilterMode"]},
+ {"fullname":"Axis", "classname":"axis", "key":["power","degrees","fxratio","esgain","idlespring","axisdamper","axisfriction","axisinertia","maxspeed","equalizer","eqb1","eqb2","eqb3","eqb4","eqb5","eqb6"]}
]
}
\ No newline at end of file
diff --git a/res/profile.ui b/res/profile.ui
index 5b6bea76..6234efbc 100644
--- a/res/profile.ui
+++ b/res/profile.ui
@@ -11,7 +11,7 @@
-
+
0
0
diff --git a/res/tmc4671_ui.ui b/res/tmc4671_ui.ui
index 06edf6a2..ebe2f4a9 100644
--- a/res/tmc4671_ui.ui
+++ b/res/tmc4671_ui.ui
@@ -7,7 +7,7 @@
0
0
744
- 731
+ 820
@@ -20,6 +20,237 @@
TMC4671
+ -
+
+
+
+ 500
+ 16777215
+
+
+
+ More
+
+
+
-
+
+
+ Torque Filter
+
+
+
+ -
+
+
+ -
+
+
+ Torque Filter freq.
+
+
+
+ -
+
+
+ false
+
+
+
+ 100
+ 0
+
+
+
+ Hz
+
+
+ 10
+
+
+ 8192
+
+
+ 1000
+
+
+
+ -
+
+
+ <html><head/><body><p>Caution: can increase max speed but can also cause steppers to accelerate exponentially if too high</p></body></html>
+
+
+ Flux field weakening
+
+
+
+ -
+
+
+ false
+
+
+
+ 100
+ 0
+
+
+
+ <html><head/><body><p>Caution: can increase max speed but can also cause steppers to accelerate exponentially if too high</p></body></html>
+
+
+ false
+
+
+ A
+
+
+ 5.000000000000000
+
+
+ 0.100000000000000
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ -
+
+
+ HW Version
+
+
+ true
+
+
+
+ -
+
+
+ Select hardware version
+
+
+
+ -
+
+
+ Force encoder alignment
+
+
+
+ -
+
+
+ Start full calibration
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ -
+
+
+ State:
+
+
+
+ -
+
+
+ State
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+ -
+
+
+ Motor current:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 0A
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+ -
+
+
+ Temperature:
+
+
+
+ -
+
+
+ 0°C
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+ -
+
+
+ Voltage:
+
+
+
+ -
+
+
+ 0V
+
+
+ Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
+
+
+
+ -
+
+
+ <html><head/><body><p>Use flux component to dissipate energy before enabling the brake resistor. </p><p>Heats up the motor slightly more. Brake resistor will still operate normally.</p><p>Only disable if suitable brake resistor is installed.</p></body></html>
+
+
+ Prefer energy dissipation in motor
+
+
+
+ -
+
+
+ Cogging calibration
+
+
+
+
+
+
-
@@ -65,7 +296,7 @@
- -
+
-
<html><head/><body><p>Inverts forces received by the driver when combine with ext. encoder is active if the internal and external encoder directions don't match</p></body></html>
@@ -78,13 +309,13 @@
- -
+
-
- QFrame::NoFrame
+ QFrame::Shape::NoFrame
- QFrame::Raised
+ QFrame::Shadow::Raised
@@ -106,7 +337,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -119,7 +350,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -132,7 +363,101 @@
- -
+
-
+
+
+ 0
+
+
+
+ Current
+
+
+
-
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+ Cogging
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+
+ -
+
+
-
+
+
+ Reload table
+
+
+
+ -
+
+
+ Reset table
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LayoutDirection::LeftToRight
+
+
+ 28000
+
+
+ 0
+
+
+ true
+
+
+ %p% Power
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ -
@@ -305,7 +630,23 @@
- -
+
-
+
+
+ Qt::Orientation::Vertical
+
+
+ QSizePolicy::Policy::Expanding
+
+
+
+ 20
+ 0
+
+
+
+
+ -
@@ -399,285 +740,85 @@
- -
-
-
- QFrame::StyledPanel
-
-
- QFrame::Raised
-
-
-
- -
-
-
-
- 0
- 1
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Expanding
-
-
-
- 20
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::LeftToRight
-
-
- 28000
-
-
- 0
-
-
- true
-
-
- %p% Power
-
-
-
- -
-
-
-
- 500
- 16777215
-
-
+
-
+
- More
+ Anticogging
-
-
-
-
-
- Torque Filter
-
-
-
- -
-
-
- -
-
+
+
+ 9
+
+
+ 9
+
+
+ 9
+
+
+ 9
+
+
-
+
- Torque Filter freq.
-
-
-
- -
-
-
- false
-
-
-
- 100
- 0
-
-
-
- Hz
-
-
- 10
-
-
- 8192
-
-
- 1000
+ Apply anti-cogging (Exp.)
-
-
-
- <html><head/><body><p>Caution: can increase max speed but can also cause steppers to accelerate exponentially if too high</p></body></html>
+
+
+
+ 0
+ 0
+
- Flux field weakening
+ Magnitude
- -
-
-
- false
-
-
-
- 100
- 0
-
-
-
- <html><head/><body><p>Caution: can increase max speed but can also cause steppers to accelerate exponentially if too high</p></body></html>
-
-
- false
-
-
- A
+
-
+
+
+ -32767
- 5.000000000000000
+ 32767
- 0.100000000000000
-
-
-
- -
-
-
- Qt::Horizontal
+ 100
-
-
- -
-
-
- HW Version
+
+ 1000
-
+
true
-
-
- -
-
-
- Select hardware version
-
-
-
- -
-
-
- Force encoder alignment
-
-
-
- -
-
-
- Start full calibration
-
-
-
- -
-
- Qt::Horizontal
-
-
-
- -
-
-
- State:
-
-
-
- -
-
-
- State
+ Qt::Orientation::Horizontal
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Motor current:
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- 0A
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Temperature:
-
-
-
- -
-
-
- 0°C
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ false
-
-
- -
-
-
- Voltage:
+
+ false
- -
-
-
- 0V
+
-
+
+
+ 4
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ -3.276700000000000
-
-
- -
-
-
- <html><head/><body><p>Use flux component to dissipate energy before enabling the brake resistor. </p><p>Heats up the motor slightly more. Brake resistor will still operate normally.</p><p>Only disable if suitable brake resistor is installed.</p></body></html>
+
+ 3.276700000000000
-
- Prefer energy dissipation in motor
+
+ 0.000100000000000
diff --git a/tmc4671_ui.py b/tmc4671_ui.py
index 2717df0e..8f34f433 100644
--- a/tmc4671_ui.py
+++ b/tmc4671_ui.py
@@ -1,5 +1,5 @@
-from PyQt6.QtWidgets import QMessageBox,QVBoxLayout,QGroupBox,QComboBox,QLabel,QApplication
-from helper import res_path,classlistToIds,updateListComboBox
+from PyQt6.QtWidgets import QMessageBox,QVBoxLayout,QGroupBox,QComboBox,QLabel,QApplication,QDialog,QTextEdit,QPushButton
+from helper import res_path,classlistToIds,updateListComboBox,qtBlockAndCall
from PyQt6.QtCore import QTime, QTimer
from PyQt6.QtCore import Qt,QMargins
from PyQt6.QtGui import QColor
@@ -7,9 +7,10 @@
from base_ui import WidgetUI
from optionsdialog import OptionsDialog,OptionsDialogGroupBox
-from PyQt6.QtCharts import QChart,QChartView,QLineSeries,QValueAxis
+from PyQt6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis, QBarSeries, QBarSet
from base_ui import CommunicationHandler
+
ext_notice = """External encoder forwards the encoder
selection of the Axis (if available).
Please select the encoder there."""
@@ -22,7 +23,7 @@
class TMC4671Ui(WidgetUI,CommunicationHandler):
- STATES = ["uninitialized","waitPower","Shutdown","Running","EncoderInit","EncoderFinished","HardError","OverTemp","IndexSearch","FullCalibration","ExternalEncoderInit","PI Autotune"]
+ STATES = ["uninitialized","waitPower","Shutdown","Running","EncoderInit","EncoderFinished","HardError","OverTemp","IndexSearch","FullCalibration","ExternalEncoderInit","PI Autotune", "CoggingCalibration", "SlewRateCalibration", "NONE"]
def __init__(self, main=None, unique=0):
WidgetUI.__init__(self, main,'tmc4671_ui.ui')
@@ -33,6 +34,12 @@ def __init__(self, main=None, unique=0):
self.axis = unique
+ self.anti_coggingEnable = False
+ self.cogging_data = [0] * 128
+ self.cogging_data_received = [False] * 128
+ self.cogging_supported = False
+ self.cogging_dialog = None
+ self.cogging_text = ""
self.max_datapoints = 10000
self.max_datapointsVisibleTime = 30
self.adc_to_amps = 0#2.5 / (0x7fff * 60.0 * 0.0015)
@@ -51,6 +58,14 @@ def __init__(self, main=None, unique=0):
self.pushButton_align.clicked.connect(self.alignEnc)
self.pushButton_autotunepid.clicked.connect(self.autotunePid)
+ self.pushButton_cogging.clicked.connect(self.coggingDetection)
+ self.pushButton_resetCoggingTable.clicked.connect(self.resetCoggingTable)
+ self.pushButton_reloadCoggingTable.clicked.connect(self.reloadCoggingTable)
+ self.tabWidget.currentChanged.connect(self.tabChanged)
+ self.doubleSpinBox_coggScale.setMinimum(-3.2767)
+ self.doubleSpinBox_coggScale.setMaximum(3.2767)
+ self.doubleSpinBox_coggScale.setSingleStep(0.001)
+ self.doubleSpinBox_coggScale.setDecimals(4)
#self.initUi()
self.timer.timeout.connect(self.updateTimer)
@@ -97,6 +112,15 @@ def __init__(self, main=None, unique=0):
self.lines_Flux.attachAxis(self.chartYaxis_Amps)
self.lines_Flux.attachAxis(self.chartXaxis)
+ self.lines_Cogging = QLineSeries(self.chart)
+ self.lines_Cogging.setName("Cogging A")
+ self.lines_Cogging.setOpacity(0.5)
+ self.lines_Cogging.setUseOpenGL(True)
+ self.chart.addSeries(self.lines_Cogging)
+ self.lines_Cogging.setColor(QColor("purple"))
+ self.lines_Cogging.attachAxis(self.chartYaxis_Amps)
+ self.lines_Cogging.attachAxis(self.chartXaxis)
+
self.lines_Temps = QLineSeries(self.chart)
self.lines_Temps.setName("Temp °C")
self.lines_Temps.setColor(QColor("orange"))
@@ -119,6 +143,40 @@ def __init__(self, main=None, unique=0):
ax.setLabelsBrush(QApplication.instance().palette().text())
+ # Cogging Chart setup
+ self.chart_cogging = QChart()
+ self.chart_cogging.setBackgroundRoundness(5)
+ self.chart_cogging.setMargins(QMargins(0,0,0,0))
+ self.chart_cogging_Xaxis = QValueAxis(self.chart_cogging)
+ self.chart_cogging_Xaxis.setGridLineColor(QColor(QApplication.instance().palette().dark().color().red(),QApplication.instance().palette().dark().color().green(),QApplication.instance().palette().dark().color().blue(),128))
+ self.chart_cogging.addAxis(self.chart_cogging_Xaxis,Qt.AlignmentFlag.AlignBottom)
+
+ self.chart_cogging_Yaxis = QValueAxis(self.chart_cogging)
+ self.chart_cogging_Yaxis.setGridLineColor(QColor(QApplication.instance().palette().dark().color().red(),QApplication.instance().palette().dark().color().green(),QApplication.instance().palette().dark().color().blue(),64))
+ self.chart_cogging.setBackgroundBrush(QApplication.instance().palette().window())
+
+ self.chart_cogging.addAxis(self.chart_cogging_Yaxis,Qt.AlignmentFlag.AlignLeft)
+
+ self.bar_set_cogging = QBarSet("Harmonics")
+ self.bar_series_cogging = QBarSeries()
+ self.bar_series_cogging.append(self.bar_set_cogging)
+
+ self.chart_cogging.addSeries(self.bar_series_cogging)
+ self.bar_set_cogging.setColor(QColor("cornflowerblue"))
+ self.bar_series_cogging.attachAxis(self.chart_cogging_Yaxis)
+ self.bar_series_cogging.attachAxis(self.chart_cogging_Xaxis)
+
+ self.chart_cogging_Xaxis.setRange(1, 128)
+ self.chart_cogging_Yaxis.setMin(0)
+ self.chart_cogging_Yaxis.setMax(10)
+ self.graphWidget_Cogging.setRubberBand(QChartView.RubberBand.VerticalRubberBand)
+ self.graphWidget_Cogging.setChart(self.chart_cogging)
+
+ self.chart_cogging.legend().setLabelBrush(QApplication.instance().palette().text())
+ for ax in self.chart_cogging.axes():
+ ax.setLabelsBrush(QApplication.instance().palette().text())
+
+
self.checkBox_advancedpid.stateChanged.connect(self.advancedPidChanged)
self.lastPrecP = self.checkBox_P_Precision.isChecked()
self.lastPrecI = self.checkBox_I_Precision.isChecked()
@@ -185,6 +243,11 @@ def __init__(self, main=None, unique=0):
self.register_callback("tmc","calibrated",self.calibrated,instance=self.axis,conversion=int)
+ self.register_callback("tmc","coggingTable",self.updateCogging,self.axis,str)
+ self.register_callback("tmc","calibrateCogging",self.coggingDetectionMsg,self.axis,str)
+ self.register_callback("tmc","cogging",self.anticoggingStatus,self.axis,int,typechar='?')
+ self.register_callback("tmc","coggingScale",self.coggingScaleCb,self.axis,int)
+
self.checkBox_combineEncoders.stateChanged.connect(self.extEncoderChanged)
@@ -207,18 +270,41 @@ def hideEvent(self,event):
self.timer.stop()
self.timer_status.stop()
- def motorselChanged(self,val):
+ def coggingSupportedCb(self, info):
+ self.cogging_supported = (info != -1)
+ self.updateMotorUI()
+
+ def anticoggingStatus(self, data):
+ self.anti_coggingEnable = (data == 1)
+ self.updateMotorUI()
+
+ def motorselChanged(self, val):
+ self.updateMotorUI()
+
+ def updateMotorUI(self):
data = self.comboBox_mtype.currentData()
- if(data == 2 or data == 3): # stepper or bldc
- self.spinBox_poles.setEnabled(True)
- self.doubleSpinBox_fluxoffset.setEnabled(True)
- self.checkBox_fluxdissipate.setEnabled(True)
- self.pushButton_autotunepid.setEnabled(True)
- else:
- self.spinBox_poles.setEnabled(False)
- self.doubleSpinBox_fluxoffset.setEnabled(False)
- self.checkBox_fluxdissipate.setEnabled(False)
- self.pushButton_autotunepid.setEnabled(False)
+ supported_motor = (data == 2 or data == 3) # stepper or bldc
+
+ self.spinBox_poles.setEnabled(supported_motor)
+ self.doubleSpinBox_fluxoffset.setEnabled(supported_motor)
+ self.checkBox_fluxdissipate.setEnabled(supported_motor)
+ self.pushButton_autotunepid.setEnabled(supported_motor)
+
+ # Cogging visibility depends on motor support AND firmware command existence
+ cogging_enabled = supported_motor and self.cogging_supported
+ self.pushButton_cogging.setEnabled(cogging_enabled)
+ self.checkBox_cogging.setEnabled(cogging_enabled)
+ self.groupBox_anticogging.setEnabled(cogging_enabled)
+ self.tabWidget.setTabEnabled(1, cogging_enabled)
+
+
+ # If anti-cogging was enabled, notify user and disable it before graying out
+ self.checkBox_cogging.setChecked(self.anti_coggingEnable)
+ if self.anti_coggingEnable:
+ if not supported_motor:
+ #msg = QMessageBox(QMessageBox.Icon.Information,self.tr("Anti-Cogging"),self.tr("Auto-disabling Anti-Cogging on this motor"))
+ #msg.exec()
+ self.checkBox_cogging.setChecked(False)
if(data == 3):
self.checkBox_svpwm.setEnabled(True)
@@ -269,24 +355,38 @@ def updateCurrent(self,torqueflux):
tflist = [(int(v)) for v in torqueflux.split(":")]
flux = None
+ cogging = None
torque = abs(tflist[0])
- if(len(tflist) == 2):
+ if len(tflist) >= 2:
flux = tflist[1]
- currents = complex(torque,flux)
+ if len(tflist) >= 3:
+ cogging = tflist[2]
+
+ currents = complex(torque, flux if flux is not None else 0)
try:
torque = abs(float(torque))
if self.adc_to_amps != 0:
amps = currents * self.adc_to_amps
txt = f"Torque: {amps.real:+.3f}A"
- if(flux != None):
+
+ total_amps = abs(amps.real)
+ if flux is not None:
txt += f"\nFlux: {amps.imag:+.3f}A"
- txt += f"\nTotal: {abs(amps):.3f}A"
+ total_amps += abs(amps.imag)
+ if cogging is not None:
+ c_amps = cogging * self.adc_to_amps
+ txt += f"\nCogging: {c_amps:+.3f}A"
+ total_amps += abs(c_amps)
+ if flux is not None or cogging is not None:
+ txt += f"\nTotal: {total_amps:.3f}A"
+
self.label_Current.setText(txt)
else:
amps = 100*currents / 0x7fff # percent
- self.label_Current.setText(str(round(amps.real,3))+"%")
+ txt = str(round(amps.real,3))+"%"
+ self.label_Current.setText(txt)
self.progressBar_power.setValue(int(abs(currents)))
@@ -294,18 +394,65 @@ def updateCurrent(self,torqueflux):
self.lines_Amps.append(self.chartLastX,amps.real)
self.lines_Flux.append(self.chartLastX,abs(amps.imag))
+ cogging_val = 0
+ if cogging is not None:
+ if self.adc_to_amps != 0:
+ cogging_val = cogging * self.adc_to_amps
+ else:
+ cogging_val = 100 * cogging / 0x7fff
+ self.lines_Cogging.append(self.chartLastX, cogging_val)
+
if(self.lines_Amps.count() > self.max_datapoints):
self.lines_Amps.remove(0)
self.lines_Flux.remove(0)
- scalemax = max(abs(amps.imag),abs(amps.real))
+
+ if self.lines_Cogging.count() > self.max_datapoints:
+ self.lines_Cogging.remove(0)
+
+ scalemax = max(abs(amps.imag), abs(amps.real), abs(cogging_val))
if(scalemax > self.chartYaxis_Amps.max()):
self.chartYaxis_Amps.setMax(round(scalemax,2)) # increase range
+
+ if cogging_val < self.chartYaxis_Amps.min():
+ self.chartYaxis_Amps.setMin(round(cogging_val, 2)) # increase range downwards
self.chartXaxis.setMax(self.chartLastX)
self.chartXaxis.setMin(max(self.lines_Amps.at(0).x(),max(0,self.chartLastX-self.max_datapointsVisibleTime)))
except Exception as e:
- self.main.log("TMC update error: " + str(e))
+ self.main.log("TMC update error: " + str(e))
+
+ def updateCogging(self,data):
+ try:
+ if "data" in data:
+ # Correctly parse the "item:X,data:(Y,Z,...)" format
+ item_str, data_str = data.split(',', 1)
+ start_index = int(item_str.split(':')[1])
+
+ # Extract the numbers from within the parentheses
+ values_str = data_str.split('(')[1].split(')')[0]
+ points = [float(p) for p in values_str.split(',') if p]
+
+ for i, p in enumerate(points):
+ if start_index + i < len(self.cogging_data):
+ self.cogging_data[start_index + i] = p
+ self.cogging_data_received[start_index + i] = True
+
+ # Redraw the entire graph with the updated data
+ self.bar_set_cogging.remove(0, self.bar_set_cogging.count())
+ # Add a dummy zero at index 0 so that harmonics 1-128 align with X axis values 1-128
+ self.bar_set_cogging.append(0.0)
+ self.bar_set_cogging.append(self.cogging_data)
+
+ self.chart_cogging_Xaxis.setRange(1, 128)
+
+ valid_data = [p for i, p in enumerate(self.cogging_data) if self.cogging_data_received[i]]
+ if valid_data:
+ self.chart_cogging_Yaxis.setMax(max(10, max(valid_data)))
+ self.chart_cogging_Yaxis.setMin(0)
+
+ except Exception as e:
+ self.main.log("TMC cogging update error: " + str(e))
def updateTemp(self,t):
t = t/100.0
@@ -367,6 +514,8 @@ def submitMotor(self):
self.send_value("tmc","combineEncoder",val = 1 if self.checkBox_combineEncoders.isChecked() else 0,instance=self.axis)
self.send_value("tmc","invertForce",val = 1 if self.checkBox_invertForce.isChecked() else 0,instance=self.axis)
+ self.send_value("tmc","cogging",val = 1 if self.checkBox_cogging.isChecked() else 0,instance=self.axis)
+ self.send_value("tmc","coggingScale",val=self.horizontalSlider_coggmag.value(),instance=self.axis)
def submitPid(self):
# PIDs
@@ -446,13 +595,19 @@ def init_ui(self):
self.lines_Amps.clear()
self.lines_Temps.clear()
self.lines_Flux.clear()
+ self.lines_Cogging.clear()
+ self.clearCoggingGraph()
self.chartYaxis_Amps.setMin(0)
self.chartYaxis_Temps.setMin(0)
self.chartYaxis_Temps.setMax(90)
try:
# Fill encoder source types
self.send_commands("tmc",["mtype","encsrc","tmcHwType","trqbq_mode"],self.axis,'!')
- self.send_commands("tmc",["tmctype","tmcHwType","iScale","trqbq_f"],self.axis)
+ self.send_commands("tmc",["tmctype","tmcHwType","iScale","calibrated","trqbq_f","coggingScale"],self.axis)
+ self.send_command("tmc","cogging",self.axis,'?')
+
+ # Check if cogging is supported
+ self.get_value_async("tmc", "cmdinfo", self.coggingSupportedCb, self.axis, conversion=int, adr=44)
self.getMotor()
self.getPids()
if not self.init_done:
@@ -461,9 +616,13 @@ def init_ui(self):
self.pushButton_submitpid.clicked.connect(self.submitPid)
self.comboBox_torqueFilter.currentIndexChanged.connect(self.torqueFilterChanged)
self.spinBox_torqueFilterFreq.valueChanged.connect(lambda x : self.send_value("tmc","trqbq_f",x,instance=self.axis))
+ self.horizontalSlider_coggmag.valueChanged.connect(self.coggingScaleChanged)
+ self.doubleSpinBox_coggScale.valueChanged.connect(self.coggingSpinBoxChanged)
self.init_done = True
# Check if calibrated
+ if self.tabWidget.currentWidget() == self.tab_6:
+ self.reloadCoggingTable()
except Exception as e:
self.main.log("Error initializing TMC tab. Please reconnect: " + str(e))
return False
@@ -485,7 +644,6 @@ def calibrated(self,v):
if not v and self.isEnabled() and self.comboBox_mtype.currentIndex() != 0 and self.comboBox_enc.currentIndex() != 0:
# Warning displayed
def cb(ret):
- print(ret)
if ret == QMessageBox.StandardButton.Ok:
self.send_command("tmc","calibrate",self.axis)
self.calmsg.finished.connect(cb)
@@ -525,6 +683,102 @@ def f(res):
self.get_value_async("tmc","encalign",f,self.axis,typechar='?')
self.main.log("Started encoder alignment")
+ def coggingDetectionMsg(self, data):
+ if data:
+ msg_text = str(data)
+ status = 0
+ # Parsing du nouveau format: ("message de log",0)
+ if msg_text.startswith('("') and msg_text.endswith(')'):
+ parts = msg_text.rsplit('",', 1)
+ if len(parts) == 2:
+ text_part = parts[0][2:] # Remove '("'
+ try:
+ status = int(parts[1][:-1]) # Remove ')'
+ msg_text = text_part
+ except ValueError:
+ pass
+
+ # Append new message to the accumulated text
+ if self.cogging_text:
+ self.cogging_text += "\n"
+ self.cogging_text += msg_text
+
+ if self.cogging_dialog is None:
+ # Create and show a resizable QDialog with a QTextEdit
+ self.cogging_dialog = QDialog(self)
+ self.cogging_dialog.setWindowTitle(self.tr("Cogging calibration"))
+ self.cogging_dialog.setMinimumSize(500, 400)
+
+ layout = QVBoxLayout(self.cogging_dialog)
+
+ self.cogging_text_edit = QTextEdit()
+ self.cogging_text_edit.setReadOnly(True)
+ layout.addWidget(self.cogging_text_edit)
+
+ close_btn = QPushButton(self.tr("Close"))
+ close_btn.clicked.connect(self.cogging_dialog.close)
+ layout.addWidget(close_btn)
+
+ self.cogging_dialog.show()
+ # Reset state when closed
+ def on_finish():
+ self.cogging_dialog = None
+ self.cogging_text = ""
+ self.cogging_text_edit = None
+ self.timer.start(50)
+ self.timer_status.start(250)
+ self.cogging_dialog.finished.connect(on_finish)
+
+ # Update text and scroll to bottom
+ self.cogging_text_edit.setText(self.cogging_text)
+ self.cogging_text_edit.verticalScrollBar().setValue(self.cogging_text_edit.verticalScrollBar().maximum())
+
+ # Automatically handle end of calibration
+ if status == 1:
+ self.pushButton_cogging.setEnabled(True)
+ self.timer.start(50)
+ self.timer_status.start(250)
+ self.reloadCoggingTable()
+ self.send_command("tmc", "cogging", self.axis, '?')
+
+ def coggingDetection(self):
+ self.pushButton_cogging.setEnabled(False)
+ self.timer.stop()
+ self.timer_status.stop()
+ self.send_command("tmc","calibrateCogging", self.axis)
+ self.main.log("Started cogging detection")
+
+ def tabChanged(self, index):
+ # Automatically reload the cogging table when its tab is selected
+ if self.tabWidget.widget(index) == self.tab_6:
+ self.reloadCoggingTable()
+
+ def clearCoggingGraph(self):
+ self.bar_set_cogging.remove(0, self.bar_set_cogging.count())
+ self.cogging_data = [0] * 128
+ self.cogging_data_received = [False] * 128
+ # Reset axes to default values
+ self.chart_cogging_Yaxis.setMin(0)
+ self.chart_cogging_Yaxis.setMax(10)
+
+ def resetCoggingTable(self):
+ self.send_value("tmc", "coggingTable", 0, instance=self.axis)
+ self.clearCoggingGraph()
+
+ def reloadCoggingTable(self):
+ self.clearCoggingGraph()
+ self.send_command("tmc", "coggingTable", self.axis, '?')
+
+ def coggingScaleChanged(self, val):
+ qtBlockAndCall(self.doubleSpinBox_coggScale, self.doubleSpinBox_coggScale.setValue, val / 10000.0)
+
+ def coggingSpinBoxChanged(self, val):
+ slider_val = int(round(val * 10000.0))
+ qtBlockAndCall(self.horizontalSlider_coggmag, self.horizontalSlider_coggmag.setValue, slider_val)
+
+ def coggingScaleCb(self, val):
+ qtBlockAndCall(self.horizontalSlider_coggmag, self.horizontalSlider_coggmag.setValue, val)
+ qtBlockAndCall(self.doubleSpinBox_coggScale, self.doubleSpinBox_coggScale.setValue, val / 10000.0)
def getMotor(self):
commands=["mtype","poles","encsrc","cpr","abnindex","abnpol","combineEncoder","invertForce","fluxbrake","calibrated"]
@@ -584,3 +838,4 @@ def typeCb(self,entries):
def readValues(self):
self.get_value_async("tmc","tmcHwType",self.typeCb,self.axis,str,typechar='!')
+