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()