Skip to content

Wip/export stage coords #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 4, 2025
2 changes: 1 addition & 1 deletion parallax/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
116 changes: 105 additions & 11 deletions parallax/stage_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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}")
2 changes: 1 addition & 1 deletion parallax/stage_server_ipconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Binary file added ui/resources/save.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading