From defee1d40efa04f97c41de3fe1a74682adeadd49 Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Mon, 20 Apr 2026 15:01:15 -0700 Subject: [PATCH 1/3] Update SEQ status flow --- pygui/layout_service.py | 21 ++++++----- pygui/zmq_status_service.py | 73 ++++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/pygui/layout_service.py b/pygui/layout_service.py index e02087c8..321e96cc 100644 --- a/pygui/layout_service.py +++ b/pygui/layout_service.py @@ -193,15 +193,16 @@ def create_system_status_group(self): # Create a mapping for status colors status_map = { - "stopped": QColor(169, 169, 169), # Grey - "idle": QColor(255, 255, 0), # Yellow - "paused": QColor(255, 165, 0), # Orange - "exposing": QColor(0, 255, 0), # Green - "readout": QColor(0, 255, 0), # Green - "acquire": QColor(255, 255, 0), # Yellow - "focus": QColor(255, 255, 0), # Yellow - "calib": QColor(255, 255, 0), # Yellow - "user": QColor(255, 255, 0), # Yellow + "stopped": QColor(169, 169, 169), # Grey + "not_ready": QColor(255, 0, 0), # Red + "idle": QColor(255, 255, 0), # Yellow + "paused": QColor(255, 165, 0), # Orange + "exposing": QColor(0, 255, 0), # Green + "readout": QColor(0, 255, 0), # Green + "acquire": QColor(255, 255, 0), # Yellow + "focus": QColor(255, 255, 0), # Yellow + "calib": QColor(255, 255, 0), # Yellow + "user": QColor(255, 255, 0), # Yellow } # Create a dictionary to hold the status widgets, which we will enable/disable @@ -223,7 +224,7 @@ def create_system_status_group(self): status_color_rect.setStyleSheet(f"background-color: {color.name()};") # Label showing the status - status_label = QLabel(status.capitalize()) + status_label = QLabel(status.replace("_", " ").title()) status_label.setMargin(0) # Remove extra margin around the label # Layout for each status (color + label) diff --git a/pygui/zmq_status_service.py b/pygui/zmq_status_service.py index 819d8eef..c78d3e8f 100644 --- a/pygui/zmq_status_service.py +++ b/pygui/zmq_status_service.py @@ -104,6 +104,7 @@ def subscribe_to_all(self): self.subscribed_topics.clear() # Clear the current subscriptions self.logger.info("Subscribed to all topics.") + def listen(self): """ Listen for incoming messages from the broker. """ if not self.is_connected: @@ -113,45 +114,38 @@ def listen(self): try: self.logger.info("Starting to listen for messages from the broker...") while True: - message = self.socket.recv_multipart() # Receive the message as multipart (topic, payload) - if len(message) == 2: # Ensure there are exactly two parts: topic and payload - topic = message[0].decode('utf-8') # The topic is the first part (byte array -> string) - payload = message[1].decode('utf-8') # The payload is the second part + message = self.socket.recv_multipart() + if len(message) == 2: + topic = message[0].decode("utf-8") + payload = message[1].decode("utf-8") - self.logger.info(f"Received message: Topic = {topic}, Payload = {payload}") + self.logger.info(f'Received message: Topic = {topic}, Payload = {payload}') - # Assuming the payload is a JSON string, parse it into a dictionary try: data = json.loads(payload) - # Emit the message to the UI thread - - # If the topic is "acamd" + if topic == "acamd": self.new_message_signal.emit(f"Topic: {topic}, Payload: {payload}") - # If the topic is "seq_daemonstate" - if topic == "seq_waitstate": + # support both old and new sequencer topics + if topic in ("seq_waitstate", "seq_seqstate"): status = self._status_from_seq_waitstate(data) - self.system_status_signal.emit(status) - - # If the topic is "slitd" + self.system_status_signal.emit(status) + if topic == "slitd": slit_width = data.get("SLITW", None) slit_offset = data.get("SLITO", None) if slit_width is not None and slit_offset is not None: self.slit_info_signal.emit(slit_width, slit_offset) - # If the topic is "calibd", update modulator states if topic == "calibd": self.new_message_signal.emit(f"Topic: {topic}, Payload: {payload}") self.update_modulator_states(data) - # If the topic is "powerinfo", update lamp states if topic == "powerd": self.new_message_signal.emit(f"Topic: {topic}, Payload: {payload}") - self.update_lamp_states(data) # Update lamp statesi + self.update_lamp_states(data) - # If the topic is "tcsd", handle TCS information if topic == "tcsd": self.update_tcs_info(data) @@ -159,7 +153,7 @@ def listen(self): self.logger.error(f"Error parsing JSON payload: {e}") else: self.logger.warning("Received malformed message (not two parts).") - + except Exception as e: self.logger.error(f"Error while listening for messages: {e}") finally: @@ -241,18 +235,39 @@ def update_tcs_info(self, data): else: self.logger.warning("AIRMASS data is not available.") - def _status_from_seq_waitstate(self, flags: Dict[str, bool]) -> str: - f = {k: bool(v) for k, v in (flags or {}).items()} - - if f.get("READOUT"): return "readout" - if f.get("EXPOSE"): return "exposing" - if f.get("ACQUIRE"): return "acquire" - if f.get("FOCUS"): return "focus" - if f.get("CALIB"): return "calib" - if f.get("USER"): return "user" + def _status_from_seq_waitstate(self, data) -> str: + if not isinstance(data, dict): + return "stopped" + + seqstate = data.get("seqstate") + if seqstate is not None: + state = str(seqstate).strip().upper() + state_map = { + "NOTREADY": "not_ready", + "READY": "idle", + "IDLE": "idle", + "READOUT": "readout", + "EXPOSE": "exposing", + "EXPOSING": "exposing", + "ACQUIRE": "acquire", + "FOCUS": "focus", + "CALIB": "calib", + "USER": "user", + "PAUSED": "paused", + "STOPPED": "stopped", + "ERROR": "stopped", + } + return state_map.get(state, "stopped") + + f = {str(k).upper(): bool(v) for k, v in data.items()} + if f.get("READOUT"): return "readout" + if f.get("EXPOSE"): return "exposing" + if f.get("ACQUIRE"): return "acquire" + if f.get("FOCUS"): return "focus" + if f.get("CALIB"): return "calib" + if f.get("USER"): return "user" return "idle" - class ZmqStatusServiceThread(QThread): def __init__(self, zmq_status_service): super().__init__() From 5eef15a449675f53c5607d81381f37a65fbd7bec Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Wed, 22 Apr 2026 17:31:24 -0700 Subject: [PATCH 2/3] Add Seq wait states --- pygui/layout_service.py | 35 +++++++---- pygui/zmq_status_service.py | 113 +++++++++++++++++++++++++----------- 2 files changed, 105 insertions(+), 43 deletions(-) diff --git a/pygui/layout_service.py b/pygui/layout_service.py index 321e96cc..fe53c1ce 100644 --- a/pygui/layout_service.py +++ b/pygui/layout_service.py @@ -193,16 +193,31 @@ def create_system_status_group(self): # Create a mapping for status colors status_map = { - "stopped": QColor(169, 169, 169), # Grey - "not_ready": QColor(255, 0, 0), # Red - "idle": QColor(255, 255, 0), # Yellow - "paused": QColor(255, 165, 0), # Orange - "exposing": QColor(0, 255, 0), # Green - "readout": QColor(0, 255, 0), # Green - "acquire": QColor(255, 255, 0), # Yellow - "focus": QColor(255, 255, 0), # Yellow - "calib": QColor(255, 255, 0), # Yellow - "user": QColor(255, 255, 0), # Yellow + "stopped": QColor(169, 169, 169), + "not_ready": QColor(255, 0, 0), + "idle": QColor(255, 255, 0), + "paused": QColor(255, 165, 0), + "exposing": QColor(0, 255, 0), + "readout": QColor(0, 255, 0), + + "moveto": QColor(255, 255, 0), + "acam_acquire": QColor(255, 255, 0), + "slicecam_fineacquire": QColor(255, 255, 0), + + "focus": QColor(255, 255, 0), + "calib": QColor(255, 255, 0), + "camera": QColor(255, 255, 0), + "flexure": QColor(255, 255, 0), + "power": QColor(255, 255, 0), + "slit": QColor(255, 255, 0), + "tcs": QColor(255, 255, 0), + "tcsop": QColor(255, 255, 0), + "user": QColor(255, 255, 0), + + # transitional / backward compatibility + "acam": QColor(255, 255, 0), + "slicecam": QColor(255, 255, 0), + "acquire": QColor(255, 255, 0), } # Create a dictionary to hold the status widgets, which we will enable/disable diff --git a/pygui/zmq_status_service.py b/pygui/zmq_status_service.py index c78d3e8f..c0f37752 100644 --- a/pygui/zmq_status_service.py +++ b/pygui/zmq_status_service.py @@ -29,6 +29,8 @@ def __init__(self, parent, broker_publish_endpoint="tcp://127.0.0.1:5556"): self.socket = None self.is_connected = False self.subscribed_topics = set() # Set of subscribed topics + self._last_seq_lifecycle_status = "stopped" + self._last_seq_wait_status = None # Set up logging self.setup_logging() @@ -127,10 +129,13 @@ def listen(self): if topic == "acamd": self.new_message_signal.emit(f"Topic: {topic}, Payload: {payload}") - # support both old and new sequencer topics - if topic in ("seq_waitstate", "seq_seqstate"): - status = self._status_from_seq_waitstate(data) - self.system_status_signal.emit(status) + elif topic == "seq_seqstate": + self._last_seq_lifecycle_status = self._status_from_seq_seqstate(data) + self._emit_resolved_system_status() + + elif topic == "seq_waitstate": + self._last_seq_wait_status = self._status_from_seq_waitstate(data) + self._emit_resolved_system_status() if topic == "slitd": slit_width = data.get("SLITW", None) @@ -235,38 +240,80 @@ def update_tcs_info(self, data): else: self.logger.warning("AIRMASS data is not available.") - def _status_from_seq_waitstate(self, data) -> str: + def _emit_resolved_system_status(self): + """ + If a wait-state is active, show that. + Otherwise show the broader sequencer lifecycle state. + """ + status = self._last_seq_wait_status or self._last_seq_lifecycle_status + self.system_status_signal.emit(status) + + def _status_from_seq_seqstate(self, data: Dict[str, Any]) -> str: + """ + Parse the overall sequencer lifecycle state. + """ if not isinstance(data, dict): return "stopped" - seqstate = data.get("seqstate") - if seqstate is not None: - state = str(seqstate).strip().upper() - state_map = { - "NOTREADY": "not_ready", - "READY": "idle", - "IDLE": "idle", - "READOUT": "readout", - "EXPOSE": "exposing", - "EXPOSING": "exposing", - "ACQUIRE": "acquire", - "FOCUS": "focus", - "CALIB": "calib", - "USER": "user", - "PAUSED": "paused", - "STOPPED": "stopped", - "ERROR": "stopped", - } - return state_map.get(state, "stopped") - - f = {str(k).upper(): bool(v) for k, v in data.items()} - if f.get("READOUT"): return "readout" - if f.get("EXPOSE"): return "exposing" - if f.get("ACQUIRE"): return "acquire" - if f.get("FOCUS"): return "focus" - if f.get("CALIB"): return "calib" - if f.get("USER"): return "user" - return "idle" + seqstate = str(data.get("seqstate", "")).strip().upper() + + state_map = { + "NOTREADY": "not_ready", + "READY": "idle", + "IDLE": "idle", + "PAUSED": "paused", + "STOPPED": "stopped", + "ERROR": "stopped", + } + + return state_map.get(seqstate, seqstate.lower() if seqstate else "stopped") + + def _status_from_seq_waitstate(self, flags: Dict[str, Any]) -> Optional[str]: + """ + Return the active wait-state if one is true, else None. + Returning None falls back to seq_seqstate. + """ + if not isinstance(flags, dict): + return None + + # Ignore metadata fields like "source" + f = { + str(k).upper(): bool(v) + for k, v in flags.items() + if str(k).lower() != "source" + } + + # Precedence matters if more than one field is true. + # Put the most user-meaningful states first. + wait_order = [ + ("READOUT", "readout"), + ("EXPOSE", "exposing"), + + # New replacement states for old ACQUIRE + ("MOVETO", "moveto"), + ("ACAM_ACQUIRE", "acam_acquire"), + ("SLICECAM_FINEACQUIRE", "slicecam_fineacquire"), + + ("FOCUS", "focus"), + ("CALIB", "calib"), + ("CAMERA", "camera"), + ("FLEXURE", "flexure"), + ("POWER", "power"), + ("SLIT", "slit"), + ("TCSOP", "tcsop"), + ("TCS", "tcs"), + ("USER", "user"), + + ("ACAM", "acam"), + ("SLICECAM", "slicecam"), + ("ACQUIRE", "acquire"), + ] + + for key, ui_status in wait_order: + if f.get(key, False): + return ui_status + + return None class ZmqStatusServiceThread(QThread): def __init__(self, zmq_status_service): From 0e38fca2fa36baf5837d4189e60c8c37762011d6 Mon Sep 17 00:00:00 2001 From: Prakriti Gupta Date: Wed, 22 Apr 2026 17:32:44 -0700 Subject: [PATCH 3/3] import --- pygui/zmq_status_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygui/zmq_status_service.py b/pygui/zmq_status_service.py index c0f37752..dcd57614 100644 --- a/pygui/zmq_status_service.py +++ b/pygui/zmq_status_service.py @@ -3,7 +3,7 @@ import logging import json from PyQt5.QtCore import pyqtSignal, QObject, QThread -from typing import Dict +from typing import Dict, Any, Optional class ZmqStatusService(QObject): # Signal to send a new message