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
ephys_link/server.py
DELETED
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
"""WebSocket server and communication handler
|
|
2
|
-
|
|
3
|
-
Manages the WebSocket server and handles connections and events from the client. For
|
|
4
|
-
every event, the server does the following:
|
|
5
|
-
|
|
6
|
-
1. Extract the arguments passed in the event
|
|
7
|
-
2. Log that the event was received
|
|
8
|
-
3. Call the appropriate function in :mod:`ephys_link.sensapex_handler` with arguments
|
|
9
|
-
4. Relay the response from :mod:`ephys_link.sensapex_handler` to the callback function
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
from asyncio import get_event_loop
|
|
15
|
-
from json import loads
|
|
16
|
-
from signal import SIGINT, SIGTERM, signal
|
|
17
|
-
from typing import TYPE_CHECKING, Any
|
|
18
|
-
from uuid import uuid4
|
|
19
|
-
|
|
20
|
-
from aiohttp import ClientConnectionError, ClientSession
|
|
21
|
-
from aiohttp.web import Application, run_app
|
|
22
|
-
from aiohttp.web_runner import GracefulExit
|
|
23
|
-
from packaging.version import parse
|
|
24
|
-
from pydantic import ValidationError
|
|
25
|
-
|
|
26
|
-
# from socketio import AsyncServer
|
|
27
|
-
from socketio import AsyncClient, AsyncServer
|
|
28
|
-
from vbl_aquarium.models.ephys_link import (
|
|
29
|
-
BooleanStateResponse,
|
|
30
|
-
CanWriteRequest,
|
|
31
|
-
DriveToDepthRequest,
|
|
32
|
-
DriveToDepthResponse,
|
|
33
|
-
GotoPositionRequest,
|
|
34
|
-
InsideBrainRequest,
|
|
35
|
-
PositionalResponse,
|
|
36
|
-
)
|
|
37
|
-
from vbl_aquarium.models.proxy import PinpointIdResponse
|
|
38
|
-
|
|
39
|
-
from ephys_link.__about__ import __version__
|
|
40
|
-
from ephys_link.common import (
|
|
41
|
-
ASCII,
|
|
42
|
-
dprint,
|
|
43
|
-
)
|
|
44
|
-
from ephys_link.platforms.new_scale_handler import NewScaleHandler
|
|
45
|
-
from ephys_link.platforms.new_scale_pathfinder_handler import NewScalePathfinderHandler
|
|
46
|
-
from ephys_link.platforms.sensapex_handler import SensapexHandler
|
|
47
|
-
from ephys_link.platforms.ump3_handler import UMP3Handler
|
|
48
|
-
|
|
49
|
-
if TYPE_CHECKING:
|
|
50
|
-
from ephys_link.platform_handler import PlatformHandler
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class Server:
|
|
54
|
-
def __init__(self) -> None:
|
|
55
|
-
"""Declare and setup server object. Launching is done is a separate function."""
|
|
56
|
-
|
|
57
|
-
# Server object.
|
|
58
|
-
self.sio: AsyncClient | AsyncServer | None = None
|
|
59
|
-
|
|
60
|
-
# Web application object.
|
|
61
|
-
self.app: Application | None = None
|
|
62
|
-
|
|
63
|
-
# Proxy server ID.
|
|
64
|
-
self.pinpoint_id: str = ""
|
|
65
|
-
|
|
66
|
-
# Manipulator platform handler.
|
|
67
|
-
self.platform: PlatformHandler | None = None
|
|
68
|
-
# Is there a client connected?
|
|
69
|
-
self.is_connected = False
|
|
70
|
-
|
|
71
|
-
# Is the server running?
|
|
72
|
-
self.is_running = False
|
|
73
|
-
|
|
74
|
-
# Register server exit handlers.
|
|
75
|
-
signal(SIGTERM, self.close_server)
|
|
76
|
-
signal(SIGINT, self.close_server)
|
|
77
|
-
|
|
78
|
-
# Server events.
|
|
79
|
-
async def connect(self, sid, _, __) -> bool:
|
|
80
|
-
"""Acknowledge connection to the server.
|
|
81
|
-
|
|
82
|
-
:param sid: Socket session ID.
|
|
83
|
-
:type sid: str
|
|
84
|
-
:param _: WSGI formatted dictionary with request info (unused).
|
|
85
|
-
:type _: dict
|
|
86
|
-
:param __: Authentication details (unused).
|
|
87
|
-
:type __: dict
|
|
88
|
-
:return: False on error to refuse connection. True otherwise.
|
|
89
|
-
:rtype: bool
|
|
90
|
-
"""
|
|
91
|
-
print(f"[CONNECTION REQUEST]:\t\t {sid}\n")
|
|
92
|
-
|
|
93
|
-
if not self.is_connected:
|
|
94
|
-
print(f"[CONNECTION GRANTED]:\t\t {sid}\n")
|
|
95
|
-
self.is_connected = True
|
|
96
|
-
return True
|
|
97
|
-
|
|
98
|
-
print(f"[CONNECTION DENIED]:\t\t {sid}: another client is already connected\n")
|
|
99
|
-
return False
|
|
100
|
-
|
|
101
|
-
async def disconnect(self, sid) -> None:
|
|
102
|
-
"""Acknowledge disconnection from the server.
|
|
103
|
-
|
|
104
|
-
:param sid: Socket session ID.
|
|
105
|
-
:type sid: str
|
|
106
|
-
:return: None
|
|
107
|
-
"""
|
|
108
|
-
print(f"[DISCONNECTION]:\t {sid}\n")
|
|
109
|
-
|
|
110
|
-
self.platform.reset()
|
|
111
|
-
self.is_connected = False
|
|
112
|
-
|
|
113
|
-
# Ephys Link Events
|
|
114
|
-
|
|
115
|
-
async def get_pinpoint_id(self) -> str:
|
|
116
|
-
"""Get the pinpoint ID.
|
|
117
|
-
|
|
118
|
-
:return: Pinpoint ID and whether the client is a requester.
|
|
119
|
-
:rtype: tuple[str, bool]
|
|
120
|
-
"""
|
|
121
|
-
return PinpointIdResponse(pinpoint_id=self.pinpoint_id, is_requester=False).to_string()
|
|
122
|
-
|
|
123
|
-
@staticmethod
|
|
124
|
-
async def get_version(_) -> str:
|
|
125
|
-
"""Get the version number of the server.
|
|
126
|
-
|
|
127
|
-
:param _: Socket session ID (unused).
|
|
128
|
-
:type _: str
|
|
129
|
-
:return: Version number as defined in :mod:`ephys_link.__about__`.
|
|
130
|
-
:rtype: str
|
|
131
|
-
"""
|
|
132
|
-
dprint("[EVENT]\t\t Get version")
|
|
133
|
-
|
|
134
|
-
return __version__
|
|
135
|
-
|
|
136
|
-
async def get_manipulators(self, _) -> str:
|
|
137
|
-
"""Get the list of discoverable manipulators.
|
|
138
|
-
|
|
139
|
-
:param _: Socket session ID (unused).
|
|
140
|
-
:type _: str
|
|
141
|
-
:return: :class:`vbl_aquarium.models.ephys_link.GetManipulatorsResponse` as JSON formatted string.
|
|
142
|
-
:rtype: str
|
|
143
|
-
"""
|
|
144
|
-
dprint("[EVENT]\t\t Get discoverable manipulators")
|
|
145
|
-
|
|
146
|
-
return self.platform.get_manipulators().to_string()
|
|
147
|
-
|
|
148
|
-
async def register_manipulator(self, _, manipulator_id: str) -> str:
|
|
149
|
-
"""Register a manipulator with the server.
|
|
150
|
-
|
|
151
|
-
:param _: Socket session ID (unused).
|
|
152
|
-
:type _: str
|
|
153
|
-
:param manipulator_id: ID of the manipulator to register.
|
|
154
|
-
:type manipulator_id: str
|
|
155
|
-
:return: Error message on error, empty string otherwise.
|
|
156
|
-
:rtype: str
|
|
157
|
-
"""
|
|
158
|
-
dprint(f"[EVENT]\t\t Register manipulator: {manipulator_id}")
|
|
159
|
-
|
|
160
|
-
return self.platform.register_manipulator(manipulator_id)
|
|
161
|
-
|
|
162
|
-
async def unregister_manipulator(self, _, manipulator_id: str) -> str:
|
|
163
|
-
"""Unregister a manipulator from the server.
|
|
164
|
-
|
|
165
|
-
:param _: Socket session ID (unused)
|
|
166
|
-
:type _: str
|
|
167
|
-
:param manipulator_id: ID of the manipulator to unregister.
|
|
168
|
-
:type manipulator_id: str
|
|
169
|
-
:return: Error message on error, empty string otherwise.
|
|
170
|
-
:rtype: str
|
|
171
|
-
"""
|
|
172
|
-
dprint(f"[EVENT]\t\t Unregister manipulator: {manipulator_id}")
|
|
173
|
-
|
|
174
|
-
return self.platform.unregister_manipulator(manipulator_id)
|
|
175
|
-
|
|
176
|
-
async def get_pos(self, _, manipulator_id: str) -> str:
|
|
177
|
-
"""Position of manipulator request.
|
|
178
|
-
|
|
179
|
-
:param _: Socket session ID (unused).
|
|
180
|
-
:type _: str
|
|
181
|
-
:param manipulator_id: ID of manipulator to pull position from.
|
|
182
|
-
:type manipulator_id: str
|
|
183
|
-
:return: :class:`vbl_aquarium.models.ephys_link.PositionalResponse` as JSON formatted string.
|
|
184
|
-
:rtype: str
|
|
185
|
-
"""
|
|
186
|
-
# dprint(f"[EVENT]\t\t Get position of manipulator" f" {manipulator_id}")
|
|
187
|
-
|
|
188
|
-
return self.platform.get_pos(manipulator_id).to_string()
|
|
189
|
-
|
|
190
|
-
async def get_angles(self, _, manipulator_id: str) -> str:
|
|
191
|
-
"""Angles of manipulator request.
|
|
192
|
-
|
|
193
|
-
:param _: Socket session ID (unused).
|
|
194
|
-
:type _: str
|
|
195
|
-
:param manipulator_id: ID of manipulator to pull angles from.
|
|
196
|
-
:type manipulator_id: str
|
|
197
|
-
:return: :class:`vbl_aquarium.models.ephys_link.AngularResponse` as JSON formatted string.
|
|
198
|
-
:rtype: str
|
|
199
|
-
"""
|
|
200
|
-
|
|
201
|
-
return self.platform.get_angles(manipulator_id).to_string()
|
|
202
|
-
|
|
203
|
-
async def get_shank_count(self, _, manipulator_id: str) -> str:
|
|
204
|
-
"""Number of shanks of manipulator request.
|
|
205
|
-
|
|
206
|
-
:param _: Socket session ID (unused).
|
|
207
|
-
:type _: str
|
|
208
|
-
:param manipulator_id: ID of manipulator to pull number of shanks from.
|
|
209
|
-
:type manipulator_id: str
|
|
210
|
-
:return: :class:`vbl_aquarium.models.ephys_link.ShankCountResponse` as JSON formatted string.
|
|
211
|
-
:rtype: str
|
|
212
|
-
"""
|
|
213
|
-
|
|
214
|
-
return self.platform.get_shank_count(manipulator_id).to_string()
|
|
215
|
-
|
|
216
|
-
async def goto_pos(self, _, data: str) -> str:
|
|
217
|
-
"""Move manipulator to position.
|
|
218
|
-
|
|
219
|
-
:param _: Socket session ID (unused).
|
|
220
|
-
:type _: str
|
|
221
|
-
:param data: :class:`vbl_aquarium.models.ephys_link.GotoPositionRequest` as JSON formatted string.
|
|
222
|
-
:type data: str
|
|
223
|
-
:return: :class:`vbl_aquarium.models.ephys_link.PositionalResponse` as JSON formatted string.
|
|
224
|
-
:rtype: str
|
|
225
|
-
"""
|
|
226
|
-
try:
|
|
227
|
-
request = GotoPositionRequest(**loads(data))
|
|
228
|
-
except ValidationError as ve:
|
|
229
|
-
print(f"[ERROR]\t\t Invalid goto_pos data: {data}\n{ve}\n")
|
|
230
|
-
return PositionalResponse(error="Invalid data format").to_string()
|
|
231
|
-
except Exception as e:
|
|
232
|
-
print(f"[ERROR]\t\t Error in goto_pos: {e}\n")
|
|
233
|
-
return PositionalResponse(error="Error in goto_pos").to_string()
|
|
234
|
-
else:
|
|
235
|
-
dprint(f"[EVENT]\t\t Move manipulator {request.manipulator_id} to position {request.position}")
|
|
236
|
-
goto_result = await self.platform.goto_pos(request)
|
|
237
|
-
return goto_result.to_string()
|
|
238
|
-
|
|
239
|
-
async def drive_to_depth(self, _, data: str) -> str:
|
|
240
|
-
"""Drive to depth.
|
|
241
|
-
|
|
242
|
-
:param _: Socket session ID (unused).
|
|
243
|
-
:type _: str
|
|
244
|
-
:param data: :class:`vbl_aquarium.models.ephys_link.DriveToDepthRequest` as JSON formatted string.
|
|
245
|
-
:type data: str
|
|
246
|
-
:return: :class:`vbl_aquarium.models.ephys_link.DriveToDepthResponse` as JSON formatted string.
|
|
247
|
-
:rtype: str
|
|
248
|
-
"""
|
|
249
|
-
try:
|
|
250
|
-
request = DriveToDepthRequest(**loads(data))
|
|
251
|
-
except KeyError:
|
|
252
|
-
print(f"[ERROR]\t\t Invalid drive_to_depth data: {data}\n")
|
|
253
|
-
return DriveToDepthResponse(error="Invalid data " "format").to_string()
|
|
254
|
-
except Exception as e:
|
|
255
|
-
print(f"[ERROR]\t\t Error in drive_to_depth: {e}\n")
|
|
256
|
-
return DriveToDepthResponse(error="Error in drive_to_depth").to_string()
|
|
257
|
-
else:
|
|
258
|
-
dprint(f"[EVENT]\t\t Drive manipulator {request.manipulator_id} to depth {request.depth}")
|
|
259
|
-
drive_result = await self.platform.drive_to_depth(request)
|
|
260
|
-
return drive_result.to_string()
|
|
261
|
-
|
|
262
|
-
async def set_inside_brain(self, _, data: str) -> str:
|
|
263
|
-
"""Set the inside brain state.
|
|
264
|
-
|
|
265
|
-
:param _: Socket session ID (unused).
|
|
266
|
-
:type _: str
|
|
267
|
-
:param data: :class:`vbl_aquarium.models.ephys_link.InsideBrainRequest` as JSON formatted string.
|
|
268
|
-
:type data: str
|
|
269
|
-
:return: :class:`vbl_aquarium.models.ephys_link.BooleanStateResponse` as JSON formatted string.
|
|
270
|
-
:rtype: str
|
|
271
|
-
"""
|
|
272
|
-
try:
|
|
273
|
-
request = InsideBrainRequest(**loads(data))
|
|
274
|
-
except KeyError:
|
|
275
|
-
print(f"[ERROR]\t\t Invalid set_inside_brain data: {data}\n")
|
|
276
|
-
return BooleanStateResponse(error="Invalid data format").to_string()
|
|
277
|
-
except Exception as e:
|
|
278
|
-
print(f"[ERROR]\t\t Error in inside_brain: {e}\n")
|
|
279
|
-
return BooleanStateResponse(error="Error in set_inside_brain").to_string()
|
|
280
|
-
else:
|
|
281
|
-
dprint(f"[EVENT]\t\t Set manipulator {request.manipulator_id} inside brain to {request.inside}")
|
|
282
|
-
return self.platform.set_inside_brain(request).to_string()
|
|
283
|
-
|
|
284
|
-
async def calibrate(self, _, manipulator_id: str) -> str:
|
|
285
|
-
"""Calibrate manipulator.
|
|
286
|
-
|
|
287
|
-
:param _: Socket session ID (unused).
|
|
288
|
-
:type _: str
|
|
289
|
-
:param manipulator_id: ID of manipulator to calibrate.
|
|
290
|
-
:type manipulator_id: str
|
|
291
|
-
:return: Error message on error, empty string otherwise.
|
|
292
|
-
:rtype: str
|
|
293
|
-
"""
|
|
294
|
-
dprint(f"[EVENT]\t\t Calibrate manipulator" f" {manipulator_id}")
|
|
295
|
-
|
|
296
|
-
return await self.platform.calibrate(manipulator_id, self.sio)
|
|
297
|
-
|
|
298
|
-
async def bypass_calibration(self, _, manipulator_id: str) -> str:
|
|
299
|
-
"""Bypass calibration of manipulator.
|
|
300
|
-
|
|
301
|
-
:param _: Socket session ID (unused).
|
|
302
|
-
:type _: str
|
|
303
|
-
:param manipulator_id: ID of manipulator to bypass calibration.
|
|
304
|
-
:type manipulator_id: str
|
|
305
|
-
:return: Error message on error, empty string otherwise.
|
|
306
|
-
:rtype: str
|
|
307
|
-
"""
|
|
308
|
-
dprint(f"[EVENT]\t\t Bypass calibration of manipulator" f" {manipulator_id}")
|
|
309
|
-
|
|
310
|
-
return self.platform.bypass_calibration(manipulator_id)
|
|
311
|
-
|
|
312
|
-
async def set_can_write(self, _, data: str) -> str:
|
|
313
|
-
"""Set manipulator can_write state.
|
|
314
|
-
|
|
315
|
-
:param _: Socket session ID (unused)
|
|
316
|
-
:type _: str
|
|
317
|
-
:param data: :class:`vbl_aquarium.models.ephys_link.CanWriteRequest` as JSON formatted string.
|
|
318
|
-
:type data: str
|
|
319
|
-
:return: :class:`vbl_aquarium.models.ephys_link.BooleanStateResponse` as JSON formatted string.
|
|
320
|
-
:rtype: str
|
|
321
|
-
"""
|
|
322
|
-
try:
|
|
323
|
-
request = CanWriteRequest(**loads(data))
|
|
324
|
-
except KeyError:
|
|
325
|
-
print(f"[ERROR]\t\t Invalid set_can_write data: {data}\n")
|
|
326
|
-
return BooleanStateResponse(error="Invalid data format").to_string()
|
|
327
|
-
except Exception as e:
|
|
328
|
-
print(f"[ERROR]\t\t Error in inside_brain: {e}\n")
|
|
329
|
-
return BooleanStateResponse(error="Error in set_can_write").to_string()
|
|
330
|
-
else:
|
|
331
|
-
dprint(f"[EVENT]\t\t Set manipulator {request.manipulator_id} can_write state to {request.can_write}")
|
|
332
|
-
return self.platform.set_can_write(request).to_string()
|
|
333
|
-
|
|
334
|
-
def stop(self, _) -> bool:
|
|
335
|
-
"""Stop all manipulators.
|
|
336
|
-
|
|
337
|
-
:param _: Socket session ID (unused).
|
|
338
|
-
:type _: str
|
|
339
|
-
:return: True if successful, False otherwise.
|
|
340
|
-
:rtype: bool
|
|
341
|
-
"""
|
|
342
|
-
dprint("[EVENT]\t\t Stop all manipulators")
|
|
343
|
-
|
|
344
|
-
return self.platform.stop()
|
|
345
|
-
|
|
346
|
-
@staticmethod
|
|
347
|
-
async def catch_all(_, __, data: Any) -> str:
|
|
348
|
-
"""Catch all event.
|
|
349
|
-
|
|
350
|
-
:param _: Socket session ID (unused).
|
|
351
|
-
:type _: str
|
|
352
|
-
:param __: Client ID (unused).
|
|
353
|
-
:type __: str
|
|
354
|
-
:param data: Data received from client.
|
|
355
|
-
:type data: Any
|
|
356
|
-
:return: "UNKNOWN_EVENT" response message.
|
|
357
|
-
:rtype: str
|
|
358
|
-
"""
|
|
359
|
-
print(f"[UNKNOWN EVENT]:\t {data}")
|
|
360
|
-
return "UNKNOWN_EVENT"
|
|
361
|
-
|
|
362
|
-
# Server functions
|
|
363
|
-
async def launch_setup(self, platform_type: str, pathfinder_port: int, ignore_updates) -> None:
|
|
364
|
-
# Import correct manipulator handler
|
|
365
|
-
match platform_type:
|
|
366
|
-
case "sensapex":
|
|
367
|
-
self.platform = SensapexHandler()
|
|
368
|
-
case "ump3":
|
|
369
|
-
self.platform = UMP3Handler()
|
|
370
|
-
case "new_scale":
|
|
371
|
-
self.platform = NewScaleHandler()
|
|
372
|
-
case "new_scale_pathfinder":
|
|
373
|
-
self.platform = NewScalePathfinderHandler(pathfinder_port)
|
|
374
|
-
case _:
|
|
375
|
-
error = f"[ERROR]\t\t Invalid manipulator type: {platform_type}"
|
|
376
|
-
raise ValueError(error)
|
|
377
|
-
|
|
378
|
-
# Preamble.
|
|
379
|
-
print(ASCII)
|
|
380
|
-
print(f"v{__version__}")
|
|
381
|
-
|
|
382
|
-
# Check for newer version.
|
|
383
|
-
if not ignore_updates:
|
|
384
|
-
try:
|
|
385
|
-
async with (
|
|
386
|
-
ClientSession() as session,
|
|
387
|
-
session.get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags") as response,
|
|
388
|
-
):
|
|
389
|
-
latest_version = (await response.json())[0]["name"]
|
|
390
|
-
if parse(latest_version) > parse(__version__):
|
|
391
|
-
print(f"New version available: {latest_version}")
|
|
392
|
-
print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
|
|
393
|
-
|
|
394
|
-
await session.close()
|
|
395
|
-
except ClientConnectionError:
|
|
396
|
-
pass
|
|
397
|
-
|
|
398
|
-
# Explain window.
|
|
399
|
-
print()
|
|
400
|
-
print("This is the Ephys Link server window.")
|
|
401
|
-
print("You may safely leave it running in the background.")
|
|
402
|
-
print("To stop it, close this window or press CTRL + Pause/Break.")
|
|
403
|
-
print()
|
|
404
|
-
|
|
405
|
-
# List available manipulators
|
|
406
|
-
print("Available Manipulators:")
|
|
407
|
-
print(self.platform.get_manipulators().manipulators)
|
|
408
|
-
print()
|
|
409
|
-
|
|
410
|
-
async def launch_for_proxy(
|
|
411
|
-
self, proxy_address: str, port: int, platform_type: str, pathfinder_port: int | None, ignore_updates: bool
|
|
412
|
-
) -> None:
|
|
413
|
-
"""Launch the server in proxy mode.
|
|
414
|
-
|
|
415
|
-
:param proxy_address: Proxy IP address.
|
|
416
|
-
:type proxy_address: str
|
|
417
|
-
:param port: Port to serve the server.
|
|
418
|
-
:type port: int
|
|
419
|
-
:param platform_type: Parsed argument for platform type.
|
|
420
|
-
:type platform_type: str
|
|
421
|
-
:param pathfinder_port: Port New Scale Pathfinder's server is on.
|
|
422
|
-
:type pathfinder_port: int
|
|
423
|
-
:param ignore_updates: Flag to ignore checking for updates.
|
|
424
|
-
:type ignore_updates: bool
|
|
425
|
-
:return: None
|
|
426
|
-
"""
|
|
427
|
-
|
|
428
|
-
# Launch setup
|
|
429
|
-
await self.launch_setup(platform_type, pathfinder_port, ignore_updates)
|
|
430
|
-
|
|
431
|
-
# Create AsyncClient.
|
|
432
|
-
self.sio = AsyncClient()
|
|
433
|
-
self.pinpoint_id = str(uuid4())[:8]
|
|
434
|
-
|
|
435
|
-
# Bind events.
|
|
436
|
-
self.bind_events()
|
|
437
|
-
|
|
438
|
-
# Connect and mark that server is running.
|
|
439
|
-
await self.sio.connect(f"http://{proxy_address}:{port}")
|
|
440
|
-
self.is_running = True
|
|
441
|
-
print(f"Pinpoint ID: {self.pinpoint_id}")
|
|
442
|
-
await self.sio.wait()
|
|
443
|
-
|
|
444
|
-
def launch(
|
|
445
|
-
self,
|
|
446
|
-
platform_type: str,
|
|
447
|
-
port: int,
|
|
448
|
-
pathfinder_port: int | None,
|
|
449
|
-
ignore_updates: bool,
|
|
450
|
-
) -> None:
|
|
451
|
-
"""Launch the server.
|
|
452
|
-
|
|
453
|
-
:param platform_type: Parsed argument for platform type.
|
|
454
|
-
:type platform_type: str
|
|
455
|
-
:param port: HTTP port to serve the server.
|
|
456
|
-
:type port: int
|
|
457
|
-
:param pathfinder_port: Port New Scale Pathfinder's server is on.
|
|
458
|
-
:type pathfinder_port: int
|
|
459
|
-
:param ignore_updates: Flag to ignore checking for updates.
|
|
460
|
-
:type ignore_updates: bool
|
|
461
|
-
:return: None
|
|
462
|
-
"""
|
|
463
|
-
|
|
464
|
-
# Launch setup (synchronously)
|
|
465
|
-
get_event_loop().run_until_complete(self.launch_setup(platform_type, pathfinder_port, ignore_updates))
|
|
466
|
-
|
|
467
|
-
# Create AsyncServer
|
|
468
|
-
self.sio = AsyncServer()
|
|
469
|
-
self.app = Application()
|
|
470
|
-
self.sio.attach(self.app)
|
|
471
|
-
|
|
472
|
-
# Bind events
|
|
473
|
-
self.sio.on("connect", self.connect)
|
|
474
|
-
self.sio.on("disconnect", self.disconnect)
|
|
475
|
-
self.bind_events()
|
|
476
|
-
|
|
477
|
-
# Mark that server is running
|
|
478
|
-
self.is_running = True
|
|
479
|
-
run_app(self.app, port=port)
|
|
480
|
-
|
|
481
|
-
def bind_events(self) -> None:
|
|
482
|
-
"""Bind Ephys Link events to the server."""
|
|
483
|
-
self.sio.on("get_pinpoint_id", self.get_pinpoint_id)
|
|
484
|
-
self.sio.on("get_version", self.get_version)
|
|
485
|
-
self.sio.on("get_manipulators", self.get_manipulators)
|
|
486
|
-
self.sio.on("register_manipulator", self.register_manipulator)
|
|
487
|
-
self.sio.on("unregister_manipulator", self.unregister_manipulator)
|
|
488
|
-
self.sio.on("get_pos", self.get_pos)
|
|
489
|
-
self.sio.on("get_angles", self.get_angles)
|
|
490
|
-
self.sio.on("get_shank_count", self.get_shank_count)
|
|
491
|
-
self.sio.on("goto_pos", self.goto_pos)
|
|
492
|
-
self.sio.on("drive_to_depth", self.drive_to_depth)
|
|
493
|
-
self.sio.on("set_inside_brain", self.set_inside_brain)
|
|
494
|
-
self.sio.on("calibrate", self.calibrate)
|
|
495
|
-
self.sio.on("bypass_calibration", self.bypass_calibration)
|
|
496
|
-
self.sio.on("set_can_write", self.set_can_write)
|
|
497
|
-
self.sio.on("stop", self.stop)
|
|
498
|
-
self.sio.on("*", self.catch_all)
|
|
499
|
-
|
|
500
|
-
def close_server(self, _, __) -> None:
|
|
501
|
-
"""Close the server."""
|
|
502
|
-
print("[INFO]\t\t Closing server")
|
|
503
|
-
|
|
504
|
-
# Stop movement
|
|
505
|
-
self.platform.stop()
|
|
506
|
-
|
|
507
|
-
# Exit
|
|
508
|
-
raise GracefulExit
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: ephys-link
|
|
3
|
-
Version: 1.3.3
|
|
4
|
-
Summary: A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments.
|
|
5
|
-
Project-URL: Documentation, https://virtualbrainlab.org/ephys_link/installation_and_use.html
|
|
6
|
-
Project-URL: Issues, https://github.com/VirtualBrainLab/ephys-link/issues
|
|
7
|
-
Project-URL: Source, https://github.com/VirtualBrainLab/ephys-link
|
|
8
|
-
Author-email: Kenneth Yang <kjy5@uw.edu>
|
|
9
|
-
Maintainer-email: Kenneth Yang <kjy5@uw.edu>
|
|
10
|
-
License-Expression: GPL-3.0-only
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Keywords: electrophysiology,ephys,manipulator,neuroscience,neurotech,new-scale,sensapex,socket-io,virtualbrainlab
|
|
13
|
-
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
-
Classifier: Intended Audience :: Healthcare Industry
|
|
15
|
-
Classifier: Intended Audience :: Science/Research
|
|
16
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
17
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
-
Classifier: Programming Language :: Python
|
|
19
|
-
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
27
|
-
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
28
|
-
Requires-Python: <3.13,>=3.10
|
|
29
|
-
Requires-Dist: aiohttp==3.9.5
|
|
30
|
-
Requires-Dist: platformdirs==4.2.2
|
|
31
|
-
Requires-Dist: pyserial==3.5
|
|
32
|
-
Requires-Dist: python-socketio[asyncio-client]==5.11.2
|
|
33
|
-
Requires-Dist: pythonnet==3.0.3
|
|
34
|
-
Requires-Dist: sensapex==1.400.0
|
|
35
|
-
Requires-Dist: vbl-aquarium==0.0.15
|
|
36
|
-
Description-Content-Type: text/markdown
|
|
37
|
-
|
|
38
|
-
# Electrophysiology Manipulator Link
|
|
39
|
-
|
|
40
|
-
[](https://badge.fury.io/py/ephys-link)
|
|
41
|
-
[](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/codeql-analysis.yml)
|
|
42
|
-
[](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/dependency-review.yml)
|
|
43
|
-
[](https://github.com/pypa/hatch)
|
|
44
|
-
[](https://github.com/astral-sh/ruff)
|
|
45
|
-
|
|
46
|
-
<!-- [](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/build.yml) -->
|
|
47
|
-
|
|
48
|
-
<img width="100%" src="https://github.com/VirtualBrainLab/ephys-link/assets/82800265/0c7c60b1-0926-4697-a461-221554f82de1" alt="Manipulator and probe in pinpoint moving in sync">
|
|
49
|
-
|
|
50
|
-
The [Electrophysiology Manipulator Link](https://github.com/VirtualBrainLab/ephys-link)
|
|
51
|
-
(or Ephys Link for short) is a Python [Socket.IO](https://socket.io/docs/v4/#what-socketio-is) server that allows any
|
|
52
|
-
Socket.IO-compliant application (such
|
|
53
|
-
as [Pinpoint](https://github.com/VirtualBrainLab/Pinpoint))
|
|
54
|
-
to communicate with manipulators used in electrophysiology experiments.
|
|
55
|
-
|
|
56
|
-
**Supported Manipulators:**
|
|
57
|
-
|
|
58
|
-
| Manufacturer | Model |
|
|
59
|
-
|--------------|-------------------------------------------------------------------------|
|
|
60
|
-
| Sensapex | <ul> <li>uMp-4</li> <li>uMp-3</li> </ul> |
|
|
61
|
-
| New Scale | <ul> <li>Pathfinder MPM Control v2.8+</li> <li>M3-USB-3:1-EP</li> </ul> |
|
|
62
|
-
|
|
63
|
-
Ephys Link is an open and extensible platform. It is designed to easily support integration with other manipulators.
|
|
64
|
-
|
|
65
|
-
For more information regarding the server's implementation and how the code is organized, see
|
|
66
|
-
the [package's development documentation](https://virtualbrainlab.org/ephys_link/development.html).
|
|
67
|
-
|
|
68
|
-
For detailed descriptions of the server's API, see
|
|
69
|
-
the [API reference](https://virtualbrainlab.org/api_reference_ephys_link.html).
|
|
70
|
-
|
|
71
|
-
# Installation
|
|
72
|
-
|
|
73
|
-
## Prerequisites
|
|
74
|
-
|
|
75
|
-
1. An **x86 Windows PC is required** to run the server.
|
|
76
|
-
2. For Sensapex devices, the controller unit must be connected via an ethernet
|
|
77
|
-
cable and powered. A USB-to-ethernet adapter is acceptable. For New Scale manipulators,
|
|
78
|
-
the controller unit must be connected via USB and be powered by a 6V power
|
|
79
|
-
supply.
|
|
80
|
-
3. To use the emergency stop feature, ensure an Arduino with
|
|
81
|
-
the [StopSignal](https://github.com/VirtualBrainLab/StopSignal) sketch is
|
|
82
|
-
connected to the computer. Follow the instructions on that repo for how to
|
|
83
|
-
set up the Arduino.
|
|
84
|
-
|
|
85
|
-
**NOTE:** Ephys Link is an HTTP server without cross-origin support. The server
|
|
86
|
-
is currently designed to interface with local/desktop instances of Pinpoint. It
|
|
87
|
-
will not work with the web browser versions of Pinpoint at this time.
|
|
88
|
-
|
|
89
|
-
## Launch from Pinpoint (Recommended)
|
|
90
|
-
|
|
91
|
-
Pinpoint comes bundled with the correct version of Ephys Link. If you are using Pinpoint on the same computer your
|
|
92
|
-
manipulators are connected to, you can launch the server from within Pinpoint. Follow the instructions in
|
|
93
|
-
the [Pinpoint documentation](https://virtualbrainlab.org/pinpoint/tutorials/tutorial_ephys_link.html#configure-and-launch-ephys-link).
|
|
94
|
-
|
|
95
|
-
## Install as Standalone Executable
|
|
96
|
-
|
|
97
|
-
1. Download the latest executable from
|
|
98
|
-
the [releases page](https://github.com/VirtualBrainLab/ephys-link/releases/latest).
|
|
99
|
-
2. Double-click the executable file to launch the configuration window.
|
|
100
|
-
1. Take note of the IP address and port. **Copy this information into Pinpoint to connect**.
|
|
101
|
-
3. Select the desired configuration and click "Launch Server".
|
|
102
|
-
|
|
103
|
-
The configuration window will close and the server will launch. Your configurations will be saved for future use.
|
|
104
|
-
|
|
105
|
-
To connect to the server from Pinpoint, provide the IP address and port. For example, if the server is running on the
|
|
106
|
-
same computer that Pinpoint is, use
|
|
107
|
-
|
|
108
|
-
- Server: `localhost`
|
|
109
|
-
- Port: `8081`
|
|
110
|
-
|
|
111
|
-
If the server is running on a different (local) computer, use the IP address of that computer as shown in the startup
|
|
112
|
-
window instead of `localhost`.
|
|
113
|
-
|
|
114
|
-
## Install as a Python package
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
pip install ephys-link
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Import the modules you need and launch the server.
|
|
121
|
-
|
|
122
|
-
```python
|
|
123
|
-
from ephys_link.server import Server
|
|
124
|
-
|
|
125
|
-
server = Server()
|
|
126
|
-
server.launch("sensapex", args.proxy_address, 8081)
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Install for Development
|
|
130
|
-
|
|
131
|
-
1. Clone the repository.
|
|
132
|
-
2. Install [Hatch](https://hatch.pypa.io/latest/install/)
|
|
133
|
-
3. In a terminal, navigate to the repository's root directory and run
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
hatch shell
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
This will create a virtual environment, install Python 12 (if not found), and install the package in editable mode.
|
|
140
|
-
|
|
141
|
-
If you encounter any dependency issues (particularly with `aiohttp`), try installing the latest Microsoft Visual C++
|
|
142
|
-
(MSVC v143+ x86/64) and the Windows SDK (10/11)
|
|
143
|
-
via [Visual Studio Build Tools Installer](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
|
|
144
|
-
|
|
145
|
-
# Documentation and More Information
|
|
146
|
-
|
|
147
|
-
Complete documentation including API usage and development installation can be
|
|
148
|
-
found on the [Virtual Brain Lab Documentation page][docs] for Ephys Link.
|
|
149
|
-
|
|
150
|
-
# Citing
|
|
151
|
-
|
|
152
|
-
If this project is used as part of a research project you should cite
|
|
153
|
-
the [Pinpoint repository][Pinpoint]. Please email
|
|
154
|
-
Dan ([dbirman@uw.edu](mailto:dbirman@uw.edu)) if you have questions.
|
|
155
|
-
|
|
156
|
-
Please reach out to Kenneth ([kjy5@uw.edu](mailto:kjy5@uw.edu)) for questions
|
|
157
|
-
about the Electrophysiology Manipulator Link server. Bugs may be reported
|
|
158
|
-
through the issues tab.
|
|
159
|
-
|
|
160
|
-
[Pinpoint]: https://github.com/VirtualBrainLab/Pinpoint
|
|
161
|
-
|
|
162
|
-
[StopSignal]: https://github.com/VirtualBrainLab/StopSignal
|
|
163
|
-
|
|
164
|
-
[docs]: https://virtualbrainlab.org/ephys_link/installation_and_use.html
|