diff --git a/client/brewpi.ui b/client/brewpi.ui
index b1a149b..db17a49 100644
--- a/client/brewpi.ui
+++ b/client/brewpi.ui
@@ -517,11 +517,27 @@
Controller
+
+
+
+ 0
+ 25
+ 141
+ 25
+
+
+
+ While disabled, the controller tries not to drive the heater (output forced to 0) and the setpoints below are inactive
+
+
+ Enabled
+
+
20
- 70
+ 100
63
281
@@ -540,7 +556,7 @@
10
- 370
+ 400
67
31
@@ -556,7 +572,7 @@
80
- 370
+ 400
51
31
@@ -575,7 +591,7 @@
0
- 30
+ 60
141
31
diff --git a/client/brewpi_gui.py b/client/brewpi_gui.py
index 15669e1..edf3004 100755
--- a/client/brewpi_gui.py
+++ b/client/brewpi_gui.py
@@ -252,6 +252,9 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_empty = True
self.sud_state = None
self.sud_user_message = None
+ # Mirrors the server's TempController.enabled - gates the
+ # temp/heatrate setpoint controls (see on_tempctrl_changed()).
+ self.tc_enabled = False
# Global, not Sud-specific - from the 'System' channel's one-time
# startup message. warp_factor defaults to 1.0 (no scaling) until
# that arrives, or for a server that doesn't send it at all.
@@ -295,6 +298,9 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.msg_system.set_recv_handler(self.on_system_changed)
self.setupUi(self)
+ # Matches tc_enabled's default (False) until the server says otherwise.
+ self.Slider_temp_soll.setEnabled(False)
+ self.doubleSpinBox_heatrate_soll.setEnabled(False)
# Permanent (right-aligned, not cleared by showMessage()/clearMessage())
# label for static environment info - distinct from the status bar's
# transient step/schedule messages.
@@ -329,6 +335,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.checkbox_heater_activate.stateChanged.connect(self.on_checkbox_changed)
self.checkbox_stirrer_activate.stateChanged.connect(self.on_checkbox_stirrer_activate_changed)
+ self.checkbox_tc_enable.stateChanged.connect(self.on_checkbox_tc_enable_changed)
self.lineEdit_ambient.returnPressed.connect(self.on_ambient_entry_changed)
self.btn_pot_reset.clicked.connect(self.on_action_pot_reset)
# Only meaningful (and only shown) once the server confirms its Pot
@@ -469,6 +476,12 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_empty = True
self.sud_state = None
self.sud_user_message = None
+ self.tc_enabled = False
+ self.checkbox_tc_enable.blockSignals(True)
+ self.checkbox_tc_enable.setChecked(False)
+ self.checkbox_tc_enable.blockSignals(False)
+ self.Slider_temp_soll.setEnabled(False)
+ self.doubleSpinBox_heatrate_soll.setEnabled(False)
self.ambient_temp = None
self.lineEdit_ambient.clear()
self.btn_pot_reset.setVisible(False)
@@ -577,6 +590,10 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
print("on_checkbox_stirrer_activate_changed {}".format(value))
self.msg_stirrer.send({'Activate': int(value == 2)})
+ def on_checkbox_tc_enable_changed(self, value):
+ print("on_checkbox_tc_enable_changed {}".format(value))
+ self.msg_tempctrl.send({'Enable': value == 2})
+
def on_action_sud_start(self):
self.msg_sud.send({'Start': True})
@@ -661,6 +678,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.doubleSpinBox_heatrate_soll.blockSignals(True)
self.doubleSpinBox_heatrate_soll.setValue(subsubmsg['Set'])
self.doubleSpinBox_heatrate_soll.blockSignals(False)
+ elif "Enabled" in key:
+ self.tc_enabled = msg['Enabled']
+ self.checkbox_tc_enable.blockSignals(True)
+ self.checkbox_tc_enable.setChecked(self.tc_enabled)
+ self.checkbox_tc_enable.blockSignals(False)
+ self.Slider_temp_soll.setEnabled(self.tc_enabled)
+ self.doubleSpinBox_heatrate_soll.setEnabled(self.tc_enabled)
if "Ist" in key:
submsg = msg['Ist']
if 'Temp' in submsg:
diff --git a/client/main_window.py b/client/main_window.py
index b024dc2..9cc0c29 100644
--- a/client/main_window.py
+++ b/client/main_window.py
@@ -190,24 +190,27 @@ class Ui_MainWindow(object):
self.groupBox_2.setSizePolicy(sizePolicy)
self.groupBox_2.setMinimumSize(QtCore.QSize(0, 0))
self.groupBox_2.setObjectName("groupBox_2")
+ self.checkbox_tc_enable = QtWidgets.QCheckBox(self.groupBox_2)
+ self.checkbox_tc_enable.setGeometry(QtCore.QRect(0, 25, 141, 25))
+ self.checkbox_tc_enable.setObjectName("checkbox_tc_enable")
self.Slider_temp_soll = QtWidgets.QSlider(self.groupBox_2)
- self.Slider_temp_soll.setGeometry(QtCore.QRect(20, 70, 63, 281))
+ self.Slider_temp_soll.setGeometry(QtCore.QRect(20, 100, 63, 281))
self.Slider_temp_soll.setMinimum(0)
self.Slider_temp_soll.setMaximum(100)
self.Slider_temp_soll.setOrientation(QtCore.Qt.Vertical)
self.Slider_temp_soll.setObjectName("Slider_temp_soll")
self.label_6 = QtWidgets.QLabel(self.groupBox_2)
- self.label_6.setGeometry(QtCore.QRect(10, 370, 67, 31))
+ self.label_6.setGeometry(QtCore.QRect(10, 400, 67, 31))
self.label_6.setAlignment(QtCore.Qt.AlignCenter)
self.label_6.setObjectName("label_6")
self.doubleSpinBox_heatrate_soll = QtWidgets.QDoubleSpinBox(self.groupBox_2)
- self.doubleSpinBox_heatrate_soll.setGeometry(QtCore.QRect(80, 370, 51, 31))
+ self.doubleSpinBox_heatrate_soll.setGeometry(QtCore.QRect(80, 400, 51, 31))
self.doubleSpinBox_heatrate_soll.setDecimals(1)
self.doubleSpinBox_heatrate_soll.setMaximum(3.0)
self.doubleSpinBox_heatrate_soll.setSingleStep(0.1)
self.doubleSpinBox_heatrate_soll.setObjectName("doubleSpinBox_heatrate_soll")
self.label_9 = QtWidgets.QLabel(self.groupBox_2)
- self.label_9.setGeometry(QtCore.QRect(0, 30, 141, 31))
+ self.label_9.setGeometry(QtCore.QRect(0, 60, 141, 31))
self.label_9.setAlignment(QtCore.Qt.AlignCenter)
self.label_9.setObjectName("label_9")
self.horizontalLayout.addWidget(self.groupBox_2)
@@ -1288,6 +1291,8 @@ class Ui_MainWindow(object):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "BrewPi"))
self.groupBox_2.setTitle(_translate("MainWindow", "Controller"))
+ self.checkbox_tc_enable.setToolTip(_translate("MainWindow", "While disabled, the controller tries not to drive the heater (output forced to 0) and the setpoints below are inactive"))
+ self.checkbox_tc_enable.setText(_translate("MainWindow", "Enabled"))
self.label_6.setText(_translate("MainWindow", "Heat rate"))
self.label_9.setText(_translate("MainWindow", "Temperature [°C]"))
self.groupBox.setTitle(_translate("MainWindow", "Heater"))
diff --git a/components/pid/temp_controller_base.py b/components/pid/temp_controller_base.py
index d3393ca..e4c3157 100644
--- a/components/pid/temp_controller_base.py
+++ b/components/pid/temp_controller_base.py
@@ -43,6 +43,11 @@ class TempControllerBase(APid):
# transition, force y to 0 as soon as the caller (SudTask) knows
# the step needs to passively cool down.
self.cooling = False
+ # Master on/off switch: while disabled, the controller tries not to
+ # drive the heater at all (output forced to 0) regardless of the
+ # FSM/setpoints - off by default, enabled either by the user
+ # (manual mode) or by SudTask for the duration of a run.
+ self.enabled = False
def on_state_entered(self, state):
pass
@@ -79,6 +84,9 @@ class TempControllerBase(APid):
def set_cooling(self, value):
self.cooling = value
+ def set_enabled(self, value):
+ self.enabled = value
+
def get_heatrate_soll(self):
return self.heatrate_soll
@@ -118,7 +126,7 @@ class TempControllerBase(APid):
self.pid_hold.process(theta_err, -self.theta_ist, hold_scale)
self.pid_rate.process(heatrate_err, -self.heatrate_ist)
- if self.state == States.IDLE or self.cooling:
+ if self.state == States.IDLE or self.cooling or not self.enabled:
self.y = 0
else:
self.y = self.pid_rate.get_y()
diff --git a/tasks/sud.py b/tasks/sud.py
index 680c00b..0ae0c78 100644
--- a/tasks/sud.py
+++ b/tasks/sud.py
@@ -96,6 +96,14 @@ class SudTask(ATask):
if value in (SudState.DONE, SudState.IDLE):
self.stirrer.set_duty_cycle(1.0)
self.stirrer.set_speed(0)
+ # A finished/stopped run no longer owns the controller - hand
+ # control back to manual mode (off by default there too).
+ self.tc.set_enabled(False)
+ else:
+ # Any other state (RAMPING/HOLDING/WAIT_USER/PAUSED) means a
+ # run is in progress and needs the controller actively driving
+ # the heater.
+ self.tc.set_enabled(True)
def on_user_message_changed(self, value):
asyncio.create_task(self.send({'UserMessage': value}))
diff --git a/tasks/tempctrl.py b/tasks/tempctrl.py
index 6d30ad6..0843dc2 100644
--- a/tasks/tempctrl.py
+++ b/tasks/tempctrl.py
@@ -35,6 +35,10 @@ class TcTask(ATask):
def on_rate_ist_changed(self, value):
asyncio.create_task(self.send({'Ist': {'Rate': value}}))
+ def on_enabled_changed(self, value):
+ print("Enabled change to {}".format(value))
+ asyncio.create_task(self.send({'Enabled': value}))
+
async def recv(self, msg):
print(msg)
for key in msg.keys():
@@ -45,6 +49,8 @@ class TcTask(ATask):
self.tc.set_theta_soll(submsg['Temp'])
if 'Rate' in subkey:
self.tc.set_heatrate_soll(submsg['Rate'])
+ if 'Enable' in key:
+ self.tc.set_enabled(bool(msg['Enable']))
async def send(self, data):
await self.msg_handler.send(data)
@@ -58,9 +64,11 @@ class TcTask(ATask):
self.tc.set_on_changed('theta_soll_set', ChangedFloat(self.on_temp_soll_changed, prec=1).set)
self.tc.set_on_changed('heatrate_soll', ChangedFloat(self.on_rate_soll_curr_changed, prec=1).set)
self.tc.set_on_changed('heatrate_soll_set', ChangedFloat(self.on_rate_soll_set_changed, prec=1).set)
+ self.tc.set_on_changed('enabled', self.on_enabled_changed)
self.tc.set_theta_soll(20.0)
self.tc.set_heatrate_soll(1.0)
+ await self.send({'Enabled': self.tc.enabled})
while True:
self.tc.process()