diff --git a/parallax/__init__.py b/parallax/__init__.py index 92592e1d..0a2b8523 100644 --- a/parallax/__init__.py +++ b/parallax/__init__.py @@ -4,7 +4,7 @@ import os -__version__ = "1.2.1" +__version__ = "1.3.0" # allow multiple OpenMP instances os.environ["KMP_DUPLICATE_LIB_OK"] = "True" diff --git a/parallax/stage_listener.py b/parallax/stage_listener.py index 1c6cc325..67f19ce4 100644 --- a/parallax/stage_listener.py +++ b/parallax/stage_listener.py @@ -3,6 +3,8 @@ applications, using PyQt5 for threading and signals, and requests for HTTP requests. """ +import os +import json import logging import time from collections import deque @@ -11,13 +13,12 @@ import numpy as np import requests from PyQt5.QtCore import QObject, QThread, QTimer, pyqtSignal +from PyQt5.QtWidgets import QFileDialog # Set logger name logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) -# Set the logging level for PyQt5.uic.uiparser/properties to WARNING, to ignore DEBUG messages -logging.getLogger("PyQt5.uic.uiparser").setLevel(logging.WARNING) -logging.getLogger("PyQt5.uic.properties").setLevel(logging.WARNING) +logger.setLevel(logging.DEBUG) +package_dir = os.path.dirname(os.path.abspath(__file__)) class StageInfo(QObject): @@ -68,6 +69,9 @@ def __init__(self, stage_info=None): self.stage_x_global = None self.stage_y_global = None self.stage_z_global = None + self.yaw = None + self.pitch = None + self.roll = None class Worker(QObject): @@ -137,7 +141,10 @@ def fetchData(self): selected_probe = data["SelectedProbe"] probe = data["ProbeArray"][selected_probe] - if self.last_stage_info is None: # Initial + # At init, update stage info for connected stages + if self.last_stage_info is None: # Initial setup + for stage in data["ProbeArray"]: + self.dataChanged.emit(stage) self.last_stage_info = probe self.last_bigmove_stage_info = probe self.dataChanged.emit(probe) @@ -221,6 +228,11 @@ def __init__(self, model, stage_ui, probeCalibrationLabel): self.stage_global_data = None self.transM_dict = {} self.scale_dict = {} + self.snapshot_folder_path = None + self.stages_info = {} + + # Connect the snapshot button + self.stage_ui.ui.snapshot_btn.clicked.connect(self._snapshot_stage) def start(self): """Start the stage listener.""" @@ -232,10 +244,8 @@ def update_url(self): """Update the URL for the worker.""" # Update URL self.worker.update_url(self.model.stage_listener_url) - # Restart worker - # If there is an timeout error, stop the worker. - def get_last_moved_time(self, millisecond=False): + def get_timestamp(self, millisecond=False): """Get the last moved time of the stage. Args: @@ -284,13 +294,14 @@ def handleDataChange(self, probe): probe (dict): Probe data. """ # Format the current timestamp - self.timestamp_local = self.get_last_moved_time(millisecond=True) + self.timestamp_local = self.get_timestamp(millisecond=True) # id = probe["Id"] sn = probe["SerialNumber"] local_coords_x = round(probe["Stage_X"] * 1000, 1) local_coords_y = round(probe["Stage_Y"] * 1000, 1) local_coords_z = 15000 - round(probe["Stage_Z"] * 1000, 1) + logger.debug(f"timestamp_local: {self.timestamp_local}, sn: {sn}") # update into model moving_stage = self.model.stages.get(sn) @@ -316,8 +327,20 @@ def handleDataChange(self, probe): self._updateGlobalDataTransformM(sn, moving_stage, transM, scale) else: logger.debug(f"Transformation matrix or scale not found for serial number: {sn}") - else: - logger.debug(f"Serial number {sn} not found in transformation or scale dictionary") + + # Update stage info + self._update_stages_info(moving_stage) + + def _update_stages_info(self, stage): + """Update stage info. + + Args: + stage (Stage): Stage object. + """ + if stage is None: + return + + self.stages_info[stage.sn] = self._get_stage_info_json(stage) def _updateGlobalDataTransformM(self, sn, moving_stage, transM, scale): """ @@ -526,3 +549,74 @@ def set_low_freq_default(self, interval=1000): self.worker.curr_interval = self.worker._low_freq_interval self.worker.start(interval=self.worker._low_freq_interval) # print("low_freq: 1000 ms") + + def _get_stage_info_json(self, stage): + """Create a JSON file for the stage info. + + Args: + stage (Stage): Stage object. + """ + stage_data = None + + if stage is None: + logger.error("Error: Stage object is None. Cannot save JSON.") + return + + if not hasattr(stage, 'sn') or not stage.sn: + logger.error("Error: Invalid stage serial number (sn). Cannot save JSON.") + return + + stage_data = { + "sn": stage.sn, + "name": stage.name, + "Stage_X": stage.stage_x, + "Stage_Y": stage.stage_y, + "Stage_Z": stage.stage_z, + "global_X": stage.stage_x_global, + "global_Y": stage.stage_y_global, + "global_Z": stage.stage_z_global, + "yaw": stage.yaw, + "pitch": stage.pitch, + "roll": stage.roll + } + + return stage_data + + def _snapshot_stage(self): + """Snapshot the current stage info. Handler for the stage snapshot button.""" + selected_sn = self.stage_ui.get_selected_stage_sn() + now = datetime.now().astimezone() + info = {"timestamp": now.isoformat(timespec='milliseconds'), + "selected_sn": selected_sn, "probes:": self.stages_info} + + # If no folder is set, default to the "Documents" directory + if self.snapshot_folder_path is None: + self.snapshot_folder_path = os.path.join(os.path.expanduser("~"), "Documents") + + # Open save file dialog, defaulting to the last used folder + now_fmt = now.strftime("%Y-%m-%dT%H%M%S%z") + file_path, _ = QFileDialog.getSaveFileName( + None, + "Save Stage Info", + os.path.join(self.snapshot_folder_path, f"{now_fmt}.json"), + "JSON Files (*.json)" + ) + + if not file_path: # User canceled the dialog + print("Save canceled by user.") + return + + # Update `snapshot_folder_path` to the selected folder + self.snapshot_folder_path = os.path.dirname(file_path) + + # Ensure the file has the correct `.json` extension + if not file_path.endswith(".json"): + file_path += ".json" + + # Write the JSON file + try: + with open(file_path, "w", encoding="utf-8") as f: + json.dump(info, f, indent=4) + print(f"Stage info saved at {file_path}") + except Exception as e: + print(f"Error saving stage info: {e}") diff --git a/parallax/stage_server_ipconfig.py b/parallax/stage_server_ipconfig.py index f142c205..7bcceaf6 100644 --- a/parallax/stage_server_ipconfig.py +++ b/parallax/stage_server_ipconfig.py @@ -15,7 +15,7 @@ from PyQt5.QtCore import Qt logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.WARNING) package_dir = os.path.dirname(os.path.abspath(__file__)) debug_dir = os.path.join(os.path.dirname(package_dir), "debug") diff --git a/ui/resources/save.png b/ui/resources/save.png new file mode 100644 index 00000000..b5b21457 Binary files /dev/null and b/ui/resources/save.png differ diff --git a/ui/stage_info.ui b/ui/stage_info.ui index 79a8b1b9..ce4c66fb 100644 --- a/ui/stage_info.ui +++ b/ui/stage_info.ui @@ -45,8 +45,8 @@ 2 - - + + 0 @@ -59,15 +59,13 @@ 30 - - - Global coords - - + + SN + - - + + 0 @@ -81,72 +79,75 @@ - Z: + X: - - - - QLabel { - color: yellow; -} - - - - + + + + + 0 + 25 + - - - - - 25 + 16777215 25 - - background-color : yellow + + X: + + + + - + μm - - - - - 50 - 0 - + + + + μm - - - 50 - 16777215 - + + + + + + QLabel { + color: yellow; +} - μm + - + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - 16777215 - 16777215 - + + + + QLabel { + color: yellow; +} - + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + - - + + 0 @@ -160,50 +161,65 @@ - X: + Y: - - + + 0 - 30 + 25 16777215 - 30 + 25 - SN + Z: - - + + 0 - 25 + 30 16777215 - 25 + 30 + + + 75 + true + + + + + Global coords + + + + + + - Y: + μm - - + + QLabel { color: yellow; @@ -212,89 +228,116 @@ - - - - - - - - + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - μm + + + + + 16777215 + 16777215 + - - - - - μm + - + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + 0 - 30 + 25 16777215 - 30 + 25 - Local coords + Y: - - - - QLabel { - color: yellow; -} + + + + + 40 + 40 + + + + + 40 + 16777215 + + + + MPM Http Server - - + + + + + resources/mpmServer.pngresources/mpmServer.png + + + + 30 + 30 + - - + + - 16777215 - 5 + 40 + 16777215 - + μm - - + + - μm + - + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + + + 16777215 + 5 + + - - + - - + + μm @@ -313,8 +356,14 @@ - - + + + + + 40 + 0 + + 40 @@ -326,27 +375,43 @@ - - + + 0 - 25 + 30 16777215 - 25 + 30 + + + 75 + true + + - X: + Local coords - - + + + + - + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + 0 @@ -364,43 +429,40 @@ - - + + - 0 - 25 + 40 + 40 - 16777215 - 25 + 40 + 16777215 + + Stage Snapshot + - Y: + - - - - - - - 0 - 40 - + + + resources/save.pngresources/save.png - + - 16777215 - 40 + 24 + 24 - - + + 0 @@ -409,25 +471,16 @@ - 45 - 16777215 + 16777215 + 40 - - MPM Http Server - - - - - - - resources/mpmServer.pngresources/mpmServer.png - - - - 36 - 36 - + + + 50 + false + true +