From e00ab99c11979196a6f4e952304e8c182de060b6 Mon Sep 17 00:00:00 2001 From: hannalee2 Date: Wed, 26 Mar 2025 11:28:33 -0700 Subject: [PATCH 1/9] Added parallax binding --- src/ephys_link/back_end/platform_handler.py | 4 + src/ephys_link/bindings/parallax_binding.py | 265 ++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 src/ephys_link/bindings/parallax_binding.py diff --git a/src/ephys_link/back_end/platform_handler.py b/src/ephys_link/back_end/platform_handler.py index b2e44f4..61e5f40 100644 --- a/src/ephys_link/back_end/platform_handler.py +++ b/src/ephys_link/back_end/platform_handler.py @@ -26,6 +26,7 @@ from vbl_aquarium.models.unity import Vector4 from ephys_link.bindings.mpm_binding import MPMBinding +from ephys_link.bindings.parallax_binding import ParallaxBinding from ephys_link.utils.base_binding import BaseBinding from ephys_link.utils.console import Console from ephys_link.utils.converters import vector4_to_array @@ -77,6 +78,9 @@ def _get_binding_instance(self, options: EphysLinkOptions) -> BaseBinding: # Pass in HTTP port for Pathfinder MPM. if binding_cli_name == "pathfinder-mpm": return MPMBinding(options.mpm_port) + + if binding_cli_name == "parallax": + return ParallaxBinding(options.parallax_port) # Otherwise just return the binding. return binding_type() diff --git a/src/ephys_link/bindings/parallax_binding.py b/src/ephys_link/bindings/parallax_binding.py new file mode 100644 index 0000000..99b6002 --- /dev/null +++ b/src/ephys_link/bindings/parallax_binding.py @@ -0,0 +1,265 @@ +from asyncio import get_running_loop, sleep +from json import dumps +from typing import Any, final, override + +from requests import JSONDecodeError, get, put +from vbl_aquarium.models.unity import Vector3, Vector4 + +from ephys_link.utils.base_binding import BaseBinding +from ephys_link.utils.converters import scalar_mm_to_um, vector4_to_array + + +@final +class ParallaxBinding(BaseBinding): + """Bindings for Parallax platform.""" + + # Server cache lifetime (60 FPS). + CACHE_LIFETIME = 1 / 60 + + # Movement polling preferences. + UNCHANGED_COUNTER_LIMIT = 10 + UNCHANGED_COUNTER_LIMIT_DEPTH = 50 + POLL_INTERVAL = 0.1 + + # Speed preferences (mm/s to use coarse mode). + COARSE_SPEED_THRESHOLD = 0.1 + INSERTION_SPEED_LIMIT = 9_000 + + def __init__(self, port: int = 8081) -> None: + """Initialize connection to MPM HTTP server. + + Args: + port: Port number for MPM HTTP server. + """ + self._url = f"http://localhost:{port}" + self._movement_stopped = False + + # Data cache. + self.cache: dict[str, Any] = {} # pyright: ignore [reportExplicitAny] + self.cache_time = 0 + + @staticmethod + @override + def get_display_name() -> str: + return "Parallax" + + @staticmethod + @override + def get_cli_name() -> str: + return "parallax" + + @override + async def get_manipulators(self) -> list[str]: + data = await self._query_data() + return list(data.keys()) + + @override + async def get_axes_count(self) -> int: + return 3 + + @override + def get_dimensions(self) -> Vector4: + return Vector4(x=15, y=15, z=15, w=15) + + @override + async def get_position(self, manipulator_id: str) -> Vector4: + manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) + global_z = float(manipulator_data.get("global_Z", 0.0) or 0.0) + await sleep(self.POLL_INTERVAL) # Wait for the stage to stabilize. + + global_x = float(manipulator_data.get("global_X", 0.0) or 0.0) + global_y = float(manipulator_data.get("global_Y", 0.0) or 0.0) + + return Vector4(x=global_x, y=global_y, z=global_z, w=global_z) + + @override + async def get_angles(self, manipulator_id: str) -> Vector3: + manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) + + yaw = int(manipulator_data.get("yaw", 0) or 0) + pitch = int(manipulator_data.get("pitch", 90) or 90) + roll = int(manipulator_data.get("roll", 0) or 0) + + return Vector3(x=yaw, y=pitch, z=roll) + + @override + async def get_shank_count(self, manipulator_id: str) -> int: + manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) + return int(manipulator_data.get("shank_cnt", 1) or 1) + + @override + def get_movement_tolerance(self) -> float: + return 0.01 + + @override + async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4: + # Keep track of the previous position to check if the manipulator stopped advancing. + current_position = await self.get_position(manipulator_id) + previous_position = current_position + unchanged_counter = 0 + + # Set step mode based on speed. + await self._put_request( + { + "move_type": "stepMode", + "stage_sn": manipulator_id, + "step_mode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1, + } + ) + + # Send move request. + await self._put_request( + { + "move_type": "moveXYZ", + "world": "global", # Use global coordinates + "stage_sn": manipulator_id, + "Absolute": 1, + "Stereotactic": 0, + "AxisMask": 7, + "x": position.x, + "y": position.y, + "z": position.z, + } + ) + # Wait for the manipulator to reach the target position or be stopped or stuck. + while ( + not self._movement_stopped + and not self._is_vector_close(current_position, position) + and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT_DEPTH + ): + # Wait for a short time before checking again. + await sleep(self.POLL_INTERVAL) + + # Update current position. + current_position = await self.get_position(manipulator_id) + + # Check if manipulator is not moving. + if self._is_vector_close(previous_position, current_position): + # Position did not change. + unchanged_counter += 1 + else: + # Position changed. + unchanged_counter = 0 + previous_position = current_position + + # Reset movement stopped flag. + self._movement_stopped = False + + # Return the final position. + return await self.get_position(manipulator_id) + + @override + async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float: + # Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly. + current_depth = (await self.get_position(manipulator_id)).w + previous_depth = current_depth + unchanged_counter = 0 + + # Send move request. + # Convert mm/s to um/min and cap speed at the limit. + await self._put_request( + { + "move_type": "insertion", + "stage_sn": manipulator_id, + "world": "global", # distance in global space + "distance": scalar_mm_to_um(current_depth - depth), + "rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT), + } + ) + + # Wait for the manipulator to reach the target depth or be stopped or get stuck. + while ( + not self._movement_stopped + and not abs(current_depth - depth) <= self.get_movement_tolerance() + and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT + ): + # Wait for a short time before checking again. + await sleep(self.POLL_INTERVAL) + + # Get the current depth. + current_depth = (await self.get_position(manipulator_id)).w + + # Check if manipulator is not moving. + if abs(previous_depth - current_depth) <= self.get_movement_tolerance(): + # Depth did not change. + unchanged_counter += 1 + else: + # Depth changed. + unchanged_counter = 0 + previous_depth = current_depth + + # Reset movement stopped flag. + self._movement_stopped = False + + # Return the final depth. + return float((await self.get_position(manipulator_id)).w) + + @override + async def stop(self, manipulator_id: str) -> None: + request: dict[str, str | int | float] = { + "PutId": "stop", + "Probe": manipulator_id, + } + await self._put_request(request) + self._movement_stopped = True + + @override + def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4: + # unified <- platform + # +x <- +x + # +y <- +z + # +z <- +y + # +w <- +w + + return Vector4( + x=platform_space.x, + y=platform_space.z, + z=platform_space.y, + w=platform_space.w, + ) + + @override + def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4: + # platform <- unified + # +x <- +x + # +y <- +z + # +z <- +y + # +w <- -w + + return Vector4( + x=unified_space.x, + y=unified_space.z, + z=unified_space.y, + w=unified_space.w, + ) + + # Helper functions. + async def _query_data(self) -> dict[str, Any]: # pyright: ignore [reportExplicitAny] + try: + # Update cache if it's expired. + if get_running_loop().time() - self.cache_time > self.CACHE_LIFETIME: + # noinspection PyTypeChecker + self.cache = (await get_running_loop().run_in_executor(None, get, self._url)).json() + self.cache_time = get_running_loop().time() + except ConnectionError as connectionError: + error_message = f"Unable to connect to MPM HTTP server: {connectionError}" + raise RuntimeError(error_message) from connectionError + except JSONDecodeError as jsonDecodeError: + error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}" + raise ValueError(error_message) from jsonDecodeError + else: + # Return cached data. + return self.cache + + async def _manipulator_data(self, manipulator_id: str) -> dict[str, Any]: # pyright: ignore [reportExplicitAny] + """Retrieve data for a specific manipulator (probe) using its serial number.""" + data = await self._query_data() + + if manipulator_id in data: + return data[manipulator_id] + + async def _put_request(self, request: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny] + _ = await get_running_loop().run_in_executor(None, put, self._url, dumps(request)) + + def _is_vector_close(self, target: Vector4, current: Vector4) -> bool: + return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3]) From 15b4bdd3b9a64b311d8f0a65e2242cade4604c60 Mon Sep 17 00:00:00 2001 From: hannalee2 Date: Wed, 26 Mar 2025 11:54:58 -0700 Subject: [PATCH 2/9] No port option provided. Need to be updated --- src/ephys_link/back_end/platform_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ephys_link/back_end/platform_handler.py b/src/ephys_link/back_end/platform_handler.py index 61e5f40..f61369e 100644 --- a/src/ephys_link/back_end/platform_handler.py +++ b/src/ephys_link/back_end/platform_handler.py @@ -80,7 +80,7 @@ def _get_binding_instance(self, options: EphysLinkOptions) -> BaseBinding: return MPMBinding(options.mpm_port) if binding_cli_name == "parallax": - return ParallaxBinding(options.parallax_port) + return ParallaxBinding() # Otherwise just return the binding. return binding_type() From 2d81b7e1d9b2a81230be8fd2910c5c0036394b42 Mon Sep 17 00:00:00 2001 From: hannalee2 Date: Thu, 27 Mar 2025 14:22:52 -0700 Subject: [PATCH 3/9] Update w to 15000-w in space conversion function --- src/ephys_link/bindings/parallax_binding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ephys_link/bindings/parallax_binding.py b/src/ephys_link/bindings/parallax_binding.py index 99b6002..15387b8 100644 --- a/src/ephys_link/bindings/parallax_binding.py +++ b/src/ephys_link/bindings/parallax_binding.py @@ -125,7 +125,7 @@ async def set_position(self, manipulator_id: str, position: Vector4, speed: floa while ( not self._movement_stopped and not self._is_vector_close(current_position, position) - and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT_DEPTH + and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT ): # Wait for a short time before checking again. await sleep(self.POLL_INTERVAL) @@ -171,7 +171,7 @@ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> fl while ( not self._movement_stopped and not abs(current_depth - depth) <= self.get_movement_tolerance() - and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT + and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT_DEPTH ): # Wait for a short time before checking again. await sleep(self.POLL_INTERVAL) @@ -215,7 +215,7 @@ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4: x=platform_space.x, y=platform_space.z, z=platform_space.y, - w=platform_space.w, + w=self.get_dimensions().w-platform_space.w, ) @override @@ -230,7 +230,7 @@ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4: x=unified_space.x, y=unified_space.z, z=unified_space.y, - w=unified_space.w, + w=self.get_dimensions().w-unified_space.w, ) # Helper functions. From 2412e65d46be6fd12ec11614cc023aecdadade7d Mon Sep 17 00:00:00 2001 From: hannalee2 Date: Mon, 5 May 2025 08:54:36 -0700 Subject: [PATCH 4/9] Update SERVER_DATA_UPDATE_RATE --- src/ephys_link/bindings/parallax_binding.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ephys_link/bindings/parallax_binding.py b/src/ephys_link/bindings/parallax_binding.py index 15387b8..47d6b47 100644 --- a/src/ephys_link/bindings/parallax_binding.py +++ b/src/ephys_link/bindings/parallax_binding.py @@ -13,13 +13,11 @@ class ParallaxBinding(BaseBinding): """Bindings for Parallax platform.""" - # Server cache lifetime (60 FPS). - CACHE_LIFETIME = 1 / 60 + # Server data update rate (30 FPS). + SERVER_DATA_UPDATE_RATE = 1 / 30 # Movement polling preferences. UNCHANGED_COUNTER_LIMIT = 10 - UNCHANGED_COUNTER_LIMIT_DEPTH = 50 - POLL_INTERVAL = 0.1 # Speed preferences (mm/s to use coarse mode). COARSE_SPEED_THRESHOLD = 0.1 @@ -65,7 +63,8 @@ def get_dimensions(self) -> Vector4: async def get_position(self, manipulator_id: str) -> Vector4: manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) global_z = float(manipulator_data.get("global_Z", 0.0) or 0.0) - await sleep(self.POLL_INTERVAL) # Wait for the stage to stabilize. + + await sleep(self.SERVER_DATA_UPDATE_RATE) # Wait for the stage to stabilize. global_x = float(manipulator_data.get("global_X", 0.0) or 0.0) global_y = float(manipulator_data.get("global_Y", 0.0) or 0.0) @@ -87,8 +86,9 @@ async def get_shank_count(self, manipulator_id: str) -> int: manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) return int(manipulator_data.get("shank_cnt", 1) or 1) + @staticmethod @override - def get_movement_tolerance(self) -> float: + def get_movement_tolerance() -> float: return 0.01 @override @@ -128,7 +128,7 @@ async def set_position(self, manipulator_id: str, position: Vector4, speed: floa and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT ): # Wait for a short time before checking again. - await sleep(self.POLL_INTERVAL) + await sleep(self.SERVER_DATA_UPDATE_RATE) # Update current position. current_position = await self.get_position(manipulator_id) @@ -171,10 +171,10 @@ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> fl while ( not self._movement_stopped and not abs(current_depth - depth) <= self.get_movement_tolerance() - and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT_DEPTH + and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT ): # Wait for a short time before checking again. - await sleep(self.POLL_INTERVAL) + await sleep(self.SERVER_DATA_UPDATE_RATE) # Get the current depth. current_depth = (await self.get_position(manipulator_id)).w @@ -237,7 +237,7 @@ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4: async def _query_data(self) -> dict[str, Any]: # pyright: ignore [reportExplicitAny] try: # Update cache if it's expired. - if get_running_loop().time() - self.cache_time > self.CACHE_LIFETIME: + if get_running_loop().time() - self.cache_time > self.SERVER_DATA_UPDATE_RATE: # noinspection PyTypeChecker self.cache = (await get_running_loop().run_in_executor(None, get, self._url)).json() self.cache_time = get_running_loop().time() From d9d68da08a14ecc0424e9e1a1e4e87a10609562c Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 29 Oct 2025 14:06:33 -0700 Subject: [PATCH 5/9] Handle Parallax custom binding startup --- src/ephys_link/bindings/parallax_binding.py | 11 ++++++----- src/ephys_link/utils/startup.py | 16 ++++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/ephys_link/bindings/parallax_binding.py b/src/ephys_link/bindings/parallax_binding.py index 47d6b47..b529d2f 100644 --- a/src/ephys_link/bindings/parallax_binding.py +++ b/src/ephys_link/bindings/parallax_binding.py @@ -169,9 +169,9 @@ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> fl # Wait for the manipulator to reach the target depth or be stopped or get stuck. while ( - not self._movement_stopped - and not abs(current_depth - depth) <= self.get_movement_tolerance() - and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT + not self._movement_stopped + and not abs(current_depth - depth) <= self.get_movement_tolerance() + and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT ): # Wait for a short time before checking again. await sleep(self.SERVER_DATA_UPDATE_RATE) @@ -215,7 +215,7 @@ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4: x=platform_space.x, y=platform_space.z, z=platform_space.y, - w=self.get_dimensions().w-platform_space.w, + w=self.get_dimensions().w - platform_space.w, ) @override @@ -230,7 +230,7 @@ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4: x=unified_space.x, y=unified_space.z, z=unified_space.y, - w=self.get_dimensions().w-unified_space.w, + w=self.get_dimensions().w - unified_space.w, ) # Helper functions. @@ -257,6 +257,7 @@ async def _manipulator_data(self, manipulator_id: str) -> dict[str, Any]: # pyr if manipulator_id in data: return data[manipulator_id] + return None async def _put_request(self, request: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny] _ = await get_running_loop().run_in_executor(None, put, self._url, dumps(request)) diff --git a/src/ephys_link/utils/startup.py b/src/ephys_link/utils/startup.py index 51353d1..d5a058b 100644 --- a/src/ephys_link/utils/startup.py +++ b/src/ephys_link/utils/startup.py @@ -10,6 +10,7 @@ from ephys_link.__about__ import __version__ from ephys_link.bindings.mpm_binding import MPMBinding +from ephys_link.bindings.parallax_binding import ParallaxBinding from ephys_link.front_end.console import Console from ephys_link.utils.base_binding import BaseBinding from ephys_link.utils.constants import ( @@ -89,12 +90,15 @@ def get_binding_instance(options: EphysLinkOptions, console: Console) -> BaseBin selected_type = "ump" if binding_cli_name == selected_type: - # Pass in HTTP port for Pathfinder MPM. - if binding_cli_name == "pathfinder-mpm": - return MPMBinding(options.mpm_port) - - # Otherwise just return the binding. - return binding_type() + # Pass in HTTP port for Pathfinder MPM and Parallax. + match binding_cli_name: + case "pathfinder-mpm": + return MPMBinding(options.mpm_port) + case "parallax": + return ParallaxBinding() + case _: + # Otherwise just return the binding. + return binding_type() # Raise an error if the platform type is not recognized. error_message = unrecognized_platform_type_error(selected_type) From fabc7dcd96a61acbb5b25985faf57b8f3abe97f8 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 29 Oct 2025 14:14:34 -0700 Subject: [PATCH 6/9] Add HTTP port option to parallax binding --- src/ephys_link/front_end/cli.py | 9 ++++++++- src/ephys_link/utils/startup.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ephys_link/front_end/cli.py b/src/ephys_link/front_end/cli.py index 7e90979..b46df40 100644 --- a/src/ephys_link/front_end/cli.py +++ b/src/ephys_link/front_end/cli.py @@ -76,7 +76,14 @@ def __init__(self) -> None: type=int, default=8080, dest="mpm_port", - help="Port New Scale Pathfinder MPM's server is on. Default: 8080.", + help="HTTP port New Scale Pathfinder MPM's server is on. Default: 8080.", + ) + _ = self._parser.add_argument( + "--parallax-port", + type=int, + default=8081, + dest="parallax_port", + help="HTTP port Parallax's server is on. Default: 8081.", ) _ = self._parser.add_argument( "-s", diff --git a/src/ephys_link/utils/startup.py b/src/ephys_link/utils/startup.py index d5a058b..b08aff0 100644 --- a/src/ephys_link/utils/startup.py +++ b/src/ephys_link/utils/startup.py @@ -95,7 +95,7 @@ def get_binding_instance(options: EphysLinkOptions, console: Console) -> BaseBin case "pathfinder-mpm": return MPMBinding(options.mpm_port) case "parallax": - return ParallaxBinding() + return ParallaxBinding(options.parallax_port) case _: # Otherwise just return the binding. return binding_type() From ef76c8001a52eab6296dd42b6e586c413fd5f609 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Mon, 3 Nov 2025 13:55:42 -0800 Subject: [PATCH 7/9] Upgrade aquarium to support parallax port --- pyproject.toml | 2 +- src/ephys_link/bindings/parallax_binding.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e005fa7..45bfbf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "requests==2.32.5", "sensapex==1.400.4", "rich==14.2.0", - "vbl-aquarium==1.0.1" + "vbl-aquarium==1.1.0" ] [project.urls] diff --git a/src/ephys_link/bindings/parallax_binding.py b/src/ephys_link/bindings/parallax_binding.py index b529d2f..aaf7eaa 100644 --- a/src/ephys_link/bindings/parallax_binding.py +++ b/src/ephys_link/bindings/parallax_binding.py @@ -1,3 +1,8 @@ +"""Bindings for Parallax for New Scale platform. + +Usage: Instantiate ParallaxBinding to interact with the Parallax for New Scale Pathfinder platform. +""" + from asyncio import get_running_loop, sleep from json import dumps from typing import Any, final, override @@ -11,7 +16,7 @@ @final class ParallaxBinding(BaseBinding): - """Bindings for Parallax platform.""" + """Bindings for Parallax for New Scale platform.""" # Server data update rate (30 FPS). SERVER_DATA_UPDATE_RATE = 1 / 30 @@ -39,7 +44,7 @@ def __init__(self, port: int = 8081) -> None: @staticmethod @override def get_display_name() -> str: - return "Parallax" + return "Parallax for New Scale" @staticmethod @override From 460201d166b3ec5d61eb0f0f504f8973ffba2be9 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Mon, 3 Nov 2025 14:02:24 -0800 Subject: [PATCH 8/9] Make _manipulator_data raise error instead of return None --- src/ephys_link/bindings/parallax_binding.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ephys_link/bindings/parallax_binding.py b/src/ephys_link/bindings/parallax_binding.py index aaf7eaa..a296eca 100644 --- a/src/ephys_link/bindings/parallax_binding.py +++ b/src/ephys_link/bindings/parallax_binding.py @@ -66,7 +66,7 @@ def get_dimensions(self) -> Vector4: @override async def get_position(self, manipulator_id: str) -> Vector4: - manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) + manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny] global_z = float(manipulator_data.get("global_Z", 0.0) or 0.0) await sleep(self.SERVER_DATA_UPDATE_RATE) # Wait for the stage to stabilize. @@ -78,7 +78,7 @@ async def get_position(self, manipulator_id: str) -> Vector4: @override async def get_angles(self, manipulator_id: str) -> Vector3: - manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) + manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny] yaw = int(manipulator_data.get("yaw", 0) or 0) pitch = int(manipulator_data.get("pitch", 90) or 90) @@ -88,7 +88,7 @@ async def get_angles(self, manipulator_id: str) -> Vector3: @override async def get_shank_count(self, manipulator_id: str) -> int: - manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) + manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny] return int(manipulator_data.get("shank_cnt", 1) or 1) @staticmethod @@ -261,8 +261,11 @@ async def _manipulator_data(self, manipulator_id: str) -> dict[str, Any]: # pyr data = await self._query_data() if manipulator_id in data: - return data[manipulator_id] - return None + return data[manipulator_id] # pyright: ignore [reportAny] + + # If we get here, that means the manipulator doesn't exist. + error_message = f"Manipulator {manipulator_id} not found." + raise ValueError(error_message) async def _put_request(self, request: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny] _ = await get_running_loop().run_in_executor(None, put, self._url, dumps(request)) From 292da089c4012a9321851a78e345eb05c8bb0b80 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Mon, 3 Nov 2025 16:43:56 -0800 Subject: [PATCH 9/9] Update documentation to add Parallax as a platform under New Scale --- docs/home/supported_manipulators.md | 12 ++++++------ src/ephys_link/front_end/cli.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/home/supported_manipulators.md b/docs/home/supported_manipulators.md index ead9ca7..6632777 100644 --- a/docs/home/supported_manipulators.md +++ b/docs/home/supported_manipulators.md @@ -4,9 +4,9 @@ This is a current list of planned and supported manipulators in Ephys Link. If y here, we suggest reaching out to your manipulator's manufacturer to request support for Ephys Link. Direct them to contact [Kenneth Yang and Daniel Birman](https://virtualbrainlab.org/about/overview.html)! -| Manufacturer | Model | -|--------------|--------------------------------------------------------| -| Sensapex |
  • uMp-4
  • uMp-3
| -| New Scale |
  • Pathfinder MPM Control v2.8+
| -| Scientifica |
  • InVivoStar (Coming Soon!)
| -| PhenoSys |
  • (Coming Soon!)
| +| Manufacturer | Model | +|--------------|----------------------------------------------------------------------------------| +| Sensapex |
  • uMp-4
  • uMp-3
| +| New Scale |
  • Pathfinder MPM Control v2.8+
  • Parallax for New Scale
| +| Scientifica |
  • InVivoStar (Coming Soon!)
| +| PhenoSys |
  • (Coming Soon!)
| diff --git a/src/ephys_link/front_end/cli.py b/src/ephys_link/front_end/cli.py index b46df40..7fb46d0 100644 --- a/src/ephys_link/front_end/cli.py +++ b/src/ephys_link/front_end/cli.py @@ -47,7 +47,7 @@ def __init__(self) -> None: type=str, dest="type", default="ump", - help='Manipulator type (i.e. "ump", "pathfinder-mpm", "fake"). Default: "ump".', + help='Manipulator type ("ump", "pathfinder-mpm", "parallax", "fake"). Default: "ump".', ) _ = self._parser.add_argument( "-d",