ephys-link 2.1.0b1__py3-none-any.whl → 2.1.2__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.0b1"
1
+ __version__ = "2.1.2"
ephys_link/__main__.py CHANGED
@@ -16,9 +16,9 @@ from keyboard import add_hotkey
16
16
  from ephys_link.back_end.platform_handler import PlatformHandler
17
17
  from ephys_link.back_end.server import Server
18
18
  from ephys_link.front_end.cli import CLI
19
+ from ephys_link.front_end.console import Console
19
20
  from ephys_link.front_end.gui import GUI
20
- from ephys_link.utils.console import Console
21
- from ephys_link.utils.startup import check_for_updates, preamble
21
+ from ephys_link.utils.startup import check_for_updates, get_binding_instance, preamble
22
22
 
23
23
 
24
24
  def main() -> None:
@@ -37,13 +37,16 @@ def main() -> None:
37
37
  if not options.ignore_updates:
38
38
  check_for_updates(console)
39
39
 
40
- # 4. Instantiate the Platform Handler with the appropriate platform bindings.
41
- platform_handler = PlatformHandler(options, console)
40
+ # 4. Instantiate the requested platform binding.
41
+ binding = get_binding_instance(options, console)
42
42
 
43
- # 5. Add hotkeys for emergency stop.
43
+ # 5. Instantiate the Platform Handler with the appropriate platform bindings.
44
+ platform_handler = PlatformHandler(binding, console)
45
+
46
+ # 6. Add hotkeys for emergency stop.
44
47
  _ = add_hotkey("ctrl+alt+shift+q", lambda: run(platform_handler.emergency_stop()))
45
48
 
46
- # 6. Start the server.
49
+ # 7. Start the server.
47
50
  Server(options, platform_handler, console).launch()
48
51
 
49
52
 
@@ -8,12 +8,10 @@ Usage:
8
8
  """
9
9
 
10
10
  from typing import final
11
- from uuid import uuid4
12
11
 
13
12
  from vbl_aquarium.models.ephys_link import (
14
13
  AngularResponse,
15
14
  BooleanStateResponse,
16
- EphysLinkOptions,
17
15
  GetManipulatorsResponse,
18
16
  PlatformInfo,
19
17
  PositionalResponse,
@@ -23,81 +21,38 @@ from vbl_aquarium.models.ephys_link import (
23
21
  SetPositionRequest,
24
22
  ShankCountResponse,
25
23
  )
26
- from vbl_aquarium.models.unity import Vector4
27
24
 
28
- from ephys_link.bindings.mpm_binding import MPMBinding
25
+ from ephys_link.front_end.console import Console
29
26
  from ephys_link.utils.base_binding import BaseBinding
30
- from ephys_link.utils.console import Console
27
+ from ephys_link.utils.constants import (
28
+ EMERGENCY_STOP_MESSAGE,
29
+ NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR,
30
+ did_not_reach_target_depth_error,
31
+ did_not_reach_target_position_error,
32
+ )
31
33
  from ephys_link.utils.converters import vector4_to_array
32
- from ephys_link.utils.startup import get_bindings
33
34
 
34
35
 
35
36
  @final
36
37
  class PlatformHandler:
37
38
  """Handler for platform commands."""
38
39
 
39
- def __init__(self, options: EphysLinkOptions, console: Console) -> None:
40
+ def __init__(self, binding: BaseBinding, console: Console) -> None:
40
41
  """Initialize platform handler.
41
42
 
42
43
  Args:
43
- options: CLI options.
44
+ binding: Binding instance for the platform.
44
45
  console: Console instance.
45
46
  """
46
- # Store the CLI options.
47
- self._options = options
48
-
49
47
  # Store the console.
50
48
  self._console = console
51
49
 
52
50
  # Define bindings based on platform type.
53
- self._bindings = self._get_binding_instance(options)
51
+ self._bindings = binding
54
52
 
55
53
  # Record which IDs are inside the brain.
56
54
  self._inside_brain: set[str] = set()
57
55
 
58
- # Generate a Pinpoint ID for proxy usage.
59
- self._pinpoint_id = str(uuid4())[:8]
60
-
61
- def _get_binding_instance(self, options: EphysLinkOptions) -> BaseBinding:
62
- """Match the platform type to the appropriate bindings.
63
-
64
- Args:
65
- options: CLI options.
66
-
67
- Raises:
68
- ValueError: If the platform type is not recognized.
69
-
70
- Returns:
71
- Bindings for the specified platform type.
72
- """
73
-
74
- # What the user supplied.
75
- selected_type = options.type
76
-
77
- for binding_type in get_bindings():
78
- binding_cli_name = binding_type.get_cli_name()
79
-
80
- # Notify deprecation of "ump-4" and "ump-3" CLI options and fix.
81
- if selected_type in ("ump-4", "ump-3"):
82
- self._console.error_print(
83
- "DEPRECATION",
84
- f"CLI option '{selected_type}' is deprecated and will be removed in v3.0.0. Use 'ump' instead.",
85
- )
86
- selected_type = "ump"
87
-
88
- if binding_cli_name == selected_type:
89
- # Pass in HTTP port for Pathfinder MPM.
90
- if binding_cli_name == "pathfinder-mpm":
91
- return MPMBinding(options.mpm_port)
92
-
93
- # Otherwise just return the binding.
94
- return binding_type()
95
-
96
- # Raise an error if the platform type is not recognized.
97
- error_message = f'Platform type "{options.type}" not recognized.'
98
- self._console.critical_print(error_message)
99
- raise ValueError(error_message)
100
-
101
56
  # Platform metadata.
102
57
 
103
58
  def get_display_name(self) -> str:
@@ -152,7 +107,7 @@ class PlatformHandler:
152
107
  )
153
108
  except Exception as e: # noqa: BLE001
154
109
  self._console.exception_error_print("Get Position", e)
155
- return PositionalResponse(error=str(e))
110
+ return PositionalResponse(error=self._console.pretty_exception(e))
156
111
  else:
157
112
  return PositionalResponse(position=unified_position)
158
113
 
@@ -202,9 +157,8 @@ class PlatformHandler:
202
157
  try:
203
158
  # Disallow setting manipulator position while inside the brain.
204
159
  if request.manipulator_id in self._inside_brain:
205
- error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
206
- self._console.error_print("Set Position", error_message)
207
- return PositionalResponse(error=error_message)
160
+ self._console.error_print("Set Position", NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR)
161
+ return PositionalResponse(error=NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR)
208
162
 
209
163
  # Move to the new position.
210
164
  final_platform_position = await self._bindings.set_position(
@@ -222,11 +176,7 @@ class PlatformHandler:
222
176
 
223
177
  # Check if the axis is within the movement tolerance.
224
178
  if abs(axis) > self._bindings.get_movement_tolerance():
225
- error_message = (
226
- f"Manipulator {request.manipulator_id} did not reach target"
227
- f" position on axis {list(Vector4.model_fields.keys())[index]}."
228
- f" Requested: {request.position}, got: {final_unified_position}."
229
- )
179
+ error_message = did_not_reach_target_position_error(request, index, final_unified_position)
230
180
  self._console.error_print("Set Position", error_message)
231
181
  return PositionalResponse(error=error_message)
232
182
  except Exception as e: # noqa: BLE001
@@ -246,26 +196,22 @@ class PlatformHandler:
246
196
  """
247
197
  try:
248
198
  # Move to the new depth.
249
- final_platform_depth = await self._bindings.set_depth(
199
+ final_depth = await self._bindings.set_depth(
250
200
  manipulator_id=request.manipulator_id,
251
- depth=self._bindings.unified_space_to_platform_space(Vector4(w=request.depth)).w,
201
+ depth=request.depth,
252
202
  speed=request.speed,
253
203
  )
254
- final_unified_depth = self._bindings.platform_space_to_unified_space(Vector4(w=final_platform_depth)).w
255
204
 
256
205
  # Return error if movement did not reach target within tolerance.
257
- if abs(final_unified_depth - request.depth) > self._bindings.get_movement_tolerance():
258
- error_message = (
259
- f"Manipulator {request.manipulator_id} did not reach target depth."
260
- f" Requested: {request.depth}, got: {final_unified_depth}."
261
- )
206
+ if abs(final_depth - request.depth) > self._bindings.get_movement_tolerance():
207
+ error_message = did_not_reach_target_depth_error(request, final_depth)
262
208
  self._console.error_print("Set Depth", error_message)
263
209
  return SetDepthResponse(error=error_message)
264
210
  except Exception as e: # noqa: BLE001
265
211
  self._console.exception_error_print("Set Depth", e)
266
212
  return SetDepthResponse(error=self._console.pretty_exception(e))
267
213
  else:
268
- return SetDepthResponse(depth=final_unified_depth)
214
+ return SetDepthResponse(depth=final_depth)
269
215
 
270
216
  async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
271
217
  """Mark a manipulator as inside the brain or not.
@@ -278,16 +224,11 @@ class PlatformHandler:
278
224
  Returns:
279
225
  Inside brain state of the manipulator and an error message if any.
280
226
  """
281
- try:
282
- if request.inside:
283
- self._inside_brain.add(request.manipulator_id)
284
- else:
285
- self._inside_brain.discard(request.manipulator_id)
286
- except Exception as e: # noqa: BLE001
287
- self._console.exception_error_print("Set Inside Brain", e)
288
- return BooleanStateResponse(error=self._console.pretty_exception(e))
227
+ if request.inside:
228
+ self._inside_brain.add(request.manipulator_id)
289
229
  else:
290
- return BooleanStateResponse(state=request.inside)
230
+ self._inside_brain.discard(request.manipulator_id)
231
+ return BooleanStateResponse(state=request.inside)
291
232
 
292
233
  async def stop(self, manipulator_id: str) -> str:
293
234
  """Stop a manipulator.
@@ -316,12 +257,12 @@ class PlatformHandler:
316
257
  for manipulator_id in await self._bindings.get_manipulators():
317
258
  await self._bindings.stop(manipulator_id)
318
259
  except Exception as e: # noqa: BLE001
319
- self._console.exception_error_print("Stop", e)
260
+ self._console.exception_error_print("Stop All", e)
320
261
  return self._console.pretty_exception(e)
321
262
  else:
322
263
  return ""
323
264
 
324
265
  async def emergency_stop(self) -> None:
325
266
  """Stops all manipulators with a message."""
326
- self._console.critical_print("Emergency Stopping All Manipulators...")
267
+ self._console.critical_print(EMERGENCY_STOP_MESSAGE)
327
268
  _ = await self.stop_all()
@@ -32,8 +32,16 @@ from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
32
32
 
33
33
  from ephys_link.__about__ import __version__
34
34
  from ephys_link.back_end.platform_handler import PlatformHandler
35
- from ephys_link.utils.console import Console
36
- from ephys_link.utils.constants import PORT
35
+ from ephys_link.front_end.console import Console
36
+ from ephys_link.utils.constants import (
37
+ MALFORMED_REQUEST_ERROR,
38
+ PORT,
39
+ PROXY_CLIENT_NOT_INITIALIZED_ERROR,
40
+ SERVER_NOT_INITIALIZED_ERROR,
41
+ UNKNOWN_EVENT_ERROR,
42
+ cannot_connect_as_client_is_already_connected_error,
43
+ client_disconnected_without_being_connected_error,
44
+ )
37
45
 
38
46
  # Server message generic types.
39
47
  INPUT_TYPE = TypeVar("INPUT_TYPE", bound=VBLBaseModel)
@@ -61,9 +69,8 @@ class Server:
61
69
  if not self._options.use_proxy:
62
70
  # Exit if _sio is not a Server.
63
71
  if not isinstance(self._sio, AsyncServer):
64
- error = "Server not initialized."
65
- self._console.critical_print(error)
66
- raise TypeError(error)
72
+ self._console.critical_print(SERVER_NOT_INITIALIZED_ERROR)
73
+ raise TypeError(SERVER_NOT_INITIALIZED_ERROR)
67
74
 
68
75
  self._app = Application()
69
76
  self._sio.attach(self._app) # pyright: ignore [reportUnknownMemberType]
@@ -101,9 +108,8 @@ class Server:
101
108
  async def connect_proxy() -> None:
102
109
  # Exit if _sio is not a proxy client.
103
110
  if not isinstance(self._sio, AsyncClient):
104
- error = "Proxy client not initialized."
105
- self._console.critical_print(error)
106
- raise TypeError(error)
111
+ self._console.critical_print(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
112
+ raise TypeError(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
107
113
 
108
114
  # noinspection HttpUrlsUsage
109
115
  await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}") # pyright: ignore [reportUnknownMemberType]
@@ -125,13 +131,13 @@ class Server:
125
131
  Response for a malformed request.
126
132
  """
127
133
  self._console.error_print("MALFORMED REQUEST", f"{request}: {data}")
128
- return dumps({"error": "Malformed request."})
134
+ return dumps(MALFORMED_REQUEST_ERROR)
129
135
 
130
136
  async def _run_if_data_available(
131
137
  self,
132
138
  function: Callable[[str], Coroutine[Any, Any, VBLBaseModel]], # pyright: ignore [reportExplicitAny]
133
139
  event: str,
134
- data: tuple[tuple[Any], ...], # pyright: ignore [reportExplicitAny]
140
+ data: Any, # pyright: ignore [reportAny, reportExplicitAny]
135
141
  ) -> str:
136
142
  """Run a function if data is available.
137
143
 
@@ -143,17 +149,16 @@ class Server:
143
149
  Returns:
144
150
  Response data from function.
145
151
  """
146
- request_data = data[1]
147
- if request_data:
148
- return str((await function(str(request_data))).to_json_string())
149
- return self._malformed_request_response(event, request_data)
152
+ if data:
153
+ return str((await function(str(data))).to_json_string()) # pyright: ignore[reportAny]
154
+ return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
150
155
 
151
156
  async def _run_if_data_parses(
152
157
  self,
153
158
  function: Callable[[INPUT_TYPE], Coroutine[Any, Any, OUTPUT_TYPE]], # pyright: ignore [reportExplicitAny]
154
159
  data_type: type[INPUT_TYPE],
155
160
  event: str,
156
- data: tuple[tuple[Any], ...], # pyright: ignore [reportExplicitAny]
161
+ data: Any, # pyright: ignore [reportAny, reportExplicitAny]
157
162
  ) -> str:
158
163
  """Run a function if data parses.
159
164
 
@@ -166,18 +171,17 @@ class Server:
166
171
  Returns:
167
172
  Response data from function.
168
173
  """
169
- request_data = data[1]
170
- if request_data:
174
+ if data:
171
175
  try:
172
- parsed_data = data_type(**loads(str(request_data)))
176
+ parsed_data = data_type(**loads(str(data))) # pyright: ignore[reportAny]
173
177
  except JSONDecodeError:
174
- return self._malformed_request_response(event, request_data)
178
+ return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
175
179
  except ValidationError as e:
176
180
  self._console.exception_error_print(event, e)
177
- return self._malformed_request_response(event, request_data)
181
+ return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
178
182
  else:
179
183
  return str((await function(parsed_data)).to_json_string())
180
- return self._malformed_request_response(event, request_data)
184
+ return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
181
185
 
182
186
  # Event Handlers.
183
187
 
@@ -199,7 +203,7 @@ class Server:
199
203
  return True
200
204
 
201
205
  self._console.error_print(
202
- "CONNECTION REFUSED", f"Cannot connect {sid} as {self._client_sid} is already connected."
206
+ "CONNECTION REFUSED", cannot_connect_as_client_is_already_connected_error(sid, self._client_sid)
203
207
  )
204
208
  return False
205
209
 
@@ -209,22 +213,24 @@ class Server:
209
213
  Args:
210
214
  sid: Socket session ID.
211
215
  """
212
- self._console.info_print("DISCONNECTED", sid)
216
+ self._console.info_print("DISCONNECTION REQUEST", sid)
213
217
 
214
218
  # Reset client SID if it matches.
215
219
  if self._client_sid == sid:
216
220
  self._client_sid = ""
221
+ self._console.info_print("DISCONNECTED", sid)
217
222
  else:
218
- self._console.error_print("DISCONNECTION", f"Client {sid} disconnected without being connected.")
223
+ self._console.error_print("DISCONNECTION", client_disconnected_without_being_connected_error(sid))
219
224
 
220
- async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str: # pyright: ignore [reportExplicitAny]
225
+ async def platform_event_handler(self, event: str, _: str, data: Any) -> str: # pyright: ignore [reportAny, reportExplicitAny]
221
226
  """Handle events from the server.
222
227
 
223
228
  Matches incoming events based on the Socket.IO API.
224
229
 
225
230
  Args:
226
231
  event: Event name.
227
- args: Event arguments.
232
+ _: Socket session ID (unused).
233
+ data: Event data.
228
234
 
229
235
  Returns:
230
236
  Response data.
@@ -247,28 +253,27 @@ class Server:
247
253
  case "get_manipulators":
248
254
  return str((await self._platform_handler.get_manipulators()).to_json_string())
249
255
  case "get_position":
250
- return await self._run_if_data_available(self._platform_handler.get_position, event, args)
256
+ return await self._run_if_data_available(self._platform_handler.get_position, event, data)
251
257
  case "get_angles":
252
- return await self._run_if_data_available(self._platform_handler.get_angles, event, args)
258
+ return await self._run_if_data_available(self._platform_handler.get_angles, event, data)
253
259
  case "get_shank_count":
254
- return await self._run_if_data_available(self._platform_handler.get_shank_count, event, args)
260
+ return await self._run_if_data_available(self._platform_handler.get_shank_count, event, data)
255
261
  case "set_position":
256
262
  return await self._run_if_data_parses(
257
- self._platform_handler.set_position, SetPositionRequest, event, args
263
+ self._platform_handler.set_position, SetPositionRequest, event, data
258
264
  )
259
265
  case "set_depth":
260
- return await self._run_if_data_parses(self._platform_handler.set_depth, SetDepthRequest, event, args)
266
+ return await self._run_if_data_parses(self._platform_handler.set_depth, SetDepthRequest, event, data)
261
267
  case "set_inside_brain":
262
268
  return await self._run_if_data_parses(
263
- self._platform_handler.set_inside_brain, SetInsideBrainRequest, event, args
269
+ self._platform_handler.set_inside_brain, SetInsideBrainRequest, event, data
264
270
  )
265
271
  case "stop":
266
- request_data = args[1]
267
- if request_data:
268
- return await self._platform_handler.stop(str(request_data))
269
- return self._malformed_request_response(event, request_data)
272
+ if data:
273
+ return await self._platform_handler.stop(str(data)) # pyright: ignore[reportAny]
274
+ return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
270
275
  case "stop_all":
271
276
  return await self._platform_handler.stop_all()
272
277
  case _:
273
278
  self._console.error_print("EVENT", f"Unknown event: {event}.")
274
- return dumps({"error": "Unknown event."})
279
+ return dumps(UNKNOWN_EVENT_ERROR)
@@ -10,7 +10,6 @@ from sensapex import UMP, SensapexDevice # pyright: ignore [reportMissingTypeSt
10
10
  from vbl_aquarium.models.unity import Vector4
11
11
 
12
12
  from ephys_link.utils.base_binding import BaseBinding
13
- from ephys_link.utils.constants import RESOURCES_DIRECTORY
14
13
  from ephys_link.utils.converters import (
15
14
  list_to_vector4,
16
15
  scalar_mm_to_um,
@@ -31,7 +30,6 @@ class UmpBinding(BaseBinding):
31
30
  """Initialize uMp bindings."""
32
31
 
33
32
  # Establish connection to Sensapex API (exit if connection fails).
34
- UMP.set_library_path(RESOURCES_DIRECTORY)
35
33
  self._ump: UMP = UMP.get_ump() # pyright: ignore [reportUnknownMemberType]
36
34
 
37
35
  # Compute axis count, assumed as the first device. 0 if no devices are connected.
@@ -46,8 +46,8 @@ class CLI:
46
46
  "--type",
47
47
  type=str,
48
48
  dest="type",
49
- default="ump-4",
50
- help='Manipulator type (i.e. "ump-4", "pathfinder-mpm", "fake"). Default: "ump-4".',
49
+ default="ump",
50
+ help='Manipulator type (i.e. "ump", "pathfinder-mpm", "fake"). Default: "ump".',
51
51
  )
52
52
  _ = self._parser.add_argument(
53
53
  "-d",
@@ -15,7 +15,7 @@ from rich.traceback import install
15
15
 
16
16
  @final
17
17
  class Console:
18
- def __init__(self, *, enable_debug: bool) -> None:
18
+ def __init__(self, *, enable_debug: bool = False) -> None:
19
19
  """Initialize console properties.
20
20
 
21
21
  Args:
@@ -20,7 +20,7 @@ from platformdirs import user_config_dir
20
20
  from vbl_aquarium.models.ephys_link import EphysLinkOptions
21
21
 
22
22
  from ephys_link.__about__ import __version__ as version
23
- from ephys_link.utils.startup import get_binding_display_to_cli_name
23
+ from ephys_link.utils.startup import get_bindings
24
24
 
25
25
  # Define options path.
26
26
  OPTIONS_DIR = join(user_config_dir(), "VBL", "Ephys Link")
@@ -156,7 +156,7 @@ class GUI:
156
156
  platform_type_settings = ttk.LabelFrame(mainframe, text="Platform Type", padding=3)
157
157
  platform_type_settings.grid(column=0, row=1, sticky="news")
158
158
 
159
- for index, (display_name, cli_name) in enumerate(get_binding_display_to_cli_name().items()):
159
+ for index, (display_name, cli_name) in enumerate(self._get_binding_display_to_cli_name().items()):
160
160
  ttk.Radiobutton(
161
161
  platform_type_settings,
162
162
  text=display_name,
@@ -202,3 +202,11 @@ class GUI:
202
202
  """
203
203
  self._submit = True
204
204
  self._root.destroy()
205
+
206
+ def _get_binding_display_to_cli_name(self) -> dict[str, str]:
207
+ """Get mapping of display to CLI option names of the available platform bindings.
208
+
209
+ Returns:
210
+ Dictionary of platform binding display name to CLI option name.
211
+ """
212
+ return {binding_type.get_display_name(): binding_type.get_cli_name() for binding_type in get_bindings()}
@@ -2,6 +2,9 @@
2
2
 
3
3
  from os.path import abspath, dirname, join
4
4
 
5
+ from vbl_aquarium.models.ephys_link import SetDepthRequest, SetPositionRequest
6
+ from vbl_aquarium.models.unity import Vector4
7
+
5
8
  # Ephys Link ASCII.
6
9
  ASCII = r"""
7
10
  ______ _ _ _ _
@@ -16,8 +19,87 @@ ASCII = r"""
16
19
 
17
20
  # Absolute path to the resource folder.
18
21
  PACKAGE_DIRECTORY = dirname(dirname(abspath(__file__)))
19
- RESOURCES_DIRECTORY = join(PACKAGE_DIRECTORY, "resources")
20
22
  BINDINGS_DIRECTORY = join(PACKAGE_DIRECTORY, "bindings")
21
23
 
22
24
  # Ephys Link Port
23
25
  PORT = 3000
26
+
27
+ # Error messages
28
+
29
+ NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR = (
30
+ 'Cannot move manipulator while inside the brain. Set the depth ("set_depth") instead.'
31
+ )
32
+
33
+
34
+ def did_not_reach_target_position_error(
35
+ request: SetPositionRequest, axis_index: int, final_unified_position: Vector4
36
+ ) -> str:
37
+ """Generate an error message for when the manipulator did not reach the target position.
38
+ Args:
39
+ request: The object containing the requested position.
40
+ axis_index: The index of the axis that did not reach the target position.
41
+ final_unified_position: The final position of the manipulator.
42
+ Returns:
43
+ str: The error message.
44
+ """
45
+ return f"Manipulator {request.manipulator_id} did not reach target position on axis {list(Vector4.model_fields.keys())[axis_index]}. Requested: {request.position}, got: {final_unified_position}"
46
+
47
+
48
+ def did_not_reach_target_depth_error(request: SetDepthRequest, final_unified_depth: float) -> str:
49
+ """Generate an error message for when the manipulator did not reach the target position.
50
+ Args:
51
+ request: The object containing the requested depth.
52
+ final_unified_depth: The final depth of the manipulator.
53
+ Returns:
54
+ str: The error message.
55
+ """
56
+ return f"Manipulator {request.manipulator_id} did not reach target depth. Requested: {request.depth}, got: {final_unified_depth}"
57
+
58
+
59
+ EMERGENCY_STOP_MESSAGE = "Emergency Stopping All Manipulators..."
60
+
61
+ SERVER_NOT_INITIALIZED_ERROR = "Server not initialized."
62
+ PROXY_CLIENT_NOT_INITIALIZED_ERROR = "Proxy client not initialized."
63
+
64
+
65
+ def cannot_connect_as_client_is_already_connected_error(new_client_sid: str, current_client_sid: str) -> str:
66
+ """Generate an error message for when the client is already connected.
67
+ Args:
68
+ new_client_sid: The SID of the new client.
69
+ current_client_sid: The SID of the current client.
70
+ Returns:
71
+ str: The error message.
72
+ """
73
+ return f"Cannot connect {new_client_sid} as {current_client_sid} is already connected."
74
+
75
+
76
+ def client_disconnected_without_being_connected_error(client_sid: str) -> str:
77
+ """Generate an error message for when the client is disconnected without being connected.
78
+ Args:
79
+ client_sid: The SID of the client.
80
+ Returns:
81
+ str: The error message.
82
+ """
83
+ return f"Client {client_sid} disconnected without being connected."
84
+
85
+
86
+ MALFORMED_REQUEST_ERROR = {"error": "Malformed request."}
87
+ UNKNOWN_EVENT_ERROR = {"error": "Unknown event."}
88
+
89
+ UNABLE_TO_CHECK_FOR_UPDATES_ERROR = (
90
+ "Unable to check for updates. Ignore updates or use the -i flag to disable checks.\n"
91
+ )
92
+
93
+
94
+ def ump_4_3_deprecation_error(cli_name: str):
95
+ return f"CLI option '{cli_name}' is deprecated and will be removed in v3.0.0. Use 'ump' instead."
96
+
97
+
98
+ def unrecognized_platform_type_error(cli_name: str) -> str:
99
+ """Generate an error message for when the platform type is not recognized.
100
+ Args:
101
+ cli_name: The platform type that is not recognized.
102
+ Returns:
103
+ str: The error message.
104
+ """
105
+ return f'Platform type "{cli_name}" not recognized.'
@@ -6,11 +6,19 @@ from pkgutil import iter_modules
6
6
 
7
7
  from packaging.version import parse
8
8
  from requests import ConnectionError, ConnectTimeout, get
9
+ from vbl_aquarium.models.ephys_link import EphysLinkOptions
9
10
 
10
11
  from ephys_link.__about__ import __version__
12
+ from ephys_link.bindings.mpm_binding import MPMBinding
13
+ from ephys_link.front_end.console import Console
11
14
  from ephys_link.utils.base_binding import BaseBinding
12
- from ephys_link.utils.console import Console
13
- from ephys_link.utils.constants import ASCII, BINDINGS_DIRECTORY
15
+ from ephys_link.utils.constants import (
16
+ ASCII,
17
+ BINDINGS_DIRECTORY,
18
+ UNABLE_TO_CHECK_FOR_UPDATES_ERROR,
19
+ ump_4_3_deprecation_error,
20
+ unrecognized_platform_type_error,
21
+ )
14
22
 
15
23
 
16
24
  def preamble() -> None:
@@ -37,9 +45,7 @@ def check_for_updates(console: Console) -> None:
37
45
  console.critical_print(f"Update available: {latest_version} (current: {__version__})")
38
46
  console.critical_print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
39
47
  except (ConnectionError, ConnectTimeout):
40
- console.error_print(
41
- "UPDATE", "Unable to check for updates. Ignore updates or use the the -i flag to disable checks.\n"
42
- )
48
+ console.error_print("UPDATE", UNABLE_TO_CHECK_FOR_UPDATES_ERROR)
43
49
 
44
50
 
45
51
  def get_bindings() -> list[type[BaseBinding]]:
@@ -56,10 +62,41 @@ def get_bindings() -> list[type[BaseBinding]]:
56
62
  ]
57
63
 
58
64
 
59
- def get_binding_display_to_cli_name() -> dict[str, str]:
60
- """Get mapping of display to CLI option names of the available platform bindings.
65
+ def get_binding_instance(options: EphysLinkOptions, console: Console) -> BaseBinding:
66
+ """Get an instance of the requested binding class.
67
+
68
+ Args:
69
+ options: Ephys Link options.
70
+ console: Console instance for printing messages.
71
+
72
+ Raises:
73
+ ValueError: If the platform type is not recognized.
61
74
 
62
75
  Returns:
63
- Dictionary of platform binding display name to CLI option name.
76
+ Instance of a platform binding class.
64
77
  """
65
- return {binding_type.get_display_name(): binding_type.get_cli_name() for binding_type in get_bindings()}
78
+ selected_type = options.type
79
+
80
+ for binding_type in get_bindings():
81
+ binding_cli_name = binding_type.get_cli_name()
82
+
83
+ # Notify deprecation of "ump-4" and "ump-3" CLI options and fix.
84
+ if selected_type in ("ump-4", "ump-3"):
85
+ console.error_print(
86
+ "DEPRECATION",
87
+ ump_4_3_deprecation_error(selected_type),
88
+ )
89
+ selected_type = "ump"
90
+
91
+ 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()
98
+
99
+ # Raise an error if the platform type is not recognized.
100
+ error_message = unrecognized_platform_type_error(selected_type)
101
+ console.critical_print(error_message)
102
+ raise ValueError(error_message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ephys-link
3
- Version: 2.1.0b1
3
+ Version: 2.1.2
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
@@ -22,16 +22,16 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
22
22
  Classifier: Programming Language :: Python :: Implementation :: PyPy
23
23
  Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
24
24
  Requires-Python: >=3.13
25
- Requires-Dist: aiohttp==3.11.16
25
+ Requires-Dist: aiohttp==3.12.15
26
26
  Requires-Dist: colorama==0.4.6
27
27
  Requires-Dist: keyboard==0.13.5
28
- Requires-Dist: packaging==24.2
29
- Requires-Dist: platformdirs==4.3.7
28
+ Requires-Dist: packaging==25.0
29
+ Requires-Dist: platformdirs==4.4.0
30
30
  Requires-Dist: pyserial==3.5
31
31
  Requires-Dist: python-socketio[asyncio-client]==5.13.0
32
- Requires-Dist: requests==2.32.3
33
- Requires-Dist: rich==14.0.0
34
- Requires-Dist: sensapex==1.400.3
32
+ Requires-Dist: requests==2.32.5
33
+ Requires-Dist: rich==14.1.0
34
+ Requires-Dist: sensapex==1.400.4
35
35
  Requires-Dist: vbl-aquarium==1.0.0
36
36
  Description-Content-Type: text/markdown
37
37
 
@@ -43,6 +43,7 @@ Description-Content-Type: text/markdown
43
43
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
44
44
  [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev)
45
45
  [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
46
+ [![Test](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/test.yml/badge.svg)](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/test.yml)
46
47
 
47
48
  <!-- [![Build](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/build.yml/badge.svg)](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/build.yml) -->
48
49
 
@@ -0,0 +1,24 @@
1
+ ephys_link/__about__.py,sha256=UiuBcRXPtXxPUBDdp0ZDvWl0U9Db1kMNfT3oAfhxqLg,22
2
+ ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ephys_link/__main__.py,sha256=54zxQ-fyxC2-LGsTUdtlvib36ZZQwNyOa6IuffYLhhs,1582
4
+ ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ ephys_link/back_end/platform_handler.py,sha256=6Lt_jaIcO9jTmWYOoWYkuhwSlCTT1Qfd53ru31X0fmc,10130
6
+ ephys_link/back_end/server.py,sha256=uZOK9UQq4gp7yx6_4dZrfRTjsio2WXT8TwjO4E5kECA,10712
7
+ ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ ephys_link/bindings/fake_binding.py,sha256=PI7zYv-SjWsGFEs_FVu5Z5l9gykIqG3C7pQISdbwdoY,2357
9
+ ephys_link/bindings/mpm_binding.py,sha256=vn7IKqdiZ6_MX91zomqDXX08ONHwVVgWncRuJTxJpOM,10872
10
+ ephys_link/bindings/ump_binding.py,sha256=wbJe6Ro4E-TPejBBDkNsKUluF2Gr0rZBuyp3LJ2TipM,7865
11
+ 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/console.py,sha256=zq67dn7T4xKMg3jjbNra_1s0N9ItvsTRXVIhClWbjP8,3912
14
+ ephys_link/front_end/gui.py,sha256=x5mNekxe1yhPKcCFNnDL3AarhNmUoBJooykFS_6pWt4,7227
15
+ ephys_link/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ 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/converters.py,sha256=ZdVmIX-LHCwM__F0SpjN_mfNGGetr1U97xvHd0hf8T0,2038
19
+ ephys_link/utils/startup.py,sha256=Yx9LSedaLmTpzkb1E0NOqxaDMhRXLShkI2PJj80_95U,3620
20
+ ephys_link-2.1.2.dist-info/METADATA,sha256=XeblqHX3PIq68by5F8ChxqFfwKbb-I63bVS_6BndetA,4775
21
+ ephys_link-2.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ ephys_link-2.1.2.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
23
+ ephys_link-2.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
24
+ ephys_link-2.1.2.dist-info/RECORD,,
Binary file
@@ -1,25 +0,0 @@
1
- ephys_link/__about__.py,sha256=ioFei0HyRaKlvpUBHHnq0pLx8kyDkQUelt4_vkwvk20,24
2
- ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- ephys_link/__main__.py,sha256=sbFdC6KJjTfXDgRraU_fmGRPcF4I1Ur9PRDiD86dkRI,1449
4
- ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- ephys_link/back_end/platform_handler.py,sha256=gdiO6d0L-DWWLEJOL6eP6685tOC6otffmhfIBtPjhq0,12604
6
- ephys_link/back_end/server.py,sha256=mb3K3pXSO-gHaSj1CGJ0v3CSOW5YCi-p0EOKoySRzKQ,10322
7
- ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- ephys_link/bindings/fake_binding.py,sha256=PI7zYv-SjWsGFEs_FVu5Z5l9gykIqG3C7pQISdbwdoY,2357
9
- ephys_link/bindings/mpm_binding.py,sha256=vn7IKqdiZ6_MX91zomqDXX08ONHwVVgWncRuJTxJpOM,10872
10
- ephys_link/bindings/ump_binding.py,sha256=HeFMA_6P9WqZDy_hVRGWmwEDmDoMlt8w8xCWHIc6S-o,7974
11
- ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- ephys_link/front_end/cli.py,sha256=isIJs_sZbz7VbNvLgi-HyDlE-TyKD12auDhMTxAkWQU,3099
13
- ephys_link/front_end/gui.py,sha256=MDcrTS_Xz9bopAgamh4HknqRC10W8E6eOS3Kss_2ZKQ,6864
14
- ephys_link/resources/libum.dll,sha256=YaD4dwiSNohx-XxHjx2eQWPOBEVvUIXARvx37e_yqNw,316316
15
- ephys_link/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- ephys_link/utils/base_binding.py,sha256=rq0FtvkjAN287nX8rZwR-j6YPzV3Pkp8PqJfZlhIzqo,5413
17
- ephys_link/utils/console.py,sha256=52SYvXv_7Fx8QDL3RMFQoggQ1n5W93Yu5aU7uuJQgfg,3904
18
- ephys_link/utils/constants.py,sha256=1aML7zBNTM5onVSf6NDrYIR33VJy-dIHd1lFORVBGbM,725
19
- ephys_link/utils/converters.py,sha256=ZdVmIX-LHCwM__F0SpjN_mfNGGetr1U97xvHd0hf8T0,2038
20
- ephys_link/utils/startup.py,sha256=jZVed78tuWjUuZqWVgii_zumDr87T-ikEtOFa6KTE_E,2500
21
- ephys_link-2.1.0b1.dist-info/METADATA,sha256=XE_kTRieGoQRBR5j1zvrl9QcmPdazuCRqUIrLG6K7eI,4609
22
- ephys_link-2.1.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- ephys_link-2.1.0b1.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
24
- ephys_link-2.1.0b1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
- ephys_link-2.1.0b1.dist-info/RECORD,,