From f6da6d4018ee1012d737ef16a93c61414ca87523 Mon Sep 17 00:00:00 2001 From: Hanna <146143766+hannalee2@users.noreply.github.com> Date: Fri, 5 Jun 2026 08:50:18 -0700 Subject: [PATCH] Fix bug) MPM stage server port update (#181) * Check stage_obj is None before the access * Change the private to public function in StageUI * Fix bug in clearing calib status for all stages * Fix bug: updating MPM stage server port * Update tag version * Ruff formatting * Fix TypeO in comments * Fix stage listener thread restart logic --- parallax/__init__.py | 2 +- parallax/control_panel/control_panel.py | 3 +- .../probe_calibration_handler.py | 6 +-- parallax/handlers/calculator.py | 3 ++ parallax/model.py | 15 +++--- parallax/stages/stage_listener.py | 48 +++++++++++-------- parallax/stages/stage_ui.py | 4 +- 7 files changed, 48 insertions(+), 33 deletions(-) diff --git a/parallax/__init__.py b/parallax/__init__.py index a46e7fab..155cacb7 100644 --- a/parallax/__init__.py +++ b/parallax/__init__.py @@ -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" diff --git a/parallax/control_panel/control_panel.py b/parallax/control_panel/control_panel.py index 7c820f00..e128779c 100644 --- a/parallax/control_panel/control_panel.py +++ b/parallax/control_panel/control_panel.py @@ -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() @@ -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): """ diff --git a/parallax/control_panel/probe_calibration_handler.py b/parallax/control_panel/probe_calibration_handler.py index 8953411e..18c9481b 100644 --- a/parallax/control_panel/probe_calibration_handler.py +++ b/parallax/control_panel/probe_calibration_handler.py @@ -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() diff --git a/parallax/handlers/calculator.py b/parallax/handlers/calculator.py index c9bac0dc..314567c1 100644 --- a/parallax/handlers/calculator.py +++ b/parallax/handlers/calculator.py @@ -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("") diff --git a/parallax/model.py b/parallax/model.py index 596b5372..cdedcd3a 100755 --- a/parallax/model.py +++ b/parallax/model.py @@ -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())) # ========================= @@ -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 diff --git a/parallax/stages/stage_listener.py b/parallax/stages/stage_listener.py index a18361ba..bcb2d276 100644 --- a/parallax/stages/stage_listener.py +++ b/parallax/stages/stage_listener.py @@ -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): @@ -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.""" @@ -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}") diff --git a/parallax/stages/stage_ui.py b/parallax/stages/stage_ui.py index 6f3dc634..02459b13 100644 --- a/parallax/stages/stage_ui.py +++ b/parallax/stages/stage_ui.py @@ -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)