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
+