forked from M-Labs/thermostat
GUI: Control Panel changes #2
|
@ -28,7 +28,7 @@ class PIDAutoTuner(QObject):
|
||||||
def load_params_and_set_ready(self, ch):
|
def load_params_and_set_ready(self, ch):
|
||||||
self.autotuners[ch].setParam(
|
self.autotuners[ch].setParam(
|
||||||
self.target_temp[ch],
|
self.target_temp[ch],
|
||||||
self.test_current[ch] / 1000,
|
self.test_current[ch],
|
||||||
self.temp_swing[ch],
|
self.temp_swing[ch],
|
||||||
1 / self.sampling_interval[ch],
|
1 / self.sampling_interval[ch],
|
||||||
self.lookback[ch],
|
self.lookback[ch],
|
||||||
|
|
|
@ -117,14 +117,14 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
||||||
async def save_cfg(self, ch):
|
async def save_cfg(self, ch):
|
||||||
await self._client.save_config(ch)
|
await self._client.save_config(ch)
|
||||||
self.info_box_trigger.emit(
|
self.info_box_trigger.emit(
|
||||||
"Config saved", f"Channel {ch} Config has been saved from flash."
|
"Settings saved", f"Channel {ch} Settings has been saved to flash."
|
||||||
)
|
)
|
||||||
|
|
||||||
@asyncSlot()
|
@asyncSlot()
|
||||||
async def load_cfg(self, ch):
|
async def load_cfg(self, ch):
|
||||||
await self._client.load_config(ch)
|
await self._client.load_config(ch)
|
||||||
self.info_box_trigger.emit(
|
self.info_box_trigger.emit(
|
||||||
"Config loaded", f"Channel {ch} Config has been loaded from flash."
|
"Settings loaded", f"Channel {ch} Settings has been loaded from flash."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def dfu(self):
|
async def dfu(self):
|
||||||
|
|
|
@ -4,42 +4,14 @@ from pyqtgraph.parametertree import (
|
||||||
Parameter,
|
Parameter,
|
||||||
registerParameterType,
|
registerParameterType,
|
||||||
)
|
)
|
||||||
|
import pytec.gui.view.lockable_unit
|
||||||
|
|
||||||
|
|
||||||
class MutexParameter(pTypes.ListParameter):
|
def set_tree_label_tips(tree):
|
||||||
"""
|
for item in tree.listAllItems():
|
||||||
Mutually exclusive parameter where only one of its children is visible at a time, list selectable.
|
p = item.param
|
||||||
|
if "tip" in p.opts:
|
||||||
The ordering of the list items determines which children will be visible.
|
item.setToolTip(0, p.opts["tip"])
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, **opts):
|
|
||||||
super().__init__(**opts)
|
|
||||||
|
|
||||||
self.sigValueChanged.connect(self.show_chosen_child)
|
|
||||||
self.sigValueChanged.emit(self, self.opts["value"])
|
|
||||||
|
|
||||||
def _get_param_from_value(self, value):
|
|
||||||
if isinstance(self.opts["limits"], dict):
|
|
||||||
values_list = list(self.opts["limits"].values())
|
|
||||||
else:
|
|
||||||
values_list = self.opts["limits"]
|
|
||||||
|
|
||||||
return self.children()[values_list.index(value)]
|
|
||||||
|
|
||||||
@pyqtSlot(object, object)
|
|
||||||
def show_chosen_child(self, value):
|
|
||||||
for param in self.children():
|
|
||||||
param.hide()
|
|
||||||
|
|
||||||
child_to_show = self._get_param_from_value(value.value())
|
|
||||||
child_to_show.show()
|
|
||||||
|
|
||||||
if child_to_show.opts.get("triggerOnShow", None):
|
|
||||||
child_to_show.sigValueChanged.emit(child_to_show, child_to_show.value())
|
|
||||||
|
|
||||||
|
|
||||||
registerParameterType("mutex", MutexParameter)
|
|
||||||
|
|
||||||
|
|
||||||
class CtrlPanel(QObject):
|
class CtrlPanel(QObject):
|
||||||
|
@ -58,55 +30,56 @@ class CtrlPanel(QObject):
|
||||||
self.trees_ui = trees_ui
|
self.trees_ui = trees_ui
|
||||||
self.NUM_CHANNELS = len(trees_ui)
|
self.NUM_CHANNELS = len(trees_ui)
|
||||||
|
|
||||||
self.THERMOSTAT_PARAMETERS = [param_tree for i in range(self.NUM_CHANNELS)]
|
def _set_value_with_lock(self, value):
|
||||||
|
if not self.opts.get("lock"):
|
||||||
|
self.setValue(value)
|
||||||
|
Parameter.set_value_with_lock = _set_value_with_lock
|
||||||
|
|
||||||
self.params = [
|
self.params = [
|
||||||
Parameter.create(
|
Parameter.create(
|
||||||
name=f"Thermostat Channel {ch} Parameters",
|
name=f"Thermostat Channel {ch} Parameters",
|
||||||
type="group",
|
type="group",
|
||||||
value=ch,
|
value=ch,
|
||||||
children=self.THERMOSTAT_PARAMETERS[ch],
|
children=param_tree,
|
||||||
)
|
)
|
||||||
for ch in range(self.NUM_CHANNELS)
|
for ch in range(self.NUM_CHANNELS)
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, param in enumerate(self.params):
|
for ch, tree in enumerate(self.trees_ui):
|
||||||
param.channel = i
|
|
||||||
|
|
||||||
for i, tree in enumerate(self.trees_ui):
|
|
||||||
tree.setHeaderHidden(True)
|
tree.setHeaderHidden(True)
|
||||||
tree.setParameters(self.params[i], showTop=False)
|
tree.setParameters(self.params[ch], showTop=False)
|
||||||
self.params[i].setValue = self._setValue
|
|
||||||
self.params[i].sigTreeStateChanged.connect(sigTreeStateChanged_handle)
|
|
||||||
|
|
||||||
for handle in sigActivated_handles[i]:
|
set_tree_label_tips(tree)
|
||||||
self.params[i].child(*handle[0]).sigActivated.connect(handle[1])
|
|
||||||
|
|
||||||
def _setValue(self, value, blockSignal=None):
|
for ch, param in enumerate(self.params):
|
||||||
"""
|
param.sigTreeStateChanged.connect(sigTreeStateChanged_handle)
|
||||||
Implement 'lock' mechanism for Parameter Type
|
|
||||||
|
|
||||||
|
|||||||
Modified from the source
|
for handle in sigActivated_handles[ch]:
|
||||||
"""
|
param.child(*handle[0]).sigActivated.connect(handle[1])
|
||||||
try:
|
|
||||||
if blockSignal is not None:
|
|
||||||
self.sigValueChanged.disconnect(blockSignal)
|
|
||||||
value = self._interpretValue(value)
|
|
||||||
if fn.eq(self.opts["value"], value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
if "lock" in self.opts.keys():
|
def _indicate_usage(param, control_method="constant_current"):
|
||||||
if self.opts["lock"]:
|
for item in param.child("i_set").items:
|
||||||
return value
|
is_constant_current = control_method == "constant_current"
|
||||||
self.opts["value"] = value
|
font = item.font(0)
|
||||||
self.sigValueChanged.emit(
|
font.setUnderline(is_constant_current)
|
||||||
self, value
|
font.setBold(is_constant_current)
|
||||||
) # value might change after signal is received by tree item
|
item.setFont(0, font)
|
||||||
finally:
|
for item in param.child("target").items:
|
||||||
if blockSignal is not None:
|
is_temperature_pid = control_method == "temperature_pid"
|
||||||
self.sigValueChanged.connect(blockSignal)
|
font = item.font(0)
|
||||||
|
font.setUnderline(is_temperature_pid)
|
||||||
|
font.setBold(is_temperature_pid)
|
||||||
|
item.setFont(0, font)
|
||||||
|
|
||||||
return self.opts["value"]
|
param.child("output", "control_method").sigValueChanged.connect(
|
||||||
|
_indicate_usage
|
||||||
|
)
|
||||||
|
_indicate_usage(param.child("output", "control_method"))
|
||||||
|
|
||||||
|
for item in param.child("output", "control_method").items:
|
||||||
|
font = item.font(0)
|
||||||
|
font.setBold(True)
|
||||||
|
item.setFont(0, font)
|
||||||
|
|
||||||
def change_params_title(self, channel, path, title):
|
def change_params_title(self, channel, path, title):
|
||||||
self.params[channel].child(*path).setOpts(title=title)
|
self.params[channel].child(*path).setOpts(title=title)
|
||||||
|
@ -116,57 +89,59 @@ class CtrlPanel(QObject):
|
||||||
for settings in pid_settings:
|
for settings in pid_settings:
|
||||||
channel = settings["channel"]
|
channel = settings["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("PID Config", "Kp").setValue(
|
self.params[channel].child("pid", "kp").set_value_with_lock(
|
||||||
settings["parameters"]["kp"]
|
settings["parameters"]["kp"]
|
||||||
)
|
)
|
||||||
self.params[channel].child("PID Config", "Ki").setValue(
|
self.params[channel].child("pid", "ki").set_value_with_lock(
|
||||||
settings["parameters"]["ki"]
|
settings["parameters"]["ki"]
|
||||||
)
|
)
|
||||||
self.params[channel].child("PID Config", "Kd").setValue(
|
self.params[channel].child("pid", "kd").set_value_with_lock(
|
||||||
settings["parameters"]["kd"]
|
settings["parameters"]["kd"]
|
||||||
)
|
)
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"PID Config", "PID Output Clamping", "Minimum"
|
"pid", "pid_output_clamping", "output_min"
|
||||||
).setValue(settings["parameters"]["output_min"] * 1000)
|
).set_value_with_lock(settings["parameters"]["output_min"])
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"PID Config", "PID Output Clamping", "Maximum"
|
"pid", "pid_output_clamping", "output_max"
|
||||||
).setValue(settings["parameters"]["output_max"] * 1000)
|
).set_value_with_lock(settings["parameters"]["output_max"])
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"Output Config", "Control Method", "Set Temperature"
|
"output", "control_method", "target"
|
||||||
).setValue(settings["target"])
|
).set_value_with_lock(settings["target"])
|
||||||
|
|
||||||
@pyqtSlot("QVariantList")
|
@pyqtSlot("QVariantList")
|
||||||
def update_report(self, report_data):
|
def update_report(self, report_data):
|
||||||
for settings in report_data:
|
for settings in report_data:
|
||||||
channel = settings["channel"]
|
channel = settings["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("Output Config", "Control Method").setValue(
|
self.params[channel].child(
|
||||||
"Temperature PID" if settings["pid_engaged"] else "Constant Current"
|
"output", "control_method"
|
||||||
|
).set_value_with_lock(
|
||||||
|
"temperature_pid" if settings["pid_engaged"] else "constant_current"
|
||||||
)
|
)
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"Output Config", "Control Method", "Set Current"
|
"output", "control_method", "i_set"
|
||||||
).setValue(settings["i_set"] * 1000)
|
).set_value_with_lock(settings["i_set"])
|
||||||
if settings["temperature"] is not None:
|
if settings["temperature"] is not None:
|
||||||
self.params[channel].child("Temperature").setValue(
|
self.params[channel].child(
|
||||||
settings["temperature"]
|
"readings", "temperature"
|
||||||
)
|
).set_value_with_lock(settings["temperature"])
|
||||||
if settings["tec_i"] is not None:
|
if settings["tec_i"] is not None:
|
||||||
self.params[channel].child("Current through TEC").setValue(
|
self.params[channel].child(
|
||||||
settings["tec_i"] * 1000
|
"readings", "tec_i"
|
||||||
)
|
).set_value_with_lock(settings["tec_i"])
|
||||||
|
|
||||||
@pyqtSlot("QVariantList")
|
@pyqtSlot("QVariantList")
|
||||||
def update_thermistor(self, sh_data):
|
def update_thermistor(self, sh_data):
|
||||||
for sh_param in sh_data:
|
for sh_param in sh_data:
|
||||||
channel = sh_param["channel"]
|
channel = sh_param["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child("Thermistor Config", "T₀").setValue(
|
self.params[channel].child("thermistor", "t0").set_value_with_lock(
|
||||||
sh_param["params"]["t0"] - 273.15
|
sh_param["params"]["t0"] - 273.15
|
||||||
)
|
)
|
||||||
self.params[channel].child("Thermistor Config", "R₀").setValue(
|
self.params[channel].child("thermistor", "r0").set_value_with_lock(
|
||||||
sh_param["params"]["r0"]
|
sh_param["params"]["r0"]
|
||||||
)
|
)
|
||||||
self.params[channel].child("Thermistor Config", "B").setValue(
|
self.params[channel].child("thermistor", "b").set_value_with_lock(
|
||||||
sh_param["params"]["b"]
|
sh_param["params"]["b"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -178,14 +153,14 @@ class CtrlPanel(QObject):
|
||||||
channel = pwm_params["channel"]
|
channel = pwm_params["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"Output Config", "Limits", "Max Voltage Difference"
|
"output", "limits", "max_v"
|
||||||
).setValue(pwm_params["max_v"]["value"])
|
).set_value_with_lock(pwm_params["max_v"]["value"])
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"Output Config", "Limits", "Max Cooling Current"
|
"output", "limits", "max_i_pos"
|
||||||
).setValue(pwm_params["max_i_pos"]["value"] * 1000)
|
).set_value_with_lock(pwm_params["max_i_pos"]["value"])
|
||||||
self.params[channel].child(
|
self.params[channel].child(
|
||||||
"Output Config", "Limits", "Max Heating Current"
|
"output", "limits", "max_i_neg"
|
||||||
).setValue(pwm_params["max_i_neg"]["value"] * 1000)
|
).set_value_with_lock(pwm_params["max_i_neg"]["value"])
|
||||||
|
|
||||||
for limit in "max_i_pos", "max_i_neg", "max_v":
|
for limit in "max_i_pos", "max_i_neg", "max_v":
|
||||||
if pwm_params[limit]["value"] == 0.0:
|
if pwm_params[limit]["value"] == 0.0:
|
||||||
|
@ -197,6 +172,6 @@ class CtrlPanel(QObject):
|
||||||
for postfilter_params in postfilter_data:
|
for postfilter_params in postfilter_data:
|
||||||
channel = postfilter_params["channel"]
|
channel = postfilter_params["channel"]
|
||||||
with QSignalBlocker(self.params[channel]):
|
with QSignalBlocker(self.params[channel]):
|
||||||
self.params[channel].child(
|
self.params[channel].child("postfilter", "rate").set_value_with_lock(
|
||||||
"Thermistor Config", "Postfilter Rate"
|
postfilter_params["rate"]
|
||||||
).setValue(postfilter_params["rate"])
|
)
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QSignalBlocker
|
||||||
|
from PyQt6.QtGui import QValidator
|
||||||
|
|
||||||
|
from pyqtgraph import SpinBox
|
||||||
|
import pyqtgraph.functions as fn
|
||||||
|
from pyqtgraph.parametertree import registerParameterItemType
|
||||||
|
from pyqtgraph.parametertree.parameterTypes import SimpleParameter, NumericParameterItem
|
||||||
|
|
||||||
|
|
||||||
|
# See https://github.com/pyqtgraph/pyqtgraph/issues/3115
|
||||||
|
fn.FLOAT_REGEX = re.compile(
|
||||||
|
r"(?P<number>[+-]?((((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)|((?i:nan)|(inf))))\s*"
|
||||||
|
+ r"((?P<siPrefix>[u" + fn.SI_PREFIXES + r"]?)(?P<suffix>.*))?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LockableUnitSpinBox(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.
|
||||||
|
* The "noUnitEditing" option, where the suffix and pinned siPrefix
|
||||||
|
of the SpinBox text is fixed and uneditable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, value=0.0, **kwargs):
|
||||||
|
super().__init__(parent, value, **kwargs)
|
||||||
|
|
||||||
|
self.lineEdit().cursorPositionChanged.connect(
|
||||||
|
self._editor_cursor_position_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, strn, pos):
|
||||||
|
ret, strn, pos = super().validate(strn, pos)
|
||||||
|
|
||||||
|
if self.opts.get("noUnitEditing") is True:
|
||||||
|
suffix = self.opts["suffix"]
|
||||||
|
pinned_si_prefix = self.opts.get("pinSiPrefix")
|
||||||
|
|
||||||
|
suffix_edited = not strn.endswith(suffix)
|
||||||
|
pinned_si_prefix_edited = (
|
||||||
|
pinned_si_prefix is not None
|
||||||
|
and not strn.removesuffix(suffix).endswith(pinned_si_prefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
if suffix_edited or pinned_si_prefix_edited:
|
||||||
|
ret = QValidator.State.Invalid
|
||||||
|
|
||||||
|
return ret, strn, pos
|
||||||
|
|
||||||
|
def _editor_cursor_position_changed(self, oldpos, newpos):
|
||||||
|
# Called on cursor position change
|
||||||
|
# Skips over the suffix and pinned SI prefix on cursor navigation if option
|
||||||
|
# noUnitEditing is enabled.
|
||||||
|
|
||||||
|
# Modified from the original Qt C++ source,
|
||||||
|
# QAbstractSpinBox::editorCursorPositionChanged.
|
||||||
|
# Their suffix is different than our suffix; there's no obvious way to set
|
||||||
|
# theirs here in the derived class since it is private.
|
||||||
|
|
||||||
|
if self.opts.get("noUnitEditing") is True:
|
||||||
|
edit = self.lineEdit()
|
||||||
|
if edit.hasSelectedText():
|
||||||
|
return # Allow for selecting units, for copy-and-paste
|
||||||
|
|
||||||
|
pinned_si_prefix = self.opts.get("pinSiPrefix") or ""
|
||||||
|
unit_len = len(pinned_si_prefix) + len(self.opts["suffix"])
|
||||||
|
text_len = len(edit.text())
|
||||||
|
|
||||||
|
pos = -1
|
||||||
|
# Cursor in unit
|
||||||
|
if text_len - unit_len < newpos < text_len:
|
||||||
|
if oldpos == text_len:
|
||||||
|
pos = text_len - unit_len
|
||||||
|
else:
|
||||||
|
pos = text_len
|
||||||
|
|
||||||
|
if pos != -1:
|
||||||
|
with QSignalBlocker(edit):
|
||||||
|
edit.setCursorPosition(pos)
|
||||||
|
|
||||||
|
def setOpts(self, **opts):
|
||||||
|
if "pinSiPrefix" in opts:
|
||||||
|
self.opts["pinSiPrefix"] = opts.pop("pinSiPrefix")
|
||||||
|
if "noUnitEditing" in opts:
|
||||||
|
self.opts["noUnitEditing"] = opts.pop("noUnitEditing")
|
||||||
|
|
||||||
|
super().setOpts(**opts)
|
||||||
|
|
||||||
|
def editingFinishedEvent(self):
|
||||||
|
# Modified from pyqtgraph.SpinBox.editingFinishedEvent source
|
||||||
|
|
||||||
|
new_text = self.lineEdit().text()
|
||||||
|
if new_text == self.lastText:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
val = self.interpret()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
if val is False:
|
||||||
|
return
|
||||||
|
if val == self.val:
|
||||||
|
self.updateText() # still update text so that values are reformatted pretty-like
|
||||||
|
return
|
||||||
|
self.setValue(val, delaySignal=False) ## allow text update so that values are reformatted pretty-like
|
||||||
|
|
||||||
|
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 UnitfulNumericParameterItem(NumericParameterItem):
|
||||||
|
"""
|
||||||
|
Subclasses PyQtGraph's `NumericParameterItem` and uses
|
||||||
|
UnitfulSpinBox 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, 'noUnitEditing': False,
|
||||||
|
}
|
||||||
|
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 = LockableUnitSpinBox()
|
||||||
|
w.setOpts(**defs)
|
||||||
|
w.sigChanged = w.sigValueChanged
|
||||||
|
w.sigChanging = w.sigValueChanging
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
registerParameterItemType(
|
||||||
|
"float", UnitfulNumericParameterItem, SimpleParameter, override=True
|
||||||
|
)
|
||||||
|
registerParameterItemType(
|
||||||
|
"int", UnitfulNumericParameterItem, SimpleParameter, override=True
|
||||||
|
)
|
|
@ -1,335 +1,459 @@
|
||||||
{
|
{
|
||||||
"ctrl_panel":[
|
"ctrl_panel": [
|
||||||
{
|
{
|
||||||
"name":"Temperature",
|
"name": "readings",
|
||||||
"type":"float",
|
"title": "Readings",
|
||||||
"format":"{value:.4f} °C",
|
"type": "group",
|
||||||
"readonly":true
|
"tip": "Thermostat readings",
|
||||||
},
|
"children": [
|
||||||
{
|
{
|
||||||
"name":"Current through TEC",
|
"name": "temperature",
|
||||||
"type":"float",
|
"title": "Temperature",
|
||||||
"suffix":"mA",
|
"type": "float",
|
||||||
"decimals":6,
|
"format": "{value:.4f} {suffix}",
|
||||||
"readonly":true
|
"suffix": "°C",
|
||||||
},
|
"readonly": true,
|
||||||
{
|
"tip": "The measured temperature at the thermistor"
|
||||||
"name":"Output Config",
|
},
|
||||||
"expanded":true,
|
{
|
||||||
"type":"group",
|
"name": "tec_i",
|
||||||
"children":[
|
"title": "Current through TEC",
|
||||||
{
|
"type": "float",
|
||||||
"name":"Control Method",
|
"siPrefix": true,
|
||||||
"type":"mutex",
|
"suffix": "A",
|
||||||
"limits":[
|
"decimals": 6,
|
||||||
"Constant Current",
|
"readonly": true,
|
||||||
"Temperature PID"
|
"tip": "The measured current through the TEC"
|
||||||
],
|
}
|
||||||
"thermostat:set_param":{
|
]
|
||||||
"topic":"pwm",
|
},
|
||||||
"field":"pid"
|
{
|
||||||
},
|
"name": "output",
|
||||||
"children":[
|
"title": "Output Settings",
|
||||||
{
|
"expanded": true,
|
||||||
"name":"Set Current",
|
"type": "group",
|
||||||
"type":"float",
|
"tip": "Settings of the output to the TEC",
|
||||||
"value":0,
|
"children": [
|
||||||
"step":100,
|
{
|
||||||
"limits":[
|
"name": "control_method",
|
||||||
-2000,
|
"title": "Control Method",
|
||||||
2000
|
"type": "list",
|
||||||
],
|
"limits": {
|
||||||
"triggerOnShow":true,
|
"Constant Current": "constant_current",
|
||||||
"decimals":6,
|
"Temperature PID": "temperature_pid"
|
||||||
"suffix":"mA",
|
},
|
||||||
"thermostat:set_param":{
|
"thermostat:set_param": {
|
||||||
"topic":"pwm",
|
"topic": "pwm",
|
||||||
"field":"i_set"
|
"field": "pid"
|
||||||
},
|
},
|
||||||
"lock":false
|
"tip": "Select control method of output",
|
||||||
},
|
"children": [
|
||||||
{
|
{
|
||||||
"name":"Set Temperature",
|
"name": "i_set",
|
||||||
"type":"float",
|
"title": "Set Current",
|
||||||
"value":25,
|
"type": "float",
|
||||||
"step":0.1,
|
"value": 0,
|
||||||
"limits":[
|
"step": 0.1,
|
||||||
-273,
|
"limits": [
|
||||||
300
|
-2,
|
||||||
],
|
2
|
||||||
"format":"{value:.4f} °C",
|
],
|
||||||
"thermostat:set_param":{
|
"decimals": 6,
|
||||||
"topic":"pid",
|
"pinSiPrefix": "m",
|
||||||
"field":"target"
|
"suffix": "A",
|
||||||
},
|
"siPrefix": true,
|
||||||
"lock":false
|
"noUnitEditing": true,
|
||||||
}
|
"compactHeight": false,
|
||||||
]
|
"thermostat:set_param": {
|
||||||
},
|
"topic": "pwm",
|
||||||
{
|
"field": "i_set"
|
||||||
"name":"Limits",
|
},
|
||||||
"expanded":true,
|
"tip": "The set current through TEC",
|
||||||
"type":"group",
|
"lock": false
|
||||||
"children":[
|
|
||||||
{
|
|
||||||
"name":"Max Cooling Current",
|
|
||||||
"type":"float",
|
|
||||||
"value":0,
|
|
||||||
"step":100,
|
|
||||||
"decimals":6,
|
|
||||||
"limits":[
|
|
||||||
0,
|
|
||||||
2000
|
|
||||||
],
|
|
||||||
"suffix":"mA",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pwm",
|
|
||||||
"field":"max_i_pos"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Max Heating Current",
|
|
||||||
"type":"float",
|
|
||||||
"value":0,
|
|
||||||
"step":100,
|
|
||||||
"decimals":6,
|
|
||||||
"limits":[
|
|
||||||
0,
|
|
||||||
2000
|
|
||||||
],
|
|
||||||
"suffix":"mA",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pwm",
|
|
||||||
"field":"max_i_neg"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Max Voltage Difference",
|
|
||||||
"type":"float",
|
|
||||||
"value":0,
|
|
||||||
"step":0.1,
|
|
||||||
"limits":[
|
|
||||||
0,
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"siPrefix":true,
|
|
||||||
"suffix":"V",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pwm",
|
|
||||||
"field":"max_v"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Thermistor Config",
|
|
||||||
"expanded":true,
|
|
||||||
"type":"group",
|
|
||||||
"children":[
|
|
||||||
{
|
|
||||||
"name":"T₀",
|
|
||||||
"type":"float",
|
|
||||||
"value":25,
|
|
||||||
"step":0.1,
|
|
||||||
"limits":[
|
|
||||||
-100,
|
|
||||||
100
|
|
||||||
],
|
|
||||||
"format":"{value:.4f} °C",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"s-h",
|
|
||||||
"field":"t0"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"R₀",
|
|
||||||
"type":"float",
|
|
||||||
"value":10000,
|
|
||||||
"step":1,
|
|
||||||
"siPrefix":true,
|
|
||||||
"suffix":"Ω",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"s-h",
|
|
||||||
"field":"r0"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"B",
|
|
||||||
"type":"float",
|
|
||||||
"value":3950,
|
|
||||||
"step":1,
|
|
||||||
"suffix":"K",
|
|
||||||
"decimals":4,
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"s-h",
|
|
||||||
"field":"b"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Postfilter Rate",
|
|
||||||
"type":"list",
|
|
||||||
"value":16.67,
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"postfilter",
|
|
||||||
"field":"rate"
|
|
||||||
},
|
|
||||||
"limits":{
|
|
||||||
"Off":null,
|
|
||||||
"16.67 Hz":16.67,
|
|
||||||
"20 Hz":20.0,
|
|
||||||
"21.25 Hz":21.25,
|
|
||||||
"27 Hz":27.0
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"PID Config",
|
|
||||||
"expanded":true,
|
|
||||||
"type":"group",
|
|
||||||
"children":[
|
|
||||||
{
|
|
||||||
"name":"Kp",
|
|
||||||
"type":"float",
|
|
||||||
"step":0.1,
|
|
||||||
"suffix":"",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pid",
|
|
||||||
"field":"kp"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Ki",
|
|
||||||
"type":"float",
|
|
||||||
"step":0.1,
|
|
||||||
"suffix":"Hz",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pid",
|
|
||||||
"field":"ki"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Kd",
|
|
||||||
"type":"float",
|
|
||||||
"step":0.1,
|
|
||||||
"suffix":"s",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pid",
|
|
||||||
"field":"kd"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"PID Output Clamping",
|
|
||||||
"expanded":true,
|
|
||||||
"type":"group",
|
|
||||||
"children":[
|
|
||||||
{
|
|
||||||
"name":"Minimum",
|
|
||||||
"type":"float",
|
|
||||||
"step":100,
|
|
||||||
"limits":[
|
|
||||||
-2000,
|
|
||||||
2000
|
|
||||||
],
|
|
||||||
"decimals":6,
|
|
||||||
"suffix":"mA",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pid",
|
|
||||||
"field":"output_min"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Maximum",
|
|
||||||
"type":"float",
|
|
||||||
"step":100,
|
|
||||||
"limits":[
|
|
||||||
-2000,
|
|
||||||
2000
|
|
||||||
],
|
|
||||||
"decimals":6,
|
|
||||||
"suffix":"mA",
|
|
||||||
"thermostat:set_param":{
|
|
||||||
"topic":"pid",
|
|
||||||
"field":"output_max"
|
|
||||||
},
|
|
||||||
"lock":false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"PID Auto Tune",
|
|
||||||
"expanded":false,
|
|
||||||
"type":"group",
|
|
||||||
"children":[
|
|
||||||
{
|
|
||||||
"name":"Target Temperature",
|
|
||||||
"type":"float",
|
|
||||||
"value":20,
|
|
||||||
"step":0.1,
|
|
||||||
"format":"{value:.4f} °C",
|
|
||||||
"pid_autotune":"target_temp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Test Current",
|
|
||||||
"type":"float",
|
|
||||||
"value":0,
|
|
||||||
"decimals":6,
|
|
||||||
"step":100,
|
|
||||||
"limits":[
|
|
||||||
-2000,
|
|
||||||
2000
|
|
||||||
],
|
|
||||||
"suffix":"mA",
|
|
||||||
"pid_autotune":"test_current"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Temperature Swing",
|
|
||||||
"type":"float",
|
|
||||||
"value":1.5,
|
|
||||||
"step":0.1,
|
|
||||||
"prefix":"±",
|
|
||||||
"format":"{value:.4f} °C",
|
|
||||||
"pid_autotune":"temp_swing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"Lookback",
|
|
||||||
"type":"float",
|
|
||||||
"value":3.0,
|
|
||||||
"step":0.1,
|
|
||||||
"format":"{value:.4f} s",
|
|
||||||
"pid_autotune":"lookback"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"Run",
|
"name": "target",
|
||||||
"type":"action",
|
"title": "Setpoint",
|
||||||
"tip":"Run"
|
"type": "float",
|
||||||
}
|
"value": 25,
|
||||||
]
|
"step": 0.1,
|
||||||
}
|
"limits": [
|
||||||
]
|
-273,
|
||||||
},
|
300
|
||||||
{
|
],
|
||||||
"name":"Save to flash",
|
"format": "{value:.4f} {suffix}",
|
||||||
"type":"action",
|
"suffix": "°C",
|
||||||
"tip":"Save config to thermostat, applies on reset"
|
"noUnitEditing": true,
|
||||||
},
|
"compactHeight": false,
|
||||||
{
|
"thermostat:set_param": {
|
||||||
"name":"Load from flash",
|
"topic": "pid",
|
||||||
"type":"action",
|
"field": "target"
|
||||||
"tip":"Load config from flash"
|
},
|
||||||
}
|
"tip": "The temperature setpoint of the TEC",
|
||||||
linuswck
commented
Replacing the incorrectly defined FLOAT_REGEX on pyqtgraph.functions is more preferable. Replacing the incorrectly defined FLOAT_REGEX on pyqtgraph.functions is more preferable.
atse
commented
How do I do this as a patch to PyQtGraph itself? Seems to be a cleaner solution compared to patching it in our own code. How do I do this as a patch to PyQtGraph itself? Seems to be a cleaner solution compared to patching it in our own code.
|
|||||||
]
|
"lock": false
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limits",
|
||||||
|
"title": "Limits",
|
||||||
|
"expanded": true,
|
||||||
|
"type": "group",
|
||||||
|
"tip": "The limits of output, with the polarity at the front panel as reference",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "max_i_pos",
|
||||||
|
"title": "Max Cooling Current",
|
||||||
|
"type": "float",
|
||||||
|
"value": 0,
|
||||||
|
"step": 0.1,
|
||||||
|
"decimals": 6,
|
||||||
|
"limits": [
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"siPrefix": true,
|
||||||
|
"pinSiPrefix": "m",
|
||||||
|
"suffix": "A",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pwm",
|
||||||
|
"field": "max_i_pos"
|
||||||
|
},
|
||||||
|
"tip": "The maximum cooling (+ve) current through the output pins",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_i_neg",
|
||||||
|
"title": "Max Heating Current",
|
||||||
|
"type": "float",
|
||||||
|
"value": 0,
|
||||||
|
"step": 0.1,
|
||||||
|
"decimals": 6,
|
||||||
|
"siPrefix": true,
|
||||||
|
"pinSiPrefix": "m",
|
||||||
|
"suffix": "A",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"limits": [
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pwm",
|
||||||
|
"field": "max_i_neg"
|
||||||
|
},
|
||||||
|
"tip": "The maximum heating (-ve) current through the output pins",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "max_v",
|
||||||
|
"title": "Max Absolute Voltage",
|
||||||
|
"type": "float",
|
||||||
|
"value": 0,
|
||||||
|
"step": 0.1,
|
||||||
|
"decimals": 3,
|
||||||
|
"limits": [
|
||||||
|
0,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"suffix": "V",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pwm",
|
||||||
|
"field": "max_v"
|
||||||
|
},
|
||||||
|
"tip": "The maximum voltage (in both directions) across the output pins",
|
||||||
|
"lock": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "thermistor",
|
||||||
|
"title": "Thermistor Settings",
|
||||||
|
"expanded": true,
|
||||||
|
"type": "group",
|
||||||
|
"tip": "Parameters for the resistance to temperature conversion with the B-Parameter equation",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "t0",
|
||||||
|
"title": "T₀",
|
||||||
|
"type": "float",
|
||||||
|
"value": 25,
|
||||||
|
"step": 0.1,
|
||||||
|
"limits": [
|
||||||
|
-100,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"format": "{value:.4f} {suffix}",
|
||||||
|
"suffix": "°C",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "s-h",
|
||||||
|
"field": "t0"
|
||||||
|
},
|
||||||
|
"tip": "The base temperature",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "r0",
|
||||||
|
"title": "R₀",
|
||||||
|
"type": "float",
|
||||||
|
"value": 10000,
|
||||||
|
"step": 100,
|
||||||
|
"min": 0,
|
||||||
|
"siPrefix": true,
|
||||||
|
"pinSiPrefix": "k",
|
||||||
|
"suffix": "Ω",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "s-h",
|
||||||
|
"field": "r0"
|
||||||
|
},
|
||||||
|
"tip": "The resistance of the thermistor at base temperature T₀",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "b",
|
||||||
|
"title": "B",
|
||||||
linuswck
commented
You should fix all the siPrefix. Especially when we do not allow unit to be modified now. It will be confusing if the siPrefix of the unit suddenly changes. You should fix **all** the siPrefix. Especially when we do not allow unit to be modified now. It will be confusing if the siPrefix of the unit suddenly changes.
|
|||||||
|
"type": "float",
|
||||||
|
"value": 3950,
|
||||||
|
"step": 10,
|
||||||
|
"suffix": "K",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"decimals": 4,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "s-h",
|
||||||
|
"field": "b"
|
||||||
|
},
|
||||||
|
"tip": "The Beta Parameter",
|
||||||
|
"lock": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "postfilter",
|
||||||
|
"title": "ADC Settings",
|
||||||
|
"type": "group",
|
||||||
|
"tip": "Settings of the ADC on the SENS input",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "rate",
|
||||||
|
"title": "50/60 Hz Rejection Filter",
|
||||||
|
"type": "list",
|
||||||
|
"value": 16.67,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "postfilter",
|
||||||
|
"field": "rate"
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"16.67 SPS": 16.67,
|
||||||
|
"20 SPS": 20.0,
|
||||||
|
"21.25 SPS": 21.25,
|
||||||
|
"27 SPS": 27.0,
|
||||||
|
"Off": null
|
||||||
|
},
|
||||||
|
"tip": "Adjust the output data rate of the enhanced 50 Hz & 60 Hz rejection filter\n(Helps avoid mains interference)",
|
||||||
|
"lock": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pid",
|
||||||
|
"title": "PID Settings",
|
||||||
|
"expanded": true,
|
||||||
|
"type": "group",
|
||||||
|
"tip": "Settings of PID parameters and clamping",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "kp",
|
||||||
|
"title": "Kp",
|
||||||
|
"type": "float",
|
||||||
|
"step": 0.1,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pid",
|
||||||
|
"field": "kp"
|
||||||
|
},
|
||||||
|
"tip": "Proportional gain",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ki",
|
||||||
|
"title": "Ki",
|
||||||
|
"type": "float",
|
||||||
|
"step": 0.1,
|
||||||
|
"suffix": "Hz",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pid",
|
||||||
|
"field": "ki"
|
||||||
|
},
|
||||||
|
"tip": "Integral gain",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kd",
|
||||||
|
"title": "Kd",
|
||||||
|
"type": "float",
|
||||||
|
"step": 0.1,
|
||||||
|
"suffix": "s",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pid",
|
||||||
|
"field": "kd"
|
||||||
|
},
|
||||||
|
"tip": "Differential gain",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pid_output_clamping",
|
||||||
|
"title": "PID Output Clamping",
|
||||||
|
"expanded": true,
|
||||||
|
"type": "group",
|
||||||
|
"tip": "Clamps PID outputs to specified range\nCould be different than output limits",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "output_min",
|
||||||
|
"title": "Minimum",
|
||||||
|
"type": "float",
|
||||||
|
"step": 0.1,
|
||||||
|
"limits": [
|
||||||
|
-2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"decimals": 6,
|
||||||
|
"siPrefix": true,
|
||||||
|
"pinSiPrefix": "m",
|
||||||
|
"suffix": "A",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pid",
|
||||||
|
"field": "output_min"
|
||||||
|
},
|
||||||
|
"tip": "Minimum PID output",
|
||||||
|
"lock": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "output_max",
|
||||||
|
"title": "Maximum",
|
||||||
|
"type": "float",
|
||||||
|
"step": 0.1,
|
||||||
|
"limits": [
|
||||||
|
-2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"decimals": 6,
|
||||||
|
"siPrefix": true,
|
||||||
|
"pinSiPrefix": "m",
|
||||||
|
"suffix": "A",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"thermostat:set_param": {
|
||||||
|
"topic": "pid",
|
||||||
|
"field": "output_max"
|
||||||
|
},
|
||||||
|
"tip": "Maximum PID output",
|
||||||
|
"lock": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pid_autotune",
|
||||||
|
"title": "PID Autotune",
|
||||||
|
"expanded": false,
|
||||||
|
"type": "group",
|
||||||
|
"tip": "Automatically tune PID parameters",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "target_temp",
|
||||||
|
"title": "Target Temperature",
|
||||||
|
"type": "float",
|
||||||
|
"value": 20,
|
||||||
|
"step": 0.1,
|
||||||
|
"format": "{value:.4f} {suffix}",
|
||||||
|
"suffix": "°C",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"pid_autotune": "target_temp",
|
||||||
|
"tip": "The target temperature to autotune for"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test_current",
|
||||||
|
"title": "Test Current",
|
||||||
|
"type": "float",
|
||||||
|
"value": 0,
|
||||||
|
"decimals": 6,
|
||||||
|
"step": 0.1,
|
||||||
|
"limits": [
|
||||||
|
0,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"siPrefix": true,
|
||||||
|
"pinSiPrefix": "m",
|
||||||
|
"suffix": "A",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"pid_autotune": "test_current",
|
||||||
|
"tip": "The testing current when autotuning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "temp_swing",
|
||||||
|
"title": "Temperature Swing",
|
||||||
|
"type": "float",
|
||||||
|
"value": 1.5,
|
||||||
|
"step": 0.1,
|
||||||
|
"format": "{value:.4f} {suffix}",
|
||||||
|
"suffix": "K",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"compactHeight": false,
|
||||||
|
"pid_autotune": "temp_swing",
|
||||||
|
"tip": "The temperature swing around the target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lookback",
|
||||||
|
"title": "Lookback",
|
||||||
|
"type": "float",
|
||||||
|
"value": 3.0,
|
||||||
|
"step": 0.1,
|
||||||
|
"format": "{value:.4f} {suffix}",
|
||||||
|
"noUnitEditing": true,
|
||||||
|
"suffix": "s",
|
||||||
|
"compactHeight": false,
|
||||||
|
"pid_autotune": "lookback",
|
||||||
|
"tip": "Amount of time referenced for tuning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "run_pid",
|
||||||
|
"title": "Run",
|
||||||
|
"type": "action",
|
||||||
|
"tip": "Run PID Autotune with above settings"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "save",
|
||||||
|
"title": "Save to flash",
|
||||||
|
"type": "action",
|
||||||
|
"tip": "Save settings to thermostat, applies on reset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "load",
|
||||||
|
"title": "Load from flash",
|
||||||
|
"type": "action",
|
||||||
|
"tip": "Load settings from thermostat"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -78,11 +78,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
param_tree_sigActivated_handles = [
|
param_tree_sigActivated_handles = [
|
||||||
[
|
[
|
||||||
[["Save to flash"], partial(self.thermostat.save_cfg, ch)],
|
[["save"], partial(self.thermostat.save_cfg, ch)],
|
||||||
[["Load from flash"], partial(self.thermostat.load_cfg, ch)],
|
[["load"], partial(self.thermostat.load_cfg, ch)],
|
||||||
[
|
[
|
||||||
["PID Config", "PID Auto Tune", "Run"],
|
["pid", "pid_autotune", "run_pid"],
|
||||||
partial(self.pid_auto_tune_request, ch),
|
partial(self.pid_autotune_request, ch),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
for ch in range(self.NUM_CHANNELS)
|
for ch in range(self.NUM_CHANNELS)
|
||||||
|
@ -262,26 +262,25 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
@asyncSlot(object, object)
|
@asyncSlot(object, object)
|
||||||
async def send_command(self, param, changes):
|
async def send_command(self, param, changes):
|
||||||
"""Translates parameter tree changes into thermostat set_param calls"""
|
"""Translates parameter tree changes into thermostat set_param calls"""
|
||||||
ch = param.channel
|
ch = param.value()
|
||||||
|
|
||||||
for inner_param, change, data in changes:
|
for inner_param, change, data in changes:
|
||||||
if change == "value":
|
if change == "value":
|
||||||
new_value = data
|
new_value = data
|
||||||
if "thermostat:set_param" in inner_param.opts:
|
if "thermostat:set_param" in inner_param.opts:
|
||||||
atse marked this conversation as resolved
Outdated
linuswck
commented
Looks like a hack. The unit conversion should not be affected by the title. Looks like a hack. The unit conversion should not be affected by the title.
atse
commented
Removed the need for it in latest force-push with the Removed the need for it in latest force-push with the `pinSiPrefix` option of `UnitfulSpinBox`.
|
|||||||
if inner_param.opts.get("suffix", None) == "mA":
|
|
||||||
new_value /= 1000 # Given in mA
|
|
||||||
|
|
||||||
thermostat_param = inner_param.opts["thermostat:set_param"]
|
thermostat_param = inner_param.opts["thermostat:set_param"]
|
||||||
|
|
||||||
# Handle thermostat command irregularities
|
# Handle thermostat command irregularities
|
||||||
match inner_param.name(), new_value:
|
match inner_param.name(), new_value:
|
||||||
case "Postfilter Rate", None:
|
case "rate", None:
|
||||||
thermostat_param = thermostat_param.copy()
|
thermostat_param = thermostat_param.copy()
|
||||||
thermostat_param["field"] = "off"
|
thermostat_param["field"] = "off"
|
||||||
new_value = ""
|
new_value = ""
|
||||||
case "Control Method", "Constant Current":
|
case "control_method", "constant_current":
|
||||||
return
|
thermostat_param = thermostat_param.copy()
|
||||||
case "Control Method", "Temperature PID":
|
thermostat_param["field"] = "i_set"
|
||||||
|
new_value = inner_param.child("i_set").value()
|
||||||
|
case "control_method", "temperature_pid":
|
||||||
new_value = ""
|
new_value = ""
|
||||||
|
|
||||||
inner_param.setOpts(lock=True)
|
inner_param.setOpts(lock=True)
|
||||||
|
@ -295,7 +294,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
self.autotuners.set_params(auto_tuner_param, ch, new_value)
|
self.autotuners.set_params(auto_tuner_param, ch, new_value)
|
||||||
|
|
||||||
@asyncSlot()
|
@asyncSlot()
|
||||||
async def pid_auto_tune_request(self, ch=0):
|
async def pid_autotune_request(self, ch=0):
|
||||||
match self.autotuners.get_state(ch):
|
match self.autotuners.get_state(ch):
|
||||||
case PIDAutotuneState.STATE_OFF | PIDAutotuneState.STATE_FAILED:
|
case PIDAutotuneState.STATE_OFF | PIDAutotuneState.STATE_FAILED:
|
||||||
self.autotuners.load_params_and_set_ready(ch)
|
self.autotuners.load_params_and_set_ready(ch)
|
||||||
|
@ -312,18 +311,18 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
match self.autotuners.get_state(ch):
|
match self.autotuners.get_state(ch):
|
||||||
case PIDAutotuneState.STATE_OFF:
|
case PIDAutotuneState.STATE_OFF:
|
||||||
self.ctrl_panel_view.change_params_title(
|
self.ctrl_panel_view.change_params_title(
|
||||||
ch, ("PID Config", "PID Auto Tune", "Run"), "Run"
|
ch, ("pid", "pid_autotune", "run_pid"), "Run"
|
||||||
)
|
)
|
||||||
case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN:
|
case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN:
|
||||||
self.ctrl_panel_view.change_params_title(
|
self.ctrl_panel_view.change_params_title(
|
||||||
ch, ("PID Config", "PID Auto Tune", "Run"), "Stop"
|
ch, ("pid", "pid_autotune", "run_pid"), "Stop"
|
||||||
)
|
)
|
||||||
ch_tuning.append(ch)
|
ch_tuning.append(ch)
|
||||||
|
|
||||||
case PIDAutotuneState.STATE_SUCCEEDED:
|
case PIDAutotuneState.STATE_SUCCEEDED:
|
||||||
self.info_box.display_info_box(
|
self.info_box.display_info_box(
|
||||||
"PID Autotune Success",
|
"PID Autotune Success",
|
||||||
f"Channel {ch} PID Config has been loaded to Thermostat. Regulating temperature.",
|
f"Channel {ch} PID Settings has been loaded to Thermostat. Regulating temperature.",
|
||||||
)
|
)
|
||||||
self.info_box.show()
|
self.info_box.show()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Usability:
I think this is duplicated. "Set Current" should not reflect the value being set by "Thermostat". That area of ctrl panel is configured by user afterall. It is confusing to me if it suddenly becomes a "Reading" item. Also, the current readings already reflect the current being set already.
Instead, I think we should keep the "Set Current" and and "Setpoint" always shown and configurable. And there are some sorts of indicator indicating which value is being used for control.