ephys-link 2.1.3__py3-none-any.whl → 2.2.1__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/back_end/server.py +21 -47
- ephys_link/bindings/parallax_binding.py +274 -0
- ephys_link/bindings/ump_binding.py +1 -1
- ephys_link/front_end/cli.py +9 -17
- ephys_link/front_end/gui.py +6 -27
- ephys_link/utils/constants.py +0 -3
- ephys_link/utils/startup.py +14 -8
- {ephys_link-2.1.3.dist-info → ephys_link-2.2.1.dist-info}/METADATA +8 -9
- {ephys_link-2.1.3.dist-info → ephys_link-2.2.1.dist-info}/RECORD +13 -12
- {ephys_link-2.1.3.dist-info → ephys_link-2.2.1.dist-info}/WHEEL +1 -1
- {ephys_link-2.1.3.dist-info → ephys_link-2.2.1.dist-info}/entry_points.txt +0 -0
- {ephys_link-2.1.3.dist-info → ephys_link-2.2.1.dist-info}/licenses/LICENSE +0 -0
ephys_link/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.1
|
|
1
|
+
__version__ = "2.2.1"
|
ephys_link/back_end/server.py
CHANGED
|
@@ -12,22 +12,20 @@ Usage:
|
|
|
12
12
|
```
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from asyncio import
|
|
15
|
+
from asyncio import new_event_loop
|
|
16
16
|
from collections.abc import Callable, Coroutine
|
|
17
17
|
from json import JSONDecodeError, dumps, loads
|
|
18
18
|
from typing import Any, TypeVar, final
|
|
19
|
-
from uuid import uuid4
|
|
20
19
|
|
|
21
20
|
from aiohttp.web import Application, run_app
|
|
22
21
|
from pydantic import ValidationError
|
|
23
|
-
from socketio import
|
|
22
|
+
from socketio import AsyncServer # pyright: ignore [reportMissingTypeStubs]
|
|
24
23
|
from vbl_aquarium.models.ephys_link import (
|
|
25
24
|
EphysLinkOptions,
|
|
26
25
|
SetDepthRequest,
|
|
27
26
|
SetInsideBrainRequest,
|
|
28
27
|
SetPositionRequest,
|
|
29
28
|
)
|
|
30
|
-
from vbl_aquarium.models.proxy import PinpointIdResponse
|
|
31
29
|
from vbl_aquarium.utils.vbl_base_model import VBLBaseModel
|
|
32
30
|
|
|
33
31
|
from ephys_link.__about__ import __version__
|
|
@@ -36,8 +34,6 @@ from ephys_link.front_end.console import Console
|
|
|
36
34
|
from ephys_link.utils.constants import (
|
|
37
35
|
MALFORMED_REQUEST_ERROR,
|
|
38
36
|
PORT,
|
|
39
|
-
PROXY_CLIENT_NOT_INITIALIZED_ERROR,
|
|
40
|
-
SERVER_NOT_INITIALIZED_ERROR,
|
|
41
37
|
UNKNOWN_EVENT_ERROR,
|
|
42
38
|
cannot_connect_as_client_is_already_connected_error,
|
|
43
39
|
client_disconnected_without_being_connected_error,
|
|
@@ -64,60 +60,40 @@ class Server:
|
|
|
64
60
|
self._platform_handler = platform_handler
|
|
65
61
|
self._console = console
|
|
66
62
|
|
|
67
|
-
# Initialize
|
|
68
|
-
self._sio: AsyncServer
|
|
69
|
-
if not self._options.use_proxy:
|
|
70
|
-
# Exit if _sio is not a Server.
|
|
71
|
-
if not isinstance(self._sio, AsyncServer):
|
|
72
|
-
self._console.critical_print(SERVER_NOT_INITIALIZED_ERROR)
|
|
73
|
-
raise TypeError(SERVER_NOT_INITIALIZED_ERROR)
|
|
63
|
+
# Initialize server.
|
|
64
|
+
self._sio: AsyncServer = AsyncServer()
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
self._app = Application()
|
|
67
|
+
self._sio.attach(self._app) # pyright: ignore [reportUnknownMemberType]
|
|
77
68
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
# Bind connection events.
|
|
70
|
+
_ = self._sio.on("connect", self.connect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
|
|
71
|
+
_ = self._sio.on("disconnect", self.disconnect) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
|
|
81
72
|
|
|
82
73
|
# Store connected client.
|
|
83
74
|
self._client_sid: str = ""
|
|
84
75
|
|
|
85
|
-
# Generate Pinpoint ID for proxy usage.
|
|
86
|
-
self._pinpoint_id = str(uuid4())[:8]
|
|
87
|
-
|
|
88
76
|
# Bind events.
|
|
89
77
|
_ = self._sio.on("*", self.platform_event_handler) # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType]
|
|
90
78
|
|
|
91
79
|
def launch(self) -> None:
|
|
92
|
-
"""Launch the server.
|
|
93
|
-
|
|
94
|
-
Based on the options, either connect to a proxy or launch the server locally.
|
|
95
|
-
"""
|
|
80
|
+
"""Launch the server."""
|
|
96
81
|
|
|
97
82
|
# List platform and available manipulators.
|
|
98
83
|
self._console.info_print("PLATFORM", self._platform_handler.get_display_name())
|
|
99
|
-
self._console.info_print(
|
|
100
|
-
"MANIPULATORS",
|
|
101
|
-
str(get_event_loop().run_until_complete(self._platform_handler.get_manipulators()).manipulators),
|
|
102
|
-
)
|
|
103
84
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
# Create a temporary event loop for getting manipulators
|
|
86
|
+
loop = new_event_loop()
|
|
87
|
+
try:
|
|
88
|
+
self._console.info_print(
|
|
89
|
+
"MANIPULATORS",
|
|
90
|
+
str(loop.run_until_complete(self._platform_handler.get_manipulators()).manipulators),
|
|
91
|
+
)
|
|
92
|
+
finally:
|
|
93
|
+
loop.close()
|
|
107
94
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if not isinstance(self._sio, AsyncClient):
|
|
111
|
-
self._console.critical_print(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
|
|
112
|
-
raise TypeError(PROXY_CLIENT_NOT_INITIALIZED_ERROR)
|
|
113
|
-
|
|
114
|
-
# noinspection HttpUrlsUsage
|
|
115
|
-
await self._sio.connect(f"http://{self._options.proxy_address}:{PORT}") # pyright: ignore [reportUnknownMemberType]
|
|
116
|
-
await self._sio.wait()
|
|
117
|
-
|
|
118
|
-
run(connect_proxy())
|
|
119
|
-
else:
|
|
120
|
-
run_app(self._app, port=PORT)
|
|
95
|
+
# Launch server
|
|
96
|
+
run_app(self._app, port=PORT)
|
|
121
97
|
|
|
122
98
|
# Helper functions.
|
|
123
99
|
def _malformed_request_response(self, request: str, data: tuple[tuple[Any], ...]) -> str: # pyright: ignore [reportExplicitAny]
|
|
@@ -244,8 +220,6 @@ class Server:
|
|
|
244
220
|
# Server metadata.
|
|
245
221
|
case "get_version":
|
|
246
222
|
return __version__
|
|
247
|
-
case "get_pinpoint_id":
|
|
248
|
-
return PinpointIdResponse(pinpoint_id=self._pinpoint_id, is_requester=False).to_json_string()
|
|
249
223
|
case "get_platform_info":
|
|
250
224
|
return (await self._platform_handler.get_platform_info()).to_json_string()
|
|
251
225
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Bindings for Parallax for New Scale platform.
|
|
2
|
+
|
|
3
|
+
Usage: Instantiate ParallaxBinding to interact with the Parallax for New Scale Pathfinder 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 ParallaxBinding(BaseBinding):
|
|
19
|
+
"""Bindings for Parallax for New Scale platform."""
|
|
20
|
+
|
|
21
|
+
# Server data update rate (30 FPS).
|
|
22
|
+
SERVER_DATA_UPDATE_RATE = 1 / 30
|
|
23
|
+
|
|
24
|
+
# Movement polling preferences.
|
|
25
|
+
UNCHANGED_COUNTER_LIMIT = 10
|
|
26
|
+
|
|
27
|
+
# Speed preferences (mm/s to use coarse mode).
|
|
28
|
+
COARSE_SPEED_THRESHOLD = 0.1
|
|
29
|
+
INSERTION_SPEED_LIMIT = 9_000
|
|
30
|
+
|
|
31
|
+
def __init__(self, port: int = 8081) -> None:
|
|
32
|
+
"""Initialize connection to MPM HTTP server.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
port: Port number for MPM HTTP server.
|
|
36
|
+
"""
|
|
37
|
+
self._url = f"http://localhost:{port}"
|
|
38
|
+
self._movement_stopped = False
|
|
39
|
+
|
|
40
|
+
# Data cache.
|
|
41
|
+
self.cache: dict[str, Any] = {} # pyright: ignore [reportExplicitAny]
|
|
42
|
+
self.cache_time = 0
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
@override
|
|
46
|
+
def get_display_name() -> str:
|
|
47
|
+
return "Parallax for New Scale"
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
@override
|
|
51
|
+
def get_cli_name() -> str:
|
|
52
|
+
return "parallax"
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
async def get_manipulators(self) -> list[str]:
|
|
56
|
+
data = await self._query_data()
|
|
57
|
+
return list(data.keys())
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
async def get_axes_count(self) -> int:
|
|
61
|
+
return 3
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
def get_dimensions(self) -> Vector4:
|
|
65
|
+
return Vector4(x=15, y=15, z=15, w=15)
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
async def get_position(self, manipulator_id: str) -> Vector4:
|
|
69
|
+
manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny]
|
|
70
|
+
global_z = float(manipulator_data.get("global_Z", 0.0) or 0.0)
|
|
71
|
+
|
|
72
|
+
await sleep(self.SERVER_DATA_UPDATE_RATE) # Wait for the stage to stabilize.
|
|
73
|
+
|
|
74
|
+
global_x = float(manipulator_data.get("global_X", 0.0) or 0.0)
|
|
75
|
+
global_y = float(manipulator_data.get("global_Y", 0.0) or 0.0)
|
|
76
|
+
|
|
77
|
+
return Vector4(x=global_x, y=global_y, z=global_z, w=global_z)
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
async def get_angles(self, manipulator_id: str) -> Vector3:
|
|
81
|
+
manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny]
|
|
82
|
+
|
|
83
|
+
yaw = int(manipulator_data.get("yaw", 0) or 0)
|
|
84
|
+
pitch = int(manipulator_data.get("pitch", 90) or 90)
|
|
85
|
+
roll = int(manipulator_data.get("roll", 0) or 0)
|
|
86
|
+
|
|
87
|
+
return Vector3(x=yaw, y=pitch, z=roll)
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
async def get_shank_count(self, manipulator_id: str) -> int:
|
|
91
|
+
manipulator_data: dict[str, Any] = await self._manipulator_data(manipulator_id) # pyright: ignore [reportExplicitAny]
|
|
92
|
+
return int(manipulator_data.get("shank_cnt", 1) or 1)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
@override
|
|
96
|
+
def get_movement_tolerance() -> float:
|
|
97
|
+
return 0.01
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
async def set_position(self, manipulator_id: str, position: Vector4, speed: float) -> Vector4:
|
|
101
|
+
# Keep track of the previous position to check if the manipulator stopped advancing.
|
|
102
|
+
current_position = await self.get_position(manipulator_id)
|
|
103
|
+
previous_position = current_position
|
|
104
|
+
unchanged_counter = 0
|
|
105
|
+
|
|
106
|
+
# Set step mode based on speed.
|
|
107
|
+
await self._put_request(
|
|
108
|
+
{
|
|
109
|
+
"move_type": "stepMode",
|
|
110
|
+
"stage_sn": manipulator_id,
|
|
111
|
+
"step_mode": 0 if speed > self.COARSE_SPEED_THRESHOLD else 1,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Send move request.
|
|
116
|
+
await self._put_request(
|
|
117
|
+
{
|
|
118
|
+
"move_type": "moveXYZ",
|
|
119
|
+
"world": "global", # Use global coordinates
|
|
120
|
+
"stage_sn": manipulator_id,
|
|
121
|
+
"Absolute": 1,
|
|
122
|
+
"Stereotactic": 0,
|
|
123
|
+
"AxisMask": 7,
|
|
124
|
+
"x": position.x,
|
|
125
|
+
"y": position.y,
|
|
126
|
+
"z": position.z,
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
# Wait for the manipulator to reach the target position or be stopped or stuck.
|
|
130
|
+
while (
|
|
131
|
+
not self._movement_stopped
|
|
132
|
+
and not self._is_vector_close(current_position, position)
|
|
133
|
+
and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
|
|
134
|
+
):
|
|
135
|
+
# Wait for a short time before checking again.
|
|
136
|
+
await sleep(self.SERVER_DATA_UPDATE_RATE)
|
|
137
|
+
|
|
138
|
+
# Update current position.
|
|
139
|
+
current_position = await self.get_position(manipulator_id)
|
|
140
|
+
|
|
141
|
+
# Check if manipulator is not moving.
|
|
142
|
+
if self._is_vector_close(previous_position, current_position):
|
|
143
|
+
# Position did not change.
|
|
144
|
+
unchanged_counter += 1
|
|
145
|
+
else:
|
|
146
|
+
# Position changed.
|
|
147
|
+
unchanged_counter = 0
|
|
148
|
+
previous_position = current_position
|
|
149
|
+
|
|
150
|
+
# Reset movement stopped flag.
|
|
151
|
+
self._movement_stopped = False
|
|
152
|
+
|
|
153
|
+
# Return the final position.
|
|
154
|
+
return await self.get_position(manipulator_id)
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
async def set_depth(self, manipulator_id: str, depth: float, speed: float) -> float:
|
|
158
|
+
# Keep track of the previous depth to check if the manipulator stopped advancing unexpectedly.
|
|
159
|
+
current_depth = (await self.get_position(manipulator_id)).w
|
|
160
|
+
previous_depth = current_depth
|
|
161
|
+
unchanged_counter = 0
|
|
162
|
+
|
|
163
|
+
# Send move request.
|
|
164
|
+
# Convert mm/s to um/min and cap speed at the limit.
|
|
165
|
+
await self._put_request(
|
|
166
|
+
{
|
|
167
|
+
"move_type": "insertion",
|
|
168
|
+
"stage_sn": manipulator_id,
|
|
169
|
+
"world": "global", # distance in global space
|
|
170
|
+
"distance": scalar_mm_to_um(current_depth - depth),
|
|
171
|
+
"rate": min(scalar_mm_to_um(speed) * 60, self.INSERTION_SPEED_LIMIT),
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Wait for the manipulator to reach the target depth or be stopped or get stuck.
|
|
176
|
+
while (
|
|
177
|
+
not self._movement_stopped
|
|
178
|
+
and not abs(current_depth - depth) <= self.get_movement_tolerance()
|
|
179
|
+
and unchanged_counter < self.UNCHANGED_COUNTER_LIMIT
|
|
180
|
+
):
|
|
181
|
+
# Wait for a short time before checking again.
|
|
182
|
+
await sleep(self.SERVER_DATA_UPDATE_RATE)
|
|
183
|
+
|
|
184
|
+
# Get the current depth.
|
|
185
|
+
current_depth = (await self.get_position(manipulator_id)).w
|
|
186
|
+
|
|
187
|
+
# Check if manipulator is not moving.
|
|
188
|
+
if abs(previous_depth - current_depth) <= self.get_movement_tolerance():
|
|
189
|
+
# Depth did not change.
|
|
190
|
+
unchanged_counter += 1
|
|
191
|
+
else:
|
|
192
|
+
# Depth changed.
|
|
193
|
+
unchanged_counter = 0
|
|
194
|
+
previous_depth = current_depth
|
|
195
|
+
|
|
196
|
+
# Reset movement stopped flag.
|
|
197
|
+
self._movement_stopped = False
|
|
198
|
+
|
|
199
|
+
# Return the final depth.
|
|
200
|
+
return float((await self.get_position(manipulator_id)).w)
|
|
201
|
+
|
|
202
|
+
@override
|
|
203
|
+
async def stop(self, manipulator_id: str) -> None:
|
|
204
|
+
request: dict[str, str | int | float] = {
|
|
205
|
+
"PutId": "stop",
|
|
206
|
+
"Probe": manipulator_id,
|
|
207
|
+
}
|
|
208
|
+
await self._put_request(request)
|
|
209
|
+
self._movement_stopped = True
|
|
210
|
+
|
|
211
|
+
@override
|
|
212
|
+
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
|
213
|
+
# unified <- platform
|
|
214
|
+
# +x <- +x
|
|
215
|
+
# +y <- +z
|
|
216
|
+
# +z <- +y
|
|
217
|
+
# +w <- +w
|
|
218
|
+
|
|
219
|
+
return Vector4(
|
|
220
|
+
x=platform_space.x,
|
|
221
|
+
y=platform_space.z,
|
|
222
|
+
z=platform_space.y,
|
|
223
|
+
w=self.get_dimensions().w - platform_space.w,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@override
|
|
227
|
+
def unified_space_to_platform_space(self, unified_space: Vector4) -> Vector4:
|
|
228
|
+
# platform <- unified
|
|
229
|
+
# +x <- +x
|
|
230
|
+
# +y <- +z
|
|
231
|
+
# +z <- +y
|
|
232
|
+
# +w <- -w
|
|
233
|
+
|
|
234
|
+
return Vector4(
|
|
235
|
+
x=unified_space.x,
|
|
236
|
+
y=unified_space.z,
|
|
237
|
+
z=unified_space.y,
|
|
238
|
+
w=self.get_dimensions().w - unified_space.w,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Helper functions.
|
|
242
|
+
async def _query_data(self) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
|
|
243
|
+
try:
|
|
244
|
+
# Update cache if it's expired.
|
|
245
|
+
if get_running_loop().time() - self.cache_time > self.SERVER_DATA_UPDATE_RATE:
|
|
246
|
+
# noinspection PyTypeChecker
|
|
247
|
+
self.cache = (await get_running_loop().run_in_executor(None, get, self._url)).json()
|
|
248
|
+
self.cache_time = get_running_loop().time()
|
|
249
|
+
except ConnectionError as connectionError:
|
|
250
|
+
error_message = f"Unable to connect to MPM HTTP server: {connectionError}"
|
|
251
|
+
raise RuntimeError(error_message) from connectionError
|
|
252
|
+
except JSONDecodeError as jsonDecodeError:
|
|
253
|
+
error_message = f"Unable to decode JSON response from MPM HTTP server: {jsonDecodeError}"
|
|
254
|
+
raise ValueError(error_message) from jsonDecodeError
|
|
255
|
+
else:
|
|
256
|
+
# Return cached data.
|
|
257
|
+
return self.cache
|
|
258
|
+
|
|
259
|
+
async def _manipulator_data(self, manipulator_id: str) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
|
|
260
|
+
"""Retrieve data for a specific manipulator (probe) using its serial number."""
|
|
261
|
+
data = await self._query_data()
|
|
262
|
+
|
|
263
|
+
if manipulator_id in data:
|
|
264
|
+
return data[manipulator_id] # pyright: ignore [reportAny]
|
|
265
|
+
|
|
266
|
+
# If we get here, that means the manipulator doesn't exist.
|
|
267
|
+
error_message = f"Manipulator {manipulator_id} not found."
|
|
268
|
+
raise ValueError(error_message)
|
|
269
|
+
|
|
270
|
+
async def _put_request(self, request: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny]
|
|
271
|
+
_ = await get_running_loop().run_in_executor(None, put, self._url, dumps(request))
|
|
272
|
+
|
|
273
|
+
def _is_vector_close(self, target: Vector4, current: Vector4) -> bool:
|
|
274
|
+
return all(abs(axis) <= self.get_movement_tolerance() for axis in vector4_to_array(target - current)[:3])
|
|
@@ -142,7 +142,7 @@ class UmpBinding(BaseBinding):
|
|
|
142
142
|
|
|
143
143
|
@override
|
|
144
144
|
async def stop(self, manipulator_id: str) -> None:
|
|
145
|
-
self._get_device(manipulator_id).stop()
|
|
145
|
+
self._get_device(manipulator_id).stop() # pyright: ignore [reportUnknownMemberType]
|
|
146
146
|
|
|
147
147
|
@override
|
|
148
148
|
def platform_space_to_unified_space(self, platform_space: Vector4) -> Vector4:
|
ephys_link/front_end/cli.py
CHANGED
|
@@ -47,7 +47,7 @@ class CLI:
|
|
|
47
47
|
type=str,
|
|
48
48
|
dest="type",
|
|
49
49
|
default="ump",
|
|
50
|
-
help='Manipulator type (
|
|
50
|
+
help='Manipulator type ("ump", "pathfinder-mpm", "parallax", "fake"). Default: "ump".',
|
|
51
51
|
)
|
|
52
52
|
_ = self._parser.add_argument(
|
|
53
53
|
"-d",
|
|
@@ -56,27 +56,19 @@ class CLI:
|
|
|
56
56
|
action="store_true",
|
|
57
57
|
help="Enable debug mode.",
|
|
58
58
|
)
|
|
59
|
-
_ = self._parser.add_argument(
|
|
60
|
-
"-p",
|
|
61
|
-
"--use-proxy",
|
|
62
|
-
dest="use_proxy",
|
|
63
|
-
action="store_true",
|
|
64
|
-
help="Enable proxy mode.",
|
|
65
|
-
)
|
|
66
|
-
_ = self._parser.add_argument(
|
|
67
|
-
"-a",
|
|
68
|
-
"--proxy-address",
|
|
69
|
-
type=str,
|
|
70
|
-
default="proxy2.virtualbrainlab.org",
|
|
71
|
-
dest="proxy_address",
|
|
72
|
-
help="Proxy IP address.",
|
|
73
|
-
)
|
|
74
59
|
_ = self._parser.add_argument(
|
|
75
60
|
"--mpm-port",
|
|
76
61
|
type=int,
|
|
77
62
|
default=8080,
|
|
78
63
|
dest="mpm_port",
|
|
79
|
-
help="
|
|
64
|
+
help="HTTP port New Scale Pathfinder MPM's server is on. Default: 8080.",
|
|
65
|
+
)
|
|
66
|
+
_ = self._parser.add_argument(
|
|
67
|
+
"--parallax-port",
|
|
68
|
+
type=int,
|
|
69
|
+
default=8081,
|
|
70
|
+
dest="parallax_port",
|
|
71
|
+
help="HTTP port Parallax's server is on. Default: 8081.",
|
|
80
72
|
)
|
|
81
73
|
_ = self._parser.add_argument(
|
|
82
74
|
"-s",
|
ephys_link/front_end/gui.py
CHANGED
|
@@ -12,7 +12,7 @@ from json import load
|
|
|
12
12
|
from os import makedirs
|
|
13
13
|
from os.path import exists, join
|
|
14
14
|
from socket import gethostbyname, gethostname
|
|
15
|
-
from sys import exit
|
|
15
|
+
from sys import exit as sys_exit
|
|
16
16
|
from tkinter import CENTER, RIGHT, BooleanVar, E, IntVar, StringVar, Tk, ttk
|
|
17
17
|
from typing import final
|
|
18
18
|
|
|
@@ -52,8 +52,6 @@ class GUI:
|
|
|
52
52
|
self._ignore_updates = BooleanVar(value=options.ignore_updates)
|
|
53
53
|
self._type = StringVar(value=options.type)
|
|
54
54
|
self._debug = BooleanVar(value=options.debug)
|
|
55
|
-
self._use_proxy = BooleanVar(value=options.use_proxy)
|
|
56
|
-
self._proxy_address = StringVar(value=options.proxy_address)
|
|
57
55
|
self._mpm_port = IntVar(value=options.mpm_port)
|
|
58
56
|
self._serial = StringVar(value=options.serial)
|
|
59
57
|
|
|
@@ -73,15 +71,13 @@ class GUI:
|
|
|
73
71
|
|
|
74
72
|
# Exit if the user did not submit options.
|
|
75
73
|
if not self._submit:
|
|
76
|
-
|
|
74
|
+
sys_exit(1)
|
|
77
75
|
|
|
78
76
|
# Extract options from GUI.
|
|
79
77
|
options = EphysLinkOptions(
|
|
80
78
|
ignore_updates=self._ignore_updates.get(),
|
|
81
79
|
type=self._type.get(),
|
|
82
80
|
debug=self._debug.get(),
|
|
83
|
-
use_proxy=self._use_proxy.get(),
|
|
84
|
-
proxy_address=self._proxy_address.get(),
|
|
85
81
|
mpm_port=self._mpm_port.get(),
|
|
86
82
|
serial=self._serial.get(),
|
|
87
83
|
)
|
|
@@ -115,40 +111,23 @@ class GUI:
|
|
|
115
111
|
ttk.Label(server_serving_settings, text="Local IP:", anchor=E, justify=RIGHT).grid(column=0, row=0, sticky="we")
|
|
116
112
|
ttk.Label(server_serving_settings, text=gethostbyname(gethostname())).grid(column=1, row=0, sticky="we")
|
|
117
113
|
|
|
118
|
-
# Proxy.
|
|
119
|
-
ttk.Label(server_serving_settings, text="Use Proxy:", anchor=E, justify=RIGHT).grid(
|
|
120
|
-
column=0, row=1, sticky="we"
|
|
121
|
-
)
|
|
122
|
-
ttk.Checkbutton(
|
|
123
|
-
server_serving_settings,
|
|
124
|
-
variable=self._use_proxy,
|
|
125
|
-
).grid(column=1, row=1, sticky="we")
|
|
126
|
-
|
|
127
|
-
# Proxy address.
|
|
128
|
-
ttk.Label(server_serving_settings, text="Proxy Address:", anchor=E, justify=RIGHT).grid(
|
|
129
|
-
column=0, row=2, sticky="we"
|
|
130
|
-
)
|
|
131
|
-
ttk.Entry(server_serving_settings, textvariable=self._proxy_address, justify=CENTER).grid(
|
|
132
|
-
column=1, row=2, sticky="we"
|
|
133
|
-
)
|
|
134
|
-
|
|
135
114
|
# Ignore updates.
|
|
136
115
|
ttk.Label(server_serving_settings, text="Ignore Updates:", anchor=E, justify=RIGHT).grid(
|
|
137
|
-
column=0, row=
|
|
116
|
+
column=0, row=1, sticky="we"
|
|
138
117
|
)
|
|
139
118
|
ttk.Checkbutton(
|
|
140
119
|
server_serving_settings,
|
|
141
120
|
variable=self._ignore_updates,
|
|
142
|
-
).grid(column=1, row=
|
|
121
|
+
).grid(column=1, row=1, sticky="we")
|
|
143
122
|
|
|
144
123
|
# Debug mode.
|
|
145
124
|
ttk.Label(server_serving_settings, text="Debug mode:", anchor=E, justify=RIGHT).grid(
|
|
146
|
-
column=0, row=
|
|
125
|
+
column=0, row=2, sticky="we"
|
|
147
126
|
)
|
|
148
127
|
ttk.Checkbutton(
|
|
149
128
|
server_serving_settings,
|
|
150
129
|
variable=self._debug,
|
|
151
|
-
).grid(column=1, row=
|
|
130
|
+
).grid(column=1, row=2, sticky="we")
|
|
152
131
|
|
|
153
132
|
# ---
|
|
154
133
|
|
ephys_link/utils/constants.py
CHANGED
|
@@ -58,9 +58,6 @@ def did_not_reach_target_depth_error(request: SetDepthRequest, final_unified_dep
|
|
|
58
58
|
|
|
59
59
|
EMERGENCY_STOP_MESSAGE = "Emergency Stopping All Manipulators..."
|
|
60
60
|
|
|
61
|
-
SERVER_NOT_INITIALIZED_ERROR = "Server not initialized."
|
|
62
|
-
PROXY_CLIENT_NOT_INITIALIZED_ERROR = "Proxy client not initialized."
|
|
63
|
-
|
|
64
61
|
|
|
65
62
|
def cannot_connect_as_client_is_already_connected_error(new_client_sid: str, current_client_sid: str) -> str:
|
|
66
63
|
"""Generate an error message for when the client is already connected.
|
ephys_link/utils/startup.py
CHANGED
|
@@ -5,11 +5,14 @@ from inspect import getmembers, isclass
|
|
|
5
5
|
from pkgutil import iter_modules
|
|
6
6
|
|
|
7
7
|
from packaging.version import parse
|
|
8
|
-
from requests import ConnectionError
|
|
8
|
+
from requests import ConnectionError as RequestsConnectionError
|
|
9
|
+
from requests import ConnectTimeout as RequestsConnectTimeout
|
|
10
|
+
from requests import get
|
|
9
11
|
from vbl_aquarium.models.ephys_link import EphysLinkOptions
|
|
10
12
|
|
|
11
13
|
from ephys_link.__about__ import __version__
|
|
12
14
|
from ephys_link.bindings.mpm_binding import MPMBinding
|
|
15
|
+
from ephys_link.bindings.parallax_binding import ParallaxBinding
|
|
13
16
|
from ephys_link.front_end.console import Console
|
|
14
17
|
from ephys_link.utils.base_binding import BaseBinding
|
|
15
18
|
from ephys_link.utils.constants import (
|
|
@@ -44,7 +47,7 @@ def check_for_updates(console: Console) -> None:
|
|
|
44
47
|
if parse(latest_version) > parse(__version__):
|
|
45
48
|
console.critical_print(f"Update available: {latest_version} (current: {__version__})")
|
|
46
49
|
console.critical_print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
|
|
47
|
-
except (
|
|
50
|
+
except (RequestsConnectionError, RequestsConnectTimeout):
|
|
48
51
|
console.error_print("UPDATE", UNABLE_TO_CHECK_FOR_UPDATES_ERROR)
|
|
49
52
|
|
|
50
53
|
|
|
@@ -89,12 +92,15 @@ def get_binding_instance(options: EphysLinkOptions, console: Console) -> BaseBin
|
|
|
89
92
|
selected_type = "ump"
|
|
90
93
|
|
|
91
94
|
if binding_cli_name == selected_type:
|
|
92
|
-
# Pass in HTTP port for Pathfinder MPM.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
# Pass in HTTP port for Pathfinder MPM and Parallax.
|
|
96
|
+
match binding_cli_name:
|
|
97
|
+
case "pathfinder-mpm":
|
|
98
|
+
return MPMBinding(options.mpm_port)
|
|
99
|
+
case "parallax":
|
|
100
|
+
return ParallaxBinding(options.parallax_port)
|
|
101
|
+
case _:
|
|
102
|
+
# Otherwise just return the binding.
|
|
103
|
+
return binding_type()
|
|
98
104
|
|
|
99
105
|
# Raise an error if the platform type is not recognized.
|
|
100
106
|
error_message = unrecognized_platform_type_error(selected_type)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ephys-link
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
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
|
|
@@ -17,22 +17,21 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
|
17
17
|
Classifier: Operating System :: Microsoft :: Windows
|
|
18
18
|
Classifier: Programming Language :: Python
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
20
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
21
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
22
|
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
24
|
-
Requires-Python: >=3.
|
|
25
|
-
Requires-Dist: aiohttp==3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: aiohttp==3.13.2
|
|
26
25
|
Requires-Dist: colorama==0.4.6
|
|
27
26
|
Requires-Dist: keyboard==0.13.5
|
|
28
27
|
Requires-Dist: packaging==25.0
|
|
29
|
-
Requires-Dist: platformdirs==4.
|
|
28
|
+
Requires-Dist: platformdirs==4.5.1
|
|
30
29
|
Requires-Dist: pyserial==3.5
|
|
31
|
-
Requires-Dist: python-socketio
|
|
30
|
+
Requires-Dist: python-socketio==5.15.1
|
|
32
31
|
Requires-Dist: requests==2.32.5
|
|
33
|
-
Requires-Dist: rich==14.
|
|
34
|
-
Requires-Dist: sensapex==1.
|
|
35
|
-
Requires-Dist: vbl-aquarium==1.
|
|
32
|
+
Requires-Dist: rich==14.2.0
|
|
33
|
+
Requires-Dist: sensapex==1.504.1
|
|
34
|
+
Requires-Dist: vbl-aquarium==1.2.0
|
|
36
35
|
Description-Content-Type: text/markdown
|
|
37
36
|
|
|
38
37
|
# Electrophysiology Manipulator Link
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
ephys_link/__about__.py,sha256
|
|
1
|
+
ephys_link/__about__.py,sha256=4dqvKTDgbqeyzbWj6hYiNdzxsI8j1YOKSLM8vF6a0j4,22
|
|
2
2
|
ephys_link/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
ephys_link/__main__.py,sha256=54zxQ-fyxC2-LGsTUdtlvib36ZZQwNyOa6IuffYLhhs,1582
|
|
4
4
|
ephys_link/back_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
ephys_link/back_end/platform_handler.py,sha256=6Lt_jaIcO9jTmWYOoWYkuhwSlCTT1Qfd53ru31X0fmc,10130
|
|
6
|
-
ephys_link/back_end/server.py,sha256=
|
|
6
|
+
ephys_link/back_end/server.py,sha256=ILrYbd4r_4GiRmdUEmGcbT1AEIo_TF3i-BZpdhYm8g8,9296
|
|
7
7
|
ephys_link/bindings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
ephys_link/bindings/fake_binding.py,sha256=PI7zYv-SjWsGFEs_FVu5Z5l9gykIqG3C7pQISdbwdoY,2357
|
|
9
9
|
ephys_link/bindings/mpm_binding.py,sha256=vn7IKqdiZ6_MX91zomqDXX08ONHwVVgWncRuJTxJpOM,10872
|
|
10
|
-
ephys_link/bindings/
|
|
10
|
+
ephys_link/bindings/parallax_binding.py,sha256=QzZ4MVUc4YGlIxr9__j8I3t3MyGuoQdsZWgHaSedZ3A,10129
|
|
11
|
+
ephys_link/bindings/ump_binding.py,sha256=X71n5P3IPlWmQ2BaHUI-UHu-RP__ZOgA3w32iT3169U,7910
|
|
11
12
|
ephys_link/front_end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
ephys_link/front_end/cli.py,sha256=
|
|
13
|
+
ephys_link/front_end/cli.py,sha256=vbJYFoaOwPwQq3ShR60HgPQnE-hK6kp1_9FEFkyZ6AU,2899
|
|
13
14
|
ephys_link/front_end/console.py,sha256=zq67dn7T4xKMg3jjbNra_1s0N9ItvsTRXVIhClWbjP8,3912
|
|
14
|
-
ephys_link/front_end/gui.py,sha256=
|
|
15
|
+
ephys_link/front_end/gui.py,sha256=MagojumetQGp7e8sK53cI_TgcqKSx54jLNeZagBjNJY,6383
|
|
15
16
|
ephys_link/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
17
|
ephys_link/utils/base_binding.py,sha256=rq0FtvkjAN287nX8rZwR-j6YPzV3Pkp8PqJfZlhIzqo,5413
|
|
17
|
-
ephys_link/utils/constants.py,sha256=
|
|
18
|
+
ephys_link/utils/constants.py,sha256=l1-KmGFzAgaBzEIgtmFLfpijum75_1JMdSq5GyR3TJk,3756
|
|
18
19
|
ephys_link/utils/converters.py,sha256=ZdVmIX-LHCwM__F0SpjN_mfNGGetr1U97xvHd0hf8T0,2038
|
|
19
|
-
ephys_link/utils/startup.py,sha256=
|
|
20
|
-
ephys_link-2.1.
|
|
21
|
-
ephys_link-2.1.
|
|
22
|
-
ephys_link-2.1.
|
|
23
|
-
ephys_link-2.1.
|
|
24
|
-
ephys_link-2.1.
|
|
20
|
+
ephys_link/utils/startup.py,sha256=lK04UffvdcASpmp8Arecfmm1dDhzZnZy2rgzQ8sqWag,3971
|
|
21
|
+
ephys_link-2.2.1.dist-info/METADATA,sha256=nHO5FaFAmKoqwYbH2gHOvPh-e8-AgyA7TqFoN9zLokM,4707
|
|
22
|
+
ephys_link-2.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
23
|
+
ephys_link-2.2.1.dist-info/entry_points.txt,sha256=o8wV3AdnJ9o47vg9ymKxPNVq9pMdPq8UZHE_iyAJx-k,124
|
|
24
|
+
ephys_link-2.2.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
25
|
+
ephys_link-2.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|