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.
Files changed (42) hide show
  1. ephys_link/__about__.py +1 -1
  2. ephys_link/__main__.py +51 -105
  3. ephys_link/back_end/__init__.py +0 -0
  4. ephys_link/back_end/platform_handler.py +315 -0
  5. ephys_link/back_end/server.py +274 -0
  6. ephys_link/bindings/__init__.py +0 -0
  7. ephys_link/bindings/fake_binding.py +84 -0
  8. ephys_link/bindings/mpm_binding.py +315 -0
  9. ephys_link/bindings/ump_4_binding.py +157 -0
  10. ephys_link/front_end/__init__.py +0 -0
  11. ephys_link/front_end/cli.py +104 -0
  12. ephys_link/front_end/gui.py +204 -0
  13. ephys_link/utils/__init__.py +0 -0
  14. ephys_link/utils/base_binding.py +176 -0
  15. ephys_link/utils/console.py +127 -0
  16. ephys_link/utils/constants.py +23 -0
  17. ephys_link/utils/converters.py +86 -0
  18. ephys_link/utils/startup.py +65 -0
  19. ephys_link-2.0.0.dist-info/METADATA +91 -0
  20. ephys_link-2.0.0.dist-info/RECORD +25 -0
  21. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/WHEEL +1 -1
  22. {ephys_link-1.3.3.dist-info → ephys_link-2.0.0.dist-info}/licenses/LICENSE +674 -674
  23. ephys_link/common.py +0 -49
  24. ephys_link/emergency_stop.py +0 -67
  25. ephys_link/gui.py +0 -217
  26. ephys_link/platform_handler.py +0 -465
  27. ephys_link/platform_manipulator.py +0 -35
  28. ephys_link/platforms/__init__.py +0 -5
  29. ephys_link/platforms/new_scale_handler.py +0 -141
  30. ephys_link/platforms/new_scale_manipulator.py +0 -312
  31. ephys_link/platforms/new_scale_pathfinder_handler.py +0 -235
  32. ephys_link/platforms/sensapex_handler.py +0 -151
  33. ephys_link/platforms/sensapex_manipulator.py +0 -227
  34. ephys_link/platforms/ump3_handler.py +0 -57
  35. ephys_link/platforms/ump3_manipulator.py +0 -147
  36. ephys_link/resources/CP210xManufacturing.dll +0 -0
  37. ephys_link/resources/NstMotorCtrl.dll +0 -0
  38. ephys_link/resources/SiUSBXp.dll +0 -0
  39. ephys_link/server.py +0 -508
  40. ephys_link-1.3.3.dist-info/METADATA +0 -164
  41. ephys_link-1.3.3.dist-info/RECORD +0 -26
  42. {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
- [![PyPI version](https://badge.fury.io/py/ephys-link.svg)](https://badge.fury.io/py/ephys-link)
41
- [![CodeQL](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/codeql-analysis.yml)
42
- [![Dependency Review](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/dependency-review.yml)
43
- [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
44
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
45
-
46
- <!-- [![Build](https://github.com/VirtualBrainLab/ephys-link/actions/workflows/build.yml/badge.svg)](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