diff --git a/client/brewpi_gui.py b/client/brewpi_gui.py index 2e6e751..c1755e5 100755 --- a/client/brewpi_gui.py +++ b/client/brewpi_gui.py @@ -236,22 +236,25 @@ class SudForecastPlot(FigureCanvasQTAgg): class Window(QtWidgets.QMainWindow, Ui_MainWindow): - # on_sud_changed() runs on the websocket recv thread, but a modal dialog - # must be shown from the GUI thread - a signal/slot crosses that thread - # boundary safely (Qt auto-queues the slot call onto the receiver's - # thread when sender and receiver differ). - user_message_signal = QtCore.pyqtSignal(str) + # on_*_changed()/on_ws_connect_changed() are invoked straight from the + # websocket client's background thread (WsClient.bg_thread), but Qt + # widgets may only be touched from the GUI thread. A signal/slot crosses + # that boundary safely (Qt auto-queues the slot call onto the receiver's + # thread when sender and receiver differ) - recv_signal carries the + # handler itself plus its one argument, so every channel can share it + # instead of needing one signal per handler. + recv_signal = QtCore.pyqtSignal(object, object) def __init__(self): QtWidgets.QMainWindow.__init__(self) self.setWindowIcon(QtGui.QIcon('res/beer.png')) - self.user_message_signal.connect(self.on_user_message_signal) + self.recv_signal.connect(self._dispatch_recv) self.user_config = UserConfig() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) self.msg_dispatch = MessageDispatcherSync(auto_subscribe=True) - self.msg_dispatch.on_connect_changed = self.on_ws_connect_changed + self.msg_dispatch.on_connect_changed = lambda connected: self.recv_signal.emit(self.on_ws_connect_changed, connected) self.ws_client = WsClient(listener=self.msg_dispatch, loop=loop) self.connected = False self.sud_loaded = False @@ -307,13 +310,16 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.msg_sud = self.msg_dispatch.msgio_get('Sud') self.msg_system = self.msg_dispatch.msgio_get('System') - self.msg_pot.set_recv_handler(self.on_pot_changed) - self.msg_sensor.set_recv_handler(self.on_sensor_changed) - self.msg_heater.set_recv_handler(self.on_heater_changed) - self.msg_stirrer.set_recv_handler(self.on_stirrer_changed) - self.msg_tempctrl.set_recv_handler(self.on_tempctrl_changed) - self.msg_sud.set_recv_handler(self.on_sud_changed) - self.msg_system.set_recv_handler(self.on_system_changed) + # set_recv_handler's callback runs on WsClient's background thread - + # route each through recv_signal instead of wiring the real handler + # directly, so its body always runs on the GUI thread. + self.msg_pot.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_pot_changed, msg)) + self.msg_sensor.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_sensor_changed, msg)) + self.msg_heater.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_heater_changed, msg)) + self.msg_stirrer.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_stirrer_changed, msg)) + self.msg_tempctrl.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_tempctrl_changed, msg)) + self.msg_sud.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_sud_changed, msg)) + self.msg_system.set_recv_handler(lambda msg: self.recv_signal.emit(self.on_system_changed, msg)) self.setupUi(self) # Matches tc_enabled's default (False) until the server says otherwise. @@ -394,6 +400,12 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): forecast_layout.setContentsMargins(0, 0, 0, 0) forecast_layout.addWidget(self.forecast_plot) + def _dispatch_recv(self, handler, msg): + """recv_signal's slot - runs on the GUI thread regardless of which + thread emitted the signal, so handler(msg) can touch Qt widgets + safely.""" + handler(msg) + def connect(self): self.slider_pwr_initial_update = True self.slider_temp_soll_initial_update = True @@ -644,10 +656,10 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): def on_action_sud_stop(self): self.msg_sud.send({'Stop': True}) - def on_user_message_signal(self, message): - # Runs on the GUI thread (queued there by the cross-thread signal - # emit in on_sud_changed()). The Sud is already blocked in - # WAIT_USER server-side; closing this dialog is what unblocks it. + def show_user_message(self, message): + # on_sud_changed() already runs on the GUI thread (via recv_signal), + # so this can show the dialog directly. The Sud is already blocked + # in WAIT_USER server-side; closing this dialog is what unblocks it. QtWidgets.QMessageBox.information(self, "Sud", message) self.msg_sud.send({'Confirm': True}) @@ -848,7 +860,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.forecast_plot.show_schedule(self.sud_schedule, start_theta, self.sud_name) if self.sud_state == SUD_WAIT_USER_STATE and prev_state != SUD_WAIT_USER_STATE and self.sud_user_message: - self.user_message_signal.emit(self.sud_user_message) + self.show_user_message(self.sud_user_message) elif "UserMessage" in key: self.sud_user_message = msg['UserMessage'] elif "Step" in key: