diff --git a/Gui/QtGUIutils/TessieCoolingApp.py b/Gui/QtGUIutils/TessieCoolingApp.py index 7641cfed..915d468a 100644 --- a/Gui/QtGUIutils/TessieCoolingApp.py +++ b/Gui/QtGUIutils/TessieCoolingApp.py @@ -16,6 +16,7 @@ class TessieMonitorWorker(QObject): temp_update = pyqtSignal(list) env_update = pyqtSignal(float, float) + set_update = pyqtSignal(list) status_msg = pyqtSignal(str) def __init__(self, instruments=None, interval_ms: int = 2000): @@ -56,6 +57,14 @@ def poll(self): except Exception as e: logger.debug(f"Worker temp read issue: {e}") + # Temperature setpoints + try: + setpoints = coldbox.read("TEMPERATURE_SET") + if isinstance(setpoints, list) and len(setpoints) == 8: + self.set_update.emit(setpoints) + except Exception as e: + logger.debug(f"Worker setpoint read issue: {e}") + # Environment rh = None dp = None @@ -94,6 +103,7 @@ class TessieCoolingApp(QWidget): temp_update_signal = pyqtSignal(list) # (rh in %, dew point in C) env_update_signal = pyqtSignal(float, float) + set_update_signal = pyqtSignal(list) def __init__(self, master=None): super(TessieCoolingApp, self).__init__() @@ -101,6 +111,14 @@ def __init__(self, master=None): self.instruments = None self._thread = None self._worker = None + # Latest cached values + self._latest_temperatures = None # type: list + self._last_temp_update_ts = 0.0 + self._latest_rh = None # type: float + self._latest_dp = None # type: float + self._last_env_update_ts = 0.0 + self._latest_setpoints = None # type: list + self._last_set_update_ts = 0.0 # Initialize UI self.initUI() @@ -108,6 +126,7 @@ def __init__(self, master=None): # Connect signals (widget-owned signals kept for compatibility) self.temp_update_signal.connect(self.update_temperature_display) self.env_update_signal.connect(self.update_environment_display) + self.set_update_signal.connect(self.update_setpoints_cache) # Start temperature monitoring if instruments are available if self.master and hasattr(self.master, 'instruments'): @@ -231,6 +250,7 @@ def start_temperature_monitoring(self): # Bridge worker signals to existing slots self._worker.temp_update.connect(self.update_temperature_display) self._worker.env_update.connect(self.update_environment_display) + self._worker.set_update.connect(self.update_setpoints_cache) # Clean-up when thread finishes self._thread.finished.connect(self._worker.deleteLater) @@ -254,6 +274,10 @@ def stop_temperature_monitoring(self): def update_temperature_display(self, temperatures): """Update the temperature display with new values.""" try: + # cache latest temperatures and timestamp + if isinstance(temperatures, list) and len(temperatures) == 8: + self._latest_temperatures = [float(t) if isinstance(t, (int, float)) else t for t in temperatures] + self._last_temp_update_ts = time.time() for i, temp in enumerate(temperatures): channel = i + 1 if channel in self.temp_labels: @@ -282,6 +306,13 @@ def update_temperature_display(self, temperatures): def update_environment_display(self, rh_value: float, dew_point: float): """Update the environment display with RH (%) and Dew Point (°C).""" try: + # cache latest env values and timestamp + if isinstance(rh_value, (int, float)): + self._latest_rh = float(rh_value) + self._last_env_update_ts = time.time() + if isinstance(dew_point, (int, float)): + self._latest_dp = float(dew_point) + self._last_env_update_ts = time.time() # Relative Humidity formatting and color coding if isinstance(rh_value, (int, float)): rh_str = f"{rh_value:.1f} %" @@ -307,6 +338,38 @@ def update_environment_display(self, rh_value: float, dew_point: float): self.dp_value_label.setStyleSheet("QLabel { color: gray; }") except Exception as e: logger.error(f"Error updating environment display: {e}") + + def update_setpoints_cache(self, setpoints): + """Cache the latest temperature setpoints list (length 8).""" + try: + if isinstance(setpoints, list) and len(setpoints) == 8: + self._latest_setpoints = [float(s) if isinstance(s, (int, float)) else s for s in setpoints] + self._last_set_update_ts = time.time() + except Exception as e: + logger.debug(f"Error caching setpoints: {e}") + + # --- Public getters for other components (e.g., TestHandler) --- + def get_latest_temperatures(self, with_timestamp: bool = False): + """Return a copy of the latest temperatures [8] or None. If with_timestamp, also return the unix ts.""" + temps = list(self._latest_temperatures) if isinstance(self._latest_temperatures, list) else None + if with_timestamp: + return temps, self._last_temp_update_ts + return temps + + def get_latest_env(self, with_timestamp: bool = False): + """Return (rh, dew_point) or (rh, dew_point, ts) if with_timestamp.""" + rh = self._latest_rh + dp = self._latest_dp + if with_timestamp: + return rh, dp, self._last_env_update_ts + return rh, dp + + def get_latest_setpoints(self, with_timestamp: bool = False): + """Return list of 8 temperature setpoints or None. If with_timestamp, also return ts.""" + s = list(self._latest_setpoints) if isinstance(self._latest_setpoints, list) else None + if with_timestamp: + return s, self._last_set_update_ts + return s def refresh_temperatures(self): """Manually refresh temperature readings.""" diff --git a/Gui/python/TestHandler.py b/Gui/python/TestHandler.py index ee327f8e..c02d86e6 100644 --- a/Gui/python/TestHandler.py +++ b/Gui/python/TestHandler.py @@ -54,6 +54,7 @@ from Gui.QtGUIutils.QtMatplotlibUtils import ScanCanvas +from Gui.QtGUIutils.TessieCoolingApp import TessieCoolingApp from Gui.python.TestValidator import ResultGrader from Gui.python.ANSIColoringParser import parseANSI @@ -670,6 +671,12 @@ def runSingleTest(self, testName, nextTest=None): self.updateOptimizedXMLValues() self.configTest() + if site_settings.cooler == "Tessie": + # Log Tessie readings (temps/env) from widget cache at the start of each test + try: + self._log_tessie_from_widget("start") + except Exception as e: + logger.debug(f"Could not log Tessie readings from widget: {e}") # Make sure that the GUI is not trying to write to the root directory try: @@ -1065,12 +1072,126 @@ def urgentStop(self): self.haltSignal.emit(self.halt) self.starttime = None + def _find_tessie_widget(self): + """Try to find a TessieCoolingApp widget in the UI tree.""" + candidates = [] + try: + candidates.append(getattr(self.master, 'window', None)) + except Exception: + pass + candidates.extend([self.master if hasattr(self, 'master') else None, self.runwindow]) + for parent in filter(None, candidates): + try: + w = parent.findChild(TessieCoolingApp) + if w is not None: + return w + except Exception: + continue + # Fallback to a direct attribute if provided by the app + w = getattr(self.master, 'tessie_widget', None) + return w + + def _log_tessie_from_widget(self, phase: str = "start"): + """Fetch cached Tessie readings from the TessieCoolingApp widget and persist them. + Uses widget cache only, no direct instrument queries. + + phase: 'start' or 'end' to control filename and console label. + - start -> writes tessie_start_temps.json (kept for backward compatibility) + - end -> writes tessie_end_values.json + """ + widget = self._find_tessie_widget() + temps = None + rh = None + dp = None + setpoints = None + if widget: + try: + if hasattr(widget, 'get_latest_temperatures'): + temps = widget.get_latest_temperatures() + except Exception: + temps = None + try: + if hasattr(widget, 'get_latest_env'): + env = widget.get_latest_env() + if isinstance(env, tuple): + rh, dp = env + except Exception: + rh, dp = None, None + try: + if hasattr(widget, 'get_latest_setpoints'): + setpoints = widget.get_latest_setpoints() + except Exception: + setpoints = None + + ts = datetime.now().strftime("%H:%M:%S") + # Build console message + label = "start-of-test" if str(phase).lower().startswith("s") else "end-of-test" + parts = [] + if isinstance(temps, list) and len(temps) == 8: + parts.append("temps=" + ", ".join(f"{t:.2f}°C" for t in temps)) + if isinstance(rh, (int, float)): + parts.append(f"RH={rh:.1f}%") + if isinstance(dp, (int, float)): + parts.append(f"DP={dp:.2f}°C") + # Include setpoints if available + try: + if isinstance(setpoints, list) and len(setpoints) == 8: + uniq = {round(float(x), 2) for x in setpoints if isinstance(x, (int, float))} + if len(uniq) == 1: + sval = next(iter(uniq)) + parts.append(f"set={sval:.2f}°C") + else: + parts.append("set=[" + ", ".join( + f"{float(x):.2f}°C" if isinstance(x, (int, float)) else str(x) + for x in setpoints + ) + "]") + except Exception: + pass + + msg = f"[{ts}] Tessie {label} (widget): " + ("; ".join(parts) if parts else "unavailable") + + try: + for console in getattr(self.runwindow, 'ConsoleViews', []): + self.outputString.emit(msg, console) + except Exception: + pass + logger.info(msg) + + # Persist snapshot to output dir + try: + if getattr(self, 'output_dir', None): + import os, json + # Keep filenames as before for compatibility + if str(phase).lower().startswith("s"): + out_path = os.path.join(self.output_dir, "tessie_start_temps.json") + else: + out_path = os.path.join(self.output_dir, "tessie_end_values.json") + payload = { + "source": "widget", + "timestamp": ts, + "temperatures": temps, + "relative_humidity": rh, + "dew_point": dp, + "phase": "start" if str(phase).lower().startswith("s") else "end", + "temperature_setpoints": setpoints, + } + with open(out_path, "w") as f: + json.dump(payload, f, indent=2) + except Exception: + pass + def validateTest(self): for process in self.run_processes: if process.state() == QProcess.Running: return self.finished_tests.append(self.currentTest) + if site_settings.cooler == "Tessie": + # Snapshot end-of-test Tessie values (from widget cache only) + try: + self._log_tessie_from_widget("end") + except Exception: + pass try: passed = [] results = []