ephys-link 2.0.2__tar.gz → 2.1.0__tar.gz
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-2.0.2 → ephys_link-2.1.0}/PKG-INFO +8 -7
- {ephys_link-2.0.2 → ephys_link-2.1.0}/README.md +1 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/pyproject.toml +27 -10
- {ephys_link-2.0.2 → ephys_link-2.1.0}/scripts/move_tester.py +4 -3
- ephys_link-2.1.0/src/ephys_link/__about__.py +1 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/__main__.py +9 -6
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/back_end/platform_handler.py +25 -72
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/back_end/server.py +42 -37
- ephys_link-2.1.0/src/ephys_link/bindings/ump_binding.py +235 -0
- {ephys_link-2.0.2/src/ephys_link/utils → ephys_link-2.1.0/src/ephys_link/front_end}/console.py +1 -1
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/front_end/gui.py +10 -2
- ephys_link-2.1.0/src/ephys_link/utils/constants.py +106 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/utils/startup.py +46 -9
- ephys_link-2.1.0/tests/back_end/__init__.py +0 -0
- ephys_link-2.1.0/tests/back_end/test_platform_handler.py +592 -0
- ephys_link-2.1.0/tests/back_end/test_server.py +640 -0
- ephys_link-2.1.0/tests/conftest.py +10 -0
- ephys_link-2.1.0/tests/utils/__init__.py +0 -0
- ephys_link-2.1.0/tests/utils/test_converters.py +136 -0
- ephys_link-2.1.0/tests/utils/test_startup.py +137 -0
- ephys_link-2.0.2/src/ephys_link/__about__.py +0 -1
- ephys_link-2.0.2/src/ephys_link/bindings/ump_4_binding.py +0 -158
- ephys_link-2.0.2/src/ephys_link/utils/constants.py +0 -23
- {ephys_link-2.0.2 → ephys_link-2.1.0}/.gitignore +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/LICENSE +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/ephys_link.spec +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/mkdocs.yml +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/scripts/__init__.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/scripts/gen_ref_pages.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/scripts/logger_test.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/scripts/server_tester.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/__init__.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/back_end/__init__.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/bindings/__init__.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/bindings/fake_binding.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/bindings/mpm_binding.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/front_end/__init__.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/front_end/cli.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/resources/libum.dll +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/utils/__init__.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/utils/base_binding.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/src/ephys_link/utils/converters.py +0 -0
- {ephys_link-2.0.2 → ephys_link-2.1.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ephys-link
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments.
|
|
5
5
|
Project-URL: Documentation, https://virtualbrainlab.org/ephys_link/installation_and_use.html
|
|
6
6
|
Project-URL: Issues, https://github.com/VirtualBrainLab/ephys-link/issues
|
|
@@ -22,16 +22,16 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
22
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
23
|
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
24
24
|
Requires-Python: >=3.13
|
|
25
|
-
Requires-Dist: aiohttp==3.
|
|
25
|
+
Requires-Dist: aiohttp==3.12.13
|
|
26
26
|
Requires-Dist: colorama==0.4.6
|
|
27
27
|
Requires-Dist: keyboard==0.13.5
|
|
28
|
-
Requires-Dist: packaging==
|
|
29
|
-
Requires-Dist: platformdirs==4.3.
|
|
28
|
+
Requires-Dist: packaging==25.0
|
|
29
|
+
Requires-Dist: platformdirs==4.3.8
|
|
30
30
|
Requires-Dist: pyserial==3.5
|
|
31
|
-
Requires-Dist: python-socketio[asyncio-client]==5.
|
|
32
|
-
Requires-Dist: requests==2.32.
|
|
31
|
+
Requires-Dist: python-socketio[asyncio-client]==5.13.0
|
|
32
|
+
Requires-Dist: requests==2.32.4
|
|
33
33
|
Requires-Dist: rich==14.0.0
|
|
34
|
-
Requires-Dist: sensapex==1.400.
|
|
34
|
+
Requires-Dist: sensapex==1.400.4
|
|
35
35
|
Requires-Dist: vbl-aquarium==1.0.0
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
|
|
@@ -43,6 +43,7 @@ Description-Content-Type: text/markdown
|
|
|
43
43
|
[](https://github.com/astral-sh/ruff)
|
|
44
44
|
[](https://pydantic.dev)
|
|
45
45
|
[](https://microsoft.github.io/pyright/)
|
|
46
|
+
[](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/test.yml)
|
|
46
47
|
|
|
47
48
|
<!-- [](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/build.yml) -->
|
|
48
49
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://github.com/astral-sh/ruff)
|
|
7
7
|
[](https://pydantic.dev)
|
|
8
8
|
[](https://microsoft.github.io/pyright/)
|
|
9
|
+
[](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/test.yml)
|
|
9
10
|
|
|
10
11
|
<!-- [](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/build.yml) -->
|
|
11
12
|
|
|
@@ -26,15 +26,15 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"aiohttp==3.
|
|
29
|
+
"aiohttp==3.12.13",
|
|
30
30
|
"colorama==0.4.6",
|
|
31
31
|
"keyboard==0.13.5",
|
|
32
|
-
"packaging==
|
|
33
|
-
"platformdirs==4.3.
|
|
32
|
+
"packaging==25.0",
|
|
33
|
+
"platformdirs==4.3.8",
|
|
34
34
|
"pyserial==3.5",
|
|
35
|
-
"python-socketio[asyncio_client]==5.
|
|
36
|
-
"requests==2.32.
|
|
37
|
-
"sensapex==1.400.
|
|
35
|
+
"python-socketio[asyncio_client]==5.13.0",
|
|
36
|
+
"requests==2.32.4",
|
|
37
|
+
"sensapex==1.400.4",
|
|
38
38
|
"rich==14.0.0",
|
|
39
39
|
"vbl-aquarium==1.0.0"
|
|
40
40
|
]
|
|
@@ -59,14 +59,20 @@ exclude = ["/.github", "/.idea", "/docs"]
|
|
|
59
59
|
installer = "uv"
|
|
60
60
|
python = "3.13"
|
|
61
61
|
dependencies = [
|
|
62
|
-
"pyinstaller==6.
|
|
63
|
-
"basedpyright==1.
|
|
62
|
+
"pyinstaller==6.13.0",
|
|
63
|
+
"basedpyright==1.29.1",
|
|
64
|
+
"pytest==8.3.5",
|
|
65
|
+
"pytest-cov==6.1.1",
|
|
66
|
+
"pytest-mock==3.14.0",
|
|
67
|
+
"pytest-asyncio==0.26.0"
|
|
64
68
|
]
|
|
65
69
|
[tool.hatch.envs.default.scripts]
|
|
66
70
|
exe = "pyinstaller.exe ephys_link.spec -y -- -d && pyinstaller.exe ephys_link.spec -y"
|
|
67
71
|
exe-clean = "pyinstaller.exe ephys_link.spec -y --clean"
|
|
68
72
|
check = "basedpyright"
|
|
69
73
|
check-watched = "basedpyright --watch"
|
|
74
|
+
tests = "pytest"
|
|
75
|
+
cov = "pytest --cov=ephys_link --cov-report=html --cov-report=term-missing"
|
|
70
76
|
|
|
71
77
|
[tool.hatch.envs.docs]
|
|
72
78
|
installer = "uv"
|
|
@@ -88,5 +94,16 @@ exclude = ["typings"]
|
|
|
88
94
|
unsafe-fixes = true
|
|
89
95
|
|
|
90
96
|
[tool.basedpyright]
|
|
91
|
-
include = ["src/ephys_link"]
|
|
92
|
-
strict = ["src/ephys_link"]
|
|
97
|
+
include = ["src/ephys_link", "tests"]
|
|
98
|
+
strict = ["src/ephys_link", "tests"]
|
|
99
|
+
|
|
100
|
+
[tool.pytest.ini_options]
|
|
101
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
102
|
+
|
|
103
|
+
[tool.coverage.run]
|
|
104
|
+
source_pkgs = ["ephys_link"]
|
|
105
|
+
branch = true
|
|
106
|
+
omit = [
|
|
107
|
+
"tests/*",
|
|
108
|
+
"scripts/*",
|
|
109
|
+
]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from asyncio import run
|
|
2
2
|
|
|
3
|
-
from vbl_aquarium.models.ephys_link import
|
|
3
|
+
from vbl_aquarium.models.ephys_link import SetDepthRequest
|
|
4
4
|
from vbl_aquarium.models.unity import Vector4
|
|
5
5
|
|
|
6
6
|
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
7
|
-
from ephys_link.
|
|
7
|
+
from ephys_link.bindings.mpm_binding import MPMBinding
|
|
8
|
+
from ephys_link.front_end.console import Console
|
|
8
9
|
|
|
9
10
|
c = Console(enable_debug=True)
|
|
10
|
-
p = PlatformHandler(
|
|
11
|
+
p = PlatformHandler(MPMBinding(), c)
|
|
11
12
|
# target = Vector4()
|
|
12
13
|
target = Vector4(x=7.5, y=7.5, z=7.5, w=7.5)
|
|
13
14
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.0"
|
|
@@ -16,9 +16,9 @@ from keyboard import add_hotkey
|
|
|
16
16
|
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
17
17
|
from ephys_link.back_end.server import Server
|
|
18
18
|
from ephys_link.front_end.cli import CLI
|
|
19
|
+
from ephys_link.front_end.console import Console
|
|
19
20
|
from ephys_link.front_end.gui import GUI
|
|
20
|
-
from ephys_link.utils.
|
|
21
|
-
from ephys_link.utils.startup import check_for_updates, preamble
|
|
21
|
+
from ephys_link.utils.startup import check_for_updates, get_binding_instance, preamble
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def main() -> None:
|
|
@@ -37,13 +37,16 @@ def main() -> None:
|
|
|
37
37
|
if not options.ignore_updates:
|
|
38
38
|
check_for_updates(console)
|
|
39
39
|
|
|
40
|
-
# 4. Instantiate the
|
|
41
|
-
|
|
40
|
+
# 4. Instantiate the requested platform binding.
|
|
41
|
+
binding = get_binding_instance(options, console)
|
|
42
42
|
|
|
43
|
-
# 5.
|
|
43
|
+
# 5. Instantiate the Platform Handler with the appropriate platform bindings.
|
|
44
|
+
platform_handler = PlatformHandler(binding, console)
|
|
45
|
+
|
|
46
|
+
# 6. Add hotkeys for emergency stop.
|
|
44
47
|
_ = add_hotkey("ctrl+alt+shift+q", lambda: run(platform_handler.emergency_stop()))
|
|
45
48
|
|
|
46
|
-
#
|
|
49
|
+
# 7. Start the server.
|
|
47
50
|
Server(options, platform_handler, console).launch()
|
|
48
51
|
|
|
49
52
|
|
|
@@ -8,12 +8,10 @@ Usage:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from typing import final
|
|
11
|
-
from uuid import uuid4
|
|
12
11
|
|
|
13
12
|
from vbl_aquarium.models.ephys_link import (
|
|
14
13
|
AngularResponse,
|
|
15
14
|
BooleanStateResponse,
|
|
16
|
-
EphysLinkOptions,
|
|
17
15
|
GetManipulatorsResponse,
|
|
18
16
|
PlatformInfo,
|
|
19
17
|
PositionalResponse,
|
|
@@ -23,69 +21,38 @@ from vbl_aquarium.models.ephys_link import (
|
|
|
23
21
|
SetPositionRequest,
|
|
24
22
|
ShankCountResponse,
|
|
25
23
|
)
|
|
26
|
-
from vbl_aquarium.models.unity import Vector4
|
|
27
24
|
|
|
28
|
-
from ephys_link.
|
|
25
|
+
from ephys_link.front_end.console import Console
|
|
29
26
|
from ephys_link.utils.base_binding import BaseBinding
|
|
30
|
-
from ephys_link.utils.
|
|
27
|
+
from ephys_link.utils.constants import (
|
|
28
|
+
EMERGENCY_STOP_MESSAGE,
|
|
29
|
+
NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR,
|
|
30
|
+
did_not_reach_target_depth_error,
|
|
31
|
+
did_not_reach_target_position_error,
|
|
32
|
+
)
|
|
31
33
|
from ephys_link.utils.converters import vector4_to_array
|
|
32
|
-
from ephys_link.utils.startup import get_bindings
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
@final
|
|
36
37
|
class PlatformHandler:
|
|
37
38
|
"""Handler for platform commands."""
|
|
38
39
|
|
|
39
|
-
def __init__(self,
|
|
40
|
+
def __init__(self, binding: BaseBinding, console: Console) -> None:
|
|
40
41
|
"""Initialize platform handler.
|
|
41
42
|
|
|
42
43
|
Args:
|
|
43
|
-
|
|
44
|
+
binding: Binding instance for the platform.
|
|
44
45
|
console: Console instance.
|
|
45
46
|
"""
|
|
46
|
-
# Store the CLI options.
|
|
47
|
-
self._options = options
|
|
48
|
-
|
|
49
47
|
# Store the console.
|
|
50
48
|
self._console = console
|
|
51
49
|
|
|
52
50
|
# Define bindings based on platform type.
|
|
53
|
-
self._bindings =
|
|
51
|
+
self._bindings = binding
|
|
54
52
|
|
|
55
53
|
# Record which IDs are inside the brain.
|
|
56
54
|
self._inside_brain: set[str] = set()
|
|
57
55
|
|
|
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
56
|
# Platform metadata.
|
|
90
57
|
|
|
91
58
|
def get_display_name(self) -> str:
|
|
@@ -140,7 +107,7 @@ class PlatformHandler:
|
|
|
140
107
|
)
|
|
141
108
|
except Exception as e: # noqa: BLE001
|
|
142
109
|
self._console.exception_error_print("Get Position", e)
|
|
143
|
-
return PositionalResponse(error=
|
|
110
|
+
return PositionalResponse(error=self._console.pretty_exception(e))
|
|
144
111
|
else:
|
|
145
112
|
return PositionalResponse(position=unified_position)
|
|
146
113
|
|
|
@@ -190,9 +157,8 @@ class PlatformHandler:
|
|
|
190
157
|
try:
|
|
191
158
|
# Disallow setting manipulator position while inside the brain.
|
|
192
159
|
if request.manipulator_id in self._inside_brain:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return PositionalResponse(error=error_message)
|
|
160
|
+
self._console.error_print("Set Position", NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR)
|
|
161
|
+
return PositionalResponse(error=NO_SET_POSITION_WHILE_INSIDE_BRAIN_ERROR)
|
|
196
162
|
|
|
197
163
|
# Move to the new position.
|
|
198
164
|
final_platform_position = await self._bindings.set_position(
|
|
@@ -210,11 +176,7 @@ class PlatformHandler:
|
|
|
210
176
|
|
|
211
177
|
# Check if the axis is within the movement tolerance.
|
|
212
178
|
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
|
-
)
|
|
179
|
+
error_message = did_not_reach_target_position_error(request, index, final_unified_position)
|
|
218
180
|
self._console.error_print("Set Position", error_message)
|
|
219
181
|
return PositionalResponse(error=error_message)
|
|
220
182
|
except Exception as e: # noqa: BLE001
|
|
@@ -234,26 +196,22 @@ class PlatformHandler:
|
|
|
234
196
|
"""
|
|
235
197
|
try:
|
|
236
198
|
# Move to the new depth.
|
|
237
|
-
|
|
199
|
+
final_depth = await self._bindings.set_depth(
|
|
238
200
|
manipulator_id=request.manipulator_id,
|
|
239
|
-
depth=
|
|
201
|
+
depth=request.depth,
|
|
240
202
|
speed=request.speed,
|
|
241
203
|
)
|
|
242
|
-
final_unified_depth = self._bindings.platform_space_to_unified_space(Vector4(w=final_platform_depth)).w
|
|
243
204
|
|
|
244
205
|
# Return error if movement did not reach target within tolerance.
|
|
245
|
-
if abs(
|
|
246
|
-
error_message = (
|
|
247
|
-
f"Manipulator {request.manipulator_id} did not reach target depth."
|
|
248
|
-
f" Requested: {request.depth}, got: {final_unified_depth}."
|
|
249
|
-
)
|
|
206
|
+
if abs(final_depth - request.depth) > self._bindings.get_movement_tolerance():
|
|
207
|
+
error_message = did_not_reach_target_depth_error(request, final_depth)
|
|
250
208
|
self._console.error_print("Set Depth", error_message)
|
|
251
209
|
return SetDepthResponse(error=error_message)
|
|
252
210
|
except Exception as e: # noqa: BLE001
|
|
253
211
|
self._console.exception_error_print("Set Depth", e)
|
|
254
212
|
return SetDepthResponse(error=self._console.pretty_exception(e))
|
|
255
213
|
else:
|
|
256
|
-
return SetDepthResponse(depth=
|
|
214
|
+
return SetDepthResponse(depth=final_depth)
|
|
257
215
|
|
|
258
216
|
async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
|
|
259
217
|
"""Mark a manipulator as inside the brain or not.
|
|
@@ -266,16 +224,11 @@ class PlatformHandler:
|
|
|
266
224
|
Returns:
|
|
267
225
|
Inside brain state of the manipulator and an error message if any.
|
|
268
226
|
"""
|
|
269
|
-
|
|
270
|
-
|
|
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))
|
|
227
|
+
if request.inside:
|
|
228
|
+
self._inside_brain.add(request.manipulator_id)
|
|
277
229
|
else:
|
|
278
|
-
|
|
230
|
+
self._inside_brain.discard(request.manipulator_id)
|
|
231
|
+
return BooleanStateResponse(state=request.inside)
|
|
279
232
|
|
|
280
233
|
async def stop(self, manipulator_id: str) -> str:
|
|
281
234
|
"""Stop a manipulator.
|
|
@@ -304,12 +257,12 @@ class PlatformHandler:
|
|
|
304
257
|
for manipulator_id in await self._bindings.get_manipulators():
|
|
305
258
|
await self._bindings.stop(manipulator_id)
|
|
306
259
|
except Exception as e: # noqa: BLE001
|
|
307
|
-
self._console.exception_error_print("Stop", e)
|
|
260
|
+
self._console.exception_error_print("Stop All", e)
|
|
308
261
|
return self._console.pretty_exception(e)
|
|
309
262
|
else:
|
|
310
263
|
return ""
|
|
311
264
|
|
|
312
265
|
async def emergency_stop(self) -> None:
|
|
313
266
|
"""Stops all manipulators with a message."""
|
|
314
|
-
self._console.critical_print(
|
|
267
|
+
self._console.critical_print(EMERGENCY_STOP_MESSAGE)
|
|
315
268
|
_ = await self.stop_all()
|
|
@@ -32,8 +32,16 @@ from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
|
|
|
32
32
|
|
|
33
33
|
from ephys_link.__about__ import __version__
|
|
34
34
|
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
35
|
-
from ephys_link.
|
|
36
|
-
from ephys_link.utils.constants import
|
|
35
|
+
from ephys_link.front_end.console import Console
|
|
36
|
+
from ephys_link.utils.constants import (
|
|
37
|
+
MALFORMED_REQUEST_ERROR,
|
|
38
|
+
PORT,
|
|
39
|
+
PROXY_CLIENT_NOT_INITIALIZED_ERROR,
|
|
40
|
+
SERVER_NOT_INITIALIZED_ERROR,
|
|
41
|
+
UNKNOWN_EVENT_ERROR,
|
|
42
|
+
cannot_connect_as_client_is_already_connected_error,
|
|
43
|
+
client_disconnected_without_being_connected_error,
|
|
44
|
+
)
|
|
37
45
|
|
|
38
46
|
# Server message generic types.
|
|
39
47
|
INPUT_TYPE = TypeVar("INPUT_TYPE", bound=VBLBaseModel)
|
|
@@ -61,9 +69,8 @@ class Server:
|
|
|
61
69
|
if not self._options.use_proxy:
|
|
62
70
|
# Exit if _sio is not a Server.
|
|
63
71
|
if not isinstance(self._sio, AsyncServer):
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
raise TypeError(error)
|
|
72
|
+
self._console.critical_print(SERVER_NOT_INITIALIZED_ERROR)
|
|
73
|
+
raise TypeError(SERVER_NOT_INITIALIZED_ERROR)
|
|
67
74
|
|
|
68
75
|
self._app = Application()
|
|
69
76
|
self._sio.attach(self._app) # pyright: ignore [reportUnknownMemberType]
|
|
@@ -101,9 +108,8 @@ class Server:
|
|
|
101
108
|
async def connect_proxy() -> None:
|
|
102
109
|
# Exit if _sio is not a proxy client.
|
|
103
110
|
if not isinstance(self._sio, AsyncClient):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
raise TypeError(error)
|
|
111
|
+
self._console.critical_print(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
|
|
112
|
+
raise TypeError(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
|
|
107
113
|
|
|
108
114
|
# noinspection HttpUrlsUsage
|
|
109
115
|
await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}") # pyright: ignore [reportUnknownMemberType]
|
|
@@ -125,13 +131,13 @@ class Server:
|
|
|
125
131
|
Response for a malformed request.
|
|
126
132
|
"""
|
|
127
133
|
self._console.error_print("MALFORMED REQUEST", f"{request}: {data}")
|
|
128
|
-
return dumps(
|
|
134
|
+
return dumps(MALFORMED_REQUEST_ERROR)
|
|
129
135
|
|
|
130
136
|
async def _run_if_data_available(
|
|
131
137
|
self,
|
|
132
138
|
function: Callable[[str], Coroutine[Any, Any, VBLBaseModel]], # pyright: ignore [reportExplicitAny]
|
|
133
139
|
event: str,
|
|
134
|
-
data:
|
|
140
|
+
data: Any, # pyright: ignore [reportAny, reportExplicitAny]
|
|
135
141
|
) -> str:
|
|
136
142
|
"""Run a function if data is available.
|
|
137
143
|
|
|
@@ -143,17 +149,16 @@ class Server:
|
|
|
143
149
|
Returns:
|
|
144
150
|
Response data from function.
|
|
145
151
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return self._malformed_request_response(event, request_data)
|
|
152
|
+
if data:
|
|
153
|
+
return str((await function(str(data))).to_json_string()) # pyright: ignore[reportAny]
|
|
154
|
+
return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
|
|
150
155
|
|
|
151
156
|
async def _run_if_data_parses(
|
|
152
157
|
self,
|
|
153
158
|
function: Callable[[INPUT_TYPE], Coroutine[Any, Any, OUTPUT_TYPE]], # pyright: ignore [reportExplicitAny]
|
|
154
159
|
data_type: type[INPUT_TYPE],
|
|
155
160
|
event: str,
|
|
156
|
-
data:
|
|
161
|
+
data: Any, # pyright: ignore [reportAny, reportExplicitAny]
|
|
157
162
|
) -> str:
|
|
158
163
|
"""Run a function if data parses.
|
|
159
164
|
|
|
@@ -166,18 +171,17 @@ class Server:
|
|
|
166
171
|
Returns:
|
|
167
172
|
Response data from function.
|
|
168
173
|
"""
|
|
169
|
-
|
|
170
|
-
if request_data:
|
|
174
|
+
if data:
|
|
171
175
|
try:
|
|
172
|
-
parsed_data = data_type(**loads(str(
|
|
176
|
+
parsed_data = data_type(**loads(str(data))) # pyright: ignore[reportAny]
|
|
173
177
|
except JSONDecodeError:
|
|
174
|
-
return self._malformed_request_response(event,
|
|
178
|
+
return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
|
|
175
179
|
except ValidationError as e:
|
|
176
180
|
self._console.exception_error_print(event, e)
|
|
177
|
-
return self._malformed_request_response(event,
|
|
181
|
+
return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
|
|
178
182
|
else:
|
|
179
183
|
return str((await function(parsed_data)).to_json_string())
|
|
180
|
-
return self._malformed_request_response(event,
|
|
184
|
+
return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
|
|
181
185
|
|
|
182
186
|
# Event Handlers.
|
|
183
187
|
|
|
@@ -199,7 +203,7 @@ class Server:
|
|
|
199
203
|
return True
|
|
200
204
|
|
|
201
205
|
self._console.error_print(
|
|
202
|
-
"CONNECTION REFUSED",
|
|
206
|
+
"CONNECTION REFUSED", cannot_connect_as_client_is_already_connected_error(sid, self._client_sid)
|
|
203
207
|
)
|
|
204
208
|
return False
|
|
205
209
|
|
|
@@ -209,22 +213,24 @@ class Server:
|
|
|
209
213
|
Args:
|
|
210
214
|
sid: Socket session ID.
|
|
211
215
|
"""
|
|
212
|
-
self._console.info_print("
|
|
216
|
+
self._console.info_print("DISCONNECTION REQUEST", sid)
|
|
213
217
|
|
|
214
218
|
# Reset client SID if it matches.
|
|
215
219
|
if self._client_sid == sid:
|
|
216
220
|
self._client_sid = ""
|
|
221
|
+
self._console.info_print("DISCONNECTED", sid)
|
|
217
222
|
else:
|
|
218
|
-
self._console.error_print("DISCONNECTION",
|
|
223
|
+
self._console.error_print("DISCONNECTION", client_disconnected_without_being_connected_error(sid))
|
|
219
224
|
|
|
220
|
-
async def platform_event_handler(self, event: str,
|
|
225
|
+
async def platform_event_handler(self, event: str, _: str, data: Any) -> str: # pyright: ignore [reportAny, reportExplicitAny]
|
|
221
226
|
"""Handle events from the server.
|
|
222
227
|
|
|
223
228
|
Matches incoming events based on the Socket.IO API.
|
|
224
229
|
|
|
225
230
|
Args:
|
|
226
231
|
event: Event name.
|
|
227
|
-
|
|
232
|
+
_: Socket session ID (unused).
|
|
233
|
+
data: Event data.
|
|
228
234
|
|
|
229
235
|
Returns:
|
|
230
236
|
Response data.
|
|
@@ -247,28 +253,27 @@ class Server:
|
|
|
247
253
|
case "get_manipulators":
|
|
248
254
|
return str((await self._platform_handler.get_manipulators()).to_json_string())
|
|
249
255
|
case "get_position":
|
|
250
|
-
return await self._run_if_data_available(self._platform_handler.get_position, event,
|
|
256
|
+
return await self._run_if_data_available(self._platform_handler.get_position, event, data)
|
|
251
257
|
case "get_angles":
|
|
252
|
-
return await self._run_if_data_available(self._platform_handler.get_angles, event,
|
|
258
|
+
return await self._run_if_data_available(self._platform_handler.get_angles, event, data)
|
|
253
259
|
case "get_shank_count":
|
|
254
|
-
return await self._run_if_data_available(self._platform_handler.get_shank_count, event,
|
|
260
|
+
return await self._run_if_data_available(self._platform_handler.get_shank_count, event, data)
|
|
255
261
|
case "set_position":
|
|
256
262
|
return await self._run_if_data_parses(
|
|
257
|
-
self._platform_handler.set_position, SetPositionRequest, event,
|
|
263
|
+
self._platform_handler.set_position, SetPositionRequest, event, data
|
|
258
264
|
)
|
|
259
265
|
case "set_depth":
|
|
260
|
-
return await self._run_if_data_parses(self._platform_handler.set_depth, SetDepthRequest, event,
|
|
266
|
+
return await self._run_if_data_parses(self._platform_handler.set_depth, SetDepthRequest, event, data)
|
|
261
267
|
case "set_inside_brain":
|
|
262
268
|
return await self._run_if_data_parses(
|
|
263
|
-
self._platform_handler.set_inside_brain, SetInsideBrainRequest, event,
|
|
269
|
+
self._platform_handler.set_inside_brain, SetInsideBrainRequest, event, data
|
|
264
270
|
)
|
|
265
271
|
case "stop":
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return self._malformed_request_response(event, request_data)
|
|
272
|
+
if data:
|
|
273
|
+
return await self._platform_handler.stop(str(data)) # pyright: ignore[reportAny]
|
|
274
|
+
return self._malformed_request_response(event, data) # pyright: ignore[reportAny]
|
|
270
275
|
case "stop_all":
|
|
271
276
|
return await self._platform_handler.stop_all()
|
|
272
277
|
case _:
|
|
273
278
|
self._console.error_print("EVENT", f"Unknown event: {event}.")
|
|
274
|
-
return dumps(
|
|
279
|
+
return dumps(UNKNOWN_EVENT_ERROR)
|