Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.16.1"
__version__ = "1.16.2"

# allow multiple OpenMP instances
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
3 changes: 1 addition & 2 deletions parallax/control_panel/control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ def init_stages(self):

def refresh_stages(self):
"""Refreshes the stages using the updated server configuration."""
print("Refreshing stages with updated server configuration...")
# If URL is not updated or invalid, do nothing
if not self.stage_server_ipconfig.update_url():
return

print("Refreshing stages with updated server configuration...")
# refresh the stage using server IP address
self.stage_server_ipconfig.refresh_stages() # Update stages server url to model # models.transforms updated
self.stageUI.initialize()
Expand All @@ -190,7 +190,6 @@ def refresh_stages(self):

# Update url on StageLinstener
self.stageListener.update_url()
print("Stages refreshed successfully.")

def stage_server_ipconfig_btn_handler(self):
"""
Expand Down
6 changes: 3 additions & 3 deletions parallax/control_panel/probe_calibration_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,9 @@ def probe_detect_default_status_ui(self, sn=None):
# Reset the probe calibration status
self.clearRequested.emit(self.selected_stage_id)
# update global coords. Set to '-' on UI
else: # Reset all probel calibration status
for sn in self.model.get_list_of_stage_sns():
self.clearRequested.emit(self.selected_stage_id)
else: # Reset all probe calibration status
for stage_sn in self.model.get_list_of_stage_sns():
self.clearRequested.emit(stage_sn)

# Set as Uncalibrated
self.calculator.set_calc_functions()
Expand Down
3 changes: 3 additions & 0 deletions parallax/handlers/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ def _disable(self, sn):
"""
if not sn:
return
if self.findChild(QGroupBox, f"groupBox_{sn}") is None:
print("Error: Group box not found")
return
# Clear the QLineEdit for the stage
self.findChild(QLineEdit, f"localX_{sn}").setText("")
self.findChild(QLineEdit, f"localY_{sn}").setText("")
Expand Down
15 changes: 9 additions & 6 deletions parallax/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,12 @@ def scan_for_usb_stages(self):
print("Scanning for USB stages...")
server = PathfinderServer(self.config.pathfinder_server.url)
instances = server.get_instances()
self.stage_instances = {} # Reset internal state before updating
for instance in instances:
stage = StageObj.from_info(info=instance)
sn = stage.sn
self.stage_instances[sn] = stage
self.stage_instances[stage.sn] = stage
self.nStages = len(self.stage_instances)
self.instantiate_session() # Sync with session after scanning
print(" Stages:", list(self.stage_instances.keys()))

# =========================
Expand Down Expand Up @@ -133,10 +134,12 @@ def get_stage(self, sn):
return self.stage_instances.get(sn)

def reset_stage_obj_info(self, sn: str):
self.stage_instances.get(sn).stage_x_global = None
self.stage_instances.get(sn).stage_y_global = None
self.stage_instances.get(sn).stage_z_global = None
self.stage_instances.get(sn).stage_bregma = None
stage_obj = self.stage_instances.get(sn)
if stage_obj is not None:
stage_obj.stage_x_global = None
stage_obj.stage_y_global = None
stage_obj.stage_z_global = None
stage_obj.stage_bregma = None

# =========================
# Stages calibration
Expand Down
48 changes: 29 additions & 19 deletions parallax/stages/stage_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,21 @@


class PathfinderServer:
"""Retrieve and manage information about the stages."""
"""Utility to fetch stage information from the hardware server."""

def __init__(self, url: str):
"""Initialize StageInfo."""
self.url = url
self.nStages = 0
self.stages_sn = []

def get_instances(self) -> list:
"""Get the instances of the stages.

Returns:
list: List of stage instance dicts.
"""
stages = []
"""Fetch raw list of stages from the server."""
try:
response = requests.get(self.url, timeout=1)
if response.status_code == 200:
data = response.json()
self.nStages = data.get("Probes", 0)
for stage in data.get("ProbeArray", []):
self.stages_sn.append(stage["SerialNumber"])
stages.append(stage)
return response.json().get("ProbeArray", [])
except Exception as e:
print("Stage HttpServer not enabled.")
logger.debug(f"Stage HttpServer not enabled: {e}")

return stages
return []


class Worker(threading.Thread):
Expand Down Expand Up @@ -77,6 +64,7 @@ def stop(self):
def update_url(self, url):
"""Change the URL for data fetching."""
self.url = url
self.is_error_log_printed = False

def run(self):
"""The main loop of the native thread with crash protection."""
Expand Down Expand Up @@ -264,5 +252,27 @@ def stageNotMovingStatus(self, probe):
probeDetector.enable_calibration(self.worker.last_move_detected_time + self.worker.IDLE_TIME, sn)

def update_url(self):
"""Update the URL for the worker thread."""
self.worker.update_url(self.model.config.pathfinder_server.url)
"""Stop the old thread and start a fresh one with the new URL."""
# Stop the current worker
if self.worker.is_alive():
self.worker.stop()
if not self.worker.join(timeout=2.0):
# If it doesn't join, it's safer to just update the URL
# rather than force-killing or starting a second thread.
logger.warning("Worker failed to join; falling back to URL update.")
self.worker.update_url(self.model.config.pathfinder_server.url)
return

# Create a new worker with the updated URL
new_url = self.model.config.pathfinder_server.url
self.worker = Worker(new_url)

# Re-connect the signals to the new worker
self.worker.dataChanged.connect(self.handleDataChange)
self.worker.stage_moving.connect(self.stageMovingStatus)
self.worker.stage_not_moving.connect(self.stageNotMovingStatus)

# Start the new worker
self.worker.start()

print(f"Stage Listener restarted with URL: {new_url}")
4 changes: 2 additions & 2 deletions parallax/stages/stage_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ def __init__(self, control_panel):
self.previous_stage_id = None

# initialize UI
self._initialize()
self.initialize()

def _initialize(self):
def initialize(self):
"""Initialize the stage UI with current state."""
# 1. Block signals while building the list
self.ui.stage_selector.blockSignals(True)
Expand Down
Loading