ephys-link 1.3.0b2__tar.gz → 1.3.3__tar.gz

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 (34) hide show
  1. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/PKG-INFO +12 -11
  2. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/README.md +6 -4
  3. ephys_link-1.3.3/ephys_link.spec +70 -0
  4. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/pyproject.toml +10 -16
  5. ephys_link-1.3.3/scripts/proxy_dev.py +24 -0
  6. ephys_link-1.3.3/src/ephys_link/__about__.py +1 -0
  7. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/__main__.py +17 -2
  8. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/gui.py +60 -6
  9. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platform_handler.py +3 -3
  10. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platform_manipulator.py +1 -0
  11. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/sensapex_handler.py +1 -1
  12. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/sensapex_manipulator.py +7 -1
  13. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/ump3_handler.py +14 -14
  14. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/ump3_manipulator.py +3 -1
  15. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/server.py +147 -75
  16. ephys_link-1.3.0b2/ephys_link.spec +0 -39
  17. ephys_link-1.3.0b2/src/ephys_link/__about__.py +0 -1
  18. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/.gitignore +0 -0
  19. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/LICENSE +0 -0
  20. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/assets/icon.ico +0 -0
  21. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/scripts/__init__.py +0 -0
  22. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/scripts/move_tester.py +0 -0
  23. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/__init__.py +0 -0
  24. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/common.py +0 -0
  25. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/emergency_stop.py +0 -0
  26. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/__init__.py +0 -0
  27. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/new_scale_handler.py +0 -0
  28. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/new_scale_manipulator.py +0 -0
  29. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/platforms/new_scale_pathfinder_handler.py +0 -0
  30. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/resources/CP210xManufacturing.dll +0 -0
  31. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/resources/NstMotorCtrl.dll +0 -0
  32. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/resources/SiUSBXp.dll +0 -0
  33. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/src/ephys_link/resources/libum.dll +0 -0
  34. {ephys_link-1.3.0b2 → ephys_link-1.3.3}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ephys-link
3
- Version: 1.3.0b2
3
+ Version: 1.3.3
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
@@ -25,15 +25,14 @@ Classifier: Programming Language :: Python :: 3.12
25
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
26
  Classifier: Programming Language :: Python :: Implementation :: PyPy
27
27
  Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
28
- Requires-Python: <3.13,>=3.8
29
- Requires-Dist: aiohttp==3.9.4
30
- Requires-Dist: platformdirs==4.2.0
28
+ Requires-Python: <3.13,>=3.10
29
+ Requires-Dist: aiohttp==3.9.5
30
+ Requires-Dist: platformdirs==4.2.2
31
31
  Requires-Dist: pyserial==3.5
32
- Requires-Dist: python-socketio==5.11.2
32
+ Requires-Dist: python-socketio[asyncio-client]==5.11.2
33
33
  Requires-Dist: pythonnet==3.0.3
34
- Requires-Dist: requests==2.31.0
35
34
  Requires-Dist: sensapex==1.400.0
36
- Requires-Dist: vbl-aquarium==0.0.12
35
+ Requires-Dist: vbl-aquarium==0.0.15
37
36
  Description-Content-Type: text/markdown
38
37
 
39
38
  # Electrophysiology Manipulator Link
@@ -124,16 +123,14 @@ Import the modules you need and launch the server.
124
123
  from ephys_link.server import Server
125
124
 
126
125
  server = Server()
127
- server.launch("sensapex", 8081)
126
+ server.launch("sensapex", args.proxy_address, 8081)
128
127
  ```
129
128
 
130
129
  ## Install for Development
131
130
 
132
131
  1. Clone the repository.
133
132
  2. Install [Hatch](https://hatch.pypa.io/latest/install/)
134
- 3. Install the latest Microsoft Visual C++ (MSVC v143+ x86/64) and the Windows SDK (10/11)
135
- via [Visual Studio Build Tools Installer](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
136
- 4. In a terminal, navigate to the repository's root directory and run
133
+ 3. In a terminal, navigate to the repository's root directory and run
137
134
 
138
135
  ```bash
139
136
  hatch shell
@@ -141,6 +138,10 @@ server.launch("sensapex", 8081)
141
138
 
142
139
  This will create a virtual environment, install Python 12 (if not found), and install the package in editable mode.
143
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
+
144
145
  # Documentation and More Information
145
146
 
146
147
  Complete documentation including API usage and development installation can be
@@ -86,16 +86,14 @@ Import the modules you need and launch the server.
86
86
  from ephys_link.server import Server
87
87
 
88
88
  server = Server()
89
- server.launch("sensapex", 8081)
89
+ server.launch("sensapex", args.proxy_address, 8081)
90
90
  ```
91
91
 
92
92
  ## Install for Development
93
93
 
94
94
  1. Clone the repository.
95
95
  2. Install [Hatch](https://hatch.pypa.io/latest/install/)
96
- 3. Install the latest Microsoft Visual C++ (MSVC v143+ x86/64) and the Windows SDK (10/11)
97
- via [Visual Studio Build Tools Installer](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
98
- 4. In a terminal, navigate to the repository's root directory and run
96
+ 3. In a terminal, navigate to the repository's root directory and run
99
97
 
100
98
  ```bash
101
99
  hatch shell
@@ -103,6 +101,10 @@ server.launch("sensapex", 8081)
103
101
 
104
102
  This will create a virtual environment, install Python 12 (if not found), and install the package in editable mode.
105
103
 
104
+ If you encounter any dependency issues (particularly with `aiohttp`), try installing the latest Microsoft Visual C++
105
+ (MSVC v143+ x86/64) and the Windows SDK (10/11)
106
+ via [Visual Studio Build Tools Installer](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
107
+
106
108
  # Documentation and More Information
107
109
 
108
110
  Complete documentation including API usage and development installation can be
@@ -0,0 +1,70 @@
1
+ # -*- mode: python ; coding: utf-8 -*-
2
+
3
+ from ephys_link.__about__ import __version__ as version
4
+
5
+ from argparse import ArgumentParser
6
+
7
+ parser = ArgumentParser()
8
+ parser.add_argument("-d", "--dir", action="store_true", help="Outputs a directory")
9
+ options = parser.parse_args()
10
+
11
+ FILE_NAME = f"EphysLink-v{version}"
12
+
13
+ a = Analysis(
14
+ ['src\\ephys_link\\__main__.py'],
15
+ pathex=[],
16
+ binaries=[('src\\ephys_link\\resources', 'ephys_link\\resources')],
17
+ datas=[],
18
+ hiddenimports=['engineio.async_drivers.aiohttp'],
19
+ hookspath=[],
20
+ hooksconfig={},
21
+ runtime_hooks=[],
22
+ excludes=[],
23
+ noarchive=False,
24
+ optimize=1,
25
+ )
26
+ pyz = PYZ(a.pure)
27
+
28
+ if options.dir:
29
+ exe = EXE(
30
+ pyz,
31
+ a.scripts,
32
+ [],
33
+ exlude_binaries=True,
34
+ name=FILE_NAME,
35
+ debug=False,
36
+ bootloader_ignore_signals=False,
37
+ strip=False,
38
+ upx=True,
39
+ upx_exclude=[],
40
+ console=True,
41
+ disable_windowed_traceback=False,
42
+ argv_emulation=False,
43
+ target_arch=None,
44
+ codesign_identity=None,
45
+ entitlements_file=None,
46
+ icon='assets\\icon.ico',
47
+ )
48
+ coll = COLLECT(exe, a.binaries, a.datas, strip=False, upx=True, upx_exclude=[], name=FILE_NAME)
49
+ else:
50
+ exe = EXE(
51
+ pyz,
52
+ a.scripts,
53
+ a.binaries,
54
+ a.datas,
55
+ [],
56
+ name=FILE_NAME,
57
+ debug=False,
58
+ bootloader_ignore_signals=False,
59
+ strip=False,
60
+ upx=True,
61
+ upx_exclude=[],
62
+ runtime_tmpdir=None,
63
+ console=True,
64
+ disable_windowed_traceback=False,
65
+ argv_emulation=False,
66
+ target_arch=None,
67
+ codesign_identity=None,
68
+ entitlements_file=None,
69
+ icon='assets\\icon.ico',
70
+ )
@@ -7,7 +7,7 @@ name = "ephys-link"
7
7
  dynamic = ["version"]
8
8
  description = "A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments."
9
9
  readme = "README.md"
10
- requires-python = ">=3.8, <3.13"
10
+ requires-python = ">=3.10, <3.13"
11
11
  license = "GPL-3.0-only"
12
12
  keywords = ["socket-io", "manipulator", "electrophysiology", "ephys", "sensapex", "neuroscience", "neurotech", "virtualbrainlab", "new-scale"]
13
13
  authors = [{ name = "Kenneth Yang", email = "kjy5@uw.edu" }]
@@ -30,14 +30,13 @@ classifiers = [
30
30
  "Topic :: Scientific/Engineering :: Medical Science Apps.",
31
31
  ]
32
32
  dependencies = [
33
- "aiohttp==3.9.4",
34
- "platformdirs==4.2.0",
33
+ "aiohttp==3.9.5",
34
+ "platformdirs==4.2.2",
35
35
  "pyserial==3.5",
36
- "python-socketio==5.11.2",
36
+ "python-socketio[asyncio_client]==5.11.2",
37
37
  "pythonnet==3.0.3",
38
- "requests==2.31.0",
39
38
  "sensapex==1.400.0",
40
- "vbl-aquarium==0.0.12"
39
+ "vbl-aquarium==0.0.15"
41
40
  ]
42
41
 
43
42
  [project.urls]
@@ -60,8 +59,8 @@ exclude = ["/.github", "/.idea"]
60
59
  python = "3.12"
61
60
  dependencies = [
62
61
  "coverage[toml]>=6.5",
62
+ "mypy>=1.0.0",
63
63
  "pytest",
64
- "python-socketio[client]==5.11.2",
65
64
  ]
66
65
  [tool.hatch.envs.default.scripts]
67
66
  test = "pytest {args:tests}"
@@ -74,24 +73,19 @@ cov = [
74
73
  "test-cov",
75
74
  "cov-report",
76
75
  ]
76
+ types = "mypy --strict --install-types --non-interactive {args:src/ephys_link tests}"
77
77
 
78
78
  #[[tool.hatch.envs.all.matrix]]
79
79
  #python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
80
80
 
81
- [tool.hatch.envs.types]
82
- dependencies = [
83
- "mypy>=1.0.0",
84
- ]
85
- [tool.hatch.envs.types.scripts]
86
- check = "mypy --install-types --non-interactive {args:src/ephys_link tests}"
87
-
88
81
  [tool.hatch.envs.exe]
89
82
  python = "3.12"
90
83
  dependencies = [
91
- "pyinstaller==6.3.0",
84
+ "pyinstaller",
92
85
  ]
93
86
  [tool.hatch.envs.exe.scripts]
94
- build = "pyinstaller.exe ephys_link.spec -y"
87
+ build = "pyinstaller.exe ephys_link.spec -y -- -d && pyinstaller.exe ephys_link.spec -y"
88
+ build_onefile = "pyinstaller.exe ephys_link.spec -y"
95
89
  build_clean = "pyinstaller.exe ephys_link.spec -y --clean"
96
90
 
97
91
  [tool.coverage.run]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from asyncio import run
4
+
5
+ from socketio import AsyncClient
6
+
7
+ pinpoint_id = "4158ebf3"
8
+ is_requester = True
9
+
10
+ sio = AsyncClient()
11
+
12
+
13
+ async def main():
14
+ await sio.connect("http://localhost:3000")
15
+ # await sio.emit("get_manipulators", lambda m: print(m))
16
+ await sio.wait()
17
+
18
+
19
+ @sio.event
20
+ async def get_pinpoint_id() -> tuple[str, bool]:
21
+ return pinpoint_id, is_requester
22
+
23
+
24
+ run(main())
@@ -0,0 +1 @@
1
+ __version__ = "1.3.3"
@@ -1,4 +1,5 @@
1
1
  from argparse import ArgumentParser
2
+ from asyncio import run
2
3
  from sys import argv
3
4
 
4
5
  from ephys_link import common as com
@@ -26,13 +27,22 @@ parser.add_argument(
26
27
  help='Manipulator type (i.e. "sensapex", "new_scale", or "new_scale_pathfinder"). Default: "sensapex".',
27
28
  )
28
29
  parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Enable debug mode.")
30
+ parser.add_argument("-x", "--use-proxy", dest="use_proxy", action="store_true", help="Enable proxy mode.")
31
+ parser.add_argument(
32
+ "-a",
33
+ "--proxy-address",
34
+ type=str,
35
+ default="proxy2.virtualbrainlab.org",
36
+ dest="proxy_address",
37
+ help="Proxy IP address.",
38
+ )
29
39
  parser.add_argument(
30
40
  "-p",
31
41
  "--port",
32
42
  type=int,
33
43
  default=8081,
34
44
  dest="port",
35
- help="Port to serve on. Default: 8081 (avoids conflict with other HTTP servers).",
45
+ help="TCP/IP port to use. Default: 8081 (avoids conflict with other HTTP servers).",
36
46
  )
37
47
  parser.add_argument(
38
48
  "--pathfinder_port",
@@ -83,7 +93,12 @@ def main() -> None:
83
93
  e_stop.watch()
84
94
 
85
95
  # Launch with parsed arguments on main thread.
86
- server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates)
96
+ if args.use_proxy:
97
+ run(
98
+ server.launch_for_proxy(args.proxy_address, args.port, args.type, args.pathfinder_port, args.ignore_updates)
99
+ )
100
+ else:
101
+ server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates)
87
102
 
88
103
 
89
104
  if __name__ == "__main__":
@@ -1,3 +1,4 @@
1
+ from asyncio import run
1
2
  from json import dumps, load
2
3
  from os import makedirs
3
4
  from os.path import exists
@@ -24,15 +25,27 @@ class GUI:
24
25
  self._root = Tk()
25
26
 
26
27
  # Create default settings dictionary
27
- settings = {"type": "sensapex", "debug": False, "port": 8081, "pathfinder_port": 8080, "serial": "no-e-stop"}
28
+ settings = {
29
+ "ignore_updates": False,
30
+ "type": "sensapex",
31
+ "debug": False,
32
+ "proxy": False,
33
+ "proxy_address": "proxy2.virtualbrainlab.org",
34
+ "port": 8081,
35
+ "pathfinder_port": 8080,
36
+ "serial": "no-e-stop",
37
+ }
28
38
 
29
39
  # Read settings.
30
40
  if exists(f"{SETTINGS_DIR}\\{SETTINGS_FILENAME}"):
31
41
  with open(f"{SETTINGS_DIR}\\{SETTINGS_FILENAME}") as settings_file:
32
42
  settings = load(settings_file)
33
43
 
44
+ self._ignore_updates = BooleanVar(value=settings["ignore_updates"])
34
45
  self._type = StringVar(value=settings["type"])
35
46
  self._debug = BooleanVar(value=settings["debug"])
47
+ self._proxy = BooleanVar(value=settings["proxy"])
48
+ self._proxy_address = StringVar(value=settings["proxy_address"])
36
49
  self._port = IntVar(value=settings["port"])
37
50
  self._pathfinder_port = IntVar(value=settings["pathfinder_port"])
38
51
  self._serial = StringVar(value=settings["serial"])
@@ -61,15 +74,41 @@ class GUI:
61
74
  server_serving_settings = ttk.LabelFrame(mainframe, text="Serving Settings", padding=3)
62
75
  server_serving_settings.grid(column=0, row=0, sticky="news")
63
76
 
64
- # IP.
65
- ttk.Label(server_serving_settings, text="IP:", anchor=E, justify=RIGHT).grid(column=0, row=0, sticky="we")
77
+ # Local IP.
78
+ ttk.Label(server_serving_settings, text="Local IP:", anchor=E, justify=RIGHT).grid(column=0, row=0, sticky="we")
66
79
  ttk.Label(server_serving_settings, text=gethostbyname(gethostname())).grid(column=1, row=0, sticky="we")
67
80
 
81
+ # Proxy.
82
+ ttk.Label(server_serving_settings, text="Use Proxy:", anchor=E, justify=RIGHT).grid(
83
+ column=0, row=1, sticky="we"
84
+ )
85
+ ttk.Checkbutton(
86
+ server_serving_settings,
87
+ variable=self._proxy,
88
+ ).grid(column=1, row=1, sticky="we")
89
+
90
+ # Proxy address.
91
+ ttk.Label(server_serving_settings, text="Proxy Address:", anchor=E, justify=RIGHT).grid(
92
+ column=0, row=2, sticky="we"
93
+ )
94
+ ttk.Entry(server_serving_settings, textvariable=self._proxy_address, justify=CENTER).grid(
95
+ column=1, row=2, sticky="we"
96
+ )
97
+
68
98
  # Port.
69
- ttk.Label(server_serving_settings, text="Port:", anchor=E, justify=RIGHT).grid(column=0, row=1, sticky="we")
99
+ ttk.Label(server_serving_settings, text="Port:", anchor=E, justify=RIGHT).grid(column=0, row=3, sticky="we")
70
100
  ttk.Entry(server_serving_settings, textvariable=self._port, width=5, justify=CENTER).grid(
71
- column=1, row=1, sticky="we"
101
+ column=1, row=3, sticky="we"
102
+ )
103
+
104
+ # Ignore updates.
105
+ ttk.Label(server_serving_settings, text="Ignore Updates:", anchor=E, justify=RIGHT).grid(
106
+ column=0, row=4, sticky="we"
72
107
  )
108
+ ttk.Checkbutton(
109
+ server_serving_settings,
110
+ variable=self._ignore_updates,
111
+ ).grid(column=1, row=4, sticky="we")
73
112
 
74
113
  # ---
75
114
 
@@ -141,8 +180,11 @@ class GUI:
141
180
 
142
181
  # Save settings.
143
182
  settings = {
183
+ "ignore_updates": self._ignore_updates.get(),
144
184
  "type": self._type.get(),
145
185
  "debug": self._debug.get(),
186
+ "proxy": self._proxy.get(),
187
+ "proxy_address": self._proxy_address.get(),
146
188
  "port": self._port.get(),
147
189
  "pathfinder_port": self._pathfinder_port.get(),
148
190
  "serial": self._serial.get(),
@@ -160,4 +202,16 @@ class GUI:
160
202
  e_stop = EmergencyStop(server, self._serial.get())
161
203
  e_stop.watch()
162
204
 
163
- server.launch(self._type.get(), self._port.get(), self._pathfinder_port.get())
205
+ # Launch with parsed arguments on main thread.
206
+ if self._proxy.get():
207
+ run(
208
+ server.launch_for_proxy(
209
+ self._proxy_address.get(),
210
+ self._port.get(),
211
+ self._type.get(),
212
+ self._pathfinder_port.get(),
213
+ self._ignore_updates.get(),
214
+ )
215
+ )
216
+ else:
217
+ server.launch(self._type.get(), self._port.get(), self._pathfinder_port.get(), self._ignore_updates.get())
@@ -34,7 +34,7 @@ from vbl_aquarium.models.unity import Vector4
34
34
  from ephys_link import common as com
35
35
 
36
36
  if TYPE_CHECKING:
37
- import socketio
37
+ from socketio import AsyncClient, AsyncServer
38
38
 
39
39
 
40
40
  class PlatformHandler(ABC):
@@ -309,7 +309,7 @@ class PlatformHandler(ABC):
309
309
  print(f"{e}\n")
310
310
  return BooleanStateResponse(error="Error setting inside brain")
311
311
 
312
- async def calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> str:
312
+ async def calibrate(self, manipulator_id: str, sio: AsyncClient | AsyncServer) -> str:
313
313
  """Calibrate manipulator
314
314
 
315
315
  :param manipulator_id: ID of manipulator to calibrate
@@ -423,7 +423,7 @@ class PlatformHandler(ABC):
423
423
  raise NotImplementedError
424
424
 
425
425
  @abstractmethod
426
- async def _calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> str:
426
+ async def _calibrate(self, manipulator_id: str, sio: AsyncClient | AsyncServer) -> str:
427
427
  """Calibrate manipulator
428
428
 
429
429
  :param manipulator_id: ID of manipulator to calibrate
@@ -3,6 +3,7 @@
3
3
  Most functionality will be implemented on the platform handler side. This is mostly
4
4
  for enforcing implementation of the stop method and hold common properties.
5
5
  """
6
+
6
7
  from abc import ABC, abstractmethod
7
8
 
8
9
  # Constants
@@ -95,7 +95,7 @@ class SensapexHandler(PlatformHandler):
95
95
  cur_pos = self.manipulators[manipulator_id].get_pos()["position"]
96
96
 
97
97
  # Check difference between current and target position
98
- for prev, cur in zip([10000, 10000, 10000, 10000], cur_pos):
98
+ for prev, cur in zip([10000, 10000, 10000, 10000], cur_pos, strict=False):
99
99
  if abs(prev - cur) > 1:
100
100
  still_working = True
101
101
  break
@@ -59,7 +59,13 @@ class SensapexManipulator(PlatformManipulator):
59
59
  # com.dprint(f"[SUCCESS]\t Got position of manipulator {self._id}\n")
60
60
  return PositionalResponse(
61
61
  position=Vector4(
62
- **dict(zip(Vector4.model_fields.keys(), [axis / MM_TO_UM for axis in self._device.get_pos(1)]))
62
+ **dict(
63
+ zip(
64
+ Vector4.model_fields.keys(),
65
+ [axis / MM_TO_UM for axis in self._device.get_pos(1)],
66
+ strict=False,
67
+ )
68
+ )
63
69
  )
64
70
  )
65
71
  except Exception as e:
@@ -28,30 +28,30 @@ class UMP3Handler(SensapexHandler):
28
28
 
29
29
  self.manipulators[manipulator_id] = UMP3Manipulator(self.ump.get_device(int(manipulator_id)))
30
30
 
31
- def _platform_space_to_unified_space(self, platform_position: list[float]) -> list[float]:
31
+ def _platform_space_to_unified_space(self, platform_position: Vector4) -> Vector4:
32
32
  # unified <- platform
33
33
  # +x <- +y
34
34
  # +y <- -x
35
35
  # +z <- -z
36
36
  # +d <- +d/x
37
37
 
38
- return [
39
- platform_position[1],
40
- self.dimensions[0] - platform_position[0],
41
- self.dimensions[2] - platform_position[2],
42
- platform_position[3],
43
- ]
38
+ return Vector4(
39
+ x=platform_position.y,
40
+ y=self.dimensions.x - platform_position.x,
41
+ z=self.dimensions.z - platform_position.z,
42
+ w=platform_position.w,
43
+ )
44
44
 
45
- def _unified_space_to_platform_space(self, unified_position: list[float]) -> list[float]:
45
+ def _unified_space_to_platform_space(self, unified_position: Vector4) -> Vector4:
46
46
  # platform <- unified
47
47
  # +x <- -y
48
48
  # +y <- +x
49
49
  # +z <- -z
50
50
  # +d/x <- +d
51
51
 
52
- return [
53
- self.dimensions[1] - unified_position[1],
54
- unified_position[0],
55
- self.dimensions[2] - unified_position[2],
56
- unified_position[3],
57
- ]
52
+ return Vector4(
53
+ x=self.dimensions.y - unified_position.y,
54
+ y=unified_position.x,
55
+ z=self.dimensions.z - unified_position.z,
56
+ w=unified_position.w,
57
+ )
@@ -55,7 +55,9 @@ class UMP3Manipulator(SensapexManipulator):
55
55
  position.append(position[0])
56
56
 
57
57
  # com.dprint(f"[SUCCESS]\t Got position of manipulator {self._id}\n")
58
- return PositionalResponse(position=Vector4(**dict(zip(Vector4.model_fields.keys(), position))))
58
+ return PositionalResponse(
59
+ position=Vector4(**dict(zip(Vector4.model_fields.keys(), position, strict=False)))
60
+ )
59
61
  except Exception as e:
60
62
  print(f"[ERROR]\t\t Getting position of manipulator {self._id}")
61
63
  print(f"{e}\n")
@@ -11,18 +11,20 @@ every event, the server does the following:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ from asyncio import get_event_loop
14
15
  from json import loads
15
16
  from signal import SIGINT, SIGTERM, signal
16
- from sys import exit
17
17
  from typing import TYPE_CHECKING, Any
18
+ from uuid import uuid4
18
19
 
19
- from aiohttp import web
20
+ from aiohttp import ClientConnectionError, ClientSession
21
+ from aiohttp.web import Application, run_app
20
22
  from aiohttp.web_runner import GracefulExit
21
- from packaging import version
23
+ from packaging.version import parse
22
24
  from pydantic import ValidationError
23
- from requests import get
24
- from requests.exceptions import ConnectionError
25
- from socketio import AsyncServer
25
+
26
+ # from socketio import AsyncServer
27
+ from socketio import AsyncClient, AsyncServer
26
28
  from vbl_aquarium.models.ephys_link import (
27
29
  BooleanStateResponse,
28
30
  CanWriteRequest,
@@ -32,6 +34,7 @@ from vbl_aquarium.models.ephys_link import (
32
34
  InsideBrainRequest,
33
35
  PositionalResponse,
34
36
  )
37
+ from vbl_aquarium.models.proxy import PinpointIdResponse
35
38
 
36
39
  from ephys_link.__about__ import __version__
37
40
  from ephys_link.common import (
@@ -48,46 +51,31 @@ if TYPE_CHECKING:
48
51
 
49
52
 
50
53
  class Server:
51
- def __init__(self):
52
- # Server and Socketio
53
- self.sio = AsyncServer()
54
- self.app = web.Application()
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
55
62
 
63
+ # Proxy server ID.
64
+ self.pinpoint_id: str = ""
65
+
66
+ # Manipulator platform handler.
67
+ self.platform: PlatformHandler | None = None
56
68
  # Is there a client connected?
57
69
  self.is_connected = False
58
70
 
59
71
  # Is the server running?
60
72
  self.is_running = False
61
73
 
62
- # Current platform handler (defaults to Sensapex).
63
- self.platform: PlatformHandler = SensapexHandler()
64
-
65
74
  # Register server exit handlers.
66
75
  signal(SIGTERM, self.close_server)
67
76
  signal(SIGINT, self.close_server)
68
77
 
69
- # Attach server to the web app.
70
- self.sio.attach(self.app)
71
-
72
- # Declare events and assign handlers.
73
- self.sio.on("connect", self.connect)
74
- self.sio.on("disconnect", self.disconnect)
75
- self.sio.on("get_version", self.get_version)
76
- self.sio.on("get_manipulators", self.get_manipulators)
77
- self.sio.on("register_manipulator", self.register_manipulator)
78
- self.sio.on("unregister_manipulator", self.unregister_manipulator)
79
- self.sio.on("get_pos", self.get_pos)
80
- self.sio.on("get_angles", self.get_angles)
81
- self.sio.on("get_shank_count", self.get_shank_count)
82
- self.sio.on("goto_pos", self.goto_pos)
83
- self.sio.on("drive_to_depth", self.drive_to_depth)
84
- self.sio.on("set_inside_brain", self.set_inside_brain)
85
- self.sio.on("calibrate", self.calibrate)
86
- self.sio.on("bypass_calibration", self.bypass_calibration)
87
- self.sio.on("set_can_write", self.set_can_write)
88
- self.sio.on("stop", self.stop)
89
- self.sio.on("*", self.catch_all)
90
-
78
+ # Server events.
91
79
  async def connect(self, sid, _, __) -> bool:
92
80
  """Acknowledge connection to the server.
93
81
 
@@ -122,7 +110,15 @@ class Server:
122
110
  self.platform.reset()
123
111
  self.is_connected = False
124
112
 
125
- # Events
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()
126
122
 
127
123
  @staticmethod
128
124
  async def get_version(_) -> str:
@@ -133,6 +129,8 @@ class Server:
133
129
  :return: Version number as defined in :mod:`ephys_link.__about__`.
134
130
  :rtype: str
135
131
  """
132
+ dprint("[EVENT]\t\t Get version")
133
+
136
134
  return __version__
137
135
 
138
136
  async def get_manipulators(self, _) -> str:
@@ -361,38 +359,21 @@ class Server:
361
359
  print(f"[UNKNOWN EVENT]:\t {data}")
362
360
  return "UNKNOWN_EVENT"
363
361
 
364
- def launch(
365
- self,
366
- platform_type: str,
367
- server_port: int,
368
- pathfinder_port: int | None = None,
369
- ignore_updates: bool = False, # noqa: FBT002
370
- ) -> None:
371
- """Launch the server.
372
-
373
- :param platform_type: Parsed argument for platform type.
374
- :type platform_type: str
375
- :param server_port: HTTP port to serve the server.
376
- :type server_port: int
377
- :param pathfinder_port: Port New Scale Pathfinder's server is on.
378
- :type pathfinder_port: int
379
- :param ignore_updates: Flag to ignore checking for updates.
380
- :type ignore_updates: bool
381
- :return: None
382
- """
383
-
362
+ # Server functions
363
+ async def launch_setup(self, platform_type: str, pathfinder_port: int, ignore_updates) -> None:
384
364
  # Import correct manipulator handler
385
- if platform_type == "sensapex":
386
- # Already assigned (was the default)
387
- pass
388
- elif platform_type == "ump3":
389
- self.platform = UMP3Handler()
390
- elif platform_type == "new_scale":
391
- self.platform = NewScaleHandler()
392
- elif platform_type == "new_scale_pathfinder":
393
- self.platform = NewScalePathfinderHandler(pathfinder_port)
394
- else:
395
- exit(f"[ERROR]\t\t Invalid manipulator type: {platform_type}")
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)
396
377
 
397
378
  # Preamble.
398
379
  print(ASCII)
@@ -401,19 +382,24 @@ class Server:
401
382
  # Check for newer version.
402
383
  if not ignore_updates:
403
384
  try:
404
- version_request = get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags", timeout=10)
405
- latest_version = version_request.json()[0]["name"]
406
- if version.parse(latest_version) > version.parse(__version__):
407
- print(f"New version available: {latest_version}")
408
- print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest")
409
- except ConnectionError:
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:
410
396
  pass
411
397
 
412
398
  # Explain window.
413
399
  print()
414
400
  print("This is the Ephys Link server window.")
415
401
  print("You may safely leave it running in the background.")
416
- print("To stop the it, close this window or press CTRL + Pause/Break.")
402
+ print("To stop it, close this window or press CTRL + Pause/Break.")
417
403
  print()
418
404
 
419
405
  # List available manipulators
@@ -421,9 +407,95 @@ class Server:
421
407
  print(self.platform.get_manipulators().manipulators)
422
408
  print()
423
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
+
424
477
  # Mark that server is running
425
478
  self.is_running = True
426
- web.run_app(self.app, port=server_port)
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)
427
499
 
428
500
  def close_server(self, _, __) -> None:
429
501
  """Close the server."""
@@ -1,39 +0,0 @@
1
- # -*- mode: python ; coding: utf-8 -*-
2
-
3
- from ephys_link.__about__ import __version__ as version
4
-
5
- a = Analysis(
6
- ['src\\ephys_link\\__main__.py'],
7
- pathex=[],
8
- binaries=[('src\\ephys_link\\resources', 'ephys_link\\resources')],
9
- datas=[],
10
- hiddenimports=['engineio.async_drivers.aiohttp'],
11
- hookspath=[],
12
- hooksconfig={},
13
- runtime_hooks=[],
14
- excludes=[],
15
- noarchive=False,
16
- )
17
- pyz = PYZ(a.pure)
18
-
19
- exe = EXE(
20
- pyz,
21
- a.scripts,
22
- a.binaries,
23
- a.datas,
24
- [],
25
- name=f"EphysLink-v{version}",
26
- debug=False,
27
- bootloader_ignore_signals=False,
28
- strip=False,
29
- upx=True,
30
- upx_exclude=[],
31
- runtime_tmpdir=None,
32
- console=True,
33
- disable_windowed_traceback=False,
34
- argv_emulation=False,
35
- target_arch=None,
36
- codesign_identity=None,
37
- entitlements_file=None,
38
- icon='assets\\icon.ico',
39
- )
@@ -1 +0,0 @@
1
- __version__ = "1.3.0b2"
File without changes
File without changes
File without changes