ephys-link 2.1.3__py3-none-any.whl → 2.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ephys_link/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.1.3"
1
+ __version__ = "2.2.1"
@@ -12,22 +12,20 @@ Usage:
12
12
  ```
13
13
  """
14
14
 
15
- from asyncio import get_event_loop, run
15
+ from asyncio import new_event_loop
16
16
  from collections.abc import Callable, Coroutine
17
17
  from json import JSONDecodeError, dumps, loads
18
18
  from typing import Any, TypeVar, final
19
- from uuid import uuid4
20
19
 
21
20
  from aiohttp.web import Application, run_app
22
21
  from pydantic import ValidationError
23
- from socketio import AsyncClient, AsyncServer # pyright: ignore [reportMissingTypeStubs]
22
+ from socketio import AsyncServer # pyright: ignore [reportMissingTypeStubs]
24
23
  from vbl_aquarium.models.ephys_link import (
25
24
  EphysLinkOptions,
26
25
  SetDepthRequest,
27
26
  SetInsideBrainRequest,
28
27
  SetPositionRequest,
29
28
  )
30
- from vbl_aquarium.models.proxy import PinpointIdResponse
31
29
  from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
32
30
 
33
31
  from ephys_link.__about__ import __version__
@@ -36,8 +34,6 @@ from ephys_link.front_end.console import Console
36
34
  from ephys_link.utils.constants import (
37
35
  MALFORMED_REQUEST_ERROR,
38
36
  PORT,
39
- PROXY_CLIENT_NOT_INITIALIZED_ERROR,
40
- SERVER_NOT_INITIALIZED_ERROR,
41
37
  UNKNOWN_EVENT_ERROR,
42
38
  cannot_connect_as_client_is_already_connected_error,
43
39
  client_disconnected_without_being_connected_error,
@@ -64,60 +60,40 @@ class Server:
64
60
  self._platform_handler = platform_handler
65
61
  self._console = console
66
62
 
67
- # Initialize based on proxy usage.
68
- self._sio: AsyncServer | AsyncClient = AsyncClient() if self._options.use_proxy else AsyncServer()
69
- if not self._options.use_proxy:
70
- # Exit if _sio is not a Server.
71
- if not isinstance(self._sio, AsyncServer):
72
- self._console.critical_print(SERVER_NOT_INITIALIZED_ERROR)
73
- raise TypeError(SERVER_NOT_INITIALIZED_ERROR)
63
+ # Initialize server.
64
+ self._sio: AsyncServer = AsyncServer()
74
65
 
75
- self._app = Application()
76
- self._sio.attach(self._app) # pyright: ignore [reportUnknownMemberType]
66
+ self._app = Application()
67
+ self._sio.attach(self._app) # pyright: ignore [reportUnknownMemberType]
77
68
 
78
- # Bind connection events.
79
- _ = self._sio.on("connect", self.connect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
80
- _ = self._sio.on("disconnect", self.disconnect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
69
+ # Bind connection events.
70
+ _ = self._sio.on("connect", self.connect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
71
+ _ = self._sio.on("disconnect", self.disconnect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
81
72
 
82
73
  # Store connected client.
83
74
  self._client_sid: str = ""
84
75
 
85
- # Generate Pinpoint ID for proxy usage.
86
- self._pinpoint_id = str(uuid4())[:8]
87
-
88
76
  # Bind events.
89
77
  _ = self._sio.on("*", self.platform_event_handler) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
90
78
 
91
79
  def launch(self) -> None:
92
- """Launch the server.
93
-
94
- Based on the options, either connect to a proxy or launch the server locally.
95
- """
80
+ """Launch the server."""
96
81
 
97
82
  # List platform and available manipulators.
98
83
  self._console.info_print("PLATFORM", self._platform_handler.get_display_name())
99
- self._console.info_print(
100
- "MANIPULATORS",
101
- str(get_event_loop().run_until_complete(self._platform_handler.get_manipulators()).manipulators),
102
- )
103
84
 
104
- # Launch server
105
- if self._options.use_proxy:
106
- self._console.info_print("PINPOINT ID", self._pinpoint_id)
85
+ # Create a temporary event loop for getting manipulators
86
+ loop = new_event_loop()
87
+ try:
88
+ self._console.info_print(
89
+ "MANIPULATORS",
90
+ str(loop.run_until_complete(self._platform_handler.get_manipulators()).manipulators),
91
+ )
92
+ finally:
93
+ loop.close()
107
94
 
108
- async def connect_proxy() -> None:
109
- # Exit if _sio is not a proxy client.
110
- if not isinstance(self._sio, AsyncClient):
111
- self._console.critical_print(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
112
- raise TypeError(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
113
-
114
- # noinspection HttpUrlsUsage
115
- await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}") # pyright: ignore [reportUnknownMemberType]
116
- await self._sio.wait()
117
-
118
- run(connect_proxy())
119
- else:
120
- run_app(self._app, port=PORT)
95
+ # Launch server
96
+ run_app(self._app, port=PORT)
121
97
 
122
98
  # Helper functions.
123
99
  def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str: # pyright: ignore [reportExplicitAny]
@@ -244,8 +220,6 @@ class Server:
244
220
  # Server metadata.
245
221
  case "get_version":
246
222
  return __version__
247
- case "get_pinpoint_id":
248
- return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False).to_json_string()
249
223
  case "get_platform_info":
250
224
  return (await self._platform_handler.get_platform_info()).to_json_string()
251
225
 
@@ -0,0 +1,274 @@
1
+ """Bindings for Parallax for New Scale platform.
2
+
3
+ Usage: Instantiate ParallaxBinding to interact with the Parallax for New Scale Pathfinder platform.
4
+ """
5
+
6
+ from asyncio import get_running_loop, sleep
7
+ from json import dumps
8
+ from typing import Any, final, override
9
+
10
+ from requests import JSONDecodeError, get, put
11
+ from vbl_aquarium.models.unity import Vector3, Vector4
12
+
13
+ from ephys_link.utils.base_binding import BaseBinding
14
+ from ephys_link.utils.converters import scalar_mm_to_um, vector4_to_array
15
+
16
+
17
+ @final
18
+ class ParallaxBinding(BaseBinding):
19
+ """Bindings for Parallax for New Scale platform."""
20
+
21
+ # Server data update rate (30 FPS).
22
+ SERVER_DATA_UPDATE_RATE = 1 / 30
23
+
24
+ # Movement polling preferences.
25
+ UNCHANGED_COUNTER_LIMIT = 10
26
+
27
+ # Speed preferences (mm/s to use coarse mode).
28
+ COARSE_SPEED_THRESHOLD = 0.1
29
+ INSERTION_SPEED_LIMIT = 9_000
30
+
31
+ def __init__(self, port: int = 8081) -> None:
32
+ """Initialize connection to MPM HTTP server.
33
+
34
+ Args:
35
+ port: Port number for MPM HTTP server.
36
+ """
37
+ self._url = f"http://localhost:{port}"
38
+ self._movement_stopped = False
39
+
40
+ # Data cache.
41
+ self.cache: dict[str, Any] = {} # pyright: ignore [reportExplicitAny]
42
+ self.cache_time = 0
43
+
44
+ @staticmethod
45
+ @override
46
+ def get_display_name() -> str:
47
+ return "Parallax for New Scale"
48
+
49
+ @staticmethod
50
+ @override
51
+ def get_cli_name() -> str:
52
+ return "parallax"
53
+
54
+ @override
55
+ async def get_manipulators(self) -> list[str]:
56
+ data = await self._query_data()
57
+ return list(data.keys())
58
+
59
+ @override
60
+ async def get_axes_count(self) -> int:
61
+ return 3
62
+
63
+ @override
64
+ def get_dimensions(self) -> Vector4:
65
+ return Vector4(x=15, y=15, z=15, w=15)
66
+
67
+ @override
68
+ async def get_position(self, manipulator_id: str) -> Vector4:
69
+ manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny]
70
+ global_z = float(manipulator_data.get("global_Z", 0.0) or 0.0)
71
+
72
+ await sleep(self.SERVER_DATA_UPDATE_RATE) # Wait for the stage to stabilize.
73
+
74
+ global_x = float(manipulator_data.get("global_X", 0.0) or 0.0)
75
+ global_y = float(manipulator_data.get("global_Y", 0.0) or 0.0)
76
+
77
+ return Vector4(x=global_x, y=global_y, z=global_z, w=global_z)
78
+
79
+ @override
80
+ async def get_angles(self, manipulator_id: str) -> Vector3:
81
+ manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny]
82
+
83
+ yaw = int(manipulator_data.get("yaw", 0) or 0)
84
+ pitch = int(manipulator_data.get("pitch", 90) or 90)
85
+ roll = int(manipulator_data.get("roll", 0) or 0)
86
+
87
+ return Vector3(x=yaw, y=pitch, z=roll)
88
+
89
+ @override
90
+ async def get_shank_count(self, manipulator_id: str) -> int:
91
+ manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny]
92
+ return int(manipulator_data.get("shank_cnt", 1) or 1)
93
+
94
+ @staticmethod
95
+ @override
96
+ def get_movement_tolerance() -> float:
97
+ return 0.01
98
+
99
+ @override
100
+ async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
101
+ # Keep track of the previous position to check if the manipulator stopped advancing.
102
+ current_position = await self.get_position(manipulator_id)
103
+ previous_position = current_position
104
+ unchanged_counter = 0
105
+
106
+ # Set step mode based on speed.
107
+ await self._put_request(
108
+ {
109
+ "move_type": "stepMode",
110
+ "stage_sn": manipulator_id,
111
+ "step_mode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1,
112
+ }
113
+ )
114
+
115
+ # Send move request.
116
+ await self._put_request(
117
+ {
118
+ "move_type": "moveXYZ",
119
+ "world": "global", # Use global coordinates
120
+ "stage_sn": manipulator_id,
121
+ "Absolute": 1,
122
+ "Stereotactic": 0,
123
+ "AxisMask": 7,
124
+ "x": position.x,
125
+ "y": position.y,
126
+ "z": position.z,
127
+ }
128
+ )
129
+ # Wait for the manipulator to reach the target position or be stopped or stuck.
130
+ while (
131
+ not self._movement_stopped
132
+ and not self._is_vector_close(current_position, position)
133
+ and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
134
+ ):
135
+ # Wait for a short time before checking again.
136
+ await sleep(self.SERVER_DATA_UPDATE_RATE)
137
+
138
+ # Update current position.
139
+ current_position = await self.get_position(manipulator_id)
140
+
141
+ # Check if manipulator is not moving.
142
+ if self._is_vector_close(previous_position, current_position):
143
+ # Position did not change.
144
+ unchanged_counter += 1
145
+ else:
146
+ # Position changed.
147
+ unchanged_counter = 0
148
+ previous_position = current_position
149
+
150
+ # Reset movement stopped flag.
151
+ self._movement_stopped = False
152
+
153
+ # Return the final position.
154
+ return await self.get_position(manipulator_id)
155
+
156
+ @override
157
+ async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
158
+ # Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly.
159
+ current_depth = (await self.get_position(manipulator_id)).w
160
+ previous_depth = current_depth
161
+ unchanged_counter = 0
162
+
163
+ # Send move request.
164
+ # Convert mm/s to um/min and cap speed at the limit.
165
+ await self._put_request(
166
+ {
167
+ "move_type": "insertion",
168
+ "stage_sn": manipulator_id,
169
+ "world": "global", # distance in global space
170
+ "distance": scalar_mm_to_um(current_depth - depth),
171
+ "rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT),
172
+ }
173
+ )
174
+
175
+ # Wait for the manipulator to reach the target depth or be stopped or get stuck.
176
+ while (
177
+ not self._movement_stopped
178
+ and not abs(current_depth - depth) <= self.get_movement_tolerance()
179
+ and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
180
+ ):
181
+ # Wait for a short time before checking again.
182
+ await sleep(self.SERVER_DATA_UPDATE_RATE)
183
+
184
+ # Get the current depth.
185
+ current_depth = (await self.get_position(manipulator_id)).w
186
+
187
+ # Check if manipulator is not moving.
188
+ if abs(previous_depth - current_depth) <= self.get_movement_tolerance():
189
+ # Depth did not change.
190
+ unchanged_counter += 1
191
+ else:
192
+ # Depth changed.
193
+ unchanged_counter = 0
194
+ previous_depth = current_depth
195
+
196
+ # Reset movement stopped flag.
197
+ self._movement_stopped = False
198
+
199
+ # Return the final depth.
200
+ return float((await self.get_position(manipulator_id)).w)
201
+
202
+ @override
203
+ async def stop(self, manipulator_id: str) -> None:
204
+ request: dict[str, str | int | float] = {
205
+ "PutId": "stop",
206
+ "Probe": manipulator_id,
207
+ }
208
+ await self._put_request(request)
209
+ self._movement_stopped = True
210
+
211
+ @override
212
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
213
+ # unified <- platform
214
+ # +x <- +x
215
+ # +y <- +z
216
+ # +z <- +y
217
+ # +w <- +w
218
+
219
+ return Vector4(
220
+ x=platform_space.x,
221
+ y=platform_space.z,
222
+ z=platform_space.y,
223
+ w=self.get_dimensions().w - platform_space.w,
224
+ )
225
+
226
+ @override
227
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
228
+ # platform <- unified
229
+ # +x <- +x
230
+ # +y <- +z
231
+ # +z <- +y
232
+ # +w <- -w
233
+
234
+ return Vector4(
235
+ x=unified_space.x,
236
+ y=unified_space.z,
237
+ z=unified_space.y,
238
+ w=self.get_dimensions().w - unified_space.w,
239
+ )
240
+
241
+ # Helper functions.
242
+ async def _query_data(self) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
243
+ try:
244
+ # Update cache if it's expired.
245
+ if get_running_loop().time() - self.cache_time > self.SERVER_DATA_UPDATE_RATE:
246
+ # noinspection PyTypeChecker
247
+ self.cache = (await get_running_loop().run_in_executor(None, get, self._url)).json()
248
+ self.cache_time = get_running_loop().time()
249
+ except ConnectionError as connectionError:
250
+ error_message = f"Unable to connect to MPM HTTP server: {connectionError}"
251
+ raise RuntimeError(error_message) from connectionError
252
+ except JSONDecodeError as jsonDecodeError:
253
+ error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}"
254
+ raise ValueError(error_message) from jsonDecodeError
255
+ else:
256
+ # Return cached data.
257
+ return self.cache
258
+
259
+ async def _manipulator_data(self, manipulator_id: str) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
260
+ """Retrieve data for a specific manipulator (probe) using its serial number."""
261
+ data = await self._query_data()
262
+
263
+ if manipulator_id in data:
264
+ return data[manipulator_id] # pyright: ignore [reportAny]
265
+
266
+ # If we get here, that means the manipulator doesn't exist.
267
+ error_message = f"Manipulator {manipulator_id} not found."
268
+ raise ValueError(error_message)
269
+
270
+ async def _put_request(self, request: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny]
271
+ _ = await get_running_loop().run_in_executor(None, put, self._url, dumps(request))
272
+
273
+ def _is_vector_close(self, target: Vector4, current: Vector4) -> bool:
274
+ return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3])
@@ -142,7 +142,7 @@ class UmpBinding(BaseBinding):
142
142
 
143
143
  @override
144
144
  async def stop(self, manipulator_id: str) -> None:
145
- self._get_device(manipulator_id).stop()
145
+ self._get_device(manipulator_id).stop() # pyright: ignore [reportUnknownMemberType]
146
146
 
147
147
  @override
148
148
  def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
@@ -47,7 +47,7 @@ class CLI:
47
47
  type=str,
48
48
  dest="type",
49
49
  default="ump",
50
- help='Manipulator type (i.e. "ump", "pathfinder-mpm", "fake"). Default: "ump".',
50
+ help='Manipulator type ("ump", "pathfinder-mpm", "parallax", "fake"). Default: "ump".',
51
51
  )
52
52
  _ = self._parser.add_argument(
53
53
  "-d",
@@ -56,27 +56,19 @@ class CLI:
56
56
  action="store_true",
57
57
  help="Enable debug mode.",
58
58
  )
59
- _ = self._parser.add_argument(
60
- "-p",
61
- "--use-proxy",
62
- dest="use_proxy",
63
- action="store_true",
64
- help="Enable proxy mode.",
65
- )
66
- _ = self._parser.add_argument(
67
- "-a",
68
- "--proxy-address",
69
- type=str,
70
- default="proxy2.virtualbrainlab.org",
71
- dest="proxy_address",
72
- help="Proxy IP address.",
73
- )
74
59
  _ = self._parser.add_argument(
75
60
  "--mpm-port",
76
61
  type=int,
77
62
  default=8080,
78
63
  dest="mpm_port",
79
- help="Port New Scale Pathfinder MPM's server is on. Default: 8080.",
64
+ help="HTTP port New Scale Pathfinder MPM's server is on. Default: 8080.",
65
+ )
66
+ _ = self._parser.add_argument(
67
+ "--parallax-port",
68
+ type=int,
69
+ default=8081,
70
+ dest="parallax_port",
71
+ help="HTTP port Parallax's server is on. Default: 8081.",
80
72
  )
81
73
  _ = self._parser.add_argument(
82
74
  "-s",
@@ -12,7 +12,7 @@ from json import load
12
12
  from os import makedirs
13
13
  from os.path import exists, join
14
14
  from socket import gethostbyname, gethostname
15
- from sys import exit
15
+ from sys import exit as sys_exit
16
16
  from tkinter import CENTER, RIGHT, BooleanVar, E, IntVar, StringVar, Tk, ttk
17
17
  from typing import final
18
18
 
@@ -52,8 +52,6 @@ class GUI:
52
52
  self._ignore_updates = BooleanVar(value=options.ignore_updates)
53
53
  self._type = StringVar(value=options.type)
54
54
  self._debug = BooleanVar(value=options.debug)
55
- self._use_proxy = BooleanVar(value=options.use_proxy)
56
- self._proxy_address = StringVar(value=options.proxy_address)
57
55
  self._mpm_port = IntVar(value=options.mpm_port)
58
56
  self._serial = StringVar(value=options.serial)
59
57
 
@@ -73,15 +71,13 @@ class GUI:
73
71
 
74
72
  # Exit if the user did not submit options.
75
73
  if not self._submit:
76
- exit(1)
74
+ sys_exit(1)
77
75
 
78
76
  # Extract options from GUI.
79
77
  options = EphysLinkOptions(
80
78
  ignore_updates=self._ignore_updates.get(),
81
79
  type=self._type.get(),
82
80
  debug=self._debug.get(),
83
- use_proxy=self._use_proxy.get(),
84
- proxy_address=self._proxy_address.get(),
85
81
  mpm_port=self._mpm_port.get(),
86
82
  serial=self._serial.get(),
87
83
  )
@@ -115,40 +111,23 @@ class GUI:
115
111
  ttk.Label(server_serving_settings, text="Local IP:", anchor=E, justify=RIGHT).grid(column=0, row=0, sticky="we")
116
112
  ttk.Label(server_serving_settings, text=gethostbyname(gethostname())).grid(column=1, row=0, sticky="we")
117
113
 
118
- # Proxy.
119
- ttk.Label(server_serving_settings, text="Use Proxy:", anchor=E, justify=RIGHT).grid(
120
- column=0, row=1, sticky="we"
121
- )
122
- ttk.Checkbutton(
123
- server_serving_settings,
124
- variable=self._use_proxy,
125
- ).grid(column=1, row=1, sticky="we")
126
-
127
- # Proxy address.
128
- ttk.Label(server_serving_settings, text="Proxy Address:", anchor=E, justify=RIGHT).grid(
129
- column=0, row=2, sticky="we"
130
- )
131
- ttk.Entry(server_serving_settings, textvariable=self._proxy_address, justify=CENTER).grid(
132
- column=1, row=2, sticky="we"
133
- )
134
-
135
114
  # Ignore updates.
136
115
  ttk.Label(server_serving_settings, text="Ignore Updates:", anchor=E, justify=RIGHT).grid(
137
- column=0, row=4, sticky="we"
116
+ column=0, row=1, sticky="we"
138
117
  )
139
118
  ttk.Checkbutton(
140
119
  server_serving_settings,
141
120
  variable=self._ignore_updates,
142
- ).grid(column=1, row=4, sticky="we")
121
+ ).grid(column=1, row=1, sticky="we")
143
122
 
144
123
  # Debug mode.
145
124
  ttk.Label(server_serving_settings, text="Debug mode:", anchor=E, justify=RIGHT).grid(
146
- column=0, row=5, sticky="we"
125
+ column=0, row=2, sticky="we"
147
126
  )
148
127
  ttk.Checkbutton(
149
128
  server_serving_settings,
150
129
  variable=self._debug,
151
- ).grid(column=1, row=5, sticky="we")
130
+ ).grid(column=1, row=2, sticky="we")
152
131
 
153
132
  # ---
154
133
 
@@ -58,9 +58,6 @@ def did_not_reach_target_depth_error(request: SetDepthRequest, final_unified_dep
58
58
 
59
59
  EMERGENCY_STOP_MESSAGE = "Emergency Stopping All Manipulators..."
60
60
 
61
- SERVER_NOT_INITIALIZED_ERROR = "Server not initialized."
62
- PROXY_CLIENT_NOT_INITIALIZED_ERROR = "Proxy client not initialized."
63
-
64
61
 
65
62
  def cannot_connect_as_client_is_already_connected_error(new_client_sid: str, current_client_sid: str) -> str:
66
63
  """Generate an error message for when the client is already connected.
@@ -5,11 +5,14 @@ from inspect import getmembers, isclass
5
5
  from pkgutil import iter_modules
6
6
 
7
7
  from packaging.version import parse
8
- from requests import ConnectionError, ConnectTimeout, get
8
+ from requests import ConnectionError as RequestsConnectionError
9
+ from requests import ConnectTimeout as RequestsConnectTimeout
10
+ from requests import get
9
11
  from vbl_aquarium.models.ephys_link import EphysLinkOptions
10
12
 
11
13
  from ephys_link.__about__ import __version__
12
14
  from ephys_link.bindings.mpm_binding import MPMBinding
15
+ from ephys_link.bindings.parallax_binding import ParallaxBinding
13
16
  from ephys_link.front_end.console import Console
14
17
  from ephys_link.utils.base_binding import BaseBinding
15
18
  from ephys_link.utils.constants import (
@@ -44,7 +47,7 @@ def check_for_updates(console: Console) -> None:
44
47
  if parse(latest_version) > parse(__version__):
45
48
  console.critical_print(f"Update available: {latest_version} (current: {__version__})")
46
49
  console.critical_print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
47
- except (ConnectionError, ConnectTimeout):
50
+ except (RequestsConnectionError, RequestsConnectTimeout):
48
51
  console.error_print("UPDATE", UNABLE_TO_CHECK_FOR_UPDATES_ERROR)
49
52
 
50
53
 
@@ -89,12 +92,15 @@ def get_binding_instance(options: EphysLinkOptions, console: Console) -> BaseBin
89
92
  selected_type = "ump"
90
93
 
91
94
  if binding_cli_name == selected_type:
92
- # Pass in HTTP port for Pathfinder MPM.
93
- if binding_cli_name == "pathfinder-mpm":
94
- return MPMBinding(options.mpm_port)
95
-
96
- # Otherwise just return the binding.
97
- return binding_type()
95
+ # Pass in HTTP port for Pathfinder MPM and Parallax.
96
+ match binding_cli_name:
97
+ case "pathfinder-mpm":
98
+ return MPMBinding(options.mpm_port)
99
+ case "parallax":
100
+ return ParallaxBinding(options.parallax_port)
101
+ case _:
102
+ # Otherwise just return the binding.
103
+ return binding_type()
98
104
 
99
105
  # Raise an error if the platform type is not recognized.
100
106
  error_message = unrecognized_platform_type_error(selected_type)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ephys-link
3
- Version: 2.1.3
3
+ Version: 2.2.1
4
4
  Summary: A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments.
5
5
  Project-URL: Documentation, https://virtualbrainlab.org/ephys_link/installation_and_use.html
6
6
  Project-URL: Issues, https://github.com/VirtualBrainLab/ephys-link/issues
@@ -17,22 +17,21 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
17
17
  Classifier: Operating System :: Microsoft :: Windows
18
18
  Classifier: Programming Language :: Python
19
19
  Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.13
21
20
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
21
  Classifier: Programming Language :: Python :: Implementation :: PyPy
23
22
  Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
24
- Requires-Python: >=3.13
25
- Requires-Dist: aiohttp==3.12.15
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: aiohttp==3.13.2
26
25
  Requires-Dist: colorama==0.4.6
27
26
  Requires-Dist: keyboard==0.13.5
28
27
  Requires-Dist: packaging==25.0
29
- Requires-Dist: platformdirs==4.4.0
28
+ Requires-Dist: platformdirs==4.5.1
30
29
  Requires-Dist: pyserial==3.5
31
- Requires-Dist: python-socketio[asyncio-client]==5.13.0
30
+ Requires-Dist: python-socketio==5.15.1
32
31
  Requires-Dist: requests==2.32.5
33
- Requires-Dist: rich==14.1.0
34
- Requires-Dist: sensapex==1.400.4
35
- Requires-Dist: vbl-aquarium==1.0.0
32
+ Requires-Dist: rich==14.2.0
33
+ Requires-Dist: sensapex==1.504.1
34
+ Requires-Dist: vbl-aquarium==1.2.0
36
35
  Description-Content-Type: text/markdown
37
36
 
38
37
  # Electrophysiology Manipulator Link
@@ -1,24 +1,25 @@
1
- ephys_link/__about__.py,sha256=-5z5R8xV0UToQjp9-3ipF_dBiBdRXtdotx4_h9ZJZT8,22
1
+ ephys_link/__about__.py,sha256=4dqvKTDgbqeyzbWj6hYiNdzxsI8j1YOKSLM8vF6a0j4,22
2
2
  ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  ephys_link/__main__.py,sha256=54zxQ-fyxC2-LGsTUdtlvib36ZZQwNyOa6IuffYLhhs,1582
4
4
  ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  ephys_link/back_end/platform_handler.py,sha256=6Lt_jaIcO9jTmWYOoWYkuhwSlCTT1Qfd53ru31X0fmc,10130
6
- ephys_link/back_end/server.py,sha256=uZOK9UQq4gp7yx6_4dZrfRTjsio2WXT8TwjO4E5kECA,10712
6
+ ephys_link/back_end/server.py,sha256=ILrYbd4r_4GiRmdUEmGcbT1AEIo_TF3i-BZpdhYm8g8,9296
7
7
  ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  ephys_link/bindings/fake_binding.py,sha256=PI7zYv-SjWsGFEs_FVu5Z5l9gykIqG3C7pQISdbwdoY,2357
9
9
  ephys_link/bindings/mpm_binding.py,sha256=vn7IKqdiZ6_MX91zomqDXX08ONHwVVgWncRuJTxJpOM,10872
10
- ephys_link/bindings/ump_binding.py,sha256=wbJe6Ro4E-TPejBBDkNsKUluF2Gr0rZBuyp3LJ2TipM,7865
10
+ ephys_link/bindings/parallax_binding.py,sha256=QzZ4MVUc4YGlIxr9__j8I3t3MyGuoQdsZWgHaSedZ3A,10129
11
+ ephys_link/bindings/ump_binding.py,sha256=X71n5P3IPlWmQ2BaHUI-UHu-RP__ZOgA3w32iT3169U,7910
11
12
  ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- ephys_link/front_end/cli.py,sha256=jWUEWhIHvWF8ZN6xPIbfoQblDWxMLsjwhtFB_jfOO_s,3093
13
+ ephys_link/front_end/cli.py,sha256=vbJYFoaOwPwQq3ShR60HgPQnE-hK6kp1_9FEFkyZ6AU,2899
13
14
  ephys_link/front_end/console.py,sha256=zq67dn7T4xKMg3jjbNra_1s0N9ItvsTRXVIhClWbjP8,3912
14
- ephys_link/front_end/gui.py,sha256=x5mNekxe1yhPKcCFNnDL3AarhNmUoBJooykFS_6pWt4,7227
15
+ ephys_link/front_end/gui.py,sha256=MagojumetQGp7e8sK53cI_TgcqKSx54jLNeZagBjNJY,6383
15
16
  ephys_link/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  ephys_link/utils/base_binding.py,sha256=rq0FtvkjAN287nX8rZwR-j6YPzV3Pkp8PqJfZlhIzqo,5413
17
- ephys_link/utils/constants.py,sha256=afD-FFrfCD3cZc500jzWw4I-dGibLPluxteX8BxlgK4,3883
18
+ ephys_link/utils/constants.py,sha256=l1-KmGFzAgaBzEIgtmFLfpijum75_1JMdSq5GyR3TJk,3756
18
19
  ephys_link/utils/converters.py,sha256=ZdVmIX-LHCwM__F0SpjN_mfNGGetr1U97xvHd0hf8T0,2038
19
- ephys_link/utils/startup.py,sha256=Yx9LSedaLmTpzkb1E0NOqxaDMhRXLShkI2PJj80_95U,3620
20
- ephys_link-2.1.3.dist-info/METADATA,sha256=n5GnLJF6kEThTRiROqyyQNhmByH6Fx7a1pyxH1osDJQ,4775
21
- ephys_link-2.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- ephys_link-2.1.3.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
23
- ephys_link-2.1.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
24
- ephys_link-2.1.3.dist-info/RECORD,,
20
+ ephys_link/utils/startup.py,sha256=lK04UffvdcASpmp8Arecfmm1dDhzZnZy2rgzQ8sqWag,3971
21
+ ephys_link-2.2.1.dist-info/METADATA,sha256=nHO5FaFAmKoqwYbH2gHOvPh-e8-AgyA7TqFoN9zLokM,4707
22
+ ephys_link-2.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
23
+ ephys_link-2.2.1.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
24
+ ephys_link-2.2.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ ephys_link-2.2.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any