ephys-link 2.0.0b1__py3-none-any.whl → 2.0.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ephys_link/__about__.py +1 -1
- ephys_link/__main__.py +1 -1
- ephys_link/back_end/platform_handler.py +37 -30
- ephys_link/back_end/server.py +7 -5
- ephys_link/bindings/fake_bindings.py +8 -3
- ephys_link/bindings/mpm_bindings.py +278 -0
- ephys_link/bindings/ump_4_bindings.py +25 -21
- ephys_link/util/base_bindings.py +18 -3
- ephys_link/util/common.py +12 -13
- ephys_link/util/console.py +72 -54
- {ephys_link-2.0.0b1.dist-info → ephys_link-2.0.0b5.dist-info}/METADATA +6 -6
- ephys_link-2.0.0b5.dist-info/RECORD +26 -0
- ephys_link-2.0.0b1.dist-info/RECORD +0 -25
- {ephys_link-2.0.0b1.dist-info → ephys_link-2.0.0b5.dist-info}/WHEEL +0 -0
- {ephys_link-2.0.0b1.dist-info → ephys_link-2.0.0b5.dist-info}/entry_points.txt +0 -0
- {ephys_link-2.0.0b1.dist-info → ephys_link-2.0.0b5.dist-info}/licenses/LICENSE +0 -0
ephys_link/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.0.
|
|
1
|
+
__version__ = "2.0.0b5"
|
ephys_link/__main__.py
CHANGED
|
@@ -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,22 +58,24 @@ 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 "{
|
|
74
|
-
self._console.
|
|
77
|
+
error_message = f'Platform type "{options.type}" not recognized.'
|
|
78
|
+
self._console.critical_print(error_message)
|
|
75
79
|
raise ValueError(error_message)
|
|
76
80
|
|
|
77
81
|
# Ephys Link metadata.
|
|
@@ -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)
|
|
@@ -185,7 +189,7 @@ class PlatformHandler:
|
|
|
185
189
|
# Disallow setting manipulator position while inside the brain.
|
|
186
190
|
if request.manipulator_id in self._inside_brain:
|
|
187
191
|
error_message = 'Can not move manipulator while inside the brain. Set the depth ("set_depth") instead.'
|
|
188
|
-
self._console.error_print(error_message)
|
|
192
|
+
self._console.error_print("Set Position", error_message)
|
|
189
193
|
return PositionalResponse(error=error_message)
|
|
190
194
|
|
|
191
195
|
# Move to the new position.
|
|
@@ -198,18 +202,18 @@ 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
|
-
self._console.error_print(error_message)
|
|
216
|
+
self._console.error_print("Set Position", error_message)
|
|
213
217
|
return PositionalResponse(error=error_message)
|
|
214
218
|
except Exception as e:
|
|
215
219
|
self._console.exception_error_print("Set Position", e)
|
|
@@ -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.
|
ephys_link/back_end/server.py
CHANGED
|
@@ -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
|
|
@@ -75,7 +75,7 @@ class Server:
|
|
|
75
75
|
# Helper functions.
|
|
76
76
|
def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str:
|
|
77
77
|
"""Return a response for a malformed request."""
|
|
78
|
-
self._console.
|
|
78
|
+
self._console.error_print("MALFORMED REQUEST", f"{request}: {data}")
|
|
79
79
|
return dumps({"error": "Malformed request."})
|
|
80
80
|
|
|
81
81
|
async def _run_if_data_available(
|
|
@@ -127,7 +127,9 @@ class Server:
|
|
|
127
127
|
self._console.info_print("CONNECTION GRANTED", sid)
|
|
128
128
|
return True
|
|
129
129
|
|
|
130
|
-
self._console.error_print(
|
|
130
|
+
self._console.error_print(
|
|
131
|
+
"CONNECTION REFUSED", f"Cannot connect {sid} as {self._client_sid} is already connected."
|
|
132
|
+
)
|
|
131
133
|
return False
|
|
132
134
|
|
|
133
135
|
async def disconnect(self, sid: str) -> None:
|
|
@@ -142,7 +144,7 @@ class Server:
|
|
|
142
144
|
if self._client_sid == sid:
|
|
143
145
|
self._client_sid = ""
|
|
144
146
|
else:
|
|
145
|
-
self._console.error_print(f"Client {sid} disconnected without being connected.")
|
|
147
|
+
self._console.error_print("DISCONNECTION", f"Client {sid} disconnected without being connected.")
|
|
146
148
|
|
|
147
149
|
# noinspection PyTypeChecker
|
|
148
150
|
async def platform_event_handler(self, event: str, *args: tuple[Any]) -> str:
|
|
@@ -196,5 +198,5 @@ class Server:
|
|
|
196
198
|
case "stop_all":
|
|
197
199
|
return await self._platform_handler.stop_all()
|
|
198
200
|
case _:
|
|
199
|
-
self._console.error_print(f"Unknown event: {event}.")
|
|
201
|
+
self._console.error_print("EVENT", f"Unknown event: {event}.")
|
|
200
202
|
return dumps({"error": "Unknown event."})
|
|
@@ -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,8 +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
|
|
13
|
-
|
|
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
|
+
)
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class Ump4Bindings(BaseBindings):
|
|
@@ -24,13 +30,12 @@ class Ump4Bindings(BaseBindings):
|
|
|
24
30
|
self._ump = UMP.get_ump()
|
|
25
31
|
if self._ump is None:
|
|
26
32
|
error_message = "Unable to connect to uMp"
|
|
27
|
-
Console.error_print(error_message)
|
|
28
33
|
raise ValueError(error_message)
|
|
29
34
|
|
|
30
35
|
async def get_manipulators(self) -> list[str]:
|
|
31
36
|
return list(map(str, self._ump.list_devices()))
|
|
32
37
|
|
|
33
|
-
async def
|
|
38
|
+
async def get_axes_count(self) -> int:
|
|
34
39
|
return 4
|
|
35
40
|
|
|
36
41
|
def get_dimensions(self) -> Vector4:
|
|
@@ -57,29 +62,17 @@ class Ump4Bindings(BaseBindings):
|
|
|
57
62
|
error_message = "UMP-4 does not support getting shank count"
|
|
58
63
|
raise AttributeError(error_message)
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
def get_movement_tolerance(self) -> float:
|
|
61
66
|
return 0.001
|
|
62
67
|
|
|
63
68
|
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
64
|
-
"""Set the position of the manipulator.
|
|
65
|
-
|
|
66
|
-
Waits using Asyncio until the movement is finished. This assumes the application is running in an event loop.
|
|
67
|
-
|
|
68
|
-
:param manipulator_id: Manipulator ID.
|
|
69
|
-
:type manipulator_id: str
|
|
70
|
-
:param position: Platform space position to set the manipulator to (mm).
|
|
71
|
-
:type position: Vector4
|
|
72
|
-
:param speed: Speed to move the manipulator to the position (mm/s).
|
|
73
|
-
:type speed: float
|
|
74
|
-
:returns: Final position of the manipulator in platform space (mm).
|
|
75
|
-
:rtype: Vector4
|
|
76
|
-
:raises RuntimeError: If the movement is interrupted.
|
|
77
|
-
"""
|
|
78
69
|
# Convert position to micrometers.
|
|
79
|
-
target_position_um =
|
|
70
|
+
target_position_um = vector_mm_to_um(position)
|
|
80
71
|
|
|
81
72
|
# Request movement.
|
|
82
|
-
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
|
+
)
|
|
83
76
|
|
|
84
77
|
# Wait for movement to finish.
|
|
85
78
|
await get_running_loop().run_in_executor(None, movement.finished_event.wait)
|
|
@@ -91,6 +84,17 @@ class Ump4Bindings(BaseBindings):
|
|
|
91
84
|
|
|
92
85
|
return um_to_mm(array_to_vector4(movement.last_pos))
|
|
93
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
|
+
|
|
94
98
|
async def stop(self, manipulator_id: str) -> None:
|
|
95
99
|
self._get_device(manipulator_id).stop()
|
|
96
100
|
|
ephys_link/util/base_bindings.py
CHANGED
|
@@ -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."""
|
ephys_link/util/common.py
CHANGED
|
@@ -9,7 +9,6 @@ from requests import get
|
|
|
9
9
|
from vbl_aquarium.models.unity import Vector4
|
|
10
10
|
|
|
11
11
|
from ephys_link.__about__ import __version__
|
|
12
|
-
from ephys_link.util.console import Console
|
|
13
12
|
|
|
14
13
|
# Ephys Link ASCII.
|
|
15
14
|
ASCII = r"""
|
|
@@ -47,30 +46,30 @@ def check_for_updates() -> None:
|
|
|
47
46
|
response = get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags", timeout=10)
|
|
48
47
|
latest_version = response.json()[0]["name"]
|
|
49
48
|
if parse(latest_version) > parse(__version__):
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
print(f"Update available: {latest_version} !")
|
|
50
|
+
print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
# Unit conversions
|
|
55
54
|
|
|
56
55
|
|
|
57
|
-
def
|
|
58
|
-
"""Convert
|
|
56
|
+
def scalar_mm_to_um(mm: float) -> float:
|
|
57
|
+
"""Convert scalar values of millimeters to micrometers.
|
|
59
58
|
|
|
60
|
-
:param
|
|
61
|
-
:type
|
|
62
|
-
:returns:
|
|
59
|
+
:param mm: Scalar value in millimeters.
|
|
60
|
+
:type mm: float
|
|
61
|
+
:returns: Scalar value in micrometers.
|
|
63
62
|
:rtype: float
|
|
64
63
|
"""
|
|
65
|
-
return
|
|
64
|
+
return mm * 1_000
|
|
66
65
|
|
|
67
66
|
|
|
68
|
-
def
|
|
69
|
-
"""Convert millimeters to micrometers.
|
|
67
|
+
def vector_mm_to_um(mm: Vector4) -> Vector4:
|
|
68
|
+
"""Convert vector values of millimeters to micrometers.
|
|
70
69
|
|
|
71
|
-
:param mm:
|
|
70
|
+
:param mm: Vector in millimeters.
|
|
72
71
|
:type mm: Vector4
|
|
73
|
-
:returns:
|
|
72
|
+
:returns: Vector in micrometers.
|
|
74
73
|
:rtype: Vector4
|
|
75
74
|
"""
|
|
76
75
|
return mm * 1_000
|
ephys_link/util/console.py
CHANGED
|
@@ -6,12 +6,10 @@ Configure the console to print error and debug messages.
|
|
|
6
6
|
Usage: Create a Console object and call the appropriate method to print messages.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from logging import DEBUG, ERROR, INFO, basicConfig, getLogger
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
# Constants.
|
|
14
|
-
TAB_BLOCK = "\t\t"
|
|
11
|
+
from rich.logging import RichHandler
|
|
12
|
+
from rich.traceback import install
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class Console:
|
|
@@ -21,34 +19,59 @@ class Console:
|
|
|
21
19
|
:param enable_debug: Enable debug mode.
|
|
22
20
|
:type enable_debug: bool
|
|
23
21
|
"""
|
|
24
|
-
self._enable_debug = enable_debug
|
|
25
|
-
|
|
26
22
|
# Repeat message fields.
|
|
27
|
-
self._last_message = ""
|
|
28
|
-
self._repeat_counter =
|
|
23
|
+
self._last_message = (0, "", "")
|
|
24
|
+
self._repeat_counter = 0
|
|
29
25
|
|
|
30
|
-
#
|
|
31
|
-
|
|
26
|
+
# Config logger.
|
|
27
|
+
basicConfig(
|
|
28
|
+
format="%(message)s",
|
|
29
|
+
datefmt="[%I:%M:%S %p]",
|
|
30
|
+
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
|
|
31
|
+
)
|
|
32
|
+
self._log = getLogger("rich")
|
|
33
|
+
self._log.setLevel(DEBUG if enable_debug else INFO)
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"""Print an error message to the console.
|
|
35
|
+
# Install Rich traceback.
|
|
36
|
+
install()
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
def debug_print(self, label: str, msg: str) -> None:
|
|
39
|
+
"""Print a debug message to the console.
|
|
40
|
+
|
|
41
|
+
:param label: Label for the debug message.
|
|
42
|
+
:type label: str
|
|
43
|
+
:param msg: Debug message to print.
|
|
38
44
|
:type msg: str
|
|
39
45
|
"""
|
|
40
|
-
|
|
46
|
+
self._repeatable_log(DEBUG, f"[b green]{label}", f"[green]{msg}")
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
def info_print(self, label: str, msg: str) -> None:
|
|
49
|
+
"""Print info to console.
|
|
50
|
+
|
|
51
|
+
:param label: Label for the message.
|
|
52
|
+
:type label: str
|
|
53
|
+
:param msg: Message to print.
|
|
54
|
+
:type msg: str
|
|
55
|
+
"""
|
|
56
|
+
self._repeatable_log(INFO, f"[b blue]{label}", msg)
|
|
57
|
+
|
|
58
|
+
def error_print(self, label: str, msg: str) -> None:
|
|
59
|
+
"""Print an error message to the console.
|
|
45
60
|
|
|
46
61
|
:param label: Label for the error message.
|
|
47
62
|
:type label: str
|
|
48
63
|
:param msg: Error message to print.
|
|
49
64
|
:type msg: str
|
|
50
65
|
"""
|
|
51
|
-
|
|
66
|
+
self._repeatable_log(ERROR, f"[b red]{label}", f"[red]{msg}")
|
|
67
|
+
|
|
68
|
+
def critical_print(self, msg: str) -> None:
|
|
69
|
+
"""Print a critical message to the console.
|
|
70
|
+
|
|
71
|
+
:param msg: Critical message to print.
|
|
72
|
+
:type msg: str
|
|
73
|
+
"""
|
|
74
|
+
self._log.critical(f"[b i red]{msg}")
|
|
52
75
|
|
|
53
76
|
@staticmethod
|
|
54
77
|
def pretty_exception(exception: Exception) -> str:
|
|
@@ -61,8 +84,7 @@ class Console:
|
|
|
61
84
|
"""
|
|
62
85
|
return f"{type(exception).__name__}: {exception}"
|
|
63
86
|
|
|
64
|
-
|
|
65
|
-
def exception_error_print(label: str, exception: Exception) -> None:
|
|
87
|
+
def exception_error_print(self, label: str, exception: Exception) -> None:
|
|
66
88
|
"""Print an error message with exception details to the console.
|
|
67
89
|
|
|
68
90
|
:param label: Label for the error message.
|
|
@@ -70,43 +92,39 @@ class Console:
|
|
|
70
92
|
:param exception: Exception to print.
|
|
71
93
|
:type exception: Exception
|
|
72
94
|
"""
|
|
73
|
-
|
|
74
|
-
print_exc()
|
|
75
|
-
|
|
76
|
-
def debug_print(self, label: str, msg: str) -> None:
|
|
77
|
-
"""Print a debug message to the console.
|
|
78
|
-
|
|
79
|
-
:param label: Label for the debug message.
|
|
80
|
-
:type label: str
|
|
81
|
-
:param msg: Debug message to print.
|
|
82
|
-
:type msg: str
|
|
83
|
-
"""
|
|
84
|
-
if self._enable_debug:
|
|
85
|
-
self._repeat_print(f"{Back.BLUE}{Style.BRIGHT} DEBUG {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.BLUE}{msg}")
|
|
95
|
+
self._log.exception(f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}")
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
def
|
|
89
|
-
"""
|
|
97
|
+
# Helper methods.
|
|
98
|
+
def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
|
|
99
|
+
"""Add a row to the output table.
|
|
90
100
|
|
|
101
|
+
:param log_type: Type of log.
|
|
102
|
+
:type log_type: int
|
|
91
103
|
:param label: Label for the message.
|
|
92
104
|
:type label: str
|
|
93
|
-
:param
|
|
94
|
-
:type
|
|
105
|
+
:param message: Message.
|
|
106
|
+
:type message: str
|
|
95
107
|
"""
|
|
96
|
-
print(f"\n{Back.GREEN}{Style.BRIGHT} {label} {Style.RESET_ALL}{TAB_BLOCK}{Fore.GREEN}{msg}")
|
|
97
|
-
|
|
98
|
-
# Helper methods.
|
|
99
|
-
def _repeat_print(self, msg: str) -> None:
|
|
100
|
-
"""Print a message to the console with repeat counter.
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
# Compute if this is a repeated message.
|
|
110
|
+
message_set = (log_type, label, message)
|
|
111
|
+
if message_set == self._last_message:
|
|
112
|
+
# Handle repeat.
|
|
106
113
|
self._repeat_counter += 1
|
|
107
|
-
else:
|
|
108
|
-
self._repeat_counter = 1
|
|
109
|
-
self._last_message = msg
|
|
110
|
-
print()
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
# Add an ellipsis row for first repeat.
|
|
116
|
+
if self._repeat_counter == 1:
|
|
117
|
+
self._log.log(log_type, "...")
|
|
118
|
+
else:
|
|
119
|
+
# Handle novel message.
|
|
120
|
+
if self._repeat_counter > 0:
|
|
121
|
+
# Complete previous repeat.
|
|
122
|
+
self._log.log(
|
|
123
|
+
self._last_message[0],
|
|
124
|
+
f"{self._last_message[1]}:[/] {self._last_message[2]}[/] x {self._repeat_counter}",
|
|
125
|
+
)
|
|
126
|
+
self._repeat_counter = 0
|
|
127
|
+
|
|
128
|
+
# Log new message.
|
|
129
|
+
self._log.log(log_type, f"{label}:[/] {message}")
|
|
130
|
+
self._last_message = message_set
|
|
@@ -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
|
|
@@ -33,8 +33,9 @@ Requires-Dist: pyserial==3.5
|
|
|
33
33
|
Requires-Dist: python-socketio[asyncio-client]==5.11.3
|
|
34
34
|
Requires-Dist: pythonnet==3.0.3
|
|
35
35
|
Requires-Dist: requests==2.32.3
|
|
36
|
+
Requires-Dist: rich==13.7.1
|
|
36
37
|
Requires-Dist: sensapex==1.400.1
|
|
37
|
-
Requires-Dist: vbl-aquarium==0.0.
|
|
38
|
+
Requires-Dist: vbl-aquarium==0.0.22
|
|
38
39
|
Description-Content-Type: text/markdown
|
|
39
40
|
|
|
40
41
|
# Electrophysiology Manipulator Link
|
|
@@ -119,13 +120,12 @@ window instead of `localhost`.
|
|
|
119
120
|
pip install ephys-link
|
|
120
121
|
```
|
|
121
122
|
|
|
122
|
-
Import
|
|
123
|
+
Import main and run (this will launch the setup GUI).
|
|
123
124
|
|
|
124
125
|
```python
|
|
125
|
-
from ephys_link.
|
|
126
|
+
from ephys_link.__main__ import main
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
server.launch("sensapex", args.proxy_address, 8081)
|
|
128
|
+
main()
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
## Install for Development
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
ephys_link/__about__.py,sha256=W8_pwZswCUyYGNDMBbTV2lPT7qpoSD2adR4yUwvZfkU,25
|
|
2
|
+
ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
ephys_link/__main__.py,sha256=KSwJO4gPQAZitNSNChD1NjkO2j3pc8RA2dkRbiaq32w,1368
|
|
4
|
+
ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
ephys_link/back_end/platform_handler.py,sha256=cAHZ3gqA-Y33vb0sfsH085BPVtIfSUGaFVqvTzpA2MA,13168
|
|
6
|
+
ephys_link/back_end/server.py,sha256=QZu8deE57BxULfUutPQ41q3HTmr94nPyzaIT-JoOK2I,8026
|
|
7
|
+
ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
ephys_link/bindings/fake_bindings.py,sha256=Dk9Bpv4XTYa6ooh5Ge_5-o3IRzdazj5pYMcp3O41UJI,1936
|
|
9
|
+
ephys_link/bindings/mpm_bindings.py,sha256=e4whLU7mJm-Mbw6O2ua6fPHBZNnCiyyGiePEVBr3zzY,9818
|
|
10
|
+
ephys_link/bindings/ump_4_bindings.py,sha256=rmkniiH5KyWjyXGpmUW3RCR_rar40b3vxHwYakrwbqg,4494
|
|
11
|
+
ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
ephys_link/front_end/cli.py,sha256=KJBSWqdz4T5z0Zor1tJSHTJKZeMcHAJf5gXXu38wQPU,3105
|
|
13
|
+
ephys_link/front_end/gui.py,sha256=_gE6zFhFnzHPSyYd9MBvfK5xmDZHsUXcETDHzH66QzU,7518
|
|
14
|
+
ephys_link/resources/CP210xManufacturing.dll,sha256=aM9k_XABjkq0TOMiIw8HeteB40zqEkUDNO8wo91EdYI,810232
|
|
15
|
+
ephys_link/resources/NstMotorCtrl.dll,sha256=Xtpr3vBcxhcsOUGvgVEwYtGPvKEqDctIUGCK36GfU2Q,155136
|
|
16
|
+
ephys_link/resources/SiUSBXp.dll,sha256=187zlclZNNezCkU1o1CbICRAmKWJxbh8ahP6L6wo-_Y,469752
|
|
17
|
+
ephys_link/resources/libum.dll,sha256=YaD4dwiSNohx-XxHjx2eQWPOBEVvUIXARvx37e_yqNw,316316
|
|
18
|
+
ephys_link/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
ephys_link/util/base_bindings.py,sha256=ukomZftpwa8s_SV-jcwPSXmox0neXf8-uyie_NmiRro,5349
|
|
20
|
+
ephys_link/util/common.py,sha256=Pk7uVqEMFMPKNpeucWda_GfnHogRszej5G5qYkt43d8,3455
|
|
21
|
+
ephys_link/util/console.py,sha256=NvUH-Fp4nzkgrqQOcylctf46x4AW-qAphrtisepk1xY,4325
|
|
22
|
+
ephys_link-2.0.0b5.dist-info/METADATA,sha256=PvHqwUICg5mNPrvMvmGjpwcDQqQEtDzDacv4AuIsj88,7943
|
|
23
|
+
ephys_link-2.0.0b5.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
24
|
+
ephys_link-2.0.0b5.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
|
|
25
|
+
ephys_link-2.0.0b5.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
26
|
+
ephys_link-2.0.0b5.dist-info/RECORD,,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
ephys_link/__about__.py,sha256=QUUi7DnZ0lfA8ZZUdMrPNiqXewCSfYtIqTvB6q6NnGQ,25
|
|
2
|
-
ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
ephys_link/__main__.py,sha256=pu7QLmS_30qWpgzeibMVD5FsVhICiK0wi7QkzX6F0qU,1373
|
|
4
|
-
ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
ephys_link/back_end/platform_handler.py,sha256=5GfyLz8rX8VYypuT9JJokV12mnyUksriFOzRgp6H4W8,12939
|
|
6
|
-
ephys_link/back_end/server.py,sha256=WP5NLJnIKtkVImdRgtZKxWEghd17FjsmpuczYpoFs3Q,7965
|
|
7
|
-
ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
ephys_link/bindings/fake_bindings.py,sha256=_ZpXx4whztbO5jNGNqwoJFdqhIIbZ0VMx4mHkMge1r0,1726
|
|
9
|
-
ephys_link/bindings/ump_4_bindings.py,sha256=1V_bQzq7xVX1qGxbK11nxX7X3L3cfh9RTdhbVJwBKg8,4651
|
|
10
|
-
ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
ephys_link/front_end/cli.py,sha256=KJBSWqdz4T5z0Zor1tJSHTJKZeMcHAJf5gXXu38wQPU,3105
|
|
12
|
-
ephys_link/front_end/gui.py,sha256=_gE6zFhFnzHPSyYd9MBvfK5xmDZHsUXcETDHzH66QzU,7518
|
|
13
|
-
ephys_link/resources/CP210xManufacturing.dll,sha256=aM9k_XABjkq0TOMiIw8HeteB40zqEkUDNO8wo91EdYI,810232
|
|
14
|
-
ephys_link/resources/NstMotorCtrl.dll,sha256=Xtpr3vBcxhcsOUGvgVEwYtGPvKEqDctIUGCK36GfU2Q,155136
|
|
15
|
-
ephys_link/resources/SiUSBXp.dll,sha256=187zlclZNNezCkU1o1CbICRAmKWJxbh8ahP6L6wo-_Y,469752
|
|
16
|
-
ephys_link/resources/libum.dll,sha256=YaD4dwiSNohx-XxHjx2eQWPOBEVvUIXARvx37e_yqNw,316316
|
|
17
|
-
ephys_link/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
ephys_link/util/base_bindings.py,sha256=FNqhB7kJf2n9g4wYHEBbQ2wEiZscR5sKCnIEZ6yujgE,4808
|
|
19
|
-
ephys_link/util/common.py,sha256=IgmTpXRU2z7jfYV2zB4RGgjV34IaYa2s3TYpzKZkpao,3519
|
|
20
|
-
ephys_link/util/console.py,sha256=kD4LLehdA1xLiGwdvjt5KxImDSyDkcoC2CQVOxTv8T4,3560
|
|
21
|
-
ephys_link-2.0.0b1.dist-info/METADATA,sha256=j_XqMK_IK955IOE0ikj_JhqschZGCOKpgMiWdGzTrQQ,7975
|
|
22
|
-
ephys_link-2.0.0b1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
23
|
-
ephys_link-2.0.0b1.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
|
|
24
|
-
ephys_link-2.0.0b1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
25
|
-
ephys_link-2.0.0b1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|