Show user_message as a modal dialog when the Sud waits for confirmation

Adds a thread-safe path from on_sud_changed() (websocket recv thread)
to a modal QMessageBox on the GUI thread via Qt's cross-thread
signal/slot queuing, fired when the State transition lands on
WAIT_USER - which the schedule is already blocked on server-side, so
the dialog just gives it a UI: closing it (OK) sends Confirm.

Deliberately scoped to WAIT_USER transitions, not every UserMessage -
most steps in sude/sud_0010.json inherit the schedule's default
placeholder text ("Put user message here") rather than overriding it,
so popping a dialog for every step would mostly show that placeholder.
This commit is contained in:
2026-06-21 00:15:31 +02:00
parent e171892804
commit e1d0deb8d2
+22
View File
@@ -19,6 +19,7 @@ import asyncio
# str(SudState.X) values (as sent in the 'State' message) that count as # str(SudState.X) values (as sent in the 'State' message) that count as
# "running" for enabling/disabling the Start/Pause/Stop toolbar actions. # "running" for enabling/disabling the Start/Pause/Stop toolbar actions.
SUD_RUNNING_STATES = {"SudState.RAMPING", "SudState.HOLDING", "SudState.WAIT_USER"} SUD_RUNNING_STATES = {"SudState.RAMPING", "SudState.HOLDING", "SudState.WAIT_USER"}
SUD_WAIT_USER_STATE = "SudState.WAIT_USER"
SUD_PAUSED_STATE = "SudState.PAUSED" SUD_PAUSED_STATE = "SudState.PAUSED"
@@ -169,9 +170,16 @@ class SudForecastPlot(FigureCanvasQTAgg):
class Window(QtWidgets.QMainWindow, Ui_MainWindow): 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)
def __init__(self): def __init__(self):
QtWidgets.QMainWindow.__init__(self) QtWidgets.QMainWindow.__init__(self)
self.setWindowIcon(QtGui.QIcon('res/beer.png')) self.setWindowIcon(QtGui.QIcon('res/beer.png'))
self.user_message_signal.connect(self.on_user_message_signal)
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
@@ -181,6 +189,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.connected = False self.connected = False
self.sud_loaded = False self.sud_loaded = False
self.sud_state = None self.sud_state = None
self.sud_user_message = None
# time.monotonic() of the last IDLE/DONE -> running transition, used # time.monotonic() of the last IDLE/DONE -> running transition, used
# to position the forecast plot's progress line; None while not # to position the forecast plot's progress line; None while not
# running. sud_paused_total accumulates time spent PAUSED so the # running. sud_paused_total accumulates time spent PAUSED so the
@@ -304,6 +313,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
# Nothing reported over a dead connection can be trusted anymore. # Nothing reported over a dead connection can be trusted anymore.
self.sud_loaded = False self.sud_loaded = False
self.sud_state = None self.sud_state = None
self.sud_user_message = None
self.sud_start_time = None self.sud_start_time = None
self.sud_paused_total = 0.0 self.sud_paused_total = 0.0
self.sud_pause_started_at = None self.sud_pause_started_at = None
@@ -352,6 +362,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
def on_action_sud_stop(self): def on_action_sud_stop(self):
self.msg_sud.send({'Stop': True}) 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.
QtWidgets.QMessageBox.information(self, "Sud", message)
self.msg_sud.send({'Confirm': True})
def on_action_sud_save(self): def on_action_sud_save(self):
# Pick the destination up front, on the GUI thread - the actual # Pick the destination up front, on the GUI thread - the actual
# write happens in on_sud_changed(), which runs off a worker thread # write happens in on_sud_changed(), which runs off a worker thread
@@ -482,6 +499,11 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_start_time = None self.sud_start_time = None
self.sud_paused_total = 0.0 self.sud_paused_total = 0.0
self.sud_pause_started_at = None self.sud_pause_started_at = None
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)
elif "UserMessage" in key:
self.sud_user_message = msg['UserMessage']
elif "Step" in key: elif "Step" in key:
step = msg['Step'] step = msg['Step']
descr = step.get('Descr') descr = step.get('Descr')