ephys-link 1.3.3__py3-none-any.whl → 2.0.0b1__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 (34) hide show
  1. ephys_link/__about__.py +1 -1
  2. ephys_link/__main__.py +28 -90
  3. ephys_link/back_end/__init__.py +0 -0
  4. ephys_link/back_end/platform_handler.py +298 -0
  5. ephys_link/back_end/server.py +200 -0
  6. ephys_link/bindings/__init__.py +0 -0
  7. ephys_link/bindings/fake_bindings.py +54 -0
  8. ephys_link/bindings/ump_4_bindings.py +127 -0
  9. ephys_link/front_end/__init__.py +0 -0
  10. ephys_link/front_end/cli.py +98 -0
  11. ephys_link/{gui.py → front_end/gui.py} +93 -95
  12. ephys_link/util/__init__.py +0 -0
  13. ephys_link/util/base_bindings.py +133 -0
  14. ephys_link/util/common.py +121 -0
  15. ephys_link/util/console.py +112 -0
  16. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/METADATA +6 -4
  17. ephys_link-2.0.0b1.dist-info/RECORD +25 -0
  18. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/WHEEL +1 -1
  19. ephys_link/common.py +0 -49
  20. ephys_link/emergency_stop.py +0 -67
  21. ephys_link/platform_handler.py +0 -465
  22. ephys_link/platform_manipulator.py +0 -35
  23. ephys_link/platforms/__init__.py +0 -5
  24. ephys_link/platforms/new_scale_handler.py +0 -141
  25. ephys_link/platforms/new_scale_manipulator.py +0 -312
  26. ephys_link/platforms/new_scale_pathfinder_handler.py +0 -235
  27. ephys_link/platforms/sensapex_handler.py +0 -151
  28. ephys_link/platforms/sensapex_manipulator.py +0 -227
  29. ephys_link/platforms/ump3_handler.py +0 -57
  30. ephys_link/platforms/ump3_manipulator.py +0 -147
  31. ephys_link/server.py +0 -508
  32. ephys_link-1.3.3.dist-info/RECORD +0 -26
  33. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/entry_points.txt +0 -0
  34. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,54 @@
1
+ from vbl_aquarium.models.unity import Vector3, Vector4
2
+
3
+ from ephys_link.util.base_bindings import BaseBindings
4
+
5
+
6
+ class FakeBindings(BaseBindings):
7
+ def __init__(self) -> None:
8
+ """Initialize fake manipulator infos."""
9
+
10
+ self._positions = [Vector4() for _ in range(8)]
11
+ self._angles = [
12
+ Vector3(x=90, y=60, z=0),
13
+ Vector3(x=-90, y=60, z=0),
14
+ Vector3(x=180, y=60, z=0),
15
+ Vector3(x=0, y=60, z=0),
16
+ Vector3(x=45, y=30, z=0),
17
+ Vector3(x=-45, y=30, z=0),
18
+ Vector3(x=135, y=30, z=0),
19
+ Vector3(x=-135, y=30, z=0),
20
+ ]
21
+
22
+ async def get_manipulators(self) -> list[str]:
23
+ return list(map(str, range(8)))
24
+
25
+ async def get_num_axes(self) -> int:
26
+ return 4
27
+
28
+ def get_dimensions(self) -> Vector4:
29
+ return Vector4(x=20, y=20, z=20, w=20)
30
+
31
+ async def get_position(self, manipulator_id: str) -> Vector4:
32
+ return self._positions[int(manipulator_id)]
33
+
34
+ async def get_angles(self, manipulator_id: str) -> Vector3:
35
+ return self._angles[int(manipulator_id)]
36
+
37
+ async def get_shank_count(self, _: str) -> int:
38
+ return 1
39
+
40
+ async def get_movement_tolerance(self) -> float:
41
+ return 0.001
42
+
43
+ async def set_position(self, manipulator_id: str, position: Vector4, _: float) -> Vector4:
44
+ self._positions[int(manipulator_id)] = position
45
+ return position
46
+
47
+ async def stop(self, _: str) -> None:
48
+ pass
49
+
50
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
51
+ pass
52
+
53
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
54
+ pass
@@ -0,0 +1,127 @@
1
+ """Bindings for Sensapex uMp-4 platform.
2
+
3
+ Usage: Instantiate Ump4Bindings to interact with the Sensapex uMp-4 platform.
4
+ """
5
+
6
+ from asyncio import get_running_loop
7
+
8
+ from sensapex import UMP, SensapexDevice
9
+ from vbl_aquarium.models.unity import Vector3, Vector4
10
+
11
+ from ephys_link.util.base_bindings import BaseBindings
12
+ from ephys_link.util.common import RESOURCES_PATH, array_to_vector4, mm_to_um, mmps_to_umps, um_to_mm, vector4_to_array
13
+ from ephys_link.util.console import Console
14
+
15
+
16
+ class Ump4Bindings(BaseBindings):
17
+ """Bindings for UMP-4 platform"""
18
+
19
+ def __init__(self) -> None:
20
+ """Initialize UMP-4 bindings."""
21
+
22
+ # Establish connection to Sensapex API (exit if connection fails).
23
+ UMP.set_library_path(RESOURCES_PATH)
24
+ self._ump = UMP.get_ump()
25
+ if self._ump is None:
26
+ error_message = "Unable to connect to uMp"
27
+ Console.error_print(error_message)
28
+ raise ValueError(error_message)
29
+
30
+ async def get_manipulators(self) -> list[str]:
31
+ return list(map(str, self._ump.list_devices()))
32
+
33
+ async def get_num_axes(self) -> int:
34
+ return 4
35
+
36
+ def get_dimensions(self) -> Vector4:
37
+ return Vector4(x=20, y=20, z=20, w=20)
38
+
39
+ async def get_position(self, manipulator_id: str) -> Vector4:
40
+ return um_to_mm(array_to_vector4(self._get_device(manipulator_id).get_pos(1)))
41
+
42
+ # noinspection PyTypeChecker
43
+ async def get_angles(self, _: str) -> Vector3:
44
+ """uMp-4 does not support getting angles so raise an error.
45
+
46
+ :raises: AttributeError
47
+ """
48
+ error_message = "UMP-4 does not support getting angles"
49
+ raise AttributeError(error_message)
50
+
51
+ # noinspection PyTypeChecker
52
+ async def get_shank_count(self, _: str) -> int:
53
+ """uMp-4 does not support getting shank count so raise an error.
54
+
55
+ :raises: AttributeError
56
+ """
57
+ error_message = "UMP-4 does not support getting shank count"
58
+ raise AttributeError(error_message)
59
+
60
+ async def get_movement_tolerance(self) -> float:
61
+ return 0.001
62
+
63
+ async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
64
+ """Set the position of the manipulator.
65
+
66
+ Waits using Asyncio until the movement is finished. This assumes the application is running in an event loop.
67
+
68
+ :param manipulator_id: Manipulator ID.
69
+ :type manipulator_id: str
70
+ :param position: Platform space position to set the manipulator to (mm).
71
+ :type position: Vector4
72
+ :param speed: Speed to move the manipulator to the position (mm/s).
73
+ :type speed: float
74
+ :returns: Final position of the manipulator in platform space (mm).
75
+ :rtype: Vector4
76
+ :raises RuntimeError: If the movement is interrupted.
77
+ """
78
+ # Convert position to micrometers.
79
+ target_position_um = mm_to_um(position)
80
+
81
+ # Request movement.
82
+ movement = self._get_device(manipulator_id).goto_pos(vector4_to_array(target_position_um), mmps_to_umps(speed))
83
+
84
+ # Wait for movement to finish.
85
+ await get_running_loop().run_in_executor(None, movement.finished_event.wait)
86
+
87
+ # Handle interrupted movement.
88
+ if movement.interrupted:
89
+ error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}"
90
+ raise RuntimeError(error_message)
91
+
92
+ return um_to_mm(array_to_vector4(movement.last_pos))
93
+
94
+ async def stop(self, manipulator_id: str) -> None:
95
+ self._get_device(manipulator_id).stop()
96
+
97
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
98
+ # unified <- platform
99
+ # +x <- +y
100
+ # +y <- -z
101
+ # +z <- +x
102
+ # +d <- +d
103
+
104
+ return Vector4(
105
+ x=platform_space.y,
106
+ y=self.get_dimensions().z - platform_space.z,
107
+ z=platform_space.x,
108
+ w=platform_space.w,
109
+ )
110
+
111
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
112
+ # platform <- unified
113
+ # +x <- +z
114
+ # +y <- +x
115
+ # +z <- -y
116
+ # +d <- +d
117
+
118
+ return Vector4(
119
+ x=unified_space.z,
120
+ y=unified_space.x,
121
+ z=self.get_dimensions().z - unified_space.y,
122
+ w=unified_space.w,
123
+ )
124
+
125
+ # Helper methods.
126
+ def _get_device(self, manipulator_id: str) -> SensapexDevice:
127
+ return self._ump.get_device(int(manipulator_id))
File without changes
@@ -0,0 +1,98 @@
1
+ """Command-line interface for the Electrophysiology Manipulator Link.
2
+
3
+ Usage: instantiate CLI and call parse_args() to get the parsed arguments.
4
+ """
5
+
6
+ from argparse import ArgumentParser
7
+
8
+ from vbl_aquarium.models.ephys_link import EphysLinkOptions
9
+
10
+ from ephys_link.__about__ import __version__ as version
11
+
12
+
13
+ class CLI:
14
+ """Command-line interface for the Electrophysiology Manipulator Link.
15
+
16
+ Configures the CLI parser and options.
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ """Initialize CLI parser."""
21
+
22
+ self._parser = ArgumentParser(
23
+ description="Electrophysiology Manipulator Link:"
24
+ " a Socket.IO interface for manipulators in electrophysiology experiments.",
25
+ prog="python -m ephys-link",
26
+ )
27
+
28
+ self._parser.add_argument(
29
+ "-b", "--background", dest="background", action="store_true", help="Skip configuration window."
30
+ )
31
+ self._parser.add_argument(
32
+ "-i",
33
+ "--ignore-updates",
34
+ dest="ignore_updates",
35
+ action="store_true",
36
+ help="Skip (ignore) checking for updates.",
37
+ )
38
+ self._parser.add_argument(
39
+ "-t",
40
+ "--type",
41
+ type=str,
42
+ dest="type",
43
+ default="ump-4",
44
+ help='Manipulator type (i.e. "ump-4", "ump-3", "pathfinder-mpm", "new-scale", "fake"). Default: "ump-4".',
45
+ )
46
+ self._parser.add_argument(
47
+ "-d",
48
+ "--debug",
49
+ dest="debug",
50
+ action="store_true",
51
+ help="Enable debug mode.",
52
+ )
53
+ self._parser.add_argument(
54
+ "-p",
55
+ "--use-proxy",
56
+ dest="use_proxy",
57
+ action="store_true",
58
+ help="Enable proxy mode.",
59
+ )
60
+ self._parser.add_argument(
61
+ "-a",
62
+ "--proxy-address",
63
+ type=str,
64
+ default="proxy2.virtualbrainlab.org",
65
+ dest="proxy_address",
66
+ help="Proxy IP address.",
67
+ )
68
+ self._parser.add_argument(
69
+ "--mpm-port",
70
+ type=int,
71
+ default=8080,
72
+ dest="mpm_port",
73
+ help="Port New Scale Pathfinder MPM's server is on. Default: 8080.",
74
+ )
75
+ self._parser.add_argument(
76
+ "-s",
77
+ "--serial",
78
+ type=str,
79
+ default="no-e-stop",
80
+ dest="serial",
81
+ nargs="?",
82
+ help="Emergency stop serial port (i.e. COM3). Default: disables emergency stop.",
83
+ )
84
+ self._parser.add_argument(
85
+ "-v",
86
+ "--version",
87
+ action="version",
88
+ version=f"Electrophysiology Manipulator Link v{version}",
89
+ help="Print version and exit.",
90
+ )
91
+
92
+ def parse_args(self) -> EphysLinkOptions:
93
+ """Parse arguments and return them
94
+
95
+ :returns: Parsed arguments
96
+ :rtype: EphysLinkOptions
97
+ """
98
+ return EphysLinkOptions(**vars(self._parser.parse_args()))
@@ -1,64 +1,89 @@
1
- from asyncio import run
2
- from json import dumps, load
1
+ """Graphical User Interface for Ephys Link.
2
+
3
+ Usage: create a GUI instance and call get_options() to get the options.
4
+ """
5
+
6
+ from json import load
3
7
  from os import makedirs
4
- from os.path import exists
8
+ from os.path import exists, join
5
9
  from socket import gethostbyname, gethostname
10
+ from sys import exit
6
11
  from tkinter import CENTER, RIGHT, BooleanVar, E, IntVar, StringVar, Tk, ttk
7
12
 
8
13
  from platformdirs import user_config_dir
14
+ from vbl_aquarium.models.ephys_link import EphysLinkOptions
9
15
 
10
- import ephys_link.common as com
11
16
  from ephys_link.__about__ import __version__ as version
12
- from ephys_link.emergency_stop import EmergencyStop
13
- from ephys_link.server import Server
14
17
 
15
- SETTINGS_DIR = f"{user_config_dir()}\\VBL\\Ephys Link"
16
- SETTINGS_FILENAME = "settings.json"
18
+ # Define options path.
19
+ OPTIONS_DIR = join(user_config_dir(), "VBL", "Ephys Link")
20
+ OPTIONS_FILENAME = "options.json"
21
+ OPTIONS_PATH = join(OPTIONS_DIR, OPTIONS_FILENAME)
17
22
 
18
23
 
19
24
  class GUI:
20
- """GUI definition for Ephys Link"""
25
+ """Graphical User Interface for Ephys Link.
26
+
27
+ Gathers options from the user and saves them to a file.
28
+ """
21
29
 
22
30
  def __init__(self) -> None:
23
- """Setup and construction of the Tk GUI"""
31
+ """Setup GUI properties."""
24
32
 
25
33
  self._root = Tk()
26
34
 
27
- # Create default settings dictionary
28
- settings = {
29
- "ignore_updates": False,
30
- "type": "sensapex",
31
- "debug": False,
32
- "proxy": False,
33
- "proxy_address": "proxy2.virtualbrainlab.org",
34
- "port": 8081,
35
- "pathfinder_port": 8080,
36
- "serial": "no-e-stop",
37
- }
38
-
39
- # Read settings.
40
- if exists(f"{SETTINGS_DIR}\\{SETTINGS_FILENAME}"):
41
- with open(f"{SETTINGS_DIR}\\{SETTINGS_FILENAME}") as settings_file:
42
- settings = load(settings_file)
43
-
44
- self._ignore_updates = BooleanVar(value=settings["ignore_updates"])
45
- self._type = StringVar(value=settings["type"])
46
- self._debug = BooleanVar(value=settings["debug"])
47
- self._proxy = BooleanVar(value=settings["proxy"])
48
- self._proxy_address = StringVar(value=settings["proxy_address"])
49
- self._port = IntVar(value=settings["port"])
50
- self._pathfinder_port = IntVar(value=settings["pathfinder_port"])
51
- self._serial = StringVar(value=settings["serial"])
52
-
53
- def launch(self) -> None:
54
- """Build and launch GUI"""
55
-
56
- # Build and run GUI.
35
+ # Create default options.
36
+ options = EphysLinkOptions()
37
+
38
+ # Read options.
39
+ if exists(OPTIONS_PATH):
40
+ with open(OPTIONS_PATH) as options_file:
41
+ options = EphysLinkOptions(**load(options_file))
42
+
43
+ # Load options into GUI variables.
44
+ self._ignore_updates = BooleanVar(value=options.ignore_updates)
45
+ self._type = StringVar(value=options.type)
46
+ self._debug = BooleanVar(value=options.debug)
47
+ self._use_proxy = BooleanVar(value=options.use_proxy)
48
+ self._proxy_address = StringVar(value=options.proxy_address)
49
+ self._mpm_port = IntVar(value=options.mpm_port)
50
+ self._serial = StringVar(value=options.serial)
51
+
52
+ # Submit flag.
53
+ self._submit = False
54
+
55
+ def get_options(self) -> EphysLinkOptions:
56
+ """Get options from GUI."""
57
+
58
+ # Launch GUI.
57
59
  self._build_gui()
58
60
  self._root.mainloop()
59
61
 
60
- def _build_gui(self):
61
- """Build GUI"""
62
+ # Exit if the user did not submit options.
63
+ if not self._submit:
64
+ exit(1)
65
+
66
+ # Extract options from GUI.
67
+ options = EphysLinkOptions(
68
+ ignore_updates=self._ignore_updates.get(),
69
+ type=self._type.get(),
70
+ debug=self._debug.get(),
71
+ use_proxy=self._use_proxy.get(),
72
+ proxy_address=self._proxy_address.get(),
73
+ mpm_port=self._mpm_port.get(),
74
+ serial=self._serial.get(),
75
+ )
76
+
77
+ # Save options.
78
+ makedirs(OPTIONS_DIR, exist_ok=True)
79
+ with open(OPTIONS_PATH, "w+") as options_file:
80
+ options_file.write(options.model_dump_json())
81
+
82
+ # Return options
83
+ return options
84
+
85
+ def _build_gui(self) -> None:
86
+ """Build GUI."""
62
87
 
63
88
  self._root.title(f"Ephys Link v{version}")
64
89
 
@@ -84,7 +109,7 @@ class GUI:
84
109
  )
85
110
  ttk.Checkbutton(
86
111
  server_serving_settings,
87
- variable=self._proxy,
112
+ variable=self._use_proxy,
88
113
  ).grid(column=1, row=1, sticky="we")
89
114
 
90
115
  # Proxy address.
@@ -95,12 +120,6 @@ class GUI:
95
120
  column=1, row=2, sticky="we"
96
121
  )
97
122
 
98
- # Port.
99
- ttk.Label(server_serving_settings, text="Port:", anchor=E, justify=RIGHT).grid(column=0, row=3, sticky="we")
100
- ttk.Entry(server_serving_settings, textvariable=self._port, width=5, justify=CENTER).grid(
101
- column=1, row=3, sticky="we"
102
- )
103
-
104
123
  # Ignore updates.
105
124
  ttk.Label(server_serving_settings, text="Ignore Updates:", anchor=E, justify=RIGHT).grid(
106
125
  column=0, row=4, sticky="we"
@@ -110,6 +129,15 @@ class GUI:
110
129
  variable=self._ignore_updates,
111
130
  ).grid(column=1, row=4, sticky="we")
112
131
 
132
+ # Debug mode.
133
+ ttk.Label(server_serving_settings, text="Debug mode:", anchor=E, justify=RIGHT).grid(
134
+ column=0, row=5, sticky="we"
135
+ )
136
+ ttk.Checkbutton(
137
+ server_serving_settings,
138
+ variable=self._debug,
139
+ ).grid(column=1, row=5, sticky="we")
140
+
113
141
  # ---
114
142
 
115
143
  # Platform type.
@@ -120,38 +148,44 @@ class GUI:
120
148
  platform_type_settings,
121
149
  text="Sensapex uMp-4",
122
150
  variable=self._type,
123
- value="sensapex",
151
+ value="ump-4",
124
152
  ).grid(column=0, row=0, sticky="we")
125
153
  ttk.Radiobutton(
126
154
  platform_type_settings,
127
155
  text="Sensapex uMp-3",
128
156
  variable=self._type,
129
- value="ump3",
157
+ value="ump-3",
130
158
  ).grid(column=0, row=1, sticky="we")
131
159
  ttk.Radiobutton(
132
160
  platform_type_settings,
133
161
  text="Pathfinder MPM Control v2.8.8+",
134
162
  variable=self._type,
135
- value="new_scale_pathfinder",
163
+ value="pathfinder-mpm",
136
164
  ).grid(column=0, row=2, sticky="we")
137
165
  ttk.Radiobutton(
138
166
  platform_type_settings,
139
167
  text="New Scale M3-USB-3:1-EP",
140
168
  variable=self._type,
141
- value="new_scale",
169
+ value="new-scale",
142
170
  ).grid(column=0, row=3, sticky="we")
171
+ ttk.Radiobutton(
172
+ platform_type_settings,
173
+ text="Fake Platform",
174
+ variable=self._type,
175
+ value="fake",
176
+ ).grid(column=0, row=4, sticky="we")
143
177
 
144
178
  # ---
145
179
 
146
180
  # New Scale Settings.
147
- new_scale_settings = ttk.LabelFrame(mainframe, text="Pathfinder Settings", padding=3)
181
+ new_scale_settings = ttk.LabelFrame(mainframe, text="Pathfinder MPM Settings", padding=3)
148
182
  new_scale_settings.grid(column=0, row=2, sticky="news")
149
183
 
150
184
  # Port
151
185
  ttk.Label(new_scale_settings, text="HTTP Server Port:", anchor=E, justify=RIGHT).grid(
152
186
  column=0, row=1, sticky="we"
153
187
  )
154
- ttk.Entry(new_scale_settings, textvariable=self._pathfinder_port, width=5, justify=CENTER).grid(
188
+ ttk.Entry(new_scale_settings, textvariable=self._mpm_port, width=5, justify=CENTER).grid(
155
189
  column=1, row=1, sticky="we"
156
190
  )
157
191
 
@@ -173,45 +207,9 @@ class GUI:
173
207
  ).grid(column=0, row=4, columnspan=2, sticky="we")
174
208
 
175
209
  def _launch_server(self) -> None:
176
- """Launch server based on GUI settings"""
210
+ """Close GUI and return to the server.
177
211
 
178
- # Close GUI.
212
+ Options are saved in fields.
213
+ """
214
+ self._submit = True
179
215
  self._root.destroy()
180
-
181
- # Save settings.
182
- settings = {
183
- "ignore_updates": self._ignore_updates.get(),
184
- "type": self._type.get(),
185
- "debug": self._debug.get(),
186
- "proxy": self._proxy.get(),
187
- "proxy_address": self._proxy_address.get(),
188
- "port": self._port.get(),
189
- "pathfinder_port": self._pathfinder_port.get(),
190
- "serial": self._serial.get(),
191
- }
192
- makedirs(SETTINGS_DIR, exist_ok=True)
193
- with open(f"{SETTINGS_DIR}\\{SETTINGS_FILENAME}", "w+") as f:
194
- f.write(dumps(settings))
195
-
196
- # Launch server.
197
- server = Server()
198
-
199
- com.DEBUG = self._debug.get()
200
-
201
- if self._serial.get() != "no-e-stop":
202
- e_stop = EmergencyStop(server, self._serial.get())
203
- e_stop.watch()
204
-
205
- # Launch with parsed arguments on main thread.
206
- if self._proxy.get():
207
- run(
208
- server.launch_for_proxy(
209
- self._proxy_address.get(),
210
- self._port.get(),
211
- self._type.get(),
212
- self._pathfinder_port.get(),
213
- self._ignore_updates.get(),
214
- )
215
- )
216
- else:
217
- server.launch(self._type.get(), self._port.get(), self._pathfinder_port.get(), self._ignore_updates.get())
File without changes
@@ -0,0 +1,133 @@
1
+ """Binding methods for Ephys Link manipulator platforms.
2
+
3
+ Definition of the methods a platform binding class must implement to be used by Ephys Link.
4
+
5
+ Usage: Implement the BaseBindings class when defining a platform binding to ensure it supports the necessary methods.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+
10
+ from vbl_aquarium.models.unity import Vector3, Vector4
11
+
12
+
13
+ class BaseBindings(ABC):
14
+ """Base class to enforce bindings manipulator platforms will support.
15
+
16
+ No need to catch exceptions as the Platform Handler will catch them.
17
+ """
18
+
19
+ @abstractmethod
20
+ async def get_manipulators(self) -> list[str]:
21
+ """Get a list of available manipulators on the current platform.
22
+
23
+ :returns: List of manipulator IDs.
24
+ :rtype: list[str]
25
+ """
26
+
27
+ @abstractmethod
28
+ async def get_num_axes(self) -> int:
29
+ """Get the number of axes for the current platform.
30
+
31
+ :returns: Number of axes.
32
+ :rtype: int
33
+ """
34
+
35
+ @abstractmethod
36
+ def get_dimensions(self) -> Vector4:
37
+ """Get the dimensions of the manipulators on the current platform (mm).
38
+
39
+ For 3-axis manipulators, copy the dimension of the axis parallel to the probe into w.
40
+
41
+ :returns: Dimensions of the manipulators.
42
+ :rtype: Vector4
43
+ """
44
+
45
+ @abstractmethod
46
+ async def get_position(self, manipulator_id: str) -> Vector4:
47
+ """Get the current position of a manipulator.
48
+
49
+ These will be the translation values of the manipulator (mm), so they may need to be rotated to unified space.
50
+ For 3-axis manipulators, copy the position of the axis parallel to the probe into w.
51
+
52
+ :param manipulator_id: Manipulator ID.
53
+ :type manipulator_id: str
54
+ :returns: Current position of the manipulator in platform space (mm).
55
+ :rtype: Vector4
56
+ """
57
+
58
+ @abstractmethod
59
+ async def get_angles(self, manipulator_id: str) -> Vector3:
60
+ """Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
61
+
62
+ :param manipulator_id: Manipulator ID.
63
+ :type manipulator_id: str
64
+ :returns: Current angles of the manipulator.
65
+ :rtype: Vector3
66
+ """
67
+
68
+ @abstractmethod
69
+ async def get_shank_count(self, manipulator_id: str) -> int:
70
+ """Get the number of shanks on a manipulator.
71
+
72
+ :param manipulator_id: Manipulator ID.
73
+ :type manipulator_id: str
74
+ :returns: Number of shanks on the manipulator.
75
+ :rtype: int
76
+ """
77
+
78
+ @abstractmethod
79
+ async def get_movement_tolerance(self) -> float:
80
+ """Get the tolerance for how close the final position must be to the target position in a movement (mm).
81
+
82
+ :returns: Movement tolerance (mm).
83
+ :rtype: float
84
+ """
85
+
86
+ @abstractmethod
87
+ async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
88
+ """Set the position of a manipulator.
89
+
90
+ This will directly set the position in the original platform space.
91
+ Unified space coordinates will need to be converted to platform space.
92
+ For 3-axis manipulators, the first 3 values of the position will be used.
93
+
94
+ :param manipulator_id: Manipulator ID.
95
+ :type manipulator_id: str
96
+ :param position: Platform space position to set the manipulator to (mm).
97
+ :type position: Vector4
98
+ :param speed: Speed to move the manipulator to the position (mm/s).
99
+ :type speed: float
100
+ :returns: Final position of the manipulator in platform space (mm).
101
+ :rtype: Vector4
102
+ """
103
+
104
+ @abstractmethod
105
+ async def stop(self, manipulator_id: str) -> None:
106
+ """Stop a manipulator."""
107
+
108
+ @abstractmethod
109
+ def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
110
+ """Convert platform space coordinates to unified space coordinates.
111
+
112
+ This is an axes-swapping transformation.
113
+
114
+ Unified coordinate space is the standard left-handed cartesian coordinate system
115
+ with an additional depth axis pointing from the base of the probe to the tip.
116
+
117
+ :param platform_space: Platform space coordinates.
118
+ :type platform_space: Vector4
119
+ :returns: Unified space coordinates.
120
+ :rtype: Vector4
121
+ """
122
+
123
+ @abstractmethod
124
+ def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
125
+ """Convert unified space coordinates to platform space coordinates.
126
+
127
+ This is an axes-swapping transformation.
128
+
129
+ :param unified_space: Unified space coordinates.
130
+ :type unified_space: Vector4
131
+ :returns: Platform space coordinates.
132
+ :rtype: Vector4
133
+ """