ephys-link 1.3.3__py3-none-any.whl → 2.0.0__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.
Files changed (42) hide show
  1. ephys_link/__about__.py +1 -1
  2. ephys_link/__main__.py +51 -105
  3. ephys_link/back_end/__init__.py +0 -0
  4. ephys_link/back_end/platform_handler.py +315 -0
  5. ephys_link/back_end/server.py +274 -0
  6. ephys_link/bindings/__init__.py +0 -0
  7. ephys_link/bindings/fake_binding.py +84 -0
  8. ephys_link/bindings/mpm_binding.py +315 -0
  9. ephys_link/bindings/ump_4_binding.py +157 -0
  10. ephys_link/front_end/__init__.py +0 -0
  11. ephys_link/front_end/cli.py +104 -0
  12. ephys_link/front_end/gui.py +204 -0
  13. ephys_link/utils/__init__.py +0 -0
  14. ephys_link/utils/base_binding.py +176 -0
  15. ephys_link/utils/console.py +127 -0
  16. ephys_link/utils/constants.py +23 -0
  17. ephys_link/utils/converters.py +86 -0
  18. ephys_link/utils/startup.py +65 -0
  19. ephys_link-2.0.0.dist-info/METADATA +91 -0
  20. ephys_link-2.0.0.dist-info/RECORD +25 -0
  21. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/WHEEL +1 -1
  22. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/licenses/LICENSE +674 -674
  23. ephys_link/common.py +0 -49
  24. ephys_link/emergency_stop.py +0 -67
  25. ephys_link/gui.py +0 -217
  26. ephys_link/platform_handler.py +0 -465
  27. ephys_link/platform_manipulator.py +0 -35
  28. ephys_link/platforms/__init__.py +0 -5
  29. ephys_link/platforms/new_scale_handler.py +0 -141
  30. ephys_link/platforms/new_scale_manipulator.py +0 -312
  31. ephys_link/platforms/new_scale_pathfinder_handler.py +0 -235
  32. ephys_link/platforms/sensapex_handler.py +0 -151
  33. ephys_link/platforms/sensapex_manipulator.py +0 -227
  34. ephys_link/platforms/ump3_handler.py +0 -57
  35. ephys_link/platforms/ump3_manipulator.py +0 -147
  36. ephys_link/resources/CP210xManufacturing.dll +0 -0
  37. ephys_link/resources/NstMotorCtrl.dll +0 -0
  38. ephys_link/resources/SiUSBXp.dll +0 -0
  39. ephys_link/server.py +0 -508
  40. ephys_link-1.3.3.dist-info/METADATA +0 -164
  41. ephys_link-1.3.3.dist-info/RECORD +0 -26
  42. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/entry_points.txt +0 -0
ephys_link/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.3"
1
+ __version__ = "2.0.0"
ephys_link/__main__.py CHANGED
@@ -1,105 +1,51 @@
1
- from argparse import ArgumentParser
2
- from asyncio import run
3
- from sys import argv
4
-
5
- from ephys_link import common as com
6
- from ephys_link.__about__ import __version__ as version
7
- from ephys_link.emergency_stop import EmergencyStop
8
- from ephys_link.gui import GUI
9
- from ephys_link.server import Server
10
-
11
- # Setup argument parser.
12
- parser = ArgumentParser(
13
- description="Electrophysiology Manipulator Link: a websocket interface for"
14
- " manipulators in electrophysiology experiments.",
15
- prog="python -m ephys-link",
16
- )
17
- parser.add_argument("-b", "--background", dest="background", action="store_true", help="Skip configuration window.")
18
- parser.add_argument(
19
- "-i", "--ignore-updates", dest="ignore_updates", action="store_true", help="Skip (ignore) checking for updates."
20
- )
21
- parser.add_argument(
22
- "-t",
23
- "--type",
24
- type=str,
25
- dest="type",
26
- default="sensapex",
27
- help='Manipulator type (i.e. "sensapex", "new_scale", or "new_scale_pathfinder"). Default: "sensapex".',
28
- )
29
- parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Enable debug mode.")
30
- parser.add_argument("-x", "--use-proxy", dest="use_proxy", action="store_true", help="Enable proxy mode.")
31
- parser.add_argument(
32
- "-a",
33
- "--proxy-address",
34
- type=str,
35
- default="proxy2.virtualbrainlab.org",
36
- dest="proxy_address",
37
- help="Proxy IP address.",
38
- )
39
- parser.add_argument(
40
- "-p",
41
- "--port",
42
- type=int,
43
- default=8081,
44
- dest="port",
45
- help="TCP/IP port to use. Default: 8081 (avoids conflict with other HTTP servers).",
46
- )
47
- parser.add_argument(
48
- "--pathfinder_port",
49
- type=int,
50
- default=8080,
51
- dest="pathfinder_port",
52
- help="Port New Scale Pathfinder's server is on. Default: 8080.",
53
- )
54
- parser.add_argument(
55
- "-s",
56
- "--serial",
57
- type=str,
58
- default="no-e-stop",
59
- dest="serial",
60
- nargs="?",
61
- help="Emergency stop serial port (i.e. COM3). Default: disables emergency stop.",
62
- )
63
- parser.add_argument(
64
- "-v",
65
- "--version",
66
- action="version",
67
- version=f"Electrophysiology Manipulator Link v{version}",
68
- help="Print version and exit.",
69
- )
70
-
71
-
72
- def main() -> None:
73
- """Main function"""
74
-
75
- # Parse arguments.
76
- args = parser.parse_args()
77
-
78
- # Launch GUI if there are no CLI arguments.
79
- if len(argv) == 1:
80
- gui = GUI()
81
- gui.launch()
82
- return None
83
-
84
- # Otherwise, create Server from CLI.
85
- server = Server()
86
-
87
- # Continue with CLI if not.
88
- com.DEBUG = args.debug
89
-
90
- # Setup serial port.
91
- if args.serial != "no-e-stop":
92
- e_stop = EmergencyStop(server, args.serial)
93
- e_stop.watch()
94
-
95
- # Launch with parsed arguments on main thread.
96
- if args.use_proxy:
97
- run(
98
- server.launch_for_proxy(args.proxy_address, args.port, args.type, args.pathfinder_port, args.ignore_updates)
99
- )
100
- else:
101
- server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates)
102
-
103
-
104
- if __name__ == "__main__":
105
- main()
1
+ """Ephys Link entry point.
2
+
3
+ Responsible for gathering launch options, instantiating the appropriate interface, and starting the application.
4
+
5
+ Usage:
6
+ ```python
7
+ main()
8
+ ```
9
+ """
10
+
11
+ from asyncio import run
12
+ from sys import argv
13
+
14
+ from keyboard import add_hotkey
15
+
16
+ from ephys_link.back_end.platform_handler import PlatformHandler
17
+ from ephys_link.back_end.server import Server
18
+ from ephys_link.front_end.cli import CLI
19
+ 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
22
+
23
+
24
+ def main() -> None:
25
+ """Ephys Link entry point."""
26
+
27
+ # 0. Print the startup preamble.
28
+ preamble()
29
+
30
+ # 1. Get options via CLI or GUI (if no CLI options are provided).
31
+ options = CLI().parse_args() if len(argv) > 1 else GUI().get_options()
32
+
33
+ # 2. Instantiate the Console and make it globally accessible.
34
+ console = Console(enable_debug=options.debug)
35
+
36
+ # 3. Check for updates if not disabled.
37
+ if not options.ignore_updates:
38
+ check_for_updates(console)
39
+
40
+ # 4. Instantiate the Platform Handler with the appropriate platform bindings.
41
+ platform_handler = PlatformHandler(options, console)
42
+
43
+ # 5. Add hotkeys for emergency stop.
44
+ _ = add_hotkey("ctrl+alt+shift+q", lambda: run(platform_handler.emergency_stop()))
45
+
46
+ # 6. Start the server.
47
+ Server(options, platform_handler, console).launch()
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
File without changes
@@ -0,0 +1,315 @@
1
+ """Manipulator platform handler.
2
+
3
+ Responsible for performing the various manipulator commands.
4
+ Instantiates the appropriate bindings based on the platform type and uses them to perform the commands.
5
+
6
+ Usage:
7
+ Instantiate PlatformHandler with the platform type and call the desired command.
8
+ """
9
+
10
+ from typing import final
11
+ from uuid import uuid4
12
+
13
+ from vbl_aquarium.models.ephys_link import (
14
+ AngularResponse,
15
+ BooleanStateResponse,
16
+ EphysLinkOptions,
17
+ GetManipulatorsResponse,
18
+ PlatformInfo,
19
+ PositionalResponse,
20
+ SetDepthRequest,
21
+ SetDepthResponse,
22
+ SetInsideBrainRequest,
23
+ SetPositionRequest,
24
+ ShankCountResponse,
25
+ )
26
+ from vbl_aquarium.models.unity import Vector4
27
+
28
+ from ephys_link.bindings.mpm_binding import MPMBinding
29
+ from ephys_link.utils.base_binding import BaseBinding
30
+ from ephys_link.utils.console import Console
31
+ from ephys_link.utils.converters import vector4_to_array
32
+ from ephys_link.utils.startup import get_bindings
33
+
34
+
35
+ @final
36
+ class PlatformHandler:
37
+ """Handler for platform commands."""
38
+
39
+ def __init__(self, options: EphysLinkOptions, console: Console) -> None:
40
+ """Initialize platform handler.
41
+
42
+ Args:
43
+ options: CLI options.
44
+ console: Console instance.
45
+ """
46
+ # Store the CLI options.
47
+ self._options = options
48
+
49
+ # Store the console.
50
+ self._console = console
51
+
52
+ # Define bindings based on platform type.
53
+ self._bindings = self._get_binding_instance(options)
54
+
55
+ # Record which IDs are inside the brain.
56
+ self._inside_brain: set[str] = set()
57
+
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
+ for binding_type in get_bindings():
74
+ binding_cli_name = binding_type.get_cli_name()
75
+
76
+ if binding_cli_name == options.type:
77
+ # Pass in HTTP port for Pathfinder MPM.
78
+ if binding_cli_name == "pathfinder-mpm":
79
+ return MPMBinding(options.mpm_port)
80
+
81
+ # Otherwise just return the binding.
82
+ return binding_type()
83
+
84
+ # Raise an error if the platform type is not recognized.
85
+ error_message = f'Platform type "{options.type}" not recognized.'
86
+ self._console.critical_print(error_message)
87
+ raise ValueError(error_message)
88
+
89
+ # Platform metadata.
90
+
91
+ def get_display_name(self) -> str:
92
+ """Get the display name for the platform.
93
+
94
+ Returns:
95
+ Display name for the platform.
96
+ """
97
+ return self._bindings.get_display_name()
98
+
99
+ async def get_platform_info(self) -> PlatformInfo:
100
+ """Get the manipulator platform type connected to Ephys Link.
101
+
102
+ Returns:
103
+ Platform type config identifier (see CLI options for examples).
104
+ """
105
+ return PlatformInfo(
106
+ name=self._bindings.get_display_name(),
107
+ cli_name=self._bindings.get_cli_name(),
108
+ axes_count=await self._bindings.get_axes_count(),
109
+ dimensions=self._bindings.get_dimensions(),
110
+ )
111
+
112
+ # Manipulator commands.
113
+
114
+ async def get_manipulators(self) -> GetManipulatorsResponse:
115
+ """Get a list of available manipulators on the current handler.
116
+
117
+ Returns:
118
+ List of manipulator IDs or an error message if any.
119
+ """
120
+ try:
121
+ manipulators = await self._bindings.get_manipulators()
122
+ except Exception as e: # noqa: BLE001
123
+ self._console.exception_error_print("Get Manipulators", e)
124
+ return GetManipulatorsResponse(error=self._console.pretty_exception(e))
125
+ else:
126
+ return GetManipulatorsResponse(manipulators=manipulators)
127
+
128
+ async def get_position(self, manipulator_id: str) -> PositionalResponse:
129
+ """Get the current translation position of a manipulator in unified coordinates (mm).
130
+
131
+ Args:
132
+ manipulator_id: Manipulator ID.
133
+
134
+ Returns:
135
+ Current position of the manipulator and an error message if any.
136
+ """
137
+ try:
138
+ unified_position = self._bindings.platform_space_to_unified_space(
139
+ await self._bindings.get_position(manipulator_id)
140
+ )
141
+ except Exception as e: # noqa: BLE001
142
+ self._console.exception_error_print("Get Position", e)
143
+ return PositionalResponse(error=str(e))
144
+ else:
145
+ return PositionalResponse(position=unified_position)
146
+
147
+ async def get_angles(self, manipulator_id: str) -> AngularResponse:
148
+ """Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
149
+
150
+ Args:
151
+ manipulator_id: Manipulator ID.
152
+
153
+ Returns:
154
+ Current angles of the manipulator and an error message if any.
155
+ """
156
+ try:
157
+ angles = await self._bindings.get_angles(manipulator_id)
158
+ except Exception as e: # noqa: BLE001
159
+ self._console.exception_error_print("Get Angles", e)
160
+ return AngularResponse(error=self._console.pretty_exception(e))
161
+ else:
162
+ return AngularResponse(angles=angles)
163
+
164
+ async def get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
165
+ """Get the number of shanks on a manipulator.
166
+
167
+ Args:
168
+ manipulator_id: Manipulator ID.
169
+
170
+ Returns:
171
+ Number of shanks on the manipulator and an error message if any.
172
+ """
173
+ try:
174
+ shank_count = await self._bindings.get_shank_count(manipulator_id)
175
+ except Exception as e: # noqa: BLE001
176
+ self._console.exception_error_print("Get Shank Count", e)
177
+ return ShankCountResponse(error=self._console.pretty_exception(e))
178
+ else:
179
+ return ShankCountResponse(shank_count=shank_count)
180
+
181
+ async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
182
+ """Move a manipulator to a specified translation position in unified coordinates (mm).
183
+
184
+ Args:
185
+ request: Request to move a manipulator to a specified position.
186
+
187
+ Returns:
188
+ Final position of the manipulator and an error message if any.
189
+ """
190
+ try:
191
+ # Disallow setting manipulator position while inside the brain.
192
+ if request.manipulator_id in self._inside_brain:
193
+ error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
194
+ self._console.error_print("Set Position", error_message)
195
+ return PositionalResponse(error=error_message)
196
+
197
+ # Move to the new position.
198
+ final_platform_position = await self._bindings.set_position(
199
+ manipulator_id=request.manipulator_id,
200
+ position=self._bindings.unified_space_to_platform_space(request.position),
201
+ speed=request.speed,
202
+ )
203
+ final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
204
+
205
+ # Return error if movement did not reach target within tolerance.
206
+ for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
207
+ # End once index is the number of axes.
208
+ if index == await self._bindings.get_axes_count():
209
+ break
210
+
211
+ # Check if the axis is within the movement tolerance.
212
+ if abs(axis) > self._bindings.get_movement_tolerance():
213
+ error_message = (
214
+ f"Manipulator {request.manipulator_id} did not reach target"
215
+ f" position on axis {list(Vector4.model_fields.keys())[index]}."
216
+ f" Requested: {request.position}, got: {final_unified_position}."
217
+ )
218
+ self._console.error_print("Set Position", error_message)
219
+ return PositionalResponse(error=error_message)
220
+ except Exception as e: # noqa: BLE001
221
+ self._console.exception_error_print("Set Position", e)
222
+ return PositionalResponse(error=self._console.pretty_exception(e))
223
+ else:
224
+ return PositionalResponse(position=final_unified_position)
225
+
226
+ async def set_depth(self, request: SetDepthRequest) -> SetDepthResponse:
227
+ """Move a manipulator's depth translation stage to a specific value (mm).
228
+
229
+ Args:
230
+ request: Request to move a manipulator to a specified depth.
231
+
232
+ Returns:
233
+ Final depth of the manipulator and an error message if any.
234
+ """
235
+ try:
236
+ # Move to the new depth.
237
+ final_platform_depth = await self._bindings.set_depth(
238
+ manipulator_id=request.manipulator_id,
239
+ depth=self._bindings.unified_space_to_platform_space(Vector4(w=request.depth)).w,
240
+ speed=request.speed,
241
+ )
242
+ final_unified_depth = self._bindings.platform_space_to_unified_space(Vector4(w=final_platform_depth)).w
243
+
244
+ # Return error if movement did not reach target within tolerance.
245
+ if abs(final_unified_depth - request.depth) > self._bindings.get_movement_tolerance():
246
+ error_message = (
247
+ f"Manipulator {request.manipulator_id} did not reach target depth."
248
+ f" Requested: {request.depth}, got: {final_unified_depth}."
249
+ )
250
+ self._console.error_print("Set Depth", error_message)
251
+ return SetDepthResponse(error=error_message)
252
+ except Exception as e: # noqa: BLE001
253
+ self._console.exception_error_print("Set Depth", e)
254
+ return SetDepthResponse(error=self._console.pretty_exception(e))
255
+ else:
256
+ return SetDepthResponse(depth=final_unified_depth)
257
+
258
+ async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
259
+ """Mark a manipulator as inside the brain or not.
260
+
261
+ This should restrict the manipulator's movement to just the depth axis.
262
+
263
+ Args:
264
+ request: Request to set
265
+
266
+ Returns:
267
+ Inside brain state of the manipulator and an error message if any.
268
+ """
269
+ try:
270
+ if request.inside:
271
+ self._inside_brain.add(request.manipulator_id)
272
+ else:
273
+ self._inside_brain.discard(request.manipulator_id)
274
+ except Exception as e: # noqa: BLE001
275
+ self._console.exception_error_print("Set Inside Brain", e)
276
+ return BooleanStateResponse(error=self._console.pretty_exception(e))
277
+ else:
278
+ return BooleanStateResponse(state=request.inside)
279
+
280
+ async def stop(self, manipulator_id: str) -> str:
281
+ """Stop a manipulator.
282
+
283
+ Args:
284
+ manipulator_id: Manipulator ID.
285
+
286
+ Returns:
287
+ Error message if any.
288
+ """
289
+ try:
290
+ await self._bindings.stop(manipulator_id)
291
+ except Exception as e: # noqa: BLE001
292
+ self._console.exception_error_print("Stop", e)
293
+ return self._console.pretty_exception(e)
294
+ else:
295
+ return ""
296
+
297
+ async def stop_all(self) -> str:
298
+ """Stop all manipulators.
299
+
300
+ Returns:
301
+ Error message if any.
302
+ """
303
+ try:
304
+ for manipulator_id in await self._bindings.get_manipulators():
305
+ await self._bindings.stop(manipulator_id)
306
+ except Exception as e: # noqa: BLE001
307
+ self._console.exception_error_print("Stop", e)
308
+ return self._console.pretty_exception(e)
309
+ else:
310
+ return ""
311
+
312
+ async def emergency_stop(self) -> None:
313
+ """Stops all manipulators with a message."""
314
+ self._console.critical_print("Emergency Stopping All Manipulators...")
315
+ _ = await self.stop_all()