ephys-link 2.0.0__py3-none-any.whl → 2.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ephys_link/__about__.py +1 -1
- ephys_link/__main__.py +43 -51
- ephys_link/back_end/platform_handler.py +298 -315
- ephys_link/back_end/server.py +200 -274
- ephys_link/bindings/{fake_binding.py → fake_bindings.py} +54 -84
- ephys_link/bindings/ump_4_bindings.py +127 -0
- ephys_link/front_end/cli.py +98 -104
- ephys_link/front_end/gui.py +215 -204
- ephys_link/resources/CP210xManufacturing.dll +0 -0
- ephys_link/resources/NstMotorCtrl.dll +0 -0
- ephys_link/resources/SiUSBXp.dll +0 -0
- ephys_link/{utils/base_binding.py → util/base_bindings.py} +133 -176
- ephys_link/util/common.py +121 -0
- ephys_link/util/console.py +112 -0
- ephys_link-2.0.0b1.dist-info/METADATA +166 -0
- ephys_link-2.0.0b1.dist-info/RECORD +25 -0
- {ephys_link-2.0.0.dist-info → ephys_link-2.0.0b1.dist-info}/WHEEL +1 -1
- {ephys_link-2.0.0.dist-info → ephys_link-2.0.0b1.dist-info}/licenses/LICENSE +674 -674
- ephys_link/bindings/mpm_binding.py +0 -315
- ephys_link/bindings/ump_4_binding.py +0 -157
- ephys_link/utils/console.py +0 -127
- ephys_link/utils/constants.py +0 -23
- ephys_link/utils/converters.py +0 -86
- ephys_link/utils/startup.py +0 -65
- ephys_link-2.0.0.dist-info/METADATA +0 -91
- ephys_link-2.0.0.dist-info/RECORD +0 -25
- /ephys_link/{utils → util}/__init__.py +0 -0
- {ephys_link-2.0.0.dist-info → ephys_link-2.0.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
"""Bindings for New Scale Pathfinder MPM HTTP server platform.
|
|
2
|
-
|
|
3
|
-
Usage: Instantiate MPMBindings to interact with the New Scale Pathfinder MPM HTTP server platform.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from asyncio import get_running_loop, sleep
|
|
7
|
-
from json import dumps
|
|
8
|
-
from typing import Any, final, override
|
|
9
|
-
|
|
10
|
-
from requests import JSONDecodeError, get, put
|
|
11
|
-
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
12
|
-
|
|
13
|
-
from ephys_link.utils.base_binding import BaseBinding
|
|
14
|
-
from ephys_link.utils.converters import scalar_mm_to_um, vector4_to_array
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@final
|
|
18
|
-
class MPMBinding(BaseBinding):
|
|
19
|
-
"""Bindings for New Scale Pathfinder MPM HTTP server platform."""
|
|
20
|
-
|
|
21
|
-
# Valid New Scale manipulator IDs
|
|
22
|
-
VALID_MANIPULATOR_IDS = (
|
|
23
|
-
"A",
|
|
24
|
-
"B",
|
|
25
|
-
"C",
|
|
26
|
-
"D",
|
|
27
|
-
"E",
|
|
28
|
-
"F",
|
|
29
|
-
"G",
|
|
30
|
-
"H",
|
|
31
|
-
"I",
|
|
32
|
-
"J",
|
|
33
|
-
"K",
|
|
34
|
-
"L",
|
|
35
|
-
"M",
|
|
36
|
-
"N",
|
|
37
|
-
"O",
|
|
38
|
-
"P",
|
|
39
|
-
"Q",
|
|
40
|
-
"R",
|
|
41
|
-
"S",
|
|
42
|
-
"T",
|
|
43
|
-
"U",
|
|
44
|
-
"V",
|
|
45
|
-
"W",
|
|
46
|
-
"X",
|
|
47
|
-
"Y",
|
|
48
|
-
"Z",
|
|
49
|
-
"AA",
|
|
50
|
-
"AB",
|
|
51
|
-
"AC",
|
|
52
|
-
"AD",
|
|
53
|
-
"AE",
|
|
54
|
-
"AF",
|
|
55
|
-
"AG",
|
|
56
|
-
"AH",
|
|
57
|
-
"AI",
|
|
58
|
-
"AJ",
|
|
59
|
-
"AK",
|
|
60
|
-
"AL",
|
|
61
|
-
"AM",
|
|
62
|
-
"AN",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Server cache lifetime (60 FPS).
|
|
66
|
-
CACHE_LIFETIME = 1 / 60
|
|
67
|
-
|
|
68
|
-
# Movement polling preferences.
|
|
69
|
-
UNCHANGED_COUNTER_LIMIT = 10
|
|
70
|
-
POLL_INTERVAL = 0.1
|
|
71
|
-
|
|
72
|
-
# Speed preferences (mm/s to use coarse mode).
|
|
73
|
-
COARSE_SPEED_THRESHOLD = 0.1
|
|
74
|
-
INSERTION_SPEED_LIMIT = 9_000
|
|
75
|
-
|
|
76
|
-
def __init__(self, port: int = 8080) -> None:
|
|
77
|
-
"""Initialize connection to MPM HTTP server.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
port: Port number for MPM HTTP server.
|
|
81
|
-
"""
|
|
82
|
-
self._url = f"http://localhost:{port}"
|
|
83
|
-
self._movement_stopped = False
|
|
84
|
-
|
|
85
|
-
# Data cache.
|
|
86
|
-
self.cache: dict[str, Any] = {} # pyright: ignore [reportExplicitAny]
|
|
87
|
-
self.cache_time = 0
|
|
88
|
-
|
|
89
|
-
@staticmethod
|
|
90
|
-
@override
|
|
91
|
-
def get_display_name() -> str:
|
|
92
|
-
return "Pathfinder MPM Control v2.8.8+"
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
@override
|
|
96
|
-
def get_cli_name() -> str:
|
|
97
|
-
return "pathfinder-mpm"
|
|
98
|
-
|
|
99
|
-
@override
|
|
100
|
-
async def get_manipulators(self) -> list[str]:
|
|
101
|
-
return [manipulator["Id"] for manipulator in (await self._query_data())["ProbeArray"]] # pyright: ignore [reportAny]
|
|
102
|
-
|
|
103
|
-
@override
|
|
104
|
-
async def get_axes_count(self) -> int:
|
|
105
|
-
return 3
|
|
106
|
-
|
|
107
|
-
@override
|
|
108
|
-
def get_dimensions(self) -> Vector4:
|
|
109
|
-
return Vector4(x=15, y=15, z=15, w=15)
|
|
110
|
-
|
|
111
|
-
@override
|
|
112
|
-
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
113
|
-
manipulator_data: dict[str, float] = await self._manipulator_data(manipulator_id)
|
|
114
|
-
stage_z: float = manipulator_data["Stage_Z"]
|
|
115
|
-
|
|
116
|
-
await sleep(self.POLL_INTERVAL) # Wait for the stage to stabilize.
|
|
117
|
-
|
|
118
|
-
return Vector4(
|
|
119
|
-
x=manipulator_data["Stage_X"],
|
|
120
|
-
y=manipulator_data["Stage_Y"],
|
|
121
|
-
z=stage_z,
|
|
122
|
-
w=stage_z,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
@override
|
|
126
|
-
async def get_angles(self, manipulator_id: str) -> Vector3:
|
|
127
|
-
manipulator_data: dict[str, float] = await self._manipulator_data(manipulator_id)
|
|
128
|
-
|
|
129
|
-
# Apply PosteriorAngle to Polar to get the correct angle.
|
|
130
|
-
adjusted_polar: int = manipulator_data["Polar"] - (await self._query_data())["PosteriorAngle"]
|
|
131
|
-
|
|
132
|
-
return Vector3(
|
|
133
|
-
x=adjusted_polar if adjusted_polar > 0 else 360 + adjusted_polar,
|
|
134
|
-
y=manipulator_data["Pitch"],
|
|
135
|
-
z=manipulator_data["ShankOrientation"],
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
@override
|
|
139
|
-
async def get_shank_count(self, manipulator_id: str) -> int:
|
|
140
|
-
return int((await self._manipulator_data(manipulator_id))["ShankCount"]) # pyright: ignore [reportAny]
|
|
141
|
-
|
|
142
|
-
@override
|
|
143
|
-
def get_movement_tolerance(self) -> float:
|
|
144
|
-
return 0.01
|
|
145
|
-
|
|
146
|
-
@override
|
|
147
|
-
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
148
|
-
# Keep track of the previous position to check if the manipulator stopped advancing.
|
|
149
|
-
current_position = await self.get_position(manipulator_id)
|
|
150
|
-
previous_position = current_position
|
|
151
|
-
unchanged_counter = 0
|
|
152
|
-
|
|
153
|
-
# Set step mode based on speed.
|
|
154
|
-
await self._put_request(
|
|
155
|
-
{
|
|
156
|
-
"PutId": "ProbeStepMode",
|
|
157
|
-
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
158
|
-
"StepMode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1,
|
|
159
|
-
}
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Send move request.
|
|
163
|
-
await self._put_request(
|
|
164
|
-
{
|
|
165
|
-
"PutId": "ProbeMotion",
|
|
166
|
-
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
167
|
-
"Absolute": 1,
|
|
168
|
-
"Stereotactic": 0,
|
|
169
|
-
"AxisMask": 7,
|
|
170
|
-
"X": position.x,
|
|
171
|
-
"Y": position.y,
|
|
172
|
-
"Z": position.z,
|
|
173
|
-
}
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
# Wait for the manipulator to reach the target position or be stopped or stuck.
|
|
177
|
-
while (
|
|
178
|
-
not self._movement_stopped
|
|
179
|
-
and not self._is_vector_close(current_position, position)
|
|
180
|
-
and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
|
|
181
|
-
):
|
|
182
|
-
# Wait for a short time before checking again.
|
|
183
|
-
await sleep(self.POLL_INTERVAL)
|
|
184
|
-
|
|
185
|
-
# Update current position.
|
|
186
|
-
current_position = await self.get_position(manipulator_id)
|
|
187
|
-
|
|
188
|
-
# Check if manipulator is not moving.
|
|
189
|
-
if self._is_vector_close(previous_position, current_position):
|
|
190
|
-
# Position did not change.
|
|
191
|
-
unchanged_counter += 1
|
|
192
|
-
else:
|
|
193
|
-
# Position changed.
|
|
194
|
-
unchanged_counter = 0
|
|
195
|
-
previous_position = current_position
|
|
196
|
-
|
|
197
|
-
# Reset movement stopped flag.
|
|
198
|
-
self._movement_stopped = False
|
|
199
|
-
|
|
200
|
-
# Return the final position.
|
|
201
|
-
return await self.get_position(manipulator_id)
|
|
202
|
-
|
|
203
|
-
@override
|
|
204
|
-
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
205
|
-
# Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly.
|
|
206
|
-
current_depth = (await self.get_position(manipulator_id)).w
|
|
207
|
-
previous_depth = current_depth
|
|
208
|
-
unchanged_counter = 0
|
|
209
|
-
|
|
210
|
-
# Send move request.
|
|
211
|
-
# Convert mm/s to um/min and cap speed at the limit.
|
|
212
|
-
await self._put_request(
|
|
213
|
-
{
|
|
214
|
-
"PutId": "ProbeInsertion",
|
|
215
|
-
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
216
|
-
"Distance": scalar_mm_to_um(current_depth - depth),
|
|
217
|
-
"Rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT),
|
|
218
|
-
}
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
# Wait for the manipulator to reach the target depth or be stopped or get stuck.
|
|
222
|
-
while not self._movement_stopped and not abs(current_depth - depth) <= self.get_movement_tolerance():
|
|
223
|
-
# Wait for a short time before checking again.
|
|
224
|
-
await sleep(self.POLL_INTERVAL)
|
|
225
|
-
|
|
226
|
-
# Get the current depth.
|
|
227
|
-
current_depth = (await self.get_position(manipulator_id)).w
|
|
228
|
-
|
|
229
|
-
# Check if manipulator is not moving.
|
|
230
|
-
if abs(previous_depth - current_depth) <= self.get_movement_tolerance():
|
|
231
|
-
# Depth did not change.
|
|
232
|
-
unchanged_counter += 1
|
|
233
|
-
else:
|
|
234
|
-
# Depth changed.
|
|
235
|
-
unchanged_counter = 0
|
|
236
|
-
previous_depth = current_depth
|
|
237
|
-
|
|
238
|
-
# Reset movement stopped flag.
|
|
239
|
-
self._movement_stopped = False
|
|
240
|
-
|
|
241
|
-
# Return the final depth.
|
|
242
|
-
return float((await self.get_position(manipulator_id)).w)
|
|
243
|
-
|
|
244
|
-
@override
|
|
245
|
-
async def stop(self, manipulator_id: str) -> None:
|
|
246
|
-
request: dict[str, str | int | float] = {
|
|
247
|
-
"PutId": "ProbeStop",
|
|
248
|
-
"Probe": self.VALID_MANIPULATOR_IDS.index(manipulator_id),
|
|
249
|
-
}
|
|
250
|
-
await self._put_request(request)
|
|
251
|
-
self._movement_stopped = True
|
|
252
|
-
|
|
253
|
-
@override
|
|
254
|
-
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
255
|
-
# unified <- platform
|
|
256
|
-
# +x <- -x
|
|
257
|
-
# +y <- +z
|
|
258
|
-
# +z <- +y
|
|
259
|
-
# +w <- -w
|
|
260
|
-
|
|
261
|
-
return Vector4(
|
|
262
|
-
x=self.get_dimensions().x - platform_space.x,
|
|
263
|
-
y=platform_space.z,
|
|
264
|
-
z=platform_space.y,
|
|
265
|
-
w=self.get_dimensions().w - platform_space.w,
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
@override
|
|
269
|
-
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
270
|
-
# platform <- unified
|
|
271
|
-
# +x <- -x
|
|
272
|
-
# +y <- +z
|
|
273
|
-
# +z <- +y
|
|
274
|
-
# +w <- -w
|
|
275
|
-
|
|
276
|
-
return Vector4(
|
|
277
|
-
x=self.get_dimensions().x - unified_space.x,
|
|
278
|
-
y=unified_space.z,
|
|
279
|
-
z=unified_space.y,
|
|
280
|
-
w=self.get_dimensions().w - unified_space.w,
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
# Helper functions.
|
|
284
|
-
async def _query_data(self) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
|
|
285
|
-
try:
|
|
286
|
-
# Update cache if it's expired.
|
|
287
|
-
if get_running_loop().time() - self.cache_time > self.CACHE_LIFETIME:
|
|
288
|
-
# noinspection PyTypeChecker
|
|
289
|
-
self.cache = (await get_running_loop().run_in_executor(None, get, self._url)).json()
|
|
290
|
-
self.cache_time = get_running_loop().time()
|
|
291
|
-
except ConnectionError as connectionError:
|
|
292
|
-
error_message = f"Unable to connect to MPM HTTP server: {connectionError}"
|
|
293
|
-
raise RuntimeError(error_message) from connectionError
|
|
294
|
-
except JSONDecodeError as jsonDecodeError:
|
|
295
|
-
error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}"
|
|
296
|
-
raise ValueError(error_message) from jsonDecodeError
|
|
297
|
-
else:
|
|
298
|
-
# Return cached data.
|
|
299
|
-
return self.cache
|
|
300
|
-
|
|
301
|
-
async def _manipulator_data(self, manipulator_id: str) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
|
|
302
|
-
probe_data: list[dict[str, Any]] = (await self._query_data())["ProbeArray"] # pyright: ignore [reportExplicitAny]
|
|
303
|
-
for probe in probe_data:
|
|
304
|
-
if probe["Id"] == manipulator_id:
|
|
305
|
-
return probe
|
|
306
|
-
|
|
307
|
-
# If we get here, that means the manipulator doesn't exist.
|
|
308
|
-
error_message = f"Manipulator {manipulator_id} not found."
|
|
309
|
-
raise ValueError(error_message)
|
|
310
|
-
|
|
311
|
-
async def _put_request(self, request: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny]
|
|
312
|
-
_ = await get_running_loop().run_in_executor(None, put, self._url, dumps(request))
|
|
313
|
-
|
|
314
|
-
def _is_vector_close(self, target: Vector4, current: Vector4) -> bool:
|
|
315
|
-
return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3])
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"""Bindings for Sensapex uMp-4 platform.
|
|
2
|
-
|
|
3
|
-
Usage: Instantiate Ump4Bindings to interact with the Sensapex uMp-4 platform.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from asyncio import get_running_loop
|
|
7
|
-
from typing import NoReturn, final, override
|
|
8
|
-
|
|
9
|
-
from sensapex import UMP, SensapexDevice # pyright: ignore [reportMissingTypeStubs]
|
|
10
|
-
from vbl_aquarium.models.unity import Vector4
|
|
11
|
-
|
|
12
|
-
from ephys_link.utils.base_binding import BaseBinding
|
|
13
|
-
from ephys_link.utils.constants import RESOURCES_DIRECTORY
|
|
14
|
-
from ephys_link.utils.converters import (
|
|
15
|
-
list_to_vector4,
|
|
16
|
-
scalar_mm_to_um,
|
|
17
|
-
um_to_mm,
|
|
18
|
-
vector4_to_array,
|
|
19
|
-
vector_mm_to_um,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@final
|
|
24
|
-
class Ump4Binding(BaseBinding):
|
|
25
|
-
"""Bindings for UMP-4 platform"""
|
|
26
|
-
|
|
27
|
-
def __init__(self) -> None:
|
|
28
|
-
"""Initialize UMP-4 bindings."""
|
|
29
|
-
|
|
30
|
-
# Establish connection to Sensapex API (exit if connection fails).
|
|
31
|
-
UMP.set_library_path(RESOURCES_DIRECTORY)
|
|
32
|
-
self._ump = UMP.get_ump() # pyright: ignore [reportUnknownMemberType]
|
|
33
|
-
|
|
34
|
-
@staticmethod
|
|
35
|
-
@override
|
|
36
|
-
def get_display_name() -> str:
|
|
37
|
-
return "Sensapex uMp-4"
|
|
38
|
-
|
|
39
|
-
@staticmethod
|
|
40
|
-
@override
|
|
41
|
-
def get_cli_name() -> str:
|
|
42
|
-
return "ump-4"
|
|
43
|
-
|
|
44
|
-
@override
|
|
45
|
-
async def get_manipulators(self) -> list[str]:
|
|
46
|
-
return list(map(str, self._ump.list_devices()))
|
|
47
|
-
|
|
48
|
-
@override
|
|
49
|
-
async def get_axes_count(self) -> int:
|
|
50
|
-
return 4
|
|
51
|
-
|
|
52
|
-
@override
|
|
53
|
-
def get_dimensions(self) -> Vector4:
|
|
54
|
-
return Vector4(x=20, y=20, z=20, w=20)
|
|
55
|
-
|
|
56
|
-
@override
|
|
57
|
-
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
58
|
-
return um_to_mm(list_to_vector4(self._get_device(manipulator_id).get_pos(1))) # pyright: ignore [reportUnknownMemberType]
|
|
59
|
-
|
|
60
|
-
@override
|
|
61
|
-
async def get_angles(self, manipulator_id: str) -> NoReturn:
|
|
62
|
-
"""uMp-4 does not support getting angles so raise an error.
|
|
63
|
-
|
|
64
|
-
Raises:
|
|
65
|
-
AttributeError: uMp-4 does not support getting angles.
|
|
66
|
-
"""
|
|
67
|
-
error_message = "UMP-4 does not support getting angles"
|
|
68
|
-
raise AttributeError(error_message)
|
|
69
|
-
|
|
70
|
-
@override
|
|
71
|
-
async def get_shank_count(self, manipulator_id: str) -> NoReturn:
|
|
72
|
-
"""uMp-4 does not support getting shank count so raise an error.
|
|
73
|
-
|
|
74
|
-
Raises:
|
|
75
|
-
AttributeError: uMp-4 does not support getting shank count.
|
|
76
|
-
"""
|
|
77
|
-
error_message = "UMP-4 does not support getting shank count"
|
|
78
|
-
raise AttributeError(error_message)
|
|
79
|
-
|
|
80
|
-
@override
|
|
81
|
-
def get_movement_tolerance(self) -> float:
|
|
82
|
-
return 0.001
|
|
83
|
-
|
|
84
|
-
@override
|
|
85
|
-
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
86
|
-
# Convert position to micrometers.
|
|
87
|
-
target_position_um = vector_mm_to_um(position)
|
|
88
|
-
|
|
89
|
-
# Request movement.
|
|
90
|
-
movement = self._get_device(manipulator_id).goto_pos( # pyright: ignore [reportUnknownMemberType]
|
|
91
|
-
vector4_to_array(target_position_um), scalar_mm_to_um(speed)
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# Wait for movement to finish.
|
|
95
|
-
_ = await get_running_loop().run_in_executor(None, movement.finished_event.wait, None)
|
|
96
|
-
|
|
97
|
-
# Handle interrupted movement.
|
|
98
|
-
if movement.interrupted:
|
|
99
|
-
error_message = f"Manipulator {manipulator_id} interrupted: {movement.interrupt_reason}" # pyright: ignore [reportUnknownMemberType]
|
|
100
|
-
raise RuntimeError(error_message)
|
|
101
|
-
|
|
102
|
-
# Handle empty end position.
|
|
103
|
-
if movement.last_pos is None or len(movement.last_pos) == 0: # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType]
|
|
104
|
-
error_message = f"Manipulator {manipulator_id} did not reach target position"
|
|
105
|
-
raise RuntimeError(error_message)
|
|
106
|
-
|
|
107
|
-
return um_to_mm(list_to_vector4(movement.last_pos)) # pyright: ignore [reportArgumentType, reportUnknownMemberType]
|
|
108
|
-
|
|
109
|
-
@override
|
|
110
|
-
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
111
|
-
# Augment current position with depth.
|
|
112
|
-
current_position = await self.get_position(manipulator_id)
|
|
113
|
-
new_platform_position = current_position.model_copy(update={"w": depth})
|
|
114
|
-
|
|
115
|
-
# Make the movement.
|
|
116
|
-
final_platform_position = await self.set_position(manipulator_id, new_platform_position, speed)
|
|
117
|
-
|
|
118
|
-
# Return the final depth.
|
|
119
|
-
return float(final_platform_position.w)
|
|
120
|
-
|
|
121
|
-
@override
|
|
122
|
-
async def stop(self, manipulator_id: str) -> None:
|
|
123
|
-
self._get_device(manipulator_id).stop()
|
|
124
|
-
|
|
125
|
-
@override
|
|
126
|
-
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
127
|
-
# unified <- platform
|
|
128
|
-
# +x <- +y
|
|
129
|
-
# +y <- -z
|
|
130
|
-
# +z <- +x
|
|
131
|
-
# +d <- +d
|
|
132
|
-
|
|
133
|
-
return Vector4(
|
|
134
|
-
x=platform_space.y,
|
|
135
|
-
y=self.get_dimensions().z - platform_space.z,
|
|
136
|
-
z=platform_space.x,
|
|
137
|
-
w=platform_space.w,
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
@override
|
|
141
|
-
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
142
|
-
# platform <- unified
|
|
143
|
-
# +x <- +z
|
|
144
|
-
# +y <- +x
|
|
145
|
-
# +z <- -y
|
|
146
|
-
# +d <- +d
|
|
147
|
-
|
|
148
|
-
return Vector4(
|
|
149
|
-
x=unified_space.z,
|
|
150
|
-
y=unified_space.x,
|
|
151
|
-
z=self.get_dimensions().z - unified_space.y,
|
|
152
|
-
w=unified_space.w,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Helper methods.
|
|
156
|
-
def _get_device(self, manipulator_id: str) -> SensapexDevice:
|
|
157
|
-
return self._ump.get_device(int(manipulator_id)) # pyright: ignore [reportUnknownMemberType]
|
ephys_link/utils/console.py
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
"""Console class for printing messages to the console.
|
|
2
|
-
|
|
3
|
-
Configure the console to print error and debug messages.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
Create a Console object and call the appropriate method to print messages.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from logging import DEBUG, ERROR, INFO, basicConfig, getLogger
|
|
10
|
-
from typing import final
|
|
11
|
-
|
|
12
|
-
from rich.logging import RichHandler
|
|
13
|
-
from rich.traceback import install
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@final
|
|
17
|
-
class Console:
|
|
18
|
-
def __init__(self, *, enable_debug: bool) -> None:
|
|
19
|
-
"""Initialize console properties.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
enable_debug: Enable debug mode.
|
|
23
|
-
"""
|
|
24
|
-
# Repeat message fields.
|
|
25
|
-
self._last_message = (0, "", "")
|
|
26
|
-
self._repeat_counter = 0
|
|
27
|
-
|
|
28
|
-
# Config logger.
|
|
29
|
-
basicConfig(
|
|
30
|
-
format="%(message)s",
|
|
31
|
-
datefmt="[%I:%M:%S %p]",
|
|
32
|
-
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
|
|
33
|
-
)
|
|
34
|
-
self._log = getLogger("rich")
|
|
35
|
-
self._log.setLevel(DEBUG if enable_debug else INFO)
|
|
36
|
-
|
|
37
|
-
# Install Rich traceback.
|
|
38
|
-
_ = install()
|
|
39
|
-
|
|
40
|
-
def debug_print(self, label: str, msg: str) -> None:
|
|
41
|
-
"""Print a debug message to the console.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
label: Label for the debug message.
|
|
45
|
-
msg: Debug message to print.
|
|
46
|
-
"""
|
|
47
|
-
self._repeatable_log(DEBUG, f"[b green]{label}", f"[green]{msg}")
|
|
48
|
-
|
|
49
|
-
def info_print(self, label: str, msg: str) -> None:
|
|
50
|
-
"""Print info to console.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
label: Label for the message.
|
|
54
|
-
msg: Message to print.
|
|
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.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
label: Label for the error message.
|
|
63
|
-
msg: Error message to print.
|
|
64
|
-
"""
|
|
65
|
-
self._repeatable_log(ERROR, f"[b red]{label}", f"[red]{msg}")
|
|
66
|
-
|
|
67
|
-
def critical_print(self, msg: str) -> None:
|
|
68
|
-
"""Print a critical message to the console.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
msg: Critical message to print.
|
|
72
|
-
"""
|
|
73
|
-
self._log.critical(f"[b i red]{msg}")
|
|
74
|
-
|
|
75
|
-
@staticmethod
|
|
76
|
-
def pretty_exception(exception: Exception) -> str:
|
|
77
|
-
"""Pretty print an exception.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
exception: Exception to pretty print.
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
Pretty printed exception.
|
|
84
|
-
"""
|
|
85
|
-
return f"{type(exception).__name__}: {exception}"
|
|
86
|
-
|
|
87
|
-
def exception_error_print(self, label: str, exception: Exception) -> None:
|
|
88
|
-
"""Print an error message with exception details to the console.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
label: Label for the error message.
|
|
92
|
-
exception: Exception to print.
|
|
93
|
-
"""
|
|
94
|
-
self._log.exception(f"[b magenta]{label}:[/] [magenta]{Console.pretty_exception(exception)}")
|
|
95
|
-
|
|
96
|
-
# Helper methods.
|
|
97
|
-
def _repeatable_log(self, log_type: int, label: str, message: str) -> None:
|
|
98
|
-
"""Add a row to the output table.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
log_type: Type of log.
|
|
102
|
-
label: Label for the message.
|
|
103
|
-
message: Message.
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
# Compute if this is a repeated message.
|
|
107
|
-
message_set = (log_type, label, message)
|
|
108
|
-
if message_set == self._last_message:
|
|
109
|
-
# Handle repeat.
|
|
110
|
-
self._repeat_counter += 1
|
|
111
|
-
|
|
112
|
-
# Add an ellipsis row for first repeat.
|
|
113
|
-
if self._repeat_counter == 1:
|
|
114
|
-
self._log.log(log_type, "...")
|
|
115
|
-
else:
|
|
116
|
-
# Handle novel message.
|
|
117
|
-
if self._repeat_counter > 0:
|
|
118
|
-
# Complete previous repeat.
|
|
119
|
-
self._log.log(
|
|
120
|
-
self._last_message[0],
|
|
121
|
-
f"{self._last_message[1]}:[/] {self._last_message[2]}[/] x {self._repeat_counter}",
|
|
122
|
-
)
|
|
123
|
-
self._repeat_counter = 0
|
|
124
|
-
|
|
125
|
-
# Log new message.
|
|
126
|
-
self._log.log(log_type, f"{label}:[/] {message}")
|
|
127
|
-
self._last_message = message_set
|
ephys_link/utils/constants.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Globally accessible constants"""
|
|
2
|
-
|
|
3
|
-
from os.path import abspath, dirname, join
|
|
4
|
-
|
|
5
|
-
# Ephys Link ASCII.
|
|
6
|
-
ASCII = r"""
|
|
7
|
-
______ _ _ _ _
|
|
8
|
-
| ____| | | | | (_) | |
|
|
9
|
-
| |__ _ __ | |__ _ _ ___ | | _ _ __ | | __
|
|
10
|
-
| __| | '_ \| '_ \| | | / __| | | | | '_ \| |/ /
|
|
11
|
-
| |____| |_) | | | | |_| \__ \ | |____| | | | | <
|
|
12
|
-
|______| .__/|_| |_|\__, |___/ |______|_|_| |_|_|\_\
|
|
13
|
-
| | __/ |
|
|
14
|
-
|_| |___/
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
# Absolute path to the resource folder.
|
|
18
|
-
PACKAGE_DIRECTORY = dirname(dirname(abspath(__file__)))
|
|
19
|
-
RESOURCES_DIRECTORY = join(PACKAGE_DIRECTORY, "resources")
|
|
20
|
-
BINDINGS_DIRECTORY = join(PACKAGE_DIRECTORY, "bindings")
|
|
21
|
-
|
|
22
|
-
# Ephys Link Port
|
|
23
|
-
PORT = 3000
|