From 4dbe6d6cf3a17b351c85b3affbc245eef9890a07 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 10 Apr 2024 12:56:07 -0700 Subject: [PATCH 1/8] Basic connection and ID response --- pyproject.toml | 2 +- scripts/proxy_dev.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 scripts/proxy_dev.py diff --git a/pyproject.toml b/pyproject.toml index 9a289b8..18c5151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ python = "3.12" dependencies = [ "coverage[toml]>=6.5", "pytest", - "python-socketio[client]==5.11.1", + "python-socketio[asyncio_client]==5.11.2", ] [tool.hatch.envs.default.scripts] test = "pytest {args:tests}" diff --git a/scripts/proxy_dev.py b/scripts/proxy_dev.py new file mode 100644 index 0000000..c2ebbeb --- /dev/null +++ b/scripts/proxy_dev.py @@ -0,0 +1,20 @@ +from asyncio import run + +from socketio import AsyncClient + +pinpoint_id = "abcdefgh" + +sio = AsyncClient() + + +async def main(): + await sio.connect("http://localhost:3000") + await sio.wait() + + +@sio.event +async def get_id(): + return pinpoint_id + + +run(main()) From 09e76d001b496d8104671bb34bb8acc1c8d95ab0 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 24 Apr 2024 14:07:22 -0700 Subject: [PATCH 2/8] Add requester flag --- scripts/proxy_dev.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/proxy_dev.py b/scripts/proxy_dev.py index c2ebbeb..4b736fe 100644 --- a/scripts/proxy_dev.py +++ b/scripts/proxy_dev.py @@ -3,6 +3,7 @@ from socketio import AsyncClient pinpoint_id = "abcdefgh" +is_requester = False sio = AsyncClient() @@ -13,8 +14,8 @@ async def main(): @sio.event -async def get_id(): - return pinpoint_id +async def get_pinpoint_id() -> tuple[str, bool]: + return pinpoint_id, is_requester run(main()) From 7b6cf70092c01bc19f18a2381df2cd112d7b5466 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 1 May 2024 17:37:17 -0700 Subject: [PATCH 3/8] Server connects as client (need to clean up) --- scripts/proxy_dev.py | 7 ++- src/ephys_link/__main__.py | 3 +- src/ephys_link/server.py | 93 ++++++++++++++++++++++---------------- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/scripts/proxy_dev.py b/scripts/proxy_dev.py index 4b736fe..3d11501 100644 --- a/scripts/proxy_dev.py +++ b/scripts/proxy_dev.py @@ -1,15 +1,18 @@ +from __future__ import annotations + from asyncio import run from socketio import AsyncClient -pinpoint_id = "abcdefgh" -is_requester = False +pinpoint_id = "4158ebf3" +is_requester = True sio = AsyncClient() async def main(): await sio.connect("http://localhost:3000") + # await sio.emit("get_manipulators", lambda m: print(m)) await sio.wait() diff --git a/src/ephys_link/__main__.py b/src/ephys_link/__main__.py index 1b0c64a..0fc36f9 100644 --- a/src/ephys_link/__main__.py +++ b/src/ephys_link/__main__.py @@ -1,4 +1,5 @@ from argparse import ArgumentParser +from asyncio import run from sys import argv from ephys_link import common as com @@ -83,7 +84,7 @@ def main() -> None: e_stop.watch() # Launch with parsed arguments on main thread. - server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates) + run(server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates)) if __name__ == "__main__": diff --git a/src/ephys_link/server.py b/src/ephys_link/server.py index dd604d1..75a805d 100644 --- a/src/ephys_link/server.py +++ b/src/ephys_link/server.py @@ -11,10 +11,12 @@ from __future__ import annotations +import json from json import loads from signal import SIGINT, SIGTERM, signal from sys import exit from typing import TYPE_CHECKING, Any +from uuid import uuid4 from aiohttp import web from aiohttp.web_runner import GracefulExit @@ -22,7 +24,9 @@ from pydantic import ValidationError from requests import get from requests.exceptions import ConnectionError -from socketio import AsyncServer + +# from socketio import AsyncServer +from socketio import AsyncClient from vbl_aquarium.models.ephys_link import ( BooleanStateResponse, CanWriteRequest, @@ -50,8 +54,10 @@ class Server: def __init__(self): # Server and Socketio - self.sio = AsyncServer() + # self.sio = AsyncServer() + self.sio = AsyncClient() self.app = web.Application() + self.pinpoint_id = str(uuid4())[:8] # Is there a client connected? self.is_connected = False @@ -67,11 +73,12 @@ def __init__(self): signal(SIGINT, self.close_server) # Attach server to the web app. - self.sio.attach(self.app) + # self.sio.attach(self.app) # Declare events and assign handlers. - self.sio.on("connect", self.connect) - self.sio.on("disconnect", self.disconnect) + # self.sio.on("connect", self.connect) + # self.sio.on("disconnect", self.disconnect) + self.sio.on("get_pinpoint_id", self.get_pinpoint_id) self.sio.on("get_version", self.get_version) self.sio.on("get_manipulators", self.get_manipulators) self.sio.on("register_manipulator", self.register_manipulator) @@ -88,41 +95,49 @@ def __init__(self): self.sio.on("stop", self.stop) self.sio.on("*", self.catch_all) - async def connect(self, sid, _, __) -> bool: - """Acknowledge connection to the server. - - :param sid: Socket session ID. - :type sid: str - :param _: WSGI formatted dictionary with request info (unused). - :type _: dict - :param __: Authentication details (unused). - :type __: dict - :return: False on error to refuse connection. True otherwise. - :rtype: bool - """ - print(f"[CONNECTION REQUEST]:\t\t {sid}\n") - - if not self.is_connected: - print(f"[CONNECTION GRANTED]:\t\t {sid}\n") - self.is_connected = True - return True + # async def connect(self, sid, _, __) -> bool: + # """Acknowledge connection to the server. + # + # :param sid: Socket session ID. + # :type sid: str + # :param _: WSGI formatted dictionary with request info (unused). + # :type _: dict + # :param __: Authentication details (unused). + # :type __: dict + # :return: False on error to refuse connection. True otherwise. + # :rtype: bool + # """ + # print(f"[CONNECTION REQUEST]:\t\t {sid}\n") + # + # if not self.is_connected: + # print(f"[CONNECTION GRANTED]:\t\t {sid}\n") + # self.is_connected = True + # return True + # + # print(f"[CONNECTION DENIED]:\t\t {sid}: another client is already connected\n") + # return False + # + # async def disconnect(self, sid) -> None: + # """Acknowledge disconnection from the server. + # + # :param sid: Socket session ID. + # :type sid: str + # :return: None + # """ + # print(f"[DISCONNECTION]:\t {sid}\n") + # + # self.platform.reset() + # self.is_connected = False - print(f"[CONNECTION DENIED]:\t\t {sid}: another client is already connected\n") - return False + # Events - async def disconnect(self, sid) -> None: - """Acknowledge disconnection from the server. + async def get_pinpoint_id(self) -> str: + """Get the pinpoint ID. - :param sid: Socket session ID. - :type sid: str - :return: None + :return: Pinpoint ID and whether the client is a requester. + :rtype: tuple[str, bool] """ - print(f"[DISCONNECTION]:\t {sid}\n") - - self.platform.reset() - self.is_connected = False - - # Events + return json.dumps({"pinpoint_id": self.pinpoint_id, "is_requester": False}) @staticmethod async def get_version(_) -> str: @@ -361,7 +376,7 @@ async def catch_all(_, __, data: Any) -> str: print(f"[UNKNOWN EVENT]:\t {data}") return "UNKNOWN_EVENT" - def launch( + async def launch( self, platform_type: str, server_port: int, @@ -423,7 +438,9 @@ def launch( # Mark that server is running self.is_running = True - web.run_app(self.app, port=server_port) + # web.run_app(self.app, port=server_port) + await self.sio.connect("http://localhost:3000") + await self.sio.wait() def close_server(self, _, __) -> None: """Close the server.""" From 88d6aa6fc13ae40096c5973c49a973b7dd68f004 Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 8 May 2024 16:52:45 -0700 Subject: [PATCH 4/8] Debugging responses --- src/ephys_link/server.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ephys_link/server.py b/src/ephys_link/server.py index 75a805d..e8f7266 100644 --- a/src/ephys_link/server.py +++ b/src/ephys_link/server.py @@ -16,7 +16,6 @@ from signal import SIGINT, SIGTERM, signal from sys import exit from typing import TYPE_CHECKING, Any -from uuid import uuid4 from aiohttp import web from aiohttp.web_runner import GracefulExit @@ -57,7 +56,7 @@ def __init__(self): # self.sio = AsyncServer() self.sio = AsyncClient() self.app = web.Application() - self.pinpoint_id = str(uuid4())[:8] + self.pinpoint_id = "abcde" # str(uuid4())[:8] # Is there a client connected? self.is_connected = False @@ -97,7 +96,7 @@ def __init__(self): # async def connect(self, sid, _, __) -> bool: # """Acknowledge connection to the server. - # + # # :param sid: Socket session ID. # :type sid: str # :param _: WSGI formatted dictionary with request info (unused). @@ -108,24 +107,24 @@ def __init__(self): # :rtype: bool # """ # print(f"[CONNECTION REQUEST]:\t\t {sid}\n") - # + # # if not self.is_connected: # print(f"[CONNECTION GRANTED]:\t\t {sid}\n") # self.is_connected = True # return True - # + # # print(f"[CONNECTION DENIED]:\t\t {sid}: another client is already connected\n") # return False - # + # # async def disconnect(self, sid) -> None: # """Acknowledge disconnection from the server. - # + # # :param sid: Socket session ID. # :type sid: str # :return: None # """ # print(f"[DISCONNECTION]:\t {sid}\n") - # + # # self.platform.reset() # self.is_connected = False @@ -137,7 +136,7 @@ async def get_pinpoint_id(self) -> str: :return: Pinpoint ID and whether the client is a requester. :rtype: tuple[str, bool] """ - return json.dumps({"pinpoint_id": self.pinpoint_id, "is_requester": False}) + return json.dumps({"pinpoint_id": self.pinpoint_id, "is_requester": False}) @staticmethod async def get_version(_) -> str: @@ -148,6 +147,8 @@ async def get_version(_) -> str: :return: Version number as defined in :mod:`ephys_link.__about__`. :rtype: str """ + dprint("[EVENT]\t\t Get version") + return __version__ async def get_manipulators(self, _) -> str: From 7036aa81f2eb430ecd702bd5c2b14e0c36fae93c Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 22 May 2024 15:03:12 -0700 Subject: [PATCH 5/8] Add proxy arg --- README.md | 2 +- src/ephys_link/__main__.py | 8 ++- src/ephys_link/gui.py | 2 +- src/ephys_link/server.py | 142 ++++++++++++++++++++++--------------- 4 files changed, 91 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 7a06d88..7a0622d 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Import the modules you need and launch the server. from ephys_link.server import Server server = Server() -server.launch("sensapex", 8081) +server.launch("sensapex", args.proxy_address, 8081) ``` ## Install for Development diff --git a/src/ephys_link/__main__.py b/src/ephys_link/__main__.py index 0fc36f9..b3ea3e0 100644 --- a/src/ephys_link/__main__.py +++ b/src/ephys_link/__main__.py @@ -27,13 +27,15 @@ help='Manipulator type (i.e. "sensapex", "new_scale", or "new_scale_pathfinder"). Default: "sensapex".', ) parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Enable debug mode.") +parser.add_argument("-x", "--use-proxy", dest="use_proxy", action="store_true", help="Enable proxy mode.") +parser.add_argument("-a", "--proxy-address", type=str, dest="proxy_address", help="Proxy IP address.") parser.add_argument( "-p", "--port", type=int, default=8081, dest="port", - help="Port to serve on. Default: 8081 (avoids conflict with other HTTP servers).", + help="TCP/IP port to use. Default: 8081 (avoids conflict with other HTTP servers).", ) parser.add_argument( "--pathfinder_port", @@ -73,7 +75,7 @@ def main() -> None: return None # Otherwise, create Server from CLI. - server = Server() + server = Server(args.use_proxy) # Continue with CLI if not. com.DEBUG = args.debug @@ -84,7 +86,7 @@ def main() -> None: e_stop.watch() # Launch with parsed arguments on main thread. - run(server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates)) + run(server.launch(args.type, args.proxy_address, args.port, args.pathfinder_port, args.ignore_updates)) if __name__ == "__main__": diff --git a/src/ephys_link/gui.py b/src/ephys_link/gui.py index de2076e..e60d7d7 100644 --- a/src/ephys_link/gui.py +++ b/src/ephys_link/gui.py @@ -160,4 +160,4 @@ def _launch_server(self) -> None: e_stop = EmergencyStop(server, self._serial.get()) e_stop.watch() - server.launch(self._type.get(), self._port.get(), self._pathfinder_port.get()) + server.launch(self._type.get(), "", self._port.get(), self._pathfinder_port.get()) diff --git a/src/ephys_link/server.py b/src/ephys_link/server.py index e8f7266..7927fd0 100644 --- a/src/ephys_link/server.py +++ b/src/ephys_link/server.py @@ -17,15 +17,14 @@ from sys import exit from typing import TYPE_CHECKING, Any -from aiohttp import web +from aiohttp import ClientSession, web from aiohttp.web_runner import GracefulExit from packaging import version from pydantic import ValidationError -from requests import get from requests.exceptions import ConnectionError # from socketio import AsyncServer -from socketio import AsyncClient +from socketio import AsyncClient, AsyncServer from vbl_aquarium.models.ephys_link import ( BooleanStateResponse, CanWriteRequest, @@ -51,12 +50,24 @@ class Server: - def __init__(self): - # Server and Socketio - # self.sio = AsyncServer() - self.sio = AsyncClient() - self.app = web.Application() - self.pinpoint_id = "abcde" # str(uuid4())[:8] + def __init__(self, proxy_mode: bool) -> None: + """Set up the server. + + :param proxy_mode: Flag to enable proxy mode. + :type proxy_mode: bool + """ + + # Proxy mode flag. + self.proxy_mode = proxy_mode + + # Server and Socketio setup. + if self.proxy_mode: + self.sio = AsyncClient() + self.pinpoint_id = "abcde" # str(uuid4())[:8] + + else: + self.sio = AsyncServer() + self.app = web.Application() # Is there a client connected? self.is_connected = False @@ -71,12 +82,12 @@ def __init__(self): signal(SIGTERM, self.close_server) signal(SIGINT, self.close_server) - # Attach server to the web app. - # self.sio.attach(self.app) - # Declare events and assign handlers. - # self.sio.on("connect", self.connect) - # self.sio.on("disconnect", self.disconnect) + if not self.proxy_mode: + self.sio.attach(self.app) + self.sio.on("connect", self.connect) + self.sio.on("disconnect", self.disconnect) + self.sio.on("get_pinpoint_id", self.get_pinpoint_id) self.sio.on("get_version", self.get_version) self.sio.on("get_manipulators", self.get_manipulators) @@ -94,41 +105,42 @@ def __init__(self): self.sio.on("stop", self.stop) self.sio.on("*", self.catch_all) - # async def connect(self, sid, _, __) -> bool: - # """Acknowledge connection to the server. - # - # :param sid: Socket session ID. - # :type sid: str - # :param _: WSGI formatted dictionary with request info (unused). - # :type _: dict - # :param __: Authentication details (unused). - # :type __: dict - # :return: False on error to refuse connection. True otherwise. - # :rtype: bool - # """ - # print(f"[CONNECTION REQUEST]:\t\t {sid}\n") - # - # if not self.is_connected: - # print(f"[CONNECTION GRANTED]:\t\t {sid}\n") - # self.is_connected = True - # return True - # - # print(f"[CONNECTION DENIED]:\t\t {sid}: another client is already connected\n") - # return False - # - # async def disconnect(self, sid) -> None: - # """Acknowledge disconnection from the server. - # - # :param sid: Socket session ID. - # :type sid: str - # :return: None - # """ - # print(f"[DISCONNECTION]:\t {sid}\n") - # - # self.platform.reset() - # self.is_connected = False - - # Events + # Server events. + async def connect(self, sid, _, __) -> bool: + """Acknowledge connection to the server. + + :param sid: Socket session ID. + :type sid: str + :param _: WSGI formatted dictionary with request info (unused). + :type _: dict + :param __: Authentication details (unused). + :type __: dict + :return: False on error to refuse connection. True otherwise. + :rtype: bool + """ + print(f"[CONNECTION REQUEST]:\t\t {sid}\n") + + if not self.is_connected: + print(f"[CONNECTION GRANTED]:\t\t {sid}\n") + self.is_connected = True + return True + + print(f"[CONNECTION DENIED]:\t\t {sid}: another client is already connected\n") + return False + + async def disconnect(self, sid) -> None: + """Acknowledge disconnection from the server. + + :param sid: Socket session ID. + :type sid: str + :return: None + """ + print(f"[DISCONNECTION]:\t {sid}\n") + + self.platform.reset() + self.is_connected = False + + # Ephys Link Events async def get_pinpoint_id(self) -> str: """Get the pinpoint ID. @@ -380,6 +392,7 @@ async def catch_all(_, __, data: Any) -> str: async def launch( self, platform_type: str, + proxy_address: str, server_port: int, pathfinder_port: int | None = None, ignore_updates: bool = False, # noqa: FBT002 @@ -388,6 +401,8 @@ async def launch( :param platform_type: Parsed argument for platform type. :type platform_type: str + :param proxy_address: Parsed argument for proxy address. + :type proxy_address: str :param server_port: HTTP port to serve the server. :type server_port: int :param pathfinder_port: Port New Scale Pathfinder's server is on. @@ -417,11 +432,13 @@ async def launch( # Check for newer version. if not ignore_updates: try: - version_request = get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags", timeout=10) - latest_version = version_request.json()[0]["name"] - if version.parse(latest_version) > version.parse(__version__): - print(f"New version available: {latest_version}") - print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest") + async with ClientSession() as session, session.get( + "https://api.github.com/repos/VirtualBrainLab/ephys-link/tags" + ) as response: + latest_version = (await response.json())[0]["name"] + if version.parse(latest_version) > version.parse(__version__): + print(f"New version available: {latest_version}") + print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest") except ConnectionError: pass @@ -429,7 +446,7 @@ async def launch( print() print("This is the Ephys Link server window.") print("You may safely leave it running in the background.") - print("To stop the it, close this window or press CTRL + Pause/Break.") + print("To stop it, close this window or press CTRL + Pause/Break.") print() # List available manipulators @@ -439,9 +456,18 @@ async def launch( # Mark that server is running self.is_running = True - # web.run_app(self.app, port=server_port) - await self.sio.connect("http://localhost:3000") - await self.sio.wait() + + if self.proxy_mode: + # Verify that the server was initialized correctly. + if type(self.sio) is not AsyncClient: + error = "Server was not initialized to a Client for proxy mode!" + raise ValueError(error) + # noinspection PyUnresolvedReferences,HttpUrlsUsage + await self.sio.connect(f"http://{proxy_address}:{server_port}") + # noinspection PyUnresolvedReferences + await self.sio.wait() + else: + web.run_app(self.app, port=server_port) def close_server(self, _, __) -> None: """Close the server.""" From 2aac8e2df39322ce69a82dcaa018623d69df670b Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 22 May 2024 16:28:15 -0700 Subject: [PATCH 6/8] Split launch declaration, switch to >= 3.10 --- pyproject.toml | 2 +- src/ephys_link/__main__.py | 9 +- src/ephys_link/gui.py | 2 +- src/ephys_link/platform_handler.py | 6 +- src/ephys_link/platforms/sensapex_handler.py | 2 +- .../platforms/sensapex_manipulator.py | 8 +- src/ephys_link/platforms/ump3_manipulator.py | 4 +- src/ephys_link/server.py | 218 ++++++++++-------- 8 files changed, 145 insertions(+), 106 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfc90cf..2ddf83d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "ephys-link" dynamic = ["version"] description = "A Python Socket.IO server that allows any Socket.IO-compliant application to communicate with manipulators used in electrophysiology experiments." readme = "README.md" -requires-python = ">=3.8, <3.13" +requires-python = ">=3.10, <3.13" license = "GPL-3.0-only" keywords = ["socket-io", "manipulator", "electrophysiology", "ephys", "sensapex", "neuroscience", "neurotech", "virtualbrainlab", "new-scale"] authors = [{ name = "Kenneth Yang", email = "kjy5@uw.edu" }] diff --git a/src/ephys_link/__main__.py b/src/ephys_link/__main__.py index b3ea3e0..1d9ef38 100644 --- a/src/ephys_link/__main__.py +++ b/src/ephys_link/__main__.py @@ -75,7 +75,7 @@ def main() -> None: return None # Otherwise, create Server from CLI. - server = Server(args.use_proxy) + server = Server() # Continue with CLI if not. com.DEBUG = args.debug @@ -86,7 +86,12 @@ def main() -> None: e_stop.watch() # Launch with parsed arguments on main thread. - run(server.launch(args.type, args.proxy_address, args.port, args.pathfinder_port, args.ignore_updates)) + if args.use_proxy: + run( + server.launch_for_proxy(args.proxy_address, args.port, args.type, args.pathfinder_port, args.ignore_updates) + ) + else: + server.launch(args.type, args.port, args.pathfinder_port, args.ignore_updates) if __name__ == "__main__": diff --git a/src/ephys_link/gui.py b/src/ephys_link/gui.py index e60d7d7..de2076e 100644 --- a/src/ephys_link/gui.py +++ b/src/ephys_link/gui.py @@ -160,4 +160,4 @@ def _launch_server(self) -> None: e_stop = EmergencyStop(server, self._serial.get()) e_stop.watch() - server.launch(self._type.get(), "", self._port.get(), self._pathfinder_port.get()) + server.launch(self._type.get(), self._port.get(), self._pathfinder_port.get()) diff --git a/src/ephys_link/platform_handler.py b/src/ephys_link/platform_handler.py index ed583cd..c570157 100644 --- a/src/ephys_link/platform_handler.py +++ b/src/ephys_link/platform_handler.py @@ -34,7 +34,7 @@ from ephys_link import common as com if TYPE_CHECKING: - import socketio + from socketio import AsyncClient, AsyncServer class PlatformHandler(ABC): @@ -309,7 +309,7 @@ def set_inside_brain(self, request: InsideBrainRequest) -> BooleanStateResponse: print(f"{e}\n") return BooleanStateResponse(error="Error setting inside brain") - async def calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> str: + async def calibrate(self, manipulator_id: str, sio: AsyncClient | AsyncServer) -> str: """Calibrate manipulator :param manipulator_id: ID of manipulator to calibrate @@ -423,7 +423,7 @@ def _set_inside_brain(self, request: InsideBrainRequest) -> BooleanStateResponse raise NotImplementedError @abstractmethod - async def _calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> str: + async def _calibrate(self, manipulator_id: str, sio: AsyncClient | AsyncServer) -> str: """Calibrate manipulator :param manipulator_id: ID of manipulator to calibrate diff --git a/src/ephys_link/platforms/sensapex_handler.py b/src/ephys_link/platforms/sensapex_handler.py index f4edbb3..66825a1 100644 --- a/src/ephys_link/platforms/sensapex_handler.py +++ b/src/ephys_link/platforms/sensapex_handler.py @@ -95,7 +95,7 @@ async def _calibrate(self, manipulator_id: str, sio: socketio.AsyncServer) -> st cur_pos = self.manipulators[manipulator_id].get_pos()["position"] # Check difference between current and target position - for prev, cur in zip([10000, 10000, 10000, 10000], cur_pos): + for prev, cur in zip([10000, 10000, 10000, 10000], cur_pos, strict=False): if abs(prev - cur) > 1: still_working = True break diff --git a/src/ephys_link/platforms/sensapex_manipulator.py b/src/ephys_link/platforms/sensapex_manipulator.py index fe79024..69139e5 100644 --- a/src/ephys_link/platforms/sensapex_manipulator.py +++ b/src/ephys_link/platforms/sensapex_manipulator.py @@ -59,7 +59,13 @@ def get_pos(self) -> PositionalResponse: # com.dprint(f"[SUCCESS]\t Got position of manipulator {self._id}\n") return PositionalResponse( position=Vector4( - **dict(zip(Vector4.model_fields.keys(), [axis / MM_TO_UM for axis in self._device.get_pos(1)])) + **dict( + zip( + Vector4.model_fields.keys(), + [axis / MM_TO_UM for axis in self._device.get_pos(1)], + strict=False, + ) + ) ) ) except Exception as e: diff --git a/src/ephys_link/platforms/ump3_manipulator.py b/src/ephys_link/platforms/ump3_manipulator.py index 53226c1..6f5105e 100644 --- a/src/ephys_link/platforms/ump3_manipulator.py +++ b/src/ephys_link/platforms/ump3_manipulator.py @@ -55,7 +55,9 @@ def get_pos(self) -> PositionalResponse: position.append(position[0]) # com.dprint(f"[SUCCESS]\t Got position of manipulator {self._id}\n") - return PositionalResponse(position=Vector4(**dict(zip(Vector4.model_fields.keys(), position)))) + return PositionalResponse( + position=Vector4(**dict(zip(Vector4.model_fields.keys(), position, strict=False))) + ) except Exception as e: print(f"[ERROR]\t\t Getting position of manipulator {self._id}") print(f"{e}\n") diff --git a/src/ephys_link/server.py b/src/ephys_link/server.py index 7927fd0..58bb7cd 100644 --- a/src/ephys_link/server.py +++ b/src/ephys_link/server.py @@ -11,15 +11,15 @@ from __future__ import annotations -import json -from json import loads +from asyncio import get_event_loop +from json import dumps, loads from signal import SIGINT, SIGTERM, signal -from sys import exit from typing import TYPE_CHECKING, Any -from aiohttp import ClientSession, web +from aiohttp import ClientSession +from aiohttp.web import Application, run_app from aiohttp.web_runner import GracefulExit -from packaging import version +from packaging.version import parse from pydantic import ValidationError from requests.exceptions import ConnectionError @@ -50,61 +50,30 @@ class Server: - def __init__(self, proxy_mode: bool) -> None: - """Set up the server. + def __init__(self) -> None: + """Declare and setup server object. Launching is done is a separate function.""" - :param proxy_mode: Flag to enable proxy mode. - :type proxy_mode: bool - """ - - # Proxy mode flag. - self.proxy_mode = proxy_mode + # Server object. + self.sio: AsyncClient | AsyncServer | None = None - # Server and Socketio setup. - if self.proxy_mode: - self.sio = AsyncClient() - self.pinpoint_id = "abcde" # str(uuid4())[:8] + # Web application object. + self.app: Application | None = None - else: - self.sio = AsyncServer() - self.app = web.Application() + # Proxy server ID. + self.pinpoint_id: str = "" + # Manipulator platform handler. + self.platform: PlatformHandler | None = None # Is there a client connected? self.is_connected = False # Is the server running? self.is_running = False - # Current platform handler (defaults to Sensapex). - self.platform: PlatformHandler = SensapexHandler() - # Register server exit handlers. signal(SIGTERM, self.close_server) signal(SIGINT, self.close_server) - # Declare events and assign handlers. - if not self.proxy_mode: - self.sio.attach(self.app) - self.sio.on("connect", self.connect) - self.sio.on("disconnect", self.disconnect) - - self.sio.on("get_pinpoint_id", self.get_pinpoint_id) - self.sio.on("get_version", self.get_version) - self.sio.on("get_manipulators", self.get_manipulators) - self.sio.on("register_manipulator", self.register_manipulator) - self.sio.on("unregister_manipulator", self.unregister_manipulator) - self.sio.on("get_pos", self.get_pos) - self.sio.on("get_angles", self.get_angles) - self.sio.on("get_shank_count", self.get_shank_count) - self.sio.on("goto_pos", self.goto_pos) - self.sio.on("drive_to_depth", self.drive_to_depth) - self.sio.on("set_inside_brain", self.set_inside_brain) - self.sio.on("calibrate", self.calibrate) - self.sio.on("bypass_calibration", self.bypass_calibration) - self.sio.on("set_can_write", self.set_can_write) - self.sio.on("stop", self.stop) - self.sio.on("*", self.catch_all) - # Server events. async def connect(self, sid, _, __) -> bool: """Acknowledge connection to the server. @@ -148,7 +117,7 @@ async def get_pinpoint_id(self) -> str: :return: Pinpoint ID and whether the client is a requester. :rtype: tuple[str, bool] """ - return json.dumps({"pinpoint_id": self.pinpoint_id, "is_requester": False}) + return dumps({"pinpoint_id": self.pinpoint_id, "is_requester": False}) @staticmethod async def get_version(_) -> str: @@ -389,41 +358,21 @@ async def catch_all(_, __, data: Any) -> str: print(f"[UNKNOWN EVENT]:\t {data}") return "UNKNOWN_EVENT" - async def launch( - self, - platform_type: str, - proxy_address: str, - server_port: int, - pathfinder_port: int | None = None, - ignore_updates: bool = False, # noqa: FBT002 - ) -> None: - """Launch the server. - - :param platform_type: Parsed argument for platform type. - :type platform_type: str - :param proxy_address: Parsed argument for proxy address. - :type proxy_address: str - :param server_port: HTTP port to serve the server. - :type server_port: int - :param pathfinder_port: Port New Scale Pathfinder's server is on. - :type pathfinder_port: int - :param ignore_updates: Flag to ignore checking for updates. - :type ignore_updates: bool - :return: None - """ - + # Server functions + async def launch_setup(self, platform_type: str, pathfinder_port: int, ignore_updates) -> None: # Import correct manipulator handler - if platform_type == "sensapex": - # Already assigned (was the default) - pass - elif platform_type == "ump3": - self.platform = UMP3Handler() - elif platform_type == "new_scale": - self.platform = NewScaleHandler() - elif platform_type == "new_scale_pathfinder": - self.platform = NewScalePathfinderHandler(pathfinder_port) - else: - exit(f"[ERROR]\t\t Invalid manipulator type: {platform_type}") + match platform_type: + case "sensapex": + self.platform = SensapexHandler() + case "ump3": + self.platform = UMP3Handler() + case "new_scale": + self.platform = NewScaleHandler() + case "new_scale_pathfinder": + self.platform = NewScalePathfinderHandler(pathfinder_port) + case _: + error = f"[ERROR]\t\t Invalid manipulator type: {platform_type}" + raise ValueError(error) # Preamble. print(ASCII) @@ -432,13 +381,16 @@ async def launch( # Check for newer version. if not ignore_updates: try: - async with ClientSession() as session, session.get( - "https://api.github.com/repos/VirtualBrainLab/ephys-link/tags" - ) as response: + async with ( + ClientSession() as session, + session.get("https://api.github.com/repos/VirtualBrainLab/ephys-link/tags") as response, + ): latest_version = (await response.json())[0]["name"] - if version.parse(latest_version) > version.parse(__version__): + if parse(latest_version) > parse(__version__): print(f"New version available: {latest_version}") print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest") + + await session.close() except ConnectionError: pass @@ -454,20 +406,94 @@ async def launch( print(self.platform.get_manipulators().manipulators) print() + async def launch_for_proxy( + self, proxy_address: str, port: int, platform_type: str, pathfinder_port: int | None, ignore_updates: bool + ) -> None: + """Launch the server in proxy mode. + + :param proxy_address: Proxy IP address. + :type proxy_address: str + :param port: Port to serve the server. + :type port: int + :param platform_type: Parsed argument for platform type. + :type platform_type: str + :param pathfinder_port: Port New Scale Pathfinder's server is on. + :type pathfinder_port: int + :param ignore_updates: Flag to ignore checking for updates. + :type ignore_updates: bool + :return: None + """ + + # Launch setup + await self.launch_setup(platform_type, pathfinder_port, ignore_updates) + + # Create AsyncClient. + self.sio = AsyncClient() + self.pinpoint_id = "abcde" # str(uuid4())[:8] + + # Bind events. + self.bind_events() + + # Connect and mark that server is running. + await self.sio.connect(f"http://{proxy_address}:{port}") + self.is_running = True + await self.sio.wait() + + def launch( + self, + platform_type: str, + port: int, + pathfinder_port: int | None, + ignore_updates: bool, + ) -> None: + """Launch the server. + + :param platform_type: Parsed argument for platform type. + :type platform_type: str + :param port: HTTP port to serve the server. + :type port: int + :param pathfinder_port: Port New Scale Pathfinder's server is on. + :type pathfinder_port: int + :param ignore_updates: Flag to ignore checking for updates. + :type ignore_updates: bool + :return: None + """ + + # Launch setup (synchronously) + get_event_loop().run_until_complete(self.launch_setup(platform_type, pathfinder_port, ignore_updates)) + + # Create AsyncServer + self.sio = AsyncServer() + self.app = Application() + self.sio.attach(self.app) + + # Bind events + self.sio.on("connect", self.connect) + self.sio.on("disconnect", self.disconnect) + self.bind_events() + # Mark that server is running self.is_running = True + run_app(self.app, port=port) - if self.proxy_mode: - # Verify that the server was initialized correctly. - if type(self.sio) is not AsyncClient: - error = "Server was not initialized to a Client for proxy mode!" - raise ValueError(error) - # noinspection PyUnresolvedReferences,HttpUrlsUsage - await self.sio.connect(f"http://{proxy_address}:{server_port}") - # noinspection PyUnresolvedReferences - await self.sio.wait() - else: - web.run_app(self.app, port=server_port) + def bind_events(self) -> None: + """Bind Ephys Link events to the server.""" + self.sio.on("get_pinpoint_id", self.get_pinpoint_id) + self.sio.on("get_version", self.get_version) + self.sio.on("get_manipulators", self.get_manipulators) + self.sio.on("register_manipulator", self.register_manipulator) + self.sio.on("unregister_manipulator", self.unregister_manipulator) + self.sio.on("get_pos", self.get_pos) + self.sio.on("get_angles", self.get_angles) + self.sio.on("get_shank_count", self.get_shank_count) + self.sio.on("goto_pos", self.goto_pos) + self.sio.on("drive_to_depth", self.drive_to_depth) + self.sio.on("set_inside_brain", self.set_inside_brain) + self.sio.on("calibrate", self.calibrate) + self.sio.on("bypass_calibration", self.bypass_calibration) + self.sio.on("set_can_write", self.set_can_write) + self.sio.on("stop", self.stop) + self.sio.on("*", self.catch_all) def close_server(self, _, __) -> None: """Close the server.""" From 84dcbb4929f2f3e7d0ec18a4eeeca47cf5d97d9d Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 22 May 2024 17:05:27 -0700 Subject: [PATCH 7/8] Create options for one-folder building --- ephys_link.spec | 9 +++++++++ pyproject.toml | 6 +++--- src/ephys_link/__about__.py | 2 +- src/ephys_link/server.py | 4 ++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ephys_link.spec b/ephys_link.spec index b0d956b..f7b14d5 100644 --- a/ephys_link.spec +++ b/ephys_link.spec @@ -2,6 +2,12 @@ from ephys_link.__about__ import __version__ as version +from argparse import ArgumentParser + +parser = ArgumentParser() +parser.add_argument("-d", "--dir", action="store_true", help="Outputs a directory") +options = parser.parse_args() + a = Analysis( ['src\\ephys_link\\__main__.py'], pathex=[], @@ -37,3 +43,6 @@ exe = EXE( entitlements_file=None, icon='assets\\icon.ico', ) + +if options.dir: + coll = COLLECT(exe, a.binaries, name=f"EphysLink-v{version}") diff --git a/pyproject.toml b/pyproject.toml index 2ddf83d..1da6f04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ dependencies = [ "pyserial==3.5", "python-socketio==5.11.2", "pythonnet==3.0.3", - "requests==2.32.0", "sensapex==1.400.0", "vbl-aquarium==0.0.14" ] @@ -88,10 +87,11 @@ check = "mypy --install-types --non-interactive {args:src/ephys_link tests}" [tool.hatch.envs.exe] python = "3.12" dependencies = [ - "pyinstaller==6.3.0", + "pyinstaller", ] [tool.hatch.envs.exe.scripts] -build = "pyinstaller.exe ephys_link.spec -y" +build = "pyinstaller.exe ephys_link.spec -y -- -d" +build_onefile = "pyinstaller.exe ephys_link.spec -y" build_clean = "pyinstaller.exe ephys_link.spec -y --clean" [tool.coverage.run] diff --git a/src/ephys_link/__about__.py b/src/ephys_link/__about__.py index 67bc602..9c73af2 100644 --- a/src/ephys_link/__about__.py +++ b/src/ephys_link/__about__.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.3.1" diff --git a/src/ephys_link/server.py b/src/ephys_link/server.py index 58bb7cd..5b5a119 100644 --- a/src/ephys_link/server.py +++ b/src/ephys_link/server.py @@ -16,12 +16,12 @@ from signal import SIGINT, SIGTERM, signal from typing import TYPE_CHECKING, Any +from aiohttp import ClientConnectionError from aiohttp import ClientSession from aiohttp.web import Application, run_app from aiohttp.web_runner import GracefulExit from packaging.version import parse from pydantic import ValidationError -from requests.exceptions import ConnectionError # from socketio import AsyncServer from socketio import AsyncClient, AsyncServer @@ -391,7 +391,7 @@ async def launch_setup(self, platform_type: str, pathfinder_port: int, ignore_up print("Download at: https://github.com/VirtualBrainLab/ephys-link/releases/latest") await session.close() - except ConnectionError: + except ClientConnectionError: pass # Explain window. From e2a3d61078d66be93963b9593bfa8d76f0cadc5f Mon Sep 17 00:00:00 2001 From: Kenneth Yang Date: Wed, 22 May 2024 17:13:55 -0700 Subject: [PATCH 8/8] Formatted --- src/ephys_link/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ephys_link/server.py b/src/ephys_link/server.py index 5b5a119..acd4aa4 100644 --- a/src/ephys_link/server.py +++ b/src/ephys_link/server.py @@ -16,8 +16,7 @@ from signal import SIGINT, SIGTERM, signal from typing import TYPE_CHECKING, Any -from aiohttp import ClientConnectionError -from aiohttp import ClientSession +from aiohttp import ClientConnectionError, ClientSession from aiohttp.web import Application, run_app from aiohttp.web_runner import GracefulExit from packaging.version import parse