ephys-link 2.0.0b6__py3-none-any.whl → 2.0.0b10__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.
@@ -1,308 +1,315 @@
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
- EphysLinkOptions,
16
- GetManipulatorsResponse,
17
- PositionalResponse,
18
- SetDepthRequest,
19
- SetDepthResponse,
20
- SetInsideBrainRequest,
21
- SetPositionRequest,
22
- ShankCountResponse,
23
- )
24
- from vbl_aquarium.models.proxy import PinpointIdResponse
25
- from vbl_aquarium.models.unity import Vector4
26
-
27
- from ephys_link.__about__ import __version__
28
- from ephys_link.bindings.fake_bindings import FakeBindings
29
- from ephys_link.bindings.mpm_bindings import MPMBinding
30
- from ephys_link.bindings.ump_3_bindings import Ump3Bindings
31
- from ephys_link.bindings.ump_4_bindings import Ump4Bindings
32
- from ephys_link.util.base_bindings import BaseBindings
33
- from ephys_link.util.common import vector4_to_array
34
- from ephys_link.util.console import Console
35
-
36
-
37
- class PlatformHandler:
38
- """Handler for platform commands."""
39
-
40
- def __init__(self, options: EphysLinkOptions, console: Console) -> None:
41
- """Initialize platform handler.
42
-
43
- :param options: CLI options.
44
- :type options: EphysLinkOptions
45
- """
46
-
47
- # Store the CLI options.
48
- self._options = options
49
-
50
- # Store the console.
51
- self._console = console
52
-
53
- # Define bindings based on platform type.
54
- self._bindings = self._match_platform_type(options)
55
-
56
- # Record which IDs are inside the brain.
57
- self._inside_brain: set[str] = set()
58
-
59
- # Generate a Pinpoint ID for proxy usage.
60
- self._pinpoint_id = str(uuid4())[:8]
61
-
62
- def _match_platform_type(self, options: EphysLinkOptions) -> BaseBindings:
63
- """Match the platform type to the appropriate bindings.
64
-
65
- :param options: CLI options.
66
- :type options: EphysLinkOptions
67
- :returns: Bindings for the specified platform type.
68
- :rtype: :class:`ephys_link.util.base_bindings.BaseBindings`
69
- """
70
- match options.type:
71
- case "ump-4":
72
- return Ump4Bindings()
73
- case "ump-3":
74
- return Ump3Bindings()
75
- case "pathfinder-mpm":
76
- return MPMBinding(options.mpm_port)
77
- case "fake":
78
- return FakeBindings()
79
- case _:
80
- error_message = f'Platform type "{options.type}" not recognized.'
81
- self._console.critical_print(error_message)
82
- raise ValueError(error_message)
83
-
84
- # Ephys Link metadata.
85
-
86
- @staticmethod
87
- def get_version() -> str:
88
- """Get Ephys Link's version.
89
-
90
- :returns: Ephys Link's version.
91
- :rtype: str
92
- """
93
- return __version__
94
-
95
- def get_pinpoint_id(self) -> PinpointIdResponse:
96
- """Get the Pinpoint ID for proxy usage.
97
-
98
- :returns: Pinpoint ID response.
99
- :rtype: :class:`vbl_aquarium.models.ephys_link.PinpointIDResponse`
100
- """
101
- return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False)
102
-
103
- def get_platform_type(self) -> str:
104
- """Get the manipulator platform type connected to Ephys Link.
105
-
106
- :returns: Platform type config identifier (see CLI options for examples).
107
- :rtype: str
108
- """
109
- return str(self._options.type)
110
-
111
- # Manipulator commands.
112
-
113
- async def get_manipulators(self) -> GetManipulatorsResponse:
114
- """Get a list of available manipulators on the current handler.
115
-
116
- :returns: List of manipulator IDs, number of axes, dimensions of manipulators (mm), and an error message if any.
117
- :rtype: :class:`vbl_aquarium.models.ephys_link.GetManipulatorsResponse`
118
- """
119
- try:
120
- manipulators = await self._bindings.get_manipulators()
121
- num_axes = await self._bindings.get_axes_count()
122
- dimensions = self._bindings.get_dimensions()
123
- except Exception as e:
124
- self._console.exception_error_print("Get Manipulators", e)
125
- return GetManipulatorsResponse(error=self._console.pretty_exception(e))
126
- else:
127
- return GetManipulatorsResponse(
128
- manipulators=manipulators,
129
- num_axes=num_axes,
130
- dimensions=dimensions,
131
- )
132
-
133
- async def get_position(self, manipulator_id: str) -> PositionalResponse:
134
- """Get the current translation position of a manipulator in unified coordinates (mm).
135
-
136
- :param manipulator_id: Manipulator ID.
137
- :type manipulator_id: str
138
- :returns: Current position of the manipulator and an error message if any.
139
- :rtype: :class:`vbl_aquarium.models.ephys_link.PositionalResponse`
140
- """
141
- try:
142
- unified_position = self._bindings.platform_space_to_unified_space(
143
- await self._bindings.get_position(manipulator_id)
144
- )
145
- except Exception as e:
146
- self._console.exception_error_print("Get Position", e)
147
- return PositionalResponse(error=str(e))
148
- else:
149
- return PositionalResponse(position=unified_position)
150
-
151
- async def get_angles(self, manipulator_id: str) -> AngularResponse:
152
- """Get the current rotation angles of a manipulator in Yaw, Pitch, Roll (degrees).
153
-
154
- :param manipulator_id: Manipulator ID.
155
- :type manipulator_id: str
156
- :returns: Current angles of the manipulator and an error message if any.
157
- :rtype: :class:`vbl_aquarium.models.ephys_link.AngularResponse`
158
- """
159
- try:
160
- angles = await self._bindings.get_angles(manipulator_id)
161
- except Exception as e:
162
- self._console.exception_error_print("Get Angles", e)
163
- return AngularResponse(error=self._console.pretty_exception(e))
164
- else:
165
- return AngularResponse(angles=angles)
166
-
167
- async def get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
168
- """Get the number of shanks on a manipulator.
169
-
170
- :param manipulator_id: Manipulator ID.
171
- :type manipulator_id: str
172
- :returns: Number of shanks on the manipulator and an error message if any.
173
- :rtype: :class:`vbl_aquarium.models.ephys_link.ShankCountResponse`
174
- """
175
- try:
176
- shank_count = await self._bindings.get_shank_count(manipulator_id)
177
- except Exception as e:
178
- self._console.exception_error_print("Get Shank Count", e)
179
- return ShankCountResponse(error=self._console.pretty_exception(e))
180
- else:
181
- return ShankCountResponse(shank_count=shank_count)
182
-
183
- async def set_position(self, request: SetPositionRequest) -> PositionalResponse:
184
- """Move a manipulator to a specified translation position in unified coordinates (mm).
185
-
186
- :param request: Request to move a manipulator to a specified position.
187
- :type request: :class:`vbl_aquarium.models.ephys_link.GotoPositionRequest`
188
- :returns: Final position of the manipulator and an error message if any.
189
- :rtype: :class:`vbl_aquarium.models.ephys_link.Position`
190
- """
191
- try:
192
- # Disallow setting manipulator position while inside the brain.
193
- if request.manipulator_id in self._inside_brain:
194
- error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
195
- self._console.error_print("Set Position", error_message)
196
- return PositionalResponse(error=error_message)
197
-
198
- # Move to the new position.
199
- final_platform_position = await self._bindings.set_position(
200
- manipulator_id=request.manipulator_id,
201
- position=self._bindings.unified_space_to_platform_space(request.position),
202
- speed=request.speed,
203
- )
204
- final_unified_position = self._bindings.platform_space_to_unified_space(final_platform_position)
205
-
206
- # Return error if movement did not reach target within tolerance.
207
- for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
208
- # End once index is the number of axes.
209
- if index == await self._bindings.get_axes_count():
210
- break
211
-
212
- # Check if the axis is within the movement tolerance.
213
- if abs(axis) > self._bindings.get_movement_tolerance():
214
- error_message = (
215
- f"Manipulator {request.manipulator_id} did not reach target"
216
- f" position on axis {list(Vector4.model_fields.keys())[index]}."
217
- f" Requested: {request.position}, got: {final_unified_position}."
218
- )
219
- self._console.error_print("Set Position", error_message)
220
- return PositionalResponse(error=error_message)
221
- except Exception as e:
222
- self._console.exception_error_print("Set Position", e)
223
- return PositionalResponse(error=self._console.pretty_exception(e))
224
- else:
225
- return PositionalResponse(position=final_unified_position)
226
-
227
- async def set_depth(self, request: SetDepthRequest) -> SetDepthResponse:
228
- """Move a manipulator's depth translation stage to a specific value (mm).
229
-
230
- :param request: Request to move a manipulator to a specified depth.
231
- :type request: :class:`vbl_aquarium.models.ephys_link.DriveToDepthRequest`
232
- :returns: Final depth of the manipulator and an error message if any.
233
- :rtype: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse`
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:
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
- :param request: Request to set a manipulator's inside brain state.
264
- :type request: :class:`vbl_aquarium.models.ephys_link.InsideBrainRequest`
265
- :returns: Inside brain state of the manipulator and an error message if any.
266
- :rtype: :class:`vbl_aquarium.models.ephys_link.BooleanStateResponse`
267
- """
268
- try:
269
- if request.inside:
270
- self._inside_brain.add(request.manipulator_id)
271
- else:
272
- self._inside_brain.discard(request.manipulator_id)
273
- except Exception as e:
274
- self._console.exception_error_print("Set Inside Brain", e)
275
- return BooleanStateResponse(error=self._console.pretty_exception(e))
276
- else:
277
- return BooleanStateResponse(state=request.inside)
278
-
279
- async def stop(self, manipulator_id: str) -> str:
280
- """Stop a manipulator.
281
-
282
- :param manipulator_id: Manipulator ID.
283
- :type manipulator_id: str
284
- :returns: Error message if any.
285
- :rtype: str
286
- """
287
- try:
288
- await self._bindings.stop(manipulator_id)
289
- except Exception as e:
290
- self._console.exception_error_print("Stop", e)
291
- return self._console.pretty_exception(e)
292
- else:
293
- return ""
294
-
295
- async def stop_all(self) -> str:
296
- """Stop all manipulators.
297
-
298
- :returns: Error message if any.
299
- :rtype: str
300
- """
301
- try:
302
- for manipulator_id in await self._bindings.get_manipulators():
303
- await self._bindings.stop(manipulator_id)
304
- except Exception as e:
305
- self._console.exception_error_print("Stop", e)
306
- return self._console.pretty_exception(e)
307
- else:
308
- return ""
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()