From c59e3e7ac4d1f755a5b2da063ca5a3fa1a771c43 Mon Sep 17 00:00:00 2001
From: atse <atse@m-labs.hk>
Date: Wed, 31 Jul 2024 16:02:12 +0800
Subject: [PATCH] ctrl_panel: Remove need for "mA" hack

Remove all instances of mA scaling scattered all around the code and
specify it in the parameter tree with a single source of truth.

Done by adding the option "pinSiPrefix" for all Parameters of type `int`
or `float`, and using it for current Parameters with unit "mA".
---
 pytec/pytec/gui/view/ctrl_panel.py    | 11 ++--
 pytec/pytec/gui/view/param_tree.json  | 56 +++++++++-------
 pytec/pytec/gui/view/pin_si_prefix.py | 95 +++++++++++++++++++++++++++
 pytec/tec_qt.py                       |  3 -
 4 files changed, 135 insertions(+), 30 deletions(-)
 create mode 100644 pytec/pytec/gui/view/pin_si_prefix.py

diff --git a/pytec/pytec/gui/view/ctrl_panel.py b/pytec/pytec/gui/view/ctrl_panel.py
index b68c4fd..c907591 100644
--- a/pytec/pytec/gui/view/ctrl_panel.py
+++ b/pytec/pytec/gui/view/ctrl_panel.py
@@ -4,6 +4,7 @@ from pyqtgraph.parametertree import (
     Parameter,
     registerParameterType,
 )
+import pytec.gui.view.pin_si_prefix
 
 
 class MutexParameter(pTypes.ListParameter):
@@ -136,10 +137,10 @@ class CtrlPanel(QObject):
                 )
                 self.params[channel].child(
                     "pid", "pid_output_clamping", "output_min"
-                ).setValue(settings["parameters"]["output_min"] * 1000)
+                ).setValue(settings["parameters"]["output_min"])
                 self.params[channel].child(
                     "pid", "pid_output_clamping", "output_max"
-                ).setValue(settings["parameters"]["output_max"] * 1000)
+                ).setValue(settings["parameters"]["output_max"])
                 self.params[channel].child(
                     "output", "control_method", "target"
                 ).setValue(settings["target"])
@@ -154,7 +155,7 @@ class CtrlPanel(QObject):
                 )
                 self.params[channel].child(
                     "output", "control_method", "i_set"
-                ).setValue(settings["i_set"] * 1000)
+                ).setValue(settings["i_set"])
                 if settings["temperature"] is not None:
                     self.params[channel].child("temperature").setValue(
                         settings["temperature"]
@@ -188,10 +189,10 @@ class CtrlPanel(QObject):
                     pwm_params["max_v"]["value"]
                 )
                 self.params[channel].child("output", "limits", "max_i_pos").setValue(
-                    pwm_params["max_i_pos"]["value"] * 1000
+                    pwm_params["max_i_pos"]["value"]
                 )
                 self.params[channel].child("output", "limits", "max_i_neg").setValue(
-                    pwm_params["max_i_neg"]["value"] * 1000
+                    pwm_params["max_i_neg"]["value"]
                 )
 
             for limit in "max_i_pos", "max_i_neg", "max_v":
diff --git a/pytec/pytec/gui/view/param_tree.json b/pytec/pytec/gui/view/param_tree.json
index 1ebc70d..d737f9c 100644
--- a/pytec/pytec/gui/view/param_tree.json
+++ b/pytec/pytec/gui/view/param_tree.json
@@ -48,14 +48,16 @@
                      "title": "Set Current",
                      "type": "float",
                      "value": 0,
-                     "step": 100,
+                     "step": 0.1,
                      "limits": [
-                        -2000,
-                        2000
+                        -2,
+                        2
                      ],
                      "triggerOnShow": true,
                      "decimals": 6,
-                     "suffix": "mA",
+                     "pinSiPrefix": "m",
+                     "suffix": "A",
+                     "siPrefix": true,
                      "param": [
                         "pwm",
                         "ch",
@@ -97,13 +99,15 @@
                      "title": "Max Cooling Current",
                      "type": "float",
                      "value": 0,
-                     "step": 100,
+                     "step": 0.1,
                      "decimals": 6,
                      "limits": [
                         0,
-                        2000
+                        2
                      ],
-                     "suffix": "mA",
+                     "siPrefix": true,
+                     "pinSiPrefix": "m",
+                     "suffix": "A",
                      "param": [
                         "pwm",
                         "ch",
@@ -117,13 +121,15 @@
                      "title": "Max Heating Current",
                      "type": "float",
                      "value": 0,
-                     "step": 100,
+                     "step": 0.1,
                      "decimals": 6,
+                     "siPrefix": true,
+                     "pinSiPrefix": "m",
+                     "suffix": "A",
                      "limits": [
                         0,
-                        2000
+                        2
                      ],
-                     "suffix": "mA",
                      "param": [
                         "pwm",
                         "ch",
@@ -296,13 +302,15 @@
                      "name": "output_min",
                      "title": "Minimum",
                      "type": "float",
-                     "step": 100,
+                     "step": 0.1,
                      "limits": [
-                        -2000,
-                        2000
+                        -2,
+                        2
                      ],
                      "decimals": 6,
-                     "suffix": "mA",
+                     "siPrefix": true,
+                     "pinSiPrefix": "m",
+                     "suffix": "A",
                      "param": [
                         "pid",
                         "ch",
@@ -315,13 +323,15 @@
                      "name": "output_max",
                      "title": "Maximum",
                      "type": "float",
-                     "step": 100,
+                     "step": 0.1,
                      "limits": [
-                        -2000,
-                        2000
+                        -2,
+                        2
                      ],
                      "decimals": 6,
-                     "suffix": "mA",
+                     "siPrefix": true,
+                     "pinSiPrefix": "m",
+                     "suffix": "A",
                      "param": [
                         "pid",
                         "ch",
@@ -358,12 +368,14 @@
                      "type": "float",
                      "value": 0,
                      "decimals": 6,
-                     "step": 100,
+                     "step": 0.1,
                      "limits": [
-                        -2000,
-                        2000
+                        -2,
+                        2
                      ],
-                     "suffix": "mA",
+                     "siPrefix": true,
+                     "pinSiPrefix": "m",
+                     "suffix": "A",
                      "pid_autotune": [
                         "test_current",
                         "ch"
diff --git a/pytec/pytec/gui/view/pin_si_prefix.py b/pytec/pytec/gui/view/pin_si_prefix.py
new file mode 100644
index 0000000..1b0ad3d
--- /dev/null
+++ b/pytec/pytec/gui/view/pin_si_prefix.py
@@ -0,0 +1,95 @@
+from pyqtgraph import SpinBox
+import pyqtgraph.functions as fn
+from pyqtgraph.parametertree import registerParameterItemType
+from pyqtgraph.parametertree.parameterTypes import SimpleParameter, NumericParameterItem
+
+
+class PinSIPrefixSpinBox(SpinBox):
+    """
+    Extension of PyQtGraph's SpinBox widget.
+    Adds:
+
+      * The "pinSiPrefix" option, where the siPrefix could be fixed to a
+        particular scale instead of as determined by its value.
+    """
+
+    def setOpts(self, **opts):
+        if "pinSiPrefix" in opts:
+            self.opts["pinSiPrefix"] = opts.pop("pinSiPrefix")
+
+        super().setOpts(**opts)
+
+    def formatText(self, prev=None):
+        """
+        In addition to pyqtgraph.SpinBox's formatting, incorporate the
+        'pinSiPrefix' mechanism, where SI prefixes could be fixed.
+        """
+        # Code modified from the PyQtGraph source
+
+        # get the number of decimal places to print
+        decimals = self.opts['decimals']
+        suffix = self.opts['suffix']
+        prefix = self.opts['prefix']
+        pin_si_prefix = self.opts.get("pinSiPrefix")
+
+        # format the string
+        val = self.value()
+        if self.opts['siPrefix'] is True:
+            # SI prefix was requested, so scale the value accordingly
+            if pin_si_prefix is not None and pin_si_prefix in fn.SI_PREFIX_EXPONENTS:
+                # fixed scale
+                s = 10**-fn.SI_PREFIX_EXPONENTS[pin_si_prefix]
+                p = pin_si_prefix
+            elif self.val == 0 and prev is not None:
+                # special case: if it's zero use the previous prefix
+                (s, p) = fn.siScale(prev)
+            else:
+                (s, p) = fn.siScale(val)
+            parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val, 'prefix':prefix}
+
+        else:
+            # no SI prefix /suffix requested; scale is 1
+            parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': '', 'scaledValue': val, 'prefix':prefix}
+
+        parts['prefixGap'] = '' if parts['prefix'] == '' else ' '
+        parts['suffixGap'] = '' if (parts['suffix'] == '' and parts['siPrefix'] == '') else ' '
+
+        return self.opts['format'].format(**parts)
+
+
+class PinSIPrefixNumericParameterItem(NumericParameterItem):
+    """
+    Subclasses PyQtGraph's `NumericParameterItem` and uses
+    PinSIPrefixSpinBox for editing.
+    """
+
+    def makeWidget(self):
+        opts = self.param.opts
+        t = opts['type']
+        defs = {
+            'value': 0, 'min': None, 'max': None,
+            'step': 1.0, 'dec': False,
+            'siPrefix': False, 'suffix': '', 'decimals': 3,
+            'pinSiPrefix': None,
+        }
+        if t == 'int':
+            defs['int'] = True
+            defs['minStep'] = 1.0
+        for k in defs:
+            if k in opts:
+                defs[k] = opts[k]
+        if 'limits' in opts:
+            defs['min'], defs['max'] = opts['limits']
+        w = PinSIPrefixSpinBox()
+        w.setOpts(**defs)
+        w.sigChanged = w.sigValueChanged
+        w.sigChanging = w.sigValueChanging
+        return w
+
+
+registerParameterItemType(
+    "float", PinSIPrefixNumericParameterItem, SimpleParameter, override=True
+)
+registerParameterItemType(
+    "int", PinSIPrefixNumericParameterItem, SimpleParameter, override=True
+)
diff --git a/pytec/tec_qt.py b/pytec/tec_qt.py
index 6047e34..0ec1a18 100644
--- a/pytec/tec_qt.py
+++ b/pytec/tec_qt.py
@@ -267,9 +267,6 @@ class MainWindow(QtWidgets.QMainWindow):
         for inner_param, change, data in changes:
             if change == "value":
                 if inner_param.opts.get("param", None) is not None:
-                    if inner_param.opts.get("suffix", None) == "mA":
-                        data /= 1000  # Given in mA
-
                     thermostat_param = inner_param.opts["param"]
                     if thermostat_param[1] == "ch":
                         thermostat_param[1] = ch