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.
- ephys_link/__about__.py +1 -1
- ephys_link/__main__.py +28 -90
- ephys_link/back_end/__init__.py +0 -0
- ephys_link/back_end/platform_handler.py +298 -0
- ephys_link/back_end/server.py +200 -0
- ephys_link/bindings/__init__.py +0 -0
- ephys_link/bindings/fake_bindings.py +54 -0
- ephys_link/bindings/ump_4_bindings.py +127 -0
- ephys_link/front_end/__init__.py +0 -0
- ephys_link/front_end/cli.py +98 -0
- ephys_link/{gui.py → front_end/gui.py} +93 -95
- ephys_link/util/__init__.py +0 -0
- ephys_link/util/base_bindings.py +133 -0
- ephys_link/util/common.py +121 -0
- ephys_link/util/console.py +112 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/METADATA +6 -4
- ephys_link-2.0.0b1.dist-info/RECORD +25 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/WHEEL +1 -1
- ephys_link/common.py +0 -49
- ephys_link/emergency_stop.py +0 -67
- ephys_link/platform_handler.py +0 -465
- ephys_link/platform_manipulator.py +0 -35
- ephys_link/platforms/__init__.py +0 -5
- ephys_link/platforms/new_scale_handler.py +0 -141
- ephys_link/platforms/new_scale_manipulator.py +0 -312
- ephys_link/platforms/new_scale_pathfinder_handler.py +0 -235
- ephys_link/platforms/sensapex_handler.py +0 -151
- ephys_link/platforms/sensapex_manipulator.py +0 -227
- ephys_link/platforms/ump3_handler.py +0 -57
- ephys_link/platforms/ump3_manipulator.py +0 -147
- ephys_link/server.py +0 -508
- ephys_link-1.3.3.dist-info/RECORD +0 -26
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/entry_points.txt +0 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0b1.dist-info}/licenses/LICENSE +0 -0
ephys_link/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "2.0.0b1"
|
ephys_link/__main__.py
CHANGED
|
@@ -1,104 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""Ephys Link entry point.
|
|
2
|
+
|
|
3
|
+
Responsible for gathering launch options, instantiating the appropriate interface, and starting the application.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
from ephys_link.server import Server
|
|
5
|
+
Usage: call main() to start.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from sys import argv
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
)
|
|
10
|
+
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
11
|
+
from ephys_link.back_end.server import Server
|
|
12
|
+
from ephys_link.front_end.cli import CLI
|
|
13
|
+
from ephys_link.front_end.gui import GUI
|
|
14
|
+
from ephys_link.util.console import Console
|
|
70
15
|
|
|
71
16
|
|
|
72
17
|
def main() -> None:
|
|
73
|
-
"""
|
|
18
|
+
"""Ephys Link entry point.
|
|
74
19
|
|
|
75
|
-
|
|
76
|
-
|
|
20
|
+
1. Get options via CLI or GUI.
|
|
21
|
+
2. Instantiate the Console and make it globally accessible.
|
|
22
|
+
3. Instantiate the Platform Handler with the appropriate platform bindings.
|
|
23
|
+
4. Instantiate the Emergency Stop service.
|
|
24
|
+
5. Start the server.
|
|
25
|
+
"""
|
|
77
26
|
|
|
78
|
-
#
|
|
79
|
-
if len(argv)
|
|
80
|
-
gui = GUI()
|
|
81
|
-
gui.launch()
|
|
82
|
-
return None
|
|
27
|
+
# 1. Get options via CLI or GUI (if no CLI options are provided).
|
|
28
|
+
options = CLI().parse_args() if len(argv) > 1 else GUI().get_options()
|
|
83
29
|
|
|
84
|
-
#
|
|
85
|
-
|
|
30
|
+
# 2. Instantiate the Console and make it globally accessible.
|
|
31
|
+
console = Console(enable_debug=options.debug)
|
|
86
32
|
|
|
87
|
-
#
|
|
88
|
-
|
|
33
|
+
# 3. Instantiate the Platform Handler with the appropriate platform bindings.
|
|
34
|
+
platform_handler = PlatformHandler(options.type, console)
|
|
89
35
|
|
|
90
|
-
#
|
|
91
|
-
if args.serial != "no-e-stop":
|
|
92
|
-
e_stop = EmergencyStop(server, args.serial)
|
|
93
|
-
e_stop.watch()
|
|
36
|
+
# 4. Instantiate the Emergency Stop service.
|
|
94
37
|
|
|
95
|
-
#
|
|
96
|
-
|
|
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)
|
|
38
|
+
# 5. Start the server.
|
|
39
|
+
Server(options, platform_handler, console).launch()
|
|
102
40
|
|
|
103
41
|
|
|
104
42
|
if __name__ == "__main__":
|
|
File without changes
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# ruff: noqa: BLE001
|
|
2
|
+
"""Manipulator platform handler.
|
|
3
|
+
|
|
4
|
+
Responsible for performing the various manipulator commands.
|
|
5
|
+
Instantiates the appropriate bindings based on the platform type and uses them to perform the commands.
|
|
6
|
+
|
|
7
|
+
Usage: Instantiate PlatformHandler with the platform type and call the desired command.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from vbl_aquarium.models.ephys_link import (
|
|
13
|
+
AngularResponse,
|
|
14
|
+
BooleanStateResponse,
|
|
15
|
+
GetManipulatorsResponse,
|
|
16
|
+
PositionalResponse,
|
|
17
|
+
SetDepthRequest,
|
|
18
|
+
SetDepthResponse,
|
|
19
|
+
SetInsideBrainRequest,
|
|
20
|
+
SetPositionRequest,
|
|
21
|
+
ShankCountResponse,
|
|
22
|
+
)
|
|
23
|
+
from vbl_aquarium.models.proxy import PinpointIdResponse
|
|
24
|
+
from vbl_aquarium.models.unity import Vector4
|
|
25
|
+
|
|
26
|
+
from ephys_link.__about__ import __version__
|
|
27
|
+
from ephys_link.bindings.fake_bindings import FakeBindings
|
|
28
|
+
from ephys_link.bindings.ump_4_bindings import Ump4Bindings
|
|
29
|
+
from ephys_link.util.base_bindings import BaseBindings
|
|
30
|
+
from ephys_link.util.common import vector4_to_array
|
|
31
|
+
from ephys_link.util.console import Console
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PlatformHandler:
|
|
35
|
+
"""Handler for platform commands."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, platform_type: str, console: Console) -> None:
|
|
38
|
+
"""Initialize platform handler.
|
|
39
|
+
|
|
40
|
+
:param platform_type: Platform type to initialize bindings from.
|
|
41
|
+
:type platform_type: str
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Store the platform type.
|
|
45
|
+
self._platform_type = platform_type
|
|
46
|
+
|
|
47
|
+
# Store the console.
|
|
48
|
+
self._console = console
|
|
49
|
+
|
|
50
|
+
# Define bindings based on platform type.
|
|
51
|
+
self._bindings = self._match_platform_type(platform_type)
|
|
52
|
+
|
|
53
|
+
# Record which IDs are inside the brain.
|
|
54
|
+
self._inside_brain: set[str] = set()
|
|
55
|
+
|
|
56
|
+
# Generate a Pinpoint ID for proxy usage.
|
|
57
|
+
self._pinpoint_id = str(uuid4())[:8]
|
|
58
|
+
|
|
59
|
+
def _match_platform_type(self, platform_type: str) -> BaseBindings:
|
|
60
|
+
"""Match the platform type to the appropriate bindings.
|
|
61
|
+
|
|
62
|
+
:param platform_type: Platform type.
|
|
63
|
+
:type platform_type: str
|
|
64
|
+
:returns: Bindings for the specified platform type.
|
|
65
|
+
:rtype: :class:`ephys_link.util.base_bindings.BaseBindings`
|
|
66
|
+
"""
|
|
67
|
+
match platform_type:
|
|
68
|
+
case "ump-4":
|
|
69
|
+
return Ump4Bindings()
|
|
70
|
+
case "fake":
|
|
71
|
+
return FakeBindings()
|
|
72
|
+
case _:
|
|
73
|
+
error_message = f'Platform type "{platform_type}" not recognized.'
|
|
74
|
+
self._console.labeled_error_print("PLATFORM", error_message)
|
|
75
|
+
raise ValueError(error_message)
|
|
76
|
+
|
|
77
|
+
# Ephys Link metadata.
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def get_version() -> str:
|
|
81
|
+
"""Get Ephys Link's version.
|
|
82
|
+
|
|
83
|
+
:returns: Ephys Link's version.
|
|
84
|
+
:rtype: str
|
|
85
|
+
"""
|
|
86
|
+
return __version__
|
|
87
|
+
|
|
88
|
+
def get_pinpoint_id(self) -> PinpointIdResponse:
|
|
89
|
+
"""Get the Pinpoint ID for proxy usage.
|
|
90
|
+
|
|
91
|
+
:returns: Pinpoint ID response.
|
|
92
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.PinpointIDResponse`
|
|
93
|
+
"""
|
|
94
|
+
return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False)
|
|
95
|
+
|
|
96
|
+
def get_platform_type(self) -> str:
|
|
97
|
+
"""Get the manipulator platform type connected to Ephys Link.
|
|
98
|
+
|
|
99
|
+
:returns: Platform type config identifier (see CLI options for examples).
|
|
100
|
+
:rtype: str
|
|
101
|
+
"""
|
|
102
|
+
return self._platform_type
|
|
103
|
+
|
|
104
|
+
# Manipulator commands.
|
|
105
|
+
|
|
106
|
+
async def get_manipulators(self) -> GetManipulatorsResponse:
|
|
107
|
+
"""Get a list of available manipulators on the current handler.
|
|
108
|
+
|
|
109
|
+
:returns: List of manipulator IDs, number of axes, dimensions of manipulators (mm), and an error message if any.
|
|
110
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.GetManipulatorsResponse`
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
manipulators = await self._bindings.get_manipulators()
|
|
114
|
+
num_axes = await self._bindings.get_num_axes()
|
|
115
|
+
dimensions = self._bindings.get_dimensions()
|
|
116
|
+
except Exception as e:
|
|
117
|
+
self._console.exception_error_print("Get Manipulators", e)
|
|
118
|
+
return GetManipulatorsResponse(error=self._console.pretty_exception(e))
|
|
119
|
+
else:
|
|
120
|
+
return GetManipulatorsResponse(
|
|
121
|
+
manipulators=manipulators,
|
|
122
|
+
num_axes=num_axes,
|
|
123
|
+
dimensions=dimensions,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def get_position(self, manipulator_id: str) -> PositionalResponse:
|
|
127
|
+
"""Get the current translation position of a manipulator in unified coordinates (mm).
|
|
128
|
+
|
|
129
|
+
:param manipulator_id: Manipulator ID.
|
|
130
|
+
:type manipulator_id: str
|
|
131
|
+
:returns: Current position of the manipulator and an error message if any.
|
|
132
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.PositionalResponse`
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
unified_position = self._bindings.platform_space_to_unified_space(
|
|
136
|
+
await self._bindings.get_position(manipulator_id)
|
|
137
|
+
)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self._console.exception_error_print("Get Position", e)
|
|
140
|
+
return PositionalResponse(error=str(e))
|
|
141
|
+
else:
|
|
142
|
+
return PositionalResponse(position=unified_position)
|
|
143
|
+
|
|
144
|
+
async def get_angles(self, manipulator_id: str) -> AngularResponse:
|
|
145
|
+
"""Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
|
|
146
|
+
|
|
147
|
+
:param manipulator_id: Manipulator ID.
|
|
148
|
+
:type manipulator_id: str
|
|
149
|
+
:returns: Current angles of the manipulator and an error message if any.
|
|
150
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.AngularResponse`
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
angles = await self._bindings.get_angles(manipulator_id)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self._console.exception_error_print("Get Angles", e)
|
|
156
|
+
return AngularResponse(error=self._console.pretty_exception(e))
|
|
157
|
+
else:
|
|
158
|
+
return AngularResponse(angles=angles)
|
|
159
|
+
|
|
160
|
+
async def get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
|
|
161
|
+
"""Get the number of shanks on a manipulator.
|
|
162
|
+
|
|
163
|
+
:param manipulator_id: Manipulator ID.
|
|
164
|
+
:type manipulator_id: str
|
|
165
|
+
:returns: Number of shanks on the manipulator and an error message if any.
|
|
166
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.ShankCountResponse`
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
shank_count = await self._bindings.get_shank_count(manipulator_id)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self._console.exception_error_print("Get Shank Count", e)
|
|
172
|
+
return ShankCountResponse(error=self._console.pretty_exception(e))
|
|
173
|
+
else:
|
|
174
|
+
return ShankCountResponse(shank_count=shank_count)
|
|
175
|
+
|
|
176
|
+
async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
|
|
177
|
+
"""Move a manipulator to a specified translation position in unified coordinates (mm).
|
|
178
|
+
|
|
179
|
+
:param request: Request to move a manipulator to a specified position.
|
|
180
|
+
:type request: :class:`vbl_aquarium.models.ephys_link.GotoPositionRequest`
|
|
181
|
+
:returns: Final position of the manipulator and an error message if any.
|
|
182
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.Position`
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
# Disallow setting manipulator position while inside the brain.
|
|
186
|
+
if request.manipulator_id in self._inside_brain:
|
|
187
|
+
error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
|
|
188
|
+
self._console.error_print(error_message)
|
|
189
|
+
return PositionalResponse(error=error_message)
|
|
190
|
+
|
|
191
|
+
# Move to the new position.
|
|
192
|
+
final_platform_position = await self._bindings.set_position(
|
|
193
|
+
manipulator_id=request.manipulator_id,
|
|
194
|
+
position=self._bindings.unified_space_to_platform_space(request.position),
|
|
195
|
+
speed=request.speed,
|
|
196
|
+
)
|
|
197
|
+
final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
|
|
198
|
+
|
|
199
|
+
# Return error if movement did not reach target within tolerance.
|
|
200
|
+
for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
|
|
201
|
+
# End once index is greater than the number of axes.
|
|
202
|
+
if index >= await self._bindings.get_num_axes():
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
# Check if the axis is within the movement tolerance.
|
|
206
|
+
if abs(axis) > await self._bindings.get_movement_tolerance():
|
|
207
|
+
error_message = (
|
|
208
|
+
f"Manipulator {request.manipulator_id} did not reach target"
|
|
209
|
+
f" position on axis {list(Vector4.model_fields.keys())[index]}."
|
|
210
|
+
f"Requested: {request.position}, got: {final_unified_position}."
|
|
211
|
+
)
|
|
212
|
+
self._console.error_print(error_message)
|
|
213
|
+
return PositionalResponse(error=error_message)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self._console.exception_error_print("Set Position", e)
|
|
216
|
+
return PositionalResponse(error=self._console.pretty_exception(e))
|
|
217
|
+
else:
|
|
218
|
+
return PositionalResponse(position=final_unified_position)
|
|
219
|
+
|
|
220
|
+
async def set_depth(self, request: SetDepthRequest) -> SetDepthResponse:
|
|
221
|
+
"""Move a manipulator's depth translation stage to a specific value (mm).
|
|
222
|
+
|
|
223
|
+
:param request: Request to move a manipulator to a specified depth.
|
|
224
|
+
:type request: :class:`vbl_aquarium.models.ephys_link.DriveToDepthRequest`
|
|
225
|
+
:returns: Final depth of the manipulator and an error message if any.
|
|
226
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse`
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
# Create a position based on the new depth.
|
|
230
|
+
current_platform_position = await self._bindings.get_position(request.manipulator_id)
|
|
231
|
+
current_unified_position = self._bindings.platform_space_to_unified_space(current_platform_position)
|
|
232
|
+
target_unified_position = current_unified_position.model_copy(update={"w": request.depth})
|
|
233
|
+
target_platform_position = self._bindings.unified_space_to_platform_space(target_unified_position)
|
|
234
|
+
|
|
235
|
+
# Move to the new depth.
|
|
236
|
+
final_platform_position = await self._bindings.set_position(
|
|
237
|
+
manipulator_id=request.manipulator_id,
|
|
238
|
+
position=target_platform_position,
|
|
239
|
+
speed=request.speed,
|
|
240
|
+
)
|
|
241
|
+
final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self._console.exception_error_print("Set Depth", e)
|
|
244
|
+
return SetDepthResponse(error=self._console.pretty_exception(e))
|
|
245
|
+
else:
|
|
246
|
+
return SetDepthResponse(depth=final_unified_position.w)
|
|
247
|
+
|
|
248
|
+
async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
|
|
249
|
+
"""Mark a manipulator as inside the brain or not.
|
|
250
|
+
|
|
251
|
+
This should restrict the manipulator's movement to just the depth axis.
|
|
252
|
+
|
|
253
|
+
:param request: Request to set a manipulator's inside brain state.
|
|
254
|
+
:type request: :class:`vbl_aquarium.models.ephys_link.InsideBrainRequest`
|
|
255
|
+
:returns: Inside brain state of the manipulator and an error message if any.
|
|
256
|
+
:rtype: :class:`vbl_aquarium.models.ephys_link.BooleanStateResponse`
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
if request.inside:
|
|
260
|
+
self._inside_brain.add(request.manipulator_id)
|
|
261
|
+
else:
|
|
262
|
+
self._inside_brain.discard(request.manipulator_id)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
self._console.exception_error_print("Set Inside Brain", e)
|
|
265
|
+
return BooleanStateResponse(error=self._console.pretty_exception(e))
|
|
266
|
+
else:
|
|
267
|
+
return BooleanStateResponse(state=request.inside)
|
|
268
|
+
|
|
269
|
+
async def stop(self, manipulator_id: str) -> str:
|
|
270
|
+
"""Stop a manipulator.
|
|
271
|
+
|
|
272
|
+
:param manipulator_id: Manipulator ID.
|
|
273
|
+
:type manipulator_id: str
|
|
274
|
+
:returns: Error message if any.
|
|
275
|
+
:rtype: str
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
await self._bindings.stop(manipulator_id)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
self._console.exception_error_print("Stop", e)
|
|
281
|
+
return self._console.pretty_exception(e)
|
|
282
|
+
else:
|
|
283
|
+
return ""
|
|
284
|
+
|
|
285
|
+
async def stop_all(self) -> str:
|
|
286
|
+
"""Stop all manipulators.
|
|
287
|
+
|
|
288
|
+
:returns: Error message if any.
|
|
289
|
+
:rtype: str
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
for manipulator_id in await self._bindings.get_manipulators():
|
|
293
|
+
await self._bindings.stop(manipulator_id)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
self._console.exception_error_print("Stop", e)
|
|
296
|
+
return self._console.pretty_exception(e)
|
|
297
|
+
else:
|
|
298
|
+
return ""
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from asyncio import get_event_loop, run
|
|
2
|
+
from collections.abc import Callable, Coroutine
|
|
3
|
+
from json import JSONDecodeError, dumps, loads
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aiohttp.web import Application, run_app
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
from socketio import AsyncClient, AsyncServer
|
|
9
|
+
from vbl_aquarium.models.ephys_link import (
|
|
10
|
+
EphysLinkOptions,
|
|
11
|
+
SetDepthRequest,
|
|
12
|
+
SetInsideBrainRequest,
|
|
13
|
+
SetPositionRequest,
|
|
14
|
+
)
|
|
15
|
+
from vbl_aquarium.models.generic import VBLBaseModel
|
|
16
|
+
|
|
17
|
+
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
18
|
+
from ephys_link.util.common import PORT, check_for_updates, server_preamble
|
|
19
|
+
from ephys_link.util.console import Console
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Server:
|
|
23
|
+
def __init__(self, options: EphysLinkOptions, platform_handler: PlatformHandler, console: Console) -> None:
|
|
24
|
+
"""Initialize server fields based on options and platform handler."""
|
|
25
|
+
|
|
26
|
+
# Save fields.
|
|
27
|
+
self._options = options
|
|
28
|
+
self._platform_handler = platform_handler
|
|
29
|
+
self._console = console
|
|
30
|
+
|
|
31
|
+
# Initialize based on proxy usage.
|
|
32
|
+
self._sio: AsyncServer | AsyncClient = AsyncClient() if self._options.use_proxy else AsyncServer()
|
|
33
|
+
if not self._options.use_proxy:
|
|
34
|
+
self._app = Application()
|
|
35
|
+
self._sio.attach(self._app)
|
|
36
|
+
|
|
37
|
+
# Bind connection events.
|
|
38
|
+
self._sio.on("connect", self.connect)
|
|
39
|
+
self._sio.on("disconnect", self.disconnect)
|
|
40
|
+
|
|
41
|
+
# Store connected client.
|
|
42
|
+
self._client_sid: str = ""
|
|
43
|
+
|
|
44
|
+
# Bind events.
|
|
45
|
+
self._sio.on("*", self.platform_event_handler)
|
|
46
|
+
|
|
47
|
+
# Server launch.
|
|
48
|
+
def launch(self) -> None:
|
|
49
|
+
# Preamble.
|
|
50
|
+
server_preamble()
|
|
51
|
+
|
|
52
|
+
# Check for updates.
|
|
53
|
+
check_for_updates()
|
|
54
|
+
|
|
55
|
+
# List platform and available manipulators.
|
|
56
|
+
self._console.info_print("PLATFORM", self._platform_handler.get_platform_type())
|
|
57
|
+
self._console.info_print(
|
|
58
|
+
"MANIPULATORS",
|
|
59
|
+
str(get_event_loop().run_until_complete(self._platform_handler.get_manipulators()).manipulators),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Launch server
|
|
63
|
+
if self._options.use_proxy:
|
|
64
|
+
self._console.info_print("PINPOINT ID", self._platform_handler.get_pinpoint_id().pinpoint_id)
|
|
65
|
+
|
|
66
|
+
async def connect_proxy() -> None:
|
|
67
|
+
# noinspection HttpUrlsUsage
|
|
68
|
+
await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}")
|
|
69
|
+
await self._sio.wait()
|
|
70
|
+
|
|
71
|
+
run(connect_proxy())
|
|
72
|
+
else:
|
|
73
|
+
run_app(self._app, port=PORT)
|
|
74
|
+
|
|
75
|
+
# Helper functions.
|
|
76
|
+
def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str:
|
|
77
|
+
"""Return a response for a malformed request."""
|
|
78
|
+
self._console.labeled_error_print("MALFORMED REQUEST", f"{request}: {data}")
|
|
79
|
+
return dumps({"error": "Malformed request."})
|
|
80
|
+
|
|
81
|
+
async def _run_if_data_available(
|
|
82
|
+
self, function: Callable[[str], Coroutine[Any, Any, VBLBaseModel]], event: str, data: tuple[tuple[Any], ...]
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Run a function if data is available."""
|
|
85
|
+
request_data = data[1]
|
|
86
|
+
if request_data:
|
|
87
|
+
return str((await function(str(request_data))).to_json_string())
|
|
88
|
+
return self._malformed_request_response(event, request_data)
|
|
89
|
+
|
|
90
|
+
async def _run_if_data_parses(
|
|
91
|
+
self,
|
|
92
|
+
function: Callable[[VBLBaseModel], Coroutine[Any, Any, VBLBaseModel]],
|
|
93
|
+
data_type: type[VBLBaseModel],
|
|
94
|
+
event: str,
|
|
95
|
+
data: tuple[tuple[Any], ...],
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Run a function if data parses."""
|
|
98
|
+
request_data = data[1]
|
|
99
|
+
if request_data:
|
|
100
|
+
try:
|
|
101
|
+
parsed_data = data_type(**loads(str(request_data)))
|
|
102
|
+
except JSONDecodeError:
|
|
103
|
+
return self._malformed_request_response(event, request_data)
|
|
104
|
+
except ValidationError as e:
|
|
105
|
+
self._console.exception_error_print(event, e)
|
|
106
|
+
return self._malformed_request_response(event, request_data)
|
|
107
|
+
else:
|
|
108
|
+
return str((await function(parsed_data)).to_json_string())
|
|
109
|
+
return self._malformed_request_response(event, request_data)
|
|
110
|
+
|
|
111
|
+
# Event Handlers.
|
|
112
|
+
|
|
113
|
+
async def connect(self, sid: str, _: str) -> bool:
|
|
114
|
+
"""Handle connections to the server
|
|
115
|
+
|
|
116
|
+
:param sid: Socket session ID.
|
|
117
|
+
:type sid: str
|
|
118
|
+
:param _: Extra connection data (unused).
|
|
119
|
+
:type _: str
|
|
120
|
+
:returns: False on error to refuse connection, True otherwise.
|
|
121
|
+
:rtype: bool
|
|
122
|
+
"""
|
|
123
|
+
self._console.info_print("CONNECTION REQUEST", sid)
|
|
124
|
+
|
|
125
|
+
if self._client_sid == "":
|
|
126
|
+
self._client_sid = sid
|
|
127
|
+
self._console.info_print("CONNECTION GRANTED", sid)
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
self._console.error_print(f"CONNECTION REFUSED to {sid}. Client {self._client_sid} already connected.")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
async def disconnect(self, sid: str) -> None:
|
|
134
|
+
"""Handle disconnections from the server
|
|
135
|
+
|
|
136
|
+
:param sid: Socket session ID.
|
|
137
|
+
:type sid: str
|
|
138
|
+
"""
|
|
139
|
+
self._console.info_print("DISCONNECTED", sid)
|
|
140
|
+
|
|
141
|
+
# Reset client SID if it matches.
|
|
142
|
+
if self._client_sid == sid:
|
|
143
|
+
self._client_sid = ""
|
|
144
|
+
else:
|
|
145
|
+
self._console.error_print(f"Client {sid} disconnected without being connected.")
|
|
146
|
+
|
|
147
|
+
# noinspection PyTypeChecker
|
|
148
|
+
async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
|
|
149
|
+
"""Handle events from the server
|
|
150
|
+
|
|
151
|
+
:param event: Event name.
|
|
152
|
+
:type event: str
|
|
153
|
+
:param args: Event arguments.
|
|
154
|
+
:type args: tuple[Any]
|
|
155
|
+
:returns: Response data.
|
|
156
|
+
:rtype: str
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
# Log event.
|
|
160
|
+
self._console.debug_print("EVENT", event)
|
|
161
|
+
|
|
162
|
+
# Handle event.
|
|
163
|
+
match event:
|
|
164
|
+
# Server metadata.
|
|
165
|
+
case "get_version":
|
|
166
|
+
return self._platform_handler.get_version()
|
|
167
|
+
case "get_pinpoint_id":
|
|
168
|
+
return str(self._platform_handler.get_pinpoint_id().to_json_string())
|
|
169
|
+
case "get_platform_type":
|
|
170
|
+
return self._platform_handler.get_platform_type()
|
|
171
|
+
|
|
172
|
+
# Manipulator commands.
|
|
173
|
+
case "get_manipulators":
|
|
174
|
+
return str((await self._platform_handler.get_manipulators()).to_json_string())
|
|
175
|
+
case "get_position":
|
|
176
|
+
return await self._run_if_data_available(self._platform_handler.get_position, event, args)
|
|
177
|
+
case "get_angles":
|
|
178
|
+
return await self._run_if_data_available(self._platform_handler.get_angles, event, args)
|
|
179
|
+
case "get_shank_count":
|
|
180
|
+
return await self._run_if_data_available(self._platform_handler.get_shank_count, event, args)
|
|
181
|
+
case "set_position":
|
|
182
|
+
return await self._run_if_data_parses(
|
|
183
|
+
self._platform_handler.set_position, SetPositionRequest, event, args
|
|
184
|
+
)
|
|
185
|
+
case "set_depth":
|
|
186
|
+
return await self._run_if_data_parses(self._platform_handler.set_depth, SetDepthRequest, event, args)
|
|
187
|
+
case "set_inside_brain":
|
|
188
|
+
return await self._run_if_data_parses(
|
|
189
|
+
self._platform_handler.set_inside_brain, SetInsideBrainRequest, event, args
|
|
190
|
+
)
|
|
191
|
+
case "stop":
|
|
192
|
+
request_data = args[1]
|
|
193
|
+
if request_data:
|
|
194
|
+
return await self._platform_handler.stop(str(request_data))
|
|
195
|
+
return self._malformed_request_response(event, request_data)
|
|
196
|
+
case "stop_all":
|
|
197
|
+
return await self._platform_handler.stop_all()
|
|
198
|
+
case _:
|
|
199
|
+
self._console.error_print(f"Unknown event: {event}.")
|
|
200
|
+
return dumps({"error": "Unknown event."})
|
|
File without changes
|