ephys-link 1.3.3__py3-none-any.whl → 2.0.0__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 +51 -105
- ephys_link/back_end/__init__.py +0 -0
- ephys_link/back_end/platform_handler.py +315 -0
- ephys_link/back_end/server.py +274 -0
- ephys_link/bindings/__init__.py +0 -0
- ephys_link/bindings/fake_binding.py +84 -0
- ephys_link/bindings/mpm_binding.py +315 -0
- ephys_link/bindings/ump_4_binding.py +157 -0
- ephys_link/front_end/__init__.py +0 -0
- ephys_link/front_end/cli.py +104 -0
- ephys_link/front_end/gui.py +204 -0
- ephys_link/utils/__init__.py +0 -0
- ephys_link/utils/base_binding.py +176 -0
- ephys_link/utils/console.py +127 -0
- ephys_link/utils/constants.py +23 -0
- ephys_link/utils/converters.py +86 -0
- ephys_link/utils/startup.py +65 -0
- ephys_link-2.0.0.dist-info/METADATA +91 -0
- ephys_link-2.0.0.dist-info/RECORD +25 -0
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/WHEEL +1 -1
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/licenses/LICENSE +674 -674
- ephys_link/common.py +0 -49
- ephys_link/emergency_stop.py +0 -67
- ephys_link/gui.py +0 -217
- ephys_link/platform_handler.py +0 -465
- ephys_link/platform_manipulator.py +0 -35
- ephys_link/platforms/__init__.py +0 -5
- ephys_link/platforms/new_scale_handler.py +0 -141
- ephys_link/platforms/new_scale_manipulator.py +0 -312
- ephys_link/platforms/new_scale_pathfinder_handler.py +0 -235
- ephys_link/platforms/sensapex_handler.py +0 -151
- ephys_link/platforms/sensapex_manipulator.py +0 -227
- ephys_link/platforms/ump3_handler.py +0 -57
- ephys_link/platforms/ump3_manipulator.py +0 -147
- ephys_link/resources/CP210xManufacturing.dll +0 -0
- ephys_link/resources/NstMotorCtrl.dll +0 -0
- ephys_link/resources/SiUSBXp.dll +0 -0
- ephys_link/server.py +0 -508
- ephys_link-1.3.3.dist-info/METADATA +0 -164
- ephys_link-1.3.3.dist-info/RECORD +0 -26
- {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
"""New Scale Manipulator class
|
|
2
|
-
|
|
3
|
-
Handles logic for calling New Scale API functions. Also includes extra logic for safe
|
|
4
|
-
function calls, error handling, managing per-manipulator attributes, and returning the
|
|
5
|
-
appropriate callback parameters like in :mod:`ephys_link.new_scale_handler`.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
import threading
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
13
|
-
|
|
14
|
-
from vbl_aquarium.models.ephys_link import (
|
|
15
|
-
CanWriteRequest,
|
|
16
|
-
DriveToDepthRequest,
|
|
17
|
-
DriveToDepthResponse,
|
|
18
|
-
GotoPositionRequest,
|
|
19
|
-
PositionalResponse,
|
|
20
|
-
)
|
|
21
|
-
from vbl_aquarium.models.unity import Vector4
|
|
22
|
-
|
|
23
|
-
import ephys_link.common as com
|
|
24
|
-
from ephys_link.common import vector4_to_array
|
|
25
|
-
from ephys_link.platform_manipulator import (
|
|
26
|
-
HOURS_TO_SECONDS,
|
|
27
|
-
MM_TO_UM,
|
|
28
|
-
POSITION_POLL_DELAY,
|
|
29
|
-
PlatformManipulator,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
# noinspection PyUnresolvedReferences
|
|
34
|
-
from NstMotorCtrl import NstCtrlAxis
|
|
35
|
-
|
|
36
|
-
# Constants
|
|
37
|
-
ACCELERATION_MULTIPLIER = 5
|
|
38
|
-
CUTOFF_MULTIPLIER = 0.005
|
|
39
|
-
AT_TARGET_FLAG = 0x040000
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class NewScaleManipulator(PlatformManipulator):
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
manipulator_id: str,
|
|
46
|
-
x_axis: NstCtrlAxis,
|
|
47
|
-
y_axis: NstCtrlAxis,
|
|
48
|
-
z_axis: NstCtrlAxis,
|
|
49
|
-
) -> None:
|
|
50
|
-
"""Construct a new Manipulator object
|
|
51
|
-
|
|
52
|
-
:param manipulator_id: Manipulator ID
|
|
53
|
-
:param x_axis: X axis object
|
|
54
|
-
:param y_axis: Y axis object
|
|
55
|
-
:param z_axis: Z axis object
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
super().__init__()
|
|
59
|
-
self._id = manipulator_id
|
|
60
|
-
self._movement_tolerance = 0.01
|
|
61
|
-
self._x = x_axis
|
|
62
|
-
self._y = y_axis
|
|
63
|
-
self._z = z_axis
|
|
64
|
-
self._axes = [self._x, self._y, self._z]
|
|
65
|
-
|
|
66
|
-
# Set to CL control
|
|
67
|
-
self._x.SetCL_Enable(True)
|
|
68
|
-
self._y.SetCL_Enable(True)
|
|
69
|
-
self._z.SetCL_Enable(True)
|
|
70
|
-
|
|
71
|
-
def query_all_axes(self):
|
|
72
|
-
"""Query all axes for their position and status"""
|
|
73
|
-
for axis in self._axes:
|
|
74
|
-
axis.QueryPosStatus()
|
|
75
|
-
|
|
76
|
-
def get_pos(self) -> PositionalResponse:
|
|
77
|
-
"""Get the current position of the manipulator and convert it into mm.
|
|
78
|
-
|
|
79
|
-
:return: Position of manipulator in (x, y, z, z) in mm (or an empty array on error) and error message (if any).
|
|
80
|
-
:rtype: :class:`ephys_link.common.PositionalOutputData`
|
|
81
|
-
"""
|
|
82
|
-
self.query_all_axes()
|
|
83
|
-
|
|
84
|
-
# Get position data and convert from µm to mm
|
|
85
|
-
try:
|
|
86
|
-
# com.dprint(f"[SUCCESS]\t Got position of manipulator {self._id}\n")
|
|
87
|
-
return PositionalResponse(
|
|
88
|
-
position=Vector4(
|
|
89
|
-
x=self._x.CurPosition / MM_TO_UM,
|
|
90
|
-
y=self._y.CurPosition / MM_TO_UM,
|
|
91
|
-
z=self._z.CurPosition / MM_TO_UM,
|
|
92
|
-
w=self._z.CurPosition / MM_TO_UM,
|
|
93
|
-
)
|
|
94
|
-
)
|
|
95
|
-
except Exception as e:
|
|
96
|
-
print(f"[ERROR]\t\t Getting position of manipulator {self._id}")
|
|
97
|
-
print(f"{e}\n")
|
|
98
|
-
return PositionalResponse(error="Error getting position")
|
|
99
|
-
|
|
100
|
-
async def goto_pos(self, request: GotoPositionRequest) -> PositionalResponse:
|
|
101
|
-
"""Move manipulator to position.
|
|
102
|
-
|
|
103
|
-
:param request: The goto request parsed from the server.
|
|
104
|
-
:type request: :class:`vbl_aquarium.models.ephys_link.GotoPositionRequest`
|
|
105
|
-
:return: Resulting position of manipulator in (x, y, z, z) in mm (or an empty array on error)
|
|
106
|
-
and error message (if any).
|
|
107
|
-
:rtype: :class:`ephys_link.common.PositionalOutputData`
|
|
108
|
-
"""
|
|
109
|
-
# Check if able to write
|
|
110
|
-
if not self._can_write:
|
|
111
|
-
print(f"[ERROR]\t\t Manipulator {self._id} movement canceled")
|
|
112
|
-
return PositionalResponse(error="Manipulator movement canceled")
|
|
113
|
-
|
|
114
|
-
# Stop current movement
|
|
115
|
-
if self._is_moving:
|
|
116
|
-
for axis in self._axes:
|
|
117
|
-
axis.Stop()
|
|
118
|
-
self._is_moving = False
|
|
119
|
-
|
|
120
|
-
try:
|
|
121
|
-
target_position_um = request.position * MM_TO_UM
|
|
122
|
-
|
|
123
|
-
# Restrict target position to just z-axis if inside brain
|
|
124
|
-
if self._inside_brain:
|
|
125
|
-
z_axis = target_position_um.z
|
|
126
|
-
target_position_um = target_position_um.model_copy(
|
|
127
|
-
update={**self.get_pos().position.model_dump(), "z": z_axis}
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
# Mark movement as started
|
|
131
|
-
self._is_moving = True
|
|
132
|
-
|
|
133
|
-
# Send move command
|
|
134
|
-
speed_um = request.speed * MM_TO_UM
|
|
135
|
-
for i in range(3):
|
|
136
|
-
self._axes[i].SetCL_Speed(
|
|
137
|
-
speed_um,
|
|
138
|
-
speed_um * ACCELERATION_MULTIPLIER,
|
|
139
|
-
speed_um * CUTOFF_MULTIPLIER,
|
|
140
|
-
)
|
|
141
|
-
self._axes[i].MoveAbsolute(vector4_to_array(target_position_um)[i])
|
|
142
|
-
|
|
143
|
-
# Check and wait for completion (while able to write)
|
|
144
|
-
self.query_all_axes()
|
|
145
|
-
while (
|
|
146
|
-
not (self._x.CurStatus & AT_TARGET_FLAG)
|
|
147
|
-
or not (self._y.CurStatus & AT_TARGET_FLAG)
|
|
148
|
-
or not (self._z.CurStatus & AT_TARGET_FLAG)
|
|
149
|
-
) and self._can_write:
|
|
150
|
-
await asyncio.sleep(POSITION_POLL_DELAY)
|
|
151
|
-
self.query_all_axes()
|
|
152
|
-
|
|
153
|
-
# Get position
|
|
154
|
-
final_position = self.get_pos().position
|
|
155
|
-
|
|
156
|
-
# Mark movement as finished
|
|
157
|
-
self._is_moving = False
|
|
158
|
-
|
|
159
|
-
# Return success unless write was disabled during movement (meaning a stop occurred)
|
|
160
|
-
if not self._can_write:
|
|
161
|
-
com.dprint(f"[ERROR]\t\t Manipulator {self._id} movement canceled")
|
|
162
|
-
return PositionalResponse(error="Manipulator movement canceled")
|
|
163
|
-
|
|
164
|
-
# Return error if movement did not reach target.
|
|
165
|
-
if not all(
|
|
166
|
-
abs(axis) < self._movement_tolerance for axis in vector4_to_array(final_position - request.position)[:3]
|
|
167
|
-
):
|
|
168
|
-
com.dprint(f"[ERROR]\t\t Manipulator {self._id} did not reach target position.")
|
|
169
|
-
com.dprint(f"\t\t\t Expected: {request.position}, Got: {final_position}")
|
|
170
|
-
return PositionalResponse(error="Manipulator did not reach target position")
|
|
171
|
-
|
|
172
|
-
# Made it to the target.
|
|
173
|
-
com.dprint(f"[SUCCESS]\t Moved manipulator {self._id} to position" f" {final_position}\n")
|
|
174
|
-
return PositionalResponse(position=final_position)
|
|
175
|
-
except Exception as e:
|
|
176
|
-
print(f"[ERROR]\t\t Moving manipulator {self._id} to position {request.position}")
|
|
177
|
-
print(f"{e}\n")
|
|
178
|
-
return PositionalResponse(error="Error moving manipulator")
|
|
179
|
-
|
|
180
|
-
async def drive_to_depth(self, request: DriveToDepthRequest) -> DriveToDepthResponse:
|
|
181
|
-
"""Drive the manipulator to a certain depth.
|
|
182
|
-
|
|
183
|
-
:param request: The drive to depth request parsed from the server.
|
|
184
|
-
:type request: :class:`vbl_aquarium.models.ephys_link.DriveToDepthRequest`
|
|
185
|
-
:return: Resulting depth of manipulator in mm (or 0 on error) and error message (if any).
|
|
186
|
-
:rtype: :class:`ephys_link.common.DriveToDepthOutputData`
|
|
187
|
-
"""
|
|
188
|
-
# Check if able to write
|
|
189
|
-
if not self._can_write:
|
|
190
|
-
print(f"[ERROR]\t\t Manipulator {self._id} movement canceled")
|
|
191
|
-
return DriveToDepthResponse(error="Manipulator movement canceled")
|
|
192
|
-
|
|
193
|
-
# Stop current movement
|
|
194
|
-
if self._is_moving:
|
|
195
|
-
for axis in self._axes:
|
|
196
|
-
axis.Stop()
|
|
197
|
-
self._is_moving = False
|
|
198
|
-
|
|
199
|
-
try:
|
|
200
|
-
target_depth_um = request.depth * MM_TO_UM
|
|
201
|
-
|
|
202
|
-
# Mark movement as started
|
|
203
|
-
self._is_moving = True
|
|
204
|
-
|
|
205
|
-
# Send move command to just z axis
|
|
206
|
-
speed_um = request.speed * MM_TO_UM
|
|
207
|
-
self._z.SetCL_Speed(
|
|
208
|
-
speed_um,
|
|
209
|
-
speed_um * ACCELERATION_MULTIPLIER,
|
|
210
|
-
speed_um * CUTOFF_MULTIPLIER,
|
|
211
|
-
)
|
|
212
|
-
self._z.MoveAbsolute(target_depth_um)
|
|
213
|
-
|
|
214
|
-
# Check and wait for completion (while able to write)
|
|
215
|
-
self._z.QueryPosStatus()
|
|
216
|
-
while not (self._z.CurStatus & AT_TARGET_FLAG) and self._can_write:
|
|
217
|
-
await asyncio.sleep(0.1)
|
|
218
|
-
self._z.QueryPosStatus()
|
|
219
|
-
|
|
220
|
-
# Get position
|
|
221
|
-
final_depth = self.get_pos().position.w
|
|
222
|
-
|
|
223
|
-
# Mark movement as finished
|
|
224
|
-
self._is_moving = False
|
|
225
|
-
|
|
226
|
-
# Return success unless write was disabled during movement (meaning a stop occurred)
|
|
227
|
-
if not self._can_write:
|
|
228
|
-
com.dprint(f"[ERROR]\t\t Manipulator {self._id} movement canceled")
|
|
229
|
-
return DriveToDepthResponse(error="Manipulator movement canceled")
|
|
230
|
-
|
|
231
|
-
# Return error if movement did not reach target.
|
|
232
|
-
if not abs(final_depth - request.depth) < self._movement_tolerance:
|
|
233
|
-
com.dprint(f"[ERROR]\t\t Manipulator {self._id} did not reach target depth")
|
|
234
|
-
com.dprint(f"\t\t\t Expected: {request.depth}, Got: {final_depth}")
|
|
235
|
-
return DriveToDepthResponse(error="Manipulator did not reach target depth")
|
|
236
|
-
|
|
237
|
-
# Made it to the target.
|
|
238
|
-
com.dprint(f"[SUCCESS]\t Moved manipulator {self._id} to position" f" {final_depth}\n")
|
|
239
|
-
return DriveToDepthResponse(depth=final_depth)
|
|
240
|
-
except Exception as e:
|
|
241
|
-
print(f"[ERROR]\t\t Moving manipulator {self._id} to depth {request.depth}")
|
|
242
|
-
print(f"{e}\n")
|
|
243
|
-
# Return 0 and error message on failure
|
|
244
|
-
return DriveToDepthResponse(error="Error driving manipulator")
|
|
245
|
-
|
|
246
|
-
def calibrate(self) -> bool:
|
|
247
|
-
"""Calibrate the manipulator.
|
|
248
|
-
|
|
249
|
-
:return: None
|
|
250
|
-
"""
|
|
251
|
-
return self._x.CalibrateFrequency() and self._y.CalibrateFrequency() and self._z.CalibrateFrequency()
|
|
252
|
-
|
|
253
|
-
def get_calibrated(self) -> bool:
|
|
254
|
-
"""Return the calibration state of the manipulator.
|
|
255
|
-
|
|
256
|
-
:return: True if the manipulator is calibrated, False otherwise.
|
|
257
|
-
:rtype: bool
|
|
258
|
-
"""
|
|
259
|
-
return self._calibrated
|
|
260
|
-
|
|
261
|
-
def set_calibrated(self) -> None:
|
|
262
|
-
"""Set the manipulator to calibrated
|
|
263
|
-
|
|
264
|
-
:return: None
|
|
265
|
-
"""
|
|
266
|
-
self._calibrated = True
|
|
267
|
-
|
|
268
|
-
def set_can_write(self, request: CanWriteRequest) -> None:
|
|
269
|
-
"""Set if the manipulator can move.
|
|
270
|
-
|
|
271
|
-
:param request: The can write request parsed from the server.
|
|
272
|
-
:type request: :class:`vbl_aquarium.models.ephys_link.CanWriteRequest`
|
|
273
|
-
:return: None
|
|
274
|
-
"""
|
|
275
|
-
self._can_write = request.can_write
|
|
276
|
-
|
|
277
|
-
if request.can_write and request.hours > 0:
|
|
278
|
-
if self._reset_timer:
|
|
279
|
-
self._reset_timer.cancel()
|
|
280
|
-
self._reset_timer = threading.Timer(request.hours * HOURS_TO_SECONDS, self.reset_can_write)
|
|
281
|
-
self._reset_timer.start()
|
|
282
|
-
|
|
283
|
-
def reset_can_write(self) -> None:
|
|
284
|
-
"""Reset the :attr:`can_write` flag."""
|
|
285
|
-
self._can_write = False
|
|
286
|
-
|
|
287
|
-
def get_can_write(self) -> bool:
|
|
288
|
-
"""Return if the manipulator can move.
|
|
289
|
-
|
|
290
|
-
:return: True if the manipulator can move, False otherwise.
|
|
291
|
-
:rtype: bool
|
|
292
|
-
"""
|
|
293
|
-
return self._can_write
|
|
294
|
-
|
|
295
|
-
def set_inside_brain(self, inside: bool) -> None:
|
|
296
|
-
"""Set if the manipulator is inside the brain.
|
|
297
|
-
|
|
298
|
-
:param inside: True if the manipulator is inside the brain, False otherwise.
|
|
299
|
-
:type inside: bool
|
|
300
|
-
:return: None
|
|
301
|
-
"""
|
|
302
|
-
self._inside_brain = inside
|
|
303
|
-
|
|
304
|
-
def stop(self) -> None:
|
|
305
|
-
"""Stop all axes on manipulator.
|
|
306
|
-
|
|
307
|
-
:returns None
|
|
308
|
-
"""
|
|
309
|
-
for axis in self._axes:
|
|
310
|
-
axis.Stop()
|
|
311
|
-
self._can_write = False
|
|
312
|
-
com.dprint(f"[SUCCESS]\t Stopped manipulator {self._id}\n")
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
"""Handle communications with New Scale's HTTP server
|
|
2
|
-
|
|
3
|
-
Implements New Scale specific API calls.
|
|
4
|
-
|
|
5
|
-
This is a subclass of :class:`ephys_link.platform_handler.PlatformHandler`.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
from sys import exit
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
13
|
-
from urllib import request
|
|
14
|
-
from urllib.error import URLError
|
|
15
|
-
|
|
16
|
-
from vbl_aquarium.models.ephys_link import (
|
|
17
|
-
AngularResponse,
|
|
18
|
-
BooleanStateResponse,
|
|
19
|
-
CanWriteRequest,
|
|
20
|
-
DriveToDepthRequest,
|
|
21
|
-
DriveToDepthResponse,
|
|
22
|
-
GotoPositionRequest,
|
|
23
|
-
InsideBrainRequest,
|
|
24
|
-
PositionalResponse,
|
|
25
|
-
ShankCountResponse,
|
|
26
|
-
)
|
|
27
|
-
from vbl_aquarium.models.unity import Vector3, Vector4
|
|
28
|
-
|
|
29
|
-
from ephys_link.platform_handler import PlatformHandler
|
|
30
|
-
|
|
31
|
-
if TYPE_CHECKING:
|
|
32
|
-
import socketio
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class NewScalePathfinderHandler(PlatformHandler):
|
|
36
|
-
"""Handler for New Scale HTTP server"""
|
|
37
|
-
|
|
38
|
-
# Valid New Scale manipulator IDs
|
|
39
|
-
VALID_MANIPULATOR_IDS = (
|
|
40
|
-
"A",
|
|
41
|
-
"B",
|
|
42
|
-
"C",
|
|
43
|
-
"D",
|
|
44
|
-
"E",
|
|
45
|
-
"F",
|
|
46
|
-
"G",
|
|
47
|
-
"H",
|
|
48
|
-
"I",
|
|
49
|
-
"J",
|
|
50
|
-
"K",
|
|
51
|
-
"L",
|
|
52
|
-
"M",
|
|
53
|
-
"N",
|
|
54
|
-
"O",
|
|
55
|
-
"P",
|
|
56
|
-
"Q",
|
|
57
|
-
"R",
|
|
58
|
-
"S",
|
|
59
|
-
"T",
|
|
60
|
-
"U",
|
|
61
|
-
"V",
|
|
62
|
-
"W",
|
|
63
|
-
"X",
|
|
64
|
-
"Y",
|
|
65
|
-
"Z",
|
|
66
|
-
"AA",
|
|
67
|
-
"AB",
|
|
68
|
-
"AC",
|
|
69
|
-
"AD",
|
|
70
|
-
"AE",
|
|
71
|
-
"AF",
|
|
72
|
-
"AG",
|
|
73
|
-
"AH",
|
|
74
|
-
"AI",
|
|
75
|
-
"AJ",
|
|
76
|
-
"AK",
|
|
77
|
-
"AL",
|
|
78
|
-
"AM",
|
|
79
|
-
"AN",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
def __init__(self, port: int = 8080) -> None:
|
|
83
|
-
"""
|
|
84
|
-
Initialize New Scale via Pathfinder handler
|
|
85
|
-
|
|
86
|
-
:param port: Port of New Scale Pathfinder HTTP server
|
|
87
|
-
:type port: int
|
|
88
|
-
"""
|
|
89
|
-
super().__init__()
|
|
90
|
-
|
|
91
|
-
self.num_axes = -1
|
|
92
|
-
self.dimensions = Vector4(x=15, y=15, z=15, w=0)
|
|
93
|
-
|
|
94
|
-
self.port = port
|
|
95
|
-
|
|
96
|
-
# Test connection to New Scale HTTP server
|
|
97
|
-
try:
|
|
98
|
-
request.urlopen(f"http://localhost:{self.port}")
|
|
99
|
-
except URLError:
|
|
100
|
-
print(f"New Scale Pathfinder HTTP server not online on port {self.port}")
|
|
101
|
-
print("Please start the HTTP server and try again.")
|
|
102
|
-
input("Press Enter to exit...")
|
|
103
|
-
exit(1)
|
|
104
|
-
|
|
105
|
-
def query_data(self) -> dict:
|
|
106
|
-
"""Query New Scale HTTP server for data and return as dict.
|
|
107
|
-
|
|
108
|
-
:return: Parsed JSON data from New Scale HTTP server.
|
|
109
|
-
:rtype: dict
|
|
110
|
-
"""
|
|
111
|
-
try:
|
|
112
|
-
return json.loads(request.urlopen(f"http://localhost:{self.port}").read())
|
|
113
|
-
except Exception as e:
|
|
114
|
-
print(f"[ERROR]\t\t Unable to query for New Scale data: {type(e)} {e}\n")
|
|
115
|
-
|
|
116
|
-
def query_manipulator_data(self, manipulator_id: str) -> dict:
|
|
117
|
-
"""Query New Scale HTTP server for data on a specific manipulator.
|
|
118
|
-
|
|
119
|
-
:param manipulator_id: manipulator ID.
|
|
120
|
-
:return: Parsed JSON data for a particular manipulator.
|
|
121
|
-
:rtype: dict
|
|
122
|
-
:raises ValueError: if manipulator ID is not found in query.
|
|
123
|
-
"""
|
|
124
|
-
data_query = self.query_data()["ProbeArray"]
|
|
125
|
-
manipulator_data = data_query[self.manipulators[manipulator_id]]
|
|
126
|
-
|
|
127
|
-
# If the order of the manipulators switched (somehow)
|
|
128
|
-
if manipulator_data["Id"] != manipulator_id:
|
|
129
|
-
# Recalculate index and get data
|
|
130
|
-
(manipulator_index, manipulator_data) = next(
|
|
131
|
-
(
|
|
132
|
-
(index, data)
|
|
133
|
-
for index, data in enumerate(self.query_data()["ProbeArray"])
|
|
134
|
-
if data["Id"] == manipulator_id
|
|
135
|
-
),
|
|
136
|
-
(None, None),
|
|
137
|
-
)
|
|
138
|
-
# Update index in manipulators dict
|
|
139
|
-
if manipulator_index:
|
|
140
|
-
self.manipulators[manipulator_id] = manipulator_index
|
|
141
|
-
|
|
142
|
-
# If data query was unsuccessful
|
|
143
|
-
if not manipulator_data:
|
|
144
|
-
msg = f"Unable to find manipulator {manipulator_id}"
|
|
145
|
-
raise ValueError(msg)
|
|
146
|
-
|
|
147
|
-
# Return data
|
|
148
|
-
return manipulator_data
|
|
149
|
-
|
|
150
|
-
def _get_manipulators(self) -> list:
|
|
151
|
-
return [probe["Id"] for probe in self.query_data()["ProbeArray"]]
|
|
152
|
-
|
|
153
|
-
def _register_manipulator(self, manipulator_id: str) -> None:
|
|
154
|
-
# Check if ID is a valid New Scale manipulator ID
|
|
155
|
-
if manipulator_id not in self.VALID_MANIPULATOR_IDS:
|
|
156
|
-
msg = f"Invalid manipulator ID {manipulator_id}"
|
|
157
|
-
raise ValueError(msg)
|
|
158
|
-
|
|
159
|
-
# Check if ID is connected
|
|
160
|
-
if manipulator_id not in self._get_manipulators():
|
|
161
|
-
msg = f"Manipulator {manipulator_id} not connected"
|
|
162
|
-
raise ValueError(msg)
|
|
163
|
-
|
|
164
|
-
# Get index of the manipulator
|
|
165
|
-
manipulator_index = next(
|
|
166
|
-
(index for index, data in enumerate(self.query_data()["ProbeArray"]) if data["Id"] == manipulator_id),
|
|
167
|
-
None,
|
|
168
|
-
)
|
|
169
|
-
if manipulator_index is None:
|
|
170
|
-
msg = f"Unable to find manipulator {manipulator_id}"
|
|
171
|
-
raise ValueError(msg)
|
|
172
|
-
self.manipulators[manipulator_id] = manipulator_index
|
|
173
|
-
|
|
174
|
-
def _unregister_manipulator(self, manipulator_id: str) -> None:
|
|
175
|
-
del self.manipulators[manipulator_id]
|
|
176
|
-
|
|
177
|
-
def _get_pos(self, manipulator_id: str) -> PositionalResponse:
|
|
178
|
-
"""Get the current position of the manipulator in mm
|
|
179
|
-
|
|
180
|
-
:param manipulator_id: manipulator ID
|
|
181
|
-
:return: Callback parameters (position in (x, y, z, w) (or an empty array on
|
|
182
|
-
error) in mm, error message)
|
|
183
|
-
"""
|
|
184
|
-
manipulator_data = self.query_manipulator_data(manipulator_id)
|
|
185
|
-
|
|
186
|
-
return PositionalResponse(
|
|
187
|
-
position=Vector4(
|
|
188
|
-
x=manipulator_data["Tip_X_ML"], y=manipulator_data["Tip_Y_AP"], z=manipulator_data["Tip_Z_DV"], w=0
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
def _get_angles(self, manipulator_id: str) -> AngularResponse:
|
|
193
|
-
manipulator_data = self.query_manipulator_data(manipulator_id)
|
|
194
|
-
|
|
195
|
-
# Apply PosteriorAngle to Polar to get the correct angle.
|
|
196
|
-
adjusted_polar = manipulator_data["Polar"] - self.query_data()["PosteriorAngle"]
|
|
197
|
-
|
|
198
|
-
return AngularResponse(
|
|
199
|
-
angles=Vector3(
|
|
200
|
-
x=adjusted_polar if adjusted_polar > 0 else 360 + adjusted_polar,
|
|
201
|
-
y=manipulator_data["Pitch"],
|
|
202
|
-
z=manipulator_data.get("ShankOrientation", 0),
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
def _get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
|
|
207
|
-
for probe in self.query_data()["ProbeArray"]:
|
|
208
|
-
if probe["Id"] == manipulator_id:
|
|
209
|
-
return ShankCountResponse(shank_count=probe.get("ShankCount", 1))
|
|
210
|
-
|
|
211
|
-
return ShankCountResponse(error="Unable to find manipulator")
|
|
212
|
-
|
|
213
|
-
async def _goto_pos(self, _: GotoPositionRequest) -> PositionalResponse:
|
|
214
|
-
raise NotImplementedError
|
|
215
|
-
|
|
216
|
-
async def _drive_to_depth(self, _: DriveToDepthRequest) -> DriveToDepthResponse:
|
|
217
|
-
raise NotImplementedError
|
|
218
|
-
|
|
219
|
-
def _set_inside_brain(self, _: InsideBrainRequest) -> BooleanStateResponse:
|
|
220
|
-
raise NotImplementedError
|
|
221
|
-
|
|
222
|
-
async def _calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> str:
|
|
223
|
-
raise NotImplementedError
|
|
224
|
-
|
|
225
|
-
def _bypass_calibration(self, manipulator_id: str) -> str:
|
|
226
|
-
return ""
|
|
227
|
-
|
|
228
|
-
def _set_can_write(self, _: CanWriteRequest) -> BooleanStateResponse:
|
|
229
|
-
raise NotImplementedError
|
|
230
|
-
|
|
231
|
-
def _unified_space_to_platform_space(self, unified_position: list[float]) -> list[float]:
|
|
232
|
-
raise NotImplementedError
|
|
233
|
-
|
|
234
|
-
def _platform_space_to_unified_space(self, platform_position: list[float]) -> list[float]:
|
|
235
|
-
raise NotImplementedError
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
"""Handle communications with Sensapex uMp API
|
|
2
|
-
|
|
3
|
-
This supports the uMp-4 manipulator. Any Sensapex variants should extend this class.
|
|
4
|
-
|
|
5
|
-
Implements Sensapex uMp specific API calls including coordinating the usage of the
|
|
6
|
-
:class:`ephys_link.platforms.sensapex_manipulator.SensapexManipulator` class.
|
|
7
|
-
|
|
8
|
-
This is a subclass of :class:`ephys_link.platform_handler.PlatformHandler`.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
|
-
|
|
16
|
-
from sensapex import UMP, UMError
|
|
17
|
-
from vbl_aquarium.models.ephys_link import (
|
|
18
|
-
AngularResponse,
|
|
19
|
-
BooleanStateResponse,
|
|
20
|
-
CanWriteRequest,
|
|
21
|
-
DriveToDepthRequest,
|
|
22
|
-
DriveToDepthResponse,
|
|
23
|
-
GotoPositionRequest,
|
|
24
|
-
InsideBrainRequest,
|
|
25
|
-
PositionalResponse,
|
|
26
|
-
ShankCountResponse,
|
|
27
|
-
)
|
|
28
|
-
from vbl_aquarium.models.unity import Vector4
|
|
29
|
-
|
|
30
|
-
import ephys_link.common as com
|
|
31
|
-
from ephys_link.platform_handler import PlatformHandler
|
|
32
|
-
from ephys_link.platforms.sensapex_manipulator import SensapexManipulator
|
|
33
|
-
|
|
34
|
-
if TYPE_CHECKING:
|
|
35
|
-
import socketio
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class SensapexHandler(PlatformHandler):
|
|
39
|
-
"""Handler for Sensapex platform."""
|
|
40
|
-
|
|
41
|
-
def __init__(self) -> None:
|
|
42
|
-
super().__init__()
|
|
43
|
-
|
|
44
|
-
# Establish connection to Sensapex API (exit if connection fails)
|
|
45
|
-
UMP.set_library_path(str(Path(__file__).parent.parent.absolute()) + "/resources/")
|
|
46
|
-
self.ump = UMP.get_ump()
|
|
47
|
-
if self.ump is None:
|
|
48
|
-
msg = "Unable to connect to uMp"
|
|
49
|
-
raise ValueError(msg)
|
|
50
|
-
|
|
51
|
-
def _get_manipulators(self) -> list:
|
|
52
|
-
return list(map(str, self.ump.list_devices()))
|
|
53
|
-
|
|
54
|
-
def _register_manipulator(self, manipulator_id: str) -> None:
|
|
55
|
-
if not manipulator_id.isnumeric():
|
|
56
|
-
msg = "Manipulator ID must be numeric"
|
|
57
|
-
raise ValueError(msg)
|
|
58
|
-
|
|
59
|
-
self.manipulators[manipulator_id] = SensapexManipulator(self.ump.get_device(int(manipulator_id)))
|
|
60
|
-
|
|
61
|
-
def _unregister_manipulator(self, manipulator_id: str) -> None:
|
|
62
|
-
del self.manipulators[manipulator_id]
|
|
63
|
-
|
|
64
|
-
def _get_pos(self, manipulator_id: str) -> PositionalResponse:
|
|
65
|
-
return self.manipulators[manipulator_id].get_pos()
|
|
66
|
-
|
|
67
|
-
def _get_angles(self, manipulator_id: str) -> AngularResponse:
|
|
68
|
-
raise NotImplementedError
|
|
69
|
-
|
|
70
|
-
def _get_shank_count(self, manipulator_id: str) -> ShankCountResponse:
|
|
71
|
-
raise NotImplementedError
|
|
72
|
-
|
|
73
|
-
async def _goto_pos(self, request: GotoPositionRequest) -> PositionalResponse:
|
|
74
|
-
return await self.manipulators[request.manipulator_id].goto_pos(request)
|
|
75
|
-
|
|
76
|
-
async def _drive_to_depth(self, request: DriveToDepthRequest) -> DriveToDepthResponse:
|
|
77
|
-
return await self.manipulators[request.manipulator_id].drive_to_depth(request)
|
|
78
|
-
|
|
79
|
-
def _set_inside_brain(self, request: InsideBrainRequest) -> BooleanStateResponse:
|
|
80
|
-
self.manipulators[request.manipulator_id].set_inside_brain(request.inside)
|
|
81
|
-
com.dprint(f"[SUCCESS]\t Set inside brain state for manipulator: {request.manipulator_id}\n")
|
|
82
|
-
return BooleanStateResponse(state=request.inside)
|
|
83
|
-
|
|
84
|
-
async def _calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> str:
|
|
85
|
-
try:
|
|
86
|
-
# Move manipulator to max position
|
|
87
|
-
await self.manipulators[manipulator_id].goto_pos([20000, 20000, 20000, 20000], 2000)
|
|
88
|
-
|
|
89
|
-
# Call calibrate
|
|
90
|
-
self.manipulators[manipulator_id].call_calibrate()
|
|
91
|
-
|
|
92
|
-
# Wait for calibration to complete
|
|
93
|
-
still_working = True
|
|
94
|
-
while still_working:
|
|
95
|
-
cur_pos = self.manipulators[manipulator_id].get_pos()["position"]
|
|
96
|
-
|
|
97
|
-
# Check difference between current and target position
|
|
98
|
-
for prev, cur in zip([10000, 10000, 10000, 10000], cur_pos, strict=False):
|
|
99
|
-
if abs(prev - cur) > 1:
|
|
100
|
-
still_working = True
|
|
101
|
-
break
|
|
102
|
-
still_working = False
|
|
103
|
-
|
|
104
|
-
# Sleep for a bit
|
|
105
|
-
await sio.sleep(0.5)
|
|
106
|
-
|
|
107
|
-
# Calibration complete
|
|
108
|
-
self.manipulators[manipulator_id].set_calibrated()
|
|
109
|
-
com.dprint(f"[SUCCESS]\t Calibrated manipulator {manipulator_id}\n")
|
|
110
|
-
except UMError as e:
|
|
111
|
-
# SDK call error
|
|
112
|
-
print(f"[ERROR]\t\t Calling calibrate manipulator {manipulator_id}")
|
|
113
|
-
print(f"{e}\n")
|
|
114
|
-
return "Error calling calibrate"
|
|
115
|
-
else:
|
|
116
|
-
return ""
|
|
117
|
-
|
|
118
|
-
def _bypass_calibration(self, manipulator_id: str) -> str:
|
|
119
|
-
self.manipulators[manipulator_id].set_calibrated()
|
|
120
|
-
com.dprint(f"[SUCCESS]\t Bypassed calibration for manipulator" f" {manipulator_id}\n")
|
|
121
|
-
return ""
|
|
122
|
-
|
|
123
|
-
def _set_can_write(self, request: CanWriteRequest) -> BooleanStateResponse:
|
|
124
|
-
self.manipulators[request.manipulator_id].set_can_write(request)
|
|
125
|
-
com.dprint(f"[SUCCESS]\t Set can_write state for manipulator {request.manipulator_id}\n")
|
|
126
|
-
return BooleanStateResponse(state=request.can_write)
|
|
127
|
-
|
|
128
|
-
def _platform_space_to_unified_space(self, platform_position: Vector4) -> Vector4:
|
|
129
|
-
# unified <- platform
|
|
130
|
-
# +x <- +y
|
|
131
|
-
# +y <- -z
|
|
132
|
-
# +z <- +x
|
|
133
|
-
# +d <- +d
|
|
134
|
-
|
|
135
|
-
return Vector4(
|
|
136
|
-
x=platform_position.y,
|
|
137
|
-
y=self.dimensions.z - platform_position.z,
|
|
138
|
-
z=platform_position.x,
|
|
139
|
-
w=platform_position.w,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
def _unified_space_to_platform_space(self, unified_position: Vector4) -> Vector4:
|
|
143
|
-
# platform <- unified
|
|
144
|
-
# +x <- +z
|
|
145
|
-
# +y <- +x
|
|
146
|
-
# +z <- -y
|
|
147
|
-
# +d <- +d
|
|
148
|
-
|
|
149
|
-
return Vector4(
|
|
150
|
-
x=unified_position.z, y=unified_position.x, z=self.dimensions.z - unified_position.y, w=unified_position.w
|
|
151
|
-
)
|