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
ephys_link/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.3"
1
+ __version__ = "2.0.0b1"
ephys_link/__main__.py CHANGED
@@ -1,104 +1,42 @@
1
- from argparse import ArgumentParser
2
- from asyncio import run
3
- from sys import argv
1
+ """Ephys Link entry point.
2
+
3
+ Responsible for gathering launch options, instantiating the appropriate interface, and starting the application.
4
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
5
+ Usage: call main() to start.
6
+ """
7
+
8
+ from sys import argv
10
9
 
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
- )
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
- """Main function"""
18
+ """Ephys Link entry point.
74
19
 
75
- # Parse arguments.
76
- args = parser.parse_args()
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
- # Launch GUI if there are no CLI arguments.
79
- if len(argv) == 1:
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
- # Otherwise, create Server from CLI.
85
- server = Server()
30
+ # 2. Instantiate the Console and make it globally accessible.
31
+ console = Console(enable_debug=options.debug)
86
32
 
87
- # Continue with CLI if not.
88
- com.DEBUG = args.debug
33
+ # 3. Instantiate the Platform Handler with the appropriate platform bindings.
34
+ platform_handler = PlatformHandler(options.type, console)
89
35
 
90
- # Setup serial port.
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
- # 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)
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