ephys-link 2.0.0b2__tar.gz → 2.0.0b5__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.0b2 → ephys_link-2.0.0b5}/PKG-INFO +5 -6
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/README.md +3 -4
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/pyproject.toml +2 -1
- ephys_link-2.0.0b5/scripts/move_tester.py +16 -0
- ephys_link-2.0.0b5/src/ephys_link/__about__.py +1 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/__main__.py +1 -1
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/back_end/platform_handler.py +34 -27
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/back_end/server.py +1 -1
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/bindings/fake_bindings.py +8 -3
- ephys_link-2.0.0b5/src/ephys_link/bindings/mpm_bindings.py +278 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/bindings/ump_4_bindings.py +25 -19
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/util/base_bindings.py +18 -3
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/util/common.py +10 -10
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/util/console.py +5 -8
- ephys_link-2.0.0b2/scripts/move_tester.py +0 -15
- ephys_link-2.0.0b2/src/ephys_link/__about__.py +0 -1
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/.gitignore +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/LICENSE +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/assets/icon.ico +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/ephys_link.spec +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/qodana.yaml +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/scripts/__init__.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/scripts/logger_test.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/scripts/server_tester.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/__init__.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/back_end/__init__.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/bindings/__init__.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/front_end/__init__.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/front_end/cli.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/front_end/gui.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/resources/CP210xManufacturing.dll +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/resources/NstMotorCtrl.dll +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/resources/SiUSBXp.dll +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/resources/libum.dll +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/src/ephys_link/util/__init__.py +0 -0
- {ephys_link-2.0.0b2 → ephys_link-2.0.0b5}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ephys-link
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b5
|
|
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
|
|
@@ -35,7 +35,7 @@ Requires-Dist: pythonnet==3.0.3
|
|
|
35
35
|
Requires-Dist: requests==2.32.3
|
|
36
36
|
Requires-Dist: rich==13.7.1
|
|
37
37
|
Requires-Dist: sensapex==1.400.1
|
|
38
|
-
Requires-Dist: vbl-aquarium==0.0.
|
|
38
|
+
Requires-Dist: vbl-aquarium==0.0.22
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
41
|
# Electrophysiology Manipulator Link
|
|
@@ -120,13 +120,12 @@ window instead of `localhost`.
|
|
|
120
120
|
pip install ephys-link
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Import
|
|
123
|
+
Import main and run (this will launch the setup GUI).
|
|
124
124
|
|
|
125
125
|
```python
|
|
126
|
-
from ephys_link.
|
|
126
|
+
from ephys_link.__main__ import main
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
server.launch("sensapex", args.proxy_address, 8081)
|
|
128
|
+
main()
|
|
130
129
|
```
|
|
131
130
|
|
|
132
131
|
## Install for Development
|
|
@@ -80,13 +80,12 @@ window instead of `localhost`.
|
|
|
80
80
|
pip install ephys-link
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Import
|
|
83
|
+
Import main and run (this will launch the setup GUI).
|
|
84
84
|
|
|
85
85
|
```python
|
|
86
|
-
from ephys_link.
|
|
86
|
+
from ephys_link.__main__ import main
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
server.launch("sensapex", args.proxy_address, 8081)
|
|
88
|
+
main()
|
|
90
89
|
```
|
|
91
90
|
|
|
92
91
|
## Install for Development
|
|
@@ -39,7 +39,7 @@ dependencies = [
|
|
|
39
39
|
"requests==2.32.3",
|
|
40
40
|
"sensapex==1.400.1",
|
|
41
41
|
"rich==13.7.1",
|
|
42
|
-
"vbl-aquarium==0.0.
|
|
42
|
+
"vbl-aquarium==0.0.22"
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[project.urls]
|
|
@@ -61,6 +61,7 @@ exclude = ["/.github", "/.idea"]
|
|
|
61
61
|
[tool.hatch.envs.default]
|
|
62
62
|
python = "3.12"
|
|
63
63
|
dependencies = [
|
|
64
|
+
"mypy",
|
|
64
65
|
"coverage[toml]>=6.5",
|
|
65
66
|
"pytest",
|
|
66
67
|
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from asyncio import run
|
|
2
|
+
|
|
3
|
+
from vbl_aquarium.models.ephys_link import EphysLinkOptions, SetDepthRequest
|
|
4
|
+
from vbl_aquarium.models.unity import Vector4
|
|
5
|
+
|
|
6
|
+
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
7
|
+
from ephys_link.util.console import Console
|
|
8
|
+
|
|
9
|
+
c = Console(enable_debug=True)
|
|
10
|
+
p = PlatformHandler(EphysLinkOptions(type="pathfinder-mpm"), c)
|
|
11
|
+
# target = Vector4()
|
|
12
|
+
target = Vector4(x=7.5, y=7.5, z=7.5, w=7.5)
|
|
13
|
+
|
|
14
|
+
# print(run(p.set_position(SetPositionRequest(manipulator_id="A", position=target, speed=5))).to_json_string())
|
|
15
|
+
print(run(p.set_depth(SetDepthRequest(manipulator_id="A", depth=7.5, speed=0.15))).to_json_string())
|
|
16
|
+
print("Done!")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.0b5"
|
|
@@ -31,7 +31,7 @@ def main() -> None:
|
|
|
31
31
|
console = Console(enable_debug=options.debug)
|
|
32
32
|
|
|
33
33
|
# 3. Instantiate the Platform Handler with the appropriate platform bindings.
|
|
34
|
-
platform_handler = PlatformHandler(options
|
|
34
|
+
platform_handler = PlatformHandler(options, console)
|
|
35
35
|
|
|
36
36
|
# 4. Instantiate the Emergency Stop service.
|
|
37
37
|
|
|
@@ -12,6 +12,7 @@ from uuid import uuid4
|
|
|
12
12
|
from vbl_aquarium.models.ephys_link import (
|
|
13
13
|
AngularResponse,
|
|
14
14
|
BooleanStateResponse,
|
|
15
|
+
EphysLinkOptions,
|
|
15
16
|
GetManipulatorsResponse,
|
|
16
17
|
PositionalResponse,
|
|
17
18
|
SetDepthRequest,
|
|
@@ -25,6 +26,7 @@ from vbl_aquarium.models.unity import Vector4
|
|
|
25
26
|
|
|
26
27
|
from ephys_link.__about__ import __version__
|
|
27
28
|
from ephys_link.bindings.fake_bindings import FakeBindings
|
|
29
|
+
from ephys_link.bindings.mpm_bindings import MPMBinding
|
|
28
30
|
from ephys_link.bindings.ump_4_bindings import Ump4Bindings
|
|
29
31
|
from ephys_link.util.base_bindings import BaseBindings
|
|
30
32
|
from ephys_link.util.common import vector4_to_array
|
|
@@ -34,21 +36,21 @@ from ephys_link.util.console import Console
|
|
|
34
36
|
class PlatformHandler:
|
|
35
37
|
"""Handler for platform commands."""
|
|
36
38
|
|
|
37
|
-
def __init__(self,
|
|
39
|
+
def __init__(self, options: EphysLinkOptions, console: Console) -> None:
|
|
38
40
|
"""Initialize platform handler.
|
|
39
41
|
|
|
40
|
-
:param
|
|
41
|
-
:type
|
|
42
|
+
:param options: CLI options.
|
|
43
|
+
:type options: EphysLinkOptions
|
|
42
44
|
"""
|
|
43
45
|
|
|
44
|
-
# Store the
|
|
45
|
-
self.
|
|
46
|
+
# Store the CLI options.
|
|
47
|
+
self._options = options
|
|
46
48
|
|
|
47
49
|
# Store the console.
|
|
48
50
|
self._console = console
|
|
49
51
|
|
|
50
52
|
# Define bindings based on platform type.
|
|
51
|
-
self._bindings = self._match_platform_type(
|
|
53
|
+
self._bindings = self._match_platform_type(options)
|
|
52
54
|
|
|
53
55
|
# Record which IDs are inside the brain.
|
|
54
56
|
self._inside_brain: set[str] = set()
|
|
@@ -56,21 +58,23 @@ class PlatformHandler:
|
|
|
56
58
|
# Generate a Pinpoint ID for proxy usage.
|
|
57
59
|
self._pinpoint_id = str(uuid4())[:8]
|
|
58
60
|
|
|
59
|
-
def _match_platform_type(self,
|
|
61
|
+
def _match_platform_type(self, options: EphysLinkOptions) -> BaseBindings:
|
|
60
62
|
"""Match the platform type to the appropriate bindings.
|
|
61
63
|
|
|
62
|
-
:param
|
|
63
|
-
:type
|
|
64
|
+
:param options: CLI options.
|
|
65
|
+
:type options: EphysLinkOptions
|
|
64
66
|
:returns: Bindings for the specified platform type.
|
|
65
67
|
:rtype: :class:`ephys_link.util.base_bindings.BaseBindings`
|
|
66
68
|
"""
|
|
67
|
-
match
|
|
69
|
+
match options.type:
|
|
68
70
|
case "ump-4":
|
|
69
71
|
return Ump4Bindings()
|
|
72
|
+
case "pathfinder-mpm":
|
|
73
|
+
return MPMBinding(options.mpm_port)
|
|
70
74
|
case "fake":
|
|
71
75
|
return FakeBindings()
|
|
72
76
|
case _:
|
|
73
|
-
error_message = f'Platform type "{
|
|
77
|
+
error_message = f'Platform type "{options.type}" not recognized.'
|
|
74
78
|
self._console.critical_print(error_message)
|
|
75
79
|
raise ValueError(error_message)
|
|
76
80
|
|
|
@@ -99,7 +103,7 @@ class PlatformHandler:
|
|
|
99
103
|
:returns: Platform type config identifier (see CLI options for examples).
|
|
100
104
|
:rtype: str
|
|
101
105
|
"""
|
|
102
|
-
return self.
|
|
106
|
+
return str(self._options.type)
|
|
103
107
|
|
|
104
108
|
# Manipulator commands.
|
|
105
109
|
|
|
@@ -111,7 +115,7 @@ class PlatformHandler:
|
|
|
111
115
|
"""
|
|
112
116
|
try:
|
|
113
117
|
manipulators = await self._bindings.get_manipulators()
|
|
114
|
-
num_axes = await self._bindings.
|
|
118
|
+
num_axes = await self._bindings.get_axes_count()
|
|
115
119
|
dimensions = self._bindings.get_dimensions()
|
|
116
120
|
except Exception as e:
|
|
117
121
|
self._console.exception_error_print("Get Manipulators", e)
|
|
@@ -198,16 +202,16 @@ class PlatformHandler:
|
|
|
198
202
|
|
|
199
203
|
# Return error if movement did not reach target within tolerance.
|
|
200
204
|
for index, axis in enumerate(vector4_to_array(final_unified_position - request.position)):
|
|
201
|
-
# End once index is
|
|
202
|
-
if index
|
|
205
|
+
# End once index is the number of axes.
|
|
206
|
+
if index == await self._bindings.get_axes_count():
|
|
203
207
|
break
|
|
204
208
|
|
|
205
209
|
# Check if the axis is within the movement tolerance.
|
|
206
|
-
if abs(axis) >
|
|
210
|
+
if abs(axis) > self._bindings.get_movement_tolerance():
|
|
207
211
|
error_message = (
|
|
208
212
|
f"Manipulator {request.manipulator_id} did not reach target"
|
|
209
213
|
f" position on axis {list(Vector4.model_fields.keys())[index]}."
|
|
210
|
-
f"Requested: {request.position}, got: {final_unified_position}."
|
|
214
|
+
f" Requested: {request.position}, got: {final_unified_position}."
|
|
211
215
|
)
|
|
212
216
|
self._console.error_print("Set Position", error_message)
|
|
213
217
|
return PositionalResponse(error=error_message)
|
|
@@ -226,24 +230,27 @@ class PlatformHandler:
|
|
|
226
230
|
:rtype: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse`
|
|
227
231
|
"""
|
|
228
232
|
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
233
|
# Move to the new depth.
|
|
236
|
-
|
|
234
|
+
final_platform_depth = await self._bindings.set_depth(
|
|
237
235
|
manipulator_id=request.manipulator_id,
|
|
238
|
-
|
|
236
|
+
depth=self._bindings.unified_space_to_platform_space(Vector4(w=request.depth)).w,
|
|
239
237
|
speed=request.speed,
|
|
240
238
|
)
|
|
241
|
-
|
|
239
|
+
final_unified_depth = self._bindings.platform_space_to_unified_space(Vector4(w=final_platform_depth)).w
|
|
240
|
+
|
|
241
|
+
# Return error if movement did not reach target within tolerance.
|
|
242
|
+
if abs(final_unified_depth - request.depth) > self._bindings.get_movement_tolerance():
|
|
243
|
+
error_message = (
|
|
244
|
+
f"Manipulator {request.manipulator_id} did not reach target depth."
|
|
245
|
+
f" Requested: {request.depth}, got: {final_unified_depth}."
|
|
246
|
+
)
|
|
247
|
+
self._console.error_print("Set Depth", error_message)
|
|
248
|
+
return SetDepthResponse(error=error_message)
|
|
242
249
|
except Exception as e:
|
|
243
250
|
self._console.exception_error_print("Set Depth", e)
|
|
244
251
|
return SetDepthResponse(error=self._console.pretty_exception(e))
|
|
245
252
|
else:
|
|
246
|
-
return SetDepthResponse(depth=
|
|
253
|
+
return SetDepthResponse(depth=final_unified_depth)
|
|
247
254
|
|
|
248
255
|
async def set_inside_brain(self, request: SetInsideBrainRequest) -> BooleanStateResponse:
|
|
249
256
|
"""Mark a manipulator as inside the brain or not.
|
|
@@ -12,7 +12,7 @@ from vbl_aquarium.models.ephys_link import (
|
|
|
12
12
|
SetInsideBrainRequest,
|
|
13
13
|
SetPositionRequest,
|
|
14
14
|
)
|
|
15
|
-
from vbl_aquarium.
|
|
15
|
+
from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
|
|
16
16
|
|
|
17
17
|
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
18
18
|
from ephys_link.util.common import PORT, check_for_updates, server_preamble
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
2
2
|
|
|
3
3
|
from ephys_link.util.base_bindings import BaseBindings
|
|
4
|
+
from ephys_link.util.common import array_to_vector4
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class FakeBindings(BaseBindings):
|
|
@@ -22,11 +23,11 @@ class FakeBindings(BaseBindings):
|
|
|
22
23
|
async def get_manipulators(self) -> list[str]:
|
|
23
24
|
return list(map(str, range(8)))
|
|
24
25
|
|
|
25
|
-
async def
|
|
26
|
+
async def get_axes_count(self) -> int:
|
|
26
27
|
return 4
|
|
27
28
|
|
|
28
29
|
def get_dimensions(self) -> Vector4:
|
|
29
|
-
return
|
|
30
|
+
return array_to_vector4([20] * 4)
|
|
30
31
|
|
|
31
32
|
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
32
33
|
return self._positions[int(manipulator_id)]
|
|
@@ -37,13 +38,17 @@ class FakeBindings(BaseBindings):
|
|
|
37
38
|
async def get_shank_count(self, _: str) -> int:
|
|
38
39
|
return 1
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
def get_movement_tolerance(self) -> float:
|
|
41
42
|
return 0.001
|
|
42
43
|
|
|
43
44
|
async def set_position(self, manipulator_id: str, position: Vector4, _: float) -> Vector4:
|
|
44
45
|
self._positions[int(manipulator_id)] = position
|
|
45
46
|
return position
|
|
46
47
|
|
|
48
|
+
async def set_depth(self, manipulator_id: str, depth: float, _: float) -> float:
|
|
49
|
+
self._positions[int(manipulator_id)].w = depth
|
|
50
|
+
return depth
|
|
51
|
+
|
|
47
52
|
async def stop(self, _: str) -> None:
|
|
48
53
|
pass
|
|
49
54
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Bindings for New Scale Pathfinder MPM HTTP server platform.
|
|
2
|
+
|
|
3
|
+
MPM works slightly differently than the other platforms since it operates in stereotactic coordinates.
|
|
4
|
+
This means exceptions need to be made for its API.
|
|
5
|
+
|
|
6
|
+
Usage: Instantiate MPMBindings to interact with the New Scale Pathfinder MPM HTTP server platform.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from asyncio import get_running_loop, sleep
|
|
10
|
+
from json import dumps
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from requests import JSONDecodeError, get, put
|
|
14
|
+
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
15
|
+
|
|
16
|
+
from ephys_link.util.base_bindings import BaseBindings
|
|
17
|
+
from ephys_link.util.common import scalar_mm_to_um, vector4_to_array
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MPMBinding(BaseBindings):
|
|
21
|
+
"""Bindings for New Scale Pathfinder MPM HTTP server platform."""
|
|
22
|
+
|
|
23
|
+
# Valid New Scale manipulator IDs
|
|
24
|
+
VALID_MANIPULATOR_IDS = (
|
|
25
|
+
"A",
|
|
26
|
+
"B",
|
|
27
|
+
"C",
|
|
28
|
+
"D",
|
|
29
|
+
"E",
|
|
30
|
+
"F",
|
|
31
|
+
"G",
|
|
32
|
+
"H",
|
|
33
|
+
"I",
|
|
34
|
+
"J",
|
|
35
|
+
"K",
|
|
36
|
+
"L",
|
|
37
|
+
"M",
|
|
38
|
+
"N",
|
|
39
|
+
"O",
|
|
40
|
+
"P",
|
|
41
|
+
"Q",
|
|
42
|
+
"R",
|
|
43
|
+
"S",
|
|
44
|
+
"T",
|
|
45
|
+
"U",
|
|
46
|
+
"V",
|
|
47
|
+
"W",
|
|
48
|
+
"X",
|
|
49
|
+
"Y",
|
|
50
|
+
"Z",
|
|
51
|
+
"AA",
|
|
52
|
+
"AB",
|
|
53
|
+
"AC",
|
|
54
|
+
"AD",
|
|
55
|
+
"AE",
|
|
56
|
+
"AF",
|
|
57
|
+
"AG",
|
|
58
|
+
"AH",
|
|
59
|
+
"AI",
|
|
60
|
+
"AJ",
|
|
61
|
+
"AK",
|
|
62
|
+
"AL",
|
|
63
|
+
"AM",
|
|
64
|
+
"AN",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Movement polling preferences.
|
|
68
|
+
UNCHANGED_COUNTER_LIMIT = 10
|
|
69
|
+
POLL_INTERVAL = 0.1
|
|
70
|
+
|
|
71
|
+
# Speed preferences (mm/s to use coarse mode).
|
|
72
|
+
COARSE_SPEED_THRESHOLD = 0.1
|
|
73
|
+
INSERTION_SPEED_LIMIT = 9_000
|
|
74
|
+
|
|
75
|
+
def __init__(self, port: int) -> None:
|
|
76
|
+
"""Initialize connection to MPM HTTP server.
|
|
77
|
+
|
|
78
|
+
:param port: Port number for MPM HTTP server.
|
|
79
|
+
:type port: int
|
|
80
|
+
"""
|
|
81
|
+
self._url = f"http://localhost:{port}"
|
|
82
|
+
self._movement_stopped = False
|
|
83
|
+
|
|
84
|
+
async def get_manipulators(self) -> list[str]:
|
|
85
|
+
return [manipulator["Id"] for manipulator in (await self._query_data())["ProbeArray"]]
|
|
86
|
+
|
|
87
|
+
async def get_axes_count(self) -> int:
|
|
88
|
+
return 3
|
|
89
|
+
|
|
90
|
+
def get_dimensions(self) -> Vector4:
|
|
91
|
+
return Vector4(x=15, y=15, z=15, w=15)
|
|
92
|
+
|
|
93
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
94
|
+
manipulator_data = await self._manipulator_data(manipulator_id)
|
|
95
|
+
stage_z = manipulator_data["Stage_Z"]
|
|
96
|
+
|
|
97
|
+
await sleep(self.POLL_INTERVAL) # Wait for the stage to stabilize.
|
|
98
|
+
|
|
99
|
+
return Vector4(
|
|
100
|
+
x=manipulator_data["Stage_X"],
|
|
101
|
+
y=manipulator_data["Stage_Y"],
|
|
102
|
+
z=stage_z,
|
|
103
|
+
w=stage_z,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def get_angles(self, manipulator_id: str) -> Vector3:
|
|
107
|
+
manipulator_data = await self._manipulator_data(manipulator_id)
|
|
108
|
+
|
|
109
|
+
# Apply PosteriorAngle to Polar to get the correct angle.
|
|
110
|
+
adjusted_polar = manipulator_data["Polar"] - (await self._query_data())["PosteriorAngle"]
|
|
111
|
+
|
|
112
|
+
return Vector3(
|
|
113
|
+
x=adjusted_polar if adjusted_polar > 0 else 360 + adjusted_polar,
|
|
114
|
+
y=manipulator_data["Pitch"],
|
|
115
|
+
z=manipulator_data["ShankOrientation"],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def get_shank_count(self, manipulator_id: str) -> int:
|
|
119
|
+
return int((await self._manipulator_data(manipulator_id))["ShankCount"])
|
|
120
|
+
|
|
121
|
+
def get_movement_tolerance(self) -> float:
|
|
122
|
+
return 0.01
|
|
123
|
+
|
|
124
|
+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
125
|
+
# Keep track of the previous position to check if the manipulator stopped advancing.
|
|
126
|
+
current_position = await self.get_position(manipulator_id)
|
|
127
|
+
previous_position = current_position
|
|
128
|
+
unchanged_counter = 0
|
|
129
|
+
|
|
130
|
+
# Set step mode based on speed.
|
|
131
|
+
await self._put_request(
|
|
132
|
+
{
|
|
133
|
+
"PutId": "ProbeStepMode",
|
|
134
|
+
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
135
|
+
"StepMode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Send move request.
|
|
140
|
+
await self._put_request(
|
|
141
|
+
{
|
|
142
|
+
"PutId": "ProbeMotion",
|
|
143
|
+
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
144
|
+
"Absolute": 1,
|
|
145
|
+
"Stereotactic": 0,
|
|
146
|
+
"AxisMask": 7,
|
|
147
|
+
"X": position.x,
|
|
148
|
+
"Y": position.y,
|
|
149
|
+
"Z": position.z,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Wait for the manipulator to reach the target position or be stopped or stuck.
|
|
154
|
+
while (
|
|
155
|
+
not self._movement_stopped
|
|
156
|
+
and not self._is_vector_close(current_position, position)
|
|
157
|
+
and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
|
|
158
|
+
):
|
|
159
|
+
# Wait for a short time before checking again.
|
|
160
|
+
await sleep(self.POLL_INTERVAL)
|
|
161
|
+
|
|
162
|
+
# Update current position.
|
|
163
|
+
current_position = await self.get_position(manipulator_id)
|
|
164
|
+
|
|
165
|
+
# Check if manipulator is not moving.
|
|
166
|
+
if self._is_vector_close(previous_position, current_position):
|
|
167
|
+
# Position did not change.
|
|
168
|
+
unchanged_counter += 1
|
|
169
|
+
else:
|
|
170
|
+
# Position changed.
|
|
171
|
+
unchanged_counter = 0
|
|
172
|
+
previous_position = current_position
|
|
173
|
+
|
|
174
|
+
# Reset movement stopped flag.
|
|
175
|
+
self._movement_stopped = False
|
|
176
|
+
|
|
177
|
+
# Return the final position.
|
|
178
|
+
return await self.get_position(manipulator_id)
|
|
179
|
+
|
|
180
|
+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
181
|
+
# Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly.
|
|
182
|
+
current_depth = (await self.get_position(manipulator_id)).w
|
|
183
|
+
previous_depth = current_depth
|
|
184
|
+
unchanged_counter = 0
|
|
185
|
+
|
|
186
|
+
# Send move request.
|
|
187
|
+
# Convert mm/s to um/min and cap speed at the limit.
|
|
188
|
+
await self._put_request(
|
|
189
|
+
{
|
|
190
|
+
"PutId": "ProbeInsertion",
|
|
191
|
+
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
192
|
+
"Distance": scalar_mm_to_um(current_depth - depth),
|
|
193
|
+
"Rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT),
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Wait for the manipulator to reach the target depth or be stopped or get stuck.
|
|
198
|
+
while not self._movement_stopped and not abs(current_depth - depth) <= self.get_movement_tolerance():
|
|
199
|
+
# Wait for a short time before checking again.
|
|
200
|
+
await sleep(self.POLL_INTERVAL)
|
|
201
|
+
|
|
202
|
+
# Get the current depth.
|
|
203
|
+
current_depth = (await self.get_position(manipulator_id)).w
|
|
204
|
+
|
|
205
|
+
# Check if manipulator is not moving.
|
|
206
|
+
if abs(previous_depth - current_depth) <= self.get_movement_tolerance():
|
|
207
|
+
# Depth did not change.
|
|
208
|
+
unchanged_counter += 1
|
|
209
|
+
else:
|
|
210
|
+
# Depth changed.
|
|
211
|
+
unchanged_counter = 0
|
|
212
|
+
previous_depth = current_depth
|
|
213
|
+
|
|
214
|
+
# Reset movement stopped flag.
|
|
215
|
+
self._movement_stopped = False
|
|
216
|
+
|
|
217
|
+
# Return the final depth.
|
|
218
|
+
return float((await self.get_position(manipulator_id)).w)
|
|
219
|
+
|
|
220
|
+
async def stop(self, manipulator_id: str) -> None:
|
|
221
|
+
request = {"PutId": "ProbeStop", "Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id)}
|
|
222
|
+
await self._put_request(request)
|
|
223
|
+
self._movement_stopped = True
|
|
224
|
+
|
|
225
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
226
|
+
# unified <- platform
|
|
227
|
+
# +x <- -x
|
|
228
|
+
# +y <- +z
|
|
229
|
+
# +z <- +y
|
|
230
|
+
# +w <- -w
|
|
231
|
+
|
|
232
|
+
return Vector4(
|
|
233
|
+
x=self.get_dimensions().x - platform_space.x,
|
|
234
|
+
y=platform_space.z,
|
|
235
|
+
z=platform_space.y,
|
|
236
|
+
w=self.get_dimensions().w - platform_space.w,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
240
|
+
# platform <- unified
|
|
241
|
+
# +x <- -x
|
|
242
|
+
# +y <- +z
|
|
243
|
+
# +z <- +y
|
|
244
|
+
# +w <- -w
|
|
245
|
+
|
|
246
|
+
return Vector4(
|
|
247
|
+
x=self.get_dimensions().x - unified_space.x,
|
|
248
|
+
y=unified_space.z,
|
|
249
|
+
z=unified_space.y,
|
|
250
|
+
w=self.get_dimensions().w - unified_space.w,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Helper functions.
|
|
254
|
+
async def _query_data(self) -> Any:
|
|
255
|
+
try:
|
|
256
|
+
return (await get_running_loop().run_in_executor(None, get, self._url)).json()
|
|
257
|
+
except ConnectionError as connectionError:
|
|
258
|
+
error_message = f"Unable to connect to MPM HTTP server: {connectionError}"
|
|
259
|
+
raise RuntimeError(error_message) from connectionError
|
|
260
|
+
except JSONDecodeError as jsonDecodeError:
|
|
261
|
+
error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}"
|
|
262
|
+
raise ValueError(error_message) from jsonDecodeError
|
|
263
|
+
|
|
264
|
+
async def _manipulator_data(self, manipulator_id: str) -> Any:
|
|
265
|
+
probe_data = (await self._query_data())["ProbeArray"]
|
|
266
|
+
for probe in probe_data:
|
|
267
|
+
if probe["Id"] == manipulator_id:
|
|
268
|
+
return probe
|
|
269
|
+
|
|
270
|
+
# If we get here, that means the manipulator doesn't exist.
|
|
271
|
+
error_message = f"Manipulator {manipulator_id} not found."
|
|
272
|
+
raise ValueError(error_message)
|
|
273
|
+
|
|
274
|
+
async def _put_request(self, request: dict[str, Any]) -> None:
|
|
275
|
+
await get_running_loop().run_in_executor(None, put, self._url, dumps(request))
|
|
276
|
+
|
|
277
|
+
def _is_vector_close(self, target: Vector4, current: Vector4) -> bool:
|
|
278
|
+
return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3])
|
|
@@ -9,7 +9,14 @@ from sensapex import UMP, SensapexDevice
|
|
|
9
9
|
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
10
10
|
|
|
11
11
|
from ephys_link.util.base_bindings import BaseBindings
|
|
12
|
-
from ephys_link.util.common import
|
|
12
|
+
from ephys_link.util.common import (
|
|
13
|
+
RESOURCES_PATH,
|
|
14
|
+
array_to_vector4,
|
|
15
|
+
scalar_mm_to_um,
|
|
16
|
+
um_to_mm,
|
|
17
|
+
vector4_to_array,
|
|
18
|
+
vector_mm_to_um,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
class Ump4Bindings(BaseBindings):
|
|
@@ -28,7 +35,7 @@ class Ump4Bindings(BaseBindings):
|
|
|
28
35
|
async def get_manipulators(self) -> list[str]:
|
|
29
36
|
return list(map(str, self._ump.list_devices()))
|
|
30
37
|
|
|
31
|
-
async def
|
|
38
|
+
async def get_axes_count(self) -> int:
|
|
32
39
|
return 4
|
|
33
40
|
|
|
34
41
|
def get_dimensions(self) -> Vector4:
|
|
@@ -55,29 +62,17 @@ class Ump4Bindings(BaseBindings):
|
|
|
55
62
|
error_message = "UMP-4 does not support getting shank count"
|
|
56
63
|
raise AttributeError(error_message)
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
def get_movement_tolerance(self) -> float:
|
|
59
66
|
return 0.001
|
|
60
67
|
|
|
61
68
|
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
62
|
-
"""Set the position of the manipulator.
|
|
63
|
-
|
|
64
|
-
Waits using Asyncio until the movement is finished. This assumes the application is running in an event loop.
|
|
65
|
-
|
|
66
|
-
:param manipulator_id: Manipulator ID.
|
|
67
|
-
:type manipulator_id: str
|
|
68
|
-
:param position: Platform space position to set the manipulator to (mm).
|
|
69
|
-
:type position: Vector4
|
|
70
|
-
:param speed: Speed to move the manipulator to the position (mm/s).
|
|
71
|
-
:type speed: float
|
|
72
|
-
:returns: Final position of the manipulator in platform space (mm).
|
|
73
|
-
:rtype: Vector4
|
|
74
|
-
:raises RuntimeError: If the movement is interrupted.
|
|
75
|
-
"""
|
|
76
69
|
# Convert position to micrometers.
|
|
77
|
-
target_position_um =
|
|
70
|
+
target_position_um = vector_mm_to_um(position)
|
|
78
71
|
|
|
79
72
|
# Request movement.
|
|
80
|
-
movement = self._get_device(manipulator_id).goto_pos(
|
|
73
|
+
movement = self._get_device(manipulator_id).goto_pos(
|
|
74
|
+
vector4_to_array(target_position_um), scalar_mm_to_um(speed)
|
|
75
|
+
)
|
|
81
76
|
|
|
82
77
|
# Wait for movement to finish.
|
|
83
78
|
await get_running_loop().run_in_executor(None, movement.finished_event.wait)
|
|
@@ -89,6 +84,17 @@ class Ump4Bindings(BaseBindings):
|
|
|
89
84
|
|
|
90
85
|
return um_to_mm(array_to_vector4(movement.last_pos))
|
|
91
86
|
|
|
87
|
+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
88
|
+
# Augment current position with depth.
|
|
89
|
+
current_position = await self.get_position(manipulator_id)
|
|
90
|
+
new_platform_position = current_position.model_copy(update={"w": depth})
|
|
91
|
+
|
|
92
|
+
# Make the movement.
|
|
93
|
+
final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
|
|
94
|
+
|
|
95
|
+
# Return the final depth.
|
|
96
|
+
return float(final_platform_position.w)
|
|
97
|
+
|
|
92
98
|
async def stop(self, manipulator_id: str) -> None:
|
|
93
99
|
self._get_device(manipulator_id).stop()
|
|
94
100
|
|
|
@@ -25,7 +25,7 @@ class BaseBindings(ABC):
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
@abstractmethod
|
|
28
|
-
async def
|
|
28
|
+
async def get_axes_count(self) -> int:
|
|
29
29
|
"""Get the number of axes for the current platform.
|
|
30
30
|
|
|
31
31
|
:returns: Number of axes.
|
|
@@ -76,7 +76,7 @@ class BaseBindings(ABC):
|
|
|
76
76
|
"""
|
|
77
77
|
|
|
78
78
|
@abstractmethod
|
|
79
|
-
|
|
79
|
+
def get_movement_tolerance(self) -> float:
|
|
80
80
|
"""Get the tolerance for how close the final position must be to the target position in a movement (mm).
|
|
81
81
|
|
|
82
82
|
:returns: Movement tolerance (mm).
|
|
@@ -88,7 +88,6 @@ class BaseBindings(ABC):
|
|
|
88
88
|
"""Set the position of a manipulator.
|
|
89
89
|
|
|
90
90
|
This will directly set the position in the original platform space.
|
|
91
|
-
Unified space coordinates will need to be converted to platform space.
|
|
92
91
|
For 3-axis manipulators, the first 3 values of the position will be used.
|
|
93
92
|
|
|
94
93
|
:param manipulator_id: Manipulator ID.
|
|
@@ -101,6 +100,22 @@ class BaseBindings(ABC):
|
|
|
101
100
|
:rtype: Vector4
|
|
102
101
|
"""
|
|
103
102
|
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
105
|
+
"""Set the depth of a manipulator.
|
|
106
|
+
|
|
107
|
+
This will directly set the depth stage in the original platform space.
|
|
108
|
+
|
|
109
|
+
:param manipulator_id: Manipulator ID.
|
|
110
|
+
:type manipulator_id: str
|
|
111
|
+
:param depth: Depth to set the manipulator to (mm).
|
|
112
|
+
:type depth: float
|
|
113
|
+
:param speed: Speed to move the manipulator to the depth (mm/s).
|
|
114
|
+
:type speed: float
|
|
115
|
+
:returns: Final depth of the manipulator in platform space (mm).
|
|
116
|
+
:rtype: float
|
|
117
|
+
"""
|
|
118
|
+
|
|
104
119
|
@abstractmethod
|
|
105
120
|
async def stop(self, manipulator_id: str) -> None:
|
|
106
121
|
"""Stop a manipulator."""
|
|
@@ -53,23 +53,23 @@ def check_for_updates() -> None:
|
|
|
53
53
|
# Unit conversions
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def
|
|
57
|
-
"""Convert
|
|
56
|
+
def scalar_mm_to_um(mm: float) -> float:
|
|
57
|
+
"""Convert scalar values of millimeters to micrometers.
|
|
58
58
|
|
|
59
|
-
:param
|
|
60
|
-
:type
|
|
61
|
-
:returns:
|
|
59
|
+
:param mm: Scalar value in millimeters.
|
|
60
|
+
:type mm: float
|
|
61
|
+
:returns: Scalar value in micrometers.
|
|
62
62
|
:rtype: float
|
|
63
63
|
"""
|
|
64
|
-
return
|
|
64
|
+
return mm * 1_000
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def
|
|
68
|
-
"""Convert millimeters to micrometers.
|
|
67
|
+
def vector_mm_to_um(mm: Vector4) -> Vector4:
|
|
68
|
+
"""Convert vector values of millimeters to micrometers.
|
|
69
69
|
|
|
70
|
-
:param mm:
|
|
70
|
+
:param mm: Vector in millimeters.
|
|
71
71
|
:type mm: Vector4
|
|
72
|
-
:returns:
|
|
72
|
+
:returns: Vector in micrometers.
|
|
73
73
|
:rtype: Vector4
|
|
74
74
|
"""
|
|
75
75
|
return mm * 1_000
|
|
@@ -25,12 +25,12 @@ class Console:
|
|
|
25
25
|
|
|
26
26
|
# Config logger.
|
|
27
27
|
basicConfig(
|
|
28
|
-
level=DEBUG if enable_debug else INFO,
|
|
29
28
|
format="%(message)s",
|
|
30
29
|
datefmt="[%I:%M:%S %p]",
|
|
31
|
-
handlers=[RichHandler(rich_tracebacks=True)],
|
|
30
|
+
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
|
|
32
31
|
)
|
|
33
32
|
self._log = getLogger("rich")
|
|
33
|
+
self._log.setLevel(DEBUG if enable_debug else INFO)
|
|
34
34
|
|
|
35
35
|
# Install Rich traceback.
|
|
36
36
|
install()
|
|
@@ -71,7 +71,7 @@ class Console:
|
|
|
71
71
|
:param msg: Critical message to print.
|
|
72
72
|
:type msg: str
|
|
73
73
|
"""
|
|
74
|
-
self._log.critical(f"[b i red]{msg}"
|
|
74
|
+
self._log.critical(f"[b i red]{msg}")
|
|
75
75
|
|
|
76
76
|
@staticmethod
|
|
77
77
|
def pretty_exception(exception: Exception) -> str:
|
|
@@ -92,9 +92,7 @@ class Console:
|
|
|
92
92
|
:param exception: Exception to print.
|
|
93
93
|
:type exception: Exception
|
|
94
94
|
"""
|
|
95
|
-
self._log.exception(
|
|
96
|
-
f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}", extra={"markup": True}
|
|
97
|
-
)
|
|
95
|
+
self._log.exception(f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}")
|
|
98
96
|
|
|
99
97
|
# Helper methods.
|
|
100
98
|
def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
|
|
@@ -124,10 +122,9 @@ class Console:
|
|
|
124
122
|
self._log.log(
|
|
125
123
|
self._last_message[0],
|
|
126
124
|
f"{self._last_message[1]}:[/] {self._last_message[2]}[/] x {self._repeat_counter}",
|
|
127
|
-
extra={"markup": True},
|
|
128
125
|
)
|
|
129
126
|
self._repeat_counter = 0
|
|
130
127
|
|
|
131
128
|
# Log new message.
|
|
132
|
-
self._log.log(log_type, f"{label}:[/] {message}"
|
|
129
|
+
self._log.log(log_type, f"{label}:[/] {message}")
|
|
133
130
|
self._last_message = message_set
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from asyncio import run
|
|
2
|
-
|
|
3
|
-
from vbl_aquarium.models.ephys_link import SetPositionRequest
|
|
4
|
-
from vbl_aquarium.models.unity import Vector4
|
|
5
|
-
|
|
6
|
-
from ephys_link.back_end.platform_handler import PlatformHandler
|
|
7
|
-
from ephys_link.util.console import Console
|
|
8
|
-
|
|
9
|
-
c = Console(enable_debug=True)
|
|
10
|
-
p = PlatformHandler("ump-4", c)
|
|
11
|
-
target = Vector4()
|
|
12
|
-
# target = Vector4(x=10, y=10, z=10, w=10)
|
|
13
|
-
|
|
14
|
-
print(run(p.set_position(SetPositionRequest(manipulator_id="6", position=target, speed=5))).to_json_string())
|
|
15
|
-
print("Done!")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0b2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|